ai-cli-mcp 2.19.0 → 2.20.1

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 (100) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +34 -8
  3. package/README.md +41 -8
  4. package/dist/app/cli.js +1 -0
  5. package/dist/app/mcp.js +64 -12
  6. package/dist/cli-builder.js +13 -6
  7. package/dist/cli-process-service.js +76 -91
  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 +8 -2
  12. package/package.json +27 -3
  13. package/server.json +3 -3
  14. package/.gemini/settings.json +0 -11
  15. package/.github/dependabot.yml +0 -28
  16. package/.github/pull_request_template.md +0 -28
  17. package/.github/workflows/ci.yml +0 -34
  18. package/.github/workflows/dependency-review.yml +0 -22
  19. package/.github/workflows/publish.yml +0 -89
  20. package/.github/workflows/test.yml +0 -20
  21. package/.github/workflows/watch-session-prs.yml +0 -276
  22. package/.husky/pre-commit +0 -1
  23. package/.mcp.json +0 -11
  24. package/.releaserc.json +0 -18
  25. package/.vscode/settings.json +0 -3
  26. package/CONTRIBUTING.md +0 -81
  27. package/dist/__tests__/app-cli.test.js +0 -392
  28. package/dist/__tests__/cli-bin-smoke.test.js +0 -101
  29. package/dist/__tests__/cli-builder.test.js +0 -442
  30. package/dist/__tests__/cli-process-service.test.js +0 -655
  31. package/dist/__tests__/cli-utils.test.js +0 -171
  32. package/dist/__tests__/e2e.test.js +0 -256
  33. package/dist/__tests__/edge-cases.test.js +0 -130
  34. package/dist/__tests__/error-cases.test.js +0 -292
  35. package/dist/__tests__/mcp-contract.test.js +0 -636
  36. package/dist/__tests__/mocks.js +0 -32
  37. package/dist/__tests__/model-alias.test.js +0 -36
  38. package/dist/__tests__/parsers.test.js +0 -646
  39. package/dist/__tests__/peek.test.js +0 -36
  40. package/dist/__tests__/process-management.test.js +0 -949
  41. package/dist/__tests__/server.test.js +0 -809
  42. package/dist/__tests__/setup.js +0 -11
  43. package/dist/__tests__/utils/claude-mock.js +0 -80
  44. package/dist/__tests__/utils/mcp-client.js +0 -121
  45. package/dist/__tests__/utils/opencode-mock.js +0 -91
  46. package/dist/__tests__/utils/persistent-mock.js +0 -28
  47. package/dist/__tests__/utils/test-helpers.js +0 -11
  48. package/dist/__tests__/validation.test.js +0 -308
  49. package/dist/__tests__/version-print.test.js +0 -65
  50. package/dist/__tests__/wait.test.js +0 -260
  51. package/docs/RELEASE_CHECKLIST.md +0 -65
  52. package/docs/cli-architecture.md +0 -275
  53. package/docs/concept.md +0 -154
  54. package/docs/development.md +0 -156
  55. package/docs/e2e-testing.md +0 -148
  56. package/docs/prd.md +0 -146
  57. package/docs/session-stacking.md +0 -67
  58. package/src/__tests__/app-cli.test.ts +0 -495
  59. package/src/__tests__/cli-bin-smoke.test.ts +0 -136
  60. package/src/__tests__/cli-builder.test.ts +0 -549
  61. package/src/__tests__/cli-process-service.test.ts +0 -759
  62. package/src/__tests__/cli-utils.test.ts +0 -200
  63. package/src/__tests__/e2e.test.ts +0 -311
  64. package/src/__tests__/edge-cases.test.ts +0 -176
  65. package/src/__tests__/error-cases.test.ts +0 -370
  66. package/src/__tests__/mcp-contract.test.ts +0 -755
  67. package/src/__tests__/mocks.ts +0 -35
  68. package/src/__tests__/model-alias.test.ts +0 -44
  69. package/src/__tests__/parsers.test.ts +0 -730
  70. package/src/__tests__/peek.test.ts +0 -44
  71. package/src/__tests__/process-management.test.ts +0 -1129
  72. package/src/__tests__/server.test.ts +0 -1020
  73. package/src/__tests__/setup.ts +0 -13
  74. package/src/__tests__/utils/claude-mock.ts +0 -87
  75. package/src/__tests__/utils/mcp-client.ts +0 -159
  76. package/src/__tests__/utils/opencode-mock.ts +0 -108
  77. package/src/__tests__/utils/persistent-mock.ts +0 -33
  78. package/src/__tests__/utils/test-helpers.ts +0 -13
  79. package/src/__tests__/validation.test.ts +0 -369
  80. package/src/__tests__/version-print.test.ts +0 -81
  81. package/src/__tests__/wait.test.ts +0 -302
  82. package/src/app/cli.ts +0 -424
  83. package/src/app/mcp.ts +0 -466
  84. package/src/bin/ai-cli-mcp.ts +0 -7
  85. package/src/bin/ai-cli.ts +0 -11
  86. package/src/cli-builder.ts +0 -274
  87. package/src/cli-parse.ts +0 -105
  88. package/src/cli-process-service.ts +0 -709
  89. package/src/cli-utils.ts +0 -258
  90. package/src/cli.ts +0 -124
  91. package/src/model-catalog.ts +0 -87
  92. package/src/parsers.ts +0 -965
  93. package/src/peek.ts +0 -95
  94. package/src/process-result.ts +0 -88
  95. package/src/process-service.ts +0 -368
  96. package/src/server.ts +0 -10
  97. package/tsconfig.json +0 -16
  98. package/vitest.config.e2e.ts +0 -27
  99. package/vitest.config.ts +0 -22
  100. package/vitest.config.unit.ts +0 -28
@@ -1,949 +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 peek Forge plain-text messages and low-precision tool calls without raw command output', async () => {
338
- const { handlers } = await setupServer();
339
- const mockProcess = new EventEmitter();
340
- mockProcess.pid = 12349;
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: 'forge peek prompt',
351
- workFolder: '/tmp',
352
- model: 'forge',
353
- }
354
- }
355
- });
356
- const peekPromise = callToolHandler({
357
- params: {
358
- name: 'peek',
359
- arguments: {
360
- pids: [12349],
361
- peek_time_sec: 1,
362
- include_tool_calls: true,
363
- }
364
- }
365
- });
366
- setTimeout(() => {
367
- mockProcess.stdout.emit('data', 'Summary: Forge started\n');
368
- mockProcess.stdout.emit('data', "● [11:28:40] Execute [/bin/zsh] /bin/sh -c 'echo hi'\n");
369
- mockProcess.stdout.emit('data', 'secret child output\n');
370
- mockProcess.stderr.emit('data', 'Summary: stderr should be ignored\n');
371
- mockProcess.stdout.emit('data', '● [11:28:41] Finished abc123\n');
372
- mockProcess.stdout.emit('data', 'Completed successfully: Forge done\n');
373
- mockProcess.emit('close', 0);
374
- }, 10);
375
- const result = await peekPromise;
376
- const response = JSON.parse(result.content[0].text);
377
- expect(response.processes).toHaveLength(1);
378
- expect(response.processes[0]).toMatchObject({
379
- pid: 12349,
380
- agent: 'forge',
381
- status: 'completed',
382
- events: [
383
- {
384
- kind: 'message',
385
- ts: expect.any(String),
386
- text: 'Forge started',
387
- },
388
- {
389
- kind: 'tool_call',
390
- phase: 'started',
391
- id: 'forge_0',
392
- tool: '/bin/zsh',
393
- summary: "/bin/sh -c 'echo hi'",
394
- },
395
- {
396
- kind: 'tool_call',
397
- phase: 'completed',
398
- id: 'forge_0',
399
- tool: '/bin/zsh',
400
- summary: "/bin/sh -c 'echo hi'",
401
- status: 'unknown',
402
- },
403
- {
404
- kind: 'message',
405
- ts: expect.any(String),
406
- text: 'Forge done',
407
- },
408
- ],
409
- truncated: false,
410
- error: null,
411
- });
412
- expect(JSON.stringify(response)).not.toContain('secret child output');
413
- expect(JSON.stringify(response)).not.toContain('stderr should be ignored');
414
- });
415
- it('should handle process with model parameter', async () => {
416
- const { handlers } = await setupServer();
417
- const mockProcess = new EventEmitter();
418
- mockProcess.pid = 12346;
419
- mockProcess.stdout = new EventEmitter();
420
- mockProcess.stderr = new EventEmitter();
421
- mockProcess.kill = vi.fn();
422
- mockSpawn.mockReturnValue(mockProcess);
423
- const callToolHandler = handlers.get('callTool');
424
- await callToolHandler({
425
- params: {
426
- name: 'run',
427
- arguments: {
428
- prompt: 'test prompt',
429
- workFolder: '/tmp',
430
- model: 'opus'
431
- }
432
- }
433
- });
434
- expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['--model', 'opus']), expect.any(Object));
435
- });
436
- it('should handle Japanese prompts with newlines', async () => {
437
- const { handlers } = await setupServer();
438
- const mockProcess = new EventEmitter();
439
- mockProcess.pid = 12360;
440
- mockProcess.stdout = new EventEmitter();
441
- mockProcess.stderr = new EventEmitter();
442
- mockProcess.kill = vi.fn();
443
- mockSpawn.mockReturnValue(mockProcess);
444
- const japanesePrompt = `日本語のテストプロンプトです。
445
- これは改行を含んでいます。
446
- さらに、特殊文字も含みます:「こんにちは」、『世界』
447
- 最後の行です。`;
448
- const callToolHandler = handlers.get('callTool');
449
- const result = await callToolHandler({
450
- params: {
451
- name: 'run',
452
- arguments: {
453
- prompt: japanesePrompt,
454
- workFolder: '/tmp'
455
- }
456
- }
457
- });
458
- // Verify PID is returned
459
- const response = JSON.parse(result.content[0].text);
460
- expect(response.pid).toBe(12360);
461
- // Verify spawn was called with the correct prompt including newlines
462
- expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['-p', japanesePrompt]), expect.any(Object));
463
- // Verify the prompt is stored correctly in process manager
464
- const getResult = await callToolHandler({
465
- params: {
466
- name: 'get_result',
467
- arguments: {
468
- pid: 12360,
469
- verbose: true
470
- }
471
- }
472
- });
473
- const processInfo = JSON.parse(getResult.content[0].text);
474
- expect(processInfo.prompt).toBe(japanesePrompt);
475
- });
476
- it('should handle very long Japanese prompts with multiple paragraphs', async () => {
477
- const { handlers } = await setupServer();
478
- const mockProcess = new EventEmitter();
479
- mockProcess.pid = 12361;
480
- mockProcess.stdout = new EventEmitter();
481
- mockProcess.stderr = new EventEmitter();
482
- mockProcess.kill = vi.fn();
483
- mockSpawn.mockReturnValue(mockProcess);
484
- const longJapanesePrompt = `# タスク:ファイル管理システムの作成
485
-
486
- 以下の要件に従って、ファイル管理システムを作成してください:
487
-
488
- 1. **基本機能**
489
- - ファイルの作成、読み取り、更新、削除(CRUD)
490
- - ディレクトリの作成と管理
491
- - ファイルの検索機能
492
-
493
- 2. **追加機能**
494
- - ファイルのバージョン管理
495
- - アクセス権限の設定
496
- - ログ記録機能
497
-
498
- 3. **技術要件**
499
- - TypeScriptを使用
500
- - テストコードを含める
501
- - ドキュメントを日本語で作成
502
-
503
- 注意事項:
504
- - エラーハンドリングを適切に行う
505
- - パフォーマンスを考慮した実装
506
- - セキュリティに配慮すること
507
-
508
- よろしくお願いします。`;
509
- const callToolHandler = handlers.get('callTool');
510
- const result = await callToolHandler({
511
- params: {
512
- name: 'run',
513
- arguments: {
514
- prompt: longJapanesePrompt,
515
- workFolder: '/tmp',
516
- model: 'sonnet'
517
- }
518
- }
519
- });
520
- // Verify PID is returned
521
- const response = JSON.parse(result.content[0].text);
522
- expect(response.pid).toBe(12361);
523
- // Verify spawn was called with the complete long prompt
524
- expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['-p', longJapanesePrompt]), expect.any(Object));
525
- // Verify list_processes returns basic info
526
- const listResult = await callToolHandler({
527
- params: {
528
- name: 'list_processes',
529
- arguments: {}
530
- }
531
- });
532
- const processes = JSON.parse(listResult.content[0].text);
533
- const process = processes.find((p) => p.pid === 12361);
534
- expect(process.pid).toBe(12361);
535
- expect(process.agent).toBe('claude');
536
- expect(process.status).toBe('running');
537
- });
538
- it('should handle prompts with special characters and escape sequences', async () => {
539
- const { handlers } = await setupServer();
540
- const mockProcess = new EventEmitter();
541
- mockProcess.pid = 12362;
542
- mockProcess.stdout = new EventEmitter();
543
- mockProcess.stderr = new EventEmitter();
544
- mockProcess.kill = vi.fn();
545
- mockSpawn.mockReturnValue(mockProcess);
546
- // Test with various special characters
547
- const specialPrompt = `特殊文字のテスト:
548
- \t- タブ文字
549
- \n- 明示的な改行
550
- "ダブルクォート" と 'シングルクォート'
551
- バックスラッシュ: \\
552
- Unicodeテスト: 🎌 🗾 ✨
553
- 環境変数風: $HOME と \${USER}`;
554
- const callToolHandler = handlers.get('callTool');
555
- const result = await callToolHandler({
556
- params: {
557
- name: 'run',
558
- arguments: {
559
- prompt: specialPrompt,
560
- workFolder: '/tmp'
561
- }
562
- }
563
- });
564
- // Verify PID is returned
565
- const response = JSON.parse(result.content[0].text);
566
- expect(response.pid).toBe(12362);
567
- // Verify spawn was called with the special characters intact
568
- expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['-p', specialPrompt]), expect.any(Object));
569
- });
570
- it('should throw error if process fails to start', async () => {
571
- const { handlers } = await setupServer();
572
- const mockProcess = new EventEmitter();
573
- mockProcess.pid = undefined; // No PID means process failed to start
574
- mockProcess.stdout = new EventEmitter();
575
- mockProcess.stderr = new EventEmitter();
576
- mockSpawn.mockReturnValue(mockProcess);
577
- const callToolHandler = handlers.get('callTool');
578
- try {
579
- await callToolHandler({
580
- params: {
581
- name: 'run',
582
- arguments: {
583
- prompt: 'test prompt',
584
- workFolder: '/tmp/test'
585
- }
586
- }
587
- });
588
- expect.fail('Should have thrown');
589
- }
590
- catch (error) {
591
- expect(error.message).toContain('Failed to start claude CLI process');
592
- expect(error.code).toBe('InternalError');
593
- }
594
- });
595
- });
596
- describe('list_processes tool', () => {
597
- it('should list all processes', async () => {
598
- const { handlers } = await setupServer();
599
- // Start a process first
600
- const mockProcess = new EventEmitter();
601
- mockProcess.pid = 12347;
602
- mockProcess.stdout = new EventEmitter();
603
- mockProcess.stderr = new EventEmitter();
604
- mockProcess.kill = vi.fn();
605
- mockSpawn.mockReturnValue(mockProcess);
606
- const callToolHandler = handlers.get('callTool');
607
- // Start a process
608
- await callToolHandler({
609
- params: {
610
- name: 'run',
611
- arguments: {
612
- prompt: 'test prompt for listing',
613
- workFolder: '/tmp',
614
- model: 'sonnet'
615
- }
616
- }
617
- });
618
- // Simulate JSON output with session_id
619
- const jsonOutput = {
620
- session_id: 'list-test-session-789',
621
- status: 'running'
622
- };
623
- mockProcess.stdout.emit('data', JSON.stringify(jsonOutput));
624
- // List processes
625
- const listResult = await callToolHandler({
626
- params: {
627
- name: 'list_processes',
628
- arguments: {}
629
- }
630
- });
631
- const processes = JSON.parse(listResult.content[0].text);
632
- expect(processes).toHaveLength(1);
633
- expect(processes[0].pid).toBe(12347);
634
- expect(processes[0].status).toBe('running');
635
- expect(processes[0].agent).toBe('claude');
636
- });
637
- it('should list process with correct agent type', async () => {
638
- const { handlers } = await setupServer();
639
- const mockProcess = new EventEmitter();
640
- mockProcess.pid = 12348;
641
- mockProcess.stdout = new EventEmitter();
642
- mockProcess.stderr = new EventEmitter();
643
- mockProcess.kill = vi.fn();
644
- mockSpawn.mockReturnValue(mockProcess);
645
- const callToolHandler = handlers.get('callTool');
646
- // Start a process
647
- await callToolHandler({
648
- params: {
649
- name: 'run',
650
- arguments: {
651
- prompt: 'test prompt',
652
- workFolder: '/tmp'
653
- }
654
- }
655
- });
656
- // List processes
657
- const listResult = await callToolHandler({
658
- params: {
659
- name: 'list_processes',
660
- arguments: {}
661
- }
662
- });
663
- const processes = JSON.parse(listResult.content[0].text);
664
- expect(processes[0].pid).toBe(12348);
665
- expect(processes[0].agent).toBe('claude');
666
- expect(processes[0].status).toBe('running');
667
- });
668
- });
669
- describe('get_result tool', () => {
670
- it('should get process output', async () => {
671
- const { handlers } = await setupServer();
672
- const mockProcess = new EventEmitter();
673
- mockProcess.pid = 12349;
674
- mockProcess.stdout = new EventEmitter();
675
- mockProcess.stderr = new EventEmitter();
676
- mockProcess.kill = vi.fn();
677
- mockSpawn.mockReturnValue(mockProcess);
678
- const callToolHandler = handlers.get('callTool');
679
- // Start a process
680
- await callToolHandler({
681
- params: {
682
- name: 'run',
683
- arguments: {
684
- prompt: 'test prompt',
685
- workFolder: '/tmp'
686
- }
687
- }
688
- });
689
- // Simulate JSON output from Claude CLI
690
- const claudeJsonOutput = {
691
- session_id: 'test-session-123',
692
- status: 'success',
693
- message: 'Task completed'
694
- };
695
- mockProcess.stdout.emit('data', JSON.stringify(claudeJsonOutput));
696
- mockProcess.stderr.emit('data', 'Warning from stderr\n');
697
- // Get result
698
- const result = await callToolHandler({
699
- params: {
700
- name: 'get_result',
701
- arguments: {
702
- pid: 12349
703
- }
704
- }
705
- });
706
- const processInfo = JSON.parse(result.content[0].text);
707
- expect(processInfo.pid).toBe(12349);
708
- expect(processInfo.status).toBe('running');
709
- expect(processInfo.agentOutput).toEqual(claudeJsonOutput);
710
- expect(processInfo.session_id).toBe('test-session-123');
711
- });
712
- it('should show completed status when process exits', async () => {
713
- const { handlers } = await setupServer();
714
- const mockProcess = new EventEmitter();
715
- mockProcess.pid = 12350;
716
- mockProcess.stdout = new EventEmitter();
717
- mockProcess.stderr = new EventEmitter();
718
- mockProcess.kill = vi.fn();
719
- mockSpawn.mockReturnValue(mockProcess);
720
- const callToolHandler = handlers.get('callTool');
721
- // Start a process
722
- await callToolHandler({
723
- params: {
724
- name: 'run',
725
- arguments: {
726
- prompt: 'test prompt',
727
- workFolder: '/tmp'
728
- }
729
- }
730
- });
731
- // Simulate process completion with JSON output
732
- const completedJsonOutput = {
733
- session_id: 'completed-session-456',
734
- status: 'completed',
735
- files_created: ['test.txt'],
736
- summary: 'Created test file successfully'
737
- };
738
- mockProcess.stdout.emit('data', JSON.stringify(completedJsonOutput));
739
- mockProcess.emit('close', 0);
740
- // Get result
741
- const result = await callToolHandler({
742
- params: {
743
- name: 'get_result',
744
- arguments: {
745
- pid: 12350
746
- }
747
- }
748
- });
749
- const processInfo = JSON.parse(result.content[0].text);
750
- expect(processInfo.status).toBe('completed');
751
- expect(processInfo.exitCode).toBe(0);
752
- expect(processInfo.agentOutput).toEqual(completedJsonOutput);
753
- expect(processInfo.session_id).toBe('completed-session-456');
754
- });
755
- it('should throw error for non-existent PID', async () => {
756
- const { handlers } = await setupServer();
757
- const callToolHandler = handlers.get('callTool');
758
- await expect(callToolHandler({
759
- params: {
760
- name: 'get_result',
761
- arguments: {
762
- pid: 99999
763
- }
764
- }
765
- })).rejects.toThrow('Process with PID 99999 not found');
766
- });
767
- it('should throw error for invalid PID parameter', async () => {
768
- const { handlers } = await setupServer();
769
- const callToolHandler = handlers.get('callTool');
770
- await expect(callToolHandler({
771
- params: {
772
- name: 'get_result',
773
- arguments: {
774
- pid: 'not-a-number'
775
- }
776
- }
777
- })).rejects.toThrow('Missing or invalid required parameter: pid');
778
- });
779
- it('should handle non-JSON output gracefully', async () => {
780
- const { handlers } = await setupServer();
781
- const mockProcess = new EventEmitter();
782
- mockProcess.pid = 12355;
783
- mockProcess.stdout = new EventEmitter();
784
- mockProcess.stderr = new EventEmitter();
785
- mockProcess.kill = vi.fn();
786
- mockSpawn.mockReturnValue(mockProcess);
787
- const callToolHandler = handlers.get('callTool');
788
- // Start a process
789
- await callToolHandler({
790
- params: {
791
- name: 'run',
792
- arguments: {
793
- prompt: 'test prompt',
794
- workFolder: '/tmp'
795
- }
796
- }
797
- });
798
- // Simulate non-JSON output
799
- mockProcess.stdout.emit('data', 'This is plain text output, not JSON');
800
- mockProcess.stderr.emit('data', 'Some error occurred');
801
- // Get result
802
- const result = await callToolHandler({
803
- params: {
804
- name: 'get_result',
805
- arguments: {
806
- pid: 12355
807
- }
808
- }
809
- });
810
- const processInfo = JSON.parse(result.content[0].text);
811
- expect(processInfo.pid).toBe(12355);
812
- expect(processInfo.status).toBe('running');
813
- expect(processInfo.stdout).toBe('This is plain text output, not JSON');
814
- expect(processInfo.stderr).toBe('Some error occurred');
815
- expect(processInfo.claudeOutput).toBeUndefined();
816
- expect(processInfo.session_id).toBeUndefined();
817
- });
818
- });
819
- describe('kill_process tool', () => {
820
- it('should kill a running process', async () => {
821
- const { handlers } = await setupServer();
822
- const mockProcess = new EventEmitter();
823
- mockProcess.pid = 12351;
824
- mockProcess.stdout = new EventEmitter();
825
- mockProcess.stderr = new EventEmitter();
826
- mockProcess.kill = vi.fn();
827
- mockSpawn.mockReturnValue(mockProcess);
828
- const callToolHandler = handlers.get('callTool');
829
- // Start a process
830
- await callToolHandler({
831
- params: {
832
- name: 'run',
833
- arguments: {
834
- prompt: 'test prompt',
835
- workFolder: '/tmp'
836
- }
837
- }
838
- });
839
- // Kill the process
840
- const killResult = await callToolHandler({
841
- params: {
842
- name: 'kill_process',
843
- arguments: {
844
- pid: 12351
845
- }
846
- }
847
- });
848
- const response = JSON.parse(killResult.content[0].text);
849
- expect(response.status).toBe('terminated');
850
- expect(response.message).toBe('Process terminated successfully');
851
- expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM');
852
- });
853
- it('should handle already terminated process', async () => {
854
- const { handlers } = await setupServer();
855
- const mockProcess = new EventEmitter();
856
- mockProcess.pid = 12352;
857
- mockProcess.stdout = new EventEmitter();
858
- mockProcess.stderr = new EventEmitter();
859
- mockProcess.kill = vi.fn();
860
- mockSpawn.mockReturnValue(mockProcess);
861
- const callToolHandler = handlers.get('callTool');
862
- // Start and complete a process
863
- await callToolHandler({
864
- params: {
865
- name: 'run',
866
- arguments: {
867
- prompt: 'test prompt',
868
- workFolder: '/tmp'
869
- }
870
- }
871
- });
872
- // Simulate process completion
873
- mockProcess.emit('close', 0);
874
- // Try to kill the already completed process
875
- const killResult = await callToolHandler({
876
- params: {
877
- name: 'kill_process',
878
- arguments: {
879
- pid: 12352
880
- }
881
- }
882
- });
883
- const response = JSON.parse(killResult.content[0].text);
884
- expect(response.status).toBe('completed');
885
- expect(response.message).toBe('Process already terminated');
886
- expect(mockProcess.kill).not.toHaveBeenCalled();
887
- });
888
- it('should throw error for non-existent PID', async () => {
889
- const { handlers } = await setupServer();
890
- const callToolHandler = handlers.get('callTool');
891
- await expect(callToolHandler({
892
- params: {
893
- name: 'kill_process',
894
- arguments: {
895
- pid: 99999
896
- }
897
- }
898
- })).rejects.toThrow('Process with PID 99999 not found');
899
- });
900
- });
901
- describe('Tool routing', () => {
902
- it('should throw error for unknown tool', async () => {
903
- const { handlers } = await setupServer();
904
- const callToolHandler = handlers.get('callTool');
905
- await expect(callToolHandler({
906
- params: {
907
- name: 'unknown_tool',
908
- arguments: {}
909
- }
910
- })).rejects.toThrow('Tool unknown_tool not found');
911
- });
912
- });
913
- describe('Process error handling', () => {
914
- it('should handle process errors', async () => {
915
- const { handlers } = await setupServer();
916
- const mockProcess = new EventEmitter();
917
- mockProcess.pid = 12353;
918
- mockProcess.stdout = new EventEmitter();
919
- mockProcess.stderr = new EventEmitter();
920
- mockProcess.kill = vi.fn();
921
- mockSpawn.mockReturnValue(mockProcess);
922
- const callToolHandler = handlers.get('callTool');
923
- // Start a process
924
- await callToolHandler({
925
- params: {
926
- name: 'run',
927
- arguments: {
928
- prompt: 'test prompt',
929
- workFolder: '/tmp'
930
- }
931
- }
932
- });
933
- // Simulate process error
934
- mockProcess.emit('error', new Error('spawn error'));
935
- // Get result to check error was recorded
936
- const result = await callToolHandler({
937
- params: {
938
- name: 'get_result',
939
- arguments: {
940
- pid: 12353
941
- }
942
- }
943
- });
944
- const processInfo = JSON.parse(result.content[0].text);
945
- expect(processInfo.status).toBe('failed');
946
- expect(processInfo.stderr).toContain('Process error: spawn error');
947
- });
948
- });
949
- });