chatccc 0.2.194 → 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/config.sample.json +13 -7
- package/package.json +1 -1
- package/src/__tests__/builtin-chat-session.test.ts +105 -0
- package/src/__tests__/builtin-file-tools.test.ts +196 -0
- package/src/__tests__/ccc-adapter.test.ts +40 -0
- package/src/__tests__/config-reload.test.ts +6 -5
- package/src/__tests__/config-sample.test.ts +1 -1
- package/src/__tests__/cursor-adapter.test.ts +113 -16
- package/src/adapters/ccc-adapter.ts +20 -0
- package/src/adapters/cursor-adapter.ts +219 -94
- package/src/builtin/cli.ts +20 -0
- package/src/builtin/file-tools.ts +915 -0
- package/src/builtin/index.ts +129 -9
- package/src/config.ts +17 -14
- package/src/session.ts +12 -10
|
@@ -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/builtin/cli.ts
CHANGED
|
@@ -182,6 +182,21 @@ function streamJsonEvent(event: ChatEvent): void {
|
|
|
182
182
|
type: "compact",
|
|
183
183
|
compacted_messages: event.compactedMessages,
|
|
184
184
|
});
|
|
185
|
+
} else if (event.type === "tool_use") {
|
|
186
|
+
writeJsonLine({
|
|
187
|
+
type: "tool_call",
|
|
188
|
+
id: event.id,
|
|
189
|
+
name: event.name,
|
|
190
|
+
input: event.input,
|
|
191
|
+
});
|
|
192
|
+
} else if (event.type === "tool_result") {
|
|
193
|
+
writeJsonLine({
|
|
194
|
+
type: "tool_result",
|
|
195
|
+
tool_call_id: event.tool_use_id,
|
|
196
|
+
name: event.name,
|
|
197
|
+
content: event.content,
|
|
198
|
+
is_error: event.is_error,
|
|
199
|
+
});
|
|
185
200
|
} else if (event.type === "done") {
|
|
186
201
|
writeJsonLine({
|
|
187
202
|
type: "done",
|
|
@@ -358,6 +373,11 @@ async function runRepl(args: ParsedArgs): Promise<void> {
|
|
|
358
373
|
console.log(`${C.dim}[done]${C.reset}`);
|
|
359
374
|
} else if (event.type === "compact") {
|
|
360
375
|
console.log(`${C.dim}[context compacted: ${event.compactedMessages} old messages]${C.reset}`);
|
|
376
|
+
} else if (event.type === "tool_use") {
|
|
377
|
+
console.log(`\n${C.dim}[tool] ${event.name} ${stringifyConsoleArg(event.input)}${C.reset}`);
|
|
378
|
+
} else if (event.type === "tool_result") {
|
|
379
|
+
const status = event.is_error ? "error" : "ok";
|
|
380
|
+
console.log(`${C.dim}[tool result] ${event.name ?? event.tool_use_id} ${status}${C.reset}`);
|
|
361
381
|
} else if (event.type === "error") {
|
|
362
382
|
console.log(`\n${C.yellow}[error] ${event.message}${C.reset}`);
|
|
363
383
|
}
|