ai-cli-mcp 2.12.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 +13 -0
- package/README.ja.md +10 -5
- package/README.md +10 -6
- package/dist/__tests__/app-cli.test.js +8 -0
- package/dist/__tests__/cli-bin-smoke.test.js +4 -0
- package/dist/__tests__/cli-builder.test.js +37 -0
- package/dist/__tests__/cli-process-service.test.js +46 -0
- package/dist/__tests__/cli-utils.test.js +31 -0
- package/dist/__tests__/mcp-contract.test.js +149 -1
- package/dist/__tests__/parsers.test.js +37 -1
- package/dist/app/cli.js +2 -2
- package/dist/app/mcp.js +8 -4
- package/dist/cli-builder.js +14 -0
- package/dist/cli-parse.js +8 -5
- package/dist/cli-process-service.js +6 -2
- package/dist/cli-utils.js +17 -0
- package/dist/cli.js +4 -3
- package/dist/model-catalog.js +4 -1
- package/dist/parsers.js +55 -0
- package/dist/process-service.js +4 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/server.json +1 -1
- package/src/__tests__/app-cli.test.ts +8 -0
- package/src/__tests__/cli-bin-smoke.test.ts +4 -0
- package/src/__tests__/cli-builder.test.ts +47 -0
- package/src/__tests__/cli-process-service.test.ts +56 -0
- package/src/__tests__/cli-utils.test.ts +34 -0
- package/src/__tests__/mcp-contract.test.ts +173 -1
- package/src/__tests__/parsers.test.ts +44 -1
- package/src/app/cli.ts +2 -2
- package/src/app/mcp.ts +8 -4
- package/src/cli-builder.ts +18 -3
- package/src/cli-parse.ts +8 -5
- package/src/cli-process-service.ts +5 -2
- package/src/cli-utils.ts +21 -1
- package/src/cli.ts +4 -3
- package/src/model-catalog.ts +5 -1
- package/src/parsers.ts +61 -0
- package/src/process-service.ts +4 -2
- package/src/server.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseCodexOutput, parseClaudeOutput } from '../parsers.js';
|
|
2
|
+
import { parseCodexOutput, parseClaudeOutput, parseForgeOutput } from '../parsers.js';
|
|
3
3
|
describe('parseCodexOutput', () => {
|
|
4
4
|
it('should parse basic Codex output with message and session_id', () => {
|
|
5
5
|
const output = `
|
|
@@ -96,3 +96,39 @@ INVALID_LINE
|
|
|
96
96
|
expect(result.message).toBe("Success");
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
|
+
describe('parseForgeOutput', () => {
|
|
100
|
+
it('should parse initialized forge output with a conversation id', () => {
|
|
101
|
+
const output = `● [21:09:01] Initialize 123e4567-e89b-12d3-a456-426614174000
|
|
102
|
+
Hello from Forge
|
|
103
|
+
● [21:09:08] Finished 123e4567-e89b-12d3-a456-426614174000
|
|
104
|
+
`;
|
|
105
|
+
expect(parseForgeOutput(output)).toEqual({
|
|
106
|
+
message: 'Hello from Forge',
|
|
107
|
+
session_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
it('should parse resumed forge output with multiline assistant content', () => {
|
|
111
|
+
const output = `● [21:09:33] Continue conv-123
|
|
112
|
+
Line one
|
|
113
|
+
|
|
114
|
+
Line three
|
|
115
|
+
● [21:09:37] Finished conv-123
|
|
116
|
+
`;
|
|
117
|
+
expect(parseForgeOutput(output)).toEqual({
|
|
118
|
+
message: 'Line one\n\nLine three',
|
|
119
|
+
session_id: 'conv-123',
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
it('should return the current message while forge output is still in progress', () => {
|
|
123
|
+
const output = `● [21:09:33] Continue conv-456
|
|
124
|
+
Partial answer
|
|
125
|
+
still streaming`;
|
|
126
|
+
expect(parseForgeOutput(output)).toEqual({
|
|
127
|
+
message: 'Partial answer\nstill streaming',
|
|
128
|
+
session_id: 'conv-456',
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
it('should return null for unrelated forge output', () => {
|
|
132
|
+
expect(parseForgeOutput('plain text')).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
package/dist/app/cli.js
CHANGED
|
@@ -24,9 +24,9 @@ Options:
|
|
|
24
24
|
--cwd <path> Working directory
|
|
25
25
|
--prompt <text> Prompt text
|
|
26
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)
|
|
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
28
|
--session-id <id> Resume a previous session
|
|
29
|
-
--reasoning-effort <level> Reasoning level for Claude/Codex
|
|
29
|
+
--reasoning-effort <level> Reasoning level for Claude/Codex only
|
|
30
30
|
--help, -h Show this help message
|
|
31
31
|
|
|
32
32
|
Compatibility aliases:
|
package/dist/app/mcp.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { debugLog, findClaudeCli, findCodexCli, findGeminiCli } from '../cli-utils.js';
|
|
5
|
+
import { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from '../cli-utils.js';
|
|
6
6
|
import { getModelParameterDescription, getSupportedModelsDescription } from '../model-catalog.js';
|
|
7
7
|
import { ProcessService } from '../process-service.js';
|
|
8
8
|
// Server version - update this when releasing new versions
|
|
@@ -58,6 +58,7 @@ export class ClaudeCodeServer {
|
|
|
58
58
|
claudeCliPath;
|
|
59
59
|
codexCliPath;
|
|
60
60
|
geminiCliPath;
|
|
61
|
+
forgeCliPath;
|
|
61
62
|
processService;
|
|
62
63
|
sigintHandler;
|
|
63
64
|
packageVersion;
|
|
@@ -65,15 +66,18 @@ export class ClaudeCodeServer {
|
|
|
65
66
|
this.claudeCliPath = findClaudeCli();
|
|
66
67
|
this.codexCliPath = findCodexCli();
|
|
67
68
|
this.geminiCliPath = findGeminiCli();
|
|
69
|
+
this.forgeCliPath = findForgeCli();
|
|
68
70
|
console.error(`[Setup] Using Claude CLI command/path: ${this.claudeCliPath}`);
|
|
69
71
|
console.error(`[Setup] Using Codex CLI command/path: ${this.codexCliPath}`);
|
|
70
72
|
console.error(`[Setup] Using Gemini CLI command/path: ${this.geminiCliPath}`);
|
|
73
|
+
console.error(`[Setup] Using Forge CLI command/path: ${this.forgeCliPath}`);
|
|
71
74
|
this.packageVersion = SERVER_VERSION;
|
|
72
75
|
this.processService = new ProcessService({
|
|
73
76
|
cliPaths: {
|
|
74
77
|
claude: this.claudeCliPath,
|
|
75
78
|
codex: this.codexCliPath,
|
|
76
79
|
gemini: this.geminiCliPath,
|
|
80
|
+
forge: this.forgeCliPath,
|
|
77
81
|
},
|
|
78
82
|
});
|
|
79
83
|
this.server = new Server({
|
|
@@ -97,7 +101,7 @@ export class ClaudeCodeServer {
|
|
|
97
101
|
tools: [
|
|
98
102
|
{
|
|
99
103
|
name: 'run',
|
|
100
|
-
description: `AI Agent Runner: Starts a Claude, Codex, or
|
|
104
|
+
description: `AI Agent Runner: Starts a Claude, Codex, Gemini, or Forge CLI process in the background and returns a PID immediately. Use list_processes and get_result to monitor progress.
|
|
101
105
|
|
|
102
106
|
• File ops: Create, read, (fuzzy) edit, move, copy, delete, list files, analyze/ocr images, file content analysis
|
|
103
107
|
• Code: Generate / analyse / refactor / fix
|
|
@@ -141,11 +145,11 @@ ${getSupportedModelsDescription()}
|
|
|
141
145
|
},
|
|
142
146
|
reasoning_effort: {
|
|
143
147
|
type: 'string',
|
|
144
|
-
description: 'Reasoning control for Claude and Codex. Claude uses --effort with "low", "medium", "high". Codex uses model_reasoning_effort with "low", "medium", "high", "xhigh".',
|
|
148
|
+
description: 'Reasoning control for Claude and Codex. Claude uses --effort with "low", "medium", "high". Codex uses model_reasoning_effort with "low", "medium", "high", "xhigh". Forge does not support reasoning_effort in this integration.',
|
|
145
149
|
},
|
|
146
150
|
session_id: {
|
|
147
151
|
type: 'string',
|
|
148
|
-
description: 'Optional session ID to resume a previous session. Supported for: haiku, sonnet, opus, gemini-2.5-pro, gemini-2.5-flash, gemini-3.1-pro-preview, gemini-3-pro-preview, gemini-3-flash-preview.',
|
|
152
|
+
description: 'Optional session ID to resume a previous session. Supported for: haiku, sonnet, opus, gemini-2.5-pro, gemini-2.5-flash, gemini-3.1-pro-preview, gemini-3-pro-preview, gemini-3-flash-preview, forge.',
|
|
149
153
|
},
|
|
150
154
|
},
|
|
151
155
|
required: ['workFolder'],
|
package/dist/cli-builder.js
CHANGED
|
@@ -4,6 +4,9 @@ import { MODEL_ALIASES } from './model-catalog.js';
|
|
|
4
4
|
export const ALLOWED_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
|
|
5
5
|
const CLAUDE_REASONING_EFFORTS = new Set(['low', 'medium', 'high']);
|
|
6
6
|
function getAgentForModel(model) {
|
|
7
|
+
if (model === 'forge') {
|
|
8
|
+
return 'forge';
|
|
9
|
+
}
|
|
7
10
|
if (model.startsWith('gpt-')) {
|
|
8
11
|
return 'codex';
|
|
9
12
|
}
|
|
@@ -38,6 +41,9 @@ export function getReasoningEffort(model, rawValue) {
|
|
|
38
41
|
throw new Error(`Invalid reasoning_effort: ${rawValue}. Allowed values: low, medium, high, xhigh.`);
|
|
39
42
|
}
|
|
40
43
|
const agent = getAgentForModel(model);
|
|
44
|
+
if (agent === 'forge') {
|
|
45
|
+
throw new Error('reasoning_effort is not supported for forge.');
|
|
46
|
+
}
|
|
41
47
|
if (agent === 'gemini') {
|
|
42
48
|
throw new Error('reasoning_effort is only supported for Claude and Codex models.');
|
|
43
49
|
}
|
|
@@ -134,6 +140,14 @@ export function buildCliCommand(options) {
|
|
|
134
140
|
}
|
|
135
141
|
args.push(prompt);
|
|
136
142
|
}
|
|
143
|
+
else if (agent === 'forge') {
|
|
144
|
+
cliPath = options.cliPaths.forge;
|
|
145
|
+
args = ['-C', cwd];
|
|
146
|
+
if (options.session_id && typeof options.session_id === 'string') {
|
|
147
|
+
args.push('--conversation-id', options.session_id);
|
|
148
|
+
}
|
|
149
|
+
args.push('-p', prompt);
|
|
150
|
+
}
|
|
137
151
|
else {
|
|
138
152
|
cliPath = options.cliPaths.claude;
|
|
139
153
|
args = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
|
package/dist/cli-parse.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
|
|
3
|
-
const AGENTS = ['claude', 'codex', 'gemini'];
|
|
4
|
-
const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini>
|
|
2
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
|
|
3
|
+
const AGENTS = ['claude', 'codex', 'gemini', 'forge'];
|
|
4
|
+
const USAGE = `Usage: npm run -s cli.run.parse -- --agent <claude|codex|gemini|forge>
|
|
5
5
|
|
|
6
6
|
Reads raw CLI output from stdin and outputs parsed JSON to stdout.
|
|
7
7
|
|
|
8
8
|
Options:
|
|
9
|
-
--agent Agent type: claude, codex, or
|
|
9
|
+
--agent Agent type: claude, codex, gemini, or forge (required)
|
|
10
10
|
--help Show this help message
|
|
11
11
|
|
|
12
12
|
Examples:
|
|
@@ -56,7 +56,7 @@ async function main() {
|
|
|
56
56
|
}
|
|
57
57
|
const agent = args.agent;
|
|
58
58
|
if (!agent || !AGENTS.includes(agent)) {
|
|
59
|
-
process.stderr.write(`Error: --agent is required (claude, codex, or
|
|
59
|
+
process.stderr.write(`Error: --agent is required (claude, codex, gemini, or forge)\n\n`);
|
|
60
60
|
process.stderr.write(USAGE);
|
|
61
61
|
process.exit(1);
|
|
62
62
|
}
|
|
@@ -76,6 +76,9 @@ async function main() {
|
|
|
76
76
|
case 'gemini':
|
|
77
77
|
parsed = parseGeminiOutput(input);
|
|
78
78
|
break;
|
|
79
|
+
case 'forge':
|
|
80
|
+
parsed = parseForgeOutput(input);
|
|
81
|
+
break;
|
|
79
82
|
}
|
|
80
83
|
process.stdout.write(JSON.stringify(parsed, null, 2) + '\n');
|
|
81
84
|
}
|
|
@@ -3,8 +3,8 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync,
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { buildCliCommand } from './cli-builder.js';
|
|
6
|
-
import { findClaudeCli, findCodexCli, findGeminiCli } from './cli-utils.js';
|
|
7
|
-
import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
|
|
6
|
+
import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
|
|
7
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
|
|
8
8
|
function resolveDefaultStateDir() {
|
|
9
9
|
return process.env.AI_CLI_STATE_DIR || join(homedir(), '.local', 'state', 'ai-cli');
|
|
10
10
|
}
|
|
@@ -35,6 +35,7 @@ export class CliProcessService {
|
|
|
35
35
|
claude: findClaudeCli(),
|
|
36
36
|
codex: findCodexCli(),
|
|
37
37
|
gemini: findGeminiCli(),
|
|
38
|
+
forge: findForgeCli(),
|
|
38
39
|
};
|
|
39
40
|
mkdirSync(this.stateDir, { recursive: true });
|
|
40
41
|
}
|
|
@@ -127,6 +128,9 @@ export class CliProcessService {
|
|
|
127
128
|
else if (refreshed.toolType === 'gemini') {
|
|
128
129
|
agentOutput = parseGeminiOutput(stdout);
|
|
129
130
|
}
|
|
131
|
+
else if (refreshed.toolType === 'forge') {
|
|
132
|
+
agentOutput = parseForgeOutput(stdout);
|
|
133
|
+
}
|
|
130
134
|
}
|
|
131
135
|
const response = {
|
|
132
136
|
pid,
|
package/dist/cli-utils.js
CHANGED
|
@@ -128,6 +128,14 @@ function getCliBinaryConfig(name) {
|
|
|
128
128
|
localInstallPath: join(homedir(), '.codex', 'local', 'codex'),
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
if (name === 'forge') {
|
|
132
|
+
return {
|
|
133
|
+
envVarName: 'FORGE_CLI_NAME',
|
|
134
|
+
customCliName: process.env.FORGE_CLI_NAME,
|
|
135
|
+
defaultCliName: 'forge',
|
|
136
|
+
localInstallPath: join(homedir(), '.forge', 'local', 'forge'),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
131
139
|
return {
|
|
132
140
|
envVarName: 'GEMINI_CLI_NAME',
|
|
133
141
|
customCliName: process.env.GEMINI_CLI_NAME,
|
|
@@ -143,6 +151,7 @@ export function getCliDoctorStatus() {
|
|
|
143
151
|
claude: getCliBinaryStatus('claude'),
|
|
144
152
|
codex: getCliBinaryStatus('codex'),
|
|
145
153
|
gemini: getCliBinaryStatus('gemini'),
|
|
154
|
+
forge: getCliBinaryStatus('forge'),
|
|
146
155
|
};
|
|
147
156
|
}
|
|
148
157
|
/**
|
|
@@ -163,6 +172,14 @@ export function findCodexCli() {
|
|
|
163
172
|
const status = getCliBinaryStatus('codex');
|
|
164
173
|
return getCliCommandOrThrow(status);
|
|
165
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Determine the Forge CLI command/path.
|
|
177
|
+
*/
|
|
178
|
+
export function findForgeCli() {
|
|
179
|
+
debugLog('[Debug] Attempting to find Forge CLI...');
|
|
180
|
+
const status = getCliBinaryStatus('forge');
|
|
181
|
+
return getCliCommandOrThrow(status);
|
|
182
|
+
}
|
|
166
183
|
/**
|
|
167
184
|
* Determine the Claude CLI command/path.
|
|
168
185
|
* 1. Checks for CLAUDE_CLI_NAME environment variable:
|
package/dist/cli.js
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
|
* Minimal argv parser. No external dependencies.
|
|
7
7
|
* Supports: --key value, --key=value
|
|
@@ -35,12 +35,12 @@ function parseArgs(argv) {
|
|
|
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:
|
|
@@ -68,6 +68,7 @@ async function main() {
|
|
|
68
68
|
claude: findClaudeCli(),
|
|
69
69
|
codex: findCodexCli(),
|
|
70
70
|
gemini: findGeminiCli(),
|
|
71
|
+
forge: findForgeCli(),
|
|
71
72
|
};
|
|
72
73
|
// Build command
|
|
73
74
|
let cmd;
|
package/dist/model-catalog.js
CHANGED
|
@@ -19,6 +19,7 @@ export const GEMINI_MODELS = [
|
|
|
19
19
|
'gemini-3-pro-preview',
|
|
20
20
|
'gemini-3-flash-preview',
|
|
21
21
|
];
|
|
22
|
+
export const FORGE_MODELS = ['forge'];
|
|
22
23
|
export const MODEL_ALIASES = {
|
|
23
24
|
'claude-ultra': 'opus',
|
|
24
25
|
'codex-ultra': 'gpt-5.4',
|
|
@@ -35,10 +36,11 @@ export function getSupportedModelsDescription() {
|
|
|
35
36
|
...CLAUDE_MODELS.map((model) => `"${model}"`),
|
|
36
37
|
...CODEX_MODELS.map((model) => `"${model}"`),
|
|
37
38
|
...GEMINI_MODELS.map((model) => `"${model}"`),
|
|
39
|
+
...FORGE_MODELS.map((model) => `"${model}"`),
|
|
38
40
|
].join(', ');
|
|
39
41
|
}
|
|
40
42
|
export function getModelParameterDescription() {
|
|
41
|
-
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(', ')}.`;
|
|
43
|
+
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.`;
|
|
42
44
|
}
|
|
43
45
|
export function getModelsPayload() {
|
|
44
46
|
return {
|
|
@@ -46,5 +48,6 @@ export function getModelsPayload() {
|
|
|
46
48
|
claude: CLAUDE_MODELS,
|
|
47
49
|
codex: CODEX_MODELS,
|
|
48
50
|
gemini: GEMINI_MODELS,
|
|
51
|
+
forge: FORGE_MODELS,
|
|
49
52
|
};
|
|
50
53
|
}
|
package/dist/parsers.js
CHANGED
|
@@ -163,3 +163,58 @@ export function parseGeminiOutput(stdout) {
|
|
|
163
163
|
return null;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Parse Forge output framed by Initialize/Continue/Finished markers.
|
|
168
|
+
*/
|
|
169
|
+
export function parseForgeOutput(stdout) {
|
|
170
|
+
if (!stdout)
|
|
171
|
+
return null;
|
|
172
|
+
const lines = stdout.split('\n');
|
|
173
|
+
const markerPattern = /^● \[[^\]]+\] (Initialize|Continue|Finished) (\S+)\s*$/;
|
|
174
|
+
let collecting = false;
|
|
175
|
+
let currentConversationId = null;
|
|
176
|
+
let currentBody = [];
|
|
177
|
+
let lastConversationId = null;
|
|
178
|
+
let lastMessage = null;
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
const match = line.match(markerPattern);
|
|
181
|
+
if (match) {
|
|
182
|
+
const [, action, conversationId] = match;
|
|
183
|
+
lastConversationId = conversationId;
|
|
184
|
+
if (action === 'Initialize' || action === 'Continue') {
|
|
185
|
+
collecting = true;
|
|
186
|
+
currentConversationId = conversationId;
|
|
187
|
+
currentBody = [];
|
|
188
|
+
}
|
|
189
|
+
else if (collecting && currentConversationId === conversationId) {
|
|
190
|
+
const message = currentBody.join('\n').trim();
|
|
191
|
+
if (message) {
|
|
192
|
+
lastMessage = message;
|
|
193
|
+
}
|
|
194
|
+
collecting = false;
|
|
195
|
+
currentConversationId = null;
|
|
196
|
+
currentBody = [];
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (collecting) {
|
|
201
|
+
currentBody.push(line);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (collecting) {
|
|
205
|
+
const message = currentBody.join('\n').trim();
|
|
206
|
+
if (message) {
|
|
207
|
+
lastMessage = message;
|
|
208
|
+
}
|
|
209
|
+
if (currentConversationId) {
|
|
210
|
+
lastConversationId = currentConversationId;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!lastMessage && !lastConversationId) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
message: lastMessage,
|
|
218
|
+
session_id: lastConversationId,
|
|
219
|
+
};
|
|
220
|
+
}
|
package/dist/process-service.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { buildCliCommand } from './cli-builder.js';
|
|
3
|
-
import { parseClaudeOutput, parseCodexOutput, parseGeminiOutput } from './parsers.js';
|
|
3
|
+
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
|
|
4
4
|
export class ProcessService {
|
|
5
5
|
processManager = new Map();
|
|
6
6
|
cliPaths;
|
|
@@ -96,6 +96,9 @@ export class ProcessService {
|
|
|
96
96
|
else if (process.toolType === 'gemini') {
|
|
97
97
|
agentOutput = parseGeminiOutput(process.stdout);
|
|
98
98
|
}
|
|
99
|
+
else if (process.toolType === 'forge') {
|
|
100
|
+
agentOutput = parseForgeOutput(process.stdout);
|
|
101
|
+
}
|
|
99
102
|
}
|
|
100
103
|
const response = {
|
|
101
104
|
pid,
|
package/dist/server.js
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
|
import { runMcpServer } from './app/mcp.js';
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-cli-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"mcpName": "io.github.mkXultra/ai-cli-mcp",
|
|
5
|
-
"description": "MCP server for AI CLI tools (Claude, Codex, and
|
|
5
|
+
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, and Forge) with background process management",
|
|
6
6
|
"author": "mkXultra",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"main": "dist/server.js",
|
package/server.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, and
|
|
4
|
+
"description": "MCP server for AI CLI tools (Claude, Codex, Gemini, and Forge) with background process management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/mkXultra/ai-cli-mcp",
|
|
7
7
|
"source": "github"
|
|
@@ -222,6 +222,7 @@ describe('ai-cli app', () => {
|
|
|
222
222
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"aliases"'));
|
|
223
223
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"claude-ultra"'));
|
|
224
224
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"gpt-5.4"'));
|
|
225
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"forge"'));
|
|
225
226
|
expect(stderr).not.toHaveBeenCalled();
|
|
226
227
|
});
|
|
227
228
|
|
|
@@ -247,6 +248,12 @@ describe('ai-cli app', () => {
|
|
|
247
248
|
available: true,
|
|
248
249
|
lookup: 'path',
|
|
249
250
|
},
|
|
251
|
+
forge: {
|
|
252
|
+
configuredCommand: 'forge',
|
|
253
|
+
resolvedPath: '/tmp/bin/forge',
|
|
254
|
+
available: true,
|
|
255
|
+
lookup: 'path',
|
|
256
|
+
},
|
|
250
257
|
});
|
|
251
258
|
|
|
252
259
|
const exitCode = await runCli(['doctor'], { stdout, stderr, getDoctorStatus });
|
|
@@ -280,6 +287,7 @@ describe('ai-cli app', () => {
|
|
|
280
287
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('claude-ultra'));
|
|
281
288
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('gpt-5.2-codex'));
|
|
282
289
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('gemini-2.5-pro'));
|
|
290
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('forge'));
|
|
283
291
|
expect(stderr).not.toHaveBeenCalled();
|
|
284
292
|
});
|
|
285
293
|
|
|
@@ -30,6 +30,7 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
30
30
|
writeExecutable(fakeBinDir, 'claude');
|
|
31
31
|
writeExecutable(fakeBinDir, 'codex');
|
|
32
32
|
writeExecutable(fakeBinDir, 'gemini');
|
|
33
|
+
writeExecutable(fakeBinDir, 'forge');
|
|
33
34
|
|
|
34
35
|
const output = execFileSync(
|
|
35
36
|
'node',
|
|
@@ -43,6 +44,7 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
43
44
|
CLAUDE_CLI_NAME: 'claude',
|
|
44
45
|
CODEX_CLI_NAME: 'codex',
|
|
45
46
|
GEMINI_CLI_NAME: 'gemini',
|
|
47
|
+
FORGE_CLI_NAME: 'forge',
|
|
46
48
|
},
|
|
47
49
|
}
|
|
48
50
|
);
|
|
@@ -50,6 +52,7 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
50
52
|
expect(output).toContain('"claude"');
|
|
51
53
|
expect(output).toContain('"codex"');
|
|
52
54
|
expect(output).toContain('"gemini"');
|
|
55
|
+
expect(output).toContain('"forge"');
|
|
53
56
|
expect(output).toContain('"available": true');
|
|
54
57
|
});
|
|
55
58
|
|
|
@@ -67,5 +70,6 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
67
70
|
expect(output).toContain('Usage: ai-cli run --cwd <path> [options]');
|
|
68
71
|
expect(output).toContain('--model <model>');
|
|
69
72
|
expect(output).toContain('claude-ultra');
|
|
73
|
+
expect(output).toContain('forge');
|
|
70
74
|
});
|
|
71
75
|
});
|
|
@@ -22,6 +22,7 @@ const DEFAULT_CLI_PATHS = {
|
|
|
22
22
|
claude: '/usr/bin/claude',
|
|
23
23
|
codex: '/usr/bin/codex',
|
|
24
24
|
gemini: '/usr/bin/gemini',
|
|
25
|
+
forge: '/usr/bin/forge',
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
describe('cli-builder', () => {
|
|
@@ -97,6 +98,12 @@ describe('cli-builder', () => {
|
|
|
97
98
|
'reasoning_effort is only supported for Claude and Codex models.'
|
|
98
99
|
);
|
|
99
100
|
});
|
|
101
|
+
|
|
102
|
+
it('should reject reasoning_effort for forge explicitly', () => {
|
|
103
|
+
expect(() => getReasoningEffort('forge', 'high')).toThrow(
|
|
104
|
+
'reasoning_effort is not supported for forge.'
|
|
105
|
+
);
|
|
106
|
+
});
|
|
100
107
|
});
|
|
101
108
|
|
|
102
109
|
describe('buildCliCommand', () => {
|
|
@@ -401,5 +408,45 @@ describe('cli-builder', () => {
|
|
|
401
408
|
expect(cmd.resolvedModel).toBe('gemini-3.1-pro-preview');
|
|
402
409
|
});
|
|
403
410
|
});
|
|
411
|
+
|
|
412
|
+
describe('forge agent', () => {
|
|
413
|
+
it('should build forge command without model flags', () => {
|
|
414
|
+
const cmd = buildCliCommand({
|
|
415
|
+
prompt: 'test',
|
|
416
|
+
workFolder: '/tmp',
|
|
417
|
+
model: 'forge',
|
|
418
|
+
cliPaths: DEFAULT_CLI_PATHS,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
expect(cmd.agent).toBe('forge');
|
|
422
|
+
expect(cmd.cliPath).toBe('/usr/bin/forge');
|
|
423
|
+
expect(cmd.resolvedModel).toBe('forge');
|
|
424
|
+
expect(cmd.args).toEqual(['-C', '/tmp', '-p', 'test']);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should map session_id to --conversation-id for forge', () => {
|
|
428
|
+
const cmd = buildCliCommand({
|
|
429
|
+
prompt: 'test',
|
|
430
|
+
workFolder: '/tmp',
|
|
431
|
+
model: 'forge',
|
|
432
|
+
session_id: 'forge-conv-123',
|
|
433
|
+
cliPaths: DEFAULT_CLI_PATHS,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(cmd.args).toEqual(['-C', '/tmp', '--conversation-id', 'forge-conv-123', '-p', 'test']);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should reject reasoning_effort for forge in command building', () => {
|
|
440
|
+
expect(() =>
|
|
441
|
+
buildCliCommand({
|
|
442
|
+
prompt: 'test',
|
|
443
|
+
workFolder: '/tmp',
|
|
444
|
+
model: 'forge',
|
|
445
|
+
reasoning_effort: 'high',
|
|
446
|
+
cliPaths: DEFAULT_CLI_PATHS,
|
|
447
|
+
})
|
|
448
|
+
).toThrow('reasoning_effort is not supported for forge.');
|
|
449
|
+
});
|
|
450
|
+
});
|
|
404
451
|
});
|
|
405
452
|
});
|
|
@@ -65,6 +65,7 @@ describe('CliProcessService', () => {
|
|
|
65
65
|
claude: scriptPath,
|
|
66
66
|
codex: scriptPath,
|
|
67
67
|
gemini: scriptPath,
|
|
68
|
+
forge: scriptPath,
|
|
68
69
|
},
|
|
69
70
|
});
|
|
70
71
|
|
|
@@ -114,6 +115,7 @@ describe('CliProcessService', () => {
|
|
|
114
115
|
claude: scriptPath,
|
|
115
116
|
codex: scriptPath,
|
|
116
117
|
gemini: scriptPath,
|
|
118
|
+
forge: scriptPath,
|
|
117
119
|
},
|
|
118
120
|
});
|
|
119
121
|
|
|
@@ -152,6 +154,7 @@ describe('CliProcessService', () => {
|
|
|
152
154
|
claude: '/bin/sh',
|
|
153
155
|
codex: '/bin/sh',
|
|
154
156
|
gemini: '/bin/sh',
|
|
157
|
+
forge: '/bin/sh',
|
|
155
158
|
},
|
|
156
159
|
});
|
|
157
160
|
|
|
@@ -254,6 +257,7 @@ describe('CliProcessService', () => {
|
|
|
254
257
|
claude: '/bin/sh',
|
|
255
258
|
codex: '/bin/sh',
|
|
256
259
|
gemini: '/bin/sh',
|
|
260
|
+
forge: '/bin/sh',
|
|
257
261
|
},
|
|
258
262
|
});
|
|
259
263
|
|
|
@@ -275,4 +279,56 @@ describe('CliProcessService', () => {
|
|
|
275
279
|
expect(existsSync(failedDir)).toBe(false);
|
|
276
280
|
killSpy.mockRestore();
|
|
277
281
|
});
|
|
282
|
+
|
|
283
|
+
it('parses forge output from detached process logs', async () => {
|
|
284
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
285
|
+
tempDirs.push(root);
|
|
286
|
+
const stateDir = join(root, 'state');
|
|
287
|
+
const workFolder = join(root, 'forge-project');
|
|
288
|
+
mkdirSync(workFolder, { recursive: true });
|
|
289
|
+
const pid = 54321;
|
|
290
|
+
const processDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)), String(pid));
|
|
291
|
+
mkdirSync(processDir, { recursive: true });
|
|
292
|
+
|
|
293
|
+
writeFileSync(
|
|
294
|
+
join(processDir, 'stdout.log'),
|
|
295
|
+
`● [21:09:01] Initialize forge-conv-1
|
|
296
|
+
Forge assistant reply
|
|
297
|
+
● [21:09:08] Finished forge-conv-1
|
|
298
|
+
`
|
|
299
|
+
);
|
|
300
|
+
writeFileSync(join(processDir, 'stderr.log'), '');
|
|
301
|
+
writeFileSync(
|
|
302
|
+
join(processDir, 'meta.json'),
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
pid,
|
|
305
|
+
prompt: 'hello forge',
|
|
306
|
+
workFolder,
|
|
307
|
+
model: 'forge',
|
|
308
|
+
toolType: 'forge',
|
|
309
|
+
startTime: new Date().toISOString(),
|
|
310
|
+
stdoutPath: join(processDir, 'stdout.log'),
|
|
311
|
+
stderrPath: join(processDir, 'stderr.log'),
|
|
312
|
+
status: 'completed',
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const service = new CliProcessService({
|
|
317
|
+
stateDir,
|
|
318
|
+
cliPaths: {
|
|
319
|
+
claude: '/bin/sh',
|
|
320
|
+
codex: '/bin/sh',
|
|
321
|
+
gemini: '/bin/sh',
|
|
322
|
+
forge: '/bin/sh',
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const result = await service.getProcessResult(pid, false);
|
|
327
|
+
expect(result.agent).toBe('forge');
|
|
328
|
+
expect(result.session_id).toBe('forge-conv-1');
|
|
329
|
+
expect(result.agentOutput).toEqual({
|
|
330
|
+
message: 'Forge assistant reply',
|
|
331
|
+
session_id: 'forge-conv-1',
|
|
332
|
+
});
|
|
333
|
+
});
|
|
278
334
|
});
|