ai-cli-mcp 2.11.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.github/workflows/publish.yml +25 -0
  2. package/CHANGELOG.md +23 -0
  3. package/README.ja.md +112 -8
  4. package/README.md +112 -9
  5. package/dist/__tests__/app-cli.test.js +293 -0
  6. package/dist/__tests__/cli-bin-smoke.test.js +58 -0
  7. package/dist/__tests__/cli-builder.test.js +37 -0
  8. package/dist/__tests__/cli-process-service.test.js +279 -0
  9. package/dist/__tests__/cli-utils.test.js +140 -0
  10. package/dist/__tests__/error-cases.test.js +2 -1
  11. package/dist/__tests__/mcp-contract.test.js +343 -0
  12. package/dist/__tests__/parsers.test.js +37 -1
  13. package/dist/__tests__/process-management.test.js +15 -8
  14. package/dist/__tests__/server.test.js +29 -3
  15. package/dist/__tests__/wait.test.js +31 -0
  16. package/dist/app/cli.js +304 -0
  17. package/dist/app/mcp.js +366 -0
  18. package/dist/bin/ai-cli-mcp.js +6 -0
  19. package/dist/bin/ai-cli.js +10 -0
  20. package/dist/cli-builder.js +15 -6
  21. package/dist/cli-parse.js +8 -5
  22. package/dist/cli-process-service.js +332 -0
  23. package/dist/cli-utils.js +159 -88
  24. package/dist/cli.js +4 -3
  25. package/dist/model-catalog.js +53 -0
  26. package/dist/parsers.js +55 -0
  27. package/dist/process-service.js +201 -0
  28. package/dist/server.js +4 -578
  29. package/docs/cli-architecture.md +275 -0
  30. package/package.json +4 -3
  31. package/server.json +1 -1
  32. package/src/__tests__/app-cli.test.ts +370 -0
  33. package/src/__tests__/cli-bin-smoke.test.ts +75 -0
  34. package/src/__tests__/cli-builder.test.ts +47 -0
  35. package/src/__tests__/cli-process-service.test.ts +334 -0
  36. package/src/__tests__/cli-utils.test.ts +166 -0
  37. package/src/__tests__/error-cases.test.ts +3 -4
  38. package/src/__tests__/mcp-contract.test.ts +422 -0
  39. package/src/__tests__/parsers.test.ts +44 -1
  40. package/src/__tests__/process-management.test.ts +15 -9
  41. package/src/__tests__/server.test.ts +27 -6
  42. package/src/__tests__/wait.test.ts +38 -0
  43. package/src/app/cli.ts +373 -0
  44. package/src/app/mcp.ts +402 -0
  45. package/src/bin/ai-cli-mcp.ts +7 -0
  46. package/src/bin/ai-cli.ts +11 -0
  47. package/src/cli-builder.ts +19 -10
  48. package/src/cli-parse.ts +8 -5
  49. package/src/cli-process-service.ts +418 -0
  50. package/src/cli-utils.ts +205 -99
  51. package/src/cli.ts +4 -3
  52. package/src/model-catalog.ts +64 -0
  53. package/src/parsers.ts +61 -0
  54. package/src/process-service.ts +263 -0
  55. package/src/server.ts +4 -668
package/dist/parsers.js CHANGED
@@ -163,3 +163,58 @@ export function parseGeminiOutput(stdout) {
163
163
  return null;
164
164
  }
165
165
  }
166
+ /**
167
+ * Parse Forge output framed by Initialize/Continue/Finished markers.
168
+ */
169
+ export function parseForgeOutput(stdout) {
170
+ if (!stdout)
171
+ return null;
172
+ const lines = stdout.split('\n');
173
+ const markerPattern = /^● \[[^\]]+\] (Initialize|Continue|Finished) (\S+)\s*$/;
174
+ let collecting = false;
175
+ let currentConversationId = null;
176
+ let currentBody = [];
177
+ let lastConversationId = null;
178
+ let lastMessage = null;
179
+ for (const line of lines) {
180
+ const match = line.match(markerPattern);
181
+ if (match) {
182
+ const [, action, conversationId] = match;
183
+ lastConversationId = conversationId;
184
+ if (action === 'Initialize' || action === 'Continue') {
185
+ collecting = true;
186
+ currentConversationId = conversationId;
187
+ currentBody = [];
188
+ }
189
+ else if (collecting && currentConversationId === conversationId) {
190
+ const message = currentBody.join('\n').trim();
191
+ if (message) {
192
+ lastMessage = message;
193
+ }
194
+ collecting = false;
195
+ currentConversationId = null;
196
+ currentBody = [];
197
+ }
198
+ continue;
199
+ }
200
+ if (collecting) {
201
+ currentBody.push(line);
202
+ }
203
+ }
204
+ if (collecting) {
205
+ const message = currentBody.join('\n').trim();
206
+ if (message) {
207
+ lastMessage = message;
208
+ }
209
+ if (currentConversationId) {
210
+ lastConversationId = currentConversationId;
211
+ }
212
+ }
213
+ if (!lastMessage && !lastConversationId) {
214
+ return null;
215
+ }
216
+ return {
217
+ message: lastMessage,
218
+ session_id: lastConversationId,
219
+ };
220
+ }
@@ -0,0 +1,201 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { buildCliCommand } from './cli-builder.js';
3
+ import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput } from './parsers.js';
4
+ export class ProcessService {
5
+ processManager = new Map();
6
+ cliPaths;
7
+ constructor(options) {
8
+ this.cliPaths = options.cliPaths;
9
+ }
10
+ startProcess(options) {
11
+ const cmd = buildCliCommand({
12
+ ...options,
13
+ cliPaths: this.cliPaths,
14
+ });
15
+ const { cliPath, args: processArgs, cwd: effectiveCwd, agent, prompt } = cmd;
16
+ const childProcess = spawn(cliPath, processArgs, {
17
+ cwd: effectiveCwd,
18
+ stdio: ['ignore', 'pipe', 'pipe'],
19
+ detached: false,
20
+ });
21
+ const pid = childProcess.pid;
22
+ if (!pid) {
23
+ throw new Error(`Failed to start ${agent} CLI process`);
24
+ }
25
+ const processEntry = {
26
+ pid,
27
+ process: childProcess,
28
+ prompt,
29
+ workFolder: effectiveCwd,
30
+ model: options.model,
31
+ toolType: agent,
32
+ startTime: new Date().toISOString(),
33
+ stdout: '',
34
+ stderr: '',
35
+ status: 'running',
36
+ };
37
+ this.processManager.set(pid, processEntry);
38
+ childProcess.stdout.on('data', (data) => {
39
+ const entry = this.processManager.get(pid);
40
+ if (entry) {
41
+ entry.stdout += data.toString();
42
+ }
43
+ });
44
+ childProcess.stderr.on('data', (data) => {
45
+ const entry = this.processManager.get(pid);
46
+ if (entry) {
47
+ entry.stderr += data.toString();
48
+ }
49
+ });
50
+ childProcess.on('close', (code) => {
51
+ const entry = this.processManager.get(pid);
52
+ if (entry) {
53
+ entry.status = code === 0 ? 'completed' : 'failed';
54
+ entry.exitCode = code !== null ? code : undefined;
55
+ }
56
+ });
57
+ childProcess.on('error', (error) => {
58
+ const entry = this.processManager.get(pid);
59
+ if (entry) {
60
+ entry.status = 'failed';
61
+ entry.stderr += `\nProcess error: ${error.message}`;
62
+ }
63
+ });
64
+ return {
65
+ pid,
66
+ status: 'started',
67
+ agent,
68
+ message: `${agent} process started successfully`,
69
+ };
70
+ }
71
+ listProcesses() {
72
+ const processes = [];
73
+ for (const [pid, process] of this.processManager.entries()) {
74
+ processes.push({
75
+ pid,
76
+ agent: process.toolType,
77
+ status: process.status,
78
+ });
79
+ }
80
+ return processes;
81
+ }
82
+ getProcessResult(pid, verbose = false) {
83
+ const process = this.processManager.get(pid);
84
+ if (!process) {
85
+ throw new Error(`Process with PID ${pid} not found`);
86
+ }
87
+ let agentOutput = null;
88
+ if (process.toolType === 'codex') {
89
+ const combinedOutput = (process.stdout || '') + '\n' + (process.stderr || '');
90
+ agentOutput = parseCodexOutput(combinedOutput);
91
+ }
92
+ else if (process.stdout) {
93
+ if (process.toolType === 'claude') {
94
+ agentOutput = parseClaudeOutput(process.stdout);
95
+ }
96
+ else if (process.toolType === 'gemini') {
97
+ agentOutput = parseGeminiOutput(process.stdout);
98
+ }
99
+ else if (process.toolType === 'forge') {
100
+ agentOutput = parseForgeOutput(process.stdout);
101
+ }
102
+ }
103
+ const response = {
104
+ pid,
105
+ agent: process.toolType,
106
+ status: process.status,
107
+ exitCode: process.exitCode,
108
+ startTime: process.startTime,
109
+ workFolder: process.workFolder,
110
+ prompt: process.prompt,
111
+ model: process.model,
112
+ };
113
+ if (agentOutput) {
114
+ if (!verbose && agentOutput.tools) {
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;
130
+ }
131
+ async waitForProcesses(pids, timeoutSeconds = 180) {
132
+ for (const pid of pids) {
133
+ if (!this.processManager.has(pid)) {
134
+ throw new Error(`Process with PID ${pid} not found`);
135
+ }
136
+ }
137
+ const waitPromises = pids.map((pid) => {
138
+ const processEntry = this.processManager.get(pid);
139
+ if (processEntry.status !== 'running') {
140
+ return Promise.resolve();
141
+ }
142
+ return new Promise((resolve) => {
143
+ processEntry.process.once('close', () => {
144
+ resolve();
145
+ });
146
+ });
147
+ });
148
+ const timeoutMs = timeoutSeconds * 1000;
149
+ let timeoutHandle;
150
+ const timeoutPromise = new Promise((_, reject) => {
151
+ timeoutHandle = setTimeout(() => {
152
+ reject(new Error(`Timed out after ${timeoutSeconds} seconds waiting for processes`));
153
+ }, timeoutMs);
154
+ timeoutHandle.unref?.();
155
+ });
156
+ try {
157
+ await Promise.race([Promise.all(waitPromises), timeoutPromise]);
158
+ return pids.map((pid) => this.getProcessResult(pid, false));
159
+ }
160
+ finally {
161
+ if (timeoutHandle) {
162
+ clearTimeout(timeoutHandle);
163
+ }
164
+ }
165
+ }
166
+ killProcess(pid) {
167
+ const processEntry = this.processManager.get(pid);
168
+ if (!processEntry) {
169
+ throw new Error(`Process with PID ${pid} not found`);
170
+ }
171
+ if (processEntry.status !== 'running') {
172
+ return {
173
+ pid,
174
+ status: processEntry.status,
175
+ message: 'Process already terminated',
176
+ };
177
+ }
178
+ processEntry.process.kill('SIGTERM');
179
+ processEntry.status = 'failed';
180
+ processEntry.stderr += '\nProcess terminated by user';
181
+ return {
182
+ pid,
183
+ status: 'terminated',
184
+ message: 'Process terminated successfully',
185
+ };
186
+ }
187
+ cleanupProcesses() {
188
+ const removedPids = [];
189
+ for (const [pid, process] of this.processManager.entries()) {
190
+ if (process.status === 'completed' || process.status === 'failed') {
191
+ removedPids.push(pid);
192
+ this.processManager.delete(pid);
193
+ }
194
+ }
195
+ return {
196
+ removed: removedPids.length,
197
+ removedPids,
198
+ message: `Cleaned up ${removedPids.length} finished process(es)`,
199
+ };
200
+ }
201
+ }