From ac6f8fb493619137be81f09db9183a4d69eb71aa Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Fri, 30 Jan 2026 16:11:19 +0800 Subject: [PATCH] feat(cli): add tab completion for skill commands Integrate SkillManager with interactive CLI: - Tab completion for /command prefixes - Combined completion for built-in and skill commands - Skill invocation handling with instructions context - Updated /help to display available skill commands Co-Authored-By: Claude Opus 4.5 --- src/agent/interactive-cli.ts | 80 ++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/src/agent/interactive-cli.ts b/src/agent/interactive-cli.ts index d56a1fc8..76f8f4d1 100644 --- a/src/agent/interactive-cli.ts +++ b/src/agent/interactive-cli.ts @@ -2,6 +2,7 @@ import * as readline from "readline"; import { Agent } from "./runner.js"; import type { AgentOptions } from "./types.js"; +import { SkillManager } from "./skills/index.js"; type CliOptions = { profile?: string | undefined; @@ -95,11 +96,24 @@ function printWelcome(sessionId: string) { console.log(""); } -function printHelp() { - console.log("\nAvailable commands:"); +function printHelp(skillManager?: SkillManager) { + console.log("\nBuilt-in commands:"); for (const [cmd, desc] of Object.entries(COMMANDS)) { console.log(` /${cmd.padEnd(12)} ${desc}`); } + + // Show skill commands if available + if (skillManager) { + const reservedNames = new Set(Object.keys(COMMANDS)); + const skillCommands = skillManager.getSkillCommands({ reservedNames }); + if (skillCommands.length > 0) { + console.log("\nSkill commands:"); + for (const cmd of skillCommands) { + console.log(` /${cmd.name.padEnd(12)} ${cmd.description}`); + } + } + } + console.log("\nJust type your message and press Enter to chat with the agent."); console.log(""); } @@ -111,15 +125,25 @@ class InteractiveCLI { private multilineMode = false; private multilineBuffer: string[] = []; private running = true; + private skillManager: SkillManager; constructor(opts: CliOptions) { this.opts = opts; this.agent = this.createAgent(opts.session); + // Initialize SkillManager for tab completion + this.skillManager = new SkillManager({ + profileId: opts.profile, + }); + + // Build list of reserved command names (built-in CLI commands) + const reservedNames = new Set(Object.keys(COMMANDS)); + this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, + completer: (line: string) => this.completer(line, reservedNames), }); this.rl.on("close", () => { @@ -129,6 +153,44 @@ class InteractiveCLI { }); } + /** + * Tab completion handler for readline + * Completes both built-in commands and skill commands + */ + private completer( + line: string, + reservedNames: Set, + ): [string[], string] { + // Only complete if line starts with / + if (!line.startsWith("/")) { + return [[], line]; + } + + const prefix = line.slice(1).toLowerCase(); + + // Get built-in command completions + const builtinCompletions = Object.keys(COMMANDS) + .filter((cmd) => cmd.toLowerCase().startsWith(prefix)) + .map((cmd) => `/${cmd}`); + + // Get skill command completions + const skillCommands = this.skillManager.getSkillCommands({ reservedNames }); + const skillCompletions = skillCommands + .filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)) + .map((cmd) => `/${cmd.name}`); + + // Combine and deduplicate + const allCompletions = [...new Set([...builtinCompletions, ...skillCompletions])]; + + // Sort: shorter first, then alphabetically + allCompletions.sort((a, b) => { + if (a.length !== b.length) return a.length - b.length; + return a.localeCompare(b); + }); + + return [allCompletions, line]; + } + private createAgent(sessionId?: string): Agent { return new Agent({ profileId: this.opts.profile, @@ -200,7 +262,7 @@ class InteractiveCLI { switch (cmd) { case "help": - printHelp(); + printHelp(this.skillManager); return true; case "exit": @@ -238,7 +300,17 @@ class InteractiveCLI { return true; default: - // Unknown command - let the agent handle it + // Check if it's a skill command + const invocation = this.skillManager.resolveCommand(input); + if (invocation) { + // Skill command found - send to agent with skill instructions as context + const skillPrompt = invocation.args + ? `[Skill: ${invocation.command.name}]\n\n${invocation.instructions}\n\nUser request: ${invocation.args}` + : `[Skill: ${invocation.command.name}]\n\n${invocation.instructions}`; + await this.handleInput(skillPrompt); + return true; + } + // Unknown command - let the agent handle it as-is return false; } }