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
|
@@ -43,22 +43,47 @@ jobs:
|
|
|
43
43
|
- name: Verify provenance attestations
|
|
44
44
|
run: npm audit signatures
|
|
45
45
|
|
|
46
|
+
- name: Capture version before release
|
|
47
|
+
id: release_before
|
|
48
|
+
run: |
|
|
49
|
+
VERSION=$(node -p 'require("./package.json").version')
|
|
50
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
51
|
+
|
|
46
52
|
- name: Release
|
|
47
53
|
env:
|
|
48
54
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
49
55
|
run: npx semantic-release
|
|
50
56
|
|
|
57
|
+
- name: Capture version after release
|
|
58
|
+
id: release_after
|
|
59
|
+
run: |
|
|
60
|
+
VERSION=$(node -p 'require("./package.json").version')
|
|
61
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
62
|
+
|
|
63
|
+
- name: Detect whether a release was published
|
|
64
|
+
id: release_status
|
|
65
|
+
run: |
|
|
66
|
+
if [ "${{ steps.release_before.outputs.version }}" != "${{ steps.release_after.outputs.version }}" ]; then
|
|
67
|
+
echo "published=true" >> "$GITHUB_OUTPUT"
|
|
68
|
+
else
|
|
69
|
+
echo "published=false" >> "$GITHUB_OUTPUT"
|
|
70
|
+
fi
|
|
71
|
+
|
|
51
72
|
- name: Install mcp-publisher
|
|
73
|
+
if: steps.release_status.outputs.published == 'true'
|
|
52
74
|
run: |
|
|
53
75
|
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
54
76
|
|
|
55
77
|
- name: Authenticate to MCP Registry
|
|
78
|
+
if: steps.release_status.outputs.published == 'true'
|
|
56
79
|
run: ./mcp-publisher login github-oidc
|
|
57
80
|
|
|
58
81
|
- name: Set version in server.json
|
|
82
|
+
if: steps.release_status.outputs.published == 'true'
|
|
59
83
|
run: |
|
|
60
84
|
VERSION=$(node -p "require('./package.json').version")
|
|
61
85
|
jq --arg v "$VERSION" '.version = $v | .packages[0].version = $v' server.json > server.tmp && mv server.tmp server.json
|
|
62
86
|
|
|
63
87
|
- name: Publish to MCP Registry
|
|
88
|
+
if: steps.release_status.outputs.published == 'true'
|
|
64
89
|
run: ./mcp-publisher publish
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# [2.13.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.12.0...v2.13.0) (2026-04-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **ci:** correct release version capture shell quoting ([2986b0b](https://github.com/mkXultra/ai-cli-mcp/commit/2986b0bb0452fcf4b34491c3d832cff3b271616c))
|
|
7
|
+
* **ci:** skip MCP Registry publish when no release is created ([9de4c83](https://github.com/mkXultra/ai-cli-mcp/commit/9de4c836a946b4a0028a46525b4bcfa2094032cc))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* Forge CLIを新しいAIバックエンドとして追加 ([35ae860](https://github.com/mkXultra/ai-cli-mcp/commit/35ae8604ff0a663e960fc758eee2872680aec503))
|
|
13
|
+
|
|
1
14
|
# [2.12.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.11.0...v2.12.0) (2026-03-12)
|
|
2
15
|
|
|
3
16
|
|
package/README.ja.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
> **📦 パッケージ移行のお知らせ**: 本パッケージは旧名 `@mkxultra/claude-code-mcp` から `ai-cli-mcp` に名称変更されました。これは、複数のAI CLIツールのサポート拡大を反映したものです。
|
|
7
7
|
|
|
8
|
-
AI CLIツール(Claude, Codex, Gemini)をバックグラウンドプロセスとして実行し、権限処理を自動化するMCP(Model Context Protocol)サーバーです。
|
|
8
|
+
AI CLIツール(Claude, Codex, Gemini, Forge)をバックグラウンドプロセスとして実行し、権限処理を自動化するMCP(Model Context Protocol)サーバーです。
|
|
9
9
|
|
|
10
10
|
Cursorなどのエディタが、複雑な手順を伴う編集や操作に苦戦していることに気づいたことはありませんか?このサーバーは、強力な統合 `run` ツールを提供し、複数のAIエージェントを活用してコーディングタスクをより効果的に処理できるようにします。
|
|
11
11
|
|
|
@@ -20,10 +20,12 @@ Cursorなどのエディタが、複雑な手順を伴う編集や操作に苦
|
|
|
20
20
|
- すべての権限確認をスキップしてClaude CLIを実行(`--dangerously-skip-permissions` を使用)
|
|
21
21
|
- 自動承認モードでCodex CLIを実行(`--full-auto` を使用)
|
|
22
22
|
- 自動承認モードでGemini CLIを実行(`-y` を使用)
|
|
23
|
+
- Forge CLI を非対話モードで実行(`forge -C <workFolder> -p <prompt>` を使用)
|
|
23
24
|
- 複数のAIモデルのサポート:
|
|
24
25
|
- Claude (sonnet, sonnet[1m], opus, opusplan, haiku)
|
|
25
26
|
- Codex (gpt-5.4, gpt-5.3-codex, gpt-5.2-codex, gpt-5.1-codex-mini, gpt-5.1-codex-max, など)
|
|
26
27
|
- Gemini (gemini-2.5-pro, gemini-2.5-flash, gemini-3.1-pro-preview, gemini-3-pro-preview, gemini-3-flash-preview)
|
|
28
|
+
- Forge (`forge`)
|
|
27
29
|
- PID追跡によるバックグラウンドプロセスの管理
|
|
28
30
|
- ツールからの構造化された出力の解析と返却
|
|
29
31
|
|
|
@@ -55,7 +57,7 @@ Cursorなどのエディタが、複雑な手順を伴う編集や操作に苦
|
|
|
55
57
|
|
|
56
58
|
- **真の非同期マルチタスク**: エージェントの実行はバックグラウンドで行われ、即座に制御が戻ります。呼び出し元のAIは実行完了を待つことなく、並行して次のタスクの実行や別のエージェントの呼び出しを行うことができます。
|
|
57
59
|
- **CLI in CLI (Agent in Agent) の実現**: MCPをサポートするあらゆるIDEやCLIから、Claude CodeやCodexといった強力なCLIツールを直接呼び出せます。ホスト環境の制限を超えた、より広範で複雑なシステム操作や自動化が可能になります。
|
|
58
|
-
- **モデル・プロバイダの制約からの解放**: 特定のエコシステムに縛られることなく、Claude、Codex (GPT)、Geminiの中から、タスクに最適な「最強のモデル」や「コスト効率の良いモデル」を自由に選択・組み合わせて利用できます。
|
|
60
|
+
- **モデル・プロバイダの制約からの解放**: 特定のエコシステムに縛られることなく、Claude、Codex (GPT)、Gemini、Forgeの中から、タスクに最適な「最強のモデル」や「コスト効率の良いモデル」を自由に選択・組み合わせて利用できます。
|
|
59
61
|
|
|
60
62
|
## 前提条件
|
|
61
63
|
|
|
@@ -64,6 +66,7 @@ Cursorなどのエディタが、複雑な手順を伴う編集や操作に苦
|
|
|
64
66
|
- **Claude Code**: `claude doctor` が通り、`--dangerously-skip-permissions` での実行が承認済み(一度手動で実行してログイン・承認済み)であること。
|
|
65
67
|
- **Codex CLI**(オプション): インストール済みで、ログインなどの初期設定が完了していること。
|
|
66
68
|
- **Gemini CLI**(オプション): インストール済みで、ログインなどの初期設定が完了していること。
|
|
69
|
+
- **Forge CLI**(オプション): インストール済みで、初期設定が完了していること。
|
|
67
70
|
|
|
68
71
|
## インストールと使い方
|
|
69
72
|
|
|
@@ -222,7 +225,7 @@ detached 実行された `ai-cli` の自然終了 exit code は、まだ永続
|
|
|
222
225
|
|
|
223
226
|
### `run`
|
|
224
227
|
|
|
225
|
-
Claude CLI、Codex CLI
|
|
228
|
+
Claude CLI、Codex CLI、Gemini CLI、または Forge CLI を使用してプロンプトを実行します。モデル名に基づいて適切なCLIが自動的に選択されます。
|
|
226
229
|
|
|
227
230
|
**引数:**
|
|
228
231
|
- `prompt` (string, 任意): AIエージェントに送信するプロンプト。`prompt` または `prompt_file` のいずれかが必須です。
|
|
@@ -233,8 +236,9 @@ Claude CLI、Codex CLI、またはGemini CLIを使用してプロンプトを実
|
|
|
233
236
|
- Claude: `sonnet`, `sonnet[1m]`, `opus`, `opusplan`, `haiku`
|
|
234
237
|
- Codex: `gpt-5.4`, `gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.1-codex-mini`, `gpt-5.1-codex-max`, `gpt-5.2`, `gpt-5.1`, `gpt-5`
|
|
235
238
|
- Gemini: `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-3.1-pro-preview`, `gemini-3-pro-preview`, `gemini-3-flash-preview`
|
|
236
|
-
-
|
|
237
|
-
- `
|
|
239
|
+
- Forge: `forge`
|
|
240
|
+
- `reasoning_effort` (string, 任意): Claude と Codex の推論制御。Claude では `--effort` を使います(許容値: "low", "medium", "high")。Codex では `model_reasoning_effort` を使います(許容値: "low", "medium", "high", "xhigh")。Forge では `reasoning_effort` はサポートしません。
|
|
241
|
+
- `session_id` (string, 任意): 以前のセッションを再開するためのセッションID。対応モデル: 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。
|
|
238
242
|
|
|
239
243
|
### `wait`
|
|
240
244
|
|
|
@@ -296,6 +300,7 @@ npm run test:e2e
|
|
|
296
300
|
- `CLAUDE_CLI_NAME`: Claude CLIのバイナリ名または絶対パスを上書き(デフォルト: `claude`)
|
|
297
301
|
- `CODEX_CLI_NAME`: Codex CLIのバイナリ名または絶対パスを上書き(デフォルト: `codex`)
|
|
298
302
|
- `GEMINI_CLI_NAME`: Gemini CLIのバイナリ名または絶対パスを上書き(デフォルト: `gemini`)
|
|
303
|
+
- `FORGE_CLI_NAME`: Forge CLIのバイナリ名または絶対パスを上書き(デフォルト: `forge`)
|
|
299
304
|
- `MCP_CLAUDE_DEBUG`: デバッグログを有効化(`true` に設定すると詳細な出力が表示されます)
|
|
300
305
|
|
|
301
306
|
**CLI名の指定方法:**
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
> **📦 Package Migration Notice**: This package was formerly `@mkxultra/claude-code-mcp` and has been renamed to `ai-cli-mcp` to reflect its expanded support for multiple AI CLI tools.
|
|
9
9
|
|
|
10
|
-
An MCP (Model Context Protocol) server that allows running AI CLI tools (Claude, Codex, and
|
|
10
|
+
An MCP (Model Context Protocol) server that allows running AI CLI tools (Claude, Codex, Gemini, and Forge) in background processes with automatic permission handling.
|
|
11
11
|
|
|
12
12
|
Did you notice that Cursor sometimes struggles with complex, multi-step edits or operations? This server, with its powerful unified `run` tool, enables multiple AI agents to handle your coding tasks more effectively.
|
|
13
13
|
|
|
@@ -22,7 +22,8 @@ This MCP server provides tools that can be used by LLMs to interact with AI CLI
|
|
|
22
22
|
- Run Claude CLI with all permissions bypassed (using `--dangerously-skip-permissions`)
|
|
23
23
|
- Execute Codex CLI with automatic approval mode (using `--full-auto`)
|
|
24
24
|
- Execute Gemini CLI with automatic approval mode (using `-y`)
|
|
25
|
-
-
|
|
25
|
+
- Execute Forge CLI in non-interactive mode (using `forge -C <workFolder> -p <prompt>`)
|
|
26
|
+
- Support multiple AI models: Claude (sonnet, sonnet[1m], opus, opusplan, haiku), Codex (gpt-5.4, gpt-5.3-codex, gpt-5.2-codex, gpt-5.1-codex-mini, gpt-5.1-codex-max, gpt-5.2, gpt-5.1, gpt-5.1-codex, gpt-5-codex, gpt-5-codex-mini, gpt-5), Gemini (gemini-2.5-pro, gemini-2.5-flash, gemini-3.1-pro-preview, gemini-3-pro-preview, gemini-3-flash-preview), and Forge (`forge`)
|
|
26
27
|
- Manage background processes with PID tracking
|
|
27
28
|
- Parse and return structured outputs from both tools
|
|
28
29
|
|
|
@@ -54,7 +55,7 @@ You can reuse heavy context (like large codebases) using session IDs to save cos
|
|
|
54
55
|
|
|
55
56
|
- **True Async Multitasking**: Agent execution happens in the background, returning control immediately. The calling AI can proceed with the next task or invoke another agent without waiting for completion.
|
|
56
57
|
- **CLI in CLI (Agent in Agent)**: Directly invoke powerful CLI tools like Claude Code or Codex from any MCP-supported IDE or CLI. This enables broader, more complex system operations and automation beyond host environment limitations.
|
|
57
|
-
- **Freedom from Model/Provider Constraints**: Freely select and combine the "strongest" or "most cost-effective" models from Claude, Codex (GPT), and
|
|
58
|
+
- **Freedom from Model/Provider Constraints**: Freely select and combine the "strongest" or "most cost-effective" models from Claude, Codex (GPT), Gemini, and Forge without being tied to a specific ecosystem.
|
|
58
59
|
|
|
59
60
|
## Prerequisites
|
|
60
61
|
|
|
@@ -63,6 +64,7 @@ The only prerequisite is that the AI CLI tools you want to use are locally insta
|
|
|
63
64
|
- **Claude Code**: `claude doctor` passes, and execution with `--dangerously-skip-permissions` is approved (you must run it manually once to login and accept terms).
|
|
64
65
|
- **Codex CLI** (Optional): Installed and initial setup (login etc.) completed.
|
|
65
66
|
- **Gemini CLI** (Optional): Installed and initial setup (login etc.) completed.
|
|
67
|
+
- **Forge CLI** (Optional): Installed and initial setup completed.
|
|
66
68
|
|
|
67
69
|
## Installation & Usage
|
|
68
70
|
|
|
@@ -221,7 +223,7 @@ This server exposes the following tools:
|
|
|
221
223
|
|
|
222
224
|
### `run`
|
|
223
225
|
|
|
224
|
-
Executes a prompt using Claude CLI, Codex CLI, or
|
|
226
|
+
Executes a prompt using Claude CLI, Codex CLI, Gemini CLI, or Forge CLI. The appropriate CLI is automatically selected based on the model name.
|
|
225
227
|
|
|
226
228
|
**Arguments:**
|
|
227
229
|
- `prompt` (string, optional): The prompt to send to the AI agent. Either `prompt` or `prompt_file` is required.
|
|
@@ -232,8 +234,9 @@ Executes a prompt using Claude CLI, Codex CLI, or Gemini CLI. The appropriate CL
|
|
|
232
234
|
- Claude: `sonnet`, `sonnet[1m]`, `opus`, `opusplan`, `haiku`
|
|
233
235
|
- Codex: `gpt-5.4`, `gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.1-codex-mini`, `gpt-5.1-codex-max`, `gpt-5.2`, `gpt-5.1`, `gpt-5`
|
|
234
236
|
- Gemini: `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-3.1-pro-preview`, `gemini-3-pro-preview`, `gemini-3-flash-preview`
|
|
235
|
-
-
|
|
236
|
-
- `
|
|
237
|
+
- Forge: `forge`
|
|
238
|
+
- `reasoning_effort` (string, optional): Reasoning control for Claude and Codex. Claude uses `--effort` (allowed: "low", "medium", "high"). Codex uses `model_reasoning_effort` (allowed: "low", "medium", "high", "xhigh"). Forge does not support `reasoning_effort`.
|
|
239
|
+
- `session_id` (string, optional): 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.
|
|
237
240
|
|
|
238
241
|
### `wait`
|
|
239
242
|
|
|
@@ -281,6 +284,7 @@ Normally not required, but useful for customizing CLI paths or debugging.
|
|
|
281
284
|
- `CLAUDE_CLI_NAME`: Override the Claude CLI binary name or provide an absolute path (default: `claude`)
|
|
282
285
|
- `CODEX_CLI_NAME`: Override the Codex CLI binary name or provide an absolute path (default: `codex`)
|
|
283
286
|
- `GEMINI_CLI_NAME`: Override the Gemini CLI binary name or provide an absolute path (default: `gemini`)
|
|
287
|
+
- `FORGE_CLI_NAME`: Override the Forge CLI binary name or provide an absolute path (default: `forge`)
|
|
284
288
|
- `MCP_CLAUDE_DEBUG`: Enable debug logging (set to `true` for verbose output)
|
|
285
289
|
|
|
286
290
|
**CLI Name Specification:**
|
|
@@ -172,6 +172,7 @@ describe('ai-cli app', () => {
|
|
|
172
172
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"aliases"'));
|
|
173
173
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"claude-ultra"'));
|
|
174
174
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"gpt-5.4"'));
|
|
175
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"forge"'));
|
|
175
176
|
expect(stderr).not.toHaveBeenCalled();
|
|
176
177
|
});
|
|
177
178
|
it('prints doctor status as structured json', async () => {
|
|
@@ -196,6 +197,12 @@ describe('ai-cli app', () => {
|
|
|
196
197
|
available: true,
|
|
197
198
|
lookup: 'path',
|
|
198
199
|
},
|
|
200
|
+
forge: {
|
|
201
|
+
configuredCommand: 'forge',
|
|
202
|
+
resolvedPath: '/tmp/bin/forge',
|
|
203
|
+
available: true,
|
|
204
|
+
lookup: 'path',
|
|
205
|
+
},
|
|
199
206
|
});
|
|
200
207
|
const exitCode = await runCli(['doctor'], { stdout, stderr, getDoctorStatus });
|
|
201
208
|
expect(exitCode).toBe(0);
|
|
@@ -221,6 +228,7 @@ describe('ai-cli app', () => {
|
|
|
221
228
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('claude-ultra'));
|
|
222
229
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('gpt-5.2-codex'));
|
|
223
230
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('gemini-2.5-pro'));
|
|
231
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('forge'));
|
|
224
232
|
expect(stderr).not.toHaveBeenCalled();
|
|
225
233
|
});
|
|
226
234
|
it('prints detailed help for wait --help', async () => {
|
|
@@ -25,6 +25,7 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
25
25
|
writeExecutable(fakeBinDir, 'claude');
|
|
26
26
|
writeExecutable(fakeBinDir, 'codex');
|
|
27
27
|
writeExecutable(fakeBinDir, 'gemini');
|
|
28
|
+
writeExecutable(fakeBinDir, 'forge');
|
|
28
29
|
const output = execFileSync('node', ['--import', 'tsx', 'src/bin/ai-cli.ts', 'doctor'], {
|
|
29
30
|
cwd: process.cwd(),
|
|
30
31
|
encoding: 'utf8',
|
|
@@ -34,11 +35,13 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
34
35
|
CLAUDE_CLI_NAME: 'claude',
|
|
35
36
|
CODEX_CLI_NAME: 'codex',
|
|
36
37
|
GEMINI_CLI_NAME: 'gemini',
|
|
38
|
+
FORGE_CLI_NAME: 'forge',
|
|
37
39
|
},
|
|
38
40
|
});
|
|
39
41
|
expect(output).toContain('"claude"');
|
|
40
42
|
expect(output).toContain('"codex"');
|
|
41
43
|
expect(output).toContain('"gemini"');
|
|
44
|
+
expect(output).toContain('"forge"');
|
|
42
45
|
expect(output).toContain('"available": true');
|
|
43
46
|
});
|
|
44
47
|
it('prints run help for the ai-cli entrypoint', () => {
|
|
@@ -50,5 +53,6 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
50
53
|
expect(output).toContain('Usage: ai-cli run --cwd <path> [options]');
|
|
51
54
|
expect(output).toContain('--model <model>');
|
|
52
55
|
expect(output).toContain('claude-ultra');
|
|
56
|
+
expect(output).toContain('forge');
|
|
53
57
|
});
|
|
54
58
|
});
|
|
@@ -14,6 +14,7 @@ const DEFAULT_CLI_PATHS = {
|
|
|
14
14
|
claude: '/usr/bin/claude',
|
|
15
15
|
codex: '/usr/bin/codex',
|
|
16
16
|
gemini: '/usr/bin/gemini',
|
|
17
|
+
forge: '/usr/bin/forge',
|
|
17
18
|
};
|
|
18
19
|
describe('cli-builder', () => {
|
|
19
20
|
beforeEach(() => {
|
|
@@ -70,6 +71,9 @@ describe('cli-builder', () => {
|
|
|
70
71
|
it('should throw for unsupported model families', () => {
|
|
71
72
|
expect(() => getReasoningEffort('gemini-2.5-pro', 'high')).toThrow('reasoning_effort is only supported for Claude and Codex models.');
|
|
72
73
|
});
|
|
74
|
+
it('should reject reasoning_effort for forge explicitly', () => {
|
|
75
|
+
expect(() => getReasoningEffort('forge', 'high')).toThrow('reasoning_effort is not supported for forge.');
|
|
76
|
+
});
|
|
73
77
|
});
|
|
74
78
|
describe('buildCliCommand', () => {
|
|
75
79
|
describe('validation', () => {
|
|
@@ -322,5 +326,38 @@ describe('cli-builder', () => {
|
|
|
322
326
|
expect(cmd.resolvedModel).toBe('gemini-3.1-pro-preview');
|
|
323
327
|
});
|
|
324
328
|
});
|
|
329
|
+
describe('forge agent', () => {
|
|
330
|
+
it('should build forge command without model flags', () => {
|
|
331
|
+
const cmd = buildCliCommand({
|
|
332
|
+
prompt: 'test',
|
|
333
|
+
workFolder: '/tmp',
|
|
334
|
+
model: 'forge',
|
|
335
|
+
cliPaths: DEFAULT_CLI_PATHS,
|
|
336
|
+
});
|
|
337
|
+
expect(cmd.agent).toBe('forge');
|
|
338
|
+
expect(cmd.cliPath).toBe('/usr/bin/forge');
|
|
339
|
+
expect(cmd.resolvedModel).toBe('forge');
|
|
340
|
+
expect(cmd.args).toEqual(['-C', '/tmp', '-p', 'test']);
|
|
341
|
+
});
|
|
342
|
+
it('should map session_id to --conversation-id for forge', () => {
|
|
343
|
+
const cmd = buildCliCommand({
|
|
344
|
+
prompt: 'test',
|
|
345
|
+
workFolder: '/tmp',
|
|
346
|
+
model: 'forge',
|
|
347
|
+
session_id: 'forge-conv-123',
|
|
348
|
+
cliPaths: DEFAULT_CLI_PATHS,
|
|
349
|
+
});
|
|
350
|
+
expect(cmd.args).toEqual(['-C', '/tmp', '--conversation-id', 'forge-conv-123', '-p', 'test']);
|
|
351
|
+
});
|
|
352
|
+
it('should reject reasoning_effort for forge in command building', () => {
|
|
353
|
+
expect(() => buildCliCommand({
|
|
354
|
+
prompt: 'test',
|
|
355
|
+
workFolder: '/tmp',
|
|
356
|
+
model: 'forge',
|
|
357
|
+
reasoning_effort: 'high',
|
|
358
|
+
cliPaths: DEFAULT_CLI_PATHS,
|
|
359
|
+
})).toThrow('reasoning_effort is not supported for forge.');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
325
362
|
});
|
|
326
363
|
});
|
|
@@ -56,6 +56,7 @@ describe('CliProcessService', () => {
|
|
|
56
56
|
claude: scriptPath,
|
|
57
57
|
codex: scriptPath,
|
|
58
58
|
gemini: scriptPath,
|
|
59
|
+
forge: scriptPath,
|
|
59
60
|
},
|
|
60
61
|
});
|
|
61
62
|
const runResult = await service.startProcess({
|
|
@@ -98,6 +99,7 @@ describe('CliProcessService', () => {
|
|
|
98
99
|
claude: scriptPath,
|
|
99
100
|
codex: scriptPath,
|
|
100
101
|
gemini: scriptPath,
|
|
102
|
+
forge: scriptPath,
|
|
101
103
|
},
|
|
102
104
|
});
|
|
103
105
|
const runResult = await service.startProcess({
|
|
@@ -130,6 +132,7 @@ describe('CliProcessService', () => {
|
|
|
130
132
|
claude: '/bin/sh',
|
|
131
133
|
codex: '/bin/sh',
|
|
132
134
|
gemini: '/bin/sh',
|
|
135
|
+
forge: '/bin/sh',
|
|
133
136
|
},
|
|
134
137
|
});
|
|
135
138
|
writeFileSync(join(processDir, 'meta.json'), JSON.stringify({
|
|
@@ -212,6 +215,7 @@ describe('CliProcessService', () => {
|
|
|
212
215
|
claude: '/bin/sh',
|
|
213
216
|
codex: '/bin/sh',
|
|
214
217
|
gemini: '/bin/sh',
|
|
218
|
+
forge: '/bin/sh',
|
|
215
219
|
},
|
|
216
220
|
});
|
|
217
221
|
const killSpy = vi.spyOn(globalThis.process, 'kill').mockImplementation((target, signal) => {
|
|
@@ -230,4 +234,46 @@ describe('CliProcessService', () => {
|
|
|
230
234
|
expect(existsSync(failedDir)).toBe(false);
|
|
231
235
|
killSpy.mockRestore();
|
|
232
236
|
});
|
|
237
|
+
it('parses forge output from detached process logs', async () => {
|
|
238
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
239
|
+
tempDirs.push(root);
|
|
240
|
+
const stateDir = join(root, 'state');
|
|
241
|
+
const workFolder = join(root, 'forge-project');
|
|
242
|
+
mkdirSync(workFolder, { recursive: true });
|
|
243
|
+
const pid = 54321;
|
|
244
|
+
const processDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)), String(pid));
|
|
245
|
+
mkdirSync(processDir, { recursive: true });
|
|
246
|
+
writeFileSync(join(processDir, 'stdout.log'), `● [21:09:01] Initialize forge-conv-1
|
|
247
|
+
Forge assistant reply
|
|
248
|
+
● [21:09:08] Finished forge-conv-1
|
|
249
|
+
`);
|
|
250
|
+
writeFileSync(join(processDir, 'stderr.log'), '');
|
|
251
|
+
writeFileSync(join(processDir, 'meta.json'), JSON.stringify({
|
|
252
|
+
pid,
|
|
253
|
+
prompt: 'hello forge',
|
|
254
|
+
workFolder,
|
|
255
|
+
model: 'forge',
|
|
256
|
+
toolType: 'forge',
|
|
257
|
+
startTime: new Date().toISOString(),
|
|
258
|
+
stdoutPath: join(processDir, 'stdout.log'),
|
|
259
|
+
stderrPath: join(processDir, 'stderr.log'),
|
|
260
|
+
status: 'completed',
|
|
261
|
+
}));
|
|
262
|
+
const service = new CliProcessService({
|
|
263
|
+
stateDir,
|
|
264
|
+
cliPaths: {
|
|
265
|
+
claude: '/bin/sh',
|
|
266
|
+
codex: '/bin/sh',
|
|
267
|
+
gemini: '/bin/sh',
|
|
268
|
+
forge: '/bin/sh',
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
const result = await service.getProcessResult(pid, false);
|
|
272
|
+
expect(result.agent).toBe('forge');
|
|
273
|
+
expect(result.session_id).toBe('forge-conv-1');
|
|
274
|
+
expect(result.agentOutput).toEqual({
|
|
275
|
+
message: 'Forge assistant reply',
|
|
276
|
+
session_id: 'forge-conv-1',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
233
279
|
});
|
|
@@ -15,6 +15,7 @@ describe('cli-utils doctor status', () => {
|
|
|
15
15
|
delete process.env.CLAUDE_CLI_NAME;
|
|
16
16
|
delete process.env.CODEX_CLI_NAME;
|
|
17
17
|
delete process.env.GEMINI_CLI_NAME;
|
|
18
|
+
delete process.env.FORGE_CLI_NAME;
|
|
18
19
|
process.env.PATH = '/mock/bin:/usr/bin';
|
|
19
20
|
});
|
|
20
21
|
afterEach(() => {
|
|
@@ -36,6 +37,12 @@ describe('cli-utils doctor status', () => {
|
|
|
36
37
|
available: true,
|
|
37
38
|
lookup: 'path',
|
|
38
39
|
});
|
|
40
|
+
expect(status.forge).toEqual({
|
|
41
|
+
configuredCommand: 'forge',
|
|
42
|
+
resolvedPath: null,
|
|
43
|
+
available: false,
|
|
44
|
+
lookup: 'path',
|
|
45
|
+
});
|
|
39
46
|
});
|
|
40
47
|
it('does not mark non-executable PATH entries as available', async () => {
|
|
41
48
|
mockAccessSync.mockImplementation(() => {
|
|
@@ -49,6 +56,12 @@ describe('cli-utils doctor status', () => {
|
|
|
49
56
|
available: false,
|
|
50
57
|
lookup: 'path',
|
|
51
58
|
});
|
|
59
|
+
expect(status.forge).toEqual({
|
|
60
|
+
configuredCommand: 'forge',
|
|
61
|
+
resolvedPath: null,
|
|
62
|
+
available: false,
|
|
63
|
+
lookup: 'path',
|
|
64
|
+
});
|
|
52
65
|
});
|
|
53
66
|
it('reports invalid relative env paths as doctor errors', async () => {
|
|
54
67
|
process.env.CLAUDE_CLI_NAME = './relative/claude';
|
|
@@ -106,4 +119,22 @@ describe('cli-utils doctor status', () => {
|
|
|
106
119
|
lookup: 'env',
|
|
107
120
|
});
|
|
108
121
|
});
|
|
122
|
+
it('supports forge lookup via FORGE_CLI_NAME', async () => {
|
|
123
|
+
process.env.FORGE_CLI_NAME = 'forge-custom';
|
|
124
|
+
mockAccessSync.mockImplementation((filePath) => {
|
|
125
|
+
if (filePath === '/mock/bin/forge-custom') {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
throw new Error('not executable');
|
|
129
|
+
});
|
|
130
|
+
const { getCliDoctorStatus, findForgeCli } = await import('../cli-utils.js');
|
|
131
|
+
const status = getCliDoctorStatus();
|
|
132
|
+
expect(status.forge).toEqual({
|
|
133
|
+
configuredCommand: 'forge-custom',
|
|
134
|
+
resolvedPath: '/mock/bin/forge-custom',
|
|
135
|
+
available: true,
|
|
136
|
+
lookup: 'env',
|
|
137
|
+
});
|
|
138
|
+
expect(findForgeCli()).toBe('forge-custom');
|
|
139
|
+
});
|
|
109
140
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { cleanupSharedMock, getSharedMock } from './utils/persistent-mock.js';
|
|
@@ -16,6 +16,49 @@ function expectProcessSummaryShape(processInfo) {
|
|
|
16
16
|
status: expect.any(String),
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
+
function createForgeMockScript(dir, argsLogPath) {
|
|
20
|
+
const scriptPath = join(dir, 'mock-forge');
|
|
21
|
+
writeFileSync(scriptPath, `#!/bin/bash
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
log_file="${argsLogPath}"
|
|
25
|
+
prompt=""
|
|
26
|
+
conversation_id=""
|
|
27
|
+
|
|
28
|
+
printf '%s\\n' "$*" >> "$log_file"
|
|
29
|
+
|
|
30
|
+
while [[ $# -gt 0 ]]; do
|
|
31
|
+
case "$1" in
|
|
32
|
+
-C)
|
|
33
|
+
shift 2
|
|
34
|
+
;;
|
|
35
|
+
-p)
|
|
36
|
+
prompt="$2"
|
|
37
|
+
shift 2
|
|
38
|
+
;;
|
|
39
|
+
--conversation-id)
|
|
40
|
+
conversation_id="$2"
|
|
41
|
+
shift 2
|
|
42
|
+
;;
|
|
43
|
+
*)
|
|
44
|
+
shift
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
if [[ -n "$conversation_id" ]]; then
|
|
50
|
+
printf '● [21:09:33] Continue %s\\n' "$conversation_id"
|
|
51
|
+
printf 'Resumed: %s\\n' "$prompt"
|
|
52
|
+
printf '● [21:09:37] Finished %s\\n' "$conversation_id"
|
|
53
|
+
else
|
|
54
|
+
printf '● [21:09:01] Initialize forge-session-1\\n'
|
|
55
|
+
printf 'Initial: %s\\n' "$prompt"
|
|
56
|
+
printf '● [21:09:08] Finished forge-session-1\\n'
|
|
57
|
+
fi
|
|
58
|
+
`);
|
|
59
|
+
chmodSync(scriptPath, 0o755);
|
|
60
|
+
return scriptPath;
|
|
61
|
+
}
|
|
19
62
|
describe('MCP Contract Tests', () => {
|
|
20
63
|
let client;
|
|
21
64
|
let testDir;
|
|
@@ -129,6 +172,111 @@ describe('MCP Contract Tests', () => {
|
|
|
129
172
|
message: expect.any(String),
|
|
130
173
|
});
|
|
131
174
|
});
|
|
175
|
+
it('covers forge end-to-end through the MCP process path', async () => {
|
|
176
|
+
await client.disconnect();
|
|
177
|
+
const forgeArgsLogPath = join(testDir, 'forge-args.log');
|
|
178
|
+
const forgeMockPath = createForgeMockScript(testDir, forgeArgsLogPath);
|
|
179
|
+
client = createTestClient({
|
|
180
|
+
debug: false,
|
|
181
|
+
env: {
|
|
182
|
+
FORGE_CLI_NAME: forgeMockPath,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
await client.connect();
|
|
186
|
+
const initialRunResponse = await client.callTool('run', {
|
|
187
|
+
prompt: 'forge-initial-prompt',
|
|
188
|
+
workFolder: testDir,
|
|
189
|
+
model: 'forge',
|
|
190
|
+
});
|
|
191
|
+
const initialRunData = parseToolJson(initialRunResponse);
|
|
192
|
+
expect(initialRunData).toEqual({
|
|
193
|
+
pid: expect.any(Number),
|
|
194
|
+
status: 'started',
|
|
195
|
+
agent: 'forge',
|
|
196
|
+
message: expect.any(String),
|
|
197
|
+
});
|
|
198
|
+
const initialWaitResponse = await client.callTool('wait', { pids: [initialRunData.pid], timeout: 5 });
|
|
199
|
+
const initialWaitData = parseToolJson(initialWaitResponse);
|
|
200
|
+
expect(initialWaitData).toHaveLength(1);
|
|
201
|
+
expect(initialWaitData[0]).toMatchObject({
|
|
202
|
+
pid: initialRunData.pid,
|
|
203
|
+
agent: 'forge',
|
|
204
|
+
status: 'completed',
|
|
205
|
+
session_id: 'forge-session-1',
|
|
206
|
+
agentOutput: {
|
|
207
|
+
message: 'Initial: forge-initial-prompt',
|
|
208
|
+
session_id: 'forge-session-1',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
const initialResultResponse = await client.callTool('get_result', { pid: initialRunData.pid });
|
|
212
|
+
const initialResultData = parseToolJson(initialResultResponse);
|
|
213
|
+
expect(initialResultData).toMatchObject({
|
|
214
|
+
pid: initialRunData.pid,
|
|
215
|
+
agent: 'forge',
|
|
216
|
+
status: 'completed',
|
|
217
|
+
session_id: 'forge-session-1',
|
|
218
|
+
agentOutput: {
|
|
219
|
+
message: 'Initial: forge-initial-prompt',
|
|
220
|
+
session_id: 'forge-session-1',
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const resumedRunResponse = await client.callTool('run', {
|
|
224
|
+
prompt: 'forge-resume-prompt',
|
|
225
|
+
workFolder: testDir,
|
|
226
|
+
model: 'forge',
|
|
227
|
+
session_id: 'forge-session-1',
|
|
228
|
+
});
|
|
229
|
+
const resumedRunData = parseToolJson(resumedRunResponse);
|
|
230
|
+
expect(resumedRunData).toEqual({
|
|
231
|
+
pid: expect.any(Number),
|
|
232
|
+
status: 'started',
|
|
233
|
+
agent: 'forge',
|
|
234
|
+
message: expect.any(String),
|
|
235
|
+
});
|
|
236
|
+
const resumedWaitResponse = await client.callTool('wait', { pids: [resumedRunData.pid], timeout: 5 });
|
|
237
|
+
const resumedWaitData = parseToolJson(resumedWaitResponse);
|
|
238
|
+
expect(resumedWaitData).toHaveLength(1);
|
|
239
|
+
expect(resumedWaitData[0]).toMatchObject({
|
|
240
|
+
pid: resumedRunData.pid,
|
|
241
|
+
agent: 'forge',
|
|
242
|
+
status: 'completed',
|
|
243
|
+
session_id: 'forge-session-1',
|
|
244
|
+
agentOutput: {
|
|
245
|
+
message: 'Resumed: forge-resume-prompt',
|
|
246
|
+
session_id: 'forge-session-1',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
const resumedResultResponse = await client.callTool('get_result', { pid: resumedRunData.pid });
|
|
250
|
+
const resumedResultData = parseToolJson(resumedResultResponse);
|
|
251
|
+
expect(resumedResultData).toMatchObject({
|
|
252
|
+
pid: resumedRunData.pid,
|
|
253
|
+
agent: 'forge',
|
|
254
|
+
status: 'completed',
|
|
255
|
+
session_id: 'forge-session-1',
|
|
256
|
+
agentOutput: {
|
|
257
|
+
message: 'Resumed: forge-resume-prompt',
|
|
258
|
+
session_id: 'forge-session-1',
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
const forgeInvocations = readFileSync(forgeArgsLogPath, 'utf-8').trim().split('\n');
|
|
262
|
+
expect(forgeInvocations).toHaveLength(2);
|
|
263
|
+
expect(forgeInvocations[0]).toContain(`-C ${testDir}`);
|
|
264
|
+
expect(forgeInvocations[0]).toContain('-p forge-initial-prompt');
|
|
265
|
+
expect(forgeInvocations[0]).not.toContain('--model');
|
|
266
|
+
expect(forgeInvocations[0]).not.toContain('--agent');
|
|
267
|
+
expect(forgeInvocations[0]).not.toContain('--conversation-id');
|
|
268
|
+
expect(forgeInvocations[1]).toContain(`-C ${testDir}`);
|
|
269
|
+
expect(forgeInvocations[1]).toContain('--conversation-id forge-session-1');
|
|
270
|
+
expect(forgeInvocations[1]).toContain('-p forge-resume-prompt');
|
|
271
|
+
expect(forgeInvocations[1]).not.toContain('--model');
|
|
272
|
+
expect(forgeInvocations[1]).not.toContain('--agent');
|
|
273
|
+
await expect(client.callTool('run', {
|
|
274
|
+
prompt: 'forge-invalid-reasoning',
|
|
275
|
+
workFolder: testDir,
|
|
276
|
+
model: 'forge',
|
|
277
|
+
reasoning_effort: 'high',
|
|
278
|
+
})).rejects.toThrow(/reasoning_effort is not supported for forge/i);
|
|
279
|
+
});
|
|
132
280
|
it('keeps key invalid-input errors stable', async () => {
|
|
133
281
|
await expect(client.callTool('run', {
|
|
134
282
|
prompt: 'missing workFolder',
|