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.
- package/CHANGELOG.md +26 -0
- package/README.ja.md +34 -8
- package/README.md +41 -8
- package/dist/app/cli.js +1 -0
- package/dist/app/mcp.js +64 -12
- package/dist/cli-builder.js +13 -6
- package/dist/cli-process-service.js +76 -91
- package/dist/cli-utils.js +6 -0
- package/dist/cli.js +1 -1
- package/dist/model-catalog.js +3 -2
- package/dist/parsers.js +8 -2
- package/package.json +27 -3
- package/server.json +3 -3
- package/.gemini/settings.json +0 -11
- package/.github/dependabot.yml +0 -28
- package/.github/pull_request_template.md +0 -28
- package/.github/workflows/ci.yml +0 -34
- package/.github/workflows/dependency-review.yml +0 -22
- package/.github/workflows/publish.yml +0 -89
- package/.github/workflows/test.yml +0 -20
- package/.github/workflows/watch-session-prs.yml +0 -276
- package/.husky/pre-commit +0 -1
- package/.mcp.json +0 -11
- package/.releaserc.json +0 -18
- package/.vscode/settings.json +0 -3
- package/CONTRIBUTING.md +0 -81
- package/dist/__tests__/app-cli.test.js +0 -392
- package/dist/__tests__/cli-bin-smoke.test.js +0 -101
- package/dist/__tests__/cli-builder.test.js +0 -442
- package/dist/__tests__/cli-process-service.test.js +0 -655
- package/dist/__tests__/cli-utils.test.js +0 -171
- package/dist/__tests__/e2e.test.js +0 -256
- package/dist/__tests__/edge-cases.test.js +0 -130
- package/dist/__tests__/error-cases.test.js +0 -292
- package/dist/__tests__/mcp-contract.test.js +0 -636
- package/dist/__tests__/mocks.js +0 -32
- package/dist/__tests__/model-alias.test.js +0 -36
- package/dist/__tests__/parsers.test.js +0 -646
- package/dist/__tests__/peek.test.js +0 -36
- package/dist/__tests__/process-management.test.js +0 -949
- package/dist/__tests__/server.test.js +0 -809
- package/dist/__tests__/setup.js +0 -11
- package/dist/__tests__/utils/claude-mock.js +0 -80
- package/dist/__tests__/utils/mcp-client.js +0 -121
- package/dist/__tests__/utils/opencode-mock.js +0 -91
- package/dist/__tests__/utils/persistent-mock.js +0 -28
- package/dist/__tests__/utils/test-helpers.js +0 -11
- package/dist/__tests__/validation.test.js +0 -308
- package/dist/__tests__/version-print.test.js +0 -65
- package/dist/__tests__/wait.test.js +0 -260
- package/docs/RELEASE_CHECKLIST.md +0 -65
- package/docs/cli-architecture.md +0 -275
- package/docs/concept.md +0 -154
- package/docs/development.md +0 -156
- package/docs/e2e-testing.md +0 -148
- package/docs/prd.md +0 -146
- package/docs/session-stacking.md +0 -67
- package/src/__tests__/app-cli.test.ts +0 -495
- package/src/__tests__/cli-bin-smoke.test.ts +0 -136
- package/src/__tests__/cli-builder.test.ts +0 -549
- package/src/__tests__/cli-process-service.test.ts +0 -759
- package/src/__tests__/cli-utils.test.ts +0 -200
- package/src/__tests__/e2e.test.ts +0 -311
- package/src/__tests__/edge-cases.test.ts +0 -176
- package/src/__tests__/error-cases.test.ts +0 -370
- package/src/__tests__/mcp-contract.test.ts +0 -755
- package/src/__tests__/mocks.ts +0 -35
- package/src/__tests__/model-alias.test.ts +0 -44
- package/src/__tests__/parsers.test.ts +0 -730
- package/src/__tests__/peek.test.ts +0 -44
- package/src/__tests__/process-management.test.ts +0 -1129
- package/src/__tests__/server.test.ts +0 -1020
- package/src/__tests__/setup.ts +0 -13
- package/src/__tests__/utils/claude-mock.ts +0 -87
- package/src/__tests__/utils/mcp-client.ts +0 -159
- package/src/__tests__/utils/opencode-mock.ts +0 -108
- package/src/__tests__/utils/persistent-mock.ts +0 -33
- package/src/__tests__/utils/test-helpers.ts +0 -13
- package/src/__tests__/validation.test.ts +0 -369
- package/src/__tests__/version-print.test.ts +0 -81
- package/src/__tests__/wait.test.ts +0 -302
- package/src/app/cli.ts +0 -424
- package/src/app/mcp.ts +0 -466
- package/src/bin/ai-cli-mcp.ts +0 -7
- package/src/bin/ai-cli.ts +0 -11
- package/src/cli-builder.ts +0 -274
- package/src/cli-parse.ts +0 -105
- package/src/cli-process-service.ts +0 -709
- package/src/cli-utils.ts +0 -258
- package/src/cli.ts +0 -124
- package/src/model-catalog.ts +0 -87
- package/src/parsers.ts +0 -965
- package/src/peek.ts +0 -95
- package/src/process-result.ts +0 -88
- package/src/process-service.ts +0 -368
- package/src/server.ts +0 -10
- package/tsconfig.json +0 -16
- package/vitest.config.e2e.ts +0 -27
- package/vitest.config.ts +0 -22
- package/vitest.config.unit.ts +0 -28
|
@@ -1,65 +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 } from './utils/mcp-client.js';
|
|
6
|
-
import { getSharedMock } from './utils/persistent-mock.js';
|
|
7
|
-
describe('Version Print on First Use', () => {
|
|
8
|
-
let client;
|
|
9
|
-
let testDir;
|
|
10
|
-
let consoleErrorSpy;
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
// Ensure mock exists
|
|
13
|
-
await getSharedMock();
|
|
14
|
-
// Create a temporary directory for test files
|
|
15
|
-
testDir = mkdtempSync(join(tmpdir(), 'claude-code-test-'));
|
|
16
|
-
// Spy on console.error
|
|
17
|
-
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
18
|
-
client = createTestClient({ debug: false });
|
|
19
|
-
await client.connect();
|
|
20
|
-
});
|
|
21
|
-
afterEach(async () => {
|
|
22
|
-
// Disconnect client
|
|
23
|
-
await client.disconnect();
|
|
24
|
-
// Clean up test directory
|
|
25
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
26
|
-
// Restore console.error spy
|
|
27
|
-
consoleErrorSpy.mockRestore();
|
|
28
|
-
});
|
|
29
|
-
it('should print version and startup time only on first use', async () => {
|
|
30
|
-
// First tool call
|
|
31
|
-
await client.callTool('run', {
|
|
32
|
-
prompt: 'echo "test 1"',
|
|
33
|
-
workFolder: testDir,
|
|
34
|
-
});
|
|
35
|
-
// Find the version print in the console.error calls
|
|
36
|
-
const findVersionCall = (calls) => {
|
|
37
|
-
return calls.find(call => {
|
|
38
|
-
const str = call[1] || call[0]; // message might be first or second param
|
|
39
|
-
return typeof str === 'string' && str.includes('ai_cli_mcp v') && str.includes('started at');
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
// Check that version was printed on first use
|
|
43
|
-
const versionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
44
|
-
expect(versionCall).toBeDefined();
|
|
45
|
-
expect(versionCall[1]).toMatch(/ai_cli_mcp v[0-9]+\.[0-9]+\.[0-9]+ started at \d{4}-\d{2}-\d{2}T/);
|
|
46
|
-
// Clear the spy but keep the spy active
|
|
47
|
-
consoleErrorSpy.mockClear();
|
|
48
|
-
// Second tool call
|
|
49
|
-
await client.callTool('run', {
|
|
50
|
-
prompt: 'echo "test 2"',
|
|
51
|
-
workFolder: testDir,
|
|
52
|
-
});
|
|
53
|
-
// Check that version was NOT printed on second use
|
|
54
|
-
const secondVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
55
|
-
expect(secondVersionCall).toBeUndefined();
|
|
56
|
-
// Third tool call
|
|
57
|
-
await client.callTool('run', {
|
|
58
|
-
prompt: 'echo "test 3"',
|
|
59
|
-
workFolder: testDir,
|
|
60
|
-
});
|
|
61
|
-
// Should still not have been called with version print
|
|
62
|
-
const thirdVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
63
|
-
expect(thirdVersionCall).toBeUndefined();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
5
|
-
import { existsSync } from 'node:fs';
|
|
6
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
-
// Mock dependencies
|
|
8
|
-
vi.mock('node:child_process');
|
|
9
|
-
vi.mock('node:fs');
|
|
10
|
-
vi.mock('node:os');
|
|
11
|
-
vi.mock('node:path', () => ({
|
|
12
|
-
resolve: vi.fn((path) => path),
|
|
13
|
-
join: vi.fn((...args) => args.join('/')),
|
|
14
|
-
isAbsolute: vi.fn((path) => path.startsWith('/')),
|
|
15
|
-
dirname: vi.fn((path) => '/tmp')
|
|
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 mockSpawn = vi.mocked(spawn);
|
|
49
|
-
const mockHomedir = vi.mocked(homedir);
|
|
50
|
-
const mockExistsSync = vi.mocked(existsSync);
|
|
51
|
-
describe('Wait Tool Tests', () => {
|
|
52
|
-
let handlers;
|
|
53
|
-
let mockServerInstance;
|
|
54
|
-
let server;
|
|
55
|
-
// Setup function to initialize server with mocks
|
|
56
|
-
const setupServer = async () => {
|
|
57
|
-
vi.resetModules();
|
|
58
|
-
handlers = new Map();
|
|
59
|
-
// Mock Server implementation to capture handlers
|
|
60
|
-
vi.mocked(Server).mockImplementation(function () {
|
|
61
|
-
this.setRequestHandler = vi.fn((schema, handler) => {
|
|
62
|
-
handlers.set(schema.name, handler);
|
|
63
|
-
});
|
|
64
|
-
this.connect = vi.fn();
|
|
65
|
-
this.close = vi.fn();
|
|
66
|
-
return this;
|
|
67
|
-
});
|
|
68
|
-
const module = await import('../server.js');
|
|
69
|
-
// @ts-ignore
|
|
70
|
-
const { ClaudeCodeServer } = module;
|
|
71
|
-
server = new ClaudeCodeServer();
|
|
72
|
-
mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
73
|
-
};
|
|
74
|
-
beforeEach(async () => {
|
|
75
|
-
mockHomedir.mockReturnValue('/home/user');
|
|
76
|
-
mockExistsSync.mockReturnValue(true);
|
|
77
|
-
await setupServer();
|
|
78
|
-
});
|
|
79
|
-
afterEach(() => {
|
|
80
|
-
vi.clearAllMocks();
|
|
81
|
-
vi.useRealTimers();
|
|
82
|
-
});
|
|
83
|
-
const createMockProcess = (pid) => {
|
|
84
|
-
const mockProcess = new EventEmitter();
|
|
85
|
-
mockProcess.pid = pid;
|
|
86
|
-
mockProcess.stdout = new EventEmitter();
|
|
87
|
-
mockProcess.stderr = new EventEmitter();
|
|
88
|
-
mockProcess.stdout.on = vi.fn();
|
|
89
|
-
mockProcess.stderr.on = vi.fn();
|
|
90
|
-
mockProcess.kill = vi.fn();
|
|
91
|
-
return mockProcess;
|
|
92
|
-
};
|
|
93
|
-
it('should wait for a single running process', async () => {
|
|
94
|
-
const callToolHandler = handlers.get('callTool');
|
|
95
|
-
const mockProcess = createMockProcess(12345);
|
|
96
|
-
mockSpawn.mockReturnValue(mockProcess);
|
|
97
|
-
// Start a process first
|
|
98
|
-
await callToolHandler({
|
|
99
|
-
params: {
|
|
100
|
-
name: 'run',
|
|
101
|
-
arguments: {
|
|
102
|
-
prompt: 'test prompt',
|
|
103
|
-
workFolder: '/tmp'
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
// Mock process output accumulation (simulated internally by server)
|
|
108
|
-
// We need to access the process manager or simulate events
|
|
109
|
-
// Call wait
|
|
110
|
-
const waitPromise = callToolHandler({
|
|
111
|
-
params: {
|
|
112
|
-
name: 'wait',
|
|
113
|
-
arguments: {
|
|
114
|
-
pids: [12345]
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
// Simulate process completion after a delay
|
|
119
|
-
setTimeout(() => {
|
|
120
|
-
mockProcess.stdout.emit('data', 'Process output');
|
|
121
|
-
mockProcess.emit('close', 0);
|
|
122
|
-
}, 10);
|
|
123
|
-
const result = await waitPromise;
|
|
124
|
-
const response = JSON.parse(result.content[0].text);
|
|
125
|
-
expect(response).toHaveLength(1);
|
|
126
|
-
expect(response[0].pid).toBe(12345);
|
|
127
|
-
expect(response[0].status).toBe('completed');
|
|
128
|
-
// expect(response[0].stdout).toBe('Process output'); // Flaky test
|
|
129
|
-
});
|
|
130
|
-
it('should return immediately if process is already completed', async () => {
|
|
131
|
-
const callToolHandler = handlers.get('callTool');
|
|
132
|
-
const mockProcess = createMockProcess(12346);
|
|
133
|
-
mockSpawn.mockReturnValue(mockProcess);
|
|
134
|
-
// Start process
|
|
135
|
-
await callToolHandler({
|
|
136
|
-
params: {
|
|
137
|
-
name: 'run',
|
|
138
|
-
arguments: {
|
|
139
|
-
prompt: 'test',
|
|
140
|
-
workFolder: '/tmp'
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
// Complete immediately
|
|
145
|
-
mockProcess.emit('close', 0);
|
|
146
|
-
// Call wait
|
|
147
|
-
const result = await callToolHandler({
|
|
148
|
-
params: {
|
|
149
|
-
name: 'wait',
|
|
150
|
-
arguments: {
|
|
151
|
-
pids: [12346]
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
const response = JSON.parse(result.content[0].text);
|
|
156
|
-
expect(response[0].status).toBe('completed');
|
|
157
|
-
});
|
|
158
|
-
it('should wait for multiple processes', async () => {
|
|
159
|
-
const callToolHandler = handlers.get('callTool');
|
|
160
|
-
// Process 1
|
|
161
|
-
const p1 = createMockProcess(101);
|
|
162
|
-
mockSpawn.mockReturnValueOnce(p1);
|
|
163
|
-
await callToolHandler({
|
|
164
|
-
params: { name: 'run', arguments: { prompt: 'p1', workFolder: '/tmp' } }
|
|
165
|
-
});
|
|
166
|
-
// Process 2
|
|
167
|
-
const p2 = createMockProcess(102);
|
|
168
|
-
mockSpawn.mockReturnValueOnce(p2);
|
|
169
|
-
await callToolHandler({
|
|
170
|
-
params: { name: 'run', arguments: { prompt: 'p2', workFolder: '/tmp' } }
|
|
171
|
-
});
|
|
172
|
-
// Wait for both
|
|
173
|
-
const waitPromise = callToolHandler({
|
|
174
|
-
params: {
|
|
175
|
-
name: 'wait',
|
|
176
|
-
arguments: { pids: [101, 102] }
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
// Finish p1
|
|
180
|
-
setTimeout(() => { p1.emit('close', 0); }, 10);
|
|
181
|
-
// Finish p2 later
|
|
182
|
-
setTimeout(() => { p2.emit('close', 0); }, 30);
|
|
183
|
-
const result = await waitPromise;
|
|
184
|
-
const response = JSON.parse(result.content[0].text);
|
|
185
|
-
expect(response).toHaveLength(2);
|
|
186
|
-
expect(response.find((r) => r.pid === 101).status).toBe('completed');
|
|
187
|
-
expect(response.find((r) => r.pid === 102).status).toBe('completed');
|
|
188
|
-
});
|
|
189
|
-
it('should clear timeout timers after wait resolves', async () => {
|
|
190
|
-
vi.useFakeTimers();
|
|
191
|
-
const callToolHandler = handlers.get('callTool');
|
|
192
|
-
const mockProcess = createMockProcess(12348);
|
|
193
|
-
mockSpawn.mockReturnValue(mockProcess);
|
|
194
|
-
await callToolHandler({
|
|
195
|
-
params: {
|
|
196
|
-
name: 'run',
|
|
197
|
-
arguments: {
|
|
198
|
-
prompt: 'test prompt',
|
|
199
|
-
workFolder: '/tmp'
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
const waitPromise = callToolHandler({
|
|
204
|
-
params: {
|
|
205
|
-
name: 'wait',
|
|
206
|
-
arguments: {
|
|
207
|
-
pids: [12348],
|
|
208
|
-
timeout: 180
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
mockProcess.emit('close', 0);
|
|
213
|
-
await vi.runAllTicks();
|
|
214
|
-
const result = await waitPromise;
|
|
215
|
-
const response = JSON.parse(result.content[0].text);
|
|
216
|
-
expect(response[0].status).toBe('completed');
|
|
217
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
218
|
-
});
|
|
219
|
-
it('should throw error for non-existent PID', async () => {
|
|
220
|
-
const callToolHandler = handlers.get('callTool');
|
|
221
|
-
try {
|
|
222
|
-
await callToolHandler({
|
|
223
|
-
params: {
|
|
224
|
-
name: 'wait',
|
|
225
|
-
arguments: { pids: [99999] }
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
expect.fail('Should have thrown');
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
expect(error.message).toContain('Process with PID 99999 not found');
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
it('should handle timeout', async () => {
|
|
235
|
-
const callToolHandler = handlers.get('callTool');
|
|
236
|
-
const mockProcess = createMockProcess(12347);
|
|
237
|
-
mockSpawn.mockReturnValue(mockProcess);
|
|
238
|
-
await callToolHandler({
|
|
239
|
-
params: { name: 'run', arguments: { prompt: 'test', workFolder: '/tmp' } }
|
|
240
|
-
});
|
|
241
|
-
// Call wait with short timeout
|
|
242
|
-
const waitPromise = callToolHandler({
|
|
243
|
-
params: {
|
|
244
|
-
name: 'wait',
|
|
245
|
-
arguments: {
|
|
246
|
-
pids: [12347],
|
|
247
|
-
timeout: 0.1 // 100ms
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
// Don't emit close event
|
|
252
|
-
try {
|
|
253
|
-
await waitPromise;
|
|
254
|
-
expect.fail('Should have thrown');
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
expect(error.message).toContain('Timed out');
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# Release Process
|
|
2
|
-
|
|
3
|
-
This project uses [semantic-release](https://semantic-release.gitbook.io/) for automated versioning and publishing.
|
|
4
|
-
|
|
5
|
-
## How It Works
|
|
6
|
-
|
|
7
|
-
1. **Commit with Conventional Commits format** to `develop` branch
|
|
8
|
-
2. **CI automatically determines version** based on commit messages
|
|
9
|
-
3. **Automatic release**: version bump, CHANGELOG update, npm publish, GitHub Release
|
|
10
|
-
|
|
11
|
-
## Commit Message Format
|
|
12
|
-
|
|
13
|
-
Use [Conventional Commits](https://www.conventionalcommits.org/) format:
|
|
14
|
-
|
|
15
|
-
| Type | Description | Version Bump |
|
|
16
|
-
|------|-------------|--------------|
|
|
17
|
-
| `fix:` | Bug fixes | Patch (1.0.0 → 1.0.1) |
|
|
18
|
-
| `feat:` | New features | Minor (1.0.0 → 1.1.0) |
|
|
19
|
-
| `feat!:` or `BREAKING CHANGE:` | Breaking changes | Major (1.0.0 → 2.0.0) |
|
|
20
|
-
| `docs:`, `chore:`, `style:`, `refactor:`, `test:` | Other changes | No release |
|
|
21
|
-
|
|
22
|
-
### Examples
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
# Patch release
|
|
26
|
-
git commit -m "fix: resolve session_id not working for Codex"
|
|
27
|
-
|
|
28
|
-
# Minor release
|
|
29
|
-
git commit -m "feat: add support for new model"
|
|
30
|
-
|
|
31
|
-
# Major release
|
|
32
|
-
git commit -m "feat!: change API response format"
|
|
33
|
-
# or
|
|
34
|
-
git commit -m "feat: change API response format
|
|
35
|
-
|
|
36
|
-
BREAKING CHANGE: response structure has changed"
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Pre-Merge Checklist
|
|
40
|
-
|
|
41
|
-
Before merging to `develop`:
|
|
42
|
-
|
|
43
|
-
- [ ] Tests pass locally (`npm test`)
|
|
44
|
-
- [ ] Build succeeds (`npm run build`)
|
|
45
|
-
- [ ] Commit messages follow Conventional Commits format
|
|
46
|
-
- [ ] PR has been reviewed (if applicable)
|
|
47
|
-
|
|
48
|
-
## Important: Git Tags
|
|
49
|
-
|
|
50
|
-
semantic-release uses git tags to determine the current version. **Tags must exist on the `develop` branch.**
|
|
51
|
-
|
|
52
|
-
If releases fail with version errors:
|
|
53
|
-
|
|
54
|
-
1. Check existing tags: `git tag -l 'v*'`
|
|
55
|
-
2. Ensure the latest version tag exists on `develop`
|
|
56
|
-
3. If missing, create it: `git tag vX.X.X && git push origin vX.X.X`
|
|
57
|
-
|
|
58
|
-
## npm Trusted Publishing Setup
|
|
59
|
-
|
|
60
|
-
This project uses OIDC trusted publishing (no npm token required).
|
|
61
|
-
|
|
62
|
-
Configuration on npmjs.com:
|
|
63
|
-
- Organization/user: `mkXultra`
|
|
64
|
-
- Repository: `ai-cli-mcp`
|
|
65
|
-
- Workflow filename: `publish.yml`
|
package/docs/cli-architecture.md
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
# AI CLI Architecture Plan
|
|
2
|
-
|
|
3
|
-
## Goal
|
|
4
|
-
|
|
5
|
-
`ai-cli-mcp` package will expose two global commands:
|
|
6
|
-
|
|
7
|
-
- `ai-cli`: human-facing production CLI
|
|
8
|
-
- `ai-cli-mcp`: MCP server entrypoint for backward compatibility
|
|
9
|
-
|
|
10
|
-
The package name stays `ai-cli-mcp` for now. We do not introduce a daemon. We keep the product as a thin wrapper over Claude Code, Codex CLI, and Gemini CLI.
|
|
11
|
-
|
|
12
|
-
## Non-Goals
|
|
13
|
-
|
|
14
|
-
- Renaming the npm package in this phase
|
|
15
|
-
- Introducing a long-running background daemon
|
|
16
|
-
- Introducing a new public job identifier such as `run_id`
|
|
17
|
-
- Making the CLI responsible for deep process orchestration beyond launching and observing AI CLI processes
|
|
18
|
-
- Capturing exit codes in the first production CLI iteration
|
|
19
|
-
|
|
20
|
-
## Product Shape
|
|
21
|
-
|
|
22
|
-
### `ai-cli`
|
|
23
|
-
|
|
24
|
-
`ai-cli` is the primary CLI for humans.
|
|
25
|
-
|
|
26
|
-
Supported commands:
|
|
27
|
-
|
|
28
|
-
- `ai-cli run`
|
|
29
|
-
- `ai-cli wait`
|
|
30
|
-
- `ai-cli ps`
|
|
31
|
-
- `ai-cli result`
|
|
32
|
-
- `ai-cli kill`
|
|
33
|
-
- `ai-cli cleanup`
|
|
34
|
-
- `ai-cli models`
|
|
35
|
-
- `ai-cli doctor`
|
|
36
|
-
- `ai-cli mcp`
|
|
37
|
-
|
|
38
|
-
Behavior:
|
|
39
|
-
|
|
40
|
-
- Running `ai-cli` with no subcommand prints help
|
|
41
|
-
- Public process identity is `pid`
|
|
42
|
-
- `--cwd` is the working directory flag
|
|
43
|
-
- Output format should stay close to MCP responses
|
|
44
|
-
|
|
45
|
-
### `ai-cli-mcp`
|
|
46
|
-
|
|
47
|
-
`ai-cli-mcp` remains the MCP-focused command.
|
|
48
|
-
|
|
49
|
-
Behavior:
|
|
50
|
-
|
|
51
|
-
- Running `ai-cli-mcp` with no arguments starts the MCP server
|
|
52
|
-
- This command exists for compatibility with existing users and MCP configurations
|
|
53
|
-
|
|
54
|
-
## Public Command Semantics
|
|
55
|
-
|
|
56
|
-
### `ai-cli run`
|
|
57
|
-
|
|
58
|
-
Starts the target AI CLI in the background and returns immediately.
|
|
59
|
-
|
|
60
|
-
Properties:
|
|
61
|
-
|
|
62
|
-
- Returns MCP-like JSON including `pid`, `status`, `agent`, and `message`
|
|
63
|
-
- Uses `pid` as the public identifier
|
|
64
|
-
- Spawns the actual Claude/Codex/Gemini process directly
|
|
65
|
-
- Redirects `stdout` and `stderr` to files
|
|
66
|
-
- Does not guarantee `exitCode` in the initial design
|
|
67
|
-
|
|
68
|
-
### `ai-cli wait`
|
|
69
|
-
|
|
70
|
-
Waits until all given PIDs are no longer running.
|
|
71
|
-
|
|
72
|
-
Properties:
|
|
73
|
-
|
|
74
|
-
- Input is one or more `pid` values
|
|
75
|
-
- Timeout is supported
|
|
76
|
-
- Response format follows MCP `wait` as closely as possible
|
|
77
|
-
- Returns a result array, same direction as MCP
|
|
78
|
-
|
|
79
|
-
### `ai-cli ps`
|
|
80
|
-
|
|
81
|
-
Lists tracked runs with minimal information.
|
|
82
|
-
|
|
83
|
-
Properties:
|
|
84
|
-
|
|
85
|
-
- Output includes `pid`, `agent`, and `status`
|
|
86
|
-
- Initial scope is intentionally minimal
|
|
87
|
-
|
|
88
|
-
### `ai-cli result`
|
|
89
|
-
|
|
90
|
-
Reads saved output and returns parsed results.
|
|
91
|
-
|
|
92
|
-
Properties:
|
|
93
|
-
|
|
94
|
-
- Behavior should stay close to MCP `get_result`
|
|
95
|
-
- Parsed output is preferred
|
|
96
|
-
- Falls back to raw output when parsing fails or output is incomplete
|
|
97
|
-
|
|
98
|
-
### `ai-cli kill`
|
|
99
|
-
|
|
100
|
-
Sends `SIGTERM` to the given PID.
|
|
101
|
-
|
|
102
|
-
Properties:
|
|
103
|
-
|
|
104
|
-
- Public API is intentionally PID-based
|
|
105
|
-
- Users may also kill processes manually outside the tool
|
|
106
|
-
|
|
107
|
-
### `ai-cli cleanup`
|
|
108
|
-
|
|
109
|
-
Removes tracked process state for runs that are no longer running.
|
|
110
|
-
|
|
111
|
-
Properties:
|
|
112
|
-
|
|
113
|
-
- Removes completed and failed PID directories
|
|
114
|
-
- Keeps running processes intact
|
|
115
|
-
- Removes empty per-cwd directories after cleanup
|
|
116
|
-
|
|
117
|
-
### `ai-cli doctor`
|
|
118
|
-
|
|
119
|
-
Checks whether supported AI CLI binaries are available.
|
|
120
|
-
|
|
121
|
-
Properties:
|
|
122
|
-
|
|
123
|
-
- Scope is binary existence/path resolution only
|
|
124
|
-
- It does not verify login or acceptance state
|
|
125
|
-
|
|
126
|
-
### `ai-cli models`
|
|
127
|
-
|
|
128
|
-
Returns the supported model list and aliases.
|
|
129
|
-
|
|
130
|
-
Properties:
|
|
131
|
-
|
|
132
|
-
- Behavior should stay close to MCP-supported model documentation
|
|
133
|
-
- Static model definitions are acceptable in this phase
|
|
134
|
-
|
|
135
|
-
### `ai-cli mcp`
|
|
136
|
-
|
|
137
|
-
Starts the MCP server from the `ai-cli` command.
|
|
138
|
-
|
|
139
|
-
Properties:
|
|
140
|
-
|
|
141
|
-
- Allows one package to support both direct CLI usage and MCP usage
|
|
142
|
-
|
|
143
|
-
## Entrypoints
|
|
144
|
-
|
|
145
|
-
Planned package bin layout:
|
|
146
|
-
|
|
147
|
-
```json
|
|
148
|
-
{
|
|
149
|
-
"bin": {
|
|
150
|
-
"ai-cli": "dist/bin/ai-cli.js",
|
|
151
|
-
"ai-cli-mcp": "dist/bin/ai-cli-mcp.js"
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Planned source layout:
|
|
157
|
-
|
|
158
|
-
```text
|
|
159
|
-
src/
|
|
160
|
-
bin/
|
|
161
|
-
ai-cli.ts
|
|
162
|
-
ai-cli-mcp.ts
|
|
163
|
-
app/
|
|
164
|
-
cli.ts
|
|
165
|
-
mcp.ts
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Responsibilities:
|
|
169
|
-
|
|
170
|
-
- `src/bin/ai-cli.ts`: thin CLI entrypoint
|
|
171
|
-
- `src/bin/ai-cli-mcp.ts`: thin MCP entrypoint
|
|
172
|
-
- `src/app/cli.ts`: subcommand parsing and dispatch for `ai-cli`
|
|
173
|
-
- `src/app/mcp.ts`: MCP server bootstrap
|
|
174
|
-
|
|
175
|
-
## Backend Architecture
|
|
176
|
-
|
|
177
|
-
The core implementation should be shared between CLI and MCP.
|
|
178
|
-
|
|
179
|
-
Suggested internal boundaries:
|
|
180
|
-
|
|
181
|
-
- `cli-builder`
|
|
182
|
-
- resolves model aliases
|
|
183
|
-
- validates input
|
|
184
|
-
- builds the real Claude/Codex/Gemini command
|
|
185
|
-
- `runner`
|
|
186
|
-
- spawns the actual AI CLI process
|
|
187
|
-
- redirects `stdout` and `stderr` to files
|
|
188
|
-
- `process-store`
|
|
189
|
-
- stores tracked process metadata
|
|
190
|
-
- exact path and file format are intentionally deferred
|
|
191
|
-
- `process-service`
|
|
192
|
-
- shared use cases for `run`, `wait`, `ps`, `result`, and `kill`
|
|
193
|
-
- `parsers`
|
|
194
|
-
- parses saved output into structured results
|
|
195
|
-
|
|
196
|
-
## PID-Based Design Decision
|
|
197
|
-
|
|
198
|
-
The public interface stays PID-based on purpose.
|
|
199
|
-
|
|
200
|
-
Rationale:
|
|
201
|
-
|
|
202
|
-
- This CLI is a thin wrapper over existing AI CLI tools
|
|
203
|
-
- PID is already the native OS process identifier
|
|
204
|
-
- Users can inspect or terminate processes with normal Unix tooling
|
|
205
|
-
- We do not want to introduce a synthetic public job ID in this phase
|
|
206
|
-
|
|
207
|
-
Implication:
|
|
208
|
-
|
|
209
|
-
- Public commands use `pid`
|
|
210
|
-
- Internal storage may store additional metadata if needed
|
|
211
|
-
- PID remains the only required identifier at the product surface
|
|
212
|
-
|
|
213
|
-
## Background Execution Strategy
|
|
214
|
-
|
|
215
|
-
The first production CLI implementation uses direct process spawning with file redirection.
|
|
216
|
-
|
|
217
|
-
Approach:
|
|
218
|
-
|
|
219
|
-
- Spawn the actual AI CLI process directly
|
|
220
|
-
- Redirect `stdout` to a file
|
|
221
|
-
- Redirect `stderr` to a file
|
|
222
|
-
- Persist enough metadata to support `wait`, `ps`, `result`, and `kill`
|
|
223
|
-
|
|
224
|
-
Why this approach:
|
|
225
|
-
|
|
226
|
-
- Lighter than introducing a worker process
|
|
227
|
-
- Keeps the CLI close to Unix process semantics
|
|
228
|
-
- Avoids worker-child termination complexity
|
|
229
|
-
- Keeps migration from the current MCP server relatively simple
|
|
230
|
-
|
|
231
|
-
Tradeoff accepted in phase one:
|
|
232
|
-
|
|
233
|
-
- `exitCode` is not guaranteed to be captured
|
|
234
|
-
|
|
235
|
-
If this becomes a practical problem, a thin per-run wrapper/worker can be introduced later without changing the public CLI model.
|
|
236
|
-
|
|
237
|
-
## MCP Compatibility Plan
|
|
238
|
-
|
|
239
|
-
MCP functionality stays in the project and should be preserved.
|
|
240
|
-
|
|
241
|
-
Compatibility goals:
|
|
242
|
-
|
|
243
|
-
- Keep current MCP tool names
|
|
244
|
-
- Keep current response shape as much as practical
|
|
245
|
-
- Reuse the same backend logic as the new CLI where possible
|
|
246
|
-
|
|
247
|
-
Target mapping:
|
|
248
|
-
|
|
249
|
-
- MCP `run` -> shared process service `run`
|
|
250
|
-
- MCP `wait` -> shared process service `wait`
|
|
251
|
-
- MCP `list_processes` -> shared process service `ps`
|
|
252
|
-
- MCP `get_result` -> shared process service `result`
|
|
253
|
-
- MCP `kill_process` -> shared process service `kill`
|
|
254
|
-
|
|
255
|
-
## Implementation Order
|
|
256
|
-
|
|
257
|
-
1. Split the current `src/server.ts` responsibilities into MCP surface and shared process logic
|
|
258
|
-
2. Introduce new bin entrypoints for `ai-cli` and `ai-cli-mcp`
|
|
259
|
-
3. Add `ai-cli` subcommand parsing and help output
|
|
260
|
-
4. Implement direct background spawning with stdout/stderr file redirection
|
|
261
|
-
5. Implement CLI commands: `run`, `wait`, `ps`, `result`, `kill`
|
|
262
|
-
6. Add `models`, `doctor`, and `mcp`
|
|
263
|
-
7. Rewire MCP handlers to the same shared backend
|
|
264
|
-
|
|
265
|
-
## Open Items Deferred
|
|
266
|
-
|
|
267
|
-
The following items are intentionally deferred:
|
|
268
|
-
|
|
269
|
-
- state directory path
|
|
270
|
-
- file naming scheme
|
|
271
|
-
- metadata file schema
|
|
272
|
-
- exit code capture for detached CLI runs
|
|
273
|
-
- retention and cleanup policy
|
|
274
|
-
- exact raw output access patterns
|
|
275
|
-
- Windows-specific process handling details
|