ai-cli-mcp 2.18.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +37 -11
  3. package/README.md +44 -11
  4. package/dist/app/cli.js +2 -1
  5. package/dist/app/mcp.js +65 -13
  6. package/dist/cli-builder.js +13 -6
  7. package/dist/cli-process-service.js +81 -95
  8. package/dist/cli-utils.js +6 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/model-catalog.js +3 -2
  11. package/dist/parsers.js +111 -8
  12. package/dist/process-service.js +5 -4
  13. package/package.json +26 -2
  14. package/server.json +3 -3
  15. package/.gemini/settings.json +0 -11
  16. package/.github/dependabot.yml +0 -28
  17. package/.github/pull_request_template.md +0 -28
  18. package/.github/workflows/ci.yml +0 -34
  19. package/.github/workflows/dependency-review.yml +0 -22
  20. package/.github/workflows/publish.yml +0 -89
  21. package/.github/workflows/test.yml +0 -20
  22. package/.github/workflows/watch-session-prs.yml +0 -276
  23. package/.husky/pre-commit +0 -1
  24. package/.mcp.json +0 -11
  25. package/.releaserc.json +0 -18
  26. package/.vscode/settings.json +0 -3
  27. package/CONTRIBUTING.md +0 -81
  28. package/dist/__tests__/app-cli.test.js +0 -392
  29. package/dist/__tests__/cli-bin-smoke.test.js +0 -101
  30. package/dist/__tests__/cli-builder.test.js +0 -442
  31. package/dist/__tests__/cli-process-service.test.js +0 -655
  32. package/dist/__tests__/cli-utils.test.js +0 -171
  33. package/dist/__tests__/e2e.test.js +0 -256
  34. package/dist/__tests__/edge-cases.test.js +0 -130
  35. package/dist/__tests__/error-cases.test.js +0 -292
  36. package/dist/__tests__/mcp-contract.test.js +0 -636
  37. package/dist/__tests__/mocks.js +0 -32
  38. package/dist/__tests__/model-alias.test.js +0 -36
  39. package/dist/__tests__/parsers.test.js +0 -500
  40. package/dist/__tests__/peek.test.js +0 -36
  41. package/dist/__tests__/process-management.test.js +0 -871
  42. package/dist/__tests__/server.test.js +0 -809
  43. package/dist/__tests__/setup.js +0 -11
  44. package/dist/__tests__/utils/claude-mock.js +0 -80
  45. package/dist/__tests__/utils/mcp-client.js +0 -121
  46. package/dist/__tests__/utils/opencode-mock.js +0 -91
  47. package/dist/__tests__/utils/persistent-mock.js +0 -28
  48. package/dist/__tests__/utils/test-helpers.js +0 -11
  49. package/dist/__tests__/validation.test.js +0 -308
  50. package/dist/__tests__/version-print.test.js +0 -65
  51. package/dist/__tests__/wait.test.js +0 -260
  52. package/docs/RELEASE_CHECKLIST.md +0 -65
  53. package/docs/cli-architecture.md +0 -275
  54. package/docs/concept.md +0 -154
  55. package/docs/development.md +0 -156
  56. package/docs/e2e-testing.md +0 -148
  57. package/docs/prd.md +0 -146
  58. package/docs/session-stacking.md +0 -67
  59. package/src/__tests__/app-cli.test.ts +0 -495
  60. package/src/__tests__/cli-bin-smoke.test.ts +0 -136
  61. package/src/__tests__/cli-builder.test.ts +0 -549
  62. package/src/__tests__/cli-process-service.test.ts +0 -759
  63. package/src/__tests__/cli-utils.test.ts +0 -200
  64. package/src/__tests__/e2e.test.ts +0 -311
  65. package/src/__tests__/edge-cases.test.ts +0 -176
  66. package/src/__tests__/error-cases.test.ts +0 -370
  67. package/src/__tests__/mcp-contract.test.ts +0 -755
  68. package/src/__tests__/mocks.ts +0 -35
  69. package/src/__tests__/model-alias.test.ts +0 -44
  70. package/src/__tests__/parsers.test.ts +0 -564
  71. package/src/__tests__/peek.test.ts +0 -44
  72. package/src/__tests__/process-management.test.ts +0 -1043
  73. package/src/__tests__/server.test.ts +0 -1020
  74. package/src/__tests__/setup.ts +0 -13
  75. package/src/__tests__/utils/claude-mock.ts +0 -87
  76. package/src/__tests__/utils/mcp-client.ts +0 -159
  77. package/src/__tests__/utils/opencode-mock.ts +0 -108
  78. package/src/__tests__/utils/persistent-mock.ts +0 -33
  79. package/src/__tests__/utils/test-helpers.ts +0 -13
  80. package/src/__tests__/validation.test.ts +0 -369
  81. package/src/__tests__/version-print.test.ts +0 -81
  82. package/src/__tests__/wait.test.ts +0 -302
  83. package/src/app/cli.ts +0 -424
  84. package/src/app/mcp.ts +0 -466
  85. package/src/bin/ai-cli-mcp.ts +0 -7
  86. package/src/bin/ai-cli.ts +0 -11
  87. package/src/cli-builder.ts +0 -274
  88. package/src/cli-parse.ts +0 -105
  89. package/src/cli-process-service.ts +0 -708
  90. package/src/cli-utils.ts +0 -258
  91. package/src/cli.ts +0 -124
  92. package/src/model-catalog.ts +0 -87
  93. package/src/parsers.ts +0 -840
  94. package/src/peek.ts +0 -95
  95. package/src/process-result.ts +0 -88
  96. package/src/process-service.ts +0 -367
  97. package/src/server.ts +0 -10
  98. package/tsconfig.json +0 -16
  99. package/vitest.config.e2e.ts +0 -27
  100. package/vitest.config.ts +0 -22
  101. package/vitest.config.unit.ts +0 -28
@@ -1,369 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { z } from 'zod';
3
- import { existsSync } from 'node:fs';
4
- import { homedir } from 'node:os';
5
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
-
7
- // Mock dependencies
8
- vi.mock('node:child_process', () => ({
9
- spawn: vi.fn()
10
- }));
11
- vi.mock('node:fs');
12
- vi.mock('node:os');
13
- vi.mock('node:path', () => ({
14
- resolve: vi.fn((path) => path),
15
- join: vi.fn((...args) => args.join('/')),
16
- isAbsolute: vi.fn((path) => path.startsWith('/'))
17
- }));
18
- vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
19
- Server: vi.fn().mockImplementation(function(this: any) {
20
- this.setRequestHandler = vi.fn();
21
- this.connect = vi.fn();
22
- this.close = vi.fn();
23
- this.onerror = undefined;
24
- return this;
25
- }),
26
- }));
27
-
28
- vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
29
- ListToolsRequestSchema: { name: 'listTools' },
30
- CallToolRequestSchema: { name: 'callTool' },
31
- ErrorCode: {
32
- InternalError: 'InternalError',
33
- MethodNotFound: 'MethodNotFound',
34
- InvalidParams: 'InvalidParams'
35
- },
36
- McpError: class McpError extends Error {
37
- code: string;
38
- constructor(code: string, message: string) {
39
- super(message);
40
- this.code = code;
41
- this.name = 'McpError';
42
- }
43
- }
44
- }));
45
-
46
- const mockExistsSync = vi.mocked(existsSync);
47
- const mockHomedir = vi.mocked(homedir);
48
-
49
- describe('Argument Validation Tests', () => {
50
- let consoleErrorSpy: any;
51
- let errorHandler: any = null;
52
-
53
- function setupServerMock() {
54
- errorHandler = null;
55
- vi.mocked(Server).mockImplementation(function(this: any) {
56
- this.setRequestHandler = vi.fn();
57
- this.connect = vi.fn();
58
- this.close = vi.fn();
59
- Object.defineProperty(this, 'onerror', {
60
- get() { return errorHandler; },
61
- set(handler) { errorHandler = handler; },
62
- enumerable: true,
63
- configurable: true
64
- });
65
- return this;
66
- });
67
- }
68
-
69
- beforeEach(() => {
70
- vi.clearAllMocks();
71
- vi.resetModules();
72
- consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
73
- // Set up process.env
74
- process.env = { ...process.env };
75
- });
76
-
77
- describe('Tool Arguments Schema', () => {
78
- it('should validate valid arguments', async () => {
79
- mockHomedir.mockReturnValue('/home/user');
80
- mockExistsSync.mockReturnValue(true);
81
- setupServerMock();
82
- vi.doUnmock('../server.js');
83
- const module = await import('../server.js');
84
- // @ts-ignore
85
- const { ClaudeCodeServer } = module;
86
-
87
- const server = new ClaudeCodeServer();
88
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
89
-
90
- // Find tool definition
91
- const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find(
92
- (call: any[]) => call[0].name === 'listTools'
93
- );
94
-
95
- const listHandler = listToolsCall[1];
96
- const tools = await listHandler();
97
- const claudeCodeTool = tools.tools[0];
98
-
99
- // Extract schema from tool definition
100
- const schema = z.object({
101
- prompt: z.string(),
102
- workFolder: z.string(),
103
- model: z.string().optional(),
104
- reasoning_effort: z.string().optional(),
105
- session_id: z.string().optional()
106
- });
107
-
108
- // Test valid cases
109
- expect(() => schema.parse({ prompt: 'test', workFolder: '/tmp' })).not.toThrow();
110
- expect(() => schema.parse({ prompt: 'test', workFolder: '/tmp', model: 'sonnet' })).not.toThrow();
111
- });
112
-
113
- it('should reject invalid arguments', async () => {
114
- mockHomedir.mockReturnValue('/home/user');
115
- mockExistsSync.mockReturnValue(true);
116
- setupServerMock();
117
- vi.doUnmock('../server.js');
118
- const module = await import('../server.js');
119
- // @ts-ignore
120
- const { ClaudeCodeServer } = module;
121
-
122
- const server = new ClaudeCodeServer();
123
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
124
-
125
- // Find tool definition
126
- const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find(
127
- (call: any[]) => call[0].name === 'listTools'
128
- );
129
-
130
- const listHandler = listToolsCall[1];
131
- const tools = await listHandler();
132
- const claudeCodeTool = tools.tools[0];
133
-
134
- // Extract schema from tool definition
135
- const schema = z.object({
136
- prompt: z.string(),
137
- workFolder: z.string(),
138
- model: z.string().optional(),
139
- reasoning_effort: z.string().optional(),
140
- session_id: z.string().optional()
141
- });
142
-
143
- // Test invalid cases
144
- expect(() => schema.parse({})).toThrow(); // Missing prompt and workFolder
145
- expect(() => schema.parse({ prompt: 'test' })).toThrow(); // Missing workFolder
146
- expect(() => schema.parse({ prompt: 123, workFolder: '/tmp' })).toThrow(); // Wrong prompt type
147
- expect(() => schema.parse({ prompt: 'test', workFolder: 123 })).toThrow(); // Wrong workFolder type
148
- });
149
-
150
- it('should handle missing required fields', async () => {
151
- const schema = z.object({
152
- prompt: z.string(),
153
- workFolder: z.string(),
154
- model: z.string().optional(),
155
- reasoning_effort: z.string().optional(),
156
- session_id: z.string().optional()
157
- });
158
-
159
- try {
160
- schema.parse({});
161
- } catch (error: any) {
162
- // Both prompt and workFolder are required
163
- expect(error.errors.length).toBe(2);
164
- expect(error.errors.some((e: any) => e.path[0] === 'prompt')).toBe(true);
165
- expect(error.errors.some((e: any) => e.path[0] === 'workFolder')).toBe(true);
166
- }
167
- });
168
-
169
- it('should allow optional fields to be undefined', async () => {
170
- const schema = z.object({
171
- prompt: z.string(),
172
- workFolder: z.string(),
173
- model: z.string().optional(),
174
- reasoning_effort: z.string().optional(),
175
- session_id: z.string().optional()
176
- });
177
-
178
- const result = schema.parse({ prompt: 'test', workFolder: '/tmp' });
179
- expect(result.model).toBeUndefined();
180
- expect(result.session_id).toBeUndefined();
181
- });
182
-
183
- it('should handle extra fields gracefully', async () => {
184
- const schema = z.object({
185
- prompt: z.string(),
186
- workFolder: z.string(),
187
- model: z.string().optional(),
188
- reasoning_effort: z.string().optional(),
189
- session_id: z.string().optional()
190
- });
191
-
192
- // By default, Zod strips unknown keys
193
- const result = schema.parse({
194
- prompt: 'test',
195
- workFolder: '/tmp',
196
- extraField: 'ignored'
197
- });
198
-
199
- expect(result).toEqual({ prompt: 'test', workFolder: '/tmp' });
200
- expect(result).not.toHaveProperty('extraField');
201
- });
202
- });
203
-
204
- describe('Runtime Argument Validation', () => {
205
- let handlers: Map<string, Function>;
206
- let mockServerInstance: any;
207
-
208
- async function setupServer() {
209
- // Reset modules to ensure fresh import
210
- vi.resetModules();
211
-
212
- // Re-setup mocks after reset
213
- const { existsSync } = await import('node:fs');
214
- const { homedir } = await import('node:os');
215
- vi.mocked(existsSync).mockReturnValue(true);
216
- vi.mocked(homedir).mockReturnValue('/home/user');
217
-
218
- const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
219
-
220
- vi.mocked(Server).mockImplementation(function(this: any) {
221
- mockServerInstance = {
222
- setRequestHandler: vi.fn((schema: any, handler: Function) => {
223
- handlers.set(schema.name, handler);
224
- }),
225
- connect: vi.fn(),
226
- close: vi.fn(),
227
- onerror: undefined
228
- };
229
- return mockServerInstance as any;
230
- });
231
-
232
- vi.doUnmock('../server.js');
233
- const module = await import('../server.js');
234
- // @ts-ignore
235
- const { ClaudeCodeServer } = module;
236
-
237
- const server = new ClaudeCodeServer();
238
- return { server, handlers };
239
- }
240
-
241
- beforeEach(() => {
242
- handlers = new Map();
243
- // Re-setup mocks after vi.resetModules() in outer beforeEach
244
- mockHomedir.mockReturnValue('/home/user');
245
- mockExistsSync.mockReturnValue(true);
246
- });
247
-
248
- it('should validate workFolder is a string when provided', async () => {
249
- mockHomedir.mockReturnValue('/home/user');
250
- mockExistsSync.mockReturnValue(true);
251
-
252
- await setupServer();
253
- const handler = handlers.get('callTool')!;
254
-
255
- // Test with non-string workFolder
256
- await expect(
257
- handler({
258
- params: {
259
- name: 'run',
260
- arguments: {
261
- prompt: 'test',
262
- workFolder: 123 // Invalid type
263
- }
264
- }
265
- })
266
- ).rejects.toThrow();
267
- });
268
-
269
- it('should reject empty string prompt', async () => {
270
- await setupServer();
271
- const handler = handlers.get('callTool')!;
272
-
273
- // Empty string prompt should be rejected
274
- await expect(
275
- handler({
276
- params: {
277
- name: 'run',
278
- arguments: {
279
- prompt: '', // Empty prompt
280
- workFolder: '/tmp'
281
- }
282
- }
283
- })
284
- ).rejects.toThrow('Either prompt or prompt_file must be provided');
285
- });
286
-
287
- it('should reject invalid reasoning_effort values', async () => {
288
- await setupServer();
289
- const handler = handlers.get('callTool')!;
290
-
291
- await expect(
292
- handler({
293
- params: {
294
- name: 'run',
295
- arguments: {
296
- prompt: 'test',
297
- workFolder: '/tmp',
298
- model: 'gpt-5.2-codex',
299
- reasoning_effort: 'fast'
300
- }
301
- }
302
- })
303
- ).rejects.toThrow(/reasoning_effort/i);
304
- });
305
-
306
- it('should reject reasoning_effort for unsupported model families', async () => {
307
- await setupServer();
308
- const handler = handlers.get('callTool')!;
309
-
310
- await expect(
311
- handler({
312
- params: {
313
- name: 'run',
314
- arguments: {
315
- prompt: 'test',
316
- workFolder: '/tmp',
317
- model: 'gemini-2.5-pro',
318
- reasoning_effort: 'low'
319
- }
320
- }
321
- })
322
- ).rejects.toThrow(/reasoning_effort/i);
323
- });
324
-
325
- it.each([
326
- 'oc-',
327
- 'oc-openai',
328
- 'oc-/gpt-5.4',
329
- 'oc-openai/',
330
- ' oc-openai/gpt-5.4',
331
- 'oc-openai/gpt-5.4 ',
332
- ])('should reject malformed OpenCode model syntax at runtime: %s', async (model) => {
333
- await setupServer();
334
- const handler = handlers.get('callTool')!;
335
-
336
- await expect(
337
- handler({
338
- params: {
339
- name: 'run',
340
- arguments: {
341
- prompt: 'test',
342
- workFolder: '/tmp',
343
- model,
344
- }
345
- }
346
- })
347
- ).rejects.toThrow('Invalid OpenCode model. Expected exact syntax oc-<provider/model>.');
348
- });
349
-
350
- it('should reject reasoning_effort for OpenCode runtime requests', async () => {
351
- await setupServer();
352
- const handler = handlers.get('callTool')!;
353
-
354
- await expect(
355
- handler({
356
- params: {
357
- name: 'run',
358
- arguments: {
359
- prompt: 'test',
360
- workFolder: '/tmp',
361
- model: 'opencode',
362
- reasoning_effort: 'high',
363
- }
364
- }
365
- })
366
- ).rejects.toThrow('reasoning_effort is not supported for opencode.');
367
- });
368
- });
369
- });
@@ -1,81 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { mkdtempSync, rmSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import { createTestClient, MCPTestClient } from './utils/mcp-client.js';
6
- import { getSharedMock } from './utils/persistent-mock.js';
7
-
8
- describe('Version Print on First Use', () => {
9
- let client: MCPTestClient;
10
- let testDir: string;
11
- let consoleErrorSpy: any;
12
-
13
- beforeEach(async () => {
14
- // Ensure mock exists
15
- await getSharedMock();
16
-
17
- // Create a temporary directory for test files
18
- testDir = mkdtempSync(join(tmpdir(), 'claude-code-test-'));
19
-
20
- // Spy on console.error
21
- consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
22
-
23
- client = createTestClient({ debug: false });
24
- await client.connect();
25
- });
26
-
27
- afterEach(async () => {
28
- // Disconnect client
29
- await client.disconnect();
30
-
31
- // Clean up test directory
32
- rmSync(testDir, { recursive: true, force: true });
33
-
34
- // Restore console.error spy
35
- consoleErrorSpy.mockRestore();
36
- });
37
-
38
- it('should print version and startup time only on first use', async () => {
39
- // First tool call
40
- await client.callTool('run', {
41
- prompt: 'echo "test 1"',
42
- workFolder: testDir,
43
- });
44
-
45
- // Find the version print in the console.error calls
46
- const findVersionCall = (calls: any[][]) => {
47
- return calls.find(call => {
48
- const str = call[1] || call[0]; // message might be first or second param
49
- return typeof str === 'string' && str.includes('ai_cli_mcp v') && str.includes('started at');
50
- });
51
- };
52
-
53
- // Check that version was printed on first use
54
- const versionCall = findVersionCall(consoleErrorSpy.mock.calls);
55
- expect(versionCall).toBeDefined();
56
- expect(versionCall![1]).toMatch(/ai_cli_mcp v[0-9]+\.[0-9]+\.[0-9]+ started at \d{4}-\d{2}-\d{2}T/);
57
-
58
- // Clear the spy but keep the spy active
59
- consoleErrorSpy.mockClear();
60
-
61
- // Second tool call
62
- await client.callTool('run', {
63
- prompt: 'echo "test 2"',
64
- workFolder: testDir,
65
- });
66
-
67
- // Check that version was NOT printed on second use
68
- const secondVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
69
- expect(secondVersionCall).toBeUndefined();
70
-
71
- // Third tool call
72
- await client.callTool('run', {
73
- prompt: 'echo "test 3"',
74
- workFolder: testDir,
75
- });
76
-
77
- // Should still not have been called with version print
78
- const thirdVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
79
- expect(thirdVersionCall).toBeUndefined();
80
- });
81
- });