ai-cli-mcp 2.12.0 → 2.14.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 (46) hide show
  1. package/.github/workflows/publish.yml +25 -0
  2. package/CHANGELOG.md +20 -0
  3. package/README.ja.md +20 -5
  4. package/README.md +20 -6
  5. package/dist/__tests__/app-cli.test.js +34 -2
  6. package/dist/__tests__/cli-bin-smoke.test.js +4 -0
  7. package/dist/__tests__/cli-builder.test.js +37 -0
  8. package/dist/__tests__/cli-process-service.test.js +180 -5
  9. package/dist/__tests__/cli-utils.test.js +31 -0
  10. package/dist/__tests__/mcp-contract.test.js +287 -9
  11. package/dist/__tests__/parsers.test.js +37 -1
  12. package/dist/__tests__/process-management.test.js +2 -1
  13. package/dist/app/cli.js +8 -6
  14. package/dist/app/mcp.js +16 -8
  15. package/dist/cli-builder.js +14 -0
  16. package/dist/cli-parse.js +8 -5
  17. package/dist/cli-process-service.js +13 -23
  18. package/dist/cli-utils.js +17 -0
  19. package/dist/cli.js +4 -3
  20. package/dist/model-catalog.js +4 -1
  21. package/dist/parsers.js +55 -0
  22. package/dist/process-result.js +51 -0
  23. package/dist/process-service.js +11 -22
  24. package/dist/server.js +1 -1
  25. package/package.json +2 -2
  26. package/server.json +1 -1
  27. package/src/__tests__/app-cli.test.ts +43 -1
  28. package/src/__tests__/cli-bin-smoke.test.ts +4 -0
  29. package/src/__tests__/cli-builder.test.ts +47 -0
  30. package/src/__tests__/cli-process-service.test.ts +200 -5
  31. package/src/__tests__/cli-utils.test.ts +34 -0
  32. package/src/__tests__/mcp-contract.test.ts +325 -9
  33. package/src/__tests__/parsers.test.ts +44 -1
  34. package/src/__tests__/process-management.test.ts +2 -1
  35. package/src/app/cli.ts +9 -7
  36. package/src/app/mcp.ts +17 -8
  37. package/src/cli-builder.ts +18 -3
  38. package/src/cli-parse.ts +8 -5
  39. package/src/cli-process-service.ts +12 -23
  40. package/src/cli-utils.ts +21 -1
  41. package/src/cli.ts +4 -3
  42. package/src/model-catalog.ts +5 -1
  43. package/src/parsers.ts +61 -0
  44. package/src/process-result.ts +79 -0
  45. package/src/process-service.ts +11 -24
  46. package/src/server.ts +1 -1
package/src/cli-parse.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
2
+ import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
3
3
 
4
- const AGENTS = ['claude', 'codex', 'gemini'] as const;
4
+ const AGENTS = ['claude', 'codex', 'gemini', 'forge'] as const;
5
5
  type Agent = typeof AGENTS[number];
6
6
 
7
- const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini>
7
+ const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini|forge>
8
8
 
9
9
  Reads raw CLI output from stdin and outputs parsed JSON to stdout.
10
10
 
11
11
  Options:
12
- --agent Agent type: claude, codex, or gemini (required)
12
+ --agent Agent type: claude, codex, gemini, or forge (required)
13
13
  --help Show this help message
14
14
 
15
15
  Examples:
@@ -62,7 +62,7 @@ async function main(): Promise<void> {
62
62
 
63
63
  const agent = args.agent as Agent;
64
64
  if (!agent || !AGENTS.includes(agent)) {
65
- process.stderr.write(`Error: --agent is required (claude, codex, or gemini)\n\n`);
65
+ process.stderr.write(`Error: --agent is required (claude, codex, gemini, or forge)\n\n`);
66
66
  process.stderr.write(USAGE);
67
67
  process.exit(1);
68
68
  }
@@ -85,6 +85,9 @@ async function main(): Promise<void> {
85
85
  case 'gemini':
86
86
  parsed = parseGeminiOutput(input);
87
87
  break;
88
+ case 'forge':
89
+ parsed = parseForgeOutput(input);
90
+ break;
88
91
  }
89
92
 
90
93
  process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');
@@ -15,8 +15,9 @@ import {
15
15
  import { join } from 'node:path';
16
16
  import { homedir } from 'node:os';
17
17
  import { buildCliCommand, type BuildCliCommandOptions } from './cli-builder.js';
18
- import { findClaudeCli, findCodexCli, findGeminiCli } from './cli-utils.js';
19
- import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
18
+ import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
19
+ import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
20
+ import { buildProcessResult } from './process-result.js';
20
21
  import type { AgentType, ProcessListItem } from './process-service.js';
21
22
 
22
23
  interface StoredProcess {
@@ -78,6 +79,7 @@ export class CliProcessService {
78
79
  claude: findClaudeCli(),
79
80
  codex: findCodexCli(),
80
81
  gemini: findGeminiCli(),
82
+ forge: findForgeCli(),
81
83
  };
82
84
  mkdirSync(this.stateDir, { recursive: true });
83
85
  }
@@ -177,10 +179,12 @@ export class CliProcessService {
177
179
  agentOutput = parseClaudeOutput(stdout);
178
180
  } else if (refreshed.toolType === 'gemini') {
179
181
  agentOutput = parseGeminiOutput(stdout);
182
+ } else if (refreshed.toolType === 'forge') {
183
+ agentOutput = parseForgeOutput(stdout);
180
184
  }
181
185
  }
182
186
 
183
- const response: any = {
187
+ return buildProcessResult({
184
188
  pid,
185
189
  agent: refreshed.toolType,
186
190
  status: refreshed.status,
@@ -189,27 +193,12 @@ export class CliProcessService {
189
193
  workFolder: refreshed.workFolder,
190
194
  prompt: refreshed.prompt,
191
195
  model: refreshed.model,
192
- };
193
-
194
- if (agentOutput) {
195
- if (!verbose && agentOutput.tools) {
196
- const { tools, ...rest } = agentOutput;
197
- response.agentOutput = rest;
198
- } else {
199
- response.agentOutput = agentOutput;
200
- }
201
- if (agentOutput.session_id) {
202
- response.session_id = agentOutput.session_id;
203
- }
204
- } else {
205
- response.stdout = stdout;
206
- response.stderr = stderr;
207
- }
208
-
209
- return response;
196
+ stdout,
197
+ stderr,
198
+ }, agentOutput, verbose);
210
199
  }
211
200
 
212
- async waitForProcesses(pids: number[], timeoutSeconds = 180): Promise<any[]> {
201
+ async waitForProcesses(pids: number[], timeoutSeconds = 180, verbose = false): Promise<any[]> {
213
202
  const start = Date.now();
214
203
  for (const pid of pids) {
215
204
  this.readProcess(pid);
@@ -218,7 +207,7 @@ export class CliProcessService {
218
207
  while (true) {
219
208
  const statuses = pids.map((pid) => this.refreshStatus(this.readProcess(pid)).status);
220
209
  if (statuses.every((status) => status !== 'running')) {
221
- return Promise.all(pids.map((pid) => this.getProcessResult(pid, false)));
210
+ return Promise.all(pids.map((pid) => this.getProcessResult(pid, verbose)));
222
211
  }
223
212
 
224
213
  if (Date.now() - start >= timeoutSeconds * 1000) {
package/src/cli-utils.ts CHANGED
@@ -148,7 +148,7 @@ function isExecutableFile(filePath: string): boolean {
148
148
  }
149
149
  }
150
150
 
151
- type CliBinaryName = 'claude' | 'codex' | 'gemini';
151
+ type CliBinaryName = 'claude' | 'codex' | 'gemini' | 'forge';
152
152
 
153
153
  function getCliBinaryConfig(name: CliBinaryName): {
154
154
  envVarName: string;
@@ -174,6 +174,15 @@ function getCliBinaryConfig(name: CliBinaryName): {
174
174
  };
175
175
  }
176
176
 
177
+ if (name === 'forge') {
178
+ return {
179
+ envVarName: 'FORGE_CLI_NAME',
180
+ customCliName: process.env.FORGE_CLI_NAME,
181
+ defaultCliName: 'forge',
182
+ localInstallPath: join(homedir(), '.forge', 'local', 'forge'),
183
+ };
184
+ }
185
+
177
186
  return {
178
187
  envVarName: 'GEMINI_CLI_NAME',
179
188
  customCliName: process.env.GEMINI_CLI_NAME,
@@ -190,11 +199,13 @@ export function getCliDoctorStatus(): {
190
199
  claude: CliBinaryStatus;
191
200
  codex: CliBinaryStatus;
192
201
  gemini: CliBinaryStatus;
202
+ forge: CliBinaryStatus;
193
203
  } {
194
204
  return {
195
205
  claude: getCliBinaryStatus('claude'),
196
206
  codex: getCliBinaryStatus('codex'),
197
207
  gemini: getCliBinaryStatus('gemini'),
208
+ forge: getCliBinaryStatus('forge'),
198
209
  };
199
210
  }
200
211
 
@@ -218,6 +229,15 @@ export function findCodexCli(): string {
218
229
  return getCliCommandOrThrow(status);
219
230
  }
220
231
 
232
+ /**
233
+ * Determine the Forge CLI command/path.
234
+ */
235
+ export function findForgeCli(): string {
236
+ debugLog('[Debug] Attempting to find Forge CLI...');
237
+ const status = getCliBinaryStatus('forge');
238
+ return getCliCommandOrThrow(status);
239
+ }
240
+
221
241
  /**
222
242
  * Determine the Claude CLI command/path.
223
243
  * 1. Checks for CLAUDE_CLI_NAME environment variable:
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
3
  import { buildCliCommand } from './cli-builder.js';
4
- import { findClaudeCli, findCodexCli, findGeminiCli } from './cli-utils.js';
4
+ import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
5
5
 
6
6
  /**
7
7
  * Minimal argv parser. No external dependencies.
@@ -35,12 +35,12 @@ function parseArgs(argv: string[]): Record<string, string> {
35
35
  const USAGE = `Usage: npm run -s cli.run -- --model <model> --workFolder <path> --prompt "..." [options]
36
36
 
37
37
  Options:
38
- --model Model name or alias (e.g. sonnet, opus, gpt-5.2-codex, gemini-2.5-pro)
38
+ --model Model name or alias (e.g. sonnet, opus, gpt-5.2-codex, gemini-2.5-pro, forge)
39
39
  --workFolder Working directory (absolute path)
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
43
- --reasoning_effort Claude/Codex: Claude=low|medium|high, Codex=low|medium|high|xhigh
43
+ --reasoning_effort Claude/Codex only: Claude=low|medium|high, Codex=low|medium|high|xhigh
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:
@@ -73,6 +73,7 @@ async function main(): Promise<void> {
73
73
  claude: findClaudeCli(),
74
74
  codex: findCodexCli(),
75
75
  gemini: findGeminiCli(),
76
+ forge: findForgeCli(),
76
77
  };
77
78
 
78
79
  // Build command
@@ -19,6 +19,7 @@ export const GEMINI_MODELS = [
19
19
  'gemini-3-pro-preview',
20
20
  'gemini-3-flash-preview',
21
21
  ] as const;
22
+ export const FORGE_MODELS = ['forge'] as const;
22
23
 
23
24
  export const MODEL_ALIASES: Record<string, string> = {
24
25
  'claude-ultra': 'opus',
@@ -38,11 +39,12 @@ export function getSupportedModelsDescription(): string {
38
39
  ...CLAUDE_MODELS.map((model) => `"${model}"`),
39
40
  ...CODEX_MODELS.map((model) => `"${model}"`),
40
41
  ...GEMINI_MODELS.map((model) => `"${model}"`),
42
+ ...FORGE_MODELS.map((model) => `"${model}"`),
41
43
  ].join(', ');
42
44
  }
43
45
 
44
46
  export function getModelParameterDescription(): string {
45
- 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].map((model) => `"${model}"`).join(', ')}.`;
47
+ 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].map((model) => `"${model}"`).join(', ')}. "forge" is a provider key, not a Forge model family selector.`;
46
48
  }
47
49
 
48
50
  export function getModelsPayload(): {
@@ -50,11 +52,13 @@ export function getModelsPayload(): {
50
52
  claude: ReadonlyArray<string>;
51
53
  codex: ReadonlyArray<string>;
52
54
  gemini: ReadonlyArray<string>;
55
+ forge: ReadonlyArray<string>;
53
56
  } {
54
57
  return {
55
58
  aliases: MODEL_ALIAS_DETAILS,
56
59
  claude: CLAUDE_MODELS,
57
60
  codex: CODEX_MODELS,
58
61
  gemini: GEMINI_MODELS,
62
+ forge: FORGE_MODELS,
59
63
  };
60
64
  }
package/src/parsers.ts CHANGED
@@ -167,3 +167,64 @@ export function parseGeminiOutput(stdout: string): any {
167
167
  return null;
168
168
  }
169
169
  }
170
+
171
+ /**
172
+ * Parse Forge output framed by Initialize/Continue/Finished markers.
173
+ */
174
+ export function parseForgeOutput(stdout: string): any {
175
+ if (!stdout) return null;
176
+
177
+ const lines = stdout.split('\n');
178
+ const markerPattern = /^● \[[^\]]+\] (Initialize|Continue|Finished) (\S+)\s*$/;
179
+ let collecting = false;
180
+ let currentConversationId: string | null = null;
181
+ let currentBody: string[] = [];
182
+ let lastConversationId: string | null = null;
183
+ let lastMessage: string | null = null;
184
+
185
+ for (const line of lines) {
186
+ const match = line.match(markerPattern);
187
+ if (match) {
188
+ const [, action, conversationId] = match;
189
+ lastConversationId = conversationId;
190
+
191
+ if (action === 'Initialize' || action === 'Continue') {
192
+ collecting = true;
193
+ currentConversationId = conversationId;
194
+ currentBody = [];
195
+ } else if (collecting && currentConversationId === conversationId) {
196
+ const message = currentBody.join('\n').trim();
197
+ if (message) {
198
+ lastMessage = message;
199
+ }
200
+ collecting = false;
201
+ currentConversationId = null;
202
+ currentBody = [];
203
+ }
204
+ continue;
205
+ }
206
+
207
+ if (collecting) {
208
+ currentBody.push(line);
209
+ }
210
+ }
211
+
212
+ if (collecting) {
213
+ const message = currentBody.join('\n').trim();
214
+ if (message) {
215
+ lastMessage = message;
216
+ }
217
+ if (currentConversationId) {
218
+ lastConversationId = currentConversationId;
219
+ }
220
+ }
221
+
222
+ if (!lastMessage && !lastConversationId) {
223
+ return null;
224
+ }
225
+
226
+ return {
227
+ message: lastMessage,
228
+ session_id: lastConversationId,
229
+ };
230
+ }
@@ -0,0 +1,79 @@
1
+ import type { AgentType, ProcessStatus } from './process-service.js';
2
+
3
+ interface ProcessResultContext {
4
+ pid: number;
5
+ agent: AgentType;
6
+ status: ProcessStatus;
7
+ exitCode?: number;
8
+ startTime: string;
9
+ workFolder: string;
10
+ prompt: string;
11
+ model?: string;
12
+ stdout: string;
13
+ stderr: string;
14
+ }
15
+
16
+ function compactAgentOutput(agentOutput: any): any | null {
17
+ if (!agentOutput || typeof agentOutput !== 'object') {
18
+ return null;
19
+ }
20
+
21
+ const { tools: _tools, ...rest } = agentOutput;
22
+ const compact = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined && value !== null));
23
+ return Object.keys(compact).length > 0 ? compact : null;
24
+ }
25
+
26
+ function hasMeaningfulParsedOutput(agentOutput: any): boolean {
27
+ if (!agentOutput || typeof agentOutput !== 'object') {
28
+ return false;
29
+ }
30
+
31
+ return Object.entries(agentOutput).some(([key, value]) => {
32
+ if (value === undefined || value === null) {
33
+ return false;
34
+ }
35
+
36
+ if (key === 'session_id') {
37
+ return false;
38
+ }
39
+
40
+ if (key === 'tools') {
41
+ return Array.isArray(value) ? value.length > 0 : true;
42
+ }
43
+
44
+ return true;
45
+ });
46
+ }
47
+
48
+ export function buildProcessResult(context: ProcessResultContext, agentOutput: any, verbose = false): any {
49
+ const response: any = {
50
+ pid: context.pid,
51
+ agent: context.agent,
52
+ status: context.status,
53
+ exitCode: context.exitCode ?? null,
54
+ model: context.model ?? null,
55
+ };
56
+
57
+ if (verbose) {
58
+ response.startTime = context.startTime;
59
+ response.workFolder = context.workFolder;
60
+ response.prompt = context.prompt;
61
+ }
62
+
63
+ if (agentOutput?.session_id) {
64
+ response.session_id = agentOutput.session_id;
65
+ }
66
+
67
+ const shapedAgentOutput = verbose ? agentOutput : compactAgentOutput(agentOutput);
68
+
69
+ if (hasMeaningfulParsedOutput(shapedAgentOutput)) {
70
+ response.agentOutput = shapedAgentOutput;
71
+ }
72
+
73
+ if (!response.agentOutput) {
74
+ response.stdout = context.stdout;
75
+ response.stderr = context.stderr;
76
+ }
77
+
78
+ return response;
79
+ }
@@ -1,8 +1,9 @@
1
1
  import { spawn, type ChildProcess } from 'node:child_process';
2
2
  import { buildCliCommand, type BuildCliCommandOptions } from './cli-builder.js';
3
- import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
3
+ import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
4
+ import { buildProcessResult } from './process-result.js';
4
5
 
5
- export type AgentType = 'claude' | 'codex' | 'gemini';
6
+ export type AgentType = 'claude' | 'codex' | 'gemini' | 'forge';
6
7
  export type ProcessStatus = 'running' | 'completed' | 'failed';
7
8
 
8
9
  interface TrackedProcess {
@@ -144,10 +145,12 @@ export class ProcessService {
144
145
  agentOutput = parseClaudeOutput(process.stdout);
145
146
  } else if (process.toolType === 'gemini') {
146
147
  agentOutput = parseGeminiOutput(process.stdout);
148
+ } else if (process.toolType === 'forge') {
149
+ agentOutput = parseForgeOutput(process.stdout);
147
150
  }
148
151
  }
149
152
 
150
- const response: any = {
153
+ return buildProcessResult({
151
154
  pid,
152
155
  agent: process.toolType,
153
156
  status: process.status,
@@ -156,28 +159,12 @@ export class ProcessService {
156
159
  workFolder: process.workFolder,
157
160
  prompt: process.prompt,
158
161
  model: process.model,
159
- };
160
-
161
- if (agentOutput) {
162
- if (!verbose && agentOutput.tools) {
163
- const { tools, ...rest } = agentOutput;
164
- response.agentOutput = rest;
165
- } else {
166
- response.agentOutput = agentOutput;
167
- }
168
-
169
- if (agentOutput.session_id) {
170
- response.session_id = agentOutput.session_id;
171
- }
172
- } else {
173
- response.stdout = process.stdout;
174
- response.stderr = process.stderr;
175
- }
176
-
177
- return response;
162
+ stdout: process.stdout,
163
+ stderr: process.stderr,
164
+ }, agentOutput, verbose);
178
165
  }
179
166
 
180
- async waitForProcesses(pids: number[], timeoutSeconds = 180): Promise<any[]> {
167
+ async waitForProcesses(pids: number[], timeoutSeconds = 180, verbose = false): Promise<any[]> {
181
168
  for (const pid of pids) {
182
169
  if (!this.processManager.has(pid)) {
183
170
  throw new Error(`Process with PID ${pid} not found`);
@@ -209,7 +196,7 @@ export class ProcessService {
209
196
 
210
197
  try {
211
198
  await Promise.race([Promise.all(waitPromises), timeoutPromise]);
212
- return pids.map((pid) => this.getProcessResult(pid, false));
199
+ return pids.map((pid) => this.getProcessResult(pid, verbose));
213
200
  } finally {
214
201
  if (timeoutHandle) {
215
202
  clearTimeout(timeoutHandle);
package/src/server.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- export { debugLog, findClaudeCli, findCodexCli, findGeminiCli } from './cli-utils.js';
2
+ export { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
3
3
  export { resolveModelAlias } from './cli-builder.js';
4
4
  export { ClaudeCodeServer, runMcpServer, spawnAsync } from './app/mcp.js';
5
5