Ship an extension, skills, and prompts as one Pi package
Walk an install, filter, and remove cycle, and keep host-provided packages in peerDependencies so the host does not warn.
A Pi package is the distribution unit for everything you have written: an
extension, its skills, prompts and themes arrive as one directory or npm package
instead of a zip of a .pi folder that breaks on someone else’s machine. There is
no standalone code file for this chapter, so this page is a command walkthrough
with placeholder names, grounded in what the shipped-binary test asserts.
Install, list, remove
pi install npm:@example/pi-tools@1.0.0 # pinned npm version
pi install git:github.com/example/pi-tools@v1 # cloned, reconciled to the ref
pi install ./local-package # loaded from its path; nothing copied
pi list # what resolves, as one instance
pi remove <source> # and its resources stop loading
pi -e npm:@example/pi-tools # try one invocation; no settings write
pi install --local ./local-package # writes the project's .pi/settings.jsonA local install records a path relative to the settings file and copies nothing;
--local writes under the project’s .pi and loads only after project trust is
granted. Identity by resolved path — a local package by its absolute path, a git
package by repository URL without the ref — means the same package listed twice
loads once.
Filter what loads
Settings can narrow a package per resource type. [] means none, !pattern
excludes, and a filter can never expose a resource the package did not declare:
{
"packages": [
{
"source": "npm:@example/pi-tools",
"extensions": ["extensions/*.ts", "!extensions/legacy.ts"],
"skills": ["x"],
"prompts": []
}
]
}The dependency rule
Pi supplies five packages to your extension code, and they must never be bundled —
a physical copy bypasses the module mapping and creates duplicate classes,
registries and initialization. Declare them as peer dependencies with a * range:
{
"name": "pi-guard",
"version": "0.3.0",
"keywords": ["pi-package"],
"peerDependencies": { "@earendil-works/pi-coding-agent": "*", "@earendil-works/pi-ai": "*" },
"dependencies": { "fast-glob": "^3.3.2" },
"pi": { "extensions": ["./src/extension.ts"] }
}The same five in dependencies draw the warning in the table below.
Expected outcome
| Command / declaration | Behaviour (observed via the shipped binary) |
|---|---|
pi install ./my-pkg |
Records ../my-pkg in the user settings, relative to the settings file; copies nothing |
pi list / pi remove |
Shows it; removes it; the template stops expanding |
| No manifest | extensions/, skills/, prompts/ discovered by convention; the extension’s factory ran once |
pi -e ./my-pkg |
Loads for one invocation; writes nothing to settings |
"prompts": [] filter |
No prompts load; the extension still loads |
| Same package listed twice | Loads once — identity is the resolved path |
pi install --local |
Written to the project’s .pi/settings.json; loads only after trust is granted |
Host package in dependencies |
Warning; with peerDependencies "*" there is none |
Mechanism and limitations
Observed: packages.test.ts drives the shipped pi binary against a real
package directory with a scripted model and asserts the table above. Documented:
packages.md pins versioned git refs (updates reconcile the checkout but do not
move a configured ref), and states “Filters narrow the package manifest. They do
not expose resources that the package itself did not declare.”
This example runs nothing for you and shows no model. Git and npm sources, pi update, and autoload: false were not run by the test. A package can execute
extension code and instruct the model to run programs, so review third-party
source before installing — the entry is a gate on executable content, not a
bookmark.
Understand this example
Copy this prompt into your AI tool. No code runs here.
Apply this example
Copy this prompt into your AI tool. No code runs here.