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 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].pid).toBe(runResult.pid);
76
- expect(waitResult[0].status).toBe('completed');
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.pid).toBe(runResult.pid);
85
- expect(result.status).toBe('completed');
86
- expect(result.stdout).toContain('Command executed successfully');
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].pid).toBe(runData.pid);
149
- expect(waitData[0].agent).toBe('claude');
150
- expect(waitData[0].status).toBe('completed');
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('accepts prompt_file and keeps the run response shape stable', async () => {
171
+ it('preserves successful prompt_file execution through the MCP process path', async () => {
161
172
  const promptFile = join(testDir, 'prompt.txt');
162
- writeFileSync(promptFile, 'create a file called from-file.txt');
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();
@@ -154,7 +154,8 @@ describe('Process Management Tests', () => {
154
154
  params: {
155
155
  name: 'get_result',
156
156
  arguments: {
157
- pid: 12360
157
+ pid: 12360,
158
+ verbose: true
158
159
  }
159
160
  }
160
161
  });
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 result for a tracked process.
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 Include verbose parsed output
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') {