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.
Files changed (101) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +37 -11
  3. package/README.md +44 -11
  4. package/dist/app/cli.js +2 -1
  5. package/dist/app/mcp.js +65 -13
  6. package/dist/cli-builder.js +13 -6
  7. package/dist/cli-process-service.js +81 -95
  8. package/dist/cli-utils.js +6 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/model-catalog.js +3 -2
  11. package/dist/parsers.js +111 -8
  12. package/dist/process-service.js +5 -4
  13. package/package.json +26 -2
  14. package/server.json +3 -3
  15. package/.gemini/settings.json +0 -11
  16. package/.github/dependabot.yml +0 -28
  17. package/.github/pull_request_template.md +0 -28
  18. package/.github/workflows/ci.yml +0 -34
  19. package/.github/workflows/dependency-review.yml +0 -22
  20. package/.github/workflows/publish.yml +0 -89
  21. package/.github/workflows/test.yml +0 -20
  22. package/.github/workflows/watch-session-prs.yml +0 -276
  23. package/.husky/pre-commit +0 -1
  24. package/.mcp.json +0 -11
  25. package/.releaserc.json +0 -18
  26. package/.vscode/settings.json +0 -3
  27. package/CONTRIBUTING.md +0 -81
  28. package/dist/__tests__/app-cli.test.js +0 -392
  29. package/dist/__tests__/cli-bin-smoke.test.js +0 -101
  30. package/dist/__tests__/cli-builder.test.js +0 -442
  31. package/dist/__tests__/cli-process-service.test.js +0 -655
  32. package/dist/__tests__/cli-utils.test.js +0 -171
  33. package/dist/__tests__/e2e.test.js +0 -256
  34. package/dist/__tests__/edge-cases.test.js +0 -130
  35. package/dist/__tests__/error-cases.test.js +0 -292
  36. package/dist/__tests__/mcp-contract.test.js +0 -636
  37. package/dist/__tests__/mocks.js +0 -32
  38. package/dist/__tests__/model-alias.test.js +0 -36
  39. package/dist/__tests__/parsers.test.js +0 -500
  40. package/dist/__tests__/peek.test.js +0 -36
  41. package/dist/__tests__/process-management.test.js +0 -871
  42. package/dist/__tests__/server.test.js +0 -809
  43. package/dist/__tests__/setup.js +0 -11
  44. package/dist/__tests__/utils/claude-mock.js +0 -80
  45. package/dist/__tests__/utils/mcp-client.js +0 -121
  46. package/dist/__tests__/utils/opencode-mock.js +0 -91
  47. package/dist/__tests__/utils/persistent-mock.js +0 -28
  48. package/dist/__tests__/utils/test-helpers.js +0 -11
  49. package/dist/__tests__/validation.test.js +0 -308
  50. package/dist/__tests__/version-print.test.js +0 -65
  51. package/dist/__tests__/wait.test.js +0 -260
  52. package/docs/RELEASE_CHECKLIST.md +0 -65
  53. package/docs/cli-architecture.md +0 -275
  54. package/docs/concept.md +0 -154
  55. package/docs/development.md +0 -156
  56. package/docs/e2e-testing.md +0 -148
  57. package/docs/prd.md +0 -146
  58. package/docs/session-stacking.md +0 -67
  59. package/src/__tests__/app-cli.test.ts +0 -495
  60. package/src/__tests__/cli-bin-smoke.test.ts +0 -136
  61. package/src/__tests__/cli-builder.test.ts +0 -549
  62. package/src/__tests__/cli-process-service.test.ts +0 -759
  63. package/src/__tests__/cli-utils.test.ts +0 -200
  64. package/src/__tests__/e2e.test.ts +0 -311
  65. package/src/__tests__/edge-cases.test.ts +0 -176
  66. package/src/__tests__/error-cases.test.ts +0 -370
  67. package/src/__tests__/mcp-contract.test.ts +0 -755
  68. package/src/__tests__/mocks.ts +0 -35
  69. package/src/__tests__/model-alias.test.ts +0 -44
  70. package/src/__tests__/parsers.test.ts +0 -564
  71. package/src/__tests__/peek.test.ts +0 -44
  72. package/src/__tests__/process-management.test.ts +0 -1043
  73. package/src/__tests__/server.test.ts +0 -1020
  74. package/src/__tests__/setup.ts +0 -13
  75. package/src/__tests__/utils/claude-mock.ts +0 -87
  76. package/src/__tests__/utils/mcp-client.ts +0 -159
  77. package/src/__tests__/utils/opencode-mock.ts +0 -108
  78. package/src/__tests__/utils/persistent-mock.ts +0 -33
  79. package/src/__tests__/utils/test-helpers.ts +0 -13
  80. package/src/__tests__/validation.test.ts +0 -369
  81. package/src/__tests__/version-print.test.ts +0 -81
  82. package/src/__tests__/wait.test.ts +0 -302
  83. package/src/app/cli.ts +0 -424
  84. package/src/app/mcp.ts +0 -466
  85. package/src/bin/ai-cli-mcp.ts +0 -7
  86. package/src/bin/ai-cli.ts +0 -11
  87. package/src/cli-builder.ts +0 -274
  88. package/src/cli-parse.ts +0 -105
  89. package/src/cli-process-service.ts +0 -708
  90. package/src/cli-utils.ts +0 -258
  91. package/src/cli.ts +0 -124
  92. package/src/model-catalog.ts +0 -87
  93. package/src/parsers.ts +0 -840
  94. package/src/peek.ts +0 -95
  95. package/src/process-result.ts +0 -88
  96. package/src/process-service.ts +0 -367
  97. package/src/server.ts +0 -10
  98. package/tsconfig.json +0 -16
  99. package/vitest.config.e2e.ts +0 -27
  100. package/vitest.config.ts +0 -22
  101. 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, unlinkSync, writeFileSync, } from 'node:fs';
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
- if (cmd.agent === 'opencode') {
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
- appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs));
243
- appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs));
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
- refreshed.status = 'failed';
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 startDetachedOpenCodeProcess(cmd, model) {
250
+ async startDetachedTrackedProcess(cmd, model) {
298
251
  const cwdKey = this.resolveCwdKey(cmd.cwd);
299
- const wrapperPath = this.ensureOpenCodeWrapperScript();
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
- if (!existsSync(stdoutPath)) {
315
- writeFileSync(stdoutPath, '');
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 = 'completed';
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
- resolveOpenCodeWrapperPath() {
474
- return join(this.stateDir, 'opencode-detached-wrapper.sh');
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
- resolveStderrPathForPidPlaceholder() {
480
- return join(this.stateDir, `pending-${Date.now()}-${Math.random().toString(36).slice(2)}.stderr.log`);
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
- "$@" >> "$stdout_path" 2>> "$stderr_path"
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
- printf '{\n "status": "%s",\n "exitCode": %s\n}\n' "$status" "$exit_code" > "$exit_meta_path"
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
- renamePlaceholderFile(fromPath, toPath) {
513
- renameSync(fromPath, toPath);
514
- }
515
- removeFileIfExists(filePath) {
516
- if (existsSync(filePath)) {
517
- unlinkSync(filePath);
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:
@@ -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: 'high' },
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 high 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
+ 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
- const line = this.pending;
273
- this.pending = '';
274
- events.push(...this.extractLines([line], observedAt));
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
- if (lastMessage || sessionId || tools.length > 0) {
581
+ const fallbackMessage = assistantTextBuffer.trim() ? assistantTextBuffer : null;
582
+ const message = lastMessage || fallbackMessage;
583
+ if (message || sessionId || tools.length > 0) {
481
584
  return {
482
- message: lastMessage,
585
+ message,
483
586
  session_id: sessionId,
484
587
  tools: tools.length > 0 ? tools : undefined
485
588
  };
@@ -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
- appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs));
214
- appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs));
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.18.0",
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 && vitest run",
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 Forge) with background process management",
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.8.2",
9
+ "version": "2.20.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ai-cli-mcp",
14
- "version": "2.8.2",
14
+ "version": "2.20.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }
@@ -1,11 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "acm-dev": {
4
- "command": "npm",
5
- "args": [
6
- "run",
7
- "dev"
8
- ]
9
- }
10
- }
11
- }
@@ -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
- - "*"