@muyichengshayu/promptx 0.2.7 → 0.2.9

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 (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/apps/runner/src/engines/claudeCodeRunner.js +69 -1
  3. package/apps/runner/src/engines/claudeCodeRunner.test.js +279 -0
  4. package/apps/runner/src/engines/index.js +2 -0
  5. package/apps/runner/src/engines/kimiCodeRunner.js +561 -0
  6. package/apps/runner/src/engines/openCodeRunner.test.js +73 -0
  7. package/apps/runner/src/engines/shellRunner.test.js +46 -0
  8. package/apps/runner/src/runManager.js +115 -11
  9. package/apps/runner/src/runManager.test.js +913 -0
  10. package/apps/runner/src/serverClient.test.js +93 -0
  11. package/apps/server/src/agentSessionDiscovery.js +136 -0
  12. package/apps/server/src/agentSessionDiscovery.test.js +186 -0
  13. package/apps/server/src/agents/claudeCodeRunner.test.js +433 -0
  14. package/apps/server/src/agents/index.js +2 -0
  15. package/apps/server/src/agents/kimiCodeRunner.js +565 -0
  16. package/apps/server/src/agents/kimiCodeRunner.test.js +127 -0
  17. package/apps/server/src/agents/openCodeRunner.test.js +236 -0
  18. package/apps/server/src/agents/runnerContract.test.js +382 -0
  19. package/apps/server/src/appPaths.test.js +52 -0
  20. package/apps/server/src/assetRoutes.test.js +168 -0
  21. package/apps/server/src/codex.test.js +518 -0
  22. package/apps/server/src/codexRoutes.test.js +376 -0
  23. package/apps/server/src/codexRuns.test.js +160 -0
  24. package/apps/server/src/codexSessions.js +1 -1
  25. package/apps/server/src/codexSessions.test.js +369 -0
  26. package/apps/server/src/db.test.js +182 -0
  27. package/apps/server/src/gitDiff.test.js +542 -0
  28. package/apps/server/src/gitDiffClient.test.js +140 -0
  29. package/apps/server/src/internalRoutes.test.js +134 -0
  30. package/apps/server/src/maintenance.test.js +154 -0
  31. package/apps/server/src/processControl.test.js +147 -0
  32. package/apps/server/src/relayClient.test.js +478 -0
  33. package/apps/server/src/relayConfig.test.js +73 -0
  34. package/apps/server/src/relayProtocol.test.js +49 -0
  35. package/apps/server/src/relayServer.test.js +798 -0
  36. package/apps/server/src/relayTenants.test.js +137 -0
  37. package/apps/server/src/relayUsageStore.test.js +65 -0
  38. package/apps/server/src/repository.test.js +150 -0
  39. package/apps/server/src/runDispatchService.js +14 -2
  40. package/apps/server/src/runDispatchService.test.js +563 -0
  41. package/apps/server/src/runEventIngest.test.js +225 -0
  42. package/apps/server/src/runRecovery.test.js +73 -0
  43. package/apps/server/src/runnerClient.test.js +80 -0
  44. package/apps/server/src/runnerDispatch.test.js +136 -0
  45. package/apps/server/src/systemConfig.test.js +112 -0
  46. package/apps/server/src/systemRoutes.test.js +319 -0
  47. package/apps/server/src/taskRoutes.test.js +726 -0
  48. package/apps/server/src/upload.test.js +30 -0
  49. package/apps/server/src/webAppRoutes.test.js +67 -0
  50. package/apps/server/src/workspaceFiles.test.js +262 -0
  51. package/apps/web/dist/assets/{CodexSessionManagerDialog-B_F9ZWKy.js → CodexSessionManagerDialog-_qLljY7F.js} +1 -1
  52. package/apps/web/dist/assets/{TaskDiffReviewDialog-CPqGk_q2.js → TaskDiffReviewDialog-DpW8S8yT.js} +1 -1
  53. package/apps/web/dist/assets/{WorkbenchSettingsDialog-CWl81vlG.js → WorkbenchSettingsDialog-CYfh5G7c.js} +1 -1
  54. package/apps/web/dist/assets/WorkbenchView-A8nm0NH9.js +60 -0
  55. package/apps/web/dist/assets/index-DHF_zkYI.js +2 -0
  56. package/apps/web/dist/index.html +1 -1
  57. package/package.json +14 -21
  58. package/packages/shared/src/index.js +6 -0
  59. package/scripts/doctor.mjs +8 -0
  60. package/apps/web/dist/assets/WorkbenchView-gbRu02Lv.js +0 -60
  61. package/apps/web/dist/assets/index-5LxHpYf5.js +0 -2
@@ -0,0 +1,561 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { execFileSync, spawn } from 'node:child_process'
4
+ import {
5
+ AGENT_ENGINES,
6
+ AGENT_RUN_ITEM_TYPES,
7
+ createAgentEventEnvelopeEvent,
8
+ createCompletedEnvelopeEvent,
9
+ createItemCompletedEvent,
10
+ createItemStartedEvent,
11
+ createStatusEnvelopeEvent,
12
+ createStderrEnvelopeEvent,
13
+ createStdoutEnvelopeEvent,
14
+ createThreadStartedEvent,
15
+ createTurnCompletedEvent,
16
+ getAgentEngineLabel,
17
+ } from '../../../../packages/shared/src/index.js'
18
+ import { createManagedSpawnOptions, forceStopChildProcess } from '../processControl.js'
19
+
20
+ const KIMI_CODE_BIN = process.env.KIMI_CODE_BIN || 'kimi'
21
+ const RESOLVED_KIMI_CODE_BIN = resolveKimiCodeBinary()
22
+
23
+ function resolveKimiCodeBinary() {
24
+ if (process.platform !== 'win32') {
25
+ return KIMI_CODE_BIN
26
+ }
27
+
28
+ if (path.extname(KIMI_CODE_BIN)) {
29
+ return KIMI_CODE_BIN
30
+ }
31
+
32
+ if (fs.existsSync(`${KIMI_CODE_BIN}.cmd`)) {
33
+ return `${KIMI_CODE_BIN}.cmd`
34
+ }
35
+
36
+ if (fs.existsSync(`${KIMI_CODE_BIN}.bat`)) {
37
+ return `${KIMI_CODE_BIN}.bat`
38
+ }
39
+
40
+ if (fs.existsSync(KIMI_CODE_BIN)) {
41
+ return KIMI_CODE_BIN
42
+ }
43
+
44
+ try {
45
+ const output = execFileSync('where.exe', [KIMI_CODE_BIN], {
46
+ encoding: 'utf8',
47
+ stdio: ['ignore', 'pipe', 'ignore'],
48
+ windowsHide: true,
49
+ }).trim()
50
+
51
+ if (!output) {
52
+ return KIMI_CODE_BIN
53
+ }
54
+
55
+ const candidates = output
56
+ .split(/\r?\n/g)
57
+ .map((line) => line.trim())
58
+ .filter(Boolean)
59
+
60
+ return candidates.find((item) => /\.(cmd|bat)$/i.test(item))
61
+ || candidates.find((item) => /\.(exe|com)$/i.test(item))
62
+ || candidates[0]
63
+ || KIMI_CODE_BIN
64
+ } catch {
65
+ return KIMI_CODE_BIN
66
+ }
67
+ }
68
+
69
+ function createKimiSpawn(commandArgs = [], cwd = '') {
70
+ const options = createManagedSpawnOptions({
71
+ cwd,
72
+ stdio: ['ignore', 'pipe', 'pipe'],
73
+ })
74
+
75
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_KIMI_CODE_BIN)) {
76
+ return spawn(
77
+ process.env.ComSpec || 'cmd.exe',
78
+ ['/d', '/s', '/c', RESOLVED_KIMI_CODE_BIN, ...commandArgs],
79
+ options
80
+ )
81
+ }
82
+
83
+ return spawn(RESOLVED_KIMI_CODE_BIN, commandArgs, options)
84
+ }
85
+
86
+ function normalizeSpawnError(error) {
87
+ if (error?.code === 'ENOENT') {
88
+ const attempted = RESOLVED_KIMI_CODE_BIN === KIMI_CODE_BIN
89
+ ? KIMI_CODE_BIN
90
+ : `${KIMI_CODE_BIN} -> ${RESOLVED_KIMI_CODE_BIN}`
91
+ return new Error(
92
+ `找不到 Kimi Code CLI(尝试执行:${attempted})。请先确认终端里可以运行 \`kimi --version\`,或设置环境变量 \`KIMI_CODE_BIN\`。`
93
+ )
94
+ }
95
+
96
+ return error
97
+ }
98
+
99
+ function parseJsonLine(line = '') {
100
+ const text = String(line || '').trim()
101
+ if (!text) {
102
+ return null
103
+ }
104
+
105
+ try {
106
+ return JSON.parse(text)
107
+ } catch {
108
+ return null
109
+ }
110
+ }
111
+
112
+ function splitBufferedLines(buffer = '') {
113
+ const text = String(buffer || '')
114
+ if (!text) {
115
+ return { lines: [], rest: '' }
116
+ }
117
+
118
+ const normalized = text.replace(/\r\n/g, '\n')
119
+ const parts = normalized.split('\n')
120
+ const rest = parts.pop() || ''
121
+
122
+ return {
123
+ lines: parts.map((line) => line.trim()).filter(Boolean),
124
+ rest,
125
+ }
126
+ }
127
+
128
+ function flushBufferedText(buffer = '') {
129
+ const { lines, rest } = splitBufferedLines(buffer)
130
+ const tail = String(rest || '').trim()
131
+ return tail ? [...lines, tail] : lines
132
+ }
133
+
134
+ function stringifyKimiToolResultContent(value) {
135
+ if (typeof value === 'string') {
136
+ return value.trim()
137
+ }
138
+
139
+ if (value == null) {
140
+ return ''
141
+ }
142
+
143
+ if (Array.isArray(value)) {
144
+ const parts = []
145
+ for (const item of value) {
146
+ if (item && typeof item === 'object') {
147
+ const text = String(item.text || '').trim()
148
+ if (text) parts.push(text)
149
+ } else if (typeof item === 'string') {
150
+ const text = item.trim()
151
+ if (text) parts.push(text)
152
+ }
153
+ }
154
+ return parts.join('\n').trim()
155
+ }
156
+
157
+ try {
158
+ const compact = JSON.stringify(value)
159
+ return compact.length <= 12000 ? compact : `${compact.slice(0, 11997)}...`
160
+ } catch {
161
+ return String(value || '').trim()
162
+ }
163
+ }
164
+
165
+ function buildKimiToolCommand(name = '', input = {}) {
166
+ const toolName = String(name || 'Kimi tool').trim() || 'Kimi tool'
167
+ if (!input || typeof input !== 'object') {
168
+ return toolName
169
+ }
170
+
171
+ const command = String(input.command || '').trim()
172
+ if (command) {
173
+ return `${toolName}: ${command}`
174
+ }
175
+
176
+ const singleValueKeys = ['file_path', 'path', 'pattern', 'query', 'url', 'description']
177
+ for (const key of singleValueKeys) {
178
+ const value = String(input[key] || '').trim()
179
+ if (value) {
180
+ return `${toolName}: ${value}`
181
+ }
182
+ }
183
+
184
+ try {
185
+ const compact = JSON.stringify(input)
186
+ return compact.length <= 240 ? `${toolName}: ${compact}` : `${toolName}: ${compact.slice(0, 237)}...`
187
+ } catch {
188
+ return toolName
189
+ }
190
+ }
191
+
192
+ function isKimiTodoToolName(name = '') {
193
+ return String(name || '').trim().toLowerCase() === 'settodolist'
194
+ }
195
+
196
+ function normalizeKimiTodoStatus(status = '') {
197
+ const normalized = String(status || '').trim().toLowerCase()
198
+ if (normalized === 'done') {
199
+ return 'completed'
200
+ }
201
+ if (normalized === 'in_progress') {
202
+ return 'in_progress'
203
+ }
204
+ return 'pending'
205
+ }
206
+
207
+ function normalizeKimiTodoItems(items = []) {
208
+ return (Array.isArray(items) ? items : [])
209
+ .map((entry) => {
210
+ if (!entry || typeof entry !== 'object') {
211
+ return null
212
+ }
213
+
214
+ const status = normalizeKimiTodoStatus(entry.status)
215
+ const text = String(entry.title || entry.text || '').trim()
216
+ if (!text) {
217
+ return null
218
+ }
219
+
220
+ return {
221
+ text,
222
+ status,
223
+ completed: status === 'completed',
224
+ }
225
+ })
226
+ .filter(Boolean)
227
+ }
228
+
229
+ function extractKimiSessionIdFromStderrLine(line = '') {
230
+ const match = String(line || '').match(/To resume this session:\s*kimi\s+(?:-r|--session|--resume)\s+([a-f0-9-]+)/i)
231
+ return match?.[1] ? String(match[1]).trim() : ''
232
+ }
233
+
234
+ export function isKimiInfoStderrLine(line = '') {
235
+ const text = String(line || '').trim()
236
+ if (!text) {
237
+ return true
238
+ }
239
+ if (/^To resume this session:/i.test(text)) {
240
+ return true
241
+ }
242
+ if (/^Shell cwd was reset to /i.test(text)) {
243
+ return true
244
+ }
245
+ return false
246
+ }
247
+
248
+ export function createKimiNormalizationState() {
249
+ return {
250
+ turnStarted: false,
251
+ toolUses: new Map(),
252
+ }
253
+ }
254
+
255
+ export function normalizeKimiEvents(event = {}, state = createKimiNormalizationState()) {
256
+ const role = String(event?.role || '').trim().toLowerCase()
257
+ const normalizedEvents = []
258
+
259
+ if (role === 'assistant') {
260
+ if (!state.turnStarted) {
261
+ state.turnStarted = true
262
+ normalizedEvents.push({ type: 'turn.started' })
263
+ }
264
+
265
+ const content = Array.isArray(event.content) ? event.content : []
266
+ content.forEach((block) => {
267
+ const blockType = String(block?.type || '').trim().toLowerCase()
268
+ if (blockType === 'think') {
269
+ const text = String(block?.think || '').trim()
270
+ if (text) {
271
+ normalizedEvents.push({
272
+ ...createItemStartedEvent({
273
+ type: AGENT_RUN_ITEM_TYPES.REASONING,
274
+ text,
275
+ }),
276
+ })
277
+ }
278
+ return
279
+ }
280
+
281
+ if (blockType === 'text') {
282
+ const text = String(block?.text || '').trim()
283
+ if (text) {
284
+ normalizedEvents.push({
285
+ ...createItemCompletedEvent({
286
+ type: AGENT_RUN_ITEM_TYPES.AGENT_MESSAGE,
287
+ text,
288
+ }),
289
+ })
290
+ }
291
+ return
292
+ }
293
+ })
294
+
295
+ const toolCalls = Array.isArray(event.tool_calls) ? event.tool_calls : []
296
+ toolCalls.forEach((toolCall) => {
297
+ const toolUseId = String(toolCall?.id || '').trim()
298
+ const name = String(toolCall?.function?.name || toolCall?.name || 'Kimi tool').trim() || 'Kimi tool'
299
+ const argsText = toolCall?.function?.arguments || toolCall?.arguments || '{}'
300
+ let parsedArgs = {}
301
+ try {
302
+ parsedArgs = JSON.parse(argsText)
303
+ } catch {
304
+ parsedArgs = {}
305
+ }
306
+ const command = buildKimiToolCommand(name, parsedArgs)
307
+ const isTodoTool = isKimiTodoToolName(name)
308
+ const todoItems = normalizeKimiTodoItems(parsedArgs.todos)
309
+
310
+ if (toolUseId) {
311
+ state.toolUses.set(toolUseId, {
312
+ name,
313
+ command,
314
+ kind: isTodoTool ? 'todo' : 'command',
315
+ todoItems,
316
+ })
317
+ }
318
+
319
+ if (isTodoTool) {
320
+ normalizedEvents.push({
321
+ ...createItemStartedEvent({
322
+ type: AGENT_RUN_ITEM_TYPES.TODO_LIST,
323
+ items: todoItems,
324
+ }),
325
+ })
326
+ return
327
+ }
328
+
329
+ normalizedEvents.push({
330
+ ...createItemStartedEvent({
331
+ type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
332
+ command,
333
+ status: 'in_progress',
334
+ }),
335
+ })
336
+ })
337
+
338
+ return normalizedEvents
339
+ }
340
+
341
+ if (role === 'tool') {
342
+ const toolCallId = String(event?.tool_call_id || '').trim()
343
+ const remembered = toolCallId ? state.toolUses.get(toolCallId) : null
344
+ const output = stringifyKimiToolResultContent(event?.content)
345
+ const todoTool = remembered?.kind === 'todo'
346
+
347
+ if (toolCallId) {
348
+ state.toolUses.delete(toolCallId)
349
+ }
350
+
351
+ if (todoTool) {
352
+ normalizedEvents.push({
353
+ ...createItemCompletedEvent({
354
+ type: AGENT_RUN_ITEM_TYPES.TODO_LIST,
355
+ items: Array.isArray(remembered?.todoItems) ? remembered.todoItems : [],
356
+ }),
357
+ })
358
+ return normalizedEvents
359
+ }
360
+
361
+ normalizedEvents.push({
362
+ ...createItemCompletedEvent({
363
+ type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
364
+ command: remembered?.command || remembered?.name || 'Kimi tool',
365
+ status: 'completed',
366
+ exit_code: 0,
367
+ aggregated_output: output,
368
+ }),
369
+ })
370
+
371
+ return normalizedEvents
372
+ }
373
+
374
+ return [{
375
+ type: `kimi.${role || 'event'}`,
376
+ detail: stringifyKimiToolResultContent(event?.content),
377
+ }]
378
+ }
379
+
380
+ export function normalizeKimiEvent(event = {}, state = createKimiNormalizationState()) {
381
+ return normalizeKimiEvents(event, state)[0] || null
382
+ }
383
+
384
+ function createExecArgs(session, prompt) {
385
+ const args = [
386
+ '--print',
387
+ '--output-format', 'stream-json',
388
+ ]
389
+
390
+ const sessionId = String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
391
+ if (sessionId) {
392
+ args.push('--session', sessionId)
393
+ }
394
+
395
+ if (session?.cwd) {
396
+ args.push('--work-dir', session.cwd)
397
+ }
398
+
399
+ args.push('-p', String(prompt || ''))
400
+
401
+ return args
402
+ }
403
+
404
+ function createKimiRunStatusEvent(session = {}) {
405
+ const hasExistingThread = Boolean(
406
+ String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
407
+ )
408
+
409
+ return createStatusEnvelopeEvent({
410
+ stage: hasExistingThread ? 'resuming' : 'starting',
411
+ message: hasExistingThread
412
+ ? '已连接 PromptX 项目,正在继续这轮执行。'
413
+ : '已创建 PromptX 项目,正在启动第一轮执行。',
414
+ })
415
+ }
416
+
417
+ export function streamPromptToKimiCodeSession(sessionInput, prompt, callbacks = {}) {
418
+ const session = sessionInput && typeof sessionInput === 'object' ? sessionInput : null
419
+ const normalizedPrompt = String(prompt || '').trim()
420
+
421
+ if (!session?.id || !session?.cwd) {
422
+ throw new Error('缺少 PromptX 项目。')
423
+ }
424
+ if (!normalizedPrompt) {
425
+ throw new Error('没有可发送的提示词。')
426
+ }
427
+
428
+ const onEvent = typeof callbacks.onEvent === 'function' ? callbacks.onEvent : () => {}
429
+ const onThreadStarted = typeof callbacks.onThreadStarted === 'function' ? callbacks.onThreadStarted : () => {}
430
+
431
+ const child = createKimiSpawn(createExecArgs(session, normalizedPrompt), session.cwd)
432
+ onEvent(createKimiRunStatusEvent(session))
433
+
434
+ let stdoutBuffer = ''
435
+ let stderrBuffer = ''
436
+ let lastStderrLine = ''
437
+ let finalMessage = ''
438
+ let finalSessionId = String(session.engineSessionId || session.engineThreadId || session.codexThreadId || '').trim()
439
+ const normalizationState = createKimiNormalizationState()
440
+
441
+ const rememberSessionId = (sessionId) => {
442
+ const value = String(sessionId || '').trim()
443
+ if (!value || value === finalSessionId) {
444
+ return
445
+ }
446
+
447
+ finalSessionId = value
448
+ onThreadStarted(value)
449
+ }
450
+
451
+ const emitKimiJsonLine = (line) => {
452
+ const event = parseJsonLine(line)
453
+ if (!event) {
454
+ onEvent(createStdoutEnvelopeEvent(line))
455
+ return
456
+ }
457
+
458
+ const normalizedEvents = normalizeKimiEvents(event, normalizationState)
459
+ normalizedEvents.forEach((normalizedEvent) => {
460
+ onEvent(createAgentEventEnvelopeEvent(normalizedEvent))
461
+ })
462
+
463
+ const role = String(event?.role || '').trim().toLowerCase()
464
+ if (role === 'assistant') {
465
+ const content = Array.isArray(event.content) ? event.content : []
466
+ const textBlocks = content
467
+ .filter((block) => String(block?.type || '').trim().toLowerCase() === 'text')
468
+ .map((block) => String(block?.text || '').trim())
469
+ .filter(Boolean)
470
+
471
+ if (textBlocks.length) {
472
+ const text = textBlocks.join('\n').trim()
473
+ finalMessage = `${finalMessage}${finalMessage ? '\n' : ''}${text}`
474
+ }
475
+ }
476
+ }
477
+
478
+ child.stdout.on('data', (chunk) => {
479
+ stdoutBuffer += chunk.toString()
480
+ const { lines, rest } = splitBufferedLines(stdoutBuffer)
481
+ stdoutBuffer = rest
482
+ lines.forEach(emitKimiJsonLine)
483
+ })
484
+
485
+ child.stderr.on('data', (chunk) => {
486
+ stderrBuffer += chunk.toString()
487
+ const { lines, rest } = splitBufferedLines(stderrBuffer)
488
+ stderrBuffer = rest
489
+ lines.forEach((line) => {
490
+ const sessionId = extractKimiSessionIdFromStderrLine(line)
491
+ if (sessionId) {
492
+ rememberSessionId(sessionId)
493
+ }
494
+
495
+ if (isKimiInfoStderrLine(line)) {
496
+ return
497
+ }
498
+
499
+ lastStderrLine = line
500
+ onEvent(createStderrEnvelopeEvent(line))
501
+ })
502
+ })
503
+
504
+ const result = new Promise((resolve, reject) => {
505
+ child.on('error', (error) => {
506
+ reject(normalizeSpawnError(error))
507
+ })
508
+
509
+ child.on('close', (code) => {
510
+ flushBufferedText(stdoutBuffer).forEach(emitKimiJsonLine)
511
+ flushBufferedText(stderrBuffer).forEach((line) => {
512
+ const sessionId = extractKimiSessionIdFromStderrLine(line)
513
+ if (sessionId) {
514
+ rememberSessionId(sessionId)
515
+ }
516
+
517
+ if (isKimiInfoStderrLine(line)) {
518
+ return
519
+ }
520
+
521
+ lastStderrLine = line
522
+ onEvent(createStderrEnvelopeEvent(line))
523
+ })
524
+
525
+ if (code !== 0) {
526
+ reject(new Error(lastStderrLine || 'Kimi Code 执行失败。'))
527
+ return
528
+ }
529
+
530
+ const message = finalMessage.trim()
531
+ onEvent(createAgentEventEnvelopeEvent(createTurnCompletedEvent()))
532
+ onEvent(createCompletedEnvelopeEvent(message))
533
+
534
+ resolve({
535
+ sessionId: session.id,
536
+ threadId: finalSessionId,
537
+ message,
538
+ })
539
+ })
540
+ })
541
+
542
+ return {
543
+ child,
544
+ result,
545
+ cancel(options = {}) {
546
+ forceStopChildProcess(child, options)
547
+ },
548
+ }
549
+ }
550
+
551
+ export const kimiCodeRunner = {
552
+ engine: AGENT_ENGINES.KIMI_CODE,
553
+ label: getAgentEngineLabel(AGENT_ENGINES.KIMI_CODE),
554
+ supportsWorkspaceHistory: false,
555
+ listKnownWorkspaces() {
556
+ return []
557
+ },
558
+ streamSessionPrompt(session, prompt, callbacks = {}) {
559
+ return streamPromptToKimiCodeSession(session, prompt, callbacks)
560
+ },
561
+ }
@@ -0,0 +1,73 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { normalizeOpenCodeEvents } from './openCodeRunner.js'
5
+
6
+ test('runner openCodeRunner maps sub-agent task tool_use to collaboration events', () => {
7
+ assert.deepEqual(
8
+ normalizeOpenCodeEvents({
9
+ type: 'tool_use',
10
+ sessionID: 'ses_main',
11
+ part: {
12
+ type: 'tool',
13
+ tool: 'task',
14
+ state: {
15
+ status: 'completed',
16
+ input: {
17
+ description: '分析 a.js 文件',
18
+ prompt: '请分析 /tmp/demo/a.js 文件',
19
+ subagent_type: 'explore',
20
+ },
21
+ output: 'task_id: ses_child_1\n\n<task_result>ok</task_result>',
22
+ metadata: {
23
+ sessionId: 'ses_child_1',
24
+ model: {
25
+ providerID: 'opencode',
26
+ modelID: 'minimax-m2.5-free',
27
+ },
28
+ },
29
+ },
30
+ },
31
+ }),
32
+ [
33
+ {
34
+ type: 'item.completed',
35
+ item: {
36
+ type: 'collab_tool_call',
37
+ tool: 'spawn_agent',
38
+ receiver_thread_ids: ['ses_child_1'],
39
+ prompt: '请分析 /tmp/demo/a.js 文件',
40
+ agents_states: {
41
+ ses_child_1: {
42
+ status: 'completed',
43
+ message: 'task_id: ses_child_1\n\n<task_result>ok</task_result>',
44
+ title: '分析 a.js 文件',
45
+ role: 'explore',
46
+ target: 'a.js',
47
+ model: 'opencode/minimax-m2.5-free',
48
+ },
49
+ },
50
+ },
51
+ },
52
+ {
53
+ type: 'item.completed',
54
+ item: {
55
+ type: 'collab_tool_call',
56
+ tool: 'wait',
57
+ receiver_thread_ids: ['ses_child_1'],
58
+ prompt: '请分析 /tmp/demo/a.js 文件',
59
+ agents_states: {
60
+ ses_child_1: {
61
+ status: 'completed',
62
+ message: 'task_id: ses_child_1\n\n<task_result>ok</task_result>',
63
+ title: '分析 a.js 文件',
64
+ role: 'explore',
65
+ target: 'a.js',
66
+ model: 'opencode/minimax-m2.5-free',
67
+ },
68
+ },
69
+ },
70
+ },
71
+ ]
72
+ )
73
+ })
@@ -0,0 +1,46 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { shellRunner } from './shellRunner.js'
5
+
6
+ function collectShellEvents(command) {
7
+ const events = []
8
+ const stream = shellRunner.streamSessionPrompt({
9
+ id: 'session-shell-test',
10
+ cwd: process.cwd(),
11
+ }, command, {
12
+ onEvent(event) {
13
+ events.push(event)
14
+ },
15
+ })
16
+
17
+ return {
18
+ events,
19
+ stream,
20
+ }
21
+ }
22
+
23
+ test('shellRunner runs a one-shot shell command and emits command events', async () => {
24
+ const command = `"${process.execPath}" -e "console.log('hello shell')"`
25
+ const { events, stream } = collectShellEvents(command)
26
+ const result = await stream.result
27
+
28
+ assert.match(result.message, /hello shell/)
29
+ assert.ok(events.some((event) => event?.event?.type === 'item.started'))
30
+ assert.ok(events.some((event) => event?.event?.type === 'item.completed'))
31
+ assert.ok(events.some((event) => event?.type === 'stdout' && /hello shell/.test(event.text || '')))
32
+ })
33
+
34
+ test('shellRunner surfaces non-zero exit output on errors', async () => {
35
+ const command = `"${process.execPath}" -e "console.error('shell failed'); process.exit(3)"`
36
+ const { events, stream } = collectShellEvents(command)
37
+
38
+ await assert.rejects(
39
+ stream.result,
40
+ (error) => /exit 3/.test(String(error?.message || '')) && /shell failed/.test(String(error?.output || ''))
41
+ )
42
+
43
+ const completed = events.find((event) => event?.event?.type === 'item.completed')
44
+ assert.equal(completed?.event?.item?.exit_code, 3)
45
+ assert.equal(completed?.event?.item?.status, 'failed')
46
+ })