diff --git a/packages/coding-agent/docs/rpc.md b/packages/coding-agent/docs/rpc.md index fb3680bd..5a0f9123 100644 --- a/packages/coding-agent/docs/rpc.md +++ b/packages/coding-agent/docs/rpc.md @@ -612,6 +612,47 @@ Response: Returns `{"text": null}` if no assistant messages exist. +### Commands + +#### get_commands + +Get available commands (extension commands, prompt templates, and skills). These can be invoked via the `prompt` command by prefixing with `/`. + +```json +{"type": "get_commands"} +``` + +Response: +```json +{ + "type": "response", + "command": "get_commands", + "success": true, + "data": { + "commands": [ + {"name": "session-name", "description": "Set or clear session name", "source": "extension", "path": "/home/user/.pi/agent/extensions/session.ts"}, + {"name": "fix-tests", "description": "Fix failing tests", "source": "template", "location": "project", "path": "/home/user/myproject/.pi/agent/prompts/fix-tests.md"}, + {"name": "skill:brave-search", "description": "Web search via Brave API", "source": "skill", "location": "user", "path": "/home/user/.pi/agent/skills/brave-search/SKILL.md"} + ] + } +} +``` + +Each command has: +- `name`: Command name (invoke with `/name`) +- `description`: Human-readable description (optional for extension commands) +- `source`: What kind of command: + - `"extension"`: Registered via `pi.registerCommand()` in an extension + - `"template"`: Loaded from a prompt template `.md` file + - `"skill"`: Loaded from a skill directory (name is prefixed with `skill:`) +- `location`: Where it was loaded from (optional, not present for extensions): + - `"user"`: User-level (`~/.pi/agent/`) + - `"project"`: Project-level (`./.pi/agent/`) + - `"path"`: Explicit path via CLI or settings +- `path`: Absolute file path to the command source (optional) + +**Note**: Built-in TUI commands (`/settings`, `/hotkeys`, etc.) are not included. They are handled only in interactive mode and would not execute if sent via `prompt`. + ## Events Events are streamed to stdout as JSON lines during agent operation. Events do NOT include an `id` field (only responses do). diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index 73197acc..dff981a6 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -372,6 +372,16 @@ export class ExtensionRunner { return commands; } + getRegisteredCommandsWithPaths(): Array<{ command: RegisteredCommand; extensionPath: string }> { + const result: Array<{ command: RegisteredCommand; extensionPath: string }> = []; + for (const ext of this.extensions) { + for (const command of ext.commands.values()) { + result.push({ command, extensionPath: ext.path }); + } + } + return result; + } + getCommand(name: string): RegisteredCommand | undefined { for (const ext of this.extensions) { const command = ext.commands.get(name); diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index 57512533..d7ed0953 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -11,7 +11,7 @@ export interface PromptTemplate { name: string; description: string; content: string; - source: string; // e.g., "user", "project", "path", "inline" + source: string; // "user", "project", or "path" filePath: string; // Absolute path to the template file } diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 739fd949..6735d031 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -11,7 +11,7 @@ import type { ImageContent } from "@mariozechner/pi-ai"; import type { SessionStats } from "../../core/agent-session.js"; import type { BashResult } from "../../core/bash-executor.js"; import type { CompactionResult } from "../../core/compaction/index.js"; -import type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc-types.js"; +import type { RpcCommand, RpcResponse, RpcSessionState, RpcSlashCommand } from "./rpc-types.js"; // ============================================================================ // Types @@ -370,6 +370,14 @@ export class RpcClient { return this.getData<{ messages: AgentMessage[] }>(response).messages; } + /** + * Get available commands (extension commands, prompt templates, skills). + */ + async getCommands(): Promise { + const response = await this.send({ type: "get_commands" }); + return this.getData<{ commands: RpcSlashCommand[] }>(response).commands; + } + // ========================================================================= // Helpers // ========================================================================= diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 3413306d..00c36ee8 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -26,6 +26,7 @@ import type { RpcExtensionUIResponse, RpcResponse, RpcSessionState, + RpcSlashCommand, } from "./rpc-types.js"; // Re-export types for consumers @@ -497,6 +498,48 @@ export async function runRpcMode(session: AgentSession): Promise { return success(id, "get_messages", { messages: session.messages }); } + // ================================================================= + // Commands (available for invocation via prompt) + // ================================================================= + + case "get_commands": { + const commands: RpcSlashCommand[] = []; + + // Extension commands + for (const { command, extensionPath } of session.extensionRunner?.getRegisteredCommandsWithPaths() ?? []) { + commands.push({ + name: command.name, + description: command.description, + source: "extension", + path: extensionPath, + }); + } + + // Prompt templates (source is always "user" | "project" | "path" in coding-agent) + for (const template of session.promptTemplates) { + commands.push({ + name: template.name, + description: template.description, + source: "template", + location: template.source as RpcSlashCommand["location"], + path: template.filePath, + }); + } + + // Skills (source is always "user" | "project" | "path" in coding-agent) + for (const skill of session.resourceLoader.getSkills().skills) { + commands.push({ + name: `skill:${skill.name}`, + description: skill.description, + source: "skill", + location: skill.source as RpcSlashCommand["location"], + path: skill.filePath, + }); + } + + return success(id, "get_commands", { commands }); + } + default: { const unknownCommand = command as { type: string }; return error(undefined, unknownCommand.type, `Unknown command: ${unknownCommand.type}`); diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index ba8abf15..7e8ccb1f 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -60,7 +60,28 @@ export type RpcCommand = | { id?: string; type: "get_last_assistant_text" } // Messages - | { id?: string; type: "get_messages" }; + | { id?: string; type: "get_messages" } + + // Commands (available for invocation via prompt) + | { id?: string; type: "get_commands" }; + +// ============================================================================ +// RPC Slash Command (for get_commands response) +// ============================================================================ + +/** A command available for invocation via prompt */ +export interface RpcSlashCommand { + /** Command name (without leading slash) */ + name: string; + /** Human-readable description */ + description?: string; + /** What kind of command this is */ + source: "extension" | "template" | "skill"; + /** Where the command was loaded from (undefined for extensions) */ + location?: "user" | "project" | "path"; + /** File path to the command source */ + path?: string; +} // ============================================================================ // RPC State @@ -168,6 +189,15 @@ export type RpcResponse = // Messages | { id?: string; type: "response"; command: "get_messages"; success: true; data: { messages: AgentMessage[] } } + // Commands + | { + id?: string; + type: "response"; + command: "get_commands"; + success: true; + data: { commands: RpcSlashCommand[] }; + } + // Error response (any command can fail) | { id?: string; type: "response"; command: string; success: false; error: string };