chatccc 0.2.196 → 0.2.198
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/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/config.sample.json +27 -27
- package/package.json +2 -1
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -0
- package/src/__tests__/builtin-chat-session.test.ts +277 -181
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +33 -33
- package/src/__tests__/builtin-context.test.ts +163 -163
- package/src/__tests__/builtin-file-tools.test.ts +224 -196
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/cards.test.ts +109 -109
- package/src/__tests__/ccc-adapter.test.ts +114 -113
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -0
- package/src/__tests__/codex-raw-stream-log.test.ts +163 -0
- package/src/__tests__/config-reload.test.ts +10 -10
- package/src/__tests__/config-sample.test.ts +18 -18
- package/src/__tests__/cursor-adapter.test.ts +167 -113
- package/src/__tests__/feishu-avatar.test.ts +40 -40
- package/src/__tests__/jsonl-stream.test.ts +79 -0
- package/src/__tests__/orchestrator.test.ts +181 -154
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +40 -40
- package/src/__tests__/sim-platform.test.ts +12 -12
- package/src/__tests__/web-ui.test.ts +209 -209
- package/src/adapters/ccc-adapter.ts +121 -119
- package/src/adapters/claude-adapter.ts +603 -566
- package/src/adapters/codex-adapter.ts +65 -52
- package/src/adapters/cursor-adapter.ts +269 -276
- package/src/adapters/jsonl-stream.ts +157 -0
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/agent-reload-config-rpc.ts +34 -0
- package/src/builtin/cli.ts +473 -461
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-tools.ts +1072 -915
- package/src/builtin/index.ts +404 -353
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/cards.ts +195 -195
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/config.ts +125 -125
- package/src/feishu-api.ts +49 -49
- package/src/index.ts +8 -13
- package/src/orchestrator.ts +166 -145
- package/src/runtime-reload.ts +34 -0
- package/src/session.ts +141 -141
- package/src/web-ui.ts +205 -205
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
|
-
import { createInterface } from "node:readline";
|
|
12
11
|
import { fileURLToPath } from "node:url";
|
|
13
12
|
|
|
14
13
|
import type {
|
|
@@ -20,16 +19,17 @@ import type {
|
|
|
20
19
|
SessionInfo,
|
|
21
20
|
} from "./adapter-interface.ts";
|
|
22
21
|
import { parseUserCommand } from "./adapter-interface.ts";
|
|
23
|
-
import { config, CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS, RAW_STREAM_LOGS_DIR } from "../config.ts";
|
|
22
|
+
import { config, CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS, RAW_STREAM_LOGS_DIR } from "../config.ts";
|
|
24
23
|
import {
|
|
25
24
|
defaultCursorSessionMetaStore,
|
|
26
25
|
type CursorSessionMetaStore,
|
|
27
26
|
} from "./cursor-session-meta-store.ts";
|
|
28
|
-
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
29
|
-
import {
|
|
30
|
-
createRawStreamLog,
|
|
31
|
-
type RawStreamLogHandle,
|
|
32
|
-
} from "./raw-stream-log.ts";
|
|
27
|
+
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
28
|
+
import {
|
|
29
|
+
createRawStreamLog,
|
|
30
|
+
type RawStreamLogHandle,
|
|
31
|
+
} from "./raw-stream-log.ts";
|
|
32
|
+
import { readJsonLinesWithBadJsonIdleWatchdog } from "./jsonl-stream.ts";
|
|
33
33
|
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
// 特殊注入提示
|
|
@@ -105,9 +105,9 @@ interface CursorMessageLine {
|
|
|
105
105
|
tool_call?: Record<string, unknown>;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
interface CursorContentBlock {
|
|
109
|
-
type?: string;
|
|
110
|
-
text?: string;
|
|
108
|
+
interface CursorContentBlock {
|
|
109
|
+
type?: string;
|
|
110
|
+
text?: string;
|
|
111
111
|
thinking?: string;
|
|
112
112
|
name?: string;
|
|
113
113
|
input?: unknown;
|
|
@@ -115,73 +115,73 @@ interface CursorContentBlock {
|
|
|
115
115
|
content?: unknown;
|
|
116
116
|
is_error?: boolean;
|
|
117
117
|
query?: string;
|
|
118
|
-
[key: string]: unknown;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface CursorProcessCloseInfo {
|
|
122
|
-
code: number | null;
|
|
123
|
-
signal: NodeJS.Signals | null;
|
|
124
|
-
stderr: string;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
interface CursorProcessHandle {
|
|
128
|
-
proc: ChildProcess;
|
|
129
|
-
getStderr(): string;
|
|
130
|
-
waitForClose(): Promise<CursorProcessCloseInfo>;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
interface CursorStreamStats {
|
|
134
|
-
stdoutLength: number;
|
|
135
|
-
rawLineCount: number;
|
|
136
|
-
parsedLineCount: number;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
type CursorSpawn = typeof spawn;
|
|
140
|
-
|
|
141
|
-
function createCursorStreamStats(): CursorStreamStats {
|
|
142
|
-
return { stdoutLength: 0, rawLineCount: 0, parsedLineCount: 0 };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function isCursorAuthRelatedError(stderr: string): boolean {
|
|
146
|
-
const text = stderr.toLowerCase();
|
|
147
|
-
return (
|
|
148
|
-
text.includes("authentication required") ||
|
|
149
|
-
text.includes("not logged in") ||
|
|
150
|
-
text.includes("login") ||
|
|
151
|
-
text.includes("sign in") ||
|
|
152
|
-
text.includes("unauthorized") ||
|
|
153
|
-
text.includes("401") ||
|
|
154
|
-
text.includes("cursor_api_key") ||
|
|
155
|
-
text.includes("api key")
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function formatCursorAgentEmptyOutputMessage(args: {
|
|
160
|
-
exitCode: number | null;
|
|
161
|
-
stdoutLength: number;
|
|
162
|
-
stderr: string;
|
|
163
|
-
}): string | null {
|
|
164
|
-
if (args.exitCode === 0) return null;
|
|
165
|
-
if (args.stdoutLength !== 0) return null;
|
|
166
|
-
if (args.stderr.trim().length === 0) return null;
|
|
167
|
-
|
|
168
|
-
if (isCursorAuthRelatedError(args.stderr)) {
|
|
169
|
-
return "Cursor Agent 没有返回内容。检测到认证相关错误,可能需要重新登录 Cursor Agent,或配置 CURSOR_API_KEY。请在本机运行 agent status 检查状态;如未登录,请运行 agent login 后重试。";
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return "Cursor Agent 没有返回内容。底层命令异常退出,请检查本机 Cursor Agent 状态后重试。";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function createCursorAgentFailureError(info: CursorProcessCloseInfo): Error {
|
|
176
|
-
const stderr = info.stderr.trim().slice(0, 500);
|
|
177
|
-
return new Error(
|
|
178
|
-
`Cursor Agent exited without stream-json output (exit=${info.code ?? "unknown"}, stderr=${stderr})`,
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
// normalizeCursorMessage — Cursor 消息 → UnifiedStreamMessage | null
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
118
|
+
[key: string]: unknown;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface CursorProcessCloseInfo {
|
|
122
|
+
code: number | null;
|
|
123
|
+
signal: NodeJS.Signals | null;
|
|
124
|
+
stderr: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface CursorProcessHandle {
|
|
128
|
+
proc: ChildProcess;
|
|
129
|
+
getStderr(): string;
|
|
130
|
+
waitForClose(): Promise<CursorProcessCloseInfo>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface CursorStreamStats {
|
|
134
|
+
stdoutLength: number;
|
|
135
|
+
rawLineCount: number;
|
|
136
|
+
parsedLineCount: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type CursorSpawn = typeof spawn;
|
|
140
|
+
|
|
141
|
+
function createCursorStreamStats(): CursorStreamStats {
|
|
142
|
+
return { stdoutLength: 0, rawLineCount: 0, parsedLineCount: 0 };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isCursorAuthRelatedError(stderr: string): boolean {
|
|
146
|
+
const text = stderr.toLowerCase();
|
|
147
|
+
return (
|
|
148
|
+
text.includes("authentication required") ||
|
|
149
|
+
text.includes("not logged in") ||
|
|
150
|
+
text.includes("login") ||
|
|
151
|
+
text.includes("sign in") ||
|
|
152
|
+
text.includes("unauthorized") ||
|
|
153
|
+
text.includes("401") ||
|
|
154
|
+
text.includes("cursor_api_key") ||
|
|
155
|
+
text.includes("api key")
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function formatCursorAgentEmptyOutputMessage(args: {
|
|
160
|
+
exitCode: number | null;
|
|
161
|
+
stdoutLength: number;
|
|
162
|
+
stderr: string;
|
|
163
|
+
}): string | null {
|
|
164
|
+
if (args.exitCode === 0) return null;
|
|
165
|
+
if (args.stdoutLength !== 0) return null;
|
|
166
|
+
if (args.stderr.trim().length === 0) return null;
|
|
167
|
+
|
|
168
|
+
if (isCursorAuthRelatedError(args.stderr)) {
|
|
169
|
+
return "Cursor Agent 没有返回内容。检测到认证相关错误,可能需要重新登录 Cursor Agent,或配置 CURSOR_API_KEY。请在本机运行 agent status 检查状态;如未登录,请运行 agent login 后重试。";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return "Cursor Agent 没有返回内容。底层命令异常退出,请检查本机 Cursor Agent 状态后重试。";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createCursorAgentFailureError(info: CursorProcessCloseInfo): Error {
|
|
176
|
+
const stderr = info.stderr.trim().slice(0, 500);
|
|
177
|
+
return new Error(
|
|
178
|
+
`Cursor Agent exited without stream-json output (exit=${info.code ?? "unknown"}, stderr=${stderr})`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// normalizeCursorMessage — Cursor 消息 → UnifiedStreamMessage | null
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
185
|
|
|
186
186
|
/** Cursor tool_call 内部 key → 统一工具名 */
|
|
187
187
|
function mapToolCallKey(key: string): string {
|
|
@@ -359,14 +359,14 @@ export function normalizeCursorMessage(
|
|
|
359
359
|
// 子进程辅助函数
|
|
360
360
|
// ---------------------------------------------------------------------------
|
|
361
361
|
|
|
362
|
-
function spawnAgent(
|
|
363
|
-
extraArgs: string[],
|
|
364
|
-
cwd?: string,
|
|
365
|
-
stdinText?: string,
|
|
366
|
-
modelOverride?: string,
|
|
367
|
-
mode?: "plan" | "ask",
|
|
368
|
-
spawnImpl: CursorSpawn = spawn,
|
|
369
|
-
): CursorProcessHandle {
|
|
362
|
+
function spawnAgent(
|
|
363
|
+
extraArgs: string[],
|
|
364
|
+
cwd?: string,
|
|
365
|
+
stdinText?: string,
|
|
366
|
+
modelOverride?: string,
|
|
367
|
+
mode?: "plan" | "ask",
|
|
368
|
+
spawnImpl: CursorSpawn = spawn,
|
|
369
|
+
): CursorProcessHandle {
|
|
370
370
|
let allArgs: string[];
|
|
371
371
|
if (mode) {
|
|
372
372
|
// plan/ask 模式:移除 --force/--yolo,添加 --mode plan/ask
|
|
@@ -385,87 +385,80 @@ function spawnAgent(
|
|
|
385
385
|
allArgs.push("--model", modelOverride);
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
|
-
const proc = spawnImpl(CURSOR_AGENT_COMMAND, allArgs, {
|
|
389
|
-
cwd,
|
|
390
|
-
stdio: [stdinText !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
391
|
-
windowsHide: true,
|
|
392
|
-
shell: true,
|
|
393
|
-
});
|
|
388
|
+
const proc = spawnImpl(CURSOR_AGENT_COMMAND, allArgs, {
|
|
389
|
+
cwd,
|
|
390
|
+
stdio: [stdinText !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
391
|
+
windowsHide: true,
|
|
392
|
+
shell: true,
|
|
393
|
+
});
|
|
394
394
|
|
|
395
395
|
console.log(`[Cursor debug] spawn: cmd=${CURSOR_AGENT_COMMAND}, args=[${allArgs.join(", ")}], cwd=${cwd ?? "(none)"}, stdinLen=${stdinText?.length ?? 0}, pid=${proc.pid}`);
|
|
396
396
|
|
|
397
|
-
// 收集 stderr,子进程异常退出时输出到日志,方便排查静默失败
|
|
398
|
-
let stderr = "";
|
|
399
|
-
let closeInfo: CursorProcessCloseInfo | null = null;
|
|
400
|
-
let resolveClose: (info: CursorProcessCloseInfo) => void = () => {};
|
|
401
|
-
const closePromise = new Promise<CursorProcessCloseInfo>((resolve) => {
|
|
402
|
-
resolveClose = resolve;
|
|
403
|
-
});
|
|
404
|
-
const settleClose = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
405
|
-
if (closeInfo) return;
|
|
406
|
-
closeInfo = { code, signal, stderr };
|
|
407
|
-
if (stderr.trim()) {
|
|
408
|
-
console.error(`[Cursor stderr] exit=${code}: ${stderr.trim().slice(0, 2000)}`);
|
|
409
|
-
}
|
|
410
|
-
resolveClose(closeInfo);
|
|
411
|
-
};
|
|
412
|
-
proc.stderr!.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });
|
|
413
|
-
proc.once("error", (err) => {
|
|
414
|
-
stderr += `${stderr ? "\n" : ""}${(err as Error).message}`;
|
|
415
|
-
settleClose(null, null);
|
|
416
|
-
});
|
|
417
|
-
proc.once("close", (code, signal) => { settleClose(code, signal); });
|
|
418
|
-
|
|
419
|
-
if (stdinText !== undefined) {
|
|
420
|
-
proc.stdin!.write(stdinText);
|
|
421
|
-
proc.stdin!.end();
|
|
422
|
-
}
|
|
423
|
-
return {
|
|
424
|
-
proc,
|
|
425
|
-
getStderr: () => stderr,
|
|
426
|
-
waitForClose: async () => {
|
|
427
|
-
if (closeInfo) return { ...closeInfo, stderr };
|
|
428
|
-
const info = await closePromise;
|
|
429
|
-
return { ...info, stderr };
|
|
430
|
-
},
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async function* readJsonLines(
|
|
435
|
-
proc: ChildProcess,
|
|
436
|
-
signal?: AbortSignal,
|
|
437
|
-
debugTag?: string,
|
|
438
|
-
rawLog?: RawStreamLogHandle | null,
|
|
439
|
-
stats?: CursorStreamStats,
|
|
440
|
-
): AsyncGenerator<CursorMessageLine> {
|
|
441
|
-
const tag = debugTag ?? "cursor";
|
|
442
|
-
const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
|
|
443
|
-
// abort 时主动 close readline,避免等待 Windows 管道自然关闭(可能延迟数分钟)
|
|
444
|
-
const onAbort = () => { rl.close(); };
|
|
445
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
446
|
-
let lineCount = 0;
|
|
447
|
-
try {
|
|
448
|
-
for await (const line of rl) {
|
|
449
|
-
if (signal?.aborted) break;
|
|
450
|
-
lineCount++;
|
|
451
|
-
if (stats) {
|
|
452
|
-
stats.rawLineCount++;
|
|
453
|
-
stats.stdoutLength += Buffer.byteLength(line, "utf-8") + 1;
|
|
454
|
-
}
|
|
455
|
-
const trimmed = line.trim();
|
|
456
|
-
if (!trimmed) continue;
|
|
457
|
-
rawLog?.writeLine(trimmed);
|
|
458
|
-
try {
|
|
459
|
-
const parsed = JSON.parse(trimmed) as CursorMessageLine;
|
|
460
|
-
if (stats) stats.parsedLineCount++;
|
|
461
|
-
yield parsed;
|
|
462
|
-
} catch { /* 非 JSON 行静默跳过 */ }
|
|
397
|
+
// 收集 stderr,子进程异常退出时输出到日志,方便排查静默失败
|
|
398
|
+
let stderr = "";
|
|
399
|
+
let closeInfo: CursorProcessCloseInfo | null = null;
|
|
400
|
+
let resolveClose: (info: CursorProcessCloseInfo) => void = () => {};
|
|
401
|
+
const closePromise = new Promise<CursorProcessCloseInfo>((resolve) => {
|
|
402
|
+
resolveClose = resolve;
|
|
403
|
+
});
|
|
404
|
+
const settleClose = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
405
|
+
if (closeInfo) return;
|
|
406
|
+
closeInfo = { code, signal, stderr };
|
|
407
|
+
if (stderr.trim()) {
|
|
408
|
+
console.error(`[Cursor stderr] exit=${code}: ${stderr.trim().slice(0, 2000)}`);
|
|
463
409
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
410
|
+
resolveClose(closeInfo);
|
|
411
|
+
};
|
|
412
|
+
proc.stderr!.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });
|
|
413
|
+
proc.once("error", (err) => {
|
|
414
|
+
stderr += `${stderr ? "\n" : ""}${(err as Error).message}`;
|
|
415
|
+
settleClose(null, null);
|
|
416
|
+
});
|
|
417
|
+
proc.once("close", (code, signal) => { settleClose(code, signal); });
|
|
418
|
+
|
|
419
|
+
if (stdinText !== undefined) {
|
|
420
|
+
proc.stdin!.write(stdinText);
|
|
421
|
+
proc.stdin!.end();
|
|
468
422
|
}
|
|
423
|
+
return {
|
|
424
|
+
proc,
|
|
425
|
+
getStderr: () => stderr,
|
|
426
|
+
waitForClose: async () => {
|
|
427
|
+
if (closeInfo) return { ...closeInfo, stderr };
|
|
428
|
+
const info = await closePromise;
|
|
429
|
+
return { ...info, stderr };
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function* readJsonLines(
|
|
435
|
+
proc: ChildProcess,
|
|
436
|
+
signal?: AbortSignal,
|
|
437
|
+
debugTag?: string,
|
|
438
|
+
rawLog?: RawStreamLogHandle | null,
|
|
439
|
+
stats?: CursorStreamStats,
|
|
440
|
+
): AsyncGenerator<CursorMessageLine> {
|
|
441
|
+
const tag = debugTag ?? "cursor";
|
|
442
|
+
yield* readJsonLinesWithBadJsonIdleWatchdog<CursorMessageLine>({
|
|
443
|
+
input: proc.stdout!,
|
|
444
|
+
tool: "cursor",
|
|
445
|
+
tag,
|
|
446
|
+
signal,
|
|
447
|
+
rawLog,
|
|
448
|
+
parse: (line) => JSON.parse(line) as CursorMessageLine,
|
|
449
|
+
onRawLine: (line) => {
|
|
450
|
+
if (stats) {
|
|
451
|
+
stats.rawLineCount++;
|
|
452
|
+
stats.stdoutLength += Buffer.byteLength(line, "utf-8") + 1;
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
onParsedLine: () => {
|
|
456
|
+
if (stats) stats.parsedLineCount++;
|
|
457
|
+
},
|
|
458
|
+
onDone: (info) => {
|
|
459
|
+
console.log(`[Cursor debug] ${tag} readJsonLines done: ${info.lineCount} raw lines, signalAborted=${info.signalAborted}`);
|
|
460
|
+
},
|
|
461
|
+
});
|
|
469
462
|
}
|
|
470
463
|
|
|
471
464
|
// ---------------------------------------------------------------------------
|
|
@@ -474,28 +467,28 @@ async function* readJsonLines(
|
|
|
474
467
|
|
|
475
468
|
class CursorAdapter implements ToolAdapter {
|
|
476
469
|
readonly displayName = "Cursor";
|
|
477
|
-
readonly sessionDescPrefix = "Cursor Session:";
|
|
478
|
-
private activeProcs = new Set<ChildProcess>();
|
|
479
|
-
private metaStore: CursorSessionMetaStore;
|
|
480
|
-
private modelOverride: string | undefined;
|
|
481
|
-
private spawnImpl: CursorSpawn;
|
|
482
|
-
|
|
483
|
-
constructor(metaStore: CursorSessionMetaStore, modelOverride?: string, spawnImpl: CursorSpawn = spawn) {
|
|
484
|
-
this.metaStore = metaStore;
|
|
485
|
-
this.modelOverride = modelOverride;
|
|
486
|
-
this.spawnImpl = spawnImpl;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
490
|
-
const handle = spawnAgent(["ok"], cwd, undefined, this.modelOverride, undefined, this.spawnImpl);
|
|
491
|
-
const proc = handle.proc;
|
|
492
|
-
const stats = createCursorStreamStats();
|
|
493
|
-
this.activeProcs.add(proc);
|
|
494
|
-
|
|
495
|
-
for await (const msg of readJsonLines(proc, undefined, "createSession", null, stats)) {
|
|
496
|
-
if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
|
|
497
|
-
const sessionId = msg.session_id;
|
|
498
|
-
await this.metaStore
|
|
470
|
+
readonly sessionDescPrefix = "Cursor Session:";
|
|
471
|
+
private activeProcs = new Set<ChildProcess>();
|
|
472
|
+
private metaStore: CursorSessionMetaStore;
|
|
473
|
+
private modelOverride: string | undefined;
|
|
474
|
+
private spawnImpl: CursorSpawn;
|
|
475
|
+
|
|
476
|
+
constructor(metaStore: CursorSessionMetaStore, modelOverride?: string, spawnImpl: CursorSpawn = spawn) {
|
|
477
|
+
this.metaStore = metaStore;
|
|
478
|
+
this.modelOverride = modelOverride;
|
|
479
|
+
this.spawnImpl = spawnImpl;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
483
|
+
const handle = spawnAgent(["ok"], cwd, undefined, this.modelOverride, undefined, this.spawnImpl);
|
|
484
|
+
const proc = handle.proc;
|
|
485
|
+
const stats = createCursorStreamStats();
|
|
486
|
+
this.activeProcs.add(proc);
|
|
487
|
+
|
|
488
|
+
for await (const msg of readJsonLines(proc, undefined, "createSession", null, stats)) {
|
|
489
|
+
if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
|
|
490
|
+
const sessionId = msg.session_id;
|
|
491
|
+
await this.metaStore
|
|
499
492
|
.set(sessionId, { cwd: msg.cwd ?? cwd, model: msg.model })
|
|
500
493
|
.catch(() => {});
|
|
501
494
|
this.activeProcs.delete(proc);
|
|
@@ -503,18 +496,18 @@ class CursorAdapter implements ToolAdapter {
|
|
|
503
496
|
return { sessionId };
|
|
504
497
|
}
|
|
505
498
|
}
|
|
506
|
-
|
|
507
|
-
await killProcessTree(proc.pid);
|
|
508
|
-
this.activeProcs.delete(proc);
|
|
509
|
-
const closeInfo = await handle.waitForClose();
|
|
510
|
-
const visibleMessage = formatCursorAgentEmptyOutputMessage({
|
|
511
|
-
exitCode: closeInfo.code,
|
|
512
|
-
stdoutLength: stats.stdoutLength,
|
|
513
|
-
stderr: closeInfo.stderr,
|
|
514
|
-
});
|
|
515
|
-
if (visibleMessage) throw new Error(visibleMessage);
|
|
516
|
-
throw new Error("No session ID in Cursor init event");
|
|
517
|
-
}
|
|
499
|
+
|
|
500
|
+
await killProcessTree(proc.pid);
|
|
501
|
+
this.activeProcs.delete(proc);
|
|
502
|
+
const closeInfo = await handle.waitForClose();
|
|
503
|
+
const visibleMessage = formatCursorAgentEmptyOutputMessage({
|
|
504
|
+
exitCode: closeInfo.code,
|
|
505
|
+
stdoutLength: stats.stdoutLength,
|
|
506
|
+
stderr: closeInfo.stderr,
|
|
507
|
+
});
|
|
508
|
+
if (visibleMessage) throw new Error(visibleMessage);
|
|
509
|
+
throw new Error("No session ID in Cursor init event");
|
|
510
|
+
}
|
|
518
511
|
|
|
519
512
|
async *prompt(
|
|
520
513
|
sessionId: string,
|
|
@@ -525,46 +518,46 @@ class CursorAdapter implements ToolAdapter {
|
|
|
525
518
|
): AsyncIterable<UnifiedStreamMessage> {
|
|
526
519
|
console.log(`[Cursor debug] prompt start: sessionId=${sessionId}, cwd=${cwd}, userTextLen=${userText.length}`);
|
|
527
520
|
const cmd = parseUserCommand(userText);
|
|
528
|
-
const handle = spawnAgent(
|
|
529
|
-
["--resume", sessionId],
|
|
530
|
-
cwd,
|
|
531
|
-
buildCursorPromptText(userText),
|
|
532
|
-
this.modelOverride,
|
|
533
|
-
cmd.mode ?? undefined,
|
|
534
|
-
this.spawnImpl,
|
|
535
|
-
);
|
|
536
|
-
const proc = handle.proc;
|
|
537
|
-
this.activeProcs.add(proc);
|
|
538
|
-
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
539
|
-
|
|
540
|
-
const rawLogConfig = config.rawStreamLogs.cursor;
|
|
541
|
-
let rawLog: RawStreamLogHandle | null = null;
|
|
542
|
-
try {
|
|
543
|
-
rawLog = await createRawStreamLog({
|
|
544
|
-
enabled: rawLogConfig.enabled,
|
|
545
|
-
rootDir: RAW_STREAM_LOGS_DIR,
|
|
546
|
-
tool: "cursor",
|
|
547
|
-
sessionId,
|
|
548
|
-
label: "prompt",
|
|
549
|
-
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
550
|
-
retentionDays: rawLogConfig.retentionDays,
|
|
551
|
-
});
|
|
552
|
-
} catch (err) {
|
|
553
|
-
console.error(`[Cursor raw stream log] create failed: ${(err as Error).message}`);
|
|
554
|
-
}
|
|
521
|
+
const handle = spawnAgent(
|
|
522
|
+
["--resume", sessionId],
|
|
523
|
+
cwd,
|
|
524
|
+
buildCursorPromptText(userText),
|
|
525
|
+
this.modelOverride,
|
|
526
|
+
cmd.mode ?? undefined,
|
|
527
|
+
this.spawnImpl,
|
|
528
|
+
);
|
|
529
|
+
const proc = handle.proc;
|
|
530
|
+
this.activeProcs.add(proc);
|
|
531
|
+
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
532
|
+
|
|
533
|
+
const rawLogConfig = config.rawStreamLogs.cursor;
|
|
534
|
+
let rawLog: RawStreamLogHandle | null = null;
|
|
535
|
+
try {
|
|
536
|
+
rawLog = await createRawStreamLog({
|
|
537
|
+
enabled: rawLogConfig.enabled,
|
|
538
|
+
rootDir: RAW_STREAM_LOGS_DIR,
|
|
539
|
+
tool: "cursor",
|
|
540
|
+
sessionId,
|
|
541
|
+
label: "prompt",
|
|
542
|
+
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
543
|
+
retentionDays: rawLogConfig.retentionDays,
|
|
544
|
+
});
|
|
545
|
+
} catch (err) {
|
|
546
|
+
console.error(`[Cursor raw stream log] create failed: ${(err as Error).message}`);
|
|
547
|
+
}
|
|
555
548
|
|
|
556
549
|
// 见 codex-adapter.ts 同位置注释:spawn 用了 shell:true,必须杀整棵树,
|
|
557
550
|
// 否则 abort 后真正在跑的孙进程 cursor-agent 还会继续输出 & 占用资源。
|
|
558
|
-
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
559
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
560
|
-
let sawResult = false;
|
|
561
|
-
const stats = createCursorStreamStats();
|
|
562
|
-
|
|
563
|
-
try {
|
|
564
|
-
for await (const raw of readJsonLines(proc, signal, sessionId, rawLog, stats)) {
|
|
565
|
-
if (signal?.aborted) break;
|
|
566
|
-
if (
|
|
567
|
-
raw.type === "system" &&
|
|
551
|
+
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
552
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
553
|
+
let sawResult = false;
|
|
554
|
+
const stats = createCursorStreamStats();
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
for await (const raw of readJsonLines(proc, signal, sessionId, rawLog, stats)) {
|
|
558
|
+
if (signal?.aborted) break;
|
|
559
|
+
if (
|
|
560
|
+
raw.type === "system" &&
|
|
568
561
|
raw.subtype === "init" &&
|
|
569
562
|
raw.session_id &&
|
|
570
563
|
(raw.cwd || raw.model)
|
|
@@ -577,32 +570,32 @@ class CursorAdapter implements ToolAdapter {
|
|
|
577
570
|
if (normalized) yield normalized;
|
|
578
571
|
|
|
579
572
|
// result 是流末事件,收到后立即结束进程,防止 CLI 僵死导致 readline 挂起。
|
|
580
|
-
if (raw.type === "result") {
|
|
581
|
-
sawResult = true;
|
|
582
|
-
void killProcessTree(proc.pid);
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
if (!signal?.aborted && !sawResult) {
|
|
587
|
-
const closeInfo = await handle.waitForClose();
|
|
588
|
-
const visibleMessage = formatCursorAgentEmptyOutputMessage({
|
|
589
|
-
exitCode: closeInfo.code,
|
|
590
|
-
stdoutLength: stats.stdoutLength,
|
|
591
|
-
stderr: closeInfo.stderr,
|
|
592
|
-
});
|
|
593
|
-
if (visibleMessage) {
|
|
594
|
-
yield {
|
|
595
|
-
type: "assistant",
|
|
596
|
-
blocks: [{ type: "text_final", text: visibleMessage }],
|
|
597
|
-
};
|
|
598
|
-
throw createCursorAgentFailureError(closeInfo);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
} finally {
|
|
602
|
-
signal?.removeEventListener("abort", onAbort);
|
|
603
|
-
await killProcessTree(proc.pid);
|
|
604
|
-
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !sawResult });
|
|
605
|
-
this.activeProcs.delete(proc);
|
|
573
|
+
if (raw.type === "result") {
|
|
574
|
+
sawResult = true;
|
|
575
|
+
void killProcessTree(proc.pid);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (!signal?.aborted && !sawResult) {
|
|
580
|
+
const closeInfo = await handle.waitForClose();
|
|
581
|
+
const visibleMessage = formatCursorAgentEmptyOutputMessage({
|
|
582
|
+
exitCode: closeInfo.code,
|
|
583
|
+
stdoutLength: stats.stdoutLength,
|
|
584
|
+
stderr: closeInfo.stderr,
|
|
585
|
+
});
|
|
586
|
+
if (visibleMessage) {
|
|
587
|
+
yield {
|
|
588
|
+
type: "assistant",
|
|
589
|
+
blocks: [{ type: "text_final", text: visibleMessage }],
|
|
590
|
+
};
|
|
591
|
+
throw createCursorAgentFailureError(closeInfo);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} finally {
|
|
595
|
+
signal?.removeEventListener("abort", onAbort);
|
|
596
|
+
await killProcessTree(proc.pid);
|
|
597
|
+
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !sawResult });
|
|
598
|
+
this.activeProcs.delete(proc);
|
|
606
599
|
if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
|
|
607
600
|
console.log(`[Cursor debug] prompt end: sessionId=${sessionId}, signalAborted=${signal?.aborted ?? false}`);
|
|
608
601
|
}
|
|
@@ -627,21 +620,21 @@ class CursorAdapter implements ToolAdapter {
|
|
|
627
620
|
// 工厂函数
|
|
628
621
|
// ---------------------------------------------------------------------------
|
|
629
622
|
|
|
630
|
-
export interface CreateCursorAdapterOptions {
|
|
631
|
-
/** 注入自定义 meta 持久化 store(测试用);未提供时使用全局默认实例。 */
|
|
632
|
-
metaStore?: CursorSessionMetaStore;
|
|
633
|
-
/** per-session 模型覆盖(/model 命令);传了就用,不传走全局 cursor.model */
|
|
634
|
-
model?: string;
|
|
635
|
-
/** 注入自定义 spawn 实现(测试用)。 */
|
|
636
|
-
spawn?: CursorSpawn;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
export function createCursorAdapter(
|
|
640
|
-
options: CreateCursorAdapterOptions = {},
|
|
641
|
-
): ToolAdapter {
|
|
642
|
-
return new CursorAdapter(
|
|
643
|
-
options.metaStore ?? defaultCursorSessionMetaStore,
|
|
644
|
-
options.model,
|
|
645
|
-
options.spawn,
|
|
646
|
-
);
|
|
647
|
-
}
|
|
623
|
+
export interface CreateCursorAdapterOptions {
|
|
624
|
+
/** 注入自定义 meta 持久化 store(测试用);未提供时使用全局默认实例。 */
|
|
625
|
+
metaStore?: CursorSessionMetaStore;
|
|
626
|
+
/** per-session 模型覆盖(/model 命令);传了就用,不传走全局 cursor.model */
|
|
627
|
+
model?: string;
|
|
628
|
+
/** 注入自定义 spawn 实现(测试用)。 */
|
|
629
|
+
spawn?: CursorSpawn;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function createCursorAdapter(
|
|
633
|
+
options: CreateCursorAdapterOptions = {},
|
|
634
|
+
): ToolAdapter {
|
|
635
|
+
return new CursorAdapter(
|
|
636
|
+
options.metaStore ?? defaultCursorSessionMetaStore,
|
|
637
|
+
options.model,
|
|
638
|
+
options.spawn,
|
|
639
|
+
);
|
|
640
|
+
}
|