PluginsInstall & Manage

Install & Manage Plugins

The CLI splits fetch into two verbs at the top level, matching what you actually get:

  • bahulam install <pack> — installs a pack (a runnable Bahulam plugin: agents + tools + workspace + optional config.composes for pi ingredients). Accepts git URLs, tarballs, local paths, and registry names. Auto-pulls any pi packages the pack composes.
  • bahulam pull <pi-source> — pulls a raw pi ingredient only. Not runnable on its own; used by packs via config.composes. You almost never call this directly.

A plugin is loaded iff its directory exists in one of these search paths, scanned in this order (later paths override earlier):

<project>/.bahulam/plugins/         # project-local, ideal for team plugins
~/.bahulam/plugins/                 # user-global, the default

Pi ingredients live in a sibling dir so they don’t clutter your pack list:

~/.bahulam/plugins-pi/              # pi packages pulled as ingredients

Install a pack

bahulam install <source> accepts four source shapes — the CLI classifies automatically:

ShapeExampleWhat happens
Registry namemanim-studioLooked up in awesome-bahulam-plugins/registry.json and installed from its repository + subdir — a shallow git clone of just that subdirectory of the community repo
Explicit registry namebahulam:manim-studioSame as above; the bahulam: prefix is optional, but useful for scripts that also handle other prefixes to make intent explicit
Git URLhttps://github.com/foo/seo-toolkit.gitShallow git clone into the target dir; use --subdir for monorepo subpaths
Tarball URLhttps://.../seo-toolkit-1.0.0.tgzDownload and untar with --strip-components=1
Local directory./my-plugin or /abs/pathRecursive copy
pi source (scaffolded)pi:pi-redminePulls the pi package AND auto-scaffolds a Bahulam pack using config.composes + config.workspace — one command, runnable pack
# by registry name (the common case — resolves via awesome-bahulam-plugins)
bahulam install manim-studio
bahulam install bahulam:manim-studio       # explicit prefix, same result
 
# by git URL
bahulam install https://github.com/community/seo-toolkit
bahulam install https://github.com/community/seo-toolkit --ref v1.2.0
 
# from a local checkout
bahulam install ./my-plugin
 
# from a tarball
bahulam install https://example.com/downloads/my-plugin-1.0.0.tgz
 
# from a pi ingredient (auto-scaffolds a pack around it)
bahulam install pi:pi-redmine

How the registry resolves. awesome-bahulam-plugins/registry.json is the source of truth. Each entry declares repository, ref (branch/tag), and subdir — for example, manim-studio points at https://github.com/BahulamAI/awesome-bahulam-plugins, ref: main, subdir: plugins/manim-studio. The installer does a shallow git clone of just that subdir, then runs preflight. The community repo IS the plugin source — no separate download step.

Flags:

  • --global (default) — install to ~/.bahulam/plugins/
  • --project — install to <cwd>/.bahulam/plugins/ (checked in with the project)
  • --ref <tag|branch> — pin a git ref
  • --force — overwrite an existing install of the same name

The installer verifies the manifest after fetch. If plugin.yaml is missing or invalid, the install is rolled back — no partial install is left behind. The install location is recorded in a .bahulam-plugin.json stamp so update knows where the plugin came from. Packs with a config.composes block auto-pull the referenced pi ingredients as part of the install (no separate pull step needed).

Pull a pi ingredient

Raw pi packages are ingredients: not runnable on their own, meant to be composed into a pack. bahulam pull fetches one without scaffolding a pack around it — useful when you’re authoring a pack locally and want to inspect what a pi package exposes before adding it to your config.composes block.

bahulam pull pi:pi-web-access
bahulam pull pi:@ffmpeg/transitions@^2.0.0

Pi ingredients land in ~/.bahulam/plugins-pi/. Any pack that later declares config.composes: [{source: pi:pi-web-access, ...}] finds the already-pulled copy instead of re-fetching it.

What compose means

After install, Bahulam-authored packs and pi-scaffolded packs are loaded the same way: plugin.yaml, config.workspace, optional config.agents_from, optional local tools/, optional config.views, and optional config.composes.

config.composes is the dependency edge. It says: this Bahulam pack depends on an external pi ingredient, and selected pi tools should be exposed inside this pack’s tool surface. A raw pi package installed by bahulam pull pi:<package> is not directly runnable as a Bahulam plugin. bahulam install pi:<package> wraps that ingredient in a normal Bahulam pack so agents, local tools, workspace views, preflight, and observability all use the same path.

If a pi author publishes a full Bahulam-format plugin directly, they do not need config.composes unless their plugin also depends on other pi packages.

Generated pi pack layout

bahulam install pi:<package> creates the same contract shape as Bahulam authored plugins such as manim-studio:

~/.bahulam/plugins/<generated-slug>/
├── plugin.yaml                 # config.tools, config.composes, config.workspace, config.views
├── config/
│   └── workspace.yaml          # entry agent
├── tools/                      # optional local state tools
│   ├── save-item.mjs
│   ├── list-items.mjs
│   └── drop-item.mjs
└── workspace/                  # optional live notebook panel
    └── panel.html

The generated plugin.yaml stays thin. It declares the pi ingredient under config.composes, points config.workspace at config/workspace.yaml, and keeps any generated local tools under tools/. The generated config/workspace.yaml uses the same apiVersion: agent.framework/v1 agent schema consumed by backend workspaces.

Legacy alias. bahulam plugin install <source> still works but prints a deprecation notice pointing at bahulam install (for packs) or bahulam pull (for pi ingredients). Scripts should migrate to the top-level verbs.

List

bahulam list
NAME              VERSION  SCOPE    STATUS   SURFACE
options-terminal  1.0.0    global   active   6t 1a 1v
seo-toolkit       0.3.1    global   active   4t 1a 1v
threejs-dev       0.8.0    project  disabled 3t 1a 1v

3 plugins · surface: t=tools a=agents v=views

--json for machine-readable output.

Info

bahulam info options-terminal

Shows the manifest details, install origin, and every tool/agent/view the plugin contributes. Useful for auditing what a plugin will do before you run it.

Enable / Disable

bahulam disable seo-toolkit    # renames to seo-toolkit.disabled
bahulam enable  seo-toolkit    # renames it back

Disabling hides a plugin from the loader without deleting it (or its state). The scanner only looks at directories with a valid manifest, so the .disabled suffix is enough.

Update

bahulam update seo-toolkit
bahulam update seo-toolkit --ref v1.3.0

Works for git-installed plugins (using the origin recorded in .bahulam-plugin.json). For local or tarball installs, re-run install --force from the new source.

Remove

bahulam remove seo-toolkit

Deletes the plugin directory. Irreversible — take a backup first if the plugin held any local state.

Validate (Preflight)

Every install runs a hard preflight and rolls back on any error — you never end up with a half-installed or broken plugin. Authors can run the same checks locally without touching disk:

bahulam validate ./my-plugin            # against a directory
bahulam validate hello-world            # against an installed one
bahulam validate ./my-plugin --json     # for CI

Errors block install; warnings surface but let the install proceed.

What the preflight verifies:

CategoryCheck
ManifestapiVersion: bahulam.plugin/1, metadata.name, metadata.version present
Manifestmetadata.name should be lowercase kebab-case (warning)
Tool namesMatch ^[A-Za-z_][A-Za-z0-9_-]{0,63}$
Tool namesDo not shadow any built-in (read_file, shell, edit_file, explore, plan, verify, debug, refactor, advise, delegate, web_search, web_fetch, remember, ask_user, …)
Tool namesNo duplicates within the plugin
Tool handlerstool path stays inside the plugin directory (no .. escapes)
Tool handlersHandler file exists on disk
Tool handlersHandler imports cleanly (import() succeeds)
Tool handlersHandler exports an async call function
Tool schemasparameters / input_schema is an object, type: "object" (warning if not)
Tool metadataDescription present and ≥8 chars (warning)
MCP serversEach has EITHER command (stdio) OR url (remote), never both, never neither
MCP serversargs is an array when present
Sub-agentsslug matches ^[a-z][a-z0-9-]{0,63}$
Sub-agentsNo duplicate slugs
Sub-agentsEvery entry in agent.tools resolves to a plugin tool, platform/meta tool, composed pi tool, or a declared MCP server via <server>.<tool>
Sub-agentssystem_prompt present (warning)
Workspace viewssource path stays inside the plugin directory
Workspace viewssource file exists on disk
Workspace viewssource ends in .html (warning)
CollisionsName doesn’t match an already-installed plugin (unless --force)

Example — a good plugin:

$ bahulam validate ./my-plugin
my-plugin  /Users/you/plugins/my-plugin
  ✓ no issues found

Example — the same plugin with a mistake:

$ bahulam validate ./my-plugin
my-plugin  /Users/you/plugins/my-plugin
  ✗ Tool "read_file": shadows a built-in tool — pick a different name
  ✗ Agent "seo-auditor": tool "nonexistent_tool" is not defined by this plugin and is not a built-in
  ! Tool "hello": description is missing or very short (<8 chars)
  ✗ 2 error(s) — install would be rejected

Wire it into your CI:

bahulam validate . --json | jq -e '.ok' > /dev/null

Verify installed

Open a workspace and confirm the plugin loaded:

bahulam plugin <name>              # workspace-open verb still uses `plugin` prefix
# then, in another terminal:
curl -s "http://127.0.0.1:<port>/api/plugin-views?token=<token>" | jq

Both port and token are in the URL the CLI printed. An empty views: [] means the plugin was found but declares no workspace view; a 404 means the plugin isn’t on disk where the scanner is looking.