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.

Diagram

yes

no

๐Ÿ–ฅ๏ธ praisonai tools add pkg

๐Ÿ“ฆ pip / uv install

๐Ÿ” fresh ToolResolver

๐Ÿ†• New tools?

โœ… Available table

๐Ÿ›‘ exit 1

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

  1. Install a tool package
    bash
    praisonai tools add praisonai-my-tools
  2. Upgrade an installed package
    bash
    praisonai tools add praisonai-my-tools --upgrade
  3. Verify without installing
    bash
    praisonai tools add praisonai-my-tools --dry-run

    --dry-run skips installation and reports whether any new tool would resolve from what’s already installed.

On refusal of a bare package name, the CLI prints:

text
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.

Diagram
ToolResolverpip / uvtools addUserToolResolverpip / uvtools addUseralt[not –dry-run]praisonai tools add pkgsnapshot list_available() (before)install pkgexit codeconstruct fresh ToolResolver()list_available() (after)table of new tools OR exit 1
StepWhat happens
SnapshotRecords set(ToolResolver().list_available().keys())
InstallSkipped on --dry-run; otherwise runs the resolved installer command
RefreshConstructs a fresh ToolResolver() โ€” see the note below
Diffsorted(after โˆ’ before) โ€” new names become table rows

Security

  • No code execution for the github: inspection path. The github: download inspects a candidate tools.py with ast.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 a github: reference calls importlib.import_module(<name>) to inspect the package. That runs its top-level code โ€” which is exactly why it is now gated behind PRAISONAI_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 --sha256 flag โ€” 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 / flagTypeDefaultDescription
packagepositional 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-runflagFalseSkip installation; run discovery only. Exits non-zero if nothing new is discovered
--upgrade, -UflagFalseAppend --upgrade so an already-installed package is upgraded
--globalflagFalseInstall 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

SituationExit code
At least one new tool became available0
Installer command returned non-zero1
Install succeeded but nothing new discovered1
--dry-run and nothing new discovered1

Output Shape

A successful run prints a Rich table titled Registered N tool(s) from <package>, with the tool’s resolution source:

text
        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:

SourceOrigin
localProject tools.py
builtinpraisonaiagents.tools
externalpraisonai-tools package
registeredWrapper registry / entry-point plugins

On --dry-run the title verb is Discovered instead of Registered.


Common Patterns

Default (pin to CLI interpreter)

bash
praisonai tools add praisonai-my-tools

When uv is present, the package is pinned to the CLI interpreter so a fresh resolver sees it.

Global install

bash
praisonai tools add praisonai-my-tools --global

Installs into the ambient/system environment (uv pip install --system, or plain pip install).

CI verification

bash
praisonai tools add praisonai-my-tools --dry-run

Fails 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.


Related