ai-cli-mcp 2.19.0 → 2.20.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 +26 -0
- package/README.ja.md +34 -8
- package/README.md +41 -8
- package/dist/app/cli.js +1 -0
- package/dist/app/mcp.js +64 -12
- package/dist/cli-builder.js +13 -6
- package/dist/cli-process-service.js +76 -91
- package/dist/cli-utils.js +6 -0
- package/dist/cli.js +1 -1
- package/dist/model-catalog.js +3 -2
- package/dist/parsers.js +8 -2
- package/package.json +27 -3
- package/server.json +3 -3
- package/.gemini/settings.json +0 -11
- package/.github/dependabot.yml +0 -28
- package/.github/pull_request_template.md +0 -28
- package/.github/workflows/ci.yml +0 -34
- package/.github/workflows/dependency-review.yml +0 -22
- package/.github/workflows/publish.yml +0 -89
- package/.github/workflows/test.yml +0 -20
- package/.github/workflows/watch-session-prs.yml +0 -276
- package/.husky/pre-commit +0 -1
- package/.mcp.json +0 -11
- package/.releaserc.json +0 -18
- package/.vscode/settings.json +0 -3
- package/CONTRIBUTING.md +0 -81
- package/dist/__tests__/app-cli.test.js +0 -392
- package/dist/__tests__/cli-bin-smoke.test.js +0 -101
- package/dist/__tests__/cli-builder.test.js +0 -442
- package/dist/__tests__/cli-process-service.test.js +0 -655
- package/dist/__tests__/cli-utils.test.js +0 -171
- package/dist/__tests__/e2e.test.js +0 -256
- package/dist/__tests__/edge-cases.test.js +0 -130
- package/dist/__tests__/error-cases.test.js +0 -292
- package/dist/__tests__/mcp-contract.test.js +0 -636
- package/dist/__tests__/mocks.js +0 -32
- package/dist/__tests__/model-alias.test.js +0 -36
- package/dist/__tests__/parsers.test.js +0 -646
- package/dist/__tests__/peek.test.js +0 -36
- package/dist/__tests__/process-management.test.js +0 -949
- package/dist/__tests__/server.test.js +0 -809
- package/dist/__tests__/setup.js +0 -11
- package/dist/__tests__/utils/claude-mock.js +0 -80
- package/dist/__tests__/utils/mcp-client.js +0 -121
- package/dist/__tests__/utils/opencode-mock.js +0 -91
- package/dist/__tests__/utils/persistent-mock.js +0 -28
- package/dist/__tests__/utils/test-helpers.js +0 -11
- package/dist/__tests__/validation.test.js +0 -308
- package/dist/__tests__/version-print.test.js +0 -65
- package/dist/__tests__/wait.test.js +0 -260
- package/docs/RELEASE_CHECKLIST.md +0 -65
- package/docs/cli-architecture.md +0 -275
- package/docs/concept.md +0 -154
- package/docs/development.md +0 -156
- package/docs/e2e-testing.md +0 -148
- package/docs/prd.md +0 -146
- package/docs/session-stacking.md +0 -67
- package/src/__tests__/app-cli.test.ts +0 -495
- package/src/__tests__/cli-bin-smoke.test.ts +0 -136
- package/src/__tests__/cli-builder.test.ts +0 -549
- package/src/__tests__/cli-process-service.test.ts +0 -759
- package/src/__tests__/cli-utils.test.ts +0 -200
- package/src/__tests__/e2e.test.ts +0 -311
- package/src/__tests__/edge-cases.test.ts +0 -176
- package/src/__tests__/error-cases.test.ts +0 -370
- package/src/__tests__/mcp-contract.test.ts +0 -755
- package/src/__tests__/mocks.ts +0 -35
- package/src/__tests__/model-alias.test.ts +0 -44
- package/src/__tests__/parsers.test.ts +0 -730
- package/src/__tests__/peek.test.ts +0 -44
- package/src/__tests__/process-management.test.ts +0 -1129
- package/src/__tests__/server.test.ts +0 -1020
- package/src/__tests__/setup.ts +0 -13
- package/src/__tests__/utils/claude-mock.ts +0 -87
- package/src/__tests__/utils/mcp-client.ts +0 -159
- package/src/__tests__/utils/opencode-mock.ts +0 -108
- package/src/__tests__/utils/persistent-mock.ts +0 -33
- package/src/__tests__/utils/test-helpers.ts +0 -13
- package/src/__tests__/validation.test.ts +0 -369
- package/src/__tests__/version-print.test.ts +0 -81
- package/src/__tests__/wait.test.ts +0 -302
- package/src/app/cli.ts +0 -424
- package/src/app/mcp.ts +0 -466
- package/src/bin/ai-cli-mcp.ts +0 -7
- package/src/bin/ai-cli.ts +0 -11
- package/src/cli-builder.ts +0 -274
- package/src/cli-parse.ts +0 -105
- package/src/cli-process-service.ts +0 -709
- package/src/cli-utils.ts +0 -258
- package/src/cli.ts +0 -124
- package/src/model-catalog.ts +0 -87
- package/src/parsers.ts +0 -965
- package/src/peek.ts +0 -95
- package/src/process-result.ts +0 -88
- package/src/process-service.ts +0 -368
- package/src/server.ts +0 -10
- package/tsconfig.json +0 -16
- package/vitest.config.e2e.ts +0 -27
- package/vitest.config.ts +0 -22
- package/vitest.config.unit.ts +0 -28
package/src/peek.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import type { PeekEvent, PeekMessage } from './parsers.js';
|
|
2
|
-
import type { AgentType, ProcessStatus } from './process-service.js';
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_PEEK_TIME_SEC = 10;
|
|
5
|
-
export const MAX_PEEK_TIME_SEC = 60;
|
|
6
|
-
export const MAX_PEEK_PIDS = 32;
|
|
7
|
-
export const PEEK_MESSAGE_CAP = 50;
|
|
8
|
-
|
|
9
|
-
export type PeekStatus = ProcessStatus | 'not_found';
|
|
10
|
-
export type PeekAgent = AgentType | string | null;
|
|
11
|
-
|
|
12
|
-
export interface PeekProcessResult {
|
|
13
|
-
pid: number;
|
|
14
|
-
agent: PeekAgent;
|
|
15
|
-
status: PeekStatus;
|
|
16
|
-
events: PeekEvent[];
|
|
17
|
-
truncated: boolean;
|
|
18
|
-
error: string | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface PeekResponse {
|
|
22
|
-
peek_started_at: string;
|
|
23
|
-
observed_duration_sec: number;
|
|
24
|
-
processes: PeekProcessResult[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function validatePeekPids(value: unknown): number[] {
|
|
28
|
-
if (!Array.isArray(value)) {
|
|
29
|
-
throw new Error('Missing or invalid required parameter: pids (must be an array of positive safe integers)');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const deduped: number[] = [];
|
|
33
|
-
const seen = new Set<number>();
|
|
34
|
-
|
|
35
|
-
for (const pid of value) {
|
|
36
|
-
if (typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0) {
|
|
37
|
-
throw new Error('All pids must be positive safe integers');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!seen.has(pid)) {
|
|
41
|
-
seen.add(pid);
|
|
42
|
-
deduped.push(pid);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (deduped.length === 0 || deduped.length > MAX_PEEK_PIDS) {
|
|
47
|
-
throw new Error(`pids must contain 1..${MAX_PEEK_PIDS} entries after dedupe`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return deduped;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function validatePeekTimeSec(value: unknown): number {
|
|
54
|
-
if (value === undefined || value === null) {
|
|
55
|
-
return DEFAULT_PEEK_TIME_SEC;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0 || value > MAX_PEEK_TIME_SEC) {
|
|
59
|
-
throw new Error(`peek_time_sec must be a positive integer no greater than ${MAX_PEEK_TIME_SEC}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return value;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function buildNotFoundPeekProcess(pid: number): PeekProcessResult {
|
|
66
|
-
return {
|
|
67
|
-
pid,
|
|
68
|
-
agent: null,
|
|
69
|
-
status: 'not_found',
|
|
70
|
-
events: [],
|
|
71
|
-
truncated: false,
|
|
72
|
-
error: 'process not found',
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function appendPeekEvents(target: PeekProcessResult, events: PeekEvent[]): void {
|
|
77
|
-
for (const event of events) {
|
|
78
|
-
if (target.events.length < PEEK_MESSAGE_CAP) {
|
|
79
|
-
target.events.push(event);
|
|
80
|
-
} else {
|
|
81
|
-
target.truncated = true;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function appendPeekMessages(target: PeekProcessResult, messages: PeekMessage[]): void {
|
|
87
|
-
appendPeekEvents(
|
|
88
|
-
target,
|
|
89
|
-
messages.map((message) => ({ kind: 'message' as const, ...message })),
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function observedDurationSec(startedAtMs: number, endedAtMs = Date.now()): number {
|
|
94
|
-
return Number(((endedAtMs - startedAtMs) / 1000).toFixed(2));
|
|
95
|
-
}
|
package/src/process-result.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { AgentType, ProcessStatus } from './process-service.js';
|
|
2
|
-
|
|
3
|
-
interface ProcessResultContext {
|
|
4
|
-
pid: number;
|
|
5
|
-
agent: AgentType;
|
|
6
|
-
status: ProcessStatus;
|
|
7
|
-
exitCode?: number;
|
|
8
|
-
startTime: string;
|
|
9
|
-
workFolder: string;
|
|
10
|
-
prompt: string;
|
|
11
|
-
model?: string;
|
|
12
|
-
stdout: string;
|
|
13
|
-
stderr: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function compactAgentOutput(agentOutput: any): any | null {
|
|
17
|
-
if (!agentOutput || typeof agentOutput !== 'object') {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const { tools: _tools, ...rest } = agentOutput;
|
|
22
|
-
const compact = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined && value !== null));
|
|
23
|
-
return Object.keys(compact).length > 0 ? compact : null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function hasMeaningfulParsedOutput(agentOutput: any): boolean {
|
|
27
|
-
if (!agentOutput || typeof agentOutput !== 'object') {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return Object.entries(agentOutput).some(([key, value]) => {
|
|
32
|
-
if (value === undefined || value === null) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (key === 'session_id') {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (key === 'tools') {
|
|
41
|
-
return Array.isArray(value) ? value.length > 0 : true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return true;
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function shouldPreserveRawFailureOutput(context: ProcessResultContext): boolean {
|
|
49
|
-
return context.agent === 'opencode' && context.status === 'failed';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function buildProcessResult(context: ProcessResultContext, agentOutput: any, verbose = false): any {
|
|
53
|
-
const response: any = {
|
|
54
|
-
pid: context.pid,
|
|
55
|
-
agent: context.agent,
|
|
56
|
-
status: context.status,
|
|
57
|
-
exitCode: context.exitCode ?? null,
|
|
58
|
-
model: context.model ?? null,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
if (verbose) {
|
|
62
|
-
response.startTime = context.startTime;
|
|
63
|
-
response.workFolder = context.workFolder;
|
|
64
|
-
response.prompt = context.prompt;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (agentOutput?.session_id) {
|
|
68
|
-
response.session_id = agentOutput.session_id;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const shapedAgentOutput = verbose ? agentOutput : compactAgentOutput(agentOutput);
|
|
72
|
-
const preserveRawFailureOutput = shouldPreserveRawFailureOutput(context);
|
|
73
|
-
|
|
74
|
-
if (hasMeaningfulParsedOutput(shapedAgentOutput) && (verbose || !preserveRawFailureOutput)) {
|
|
75
|
-
response.agentOutput = shapedAgentOutput;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!response.agentOutput || preserveRawFailureOutput) {
|
|
79
|
-
response.stdout = context.stdout;
|
|
80
|
-
response.stderr = context.stderr;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (verbose && preserveRawFailureOutput && hasMeaningfulParsedOutput(shapedAgentOutput)) {
|
|
84
|
-
response.agentOutput = shapedAgentOutput;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return response;
|
|
88
|
-
}
|
package/src/process-service.ts
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
-
import { buildCliCommand, type BuildCliCommandOptions } from './cli-builder.js';
|
|
3
|
-
import { parseClaudeOutput, parseCodexOutput, parseForgeOutput, parseGeminiOutput, parseOpenCodeOutput, PeekEventExtractor } from './parsers.js';
|
|
4
|
-
import {
|
|
5
|
-
appendPeekEvents,
|
|
6
|
-
buildNotFoundPeekProcess,
|
|
7
|
-
observedDurationSec,
|
|
8
|
-
validatePeekPids,
|
|
9
|
-
validatePeekTimeSec,
|
|
10
|
-
type PeekProcessResult,
|
|
11
|
-
type PeekResponse,
|
|
12
|
-
} from './peek.js';
|
|
13
|
-
import { buildProcessResult } from './process-result.js';
|
|
14
|
-
|
|
15
|
-
export type AgentType = 'claude' | 'codex' | 'gemini' | 'forge' | 'opencode';
|
|
16
|
-
export type ProcessStatus = 'running' | 'completed' | 'failed';
|
|
17
|
-
|
|
18
|
-
interface TrackedProcess {
|
|
19
|
-
pid: number;
|
|
20
|
-
process: ChildProcess;
|
|
21
|
-
prompt: string;
|
|
22
|
-
workFolder: string;
|
|
23
|
-
model?: string;
|
|
24
|
-
toolType: AgentType;
|
|
25
|
-
startTime: string;
|
|
26
|
-
stdout: string;
|
|
27
|
-
stderr: string;
|
|
28
|
-
status: ProcessStatus;
|
|
29
|
-
exitCode?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ProcessListItem {
|
|
33
|
-
pid: number;
|
|
34
|
-
agent: AgentType;
|
|
35
|
-
status: ProcessStatus;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface StartProcessResult {
|
|
39
|
-
pid: number;
|
|
40
|
-
status: 'started';
|
|
41
|
-
agent: AgentType;
|
|
42
|
-
message: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface ProcessServiceOptions {
|
|
46
|
-
cliPaths: BuildCliCommandOptions['cliPaths'];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function parseAgentOutput(agent: AgentType, stdout: string, stderr: string): any {
|
|
50
|
-
if (agent === 'codex') {
|
|
51
|
-
return parseCodexOutput(`${stdout || ''}\n${stderr || ''}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!stdout) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (agent === 'claude') {
|
|
59
|
-
return parseClaudeOutput(stdout);
|
|
60
|
-
}
|
|
61
|
-
if (agent === 'gemini') {
|
|
62
|
-
return parseGeminiOutput(stdout);
|
|
63
|
-
}
|
|
64
|
-
if (agent === 'forge') {
|
|
65
|
-
return parseForgeOutput(stdout);
|
|
66
|
-
}
|
|
67
|
-
if (agent === 'opencode') {
|
|
68
|
-
return parseOpenCodeOutput(stdout);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export class ProcessService {
|
|
75
|
-
private readonly processManager = new Map<number, TrackedProcess>();
|
|
76
|
-
private readonly cliPaths: BuildCliCommandOptions['cliPaths'];
|
|
77
|
-
|
|
78
|
-
constructor(options: ProcessServiceOptions) {
|
|
79
|
-
this.cliPaths = options.cliPaths;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
startProcess(options: Omit<BuildCliCommandOptions, 'cliPaths'>): StartProcessResult {
|
|
83
|
-
const cmd = buildCliCommand({
|
|
84
|
-
...options,
|
|
85
|
-
cliPaths: this.cliPaths,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const { cliPath, args: processArgs, cwd: effectiveCwd, agent, prompt } = cmd;
|
|
89
|
-
const childProcess = spawn(cliPath, processArgs, {
|
|
90
|
-
cwd: effectiveCwd,
|
|
91
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
92
|
-
detached: false,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const pid = childProcess.pid;
|
|
96
|
-
if (!pid) {
|
|
97
|
-
throw new Error(`Failed to start ${agent} CLI process`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const processEntry: TrackedProcess = {
|
|
101
|
-
pid,
|
|
102
|
-
process: childProcess,
|
|
103
|
-
prompt,
|
|
104
|
-
workFolder: effectiveCwd,
|
|
105
|
-
model: options.model,
|
|
106
|
-
toolType: agent,
|
|
107
|
-
startTime: new Date().toISOString(),
|
|
108
|
-
stdout: '',
|
|
109
|
-
stderr: '',
|
|
110
|
-
status: 'running',
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
this.processManager.set(pid, processEntry);
|
|
114
|
-
|
|
115
|
-
childProcess.stdout.on('data', (data) => {
|
|
116
|
-
const entry = this.processManager.get(pid);
|
|
117
|
-
if (entry) {
|
|
118
|
-
entry.stdout += data.toString();
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
childProcess.stderr.on('data', (data) => {
|
|
123
|
-
const entry = this.processManager.get(pid);
|
|
124
|
-
if (entry) {
|
|
125
|
-
entry.stderr += data.toString();
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
childProcess.on('close', (code) => {
|
|
130
|
-
const entry = this.processManager.get(pid);
|
|
131
|
-
if (entry) {
|
|
132
|
-
entry.status = code === 0 ? 'completed' : 'failed';
|
|
133
|
-
entry.exitCode = code !== null ? code : undefined;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
childProcess.on('error', (error) => {
|
|
138
|
-
const entry = this.processManager.get(pid);
|
|
139
|
-
if (entry) {
|
|
140
|
-
entry.status = 'failed';
|
|
141
|
-
entry.stderr += `\nProcess error: ${error.message}`;
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
pid,
|
|
147
|
-
status: 'started',
|
|
148
|
-
agent,
|
|
149
|
-
message: `${agent} process started successfully`,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
listProcesses(): ProcessListItem[] {
|
|
154
|
-
const processes: ProcessListItem[] = [];
|
|
155
|
-
|
|
156
|
-
for (const [pid, process] of this.processManager.entries()) {
|
|
157
|
-
processes.push({
|
|
158
|
-
pid,
|
|
159
|
-
agent: process.toolType,
|
|
160
|
-
status: process.status,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return processes;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
getProcessResult(pid: number, verbose = false): any {
|
|
168
|
-
const process = this.processManager.get(pid);
|
|
169
|
-
if (!process) {
|
|
170
|
-
throw new Error(`Process with PID ${pid} not found`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const agentOutput = parseAgentOutput(process.toolType, process.stdout, process.stderr);
|
|
174
|
-
|
|
175
|
-
return buildProcessResult({
|
|
176
|
-
pid,
|
|
177
|
-
agent: process.toolType,
|
|
178
|
-
status: process.status,
|
|
179
|
-
exitCode: process.exitCode,
|
|
180
|
-
startTime: process.startTime,
|
|
181
|
-
workFolder: process.workFolder,
|
|
182
|
-
prompt: process.prompt,
|
|
183
|
-
model: process.model,
|
|
184
|
-
stdout: process.stdout,
|
|
185
|
-
stderr: process.stderr,
|
|
186
|
-
}, agentOutput, verbose);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async waitForProcesses(pids: number[], timeoutSeconds = 180, verbose = false): Promise<any[]> {
|
|
190
|
-
for (const pid of pids) {
|
|
191
|
-
if (!this.processManager.has(pid)) {
|
|
192
|
-
throw new Error(`Process with PID ${pid} not found`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const waitPromises = pids.map((pid) => {
|
|
197
|
-
const processEntry = this.processManager.get(pid)!;
|
|
198
|
-
|
|
199
|
-
if (processEntry.status !== 'running') {
|
|
200
|
-
return Promise.resolve();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return new Promise<void>((resolve) => {
|
|
204
|
-
processEntry.process.once('close', () => {
|
|
205
|
-
resolve();
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const timeoutMs = timeoutSeconds * 1000;
|
|
211
|
-
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
212
|
-
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
213
|
-
timeoutHandle = setTimeout(() => {
|
|
214
|
-
reject(new Error(`Timed out after ${timeoutSeconds} seconds waiting for processes`));
|
|
215
|
-
}, timeoutMs);
|
|
216
|
-
timeoutHandle.unref?.();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
await Promise.race([Promise.all(waitPromises), timeoutPromise]);
|
|
221
|
-
return pids.map((pid) => this.getProcessResult(pid, verbose));
|
|
222
|
-
} finally {
|
|
223
|
-
if (timeoutHandle) {
|
|
224
|
-
clearTimeout(timeoutHandle);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async peekProcesses(pids: number[], peekTimeSec = 10, includeToolCalls = false): Promise<PeekResponse> {
|
|
230
|
-
const targetPids = validatePeekPids(pids);
|
|
231
|
-
const targetPeekTimeSec = validatePeekTimeSec(peekTimeSec);
|
|
232
|
-
const processes: PeekProcessResult[] = [];
|
|
233
|
-
const observers: Array<{
|
|
234
|
-
entry: TrackedProcess;
|
|
235
|
-
result: PeekProcessResult;
|
|
236
|
-
stdoutExtractor: PeekEventExtractor;
|
|
237
|
-
stderrExtractor: PeekEventExtractor;
|
|
238
|
-
onStdout: (data: Buffer | string) => void;
|
|
239
|
-
onStderr: (data: Buffer | string) => void;
|
|
240
|
-
}> = [];
|
|
241
|
-
|
|
242
|
-
for (const pid of targetPids) {
|
|
243
|
-
const entry = this.processManager.get(pid);
|
|
244
|
-
if (!entry) {
|
|
245
|
-
processes.push(buildNotFoundPeekProcess(pid));
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const result: PeekProcessResult = {
|
|
250
|
-
pid,
|
|
251
|
-
agent: entry.toolType,
|
|
252
|
-
status: entry.status,
|
|
253
|
-
events: [],
|
|
254
|
-
truncated: false,
|
|
255
|
-
error: null,
|
|
256
|
-
};
|
|
257
|
-
processes.push(result);
|
|
258
|
-
|
|
259
|
-
const stdoutExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls, source: 'stdout' });
|
|
260
|
-
const stderrExtractor = new PeekEventExtractor(entry.toolType, { includeToolCalls, source: 'stderr' });
|
|
261
|
-
const onStdout = (data: Buffer | string) => {
|
|
262
|
-
appendPeekEvents(result, stdoutExtractor.push(data.toString(), new Date().toISOString()));
|
|
263
|
-
};
|
|
264
|
-
const onStderr = (data: Buffer | string) => {
|
|
265
|
-
appendPeekEvents(result, stderrExtractor.push(data.toString(), new Date().toISOString()));
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
if (entry.status === 'running') {
|
|
269
|
-
entry.process.stdout?.on('data', onStdout);
|
|
270
|
-
entry.process.stderr?.on('data', onStderr);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
observers.push({ entry, result, stdoutExtractor, stderrExtractor, onStdout, onStderr });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const startedAt = new Date();
|
|
277
|
-
const startedAtMs = Date.now();
|
|
278
|
-
const runningObservers = observers.filter((observer) => observer.entry.status === 'running');
|
|
279
|
-
const terminalPromise = Promise.all(runningObservers.map((observer) => this.waitForProcessTerminal(observer.entry)));
|
|
280
|
-
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
281
|
-
const timeoutPromise = new Promise<void>((resolve) => {
|
|
282
|
-
timeoutHandle = setTimeout(resolve, targetPeekTimeSec * 1000);
|
|
283
|
-
timeoutHandle.unref?.();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
try {
|
|
287
|
-
await Promise.race([terminalPromise, timeoutPromise]);
|
|
288
|
-
} finally {
|
|
289
|
-
if (timeoutHandle) {
|
|
290
|
-
clearTimeout(timeoutHandle);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const flushTs = new Date().toISOString();
|
|
294
|
-
for (const observer of observers) {
|
|
295
|
-
observer.entry.process.stdout?.off('data', observer.onStdout);
|
|
296
|
-
observer.entry.process.stderr?.off('data', observer.onStderr);
|
|
297
|
-
const terminal = observer.entry.status !== 'running';
|
|
298
|
-
appendPeekEvents(observer.result, observer.stdoutExtractor.flush(flushTs, { terminal }));
|
|
299
|
-
appendPeekEvents(observer.result, observer.stderrExtractor.flush(flushTs, { terminal }));
|
|
300
|
-
observer.result.status = observer.entry.status;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
peek_started_at: startedAt.toISOString(),
|
|
306
|
-
observed_duration_sec: observedDurationSec(startedAtMs),
|
|
307
|
-
processes,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
private waitForProcessTerminal(processEntry: TrackedProcess): Promise<void> {
|
|
312
|
-
if (processEntry.status !== 'running') {
|
|
313
|
-
return Promise.resolve();
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return new Promise<void>((resolve) => {
|
|
317
|
-
const done = () => {
|
|
318
|
-
processEntry.process.off('close', done);
|
|
319
|
-
processEntry.process.off('error', done);
|
|
320
|
-
resolve();
|
|
321
|
-
};
|
|
322
|
-
processEntry.process.once('close', done);
|
|
323
|
-
processEntry.process.once('error', done);
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
killProcess(pid: number): { pid: number; status: string; message: string } {
|
|
328
|
-
const processEntry = this.processManager.get(pid);
|
|
329
|
-
if (!processEntry) {
|
|
330
|
-
throw new Error(`Process with PID ${pid} not found`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (processEntry.status !== 'running') {
|
|
334
|
-
return {
|
|
335
|
-
pid,
|
|
336
|
-
status: processEntry.status,
|
|
337
|
-
message: 'Process already terminated',
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
processEntry.process.kill('SIGTERM');
|
|
342
|
-
processEntry.status = 'failed';
|
|
343
|
-
processEntry.stderr += '\nProcess terminated by user';
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
pid,
|
|
347
|
-
status: 'terminated',
|
|
348
|
-
message: 'Process terminated successfully',
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
cleanupProcesses(): { removed: number; removedPids: number[]; message: string } {
|
|
353
|
-
const removedPids: number[] = [];
|
|
354
|
-
|
|
355
|
-
for (const [pid, process] of this.processManager.entries()) {
|
|
356
|
-
if (process.status === 'completed' || process.status === 'failed') {
|
|
357
|
-
removedPids.push(pid);
|
|
358
|
-
this.processManager.delete(pid);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return {
|
|
363
|
-
removed: removedPids.length,
|
|
364
|
-
removedPids,
|
|
365
|
-
message: `Cleaned up ${removedPids.length} finished process(es)`,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
}
|
package/src/server.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
|
|
2
|
-
export { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli, findOpencodeCli } from './cli-utils.js';
|
|
3
|
-
export { resolveModelAlias } from './cli-builder.js';
|
|
4
|
-
export { ClaudeCodeServer, runMcpServer, spawnAsync } from './app/mcp.js';
|
|
5
|
-
|
|
6
|
-
import { runMcpServer } from './app/mcp.js';
|
|
7
|
-
|
|
8
|
-
if (!process.env.VITEST) {
|
|
9
|
-
runMcpServer().catch(console.error);
|
|
10
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"resolveJsonModule": true,
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"],
|
|
15
|
-
"exclude": ["node_modules"]
|
|
16
|
-
}
|
package/vitest.config.e2e.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
globals: true,
|
|
6
|
-
environment: 'node',
|
|
7
|
-
testTimeout: 30000, // Longer timeout for e2e tests
|
|
8
|
-
hookTimeout: 20000,
|
|
9
|
-
setupFiles: ['./src/__tests__/setup.ts'],
|
|
10
|
-
coverage: {
|
|
11
|
-
provider: 'v8',
|
|
12
|
-
reporter: ['text', 'json', 'html'],
|
|
13
|
-
exclude: [
|
|
14
|
-
'node_modules/**',
|
|
15
|
-
'dist/**',
|
|
16
|
-
'**/*.d.ts',
|
|
17
|
-
'**/*.test.ts',
|
|
18
|
-
'**/*.spec.ts',
|
|
19
|
-
'src/__tests__/utils/**',
|
|
20
|
-
],
|
|
21
|
-
},
|
|
22
|
-
include: ['src/__tests__/e2e.test.ts', 'src/__tests__/edge-cases.test.ts'],
|
|
23
|
-
mockReset: true,
|
|
24
|
-
clearMocks: true,
|
|
25
|
-
restoreMocks: true,
|
|
26
|
-
},
|
|
27
|
-
});
|
package/vitest.config.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
globals: true,
|
|
6
|
-
environment: 'node',
|
|
7
|
-
coverage: {
|
|
8
|
-
provider: 'v8',
|
|
9
|
-
reporter: ['text', 'json', 'html'],
|
|
10
|
-
exclude: [
|
|
11
|
-
'node_modules/**',
|
|
12
|
-
'dist/**',
|
|
13
|
-
'**/*.d.ts',
|
|
14
|
-
'**/*.test.ts',
|
|
15
|
-
'**/*.spec.ts',
|
|
16
|
-
],
|
|
17
|
-
},
|
|
18
|
-
mockReset: true,
|
|
19
|
-
clearMocks: true,
|
|
20
|
-
restoreMocks: true,
|
|
21
|
-
},
|
|
22
|
-
});
|
package/vitest.config.unit.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
globals: true,
|
|
6
|
-
environment: 'node',
|
|
7
|
-
coverage: {
|
|
8
|
-
provider: 'v8',
|
|
9
|
-
reporter: ['text', 'json', 'html'],
|
|
10
|
-
exclude: [
|
|
11
|
-
'node_modules/**',
|
|
12
|
-
'dist/**',
|
|
13
|
-
'**/*.d.ts',
|
|
14
|
-
'**/*.test.ts',
|
|
15
|
-
'**/*.spec.ts',
|
|
16
|
-
],
|
|
17
|
-
},
|
|
18
|
-
exclude: [
|
|
19
|
-
'node_modules/**',
|
|
20
|
-
'dist/**',
|
|
21
|
-
'src/__tests__/e2e.test.ts',
|
|
22
|
-
'src/__tests__/edge-cases.test.ts',
|
|
23
|
-
],
|
|
24
|
-
mockReset: true,
|
|
25
|
-
clearMocks: true,
|
|
26
|
-
restoreMocks: true,
|
|
27
|
-
},
|
|
28
|
-
});
|