chatccc 0.2.195 → 0.2.196
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/package.json +1 -1
- package/src/__tests__/cursor-adapter.test.ts +113 -16
- package/src/adapters/cursor-adapter.ts +219 -94
- package/src/session.ts +12 -10
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { PassThrough } from "node:stream";
|
|
6
|
+
import type { ChildProcess } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
normalizeCursorMessage,
|
|
10
|
+
createCursorAdapter,
|
|
11
|
+
formatCursorAgentEmptyOutputMessage,
|
|
12
|
+
} from "../adapters/cursor-adapter.ts";
|
|
13
|
+
import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
|
|
7
14
|
import type {
|
|
8
15
|
CursorSessionMeta,
|
|
9
16
|
CursorSessionMetaStore,
|
|
@@ -37,12 +44,43 @@ function createInMemoryMetaStore(
|
|
|
37
44
|
};
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
|
|
49
|
+
type CursorSpawnForTest = NonNullable<
|
|
50
|
+
NonNullable<Parameters<typeof createCursorAdapter>[0]>["spawn"]
|
|
51
|
+
>;
|
|
52
|
+
|
|
53
|
+
function readFixture(name: string): unknown[] {
|
|
54
|
+
const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
|
|
55
|
+
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createMockCursorProcess(args: {
|
|
59
|
+
stdout?: string;
|
|
60
|
+
stderr?: string;
|
|
61
|
+
exitCode?: number;
|
|
62
|
+
}): ChildProcess {
|
|
63
|
+
const child = new EventEmitter() as ChildProcess;
|
|
64
|
+
const stdout = new PassThrough();
|
|
65
|
+
const stderr = new PassThrough();
|
|
66
|
+
const stdin = new PassThrough();
|
|
67
|
+
Object.assign(child, {
|
|
68
|
+
stdout,
|
|
69
|
+
stderr,
|
|
70
|
+
stdin,
|
|
71
|
+
pid: undefined,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
setImmediate(() => {
|
|
75
|
+
if (args.stdout) stdout.write(args.stdout);
|
|
76
|
+
if (args.stderr) stderr.write(args.stderr);
|
|
77
|
+
stdout.end();
|
|
78
|
+
stderr.end();
|
|
79
|
+
child.emit("close", args.exitCode ?? 0, null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return child;
|
|
83
|
+
}
|
|
46
84
|
|
|
47
85
|
// ---------------------------------------------------------------------------
|
|
48
86
|
// normalizeCursorMessage — 核心映射逻辑测试(纯函数)
|
|
@@ -642,7 +680,7 @@ describe("Cursor stream fixture - 端到端不重复", () => {
|
|
|
642
680
|
});
|
|
643
681
|
});
|
|
644
682
|
|
|
645
|
-
it("mapToolCallKey: maps known keys to readable names", () => {
|
|
683
|
+
it("mapToolCallKey: maps known keys to readable names", () => {
|
|
646
684
|
const cases: [string, string][] = [
|
|
647
685
|
["globToolCall", "Glob"],
|
|
648
686
|
["shellToolCall", "Bash"],
|
|
@@ -658,6 +696,65 @@ describe("Cursor stream fixture - 端到端不重复", () => {
|
|
|
658
696
|
tool_call: { [raw]: { args: {} } },
|
|
659
697
|
} as Parameters<typeof normalizeCursorMessage>[0]);
|
|
660
698
|
expect(r!.blocks[0]).toMatchObject({ type: "tool_use", name: expected });
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
});
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe("Cursor adapter process failures", () => {
|
|
704
|
+
it("formats empty stdout auth stderr as a cautious visible message", () => {
|
|
705
|
+
const message = formatCursorAgentEmptyOutputMessage({
|
|
706
|
+
exitCode: 1,
|
|
707
|
+
stdoutLength: 0,
|
|
708
|
+
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
expect(message).toContain("检测到认证相关错误");
|
|
712
|
+
expect(message).toContain("可能需要重新登录 Cursor Agent");
|
|
713
|
+
expect(message).toContain("CURSOR_API_KEY");
|
|
714
|
+
expect(message).toContain("agent status");
|
|
715
|
+
expect(message).toContain("agent login");
|
|
716
|
+
expect(message).not.toContain("登录态已失效");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("does not format a message when stdout is non-empty", () => {
|
|
720
|
+
expect(
|
|
721
|
+
formatCursorAgentEmptyOutputMessage({
|
|
722
|
+
exitCode: 1,
|
|
723
|
+
stdoutLength: 10,
|
|
724
|
+
stderr: "Error: Authentication required",
|
|
725
|
+
}),
|
|
726
|
+
).toBeNull();
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("prompt surfaces auth-related empty output before failing the stream", async () => {
|
|
730
|
+
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
731
|
+
const spawnImpl = (() =>
|
|
732
|
+
createMockCursorProcess({
|
|
733
|
+
exitCode: 1,
|
|
734
|
+
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
735
|
+
})) as CursorSpawnForTest;
|
|
736
|
+
const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
|
|
737
|
+
const iterator = adapter.prompt(
|
|
738
|
+
"sid",
|
|
739
|
+
"[User message]\nhello\n[/User message]",
|
|
740
|
+
"F:/repo",
|
|
741
|
+
)[Symbol.asyncIterator]();
|
|
742
|
+
|
|
743
|
+
const first = await iterator.next();
|
|
744
|
+
expect(first.done).toBe(false);
|
|
745
|
+
expect(first.value.blocks).toHaveLength(1);
|
|
746
|
+
expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
|
|
747
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
748
|
+
"text",
|
|
749
|
+
expect.stringContaining("可能需要重新登录 Cursor Agent"),
|
|
750
|
+
);
|
|
751
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
752
|
+
"text",
|
|
753
|
+
expect.not.stringContaining("登录态已失效"),
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
await expect(iterator.next()).rejects.toThrow(
|
|
757
|
+
/Cursor Agent exited without stream-json output/,
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
@@ -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,12 +115,73 @@ interface CursorContentBlock {
|
|
|
115
115
|
content?: unknown;
|
|
116
116
|
is_error?: boolean;
|
|
117
117
|
query?: string;
|
|
118
|
-
[key: string]: unknown;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
124
185
|
|
|
125
186
|
/** Cursor tool_call 内部 key → 统一工具名 */
|
|
126
187
|
function mapToolCallKey(key: string): string {
|
|
@@ -298,13 +359,14 @@ export function normalizeCursorMessage(
|
|
|
298
359
|
// 子进程辅助函数
|
|
299
360
|
// ---------------------------------------------------------------------------
|
|
300
361
|
|
|
301
|
-
function spawnAgent(
|
|
302
|
-
extraArgs: string[],
|
|
303
|
-
cwd?: string,
|
|
304
|
-
stdinText?: string,
|
|
305
|
-
modelOverride?: string,
|
|
306
|
-
mode?: "plan" | "ask",
|
|
307
|
-
|
|
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 {
|
|
308
370
|
let allArgs: string[];
|
|
309
371
|
if (mode) {
|
|
310
372
|
// plan/ask 模式:移除 --force/--yolo,添加 --mode plan/ask
|
|
@@ -323,36 +385,58 @@ function spawnAgent(
|
|
|
323
385
|
allArgs.push("--model", modelOverride);
|
|
324
386
|
}
|
|
325
387
|
}
|
|
326
|
-
const proc =
|
|
327
|
-
cwd,
|
|
328
|
-
stdio: [stdinText !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
329
|
-
windowsHide: true,
|
|
330
|
-
shell: true,
|
|
331
|
-
});
|
|
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
|
+
});
|
|
332
394
|
|
|
333
395
|
console.log(`[Cursor debug] spawn: cmd=${CURSOR_AGENT_COMMAND}, args=[${allArgs.join(", ")}], cwd=${cwd ?? "(none)"}, stdinLen=${stdinText?.length ?? 0}, pid=${proc.pid}`);
|
|
334
396
|
|
|
335
|
-
// 收集 stderr,子进程异常退出时输出到日志,方便排查静默失败
|
|
336
|
-
let stderr = "";
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
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
|
+
|
|
351
434
|
async function* readJsonLines(
|
|
352
435
|
proc: ChildProcess,
|
|
353
436
|
signal?: AbortSignal,
|
|
354
437
|
debugTag?: string,
|
|
355
438
|
rawLog?: RawStreamLogHandle | null,
|
|
439
|
+
stats?: CursorStreamStats,
|
|
356
440
|
): AsyncGenerator<CursorMessageLine> {
|
|
357
441
|
const tag = debugTag ?? "cursor";
|
|
358
442
|
const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
|
|
@@ -361,15 +445,21 @@ async function* readJsonLines(
|
|
|
361
445
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
362
446
|
let lineCount = 0;
|
|
363
447
|
try {
|
|
364
|
-
for await (const line of rl) {
|
|
365
|
-
if (signal?.aborted) break;
|
|
366
|
-
lineCount++;
|
|
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
|
+
}
|
|
367
455
|
const trimmed = line.trim();
|
|
368
456
|
if (!trimmed) continue;
|
|
369
457
|
rawLog?.writeLine(trimmed);
|
|
370
458
|
try {
|
|
371
|
-
|
|
372
|
-
|
|
459
|
+
const parsed = JSON.parse(trimmed) as CursorMessageLine;
|
|
460
|
+
if (stats) stats.parsedLineCount++;
|
|
461
|
+
yield parsed;
|
|
462
|
+
} catch { /* 非 JSON 行静默跳过 */ }
|
|
373
463
|
}
|
|
374
464
|
} finally {
|
|
375
465
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -384,24 +474,28 @@ async function* readJsonLines(
|
|
|
384
474
|
|
|
385
475
|
class CursorAdapter implements ToolAdapter {
|
|
386
476
|
readonly displayName = "Cursor";
|
|
387
|
-
readonly sessionDescPrefix = "Cursor Session:";
|
|
388
|
-
private activeProcs = new Set<ChildProcess>();
|
|
389
|
-
private metaStore: CursorSessionMetaStore;
|
|
390
|
-
private modelOverride: string | undefined;
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
this.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
405
499
|
.set(sessionId, { cwd: msg.cwd ?? cwd, model: msg.model })
|
|
406
500
|
.catch(() => {});
|
|
407
501
|
this.activeProcs.delete(proc);
|
|
@@ -409,11 +503,18 @@ class CursorAdapter implements ToolAdapter {
|
|
|
409
503
|
return { sessionId };
|
|
410
504
|
}
|
|
411
505
|
}
|
|
412
|
-
|
|
413
|
-
await killProcessTree(proc.pid);
|
|
414
|
-
this.activeProcs.delete(proc);
|
|
415
|
-
|
|
416
|
-
|
|
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
|
+
}
|
|
417
518
|
|
|
418
519
|
async *prompt(
|
|
419
520
|
sessionId: string,
|
|
@@ -424,14 +525,16 @@ 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
|
|
428
|
-
["--resume", sessionId],
|
|
429
|
-
cwd,
|
|
430
|
-
buildCursorPromptText(userText),
|
|
431
|
-
this.modelOverride,
|
|
432
|
-
cmd.mode ?? undefined,
|
|
433
|
-
|
|
434
|
-
|
|
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);
|
|
435
538
|
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
436
539
|
|
|
437
540
|
const rawLogConfig = config.rawStreamLogs.cursor;
|
|
@@ -455,12 +558,13 @@ class CursorAdapter implements ToolAdapter {
|
|
|
455
558
|
const onAbort = () => { void killProcessTree(proc.pid); };
|
|
456
559
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
457
560
|
let sawResult = false;
|
|
561
|
+
const stats = createCursorStreamStats();
|
|
458
562
|
|
|
459
563
|
try {
|
|
460
|
-
for await (const raw of readJsonLines(proc, signal, sessionId, rawLog)) {
|
|
461
|
-
if (signal?.aborted) break;
|
|
462
|
-
if (
|
|
463
|
-
raw.type === "system" &&
|
|
564
|
+
for await (const raw of readJsonLines(proc, signal, sessionId, rawLog, stats)) {
|
|
565
|
+
if (signal?.aborted) break;
|
|
566
|
+
if (
|
|
567
|
+
raw.type === "system" &&
|
|
464
568
|
raw.subtype === "init" &&
|
|
465
569
|
raw.session_id &&
|
|
466
570
|
(raw.cwd || raw.model)
|
|
@@ -476,11 +580,26 @@ class CursorAdapter implements ToolAdapter {
|
|
|
476
580
|
if (raw.type === "result") {
|
|
477
581
|
sawResult = true;
|
|
478
582
|
void killProcessTree(proc.pid);
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
|
|
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);
|
|
484
603
|
await killProcessTree(proc.pid);
|
|
485
604
|
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !sawResult });
|
|
486
605
|
this.activeProcs.delete(proc);
|
|
@@ -508,15 +627,21 @@ class CursorAdapter implements ToolAdapter {
|
|
|
508
627
|
// 工厂函数
|
|
509
628
|
// ---------------------------------------------------------------------------
|
|
510
629
|
|
|
511
|
-
export interface CreateCursorAdapterOptions {
|
|
512
|
-
/** 注入自定义 meta 持久化 store(测试用);未提供时使用全局默认实例。 */
|
|
513
|
-
metaStore?: CursorSessionMetaStore;
|
|
514
|
-
/** per-session 模型覆盖(/model 命令);传了就用,不传走全局 cursor.model */
|
|
515
|
-
model?: string;
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
+
);
|
|
522
647
|
}
|
package/src/session.ts
CHANGED
|
@@ -1168,11 +1168,12 @@ export async function runAgentSession(
|
|
|
1168
1168
|
chunkCount: 0,
|
|
1169
1169
|
};
|
|
1170
1170
|
|
|
1171
|
-
let lastFileWrite = Date.now();
|
|
1172
|
-
const FILE_WRITE_INTERVAL_MS = 2000;
|
|
1173
|
-
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
1174
|
-
|
|
1175
|
-
|
|
1171
|
+
let lastFileWrite = Date.now();
|
|
1172
|
+
const FILE_WRITE_INTERVAL_MS = 2000;
|
|
1173
|
+
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
1174
|
+
let streamErrored = false;
|
|
1175
|
+
|
|
1176
|
+
try {
|
|
1176
1177
|
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1177
1178
|
onProcessStart: (processInfo) => {
|
|
1178
1179
|
startPromptProcessMonitor(sessionId, processInfo);
|
|
@@ -1222,10 +1223,11 @@ export async function runAgentSession(
|
|
|
1222
1223
|
tool,
|
|
1223
1224
|
});
|
|
1224
1225
|
}
|
|
1225
|
-
}
|
|
1226
|
-
} catch (streamErr) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1226
|
+
}
|
|
1227
|
+
} catch (streamErr) {
|
|
1228
|
+
streamErrored = true;
|
|
1229
|
+
console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
|
|
1230
|
+
} finally {
|
|
1229
1231
|
// 标记 prompt 结束
|
|
1230
1232
|
resourceMonitor.off("stuck", onResourceStuck);
|
|
1231
1233
|
const prompt = activePrompts.get(sessionId);
|
|
@@ -1239,7 +1241,7 @@ export async function runAgentSession(
|
|
|
1239
1241
|
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
1240
1242
|
// writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
|
|
1241
1243
|
// 运行中并更新旧卡片,而不是新建卡片。
|
|
1242
|
-
const finalStatus = (wasAbnormalExit || wasResourceStuck) ? "error" : wasStopped ? "stopped" : "done";
|
|
1244
|
+
const finalStatus = (streamErrored || wasAbnormalExit || wasResourceStuck) ? "error" : wasStopped ? "stopped" : "done";
|
|
1243
1245
|
const finalReply = pickFinalReply(state).trim();
|
|
1244
1246
|
|
|
1245
1247
|
// stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
|