ai-cli-mcp 2.13.0 → 2.14.1
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/CHANGELOG.md +14 -0
- package/README.ja.md +10 -0
- package/README.md +10 -0
- package/dist/__tests__/app-cli.test.js +26 -2
- package/dist/__tests__/cli-process-service.test.js +218 -5
- package/dist/__tests__/mcp-contract.test.js +138 -8
- package/dist/__tests__/process-management.test.js +2 -1
- package/dist/app/cli.js +6 -4
- package/dist/app/mcp.js +8 -4
- package/dist/cli-process-service.js +26 -26
- package/dist/process-result.js +51 -0
- package/dist/process-service.js +7 -21
- package/package.json +1 -1
- package/src/__tests__/app-cli.test.ts +35 -1
- package/src/__tests__/cli-process-service.test.ts +249 -5
- package/src/__tests__/mcp-contract.test.ts +152 -8
- package/src/__tests__/process-management.test.ts +2 -1
- package/src/app/cli.ts +7 -5
- package/src/app/mcp.ts +9 -4
- package/src/cli-process-service.ts +29 -26
- package/src/process-result.ts +79 -0
- package/src/process-service.ts +7 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.14.1](https://github.com/mkXultra/ai-cli-mcp/compare/v2.14.0...v2.14.1) (2026-04-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* 削除済み作業フォルダでのプロセス操作クラッシュを修正 ([02d765f](https://github.com/mkXultra/ai-cli-mcp/commit/02d765ff76ebd295118e16a112ea3e7ac6fab111))
|
|
7
|
+
|
|
8
|
+
# [2.14.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.13.0...v2.14.0) (2026-04-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* get_result・waitにcompact/verbose出力形式を追加 ([21fdff3](https://github.com/mkXultra/ai-cli-mcp/commit/21fdff3813f28327a2da94f4b5bed94b54abcd74))
|
|
14
|
+
|
|
1
15
|
# [2.13.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.12.0...v2.13.0) (2026-04-07)
|
|
2
16
|
|
|
3
17
|
|
package/README.ja.md
CHANGED
|
@@ -118,7 +118,9 @@ ai-cli models
|
|
|
118
118
|
ai-cli run --cwd "$PWD" --model sonnet --prompt "summarize this repository"
|
|
119
119
|
ai-cli ps
|
|
120
120
|
ai-cli result 12345
|
|
121
|
+
ai-cli result 12345 --verbose
|
|
121
122
|
ai-cli wait 12345 --timeout 300
|
|
123
|
+
ai-cli wait 12345 --verbose
|
|
122
124
|
ai-cli kill 12345
|
|
123
125
|
ai-cli cleanup
|
|
124
126
|
ai-cli-mcp
|
|
@@ -185,7 +187,9 @@ ai-cli models
|
|
|
185
187
|
ai-cli run --cwd "$PWD" --model codex-ultra --prompt "fix failing tests"
|
|
186
188
|
ai-cli ps
|
|
187
189
|
ai-cli wait 12345
|
|
190
|
+
ai-cli wait 12345 --verbose
|
|
188
191
|
ai-cli result 12345
|
|
192
|
+
ai-cli result 12345 --verbose
|
|
189
193
|
ai-cli cleanup
|
|
190
194
|
```
|
|
191
195
|
|
|
@@ -244,9 +248,12 @@ Claude CLI、Codex CLI、Gemini CLI、または Forge CLI を使用してプロ
|
|
|
244
248
|
|
|
245
249
|
複数のAIエージェントプロセスの完了を待機し、結果をまとめて返します。指定されたすべてのPIDが終了するか、タイムアウトになるまでブロックします。
|
|
246
250
|
|
|
251
|
+
デフォルトでは、返される各結果項目は `get_result(verbose: false)` と同じ compact 形を使います。`pid`、`agent`、`status`、`exitCode`、`model` などの運用上必要な項目に加え、利用可能であれば `agentOutput` やトップレベルの `session_id` を含みます。`verbose: true` を指定すると、`startTime`、`workFolder`、`prompt` などの完全なメタデータや、`agentOutput.tools` のような詳細な解析結果を含む full 形を返します。
|
|
252
|
+
|
|
247
253
|
**引数:**
|
|
248
254
|
- `pids` (array of numbers, 必須): 待機するプロセスIDのリスト(`run` ツールから返されたもの)。
|
|
249
255
|
- `timeout` (number, 任意): 最大待機時間(秒)。デフォルトは180秒(3分)です。
|
|
256
|
+
- `verbose` (boolean, 任意): `true` の場合、各結果項目を full 形で返します。デフォルトは `false` です。
|
|
250
257
|
|
|
251
258
|
### `list_processes`
|
|
252
259
|
|
|
@@ -256,8 +263,11 @@ Claude CLI、Codex CLI、Gemini CLI、または Forge CLI を使用してプロ
|
|
|
256
263
|
|
|
257
264
|
PIDを指定して、AIエージェントプロセスの現在の出力とステータスを取得します。
|
|
258
265
|
|
|
266
|
+
デフォルトでは compact 形を返します。これには `pid`、`agent`、`status`、`exitCode`、`model` などの運用上必要な項目に加え、利用可能であれば `agentOutput` やトップレベルの `session_id` を含みます。`startTime`、`workFolder`、`prompt` は含みません。`verbose: true` を指定すると、これらのメタデータや `agentOutput.tools` のような詳細な解析結果を含む full 形を返します。解析結果が得られない場合や不完全な場合は、従来どおり `stdout` / `stderr` のフォールバックを維持します。
|
|
267
|
+
|
|
259
268
|
**引数:**
|
|
260
269
|
- `pid` (number, 必須): `run` ツールによって返されたプロセスID。
|
|
270
|
+
- `verbose` (boolean, 任意): `true` の場合、full 形で返します。デフォルトは `false` です。
|
|
261
271
|
|
|
262
272
|
### `kill_process`
|
|
263
273
|
|
package/README.md
CHANGED
|
@@ -116,7 +116,9 @@ ai-cli models
|
|
|
116
116
|
ai-cli run --cwd "$PWD" --model sonnet --prompt "summarize this repository"
|
|
117
117
|
ai-cli ps
|
|
118
118
|
ai-cli result 12345
|
|
119
|
+
ai-cli result 12345 --verbose
|
|
119
120
|
ai-cli wait 12345 --timeout 300
|
|
121
|
+
ai-cli wait 12345 --verbose
|
|
120
122
|
ai-cli kill 12345
|
|
121
123
|
ai-cli cleanup
|
|
122
124
|
ai-cli-mcp
|
|
@@ -183,7 +185,9 @@ ai-cli models
|
|
|
183
185
|
ai-cli run --cwd "$PWD" --model codex-ultra --prompt "fix failing tests"
|
|
184
186
|
ai-cli ps
|
|
185
187
|
ai-cli wait 12345
|
|
188
|
+
ai-cli wait 12345 --verbose
|
|
186
189
|
ai-cli result 12345
|
|
190
|
+
ai-cli result 12345 --verbose
|
|
187
191
|
ai-cli cleanup
|
|
188
192
|
```
|
|
189
193
|
|
|
@@ -242,9 +246,12 @@ Executes a prompt using Claude CLI, Codex CLI, Gemini CLI, or Forge CLI. The app
|
|
|
242
246
|
|
|
243
247
|
Waits for multiple AI agent processes to complete and returns their combined results. Blocks until all specified PIDs finish or a timeout occurs.
|
|
244
248
|
|
|
249
|
+
By default, each returned result item uses the compact shape shared with `get_result(verbose: false)`: operational fields such as `pid`, `agent`, `status`, `exitCode`, `model`, parsed output such as `agentOutput`, and top-level `session_id` when available. Set `verbose: true` to include full metadata like `startTime`, `workFolder`, `prompt`, and detailed parsed output such as `agentOutput.tools`.
|
|
250
|
+
|
|
245
251
|
**Arguments:**
|
|
246
252
|
- `pids` (array of numbers, required): List of process IDs to wait for (returned by the `run` tool).
|
|
247
253
|
- `timeout` (number, optional): Maximum wait time in seconds. Defaults to 180 (3 minutes).
|
|
254
|
+
- `verbose` (boolean, optional): If `true`, each result item uses the full result shape. Defaults to `false`.
|
|
248
255
|
|
|
249
256
|
### `list_processes`
|
|
250
257
|
|
|
@@ -254,8 +261,11 @@ Lists all running and completed AI agent processes with their status, PID, and b
|
|
|
254
261
|
|
|
255
262
|
Gets the current output and status of an AI agent process by PID.
|
|
256
263
|
|
|
264
|
+
By default, this returns the compact result shape: operational fields such as `pid`, `agent`, `status`, `exitCode`, `model`, parsed output such as `agentOutput`, and top-level `session_id` when available. It omits metadata fields like `startTime`, `workFolder`, and `prompt`. Set `verbose: true` to return the full result shape including those metadata fields and detailed parsed output such as `agentOutput.tools`. If parsed output is unavailable or incomplete, the raw `stdout`/`stderr` fallback is preserved.
|
|
265
|
+
|
|
257
266
|
**Arguments:**
|
|
258
267
|
- `pid` (number, required): The process ID returned by the `run` tool.
|
|
268
|
+
- `verbose` (boolean, optional): If `true`, returns the full result shape. Defaults to `false`.
|
|
259
269
|
|
|
260
270
|
### `kill_process`
|
|
261
271
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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';
|
|
2
|
+
import { CLI_HELP_TEXT, DOCTOR_HELP_TEXT, MODELS_HELP_TEXT, RESULT_HELP_TEXT, RUN_HELP_TEXT, WAIT_HELP_TEXT, runCli, } from '../app/cli.js';
|
|
3
3
|
describe('ai-cli app', () => {
|
|
4
4
|
it('prints help and exits successfully when no subcommand is provided', async () => {
|
|
5
5
|
const stdout = vi.fn();
|
|
@@ -108,9 +108,21 @@ describe('ai-cli app', () => {
|
|
|
108
108
|
waitForProcesses,
|
|
109
109
|
});
|
|
110
110
|
expect(exitCode).toBe(0);
|
|
111
|
-
expect(waitForProcesses).toHaveBeenCalledWith([123, 456], 5);
|
|
111
|
+
expect(waitForProcesses).toHaveBeenCalledWith([123, 456], 5, false);
|
|
112
112
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"status": "completed"'));
|
|
113
113
|
});
|
|
114
|
+
it('passes verbose through to wait', async () => {
|
|
115
|
+
const stdout = vi.fn();
|
|
116
|
+
const stderr = vi.fn();
|
|
117
|
+
const waitForProcesses = vi.fn().mockResolvedValue([{ pid: 123, status: 'completed' }]);
|
|
118
|
+
const exitCode = await runCli(['wait', '123', '--verbose'], {
|
|
119
|
+
stdout,
|
|
120
|
+
stderr,
|
|
121
|
+
waitForProcesses,
|
|
122
|
+
});
|
|
123
|
+
expect(exitCode).toBe(0);
|
|
124
|
+
expect(waitForProcesses).toHaveBeenCalledWith([123], undefined, true);
|
|
125
|
+
});
|
|
114
126
|
it('rejects invalid wait timeout values', async () => {
|
|
115
127
|
const stdout = vi.fn();
|
|
116
128
|
const stderr = vi.fn();
|
|
@@ -231,12 +243,24 @@ describe('ai-cli app', () => {
|
|
|
231
243
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('forge'));
|
|
232
244
|
expect(stderr).not.toHaveBeenCalled();
|
|
233
245
|
});
|
|
246
|
+
it('prints detailed help for result --help', async () => {
|
|
247
|
+
const stdout = vi.fn();
|
|
248
|
+
const stderr = vi.fn();
|
|
249
|
+
const exitCode = await runCli(['result', '--help'], { stdout, stderr });
|
|
250
|
+
expect(exitCode).toBe(0);
|
|
251
|
+
expect(stdout).toHaveBeenCalledWith(RESULT_HELP_TEXT);
|
|
252
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('compact result shape'));
|
|
253
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('--verbose'));
|
|
254
|
+
expect(stderr).not.toHaveBeenCalled();
|
|
255
|
+
});
|
|
234
256
|
it('prints detailed help for wait --help', async () => {
|
|
235
257
|
const stdout = vi.fn();
|
|
236
258
|
const stderr = vi.fn();
|
|
237
259
|
const exitCode = await runCli(['wait', '--help'], { stdout, stderr });
|
|
238
260
|
expect(exitCode).toBe(0);
|
|
239
261
|
expect(stdout).toHaveBeenCalledWith(WAIT_HELP_TEXT);
|
|
262
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('compact shape'));
|
|
263
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('--verbose'));
|
|
240
264
|
expect(stderr).not.toHaveBeenCalled();
|
|
241
265
|
});
|
|
242
266
|
it('prints detailed help for models --help', async () => {
|
|
@@ -72,8 +72,18 @@ describe('CliProcessService', () => {
|
|
|
72
72
|
expect(existsSync(join(processDir, 'stderr.log'))).toBe(true);
|
|
73
73
|
const waitResult = await service.waitForProcesses([runResult.pid], 5);
|
|
74
74
|
expect(waitResult).toHaveLength(1);
|
|
75
|
-
expect(waitResult[0]
|
|
76
|
-
|
|
75
|
+
expect(waitResult[0]).toMatchObject({
|
|
76
|
+
pid: runResult.pid,
|
|
77
|
+
agent: 'claude',
|
|
78
|
+
status: 'completed',
|
|
79
|
+
exitCode: null,
|
|
80
|
+
model: 'sonnet',
|
|
81
|
+
stdout: expect.any(String),
|
|
82
|
+
stderr: expect.any(String),
|
|
83
|
+
});
|
|
84
|
+
expect(waitResult[0]).not.toHaveProperty('startTime');
|
|
85
|
+
expect(waitResult[0]).not.toHaveProperty('workFolder');
|
|
86
|
+
expect(waitResult[0]).not.toHaveProperty('prompt');
|
|
77
87
|
const listed = await service.listProcesses();
|
|
78
88
|
expect(listed).toContainEqual({
|
|
79
89
|
pid: runResult.pid,
|
|
@@ -81,11 +91,130 @@ describe('CliProcessService', () => {
|
|
|
81
91
|
status: 'completed',
|
|
82
92
|
});
|
|
83
93
|
const result = await service.getProcessResult(runResult.pid, false);
|
|
84
|
-
expect(result
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
expect(result).toMatchObject({
|
|
95
|
+
pid: runResult.pid,
|
|
96
|
+
agent: 'claude',
|
|
97
|
+
status: 'completed',
|
|
98
|
+
exitCode: null,
|
|
99
|
+
model: 'sonnet',
|
|
100
|
+
stdout: expect.stringContaining('Command executed successfully'),
|
|
101
|
+
stderr: expect.any(String),
|
|
102
|
+
});
|
|
103
|
+
expect(result).not.toHaveProperty('startTime');
|
|
104
|
+
expect(result).not.toHaveProperty('workFolder');
|
|
105
|
+
expect(result).not.toHaveProperty('prompt');
|
|
87
106
|
expect(readFileSync(join(processDir, 'meta.json'), 'utf-8')).toContain('"status": "completed"');
|
|
88
107
|
});
|
|
108
|
+
it('returns compact results by default and full results when verbose is true', async () => {
|
|
109
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
110
|
+
tempDirs.push(root);
|
|
111
|
+
const scriptPath = join(root, 'mock-claude-json');
|
|
112
|
+
writeFileSync(scriptPath, `#!/bin/bash
|
|
113
|
+
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/demo.txt"}}]}}'
|
|
114
|
+
printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":[{"type":"text","text":"demo output"}]}]}}'
|
|
115
|
+
printf '%s\n' '{"type":"result","result":"Completed cli-process-service test"}'
|
|
116
|
+
printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
|
|
117
|
+
`);
|
|
118
|
+
chmodSync(scriptPath, 0o755);
|
|
119
|
+
const stateDir = join(root, 'state');
|
|
120
|
+
const workFolder = join(root, 'work');
|
|
121
|
+
mkdirSync(workFolder, { recursive: true });
|
|
122
|
+
const service = new CliProcessService({
|
|
123
|
+
stateDir,
|
|
124
|
+
cliPaths: {
|
|
125
|
+
claude: scriptPath,
|
|
126
|
+
codex: scriptPath,
|
|
127
|
+
gemini: scriptPath,
|
|
128
|
+
forge: scriptPath,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const runResult = await service.startProcess({
|
|
132
|
+
prompt: 'hello structured output',
|
|
133
|
+
cwd: workFolder,
|
|
134
|
+
});
|
|
135
|
+
const compactWait = await service.waitForProcesses([runResult.pid], 5);
|
|
136
|
+
expect(compactWait).toHaveLength(1);
|
|
137
|
+
expect(compactWait[0]).toMatchObject({
|
|
138
|
+
pid: runResult.pid,
|
|
139
|
+
agent: 'claude',
|
|
140
|
+
status: 'completed',
|
|
141
|
+
exitCode: null,
|
|
142
|
+
model: null,
|
|
143
|
+
session_id: 'session-cli-1',
|
|
144
|
+
agentOutput: {
|
|
145
|
+
message: 'Completed cli-process-service test',
|
|
146
|
+
session_id: 'session-cli-1',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
expect(compactWait[0]).not.toHaveProperty('startTime');
|
|
150
|
+
expect(compactWait[0]).not.toHaveProperty('workFolder');
|
|
151
|
+
expect(compactWait[0]).not.toHaveProperty('prompt');
|
|
152
|
+
expect(compactWait[0].agentOutput).not.toHaveProperty('tools');
|
|
153
|
+
const compactResult = await service.getProcessResult(runResult.pid, false);
|
|
154
|
+
expect(compactResult).toMatchObject({
|
|
155
|
+
pid: runResult.pid,
|
|
156
|
+
agent: 'claude',
|
|
157
|
+
status: 'completed',
|
|
158
|
+
exitCode: null,
|
|
159
|
+
model: null,
|
|
160
|
+
session_id: 'session-cli-1',
|
|
161
|
+
agentOutput: {
|
|
162
|
+
message: 'Completed cli-process-service test',
|
|
163
|
+
session_id: 'session-cli-1',
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
expect(compactResult).not.toHaveProperty('startTime');
|
|
167
|
+
expect(compactResult).not.toHaveProperty('workFolder');
|
|
168
|
+
expect(compactResult).not.toHaveProperty('prompt');
|
|
169
|
+
expect(compactResult.agentOutput).not.toHaveProperty('tools');
|
|
170
|
+
const verboseWait = await service.waitForProcesses([runResult.pid], 5, true);
|
|
171
|
+
expect(verboseWait).toHaveLength(1);
|
|
172
|
+
expect(verboseWait[0]).toMatchObject({
|
|
173
|
+
pid: runResult.pid,
|
|
174
|
+
agent: 'claude',
|
|
175
|
+
status: 'completed',
|
|
176
|
+
exitCode: null,
|
|
177
|
+
model: null,
|
|
178
|
+
startTime: expect.any(String),
|
|
179
|
+
workFolder,
|
|
180
|
+
prompt: 'hello structured output',
|
|
181
|
+
session_id: 'session-cli-1',
|
|
182
|
+
agentOutput: {
|
|
183
|
+
message: 'Completed cli-process-service test',
|
|
184
|
+
session_id: 'session-cli-1',
|
|
185
|
+
tools: [
|
|
186
|
+
{
|
|
187
|
+
tool: 'Read',
|
|
188
|
+
input: { file_path: '/tmp/demo.txt' },
|
|
189
|
+
output: 'demo output',
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
const verboseResult = await service.getProcessResult(runResult.pid, true);
|
|
195
|
+
expect(verboseResult).toMatchObject({
|
|
196
|
+
pid: runResult.pid,
|
|
197
|
+
agent: 'claude',
|
|
198
|
+
status: 'completed',
|
|
199
|
+
exitCode: null,
|
|
200
|
+
model: null,
|
|
201
|
+
startTime: expect.any(String),
|
|
202
|
+
workFolder,
|
|
203
|
+
prompt: 'hello structured output',
|
|
204
|
+
session_id: 'session-cli-1',
|
|
205
|
+
agentOutput: {
|
|
206
|
+
message: 'Completed cli-process-service test',
|
|
207
|
+
session_id: 'session-cli-1',
|
|
208
|
+
tools: [
|
|
209
|
+
{
|
|
210
|
+
tool: 'Read',
|
|
211
|
+
input: { file_path: '/tmp/demo.txt' },
|
|
212
|
+
output: 'demo output',
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
});
|
|
89
218
|
it('can terminate a tracked process', async () => {
|
|
90
219
|
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
91
220
|
tempDirs.push(root);
|
|
@@ -165,6 +294,90 @@ describe('CliProcessService', () => {
|
|
|
165
294
|
expect(stored.status).toBe('running');
|
|
166
295
|
killSpy.mockRestore();
|
|
167
296
|
});
|
|
297
|
+
it('lists processes without crashing when a tracked work folder has been deleted', async () => {
|
|
298
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
299
|
+
tempDirs.push(root);
|
|
300
|
+
const stateDir = join(root, 'state');
|
|
301
|
+
const workFolder = join(root, 'deleted-project');
|
|
302
|
+
mkdirSync(workFolder, { recursive: true });
|
|
303
|
+
const pid = 45678;
|
|
304
|
+
const processDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)), String(pid));
|
|
305
|
+
mkdirSync(processDir, { recursive: true });
|
|
306
|
+
writeFileSync(join(processDir, 'meta.json'), JSON.stringify({
|
|
307
|
+
pid,
|
|
308
|
+
prompt: 'deleted cwd',
|
|
309
|
+
workFolder,
|
|
310
|
+
toolType: 'claude',
|
|
311
|
+
startTime: new Date().toISOString(),
|
|
312
|
+
stdoutPath: join(processDir, 'stdout.log'),
|
|
313
|
+
stderrPath: join(processDir, 'stderr.log'),
|
|
314
|
+
status: 'running',
|
|
315
|
+
}));
|
|
316
|
+
rmSync(workFolder, { recursive: true, force: true });
|
|
317
|
+
const service = new CliProcessService({
|
|
318
|
+
stateDir,
|
|
319
|
+
cliPaths: {
|
|
320
|
+
claude: '/bin/sh',
|
|
321
|
+
codex: '/bin/sh',
|
|
322
|
+
gemini: '/bin/sh',
|
|
323
|
+
forge: '/bin/sh',
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
const killSpy = vi.spyOn(globalThis.process, 'kill').mockImplementation((target, signal) => {
|
|
327
|
+
if (signal === 0 && target === pid) {
|
|
328
|
+
throw Object.assign(new Error('not running'), { code: 'ESRCH' });
|
|
329
|
+
}
|
|
330
|
+
return true;
|
|
331
|
+
});
|
|
332
|
+
const listed = await service.listProcesses();
|
|
333
|
+
expect(listed).toEqual([
|
|
334
|
+
{
|
|
335
|
+
pid,
|
|
336
|
+
agent: 'claude',
|
|
337
|
+
status: 'completed',
|
|
338
|
+
},
|
|
339
|
+
]);
|
|
340
|
+
expect(JSON.parse(readFileSync(join(processDir, 'meta.json'), 'utf-8')).status).toBe('completed');
|
|
341
|
+
killSpy.mockRestore();
|
|
342
|
+
});
|
|
343
|
+
it('cleans up finished process directories even when their work folder has been deleted', async () => {
|
|
344
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
345
|
+
tempDirs.push(root);
|
|
346
|
+
const stateDir = join(root, 'state');
|
|
347
|
+
const workFolder = join(root, 'deleted-finished-project');
|
|
348
|
+
mkdirSync(workFolder, { recursive: true });
|
|
349
|
+
const pid = 56789;
|
|
350
|
+
const cwdDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)));
|
|
351
|
+
const processDir = join(cwdDir, String(pid));
|
|
352
|
+
mkdirSync(processDir, { recursive: true });
|
|
353
|
+
writeFileSync(join(processDir, 'meta.json'), JSON.stringify({
|
|
354
|
+
pid,
|
|
355
|
+
prompt: 'done',
|
|
356
|
+
workFolder,
|
|
357
|
+
toolType: 'claude',
|
|
358
|
+
startTime: new Date().toISOString(),
|
|
359
|
+
stdoutPath: join(processDir, 'stdout.log'),
|
|
360
|
+
stderrPath: join(processDir, 'stderr.log'),
|
|
361
|
+
status: 'completed',
|
|
362
|
+
}));
|
|
363
|
+
rmSync(workFolder, { recursive: true, force: true });
|
|
364
|
+
const service = new CliProcessService({
|
|
365
|
+
stateDir,
|
|
366
|
+
cliPaths: {
|
|
367
|
+
claude: '/bin/sh',
|
|
368
|
+
codex: '/bin/sh',
|
|
369
|
+
gemini: '/bin/sh',
|
|
370
|
+
forge: '/bin/sh',
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
const result = await service.cleanupProcesses();
|
|
374
|
+
expect(result).toEqual({
|
|
375
|
+
removed: 1,
|
|
376
|
+
message: 'Removed 1 processes',
|
|
377
|
+
});
|
|
378
|
+
expect(existsSync(processDir)).toBe(false);
|
|
379
|
+
expect(existsSync(cwdDir)).toBe(false);
|
|
380
|
+
});
|
|
168
381
|
it('cleans up completed and failed process directories but preserves running ones', async () => {
|
|
169
382
|
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
170
383
|
tempDirs.push(root);
|
|
@@ -107,6 +107,7 @@ describe('MCP Contract Tests', () => {
|
|
|
107
107
|
expect(Object.keys(waitTool.inputSchema.properties).sort()).toEqual([
|
|
108
108
|
'pids',
|
|
109
109
|
'timeout',
|
|
110
|
+
'verbose',
|
|
110
111
|
]);
|
|
111
112
|
});
|
|
112
113
|
it('preserves the stdio MCP smoke flow and response shapes', async () => {
|
|
@@ -134,20 +135,30 @@ describe('MCP Contract Tests', () => {
|
|
|
134
135
|
pid: runData.pid,
|
|
135
136
|
agent: 'claude',
|
|
136
137
|
status: expect.any(String),
|
|
137
|
-
startTime: expect.any(String),
|
|
138
|
-
workFolder: testDir,
|
|
139
|
-
prompt: 'create a file called contract.txt with content "hello"',
|
|
140
138
|
model: 'haiku',
|
|
141
139
|
stdout: expect.any(String),
|
|
142
140
|
stderr: expect.any(String),
|
|
143
141
|
});
|
|
142
|
+
expect(getResultData).toHaveProperty('exitCode');
|
|
143
|
+
expect(getResultData).not.toHaveProperty('startTime');
|
|
144
|
+
expect(getResultData).not.toHaveProperty('workFolder');
|
|
145
|
+
expect(getResultData).not.toHaveProperty('prompt');
|
|
144
146
|
const waitResponse = await client.callTool('wait', { pids: [runData.pid], timeout: 5 });
|
|
145
147
|
const waitData = parseToolJson(waitResponse);
|
|
146
148
|
expect(Array.isArray(waitData)).toBe(true);
|
|
147
149
|
expect(waitData).toHaveLength(1);
|
|
148
|
-
expect(waitData[0]
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
expect(waitData[0]).toMatchObject({
|
|
151
|
+
pid: runData.pid,
|
|
152
|
+
agent: 'claude',
|
|
153
|
+
status: 'completed',
|
|
154
|
+
exitCode: 0,
|
|
155
|
+
model: 'haiku',
|
|
156
|
+
stdout: expect.any(String),
|
|
157
|
+
stderr: expect.any(String),
|
|
158
|
+
});
|
|
159
|
+
expect(waitData[0]).not.toHaveProperty('startTime');
|
|
160
|
+
expect(waitData[0]).not.toHaveProperty('workFolder');
|
|
161
|
+
expect(waitData[0]).not.toHaveProperty('prompt');
|
|
151
162
|
const cleanupResponse = await client.callTool('cleanup_processes', {});
|
|
152
163
|
const cleanupData = parseToolJson(cleanupResponse);
|
|
153
164
|
expect(cleanupData).toEqual({
|
|
@@ -157,12 +168,13 @@ describe('MCP Contract Tests', () => {
|
|
|
157
168
|
});
|
|
158
169
|
expect(cleanupData.removedPids).toContain(runData.pid);
|
|
159
170
|
});
|
|
160
|
-
it('
|
|
171
|
+
it('preserves successful prompt_file execution through the MCP process path', async () => {
|
|
161
172
|
const promptFile = join(testDir, 'prompt.txt');
|
|
162
|
-
writeFileSync(promptFile, '
|
|
173
|
+
writeFileSync(promptFile, 'Create a file from prompt_file');
|
|
163
174
|
const runResponse = await client.callTool('run', {
|
|
164
175
|
prompt_file: promptFile,
|
|
165
176
|
workFolder: testDir,
|
|
177
|
+
model: 'haiku',
|
|
166
178
|
});
|
|
167
179
|
const runData = parseToolJson(runResponse);
|
|
168
180
|
expect(runData).toEqual({
|
|
@@ -171,6 +183,124 @@ describe('MCP Contract Tests', () => {
|
|
|
171
183
|
agent: 'claude',
|
|
172
184
|
message: expect.any(String),
|
|
173
185
|
});
|
|
186
|
+
const waitResponse = await client.callTool('wait', { pids: [runData.pid], timeout: 5 });
|
|
187
|
+
const waitData = parseToolJson(waitResponse);
|
|
188
|
+
expect(waitData).toHaveLength(1);
|
|
189
|
+
expect(waitData[0]).toMatchObject({
|
|
190
|
+
pid: runData.pid,
|
|
191
|
+
agent: 'claude',
|
|
192
|
+
status: 'completed',
|
|
193
|
+
exitCode: 0,
|
|
194
|
+
model: 'haiku',
|
|
195
|
+
stdout: expect.stringContaining('Created file successfully'),
|
|
196
|
+
stderr: '',
|
|
197
|
+
});
|
|
198
|
+
expect(waitData[0]).not.toHaveProperty('prompt');
|
|
199
|
+
expect(waitData[0]).not.toHaveProperty('workFolder');
|
|
200
|
+
expect(waitData[0]).not.toHaveProperty('startTime');
|
|
201
|
+
});
|
|
202
|
+
it('returns compact results by default and full results when verbose is true for parsed output', async () => {
|
|
203
|
+
await client.disconnect();
|
|
204
|
+
const verboseMockPath = join(testDir, 'verbose-claude');
|
|
205
|
+
writeFileSync(verboseMockPath, `#!/bin/bash
|
|
206
|
+
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/demo.txt"}}]}}'
|
|
207
|
+
printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":[{"type":"text","text":"demo output"}]}]}}'
|
|
208
|
+
printf '%s\n' '{"type":"result","result":"Completed contract verbose test"}'
|
|
209
|
+
printf '%s\n' '{"type":"system","session_id":"session-verbose-1"}'
|
|
210
|
+
`);
|
|
211
|
+
chmodSync(verboseMockPath, 0o755);
|
|
212
|
+
client = createTestClient({ claudeCliName: verboseMockPath, debug: false });
|
|
213
|
+
await client.connect();
|
|
214
|
+
const runResponse = await client.callTool('run', {
|
|
215
|
+
prompt: 'verbose-shape-test',
|
|
216
|
+
workFolder: testDir,
|
|
217
|
+
});
|
|
218
|
+
const runData = parseToolJson(runResponse);
|
|
219
|
+
const completedWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5 }));
|
|
220
|
+
expect(completedWait).toHaveLength(1);
|
|
221
|
+
expect(completedWait[0].status).toBe('completed');
|
|
222
|
+
const compactResult = parseToolJson(await client.callTool('get_result', { pid: runData.pid }));
|
|
223
|
+
expect(compactResult).toMatchObject({
|
|
224
|
+
pid: runData.pid,
|
|
225
|
+
agent: 'claude',
|
|
226
|
+
status: 'completed',
|
|
227
|
+
exitCode: 0,
|
|
228
|
+
model: null,
|
|
229
|
+
session_id: 'session-verbose-1',
|
|
230
|
+
agentOutput: {
|
|
231
|
+
message: 'Completed contract verbose test',
|
|
232
|
+
session_id: 'session-verbose-1',
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
expect(compactResult).not.toHaveProperty('startTime');
|
|
236
|
+
expect(compactResult).not.toHaveProperty('workFolder');
|
|
237
|
+
expect(compactResult).not.toHaveProperty('prompt');
|
|
238
|
+
expect(compactResult.agentOutput).not.toHaveProperty('tools');
|
|
239
|
+
const verboseResult = parseToolJson(await client.callTool('get_result', { pid: runData.pid, verbose: true }));
|
|
240
|
+
expect(verboseResult).toMatchObject({
|
|
241
|
+
pid: runData.pid,
|
|
242
|
+
agent: 'claude',
|
|
243
|
+
status: 'completed',
|
|
244
|
+
exitCode: 0,
|
|
245
|
+
model: null,
|
|
246
|
+
startTime: expect.any(String),
|
|
247
|
+
workFolder: testDir,
|
|
248
|
+
prompt: 'verbose-shape-test',
|
|
249
|
+
session_id: 'session-verbose-1',
|
|
250
|
+
agentOutput: {
|
|
251
|
+
message: 'Completed contract verbose test',
|
|
252
|
+
session_id: 'session-verbose-1',
|
|
253
|
+
tools: [
|
|
254
|
+
{
|
|
255
|
+
tool: 'Read',
|
|
256
|
+
input: { file_path: '/tmp/demo.txt' },
|
|
257
|
+
output: 'demo output',
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
const compactWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5 }));
|
|
263
|
+
expect(compactWait).toHaveLength(1);
|
|
264
|
+
expect(compactWait[0]).toMatchObject({
|
|
265
|
+
pid: runData.pid,
|
|
266
|
+
agent: 'claude',
|
|
267
|
+
status: 'completed',
|
|
268
|
+
exitCode: 0,
|
|
269
|
+
model: null,
|
|
270
|
+
session_id: 'session-verbose-1',
|
|
271
|
+
agentOutput: {
|
|
272
|
+
message: 'Completed contract verbose test',
|
|
273
|
+
session_id: 'session-verbose-1',
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
expect(compactWait[0]).not.toHaveProperty('startTime');
|
|
277
|
+
expect(compactWait[0]).not.toHaveProperty('workFolder');
|
|
278
|
+
expect(compactWait[0]).not.toHaveProperty('prompt');
|
|
279
|
+
expect(compactWait[0].agentOutput).not.toHaveProperty('tools');
|
|
280
|
+
const verboseWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5, verbose: true }));
|
|
281
|
+
expect(verboseWait).toHaveLength(1);
|
|
282
|
+
expect(verboseWait[0]).toMatchObject({
|
|
283
|
+
pid: runData.pid,
|
|
284
|
+
agent: 'claude',
|
|
285
|
+
status: 'completed',
|
|
286
|
+
exitCode: 0,
|
|
287
|
+
model: null,
|
|
288
|
+
startTime: expect.any(String),
|
|
289
|
+
workFolder: testDir,
|
|
290
|
+
prompt: 'verbose-shape-test',
|
|
291
|
+
session_id: 'session-verbose-1',
|
|
292
|
+
agentOutput: {
|
|
293
|
+
message: 'Completed contract verbose test',
|
|
294
|
+
session_id: 'session-verbose-1',
|
|
295
|
+
tools: [
|
|
296
|
+
{
|
|
297
|
+
tool: 'Read',
|
|
298
|
+
input: { file_path: '/tmp/demo.txt' },
|
|
299
|
+
output: 'demo output',
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
});
|
|
174
304
|
});
|
|
175
305
|
it('covers forge end-to-end through the MCP process path', async () => {
|
|
176
306
|
await client.disconnect();
|
package/dist/app/cli.js
CHANGED
|
@@ -38,17 +38,19 @@ Compatibility aliases:
|
|
|
38
38
|
export const WAIT_HELP_TEXT = `Usage: ai-cli wait <pid...> [options]
|
|
39
39
|
|
|
40
40
|
Wait for one or more tracked processes to finish.
|
|
41
|
+
By default each result uses the compact shape; set --verbose to include full metadata and detailed parsed output.
|
|
41
42
|
|
|
42
43
|
Options:
|
|
43
44
|
--timeout <seconds> Maximum wait time in seconds
|
|
45
|
+
--verbose Return full metadata and detailed parsed output
|
|
44
46
|
--help, -h Show this help message
|
|
45
47
|
`;
|
|
46
48
|
export const RESULT_HELP_TEXT = `Usage: ai-cli result <pid> [options]
|
|
47
49
|
|
|
48
|
-
Get the current
|
|
50
|
+
Get the current output and status of a tracked process. By default this returns a compact result shape; set --verbose to include full metadata and detailed parsed output.
|
|
49
51
|
|
|
50
52
|
Options:
|
|
51
|
-
--verbose
|
|
53
|
+
--verbose Return full metadata and detailed parsed output
|
|
52
54
|
--help, -h Show this help message
|
|
53
55
|
`;
|
|
54
56
|
export const KILL_HELP_TEXT = `Usage: ai-cli kill <pid>
|
|
@@ -104,7 +106,7 @@ const defaultDeps = {
|
|
|
104
106
|
runProcess: (options) => getCliProcessService().startProcess(options),
|
|
105
107
|
listProcesses: () => getCliProcessService().listProcesses(),
|
|
106
108
|
getProcessResult: (pid, verbose) => getCliProcessService().getProcessResult(pid, verbose),
|
|
107
|
-
waitForProcesses: (pids, timeoutSeconds) => getCliProcessService().waitForProcesses(pids, timeoutSeconds),
|
|
109
|
+
waitForProcesses: (pids, timeoutSeconds, verbose) => getCliProcessService().waitForProcesses(pids, timeoutSeconds, verbose),
|
|
108
110
|
killProcess: (pid) => getCliProcessService().killProcess(pid),
|
|
109
111
|
cleanupProcesses: () => getCliProcessService().cleanupProcesses(),
|
|
110
112
|
getDoctorStatus: () => getCliDoctorStatus(),
|
|
@@ -253,7 +255,7 @@ export async function runCli(argv, deps = {}) {
|
|
|
253
255
|
stdout(CLI_HELP_TEXT);
|
|
254
256
|
return 1;
|
|
255
257
|
}
|
|
256
|
-
writeJson(stdout, await waitForProcesses(pids, timeout));
|
|
258
|
+
writeJson(stdout, await waitForProcesses(pids, timeout, 'verbose' in flags));
|
|
257
259
|
return 0;
|
|
258
260
|
}
|
|
259
261
|
if (command === 'kill') {
|