chatccc 0.2.191 → 0.2.193

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.
@@ -942,19 +942,29 @@ describe("getSessionStatus", () => {
942
942
  expect(status!.model).toBe(UNKNOWN_MODEL_PLACEHOLDER);
943
943
  });
944
944
 
945
- it("Cursor 会话:adapter.getSessionInfo 抛错时降级为占位符(不阻塞 /state)", async () => {
946
- mockSessionInfo("chat-cursor", { sessionId: "sid-cur", tool: "cursor" });
947
- _setAdapterForToolForTest(
948
- "cursor",
949
- mockAdapter(() => {
950
- throw new Error("simulated adapter failure");
951
- }),
952
- );
953
- const status = await getSessionStatus("chat-cursor");
954
- expect(status!.model).toBe(UNKNOWN_MODEL_PLACEHOLDER);
955
- expect(status!.effort).toBeNull();
956
- });
957
- });
945
+ it("Cursor 会话:adapter.getSessionInfo 抛错时降级为占位符(不阻塞 /state)", async () => {
946
+ mockSessionInfo("chat-cursor", { sessionId: "sid-cur", tool: "cursor" });
947
+ _setAdapterForToolForTest(
948
+ "cursor",
949
+ mockAdapter(() => {
950
+ throw new Error("simulated adapter failure");
951
+ }),
952
+ );
953
+ const status = await getSessionStatus("chat-cursor");
954
+ expect(status!.model).toBe(UNKNOWN_MODEL_PLACEHOLDER);
955
+ expect(status!.effort).toBeNull();
956
+ });
957
+
958
+ it("CCC Agent 会话:model 来自 ccc 配置且 effort 恒为 null", async () => {
959
+ mockSessionInfo("chat-ccc", { sessionId: "session-ccc", tool: "ccc" });
960
+
961
+ const status = await getSessionStatus("chat-ccc");
962
+
963
+ expect(status!.model).toBeTruthy();
964
+ expect(status!.model).not.toBe(UNKNOWN_MODEL_PLACEHOLDER);
965
+ expect(status!.effort).toBeNull();
966
+ });
967
+ });
958
968
 
959
969
  describe("getAllSessionsStatus", () => {
960
970
  let registryFile = "";
@@ -62,16 +62,21 @@ describe("SimulatedPlatform", () => {
62
62
  await expect(SimulatedPlatform.setChatAvatar("t", "ch1", "claude", "busy")).resolves.toBeUndefined();
63
63
  });
64
64
 
65
- it("纯函数 extractSessionInfo 正常工作", () => {
66
- const result = SimulatedPlatform.extractSessionInfo("Claude Code Session: a1b2c3d4-e5f6-7890-abcd-ef1234567890");
67
- expect(result).toEqual({ sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", tool: "claude" });
68
- });
69
-
70
- it("纯函数 formatDelayNotice 正常工作", () => {
65
+ it("纯函数 extractSessionInfo 正常工作", () => {
66
+ const result = SimulatedPlatform.extractSessionInfo("Claude Code Session: a1b2c3d4-e5f6-7890-abcd-ef1234567890");
67
+ expect(result).toEqual({ sessionId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", tool: "claude" });
68
+ });
69
+
70
+ it("pure extractSessionInfo recognizes ccc session ids", () => {
71
+ const result = SimulatedPlatform.extractSessionInfo("CCC Session: session-20260702-121530-a1b2c3");
72
+ expect(result).toEqual({ sessionId: "session-20260702-121530-a1b2c3", tool: "ccc" });
73
+ });
74
+
75
+ it("纯函数 formatDelayNotice 正常工作", () => {
71
76
  const notice = SimulatedPlatform.formatDelayNotice(Date.now() - 20 * 60 * 1000, "测试消息");
72
77
  expect(notice).toBeDefined();
73
78
  expect(notice).toContain("延迟送达");
74
79
  // 近期消息不触发
75
80
  expect(SimulatedPlatform.formatDelayNotice(Date.now(), "test")).toBeNull();
76
81
  });
77
- });
82
+ });
@@ -0,0 +1,99 @@
1
+ import { ChatSession, type ChatSessionConfig, type ChatSessionOptions } from "../builtin/index.ts";
2
+ import {
3
+ getBuiltinContextSession,
4
+ newBuiltinSessionId,
5
+ normalizeBuiltinSessionId,
6
+ } from "../builtin/context.ts";
7
+ import { config, CCC_SESSION_PREFIX } from "../config.ts";
8
+ import type {
9
+ CreateSessionResult,
10
+ SessionInfo,
11
+ ToolAdapter,
12
+ ToolPromptOptions,
13
+ UnifiedStreamMessage,
14
+ } from "./adapter-interface.ts";
15
+
16
+ export interface CccAdapterOptions extends ChatSessionConfig {
17
+ contextDir?: string;
18
+ compactAtTokens?: number;
19
+ keepRecentMessages?: number;
20
+ }
21
+
22
+ function toChatSessionOptions(
23
+ sessionId: string,
24
+ cwd: string,
25
+ options: CccAdapterOptions,
26
+ ): ChatSessionOptions {
27
+ return {
28
+ cwd,
29
+ persist: true,
30
+ sessionId,
31
+ contextDir: options.contextDir,
32
+ compactAtTokens: options.compactAtTokens,
33
+ keepRecentMessages: options.keepRecentMessages,
34
+ };
35
+ }
36
+
37
+ export function createCccAdapter(options: CccAdapterOptions = {}): ToolAdapter {
38
+ const chatConfig: ChatSessionConfig = {
39
+ ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),
40
+ ...(options.baseURL !== undefined ? { baseURL: options.baseURL } : {}),
41
+ ...(options.model !== undefined ? { model: options.model } : {}),
42
+ };
43
+
44
+ return {
45
+ displayName: "CCC Agent",
46
+ sessionDescPrefix: CCC_SESSION_PREFIX,
47
+
48
+ async createSession(cwd: string): Promise<CreateSessionResult> {
49
+ const sessionId = newBuiltinSessionId();
50
+ const session = new ChatSession(
51
+ chatConfig,
52
+ toChatSessionOptions(sessionId, cwd, options),
53
+ );
54
+ session.reset();
55
+ return { sessionId };
56
+ },
57
+
58
+ async *prompt(
59
+ sessionId: string,
60
+ userText: string,
61
+ cwd: string,
62
+ signal?: AbortSignal,
63
+ _promptOptions?: ToolPromptOptions,
64
+ ): AsyncIterable<UnifiedStreamMessage> {
65
+ const normalizedSessionId = normalizeBuiltinSessionId(sessionId);
66
+ const session = new ChatSession(
67
+ chatConfig,
68
+ toChatSessionOptions(normalizedSessionId, cwd, options),
69
+ );
70
+
71
+ for await (const event of session.chat(userText, signal)) {
72
+ if (event.type === "text") {
73
+ yield {
74
+ type: "assistant",
75
+ blocks: [{ type: "text", text: event.text }],
76
+ };
77
+ } else if (event.type === "error") {
78
+ throw new Error(event.message);
79
+ }
80
+ }
81
+ },
82
+
83
+ async getSessionInfo(sessionId: string): Promise<SessionInfo | undefined> {
84
+ const normalizedSessionId = normalizeBuiltinSessionId(sessionId);
85
+ const info = getBuiltinContextSession(normalizedSessionId, options.contextDir);
86
+ if (!info) return undefined;
87
+ return {
88
+ sessionId: info.sessionId,
89
+ cwd: info.cwd,
90
+ lastModified: info.updatedAt,
91
+ model: options.model ?? config.ccc.model,
92
+ };
93
+ },
94
+
95
+ async closeSession(_sessionId: string): Promise<void> {
96
+ // ChatSession uses one request-scoped stream per prompt. AbortSignal handles cancellation.
97
+ },
98
+ };
99
+ }
@@ -1,101 +1,305 @@
1
1
  /**
2
- * builtin/cli.ts ChatCCC 内置 Agent 终端 REPL
2
+ * ChatCCC builtin Agent terminal REPL and JSONL streaming entrypoint.
3
3
  *
4
- * 用法:
4
+ * Usage:
5
5
  * npx tsx src/builtin/cli.ts
6
- * npx tsx src/builtin/cli.ts --model deepseek-chat
7
- * npx tsx src/builtin/cli.ts --cwd /path/to/project
6
+ * npx tsx src/builtin/cli.ts --model deepseek-v4-pro
7
+ * npx tsx src/builtin/cli.ts --stream-json --prompt "hello"
8
8
  */
9
9
 
10
10
  import * as readline from "node:readline";
11
11
  import * as process from "node:process";
12
- import { config as appConfig } from "../config.ts";
13
- import { ChatSession, type ChatSessionConfig, type ChatSessionOptions } from "./index.js";
12
+ import { resolve as resolvePath } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
14
 
15
- // ---------------------------------------------------------------------------
16
- // 命令行参数解析
17
- // ---------------------------------------------------------------------------
15
+ import { listBuiltinContextSessions } from "./context.js";
16
+ import { resolveBuiltinSession, type BuiltinResumeRequest } from "./session-select.js";
17
+ import { createCtrlCState } from "./sigint.js";
18
+ import type { ChatEvent, ChatSessionConfig, ChatSessionOptions } from "./index.js";
18
19
 
19
- function parseArgs(): { config: ChatSessionConfig; options: ChatSessionOptions } {
20
- const args = process.argv.slice(2);
20
+ interface ParsedArgs {
21
+ config: ChatSessionConfig;
22
+ options: ChatSessionOptions;
23
+ listSessions: boolean;
24
+ resume: BuiltinResumeRequest;
25
+ help: boolean;
26
+ streamJson: boolean;
27
+ prompt: string | null;
28
+ }
29
+
30
+ interface RuntimeDeps {
31
+ ChatSession: typeof import("./index.js").ChatSession;
32
+ appConfig: typeof import("../config.ts").config;
33
+ }
34
+
35
+ interface JsonLine {
36
+ type: string;
37
+ [key: string]: unknown;
38
+ }
39
+
40
+ function parseArgs(argv = process.argv.slice(2)): ParsedArgs {
21
41
  const config: ChatSessionConfig = {};
22
42
  const options: ChatSessionOptions = {};
43
+ let listSessions = false;
44
+ let resume: BuiltinResumeRequest;
45
+ let help = false;
46
+ let streamJson = false;
47
+ let prompt: string | null = null;
23
48
 
24
- for (let i = 0; i < args.length; i++) {
25
- const arg = args[i];
26
- const next = args[i + 1];
27
- if (arg === "--model" && next) {
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const arg = argv[i];
51
+ const next = argv[i + 1];
52
+ if (arg === "--model" && next !== undefined) {
28
53
  config.model = next;
29
54
  i++;
30
- } else if (arg === "--base-url" && next) {
55
+ } else if (arg === "--base-url" && next !== undefined) {
31
56
  config.baseURL = next;
32
57
  i++;
33
- } else if (arg === "--api-key" && next) {
58
+ } else if (arg === "--api-key" && next !== undefined) {
34
59
  config.apiKey = next;
35
60
  i++;
36
- } else if (arg === "--cwd" && next) {
61
+ } else if (arg === "--cwd" && next !== undefined) {
37
62
  options.cwd = next;
38
63
  i++;
64
+ } else if (arg === "--resume") {
65
+ if (next !== undefined && !next.startsWith("--")) {
66
+ resume = next;
67
+ i++;
68
+ } else {
69
+ resume = true;
70
+ }
71
+ } else if (arg === "--list-sessions") {
72
+ listSessions = true;
73
+ } else if (arg === "--stream-json") {
74
+ streamJson = true;
75
+ } else if (arg === "--prompt" && next !== undefined) {
76
+ prompt = next;
77
+ i++;
39
78
  } else if (arg === "--help" || arg === "-h") {
40
- printHelp();
41
- process.exit(0);
79
+ help = true;
42
80
  }
43
81
  }
44
82
 
45
- return { config, options };
83
+ return { config, options, listSessions, resume, help, streamJson, prompt };
46
84
  }
47
85
 
48
- function printHelp(): void {
86
+ async function loadRuntime(): Promise<RuntimeDeps> {
87
+ const [{ ChatSession }, { config: appConfig }] = await Promise.all([
88
+ import("./index.js"),
89
+ import("../config.ts"),
90
+ ]);
91
+ return { ChatSession, appConfig };
92
+ }
93
+
94
+ function printHelp(appConfig: RuntimeDeps["appConfig"]): void {
49
95
  console.log([
50
- "ChatCCC 内置 Agent 终端 REPL",
96
+ "ChatCCC builtin Agent terminal REPL",
51
97
  "",
52
- "用法: npx tsx src/builtin/cli.ts [选项]",
98
+ "Usage: npx tsx src/builtin/cli.ts [options]",
53
99
  "",
54
- "选项:",
55
- ` --model <name> 模型名称(覆盖 config.ccc.model,当前默认 ${appConfig.ccc.model})`,
56
- ` --base-url <url> API 地址(覆盖 config.ccc.DEEPSEEK_BASE_URL,当前默认 ${appConfig.ccc.DEEPSEEK_BASE_URL})`,
57
- " --api-key <key> API Key(覆盖 config.ccc.DEEPSEEK_API_KEY",
58
- " --cwd <path> 工作目录",
59
- " --help, -h 显示帮助",
100
+ "Options:",
101
+ ` --model <name> Model name (overrides config.ccc.model, current default ${appConfig.ccc.model})`,
102
+ ` --base-url <url> API base URL (current default ${appConfig.ccc.DEEPSEEK_BASE_URL})`,
103
+ " --api-key <key> API key (overrides config.ccc.DEEPSEEK_API_KEY)",
104
+ " --cwd <path> Working directory",
105
+ " --resume [id] Resume latest cwd session, or the explicit session id",
106
+ " --list-sessions List saved ccc sessions and exit",
107
+ " --stream-json One-shot mode: write JSONL events to stdout",
108
+ " --prompt <text> Prompt text for --stream-json",
109
+ " --help, -h Show help",
60
110
  "",
61
- "默认配置来源:",
62
- " ~/.chatccc/config.json ccc.DEEPSEEK_API_KEY / ccc.DEEPSEEK_BASE_URL / ccc.model",
111
+ "Default config source:",
112
+ " ~/.chatccc/config.json ccc.DEEPSEEK_API_KEY / ccc.DEEPSEEK_BASE_URL / ccc.model",
63
113
  "",
64
114
  ].join("\n"));
65
115
  }
66
116
 
67
- // ---------------------------------------------------------------------------
68
- // ANSI 颜色
69
- // ---------------------------------------------------------------------------
117
+ function formatTime(ms: number): string {
118
+ return new Date(ms).toLocaleString();
119
+ }
120
+
121
+ function printSessions(streamJson = false): void {
122
+ const sessions = listBuiltinContextSessions();
123
+ if (streamJson) {
124
+ writeJsonLine({
125
+ type: "sessions",
126
+ sessions: sessions.map((session) => ({
127
+ session_id: session.sessionId,
128
+ turns: session.totalMessages,
129
+ compacted_messages: session.compactedMessages,
130
+ has_summary: session.hasSummary,
131
+ updated_at: session.updatedAt,
132
+ cwd: session.cwd,
133
+ })),
134
+ });
135
+ return;
136
+ }
137
+
138
+ if (sessions.length === 0) {
139
+ console.log("No saved ccc sessions");
140
+ return;
141
+ }
142
+
143
+ for (const session of sessions) {
144
+ const summary = session.hasSummary ? " summary=yes" : "";
145
+ const cwd = session.cwd ? ` cwd=${session.cwd}` : "";
146
+ console.log(`${session.sessionId} turns=${session.totalMessages} compacted=${session.compactedMessages}${summary} updated=${formatTime(session.updatedAt)}${cwd}`);
147
+ }
148
+ }
149
+
150
+ function stringifyConsoleArg(value: unknown): string {
151
+ if (typeof value === "string") return value;
152
+ if (value instanceof Error) return value.stack ?? value.message;
153
+ try {
154
+ return JSON.stringify(value);
155
+ } catch {
156
+ return String(value);
157
+ }
158
+ }
159
+
160
+ function redirectConsoleLogsToStderr(): void {
161
+ const write = (...args: unknown[]) => {
162
+ process.stderr.write(`${args.map(stringifyConsoleArg).join(" ")}\n`);
163
+ };
164
+ console.log = write;
165
+ console.info = write;
166
+ console.warn = write;
167
+ }
168
+
169
+ function writeJsonLine(event: JsonLine): void {
170
+ process.stdout.write(`${JSON.stringify(event)}\n`);
171
+ }
172
+
173
+ function streamJsonEvent(event: ChatEvent): void {
174
+ if (event.type === "text") {
175
+ writeJsonLine({
176
+ type: "text_delta",
177
+ text: event.text,
178
+ accumulated: event.accumulated,
179
+ });
180
+ } else if (event.type === "compact") {
181
+ writeJsonLine({
182
+ type: "compact",
183
+ compacted_messages: event.compactedMessages,
184
+ });
185
+ } else if (event.type === "done") {
186
+ writeJsonLine({
187
+ type: "done",
188
+ text: event.text,
189
+ });
190
+ } else if (event.type === "error") {
191
+ writeJsonLine({
192
+ type: "error",
193
+ message: event.message,
194
+ });
195
+ }
196
+ }
197
+
198
+ async function readPromptFromStdin(): Promise<string> {
199
+ const chunks: Buffer[] = [];
200
+ for await (const chunk of process.stdin) {
201
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
202
+ }
203
+ return Buffer.concat(chunks).toString("utf8");
204
+ }
205
+
206
+ async function runStreamJson(args: ParsedArgs): Promise<number> {
207
+ redirectConsoleLogsToStderr();
208
+
209
+ if (args.listSessions) {
210
+ printSessions(true);
211
+ return 0;
212
+ }
213
+
214
+ const prompt = args.prompt ?? (!process.stdin.isTTY ? await readPromptFromStdin() : "");
215
+ if (!prompt.trim()) {
216
+ writeJsonLine({ type: "error", message: "--stream-json requires --prompt <text> or stdin input" });
217
+ return 1;
218
+ }
219
+
220
+ let runtime: RuntimeDeps;
221
+ try {
222
+ runtime = await loadRuntime();
223
+ } catch (err) {
224
+ writeJsonLine({ type: "error", message: (err as Error).message });
225
+ return 1;
226
+ }
227
+
228
+ const cwd = resolvePath(args.options.cwd ?? process.cwd());
229
+ let resolvedSession;
230
+ try {
231
+ resolvedSession = resolveBuiltinSession({ cwd, resume: args.resume });
232
+ } catch (err) {
233
+ writeJsonLine({ type: "error", message: (err as Error).message });
234
+ return 1;
235
+ }
236
+
237
+ let session: InstanceType<RuntimeDeps["ChatSession"]>;
238
+ try {
239
+ session = new runtime.ChatSession(args.config, {
240
+ ...args.options,
241
+ cwd,
242
+ persist: true,
243
+ sessionId: resolvedSession.sessionId,
244
+ });
245
+ } catch (err) {
246
+ writeJsonLine({ type: "error", message: (err as Error).message });
247
+ return 1;
248
+ }
249
+
250
+ writeJsonLine({
251
+ type: "start",
252
+ session_id: resolvedSession.sessionId,
253
+ mode: resolvedSession.mode,
254
+ cwd,
255
+ model: args.config.model ?? runtime.appConfig.ccc.model,
256
+ });
257
+
258
+ try {
259
+ for await (const event of session.chat(prompt)) {
260
+ streamJsonEvent(event);
261
+ }
262
+ return 0;
263
+ } catch (err) {
264
+ writeJsonLine({ type: "error", message: (err as Error).message });
265
+ return 1;
266
+ }
267
+ }
70
268
 
71
269
  const C = {
72
270
  reset: "\x1b[0m",
73
271
  dim: "\x1b[2m",
74
272
  green: "\x1b[32m",
75
- cyan: "\x1b[36m",
76
273
  yellow: "\x1b[33m",
77
274
  };
78
275
 
79
- const DOUBLE_CTRL_C_EXIT_WINDOW_MS = 2000;
80
-
81
- // ---------------------------------------------------------------------------
82
- // 主程序
83
- // ---------------------------------------------------------------------------
276
+ async function runRepl(args: ParsedArgs): Promise<void> {
277
+ const { ChatSession, appConfig } = await loadRuntime();
84
278
 
85
- async function main(): Promise<void> {
86
- const { config, options } = parseArgs();
87
-
88
- console.log(`${C.dim}ChatCCC 内置 Agent 原型${C.reset}`);
89
- console.log(`${C.dim}模型: ${config.model ?? appConfig.ccc.model}${C.reset}`);
90
- if (options.cwd) {
91
- console.log(`${C.dim}目录: ${options.cwd}${C.reset}`);
279
+ const cwd = resolvePath(args.options.cwd ?? process.cwd());
280
+ let resolvedSession;
281
+ try {
282
+ resolvedSession = resolveBuiltinSession({ cwd, resume: args.resume });
283
+ } catch (err) {
284
+ console.error(`${C.yellow}${(err as Error).message}${C.reset}`);
285
+ process.exit(1);
92
286
  }
93
- console.log(`${C.dim}输入消息开始对话,Ctrl+C 中断当前回复,exit 退出${C.reset}`);
287
+
288
+ console.log(`${C.dim}ChatCCC builtin Agent${C.reset}`);
289
+ console.log(`${C.dim}Model: ${args.config.model ?? appConfig.ccc.model}${C.reset}`);
290
+ console.log(`${C.dim}Directory: ${cwd}${C.reset}`);
291
+ console.log(`${C.dim}Session: ${resolvedSession.sessionId} (${resolvedSession.mode === "new" ? "new" : "resumed"})${C.reset}`);
292
+ console.log(`${C.dim}Type a message to chat. Double Ctrl+C interrupts generation or exits. Type exit to quit.${C.reset}`);
94
293
  console.log("");
95
294
 
96
- let session: ChatSession;
295
+ let session: InstanceType<typeof ChatSession>;
97
296
  try {
98
- session = new ChatSession(config, options);
297
+ session = new ChatSession(args.config, {
298
+ ...args.options,
299
+ cwd,
300
+ persist: true,
301
+ sessionId: resolvedSession.sessionId,
302
+ });
99
303
  } catch (err) {
100
304
  console.error(`${C.yellow}${(err as Error).message}${C.reset}`);
101
305
  process.exit(1);
@@ -107,41 +311,38 @@ async function main(): Promise<void> {
107
311
  prompt: `${C.green}>${C.reset} `,
108
312
  });
109
313
 
110
- // 用于中断当前 LLM 调用的 AbortController
111
314
  let currentAbort: AbortController | null = null;
112
- let lastCtrlCAt = 0;
315
+ const ctrlCState = createCtrlCState();
113
316
 
114
317
  rl.prompt();
115
318
 
116
319
  rl.on("line", async (line: string) => {
117
- lastCtrlCAt = 0;
320
+ ctrlCState.reset();
118
321
  const input = line.trim();
119
322
  if (!input) {
120
323
  rl.prompt();
121
324
  return;
122
325
  }
123
326
 
124
- // 特殊命令
125
327
  if (input === "exit") {
126
- console.log(`${C.dim}再见${C.reset}`);
328
+ console.log(`${C.dim}bye${C.reset}`);
127
329
  rl.close();
128
330
  return;
129
331
  }
130
332
 
131
333
  if (input === "/clear") {
132
334
  session.reset();
133
- console.log(`${C.dim}会话已重置${C.reset}`);
335
+ console.log(`${C.dim}session cleared${C.reset}`);
134
336
  rl.prompt();
135
337
  return;
136
338
  }
137
339
 
138
340
  if (input === "/history") {
139
- console.log(`${C.dim}${session.turnCount} 轮对话${C.reset}`);
341
+ console.log(`${C.dim}${session.turnCount} conversation turns${C.reset}`);
140
342
  rl.prompt();
141
343
  return;
142
344
  }
143
345
 
144
- // 发送消息
145
346
  currentAbort = new AbortController();
146
347
  const signal = currentAbort.signal;
147
348
 
@@ -149,45 +350,51 @@ async function main(): Promise<void> {
149
350
  let lastAccumulated = "";
150
351
  for await (const event of session.chat(input, signal)) {
151
352
  if (event.type === "text") {
152
- // 增量输出(仅在首次和行首时不换行)
153
353
  const newText = event.accumulated.slice(lastAccumulated.length);
154
354
  process.stdout.write(newText);
155
355
  lastAccumulated = event.accumulated;
156
356
  } else if (event.type === "done") {
157
357
  if (lastAccumulated) console.log("");
158
- console.log(`${C.dim}[完成]${C.reset}`);
358
+ console.log(`${C.dim}[done]${C.reset}`);
359
+ } else if (event.type === "compact") {
360
+ console.log(`${C.dim}[context compacted: ${event.compactedMessages} old messages]${C.reset}`);
159
361
  } else if (event.type === "error") {
160
- console.log(`\n${C.yellow}[错误] ${event.message}${C.reset}`);
362
+ console.log(`\n${C.yellow}[error] ${event.message}${C.reset}`);
161
363
  }
162
364
  }
163
365
  } catch (err) {
164
- console.log(`\n${C.yellow}[错误] ${(err as Error).message}${C.reset}`);
366
+ console.log(`\n${C.yellow}[error] ${(err as Error).message}${C.reset}`);
165
367
  } finally {
166
368
  currentAbort = null;
369
+ ctrlCState.reset();
167
370
  }
168
371
 
169
372
  rl.prompt();
170
373
  });
171
374
 
172
- // Ctrl+C → 生成中中断;空闲或连续按下时退出
173
375
  rl.on("SIGINT", () => {
174
- const now = Date.now();
175
- const shouldExit = now - lastCtrlCAt <= DOUBLE_CTRL_C_EXIT_WINDOW_MS;
376
+ const action = ctrlCState.press(currentAbort !== null);
176
377
 
177
- if (shouldExit) {
178
- console.log(`\n${C.dim}再见${C.reset}`);
378
+ if (action === "exit") {
379
+ console.log(`\n${C.dim}bye${C.reset}`);
179
380
  rl.close();
180
381
  return;
181
382
  }
182
383
 
183
- lastCtrlCAt = now;
184
-
185
- if (currentAbort) {
186
- console.log(`\n${C.yellow}[中断中...]${C.reset} ${C.dim}再次 Ctrl+C 退出${C.reset}`);
187
- currentAbort.abort();
384
+ if (action === "interrupt") {
385
+ console.log(`\n${C.yellow}[interrupting...]${C.reset}`);
386
+ currentAbort?.abort();
188
387
  currentAbort = null;
189
- } else {
190
- console.log(`\n${C.dim}再次 Ctrl+C 退出,或输入 exit 退出${C.reset}`);
388
+ return;
389
+ }
390
+
391
+ if (action === "arm-interrupt") {
392
+ console.log(`\n${C.dim}Press Ctrl+C again to interrupt current response${C.reset}`);
393
+ return;
394
+ }
395
+
396
+ if (action === "arm-exit") {
397
+ console.log(`\n${C.dim}Press Ctrl+C again to exit, or type exit${C.reset}`);
191
398
  rl.prompt();
192
399
  }
193
400
  });
@@ -198,7 +405,37 @@ async function main(): Promise<void> {
198
405
  });
199
406
  }
200
407
 
201
- main().catch((err) => {
202
- console.error(`${C.yellow}启动失败: ${(err as Error).message}${C.reset}`);
203
- process.exit(1);
204
- });
408
+ async function main(): Promise<void> {
409
+ const args = parseArgs();
410
+
411
+ if (args.streamJson) {
412
+ const code = await runStreamJson(args);
413
+ process.exit(code);
414
+ }
415
+
416
+ if (args.help) {
417
+ const { appConfig } = await loadRuntime();
418
+ printHelp(appConfig);
419
+ return;
420
+ }
421
+
422
+ if (args.listSessions) {
423
+ printSessions();
424
+ return;
425
+ }
426
+
427
+ await runRepl(args);
428
+ }
429
+
430
+ function isDirectCliInvocation(): boolean {
431
+ const current = resolvePath(fileURLToPath(import.meta.url));
432
+ const invoked = process.argv[1] ? resolvePath(process.argv[1]) : "";
433
+ return current === invoked;
434
+ }
435
+
436
+ if (isDirectCliInvocation()) {
437
+ main().catch((err) => {
438
+ console.error(`${C.yellow}startup failed: ${(err as Error).message}${C.reset}`);
439
+ process.exit(1);
440
+ });
441
+ }