ai-cli-mcp 2.11.0 → 2.12.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 (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.ja.md +102 -3
  3. package/README.md +102 -3
  4. package/dist/__tests__/app-cli.test.js +285 -0
  5. package/dist/__tests__/cli-bin-smoke.test.js +54 -0
  6. package/dist/__tests__/cli-process-service.test.js +233 -0
  7. package/dist/__tests__/cli-utils.test.js +109 -0
  8. package/dist/__tests__/error-cases.test.js +2 -1
  9. package/dist/__tests__/mcp-contract.test.js +195 -0
  10. package/dist/__tests__/process-management.test.js +15 -8
  11. package/dist/__tests__/server.test.js +29 -3
  12. package/dist/__tests__/wait.test.js +31 -0
  13. package/dist/app/cli.js +304 -0
  14. package/dist/app/mcp.js +362 -0
  15. package/dist/bin/ai-cli-mcp.js +6 -0
  16. package/dist/bin/ai-cli.js +10 -0
  17. package/dist/cli-builder.js +1 -6
  18. package/dist/cli-process-service.js +328 -0
  19. package/dist/cli-utils.js +142 -88
  20. package/dist/model-catalog.js +50 -0
  21. package/dist/process-service.js +198 -0
  22. package/dist/server.js +3 -577
  23. package/docs/cli-architecture.md +275 -0
  24. package/package.json +3 -2
  25. package/src/__tests__/app-cli.test.ts +362 -0
  26. package/src/__tests__/cli-bin-smoke.test.ts +71 -0
  27. package/src/__tests__/cli-process-service.test.ts +278 -0
  28. package/src/__tests__/cli-utils.test.ts +132 -0
  29. package/src/__tests__/error-cases.test.ts +3 -4
  30. package/src/__tests__/mcp-contract.test.ts +250 -0
  31. package/src/__tests__/process-management.test.ts +15 -9
  32. package/src/__tests__/server.test.ts +27 -6
  33. package/src/__tests__/wait.test.ts +38 -0
  34. package/src/app/cli.ts +373 -0
  35. package/src/app/mcp.ts +398 -0
  36. package/src/bin/ai-cli-mcp.ts +7 -0
  37. package/src/bin/ai-cli.ts +11 -0
  38. package/src/cli-builder.ts +1 -7
  39. package/src/cli-process-service.ts +415 -0
  40. package/src/cli-utils.ts +185 -99
  41. package/src/model-catalog.ts +60 -0
  42. package/src/process-service.ts +261 -0
  43. package/src/server.ts +3 -667
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # [2.12.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.11.0...v2.12.0) (2026-03-12)
2
+
3
+
4
+ ### Features
5
+
6
+ * ai-cliコマンドを追加しアーキテクチャをリファクタリング ([10a1209](https://github.com/mkXultra/ai-cli-mcp/commit/10a120933d5893d47e47001a165f0abc5e4557aa))
7
+ * CliProcessServiceとCLIサブコマンドを追加 ([0043ee4](https://github.com/mkXultra/ai-cli-mcp/commit/0043ee4a73bbf2d48f57b0b0ded8f12e29e8ebf2))
8
+ * CLIサブコマンドhelpとcleanupを追加し状態保存先を再設計 ([63dc1a2](https://github.com/mkXultra/ai-cli-mcp/commit/63dc1a238e9883cab04e818413576ad265b600ab))
9
+ * doctorとmodelsサブコマンドを追加しモデルカタログをモジュール化 ([15222e3](https://github.com/mkXultra/ai-cli-mcp/commit/15222e3c29c18bc16fa0bc7d6c4f3bdc621f6ff6))
10
+
1
11
  # [2.11.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.10.0...v2.11.0) (2026-03-08)
2
12
 
3
13
 
package/README.ja.md CHANGED
@@ -9,6 +9,10 @@ AI CLIツール(Claude, Codex, Gemini)をバックグラウンドプロセ
9
9
 
10
10
  Cursorなどのエディタが、複雑な手順を伴う編集や操作に苦戦していることに気づいたことはありませんか?このサーバーは、強力な統合 `run` ツールを提供し、複数のAIエージェントを活用してコーディングタスクをより効果的に処理できるようにします。
11
11
 
12
+ ## デモ
13
+
14
+ [![デモ](docs/assets/demo-jp.gif)](https://github.com/mkXultra/ai-cli-mcp/releases/download/v2.11.0/demo-jp.mp4)
15
+
12
16
  ## 概要
13
17
 
14
18
  このMCPサーバーは、LLMがAI CLIツールと対話するためのツールを提供します。MCPクライアントと統合することで、LLMは以下のことが可能になります:
@@ -45,6 +49,8 @@ Cursorなどのエディタが、複雑な手順を伴う編集や操作に苦
45
49
  > - `gpt-5.2-codex` で `README.md` にアーキテクチャの解説を追記
46
50
  > 4. 最後に再び `wait` して、両方の結果をまとめてください。
47
51
 
52
+ [![セッション再開デモ](docs/assets/demo-resume-jp.gif)](https://github.com/mkXultra/ai-cli-mcp/releases/download/v2.11.0/demo-resume-jp.mp4)
53
+
48
54
  ## メリット
49
55
 
50
56
  - **真の非同期マルチタスク**: エージェントの実行はバックグラウンドで行われ、即座に制御が戻ります。呼び出し元のAIは実行完了を待つことなく、並行して次のタスクの実行や別のエージェントの呼び出しを行うことができます。
@@ -61,9 +67,16 @@ Cursorなどのエディタが、複雑な手順を伴う編集や操作に苦
61
67
 
62
68
  ## インストールと使い方
63
69
 
64
- 推奨される使用方法は、`npx` を使用してインストールすることです。
70
+ 現在の主な使い方は 2 つあります。
71
+
72
+ - `ai-cli-mcp`: MCP サーバーの起動
73
+ - `ai-cli`: 人間向け CLI
74
+
75
+ ### MCP 利用 (`npx`)
65
76
 
66
- ### MCP設定ファイルでnpxを使用する場合:
77
+ MCP サーバーとして使う場合は、`npx` 経由が推奨です。
78
+
79
+ #### MCP設定ファイルでnpxを使用する場合:
67
80
 
68
81
  ```json
69
82
  "ai-cli-mcp": {
@@ -75,12 +88,47 @@ Cursorなどのエディタが、複雑な手順を伴う編集や操作に苦
75
88
  },
76
89
  ```
77
90
 
78
- ### Claude CLI mcp add コマンドを使用する場合:
91
+ #### Claude CLI mcp add コマンドを使用する場合:
79
92
 
80
93
  ```bash
81
94
  claude mcp add ai-cli '{"name":"ai-cli","command":"npx","args":["-y","ai-cli-mcp@latest"]}'
82
95
  ```
83
96
 
97
+ ### 人間向け CLI 利用 (グローバルインストール)
98
+
99
+ シェルから `ai-cli` を直接使いたい場合は、グローバルインストールしてください。
100
+
101
+ ```bash
102
+ npm install -g ai-cli-mcp
103
+ ```
104
+
105
+ これで以下の 2 つのコマンドが使えるようになります。
106
+
107
+ - `ai-cli`
108
+ - `ai-cli-mcp`
109
+
110
+ 例:
111
+
112
+ ```bash
113
+ ai-cli doctor
114
+ ai-cli models
115
+ ai-cli run --cwd "$PWD" --model sonnet --prompt "summarize this repository"
116
+ ai-cli ps
117
+ ai-cli result 12345
118
+ ai-cli wait 12345 --timeout 300
119
+ ai-cli kill 12345
120
+ ai-cli cleanup
121
+ ai-cli-mcp
122
+ ```
123
+
124
+ ### 人間向け CLI 利用 (`npx`)
125
+
126
+ 公開パッケージ名はまだ `ai-cli-mcp` のままなので、`npx` で `ai-cli` を使う場合は次の形になります。
127
+
128
+ ```bash
129
+ npx -y --package ai-cli-mcp@latest ai-cli run --cwd "$PWD" --model sonnet --prompt "hello"
130
+ ```
131
+
84
132
  ## 重要な初回セットアップ
85
133
 
86
134
  ### Claude CLIの場合:
@@ -112,6 +160,56 @@ gemini auth login
112
160
 
113
161
  macOSでは、これらのツールを初めて実行する際にフォルダへのアクセス許可を求められる場合があります。最初の実行が失敗しても、2回目以降は動作するはずです。
114
162
 
163
+ ## CLI コマンド
164
+
165
+ `ai-cli` は現在以下をサポートしています。
166
+
167
+ - `run`
168
+ - `ps`
169
+ - `result`
170
+ - `wait`
171
+ - `kill`
172
+ - `cleanup`
173
+ - `doctor`
174
+ - `models`
175
+ - `mcp`
176
+
177
+ 基本的な流れ:
178
+
179
+ ```bash
180
+ ai-cli doctor
181
+ ai-cli models
182
+ ai-cli run --cwd "$PWD" --model codex-ultra --prompt "fix failing tests"
183
+ ai-cli ps
184
+ ai-cli wait 12345
185
+ ai-cli result 12345
186
+ ai-cli cleanup
187
+ ```
188
+
189
+ `run` の作業ディレクトリ指定は `--cwd` が基本です。互換性のために `--workFolder` / `--work-folder` も受け付けます。
190
+
191
+ `doctor` は CLI バイナリの存在確認と path 解決だけを行います。ログイン状態や利用規約同意までは確認しません。
192
+
193
+ ## CLI の状態保存先
194
+
195
+ バックグラウンド実行した `ai-cli` の状態は、次のディレクトリに保存されます。
196
+
197
+ ```text
198
+ ~/.local/state/ai-cli/cwds/<normalized-cwd>/<pid>/
199
+ ```
200
+
201
+ 各 PID ディレクトリには以下が入ります。
202
+
203
+ - `meta.json`
204
+ - `stdout.log`
205
+ - `stderr.log`
206
+
207
+ 完了済み・失敗済みの実行は `ai-cli cleanup` で削除できます。`running` のものは保持されます。
208
+
209
+ ## 既知の制約
210
+
211
+ detached 実行された `ai-cli` の自然終了 exit code は、まだ永続化していません。そのため、CLI は出力と running/completed 状態は返せますが、自然終了したバックグラウンド実行の `exitCode` は現時点では保証しません。
212
+
115
213
  ## MCPクライアントへの接続
116
214
 
117
215
  サーバーのセットアップ後、MCPクライアント(CursorやWindsurfなど)の設定ファイル(`mcp.json` や `mcp_config.json`)に設定を追加してください。
@@ -167,6 +265,7 @@ PIDを指定して、実行中のAIエージェントプロセスを終了しま
167
265
  ## トラブルシューティング
168
266
 
169
267
  - **"Command not found" (claude-code-mcp):** グローバルにインストールした場合、npmのグローバルbinディレクトリがシステムのPATHに含まれているか確認してください。`npx` を使用している場合、`npx` 自体が機能しているか確認してください。
268
+ - **"Command not found" (`ai-cli`):** グローバルインストール時は npm のグローバル bin ディレクトリが `PATH` に入っているか確認してください。`npx` の場合は `npx -y --package ai-cli-mcp@latest ai-cli ...` を使ってください。
170
269
  - **"Command not found" (claude または ~/.claude/local/claude):** Claude CLIが正しくインストールされていることを確認してください。`claude/doctor` を実行するか、公式ドキュメントを確認してください。
171
270
  - **権限の問題:** 「重要な初回セットアップ」の手順を実行したか確認してください。
172
271
  - **サーバーからのJSONエラー:** `MCP_CLAUDE_DEBUG` が `true` の場合、エラーメッセージやログがMCPのJSON解析を妨げる可能性があります。通常動作時は `false` に設定してください。
package/README.md CHANGED
@@ -11,6 +11,10 @@ An MCP (Model Context Protocol) server that allows running AI CLI tools (Claude,
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
 
14
+ ## Demo
15
+
16
+ [![Demo](docs/assets/demo.gif)](https://github.com/mkXultra/ai-cli-mcp/releases/download/v2.11.0/demo.mp4)
17
+
14
18
  ## Overview
15
19
 
16
20
  This MCP server provides tools that can be used by LLMs to interact with AI CLI tools. When integrated with MCP clients, it allows LLMs to:
@@ -44,6 +48,8 @@ You can reuse heavy context (like large codebases) using session IDs to save cos
44
48
  > - Add architecture documentation to `README.md` using `gpt-5.2-codex`
45
49
  > 4. Finally, `wait` again to combine both results.
46
50
 
51
+ [![Session Resume Demo](docs/assets/demo-resume.gif)](https://github.com/mkXultra/ai-cli-mcp/releases/download/v2.11.0/demo-resume.mp4)
52
+
47
53
  ## Benefits
48
54
 
49
55
  - **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.
@@ -60,9 +66,16 @@ The only prerequisite is that the AI CLI tools you want to use are locally insta
60
66
 
61
67
  ## Installation & Usage
62
68
 
63
- The recommended way to use this server is by installing it by using `npx`.
69
+ There are now two primary ways to use this package:
70
+
71
+ - `ai-cli-mcp`: MCP server entrypoint
72
+ - `ai-cli`: human-facing CLI for background AI runs
73
+
74
+ ### MCP usage with `npx`
64
75
 
65
- ### Using npx in your MCP configuration:
76
+ The recommended way to use the MCP server is via `npx`.
77
+
78
+ #### Using npx in your MCP configuration:
66
79
 
67
80
  ```json
68
81
  "ai-cli-mcp": {
@@ -74,12 +87,47 @@ The recommended way to use this server is by installing it by using `npx`.
74
87
  },
75
88
  ```
76
89
 
77
- ### Using Claude CLI mcp add command:
90
+ #### Using Claude CLI mcp add command:
78
91
 
79
92
  ```bash
80
93
  claude mcp add ai-cli '{"name":"ai-cli","command":"npx","args":["-y","ai-cli-mcp@latest"]}'
81
94
  ```
82
95
 
96
+ ### Human CLI usage with global install
97
+
98
+ If you want to use the production CLI directly from your shell, install the package globally:
99
+
100
+ ```bash
101
+ npm install -g ai-cli-mcp
102
+ ```
103
+
104
+ This exposes both commands:
105
+
106
+ - `ai-cli`
107
+ - `ai-cli-mcp`
108
+
109
+ Examples:
110
+
111
+ ```bash
112
+ ai-cli doctor
113
+ ai-cli models
114
+ ai-cli run --cwd "$PWD" --model sonnet --prompt "summarize this repository"
115
+ ai-cli ps
116
+ ai-cli result 12345
117
+ ai-cli wait 12345 --timeout 300
118
+ ai-cli kill 12345
119
+ ai-cli cleanup
120
+ ai-cli-mcp
121
+ ```
122
+
123
+ ### Human CLI usage with `npx`
124
+
125
+ Because the published package name is still `ai-cli-mcp`, the shortest `npx` form for the CLI is:
126
+
127
+ ```bash
128
+ npx -y --package ai-cli-mcp@latest ai-cli run --cwd "$PWD" --model sonnet --prompt "hello"
129
+ ```
130
+
83
131
  ## Important First-Time Setup
84
132
 
85
133
  ### For Claude CLI:
@@ -111,6 +159,56 @@ gemini auth login
111
159
 
112
160
  macOS might ask for folder permissions the first time any of these tools run. If the first run fails, subsequent runs should work.
113
161
 
162
+ ## CLI Commands
163
+
164
+ `ai-cli` currently supports:
165
+
166
+ - `run`
167
+ - `ps`
168
+ - `result`
169
+ - `wait`
170
+ - `kill`
171
+ - `cleanup`
172
+ - `doctor`
173
+ - `models`
174
+ - `mcp`
175
+
176
+ Example flow:
177
+
178
+ ```bash
179
+ ai-cli doctor
180
+ ai-cli models
181
+ ai-cli run --cwd "$PWD" --model codex-ultra --prompt "fix failing tests"
182
+ ai-cli ps
183
+ ai-cli wait 12345
184
+ ai-cli result 12345
185
+ ai-cli cleanup
186
+ ```
187
+
188
+ `run` accepts `--cwd` as the primary working-directory flag and also accepts the older aliases `--workFolder` / `--work-folder` for compatibility.
189
+
190
+ `doctor` checks only binary existence and path resolution. It does not verify login state or terms acceptance.
191
+
192
+ ## CLI State Storage
193
+
194
+ Background CLI runs are stored under:
195
+
196
+ ```text
197
+ ~/.local/state/ai-cli/cwds/<normalized-cwd>/<pid>/
198
+ ```
199
+
200
+ Each PID directory contains:
201
+
202
+ - `meta.json`
203
+ - `stdout.log`
204
+ - `stderr.log`
205
+
206
+ Use `ai-cli cleanup` to remove completed and failed runs. Running processes are preserved.
207
+
208
+ ## Known Limitation
209
+
210
+ Detached `ai-cli` runs do not currently persist natural process exit codes. As a result, the CLI can report process output and running/completed state, but it does not yet guarantee `exitCode` for naturally finished background runs.
211
+
114
212
  ## Connecting to Your MCP Client
115
213
 
116
214
  After setting up the server, add the configuration to your MCP client's settings file (e.g., `mcp.json` for Cursor, `mcp_config.json` for Windsurf).
@@ -166,6 +264,7 @@ Terminates a running AI agent process by PID.
166
264
  ## Troubleshooting
167
265
 
168
266
  - **"Command not found" (claude-code-mcp):** If installed globally, ensure the npm global bin directory is in your system's PATH. If using `npx`, ensure `npx` itself is working.
267
+ - **"Command not found" (`ai-cli`):** If installed globally, ensure your npm global bin directory is in `PATH`. If using `npx`, use `npx -y --package ai-cli-mcp@latest ai-cli ...`.
169
268
  - **"Command not found" (claude or ~/.claude/local/claude):** Ensure the Claude CLI is installed correctly. Run `claude/doctor` or check its documentation.
170
269
  - **Permissions Issues:** Make sure you've run the "Important First-Time Setup" step.
171
270
  - **JSON Errors from Server:** If `MCP_CLAUDE_DEBUG` is `true`, error messages or logs might interfere with MCP's JSON parsing. Set to `false` for normal operation.
@@ -0,0 +1,285 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { CLI_HELP_TEXT, DOCTOR_HELP_TEXT, MODELS_HELP_TEXT, RUN_HELP_TEXT, WAIT_HELP_TEXT, runCli, } from '../app/cli.js';
3
+ describe('ai-cli app', () => {
4
+ it('prints help and exits successfully when no subcommand is provided', async () => {
5
+ const stdout = vi.fn();
6
+ const stderr = vi.fn();
7
+ const startMcpServer = vi.fn();
8
+ const exitCode = await runCli([], {
9
+ stdout,
10
+ stderr,
11
+ startMcpServer,
12
+ });
13
+ expect(exitCode).toBe(0);
14
+ expect(stdout).toHaveBeenCalledWith(CLI_HELP_TEXT);
15
+ expect(stderr).not.toHaveBeenCalled();
16
+ expect(startMcpServer).not.toHaveBeenCalled();
17
+ });
18
+ it('starts MCP mode when the mcp subcommand is provided', async () => {
19
+ const stdout = vi.fn();
20
+ const stderr = vi.fn();
21
+ const startMcpServer = vi.fn().mockResolvedValue(undefined);
22
+ const exitCode = await runCli(['mcp'], {
23
+ stdout,
24
+ stderr,
25
+ startMcpServer,
26
+ });
27
+ expect(exitCode).toBe(0);
28
+ expect(startMcpServer).toHaveBeenCalledTimes(1);
29
+ expect(stdout).not.toHaveBeenCalled();
30
+ expect(stderr).not.toHaveBeenCalled();
31
+ });
32
+ it('dispatches run with parsed CLI options', async () => {
33
+ const stdout = vi.fn();
34
+ const stderr = vi.fn();
35
+ const startMcpServer = vi.fn();
36
+ const runProcess = vi.fn().mockResolvedValue({
37
+ pid: 123,
38
+ status: 'started',
39
+ agent: 'claude',
40
+ message: 'claude process started successfully',
41
+ });
42
+ const exitCode = await runCli(['run', '--cwd', '/tmp/project', '--prompt', 'hello', '--model', 'sonnet'], {
43
+ stdout,
44
+ stderr,
45
+ startMcpServer,
46
+ runProcess,
47
+ });
48
+ expect(exitCode).toBe(0);
49
+ expect(runProcess).toHaveBeenCalledWith({
50
+ cwd: '/tmp/project',
51
+ prompt: 'hello',
52
+ model: 'sonnet',
53
+ });
54
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"pid": 123'));
55
+ expect(stderr).not.toHaveBeenCalled();
56
+ });
57
+ it('accepts legacy run option aliases', async () => {
58
+ const stdout = vi.fn();
59
+ const stderr = vi.fn();
60
+ const runProcess = vi.fn().mockResolvedValue({
61
+ pid: 123,
62
+ status: 'started',
63
+ agent: 'claude',
64
+ message: 'claude process started successfully',
65
+ });
66
+ const exitCode = await runCli([
67
+ 'run',
68
+ '--workFolder',
69
+ '/tmp/project',
70
+ '--prompt_file',
71
+ '/tmp/prompt.txt',
72
+ '--session_id',
73
+ 'session-123',
74
+ '--reasoning_effort',
75
+ 'high',
76
+ ], {
77
+ stdout,
78
+ stderr,
79
+ runProcess,
80
+ });
81
+ expect(exitCode).toBe(0);
82
+ expect(runProcess).toHaveBeenCalledWith({
83
+ cwd: '/tmp/project',
84
+ prompt_file: '/tmp/prompt.txt',
85
+ session_id: 'session-123',
86
+ reasoning_effort: 'high',
87
+ });
88
+ expect(stderr).not.toHaveBeenCalled();
89
+ });
90
+ it('requires a prompt or prompt file for run', async () => {
91
+ const stdout = vi.fn();
92
+ const stderr = vi.fn();
93
+ const exitCode = await runCli(['run', '--cwd', '/tmp/project'], {
94
+ stdout,
95
+ stderr,
96
+ });
97
+ expect(exitCode).toBe(1);
98
+ expect(stderr).toHaveBeenCalledWith('Missing required option: --prompt or --prompt-file\n');
99
+ expect(stdout).toHaveBeenCalledWith(CLI_HELP_TEXT);
100
+ });
101
+ it('dispatches wait with pid arguments and timeout', async () => {
102
+ const stdout = vi.fn();
103
+ const stderr = vi.fn();
104
+ const waitForProcesses = vi.fn().mockResolvedValue([{ pid: 123, status: 'completed' }]);
105
+ const exitCode = await runCli(['wait', '123', '456', '--timeout', '5'], {
106
+ stdout,
107
+ stderr,
108
+ waitForProcesses,
109
+ });
110
+ expect(exitCode).toBe(0);
111
+ expect(waitForProcesses).toHaveBeenCalledWith([123, 456], 5);
112
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"status": "completed"'));
113
+ });
114
+ it('rejects invalid wait timeout values', async () => {
115
+ const stdout = vi.fn();
116
+ const stderr = vi.fn();
117
+ const waitForProcesses = vi.fn();
118
+ const exitCode = await runCli(['wait', '123', '--timeout', 'abc'], {
119
+ stdout,
120
+ stderr,
121
+ waitForProcesses,
122
+ });
123
+ expect(exitCode).toBe(1);
124
+ expect(stderr).toHaveBeenCalledWith('Invalid --timeout value\n');
125
+ expect(stdout).toHaveBeenCalledWith(CLI_HELP_TEXT);
126
+ expect(waitForProcesses).not.toHaveBeenCalled();
127
+ });
128
+ it('rejects non-integer pid arguments for wait', async () => {
129
+ const stdout = vi.fn();
130
+ const stderr = vi.fn();
131
+ const waitForProcesses = vi.fn();
132
+ const exitCode = await runCli(['wait', '123', 'abc'], {
133
+ stdout,
134
+ stderr,
135
+ waitForProcesses,
136
+ });
137
+ expect(exitCode).toBe(1);
138
+ expect(stderr).toHaveBeenCalledWith('All pid arguments must be positive integers\n');
139
+ expect(stdout).toHaveBeenCalledWith(CLI_HELP_TEXT);
140
+ expect(waitForProcesses).not.toHaveBeenCalled();
141
+ });
142
+ it('dispatches ps, result, and kill', async () => {
143
+ const stdout = vi.fn();
144
+ const stderr = vi.fn();
145
+ const listProcesses = vi.fn().mockResolvedValue([{ pid: 123, agent: 'claude', status: 'running' }]);
146
+ const getProcessResult = vi.fn().mockResolvedValue({ pid: 123, status: 'completed' });
147
+ const killProcess = vi.fn().mockResolvedValue({ pid: 123, status: 'terminated' });
148
+ const psExitCode = await runCli(['ps'], { stdout, stderr, listProcesses });
149
+ expect(psExitCode).toBe(0);
150
+ expect(listProcesses).toHaveBeenCalledTimes(1);
151
+ const resultExitCode = await runCli(['result', '123'], { stdout, stderr, getProcessResult });
152
+ expect(resultExitCode).toBe(0);
153
+ expect(getProcessResult).toHaveBeenCalledWith(123, false);
154
+ const killExitCode = await runCli(['kill', '123'], { stdout, stderr, killProcess });
155
+ expect(killExitCode).toBe(0);
156
+ expect(killProcess).toHaveBeenCalledWith(123);
157
+ });
158
+ it('dispatches cleanup', async () => {
159
+ const stdout = vi.fn();
160
+ const stderr = vi.fn();
161
+ const cleanupProcesses = vi.fn().mockResolvedValue({ removed: 2, message: 'Removed 2 processes' });
162
+ const exitCode = await runCli(['cleanup'], { stdout, stderr, cleanupProcesses });
163
+ expect(exitCode).toBe(0);
164
+ expect(cleanupProcesses).toHaveBeenCalledTimes(1);
165
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"removed": 2'));
166
+ });
167
+ it('prints models as structured json', async () => {
168
+ const stdout = vi.fn();
169
+ const stderr = vi.fn();
170
+ const exitCode = await runCli(['models'], { stdout, stderr });
171
+ expect(exitCode).toBe(0);
172
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"aliases"'));
173
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"claude-ultra"'));
174
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"gpt-5.4"'));
175
+ expect(stderr).not.toHaveBeenCalled();
176
+ });
177
+ it('prints doctor status as structured json', async () => {
178
+ const stdout = vi.fn();
179
+ const stderr = vi.fn();
180
+ const getDoctorStatus = vi.fn().mockReturnValue({
181
+ claude: {
182
+ configuredCommand: 'claude',
183
+ resolvedPath: '/tmp/bin/claude',
184
+ available: true,
185
+ lookup: 'path',
186
+ },
187
+ codex: {
188
+ configuredCommand: 'codex',
189
+ resolvedPath: null,
190
+ available: false,
191
+ lookup: 'path',
192
+ },
193
+ gemini: {
194
+ configuredCommand: 'gemini',
195
+ resolvedPath: '/tmp/bin/gemini',
196
+ available: true,
197
+ lookup: 'path',
198
+ },
199
+ });
200
+ const exitCode = await runCli(['doctor'], { stdout, stderr, getDoctorStatus });
201
+ expect(exitCode).toBe(0);
202
+ expect(getDoctorStatus).toHaveBeenCalledTimes(1);
203
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"configuredCommand": "claude"'));
204
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"available": false'));
205
+ expect(stderr).not.toHaveBeenCalled();
206
+ });
207
+ it('passes verbose through to result', async () => {
208
+ const stdout = vi.fn();
209
+ const stderr = vi.fn();
210
+ const getProcessResult = vi.fn().mockResolvedValue({ pid: 123, status: 'completed' });
211
+ const exitCode = await runCli(['result', '123', '--verbose'], { stdout, stderr, getProcessResult });
212
+ expect(exitCode).toBe(0);
213
+ expect(getProcessResult).toHaveBeenCalledWith(123, true);
214
+ });
215
+ it('prints detailed help for run --help', async () => {
216
+ const stdout = vi.fn();
217
+ const stderr = vi.fn();
218
+ const exitCode = await runCli(['run', '--help'], { stdout, stderr });
219
+ expect(exitCode).toBe(0);
220
+ expect(stdout).toHaveBeenCalledWith(RUN_HELP_TEXT);
221
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('claude-ultra'));
222
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('gpt-5.2-codex'));
223
+ expect(stdout).toHaveBeenCalledWith(expect.stringContaining('gemini-2.5-pro'));
224
+ expect(stderr).not.toHaveBeenCalled();
225
+ });
226
+ it('prints detailed help for wait --help', async () => {
227
+ const stdout = vi.fn();
228
+ const stderr = vi.fn();
229
+ const exitCode = await runCli(['wait', '--help'], { stdout, stderr });
230
+ expect(exitCode).toBe(0);
231
+ expect(stdout).toHaveBeenCalledWith(WAIT_HELP_TEXT);
232
+ expect(stderr).not.toHaveBeenCalled();
233
+ });
234
+ it('prints detailed help for models --help', async () => {
235
+ const stdout = vi.fn();
236
+ const stderr = vi.fn();
237
+ const exitCode = await runCli(['models', '--help'], { stdout, stderr });
238
+ expect(exitCode).toBe(0);
239
+ expect(stdout).toHaveBeenCalledWith(MODELS_HELP_TEXT);
240
+ expect(stderr).not.toHaveBeenCalled();
241
+ });
242
+ it('prints detailed help for doctor --help', async () => {
243
+ const stdout = vi.fn();
244
+ const stderr = vi.fn();
245
+ const exitCode = await runCli(['doctor', '--help'], { stdout, stderr });
246
+ expect(exitCode).toBe(0);
247
+ expect(stdout).toHaveBeenCalledWith(DOCTOR_HELP_TEXT);
248
+ expect(stderr).not.toHaveBeenCalled();
249
+ });
250
+ it('prints detailed help for doctor -h', async () => {
251
+ const stdout = vi.fn();
252
+ const stderr = vi.fn();
253
+ const exitCode = await runCli(['doctor', '-h'], { stdout, stderr });
254
+ expect(exitCode).toBe(0);
255
+ expect(stdout).toHaveBeenCalledWith(DOCTOR_HELP_TEXT);
256
+ expect(stderr).not.toHaveBeenCalled();
257
+ });
258
+ it('prints help for --help', async () => {
259
+ const stdout = vi.fn();
260
+ const stderr = vi.fn();
261
+ const startMcpServer = vi.fn();
262
+ const exitCode = await runCli(['--help'], {
263
+ stdout,
264
+ stderr,
265
+ startMcpServer,
266
+ });
267
+ expect(exitCode).toBe(0);
268
+ expect(stdout).toHaveBeenCalledWith(CLI_HELP_TEXT);
269
+ expect(stderr).not.toHaveBeenCalled();
270
+ });
271
+ it('returns a non-zero exit code for unknown subcommands', async () => {
272
+ const stdout = vi.fn();
273
+ const stderr = vi.fn();
274
+ const startMcpServer = vi.fn();
275
+ const exitCode = await runCli(['unknown'], {
276
+ stdout,
277
+ stderr,
278
+ startMcpServer,
279
+ });
280
+ expect(exitCode).toBe(1);
281
+ expect(stderr).toHaveBeenCalledWith(expect.stringContaining('Unknown subcommand: unknown'));
282
+ expect(stdout).toHaveBeenCalledWith(CLI_HELP_TEXT);
283
+ expect(startMcpServer).not.toHaveBeenCalled();
284
+ });
285
+ });
@@ -0,0 +1,54 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { delimiter, join } from 'node:path';
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+ const tempDirs = [];
7
+ function makeTempDir(prefix) {
8
+ const dir = mkdtempSync(join(tmpdir(), prefix));
9
+ tempDirs.push(dir);
10
+ return dir;
11
+ }
12
+ function writeExecutable(dir, name) {
13
+ const filePath = join(dir, name);
14
+ writeFileSync(filePath, '#!/bin/sh\nexit 0\n', 'utf8');
15
+ chmodSync(filePath, 0o755);
16
+ }
17
+ afterEach(() => {
18
+ for (const dir of tempDirs.splice(0)) {
19
+ rmSync(dir, { recursive: true, force: true });
20
+ }
21
+ });
22
+ describe('ai-cli entrypoint smoke', () => {
23
+ it('prints doctor output for the ai-cli entrypoint', () => {
24
+ const fakeBinDir = makeTempDir('ai-cli-bin-');
25
+ writeExecutable(fakeBinDir, 'claude');
26
+ writeExecutable(fakeBinDir, 'codex');
27
+ writeExecutable(fakeBinDir, 'gemini');
28
+ const output = execFileSync('node', ['--import', 'tsx', 'src/bin/ai-cli.ts', 'doctor'], {
29
+ cwd: process.cwd(),
30
+ encoding: 'utf8',
31
+ env: {
32
+ ...process.env,
33
+ PATH: `${fakeBinDir}${delimiter}${process.env.PATH || ''}`,
34
+ CLAUDE_CLI_NAME: 'claude',
35
+ CODEX_CLI_NAME: 'codex',
36
+ GEMINI_CLI_NAME: 'gemini',
37
+ },
38
+ });
39
+ expect(output).toContain('"claude"');
40
+ expect(output).toContain('"codex"');
41
+ expect(output).toContain('"gemini"');
42
+ expect(output).toContain('"available": true');
43
+ });
44
+ it('prints run help for the ai-cli entrypoint', () => {
45
+ const output = execFileSync('node', ['--import', 'tsx', 'src/bin/ai-cli.ts', 'run', '--help'], {
46
+ cwd: process.cwd(),
47
+ encoding: 'utf8',
48
+ env: process.env,
49
+ });
50
+ expect(output).toContain('Usage: ai-cli run --cwd <path> [options]');
51
+ expect(output).toContain('--model <model>');
52
+ expect(output).toContain('claude-ultra');
53
+ });
54
+ });