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.
- package/CHANGELOG.md +26 -0
- package/README.ja.md +37 -11
- package/README.md +44 -11
- package/dist/app/cli.js +2 -1
- package/dist/app/mcp.js +65 -13
- package/dist/cli-builder.js +13 -6
- package/dist/cli-process-service.js +81 -95
- package/dist/cli-utils.js +6 -0
- package/dist/cli.js +1 -1
- package/dist/model-catalog.js +3 -2
- package/dist/parsers.js +111 -8
- package/dist/process-service.js +5 -4
- package/package.json +26 -2
- 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 -500
- package/dist/__tests__/peek.test.js +0 -36
- package/dist/__tests__/process-management.test.js +0 -871
- 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 -564
- package/src/__tests__/peek.test.ts +0 -44
- package/src/__tests__/process-management.test.ts +0 -1043
- 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 -708
- 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 -840
- package/src/peek.ts +0 -95
- package/src/process-result.ts +0 -88
- package/src/process-service.ts +0 -367
- 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,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync,
|
|
2
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import { join, basename, dirname } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { buildCliCommand } from './cli-builder.js';
|
|
@@ -7,6 +7,7 @@ import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeC
|
|
|
7
7
|
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor } from './parsers.js';
|
|
8
8
|
import { buildProcessResult } from './process-result.js';
|
|
9
9
|
import { appendPeekEvents, buildNotFoundPeekProcess, observedDurationSec, validatePeekPids, validatePeekTimeSec, } from './peek.js';
|
|
10
|
+
const SIGTERM_EXIT_CODE = 143;
|
|
10
11
|
function resolveDefaultStateDir() {
|
|
11
12
|
return process.env.AI_CLI_STATE_DIR || join(homedir(), '.local', 'state', 'ai-cli');
|
|
12
13
|
}
|
|
@@ -73,65 +74,7 @@ export class CliProcessService {
|
|
|
73
74
|
reasoning_effort: options.reasoning_effort,
|
|
74
75
|
cliPaths: this.cliPaths,
|
|
75
76
|
});
|
|
76
|
-
|
|
77
|
-
return this.startDetachedOpenCodeProcess(cmd, options.model);
|
|
78
|
-
}
|
|
79
|
-
const stdoutPath = this.resolveStdoutPathForPidPlaceholder();
|
|
80
|
-
const stderrPath = this.resolveStderrPathForPidPlaceholder();
|
|
81
|
-
let stdoutFd;
|
|
82
|
-
let stderrFd;
|
|
83
|
-
try {
|
|
84
|
-
stdoutFd = openSync(stdoutPath, 'w');
|
|
85
|
-
stderrFd = openSync(stderrPath, 'w');
|
|
86
|
-
const childProcess = spawn(cmd.cliPath, cmd.args, {
|
|
87
|
-
cwd: cmd.cwd,
|
|
88
|
-
detached: true,
|
|
89
|
-
stdio: ['ignore', stdoutFd, stderrFd],
|
|
90
|
-
});
|
|
91
|
-
const pid = childProcess.pid;
|
|
92
|
-
childProcess.unref();
|
|
93
|
-
if (!pid) {
|
|
94
|
-
throw new Error(`Failed to start ${cmd.agent} CLI process`);
|
|
95
|
-
}
|
|
96
|
-
const processDir = this.resolveProcessDir(cmd.cwd, pid);
|
|
97
|
-
mkdirSync(processDir, { recursive: true });
|
|
98
|
-
const finalStdoutPath = this.resolveStdoutPath(processDir);
|
|
99
|
-
const finalStderrPath = this.resolveStderrPath(processDir);
|
|
100
|
-
this.renamePlaceholderFile(stdoutPath, finalStdoutPath);
|
|
101
|
-
this.renamePlaceholderFile(stderrPath, finalStderrPath);
|
|
102
|
-
const storedProcess = {
|
|
103
|
-
pid,
|
|
104
|
-
prompt: cmd.prompt,
|
|
105
|
-
workFolder: cmd.cwd,
|
|
106
|
-
cwdKey: this.resolveCwdKey(cmd.cwd),
|
|
107
|
-
model: options.model,
|
|
108
|
-
toolType: cmd.agent,
|
|
109
|
-
startTime: new Date().toISOString(),
|
|
110
|
-
stdoutPath: finalStdoutPath,
|
|
111
|
-
stderrPath: finalStderrPath,
|
|
112
|
-
status: 'running',
|
|
113
|
-
};
|
|
114
|
-
this.writeProcess(storedProcess);
|
|
115
|
-
return {
|
|
116
|
-
pid,
|
|
117
|
-
status: 'started',
|
|
118
|
-
agent: cmd.agent,
|
|
119
|
-
message: `${cmd.agent} process started successfully`,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
this.removeFileIfExists(stdoutPath);
|
|
124
|
-
this.removeFileIfExists(stderrPath);
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
finally {
|
|
128
|
-
if (stdoutFd !== undefined) {
|
|
129
|
-
closeSync(stdoutFd);
|
|
130
|
-
}
|
|
131
|
-
if (stderrFd !== undefined) {
|
|
132
|
-
closeSync(stderrFd);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
77
|
+
return this.startDetachedTrackedProcess(cmd, options.model);
|
|
135
78
|
}
|
|
136
79
|
async listProcesses() {
|
|
137
80
|
return this.readAllProcesses().map((process) => ({
|
|
@@ -201,8 +144,8 @@ export class CliProcessService {
|
|
|
201
144
|
observers.push({
|
|
202
145
|
process,
|
|
203
146
|
result,
|
|
204
|
-
stdoutExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls }),
|
|
205
|
-
stderrExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls }),
|
|
147
|
+
stdoutExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls, source: 'stdout' }),
|
|
148
|
+
stderrExtractor: new PeekEventExtractor(process.toolType, { includeToolCalls, source: 'stderr' }),
|
|
206
149
|
stdoutOffset: this.fileSizeSafe(process.stdoutPath),
|
|
207
150
|
stderrOffset: this.fileSizeSafe(process.stderrPath),
|
|
208
151
|
});
|
|
@@ -239,8 +182,9 @@ export class CliProcessService {
|
|
|
239
182
|
for (const observer of observers) {
|
|
240
183
|
observer.process = this.refreshStatus(this.readProcess(observer.process.pid));
|
|
241
184
|
observer.result.status = observer.process.status;
|
|
242
|
-
|
|
243
|
-
appendPeekEvents(observer.result, observer.
|
|
185
|
+
const terminal = observer.process.status !== 'running';
|
|
186
|
+
appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs, { terminal }));
|
|
187
|
+
appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs, { terminal }));
|
|
244
188
|
}
|
|
245
189
|
return {
|
|
246
190
|
peek_started_at: startedAt.toISOString(),
|
|
@@ -267,7 +211,16 @@ export class CliProcessService {
|
|
|
267
211
|
message: 'Signal sent but process is still running',
|
|
268
212
|
};
|
|
269
213
|
}
|
|
270
|
-
|
|
214
|
+
const exitStatus = this.readExitStatus(refreshed);
|
|
215
|
+
if (exitStatus) {
|
|
216
|
+
refreshed.status = exitStatus.status;
|
|
217
|
+
refreshed.exitCode = exitStatus.exitCode;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
refreshed.status = 'failed';
|
|
221
|
+
refreshed.exitCode = SIGTERM_EXIT_CODE;
|
|
222
|
+
this.writeExitStatus(refreshed, { status: 'failed', exitCode: SIGTERM_EXIT_CODE });
|
|
223
|
+
}
|
|
271
224
|
this.writeProcess(refreshed);
|
|
272
225
|
return {
|
|
273
226
|
pid,
|
|
@@ -294,9 +247,9 @@ export class CliProcessService {
|
|
|
294
247
|
message: `Removed ${removed} processes`,
|
|
295
248
|
};
|
|
296
249
|
}
|
|
297
|
-
async
|
|
250
|
+
async startDetachedTrackedProcess(cmd, model) {
|
|
298
251
|
const cwdKey = this.resolveCwdKey(cmd.cwd);
|
|
299
|
-
const wrapperPath = this.
|
|
252
|
+
const wrapperPath = this.ensureDetachedWrapperScript();
|
|
300
253
|
const childProcess = spawn(wrapperPath, [this.stateDir, cwdKey, cmd.cliPath, ...cmd.args], {
|
|
301
254
|
cwd: cmd.cwd,
|
|
302
255
|
detached: true,
|
|
@@ -311,12 +264,8 @@ export class CliProcessService {
|
|
|
311
264
|
mkdirSync(processDir, { recursive: true });
|
|
312
265
|
const stdoutPath = this.resolveStdoutPath(processDir);
|
|
313
266
|
const stderrPath = this.resolveStderrPath(processDir);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
if (!existsSync(stderrPath)) {
|
|
318
|
-
writeFileSync(stderrPath, '');
|
|
319
|
-
}
|
|
267
|
+
this.touchFile(stdoutPath);
|
|
268
|
+
this.touchFile(stderrPath);
|
|
320
269
|
const storedProcess = {
|
|
321
270
|
pid,
|
|
322
271
|
prompt: cmd.prompt,
|
|
@@ -385,15 +334,13 @@ export class CliProcessService {
|
|
|
385
334
|
return process;
|
|
386
335
|
}
|
|
387
336
|
if (!isProcessRunning(process.pid)) {
|
|
388
|
-
process.status = '
|
|
337
|
+
process.status = 'failed';
|
|
338
|
+
this.appendTextFileSafe(process.stderrPath, '\nProcess exited without exit-status metadata; marking as failed.\n');
|
|
389
339
|
this.writeProcess(process);
|
|
390
340
|
}
|
|
391
341
|
return process;
|
|
392
342
|
}
|
|
393
343
|
readExitStatus(process) {
|
|
394
|
-
if (process.toolType !== 'opencode') {
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
397
344
|
const exitMetaPath = this.resolveExitStatusPath(this.resolveStoredProcessDir(process));
|
|
398
345
|
if (!existsSync(exitMetaPath)) {
|
|
399
346
|
return null;
|
|
@@ -409,12 +356,28 @@ export class CliProcessService {
|
|
|
409
356
|
}
|
|
410
357
|
return null;
|
|
411
358
|
}
|
|
359
|
+
writeExitStatus(process, exitStatus) {
|
|
360
|
+
const exitStatusPath = this.resolveExitStatusPath(this.resolveStoredProcessDir(process));
|
|
361
|
+
const tempPath = `${exitStatusPath}.${process.pid}.${Date.now()}.tmp`;
|
|
362
|
+
writeFileSync(tempPath, JSON.stringify(exitStatus, null, 2) + '\n');
|
|
363
|
+
renameSync(tempPath, exitStatusPath);
|
|
364
|
+
}
|
|
412
365
|
readTextFileSafe(filePath) {
|
|
413
366
|
if (!existsSync(filePath)) {
|
|
414
367
|
return '';
|
|
415
368
|
}
|
|
416
369
|
return readFileSync(filePath, 'utf-8');
|
|
417
370
|
}
|
|
371
|
+
touchFile(filePath) {
|
|
372
|
+
closeSync(openSync(filePath, 'a'));
|
|
373
|
+
}
|
|
374
|
+
appendTextFileSafe(filePath, text) {
|
|
375
|
+
try {
|
|
376
|
+
appendFileSync(filePath, text);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
418
381
|
fileSizeSafe(filePath) {
|
|
419
382
|
if (!existsSync(filePath)) {
|
|
420
383
|
return 0;
|
|
@@ -470,17 +433,12 @@ export class CliProcessService {
|
|
|
470
433
|
resolveExitStatusPath(processDir) {
|
|
471
434
|
return join(processDir, 'exit-status.json');
|
|
472
435
|
}
|
|
473
|
-
|
|
474
|
-
return join(this.stateDir, '
|
|
475
|
-
}
|
|
476
|
-
resolveStdoutPathForPidPlaceholder() {
|
|
477
|
-
return join(this.stateDir, `pending-${Date.now()}-${Math.random().toString(36).slice(2)}.stdout.log`);
|
|
436
|
+
resolveDetachedWrapperPath() {
|
|
437
|
+
return join(this.stateDir, 'detached-runner-v2.sh');
|
|
478
438
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
ensureOpenCodeWrapperScript() {
|
|
483
|
-
const wrapperPath = this.resolveOpenCodeWrapperPath();
|
|
439
|
+
ensureDetachedWrapperScript() {
|
|
440
|
+
const wrapperPath = this.resolveDetachedWrapperPath();
|
|
441
|
+
this.removeLegacyDetachedWrappers();
|
|
484
442
|
if (existsSync(wrapperPath)) {
|
|
485
443
|
return wrapperPath;
|
|
486
444
|
}
|
|
@@ -497,24 +455,52 @@ exit_meta_path="$process_dir/exit-status.json"
|
|
|
497
455
|
mkdir -p "$process_dir"
|
|
498
456
|
: > "$stdout_path"
|
|
499
457
|
: > "$stderr_path"
|
|
500
|
-
|
|
458
|
+
write_exit_status() {
|
|
459
|
+
status="$1"
|
|
460
|
+
exit_code="$2"
|
|
461
|
+
tmp_exit_meta_path="$exit_meta_path.$$"
|
|
462
|
+
printf '{\n "status": "%s",\n "exitCode": %s\n}\n' "$status" "$exit_code" > "$tmp_exit_meta_path"
|
|
463
|
+
mv "$tmp_exit_meta_path" "$exit_meta_path"
|
|
464
|
+
}
|
|
465
|
+
handle_signal() {
|
|
466
|
+
signal="$1"
|
|
467
|
+
exit_code="$2"
|
|
468
|
+
if [ -n "\${child_pid:-}" ]; then
|
|
469
|
+
kill "-$signal" "$child_pid" 2>/dev/null || true
|
|
470
|
+
wait "$child_pid" 2>/dev/null
|
|
471
|
+
fi
|
|
472
|
+
write_exit_status "failed" "$exit_code"
|
|
473
|
+
exit "$exit_code"
|
|
474
|
+
}
|
|
475
|
+
trap 'handle_signal TERM 143' TERM
|
|
476
|
+
trap 'handle_signal INT 130' INT
|
|
477
|
+
trap 'handle_signal HUP 129' HUP
|
|
478
|
+
"$@" >> "$stdout_path" 2>> "$stderr_path" &
|
|
479
|
+
child_pid="$!"
|
|
480
|
+
wait "$child_pid"
|
|
501
481
|
exit_code="$?"
|
|
482
|
+
trap - TERM INT HUP
|
|
502
483
|
status="completed"
|
|
503
484
|
if [ "$exit_code" -ne 0 ]; then
|
|
504
485
|
status="failed"
|
|
505
486
|
fi
|
|
506
|
-
|
|
487
|
+
write_exit_status "$status" "$exit_code"
|
|
507
488
|
exit "$exit_code"
|
|
508
489
|
`);
|
|
509
490
|
chmodSync(wrapperPath, 0o755);
|
|
510
491
|
return wrapperPath;
|
|
511
492
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
493
|
+
removeLegacyDetachedWrappers() {
|
|
494
|
+
for (const fileName of ['detached-runner-v1.sh']) {
|
|
495
|
+
const legacyPath = join(this.stateDir, fileName);
|
|
496
|
+
if (!existsSync(legacyPath)) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
rmSync(legacyPath, { force: true });
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
}
|
|
518
504
|
}
|
|
519
505
|
}
|
|
520
506
|
killPidOrGroup(pid, signal) {
|
package/dist/cli-utils.js
CHANGED
|
@@ -153,6 +153,12 @@ function getCliBinaryStatus(name) {
|
|
|
153
153
|
}
|
|
154
154
|
export function getCliDoctorStatus() {
|
|
155
155
|
return {
|
|
156
|
+
checks: {
|
|
157
|
+
binaryAvailability: true,
|
|
158
|
+
pathResolution: true,
|
|
159
|
+
loginState: false,
|
|
160
|
+
termsAcceptance: false,
|
|
161
|
+
},
|
|
156
162
|
claude: getCliBinaryStatus('claude'),
|
|
157
163
|
codex: getCliBinaryStatus('codex'),
|
|
158
164
|
gemini: getCliBinaryStatus('gemini'),
|
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,7 @@ Options:
|
|
|
40
40
|
--prompt Prompt string (mutually exclusive with --prompt_file)
|
|
41
41
|
--prompt_file Path to a file containing the prompt
|
|
42
42
|
--session_id Session ID to resume, including OpenCode in-place resumes
|
|
43
|
-
--reasoning_effort Claude/Codex only: Claude=low|medium|high, Codex=low|medium|high|xhigh; unsupported for Gemini, Forge, and OpenCode
|
|
43
|
+
--reasoning_effort Claude/Codex only: Claude=low|medium|high|xhigh|max, Codex=low|medium|high|xhigh; unsupported for Gemini, Forge, and OpenCode
|
|
44
44
|
--help Show this help message
|
|
45
45
|
|
|
46
46
|
Raw CLI output goes to stdout. Use cli.run.parse to parse the output:
|
package/dist/model-catalog.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const CLAUDE_MODELS = ['sonnet', 'sonnet[1m]', 'opus', 'opusplan', 'haiku'];
|
|
2
2
|
export const CODEX_MODELS = [
|
|
3
|
+
'codex',
|
|
3
4
|
'gpt-5.4',
|
|
4
5
|
'gpt-5.3-codex',
|
|
5
6
|
'gpt-5.2-codex',
|
|
@@ -27,7 +28,7 @@ export const MODEL_ALIASES = {
|
|
|
27
28
|
'gemini-ultra': 'gemini-3.1-pro-preview',
|
|
28
29
|
};
|
|
29
30
|
export const MODEL_ALIAS_DETAILS = [
|
|
30
|
-
{ name: 'claude-ultra', resolvesTo: 'opus', agent: 'claude', defaultReasoningEffort: '
|
|
31
|
+
{ name: 'claude-ultra', resolvesTo: 'opus', agent: 'claude', defaultReasoningEffort: 'max' },
|
|
31
32
|
{ name: 'codex-ultra', resolvesTo: 'gpt-5.4', agent: 'codex', defaultReasoningEffort: 'xhigh' },
|
|
32
33
|
{ name: 'gemini-ultra', resolvesTo: 'gemini-3.1-pro-preview', agent: 'gemini' },
|
|
33
34
|
];
|
|
@@ -43,7 +44,7 @@ export function getSupportedModelsDescription() {
|
|
|
43
44
|
].join(', ');
|
|
44
45
|
}
|
|
45
46
|
export function getModelParameterDescription() {
|
|
46
|
-
return `The model to use. Aliases: "claude-ultra" (auto
|
|
47
|
+
return `The model to use. Aliases: "claude-ultra" (auto max effort), "codex-ultra" (auto xhigh reasoning), "gemini-ultra". Standard: ${[...CLAUDE_MODELS, ...CODEX_MODELS, ...GEMINI_MODELS, ...FORGE_MODELS, ...OPENCODE_MODELS].map((model) => `"${model}"`).join(', ')}. OpenCode also accepts explicit dynamic models using "oc-<provider/model>". "forge" is a provider key, not a Forge model family selector.`;
|
|
47
48
|
}
|
|
48
49
|
export function getModelsPayload() {
|
|
49
50
|
return {
|
package/dist/parsers.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { debugLog } from './cli-utils.js';
|
|
2
2
|
const PEEK_TOOL_SUMMARY_MAX_LENGTH = 200;
|
|
3
|
+
const FORGE_EXECUTE_PATTERN = /^● \[[^\]]+\] Execute \[([^\]]*)\]\s+(.+)$/;
|
|
4
|
+
const FORGE_FINISHED_PATTERN = /^● \[[^\]]+\] Finished(?:\s+\S+)?\s*$/;
|
|
3
5
|
function isGeminiAssistantMessageEvent(parsed) {
|
|
4
6
|
return parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string';
|
|
5
7
|
}
|
|
@@ -253,12 +255,19 @@ export class PeekEventExtractor {
|
|
|
253
255
|
pending = '';
|
|
254
256
|
geminiAssistantBuffer = '';
|
|
255
257
|
includeToolCalls;
|
|
258
|
+
source;
|
|
256
259
|
toolMemory = new Map();
|
|
260
|
+
forgePendingTool = null;
|
|
261
|
+
forgeToolSequence = 0;
|
|
257
262
|
constructor(agent, options = {}) {
|
|
258
263
|
this.agent = agent;
|
|
259
264
|
this.includeToolCalls = options.includeToolCalls === true;
|
|
265
|
+
this.source = options.source || 'stdout';
|
|
260
266
|
}
|
|
261
267
|
push(chunk, observedAt = new Date().toISOString()) {
|
|
268
|
+
if (this.agent === 'forge' && this.source === 'stderr') {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
262
271
|
if (!chunk) {
|
|
263
272
|
return [];
|
|
264
273
|
}
|
|
@@ -266,17 +275,27 @@ export class PeekEventExtractor {
|
|
|
266
275
|
this.pending = lines.pop() || '';
|
|
267
276
|
return this.extractLines(lines, observedAt);
|
|
268
277
|
}
|
|
269
|
-
flush(observedAt = new Date().toISOString()) {
|
|
278
|
+
flush(observedAt = new Date().toISOString(), options = {}) {
|
|
279
|
+
if (this.agent === 'forge' && this.source === 'stderr') {
|
|
280
|
+
this.pending = '';
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
270
283
|
const events = [];
|
|
271
284
|
if (this.pending) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
285
|
+
if (this.agent !== 'forge' || options.terminal === true) {
|
|
286
|
+
const line = this.pending;
|
|
287
|
+
this.pending = '';
|
|
288
|
+
events.push(...this.extractLines([line], observedAt));
|
|
289
|
+
}
|
|
275
290
|
}
|
|
276
291
|
events.push(...this.flushGeminiAssistantBuffer(observedAt));
|
|
292
|
+
events.push(...this.flushForgePendingTool(observedAt, options.terminal === true));
|
|
277
293
|
return events;
|
|
278
294
|
}
|
|
279
295
|
extractLines(lines, observedAt) {
|
|
296
|
+
if (this.agent === 'forge') {
|
|
297
|
+
return this.extractForgeLines(lines, observedAt);
|
|
298
|
+
}
|
|
280
299
|
const events = [];
|
|
281
300
|
for (const line of lines) {
|
|
282
301
|
if (!line.trim()) {
|
|
@@ -292,6 +311,58 @@ export class PeekEventExtractor {
|
|
|
292
311
|
}
|
|
293
312
|
return events;
|
|
294
313
|
}
|
|
314
|
+
extractForgeLines(lines, observedAt) {
|
|
315
|
+
const events = [];
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
if (!line.trim()) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const summary = this.extractForgeMessage(line, 'Summary:');
|
|
321
|
+
if (summary !== null) {
|
|
322
|
+
events.push({ kind: 'message', ts: observedAt, text: summary });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const completed = this.extractForgeMessage(line, 'Completed successfully:');
|
|
326
|
+
if (completed !== null) {
|
|
327
|
+
events.push({ kind: 'message', ts: observedAt, text: completed });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (this.includeToolCalls) {
|
|
331
|
+
const executeMatch = line.match(FORGE_EXECUTE_PATTERN);
|
|
332
|
+
if (executeMatch) {
|
|
333
|
+
events.push(...this.completeForgePendingTool(observedAt));
|
|
334
|
+
const [, rawTool, rawSummary] = executeMatch;
|
|
335
|
+
const tool = rawTool.trim() && !/\s/.test(rawTool.trim()) ? rawTool.trim() : 'shell';
|
|
336
|
+
const event = createToolCallEvent({
|
|
337
|
+
ts: observedAt,
|
|
338
|
+
phase: 'started',
|
|
339
|
+
id: `forge_${this.forgeToolSequence++}`,
|
|
340
|
+
tool,
|
|
341
|
+
command: rawSummary,
|
|
342
|
+
});
|
|
343
|
+
this.forgePendingTool = {
|
|
344
|
+
id: event.id,
|
|
345
|
+
tool: event.tool,
|
|
346
|
+
summary: event.summary,
|
|
347
|
+
summary_truncated: event.summary_truncated,
|
|
348
|
+
};
|
|
349
|
+
events.push(event);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (FORGE_FINISHED_PATTERN.test(line)) {
|
|
353
|
+
events.push(...this.completeForgePendingTool(observedAt));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return events;
|
|
358
|
+
}
|
|
359
|
+
extractForgeMessage(line, prefix) {
|
|
360
|
+
if (!line.startsWith(prefix)) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const text = line.slice(prefix.length).trim();
|
|
364
|
+
return text || null;
|
|
365
|
+
}
|
|
295
366
|
extractParsedEvent(parsed, observedAt) {
|
|
296
367
|
if (this.agent === 'gemini') {
|
|
297
368
|
const events = this.extractGeminiParsedEvent(parsed, observedAt);
|
|
@@ -339,6 +410,32 @@ export class PeekEventExtractor {
|
|
|
339
410
|
}
|
|
340
411
|
return [{ kind: 'message', ts: observedAt, text }];
|
|
341
412
|
}
|
|
413
|
+
completeForgePendingTool(observedAt) {
|
|
414
|
+
if (!this.forgePendingTool) {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
const pending = this.forgePendingTool;
|
|
418
|
+
this.forgePendingTool = null;
|
|
419
|
+
const event = createToolCallEvent({
|
|
420
|
+
ts: observedAt,
|
|
421
|
+
phase: 'completed',
|
|
422
|
+
id: pending.id,
|
|
423
|
+
tool: pending.tool,
|
|
424
|
+
status: 'unknown',
|
|
425
|
+
defaultStatus: 'unknown',
|
|
426
|
+
});
|
|
427
|
+
event.summary = pending.summary;
|
|
428
|
+
if (pending.summary_truncated) {
|
|
429
|
+
event.summary_truncated = true;
|
|
430
|
+
}
|
|
431
|
+
return [event];
|
|
432
|
+
}
|
|
433
|
+
flushForgePendingTool(observedAt, terminal) {
|
|
434
|
+
if (this.agent !== 'forge' || !terminal) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
return this.completeForgePendingTool(observedAt);
|
|
438
|
+
}
|
|
342
439
|
}
|
|
343
440
|
export class PeekMessageExtractor {
|
|
344
441
|
extractor;
|
|
@@ -348,8 +445,8 @@ export class PeekMessageExtractor {
|
|
|
348
445
|
push(chunk, observedAt = new Date().toISOString()) {
|
|
349
446
|
return this.toMessages(this.extractor.push(chunk, observedAt));
|
|
350
447
|
}
|
|
351
|
-
flush(observedAt = new Date().toISOString()) {
|
|
352
|
-
return this.toMessages(this.extractor.flush(observedAt));
|
|
448
|
+
flush(observedAt = new Date().toISOString(), options = {}) {
|
|
449
|
+
return this.toMessages(this.extractor.flush(observedAt, options));
|
|
353
450
|
}
|
|
354
451
|
toMessages(events) {
|
|
355
452
|
return events
|
|
@@ -431,6 +528,7 @@ export function parseClaudeOutput(stdout) {
|
|
|
431
528
|
try {
|
|
432
529
|
const lines = stdout.trim().split('\n');
|
|
433
530
|
let lastMessage = null;
|
|
531
|
+
let assistantTextBuffer = '';
|
|
434
532
|
let sessionId = null;
|
|
435
533
|
const toolsMap = new Map();
|
|
436
534
|
for (const line of lines) {
|
|
@@ -446,6 +544,9 @@ export function parseClaudeOutput(stdout) {
|
|
|
446
544
|
}
|
|
447
545
|
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
448
546
|
for (const content of parsed.message.content) {
|
|
547
|
+
if (content.type === 'text' && typeof content.text === 'string') {
|
|
548
|
+
assistantTextBuffer += content.text;
|
|
549
|
+
}
|
|
449
550
|
if (content.type === 'tool_use') {
|
|
450
551
|
toolsMap.set(content.id, {
|
|
451
552
|
tool: content.name,
|
|
@@ -477,9 +578,11 @@ export function parseClaudeOutput(stdout) {
|
|
|
477
578
|
}
|
|
478
579
|
}
|
|
479
580
|
const tools = Array.from(toolsMap.values());
|
|
480
|
-
|
|
581
|
+
const fallbackMessage = assistantTextBuffer.trim() ? assistantTextBuffer : null;
|
|
582
|
+
const message = lastMessage || fallbackMessage;
|
|
583
|
+
if (message || sessionId || tools.length > 0) {
|
|
481
584
|
return {
|
|
482
|
-
message
|
|
585
|
+
message,
|
|
483
586
|
session_id: sessionId,
|
|
484
587
|
tools: tools.length > 0 ? tools : undefined
|
|
485
588
|
};
|
package/dist/process-service.js
CHANGED
|
@@ -176,8 +176,8 @@ export class ProcessService {
|
|
|
176
176
|
error: null,
|
|
177
177
|
};
|
|
178
178
|
processes.push(result);
|
|
179
|
-
const stdoutExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls });
|
|
180
|
-
const stderrExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls });
|
|
179
|
+
const stdoutExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls, source: 'stdout' });
|
|
180
|
+
const stderrExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls, source: 'stderr' });
|
|
181
181
|
const onStdout = (data) => {
|
|
182
182
|
appendPeekEvents(result, stdoutExtractor.push(data.toString(), new Date().toISOString()));
|
|
183
183
|
};
|
|
@@ -210,8 +210,9 @@ export class ProcessService {
|
|
|
210
210
|
for (const observer of observers) {
|
|
211
211
|
observer.entry.process.stdout?.off('data', observer.onStdout);
|
|
212
212
|
observer.entry.process.stderr?.off('data', observer.onStderr);
|
|
213
|
-
|
|
214
|
-
appendPeekEvents(observer.result, observer.
|
|
213
|
+
const terminal = observer.entry.status !== 'running';
|
|
214
|
+
appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs, { terminal }));
|
|
215
|
+
appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs, { terminal }));
|
|
215
216
|
observer.result.status = observer.entry.status;
|
|
216
217
|
}
|
|
217
218
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-cli-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"mcpName": "io.github.mkXultra/ai-cli-mcp",
|
|
5
5
|
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, Forge, and OpenCode) with background process management",
|
|
6
6
|
"author": "mkXultra",
|
|
@@ -10,14 +10,38 @@
|
|
|
10
10
|
"ai-cli": "dist/bin/ai-cli.js",
|
|
11
11
|
"ai-cli-mcp": "dist/bin/ai-cli-mcp.js"
|
|
12
12
|
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/app",
|
|
15
|
+
"dist/bin",
|
|
16
|
+
"dist/cli-builder.js",
|
|
17
|
+
"dist/cli-parse.js",
|
|
18
|
+
"dist/cli-process-service.js",
|
|
19
|
+
"dist/cli-utils.js",
|
|
20
|
+
"dist/cli.js",
|
|
21
|
+
"dist/model-catalog.js",
|
|
22
|
+
"dist/parsers.js",
|
|
23
|
+
"dist/peek.js",
|
|
24
|
+
"dist/process-result.js",
|
|
25
|
+
"dist/process-service.js",
|
|
26
|
+
"dist/server.js",
|
|
27
|
+
"server.json",
|
|
28
|
+
"README.ja.md",
|
|
29
|
+
"CHANGELOG.md"
|
|
30
|
+
],
|
|
13
31
|
"scripts": {
|
|
14
32
|
"build": "tsc",
|
|
15
33
|
"start": "node dist/server.js",
|
|
16
34
|
"dev": "tsx src/server.ts",
|
|
17
|
-
"test": "npm run build &&
|
|
35
|
+
"test": "npm run build && npm run test:run",
|
|
36
|
+
"test:run": "vitest run",
|
|
18
37
|
"test:unit": "vitest run --config vitest.config.unit.ts",
|
|
19
38
|
"test:e2e": "npm run build && vitest run --config vitest.config.e2e.ts",
|
|
20
39
|
"test:e2e:local": "npm run build && vitest run --config vitest.config.e2e.ts",
|
|
40
|
+
"test:live": "npm run build && npm run test:live:run",
|
|
41
|
+
"test:live:run": "vitest run --config vitest.config.live.ts",
|
|
42
|
+
"test:package": "npm run build && npm run test:package:run",
|
|
43
|
+
"test:package:run": "vitest run --config vitest.config.package.ts",
|
|
44
|
+
"test:release": "npm run build && npm run test:run && npm run test:live:run && npm run test:package:run",
|
|
21
45
|
"test:coverage": "npm run build && vitest --coverage --config vitest.config.unit.ts",
|
|
22
46
|
"test:watch": "vitest --watch",
|
|
23
47
|
"cli.run": "tsx src/cli.ts",
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.mkXultra/ai-cli-mcp",
|
|
4
|
-
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, and
|
|
4
|
+
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, Forge, and OpenCode) with background process management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/mkXultra/ai-cli-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.20.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ai-cli-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.20.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|
package/.gemini/settings.json
DELETED
package/.github/dependabot.yml
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
version: 2
|
|
2
|
-
updates:
|
|
3
|
-
- package-ecosystem: "npm"
|
|
4
|
-
directory: "/"
|
|
5
|
-
target-branch: "develop"
|
|
6
|
-
schedule:
|
|
7
|
-
interval: "weekly"
|
|
8
|
-
open-pull-requests-limit: 5
|
|
9
|
-
groups:
|
|
10
|
-
npm-production:
|
|
11
|
-
dependency-type: "production"
|
|
12
|
-
patterns:
|
|
13
|
-
- "*"
|
|
14
|
-
npm-development:
|
|
15
|
-
dependency-type: "development"
|
|
16
|
-
patterns:
|
|
17
|
-
- "*"
|
|
18
|
-
|
|
19
|
-
- package-ecosystem: "github-actions"
|
|
20
|
-
directory: "/"
|
|
21
|
-
target-branch: "develop"
|
|
22
|
-
schedule:
|
|
23
|
-
interval: "weekly"
|
|
24
|
-
open-pull-requests-limit: 2
|
|
25
|
-
groups:
|
|
26
|
-
github-actions:
|
|
27
|
-
patterns:
|
|
28
|
-
- "*"
|