ai-cli-mcp 2.13.0 → 2.14.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/CHANGELOG.md +7 -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 +134 -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 +7 -21
- 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 +144 -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 +7 -21
- package/src/process-result.ts +79 -0
- package/src/process-service.ts +7 -22
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function compactAgentOutput(agentOutput) {
|
|
2
|
+
if (!agentOutput || typeof agentOutput !== 'object') {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const { tools: _tools, ...rest } = agentOutput;
|
|
6
|
+
const compact = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined && value !== null));
|
|
7
|
+
return Object.keys(compact).length > 0 ? compact : null;
|
|
8
|
+
}
|
|
9
|
+
function hasMeaningfulParsedOutput(agentOutput) {
|
|
10
|
+
if (!agentOutput || typeof agentOutput !== 'object') {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return Object.entries(agentOutput).some(([key, value]) => {
|
|
14
|
+
if (value === undefined || value === null) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (key === 'session_id') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (key === 'tools') {
|
|
21
|
+
return Array.isArray(value) ? value.length > 0 : true;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function buildProcessResult(context, agentOutput, verbose = false) {
|
|
27
|
+
const response = {
|
|
28
|
+
pid: context.pid,
|
|
29
|
+
agent: context.agent,
|
|
30
|
+
status: context.status,
|
|
31
|
+
exitCode: context.exitCode ?? null,
|
|
32
|
+
model: context.model ?? null,
|
|
33
|
+
};
|
|
34
|
+
if (verbose) {
|
|
35
|
+
response.startTime = context.startTime;
|
|
36
|
+
response.workFolder = context.workFolder;
|
|
37
|
+
response.prompt = context.prompt;
|
|
38
|
+
}
|
|
39
|
+
if (agentOutput?.session_id) {
|
|
40
|
+
response.session_id = agentOutput.session_id;
|
|
41
|
+
}
|
|
42
|
+
const shapedAgentOutput = verbose ? agentOutput : compactAgentOutput(agentOutput);
|
|
43
|
+
if (hasMeaningfulParsedOutput(shapedAgentOutput)) {
|
|
44
|
+
response.agentOutput = shapedAgentOutput;
|
|
45
|
+
}
|
|
46
|
+
if (!response.agentOutput) {
|
|
47
|
+
response.stdout = context.stdout;
|
|
48
|
+
response.stderr = context.stderr;
|
|
49
|
+
}
|
|
50
|
+
return response;
|
|
51
|
+
}
|
package/dist/process-service.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { buildCliCommand } from './cli-builder.js';
|
|
3
3
|
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
|
|
4
|
+
import { buildProcessResult } from './process-result.js';
|
|
4
5
|
export class ProcessService {
|
|
5
6
|
processManager = new Map();
|
|
6
7
|
cliPaths;
|
|
@@ -100,7 +101,7 @@ export class ProcessService {
|
|
|
100
101
|
agentOutput = parseForgeOutput(process.stdout);
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
+
return buildProcessResult({
|
|
104
105
|
pid,
|
|
105
106
|
agent: process.toolType,
|
|
106
107
|
status: process.status,
|
|
@@ -109,26 +110,11 @@ export class ProcessService {
|
|
|
109
110
|
workFolder: process.workFolder,
|
|
110
111
|
prompt: process.prompt,
|
|
111
112
|
model: process.model,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const { tools, ...rest } = agentOutput;
|
|
116
|
-
response.agentOutput = rest;
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
response.agentOutput = agentOutput;
|
|
120
|
-
}
|
|
121
|
-
if (agentOutput.session_id) {
|
|
122
|
-
response.session_id = agentOutput.session_id;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
response.stdout = process.stdout;
|
|
127
|
-
response.stderr = process.stderr;
|
|
128
|
-
}
|
|
129
|
-
return response;
|
|
113
|
+
stdout: process.stdout,
|
|
114
|
+
stderr: process.stderr,
|
|
115
|
+
}, agentOutput, verbose);
|
|
130
116
|
}
|
|
131
|
-
async waitForProcesses(pids, timeoutSeconds = 180) {
|
|
117
|
+
async waitForProcesses(pids, timeoutSeconds = 180, verbose = false) {
|
|
132
118
|
for (const pid of pids) {
|
|
133
119
|
if (!this.processManager.has(pid)) {
|
|
134
120
|
throw new Error(`Process with PID ${pid} not found`);
|
|
@@ -155,7 +141,7 @@ export class ProcessService {
|
|
|
155
141
|
});
|
|
156
142
|
try {
|
|
157
143
|
await Promise.race([Promise.all(waitPromises), timeoutPromise]);
|
|
158
|
-
return pids.map((pid) => this.getProcessResult(pid,
|
|
144
|
+
return pids.map((pid) => this.getProcessResult(pid, verbose));
|
|
159
145
|
}
|
|
160
146
|
finally {
|
|
161
147
|
if (timeoutHandle) {
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CLI_HELP_TEXT,
|
|
4
4
|
DOCTOR_HELP_TEXT,
|
|
5
5
|
MODELS_HELP_TEXT,
|
|
6
|
+
RESULT_HELP_TEXT,
|
|
6
7
|
RUN_HELP_TEXT,
|
|
7
8
|
WAIT_HELP_TEXT,
|
|
8
9
|
runCli,
|
|
@@ -142,10 +143,28 @@ describe('ai-cli app', () => {
|
|
|
142
143
|
);
|
|
143
144
|
|
|
144
145
|
expect(exitCode).toBe(0);
|
|
145
|
-
expect(waitForProcesses).toHaveBeenCalledWith([123, 456], 5);
|
|
146
|
+
expect(waitForProcesses).toHaveBeenCalledWith([123, 456], 5, false);
|
|
146
147
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"status": "completed"'));
|
|
147
148
|
});
|
|
148
149
|
|
|
150
|
+
it('passes verbose through to wait', async () => {
|
|
151
|
+
const stdout = vi.fn();
|
|
152
|
+
const stderr = vi.fn();
|
|
153
|
+
const waitForProcesses = vi.fn().mockResolvedValue([{ pid: 123, status: 'completed' }]);
|
|
154
|
+
|
|
155
|
+
const exitCode = await runCli(
|
|
156
|
+
['wait', '123', '--verbose'],
|
|
157
|
+
{
|
|
158
|
+
stdout,
|
|
159
|
+
stderr,
|
|
160
|
+
waitForProcesses,
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(exitCode).toBe(0);
|
|
165
|
+
expect(waitForProcesses).toHaveBeenCalledWith([123], undefined, true);
|
|
166
|
+
});
|
|
167
|
+
|
|
149
168
|
it('rejects invalid wait timeout values', async () => {
|
|
150
169
|
const stdout = vi.fn();
|
|
151
170
|
const stderr = vi.fn();
|
|
@@ -291,6 +310,19 @@ describe('ai-cli app', () => {
|
|
|
291
310
|
expect(stderr).not.toHaveBeenCalled();
|
|
292
311
|
});
|
|
293
312
|
|
|
313
|
+
it('prints detailed help for result --help', async () => {
|
|
314
|
+
const stdout = vi.fn();
|
|
315
|
+
const stderr = vi.fn();
|
|
316
|
+
|
|
317
|
+
const exitCode = await runCli(['result', '--help'], { stdout, stderr });
|
|
318
|
+
|
|
319
|
+
expect(exitCode).toBe(0);
|
|
320
|
+
expect(stdout).toHaveBeenCalledWith(RESULT_HELP_TEXT);
|
|
321
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('compact result shape'));
|
|
322
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('--verbose'));
|
|
323
|
+
expect(stderr).not.toHaveBeenCalled();
|
|
324
|
+
});
|
|
325
|
+
|
|
294
326
|
it('prints detailed help for wait --help', async () => {
|
|
295
327
|
const stdout = vi.fn();
|
|
296
328
|
const stderr = vi.fn();
|
|
@@ -299,6 +331,8 @@ describe('ai-cli app', () => {
|
|
|
299
331
|
|
|
300
332
|
expect(exitCode).toBe(0);
|
|
301
333
|
expect(stdout).toHaveBeenCalledWith(WAIT_HELP_TEXT);
|
|
334
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('compact shape'));
|
|
335
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('--verbose'));
|
|
302
336
|
expect(stderr).not.toHaveBeenCalled();
|
|
303
337
|
});
|
|
304
338
|
|
|
@@ -84,8 +84,18 @@ describe('CliProcessService', () => {
|
|
|
84
84
|
|
|
85
85
|
const waitResult = await service.waitForProcesses([runResult.pid], 5);
|
|
86
86
|
expect(waitResult).toHaveLength(1);
|
|
87
|
-
expect(waitResult[0]
|
|
88
|
-
|
|
87
|
+
expect(waitResult[0]).toMatchObject({
|
|
88
|
+
pid: runResult.pid,
|
|
89
|
+
agent: 'claude',
|
|
90
|
+
status: 'completed',
|
|
91
|
+
exitCode: null,
|
|
92
|
+
model: 'sonnet',
|
|
93
|
+
stdout: expect.any(String),
|
|
94
|
+
stderr: expect.any(String),
|
|
95
|
+
});
|
|
96
|
+
expect(waitResult[0]).not.toHaveProperty('startTime');
|
|
97
|
+
expect(waitResult[0]).not.toHaveProperty('workFolder');
|
|
98
|
+
expect(waitResult[0]).not.toHaveProperty('prompt');
|
|
89
99
|
|
|
90
100
|
const listed = await service.listProcesses();
|
|
91
101
|
expect(listed).toContainEqual({
|
|
@@ -95,12 +105,141 @@ describe('CliProcessService', () => {
|
|
|
95
105
|
});
|
|
96
106
|
|
|
97
107
|
const result = await service.getProcessResult(runResult.pid, false);
|
|
98
|
-
expect(result
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
expect(result).toMatchObject({
|
|
109
|
+
pid: runResult.pid,
|
|
110
|
+
agent: 'claude',
|
|
111
|
+
status: 'completed',
|
|
112
|
+
exitCode: null,
|
|
113
|
+
model: 'sonnet',
|
|
114
|
+
stdout: expect.stringContaining('Command executed successfully'),
|
|
115
|
+
stderr: expect.any(String),
|
|
116
|
+
});
|
|
117
|
+
expect(result).not.toHaveProperty('startTime');
|
|
118
|
+
expect(result).not.toHaveProperty('workFolder');
|
|
119
|
+
expect(result).not.toHaveProperty('prompt');
|
|
101
120
|
expect(readFileSync(join(processDir, 'meta.json'), 'utf-8')).toContain('"status": "completed"');
|
|
102
121
|
});
|
|
103
122
|
|
|
123
|
+
it('returns compact results by default and full results when verbose is true', async () => {
|
|
124
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
125
|
+
tempDirs.push(root);
|
|
126
|
+
const scriptPath = join(root, 'mock-claude-json');
|
|
127
|
+
writeFileSync(
|
|
128
|
+
scriptPath,
|
|
129
|
+
`#!/bin/bash
|
|
130
|
+
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/demo.txt"}}]}}'
|
|
131
|
+
printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":[{"type":"text","text":"demo output"}]}]}}'
|
|
132
|
+
printf '%s\n' '{"type":"result","result":"Completed cli-process-service test"}'
|
|
133
|
+
printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
|
|
134
|
+
`
|
|
135
|
+
);
|
|
136
|
+
chmodSync(scriptPath, 0o755);
|
|
137
|
+
const stateDir = join(root, 'state');
|
|
138
|
+
const workFolder = join(root, 'work');
|
|
139
|
+
mkdirSync(workFolder, { recursive: true });
|
|
140
|
+
|
|
141
|
+
const service = new CliProcessService({
|
|
142
|
+
stateDir,
|
|
143
|
+
cliPaths: {
|
|
144
|
+
claude: scriptPath,
|
|
145
|
+
codex: scriptPath,
|
|
146
|
+
gemini: scriptPath,
|
|
147
|
+
forge: scriptPath,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const runResult = await service.startProcess({
|
|
152
|
+
prompt: 'hello structured output',
|
|
153
|
+
cwd: workFolder,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const compactWait = await service.waitForProcesses([runResult.pid], 5);
|
|
157
|
+
expect(compactWait).toHaveLength(1);
|
|
158
|
+
expect(compactWait[0]).toMatchObject({
|
|
159
|
+
pid: runResult.pid,
|
|
160
|
+
agent: 'claude',
|
|
161
|
+
status: 'completed',
|
|
162
|
+
exitCode: null,
|
|
163
|
+
model: null,
|
|
164
|
+
session_id: 'session-cli-1',
|
|
165
|
+
agentOutput: {
|
|
166
|
+
message: 'Completed cli-process-service test',
|
|
167
|
+
session_id: 'session-cli-1',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
expect(compactWait[0]).not.toHaveProperty('startTime');
|
|
171
|
+
expect(compactWait[0]).not.toHaveProperty('workFolder');
|
|
172
|
+
expect(compactWait[0]).not.toHaveProperty('prompt');
|
|
173
|
+
expect(compactWait[0].agentOutput).not.toHaveProperty('tools');
|
|
174
|
+
|
|
175
|
+
const compactResult = await service.getProcessResult(runResult.pid, false);
|
|
176
|
+
expect(compactResult).toMatchObject({
|
|
177
|
+
pid: runResult.pid,
|
|
178
|
+
agent: 'claude',
|
|
179
|
+
status: 'completed',
|
|
180
|
+
exitCode: null,
|
|
181
|
+
model: null,
|
|
182
|
+
session_id: 'session-cli-1',
|
|
183
|
+
agentOutput: {
|
|
184
|
+
message: 'Completed cli-process-service test',
|
|
185
|
+
session_id: 'session-cli-1',
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
expect(compactResult).not.toHaveProperty('startTime');
|
|
189
|
+
expect(compactResult).not.toHaveProperty('workFolder');
|
|
190
|
+
expect(compactResult).not.toHaveProperty('prompt');
|
|
191
|
+
expect(compactResult.agentOutput).not.toHaveProperty('tools');
|
|
192
|
+
|
|
193
|
+
const verboseWait = await service.waitForProcesses([runResult.pid], 5, true);
|
|
194
|
+
expect(verboseWait).toHaveLength(1);
|
|
195
|
+
expect(verboseWait[0]).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
|
+
|
|
218
|
+
const verboseResult = await service.getProcessResult(runResult.pid, true);
|
|
219
|
+
expect(verboseResult).toMatchObject({
|
|
220
|
+
pid: runResult.pid,
|
|
221
|
+
agent: 'claude',
|
|
222
|
+
status: 'completed',
|
|
223
|
+
exitCode: null,
|
|
224
|
+
model: null,
|
|
225
|
+
startTime: expect.any(String),
|
|
226
|
+
workFolder,
|
|
227
|
+
prompt: 'hello structured output',
|
|
228
|
+
session_id: 'session-cli-1',
|
|
229
|
+
agentOutput: {
|
|
230
|
+
message: 'Completed cli-process-service test',
|
|
231
|
+
session_id: 'session-cli-1',
|
|
232
|
+
tools: [
|
|
233
|
+
{
|
|
234
|
+
tool: 'Read',
|
|
235
|
+
input: { file_path: '/tmp/demo.txt' },
|
|
236
|
+
output: 'demo output',
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
104
243
|
it('can terminate a tracked process', async () => {
|
|
105
244
|
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
106
245
|
tempDirs.push(root);
|
|
@@ -122,6 +122,7 @@ describe('MCP Contract Tests', () => {
|
|
|
122
122
|
expect(Object.keys(waitTool.inputSchema.properties).sort()).toEqual([
|
|
123
123
|
'pids',
|
|
124
124
|
'timeout',
|
|
125
|
+
'verbose',
|
|
125
126
|
]);
|
|
126
127
|
});
|
|
127
128
|
|
|
@@ -155,22 +156,32 @@ describe('MCP Contract Tests', () => {
|
|
|
155
156
|
pid: runData.pid,
|
|
156
157
|
agent: 'claude',
|
|
157
158
|
status: expect.any(String),
|
|
158
|
-
startTime: expect.any(String),
|
|
159
|
-
workFolder: testDir,
|
|
160
|
-
prompt: 'create a file called contract.txt with content "hello"',
|
|
161
159
|
model: 'haiku',
|
|
162
160
|
stdout: expect.any(String),
|
|
163
161
|
stderr: expect.any(String),
|
|
164
162
|
});
|
|
163
|
+
expect(getResultData).toHaveProperty('exitCode');
|
|
164
|
+
expect(getResultData).not.toHaveProperty('startTime');
|
|
165
|
+
expect(getResultData).not.toHaveProperty('workFolder');
|
|
166
|
+
expect(getResultData).not.toHaveProperty('prompt');
|
|
165
167
|
|
|
166
168
|
const waitResponse = await client.callTool('wait', { pids: [runData.pid], timeout: 5 });
|
|
167
169
|
const waitData = parseToolJson(waitResponse);
|
|
168
170
|
|
|
169
171
|
expect(Array.isArray(waitData)).toBe(true);
|
|
170
172
|
expect(waitData).toHaveLength(1);
|
|
171
|
-
expect(waitData[0]
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
expect(waitData[0]).toMatchObject({
|
|
174
|
+
pid: runData.pid,
|
|
175
|
+
agent: 'claude',
|
|
176
|
+
status: 'completed',
|
|
177
|
+
exitCode: 0,
|
|
178
|
+
model: 'haiku',
|
|
179
|
+
stdout: expect.any(String),
|
|
180
|
+
stderr: expect.any(String),
|
|
181
|
+
});
|
|
182
|
+
expect(waitData[0]).not.toHaveProperty('startTime');
|
|
183
|
+
expect(waitData[0]).not.toHaveProperty('workFolder');
|
|
184
|
+
expect(waitData[0]).not.toHaveProperty('prompt');
|
|
174
185
|
|
|
175
186
|
const cleanupResponse = await client.callTool('cleanup_processes', {});
|
|
176
187
|
const cleanupData = parseToolJson(cleanupResponse);
|
|
@@ -183,13 +194,14 @@ describe('MCP Contract Tests', () => {
|
|
|
183
194
|
expect(cleanupData.removedPids).toContain(runData.pid);
|
|
184
195
|
});
|
|
185
196
|
|
|
186
|
-
it('
|
|
197
|
+
it('preserves successful prompt_file execution through the MCP process path', async () => {
|
|
187
198
|
const promptFile = join(testDir, 'prompt.txt');
|
|
188
|
-
writeFileSync(promptFile, '
|
|
199
|
+
writeFileSync(promptFile, 'Create a file from prompt_file');
|
|
189
200
|
|
|
190
201
|
const runResponse = await client.callTool('run', {
|
|
191
202
|
prompt_file: promptFile,
|
|
192
203
|
workFolder: testDir,
|
|
204
|
+
model: 'haiku',
|
|
193
205
|
});
|
|
194
206
|
const runData = parseToolJson(runResponse);
|
|
195
207
|
|
|
@@ -199,6 +211,138 @@ describe('MCP Contract Tests', () => {
|
|
|
199
211
|
agent: 'claude',
|
|
200
212
|
message: expect.any(String),
|
|
201
213
|
});
|
|
214
|
+
|
|
215
|
+
const waitResponse = await client.callTool('wait', { pids: [runData.pid], timeout: 5 });
|
|
216
|
+
const waitData = parseToolJson(waitResponse);
|
|
217
|
+
|
|
218
|
+
expect(waitData).toHaveLength(1);
|
|
219
|
+
expect(waitData[0]).toMatchObject({
|
|
220
|
+
pid: runData.pid,
|
|
221
|
+
agent: 'claude',
|
|
222
|
+
status: 'completed',
|
|
223
|
+
exitCode: 0,
|
|
224
|
+
model: 'haiku',
|
|
225
|
+
stdout: expect.stringContaining('Created file successfully'),
|
|
226
|
+
stderr: '',
|
|
227
|
+
});
|
|
228
|
+
expect(waitData[0]).not.toHaveProperty('prompt');
|
|
229
|
+
expect(waitData[0]).not.toHaveProperty('workFolder');
|
|
230
|
+
expect(waitData[0]).not.toHaveProperty('startTime');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('returns compact results by default and full results when verbose is true for parsed output', async () => {
|
|
234
|
+
await client.disconnect();
|
|
235
|
+
|
|
236
|
+
const verboseMockPath = join(testDir, 'verbose-claude');
|
|
237
|
+
writeFileSync(
|
|
238
|
+
verboseMockPath,
|
|
239
|
+
`#!/bin/bash
|
|
240
|
+
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/demo.txt"}}]}}'
|
|
241
|
+
printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":[{"type":"text","text":"demo output"}]}]}}'
|
|
242
|
+
printf '%s\n' '{"type":"result","result":"Completed contract verbose test"}'
|
|
243
|
+
printf '%s\n' '{"type":"system","session_id":"session-verbose-1"}'
|
|
244
|
+
`
|
|
245
|
+
);
|
|
246
|
+
chmodSync(verboseMockPath, 0o755);
|
|
247
|
+
|
|
248
|
+
client = createTestClient({ claudeCliName: verboseMockPath, debug: false });
|
|
249
|
+
await client.connect();
|
|
250
|
+
|
|
251
|
+
const runResponse = await client.callTool('run', {
|
|
252
|
+
prompt: 'verbose-shape-test',
|
|
253
|
+
workFolder: testDir,
|
|
254
|
+
});
|
|
255
|
+
const runData = parseToolJson(runResponse);
|
|
256
|
+
|
|
257
|
+
const completedWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5 }));
|
|
258
|
+
expect(completedWait).toHaveLength(1);
|
|
259
|
+
expect(completedWait[0].status).toBe('completed');
|
|
260
|
+
|
|
261
|
+
const compactResult = parseToolJson(await client.callTool('get_result', { pid: runData.pid }));
|
|
262
|
+
expect(compactResult).toMatchObject({
|
|
263
|
+
pid: runData.pid,
|
|
264
|
+
agent: 'claude',
|
|
265
|
+
status: 'completed',
|
|
266
|
+
exitCode: 0,
|
|
267
|
+
model: null,
|
|
268
|
+
session_id: 'session-verbose-1',
|
|
269
|
+
agentOutput: {
|
|
270
|
+
message: 'Completed contract verbose test',
|
|
271
|
+
session_id: 'session-verbose-1',
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
expect(compactResult).not.toHaveProperty('startTime');
|
|
275
|
+
expect(compactResult).not.toHaveProperty('workFolder');
|
|
276
|
+
expect(compactResult).not.toHaveProperty('prompt');
|
|
277
|
+
expect(compactResult.agentOutput).not.toHaveProperty('tools');
|
|
278
|
+
|
|
279
|
+
const verboseResult = parseToolJson(await client.callTool('get_result', { pid: runData.pid, verbose: true }));
|
|
280
|
+
expect(verboseResult).toMatchObject({
|
|
281
|
+
pid: runData.pid,
|
|
282
|
+
agent: 'claude',
|
|
283
|
+
status: 'completed',
|
|
284
|
+
exitCode: 0,
|
|
285
|
+
model: null,
|
|
286
|
+
startTime: expect.any(String),
|
|
287
|
+
workFolder: testDir,
|
|
288
|
+
prompt: 'verbose-shape-test',
|
|
289
|
+
session_id: 'session-verbose-1',
|
|
290
|
+
agentOutput: {
|
|
291
|
+
message: 'Completed contract verbose test',
|
|
292
|
+
session_id: 'session-verbose-1',
|
|
293
|
+
tools: [
|
|
294
|
+
{
|
|
295
|
+
tool: 'Read',
|
|
296
|
+
input: { file_path: '/tmp/demo.txt' },
|
|
297
|
+
output: 'demo output',
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const compactWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5 }));
|
|
304
|
+
expect(compactWait).toHaveLength(1);
|
|
305
|
+
expect(compactWait[0]).toMatchObject({
|
|
306
|
+
pid: runData.pid,
|
|
307
|
+
agent: 'claude',
|
|
308
|
+
status: 'completed',
|
|
309
|
+
exitCode: 0,
|
|
310
|
+
model: null,
|
|
311
|
+
session_id: 'session-verbose-1',
|
|
312
|
+
agentOutput: {
|
|
313
|
+
message: 'Completed contract verbose test',
|
|
314
|
+
session_id: 'session-verbose-1',
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
expect(compactWait[0]).not.toHaveProperty('startTime');
|
|
318
|
+
expect(compactWait[0]).not.toHaveProperty('workFolder');
|
|
319
|
+
expect(compactWait[0]).not.toHaveProperty('prompt');
|
|
320
|
+
expect(compactWait[0].agentOutput).not.toHaveProperty('tools');
|
|
321
|
+
|
|
322
|
+
const verboseWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5, verbose: true }));
|
|
323
|
+
expect(verboseWait).toHaveLength(1);
|
|
324
|
+
expect(verboseWait[0]).toMatchObject({
|
|
325
|
+
pid: runData.pid,
|
|
326
|
+
agent: 'claude',
|
|
327
|
+
status: 'completed',
|
|
328
|
+
exitCode: 0,
|
|
329
|
+
model: null,
|
|
330
|
+
startTime: expect.any(String),
|
|
331
|
+
workFolder: testDir,
|
|
332
|
+
prompt: 'verbose-shape-test',
|
|
333
|
+
session_id: 'session-verbose-1',
|
|
334
|
+
agentOutput: {
|
|
335
|
+
message: 'Completed contract verbose test',
|
|
336
|
+
session_id: 'session-verbose-1',
|
|
337
|
+
tools: [
|
|
338
|
+
{
|
|
339
|
+
tool: 'Read',
|
|
340
|
+
input: { file_path: '/tmp/demo.txt' },
|
|
341
|
+
output: 'demo output',
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
});
|
|
202
346
|
});
|
|
203
347
|
|
|
204
348
|
it('covers forge end-to-end through the MCP process path', async () => {
|
package/src/app/cli.ts
CHANGED
|
@@ -41,18 +41,20 @@ Compatibility aliases:
|
|
|
41
41
|
export const WAIT_HELP_TEXT = `Usage: ai-cli wait <pid...> [options]
|
|
42
42
|
|
|
43
43
|
Wait for one or more tracked processes to finish.
|
|
44
|
+
By default each result uses the compact shape; set --verbose to include full metadata and detailed parsed output.
|
|
44
45
|
|
|
45
46
|
Options:
|
|
46
47
|
--timeout <seconds> Maximum wait time in seconds
|
|
48
|
+
--verbose Return full metadata and detailed parsed output
|
|
47
49
|
--help, -h Show this help message
|
|
48
50
|
`;
|
|
49
51
|
|
|
50
52
|
export const RESULT_HELP_TEXT = `Usage: ai-cli result <pid> [options]
|
|
51
53
|
|
|
52
|
-
Get the current
|
|
54
|
+
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.
|
|
53
55
|
|
|
54
56
|
Options:
|
|
55
|
-
--verbose
|
|
57
|
+
--verbose Return full metadata and detailed parsed output
|
|
56
58
|
--help, -h Show this help message
|
|
57
59
|
`;
|
|
58
60
|
|
|
@@ -115,7 +117,7 @@ interface CliDeps {
|
|
|
115
117
|
}) => Promise<any>;
|
|
116
118
|
listProcesses: () => Promise<any>;
|
|
117
119
|
getProcessResult: (pid: number, verbose: boolean) => Promise<any>;
|
|
118
|
-
waitForProcesses: (pids: number[], timeoutSeconds?: number) => Promise<any>;
|
|
120
|
+
waitForProcesses: (pids: number[], timeoutSeconds?: number, verbose?: boolean) => Promise<any>;
|
|
119
121
|
killProcess: (pid: number) => Promise<any>;
|
|
120
122
|
cleanupProcesses: () => Promise<any>;
|
|
121
123
|
getDoctorStatus: () => any;
|
|
@@ -137,7 +139,7 @@ const defaultDeps: CliDeps = {
|
|
|
137
139
|
runProcess: (options) => getCliProcessService().startProcess(options),
|
|
138
140
|
listProcesses: () => getCliProcessService().listProcesses(),
|
|
139
141
|
getProcessResult: (pid, verbose) => getCliProcessService().getProcessResult(pid, verbose),
|
|
140
|
-
waitForProcesses: (pids, timeoutSeconds) => getCliProcessService().waitForProcesses(pids, timeoutSeconds),
|
|
142
|
+
waitForProcesses: (pids, timeoutSeconds, verbose) => getCliProcessService().waitForProcesses(pids, timeoutSeconds, verbose),
|
|
141
143
|
killProcess: (pid) => getCliProcessService().killProcess(pid),
|
|
142
144
|
cleanupProcesses: () => getCliProcessService().cleanupProcesses(),
|
|
143
145
|
getDoctorStatus: () => getCliDoctorStatus(),
|
|
@@ -317,7 +319,7 @@ export async function runCli(argv: string[], deps: Partial<CliDeps> = {}): Promi
|
|
|
317
319
|
return 1;
|
|
318
320
|
}
|
|
319
321
|
|
|
320
|
-
writeJson(stdout, await waitForProcesses(pids as number[], timeout));
|
|
322
|
+
writeJson(stdout, await waitForProcesses(pids as number[], timeout, 'verbose' in flags));
|
|
321
323
|
return 0;
|
|
322
324
|
}
|
|
323
325
|
|
package/src/app/mcp.ts
CHANGED
|
@@ -187,7 +187,7 @@ ${getSupportedModelsDescription()}
|
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
name: 'get_result',
|
|
190
|
-
description: 'Get the current output and status of an AI agent process by PID.
|
|
190
|
+
description: 'Get the current output and status of an AI agent process by PID. Defaults to a compact result shape; set verbose to true for full metadata and detailed parsed output.',
|
|
191
191
|
inputSchema: {
|
|
192
192
|
type: 'object',
|
|
193
193
|
properties: {
|
|
@@ -197,7 +197,7 @@ ${getSupportedModelsDescription()}
|
|
|
197
197
|
},
|
|
198
198
|
verbose: {
|
|
199
199
|
type: 'boolean',
|
|
200
|
-
description: 'Optional: If true, returns
|
|
200
|
+
description: 'Optional: If true, returns the full result shape including metadata fields and detailed parsed output such as tool usage history. Defaults to false.',
|
|
201
201
|
}
|
|
202
202
|
},
|
|
203
203
|
required: ['pid'],
|
|
@@ -205,7 +205,7 @@ ${getSupportedModelsDescription()}
|
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
207
|
name: 'wait',
|
|
208
|
-
description: 'Wait for multiple AI agent processes to complete and return their results.
|
|
208
|
+
description: 'Wait for multiple AI agent processes to complete and return their results. Defaults to compact result items; set verbose to true for full metadata and detailed parsed output.',
|
|
209
209
|
inputSchema: {
|
|
210
210
|
type: 'object',
|
|
211
211
|
properties: {
|
|
@@ -218,6 +218,10 @@ ${getSupportedModelsDescription()}
|
|
|
218
218
|
type: 'number',
|
|
219
219
|
description: 'Optional: Maximum time to wait in seconds. Defaults to 180 (3 minutes).',
|
|
220
220
|
},
|
|
221
|
+
verbose: {
|
|
222
|
+
type: 'boolean',
|
|
223
|
+
description: 'Optional: If true, each result item uses the full result shape including metadata fields and detailed parsed output. Defaults to false.',
|
|
224
|
+
},
|
|
221
225
|
},
|
|
222
226
|
required: ['pids'],
|
|
223
227
|
},
|
|
@@ -336,7 +340,8 @@ ${getSupportedModelsDescription()}
|
|
|
336
340
|
try {
|
|
337
341
|
const results = await this.processService.waitForProcesses(
|
|
338
342
|
toolArguments.pids,
|
|
339
|
-
typeof toolArguments.timeout === 'number' ? toolArguments.timeout : 180
|
|
343
|
+
typeof toolArguments.timeout === 'number' ? toolArguments.timeout : 180,
|
|
344
|
+
!!toolArguments.verbose
|
|
340
345
|
);
|
|
341
346
|
return {
|
|
342
347
|
content: [{
|