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.
- package/.github/workflows/publish.yml +25 -0
- package/CHANGELOG.md +23 -0
- package/README.ja.md +112 -8
- package/README.md +112 -9
- package/dist/__tests__/app-cli.test.js +293 -0
- package/dist/__tests__/cli-bin-smoke.test.js +58 -0
- package/dist/__tests__/cli-builder.test.js +37 -0
- package/dist/__tests__/cli-process-service.test.js +279 -0
- package/dist/__tests__/cli-utils.test.js +140 -0
- package/dist/__tests__/error-cases.test.js +2 -1
- package/dist/__tests__/mcp-contract.test.js +343 -0
- package/dist/__tests__/parsers.test.js +37 -1
- package/dist/__tests__/process-management.test.js +15 -8
- package/dist/__tests__/server.test.js +29 -3
- package/dist/__tests__/wait.test.js +31 -0
- package/dist/app/cli.js +304 -0
- package/dist/app/mcp.js +366 -0
- package/dist/bin/ai-cli-mcp.js +6 -0
- package/dist/bin/ai-cli.js +10 -0
- package/dist/cli-builder.js +15 -6
- package/dist/cli-parse.js +8 -5
- package/dist/cli-process-service.js +332 -0
- package/dist/cli-utils.js +159 -88
- package/dist/cli.js +4 -3
- package/dist/model-catalog.js +53 -0
- package/dist/parsers.js +55 -0
- package/dist/process-service.js +201 -0
- package/dist/server.js +4 -578
- package/docs/cli-architecture.md +275 -0
- package/package.json +4 -3
- package/server.json +1 -1
- package/src/__tests__/app-cli.test.ts +370 -0
- package/src/__tests__/cli-bin-smoke.test.ts +75 -0
- package/src/__tests__/cli-builder.test.ts +47 -0
- package/src/__tests__/cli-process-service.test.ts +334 -0
- package/src/__tests__/cli-utils.test.ts +166 -0
- package/src/__tests__/error-cases.test.ts +3 -4
- package/src/__tests__/mcp-contract.test.ts +422 -0
- package/src/__tests__/parsers.test.ts +44 -1
- package/src/__tests__/process-management.test.ts +15 -9
- package/src/__tests__/server.test.ts +27 -6
- package/src/__tests__/wait.test.ts +38 -0
- package/src/app/cli.ts +373 -0
- package/src/app/mcp.ts +402 -0
- package/src/bin/ai-cli-mcp.ts +7 -0
- package/src/bin/ai-cli.ts +11 -0
- package/src/cli-builder.ts +19 -10
- package/src/cli-parse.ts +8 -5
- package/src/cli-process-service.ts +418 -0
- package/src/cli-utils.ts +205 -99
- package/src/cli.ts +4 -3
- package/src/model-catalog.ts +64 -0
- package/src/parsers.ts +61 -0
- package/src/process-service.ts +263 -0
- 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 {
|
package/dist/app/cli.js
ADDED
|
@@ -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
|
+
}
|