ai-cli-mcp 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.ja.md +102 -3
  3. package/README.md +102 -3
  4. package/dist/__tests__/app-cli.test.js +285 -0
  5. package/dist/__tests__/cli-bin-smoke.test.js +54 -0
  6. package/dist/__tests__/cli-process-service.test.js +233 -0
  7. package/dist/__tests__/cli-utils.test.js +109 -0
  8. package/dist/__tests__/error-cases.test.js +2 -1
  9. package/dist/__tests__/mcp-contract.test.js +195 -0
  10. package/dist/__tests__/process-management.test.js +15 -8
  11. package/dist/__tests__/server.test.js +29 -3
  12. package/dist/__tests__/wait.test.js +31 -0
  13. package/dist/app/cli.js +304 -0
  14. package/dist/app/mcp.js +362 -0
  15. package/dist/bin/ai-cli-mcp.js +6 -0
  16. package/dist/bin/ai-cli.js +10 -0
  17. package/dist/cli-builder.js +1 -6
  18. package/dist/cli-process-service.js +328 -0
  19. package/dist/cli-utils.js +142 -88
  20. package/dist/model-catalog.js +50 -0
  21. package/dist/process-service.js +198 -0
  22. package/dist/server.js +3 -577
  23. package/docs/cli-architecture.md +275 -0
  24. package/package.json +3 -2
  25. package/src/__tests__/app-cli.test.ts +362 -0
  26. package/src/__tests__/cli-bin-smoke.test.ts +71 -0
  27. package/src/__tests__/cli-process-service.test.ts +278 -0
  28. package/src/__tests__/cli-utils.test.ts +132 -0
  29. package/src/__tests__/error-cases.test.ts +3 -4
  30. package/src/__tests__/mcp-contract.test.ts +250 -0
  31. package/src/__tests__/process-management.test.ts +15 -9
  32. package/src/__tests__/server.test.ts +27 -6
  33. package/src/__tests__/wait.test.ts +38 -0
  34. package/src/app/cli.ts +373 -0
  35. package/src/app/mcp.ts +398 -0
  36. package/src/bin/ai-cli-mcp.ts +7 -0
  37. package/src/bin/ai-cli.ts +11 -0
  38. package/src/cli-builder.ts +1 -7
  39. package/src/cli-process-service.ts +415 -0
  40. package/src/cli-utils.ts +185 -99
  41. package/src/model-catalog.ts +60 -0
  42. package/src/process-service.ts +261 -0
  43. package/src/server.ts +3 -667
@@ -0,0 +1,198 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { buildCliCommand } from './cli-builder.js';
3
+ import { parseClaudeOutput, parseCodexOutput, 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
+ }
100
+ const response = {
101
+ pid,
102
+ agent: process.toolType,
103
+ status: process.status,
104
+ exitCode: process.exitCode,
105
+ startTime: process.startTime,
106
+ workFolder: process.workFolder,
107
+ prompt: process.prompt,
108
+ model: process.model,
109
+ };
110
+ if (agentOutput) {
111
+ if (!verbose && agentOutput.tools) {
112
+ const { tools, ...rest } = agentOutput;
113
+ response.agentOutput = rest;
114
+ }
115
+ else {
116
+ response.agentOutput = agentOutput;
117
+ }
118
+ if (agentOutput.session_id) {
119
+ response.session_id = agentOutput.session_id;
120
+ }
121
+ }
122
+ else {
123
+ response.stdout = process.stdout;
124
+ response.stderr = process.stderr;
125
+ }
126
+ return response;
127
+ }
128
+ async waitForProcesses(pids, timeoutSeconds = 180) {
129
+ for (const pid of pids) {
130
+ if (!this.processManager.has(pid)) {
131
+ throw new Error(`Process with PID ${pid} not found`);
132
+ }
133
+ }
134
+ const waitPromises = pids.map((pid) => {
135
+ const processEntry = this.processManager.get(pid);
136
+ if (processEntry.status !== 'running') {
137
+ return Promise.resolve();
138
+ }
139
+ return new Promise((resolve) => {
140
+ processEntry.process.once('close', () => {
141
+ resolve();
142
+ });
143
+ });
144
+ });
145
+ const timeoutMs = timeoutSeconds * 1000;
146
+ let timeoutHandle;
147
+ const timeoutPromise = new Promise((_, reject) => {
148
+ timeoutHandle = setTimeout(() => {
149
+ reject(new Error(`Timed out after ${timeoutSeconds} seconds waiting for processes`));
150
+ }, timeoutMs);
151
+ timeoutHandle.unref?.();
152
+ });
153
+ try {
154
+ await Promise.race([Promise.all(waitPromises), timeoutPromise]);
155
+ return pids.map((pid) => this.getProcessResult(pid, false));
156
+ }
157
+ finally {
158
+ if (timeoutHandle) {
159
+ clearTimeout(timeoutHandle);
160
+ }
161
+ }
162
+ }
163
+ killProcess(pid) {
164
+ const processEntry = this.processManager.get(pid);
165
+ if (!processEntry) {
166
+ throw new Error(`Process with PID ${pid} not found`);
167
+ }
168
+ if (processEntry.status !== 'running') {
169
+ return {
170
+ pid,
171
+ status: processEntry.status,
172
+ message: 'Process already terminated',
173
+ };
174
+ }
175
+ processEntry.process.kill('SIGTERM');
176
+ processEntry.status = 'failed';
177
+ processEntry.stderr += '\nProcess terminated by user';
178
+ return {
179
+ pid,
180
+ status: 'terminated',
181
+ message: 'Process terminated successfully',
182
+ };
183
+ }
184
+ cleanupProcesses() {
185
+ const removedPids = [];
186
+ for (const [pid, process] of this.processManager.entries()) {
187
+ if (process.status === 'completed' || process.status === 'failed') {
188
+ removedPids.push(pid);
189
+ this.processManager.delete(pid);
190
+ }
191
+ }
192
+ return {
193
+ removed: removedPids.length,
194
+ removedPids,
195
+ message: `Cleaned up ${removedPids.length} finished process(es)`,
196
+ };
197
+ }
198
+ }