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