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 + optionalconfig.composesfor 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 viaconfig.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 defaultPi ingredients live in a sibling dir so they don’t clutter your pack list:
~/.bahulam/plugins-pi/ # pi packages pulled as ingredientsInstall a pack
bahulam install <source> accepts four source shapes — the CLI classifies automatically:
| Shape | Example | What happens |
|---|---|---|
| Registry name | manim-studio | Looked 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 name | bahulam:manim-studio | Same as above; the bahulam: prefix is optional, but useful for scripts that also handle other prefixes to make intent explicit |
| Git URL | https://github.com/foo/seo-toolkit.git | Shallow git clone into the target dir; use --subdir for monorepo subpaths |
| Tarball URL | https://.../seo-toolkit-1.0.0.tgz | Download and untar with --strip-components=1 |
| Local directory | ./my-plugin or /abs/path | Recursive copy |
| pi source (scaffolded) | pi:pi-redmine | Pulls 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-redmineHow 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.0Pi 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.htmlThe 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 atbahulam install(for packs) orbahulam pull(for pi ingredients). Scripts should migrate to the top-level verbs.
List
bahulam listNAME 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-terminalShows 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 backDisabling 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.0Works 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-toolkitDeletes 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 CIErrors block install; warnings surface but let the install proceed.
What the preflight verifies:
| Category | Check |
|---|---|
| Manifest | apiVersion: bahulam.plugin/1, metadata.name, metadata.version present |
| Manifest | metadata.name should be lowercase kebab-case (warning) |
| Tool names | Match ^[A-Za-z_][A-Za-z0-9_-]{0,63}$ |
| Tool names | Do 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 names | No duplicates within the plugin |
| Tool handlers | tool path stays inside the plugin directory (no .. escapes) |
| Tool handlers | Handler file exists on disk |
| Tool handlers | Handler imports cleanly (import() succeeds) |
| Tool handlers | Handler exports an async call function |
| Tool schemas | parameters / input_schema is an object, type: "object" (warning if not) |
| Tool metadata | Description present and ≥8 chars (warning) |
| MCP servers | Each has EITHER command (stdio) OR url (remote), never both, never neither |
| MCP servers | args is an array when present |
| Sub-agents | slug matches ^[a-z][a-z0-9-]{0,63}$ |
| Sub-agents | No duplicate slugs |
| Sub-agents | Every entry in agent.tools resolves to a plugin tool, platform/meta tool, composed pi tool, or a declared MCP server via <server>.<tool> |
| Sub-agents | system_prompt present (warning) |
| Workspace views | source path stays inside the plugin directory |
| Workspace views | source file exists on disk |
| Workspace views | source ends in .html (warning) |
| Collisions | Name 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 foundExample — 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 rejectedWire it into your CI:
bahulam validate . --json | jq -e '.ok' > /dev/nullVerify 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>" | jqBoth 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.