agents.ts
1 import { parseCliAgentBackendId } from '@/server/agents/session-bridge' 2 import type { CliAgentBackendId } from '@/server/agents/session-bridge' 3 import type { SlashCommand } from './ast' 4 import { HELPER_SESSIONS_COMMAND_NAMES } from './ast' 5 import { firstWord, parseSubcommand } from './lexer' 6 import type { ParsedSlashInput } from './lexer' 7 8 export function parseAgentSlashCommand(input: ParsedSlashInput): SlashCommand | null { 9 if (input.name === 'agents' || input.name === 'listagents') { 10 return parseAgentsCommand(input.rest) 11 } 12 13 if (input.name === 'agentsessions' || input.name === 'listagentsessions') { 14 return parseAgentSessionsCommand(input.rest) 15 } 16 17 if (HELPER_SESSIONS_COMMAND_NAMES.has(input.name)) { 18 return { kind: 'list-helper-sessions' } 19 } 20 21 if (input.name === 'listcodexsessions' || input.name === 'codexsessions') { 22 return { kind: 'list-agent-sessions', backendId: 'codex-cli' } 23 } 24 25 if (input.name === 'listopencodesessions' || input.name === 'opencodesessions') { 26 return { kind: 'list-agent-sessions', backendId: 'opencode' } 27 } 28 29 if (input.name === 'agentlatest') { 30 return parseAgentLatestSlashCommand({ rest: input.rest }) 31 } 32 if (input.name === 'codexlatest') { 33 return parseAgentLatestSlashCommand({ 34 rest: input.rest, 35 fixedBackendId: 'codex-cli', 36 }) 37 } 38 if (input.name === 'opencodelatest') { 39 return parseAgentLatestSlashCommand({ 40 rest: input.rest, 41 fixedBackendId: 'opencode', 42 }) 43 } 44 45 if (input.name === 'agentsend' || input.name === 'agendsend') { 46 return parseAgentSendSlashCommand({ rest: input.rest }) 47 } 48 if (input.name === 'codexsend') { 49 return parseAgentSendSlashCommand({ 50 rest: input.rest, 51 fixedBackendId: 'codex-cli', 52 }) 53 } 54 if (input.name === 'opencodesend') { 55 return parseAgentSendSlashCommand({ 56 rest: input.rest, 57 fixedBackendId: 'opencode', 58 }) 59 } 60 61 if (input.name === 'agent') { 62 return parseAgentGroupCommand(input.rest) 63 } 64 65 return null 66 } 67 68 export function parseAgentsCommand(rest: string): SlashCommand { 69 if (!rest) return { kind: 'list-agent-backends' } 70 71 const { subcommand, subcommandRest } = parseSubcommand(rest) 72 if (subcommand === 'probe' || subcommand === 'check') { 73 if (!subcommandRest) return { kind: 'agent-probe' } 74 const backendId = parseCliAgentBackendId(firstWord(subcommandRest)) 75 return backendId ? { kind: 'agent-probe', backendId } : { kind: 'help-agents' } 76 } 77 if (subcommand === 'list' || subcommand === 'backends') { 78 return { kind: 'list-agent-backends' } 79 } 80 return { kind: 'help-agents' } 81 } 82 83 export function parseAgentSessionsCommand(rest: string): SlashCommand { 84 if (!rest) return { kind: 'list-agent-sessions' } 85 const backendId = parseCliAgentBackendId(firstWord(rest)) 86 return backendId ? { kind: 'list-agent-sessions', backendId } : { kind: 'help-agents' } 87 } 88 89 export function parseAgentGroupCommand(rest: string): SlashCommand { 90 const { subcommand, subcommandRest } = parseSubcommand(rest) 91 if (!subcommand) return { kind: 'help-agents' } 92 if (subcommand === 'list' || subcommand === 'backends') { 93 return { kind: 'list-agent-backends' } 94 } 95 if (subcommand === 'probe' || subcommand === 'check') { 96 if (!subcommandRest) return { kind: 'agent-probe' } 97 const backendId = parseCliAgentBackendId(firstWord(subcommandRest)) 98 return backendId ? { kind: 'agent-probe', backendId } : { kind: 'help-agents' } 99 } 100 if (subcommand === 'sessions') { 101 if (!subcommandRest) return { kind: 'list-agent-sessions' } 102 const backendId = parseCliAgentBackendId(firstWord(subcommandRest)) 103 return backendId ? { kind: 'list-agent-sessions', backendId } : { kind: 'help-agents' } 104 } 105 if (subcommand === 'latest') { 106 return parseAgentLatestSlashCommand({ rest: subcommandRest }) 107 } 108 if (subcommand === 'send') { 109 return parseAgentSendSlashCommand({ rest: subcommandRest }) 110 } 111 return { kind: 'help-agents' } 112 } 113 114 export function parseAgentLatestSlashCommand(input: { 115 rest: string 116 fixedBackendId?: CliAgentBackendId 117 }): SlashCommand { 118 const rest = input.rest.trim() 119 if (input.fixedBackendId) { 120 return { 121 kind: 'agent-latest', 122 backendId: input.fixedBackendId, 123 selector: rest || undefined, 124 } 125 } 126 127 if (!rest) return { kind: 'help-agents' } 128 const [backendToken = '', ...selectorWords] = rest.split(/\s+/) 129 const backendId = parseCliAgentBackendId(backendToken) 130 if (!backendId) return { kind: 'help-agents' } 131 132 return { 133 kind: 'agent-latest', 134 backendId, 135 selector: selectorWords.join(' ').trim() || undefined, 136 } 137 } 138 139 export function parseAgentSendSlashCommand(input: { 140 rest: string 141 fixedBackendId?: CliAgentBackendId 142 }): SlashCommand { 143 const rest = input.rest.trim() 144 if (!rest && !input.fixedBackendId) return { kind: 'help-agents' } 145 146 const delimiter = rest.indexOf('::') 147 if (delimiter >= 0) { 148 const before = rest.slice(0, delimiter).trim() 149 const prompt = rest.slice(delimiter + 2).trim() 150 if (!prompt) return { kind: 'help-agents' } 151 const options = parseAgentSendOptions( 152 before 153 .split(/\s+/) 154 .filter(Boolean), 155 ) 156 if (options.invalid) return { kind: 'help-agents' } 157 158 if (input.fixedBackendId) { 159 return { 160 kind: 'agent-send', 161 backendId: input.fixedBackendId, 162 selector: options.selectorWords.join(' ').trim() || undefined, 163 cwd: options.cwd, 164 prompt, 165 } 166 } 167 168 const [backendToken = '', ...selectorWords] = options.selectorWords 169 const backendId = parseCliAgentBackendId(backendToken) 170 if (!backendId) return { kind: 'help-agents' } 171 172 return { 173 kind: 'agent-send', 174 backendId, 175 selector: selectorWords.join(' ').trim() || undefined, 176 cwd: options.cwd, 177 prompt, 178 } 179 } 180 181 if (input.fixedBackendId) { 182 const options = parseAgentSendOptions(rest.split(/\s+/).filter(Boolean)) 183 if (options.invalid) return { kind: 'help-agents' } 184 const [selectorWord, ...promptWords] = options.selectorWords 185 const prompt = promptWords.join(' ').trim() 186 if (!selectorWord || !prompt) return { kind: 'help-agents' } 187 return { 188 kind: 'agent-send', 189 backendId: input.fixedBackendId, 190 selector: selectorWord, 191 cwd: options.cwd, 192 prompt, 193 } 194 } 195 196 const rawWords = rest.split(/\s+/).filter(Boolean) 197 const [backendToken = '', ...remainingWords] = rawWords 198 const options = parseAgentSendOptions(remainingWords) 199 if (options.invalid) return { kind: 'help-agents' } 200 const [selectorWord, ...promptWords] = options.selectorWords 201 const backendId = parseCliAgentBackendId(backendToken) 202 const prompt = promptWords.join(' ').trim() 203 if (!backendId || !selectorWord || !prompt) return { kind: 'help-agents' } 204 205 return { 206 kind: 'agent-send', 207 backendId, 208 selector: selectorWord, 209 cwd: options.cwd, 210 prompt, 211 } 212 } 213 214 function parseAgentSendOptions(words: string[]): { 215 selectorWords: string[] 216 cwd?: string 217 invalid: boolean 218 } { 219 const selectorWords: string[] = [] 220 let cwd: string | undefined 221 222 for (let index = 0; index < words.length; index += 1) { 223 const token = words[index] 224 if (token === '--cwd' || token === '-C') { 225 const value = words[index + 1] 226 if (!value) { 227 return { selectorWords: [], invalid: true } 228 } 229 cwd = value 230 index += 1 231 continue 232 } 233 selectorWords.push(token) 234 } 235 236 return { 237 selectorWords, 238 cwd, 239 invalid: false, 240 } 241 }