pi-namespace

Namespace tools and skills in pi — group tools by extension with configurable prefix rewriting

Packages

Package details

extension

Install pi-namespace from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-namespace
Package
pi-namespace
Version
0.1.1
Published
Jun 15, 2026
Downloads
not available
Author
monotykamary
License
MIT
Types
extension
Size
33.8 KB
Dependencies
0 dependencies · 0 peers
Pi manifest JSON
{
  "extensions": [
    "./namespace.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

🏷️ pi-namespace

Namespace tools and skills in pi

Group tools by extension with configurable prefix rewriting — flat tool names become deploy:push, fs:read, msg:send.

pi extension license


The Problem

Pi registers all tools in a flat namespace. When multiple extensions register tools, colliding names silently shadow each other — the first registration wins. There's no grouping to tell the LLM that push, status, and rollback all belong to the deploy extension.

Available tools
- read         ← built-in? extension? override?
- push         ← what does this do? where from?
- status       ← deploy status? git status? system status?
- search       ← brave? vector? code?
- deploy       ← the tool? or a namespace collision?

Skills are similarly flat — every skill appears in the system prompt without provenance or grouping.


The Solution

pi-namespace patches ExtensionRunner.prototype.getAllRegisteredTools — the single chokepoint where pi's AgentSession pulls tool definitions before they're frozen into the LLM schema, system prompt, and TUI.

Before After
push deploy:push
status deploy:status
search msg:search
read fs:read (with builtinNamespace)

Namespaced names flow through to everything: LLM tool schemas, system prompt snippets, setActiveTools(), --tools, --exclude-tools, and the TUI.

Available tools
- fs:read         ← built-in (namespaced)
- fs:bash         ← built-in (namespaced)
- fs:edit         ← built-in (namespaced)
- fs:write        ← built-in (namespaced)
- deploy:push     ← from pi-deploy
- deploy:status   ← from pi-deploy
- deploy:rollback  ← from pi-deploy
- msg:search      ← from pi-messenger
- msg:send        ← from pi-messenger

Install

With pi install (recommended):

pi install https://github.com/monotykamary/pi-namespace

Manual — copy to global extensions:

cp namespace.ts ~/.pi/agent/extensions/

Project-local:

mkdir -p .pi/extensions
cp namespace.ts .pi/extensions/

Quick test:

pi -e ./namespace.ts

Reload with /reload after any install method.


Configuration

Create ~/.pi/agent/namespace.json (global) or .pi/namespace.json (project):

{
  "namespaces": {
    "deploy-extension": "deploy",
    "pi-messenger": "msg",
    "pi-code-previews": "preview"
  },
  "builtinNamespace": "fs",
  "autoNamespace": false
}
Option Type Default Description
namespaces Record<string, string> {} Map of extension sourceInfo.path (or substring) → namespace prefix
builtinNamespace string Optional prefix for built-in tools (read, bash, edit, write, grep, find, ls)
autoNamespace boolean false Auto-derive namespace from extension directory/package name

Namespace matching

The key in namespaces is matched against each tool's sourceInfo.path:

  1. Exact match — key equals the full path
  2. Substring match — key is contained within the path
"deploy-extension" matches:
  ✓ /home/user/.pi/agent/extensions/deploy-extension/index.ts
  ✓ /home/user/.npm-global/lib/node_modules/@foo/deploy-extension/dist/index.js
  ✗ /home/user/.pi/agent/extensions/my-deploy/index.ts

Auto-namespace

When autoNamespace is true, tools from extensions without an explicit mapping get a namespace derived from:

Source format Example Derived namespace
File path with pi- segment /home/user/.pi/agent/extensions/pi-messenger/index.ts messenger
npm: package npm:@foo/pi-deploy deploy
git: package git:github.com/user/pi-tools tools
No pi- prefix, falls back /home/user/.pi/agent/extensions/myext/index.ts myext

Commands

Command Description
/namespace Show namespaced tool count and config summary
/namespace list Same as above
/namespace config Show the full namespace config JSON
/namespace map Show per-tool namespace mapping with source paths

Example output

/namespace map

  read → fs:read (<builtin:read>)
  bash → fs:bash (<builtin:bash>)
  edit → fs:edit (<builtin:edit>)
  write → fs:write (<builtin:write>)
  push → deploy:push (/home/user/.pi/agent/extensions/deploy-extension/index.ts)
  status → deploy:status (/home/user/.pi/agent/extensions/deploy-extension/index.ts)
  search (no namespace, local)

How It Works

┌─────────────────────────────────────────────────────────────┐
│  session_start                                              │
│                                                             │
│  Patch ExtensionRunner.prototype.getAllRegisteredTools      │
│  (once per process — all future calls go through the patch) │
│                                                             │
│  Build reverse namespace map: namespaced → original         │
│  (e.g. "deploy:push" → "push")                              │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  AgentSession._refreshToolRegistry()                        │
│                                                             │
│  Calls getAllRegisteredTools() ──→ PATCH INTERCEPTS ──→     │
│                                                             │
│  1. resolveNamespace(): find matching prefix from config    │
│  2. applyNamespace(): rewrite name "push" → "deploy:push"   │
│  3. Object.create(): delegate to original definition        │
│     → execute() inherited untouched                         │
│     → only name, promptSnippet, promptGuidelines overridden │
│                                                             │
│  Namespaced names flow into:                                │
│    _toolDefinitions → _toolRegistry → system prompt → TUI   │
│    setActiveTools(), --tools, --exclude-tools               │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  before_agent_start                                         │
│                                                             │
│  Rewrite <skill name="..."> attributes in system prompt     │
│  e.g. <skill name="brave-search"> → <skill name="web:brave- │
│  search">                                                   │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
               LLM calls "deploy:push"
               → matches namespaced tool in registry
               → original execute() runs via Object.create delegation

Built-in tool override safety

If an extension overrides a built-in tool by name (like pi-code-previews overrides read), and that extension also has a namespace, pi-namespace skips the rewrite — namespacing it would break the override mechanism:

pi-code-previews registers tool { name: "read" }  (override)
  → namespace config maps pi-code-previews → "preview"
  → BUT "read" is a built-in override → SKIP
  → tool stays as "read", not "preview:read"

With explicit builtinNamespace: "fs":
  → built-in "read" → "fs:read"
  → override "read" from pi-code-previews → still "read" (override wins)

Only use builtinNamespace when you want to namespace all built-in tools explicitly. It intentionally does not affect extension overrides of those built-ins.

Object.create delegation

The patch does not copy tool definitions. It creates a delegating object:

const namespacedDef = Object.create(tool.definition);
namespacedDef.name = "deploy:push";                    // override name
namespacedDef.promptSnippet = "Use deploy:push to..."; // override snippet
namespacedDef.promptGuidelines = ["Use deploy:push"];  // override guidelines
// execute(), parameters(), renderCall(), renderResult() — all inherited

This means the original execute function runs with zero wrapping overhead. No proxy, no shim — just prototype chain delegation.


Known Limitations

Limitation Details
Built-in type guards break isToolCallEventType("bash", event) won't match fs:bash. Use stripNamespace() to resolve back to bash.
Tool override by name Registering shell:bash won't override built-in bash since the names differ.
Fragile to pi internals A pi update could change ExtensionRunner API. The patch targets a single stable method.
Skill namespacing is prompt-only Skills are XML text in the system prompt — we rewrite <skill name> attributes but can't intercept /skill:ns:name invocations.

Development

npm install            # install dev dependencies
npm test               # run tests (25 tests)
npm run typecheck      # TypeScript validation
npm run lint:dead      # dead code detection (knip)
npm run test:coverage  # tests with coverage

Related Projects

Project Description
pi-tool-repair Fix common LLM tool-call mistakes before tools execute
pi-lazy-extensions Lazy-load extensions on demand via ToolSearch-style proxy
pi-startup-tracer Trace per-extension load and handler timing
pi-toggle-skills Toggle skill visibility in the system prompt
pi-trust-defer Skip the startup trust prompt

License

MIT