chatccc 0.2.196 → 0.2.197
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 +1 -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 +120 -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 +113 -113
- package/src/__tests__/feishu-avatar.test.ts +40 -40
- 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 +57 -32
- package/src/adapters/cursor-adapter.ts +264 -264
- 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
|
@@ -20,16 +20,16 @@ import type {
|
|
|
20
20
|
SessionInfo,
|
|
21
21
|
} from "./adapter-interface.ts";
|
|
22
22
|
import { parseUserCommand } from "./adapter-interface.ts";
|
|
23
|
-
import { config, CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS, RAW_STREAM_LOGS_DIR } from "../config.ts";
|
|
23
|
+
import { config, CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS, RAW_STREAM_LOGS_DIR } from "../config.ts";
|
|
24
24
|
import {
|
|
25
25
|
defaultCursorSessionMetaStore,
|
|
26
26
|
type CursorSessionMetaStore,
|
|
27
27
|
} 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";
|
|
28
|
+
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
29
|
+
import {
|
|
30
|
+
createRawStreamLog,
|
|
31
|
+
type RawStreamLogHandle,
|
|
32
|
+
} from "./raw-stream-log.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,59 +385,59 @@ 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> {
|
|
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
441
|
const tag = debugTag ?? "cursor";
|
|
442
442
|
const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
|
|
443
443
|
// abort 时主动 close readline,避免等待 Windows 管道自然关闭(可能延迟数分钟)
|
|
@@ -445,21 +445,21 @@ async function* readJsonLines(
|
|
|
445
445
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
446
446
|
let lineCount = 0;
|
|
447
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 行静默跳过 */ }
|
|
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 行静默跳过 */ }
|
|
463
463
|
}
|
|
464
464
|
} finally {
|
|
465
465
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -474,28 +474,28 @@ async function* readJsonLines(
|
|
|
474
474
|
|
|
475
475
|
class CursorAdapter implements ToolAdapter {
|
|
476
476
|
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
|
|
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
|
|
499
499
|
.set(sessionId, { cwd: msg.cwd ?? cwd, model: msg.model })
|
|
500
500
|
.catch(() => {});
|
|
501
501
|
this.activeProcs.delete(proc);
|
|
@@ -503,18 +503,18 @@ class CursorAdapter implements ToolAdapter {
|
|
|
503
503
|
return { sessionId };
|
|
504
504
|
}
|
|
505
505
|
}
|
|
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
|
-
}
|
|
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
|
+
}
|
|
518
518
|
|
|
519
519
|
async *prompt(
|
|
520
520
|
sessionId: string,
|
|
@@ -525,46 +525,46 @@ class CursorAdapter implements ToolAdapter {
|
|
|
525
525
|
): AsyncIterable<UnifiedStreamMessage> {
|
|
526
526
|
console.log(`[Cursor debug] prompt start: sessionId=${sessionId}, cwd=${cwd}, userTextLen=${userText.length}`);
|
|
527
527
|
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
|
-
}
|
|
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
|
+
}
|
|
555
555
|
|
|
556
556
|
// 见 codex-adapter.ts 同位置注释:spawn 用了 shell:true,必须杀整棵树,
|
|
557
557
|
// 否则 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" &&
|
|
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" &&
|
|
568
568
|
raw.subtype === "init" &&
|
|
569
569
|
raw.session_id &&
|
|
570
570
|
(raw.cwd || raw.model)
|
|
@@ -577,32 +577,32 @@ class CursorAdapter implements ToolAdapter {
|
|
|
577
577
|
if (normalized) yield normalized;
|
|
578
578
|
|
|
579
579
|
// 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);
|
|
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);
|
|
606
606
|
if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
|
|
607
607
|
console.log(`[Cursor debug] prompt end: sessionId=${sessionId}, signalAborted=${signal?.aborted ?? false}`);
|
|
608
608
|
}
|
|
@@ -627,21 +627,21 @@ class CursorAdapter implements ToolAdapter {
|
|
|
627
627
|
// 工厂函数
|
|
628
628
|
// ---------------------------------------------------------------------------
|
|
629
629
|
|
|
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
|
-
}
|
|
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
|
+
}
|