> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Cmd["🖥️ praisonai tools add pkg"] --> Install["📦 pip / uv install"]
    Install --> Resolver["🔍 fresh ToolResolver"]
    Resolver --> Diff{"🆕 New tools?"}
    Diff -->|yes| Table["✅ Available table"]
    Diff -->|no| Fail["🛑 exit 1"]

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef output fill:#10B981,stroke:#7C90A0,color:#fff
    classDef error fill:#8B0000,stroke:#7C90A0,color:#fff

    class Cmd input
    class Install,Resolver,Diff process
    class Table output
    class Fail error
```

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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai tools add praisonai-my-tools
```

> [!NOTE]
>
> `praisonai-my-tools` is a placeholder — replace it with a real pip requirement spec.

**2. Upgrade an installed package**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai tools add praisonai-my-tools --upgrade
```

**3. Verify without installing**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

> [!WARNING]
>
> **Enable code execution during install first.** `praisonai tools add` refuses to install a tool package when the source is a local file path, a `github:` reference, **or a bare package name** unless `PRAISONAI_ALLOW_LOCAL_TOOLS=true` is set. All three branches run user code — a local file executes on load, a `github:` fetch downloads then loads, and a bare package name is resolved on `sys.path` and its `__init__` runs at import time. The gate is the same one that governs `tools.py` autoload (see [Security Environment Variables](https://praison.ai/docs/features/security-environment-variables.md#praisonai_allow_local_tools)). A published `pip install`-style spec that is already installed and is only being resolved by name goes through the same `import`, so the gate applies there too.

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

```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant CLI as tools add
    participant Installer as pip / uv
    participant Resolver as ToolResolver

    User->>CLI: praisonai tools add pkg
    CLI->>Resolver: snapshot list_available() (before)
    alt not --dry-run
        CLI->>Installer: install pkg
        Installer-->>CLI: exit code
    end
    CLI->>Resolver: construct fresh ToolResolver()
    Resolver-->>CLI: list_available() (after)
    CLI->>User: table of new tools OR exit 1
```

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

> [!WARNING]
>
> Discovery constructs a **fresh** `ToolResolver()`. Calling `resolver.invalidate()` alone is **not** enough — it only clears the per-name resolution cache, not instance-level availability caches (like whether `praisonai_tools` is importable). A new instance re-evaluates those against the now-updated environment.

***

## 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 / 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](https://praison.ai/docs/features/plugins-add.md#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:

```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
        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

> [tab] Default (pin to CLI interpreter)
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> praisonai tools add praisonai-my-tools
> ```
>
> When `uv` is present, the package is pinned to the CLI interpreter so a fresh resolver sees it.

> [tab] Global install
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> praisonai tools add praisonai-my-tools --global
> ```
>
> Installs into the ambient/system environment (`uv pip install --system`, or plain `pip install`).

> [tab] CI verification
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> 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

> [accordion] 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.

> [accordion] 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.

> [accordion] 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.

> [accordion] Inspect after installing
>
> Run `praisonai tools list` to see every resolvable tool and its source, not just what this command added.

***

## Related

> [card] [plugins add](https://praison.ai/docs/features/plugins-add.md)
>
> Install a plugin package and verify it registered

> [card] [Tool Source Registry](https://praison.ai/docs/features/tool-source-registry.md)
>
> Plug third-party tool sources via entry points

> [card] [Tool Resolver](https://praison.ai/docs/features/tool-resolver.md)
>
> Single source of truth for loading tools

> [card] [Tool Discovery Order](https://praison.ai/docs/features/tool-discovery-order.md)
>
> How Agent resolves tool names at runtime


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.