chatccc 0.2.192 → 0.2.194

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.
@@ -1,100 +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
+
15
+ import { listBuiltinContextSessions } from "./context.js";
16
+ import { resolveBuiltinSession, type BuiltinResumeRequest } from "./session-select.js";
14
17
  import { createCtrlCState } from "./sigint.js";
18
+ import type { ChatEvent, ChatSessionConfig, ChatSessionOptions } from "./index.js";
19
+
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
+ }
15
34
 
16
- // ---------------------------------------------------------------------------
17
- // 命令行参数解析
18
- // ---------------------------------------------------------------------------
35
+ interface JsonLine {
36
+ type: string;
37
+ [key: string]: unknown;
38
+ }
19
39
 
20
- function parseArgs(): { config: ChatSessionConfig; options: ChatSessionOptions } {
21
- const args = process.argv.slice(2);
40
+ function parseArgs(argv = process.argv.slice(2)): ParsedArgs {
22
41
  const config: ChatSessionConfig = {};
23
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;
24
48
 
25
- for (let i = 0; i < args.length; i++) {
26
- const arg = args[i];
27
- const next = args[i + 1];
28
- 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) {
29
53
  config.model = next;
30
54
  i++;
31
- } else if (arg === "--base-url" && next) {
55
+ } else if (arg === "--base-url" && next !== undefined) {
32
56
  config.baseURL = next;
33
57
  i++;
34
- } else if (arg === "--api-key" && next) {
58
+ } else if (arg === "--api-key" && next !== undefined) {
35
59
  config.apiKey = next;
36
60
  i++;
37
- } else if (arg === "--cwd" && next) {
61
+ } else if (arg === "--cwd" && next !== undefined) {
38
62
  options.cwd = next;
39
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++;
40
78
  } else if (arg === "--help" || arg === "-h") {
41
- printHelp();
42
- process.exit(0);
79
+ help = true;
43
80
  }
44
81
  }
45
82
 
46
- return { config, options };
83
+ return { config, options, listSessions, resume, help, streamJson, prompt };
84
+ }
85
+
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 };
47
92
  }
48
93
 
49
- function printHelp(): void {
94
+ function printHelp(appConfig: RuntimeDeps["appConfig"]): void {
50
95
  console.log([
51
- "ChatCCC 内置 Agent 终端 REPL",
96
+ "ChatCCC builtin Agent terminal REPL",
52
97
  "",
53
- "用法: npx tsx src/builtin/cli.ts [选项]",
98
+ "Usage: npx tsx src/builtin/cli.ts [options]",
54
99
  "",
55
- "选项:",
56
- ` --model <name> 模型名称(覆盖 config.ccc.model,当前默认 ${appConfig.ccc.model})`,
57
- ` --base-url <url> API 地址(覆盖 config.ccc.DEEPSEEK_BASE_URL,当前默认 ${appConfig.ccc.DEEPSEEK_BASE_URL})`,
58
- " --api-key <key> API Key(覆盖 config.ccc.DEEPSEEK_API_KEY",
59
- " --cwd <path> 工作目录",
60
- " --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",
61
110
  "",
62
- "默认配置来源:",
63
- " ~/.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",
64
113
  "",
65
114
  ].join("\n"));
66
115
  }
67
116
 
68
- // ---------------------------------------------------------------------------
69
- // ANSI 颜色
70
- // ---------------------------------------------------------------------------
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
+ }
71
268
 
72
269
  const C = {
73
270
  reset: "\x1b[0m",
74
271
  dim: "\x1b[2m",
75
272
  green: "\x1b[32m",
76
- cyan: "\x1b[36m",
77
273
  yellow: "\x1b[33m",
78
274
  };
79
275
 
80
- // ---------------------------------------------------------------------------
81
- // 主程序
82
- // ---------------------------------------------------------------------------
276
+ async function runRepl(args: ParsedArgs): Promise<void> {
277
+ const { ChatSession, appConfig } = await loadRuntime();
83
278
 
84
- async function main(): Promise<void> {
85
- const { config, options } = parseArgs();
86
-
87
- console.log(`${C.dim}ChatCCC 内置 Agent 原型${C.reset}`);
88
- console.log(`${C.dim}模型: ${config.model ?? appConfig.ccc.model}${C.reset}`);
89
- if (options.cwd) {
90
- 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);
91
286
  }
92
- 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}`);
93
293
  console.log("");
94
294
 
95
- let session: ChatSession;
295
+ let session: InstanceType<typeof ChatSession>;
96
296
  try {
97
- session = new ChatSession(config, { ...options, persist: true });
297
+ session = new ChatSession(args.config, {
298
+ ...args.options,
299
+ cwd,
300
+ persist: true,
301
+ sessionId: resolvedSession.sessionId,
302
+ });
98
303
  } catch (err) {
99
304
  console.error(`${C.yellow}${(err as Error).message}${C.reset}`);
100
305
  process.exit(1);
@@ -106,7 +311,6 @@ async function main(): Promise<void> {
106
311
  prompt: `${C.green}>${C.reset} `,
107
312
  });
108
313
 
109
- // 用于中断当前 LLM 调用的 AbortController
110
314
  let currentAbort: AbortController | null = null;
111
315
  const ctrlCState = createCtrlCState();
112
316
 
@@ -120,27 +324,25 @@ async function main(): Promise<void> {
120
324
  return;
121
325
  }
122
326
 
123
- // 特殊命令
124
327
  if (input === "exit") {
125
- console.log(`${C.dim}再见${C.reset}`);
328
+ console.log(`${C.dim}bye${C.reset}`);
126
329
  rl.close();
127
330
  return;
128
331
  }
129
332
 
130
333
  if (input === "/clear") {
131
334
  session.reset();
132
- console.log(`${C.dim}会话已重置${C.reset}`);
335
+ console.log(`${C.dim}session cleared${C.reset}`);
133
336
  rl.prompt();
134
337
  return;
135
338
  }
136
339
 
137
340
  if (input === "/history") {
138
- console.log(`${C.dim}${session.turnCount} 轮对话${C.reset}`);
341
+ console.log(`${C.dim}${session.turnCount} conversation turns${C.reset}`);
139
342
  rl.prompt();
140
343
  return;
141
344
  }
142
345
 
143
- // 发送消息
144
346
  currentAbort = new AbortController();
145
347
  const signal = currentAbort.signal;
146
348
 
@@ -148,21 +350,20 @@ async function main(): Promise<void> {
148
350
  let lastAccumulated = "";
149
351
  for await (const event of session.chat(input, signal)) {
150
352
  if (event.type === "text") {
151
- // 增量输出(仅在首次和行首时不换行)
152
353
  const newText = event.accumulated.slice(lastAccumulated.length);
153
354
  process.stdout.write(newText);
154
355
  lastAccumulated = event.accumulated;
155
356
  } else if (event.type === "done") {
156
357
  if (lastAccumulated) console.log("");
157
- console.log(`${C.dim}[完成]${C.reset}`);
358
+ console.log(`${C.dim}[done]${C.reset}`);
158
359
  } else if (event.type === "compact") {
159
- console.log(`${C.dim}[上下文已压缩:${event.compactedMessages} 条旧消息]${C.reset}`);
360
+ console.log(`${C.dim}[context compacted: ${event.compactedMessages} old messages]${C.reset}`);
160
361
  } else if (event.type === "error") {
161
- console.log(`\n${C.yellow}[错误] ${event.message}${C.reset}`);
362
+ console.log(`\n${C.yellow}[error] ${event.message}${C.reset}`);
162
363
  }
163
364
  }
164
365
  } catch (err) {
165
- console.log(`\n${C.yellow}[错误] ${(err as Error).message}${C.reset}`);
366
+ console.log(`\n${C.yellow}[error] ${(err as Error).message}${C.reset}`);
166
367
  } finally {
167
368
  currentAbort = null;
168
369
  ctrlCState.reset();
@@ -171,30 +372,29 @@ async function main(): Promise<void> {
171
372
  rl.prompt();
172
373
  });
173
374
 
174
- // Ctrl+C -> generating requires confirmation to interrupt; idle requires confirmation to exit.
175
375
  rl.on("SIGINT", () => {
176
376
  const action = ctrlCState.press(currentAbort !== null);
177
377
 
178
378
  if (action === "exit") {
179
- console.log(`\n${C.dim}再见${C.reset}`);
379
+ console.log(`\n${C.dim}bye${C.reset}`);
180
380
  rl.close();
181
381
  return;
182
382
  }
183
383
 
184
384
  if (action === "interrupt") {
185
- console.log(`\n${C.yellow}[中断中...]${C.reset}`);
385
+ console.log(`\n${C.yellow}[interrupting...]${C.reset}`);
186
386
  currentAbort?.abort();
187
387
  currentAbort = null;
188
388
  return;
189
389
  }
190
390
 
191
391
  if (action === "arm-interrupt") {
192
- console.log(`\n${C.dim}再次 Ctrl+C 中断当前回复${C.reset}`);
392
+ console.log(`\n${C.dim}Press Ctrl+C again to interrupt current response${C.reset}`);
193
393
  return;
194
394
  }
195
395
 
196
396
  if (action === "arm-exit") {
197
- console.log(`\n${C.dim}再次 Ctrl+C 退出,或输入 exit 退出${C.reset}`);
397
+ console.log(`\n${C.dim}Press Ctrl+C again to exit, or type exit${C.reset}`);
198
398
  rl.prompt();
199
399
  }
200
400
  });
@@ -205,7 +405,37 @@ async function main(): Promise<void> {
205
405
  });
206
406
  }
207
407
 
208
- main().catch((err) => {
209
- console.error(`${C.yellow}启动失败: ${(err as Error).message}${C.reset}`);
210
- process.exit(1);
211
- });
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
+ }