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
package/src/app/cli.ts ADDED
@@ -0,0 +1,373 @@
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
+
6
+ export const CLI_HELP_TEXT = `Usage: ai-cli <command> [options]
7
+
8
+ Commands:
9
+ run Start an AI CLI process in the background
10
+ wait Wait for one or more pids
11
+ ps List tracked processes
12
+ result Get the current result for a pid
13
+ kill Terminate a tracked pid
14
+ cleanup Remove completed and failed tracked processes
15
+ doctor Check supported AI CLI binaries
16
+ models List supported models and aliases
17
+ mcp Start the MCP server
18
+ help Show this help message
19
+ `;
20
+
21
+ export const RUN_HELP_TEXT = `Usage: ai-cli run --cwd <path> [options]
22
+
23
+ Start an AI CLI process in the background.
24
+
25
+ Options:
26
+ --cwd <path> Working directory
27
+ --prompt <text> Prompt text
28
+ --prompt-file <path> Path to a prompt file
29
+ --model <model> Model name or alias (e.g. sonnet, claude-ultra, gpt-5.2-codex, codex-ultra, gemini-2.5-pro, gemini-ultra, forge)
30
+ --session-id <id> Resume a previous session
31
+ --reasoning-effort <level> Reasoning level for Claude/Codex only
32
+ --help, -h Show this help message
33
+
34
+ Compatibility aliases:
35
+ --workFolder, --work-folder
36
+ --prompt_file
37
+ --session_id
38
+ --reasoning_effort
39
+ `;
40
+
41
+ export const WAIT_HELP_TEXT = `Usage: ai-cli wait <pid...> [options]
42
+
43
+ Wait for one or more tracked processes to finish.
44
+
45
+ Options:
46
+ --timeout <seconds> Maximum wait time in seconds
47
+ --help, -h Show this help message
48
+ `;
49
+
50
+ export const RESULT_HELP_TEXT = `Usage: ai-cli result <pid> [options]
51
+
52
+ Get the current result for a tracked process.
53
+
54
+ Options:
55
+ --verbose Include verbose parsed output
56
+ --help, -h Show this help message
57
+ `;
58
+
59
+ export const KILL_HELP_TEXT = `Usage: ai-cli kill <pid>
60
+
61
+ Terminate a tracked process.
62
+
63
+ Options:
64
+ --help, -h Show this help message
65
+ `;
66
+
67
+ export const CLEANUP_HELP_TEXT = `Usage: ai-cli cleanup
68
+
69
+ Remove completed and failed tracked processes.
70
+
71
+ Options:
72
+ --help, -h Show this help message
73
+ `;
74
+
75
+ export const PS_HELP_TEXT = `Usage: ai-cli ps
76
+
77
+ List tracked processes.
78
+
79
+ Options:
80
+ --help, -h Show this help message
81
+ `;
82
+
83
+ export const MODELS_HELP_TEXT = `Usage: ai-cli models
84
+
85
+ List supported models and aliases.
86
+
87
+ Options:
88
+ --help, -h Show this help message
89
+ `;
90
+
91
+ export const DOCTOR_HELP_TEXT = `Usage: ai-cli doctor
92
+
93
+ Check whether supported AI CLI binaries are available.
94
+
95
+ Options:
96
+ --help, -h Show this help message
97
+ `;
98
+
99
+ export const MCP_HELP_TEXT = `Usage: ai-cli mcp
100
+
101
+ Start the MCP server.
102
+ `;
103
+
104
+ interface CliDeps {
105
+ stdout: (text: string) => void;
106
+ stderr: (text: string) => void;
107
+ startMcpServer: () => Promise<void>;
108
+ runProcess: (options: {
109
+ cwd: string;
110
+ prompt?: string;
111
+ prompt_file?: string;
112
+ model?: string;
113
+ session_id?: string;
114
+ reasoning_effort?: string;
115
+ }) => Promise<any>;
116
+ listProcesses: () => Promise<any>;
117
+ getProcessResult: (pid: number, verbose: boolean) => Promise<any>;
118
+ waitForProcesses: (pids: number[], timeoutSeconds?: number) => Promise<any>;
119
+ killProcess: (pid: number) => Promise<any>;
120
+ cleanupProcesses: () => Promise<any>;
121
+ getDoctorStatus: () => any;
122
+ }
123
+
124
+ let cliProcessService: CliProcessService | null = null;
125
+
126
+ function getCliProcessService(): CliProcessService {
127
+ if (!cliProcessService) {
128
+ cliProcessService = new CliProcessService();
129
+ }
130
+ return cliProcessService;
131
+ }
132
+
133
+ const defaultDeps: CliDeps = {
134
+ stdout: (text: string) => process.stdout.write(text),
135
+ stderr: (text: string) => process.stderr.write(text),
136
+ startMcpServer: () => runMcpServer(),
137
+ runProcess: (options) => getCliProcessService().startProcess(options),
138
+ listProcesses: () => getCliProcessService().listProcesses(),
139
+ getProcessResult: (pid, verbose) => getCliProcessService().getProcessResult(pid, verbose),
140
+ waitForProcesses: (pids, timeoutSeconds) => getCliProcessService().waitForProcesses(pids, timeoutSeconds),
141
+ killProcess: (pid) => getCliProcessService().killProcess(pid),
142
+ cleanupProcesses: () => getCliProcessService().cleanupProcesses(),
143
+ getDoctorStatus: () => getCliDoctorStatus(),
144
+ };
145
+
146
+ function parseArgs(argv: string[]): { positionals: string[]; flags: Record<string, string> } {
147
+ const positionals: string[] = [];
148
+ const flags: Record<string, string> = {};
149
+
150
+ for (let i = 0; i < argv.length; i++) {
151
+ const arg = argv[i];
152
+ if (arg === '-h') {
153
+ flags.h = '';
154
+ continue;
155
+ }
156
+
157
+ if (!arg.startsWith('--')) {
158
+ positionals.push(arg);
159
+ continue;
160
+ }
161
+
162
+ const eqIdx = arg.indexOf('=');
163
+ if (eqIdx !== -1) {
164
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
165
+ continue;
166
+ }
167
+
168
+ const next = argv[i + 1];
169
+ if (next !== undefined && !next.startsWith('--')) {
170
+ flags[arg.slice(2)] = next;
171
+ i++;
172
+ } else {
173
+ flags[arg.slice(2)] = '';
174
+ }
175
+ }
176
+
177
+ return { positionals, flags };
178
+ }
179
+
180
+ function getFirstFlag(flags: Record<string, string>, names: string[]): string | undefined {
181
+ for (const name of names) {
182
+ if (name in flags) {
183
+ return flags[name];
184
+ }
185
+ }
186
+ return undefined;
187
+ }
188
+
189
+ function parsePositivePid(value: string | undefined): number | null {
190
+ const pid = Number(value);
191
+ if (!Number.isInteger(pid) || pid <= 0) {
192
+ return null;
193
+ }
194
+ return pid;
195
+ }
196
+
197
+ function writeJson(stdout: (text: string) => void, value: unknown): void {
198
+ stdout(`${JSON.stringify(value, null, 2)}\n`);
199
+ }
200
+
201
+ function hasHelpFlag(flags: Record<string, string>): boolean {
202
+ return 'help' in flags || 'h' in flags;
203
+ }
204
+
205
+ export async function runCli(argv: string[], deps: Partial<CliDeps> = {}): Promise<number> {
206
+ const {
207
+ stdout,
208
+ stderr,
209
+ startMcpServer,
210
+ runProcess,
211
+ listProcesses,
212
+ getProcessResult,
213
+ waitForProcesses,
214
+ killProcess,
215
+ cleanupProcesses,
216
+ getDoctorStatus,
217
+ } = { ...defaultDeps, ...deps };
218
+ const [command] = argv;
219
+
220
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
221
+ stdout(CLI_HELP_TEXT);
222
+ return 0;
223
+ }
224
+
225
+ if (command === 'mcp') {
226
+ const { flags } = parseArgs(argv.slice(1));
227
+ if (hasHelpFlag(flags)) {
228
+ stdout(MCP_HELP_TEXT);
229
+ return 0;
230
+ }
231
+ await startMcpServer();
232
+ return 0;
233
+ }
234
+
235
+ if (command === 'run') {
236
+ const { flags } = parseArgs(argv.slice(1));
237
+ if (hasHelpFlag(flags)) {
238
+ stdout(RUN_HELP_TEXT);
239
+ return 0;
240
+ }
241
+ const cwd = getFirstFlag(flags, ['cwd', 'workFolder', 'work-folder']);
242
+ if (!cwd) {
243
+ stderr('Missing required option: --cwd\n');
244
+ stdout(CLI_HELP_TEXT);
245
+ return 1;
246
+ }
247
+
248
+ const prompt = getFirstFlag(flags, ['prompt']);
249
+ const promptFile = getFirstFlag(flags, ['prompt-file', 'prompt_file']);
250
+ if (!prompt && !promptFile) {
251
+ stderr('Missing required option: --prompt or --prompt-file\n');
252
+ stdout(CLI_HELP_TEXT);
253
+ return 1;
254
+ }
255
+
256
+ const result = await runProcess({
257
+ cwd,
258
+ prompt: prompt || undefined,
259
+ prompt_file: promptFile || undefined,
260
+ model: getFirstFlag(flags, ['model']) || undefined,
261
+ session_id: getFirstFlag(flags, ['session-id', 'session_id']) || undefined,
262
+ reasoning_effort: getFirstFlag(flags, ['reasoning-effort', 'reasoning_effort']) || undefined,
263
+ });
264
+ writeJson(stdout, result);
265
+ return 0;
266
+ }
267
+
268
+ if (command === 'ps') {
269
+ const { flags } = parseArgs(argv.slice(1));
270
+ if (hasHelpFlag(flags)) {
271
+ stdout(PS_HELP_TEXT);
272
+ return 0;
273
+ }
274
+ writeJson(stdout, await listProcesses());
275
+ return 0;
276
+ }
277
+
278
+ if (command === 'result') {
279
+ const { positionals, flags } = parseArgs(argv.slice(1));
280
+ if (hasHelpFlag(flags)) {
281
+ stdout(RESULT_HELP_TEXT);
282
+ return 0;
283
+ }
284
+ const pid = parsePositivePid(positionals[0]);
285
+ if (pid === null) {
286
+ stderr('Missing required pid argument\n');
287
+ stdout(CLI_HELP_TEXT);
288
+ return 1;
289
+ }
290
+ writeJson(stdout, await getProcessResult(pid, 'verbose' in flags));
291
+ return 0;
292
+ }
293
+
294
+ if (command === 'wait') {
295
+ const { positionals, flags } = parseArgs(argv.slice(1));
296
+ if (hasHelpFlag(flags)) {
297
+ stdout(WAIT_HELP_TEXT);
298
+ return 0;
299
+ }
300
+ const pids = positionals.map((value) => parsePositivePid(value));
301
+ if (pids.length === 0) {
302
+ stderr('Missing required pid arguments\n');
303
+ stdout(CLI_HELP_TEXT);
304
+ return 1;
305
+ }
306
+ if (pids.some((pid) => pid === null)) {
307
+ stderr('All pid arguments must be positive integers\n');
308
+ stdout(CLI_HELP_TEXT);
309
+ return 1;
310
+ }
311
+
312
+ const timeoutRaw = getFirstFlag(flags, ['timeout']);
313
+ const timeout = timeoutRaw ? Number(timeoutRaw) : undefined;
314
+ if (timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0)) {
315
+ stderr('Invalid --timeout value\n');
316
+ stdout(CLI_HELP_TEXT);
317
+ return 1;
318
+ }
319
+
320
+ writeJson(stdout, await waitForProcesses(pids as number[], timeout));
321
+ return 0;
322
+ }
323
+
324
+ if (command === 'kill') {
325
+ const { positionals, flags } = parseArgs(argv.slice(1));
326
+ if (hasHelpFlag(flags)) {
327
+ stdout(KILL_HELP_TEXT);
328
+ return 0;
329
+ }
330
+ const pid = parsePositivePid(positionals[0]);
331
+ if (pid === null) {
332
+ stderr('Missing required pid argument\n');
333
+ stdout(CLI_HELP_TEXT);
334
+ return 1;
335
+ }
336
+ writeJson(stdout, await killProcess(pid));
337
+ return 0;
338
+ }
339
+
340
+ if (command === 'cleanup') {
341
+ const { flags } = parseArgs(argv.slice(1));
342
+ if (hasHelpFlag(flags)) {
343
+ stdout(CLEANUP_HELP_TEXT);
344
+ return 0;
345
+ }
346
+ writeJson(stdout, await cleanupProcesses());
347
+ return 0;
348
+ }
349
+
350
+ if (command === 'models') {
351
+ const { flags } = parseArgs(argv.slice(1));
352
+ if (hasHelpFlag(flags)) {
353
+ stdout(MODELS_HELP_TEXT);
354
+ return 0;
355
+ }
356
+ writeJson(stdout, getModelsPayload());
357
+ return 0;
358
+ }
359
+
360
+ if (command === 'doctor') {
361
+ const { flags } = parseArgs(argv.slice(1));
362
+ if (hasHelpFlag(flags)) {
363
+ stdout(DOCTOR_HELP_TEXT);
364
+ return 0;
365
+ }
366
+ writeJson(stdout, getDoctorStatus());
367
+ return 0;
368
+ }
369
+
370
+ stderr(`Unknown subcommand: ${command}\n`);
371
+ stdout(CLI_HELP_TEXT);
372
+ return 1;
373
+ }