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.
Files changed (53) hide show
  1. package/agent-prompts/cursor_specific.md +13 -13
  2. package/bin/cccagent.mjs +17 -17
  3. package/config.sample.json +27 -27
  4. package/package.json +2 -1
  5. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -0
  6. package/src/__tests__/builtin-chat-session.test.ts +277 -181
  7. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  8. package/src/__tests__/builtin-config.test.ts +33 -33
  9. package/src/__tests__/builtin-context.test.ts +163 -163
  10. package/src/__tests__/builtin-file-tools.test.ts +224 -196
  11. package/src/__tests__/builtin-session-select.test.ts +116 -116
  12. package/src/__tests__/builtin-sigint.test.ts +56 -56
  13. package/src/__tests__/cards.test.ts +109 -109
  14. package/src/__tests__/ccc-adapter.test.ts +114 -113
  15. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  16. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  17. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  18. package/src/__tests__/claude-raw-stream-log.test.ts +87 -0
  19. package/src/__tests__/codex-raw-stream-log.test.ts +163 -0
  20. package/src/__tests__/config-reload.test.ts +10 -10
  21. package/src/__tests__/config-sample.test.ts +18 -18
  22. package/src/__tests__/cursor-adapter.test.ts +167 -113
  23. package/src/__tests__/feishu-avatar.test.ts +40 -40
  24. package/src/__tests__/jsonl-stream.test.ts +79 -0
  25. package/src/__tests__/orchestrator.test.ts +181 -154
  26. package/src/__tests__/raw-stream-log.test.ts +106 -106
  27. package/src/__tests__/session.test.ts +40 -40
  28. package/src/__tests__/sim-platform.test.ts +12 -12
  29. package/src/__tests__/web-ui.test.ts +209 -209
  30. package/src/adapters/ccc-adapter.ts +121 -119
  31. package/src/adapters/claude-adapter.ts +603 -566
  32. package/src/adapters/codex-adapter.ts +65 -52
  33. package/src/adapters/cursor-adapter.ts +269 -276
  34. package/src/adapters/jsonl-stream.ts +157 -0
  35. package/src/adapters/raw-stream-log.ts +124 -124
  36. package/src/agent-reload-config-rpc.ts +34 -0
  37. package/src/builtin/cli.ts +473 -461
  38. package/src/builtin/context.ts +323 -323
  39. package/src/builtin/file-tools.ts +1072 -915
  40. package/src/builtin/index.ts +404 -353
  41. package/src/builtin/session-select.ts +48 -48
  42. package/src/builtin/sigint.ts +50 -50
  43. package/src/cards.ts +195 -195
  44. package/src/chatgpt-subscription-rpc.ts +27 -27
  45. package/src/chatgpt-subscription.ts +299 -299
  46. package/src/chrome-devtools-guard.ts +318 -318
  47. package/src/config.ts +125 -125
  48. package/src/feishu-api.ts +49 -49
  49. package/src/index.ts +8 -13
  50. package/src/orchestrator.ts +166 -145
  51. package/src/runtime-reload.ts +34 -0
  52. package/src/session.ts +141 -141
  53. 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
- } finally {
465
- signal?.removeEventListener("abort", onAbort);
466
- rl.close();
467
- console.log(`[Cursor debug] ${tag} readJsonLines done: ${lineCount} raw lines, signalAborted=${signal?.aborted ?? false}`);
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
+ }