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  }