chatccc 0.2.195 → 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 +99 -2
- 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 +182 -57
- 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 +132 -130
- package/src/web-ui.ts +205 -205
|
@@ -28,7 +28,11 @@ import {
|
|
|
28
28
|
type CodexSessionMetaStore,
|
|
29
29
|
} from "./codex-session-meta-store.ts";
|
|
30
30
|
import { killProcessTree } from "./proc-tree-kill.ts";
|
|
31
|
-
import { config } from "../config.ts";
|
|
31
|
+
import { config, RAW_STREAM_LOGS_DIR } from "../config.ts";
|
|
32
|
+
import {
|
|
33
|
+
createRawStreamLog,
|
|
34
|
+
type RawStreamLogHandle,
|
|
35
|
+
} from "./raw-stream-log.ts";
|
|
32
36
|
|
|
33
37
|
// ---------------------------------------------------------------------------
|
|
34
38
|
// 特殊注入提示
|
|
@@ -69,9 +73,9 @@ function buildCodexPromptText(userText: string): string {
|
|
|
69
73
|
// ---------------------------------------------------------------------------
|
|
70
74
|
|
|
71
75
|
/** 可通过 config.json codex.path 自定义 Codex 可执行文件路径 */
|
|
72
|
-
function detectCodexCommand(): string {
|
|
73
|
-
return config.codex.path || "codex";
|
|
74
|
-
}
|
|
76
|
+
function detectCodexCommand(): string {
|
|
77
|
+
return config.codex.path || "codex";
|
|
78
|
+
}
|
|
75
79
|
|
|
76
80
|
/** exec 模式共用参数:JSONL 输出、绕过沙盒和确认、跳过 git 仓库检查 */
|
|
77
81
|
const CODEX_BASE_ARGS = [
|
|
@@ -183,13 +187,13 @@ export function normalizeCodexMessage(
|
|
|
183
187
|
// 子进程辅助函数
|
|
184
188
|
// ---------------------------------------------------------------------------
|
|
185
189
|
|
|
186
|
-
function spawnCodex(
|
|
187
|
-
args: string[],
|
|
188
|
-
cwd?: string,
|
|
189
|
-
stdinText?: string,
|
|
190
|
-
modelOverride?: string,
|
|
191
|
-
effortOverride?: string,
|
|
192
|
-
): ChildProcess {
|
|
190
|
+
function spawnCodex(
|
|
191
|
+
args: string[],
|
|
192
|
+
cwd?: string,
|
|
193
|
+
stdinText?: string,
|
|
194
|
+
modelOverride?: string,
|
|
195
|
+
effortOverride?: string,
|
|
196
|
+
): ChildProcess {
|
|
193
197
|
const allArgs = [...args];
|
|
194
198
|
const model = modelOverride ?? resolveCodexModel();
|
|
195
199
|
if (model) {
|
|
@@ -197,12 +201,12 @@ function spawnCodex(
|
|
|
197
201
|
const execIdx = allArgs.indexOf("exec");
|
|
198
202
|
allArgs.splice(execIdx + 1, 0, "-m", model);
|
|
199
203
|
}
|
|
200
|
-
const effort = effortOverride ?? resolveCodexEffort();
|
|
204
|
+
const effort = effortOverride ?? resolveCodexEffort();
|
|
201
205
|
if (effort) {
|
|
202
206
|
allArgs.push("-c", `model_reasoning_effort="${effort}"`);
|
|
203
207
|
}
|
|
204
208
|
|
|
205
|
-
const proc = spawn(detectCodexCommand(), allArgs, {
|
|
209
|
+
const proc = spawn(detectCodexCommand(), allArgs, {
|
|
206
210
|
cwd,
|
|
207
211
|
stdio: [stdinText !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
208
212
|
windowsHide: true,
|
|
@@ -231,6 +235,7 @@ function spawnCodex(
|
|
|
231
235
|
async function* readJsonLines(
|
|
232
236
|
proc: ChildProcess,
|
|
233
237
|
signal?: AbortSignal,
|
|
238
|
+
rawLog?: RawStreamLogHandle | null,
|
|
234
239
|
): AsyncGenerator<CodexEvent> {
|
|
235
240
|
const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
|
|
236
241
|
// abort 时主动 close readline,避免等待 Windows 管道自然关闭(可能延迟数分钟)
|
|
@@ -241,6 +246,7 @@ async function* readJsonLines(
|
|
|
241
246
|
if (signal?.aborted) break;
|
|
242
247
|
const trimmed = line.trim();
|
|
243
248
|
if (!trimmed) continue;
|
|
249
|
+
rawLog?.writeLine(trimmed);
|
|
244
250
|
try {
|
|
245
251
|
yield JSON.parse(trimmed) as CodexEvent;
|
|
246
252
|
} catch {
|
|
@@ -258,17 +264,17 @@ async function* readJsonLines(
|
|
|
258
264
|
// ---------------------------------------------------------------------------
|
|
259
265
|
|
|
260
266
|
class CodexAdapter implements ToolAdapter {
|
|
261
|
-
readonly displayName = "Codex";
|
|
262
|
-
readonly sessionDescPrefix = "Codex Session:";
|
|
263
|
-
private metaStore: CodexSessionMetaStore;
|
|
264
|
-
private modelOverride: string | undefined;
|
|
265
|
-
private effortOverride: string | undefined;
|
|
266
|
-
|
|
267
|
-
constructor(metaStore: CodexSessionMetaStore, modelOverride?: string, effortOverride?: string) {
|
|
268
|
-
this.metaStore = metaStore;
|
|
269
|
-
this.modelOverride = modelOverride;
|
|
270
|
-
this.effortOverride = effortOverride;
|
|
271
|
-
}
|
|
267
|
+
readonly displayName = "Codex";
|
|
268
|
+
readonly sessionDescPrefix = "Codex Session:";
|
|
269
|
+
private metaStore: CodexSessionMetaStore;
|
|
270
|
+
private modelOverride: string | undefined;
|
|
271
|
+
private effortOverride: string | undefined;
|
|
272
|
+
|
|
273
|
+
constructor(metaStore: CodexSessionMetaStore, modelOverride?: string, effortOverride?: string) {
|
|
274
|
+
this.metaStore = metaStore;
|
|
275
|
+
this.modelOverride = modelOverride;
|
|
276
|
+
this.effortOverride = effortOverride;
|
|
277
|
+
}
|
|
272
278
|
|
|
273
279
|
// createSession: 生成 sessionId,记录 cwd,不创建 Codex 线程(延迟到首次 prompt)
|
|
274
280
|
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
@@ -298,19 +304,37 @@ class CodexAdapter implements ToolAdapter {
|
|
|
298
304
|
? [...baseArgs, "-C", cwd, "-"]
|
|
299
305
|
: [...baseArgs, "resume", threadId, "-"];
|
|
300
306
|
|
|
301
|
-
const proc = spawnCodex(args, cwd, buildCodexPromptText(userText), this.modelOverride, this.effortOverride);
|
|
307
|
+
const proc = spawnCodex(args, cwd, buildCodexPromptText(userText), this.modelOverride, this.effortOverride);
|
|
302
308
|
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
303
309
|
|
|
310
|
+
const rawLogConfig = config.rawStreamLogs.codex;
|
|
311
|
+
let rawLog: RawStreamLogHandle | null = null;
|
|
312
|
+
try {
|
|
313
|
+
rawLog = await createRawStreamLog({
|
|
314
|
+
enabled: rawLogConfig.enabled,
|
|
315
|
+
rootDir: RAW_STREAM_LOGS_DIR,
|
|
316
|
+
tool: "codex",
|
|
317
|
+
sessionId,
|
|
318
|
+
label: "prompt",
|
|
319
|
+
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
320
|
+
retentionDays: rawLogConfig.retentionDays,
|
|
321
|
+
});
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error(`[Codex raw stream log] create failed: ${(err as Error).message}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
304
326
|
// 关键:spawn 用了 shell:true,proc.pid 指向的是壳进程(cmd.exe / sh)。
|
|
305
327
|
// 真正干活的是壳的孙子 codex.exe。普通 proc.kill() 在 Windows 上只杀第一层,
|
|
306
328
|
// 会留下幽灵 node + codex.exe 继续烧 token、stream-state 永远停在 running。
|
|
307
329
|
// 因此 abort 与 finally 都必须用 killProcessTree 整棵进程树一起收尸。
|
|
308
330
|
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
309
331
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
332
|
+
let completed = false;
|
|
310
333
|
|
|
311
334
|
try {
|
|
312
|
-
for await (const raw of readJsonLines(proc, signal)) {
|
|
335
|
+
for await (const raw of readJsonLines(proc, signal, rawLog)) {
|
|
313
336
|
if (signal?.aborted) break;
|
|
337
|
+
if (raw.type === "turn.completed") completed = true;
|
|
314
338
|
|
|
315
339
|
if (
|
|
316
340
|
isFirstPrompt &&
|
|
@@ -328,6 +352,7 @@ class CodexAdapter implements ToolAdapter {
|
|
|
328
352
|
} finally {
|
|
329
353
|
signal?.removeEventListener("abort", onAbort);
|
|
330
354
|
await killProcessTree(proc.pid);
|
|
355
|
+
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !completed });
|
|
331
356
|
if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
|
|
332
357
|
}
|
|
333
358
|
}
|
|
@@ -352,8 +377,8 @@ class CodexAdapter implements ToolAdapter {
|
|
|
352
377
|
export interface CreateCodexAdapterOptions {
|
|
353
378
|
metaStore?: CodexSessionMetaStore;
|
|
354
379
|
/** per-session 模型覆盖(/model 命令);传了就用,不传走全局 codex.model */
|
|
355
|
-
model?: string;
|
|
356
|
-
effort?: string;
|
|
380
|
+
model?: string;
|
|
381
|
+
effort?: string;
|
|
357
382
|
}
|
|
358
383
|
|
|
359
384
|
export function createCodexAdapter(
|
|
@@ -361,7 +386,7 @@ export function createCodexAdapter(
|
|
|
361
386
|
): ToolAdapter {
|
|
362
387
|
return new CodexAdapter(
|
|
363
388
|
options.metaStore ?? defaultCodexSessionMetaStore,
|
|
364
|
-
options.model,
|
|
365
|
-
options.effort,
|
|
366
|
-
);
|
|
367
|
-
}
|
|
389
|
+
options.model,
|
|
390
|
+
options.effort,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
@@ -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
|
// 特殊注入提示
|
|
@@ -118,6 +118,67 @@ interface CursorContentBlock {
|
|
|
118
118
|
[key: string]: unknown;
|
|
119
119
|
}
|
|
120
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
|
+
|
|
121
182
|
// ---------------------------------------------------------------------------
|
|
122
183
|
// normalizeCursorMessage — Cursor 消息 → UnifiedStreamMessage | null
|
|
123
184
|
// ---------------------------------------------------------------------------
|
|
@@ -304,7 +365,8 @@ function spawnAgent(
|
|
|
304
365
|
stdinText?: string,
|
|
305
366
|
modelOverride?: string,
|
|
306
367
|
mode?: "plan" | "ask",
|
|
307
|
-
|
|
368
|
+
spawnImpl: CursorSpawn = spawn,
|
|
369
|
+
): CursorProcessHandle {
|
|
308
370
|
let allArgs: string[];
|
|
309
371
|
if (mode) {
|
|
310
372
|
// plan/ask 模式:移除 --force/--yolo,添加 --mode plan/ask
|
|
@@ -323,7 +385,7 @@ function spawnAgent(
|
|
|
323
385
|
allArgs.push("--model", modelOverride);
|
|
324
386
|
}
|
|
325
387
|
}
|
|
326
|
-
const proc =
|
|
388
|
+
const proc = spawnImpl(CURSOR_AGENT_COMMAND, allArgs, {
|
|
327
389
|
cwd,
|
|
328
390
|
stdio: [stdinText !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
329
391
|
windowsHide: true,
|
|
@@ -334,26 +396,48 @@ function spawnAgent(
|
|
|
334
396
|
|
|
335
397
|
// 收集 stderr,子进程异常退出时输出到日志,方便排查静默失败
|
|
336
398
|
let stderr = "";
|
|
337
|
-
|
|
338
|
-
|
|
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 };
|
|
339
407
|
if (stderr.trim()) {
|
|
340
408
|
console.error(`[Cursor stderr] exit=${code}: ${stderr.trim().slice(0, 2000)}`);
|
|
341
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);
|
|
342
416
|
});
|
|
417
|
+
proc.once("close", (code, signal) => { settleClose(code, signal); });
|
|
343
418
|
|
|
344
419
|
if (stdinText !== undefined) {
|
|
345
420
|
proc.stdin!.write(stdinText);
|
|
346
421
|
proc.stdin!.end();
|
|
347
422
|
}
|
|
348
|
-
return
|
|
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
|
+
};
|
|
349
432
|
}
|
|
350
433
|
|
|
351
|
-
async function* readJsonLines(
|
|
352
|
-
proc: ChildProcess,
|
|
353
|
-
signal?: AbortSignal,
|
|
354
|
-
debugTag?: string,
|
|
355
|
-
rawLog?: RawStreamLogHandle | null,
|
|
356
|
-
|
|
434
|
+
async function* readJsonLines(
|
|
435
|
+
proc: ChildProcess,
|
|
436
|
+
signal?: AbortSignal,
|
|
437
|
+
debugTag?: string,
|
|
438
|
+
rawLog?: RawStreamLogHandle | null,
|
|
439
|
+
stats?: CursorStreamStats,
|
|
440
|
+
): AsyncGenerator<CursorMessageLine> {
|
|
357
441
|
const tag = debugTag ?? "cursor";
|
|
358
442
|
const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
|
|
359
443
|
// abort 时主动 close readline,避免等待 Windows 管道自然关闭(可能延迟数分钟)
|
|
@@ -364,11 +448,17 @@ async function* readJsonLines(
|
|
|
364
448
|
for await (const line of rl) {
|
|
365
449
|
if (signal?.aborted) break;
|
|
366
450
|
lineCount++;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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;
|
|
372
462
|
} catch { /* 非 JSON 行静默跳过 */ }
|
|
373
463
|
}
|
|
374
464
|
} finally {
|
|
@@ -388,17 +478,21 @@ class CursorAdapter implements ToolAdapter {
|
|
|
388
478
|
private activeProcs = new Set<ChildProcess>();
|
|
389
479
|
private metaStore: CursorSessionMetaStore;
|
|
390
480
|
private modelOverride: string | undefined;
|
|
481
|
+
private spawnImpl: CursorSpawn;
|
|
391
482
|
|
|
392
|
-
constructor(metaStore: CursorSessionMetaStore, modelOverride?: string) {
|
|
483
|
+
constructor(metaStore: CursorSessionMetaStore, modelOverride?: string, spawnImpl: CursorSpawn = spawn) {
|
|
393
484
|
this.metaStore = metaStore;
|
|
394
485
|
this.modelOverride = modelOverride;
|
|
486
|
+
this.spawnImpl = spawnImpl;
|
|
395
487
|
}
|
|
396
488
|
|
|
397
489
|
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
398
|
-
const
|
|
490
|
+
const handle = spawnAgent(["ok"], cwd, undefined, this.modelOverride, undefined, this.spawnImpl);
|
|
491
|
+
const proc = handle.proc;
|
|
492
|
+
const stats = createCursorStreamStats();
|
|
399
493
|
this.activeProcs.add(proc);
|
|
400
494
|
|
|
401
|
-
for await (const msg of readJsonLines(proc, undefined, "createSession")) {
|
|
495
|
+
for await (const msg of readJsonLines(proc, undefined, "createSession", null, stats)) {
|
|
402
496
|
if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
|
|
403
497
|
const sessionId = msg.session_id;
|
|
404
498
|
await this.metaStore
|
|
@@ -412,6 +506,13 @@ class CursorAdapter implements ToolAdapter {
|
|
|
412
506
|
|
|
413
507
|
await killProcessTree(proc.pid);
|
|
414
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);
|
|
415
516
|
throw new Error("No session ID in Cursor init event");
|
|
416
517
|
}
|
|
417
518
|
|
|
@@ -424,40 +525,43 @@ class CursorAdapter implements ToolAdapter {
|
|
|
424
525
|
): AsyncIterable<UnifiedStreamMessage> {
|
|
425
526
|
console.log(`[Cursor debug] prompt start: sessionId=${sessionId}, cwd=${cwd}, userTextLen=${userText.length}`);
|
|
426
527
|
const cmd = parseUserCommand(userText);
|
|
427
|
-
const
|
|
528
|
+
const handle = spawnAgent(
|
|
428
529
|
["--resume", sessionId],
|
|
429
530
|
cwd,
|
|
430
531
|
buildCursorPromptText(userText),
|
|
431
532
|
this.modelOverride,
|
|
432
533
|
cmd.mode ?? undefined,
|
|
534
|
+
this.spawnImpl,
|
|
433
535
|
);
|
|
536
|
+
const proc = handle.proc;
|
|
434
537
|
this.activeProcs.add(proc);
|
|
435
|
-
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
436
|
-
|
|
437
|
-
const rawLogConfig = config.rawStreamLogs.cursor;
|
|
438
|
-
let rawLog: RawStreamLogHandle | null = null;
|
|
439
|
-
try {
|
|
440
|
-
rawLog = await createRawStreamLog({
|
|
441
|
-
enabled: rawLogConfig.enabled,
|
|
442
|
-
rootDir: RAW_STREAM_LOGS_DIR,
|
|
443
|
-
tool: "cursor",
|
|
444
|
-
sessionId,
|
|
445
|
-
label: "prompt",
|
|
446
|
-
maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
|
|
447
|
-
retentionDays: rawLogConfig.retentionDays,
|
|
448
|
-
});
|
|
449
|
-
} catch (err) {
|
|
450
|
-
console.error(`[Cursor raw stream log] create failed: ${(err as Error).message}`);
|
|
451
|
-
}
|
|
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
|
+
}
|
|
452
555
|
|
|
453
556
|
// 见 codex-adapter.ts 同位置注释:spawn 用了 shell:true,必须杀整棵树,
|
|
454
557
|
// 否则 abort 后真正在跑的孙进程 cursor-agent 还会继续输出 & 占用资源。
|
|
455
|
-
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
456
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
457
|
-
let sawResult = false;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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)) {
|
|
461
565
|
if (signal?.aborted) break;
|
|
462
566
|
if (
|
|
463
567
|
raw.type === "system" &&
|
|
@@ -473,17 +577,32 @@ class CursorAdapter implements ToolAdapter {
|
|
|
473
577
|
if (normalized) yield normalized;
|
|
474
578
|
|
|
475
579
|
// result 是流末事件,收到后立即结束进程,防止 CLI 僵死导致 readline 挂起。
|
|
476
|
-
if (raw.type === "result") {
|
|
477
|
-
sawResult = true;
|
|
478
|
-
void killProcessTree(proc.pid);
|
|
580
|
+
if (raw.type === "result") {
|
|
581
|
+
sawResult = true;
|
|
582
|
+
void killProcessTree(proc.pid);
|
|
479
583
|
break;
|
|
480
584
|
}
|
|
481
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
|
+
}
|
|
482
601
|
} finally {
|
|
483
602
|
signal?.removeEventListener("abort", onAbort);
|
|
484
|
-
await killProcessTree(proc.pid);
|
|
485
|
-
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !sawResult });
|
|
486
|
-
this.activeProcs.delete(proc);
|
|
603
|
+
await killProcessTree(proc.pid);
|
|
604
|
+
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !sawResult });
|
|
605
|
+
this.activeProcs.delete(proc);
|
|
487
606
|
if (proc.pid !== undefined) options?.onProcessExit?.({ pid: proc.pid });
|
|
488
607
|
console.log(`[Cursor debug] prompt end: sessionId=${sessionId}, signalAborted=${signal?.aborted ?? false}`);
|
|
489
608
|
}
|
|
@@ -513,10 +632,16 @@ export interface CreateCursorAdapterOptions {
|
|
|
513
632
|
metaStore?: CursorSessionMetaStore;
|
|
514
633
|
/** per-session 模型覆盖(/model 命令);传了就用,不传走全局 cursor.model */
|
|
515
634
|
model?: string;
|
|
635
|
+
/** 注入自定义 spawn 实现(测试用)。 */
|
|
636
|
+
spawn?: CursorSpawn;
|
|
516
637
|
}
|
|
517
638
|
|
|
518
639
|
export function createCursorAdapter(
|
|
519
640
|
options: CreateCursorAdapterOptions = {},
|
|
520
641
|
): ToolAdapter {
|
|
521
|
-
return new CursorAdapter(
|
|
522
|
-
|
|
642
|
+
return new CursorAdapter(
|
|
643
|
+
options.metaStore ?? defaultCursorSessionMetaStore,
|
|
644
|
+
options.model,
|
|
645
|
+
options.spawn,
|
|
646
|
+
);
|
|
647
|
+
}
|