- pi install <type> <source> (global default, -p for project) - pi remove <type> <name> - pi update [types...] - Install adds to settings.json + installs to disk - Global is default, -p/--project for project-local
28 KiB
Extension Loading
Unified system for loading hooks, tools, skills, and themes from local files, directories, npm packages, and git repositories.
Extension Types
| Type | Root Entry | Subdir Entry | Purpose |
|---|---|---|---|
| Hooks | *.ts / *.js |
index.ts / index.js / package.json main |
Event handlers for agent lifecycle |
| Tools | *.ts / *.js |
index.ts / index.js / package.json main |
Custom tools for the agent |
| Skills | *.md |
SKILL.md |
Context/instructions loaded into agent |
| Themes | *.theme.json |
*.theme.json (recursive) |
Color schemes for TUI |
Note: Themes use *.theme.json pattern scanned recursively at all levels, allowing flat theme packs without requiring subdirectories.
Sources
Extensions can be loaded from:
File Paths
./my-hook.ts
~/global-hook.ts
/absolute/path/hook.ts
Directories
./my-hooks/
~/.pi/agent/hooks/
npm Packages
npm:package-name
npm:package-name@1.2.3
npm:package-name@latest
npm:@scope/package-name
npm:@scope/package-name@1.2.3
Git Repositories
git:https://github.com/user/repo
git:https://github.com/user/repo@v1.0.0 # tag
git:https://github.com/user/repo@abc123f # commit
git:https://github.com/user/repo#branch # branch
Storage Layout
Permanent (settings.json)
~/.pi/agent/
hooks/
my-local-hook.ts # root-level file
complex-hook/ # directory with entry point
index.ts
utils.ts
npm/
my-hook@1.2.3/ # npm package
package.json
node_modules/
index.js
@scope/
scoped-hook@2.0.0/
...
git/
github.com/user/repo@v1.0.0/ # git repo
...
tools/
... (same structure)
skills/
... (same structure, but SKILL.md instead of index.ts)
themes/
dark.theme.json # root-level theme
light.theme.json
community-pack/ # theme pack (no entry point needed)
nord.theme.json
dracula.theme.json
npm/
cool-themes@1.0.0/
monokai.theme.json
solarized.theme.json
Ephemeral (CLI flags)
/tmp/pi-extensions/
hooks/
npm/
my-hook@1.0.0/
git/
github.com/user/repo@v1.0.0/
tools/
...
skills/
...
themes/
...
Temp directory persists until OS clears /tmp/. No re-download needed across sessions (usually).
Entry Point Resolution
For each discovered directory, resolve entry point in order:
Hooks & Tools
index.ts(if exists)index.js(if exists)mainfield inpackage.json(if exists)
Skills
SKILL.md(required)
Themes
Themes use recursive pattern matching instead of fixed entry points:
- Scan recursively for
*.theme.jsonfiles at all levels - Each matching file is a separate theme
- Path derived from filename (e.g.,
dark.theme.json→dark,pack/nord.theme.json→pack/nord)
Scanning Algorithm
scan(baseDir, config):
results = []
for entry in baseDir:
skip if entry.name starts with "."
skip if entry.name == "node_modules"
skip if entry.name ends with ".installing"
if entry is file:
if matches rootPattern (e.g., *.ts, *.js, *.md, *.theme.json):
results.add(entry)
if entry is directory:
if config.recursivePattern:
# For themes: scan recursively for *.theme.json everywhere
results.addAll(scan(directory, config))
else if has entryPoint (index.ts, index.js, SKILL.md):
results.add(directory) # load as single extension
else:
results.addAll(scan(directory, config)) # recurse to find extensions
return results
Default directories scanned (always, regardless of settings.json):
~/.pi/agent/<type>/<cwd>/.pi/<type>/
Extension Packs
A key use case is pulling in a pack (collection) of extensions via a directory, npm package, or git repo, then filtering to a subset.
Example: Skill pack
npm:pi-skills@1.0.0 contains:
skills/
brave-search/SKILL.md
browser-tools/SKILL.md
transcribe/SKILL.md
youtube-transcript/SKILL.md
... (10+ skills)
You want only 2 of them:
{
"skills": {
"paths": ["npm:pi-skills@1.0.0"],
"filter": ["brave-search", "youtube-transcript"]
}
}
Example: Theme pack
npm:community-themes@1.0.0 contains:
themes/
nord.theme.json
dracula.theme.json
solarized-dark.theme.json
solarized-light.theme.json
monokai.theme.json
Exclude solarized variants:
{
"themes": {
"paths": ["npm:community-themes@1.0.0"],
"filter": ["!solarized-*"]
}
}
Example: Hook pack
npm:audit-hooks@1.0.0 contains:
hooks/
file-audit/index.ts
command-audit/index.ts
network-audit/index.ts
debug-logger/index.ts
All except debug:
{
"hooks": {
"paths": ["npm:audit-hooks@1.0.0"],
"filter": ["!debug-*"]
}
}
Filtering
Single filter array with ! prefix for exclusion. Patterns are matched against extension paths (directory or filename without extension).
{
"filter": ["pattern1", "pattern2", "!excluded-pattern"]
}
Logic:
- Collect all patterns without
!prefix → include patterns - Collect all patterns with
!prefix → exclude patterns - If include patterns exist: start with extensions matching any include pattern
- If no include patterns: start with all extensions
- Remove extensions matching any exclude pattern
Examples:
["brave-search"] // only brave-search
["brave-*", "docker"] // brave-search, brave-api, docker
["!transcribe"] // all except transcribe
["audit-*", "!audit-debug"] // audit-* except audit-debug
Patterns are glob patterns matched against extension paths.
CLI Arguments
Adding Sources
pi --hook <path|npm:|git:> # add hook source (repeatable)
pi --tool <path|npm:|git:> # add custom tool source (repeatable)
pi --skill <path|npm:|git:> # add skill source (repeatable)
pi --theme <path|npm:|git:> # add theme source (repeatable)
Installation locations for npm/git sources:
| Source | Install location |
|---|---|
| CLI flags | /tmp/pi-extensions/<type>/npm/ or git/ |
Global settings (~/.pi/agent/settings.json) |
~/.pi/agent/<type>/npm/ or git/ |
Project settings (<cwd>/.pi/settings.json) |
<cwd>/.pi/<type>/npm/ or git/ |
File/directory paths are used directly (no installation).
- CLI = ephemeral: cached in temp until OS clears
/tmp/ - Global settings = permanent: installed to user's agent directory
- Project settings = project-local: installed to project's
.pi/directory
Examples:
--hook npm:my-hook@1.0.0→/tmp/pi-extensions/hooks/npm/my-hook@1.0.0/- Global settings.json
npm:my-hook@1.0.0→~/.pi/agent/hooks/npm/my-hook@1.0.0/ - Project settings.json
npm:my-hook@1.0.0→<cwd>/.pi/hooks/npm/my-hook@1.0.0/
This encourages: try via CLI, if you like it, add to settings.json for permanent install.
Filtering
pi --hooks "pattern1,pattern2,!excluded" # filter hooks
pi --custom-tools "pattern1,!excluded" # filter custom tools
pi --skills "pattern1,pattern2" # filter skills
pi --themes "pattern1" # filter themes
Disabling
pi --no-hooks # disable all hooks
pi --no-custom-tools # disable all custom tools
pi --no-skills # disable all skills (already exists)
Built-in Tools
pi --tools read,bash,edit,write # select which built-in tools to enable (unchanged)
Settings Hierarchy
Extensions are configured in settings.json at two levels:
- Global:
~/.pi/agent/settings.json - Project:
<cwd>/.pi/settings.json
Merge behavior:
paths: additive - project paths are added to global pathsfilter: override - project filter replaces global filter if specified
Example:
// Global: ~/.pi/agent/settings.json
{
"hooks": {
"paths": ["npm:audit-hooks@1.0.0"],
"filter": ["!debug-*"]
}
}
// Project: .pi/settings.json
{
"hooks": {
"paths": ["./project-hooks/"],
"filter": ["audit-*"] // overrides global filter
}
}
// Effective:
{
"hooks": {
"paths": ["npm:audit-hooks@1.0.0", "./project-hooks/"],
"filter": ["audit-*"]
}
}
settings.json Structure
{
"hooks": {
"paths": [
"./my-hooks/",
"npm:@scope/hook@1.0.0",
"git:https://github.com/user/hooks@v1.0.0"
],
"filter": ["audit-*", "!audit-debug"]
},
"tools": {
"paths": ["npm:cool-tools@2.0.0"],
"filter": ["!dangerous-tool"]
},
"skills": {
"paths": ["npm:pi-skills@1.0.0", "~/my-skills/"],
"filter": ["brave-search", "git-*", "!git-legacy"]
},
"themes": {
"paths": ["npm:community-themes@1.0.0"]
}
}
Migration from current format:
hooks: string[]→hooks.paths: string[]customTools: string[]→tools.paths: string[]skills.customDirectories→skills.pathsskills.includeSkills→skills.filter(patterns without!)skills.ignoredSkills→skills.filter(patterns with!prefix)
Installation Flow
Target directory depends on source:
- CLI flags:
/tmp/pi-extensions/<type>/npm/orgit/ - Global settings.json:
~/.pi/agent/<type>/npm/orgit/ - Project settings.json:
<cwd>/.pi/<type>/npm/orgit/
Atomic Installation
To prevent corrupted state from interrupted installs (Ctrl+C):
- Install to
<target>.installing/(temporary) - On success, atomically rename to
<target>/ - If interrupted,
<target>/doesn't exist → next run retries cleanly - Scanner filters out
*.installingdirectories (see Scanning Algorithm)
npm Packages
- Parse specifier:
npm:@scope/pkg@1.2.3→ name:@scope/pkg, version:1.2.3 - Determine target dir based on source (CLI → temp, global → agent dir, project → cwd/.pi/)
- If
<target>/exists and has matching version in package.json → skip install - Otherwise:
- Remove stale
<target>.installing/if exists npm pack @scope/pkg@1.2.3→ download tarball- Extract to
<target>.installing/ - If
package.jsonhasdependencies→ runnpm install - Rename
<target>.installing/→<target>/
- Remove stale
Git Repositories
- Parse specifier:
git:https://github.com/user/repo@v1.0.0 - Determine target dir based on source (CLI → temp, global → agent dir, project → cwd/.pi/)
- If
<target>/exists → skip clone - Otherwise:
- Remove stale
<target>.installing/if exists git clone <url>to<target>.installing/git checkout <tag|commit|branch>- If
package.jsonhasdependencies→ runnpm install - Rename
<target>.installing/→<target>/
- Remove stale
Extension Management Commands
Install
Adds extension to settings.json and installs to disk.
pi install <type> <source> # global (default)
pi install <type> -p <source> # project-local
pi install <type> --project <source> # project-local
# Examples:
pi install hook npm:@scope/my-hook@1.0.0
# → adds to ~/.pi/agent/settings.json
# → installs to ~/.pi/agent/hooks/npm/@scope/my-hook@1.0.0/
pi install tool -p git:https://github.com/user/tool@v1.0.0
# → adds to <cwd>/.pi/settings.json
# → installs to <cwd>/.pi/tools/git/github.com/user/tool@v1.0.0/
Remove
Removes extension from settings.json and deletes from disk.
pi remove <type> <name> # from global
pi remove <type> -p <name> # from project
# Examples:
pi remove hook my-hook # from ~/.pi/agent/settings.json + delete
pi remove skill -p brave-search # from <cwd>/.pi/settings.json + delete
Update
Updates npm/git extensions to latest versions.
pi update # all (project + global)
pi update -p # project only
pi update <type>... # specific types
pi update -p <type>... # project, specific types
# Examples:
pi update # update everything
pi update hook tool # update hooks and tools
pi update -p skill # update project skills only
Update behavior:
npm:pkg@<version>: check if newer version exists (e.g.,@latestresolves to newer)git:repo#branch:git pullgit:repo@tagorgit:repo@commit: no-op (pinned)- Local files/directories: no-op
Loading Flow (Full)
-
Collect sources:
- Default directories:
~/.pi/agent/<type>/,./.pi/<type>/ - settings.json
<type>.paths - CLI
--<type>arguments
- Default directories:
-
Install remote sources:
- Process
npm:andgit:specifiers - Install to
~/.pi/agent/<type>/npm/orgit/
- Process
-
Scan all sources:
- Recursively discover extensions
- Compute relative path for each
-
Apply filter:
- Combine settings.json
<type>.filterand CLI--<type>spatterns - Filter by path (no loading yet)
- Combine settings.json
-
Load survivors:
- Parse/execute only extensions that passed filter
- Validate (frontmatter, exports, schema)
- Report errors for invalid extensions
Implementation Plan
Overview
This implementation consolidates four separate loading systems (hooks, tools, skills, themes) into a unified extension loading framework with shared logic for source resolution, installation, scanning, filtering, and loading.
New Files
src/core/extensions/types.ts
Extension type definitions shared across all loaders.
export type ExtensionType = "hooks" | "tools" | "skills" | "themes";
export interface ExtensionSource {
type: "file" | "directory" | "npm" | "git";
specifier: string; // original specifier from config/CLI
resolvedPath?: string; // resolved local path after install
}
export interface DiscoveredExtension {
path: string; // relative path (e.g., "brave-search", "npm/@scope/pkg@1.0.0")
absolutePath: string; // absolute filesystem path
entryPoint: string; // resolved entry point file
source: ExtensionSource;
}
export interface ExtensionConfig {
paths?: string[];
filter?: string[];
}
export interface ExtensionTypeConfig {
rootPatterns: string[]; // e.g., ["*.ts", "*.js"]
subdirEntryPoints: string[]; // e.g., ["index.ts", "index.js"]
packageJsonFallback: boolean; // whether to check package.json main
}
export const EXTENSION_CONFIGS: Record<ExtensionType, ExtensionTypeConfig> = {
hooks: {
rootPatterns: ["*.ts", "*.js"],
subdirEntryPoints: ["index.ts", "index.js"],
packageJsonFallback: true,
recursivePattern: false,
},
tools: {
rootPatterns: ["*.ts", "*.js"],
subdirEntryPoints: ["index.ts", "index.js"],
packageJsonFallback: true,
recursivePattern: false,
},
skills: {
rootPatterns: ["*.md"],
subdirEntryPoints: ["SKILL.md"],
packageJsonFallback: false,
recursivePattern: false,
},
themes: {
rootPatterns: ["*.theme.json"],
subdirEntryPoints: [], // not used
packageJsonFallback: false,
recursivePattern: true, // scan for *.theme.json at all levels
},
};
src/core/extensions/source-resolver.ts
Handles parsing and installing npm/git sources.
export function parseSource(specifier: string): ExtensionSource;
export type InstallLocation = "cli" | "global" | "project";
export async function installSource(
source: ExtensionSource,
type: ExtensionType,
location: InstallLocation,
cwd: string, // needed for project-local installs
): Promise<string>;
export function isRemoteSource(specifier: string): boolean;
export function getInstallDir(type: ExtensionType, location: InstallLocation, cwd: string): string;
Key functions:
parseNpmSpecifier(spec): Parsenpm:@scope/pkg@1.2.3→{ name, version }parseGitSpecifier(spec): Parsegit:url@tagorgit:url#branchinstallNpmPackage(name, version, targetDir):npm pack+ extract +npm installinstallGitRepo(url, ref, targetDir):git clone+ checkout +npm installgetTargetDir(type, ephemeral): Returns temp dir or agent dir based on source
src/core/extensions/scanner.ts
Unified recursive scanning for all extension types.
export function scanDirectory(
baseDir: string,
config: ExtensionTypeConfig,
): DiscoveredExtension[];
export function resolveEntryPoint(
dir: string,
config: ExtensionTypeConfig,
): string | null;
export function getRelativePath(
absolutePath: string,
baseDir: string,
config: ExtensionTypeConfig,
): string; // strips entry point filename and extension
src/core/extensions/filter.ts
Filter logic using glob patterns with ! exclusion. Matches against extension.path.
export function applyFilter(
extensions: DiscoveredExtension[],
patterns: string[],
): DiscoveredExtension[];
export function parseFilterPatterns(patterns: string[]): {
include: string[];
exclude: string[];
};
export function matchesPattern(path: string, pattern: string): boolean;
src/core/extensions/loader.ts
Main entry point coordinating the full loading flow.
export interface LoadExtensionsOptions {
type: ExtensionType;
cwd: string;
agentDir: string;
globalPaths: string[]; // from global settings.json → install to agentDir
projectPaths: string[]; // from project settings.json → install to cwd/.pi/
cliPaths: string[]; // from CLI flags → install to /tmp/
filter: string[]; // combined filter patterns
}
export interface LoadExtensionsResult<T> {
extensions: T[];
errors: Array<{ path: string; error: string }>;
}
export async function discoverExtensions(
options: LoadExtensionsOptions,
): Promise<DiscoveredExtension[]>;
src/core/extensions/index.ts
Public exports.
Modified Files
src/config.ts
Add directory getters:
export function getHooksDir(): string {
return join(getAgentDir(), "hooks");
}
export function getSkillsDir(): string {
return join(getAgentDir(), "skills");
}
// getToolsDir() already exists
// getThemesDir() = bundled themes (in package)
// getCustomThemesDir() = ~/.pi/agent/themes/ (user themes) - already exists
src/core/settings-manager.ts
Update Settings interface:
// Old:
hooks?: string[];
customTools?: string[];
skills?: SkillsSettings;
// New:
hooks?: ExtensionConfig;
tools?: ExtensionConfig;
skills?: ExtensionConfig; // simplified from SkillsSettings
themes?: ExtensionConfig;
Add migration logic for old format:
function migrateSettings(settings: unknown): Settings {
// Convert hooks: string[] → hooks: { paths: string[] }
// Convert customTools: string[] → tools: { paths: string[] }
// Convert skills.customDirectories → skills.paths
// Convert skills.includeSkills/ignoredSkills → skills.filter
}
Add unified getters:
getExtensionConfig(type: ExtensionType): ExtensionConfig;
getExtensionPaths(type: ExtensionType): string[];
getExtensionFilter(type: ExtensionType): string[];
Update merge logic (paths are additive, filter overrides):
function mergeExtensionConfig(global: ExtensionConfig, project: ExtensionConfig): ExtensionConfig {
return {
paths: [...(global.paths ?? []), ...(project.paths ?? [])], // additive
filter: project.filter ?? global.filter, // override
};
}
src/cli/args.ts
Update Args interface:
// Built-in tools (unchanged)
tools?: ToolName[]; // --tools read,bash,edit,write
// Source flags
hooks?: string[]; // --hook (existing, repeatable)
customTools?: string[]; // --tool (existing, repeatable)
skills?: string[]; // --skill (new, repeatable)
themes?: string[]; // --theme (new, repeatable)
// Filter flags
hooksFilter?: string[]; // --hooks "patterns"
customToolsFilter?: string[];// --custom-tools "patterns"
skillsFilter?: string[]; // --skills "patterns" (existing)
themesFilter?: string[]; // --themes "patterns"
// Disable flags
noHooks?: boolean; // --no-hooks
noCustomTools?: boolean; // --no-custom-tools
noSkills?: boolean; // --no-skills (existing)
Update argument parsing:
// --tools (built-in tools, unchanged)
} else if (arg === "--tools" && i + 1 < args.length) {
// ... existing logic for built-in tools
// --tool (add custom tool source)
} else if (arg === "--tool" && i + 1 < args.length) {
result.customTools = result.customTools ?? [];
result.customTools.push(args[++i]);
// --custom-tools (filter custom tools)
} else if (arg === "--custom-tools" && i + 1 < args.length) {
result.customToolsFilter = args[++i].split(",").map(s => s.trim());
// --no-custom-tools
} else if (arg === "--no-custom-tools") {
result.noCustomTools = true;
// --skill (add source) - new
} else if (arg === "--skill" && i + 1 < args.length) {
result.skills = result.skills ?? [];
result.skills.push(args[++i]);
// --theme (add source) - new
} else if (arg === "--theme" && i + 1 < args.length) {
result.themes = result.themes ?? [];
result.themes.push(args[++i]);
// --themes (filter) - new
} else if (arg === "--themes" && i + 1 < args.length) {
result.themesFilter = args[++i].split(",").map(s => s.trim());
// --hooks (filter) - new
} else if (arg === "--hooks" && i + 1 < args.length) {
result.hooksFilter = args[++i].split(",").map(s => s.trim());
// --no-hooks - new
} else if (arg === "--no-hooks") {
result.noHooks = true;
Add subcommand handling for pi install, pi remove, pi update.
src/core/hooks/loader.ts
Refactor to use extension system:
import { discoverExtensions, type DiscoveredExtension } from "../extensions/index.js";
export async function discoverAndLoadHooks(
options: {
cwd: string;
agentDir?: string;
configuredPaths?: string[];
cliPaths?: string[];
filter?: string[];
}
): Promise<LoadHooksResult> {
const discovered = await discoverExtensions({
type: "hooks",
defaultDirs: [join(agentDir, "hooks"), join(cwd, ".pi", "hooks")],
configuredPaths: options.configuredPaths ?? [],
cliPaths: options.cliPaths ?? [],
filter: options.filter ?? [],
});
// Load each discovered hook using existing jiti logic
const results = await Promise.all(
discovered.map(ext => loadHook(ext.entryPoint, cwd))
);
// ... rest of existing logic
}
Remove duplicate code:
expandPath()→ use from extensions/source-resolver.tsresolveHookPath()→ use from extensions/scanner.ts- Discovery logic → use discoverExtensions()
src/core/custom-tools/loader.ts
Same refactoring pattern as hooks/loader.ts.
src/core/skills.ts
Refactor loadSkills() and loadSkillsFromDir():
export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
const discovered = await discoverExtensions({
type: "skills",
defaultDirs: [
// existing default dirs
join(homedir(), ".codex", "skills"),
join(homedir(), ".claude", "skills"),
join(agentDir, "skills"),
join(cwd, ".pi", "skills"),
],
configuredPaths: options.paths ?? [],
cliPaths: options.cliPaths ?? [],
filter: options.filter ?? [],
});
// Load each discovered skill using existing parsing logic
// ...
}
Remove:
loadSkillsFromDirInternal()recursive logic → use scanner.tsmatchesIncludePatterns()/matchesIgnorePatterns()→ use filter.ts
src/modes/interactive/theme/theme.ts
Refactor getAvailableThemes() and loadThemeJson():
export function getAvailableThemes(): string[] {
const discovered = discoverExtensions({
type: "themes",
defaultDirs: [getThemesDir(), getCustomThemesDir()],
configuredPaths: settingsManager.getExtensionPaths("themes"),
cliPaths: [], // from args
filter: settingsManager.getExtensionFilter("themes"),
});
return discovered.map(ext => ext.path);
}
src/core/sdk.ts
Update to pass new options structure:
// Hooks
const { hooks, errors } = await discoverAndLoadHooks({
cwd,
agentDir,
configuredPaths: settingsManager.getExtensionPaths("hooks"),
cliPaths: options.additionalHookPaths,
filter: [...settingsManager.getExtensionFilter("hooks"), ...(options.hooksFilter ?? [])],
});
// Tools
const result = await discoverAndLoadCustomTools({
cwd,
agentDir,
configuredPaths: settingsManager.getExtensionPaths("tools"),
cliPaths: options.additionalToolPaths,
filter: [...settingsManager.getExtensionFilter("tools"), ...(options.toolsFilter ?? [])],
builtInToolNames: Object.keys(allTools),
});
src/modes/interactive/interactive-mode.ts
Update skill loading to use new options structure.
Migration & Backwards Compatibility
Settings Migration
When loading settings.json, detect old format and migrate:
// Old format detection
if (Array.isArray(settings.hooks)) {
settings.hooks = { paths: settings.hooks };
}
if (Array.isArray(settings.customTools)) {
settings.tools = { paths: settings.customTools };
delete settings.customTools;
}
if (settings.skills?.customDirectories) {
settings.skills.paths = settings.skills.customDirectories;
delete settings.skills.customDirectories;
}
// ... etc
CLI Compatibility
--toolswith built-in tool names still works (detected by checking if values match known tool names)- Alternatively, deprecation warning and suggest
--builtin-tools
Implementation Order
-
Phase 1: Core extension framework
- Create
src/core/extensions/directory - Implement types.ts, scanner.ts, filter.ts
- Unit tests for scanning and filtering
- Create
-
Phase 2: Source resolution
- Implement source-resolver.ts (npm + git)
- Add
npm packandgit clonelogic - Unit tests for source parsing and installation
-
Phase 3: Settings migration
- Update settings-manager.ts with new types
- Add migration logic
- Update config.ts with new directory getters
-
Phase 4: Refactor loaders
- Refactor hooks/loader.ts
- Refactor custom-tools/loader.ts
- Refactor skills.ts
- Refactor theme.ts
- Remove duplicate code
-
Phase 5: CLI updates
- Add new flags to args.ts
- Update help text
- Add
pi install,pi remove,pi updatesubcommands
-
Phase 6: Integration
- Update sdk.ts
- Update interactive-mode.ts
- End-to-end testing
-
Phase 7: Documentation
- Update README.md
- Update docs/hooks.md, docs/custom-tools.md
- Add examples for npm/git extensions
Testing Strategy
Unit Tests
test/extensions/scanner.test.ts: Directory scanning, entry point resolutiontest/extensions/filter.test.ts: Pattern matching, include/exclude logictest/extensions/source-resolver.test.ts: npm/git specifier parsing
Integration Tests
test/extensions/npm-install.test.ts: Full npm package installation flowtest/extensions/git-clone.test.ts: Full git repository cloning flowtest/extensions/loading.test.ts: End-to-end extension discovery and loading
Migration Tests
test/settings-migration.test.ts: Old → new settings format conversion
File Summary
New Files (7)
src/core/extensions/types.tssrc/core/extensions/source-resolver.tssrc/core/extensions/scanner.tssrc/core/extensions/filter.tssrc/core/extensions/loader.tssrc/core/extensions/index.tsdocs/extension-loading.md(this file)
Modified Files (9)
src/config.ts- add directory getterssrc/core/settings-manager.ts- new types, migrationsrc/cli/args.ts- new flags, update parsingsrc/core/hooks/loader.ts- refactor to use extensionssrc/core/custom-tools/loader.ts- refactor to use extensionssrc/core/skills.ts- refactor to use extensionssrc/modes/interactive/theme/theme.ts- refactor to use extensionssrc/core/sdk.ts- update option passingsrc/modes/interactive/interactive-mode.ts- update skill loading
Deleted Code (moved to extensions/)
- Duplicate
expandPath(),normalizeUnicodeSpaces()functions - Duplicate discovery/scanning logic
- Duplicate path resolution logic