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,1020 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { spawn } from 'node:child_process';
3
- import { accessSync, existsSync } from 'node:fs';
4
- import { homedir } from 'node:os';
5
- import { resolve as pathResolve } from 'node:path';
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
- import { EventEmitter } from 'node:events';
9
-
10
- // Mock dependencies
11
- vi.mock('node:child_process');
12
- vi.mock('node:fs');
13
- vi.mock('node:os');
14
- vi.mock('node:path', () => ({
15
- resolve: vi.fn((path) => path),
16
- join: vi.fn((...args) => args.join('/')),
17
- isAbsolute: vi.fn((path) => path.startsWith('/'))
18
- }));
19
- vi.mock('@modelcontextprotocol/sdk/server/stdio.js');
20
- vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
21
- ListToolsRequestSchema: { name: 'listTools' },
22
- CallToolRequestSchema: { name: 'callTool' },
23
- ErrorCode: {
24
- InternalError: 'InternalError',
25
- MethodNotFound: 'MethodNotFound',
26
- InvalidParams: 'InvalidParams'
27
- },
28
- McpError: class extends Error {
29
- code: any;
30
- constructor(code: any, message: string) {
31
- super(message);
32
- this.code = code;
33
- }
34
- }
35
- }));
36
- vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
37
- Server: vi.fn().mockImplementation(function(this: any) {
38
- this.setRequestHandler = vi.fn();
39
- this.connect = vi.fn();
40
- this.close = vi.fn();
41
- this.onerror = undefined;
42
- return this;
43
- }),
44
- }));
45
-
46
- // Mock package.json
47
- vi.mock('../../package.json', () => ({
48
- default: { version: '1.0.0-test' }
49
- }));
50
-
51
- // Re-import after mocks
52
- const mockExistsSync = vi.mocked(existsSync);
53
- const mockAccessSync = vi.mocked(accessSync);
54
- const mockSpawn = vi.mocked(spawn);
55
- const mockHomedir = vi.mocked(homedir);
56
- const mockPathResolve = vi.mocked(pathResolve);
57
-
58
- // Module loading will happen in tests
59
-
60
- describe('ClaudeCodeServer Unit Tests', () => {
61
- let consoleErrorSpy: any;
62
- let consoleWarnSpy: any;
63
- let originalEnv: any;
64
-
65
- beforeEach(() => {
66
- vi.clearAllMocks();
67
- vi.resetModules();
68
- vi.unmock('../server.js');
69
- consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
70
- consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
71
- originalEnv = { ...process.env };
72
- // Reset env
73
- process.env = { ...originalEnv };
74
- mockAccessSync.mockImplementation((filePath) => {
75
- if (typeof filePath === 'string' && mockExistsSync(filePath)) {
76
- return undefined;
77
- }
78
- throw new Error('not executable');
79
- });
80
- });
81
-
82
- afterEach(() => {
83
- consoleErrorSpy.mockRestore();
84
- consoleWarnSpy.mockRestore();
85
- process.env = originalEnv;
86
- });
87
-
88
- describe('debugLog function', () => {
89
- it('should log when debug mode is enabled', async () => {
90
- process.env.MCP_CLAUDE_DEBUG = 'true';
91
- const module = await import('../server.js');
92
- // @ts-ignore - accessing private function for testing
93
- const { debugLog } = module;
94
-
95
- debugLog('Test message');
96
- expect(consoleErrorSpy).toHaveBeenCalledWith('Test message');
97
- });
98
-
99
- it('should not log when debug mode is disabled', async () => {
100
- // Reset modules to clear cache
101
- vi.resetModules();
102
- consoleErrorSpy.mockClear();
103
- process.env.MCP_CLAUDE_DEBUG = 'false';
104
- const module = await import('../server.js');
105
- // @ts-ignore
106
- const { debugLog } = module;
107
-
108
- debugLog('Test message');
109
- expect(consoleErrorSpy).not.toHaveBeenCalled();
110
- });
111
- });
112
-
113
- describe('findClaudeCli function', () => {
114
- it('should return local path when it exists', async () => {
115
- mockHomedir.mockReturnValue('/home/user');
116
- mockExistsSync.mockImplementation((path) => {
117
- // Mock returns true for real CLI path
118
- if (path === '/home/user/.claude/local/claude') return true;
119
- return false;
120
- });
121
- mockAccessSync.mockImplementation((filePath) => {
122
- if (filePath === '/home/user/.claude/local/claude') return undefined;
123
- throw new Error('not executable');
124
- });
125
-
126
- const module = await import('../server.js');
127
- // @ts-ignore
128
- const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
129
-
130
- const result = findClaudeCli();
131
- expect(result).toBe('/home/user/.claude/local/claude');
132
- });
133
-
134
- it('should fallback to PATH when local does not exist', async () => {
135
- mockHomedir.mockReturnValue('/home/user');
136
- mockExistsSync.mockReturnValue(false);
137
- mockAccessSync.mockImplementation(() => {
138
- throw new Error('not executable');
139
- });
140
-
141
- const module = await import('../server.js');
142
- // @ts-ignore
143
- const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
144
-
145
- const result = findClaudeCli();
146
- expect(result).toBe('claude');
147
- expect(consoleWarnSpy).not.toHaveBeenCalled();
148
- });
149
-
150
- it('should use custom name from CLAUDE_CLI_NAME', async () => {
151
- process.env.CLAUDE_CLI_NAME = 'my-claude';
152
- mockHomedir.mockReturnValue('/home/user');
153
- mockExistsSync.mockImplementation((path) => path === '/usr/bin/my-claude');
154
- mockAccessSync.mockImplementation((filePath) => {
155
- if (filePath === '/usr/bin/my-claude') return undefined;
156
- throw new Error('not executable');
157
- });
158
- process.env.PATH = '/usr/bin';
159
-
160
- const module = await import('../server.js');
161
- // @ts-ignore
162
- const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
163
-
164
- const result = findClaudeCli();
165
- expect(result).toBe('my-claude');
166
- });
167
-
168
- it('should use absolute path from CLAUDE_CLI_NAME', async () => {
169
- process.env.CLAUDE_CLI_NAME = '/absolute/path/to/claude';
170
- mockAccessSync.mockImplementation((filePath) => {
171
- if (filePath === '/absolute/path/to/claude') return undefined;
172
- throw new Error('not executable');
173
- });
174
-
175
- const module = await import('../server.js');
176
- // @ts-ignore
177
- const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
178
-
179
- const result = findClaudeCli();
180
- expect(result).toBe('/absolute/path/to/claude');
181
- });
182
-
183
- it('should throw error for relative paths in CLAUDE_CLI_NAME', async () => {
184
- process.env.CLAUDE_CLI_NAME = './relative/path/claude';
185
-
186
- const module = await import('../server.js');
187
- // @ts-ignore
188
- const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
189
-
190
- expect(() => findClaudeCli()).toThrow('Invalid CLAUDE_CLI_NAME: Relative paths are not allowed');
191
- });
192
-
193
- it('should throw error for paths with ../ in CLAUDE_CLI_NAME', async () => {
194
- process.env.CLAUDE_CLI_NAME = '../relative/path/claude';
195
-
196
- const module = await import('../server.js');
197
- // @ts-ignore
198
- const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
199
-
200
- expect(() => findClaudeCli()).toThrow('Invalid CLAUDE_CLI_NAME: Relative paths are not allowed');
201
- });
202
- });
203
-
204
- describe('findOpencodeCli function', () => {
205
- it('should fallback to PATH for OpenCode when no override is configured', async () => {
206
- mockHomedir.mockReturnValue('/home/user');
207
- mockExistsSync.mockImplementation((path) => path === '/usr/bin/opencode');
208
- mockAccessSync.mockImplementation((filePath) => {
209
- if (filePath === '/usr/bin/opencode') return undefined;
210
- throw new Error('not executable');
211
- });
212
- process.env.PATH = '/usr/bin';
213
-
214
- const module = await import('../server.js');
215
- // @ts-ignore
216
- const findOpencodeCli = module.default?.findOpencodeCli || module.findOpencodeCli;
217
-
218
- expect(findOpencodeCli()).toBe('/usr/bin/opencode');
219
- });
220
-
221
- it('should use custom name from OPENCODE_CLI_NAME', async () => {
222
- process.env.OPENCODE_CLI_NAME = 'opencode-custom';
223
- mockHomedir.mockReturnValue('/home/user');
224
- mockExistsSync.mockImplementation((path) => path === '/usr/bin/opencode-custom');
225
- mockAccessSync.mockImplementation((filePath) => {
226
- if (filePath === '/usr/bin/opencode-custom') return undefined;
227
- throw new Error('not executable');
228
- });
229
- process.env.PATH = '/usr/bin';
230
-
231
- const module = await import('../server.js');
232
- // @ts-ignore
233
- const findOpencodeCli = module.default?.findOpencodeCli || module.findOpencodeCli;
234
-
235
- expect(findOpencodeCli()).toBe('opencode-custom');
236
- });
237
- });
238
-
239
- describe('spawnAsync function', () => {
240
- let mockProcess: any;
241
-
242
- beforeEach(() => {
243
- // Create a mock process
244
- mockProcess = new EventEmitter() as any;
245
- mockProcess.stdout = new EventEmitter();
246
- mockProcess.stderr = new EventEmitter();
247
- mockProcess.stdout.on = vi.fn((event, handler) => {
248
- mockProcess.stdout[event] = handler;
249
- });
250
- mockProcess.stderr.on = vi.fn((event, handler) => {
251
- mockProcess.stderr[event] = handler;
252
- });
253
- mockSpawn.mockReturnValue(mockProcess);
254
- });
255
-
256
- it('should execute command successfully', async () => {
257
- const module = await import('../server.js');
258
- // @ts-ignore
259
- const { spawnAsync } = module;
260
-
261
- // mockProcess is already defined in the outer scope
262
-
263
- // Start the async operation
264
- const promise = spawnAsync('echo', ['test']);
265
-
266
- // Simulate successful execution
267
- setTimeout(() => {
268
- mockProcess.stdout['data']('test output');
269
- mockProcess.stderr['data']('');
270
- mockProcess.emit('close', 0);
271
- }, 10);
272
-
273
- const result = await promise;
274
- expect(result).toEqual({
275
- stdout: 'test output',
276
- stderr: ''
277
- });
278
- });
279
-
280
- it('should handle command failure', async () => {
281
- const module = await import('../server.js');
282
- // @ts-ignore
283
- const { spawnAsync } = module;
284
-
285
- // mockProcess is already defined in the outer scope
286
-
287
- // Start the async operation
288
- const promise = spawnAsync('false', []);
289
-
290
- // Simulate failed execution
291
- setTimeout(() => {
292
- mockProcess.stderr['data']('error output');
293
- mockProcess.emit('close', 1);
294
- }, 10);
295
-
296
- await expect(promise).rejects.toThrow('Command failed with exit code 1');
297
- });
298
-
299
- it('should handle spawn error', async () => {
300
- const module = await import('../server.js');
301
- // @ts-ignore
302
- const { spawnAsync } = module;
303
-
304
- // mockProcess is already defined in the outer scope
305
-
306
- // Start the async operation
307
- const promise = spawnAsync('nonexistent', []);
308
-
309
- // Simulate spawn error
310
- setTimeout(() => {
311
- const error: any = new Error('spawn error');
312
- error.code = 'ENOENT';
313
- error.path = 'nonexistent';
314
- error.syscall = 'spawn';
315
- mockProcess.emit('error', error);
316
- }, 10);
317
-
318
- await expect(promise).rejects.toThrow('Spawn error');
319
- });
320
-
321
- it('should respect timeout option', async () => {
322
- const module = await import('../server.js');
323
- // @ts-ignore
324
- const { spawnAsync } = module;
325
-
326
- const result = spawnAsync('sleep', ['10'], { timeout: 100 });
327
-
328
- expect(mockSpawn).toHaveBeenCalledWith('sleep', ['10'], expect.objectContaining({
329
- timeout: 100
330
- }));
331
- });
332
-
333
- it('should use provided cwd option', async () => {
334
- const module = await import('../server.js');
335
- // @ts-ignore
336
- const { spawnAsync } = module;
337
-
338
- const result = spawnAsync('ls', [], { cwd: '/tmp' });
339
-
340
- expect(mockSpawn).toHaveBeenCalledWith('ls', [], expect.objectContaining({
341
- cwd: '/tmp'
342
- }));
343
- });
344
- });
345
-
346
- describe('ClaudeCodeServer class', () => {
347
- it('should initialize with correct settings', async () => {
348
- mockHomedir.mockReturnValue('/home/user');
349
- mockExistsSync.mockReturnValue(true);
350
-
351
- // Set up Server mock before resetting modules
352
- vi.mocked(Server).mockImplementation(function(this: any) {
353
- this.setRequestHandler = vi.fn();
354
- this.connect = vi.fn();
355
- this.close = vi.fn();
356
- this.onerror = undefined;
357
- return this;
358
- });
359
-
360
- const module = await import('../server.js');
361
- // @ts-ignore
362
- const { ClaudeCodeServer } = module;
363
-
364
- const server = new ClaudeCodeServer();
365
-
366
- expect(consoleErrorSpy).toHaveBeenCalledWith(
367
- expect.stringContaining('[Setup] Using Claude CLI command/path:')
368
- );
369
- });
370
-
371
- it('should include OpenCode in setup logging', async () => {
372
- mockHomedir.mockReturnValue('/home/user');
373
- mockExistsSync.mockReturnValue(true);
374
-
375
- vi.mocked(Server).mockImplementation(function(this: any) {
376
- this.setRequestHandler = vi.fn();
377
- this.connect = vi.fn();
378
- this.close = vi.fn();
379
- this.onerror = undefined;
380
- return this;
381
- });
382
-
383
- const module = await import('../server.js');
384
- // @ts-ignore
385
- const { ClaudeCodeServer } = module;
386
- new ClaudeCodeServer();
387
-
388
- expect(consoleErrorSpy).toHaveBeenCalledWith(
389
- expect.stringContaining('[Setup] Using OpenCode CLI command/path:')
390
- );
391
- });
392
-
393
-
394
- it('should set up error handler', async () => {
395
- mockHomedir.mockReturnValue('/home/user');
396
- mockExistsSync.mockReturnValue(true);
397
-
398
- const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
399
- let errorHandler: any = null;
400
- vi.mocked(Server).mockImplementation(function(this: any) {
401
- this.setRequestHandler = vi.fn();
402
- this.connect = vi.fn();
403
- this.close = vi.fn();
404
- Object.defineProperty(this, 'onerror', {
405
- get() { return errorHandler; },
406
- set(handler) { errorHandler = handler; },
407
- enumerable: true,
408
- configurable: true
409
- });
410
- return this;
411
- });
412
-
413
- const module = await import('../server.js');
414
- // @ts-ignore
415
- const { ClaudeCodeServer } = module;
416
-
417
- const server = new ClaudeCodeServer();
418
-
419
- // Test error handler
420
- errorHandler(new Error('Test error'));
421
- expect(consoleErrorSpy).toHaveBeenCalledWith('[Error]', expect.any(Error));
422
- });
423
-
424
- it('should handle SIGINT', async () => {
425
- mockHomedir.mockReturnValue('/home/user');
426
- mockExistsSync.mockReturnValue(true);
427
-
428
- // Set up Server mock first
429
- vi.mocked(Server).mockImplementation(function(this: any) {
430
- this.setRequestHandler = vi.fn();
431
- this.connect = vi.fn();
432
- this.close = vi.fn();
433
- this.onerror = undefined;
434
- return this;
435
- });
436
-
437
- const module = await import('../server.js');
438
- // @ts-ignore
439
- const { ClaudeCodeServer } = module;
440
-
441
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
442
- const server = new ClaudeCodeServer();
443
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
444
-
445
- // Emit SIGINT
446
- const sigintHandler = process.listeners('SIGINT').slice(-1)[0] as any;
447
- await sigintHandler();
448
-
449
- expect(mockServerInstance.close).toHaveBeenCalled();
450
- expect(exitSpy).toHaveBeenCalledWith(0);
451
-
452
- exitSpy.mockRestore();
453
- });
454
- });
455
-
456
- describe('Tool handler implementation', () => {
457
- // Define setupServerMock for this describe block
458
- let errorHandler: any = null;
459
- function setupServerMock() {
460
- errorHandler = null;
461
- vi.mocked(Server).mockImplementation(function(this: any) {
462
- this.setRequestHandler = vi.fn();
463
- this.connect = vi.fn();
464
- this.close = vi.fn();
465
- Object.defineProperty(this, 'onerror', {
466
- get() { return errorHandler; },
467
- set(handler) { errorHandler = handler; },
468
- enumerable: true,
469
- configurable: true
470
- });
471
- return this;
472
- });
473
- }
474
-
475
- it('should handle ListToolsRequest', async () => {
476
- mockHomedir.mockReturnValue('/home/user');
477
- mockExistsSync.mockReturnValue(true);
478
-
479
- // Use the setupServerMock function from the beginning of the file
480
- setupServerMock();
481
-
482
- const module = await import('../server.js');
483
- // @ts-ignore
484
- const { ClaudeCodeServer } = module;
485
-
486
- const server = new ClaudeCodeServer();
487
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
488
-
489
- // Find the ListToolsRequest handler
490
- const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find(
491
- (call: any[]) => call[0].name === 'listTools'
492
- );
493
-
494
- expect(listToolsCall).toBeDefined();
495
-
496
- // Test the handler
497
- const handler = listToolsCall[1];
498
- const result = await handler();
499
-
500
- expect(result.tools).toHaveLength(7);
501
- expect(result.tools[0].name).toBe('run');
502
- expect(result.tools[0].description).toContain('AI Agent Runner');
503
- expect(result.tools[1].name).toBe('list_processes');
504
- expect(result.tools[2].name).toBe('get_result');
505
- expect(result.tools[3].name).toBe('wait');
506
- expect(result.tools[4].name).toBe('peek');
507
- expect(result.tools[5].name).toBe('kill_process');
508
- expect(result.tools[6].name).toBe('cleanup_processes');
509
- });
510
-
511
- it('should handle CallToolRequest', async () => {
512
- mockHomedir.mockReturnValue('/home/user');
513
- mockExistsSync.mockReturnValue(true);
514
-
515
- // Set up Server mock
516
- setupServerMock();
517
-
518
- const module = await import('../server.js');
519
- // @ts-ignore
520
- const { ClaudeCodeServer } = module;
521
-
522
- const server = new ClaudeCodeServer();
523
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
524
-
525
- // Find the CallToolRequest handler
526
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
527
- (call: any[]) => call[0].name === 'callTool'
528
- );
529
-
530
- expect(callToolCall).toBeDefined();
531
-
532
- // Create a mock process for the tool execution
533
- const mockProcess = new EventEmitter() as any;
534
- mockProcess.pid = 12345;
535
- mockProcess.stdout = new EventEmitter();
536
- mockProcess.stderr = new EventEmitter();
537
- mockProcess.stdout.on = vi.fn();
538
- mockProcess.stderr.on = vi.fn();
539
- mockProcess.kill = vi.fn();
540
-
541
- mockSpawn.mockReturnValue(mockProcess);
542
-
543
- // Test the handler
544
- const handler = callToolCall[1];
545
- const result = await handler({
546
- params: {
547
- name: 'run',
548
- arguments: {
549
- prompt: 'test prompt',
550
- workFolder: '/tmp'
551
- }
552
- }
553
- });
554
-
555
- // run now returns PID immediately
556
- expect(result.content[0].type).toBe('text');
557
- const response = JSON.parse(result.content[0].text);
558
- expect(response.pid).toBe(12345);
559
- expect(response.status).toBe('started');
560
- });
561
-
562
- it('should require workFolder parameter', async () => {
563
- mockHomedir.mockReturnValue('/home/user');
564
- mockExistsSync.mockReturnValue(true);
565
-
566
- // Set up Server mock
567
- setupServerMock();
568
-
569
- const module = await import('../server.js');
570
- // @ts-ignore
571
- const { ClaudeCodeServer } = module;
572
- const server = new ClaudeCodeServer();
573
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
574
-
575
- // Find the CallToolRequest handler
576
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
577
- (call: any[]) => call[0].name === 'callTool'
578
- );
579
-
580
- const handler = callToolCall[1];
581
-
582
- // Test missing workFolder
583
- try {
584
- await handler({
585
- params: {
586
- name: 'run',
587
- arguments: {
588
- prompt: 'test'
589
- }
590
- }
591
- });
592
- expect.fail('Should have thrown');
593
- } catch (error: any) {
594
- expect(error.message).toContain('Missing or invalid required parameter: workFolder');
595
- }
596
- });
597
-
598
- it('should handle non-existent workFolder', async () => {
599
- mockHomedir.mockReturnValue('/home/user');
600
- mockExistsSync.mockImplementation((path) => {
601
- // Make the CLI path exist but the workFolder not exist
602
- if (String(path).includes('.claude')) return true;
603
- if (path === '/nonexistent') return false;
604
- return false;
605
- });
606
-
607
- // Set up Server mock
608
- setupServerMock();
609
-
610
- const module = await import('../server.js');
611
- // @ts-ignore
612
- const { ClaudeCodeServer } = module;
613
- const server = new ClaudeCodeServer();
614
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
615
-
616
- // Find the CallToolRequest handler
617
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
618
- (call: any[]) => call[0].name === 'callTool'
619
- );
620
-
621
- const handler = callToolCall[1];
622
-
623
- // Should throw error for non-existent workFolder
624
- try {
625
- await handler({
626
- params: {
627
- name: 'run',
628
- arguments: {
629
- prompt: 'test',
630
- workFolder: '/nonexistent'
631
- }
632
- }
633
- });
634
- expect.fail('Should have thrown');
635
- } catch (error: any) {
636
- expect(error.message).toContain('Working folder does not exist');
637
- }
638
- });
639
-
640
- it('should handle session_id parameter', async () => {
641
- mockHomedir.mockReturnValue('/home/user');
642
- mockExistsSync.mockReturnValue(true);
643
-
644
- // Set up Server mock
645
- setupServerMock();
646
-
647
- const module = await import('../server.js');
648
- // @ts-ignore
649
- const { ClaudeCodeServer } = module;
650
- const server = new ClaudeCodeServer();
651
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
652
-
653
- // Find the CallToolRequest handler
654
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
655
- (call: any[]) => call[0].name === 'callTool'
656
- );
657
-
658
- const handler = callToolCall[1];
659
-
660
- // Create mock process
661
- const mockProcess = new EventEmitter() as any;
662
- mockProcess.pid = 12347;
663
- mockProcess.stdout = new EventEmitter();
664
- mockProcess.stderr = new EventEmitter();
665
- mockProcess.stdout.on = vi.fn();
666
- mockProcess.stderr.on = vi.fn();
667
- mockProcess.kill = vi.fn();
668
- mockSpawn.mockReturnValue(mockProcess);
669
-
670
- const result = await handler({
671
- params: {
672
- name: 'run',
673
- arguments: {
674
- prompt: 'test prompt',
675
- workFolder: '/tmp',
676
- session_id: 'test-session-123'
677
- }
678
- }
679
- });
680
-
681
- // Verify spawn was called with -r flag
682
- expect(mockSpawn).toHaveBeenCalledWith(
683
- expect.any(String),
684
- expect.arrayContaining(['-r', 'test-session-123', '--fork-session', '-p', 'test prompt']),
685
- expect.any(Object)
686
- );
687
- });
688
-
689
- it('should handle session_id parameter for Codex using exec resume', async () => {
690
- mockHomedir.mockReturnValue('/home/user');
691
- mockExistsSync.mockReturnValue(true);
692
-
693
- // Set up Server mock
694
- setupServerMock();
695
-
696
- const module = await import('../server.js');
697
- // @ts-ignore
698
- const { ClaudeCodeServer } = module;
699
- const server = new ClaudeCodeServer();
700
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
701
-
702
- // Find the CallToolRequest handler
703
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
704
- (call: any[]) => call[0].name === 'callTool'
705
- );
706
-
707
- const handler = callToolCall[1];
708
-
709
- // Create mock process
710
- const mockProcess = new EventEmitter() as any;
711
- mockProcess.pid = 12350;
712
- mockProcess.stdout = new EventEmitter();
713
- mockProcess.stderr = new EventEmitter();
714
- mockProcess.stdout.on = vi.fn();
715
- mockProcess.stderr.on = vi.fn();
716
- mockProcess.kill = vi.fn();
717
- mockSpawn.mockReturnValue(mockProcess);
718
-
719
- const result = await handler({
720
- params: {
721
- name: 'run',
722
- arguments: {
723
- prompt: 'test prompt',
724
- workFolder: '/tmp',
725
- model: 'gpt-5.2',
726
- session_id: 'codex-session-456'
727
- }
728
- }
729
- });
730
-
731
- // Verify spawn was called with 'exec resume' subcommand for Codex
732
- expect(mockSpawn).toHaveBeenCalledWith(
733
- expect.any(String),
734
- expect.arrayContaining(['exec', 'resume', 'codex-session-456']),
735
- expect.any(Object)
736
- );
737
- });
738
-
739
- it('should handle session_id parameter for Gemini using -r flag', async () => {
740
- mockHomedir.mockReturnValue('/home/user');
741
- mockExistsSync.mockReturnValue(true);
742
-
743
- // Set up Server mock
744
- setupServerMock();
745
-
746
- const module = await import('../server.js');
747
- // @ts-ignore
748
- const { ClaudeCodeServer } = module;
749
- const server = new ClaudeCodeServer();
750
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
751
-
752
- // Find the CallToolRequest handler
753
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
754
- (call: any[]) => call[0].name === 'callTool'
755
- );
756
-
757
- const handler = callToolCall[1];
758
-
759
- // Create mock process
760
- const mockProcess = new EventEmitter() as any;
761
- mockProcess.pid = 12351;
762
- mockProcess.stdout = new EventEmitter();
763
- mockProcess.stderr = new EventEmitter();
764
- mockProcess.stdout.on = vi.fn();
765
- mockProcess.stderr.on = vi.fn();
766
- mockProcess.kill = vi.fn();
767
- mockSpawn.mockReturnValue(mockProcess);
768
-
769
- const result = await handler({
770
- params: {
771
- name: 'run',
772
- arguments: {
773
- prompt: 'test prompt',
774
- workFolder: '/tmp',
775
- model: 'gemini-2.5-pro',
776
- session_id: 'gemini-session-789'
777
- }
778
- }
779
- });
780
-
781
- // Verify spawn was called with -r flag for Gemini
782
- expect(mockSpawn).toHaveBeenCalledWith(
783
- expect.any(String),
784
- expect.arrayContaining(['-r', 'gemini-session-789']),
785
- expect.any(Object)
786
- );
787
- });
788
-
789
- it('should handle prompt_file parameter', async () => {
790
- mockHomedir.mockReturnValue('/home/user');
791
- mockExistsSync.mockImplementation((path) => {
792
- if (String(path).includes('.claude')) return true;
793
- if (path === '/tmp') return true;
794
- if (path === '/tmp/prompt.txt') return true;
795
- return false;
796
- });
797
-
798
- // Mock readFileSync
799
- const readFileSyncMock = vi.fn().mockReturnValue('Content from file');
800
- vi.doMock('node:fs', () => ({
801
- existsSync: mockExistsSync,
802
- readFileSync: readFileSyncMock
803
- }));
804
-
805
- // Set up Server mock
806
- setupServerMock();
807
-
808
- const module = await import('../server.js');
809
- // @ts-ignore
810
- const { ClaudeCodeServer } = module;
811
- const server = new ClaudeCodeServer();
812
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
813
-
814
- // Find the CallToolRequest handler
815
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
816
- (call: any[]) => call[0].name === 'callTool'
817
- );
818
-
819
- const handler = callToolCall[1];
820
-
821
- // Create mock process
822
- const mockProcess = new EventEmitter() as any;
823
- mockProcess.pid = 12348;
824
- mockProcess.stdout = new EventEmitter();
825
- mockProcess.stderr = new EventEmitter();
826
- mockProcess.stdout.on = vi.fn();
827
- mockProcess.stderr.on = vi.fn();
828
- mockProcess.kill = vi.fn();
829
- mockSpawn.mockReturnValue(mockProcess);
830
-
831
- const result = await handler({
832
- params: {
833
- name: 'run',
834
- arguments: {
835
- prompt_file: '/tmp/prompt.txt',
836
- workFolder: '/tmp'
837
- }
838
- }
839
- });
840
-
841
- // Verify file was read and spawn was called with content
842
- expect(mockSpawn).toHaveBeenCalledWith(
843
- expect.any(String),
844
- expect.arrayContaining(['-p', 'Content from file']),
845
- expect.any(Object)
846
- );
847
- });
848
-
849
- it('should resolve model aliases when calling run tool', async () => {
850
- mockHomedir.mockReturnValue('/home/user');
851
- mockExistsSync.mockReturnValue(true);
852
-
853
- // Set up spawn mock to return a process
854
- const mockProcess = new EventEmitter() as any;
855
- mockProcess.stdout = new EventEmitter();
856
- mockProcess.stderr = new EventEmitter();
857
- mockProcess.pid = 12345;
858
- mockSpawn.mockReturnValue(mockProcess);
859
-
860
- // Set up Server mock
861
- setupServerMock();
862
-
863
- const module = await import('../server.js');
864
- // @ts-ignore
865
- const { ClaudeCodeServer } = module;
866
- const server = new ClaudeCodeServer();
867
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
868
-
869
- // Find the CallToolRequest handler
870
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
871
- (call: any[]) => call[0].name === 'callTool'
872
- );
873
-
874
- const handler = callToolCall[1];
875
-
876
- // Test with haiku alias
877
- const result = await handler({
878
- params: {
879
- name: 'run',
880
- arguments: {
881
- prompt: 'test prompt',
882
- workFolder: '/tmp',
883
- model: 'haiku'
884
- }
885
- }
886
- });
887
-
888
- // Verify spawn was called with resolved model name
889
- expect(mockSpawn).toHaveBeenCalledWith(
890
- expect.any(String),
891
- expect.arrayContaining(['--model', 'haiku']),
892
- expect.any(Object)
893
- );
894
-
895
- // Verify PID is returned
896
- expect(result.content[0].text).toContain('"pid": 12345');
897
- });
898
-
899
- it('should pass non-alias model names unchanged', async () => {
900
- mockHomedir.mockReturnValue('/home/user');
901
- mockExistsSync.mockReturnValue(true);
902
-
903
- // Set up spawn mock to return a process
904
- const mockProcess = new EventEmitter() as any;
905
- mockProcess.stdout = new EventEmitter();
906
- mockProcess.stderr = new EventEmitter();
907
- mockProcess.pid = 12346;
908
- mockSpawn.mockReturnValue(mockProcess);
909
-
910
- // Set up Server mock
911
- setupServerMock();
912
-
913
- const module = await import('../server.js');
914
- // @ts-ignore
915
- const { ClaudeCodeServer } = module;
916
- const server = new ClaudeCodeServer();
917
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
918
-
919
- // Find the CallToolRequest handler
920
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
921
- (call: any[]) => call[0].name === 'callTool'
922
- );
923
-
924
- const handler = callToolCall[1];
925
-
926
- // Test with non-alias model name
927
- const result = await handler({
928
- params: {
929
- name: 'run',
930
- arguments: {
931
- prompt: 'test prompt',
932
- workFolder: '/tmp',
933
- model: 'sonnet'
934
- }
935
- }
936
- });
937
-
938
- // Verify spawn was called with unchanged model name
939
- expect(mockSpawn).toHaveBeenCalledWith(
940
- expect.any(String),
941
- expect.arrayContaining(['--model', 'sonnet']),
942
- expect.any(Object)
943
- );
944
- });
945
-
946
- it('should reject when both prompt and prompt_file are provided', async () => {
947
- mockHomedir.mockReturnValue('/home/user');
948
- mockExistsSync.mockReturnValue(true);
949
-
950
- // Set up Server mock
951
- setupServerMock();
952
-
953
- const module = await import('../server.js');
954
- // @ts-ignore
955
- const { ClaudeCodeServer } = module;
956
- const server = new ClaudeCodeServer();
957
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
958
-
959
- // Find the CallToolRequest handler
960
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
961
- (call: any[]) => call[0].name === 'callTool'
962
- );
963
-
964
- const handler = callToolCall[1];
965
-
966
- // Test both parameters provided
967
- try {
968
- await handler({
969
- params: {
970
- name: 'run',
971
- arguments: {
972
- prompt: 'test prompt',
973
- prompt_file: '/tmp/prompt.txt',
974
- workFolder: '/tmp'
975
- }
976
- }
977
- });
978
- expect.fail('Should have thrown an error');
979
- } catch (error: any) {
980
- expect(error.message).toContain('Cannot specify both prompt and prompt_file');
981
- }
982
- });
983
-
984
- it('should reject when neither prompt nor prompt_file are provided', async () => {
985
- mockHomedir.mockReturnValue('/home/user');
986
- mockExistsSync.mockReturnValue(true);
987
-
988
- // Set up Server mock
989
- setupServerMock();
990
-
991
- const module = await import('../server.js');
992
- // @ts-ignore
993
- const { ClaudeCodeServer } = module;
994
- const server = new ClaudeCodeServer();
995
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
996
-
997
- // Find the CallToolRequest handler
998
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
999
- (call: any[]) => call[0].name === 'callTool'
1000
- );
1001
-
1002
- const handler = callToolCall[1];
1003
-
1004
- // Test neither parameter provided
1005
- try {
1006
- await handler({
1007
- params: {
1008
- name: 'run',
1009
- arguments: {
1010
- workFolder: '/tmp'
1011
- }
1012
- }
1013
- });
1014
- expect.fail('Should have thrown an error');
1015
- } catch (error: any) {
1016
- expect(error.message).toContain('Either prompt or prompt_file must be provided');
1017
- }
1018
- });
1019
- });
1020
- });