mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 17:01:02 +00:00
Merge hooks and custom-tools into unified extensions system (#454)
Breaking changes: - Settings: 'hooks' and 'customTools' arrays replaced with 'extensions' - CLI: '--hook' and '--tool' flags replaced with '--extension' / '-e' - API: HookMessage renamed to CustomMessage, role 'hookMessage' to 'custom' - API: FileSlashCommand renamed to PromptTemplate - API: discoverSlashCommands() renamed to discoverPromptTemplates() - Directories: commands/ renamed to prompts/ for prompt templates Migration: - Session version bumped to 3 (auto-migrates v2 sessions) - Old 'hookMessage' role entries converted to 'custom' Structural changes: - src/core/hooks/ and src/core/custom-tools/ merged into src/core/extensions/ - src/core/slash-commands.ts renamed to src/core/prompt-templates.ts - examples/hooks/ and examples/custom-tools/ merged into examples/extensions/ - docs/hooks.md and docs/custom-tools.md merged into docs/extensions.md New test coverage: - test/extensions-runner.test.ts (10 tests) - test/extensions-discovery.test.ts (26 tests) - test/prompt-templates.test.ts
This commit is contained in:
parent
9794868b38
commit
c6fc084534
112 changed files with 2842 additions and 6747 deletions
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Minimal SDK Usage
|
||||
*
|
||||
* Uses all defaults: discovers skills, hooks, tools, context files
|
||||
* Uses all defaults: discovers skills, extensions, tools, context files
|
||||
* from cwd and ~/.pi/agent. Model chosen from settings or first available.
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,28 @@
|
|||
/**
|
||||
* Tools Configuration
|
||||
*
|
||||
* Use built-in tool sets, individual tools, or add custom tools.
|
||||
* Use built-in tool sets or individual tools.
|
||||
*
|
||||
* IMPORTANT: When using a custom `cwd`, you must use the tool factory functions
|
||||
* (createCodingTools, createReadOnlyTools, createReadTool, etc.) to ensure
|
||||
* tools resolve paths relative to your cwd, not process.cwd().
|
||||
*
|
||||
* For custom tools, see 06-extensions.ts - custom tools are now registered
|
||||
* via the extensions system using pi.registerTool().
|
||||
*/
|
||||
|
||||
import {
|
||||
bashTool, // read, bash, edit, write - uses process.cwd()
|
||||
type CustomTool,
|
||||
bashTool,
|
||||
createAgentSession,
|
||||
createBashTool,
|
||||
createCodingTools, // Factory: creates tools for specific cwd
|
||||
createCodingTools,
|
||||
createGrepTool,
|
||||
createReadTool,
|
||||
grepTool,
|
||||
readOnlyTools, // read, grep, find, ls - uses process.cwd()
|
||||
readOnlyTools,
|
||||
readTool,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
// Read-only mode (no edit/write) - uses process.cwd()
|
||||
await createAgentSession({
|
||||
|
|
@ -53,38 +54,3 @@ await createAgentSession({
|
|||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
console.log("Specific tools with custom cwd session created");
|
||||
|
||||
// Inline custom tool (needs TypeBox schema)
|
||||
const weatherTool: CustomTool = {
|
||||
name: "get_weather",
|
||||
label: "Get Weather",
|
||||
description: "Get current weather for a city",
|
||||
parameters: Type.Object({
|
||||
city: Type.String({ description: "City name" }),
|
||||
}),
|
||||
execute: async (_toolCallId, params) => ({
|
||||
content: [{ type: "text", text: `Weather in ${(params as { city: string }).city}: 22°C, sunny` }],
|
||||
details: {},
|
||||
}),
|
||||
};
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
customTools: [{ tool: weatherTool }],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
await session.prompt("What's the weather in Tokyo?");
|
||||
console.log();
|
||||
|
||||
// Merge with discovered tools from cwd/.pi/tools and ~/.pi/agent/tools:
|
||||
// const discovered = await discoverCustomTools();
|
||||
// customTools: [...discovered, { tool: myTool }]
|
||||
|
||||
// Or add paths without replacing discovery:
|
||||
// additionalCustomToolPaths: ["/extra/tools"]
|
||||
|
|
|
|||
81
packages/coding-agent/examples/sdk/06-extensions.ts
Normal file
81
packages/coding-agent/examples/sdk/06-extensions.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Extensions Configuration
|
||||
*
|
||||
* Extensions intercept agent events and can register custom tools.
|
||||
* They provide a unified system for extensions, custom tools, commands, and more.
|
||||
*
|
||||
* Extension files are discovered from:
|
||||
* - ~/.pi/agent/extensions/
|
||||
* - <cwd>/.pi/extensions/
|
||||
* - Paths specified in settings.json "extensions" array
|
||||
* - Paths passed via --extension CLI flag
|
||||
*
|
||||
* An extension is a TypeScript file that exports a default function:
|
||||
* export default function (pi: ExtensionAPI) { ... }
|
||||
*/
|
||||
|
||||
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
// Extensions are loaded from disk, not passed inline to createAgentSession.
|
||||
// Use the discovery mechanism:
|
||||
// 1. Place extension files in ~/.pi/agent/extensions/ or .pi/extensions/
|
||||
// 2. Add paths to settings.json: { "extensions": ["./my-extension.ts"] }
|
||||
// 3. Use --extension flag: pi --extension ./my-extension.ts
|
||||
|
||||
// To add additional extension paths beyond discovery:
|
||||
const { session } = await createAgentSession({
|
||||
additionalExtensionPaths: ["./my-logging-extension.ts", "./my-safety-extension.ts"],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
await session.prompt("List files in the current directory.");
|
||||
console.log();
|
||||
|
||||
// Example extension file (./my-logging-extension.ts):
|
||||
/*
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.on("agent_start", async () => {
|
||||
console.log("[Extension] Agent starting");
|
||||
});
|
||||
|
||||
pi.on("tool_call", async (event) => {
|
||||
console.log(\`[Extension] Tool: \${event.toolName}\`);
|
||||
// Return { block: true, reason: "..." } to block execution
|
||||
return undefined;
|
||||
});
|
||||
|
||||
pi.on("agent_end", async (event) => {
|
||||
console.log(\`[Extension] Done, \${event.messages.length} messages\`);
|
||||
});
|
||||
|
||||
// Register a custom tool
|
||||
pi.registerTool({
|
||||
name: "my_tool",
|
||||
label: "My Tool",
|
||||
description: "Does something useful",
|
||||
parameters: Type.Object({
|
||||
input: Type.String(),
|
||||
}),
|
||||
execute: async (_toolCallId, params, _onUpdate, _ctx, _signal) => ({
|
||||
content: [{ type: "text", text: \`Processed: \${params.input}\` }],
|
||||
details: {},
|
||||
}),
|
||||
});
|
||||
|
||||
// Register a command
|
||||
pi.registerCommand("mycommand", {
|
||||
description: "Do something",
|
||||
handler: async (args, ctx) => {
|
||||
ctx.ui.notify(\`Command executed with: \${args}\`);
|
||||
},
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* Hooks Configuration
|
||||
*
|
||||
* Hooks intercept agent events for logging, blocking, or modification.
|
||||
*/
|
||||
|
||||
import { createAgentSession, type HookFactory, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
// Logging hook
|
||||
const loggingHook: HookFactory = (api) => {
|
||||
api.on("agent_start", async () => {
|
||||
console.log("[Hook] Agent starting");
|
||||
});
|
||||
|
||||
api.on("tool_call", async (event) => {
|
||||
console.log(`[Hook] Tool: ${event.toolName}`);
|
||||
return undefined; // Don't block
|
||||
});
|
||||
|
||||
api.on("agent_end", async (event) => {
|
||||
console.log(`[Hook] Done, ${event.messages.length} messages`);
|
||||
});
|
||||
};
|
||||
|
||||
// Blocking hook (returns { block: true, reason: "..." })
|
||||
const safetyHook: HookFactory = (api) => {
|
||||
api.on("tool_call", async (event) => {
|
||||
if (event.toolName === "bash") {
|
||||
const cmd = (event.input as { command?: string }).command ?? "";
|
||||
if (cmd.includes("rm -rf")) {
|
||||
return { block: true, reason: "Dangerous command blocked" };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
// Use inline hooks
|
||||
const { session } = await createAgentSession({
|
||||
hooks: [{ factory: loggingHook }, { factory: safetyHook }],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
|
||||
await session.prompt("List files in the current directory.");
|
||||
console.log();
|
||||
|
||||
// Disable all hooks:
|
||||
// hooks: []
|
||||
|
||||
// Merge with discovered hooks:
|
||||
// const discovered = await discoverHooks();
|
||||
// hooks: [...discovered, { factory: myHook }]
|
||||
|
||||
// Add paths without replacing discovery:
|
||||
// additionalHookPaths: ["/extra/hooks"]
|
||||
42
packages/coding-agent/examples/sdk/08-prompt-templates.ts
Normal file
42
packages/coding-agent/examples/sdk/08-prompt-templates.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Prompt Templates
|
||||
*
|
||||
* File-based templates that inject content when invoked with /templatename.
|
||||
*/
|
||||
|
||||
import {
|
||||
createAgentSession,
|
||||
discoverPromptTemplates,
|
||||
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",
|
||||
description: "Deploy the application",
|
||||
source: "(custom)",
|
||||
content: `# Deploy Instructions
|
||||
|
||||
1. Build: npm run build
|
||||
2. Test: npm test
|
||||
3. Deploy: npm run deploy`,
|
||||
};
|
||||
|
||||
// Use discovered + custom templates
|
||||
await createAgentSession({
|
||||
promptTemplates: [...discovered, deployTemplate],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
console.log(`Session created with ${discovered.length + 1} prompt templates`);
|
||||
|
||||
// Disable prompt templates:
|
||||
// promptTemplates: []
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
/**
|
||||
* Slash Commands
|
||||
*
|
||||
* File-based commands that inject content when invoked with /commandname.
|
||||
*/
|
||||
|
||||
import {
|
||||
createAgentSession,
|
||||
discoverSlashCommands,
|
||||
type FileSlashCommand,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
|
||||
// Discover commands from cwd/.pi/commands/ and ~/.pi/agent/commands/
|
||||
const discovered = discoverSlashCommands();
|
||||
console.log("Discovered slash commands:");
|
||||
for (const cmd of discovered) {
|
||||
console.log(` /${cmd.name}: ${cmd.description}`);
|
||||
}
|
||||
|
||||
// Define custom commands
|
||||
const deployCommand: FileSlashCommand = {
|
||||
name: "deploy",
|
||||
description: "Deploy the application",
|
||||
source: "(custom)",
|
||||
content: `# Deploy Instructions
|
||||
|
||||
1. Build: npm run build
|
||||
2. Test: npm test
|
||||
3. Deploy: npm run deploy`,
|
||||
};
|
||||
|
||||
// Use discovered + custom commands
|
||||
await createAgentSession({
|
||||
slashCommands: [...discovered, deployCommand],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
console.log(`Session created with ${discovered.length + 1} slash commands`);
|
||||
|
||||
// Disable slash commands:
|
||||
// slashCommands: []
|
||||
|
|
@ -6,21 +6,22 @@
|
|||
* IMPORTANT: When providing `tools` with a custom `cwd`, use the tool factory
|
||||
* functions (createReadTool, createBashTool, etc.) to ensure tools resolve
|
||||
* paths relative to your cwd.
|
||||
*
|
||||
* NOTE: Extensions (extensions, custom tools) are always loaded via discovery.
|
||||
* To use custom extensions, place them in the extensions directory or
|
||||
* pass paths via additionalExtensionPaths.
|
||||
*/
|
||||
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
AuthStorage,
|
||||
type CustomTool,
|
||||
createAgentSession,
|
||||
createBashTool,
|
||||
createReadTool,
|
||||
type HookFactory,
|
||||
ModelRegistry,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
// Custom auth storage location
|
||||
const authStorage = new AuthStorage("/tmp/my-agent/auth.json");
|
||||
|
|
@ -33,27 +34,7 @@ if (process.env.MY_ANTHROPIC_KEY) {
|
|||
// Model registry with no custom models.json
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
// Inline hook
|
||||
const auditHook: HookFactory = (api) => {
|
||||
api.on("tool_call", async (event) => {
|
||||
console.log(`[Audit] ${event.toolName}`);
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
// Inline custom tool
|
||||
const statusTool: CustomTool = {
|
||||
name: "status",
|
||||
label: "Status",
|
||||
description: "Get system status",
|
||||
parameters: Type.Object({}),
|
||||
execute: async () => ({
|
||||
content: [{ type: "text", text: `Uptime: ${process.uptime()}s, Node: ${process.version}` }],
|
||||
details: {},
|
||||
}),
|
||||
};
|
||||
|
||||
const model = getModel("anthropic", "claude-opus-4-5");
|
||||
const model = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||
if (!model) throw new Error("Model not found");
|
||||
|
||||
// In-memory settings with overrides
|
||||
|
|
@ -73,14 +54,14 @@ const { session } = await createAgentSession({
|
|||
authStorage,
|
||||
modelRegistry,
|
||||
systemPrompt: `You are a minimal assistant.
|
||||
Available: read, bash, status. Be concise.`,
|
||||
Available: read, bash. Be concise.`,
|
||||
// Use factory functions with the same cwd to ensure path resolution works correctly
|
||||
tools: [createReadTool(cwd), createBashTool(cwd)],
|
||||
customTools: [{ tool: statusTool }],
|
||||
hooks: [{ factory: auditHook }],
|
||||
// Extensions are loaded from disk - use additionalExtensionPaths to add custom ones
|
||||
// additionalExtensionPaths: ["./my-extension.ts"],
|
||||
skills: [],
|
||||
contextFiles: [],
|
||||
slashCommands: [],
|
||||
promptTemplates: [],
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
settingsManager,
|
||||
});
|
||||
|
|
@ -91,5 +72,5 @@ session.subscribe((event) => {
|
|||
}
|
||||
});
|
||||
|
||||
await session.prompt("Get status and list files.");
|
||||
await session.prompt("List files in the current directory.");
|
||||
console.log();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Programmatic usage of pi-coding-agent via `createAgentSession()`.
|
|||
| `03-custom-prompt.ts` | Replace or modify system prompt |
|
||||
| `04-skills.ts` | Discover, filter, or replace skills |
|
||||
| `05-tools.ts` | Built-in tools, custom tools |
|
||||
| `06-hooks.ts` | Logging, blocking, result modification |
|
||||
| `06-extensions.ts` | Logging, blocking, result modification |
|
||||
| `07-context-files.ts` | AGENTS.md context files |
|
||||
| `08-slash-commands.ts` | File-based slash commands |
|
||||
| `09-api-keys-and-oauth.ts` | API key resolution, OAuth config |
|
||||
|
|
@ -36,7 +36,7 @@ import {
|
|||
discoverAuthStorage,
|
||||
discoverModels,
|
||||
discoverSkills,
|
||||
discoverHooks,
|
||||
discoverExtensions,
|
||||
discoverCustomTools,
|
||||
discoverContextFiles,
|
||||
discoverSlashCommands,
|
||||
|
|
@ -89,7 +89,7 @@ const { session } = await createAgentSession({
|
|||
systemPrompt: "You are helpful.",
|
||||
tools: [readTool, bashTool],
|
||||
customTools: [{ tool: myTool }],
|
||||
hooks: [{ factory: myHook }],
|
||||
extensions: [{ factory: myExtension }],
|
||||
skills: [],
|
||||
contextFiles: [],
|
||||
slashCommands: [],
|
||||
|
|
@ -119,8 +119,8 @@ await session.prompt("Hello");
|
|||
| `tools` | `codingTools` | Built-in tools |
|
||||
| `customTools` | Discovered | Replaces discovery |
|
||||
| `additionalCustomToolPaths` | `[]` | Merge with discovery |
|
||||
| `hooks` | Discovered | Replaces discovery |
|
||||
| `additionalHookPaths` | `[]` | Merge with discovery |
|
||||
| `extensions` | Discovered | Replaces discovery |
|
||||
| `additionalExtensionPaths` | `[]` | Merge with discovery |
|
||||
| `skills` | Discovered | Skills for prompt |
|
||||
| `contextFiles` | Discovered | AGENTS.md files |
|
||||
| `slashCommands` | Discovered | File commands |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue