ai-cli-mcp 2.11.0 → 2.13.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 (55) hide show
  1. package/.github/workflows/publish.yml +25 -0
  2. package/CHANGELOG.md +23 -0
  3. package/README.ja.md +112 -8
  4. package/README.md +112 -9
  5. package/dist/__tests__/app-cli.test.js +293 -0
  6. package/dist/__tests__/cli-bin-smoke.test.js +58 -0
  7. package/dist/__tests__/cli-builder.test.js +37 -0
  8. package/dist/__tests__/cli-process-service.test.js +279 -0
  9. package/dist/__tests__/cli-utils.test.js +140 -0
  10. package/dist/__tests__/error-cases.test.js +2 -1
  11. package/dist/__tests__/mcp-contract.test.js +343 -0
  12. package/dist/__tests__/parsers.test.js +37 -1
  13. package/dist/__tests__/process-management.test.js +15 -8
  14. package/dist/__tests__/server.test.js +29 -3
  15. package/dist/__tests__/wait.test.js +31 -0
  16. package/dist/app/cli.js +304 -0
  17. package/dist/app/mcp.js +366 -0
  18. package/dist/bin/ai-cli-mcp.js +6 -0
  19. package/dist/bin/ai-cli.js +10 -0
  20. package/dist/cli-builder.js +15 -6
  21. package/dist/cli-parse.js +8 -5
  22. package/dist/cli-process-service.js +332 -0
  23. package/dist/cli-utils.js +159 -88
  24. package/dist/cli.js +4 -3
  25. package/dist/model-catalog.js +53 -0
  26. package/dist/parsers.js +55 -0
  27. package/dist/process-service.js +201 -0
  28. package/dist/server.js +4 -578
  29. package/docs/cli-architecture.md +275 -0
  30. package/package.json +4 -3
  31. package/server.json +1 -1
  32. package/src/__tests__/app-cli.test.ts +370 -0
  33. package/src/__tests__/cli-bin-smoke.test.ts +75 -0
  34. package/src/__tests__/cli-builder.test.ts +47 -0
  35. package/src/__tests__/cli-process-service.test.ts +334 -0
  36. package/src/__tests__/cli-utils.test.ts +166 -0
  37. package/src/__tests__/error-cases.test.ts +3 -4
  38. package/src/__tests__/mcp-contract.test.ts +422 -0
  39. package/src/__tests__/parsers.test.ts +44 -1
  40. package/src/__tests__/process-management.test.ts +15 -9
  41. package/src/__tests__/server.test.ts +27 -6
  42. package/src/__tests__/wait.test.ts +38 -0
  43. package/src/app/cli.ts +373 -0
  44. package/src/app/mcp.ts +402 -0
  45. package/src/bin/ai-cli-mcp.ts +7 -0
  46. package/src/bin/ai-cli.ts +11 -0
  47. package/src/cli-builder.ts +19 -10
  48. package/src/cli-parse.ts +8 -5
  49. package/src/cli-process-service.ts +418 -0
  50. package/src/cli-utils.ts +205 -99
  51. package/src/cli.ts +4 -3
  52. package/src/model-catalog.ts +64 -0
  53. package/src/parsers.ts +61 -0
  54. package/src/process-service.ts +263 -0
  55. package/src/server.ts +4 -668
@@ -78,6 +78,7 @@ describe('Wait Tool Tests', () => {
78
78
  });
79
79
  afterEach(() => {
80
80
  vi.clearAllMocks();
81
+ vi.useRealTimers();
81
82
  });
82
83
  const createMockProcess = (pid) => {
83
84
  const mockProcess = new EventEmitter();
@@ -185,6 +186,36 @@ describe('Wait Tool Tests', () => {
185
186
  expect(response.find((r) => r.pid === 101).status).toBe('completed');
186
187
  expect(response.find((r) => r.pid === 102).status).toBe('completed');
187
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
+ });
188
219
  it('should throw error for non-existent PID', async () => {
189
220
  const callToolHandler = handlers.get('callTool');
190
221
  try {
@@ -0,0 +1,304 @@
1
+ import { runMcpServer } from './mcp.js';
2
+ import { CliProcessService } from '../cli-process-service.js';
3
+ import { getCliDoctorStatus } from '../cli-utils.js';
4
+ import { getModelsPayload } from '../model-catalog.js';
5
+ export const CLI_HELP_TEXT = `Usage: ai-cli <command> [options]
6
+
7
+ Commands:
8
+ run Start an AI CLI process in the background
9
+ wait Wait for one or more pids
10
+ ps List tracked processes
11
+ result Get the current result for a pid
12
+ kill Terminate a tracked pid
13
+ cleanup Remove completed and failed tracked processes
14
+ doctor Check supported AI CLI binaries
15
+ models List supported models and aliases
16
+ mcp Start the MCP server
17
+ help Show this help message
18
+ `;
19
+ export const RUN_HELP_TEXT = `Usage: ai-cli run --cwd <path> [options]
20
+
21
+ Start an AI CLI process in the background.
22
+
23
+ Options:
24
+ --cwd <path> Working directory
25
+ --prompt <text> Prompt text
26
+ --prompt-file <path> Path to a prompt file
27
+ --model <model> Model name or alias (e.g. sonnet, claude-ultra, gpt-5.2-codex, codex-ultra, gemini-2.5-pro, gemini-ultra, forge)
28
+ --session-id <id> Resume a previous session
29
+ --reasoning-effort <level> Reasoning level for Claude/Codex only
30
+ --help, -h Show this help message
31
+
32
+ Compatibility aliases:
33
+ --workFolder, --work-folder
34
+ --prompt_file
35
+ --session_id
36
+ --reasoning_effort
37
+ `;
38
+ export const WAIT_HELP_TEXT = `Usage: ai-cli wait <pid...> [options]
39
+
40
+ Wait for one or more tracked processes to finish.
41
+
42
+ Options:
43
+ --timeout <seconds> Maximum wait time in seconds
44
+ --help, -h Show this help message
45
+ `;
46
+ export const RESULT_HELP_TEXT = `Usage: ai-cli result <pid> [options]
47
+
48
+ Get the current result for a tracked process.
49
+
50
+ Options:
51
+ --verbose Include verbose parsed output
52
+ --help, -h Show this help message
53
+ `;
54
+ export const KILL_HELP_TEXT = `Usage: ai-cli kill <pid>
55
+
56
+ Terminate a tracked process.
57
+
58
+ Options:
59
+ --help, -h Show this help message
60
+ `;
61
+ export const CLEANUP_HELP_TEXT = `Usage: ai-cli cleanup
62
+
63
+ Remove completed and failed tracked processes.
64
+
65
+ Options:
66
+ --help, -h Show this help message
67
+ `;
68
+ export const PS_HELP_TEXT = `Usage: ai-cli ps
69
+
70
+ List tracked processes.
71
+
72
+ Options:
73
+ --help, -h Show this help message
74
+ `;
75
+ export const MODELS_HELP_TEXT = `Usage: ai-cli models
76
+
77
+ List supported models and aliases.
78
+
79
+ Options:
80
+ --help, -h Show this help message
81
+ `;
82
+ export const DOCTOR_HELP_TEXT = `Usage: ai-cli doctor
83
+
84
+ Check whether supported AI CLI binaries are available.
85
+
86
+ Options:
87
+ --help, -h Show this help message
88
+ `;
89
+ export const MCP_HELP_TEXT = `Usage: ai-cli mcp
90
+
91
+ Start the MCP server.
92
+ `;
93
+ let cliProcessService = null;
94
+ function getCliProcessService() {
95
+ if (!cliProcessService) {
96
+ cliProcessService = new CliProcessService();
97
+ }
98
+ return cliProcessService;
99
+ }
100
+ const defaultDeps = {
101
+ stdout: (text) => process.stdout.write(text),
102
+ stderr: (text) => process.stderr.write(text),
103
+ startMcpServer: () => runMcpServer(),
104
+ runProcess: (options) => getCliProcessService().startProcess(options),
105
+ listProcesses: () => getCliProcessService().listProcesses(),
106
+ getProcessResult: (pid, verbose) => getCliProcessService().getProcessResult(pid, verbose),
107
+ waitForProcesses: (pids, timeoutSeconds) => getCliProcessService().waitForProcesses(pids, timeoutSeconds),
108
+ killProcess: (pid) => getCliProcessService().killProcess(pid),
109
+ cleanupProcesses: () => getCliProcessService().cleanupProcesses(),
110
+ getDoctorStatus: () => getCliDoctorStatus(),
111
+ };
112
+ function parseArgs(argv) {
113
+ const positionals = [];
114
+ const flags = {};
115
+ for (let i = 0; i < argv.length; i++) {
116
+ const arg = argv[i];
117
+ if (arg === '-h') {
118
+ flags.h = '';
119
+ continue;
120
+ }
121
+ if (!arg.startsWith('--')) {
122
+ positionals.push(arg);
123
+ continue;
124
+ }
125
+ const eqIdx = arg.indexOf('=');
126
+ if (eqIdx !== -1) {
127
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
128
+ continue;
129
+ }
130
+ const next = argv[i + 1];
131
+ if (next !== undefined && !next.startsWith('--')) {
132
+ flags[arg.slice(2)] = next;
133
+ i++;
134
+ }
135
+ else {
136
+ flags[arg.slice(2)] = '';
137
+ }
138
+ }
139
+ return { positionals, flags };
140
+ }
141
+ function getFirstFlag(flags, names) {
142
+ for (const name of names) {
143
+ if (name in flags) {
144
+ return flags[name];
145
+ }
146
+ }
147
+ return undefined;
148
+ }
149
+ function parsePositivePid(value) {
150
+ const pid = Number(value);
151
+ if (!Number.isInteger(pid) || pid <= 0) {
152
+ return null;
153
+ }
154
+ return pid;
155
+ }
156
+ function writeJson(stdout, value) {
157
+ stdout(`${JSON.stringify(value, null, 2)}\n`);
158
+ }
159
+ function hasHelpFlag(flags) {
160
+ return 'help' in flags || 'h' in flags;
161
+ }
162
+ export async function runCli(argv, deps = {}) {
163
+ const { stdout, stderr, startMcpServer, runProcess, listProcesses, getProcessResult, waitForProcesses, killProcess, cleanupProcesses, getDoctorStatus, } = { ...defaultDeps, ...deps };
164
+ const [command] = argv;
165
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
166
+ stdout(CLI_HELP_TEXT);
167
+ return 0;
168
+ }
169
+ if (command === 'mcp') {
170
+ const { flags } = parseArgs(argv.slice(1));
171
+ if (hasHelpFlag(flags)) {
172
+ stdout(MCP_HELP_TEXT);
173
+ return 0;
174
+ }
175
+ await startMcpServer();
176
+ return 0;
177
+ }
178
+ if (command === 'run') {
179
+ const { flags } = parseArgs(argv.slice(1));
180
+ if (hasHelpFlag(flags)) {
181
+ stdout(RUN_HELP_TEXT);
182
+ return 0;
183
+ }
184
+ const cwd = getFirstFlag(flags, ['cwd', 'workFolder', 'work-folder']);
185
+ if (!cwd) {
186
+ stderr('Missing required option: --cwd\n');
187
+ stdout(CLI_HELP_TEXT);
188
+ return 1;
189
+ }
190
+ const prompt = getFirstFlag(flags, ['prompt']);
191
+ const promptFile = getFirstFlag(flags, ['prompt-file', 'prompt_file']);
192
+ if (!prompt && !promptFile) {
193
+ stderr('Missing required option: --prompt or --prompt-file\n');
194
+ stdout(CLI_HELP_TEXT);
195
+ return 1;
196
+ }
197
+ const result = await runProcess({
198
+ cwd,
199
+ prompt: prompt || undefined,
200
+ prompt_file: promptFile || undefined,
201
+ model: getFirstFlag(flags, ['model']) || undefined,
202
+ session_id: getFirstFlag(flags, ['session-id', 'session_id']) || undefined,
203
+ reasoning_effort: getFirstFlag(flags, ['reasoning-effort', 'reasoning_effort']) || undefined,
204
+ });
205
+ writeJson(stdout, result);
206
+ return 0;
207
+ }
208
+ if (command === 'ps') {
209
+ const { flags } = parseArgs(argv.slice(1));
210
+ if (hasHelpFlag(flags)) {
211
+ stdout(PS_HELP_TEXT);
212
+ return 0;
213
+ }
214
+ writeJson(stdout, await listProcesses());
215
+ return 0;
216
+ }
217
+ if (command === 'result') {
218
+ const { positionals, flags } = parseArgs(argv.slice(1));
219
+ if (hasHelpFlag(flags)) {
220
+ stdout(RESULT_HELP_TEXT);
221
+ return 0;
222
+ }
223
+ const pid = parsePositivePid(positionals[0]);
224
+ if (pid === null) {
225
+ stderr('Missing required pid argument\n');
226
+ stdout(CLI_HELP_TEXT);
227
+ return 1;
228
+ }
229
+ writeJson(stdout, await getProcessResult(pid, 'verbose' in flags));
230
+ return 0;
231
+ }
232
+ if (command === 'wait') {
233
+ const { positionals, flags } = parseArgs(argv.slice(1));
234
+ if (hasHelpFlag(flags)) {
235
+ stdout(WAIT_HELP_TEXT);
236
+ return 0;
237
+ }
238
+ const pids = positionals.map((value) => parsePositivePid(value));
239
+ if (pids.length === 0) {
240
+ stderr('Missing required pid arguments\n');
241
+ stdout(CLI_HELP_TEXT);
242
+ return 1;
243
+ }
244
+ if (pids.some((pid) => pid === null)) {
245
+ stderr('All pid arguments must be positive integers\n');
246
+ stdout(CLI_HELP_TEXT);
247
+ return 1;
248
+ }
249
+ const timeoutRaw = getFirstFlag(flags, ['timeout']);
250
+ const timeout = timeoutRaw ? Number(timeoutRaw) : undefined;
251
+ if (timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0)) {
252
+ stderr('Invalid --timeout value\n');
253
+ stdout(CLI_HELP_TEXT);
254
+ return 1;
255
+ }
256
+ writeJson(stdout, await waitForProcesses(pids, timeout));
257
+ return 0;
258
+ }
259
+ if (command === 'kill') {
260
+ const { positionals, flags } = parseArgs(argv.slice(1));
261
+ if (hasHelpFlag(flags)) {
262
+ stdout(KILL_HELP_TEXT);
263
+ return 0;
264
+ }
265
+ const pid = parsePositivePid(positionals[0]);
266
+ if (pid === null) {
267
+ stderr('Missing required pid argument\n');
268
+ stdout(CLI_HELP_TEXT);
269
+ return 1;
270
+ }
271
+ writeJson(stdout, await killProcess(pid));
272
+ return 0;
273
+ }
274
+ if (command === 'cleanup') {
275
+ const { flags } = parseArgs(argv.slice(1));
276
+ if (hasHelpFlag(flags)) {
277
+ stdout(CLEANUP_HELP_TEXT);
278
+ return 0;
279
+ }
280
+ writeJson(stdout, await cleanupProcesses());
281
+ return 0;
282
+ }
283
+ if (command === 'models') {
284
+ const { flags } = parseArgs(argv.slice(1));
285
+ if (hasHelpFlag(flags)) {
286
+ stdout(MODELS_HELP_TEXT);
287
+ return 0;
288
+ }
289
+ writeJson(stdout, getModelsPayload());
290
+ return 0;
291
+ }
292
+ if (command === 'doctor') {
293
+ const { flags } = parseArgs(argv.slice(1));
294
+ if (hasHelpFlag(flags)) {
295
+ stdout(DOCTOR_HELP_TEXT);
296
+ return 0;
297
+ }
298
+ writeJson(stdout, getDoctorStatus());
299
+ return 0;
300
+ }
301
+ stderr(`Unknown subcommand: ${command}\n`);
302
+ stdout(CLI_HELP_TEXT);
303
+ return 1;
304
+ }