feat(coding-agent): ResourceLoader, package management, and /reload command (#645)

- Add ResourceLoader interface and DefaultResourceLoader implementation
- Add PackageManager for npm/git extension sources with install/remove/update
- Add session.reload() and session.bindExtensions() APIs
- Add /reload command in interactive mode
- Add CLI flags: --skill, --theme, --prompt-template, --no-themes, --no-prompt-templates
- Add pi install/remove/update commands for extension management
- Refactor settings.json to use arrays for skills, prompts, themes
- Remove legacy SkillsSettings source flags and filters
- Update SDK examples and documentation for ResourceLoader pattern
- Add theme registration and loadThemeFromPath for dynamic themes
- Add getShellEnv to include bin dir in PATH for bash commands
This commit is contained in:
Mario Zechner 2026-01-20 23:34:53 +01:00
parent 866d21c252
commit b846a4bfcf
51 changed files with 2724 additions and 1852 deletions

View file

@ -5,11 +5,11 @@
*/
import { getModel } from "@mariozechner/pi-ai";
import { createAgentSession, discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
import { AuthStorage, createAgentSession, ModelRegistry } from "@mariozechner/pi-coding-agent";
// Set up auth storage and model registry
const authStorage = discoverAuthStorage();
const modelRegistry = discoverModels(authStorage);
const authStorage = new AuthStorage();
const modelRegistry = new ModelRegistry(authStorage);
// Option 1: Find a specific built-in model by provider/id
const opus = getModel("anthropic", "claude-opus-4-5");

View file

@ -4,12 +4,18 @@
* Shows how to replace or modify the default system prompt.
*/
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
import { createAgentSession, DefaultResourceLoader, SessionManager } from "@mariozechner/pi-coding-agent";
// Option 1: Replace prompt entirely
const { session: session1 } = await createAgentSession({
systemPrompt: `You are a helpful assistant that speaks like a pirate.
const loader1 = new DefaultResourceLoader({
systemPromptOverride: () => `You are a helpful assistant that speaks like a pirate.
Always end responses with "Arrr!"`,
appendSystemPromptOverride: () => [],
});
await loader1.reload();
const { session: session1 } = await createAgentSession({
resourceLoader: loader1,
sessionManager: SessionManager.inMemory(),
});
@ -23,13 +29,17 @@ console.log("=== Replace prompt ===");
await session1.prompt("What is 2 + 2?");
console.log("\n");
// Option 2: Modify default prompt (receives default, returns modified)
const { session: session2 } = await createAgentSession({
systemPrompt: (defaultPrompt) => `${defaultPrompt}
// Option 2: Append instructions to the default prompt
const loader2 = new DefaultResourceLoader({
appendSystemPromptOverride: (base) => [
...base,
"## Additional Instructions\n- Always be concise\n- Use bullet points when listing things",
],
});
await loader2.reload();
## Additional Instructions
- Always be concise
- Use bullet points when listing things`,
const { session: session2 } = await createAgentSession({
resourceLoader: loader2,
sessionManager: SessionManager.inMemory(),
});

View file

@ -5,20 +5,7 @@
* Discover, filter, merge, or replace them.
*/
import { createAgentSession, discoverSkills, SessionManager, type Skill } from "@mariozechner/pi-coding-agent";
// Discover all skills from cwd/.pi/skills, ~/.pi/agent/skills, etc.
const { skills: allSkills, warnings } = discoverSkills();
console.log(
"Discovered skills:",
allSkills.map((s) => s.name),
);
if (warnings.length > 0) {
console.log("Warnings:", warnings);
}
// Filter to specific skills
const filteredSkills = allSkills.filter((s) => s.name.includes("browser") || s.name.includes("search"));
import { createAgentSession, DefaultResourceLoader, SessionManager, type Skill } from "@mariozechner/pi-coding-agent";
// Or define custom skills inline
const customSkill: Skill = {
@ -29,19 +16,30 @@ const customSkill: Skill = {
source: "custom",
};
// Use filtered + custom skills
const loader = new DefaultResourceLoader({
skillsOverride: (current) => {
const filteredSkills = current.skills.filter((s) => s.name.includes("browser") || s.name.includes("search"));
return {
skills: [...filteredSkills, customSkill],
diagnostics: current.diagnostics,
};
},
});
await loader.reload();
// Discover all skills from cwd/.pi/skills, ~/.pi/agent/skills, etc.
const discovered = loader.getSkills();
console.log(
"Discovered skills:",
discovered.skills.map((s) => s.name),
);
if (discovered.diagnostics.length > 0) {
console.log("Warnings:", discovered.diagnostics);
}
await createAgentSession({
skills: [...filteredSkills, customSkill],
resourceLoader: loader,
sessionManager: SessionManager.inMemory(),
});
console.log(`Session created with ${filteredSkills.length + 1} skills`);
// To disable all skills:
// skills: []
// To use discovery with filtering via settings:
// discoverSkills(process.cwd(), undefined, {
// ignoredSkills: ["browser-tools"], // glob patterns to exclude
// includeSkills: ["brave-*"], // glob patterns to include (empty = all)
// })
console.log("Session created with filtered skills");

View file

@ -13,16 +13,25 @@
* export default function (pi: ExtensionAPI) { ... }
*/
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
import { createAgentSession, DefaultResourceLoader, SessionManager } from "@mariozechner/pi-coding-agent";
// Extensions are discovered automatically from standard locations.
// You can also add paths via:
// 1. settings.json: { "extensions": ["./my-extension.ts"] }
// 2. additionalExtensionPaths option (see below)
// You can also add paths via settings.json or DefaultResourceLoader options.
// To add additional extension paths beyond discovery:
const { session } = await createAgentSession({
const resourceLoader = new DefaultResourceLoader({
additionalExtensionPaths: ["./my-logging-extension.ts", "./my-safety-extension.ts"],
extensionFactories: [
(pi) => {
pi.on("agent_start", () => {
console.log("[Inline Extension] Agent starting");
});
},
],
});
await resourceLoader.reload();
const { session } = await createAgentSession({
resourceLoader,
sessionManager: SessionManager.inMemory(),
});

View file

@ -4,33 +4,36 @@
* Context files provide project-specific instructions loaded into the system prompt.
*/
import { createAgentSession, discoverContextFiles, SessionManager } from "@mariozechner/pi-coding-agent";
import { createAgentSession, DefaultResourceLoader, SessionManager } from "@mariozechner/pi-coding-agent";
// Discover AGENTS.md files walking up from cwd
const discovered = discoverContextFiles();
console.log("Discovered context files:");
for (const file of discovered) {
console.log(` - ${file.path} (${file.content.length} chars)`);
}
// Use custom context files
await createAgentSession({
contextFiles: [
...discovered,
{
path: "/virtual/AGENTS.md",
content: `# Project Guidelines
const loader = new DefaultResourceLoader({
agentsFilesOverride: (current) => ({
agentsFiles: [
...current.agentsFiles,
{
path: "/virtual/AGENTS.md",
content: `# Project Guidelines
## Code Style
- Use TypeScript strict mode
- No any types
- Prefer const over let`,
},
],
},
],
}),
});
await loader.reload();
// Discover AGENTS.md files walking up from cwd
const discovered = loader.getAgentsFiles().agentsFiles;
console.log("Discovered context files:");
for (const file of discovered) {
console.log(` - ${file.path} (${file.content.length} chars)`);
}
await createAgentSession({
resourceLoader: loader,
sessionManager: SessionManager.inMemory(),
});
console.log(`Session created with ${discovered.length + 1} context files`);
// Disable context files:
// contextFiles: []

View file

@ -6,18 +6,11 @@
import {
createAgentSession,
discoverPromptTemplates,
DefaultResourceLoader,
type PromptTemplate,
SessionManager,
} from "@mariozechner/pi-coding-agent";
// Discover templates from cwd/.pi/prompts/ and ~/.pi/agent/prompts/
const discovered = discoverPromptTemplates();
console.log("Discovered prompt templates:");
for (const template of discovered) {
console.log(` /${template.name}: ${template.description}`);
}
// Define custom templates
const deployTemplate: PromptTemplate = {
name: "deploy",
@ -30,13 +23,24 @@ const deployTemplate: PromptTemplate = {
3. Deploy: npm run deploy`,
};
// Use discovered + custom templates
const loader = new DefaultResourceLoader({
promptsOverride: (current) => ({
prompts: [...current.prompts, deployTemplate],
diagnostics: current.diagnostics,
}),
});
await loader.reload();
// Discover templates from cwd/.pi/prompts/ and ~/.pi/agent/prompts/
const discovered = loader.getPrompts().prompts;
console.log("Discovered prompt templates:");
for (const template of discovered) {
console.log(` /${template.name}: ${template.description}`);
}
await createAgentSession({
promptTemplates: [...discovered, deployTemplate],
resourceLoader: loader,
sessionManager: SessionManager.inMemory(),
});
console.log(`Session created with ${discovered.length + 1} prompt templates`);
// Disable prompt templates:
// promptTemplates: []

View file

@ -4,19 +4,12 @@
* Configure API key resolution via AuthStorage and ModelRegistry.
*/
import {
AuthStorage,
createAgentSession,
discoverAuthStorage,
discoverModels,
ModelRegistry,
SessionManager,
} from "@mariozechner/pi-coding-agent";
import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "@mariozechner/pi-coding-agent";
// Default: discoverAuthStorage() uses ~/.pi/agent/auth.json
// discoverModels() loads built-in + custom models from ~/.pi/agent/models.json
const authStorage = discoverAuthStorage();
const modelRegistry = discoverModels(authStorage);
// Default: AuthStorage uses ~/.pi/agent/auth.json
// ModelRegistry loads built-in + custom models from ~/.pi/agent/models.json
const authStorage = new AuthStorage();
const modelRegistry = new ModelRegistry(authStorage);
await createAgentSession({
sessionManager: SessionManager.inMemory(),

View file

@ -4,11 +4,11 @@
* Override settings using SettingsManager.
*/
import { createAgentSession, loadSettings, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
// Load current settings (merged global + project)
const settings = loadSettings();
console.log("Current settings:", JSON.stringify(settings, null, 2));
const settingsManagerFromDisk = SettingsManager.create();
console.log("Current settings:", JSON.stringify(settingsManagerFromDisk.getGlobalSettings(), null, 2));
// Override specific settings
const settingsManager = SettingsManager.create();

View file

@ -13,8 +13,10 @@ import {
AuthStorage,
createAgentSession,
createBashTool,
createExtensionRuntime,
createReadTool,
ModelRegistry,
type ResourceLoader,
SessionManager,
SettingsManager,
} from "@mariozechner/pi-coding-agent";
@ -42,6 +44,18 @@ const settingsManager = SettingsManager.inMemory({
// When using a custom cwd with explicit tools, use the factory functions
const cwd = process.cwd();
const resourceLoader: ResourceLoader = {
getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
getSkills: () => ({ skills: [], diagnostics: [] }),
getPrompts: () => ({ prompts: [], diagnostics: [] }),
getThemes: () => ({ themes: [], diagnostics: [] }),
getAgentsFiles: () => ({ agentsFiles: [] }),
getSystemPrompt: () => `You are a minimal assistant.
Available: read, bash. Be concise.`,
getAppendSystemPrompt: () => [],
reload: async () => {},
};
const { session } = await createAgentSession({
cwd,
agentDir: "/tmp/my-agent",
@ -49,15 +63,9 @@ const { session } = await createAgentSession({
thinkingLevel: "off",
authStorage,
modelRegistry,
systemPrompt: `You are a minimal assistant.
Available: read, bash. Be concise.`,
resourceLoader,
// Use factory functions with the same cwd to ensure path resolution works correctly
tools: [createReadTool(cwd), createBashTool(cwd)],
// Pass empty array to disable extension discovery, or provide inline factories
extensions: [],
skills: [],
contextFiles: [],
promptTemplates: [],
sessionManager: SessionManager.inMemory(),
settingsManager,
});

View file

@ -33,24 +33,18 @@ import { getModel } from "@mariozechner/pi-ai";
import {
AuthStorage,
createAgentSession,
discoverAuthStorage,
discoverModels,
discoverSkills,
discoverExtensions,
discoverContextFiles,
discoverPromptTemplates,
loadSettings,
buildSystemPrompt,
DefaultResourceLoader,
ModelRegistry,
SessionManager,
SettingsManager,
codingTools,
readOnlyTools,
readTool, bashTool, editTool, writeTool,
} from "@mariozechner/pi-coding-agent";
// Auth and models setup
const authStorage = discoverAuthStorage();
const modelRegistry = discoverModels(authStorage);
const authStorage = new AuthStorage();
const modelRegistry = new ModelRegistry(authStorage);
// Minimal
const { session } = await createAgentSession({ authStorage, modelRegistry });
@ -60,11 +54,11 @@ const model = getModel("anthropic", "claude-opus-4-5");
const { session } = await createAgentSession({ model, thinkingLevel: "high", authStorage, modelRegistry });
// Modify prompt
const { session } = await createAgentSession({
systemPrompt: (defaultPrompt) => defaultPrompt + "\n\nBe concise.",
authStorage,
modelRegistry,
const loader = new DefaultResourceLoader({
systemPromptOverride: (base) => `${base}\n\nBe concise.`,
});
await loader.reload();
const { session } = await createAgentSession({ resourceLoader: loader, authStorage, modelRegistry });
// Read-only
const { session } = await createAgentSession({ tools: readOnlyTools, authStorage, modelRegistry });
@ -81,18 +75,24 @@ const customAuth = new AuthStorage("/my/app/auth.json");
customAuth.setRuntimeApiKey("anthropic", process.env.MY_KEY!);
const customRegistry = new ModelRegistry(customAuth);
const resourceLoader = new DefaultResourceLoader({
systemPromptOverride: () => "You are helpful.",
extensionFactories: [myExtension],
skillsOverride: () => ({ skills: [], diagnostics: [] }),
agentsFilesOverride: () => ({ agentsFiles: [] }),
promptsOverride: () => ({ prompts: [], diagnostics: [] }),
});
await resourceLoader.reload();
const { session } = await createAgentSession({
model,
authStorage: customAuth,
modelRegistry: customRegistry,
systemPrompt: "You are helpful.",
resourceLoader,
tools: [readTool, bashTool],
customTools: [{ tool: myTool }],
extensions: [{ factory: myExtension }],
skills: [],
contextFiles: [],
promptTemplates: [],
sessionManager: SessionManager.inMemory(),
settingsManager: SettingsManager.inMemory(),
});
// Run prompts
@ -108,23 +108,17 @@ await session.prompt("Hello");
| Option | Default | Description |
|--------|---------|-------------|
| `authStorage` | `discoverAuthStorage()` | Credential storage |
| `modelRegistry` | `discoverModels(authStorage)` | Model registry |
| `authStorage` | `new AuthStorage()` | Credential storage |
| `modelRegistry` | `new ModelRegistry(authStorage)` | Model registry |
| `cwd` | `process.cwd()` | Working directory |
| `agentDir` | `~/.pi/agent` | Config directory |
| `model` | From settings/first available | Model to use |
| `thinkingLevel` | From settings/"off" | off, low, medium, high |
| `systemPrompt` | Discovered | String or `(default) => modified` |
| `tools` | `codingTools` | Built-in tools |
| `customTools` | Discovered | Replaces discovery |
| `additionalCustomToolPaths` | `[]` | Merge with discovery |
| `extensions` | Discovered | Replaces discovery |
| `additionalExtensionPaths` | `[]` | Merge with discovery |
| `skills` | Discovered | Skills for prompt |
| `contextFiles` | Discovered | AGENTS.md files |
| `promptTemplates` | Discovered | Prompt templates (slash commands) |
| `customTools` | `[]` | Additional tool definitions |
| `resourceLoader` | DefaultResourceLoader | Resource loader for extensions, skills, prompts, themes |
| `sessionManager` | `SessionManager.create(cwd)` | Persistence |
| `settingsManager` | From agentDir | Settings overrides |
| `settingsManager` | `SettingsManager.create(cwd, agentDir)` | Settings overrides |
## Events