145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
export type CommandAction =
|
|||
|
|
| { type: 'none' }
|
||
|
|
| { type: 'prompt'; text: string }
|
||
|
|
| { type: 'exit' }
|
||
|
|
| { type: 'clear' }
|
||
|
|
| { type: 'compact' }
|
||
|
|
| { type: 'tools' }
|
||
|
|
| { type: 'cost' }
|
||
|
|
| { type: 'sessions' }
|
||
|
|
| { type: 'save' }
|
||
|
|
| { type: 'provider' }
|
||
|
|
| { type: 'models' }
|
||
|
|
| { type: 'init' }
|
||
|
|
| { type: 'context' }
|
||
|
|
| { type: 'todos' }
|
||
|
|
| { type: 'notes' }
|
||
|
|
| { type: 'skills' }
|
||
|
|
| { type: 'plugins' }
|
||
|
|
| { type: 'memory' }
|
||
|
|
| { type: 'agent'; agent?: string }
|
||
|
|
| { type: 'think'; level?: string }
|
||
|
|
| { type: 'info'; text: string }
|
||
|
|
| { type: 'model'; model: string }
|
||
|
|
| { type: 'resume'; id: string }
|
||
|
|
| { type: 'unknown'; name: string };
|
||
|
|
|
||
|
|
export type CommandSpec = {
|
||
|
|
name: string;
|
||
|
|
/** Extra names that resolve to the same command, hidden from the menu. */
|
||
|
|
aliases?: string[];
|
||
|
|
arg?: string;
|
||
|
|
summary: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Single source of truth for the menu, `/help`, and the parser. */
|
||
|
|
export const COMMANDS: CommandSpec[] = [
|
||
|
|
{ name: 'help', aliases: ['?'], summary: 'list these commands' },
|
||
|
|
{ name: 'agent', arg: '[name]', summary: 'switch agent: default, quick, deep, plan, review' },
|
||
|
|
{ name: 'think', arg: '[level]', summary: 'thinking level: off, low, medium, high, max' },
|
||
|
|
{ name: 'provider', aliases: ['login'], summary: 'set up a provider: pick, paste API key, choose model' },
|
||
|
|
{ name: 'models', summary: 'pick a model from the current provider' },
|
||
|
|
{ name: 'model', arg: '<id>', summary: 'switch model by name' },
|
||
|
|
{ name: 'skills', summary: 'list loaded skills' },
|
||
|
|
{ name: 'plugins', summary: 'list active plugins' },
|
||
|
|
{ name: 'init', summary: 'have the agent write AGENTS.md for this project' },
|
||
|
|
{ name: 'context', summary: 'show which instruction files are loaded' },
|
||
|
|
{ name: 'todos', summary: "show the agent's task list" },
|
||
|
|
{ name: 'notes', summary: 'show what the agent remembers about this project' },
|
||
|
|
{ name: 'memory', summary: 'compact the project memory with the model' },
|
||
|
|
{ name: 'tools', summary: 'list available tools' },
|
||
|
|
{ name: 'compact', summary: 'replace history with a model-written summary' },
|
||
|
|
{ name: 'cost', summary: 'tokens and estimated spend this session' },
|
||
|
|
{ name: 'sessions', summary: 'list saved sessions' },
|
||
|
|
{ name: 'resume', arg: '<id>', summary: 'load a saved session' },
|
||
|
|
{ name: 'save', summary: 'write the session to disk now' },
|
||
|
|
{ name: 'clear', summary: 'clear the transcript and history' },
|
||
|
|
{ name: 'exit', aliases: ['quit'], summary: 'quit' },
|
||
|
|
];
|
||
|
|
|
||
|
|
const usage = (c: CommandSpec) => `/${c.name}${c.arg ? ` ${c.arg}` : ''}`;
|
||
|
|
|
||
|
|
export const HELP = [
|
||
|
|
...COMMANDS.map((c) => `${usage(c).padEnd(18)} ${c.summary}`),
|
||
|
|
'',
|
||
|
|
'esc interrupt the running turn',
|
||
|
|
'tab complete the highlighted command',
|
||
|
|
'up / down recall earlier prompts',
|
||
|
|
].join('\n');
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Commands whose name starts with the typed prefix, for the `/` menu.
|
||
|
|
* An exact name sorts first so pressing enter on `/model` cannot run `/models`.
|
||
|
|
* Aliases stay hidden to keep the list short.
|
||
|
|
*/
|
||
|
|
export function matchCommands(input: string): CommandSpec[] {
|
||
|
|
if (!input.startsWith('/')) return [];
|
||
|
|
const typed = input.slice(1).toLowerCase();
|
||
|
|
if (typed.includes(' ')) return [];
|
||
|
|
const hits = COMMANDS.filter((c) => c.name.startsWith(typed));
|
||
|
|
const exact = hits.findIndex((c) => c.name === typed);
|
||
|
|
return exact > 0 ? [hits[exact]!, ...hits.filter((_, i) => i !== exact)] : hits;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** True while the input is a bare command name being typed, so the menu should show. */
|
||
|
|
export const isMenuOpen = (input: string) => input.startsWith('/') && !input.includes(' ');
|
||
|
|
|
||
|
|
/** Pure parser: no IO, so the TUI and headless mode share one definition. */
|
||
|
|
export function parseCommand(raw: string): CommandAction {
|
||
|
|
const input = raw.trim();
|
||
|
|
if (!input) return { type: 'none' };
|
||
|
|
if (!input.startsWith('/')) return { type: 'prompt', text: input };
|
||
|
|
|
||
|
|
const [name = '', ...rest] = input.slice(1).split(/\s+/);
|
||
|
|
const arg = rest.join(' ').trim();
|
||
|
|
|
||
|
|
switch (name) {
|
||
|
|
case 'help':
|
||
|
|
case '?':
|
||
|
|
return { type: 'info', text: HELP };
|
||
|
|
case 'exit':
|
||
|
|
case 'quit':
|
||
|
|
return { type: 'exit' };
|
||
|
|
case 'clear':
|
||
|
|
return { type: 'clear' };
|
||
|
|
case 'compact':
|
||
|
|
return { type: 'compact' };
|
||
|
|
case 'tools':
|
||
|
|
return { type: 'tools' };
|
||
|
|
case 'cost':
|
||
|
|
return { type: 'cost' };
|
||
|
|
case 'sessions':
|
||
|
|
return { type: 'sessions' };
|
||
|
|
case 'save':
|
||
|
|
return { type: 'save' };
|
||
|
|
case 'provider':
|
||
|
|
case 'login':
|
||
|
|
return { type: 'provider' };
|
||
|
|
case 'models':
|
||
|
|
return { type: 'models' };
|
||
|
|
case 'init':
|
||
|
|
return { type: 'init' };
|
||
|
|
case 'context':
|
||
|
|
return { type: 'context' };
|
||
|
|
case 'todos':
|
||
|
|
return { type: 'todos' };
|
||
|
|
case 'notes':
|
||
|
|
return { type: 'notes' };
|
||
|
|
case 'skills':
|
||
|
|
return { type: 'skills' };
|
||
|
|
case 'plugins':
|
||
|
|
return { type: 'plugins' };
|
||
|
|
case 'memory':
|
||
|
|
return { type: 'memory' };
|
||
|
|
case 'agent':
|
||
|
|
return arg ? { type: 'agent', agent: arg } : { type: 'agent' };
|
||
|
|
case 'think':
|
||
|
|
return arg ? { type: 'think', level: arg } : { type: 'think' };
|
||
|
|
case 'model':
|
||
|
|
return arg ? { type: 'model', model: arg } : { type: 'models' };
|
||
|
|
case 'resume':
|
||
|
|
return arg ? { type: 'resume', id: arg } : { type: 'info', text: 'usage: /resume <session-id>' };
|
||
|
|
default:
|
||
|
|
return { type: 'unknown', name };
|
||
|
|
}
|
||
|
|
}
|