tools add
Install a tool package from the CLI and verify its tools became available
One command installs a tool package and tells you exactly which tools became available โ or exits non-zero if none did.
Previously a package could pip install cleanly yet expose no resolvable tools โ a silent failure. tools add turns that into a surfaced non-zero exit.
Quick Start
- Install a tool package
bash praisonai tools add praisonai-my-tools - Upgrade an installed package
bash praisonai tools add praisonai-my-tools --upgrade - Verify without installing
bash praisonai tools add praisonai-my-tools --dry-run--dry-runskips installation and reports whether any new tool would resolve from what’s already installed.
On refusal of a bare package name, the CLI prints:
Refusing to add package tools: set PRAISONAI_ALLOW_LOCAL_TOOLS=true to enable
(importing a package runs its code; same opt-in the runtime loader requires).and returns {"success": False, "error": "PRAISONAI_ALLOW_LOCAL_TOOLS not set"}.
How It Works
The command snapshots resolvable tools, installs, builds a fresh resolver, then reports the diff.
| Step | What happens |
|---|---|
| Snapshot | Records set(ToolResolver().list_available().keys()) |
| Install | Skipped on --dry-run; otherwise runs the resolved installer command |
| Refresh | Constructs a fresh ToolResolver() โ see the note below |
| Diff | sorted(after โ before) โ new names become table rows |
Security
- No code execution for the
github:inspection path. Thegithub:download inspects a candidatetools.pywithast.parseโ it does not execute that downloaded file. But the fetch itself and the load run under the same opt-in. - Package-name branch executes
__init__on resolve.praisonai tools add <name>when<name>is neither a local path nor agithub:reference callsimportlib.import_module(<name>)to inspect the package. That runs its top-level code โ which is exactly why it is now gated behindPRAISONAI_ALLOW_LOCAL_TOOLS=true. github:downloads are locked down: HTTPS raw URLs only (raw.githubusercontent.com/raw.github.com), no redirect follow, a 1 MiB size cap, and the on-disk filename is derived from the URL’s last path segment as a safe single basename (no directory traversal).- No
--sha256flag โ the PR that hardened this path deliberately left it out to avoid a new CLI knob. Rely on PyPI signing or your own out-of-band verification if you need a pinned artefact.
Options
| Argument / flag | Type | Default | Description |
|---|---|---|---|
package | positional str | (required) | pip requirement spec โ anything pip install / uv pip install accepts (name, name==1.2.3, VCS URL, path, extras). Any source (local file, github: reference, or bare package name resolved on sys.path) requires PRAISONAI_ALLOW_LOCAL_TOOLS=true โ the package-name branch runs importlib.import_module(name), which executes the package’s __init__. github: downloads are HTTPS raw URLs only, capped at 1 MiB. |
--dry-run | flag | False | Skip installation; run discovery only. Exits non-zero if nothing new is discovered |
--upgrade, -U | flag | False | Append --upgrade so an already-installed package is upgraded |
--global | flag | False | Install into the ambient/system environment instead of the CLI interpreter |
How the Installer Is Chosen
Installation is pinned to the interpreter running the CLI so the package lands where discovery will look. See plugins add โ How the Installer Is Chosen for the shared decision diagram โ both commands use the same _resolve_installer() logic.
Exit Codes
| Situation | Exit code |
|---|---|
| At least one new tool became available | 0 |
| Installer command returned non-zero | 1 |
| Install succeeded but nothing new discovered | 1 |
--dry-run and nothing new discovered | 1 |
Output Shape
A successful run prints a Rich table titled Registered N tool(s) from <package>, with the tool’s resolution source:
Registered 2 tools from praisonai-my-tools
โโโโโโโโโโโโโโโโโโณโโโโโโโโโโโโโ
โ Tool Name โ Source โ
โกโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฉ
โ fetch_report โ external โ
โ send_invoice โ registered โ
โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโSource values come from ToolResolver.list_available_sources() and are one of:
| Source | Origin |
|---|---|
local | Project tools.py |
builtin | praisonaiagents.tools |
external | praisonai-tools package |
registered | Wrapper registry / entry-point plugins |
On --dry-run the title verb is Discovered instead of Registered.
Common Patterns
Default (pin to CLI interpreter)
praisonai tools add praisonai-my-toolsWhen uv is present, the package is pinned to the CLI interpreter so a fresh resolver sees it.
Global install
praisonai tools add praisonai-my-tools --globalInstalls into the ambient/system environment (uv pip install --system, or plain pip install).
CI verification
praisonai tools add praisonai-my-tools --dry-runFails the job (exit 1) if the package no longer exposes resolvable tools after another change.
Best Practices
Construct a fresh resolver in wrapper scripts
If you script installs yourself, build a new ToolResolver() after installing โ invalidate() clears only the per-name cache, not instance-level availability caches.
Use –dry-run in CI to catch silent breakage
A package can install cleanly yet expose no tools. --dry-run exits non-zero when discovery finds nothing new.
Prefer the default over –global
The default pins to the CLI interpreter so the tools are resolvable by the same process. Reach for --global only when you want the system environment.
Inspect after installing
Run praisonai tools list to see every resolvable tool and its source, not just what this command added.