mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-22 00:00:27 +00:00
Add get_commands RPC for headless clients (#995)
* Add get_commands RPC for headless clients Headless clients like Emacs can now query which commands are available. Previously they could only discover file-based prompt templates by scanning the filesystem; extension commands and skills were invisible. The response includes each command's name, description, and source (extension, template, or skill). Commands appear in the same order as the TUI's autocomplete: extension commands first, then templates, then skills. Built-in TUI commands (/settings, /fork, etc.) are excluded since they require the interactive UI. Commands like /compact have dedicated RPC equivalents instead. * Add location and path to get_commands response Clients can show where commands come from (user/project/path) and display file paths in tooltips. The data is already available on templates and skills - just exposing it.
This commit is contained in:
parent
c67b582fc4
commit
0ad189f12a
6 changed files with 135 additions and 3 deletions
|
|
@ -612,6 +612,47 @@ Response:
|
||||||
|
|
||||||
Returns `{"text": null}` if no assistant messages exist.
|
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
|
||||||
|
|
||||||
Events are streamed to stdout as JSON lines during agent operation. Events do NOT include an `id` field (only responses do).
|
Events are streamed to stdout as JSON lines during agent operation. Events do NOT include an `id` field (only responses do).
|
||||||
|
|
|
||||||
|
|
@ -372,6 +372,16 @@ export class ExtensionRunner {
|
||||||
return commands;
|
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 {
|
getCommand(name: string): RegisteredCommand | undefined {
|
||||||
for (const ext of this.extensions) {
|
for (const ext of this.extensions) {
|
||||||
const command = ext.commands.get(name);
|
const command = ext.commands.get(name);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export interface PromptTemplate {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
content: 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
|
filePath: string; // Absolute path to the template file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
import type { SessionStats } from "../../core/agent-session.js";
|
import type { SessionStats } from "../../core/agent-session.js";
|
||||||
import type { BashResult } from "../../core/bash-executor.js";
|
import type { BashResult } from "../../core/bash-executor.js";
|
||||||
import type { CompactionResult } from "../../core/compaction/index.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
|
// Types
|
||||||
|
|
@ -370,6 +370,14 @@ export class RpcClient {
|
||||||
return this.getData<{ messages: AgentMessage[] }>(response).messages;
|
return this.getData<{ messages: AgentMessage[] }>(response).messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available commands (extension commands, prompt templates, skills).
|
||||||
|
*/
|
||||||
|
async getCommands(): Promise<RpcSlashCommand[]> {
|
||||||
|
const response = await this.send({ type: "get_commands" });
|
||||||
|
return this.getData<{ commands: RpcSlashCommand[] }>(response).commands;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Helpers
|
// Helpers
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import type {
|
||||||
RpcExtensionUIResponse,
|
RpcExtensionUIResponse,
|
||||||
RpcResponse,
|
RpcResponse,
|
||||||
RpcSessionState,
|
RpcSessionState,
|
||||||
|
RpcSlashCommand,
|
||||||
} from "./rpc-types.js";
|
} from "./rpc-types.js";
|
||||||
|
|
||||||
// Re-export types for consumers
|
// Re-export types for consumers
|
||||||
|
|
@ -497,6 +498,48 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
||||||
return success(id, "get_messages", { messages: session.messages });
|
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: {
|
default: {
|
||||||
const unknownCommand = command as { type: string };
|
const unknownCommand = command as { type: string };
|
||||||
return error(undefined, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);
|
return error(undefined, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,28 @@ export type RpcCommand =
|
||||||
| { id?: string; type: "get_last_assistant_text" }
|
| { id?: string; type: "get_last_assistant_text" }
|
||||||
|
|
||||||
// Messages
|
// 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
|
// RPC State
|
||||||
|
|
@ -168,6 +189,15 @@ export type RpcResponse =
|
||||||
// Messages
|
// Messages
|
||||||
| { id?: string; type: "response"; command: "get_messages"; success: true; data: { messages: AgentMessage[] } }
|
| { 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)
|
// Error response (any command can fail)
|
||||||
| { id?: string; type: "response"; command: string; success: false; error: string };
|
| { id?: string; type: "response"; command: string; success: false; error: string };
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue