450 lines
14 KiB
TypeScript
450 lines
14 KiB
TypeScript
export type Command =
|
|
| { type: 'quit' }
|
|
| { type: 'reset' }
|
|
| { type: 'help' }
|
|
| { type: 'multiline' }
|
|
| { type: 'status' }
|
|
| { type: 'tools' }
|
|
| { type: 'research'; task: string }
|
|
| { type: 'council'; task: string }
|
|
| { type: 'fullscreen' }
|
|
| { type: 'compact' }
|
|
| { type: 'usage' }
|
|
| { type: 'context' }
|
|
| { type: 'verbose' }
|
|
| { type: 'model'; name?: string; providerModel?: string }
|
|
| { type: 'backend'; provider?: string }
|
|
| { type: 'runtime'; input?: string }
|
|
| { type: 'login'; provider?: string }
|
|
| { type: 'transfer'; target: string }
|
|
| { type: 'pair'; action?: 'generate' | 'list' | 'revoke'; args?: string }
|
|
| { type: 'queue'; action?: 'show' | 'set' | 'reset'; args?: string }
|
|
| { type: 'elevate'; args?: string }
|
|
| { type: 'message'; content: string };
|
|
|
|
export function isToolInventoryQuery(input: string): boolean {
|
|
const normalized = input.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
if (
|
|
normalized.includes('available tools')
|
|
|| normalized.includes('what tools')
|
|
|| normalized.includes('which tools')
|
|
|| normalized.includes('tool list')
|
|
|| normalized.includes('list tools')
|
|
|| normalized.includes('what can you do')
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return (
|
|
/\b(?:show|list|check)\s+(?:me\s+)?(?:your\s+)?(?:available\s+|new\s+)?tools?\b/.test(normalized)
|
|
|| /\b(?:what|which)\s+tools?\b/.test(normalized)
|
|
|| /\btools?\s+(?:do\s+you\s+have|are\s+available)\b/.test(normalized)
|
|
|| /\b(?:show|list|what\s+are)\s+(?:your\s+)?capabilities\b/.test(normalized)
|
|
);
|
|
}
|
|
|
|
export function parseCommand(input: string): Command | null {
|
|
const trimmed = input.trim();
|
|
if (!trimmed) {return null;}
|
|
|
|
// Quit
|
|
if (trimmed === '/quit' || trimmed === '/exit') {
|
|
return { type: 'quit' };
|
|
}
|
|
|
|
// Reset
|
|
if (trimmed === '/reset' || trimmed === '/clear' || trimmed === '/new') {
|
|
return { type: 'reset' };
|
|
}
|
|
|
|
// Help
|
|
if (trimmed === '/help' || trimmed === '/?') {
|
|
return { type: 'help' };
|
|
}
|
|
|
|
// Multiline paste mode
|
|
if (trimmed === '/paste' || trimmed === '/multiline') {
|
|
return { type: 'multiline' };
|
|
}
|
|
|
|
// Status
|
|
if (trimmed === '/status') {
|
|
return { type: 'status' };
|
|
}
|
|
|
|
// Tools
|
|
if (trimmed === '/tools') {
|
|
return { type: 'tools' };
|
|
}
|
|
|
|
// Research
|
|
if (trimmed.startsWith('/research ')) {
|
|
const task = trimmed.slice('/research '.length).trim();
|
|
return { type: 'research', task };
|
|
}
|
|
if (trimmed === '/research') {
|
|
return { type: 'research', task: '' };
|
|
}
|
|
|
|
// Council
|
|
if (trimmed.startsWith('/council ')) {
|
|
const task = trimmed.slice('/council '.length).trim();
|
|
return { type: 'council', task };
|
|
}
|
|
if (trimmed === '/council') {
|
|
return { type: 'council', task: '' };
|
|
}
|
|
// Natural-language council shortcut for common flows.
|
|
const councilShortcut = trimmed.match(/^(?:yes\s+)?run\s+(?:the\s+)?council(?:\s+on)?(?:\s+(.+))?$/i);
|
|
if (councilShortcut) {
|
|
return { type: 'council', task: (councilShortcut[1] ?? '').trim() };
|
|
}
|
|
|
|
// Fullscreen
|
|
if (trimmed === '/fullscreen' || trimmed === '/fs') {
|
|
return { type: 'fullscreen' };
|
|
}
|
|
|
|
// Compact
|
|
if (trimmed === '/compact') {
|
|
return { type: 'compact' };
|
|
}
|
|
|
|
// Usage
|
|
if (trimmed === '/usage') {
|
|
return { type: 'usage' };
|
|
}
|
|
|
|
// Context
|
|
if (trimmed === '/context') {
|
|
return { type: 'context' };
|
|
}
|
|
|
|
// Verbose
|
|
if (trimmed === '/verbose') {
|
|
return { type: 'verbose' };
|
|
}
|
|
|
|
// Model (with optional argument)
|
|
if (trimmed === '/model') {
|
|
return { type: 'model' };
|
|
}
|
|
if (trimmed.startsWith('/model ')) {
|
|
const args = trimmed.slice('/model '.length).trim();
|
|
const parts = args.split(/\s+/);
|
|
|
|
// /model <tier> <provider/model> - change tier's provider/model
|
|
if (parts.length === 2 && parts[1].includes('/')) {
|
|
return { type: 'model', name: parts[0], providerModel: parts[1] };
|
|
}
|
|
|
|
// /model <name> - single word (backward compatibility)
|
|
const name = parts[0];
|
|
return { type: 'model', name };
|
|
}
|
|
|
|
// Backend (with optional argument)
|
|
if (trimmed === '/backend') {
|
|
return { type: 'backend' };
|
|
}
|
|
if (trimmed.startsWith('/backend ')) {
|
|
const provider = trimmed.slice('/backend '.length).trim();
|
|
return { type: 'backend', provider };
|
|
}
|
|
|
|
// Runtime backend mode control (daemon/channel command; reserved in TUI)
|
|
if (trimmed === '/runtime') {
|
|
return { type: 'runtime' };
|
|
}
|
|
if (trimmed.startsWith('/runtime ')) {
|
|
const input = trimmed.slice('/runtime '.length).trim();
|
|
return { type: 'runtime', input };
|
|
}
|
|
|
|
// Transfer
|
|
if (trimmed === '/transfer') {
|
|
return { type: 'transfer', target: '' };
|
|
}
|
|
if (trimmed.startsWith('/transfer ')) {
|
|
const target = trimmed.slice('/transfer '.length).trim();
|
|
return { type: 'transfer', target };
|
|
}
|
|
|
|
// Login
|
|
if (trimmed === '/login') {
|
|
return { type: 'login' };
|
|
}
|
|
if (trimmed.startsWith('/login ')) {
|
|
const provider = trimmed.slice('/login '.length).trim();
|
|
return { type: 'login', provider: provider || undefined };
|
|
}
|
|
|
|
// Pair
|
|
if (trimmed === '/pair' || trimmed === '/pair list') {
|
|
return { type: 'pair', action: 'list' };
|
|
}
|
|
if (trimmed === '/pair generate' || trimmed.startsWith('/pair generate ')) {
|
|
const label = trimmed.slice('/pair generate'.length).trim() || undefined;
|
|
return { type: 'pair', action: 'generate', args: label };
|
|
}
|
|
if (trimmed.startsWith('/pair revoke ')) {
|
|
const args = trimmed.slice('/pair revoke '.length).trim();
|
|
return { type: 'pair', action: 'revoke', args };
|
|
}
|
|
|
|
// Queue
|
|
if (trimmed === '/queue' || trimmed === '/queue show') {
|
|
return { type: 'queue', action: 'show' };
|
|
}
|
|
if (trimmed === '/queue reset') {
|
|
return { type: 'queue', action: 'reset' };
|
|
}
|
|
if (trimmed.startsWith('/queue set ')) {
|
|
const args = trimmed.slice('/queue set '.length).trim();
|
|
return { type: 'queue', action: 'set', args };
|
|
}
|
|
if (trimmed.startsWith('/queue ')) {
|
|
const args = trimmed.slice('/queue '.length).trim();
|
|
return { type: 'queue', action: 'set', args };
|
|
}
|
|
|
|
// Elevate
|
|
if (trimmed === '/elevate') {
|
|
return { type: 'elevate' };
|
|
}
|
|
if (trimmed.startsWith('/elevate ')) {
|
|
const args = trimmed.slice('/elevate '.length).trim();
|
|
return { type: 'elevate', args };
|
|
}
|
|
|
|
// Regular message
|
|
return { type: 'message', content: trimmed };
|
|
}
|
|
|
|
export function getHelpText(): string {
|
|
return `
|
|
Commands:
|
|
/help, /? Show this help
|
|
/paste, /multiline Enter multiline mode (finish with single '.' line)
|
|
/tools Show available tools in this session
|
|
/model [name] Show or switch model tier (local, default, fast, complex)
|
|
/model <tier> <p/m> Change tier's provider/model (e.g. /model default anthropic/claude-sonnet-4)
|
|
/backend [provider] Show or switch local backend (ollama, llamacpp)
|
|
/runtime [args] Runtime backend mode control (daemon/channel sessions)
|
|
/research <task> Delegate a task to the configured research agent
|
|
/council <task> Run the councils pipeline for a task
|
|
/council preflight Check council tier routing, endpoint/auth mode, and probe latency
|
|
/login [provider] Authenticate with GitHub, OpenAI, Anthropic, or Z.AI
|
|
/pair List pending pairing codes and approved senders
|
|
/pair generate [label] Generate a new DM pairing code
|
|
/pair revoke <ch> <id> Revoke an approved sender
|
|
/queue Show queue policy for this session
|
|
/queue set <k> <v> Set queue override (mode/cap/overflow/debounce_ms/summarize_overflow)
|
|
/queue reset Clear queue overrides for this session
|
|
/approvals List pending guarded actions for this session
|
|
/approve [id] Approve latest pending action (or specific id)
|
|
/deny [id] [reason] Deny latest pending action (or specific id)
|
|
/elevate [args] Show or manage elevated mode
|
|
/reset, /clear, /new Clear conversation history
|
|
/compact Compact conversation history
|
|
/usage Show token usage and estimated cost
|
|
/context Show estimated context-window usage
|
|
/verbose Toggle verbose mode (show raw streaming and tool output)
|
|
/status Show session info and token usage
|
|
/fullscreen, /fs Switch to fullscreen mode
|
|
/transfer <dest> Transfer session to another frontend (telegram|tui)
|
|
/quit, /exit Exit TUI
|
|
`.trim();
|
|
}
|
|
|
|
import { MODEL_PROVIDERS } from '../../config/index.js';
|
|
|
|
export type ModelAlias = 'local' | 'default' | 'fast' | 'complex' | 'opus' | 'sonnet' | 'haiku' | 'ollama';
|
|
|
|
// List of all slash commands for autocompletion
|
|
export const SLASH_COMMANDS = [
|
|
'/help',
|
|
'/paste',
|
|
'/multiline',
|
|
'/tools',
|
|
'/model',
|
|
'/backend',
|
|
'/runtime',
|
|
'/research',
|
|
'/council',
|
|
'/reset',
|
|
'/clear',
|
|
'/new',
|
|
'/compact',
|
|
'/usage',
|
|
'/context',
|
|
'/verbose',
|
|
'/status',
|
|
'/fullscreen',
|
|
'/fs',
|
|
'/login',
|
|
'/pair',
|
|
'/queue',
|
|
'/approvals',
|
|
'/approve',
|
|
'/deny',
|
|
'/elevate',
|
|
'/transfer',
|
|
'/quit',
|
|
'/exit',
|
|
];
|
|
|
|
// Command descriptions for tooltips
|
|
export const COMMAND_TOOLTIPS: Record<string, string> = {
|
|
'/help': 'Show available commands',
|
|
'/paste': 'Compose a multiline message; finish with a single "." line',
|
|
'/multiline': 'Compose a multiline message; finish with a single "." line',
|
|
'/tools': 'Show authoritative runtime tool list for this session',
|
|
'/model': 'Show or switch model (local, default, fast, complex)',
|
|
'/backend': 'Show or switch local backend (ollama, llamacpp)',
|
|
'/runtime': 'Runtime backend mode control (daemon/channel command; not local TUI backend switch)',
|
|
'/research': 'Delegate a task to the configured research agent',
|
|
'/council': 'Run the councils pipeline for a task; use "/council preflight" for route/auth checks',
|
|
'/reset': 'Clear conversation history',
|
|
'/clear': 'Clear conversation history',
|
|
'/new': 'Start a new conversation',
|
|
'/compact': 'Compact conversation history to save context space',
|
|
'/usage': 'Show token usage and estimated cost',
|
|
'/context': 'Show estimated context-window usage',
|
|
'/verbose': 'Toggle verbose mode (show raw streaming and tool output)',
|
|
'/status': 'Show session info and token usage',
|
|
'/fullscreen': 'Switch to fullscreen mode',
|
|
'/fs': 'Switch to fullscreen mode',
|
|
'/login': 'Authenticate with GitHub/OpenAI/Anthropic (OAuth/token or API key) or Z.AI (API key store)',
|
|
'/pair': 'Generate/list/revoke DM pairing codes',
|
|
'/queue': 'Show or update per-session queue policy',
|
|
'/approvals': 'List pending guarded actions for this session',
|
|
'/approve': 'Approve latest pending action (or specific id)',
|
|
'/deny': 'Deny latest pending action (or specific id and reason)',
|
|
'/elevate': 'Show or manage elevated mode',
|
|
'/transfer': 'Transfer session to another frontend (telegram|tui)',
|
|
'/quit': 'Exit TUI',
|
|
'/exit': 'Exit TUI',
|
|
};
|
|
|
|
// Model aliases for /model command autocompletion
|
|
export const MODEL_ALIASES = ['local', 'default', 'fast', 'complex', 'opus', 'sonnet', 'haiku', 'ollama'];
|
|
|
|
// Provider names for /model <tier> <provider/model> syntax — derived from config schema
|
|
export const PROVIDER_NAMES: readonly string[] = MODEL_PROVIDERS;
|
|
|
|
// Model alias descriptions
|
|
export const MODEL_TOOLTIPS: Record<string, string> = {
|
|
local: 'Local model (Ollama/llama.cpp)',
|
|
default: 'Default model tier',
|
|
fast: 'Fast/lightweight model tier',
|
|
complex: 'Complex reasoning model tier',
|
|
opus: 'Alias for complex tier',
|
|
sonnet: 'Alias for default tier',
|
|
haiku: 'Alias for fast tier',
|
|
ollama: 'Alias for local tier',
|
|
};
|
|
|
|
export function getCommandCompletions(partial: string): string[] {
|
|
const trimmed = partial.trim();
|
|
|
|
if (trimmed.startsWith('/council ')) {
|
|
const suffix = trimmed.slice('/council '.length).toLowerCase();
|
|
if ('preflight'.startsWith(suffix)) {
|
|
return ['/council preflight'];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
// Complete /model <tier> <provider/model>
|
|
if (trimmed.startsWith('/model ')) {
|
|
const args = trimmed.slice('/model '.length).trim();
|
|
const parts = args.split(/\s+/);
|
|
|
|
if (parts.length === 1) {
|
|
// Single word - suggest model aliases
|
|
const modelPartial = parts[0].toLowerCase();
|
|
return MODEL_ALIASES
|
|
.filter(alias => alias.startsWith(modelPartial))
|
|
.map(alias => `/model ${alias}`);
|
|
} else if (parts.length === 2) {
|
|
// Two words - suggest provider prefixes
|
|
const providerPartial = parts[1].toLowerCase();
|
|
return PROVIDER_NAMES
|
|
.filter(provider => provider.startsWith(providerPartial))
|
|
.map(provider => `/model ${parts[0]} ${provider}`);
|
|
}
|
|
}
|
|
|
|
// Complete slash commands
|
|
if (trimmed.startsWith('/')) {
|
|
return SLASH_COMMANDS.filter(cmd => cmd.startsWith(trimmed.toLowerCase()));
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
export function getCommandTooltip(partial: string): string | null {
|
|
const trimmed = partial.trim().toLowerCase();
|
|
|
|
// Tooltip for /model arguments
|
|
if (trimmed.startsWith('/model ')) {
|
|
const args = trimmed.slice('/model '.length).trim();
|
|
const parts = args.split(/\s+/);
|
|
|
|
if (parts.length === 1) {
|
|
// Single word - model tier or provider
|
|
const modelArg = parts[0].toLowerCase();
|
|
if (modelArg && MODEL_TOOLTIPS[modelArg]) {
|
|
return MODEL_TOOLTIPS[modelArg];
|
|
}
|
|
// Show tooltip for partial match
|
|
const matches = MODEL_ALIASES.filter(a => a.startsWith(modelArg));
|
|
if (matches.length === 1 && MODEL_TOOLTIPS[matches[0]]) {
|
|
return MODEL_TOOLTIPS[matches[0]];
|
|
}
|
|
return 'Choose: local, default, fast, complex';
|
|
} else if (parts.length === 2) {
|
|
// Two words - tier + provider
|
|
const providerPartial = parts[1].toLowerCase();
|
|
const matches = PROVIDER_NAMES.filter(p => p.startsWith(providerPartial));
|
|
if (matches.length === 1) {
|
|
return `Enter provider/model (e.g. ${matches[0]}/...)`;
|
|
}
|
|
return 'Enter provider/model (e.g. anthropic/claude-sonnet-4)';
|
|
}
|
|
}
|
|
|
|
// Exact match tooltip
|
|
if (COMMAND_TOOLTIPS[trimmed]) {
|
|
return COMMAND_TOOLTIPS[trimmed];
|
|
}
|
|
|
|
// Partial match - show tooltip if only one command matches
|
|
if (trimmed.startsWith('/')) {
|
|
const matches = SLASH_COMMANDS.filter(cmd => cmd.startsWith(trimmed));
|
|
if (matches.length === 1 && COMMAND_TOOLTIPS[matches[0]]) {
|
|
return COMMAND_TOOLTIPS[matches[0]];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function resolveModelAlias(alias: string): 'local' | 'default' | 'fast' | 'complex' {
|
|
const map: Record<string, 'local' | 'default' | 'fast' | 'complex'> = {
|
|
local: 'local',
|
|
ollama: 'local',
|
|
default: 'default',
|
|
sonnet: 'default',
|
|
fast: 'fast',
|
|
haiku: 'fast',
|
|
complex: 'complex',
|
|
opus: 'complex',
|
|
};
|
|
return map[alias.toLowerCase()] ?? 'default';
|
|
}
|