dsh-agora-plugin 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +520 -0
  2. package/client/index.js +492 -0
  3. package/cordis.patch.yml +14 -0
  4. package/dsh.plugin.json +7 -0
  5. package/lib/agora-client.d.ts +73 -0
  6. package/lib/agora-client.d.ts.map +1 -0
  7. package/lib/agora-client.js +207 -0
  8. package/lib/agora-client.js.map +1 -0
  9. package/lib/client.js +492 -0
  10. package/lib/command-adapter.d.ts +44 -0
  11. package/lib/command-adapter.d.ts.map +1 -0
  12. package/lib/command-adapter.js +70 -0
  13. package/lib/command-adapter.js.map +1 -0
  14. package/lib/command.d.ts +53 -0
  15. package/lib/command.d.ts.map +1 -0
  16. package/lib/command.js +375 -0
  17. package/lib/command.js.map +1 -0
  18. package/lib/context-types.d.ts +51 -0
  19. package/lib/context-types.d.ts.map +1 -0
  20. package/lib/context-types.js +2 -0
  21. package/lib/context-types.js.map +1 -0
  22. package/lib/contracts.d.ts +402 -0
  23. package/lib/contracts.d.ts.map +1 -0
  24. package/lib/contracts.js +2 -0
  25. package/lib/contracts.js.map +1 -0
  26. package/lib/extension-sdk.d.ts +74 -0
  27. package/lib/extension-sdk.d.ts.map +1 -0
  28. package/lib/extension-sdk.js +160 -0
  29. package/lib/extension-sdk.js.map +1 -0
  30. package/lib/harness-runtime.d.ts +29 -0
  31. package/lib/harness-runtime.d.ts.map +1 -0
  32. package/lib/harness-runtime.js +422 -0
  33. package/lib/harness-runtime.js.map +1 -0
  34. package/lib/http-api.d.ts +10 -0
  35. package/lib/http-api.d.ts.map +1 -0
  36. package/lib/http-api.js +282 -0
  37. package/lib/http-api.js.map +1 -0
  38. package/lib/im-bridge-v1.d.ts +37 -0
  39. package/lib/im-bridge-v1.d.ts.map +1 -0
  40. package/lib/im-bridge-v1.js +43 -0
  41. package/lib/im-bridge-v1.js.map +1 -0
  42. package/lib/im-gateway.d.ts +24 -0
  43. package/lib/im-gateway.d.ts.map +1 -0
  44. package/lib/im-gateway.js +53 -0
  45. package/lib/im-gateway.js.map +1 -0
  46. package/lib/index.d.ts +46 -0
  47. package/lib/index.d.ts.map +1 -0
  48. package/lib/index.js +210 -0
  49. package/lib/index.js.map +1 -0
  50. package/lib/node-worker.d.ts +52 -0
  51. package/lib/node-worker.d.ts.map +1 -0
  52. package/lib/node-worker.js +372 -0
  53. package/lib/node-worker.js.map +1 -0
  54. package/lib/service.d.ts +51 -0
  55. package/lib/service.d.ts.map +1 -0
  56. package/lib/service.js +196 -0
  57. package/lib/service.js.map +1 -0
  58. package/lib/tool.d.ts +72 -0
  59. package/lib/tool.d.ts.map +1 -0
  60. package/lib/tool.js +206 -0
  61. package/lib/tool.js.map +1 -0
  62. package/package.json +74 -0
  63. package/patches/dsh-im/@xmanrui__dsh-im@2.1.0.patch +920 -0
  64. package/patches/dsh-im/@xmanrui__dsh-im@2.3.0.patch +984 -0
  65. package/patches/dsh-im/README.md +82 -0
  66. package/src/agora-client.ts +333 -0
  67. package/src/command-adapter.ts +106 -0
  68. package/src/command.ts +424 -0
  69. package/src/context-types.ts +54 -0
  70. package/src/contracts.ts +413 -0
  71. package/src/extension-sdk.ts +225 -0
  72. package/src/harness-runtime.ts +518 -0
  73. package/src/http-api.ts +269 -0
  74. package/src/im-bridge-v1.ts +73 -0
  75. package/src/im-gateway.ts +82 -0
  76. package/src/index.ts +260 -0
  77. package/src/node-worker.ts +442 -0
  78. package/src/service.ts +262 -0
  79. package/src/tool.ts +269 -0
package/src/command.ts ADDED
@@ -0,0 +1,424 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import type {
3
+ AgoraCommandResult,
4
+ AgoraRequestContext,
5
+ AgoraTask,
6
+ AgoraTaskStatus,
7
+ CreateAgoraTaskInput,
8
+ DshAgoraServiceApi,
9
+ TaskPriority,
10
+ CoordinationMode,
11
+ CoordinationRun,
12
+ } from './contracts.js'
13
+
14
+ export type AgoraCommand =
15
+ | { readonly kind: 'help' }
16
+ | { readonly kind: 'health' }
17
+ | { readonly kind: 'nodes' | 'agents' }
18
+ | { readonly kind: 'dashboard' }
19
+ | { readonly kind: 'im' }
20
+ | { readonly kind: 'list'; readonly state?: string; readonly projectId?: string }
21
+ | { readonly kind: 'show'; readonly taskId: string }
22
+ | { readonly kind: 'status'; readonly taskId: string }
23
+ | { readonly kind: 'dispatch-status'; readonly dispatchId: string }
24
+ | { readonly kind: 'runs' }
25
+ | { readonly kind: 'run-status'; readonly runId: string }
26
+ | { readonly kind: 'scorecards'; readonly taskType?: string }
27
+ | { readonly kind: 'run'; readonly input: {
28
+ readonly mode: CoordinationMode
29
+ readonly runtimeTargets: readonly string[]
30
+ readonly prompt: string
31
+ readonly taskType: string
32
+ readonly maxAgents?: number
33
+ readonly maxDispatches?: number
34
+ readonly maxSeconds?: number
35
+ } }
36
+ | { readonly kind: 'create'; readonly input: CreateAgoraTaskInput }
37
+
38
+ export class AgoraCommandParseError extends Error {
39
+ constructor(message: string) {
40
+ super(message)
41
+ this.name = 'AgoraCommandParseError'
42
+ }
43
+ }
44
+
45
+ const USAGE = [
46
+ 'Agora commands:',
47
+ '/agora health',
48
+ '/agora nodes',
49
+ '/agora agents',
50
+ '/agora list [--state <state>] [--project <project-id>]',
51
+ '/agora show <task-id>',
52
+ '/agora status <task-id>',
53
+ '/agora dispatch-status <dispatch-id>',
54
+ '/agora runs',
55
+ '/agora run-status <run-id>',
56
+ '/agora scorecards [task-type]',
57
+ '/agora run --mode fanout --agents <target,target,...> [--max-agents N] [--max-dispatches N] [--max-seconds N] <prompt>',
58
+ '/agora create [--type <type>] [--priority low|normal|high] [--project <id>] <title>',
59
+ '/agora dashboard',
60
+ '/agora im',
61
+ '',
62
+ 'Human approval and rejection remain in the authenticated Agora surface.',
63
+ ].join('\n')
64
+
65
+ export function parseAgoraCommand(rawInput: string): AgoraCommand {
66
+ const tokens = tokenize(rawInput)
67
+ const verb = tokens.shift()?.toLowerCase() ?? 'help'
68
+ switch (verb) {
69
+ case 'help':
70
+ case '--help':
71
+ case '-h':
72
+ noArguments(tokens, verb)
73
+ return { kind: 'help' }
74
+ case 'health':
75
+ noArguments(tokens, verb)
76
+ return { kind: 'health' }
77
+ case 'nodes':
78
+ case 'agents':
79
+ noArguments(tokens, verb)
80
+ return { kind: verb }
81
+ case 'dashboard':
82
+ noArguments(tokens, verb)
83
+ return { kind: 'dashboard' }
84
+ case 'im':
85
+ noArguments(tokens, verb)
86
+ return { kind: 'im' }
87
+ case 'show':
88
+ return { kind: 'show', taskId: oneArgument(tokens, 'show') }
89
+ case 'status':
90
+ return { kind: 'status', taskId: oneArgument(tokens, 'status') }
91
+ case 'dispatch-status':
92
+ return { kind: 'dispatch-status', dispatchId: oneArgument(tokens, 'dispatch-status') }
93
+ case 'runs':
94
+ noArguments(tokens, verb)
95
+ return { kind: 'runs' }
96
+ case 'run-status':
97
+ return { kind: 'run-status', runId: oneArgument(tokens, 'run-status') }
98
+ case 'scorecards':
99
+ if (tokens.length > 1) throw new AgoraCommandParseError('scorecards accepts at most one task type')
100
+ return { kind: 'scorecards', ...(tokens[0] ? { taskType: tokens[0] } : {}) }
101
+ case 'run': {
102
+ const flags = parseFlags(tokens, new Set(['mode', 'agents', 'task-type', 'max-agents', 'max-dispatches', 'max-seconds']))
103
+ const prompt = flags.positionals.join(' ').trim()
104
+ if (!prompt) throw new AgoraCommandParseError('run requires a prompt')
105
+ const mode = parseCoordinationMode(flags.values.mode)
106
+ const runtimeTargets = (flags.values.agents ?? '').split(',').map(value => value.trim()).filter(Boolean)
107
+ if (runtimeTargets.length === 0) throw new AgoraCommandParseError('run requires --agents <target,target,...>')
108
+ return { kind: 'run', input: {
109
+ mode,
110
+ runtimeTargets,
111
+ prompt,
112
+ taskType: flags.values['task-type']?.trim() || 'general',
113
+ ...optionalPositiveInteger(flags.values['max-agents'], 'max-agents'),
114
+ ...optionalPositiveInteger(flags.values['max-dispatches'], 'max-dispatches'),
115
+ ...optionalPositiveInteger(flags.values['max-seconds'], 'max-seconds'),
116
+ } }
117
+ }
118
+ case 'list': {
119
+ const flags = parseFlags(tokens, new Set(['state', 'project']))
120
+ if (flags.positionals.length > 0) throw new AgoraCommandParseError('list only accepts --state and --project')
121
+ return {
122
+ kind: 'list',
123
+ ...(flags.values.state === undefined ? {} : { state: flags.values.state }),
124
+ ...(flags.values.project === undefined ? {} : { projectId: flags.values.project }),
125
+ }
126
+ }
127
+ case 'create': {
128
+ const flags = parseFlags(tokens, new Set(['type', 'priority', 'project']))
129
+ const title = flags.positionals.join(' ').trim()
130
+ if (title === '') throw new AgoraCommandParseError('create requires a task title')
131
+ const priority = parsePriority(flags.values.priority)
132
+ return {
133
+ kind: 'create',
134
+ input: {
135
+ title,
136
+ ...(flags.values.type === undefined ? {} : { type: flags.values.type }),
137
+ ...(priority === undefined ? {} : { priority }),
138
+ ...(flags.values.project === undefined ? {} : { projectId: flags.values.project }),
139
+ },
140
+ }
141
+ }
142
+ default:
143
+ throw new AgoraCommandParseError(`unknown Agora command "${verb}"`)
144
+ }
145
+ }
146
+
147
+ export async function executeAgoraCommand(
148
+ service: DshAgoraServiceApi,
149
+ rawInput: string,
150
+ context: AgoraRequestContext = {},
151
+ signal?: AbortSignal,
152
+ ): Promise<AgoraCommandResult> {
153
+ let command: AgoraCommand
154
+ try {
155
+ command = parseAgoraCommand(rawInput)
156
+ } catch (error) {
157
+ return { kind: 'error', text: `${messageOf(error)}\n\n${USAGE}` }
158
+ }
159
+
160
+ try {
161
+ switch (command.kind) {
162
+ case 'help': return { kind: 'success', text: USAGE }
163
+ case 'dashboard': return { kind: 'success', text: `${service.serverUrl}/dashboard/` }
164
+ case 'im': return { kind: 'success', text: formatImStatus(service.snapshot()) }
165
+ case 'health': {
166
+ const health = await service.health(signal)
167
+ const label = health.service ?? health.status ?? (health.ok === false ? 'unhealthy' : 'online')
168
+ return { kind: 'success', text: `Agora: ${String(label)}\nServer: ${service.serverUrl}` }
169
+ }
170
+ case 'nodes': {
171
+ const nodes = await service.listRuntimeNodes(signal)
172
+ return {
173
+ kind: 'success',
174
+ text: nodes.length === 0 ? 'No DSH runtime nodes registered.' : nodes.map(node => (
175
+ `${node.node_id} [${node.presence}] agents=${node.agents.length} bots=${node.bots.filter(bot => bot.connected).length}/${node.bots.length}`
176
+ )).join('\n'),
177
+ }
178
+ }
179
+ case 'agents': {
180
+ const targets = (await service.listRuntimeTargets(signal)).filter(target => target.runtime_provider === 'dsh')
181
+ return {
182
+ kind: 'success',
183
+ text: targets.length === 0 ? 'No DSH runtime agents registered.' : targets.map(target => (
184
+ `${target.runtime_target_ref} ${target.primary_model ?? '-'} [${target.presentation_mode}]`
185
+ )).join('\n'),
186
+ }
187
+ }
188
+ case 'list': return { kind: 'success', text: formatTaskList(await service.listTasks(command.state, command.projectId, signal)) }
189
+ case 'show': return { kind: 'success', text: formatTask(await service.getTask(command.taskId, signal)) }
190
+ case 'status': return { kind: 'success', text: formatStatus(await service.taskStatus(command.taskId, signal)) }
191
+ case 'dispatch-status': {
192
+ const dispatch = await service.getRuntimeDispatch(command.dispatchId, signal)
193
+ const progress = await service.listRuntimeDispatchProgress(command.dispatchId, signal)
194
+ const latest = dispatch.latest_progress ?? progress.at(-1) ?? null
195
+ const envelope = dispatch.result_envelope
196
+ return {
197
+ kind: dispatch.status === 'failed' ? 'error' : 'success',
198
+ text: [
199
+ `${dispatch.id}: ${dispatch.status}`,
200
+ `Target: ${dispatch.runtime_target_ref}`,
201
+ `Session: ${dispatch.session_id ?? '-'}`,
202
+ `Lease heartbeat: ${dispatch.claim_renewed_at ?? '-'}`,
203
+ `Work progress: ${latest ? `${latest.phase} (#${latest.attempt}.${latest.sequence})${latest.percent === null || latest.percent === undefined ? '' : ` ${latest.percent}%`}` : '-'}`,
204
+ ...(latest?.message ? [`Progress detail: ${latest.message}`] : []),
205
+ ...(envelope ? [
206
+ `Evidence: ${envelope.evidence.length} item(s), ${envelope.claims.length} claim(s)`,
207
+ ...(envelope.confidence === null || envelope.confidence === undefined ? [] : [`Confidence: ${Math.round(envelope.confidence * 100)}%`]),
208
+ ] : []),
209
+ ...(dispatch.error ? [`Error: ${dispatch.error}`] : []),
210
+ ].join('\n'),
211
+ }
212
+ }
213
+ case 'runs': {
214
+ const runs = await service.listCoordinationRuns(undefined, signal)
215
+ return { kind: 'success', text: runs.length === 0 ? 'No coordination runs.' : runs.map(formatRunLine).join('\n') }
216
+ }
217
+ case 'run-status': return { kind: 'success', text: formatRun(await service.getCoordinationRun(command.runId, signal)) }
218
+ case 'scorecards': {
219
+ const cards = await service.listAgentScorecards(command.taskType, signal)
220
+ return { kind: 'success', text: cards.length === 0 ? 'No Agent scorecard observations.' : cards.map(card => (
221
+ `${card.runtime_target_ref} [${card.task_type}] score=${card.score.toFixed(1)} observations=${card.observations} success=${formatRatio(card.success_rate)} p95=${card.p95_duration_ms ?? '-'}ms evidence=${card.evidence_yield?.toFixed(2) ?? '-'}`
222
+ )).join('\n') }
223
+ }
224
+ case 'run': {
225
+ const budget = {
226
+ ...(command.input.maxAgents === undefined ? {} : { max_agents: command.input.maxAgents }),
227
+ ...(command.input.maxDispatches === undefined ? {} : { max_dispatches: command.input.maxDispatches }),
228
+ ...(command.input.maxSeconds === undefined ? {} : { max_wall_clock_seconds: command.input.maxSeconds }),
229
+ }
230
+ const run = await service.createCoordinationRun({
231
+ mode: command.input.mode,
232
+ prompt: command.input.prompt,
233
+ task_type: command.input.taskType,
234
+ candidates: command.input.runtimeTargets.map(runtime_target_ref => ({ runtime_target_ref })),
235
+ ...(Object.keys(budget).length === 0 ? {} : { budget }),
236
+ idempotency_key: `dsh-command:${randomUUID()}`,
237
+ metadata: context.actorId ? { requested_by: context.actorId } : null,
238
+ }, signal)
239
+ return { kind: 'success', text: `Created coordination run ${run.id}\n${formatRun(run)}` }
240
+ }
241
+ case 'create': {
242
+ const task = await service.createTask({
243
+ ...command.input,
244
+ ...(context.actorId === undefined ? {} : { creator: context.actorId }),
245
+ ...imTargetFrom(context),
246
+ }, signal)
247
+ return { kind: 'success', text: `Created ${task.id}: ${task.title}\nState: ${task.state}\nStage: ${task.current_stage ?? '-'}` }
248
+ }
249
+ }
250
+ } catch (error) {
251
+ return { kind: 'error', text: messageOf(error) }
252
+ }
253
+ }
254
+
255
+ function formatTaskList(tasks: readonly AgoraTask[]): string {
256
+ if (tasks.length === 0) return 'No Agora tasks matched.'
257
+ return tasks.map(task => `${task.id} [${task.state}] ${task.title}${task.current_stage ? ` (${task.current_stage})` : ''}`).join('\n')
258
+ }
259
+
260
+ function formatTask(task: AgoraTask): string {
261
+ return [
262
+ `${task.id}: ${task.title}`,
263
+ `State: ${task.state}`,
264
+ `Stage: ${task.current_stage ?? '-'}`,
265
+ `Type: ${task.type}`,
266
+ `Priority: ${task.priority}`,
267
+ `Project: ${task.project_id ?? '-'}`,
268
+ ...(task.error_detail ? [`Error: ${task.error_detail}`] : []),
269
+ ].join('\n')
270
+ }
271
+
272
+ function formatStatus(status: AgoraTaskStatus): string {
273
+ return [
274
+ formatTask(status.task),
275
+ `Subtasks: ${status.subtasks.length}`,
276
+ `Flow events: ${status.flow_log.length}`,
277
+ `Progress entries: ${status.progress_log.length}`,
278
+ ].join('\n')
279
+ }
280
+
281
+ function formatRunLine(run: CoordinationRun): string {
282
+ return `${run.id} [${run.status}] ${run.mode} members=${run.members.length} ${run.stop_reason ?? ''}`.trim()
283
+ }
284
+
285
+ function formatRun(run: CoordinationRun): string {
286
+ return [
287
+ formatRunLine(run),
288
+ `Deadline: ${run.deadline_at}`,
289
+ `Usage: tokens=${run.usage.total_tokens ?? '-'} tools=${run.usage.tool_calls ?? '-'} cost=${run.usage.cost_usd ?? '-'}`,
290
+ `Evidence: ${run.synthesis?.evidence_ids.length ?? 0}; conflicts: ${run.synthesis?.conflicts.length ?? 0}; verified: ${run.synthesis?.verified ?? false}`,
291
+ ...run.members.map(member => `- r${member.round} ${member.role} ${member.runtime_target_ref}: ${member.status} score=${member.selection_score.toFixed(1)}`),
292
+ ...(run.synthesis?.answer ? ['', run.synthesis.answer] : []),
293
+ ].join('\n')
294
+ }
295
+
296
+ function formatImStatus(snapshot: ReturnType<DshAgoraServiceApi['snapshot']>): string {
297
+ const adapter = `First-party command adapter: ${snapshot.commandAdapter.state} (${snapshot.commandAdapter.protocol})`
298
+ const gateway = snapshot.im.state === 'connected'
299
+ ? `Command gateway: connected (${snapshot.im.service}, ${snapshot.im.protocol})`
300
+ : `Command gateway: ${snapshot.im.state} (${snapshot.im.reason})`
301
+ const bridge = snapshot.imBridge.state === 'connected'
302
+ ? `IM bridge: connected (${snapshot.imBridge.service}, ${snapshot.imBridge.protocol})`
303
+ : `IM bridge: ${snapshot.imBridge.state} (${snapshot.imBridge.reason})`
304
+ const node = snapshot.node.state === 'online'
305
+ ? `Runtime node: online (${snapshot.node.nodeId})`
306
+ : snapshot.node.state === 'error'
307
+ ? `Runtime node: error (${snapshot.node.nodeId}, ${snapshot.node.error})`
308
+ : `Runtime node: ${snapshot.node.state} (${snapshot.node.nodeId})`
309
+ return [adapter, gateway, bridge, node, `Extensions: ${snapshot.extensions.map(item => item.id).join(', ') || '-'}`].join('\n')
310
+ }
311
+
312
+ function imTargetFrom(context: AgoraRequestContext): Pick<CreateAgoraTaskInput, 'imTarget'> | Record<string, never> {
313
+ if (context.provider === undefined && context.conversationRef === undefined && context.threadRef === undefined) return {}
314
+ return {
315
+ imTarget: {
316
+ ...(context.provider === undefined ? {} : { provider: context.provider }),
317
+ ...(context.conversationRef === undefined ? {} : { conversation_ref: context.conversationRef }),
318
+ ...(context.threadRef === undefined ? {} : { thread_ref: context.threadRef }),
319
+ },
320
+ }
321
+ }
322
+
323
+ function parsePriority(value: string | undefined): TaskPriority | undefined {
324
+ if (value === undefined) return undefined
325
+ if (value === 'low' || value === 'normal' || value === 'high') return value
326
+ throw new AgoraCommandParseError('priority must be low, normal, or high')
327
+ }
328
+
329
+ function parseCoordinationMode(value: string | undefined): CoordinationMode {
330
+ if (value === 'single' || value === 'fanout' || value === 'review' || value === 'debate' || value === 'council') return value
331
+ throw new AgoraCommandParseError('run requires --mode single|fanout|review|debate|council')
332
+ }
333
+
334
+ function optionalPositiveInteger(
335
+ value: string | undefined,
336
+ field: 'max-agents' | 'max-dispatches' | 'max-seconds',
337
+ ): Partial<{ maxAgents: number; maxDispatches: number; maxSeconds: number }> {
338
+ if (value === undefined) return {}
339
+ const parsed = Number(value)
340
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new AgoraCommandParseError(`--${field} must be a positive integer`)
341
+ const key = field === 'max-agents' ? 'maxAgents' : field === 'max-dispatches' ? 'maxDispatches' : 'maxSeconds'
342
+ return { [key]: parsed }
343
+ }
344
+
345
+ function formatRatio(value: number | null): string { return value === null ? '-' : `${Math.round(value * 100)}%` }
346
+
347
+ function oneArgument(tokens: string[], verb: string): string {
348
+ if (tokens.length !== 1) throw new AgoraCommandParseError(`${verb} requires exactly one task id`)
349
+ return tokens[0]!
350
+ }
351
+
352
+ function noArguments(tokens: string[], verb: string): void {
353
+ if (tokens.length > 0) throw new AgoraCommandParseError(`${verb} does not accept arguments`)
354
+ }
355
+
356
+ function parseFlags(tokens: string[], allowed: ReadonlySet<string>): {
357
+ values: Record<string, string | undefined>
358
+ positionals: string[]
359
+ } {
360
+ const values: Record<string, string | undefined> = {}
361
+ const positionals: string[] = []
362
+ for (let index = 0; index < tokens.length; index += 1) {
363
+ const token = tokens[index]!
364
+ if (!token.startsWith('--')) {
365
+ positionals.push(token)
366
+ continue
367
+ }
368
+ const name = token.slice(2)
369
+ if (!allowed.has(name)) throw new AgoraCommandParseError(`unknown option --${name}`)
370
+ if (values[name] !== undefined) throw new AgoraCommandParseError(`option --${name} was supplied more than once`)
371
+ const value = tokens[index + 1]
372
+ if (value === undefined || value.startsWith('--')) throw new AgoraCommandParseError(`option --${name} requires a value`)
373
+ values[name] = value
374
+ index += 1
375
+ }
376
+ return { values, positionals }
377
+ }
378
+
379
+ function tokenize(input: string): string[] {
380
+ const tokens: string[] = []
381
+ let token = ''
382
+ let quote: 'single' | 'double' | undefined
383
+ let escaped = false
384
+ let started = false
385
+ for (const char of input.trim()) {
386
+ if (escaped) {
387
+ token += char
388
+ escaped = false
389
+ started = true
390
+ continue
391
+ }
392
+ if (char === '\\' && quote !== 'single') {
393
+ escaped = true
394
+ started = true
395
+ continue
396
+ }
397
+ if (char === "'" && quote !== 'double') {
398
+ quote = quote === 'single' ? undefined : 'single'
399
+ started = true
400
+ continue
401
+ }
402
+ if (char === '"' && quote !== 'single') {
403
+ quote = quote === 'double' ? undefined : 'double'
404
+ started = true
405
+ continue
406
+ }
407
+ if (/\s/u.test(char) && quote === undefined) {
408
+ if (started) tokens.push(token)
409
+ token = ''
410
+ started = false
411
+ continue
412
+ }
413
+ token += char
414
+ started = true
415
+ }
416
+ if (escaped) throw new AgoraCommandParseError('unfinished escape at end of command')
417
+ if (quote !== undefined) throw new AgoraCommandParseError('unclosed quote in command')
418
+ if (started) tokens.push(token)
419
+ return tokens
420
+ }
421
+
422
+ function messageOf(error: unknown): string {
423
+ return error instanceof Error ? error.message : String(error)
424
+ }
@@ -0,0 +1,54 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+ import type {} from 'cordis'
3
+ import type { AgoraCommandResult, DshAgoraServiceApi } from './contracts.js'
4
+ import type { DshToolRegistry } from './tool.js'
5
+ import type { DshAgoraExtensionRegistryApi } from './extension-sdk.js'
6
+
7
+ export interface DshAgentLike {
8
+ readonly id?: string
9
+ readonly session?: { readonly id?: string }
10
+ }
11
+
12
+ export interface DshCommandInvocation {
13
+ readonly rawInput: string
14
+ readonly agent: DshAgentLike
15
+ readonly signal: AbortSignal
16
+ }
17
+
18
+ export interface DshCommandDefinition {
19
+ readonly name: string
20
+ readonly description: string
21
+ readonly input?: { readonly hint: string; readonly images?: boolean }
22
+ handler(invocation: DshCommandInvocation): AgoraCommandResult | Promise<AgoraCommandResult>
23
+ }
24
+
25
+ export interface DshCommands {
26
+ register(definition: DshCommandDefinition): () => void
27
+ }
28
+
29
+ export interface DshWebServer {
30
+ readonly port?: number
31
+ register(route: {
32
+ readonly kind: 'exact' | 'prefix'
33
+ readonly path: string
34
+ handler(req: IncomingMessage, res: ServerResponse): void | Promise<void>
35
+ }): () => void
36
+ }
37
+
38
+ export interface DshAgoraContext {
39
+ readonly commands: DshCommands
40
+ readonly tools: DshToolRegistry
41
+ get?(name: string): unknown
42
+ inject?(
43
+ services: readonly string[],
44
+ callback: (ctx: DshAgoraContext) => void | (() => void),
45
+ ): unknown
46
+ accessor?(name: string, descriptor: { get(): unknown }): unknown
47
+ effect?(effect: () => void | (() => void), label?: string): void
48
+ }
49
+
50
+ declare module 'cordis' {
51
+ interface Context {
52
+ dshAgora: DshAgoraServiceApi & DshAgoraExtensionRegistryApi
53
+ }
54
+ }