ai-cli-mcp 2.18.0 → 2.20.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 (101) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +37 -11
  3. package/README.md +44 -11
  4. package/dist/app/cli.js +2 -1
  5. package/dist/app/mcp.js +65 -13
  6. package/dist/cli-builder.js +13 -6
  7. package/dist/cli-process-service.js +81 -95
  8. package/dist/cli-utils.js +6 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/model-catalog.js +3 -2
  11. package/dist/parsers.js +111 -8
  12. package/dist/process-service.js +5 -4
  13. package/package.json +26 -2
  14. package/server.json +3 -3
  15. package/.gemini/settings.json +0 -11
  16. package/.github/dependabot.yml +0 -28
  17. package/.github/pull_request_template.md +0 -28
  18. package/.github/workflows/ci.yml +0 -34
  19. package/.github/workflows/dependency-review.yml +0 -22
  20. package/.github/workflows/publish.yml +0 -89
  21. package/.github/workflows/test.yml +0 -20
  22. package/.github/workflows/watch-session-prs.yml +0 -276
  23. package/.husky/pre-commit +0 -1
  24. package/.mcp.json +0 -11
  25. package/.releaserc.json +0 -18
  26. package/.vscode/settings.json +0 -3
  27. package/CONTRIBUTING.md +0 -81
  28. package/dist/__tests__/app-cli.test.js +0 -392
  29. package/dist/__tests__/cli-bin-smoke.test.js +0 -101
  30. package/dist/__tests__/cli-builder.test.js +0 -442
  31. package/dist/__tests__/cli-process-service.test.js +0 -655
  32. package/dist/__tests__/cli-utils.test.js +0 -171
  33. package/dist/__tests__/e2e.test.js +0 -256
  34. package/dist/__tests__/edge-cases.test.js +0 -130
  35. package/dist/__tests__/error-cases.test.js +0 -292
  36. package/dist/__tests__/mcp-contract.test.js +0 -636
  37. package/dist/__tests__/mocks.js +0 -32
  38. package/dist/__tests__/model-alias.test.js +0 -36
  39. package/dist/__tests__/parsers.test.js +0 -500
  40. package/dist/__tests__/peek.test.js +0 -36
  41. package/dist/__tests__/process-management.test.js +0 -871
  42. package/dist/__tests__/server.test.js +0 -809
  43. package/dist/__tests__/setup.js +0 -11
  44. package/dist/__tests__/utils/claude-mock.js +0 -80
  45. package/dist/__tests__/utils/mcp-client.js +0 -121
  46. package/dist/__tests__/utils/opencode-mock.js +0 -91
  47. package/dist/__tests__/utils/persistent-mock.js +0 -28
  48. package/dist/__tests__/utils/test-helpers.js +0 -11
  49. package/dist/__tests__/validation.test.js +0 -308
  50. package/dist/__tests__/version-print.test.js +0 -65
  51. package/dist/__tests__/wait.test.js +0 -260
  52. package/docs/RELEASE_CHECKLIST.md +0 -65
  53. package/docs/cli-architecture.md +0 -275
  54. package/docs/concept.md +0 -154
  55. package/docs/development.md +0 -156
  56. package/docs/e2e-testing.md +0 -148
  57. package/docs/prd.md +0 -146
  58. package/docs/session-stacking.md +0 -67
  59. package/src/__tests__/app-cli.test.ts +0 -495
  60. package/src/__tests__/cli-bin-smoke.test.ts +0 -136
  61. package/src/__tests__/cli-builder.test.ts +0 -549
  62. package/src/__tests__/cli-process-service.test.ts +0 -759
  63. package/src/__tests__/cli-utils.test.ts +0 -200
  64. package/src/__tests__/e2e.test.ts +0 -311
  65. package/src/__tests__/edge-cases.test.ts +0 -176
  66. package/src/__tests__/error-cases.test.ts +0 -370
  67. package/src/__tests__/mcp-contract.test.ts +0 -755
  68. package/src/__tests__/mocks.ts +0 -35
  69. package/src/__tests__/model-alias.test.ts +0 -44
  70. package/src/__tests__/parsers.test.ts +0 -564
  71. package/src/__tests__/peek.test.ts +0 -44
  72. package/src/__tests__/process-management.test.ts +0 -1043
  73. package/src/__tests__/server.test.ts +0 -1020
  74. package/src/__tests__/setup.ts +0 -13
  75. package/src/__tests__/utils/claude-mock.ts +0 -87
  76. package/src/__tests__/utils/mcp-client.ts +0 -159
  77. package/src/__tests__/utils/opencode-mock.ts +0 -108
  78. package/src/__tests__/utils/persistent-mock.ts +0 -33
  79. package/src/__tests__/utils/test-helpers.ts +0 -13
  80. package/src/__tests__/validation.test.ts +0 -369
  81. package/src/__tests__/version-print.test.ts +0 -81
  82. package/src/__tests__/wait.test.ts +0 -302
  83. package/src/app/cli.ts +0 -424
  84. package/src/app/mcp.ts +0 -466
  85. package/src/bin/ai-cli-mcp.ts +0 -7
  86. package/src/bin/ai-cli.ts +0 -11
  87. package/src/cli-builder.ts +0 -274
  88. package/src/cli-parse.ts +0 -105
  89. package/src/cli-process-service.ts +0 -708
  90. package/src/cli-utils.ts +0 -258
  91. package/src/cli.ts +0 -124
  92. package/src/model-catalog.ts +0 -87
  93. package/src/parsers.ts +0 -840
  94. package/src/peek.ts +0 -95
  95. package/src/process-result.ts +0 -88
  96. package/src/process-service.ts +0 -367
  97. package/src/server.ts +0 -10
  98. package/tsconfig.json +0 -16
  99. package/vitest.config.e2e.ts +0 -27
  100. package/vitest.config.ts +0 -22
  101. package/vitest.config.unit.ts +0 -28
@@ -1,871 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { spawn } from 'node:child_process';
3
- import { existsSync } from 'node:fs';
4
- import { homedir } from 'node:os';
5
- import { EventEmitter } from 'node:events';
6
- // Mock dependencies
7
- vi.mock('node:child_process');
8
- vi.mock('node:fs');
9
- vi.mock('node:os');
10
- vi.mock('@modelcontextprotocol/sdk/server/stdio.js');
11
- vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
12
- ListToolsRequestSchema: { name: 'listTools' },
13
- CallToolRequestSchema: { name: 'callTool' },
14
- ErrorCode: {
15
- InternalError: 'InternalError',
16
- MethodNotFound: 'MethodNotFound',
17
- InvalidParams: 'InvalidParams'
18
- },
19
- McpError: class McpError extends Error {
20
- code;
21
- constructor(code, message) {
22
- super(message);
23
- this.code = code;
24
- this.name = 'McpError';
25
- }
26
- }
27
- }));
28
- vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
29
- Server: vi.fn().mockImplementation(function () {
30
- return {
31
- setRequestHandler: vi.fn(),
32
- connect: vi.fn(),
33
- close: vi.fn(),
34
- onerror: undefined,
35
- };
36
- }),
37
- }));
38
- const mockExistsSync = vi.mocked(existsSync);
39
- const mockSpawn = vi.mocked(spawn);
40
- const mockHomedir = vi.mocked(homedir);
41
- describe('Process Management Tests', () => {
42
- let consoleErrorSpy;
43
- let originalEnv;
44
- let mockServerInstance;
45
- let handlers;
46
- beforeEach(() => {
47
- vi.clearAllMocks();
48
- vi.resetModules();
49
- consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
50
- originalEnv = { ...process.env };
51
- process.env = { ...originalEnv };
52
- handlers = new Map();
53
- // Set up default mocks
54
- mockHomedir.mockReturnValue('/home/user');
55
- mockExistsSync.mockReturnValue(true);
56
- });
57
- afterEach(() => {
58
- consoleErrorSpy.mockRestore();
59
- process.env = originalEnv;
60
- });
61
- async function setupServer() {
62
- const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
63
- vi.mocked(Server).mockImplementation(function () {
64
- mockServerInstance = {
65
- setRequestHandler: vi.fn((schema, handler) => {
66
- handlers.set(schema.name, handler);
67
- }),
68
- connect: vi.fn(),
69
- close: vi.fn(),
70
- onerror: undefined
71
- };
72
- return mockServerInstance;
73
- });
74
- const module = await import('../server.js');
75
- const { ClaudeCodeServer } = module;
76
- const server = new ClaudeCodeServer();
77
- return { server, module, handlers };
78
- }
79
- describe('run tool with PID return', () => {
80
- it('should return PID immediately when starting a process', async () => {
81
- const { handlers } = await setupServer();
82
- // Create a mock process
83
- const mockProcess = new EventEmitter();
84
- mockProcess.pid = 12345;
85
- mockProcess.stdout = new EventEmitter();
86
- mockProcess.stderr = new EventEmitter();
87
- mockProcess.kill = vi.fn();
88
- mockSpawn.mockReturnValue(mockProcess);
89
- const callToolHandler = handlers.get('callTool');
90
- const result = await callToolHandler({
91
- params: {
92
- name: 'run',
93
- arguments: {
94
- prompt: 'test prompt',
95
- workFolder: '/tmp'
96
- }
97
- }
98
- });
99
- const response = JSON.parse(result.content[0].text);
100
- expect(response.pid).toBe(12345);
101
- expect(response.status).toBe('started');
102
- expect(response.message).toBe('claude process started successfully');
103
- });
104
- it('should peek only natural-language messages observed after registration', async () => {
105
- const { handlers } = await setupServer();
106
- const mockProcess = new EventEmitter();
107
- mockProcess.pid = 12345;
108
- mockProcess.stdout = new EventEmitter();
109
- mockProcess.stderr = new EventEmitter();
110
- mockProcess.kill = vi.fn();
111
- mockSpawn.mockReturnValue(mockProcess);
112
- const callToolHandler = handlers.get('callTool');
113
- await callToolHandler({
114
- params: {
115
- name: 'run',
116
- arguments: {
117
- prompt: 'test prompt',
118
- workFolder: '/tmp'
119
- }
120
- }
121
- });
122
- mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"text","text":"old message"}]}}\n');
123
- const peekPromise = callToolHandler({
124
- params: {
125
- name: 'peek',
126
- arguments: {
127
- pids: [12345, 12345, 99999],
128
- peek_time_sec: 1,
129
- }
130
- }
131
- });
132
- setTimeout(() => {
133
- mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"text","text":"new message"},{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/a"}}]}}\n');
134
- mockProcess.stdout.emit('data', '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":"secret"}]}}\n');
135
- mockProcess.emit('close', 0);
136
- }, 10);
137
- const result = await peekPromise;
138
- const response = JSON.parse(result.content[0].text);
139
- expect(response.processes).toHaveLength(2);
140
- expect(response.processes[0]).toMatchObject({
141
- pid: 12345,
142
- agent: 'claude',
143
- status: 'completed',
144
- events: [
145
- {
146
- kind: 'message',
147
- ts: expect.any(String),
148
- text: 'new message',
149
- },
150
- ],
151
- truncated: false,
152
- error: null,
153
- });
154
- expect(response.processes[1]).toEqual({
155
- pid: 99999,
156
- agent: null,
157
- status: 'not_found',
158
- events: [],
159
- truncated: false,
160
- error: 'process not found',
161
- });
162
- });
163
- it('should peek OpenCode text events and exclude OpenCode tool output', async () => {
164
- const { handlers } = await setupServer();
165
- const mockProcess = new EventEmitter();
166
- mockProcess.pid = 12346;
167
- mockProcess.stdout = new EventEmitter();
168
- mockProcess.stderr = new EventEmitter();
169
- mockProcess.kill = vi.fn();
170
- mockSpawn.mockReturnValue(mockProcess);
171
- const callToolHandler = handlers.get('callTool');
172
- await callToolHandler({
173
- params: {
174
- name: 'run',
175
- arguments: {
176
- prompt: 'opencode peek prompt',
177
- workFolder: '/tmp',
178
- model: 'opencode',
179
- }
180
- }
181
- });
182
- const peekPromise = callToolHandler({
183
- params: {
184
- name: 'peek',
185
- arguments: {
186
- pids: [12346],
187
- peek_time_sec: 1,
188
- }
189
- }
190
- });
191
- setTimeout(() => {
192
- mockProcess.stdout.emit('data', '{"type":"text","timestamp":1775918783605,"sessionID":"ses-1","part":{"type":"text","text":"OpenCode visible text"}}\n');
193
- mockProcess.stdout.emit('data', '{"type":"tool_use","timestamp":1775918783606,"sessionID":"ses-1","part":{"type":"tool","state":{"output":"secret command output"},"metadata":{"output":"secret metadata output"}}}\n');
194
- mockProcess.emit('close', 0);
195
- }, 10);
196
- const result = await peekPromise;
197
- const response = JSON.parse(result.content[0].text);
198
- expect(response.processes).toHaveLength(1);
199
- expect(response.processes[0]).toMatchObject({
200
- pid: 12346,
201
- agent: 'opencode',
202
- status: 'completed',
203
- events: [
204
- {
205
- kind: 'message',
206
- ts: expect.any(String),
207
- text: 'OpenCode visible text',
208
- },
209
- ],
210
- truncated: false,
211
- error: null,
212
- });
213
- });
214
- it('should peek Gemini assistant message events and exclude tool output', async () => {
215
- const { handlers } = await setupServer();
216
- const mockProcess = new EventEmitter();
217
- mockProcess.pid = 12347;
218
- mockProcess.stdout = new EventEmitter();
219
- mockProcess.stderr = new EventEmitter();
220
- mockProcess.kill = vi.fn();
221
- mockSpawn.mockReturnValue(mockProcess);
222
- const callToolHandler = handlers.get('callTool');
223
- await callToolHandler({
224
- params: {
225
- name: 'run',
226
- arguments: {
227
- prompt: 'gemini peek prompt',
228
- workFolder: '/tmp',
229
- model: 'gemini-2.5-pro',
230
- }
231
- }
232
- });
233
- const peekPromise = callToolHandler({
234
- params: {
235
- name: 'peek',
236
- arguments: {
237
- pids: [12347],
238
- peek_time_sec: 1,
239
- }
240
- }
241
- });
242
- setTimeout(() => {
243
- mockProcess.stdout.emit('data', '{"type":"message","timestamp":"2026-04-11T14:44:42.294Z","role":"user","content":"hidden user text"}\n');
244
- mockProcess.stdout.emit('data', '{"type":"message","timestamp":"2026-04-11T14:44:53.820Z","role":"assistant","content":"Visible Gemini text","delta":true}\n');
245
- mockProcess.stdout.emit('data', '{"type":"tool_result","timestamp":"2026-04-11T14:45:03.011Z","status":"success","output":"secret command output"}\n');
246
- mockProcess.emit('close', 0);
247
- }, 10);
248
- const result = await peekPromise;
249
- const response = JSON.parse(result.content[0].text);
250
- expect(response.processes).toHaveLength(1);
251
- expect(response.processes[0]).toMatchObject({
252
- pid: 12347,
253
- agent: 'gemini',
254
- status: 'completed',
255
- events: [
256
- {
257
- kind: 'message',
258
- ts: expect.any(String),
259
- text: 'Visible Gemini text',
260
- },
261
- ],
262
- truncated: false,
263
- error: null,
264
- });
265
- });
266
- it('should include normalized tool_call events when requested', async () => {
267
- const { handlers } = await setupServer();
268
- const mockProcess = new EventEmitter();
269
- mockProcess.pid = 12348;
270
- mockProcess.stdout = new EventEmitter();
271
- mockProcess.stderr = new EventEmitter();
272
- mockProcess.kill = vi.fn();
273
- mockSpawn.mockReturnValue(mockProcess);
274
- const callToolHandler = handlers.get('callTool');
275
- await callToolHandler({
276
- params: {
277
- name: 'run',
278
- arguments: {
279
- prompt: 'claude mcp peek prompt',
280
- workFolder: '/tmp',
281
- model: 'haiku',
282
- }
283
- }
284
- });
285
- const peekPromise = callToolHandler({
286
- params: {
287
- name: 'peek',
288
- arguments: {
289
- pids: [12348],
290
- peek_time_sec: 1,
291
- include_tool_calls: true,
292
- }
293
- }
294
- });
295
- setTimeout(() => {
296
- mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"mcp__acm__list_processes","input":{}}]}}\n');
297
- mockProcess.stdout.emit('data', '{"type":"user","message":{"content":[{"tool_use_id":"toolu_1","type":"tool_result","content":[{"type":"text","text":"secret result"}]}]}}\n');
298
- mockProcess.stdout.emit('data', '{"type":"assistant","message":{"content":[{"type":"text","text":"MCP succeeded."}]}}\n');
299
- mockProcess.emit('close', 0);
300
- }, 10);
301
- const result = await peekPromise;
302
- const response = JSON.parse(result.content[0].text);
303
- expect(response.processes).toHaveLength(1);
304
- expect(response.processes[0]).toMatchObject({
305
- pid: 12348,
306
- agent: 'claude',
307
- status: 'completed',
308
- events: [
309
- {
310
- kind: 'tool_call',
311
- phase: 'started',
312
- id: 'toolu_1',
313
- tool: 'mcp__acm__list_processes',
314
- server: 'acm',
315
- summary: 'acm.list_processes',
316
- },
317
- {
318
- kind: 'tool_call',
319
- phase: 'completed',
320
- id: 'toolu_1',
321
- tool: 'mcp__acm__list_processes',
322
- server: 'acm',
323
- summary: 'acm.list_processes',
324
- status: 'success',
325
- },
326
- {
327
- kind: 'message',
328
- ts: expect.any(String),
329
- text: 'MCP succeeded.',
330
- },
331
- ],
332
- truncated: false,
333
- error: null,
334
- });
335
- expect(JSON.stringify(response)).not.toContain('secret result');
336
- });
337
- it('should handle process with model parameter', async () => {
338
- const { handlers } = await setupServer();
339
- const mockProcess = new EventEmitter();
340
- mockProcess.pid = 12346;
341
- mockProcess.stdout = new EventEmitter();
342
- mockProcess.stderr = new EventEmitter();
343
- mockProcess.kill = vi.fn();
344
- mockSpawn.mockReturnValue(mockProcess);
345
- const callToolHandler = handlers.get('callTool');
346
- await callToolHandler({
347
- params: {
348
- name: 'run',
349
- arguments: {
350
- prompt: 'test prompt',
351
- workFolder: '/tmp',
352
- model: 'opus'
353
- }
354
- }
355
- });
356
- expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['--model', 'opus']), expect.any(Object));
357
- });
358
- it('should handle Japanese prompts with newlines', async () => {
359
- const { handlers } = await setupServer();
360
- const mockProcess = new EventEmitter();
361
- mockProcess.pid = 12360;
362
- mockProcess.stdout = new EventEmitter();
363
- mockProcess.stderr = new EventEmitter();
364
- mockProcess.kill = vi.fn();
365
- mockSpawn.mockReturnValue(mockProcess);
366
- const japanesePrompt = `日本語のテストプロンプトです。
367
- これは改行を含んでいます。
368
- さらに、特殊文字も含みます:「こんにちは」、『世界』
369
- 最後の行です。`;
370
- const callToolHandler = handlers.get('callTool');
371
- const result = await callToolHandler({
372
- params: {
373
- name: 'run',
374
- arguments: {
375
- prompt: japanesePrompt,
376
- workFolder: '/tmp'
377
- }
378
- }
379
- });
380
- // Verify PID is returned
381
- const response = JSON.parse(result.content[0].text);
382
- expect(response.pid).toBe(12360);
383
- // Verify spawn was called with the correct prompt including newlines
384
- expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['-p', japanesePrompt]), expect.any(Object));
385
- // Verify the prompt is stored correctly in process manager
386
- const getResult = await callToolHandler({
387
- params: {
388
- name: 'get_result',
389
- arguments: {
390
- pid: 12360,
391
- verbose: true
392
- }
393
- }
394
- });
395
- const processInfo = JSON.parse(getResult.content[0].text);
396
- expect(processInfo.prompt).toBe(japanesePrompt);
397
- });
398
- it('should handle very long Japanese prompts with multiple paragraphs', async () => {
399
- const { handlers } = await setupServer();
400
- const mockProcess = new EventEmitter();
401
- mockProcess.pid = 12361;
402
- mockProcess.stdout = new EventEmitter();
403
- mockProcess.stderr = new EventEmitter();
404
- mockProcess.kill = vi.fn();
405
- mockSpawn.mockReturnValue(mockProcess);
406
- const longJapanesePrompt = `# タスク:ファイル管理システムの作成
407
-
408
- 以下の要件に従って、ファイル管理システムを作成してください:
409
-
410
- 1. **基本機能**
411
- - ファイルの作成、読み取り、更新、削除(CRUD)
412
- - ディレクトリの作成と管理
413
- - ファイルの検索機能
414
-
415
- 2. **追加機能**
416
- - ファイルのバージョン管理
417
- - アクセス権限の設定
418
- - ログ記録機能
419
-
420
- 3. **技術要件**
421
- - TypeScriptを使用
422
- - テストコードを含める
423
- - ドキュメントを日本語で作成
424
-
425
- 注意事項:
426
- - エラーハンドリングを適切に行う
427
- - パフォーマンスを考慮した実装
428
- - セキュリティに配慮すること
429
-
430
- よろしくお願いします。`;
431
- const callToolHandler = handlers.get('callTool');
432
- const result = await callToolHandler({
433
- params: {
434
- name: 'run',
435
- arguments: {
436
- prompt: longJapanesePrompt,
437
- workFolder: '/tmp',
438
- model: 'sonnet'
439
- }
440
- }
441
- });
442
- // Verify PID is returned
443
- const response = JSON.parse(result.content[0].text);
444
- expect(response.pid).toBe(12361);
445
- // Verify spawn was called with the complete long prompt
446
- expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['-p', longJapanesePrompt]), expect.any(Object));
447
- // Verify list_processes returns basic info
448
- const listResult = await callToolHandler({
449
- params: {
450
- name: 'list_processes',
451
- arguments: {}
452
- }
453
- });
454
- const processes = JSON.parse(listResult.content[0].text);
455
- const process = processes.find((p) => p.pid === 12361);
456
- expect(process.pid).toBe(12361);
457
- expect(process.agent).toBe('claude');
458
- expect(process.status).toBe('running');
459
- });
460
- it('should handle prompts with special characters and escape sequences', async () => {
461
- const { handlers } = await setupServer();
462
- const mockProcess = new EventEmitter();
463
- mockProcess.pid = 12362;
464
- mockProcess.stdout = new EventEmitter();
465
- mockProcess.stderr = new EventEmitter();
466
- mockProcess.kill = vi.fn();
467
- mockSpawn.mockReturnValue(mockProcess);
468
- // Test with various special characters
469
- const specialPrompt = `特殊文字のテスト:
470
- \t- タブ文字
471
- \n- 明示的な改行
472
- "ダブルクォート" と 'シングルクォート'
473
- バックスラッシュ: \\
474
- Unicodeテスト: 🎌 🗾 ✨
475
- 環境変数風: $HOME と \${USER}`;
476
- const callToolHandler = handlers.get('callTool');
477
- const result = await callToolHandler({
478
- params: {
479
- name: 'run',
480
- arguments: {
481
- prompt: specialPrompt,
482
- workFolder: '/tmp'
483
- }
484
- }
485
- });
486
- // Verify PID is returned
487
- const response = JSON.parse(result.content[0].text);
488
- expect(response.pid).toBe(12362);
489
- // Verify spawn was called with the special characters intact
490
- expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['-p', specialPrompt]), expect.any(Object));
491
- });
492
- it('should throw error if process fails to start', async () => {
493
- const { handlers } = await setupServer();
494
- const mockProcess = new EventEmitter();
495
- mockProcess.pid = undefined; // No PID means process failed to start
496
- mockProcess.stdout = new EventEmitter();
497
- mockProcess.stderr = new EventEmitter();
498
- mockSpawn.mockReturnValue(mockProcess);
499
- const callToolHandler = handlers.get('callTool');
500
- try {
501
- await callToolHandler({
502
- params: {
503
- name: 'run',
504
- arguments: {
505
- prompt: 'test prompt',
506
- workFolder: '/tmp/test'
507
- }
508
- }
509
- });
510
- expect.fail('Should have thrown');
511
- }
512
- catch (error) {
513
- expect(error.message).toContain('Failed to start claude CLI process');
514
- expect(error.code).toBe('InternalError');
515
- }
516
- });
517
- });
518
- describe('list_processes tool', () => {
519
- it('should list all processes', async () => {
520
- const { handlers } = await setupServer();
521
- // Start a process first
522
- const mockProcess = new EventEmitter();
523
- mockProcess.pid = 12347;
524
- mockProcess.stdout = new EventEmitter();
525
- mockProcess.stderr = new EventEmitter();
526
- mockProcess.kill = vi.fn();
527
- mockSpawn.mockReturnValue(mockProcess);
528
- const callToolHandler = handlers.get('callTool');
529
- // Start a process
530
- await callToolHandler({
531
- params: {
532
- name: 'run',
533
- arguments: {
534
- prompt: 'test prompt for listing',
535
- workFolder: '/tmp',
536
- model: 'sonnet'
537
- }
538
- }
539
- });
540
- // Simulate JSON output with session_id
541
- const jsonOutput = {
542
- session_id: 'list-test-session-789',
543
- status: 'running'
544
- };
545
- mockProcess.stdout.emit('data', JSON.stringify(jsonOutput));
546
- // List processes
547
- const listResult = await callToolHandler({
548
- params: {
549
- name: 'list_processes',
550
- arguments: {}
551
- }
552
- });
553
- const processes = JSON.parse(listResult.content[0].text);
554
- expect(processes).toHaveLength(1);
555
- expect(processes[0].pid).toBe(12347);
556
- expect(processes[0].status).toBe('running');
557
- expect(processes[0].agent).toBe('claude');
558
- });
559
- it('should list process with correct agent type', async () => {
560
- const { handlers } = await setupServer();
561
- const mockProcess = new EventEmitter();
562
- mockProcess.pid = 12348;
563
- mockProcess.stdout = new EventEmitter();
564
- mockProcess.stderr = new EventEmitter();
565
- mockProcess.kill = vi.fn();
566
- mockSpawn.mockReturnValue(mockProcess);
567
- const callToolHandler = handlers.get('callTool');
568
- // Start a process
569
- await callToolHandler({
570
- params: {
571
- name: 'run',
572
- arguments: {
573
- prompt: 'test prompt',
574
- workFolder: '/tmp'
575
- }
576
- }
577
- });
578
- // List processes
579
- const listResult = await callToolHandler({
580
- params: {
581
- name: 'list_processes',
582
- arguments: {}
583
- }
584
- });
585
- const processes = JSON.parse(listResult.content[0].text);
586
- expect(processes[0].pid).toBe(12348);
587
- expect(processes[0].agent).toBe('claude');
588
- expect(processes[0].status).toBe('running');
589
- });
590
- });
591
- describe('get_result tool', () => {
592
- it('should get process output', async () => {
593
- const { handlers } = await setupServer();
594
- const mockProcess = new EventEmitter();
595
- mockProcess.pid = 12349;
596
- mockProcess.stdout = new EventEmitter();
597
- mockProcess.stderr = new EventEmitter();
598
- mockProcess.kill = vi.fn();
599
- mockSpawn.mockReturnValue(mockProcess);
600
- const callToolHandler = handlers.get('callTool');
601
- // Start a process
602
- await callToolHandler({
603
- params: {
604
- name: 'run',
605
- arguments: {
606
- prompt: 'test prompt',
607
- workFolder: '/tmp'
608
- }
609
- }
610
- });
611
- // Simulate JSON output from Claude CLI
612
- const claudeJsonOutput = {
613
- session_id: 'test-session-123',
614
- status: 'success',
615
- message: 'Task completed'
616
- };
617
- mockProcess.stdout.emit('data', JSON.stringify(claudeJsonOutput));
618
- mockProcess.stderr.emit('data', 'Warning from stderr\n');
619
- // Get result
620
- const result = await callToolHandler({
621
- params: {
622
- name: 'get_result',
623
- arguments: {
624
- pid: 12349
625
- }
626
- }
627
- });
628
- const processInfo = JSON.parse(result.content[0].text);
629
- expect(processInfo.pid).toBe(12349);
630
- expect(processInfo.status).toBe('running');
631
- expect(processInfo.agentOutput).toEqual(claudeJsonOutput);
632
- expect(processInfo.session_id).toBe('test-session-123');
633
- });
634
- it('should show completed status when process exits', async () => {
635
- const { handlers } = await setupServer();
636
- const mockProcess = new EventEmitter();
637
- mockProcess.pid = 12350;
638
- mockProcess.stdout = new EventEmitter();
639
- mockProcess.stderr = new EventEmitter();
640
- mockProcess.kill = vi.fn();
641
- mockSpawn.mockReturnValue(mockProcess);
642
- const callToolHandler = handlers.get('callTool');
643
- // Start a process
644
- await callToolHandler({
645
- params: {
646
- name: 'run',
647
- arguments: {
648
- prompt: 'test prompt',
649
- workFolder: '/tmp'
650
- }
651
- }
652
- });
653
- // Simulate process completion with JSON output
654
- const completedJsonOutput = {
655
- session_id: 'completed-session-456',
656
- status: 'completed',
657
- files_created: ['test.txt'],
658
- summary: 'Created test file successfully'
659
- };
660
- mockProcess.stdout.emit('data', JSON.stringify(completedJsonOutput));
661
- mockProcess.emit('close', 0);
662
- // Get result
663
- const result = await callToolHandler({
664
- params: {
665
- name: 'get_result',
666
- arguments: {
667
- pid: 12350
668
- }
669
- }
670
- });
671
- const processInfo = JSON.parse(result.content[0].text);
672
- expect(processInfo.status).toBe('completed');
673
- expect(processInfo.exitCode).toBe(0);
674
- expect(processInfo.agentOutput).toEqual(completedJsonOutput);
675
- expect(processInfo.session_id).toBe('completed-session-456');
676
- });
677
- it('should throw error for non-existent PID', async () => {
678
- const { handlers } = await setupServer();
679
- const callToolHandler = handlers.get('callTool');
680
- await expect(callToolHandler({
681
- params: {
682
- name: 'get_result',
683
- arguments: {
684
- pid: 99999
685
- }
686
- }
687
- })).rejects.toThrow('Process with PID 99999 not found');
688
- });
689
- it('should throw error for invalid PID parameter', async () => {
690
- const { handlers } = await setupServer();
691
- const callToolHandler = handlers.get('callTool');
692
- await expect(callToolHandler({
693
- params: {
694
- name: 'get_result',
695
- arguments: {
696
- pid: 'not-a-number'
697
- }
698
- }
699
- })).rejects.toThrow('Missing or invalid required parameter: pid');
700
- });
701
- it('should handle non-JSON output gracefully', async () => {
702
- const { handlers } = await setupServer();
703
- const mockProcess = new EventEmitter();
704
- mockProcess.pid = 12355;
705
- mockProcess.stdout = new EventEmitter();
706
- mockProcess.stderr = new EventEmitter();
707
- mockProcess.kill = vi.fn();
708
- mockSpawn.mockReturnValue(mockProcess);
709
- const callToolHandler = handlers.get('callTool');
710
- // Start a process
711
- await callToolHandler({
712
- params: {
713
- name: 'run',
714
- arguments: {
715
- prompt: 'test prompt',
716
- workFolder: '/tmp'
717
- }
718
- }
719
- });
720
- // Simulate non-JSON output
721
- mockProcess.stdout.emit('data', 'This is plain text output, not JSON');
722
- mockProcess.stderr.emit('data', 'Some error occurred');
723
- // Get result
724
- const result = await callToolHandler({
725
- params: {
726
- name: 'get_result',
727
- arguments: {
728
- pid: 12355
729
- }
730
- }
731
- });
732
- const processInfo = JSON.parse(result.content[0].text);
733
- expect(processInfo.pid).toBe(12355);
734
- expect(processInfo.status).toBe('running');
735
- expect(processInfo.stdout).toBe('This is plain text output, not JSON');
736
- expect(processInfo.stderr).toBe('Some error occurred');
737
- expect(processInfo.claudeOutput).toBeUndefined();
738
- expect(processInfo.session_id).toBeUndefined();
739
- });
740
- });
741
- describe('kill_process tool', () => {
742
- it('should kill a running process', async () => {
743
- const { handlers } = await setupServer();
744
- const mockProcess = new EventEmitter();
745
- mockProcess.pid = 12351;
746
- mockProcess.stdout = new EventEmitter();
747
- mockProcess.stderr = new EventEmitter();
748
- mockProcess.kill = vi.fn();
749
- mockSpawn.mockReturnValue(mockProcess);
750
- const callToolHandler = handlers.get('callTool');
751
- // Start a process
752
- await callToolHandler({
753
- params: {
754
- name: 'run',
755
- arguments: {
756
- prompt: 'test prompt',
757
- workFolder: '/tmp'
758
- }
759
- }
760
- });
761
- // Kill the process
762
- const killResult = await callToolHandler({
763
- params: {
764
- name: 'kill_process',
765
- arguments: {
766
- pid: 12351
767
- }
768
- }
769
- });
770
- const response = JSON.parse(killResult.content[0].text);
771
- expect(response.status).toBe('terminated');
772
- expect(response.message).toBe('Process terminated successfully');
773
- expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM');
774
- });
775
- it('should handle already terminated process', async () => {
776
- const { handlers } = await setupServer();
777
- const mockProcess = new EventEmitter();
778
- mockProcess.pid = 12352;
779
- mockProcess.stdout = new EventEmitter();
780
- mockProcess.stderr = new EventEmitter();
781
- mockProcess.kill = vi.fn();
782
- mockSpawn.mockReturnValue(mockProcess);
783
- const callToolHandler = handlers.get('callTool');
784
- // Start and complete a process
785
- await callToolHandler({
786
- params: {
787
- name: 'run',
788
- arguments: {
789
- prompt: 'test prompt',
790
- workFolder: '/tmp'
791
- }
792
- }
793
- });
794
- // Simulate process completion
795
- mockProcess.emit('close', 0);
796
- // Try to kill the already completed process
797
- const killResult = await callToolHandler({
798
- params: {
799
- name: 'kill_process',
800
- arguments: {
801
- pid: 12352
802
- }
803
- }
804
- });
805
- const response = JSON.parse(killResult.content[0].text);
806
- expect(response.status).toBe('completed');
807
- expect(response.message).toBe('Process already terminated');
808
- expect(mockProcess.kill).not.toHaveBeenCalled();
809
- });
810
- it('should throw error for non-existent PID', async () => {
811
- const { handlers } = await setupServer();
812
- const callToolHandler = handlers.get('callTool');
813
- await expect(callToolHandler({
814
- params: {
815
- name: 'kill_process',
816
- arguments: {
817
- pid: 99999
818
- }
819
- }
820
- })).rejects.toThrow('Process with PID 99999 not found');
821
- });
822
- });
823
- describe('Tool routing', () => {
824
- it('should throw error for unknown tool', async () => {
825
- const { handlers } = await setupServer();
826
- const callToolHandler = handlers.get('callTool');
827
- await expect(callToolHandler({
828
- params: {
829
- name: 'unknown_tool',
830
- arguments: {}
831
- }
832
- })).rejects.toThrow('Tool unknown_tool not found');
833
- });
834
- });
835
- describe('Process error handling', () => {
836
- it('should handle process errors', async () => {
837
- const { handlers } = await setupServer();
838
- const mockProcess = new EventEmitter();
839
- mockProcess.pid = 12353;
840
- mockProcess.stdout = new EventEmitter();
841
- mockProcess.stderr = new EventEmitter();
842
- mockProcess.kill = vi.fn();
843
- mockSpawn.mockReturnValue(mockProcess);
844
- const callToolHandler = handlers.get('callTool');
845
- // Start a process
846
- await callToolHandler({
847
- params: {
848
- name: 'run',
849
- arguments: {
850
- prompt: 'test prompt',
851
- workFolder: '/tmp'
852
- }
853
- }
854
- });
855
- // Simulate process error
856
- mockProcess.emit('error', new Error('spawn error'));
857
- // Get result to check error was recorded
858
- const result = await callToolHandler({
859
- params: {
860
- name: 'get_result',
861
- arguments: {
862
- pid: 12353
863
- }
864
- }
865
- });
866
- const processInfo = JSON.parse(result.content[0].text);
867
- expect(processInfo.status).toBe('failed');
868
- expect(processInfo.stderr).toContain('Process error: spawn error');
869
- });
870
- });
871
- });