chatccc 0.2.198 → 0.2.199

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 (52) 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 +1 -1
  5. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  6. package/src/__tests__/builtin-chat-session.test.ts +277 -277
  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 -224
  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 -114
  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 -87
  19. package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
  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 +66 -7
  23. package/src/__tests__/feishu-avatar.test.ts +40 -40
  24. package/src/__tests__/jsonl-stream.test.ts +79 -79
  25. package/src/__tests__/orchestrator.test.ts +200 -200
  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 -121
  31. package/src/adapters/claude-adapter.ts +603 -603
  32. package/src/adapters/codex-adapter.ts +380 -380
  33. package/src/adapters/cursor-adapter.ts +39 -6
  34. package/src/adapters/jsonl-stream.ts +157 -157
  35. package/src/adapters/raw-stream-log.ts +124 -124
  36. package/src/agent-reload-config-rpc.ts +34 -34
  37. package/src/builtin/cli.ts +473 -473
  38. package/src/builtin/context.ts +323 -323
  39. package/src/builtin/file-tools.ts +1072 -1072
  40. package/src/builtin/index.ts +404 -404
  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/orchestrator.ts +179 -179
  50. package/src/runtime-reload.ts +34 -34
  51. package/src/session.ts +141 -141
  52. package/src/web-ui.ts +205 -205
@@ -1,473 +1,473 @@
1
- /**
2
- * ChatCCC builtin Agent terminal REPL and JSONL streaming entrypoint.
3
- *
4
- * Usage:
5
- * npx tsx src/builtin/cli.ts
6
- * npx tsx src/builtin/cli.ts --model deepseek-v4-pro
7
- * npx tsx src/builtin/cli.ts --stream-json --prompt "hello"
8
- */
9
-
10
- import * as readline from "node:readline";
11
- import * as process from "node:process";
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";
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
- }
34
-
35
- interface JsonLine {
36
- type: string;
37
- [key: string]: unknown;
38
- }
39
-
40
- function parsePositiveIntegerOption(name: string, value: string): number {
41
- const parsed = Number(value);
42
- if (!Number.isInteger(parsed) || parsed <= 0) {
43
- throw new Error(`${name} must be a positive integer`);
44
- }
45
- return parsed;
46
- }
47
-
48
- function parseArgs(argv = process.argv.slice(2)): ParsedArgs {
49
- const config: ChatSessionConfig = {};
50
- const options: ChatSessionOptions = {};
51
- let listSessions = false;
52
- let resume: BuiltinResumeRequest;
53
- let help = false;
54
- let streamJson = false;
55
- let prompt: string | null = null;
56
-
57
- for (let i = 0; i < argv.length; i++) {
58
- const arg = argv[i];
59
- const next = argv[i + 1];
60
- if (arg === "--model" && next !== undefined) {
61
- config.model = next;
62
- i++;
63
- } else if (arg === "--base-url" && next !== undefined) {
64
- config.baseURL = next;
65
- i++;
66
- } else if (arg === "--api-key" && next !== undefined) {
67
- config.apiKey = next;
68
- i++;
69
- } else if (arg === "--cwd" && next !== undefined) {
70
- options.cwd = next;
71
- i++;
72
- } else if (arg === "--max-steps" && next !== undefined) {
73
- options.maxSteps = parsePositiveIntegerOption("--max-steps", next);
74
- i++;
75
- } else if (arg === "--resume") {
76
- if (next !== undefined && !next.startsWith("--")) {
77
- resume = next;
78
- i++;
79
- } else {
80
- resume = true;
81
- }
82
- } else if (arg === "--list-sessions") {
83
- listSessions = true;
84
- } else if (arg === "--stream-json") {
85
- streamJson = true;
86
- } else if (arg === "--prompt" && next !== undefined) {
87
- prompt = next;
88
- i++;
89
- } else if (arg === "--help" || arg === "-h") {
90
- help = true;
91
- }
92
- }
93
-
94
- return { config, options, listSessions, resume, help, streamJson, prompt };
95
- }
96
-
97
- async function loadRuntime(): Promise<RuntimeDeps> {
98
- const [{ ChatSession }, { config: appConfig }] = await Promise.all([
99
- import("./index.js"),
100
- import("../config.ts"),
101
- ]);
102
- return { ChatSession, appConfig };
103
- }
104
-
105
- function printHelp(appConfig: RuntimeDeps["appConfig"]): void {
106
- console.log([
107
- "ChatCCC builtin Agent terminal REPL",
108
- "",
109
- "Usage: npx tsx src/builtin/cli.ts [options]",
110
- "",
111
- "Options:",
112
- ` --model <name> Model name (overrides config.ccc.model, current default ${appConfig.ccc.model})`,
113
- ` --base-url <url> API base URL (current default ${appConfig.ccc.DEEPSEEK_BASE_URL})`,
114
- " --api-key <key> API key (overrides config.ccc.DEEPSEEK_API_KEY)",
115
- " --cwd <path> Working directory",
116
- " --max-steps <n> Optional tool-step limit. Omit for no step limit",
117
- " --resume [id] Resume latest cwd session, or the explicit session id",
118
- " --list-sessions List saved ccc sessions and exit",
119
- " --stream-json One-shot mode: write JSONL events to stdout",
120
- " --prompt <text> Prompt text for --stream-json",
121
- " --help, -h Show help",
122
- "",
123
- "Default config source:",
124
- " ~/.chatccc/config.json ccc.DEEPSEEK_API_KEY / ccc.DEEPSEEK_BASE_URL / ccc.model",
125
- "",
126
- ].join("\n"));
127
- }
128
-
129
- function formatTime(ms: number): string {
130
- return new Date(ms).toLocaleString();
131
- }
132
-
133
- function printSessions(streamJson = false): void {
134
- const sessions = listBuiltinContextSessions();
135
- if (streamJson) {
136
- writeJsonLine({
137
- type: "sessions",
138
- sessions: sessions.map((session) => ({
139
- session_id: session.sessionId,
140
- turns: session.totalMessages,
141
- compacted_messages: session.compactedMessages,
142
- has_summary: session.hasSummary,
143
- updated_at: session.updatedAt,
144
- cwd: session.cwd,
145
- })),
146
- });
147
- return;
148
- }
149
-
150
- if (sessions.length === 0) {
151
- console.log("No saved ccc sessions");
152
- return;
153
- }
154
-
155
- for (const session of sessions) {
156
- const summary = session.hasSummary ? " summary=yes" : "";
157
- const cwd = session.cwd ? ` cwd=${session.cwd}` : "";
158
- console.log(`${session.sessionId} turns=${session.totalMessages} compacted=${session.compactedMessages}${summary} updated=${formatTime(session.updatedAt)}${cwd}`);
159
- }
160
- }
161
-
162
- function stringifyConsoleArg(value: unknown): string {
163
- if (typeof value === "string") return value;
164
- if (value instanceof Error) return value.stack ?? value.message;
165
- try {
166
- return JSON.stringify(value);
167
- } catch {
168
- return String(value);
169
- }
170
- }
171
-
172
- function redirectConsoleLogsToStderr(): void {
173
- const write = (...args: unknown[]) => {
174
- process.stderr.write(`${args.map(stringifyConsoleArg).join(" ")}\n`);
175
- };
176
- console.log = write;
177
- console.info = write;
178
- console.warn = write;
179
- }
180
-
181
- function writeJsonLine(event: JsonLine): void {
182
- process.stdout.write(`${JSON.stringify(event)}\n`);
183
- }
184
-
185
- function streamJsonEvent(event: ChatEvent): void {
186
- if (event.type === "text") {
187
- writeJsonLine({
188
- type: "text_delta",
189
- text: event.text,
190
- accumulated: event.accumulated,
191
- });
192
- } else if (event.type === "compact") {
193
- writeJsonLine({
194
- type: "compact",
195
- compacted_messages: event.compactedMessages,
196
- });
197
- } else if (event.type === "tool_use") {
198
- writeJsonLine({
199
- type: "tool_call",
200
- id: event.id,
201
- name: event.name,
202
- input: event.input,
203
- });
204
- } else if (event.type === "tool_result") {
205
- writeJsonLine({
206
- type: "tool_result",
207
- tool_call_id: event.tool_use_id,
208
- name: event.name,
209
- content: event.content,
210
- is_error: event.is_error,
211
- });
212
- } else if (event.type === "done") {
213
- writeJsonLine({
214
- type: "done",
215
- text: event.text,
216
- });
217
- } else if (event.type === "error") {
218
- writeJsonLine({
219
- type: "error",
220
- message: event.message,
221
- });
222
- }
223
- }
224
-
225
- async function readPromptFromStdin(): Promise<string> {
226
- const chunks: Buffer[] = [];
227
- for await (const chunk of process.stdin) {
228
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
229
- }
230
- return Buffer.concat(chunks).toString("utf8");
231
- }
232
-
233
- async function runStreamJson(args: ParsedArgs): Promise<number> {
234
- redirectConsoleLogsToStderr();
235
-
236
- if (args.listSessions) {
237
- printSessions(true);
238
- return 0;
239
- }
240
-
241
- const prompt = args.prompt ?? (!process.stdin.isTTY ? await readPromptFromStdin() : "");
242
- if (!prompt.trim()) {
243
- writeJsonLine({ type: "error", message: "--stream-json requires --prompt <text> or stdin input" });
244
- return 1;
245
- }
246
-
247
- let runtime: RuntimeDeps;
248
- try {
249
- runtime = await loadRuntime();
250
- } catch (err) {
251
- writeJsonLine({ type: "error", message: (err as Error).message });
252
- return 1;
253
- }
254
-
255
- const cwd = resolvePath(args.options.cwd ?? process.cwd());
256
- let resolvedSession;
257
- try {
258
- resolvedSession = resolveBuiltinSession({ cwd, resume: args.resume });
259
- } catch (err) {
260
- writeJsonLine({ type: "error", message: (err as Error).message });
261
- return 1;
262
- }
263
-
264
- let session: InstanceType<RuntimeDeps["ChatSession"]>;
265
- try {
266
- session = new runtime.ChatSession(args.config, {
267
- ...args.options,
268
- cwd,
269
- persist: true,
270
- sessionId: resolvedSession.sessionId,
271
- });
272
- } catch (err) {
273
- writeJsonLine({ type: "error", message: (err as Error).message });
274
- return 1;
275
- }
276
-
277
- writeJsonLine({
278
- type: "start",
279
- session_id: resolvedSession.sessionId,
280
- mode: resolvedSession.mode,
281
- cwd,
282
- model: args.config.model ?? runtime.appConfig.ccc.model,
283
- });
284
-
285
- try {
286
- for await (const event of session.chat(prompt)) {
287
- streamJsonEvent(event);
288
- }
289
- return 0;
290
- } catch (err) {
291
- writeJsonLine({ type: "error", message: (err as Error).message });
292
- return 1;
293
- }
294
- }
295
-
296
- const C = {
297
- reset: "\x1b[0m",
298
- dim: "\x1b[2m",
299
- green: "\x1b[32m",
300
- yellow: "\x1b[33m",
301
- };
302
-
303
- async function runRepl(args: ParsedArgs): Promise<void> {
304
- const { ChatSession, appConfig } = await loadRuntime();
305
-
306
- const cwd = resolvePath(args.options.cwd ?? process.cwd());
307
- let resolvedSession;
308
- try {
309
- resolvedSession = resolveBuiltinSession({ cwd, resume: args.resume });
310
- } catch (err) {
311
- console.error(`${C.yellow}${(err as Error).message}${C.reset}`);
312
- process.exit(1);
313
- }
314
-
315
- console.log(`${C.dim}ChatCCC builtin Agent${C.reset}`);
316
- console.log(`${C.dim}Model: ${args.config.model ?? appConfig.ccc.model}${C.reset}`);
317
- console.log(`${C.dim}Directory: ${cwd}${C.reset}`);
318
- console.log(`${C.dim}Session: ${resolvedSession.sessionId} (${resolvedSession.mode === "new" ? "new" : "resumed"})${C.reset}`);
319
- console.log(`${C.dim}Type a message to chat. Double Ctrl+C interrupts generation or exits. Type exit to quit.${C.reset}`);
320
- console.log("");
321
-
322
- let session: InstanceType<typeof ChatSession>;
323
- try {
324
- session = new ChatSession(args.config, {
325
- ...args.options,
326
- cwd,
327
- persist: true,
328
- sessionId: resolvedSession.sessionId,
329
- });
330
- } catch (err) {
331
- console.error(`${C.yellow}${(err as Error).message}${C.reset}`);
332
- process.exit(1);
333
- }
334
-
335
- const rl = readline.createInterface({
336
- input: process.stdin,
337
- output: process.stdout,
338
- prompt: `${C.green}>${C.reset} `,
339
- });
340
-
341
- let currentAbort: AbortController | null = null;
342
- const ctrlCState = createCtrlCState();
343
-
344
- rl.prompt();
345
-
346
- rl.on("line", async (line: string) => {
347
- ctrlCState.reset();
348
- const input = line.trim();
349
- if (!input) {
350
- rl.prompt();
351
- return;
352
- }
353
-
354
- if (input === "exit") {
355
- console.log(`${C.dim}bye${C.reset}`);
356
- rl.close();
357
- return;
358
- }
359
-
360
- if (input === "/clear") {
361
- session.reset();
362
- console.log(`${C.dim}session cleared${C.reset}`);
363
- rl.prompt();
364
- return;
365
- }
366
-
367
- if (input === "/history") {
368
- console.log(`${C.dim}${session.turnCount} conversation turns${C.reset}`);
369
- rl.prompt();
370
- return;
371
- }
372
-
373
- currentAbort = new AbortController();
374
- const signal = currentAbort.signal;
375
-
376
- try {
377
- let lastAccumulated = "";
378
- for await (const event of session.chat(input, signal)) {
379
- if (event.type === "text") {
380
- const newText = event.accumulated.slice(lastAccumulated.length);
381
- process.stdout.write(newText);
382
- lastAccumulated = event.accumulated;
383
- } else if (event.type === "done") {
384
- if (lastAccumulated) console.log("");
385
- console.log(`${C.dim}[done]${C.reset}`);
386
- } else if (event.type === "compact") {
387
- console.log(`${C.dim}[context compacted: ${event.compactedMessages} old messages]${C.reset}`);
388
- } else if (event.type === "tool_use") {
389
- console.log(`\n${C.dim}[tool] ${event.name} ${stringifyConsoleArg(event.input)}${C.reset}`);
390
- } else if (event.type === "tool_result") {
391
- const status = event.is_error ? "error" : "ok";
392
- console.log(`${C.dim}[tool result] ${event.name ?? event.tool_use_id} ${status}${C.reset}`);
393
- } else if (event.type === "error") {
394
- console.log(`\n${C.yellow}[error] ${event.message}${C.reset}`);
395
- }
396
- }
397
- } catch (err) {
398
- console.log(`\n${C.yellow}[error] ${(err as Error).message}${C.reset}`);
399
- } finally {
400
- currentAbort = null;
401
- ctrlCState.reset();
402
- }
403
-
404
- rl.prompt();
405
- });
406
-
407
- rl.on("SIGINT", () => {
408
- const action = ctrlCState.press(currentAbort !== null);
409
-
410
- if (action === "exit") {
411
- console.log(`\n${C.dim}bye${C.reset}`);
412
- rl.close();
413
- return;
414
- }
415
-
416
- if (action === "interrupt") {
417
- console.log(`\n${C.yellow}[interrupting...]${C.reset}`);
418
- currentAbort?.abort();
419
- currentAbort = null;
420
- return;
421
- }
422
-
423
- if (action === "arm-interrupt") {
424
- console.log(`\n${C.dim}Press Ctrl+C again to interrupt current response${C.reset}`);
425
- return;
426
- }
427
-
428
- if (action === "arm-exit") {
429
- console.log(`\n${C.dim}Press Ctrl+C again to exit, or type exit${C.reset}`);
430
- rl.prompt();
431
- }
432
- });
433
-
434
- rl.on("close", () => {
435
- console.log("");
436
- process.exit(0);
437
- });
438
- }
439
-
440
- async function main(): Promise<void> {
441
- const args = parseArgs();
442
-
443
- if (args.streamJson) {
444
- const code = await runStreamJson(args);
445
- process.exit(code);
446
- }
447
-
448
- if (args.help) {
449
- const { appConfig } = await loadRuntime();
450
- printHelp(appConfig);
451
- return;
452
- }
453
-
454
- if (args.listSessions) {
455
- printSessions();
456
- return;
457
- }
458
-
459
- await runRepl(args);
460
- }
461
-
462
- function isDirectCliInvocation(): boolean {
463
- const current = resolvePath(fileURLToPath(import.meta.url));
464
- const invoked = process.argv[1] ? resolvePath(process.argv[1]) : "";
465
- return current === invoked;
466
- }
467
-
468
- if (isDirectCliInvocation()) {
469
- main().catch((err) => {
470
- console.error(`${C.yellow}startup failed: ${(err as Error).message}${C.reset}`);
471
- process.exit(1);
472
- });
473
- }
1
+ /**
2
+ * ChatCCC builtin Agent terminal REPL and JSONL streaming entrypoint.
3
+ *
4
+ * Usage:
5
+ * npx tsx src/builtin/cli.ts
6
+ * npx tsx src/builtin/cli.ts --model deepseek-v4-pro
7
+ * npx tsx src/builtin/cli.ts --stream-json --prompt "hello"
8
+ */
9
+
10
+ import * as readline from "node:readline";
11
+ import * as process from "node:process";
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";
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
+ }
34
+
35
+ interface JsonLine {
36
+ type: string;
37
+ [key: string]: unknown;
38
+ }
39
+
40
+ function parsePositiveIntegerOption(name: string, value: string): number {
41
+ const parsed = Number(value);
42
+ if (!Number.isInteger(parsed) || parsed <= 0) {
43
+ throw new Error(`${name} must be a positive integer`);
44
+ }
45
+ return parsed;
46
+ }
47
+
48
+ function parseArgs(argv = process.argv.slice(2)): ParsedArgs {
49
+ const config: ChatSessionConfig = {};
50
+ const options: ChatSessionOptions = {};
51
+ let listSessions = false;
52
+ let resume: BuiltinResumeRequest;
53
+ let help = false;
54
+ let streamJson = false;
55
+ let prompt: string | null = null;
56
+
57
+ for (let i = 0; i < argv.length; i++) {
58
+ const arg = argv[i];
59
+ const next = argv[i + 1];
60
+ if (arg === "--model" && next !== undefined) {
61
+ config.model = next;
62
+ i++;
63
+ } else if (arg === "--base-url" && next !== undefined) {
64
+ config.baseURL = next;
65
+ i++;
66
+ } else if (arg === "--api-key" && next !== undefined) {
67
+ config.apiKey = next;
68
+ i++;
69
+ } else if (arg === "--cwd" && next !== undefined) {
70
+ options.cwd = next;
71
+ i++;
72
+ } else if (arg === "--max-steps" && next !== undefined) {
73
+ options.maxSteps = parsePositiveIntegerOption("--max-steps", next);
74
+ i++;
75
+ } else if (arg === "--resume") {
76
+ if (next !== undefined && !next.startsWith("--")) {
77
+ resume = next;
78
+ i++;
79
+ } else {
80
+ resume = true;
81
+ }
82
+ } else if (arg === "--list-sessions") {
83
+ listSessions = true;
84
+ } else if (arg === "--stream-json") {
85
+ streamJson = true;
86
+ } else if (arg === "--prompt" && next !== undefined) {
87
+ prompt = next;
88
+ i++;
89
+ } else if (arg === "--help" || arg === "-h") {
90
+ help = true;
91
+ }
92
+ }
93
+
94
+ return { config, options, listSessions, resume, help, streamJson, prompt };
95
+ }
96
+
97
+ async function loadRuntime(): Promise<RuntimeDeps> {
98
+ const [{ ChatSession }, { config: appConfig }] = await Promise.all([
99
+ import("./index.js"),
100
+ import("../config.ts"),
101
+ ]);
102
+ return { ChatSession, appConfig };
103
+ }
104
+
105
+ function printHelp(appConfig: RuntimeDeps["appConfig"]): void {
106
+ console.log([
107
+ "ChatCCC builtin Agent terminal REPL",
108
+ "",
109
+ "Usage: npx tsx src/builtin/cli.ts [options]",
110
+ "",
111
+ "Options:",
112
+ ` --model <name> Model name (overrides config.ccc.model, current default ${appConfig.ccc.model})`,
113
+ ` --base-url <url> API base URL (current default ${appConfig.ccc.DEEPSEEK_BASE_URL})`,
114
+ " --api-key <key> API key (overrides config.ccc.DEEPSEEK_API_KEY)",
115
+ " --cwd <path> Working directory",
116
+ " --max-steps <n> Optional tool-step limit. Omit for no step limit",
117
+ " --resume [id] Resume latest cwd session, or the explicit session id",
118
+ " --list-sessions List saved ccc sessions and exit",
119
+ " --stream-json One-shot mode: write JSONL events to stdout",
120
+ " --prompt <text> Prompt text for --stream-json",
121
+ " --help, -h Show help",
122
+ "",
123
+ "Default config source:",
124
+ " ~/.chatccc/config.json ccc.DEEPSEEK_API_KEY / ccc.DEEPSEEK_BASE_URL / ccc.model",
125
+ "",
126
+ ].join("\n"));
127
+ }
128
+
129
+ function formatTime(ms: number): string {
130
+ return new Date(ms).toLocaleString();
131
+ }
132
+
133
+ function printSessions(streamJson = false): void {
134
+ const sessions = listBuiltinContextSessions();
135
+ if (streamJson) {
136
+ writeJsonLine({
137
+ type: "sessions",
138
+ sessions: sessions.map((session) => ({
139
+ session_id: session.sessionId,
140
+ turns: session.totalMessages,
141
+ compacted_messages: session.compactedMessages,
142
+ has_summary: session.hasSummary,
143
+ updated_at: session.updatedAt,
144
+ cwd: session.cwd,
145
+ })),
146
+ });
147
+ return;
148
+ }
149
+
150
+ if (sessions.length === 0) {
151
+ console.log("No saved ccc sessions");
152
+ return;
153
+ }
154
+
155
+ for (const session of sessions) {
156
+ const summary = session.hasSummary ? " summary=yes" : "";
157
+ const cwd = session.cwd ? ` cwd=${session.cwd}` : "";
158
+ console.log(`${session.sessionId} turns=${session.totalMessages} compacted=${session.compactedMessages}${summary} updated=${formatTime(session.updatedAt)}${cwd}`);
159
+ }
160
+ }
161
+
162
+ function stringifyConsoleArg(value: unknown): string {
163
+ if (typeof value === "string") return value;
164
+ if (value instanceof Error) return value.stack ?? value.message;
165
+ try {
166
+ return JSON.stringify(value);
167
+ } catch {
168
+ return String(value);
169
+ }
170
+ }
171
+
172
+ function redirectConsoleLogsToStderr(): void {
173
+ const write = (...args: unknown[]) => {
174
+ process.stderr.write(`${args.map(stringifyConsoleArg).join(" ")}\n`);
175
+ };
176
+ console.log = write;
177
+ console.info = write;
178
+ console.warn = write;
179
+ }
180
+
181
+ function writeJsonLine(event: JsonLine): void {
182
+ process.stdout.write(`${JSON.stringify(event)}\n`);
183
+ }
184
+
185
+ function streamJsonEvent(event: ChatEvent): void {
186
+ if (event.type === "text") {
187
+ writeJsonLine({
188
+ type: "text_delta",
189
+ text: event.text,
190
+ accumulated: event.accumulated,
191
+ });
192
+ } else if (event.type === "compact") {
193
+ writeJsonLine({
194
+ type: "compact",
195
+ compacted_messages: event.compactedMessages,
196
+ });
197
+ } else if (event.type === "tool_use") {
198
+ writeJsonLine({
199
+ type: "tool_call",
200
+ id: event.id,
201
+ name: event.name,
202
+ input: event.input,
203
+ });
204
+ } else if (event.type === "tool_result") {
205
+ writeJsonLine({
206
+ type: "tool_result",
207
+ tool_call_id: event.tool_use_id,
208
+ name: event.name,
209
+ content: event.content,
210
+ is_error: event.is_error,
211
+ });
212
+ } else if (event.type === "done") {
213
+ writeJsonLine({
214
+ type: "done",
215
+ text: event.text,
216
+ });
217
+ } else if (event.type === "error") {
218
+ writeJsonLine({
219
+ type: "error",
220
+ message: event.message,
221
+ });
222
+ }
223
+ }
224
+
225
+ async function readPromptFromStdin(): Promise<string> {
226
+ const chunks: Buffer[] = [];
227
+ for await (const chunk of process.stdin) {
228
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
229
+ }
230
+ return Buffer.concat(chunks).toString("utf8");
231
+ }
232
+
233
+ async function runStreamJson(args: ParsedArgs): Promise<number> {
234
+ redirectConsoleLogsToStderr();
235
+
236
+ if (args.listSessions) {
237
+ printSessions(true);
238
+ return 0;
239
+ }
240
+
241
+ const prompt = args.prompt ?? (!process.stdin.isTTY ? await readPromptFromStdin() : "");
242
+ if (!prompt.trim()) {
243
+ writeJsonLine({ type: "error", message: "--stream-json requires --prompt <text> or stdin input" });
244
+ return 1;
245
+ }
246
+
247
+ let runtime: RuntimeDeps;
248
+ try {
249
+ runtime = await loadRuntime();
250
+ } catch (err) {
251
+ writeJsonLine({ type: "error", message: (err as Error).message });
252
+ return 1;
253
+ }
254
+
255
+ const cwd = resolvePath(args.options.cwd ?? process.cwd());
256
+ let resolvedSession;
257
+ try {
258
+ resolvedSession = resolveBuiltinSession({ cwd, resume: args.resume });
259
+ } catch (err) {
260
+ writeJsonLine({ type: "error", message: (err as Error).message });
261
+ return 1;
262
+ }
263
+
264
+ let session: InstanceType<RuntimeDeps["ChatSession"]>;
265
+ try {
266
+ session = new runtime.ChatSession(args.config, {
267
+ ...args.options,
268
+ cwd,
269
+ persist: true,
270
+ sessionId: resolvedSession.sessionId,
271
+ });
272
+ } catch (err) {
273
+ writeJsonLine({ type: "error", message: (err as Error).message });
274
+ return 1;
275
+ }
276
+
277
+ writeJsonLine({
278
+ type: "start",
279
+ session_id: resolvedSession.sessionId,
280
+ mode: resolvedSession.mode,
281
+ cwd,
282
+ model: args.config.model ?? runtime.appConfig.ccc.model,
283
+ });
284
+
285
+ try {
286
+ for await (const event of session.chat(prompt)) {
287
+ streamJsonEvent(event);
288
+ }
289
+ return 0;
290
+ } catch (err) {
291
+ writeJsonLine({ type: "error", message: (err as Error).message });
292
+ return 1;
293
+ }
294
+ }
295
+
296
+ const C = {
297
+ reset: "\x1b[0m",
298
+ dim: "\x1b[2m",
299
+ green: "\x1b[32m",
300
+ yellow: "\x1b[33m",
301
+ };
302
+
303
+ async function runRepl(args: ParsedArgs): Promise<void> {
304
+ const { ChatSession, appConfig } = await loadRuntime();
305
+
306
+ const cwd = resolvePath(args.options.cwd ?? process.cwd());
307
+ let resolvedSession;
308
+ try {
309
+ resolvedSession = resolveBuiltinSession({ cwd, resume: args.resume });
310
+ } catch (err) {
311
+ console.error(`${C.yellow}${(err as Error).message}${C.reset}`);
312
+ process.exit(1);
313
+ }
314
+
315
+ console.log(`${C.dim}ChatCCC builtin Agent${C.reset}`);
316
+ console.log(`${C.dim}Model: ${args.config.model ?? appConfig.ccc.model}${C.reset}`);
317
+ console.log(`${C.dim}Directory: ${cwd}${C.reset}`);
318
+ console.log(`${C.dim}Session: ${resolvedSession.sessionId} (${resolvedSession.mode === "new" ? "new" : "resumed"})${C.reset}`);
319
+ console.log(`${C.dim}Type a message to chat. Double Ctrl+C interrupts generation or exits. Type exit to quit.${C.reset}`);
320
+ console.log("");
321
+
322
+ let session: InstanceType<typeof ChatSession>;
323
+ try {
324
+ session = new ChatSession(args.config, {
325
+ ...args.options,
326
+ cwd,
327
+ persist: true,
328
+ sessionId: resolvedSession.sessionId,
329
+ });
330
+ } catch (err) {
331
+ console.error(`${C.yellow}${(err as Error).message}${C.reset}`);
332
+ process.exit(1);
333
+ }
334
+
335
+ const rl = readline.createInterface({
336
+ input: process.stdin,
337
+ output: process.stdout,
338
+ prompt: `${C.green}>${C.reset} `,
339
+ });
340
+
341
+ let currentAbort: AbortController | null = null;
342
+ const ctrlCState = createCtrlCState();
343
+
344
+ rl.prompt();
345
+
346
+ rl.on("line", async (line: string) => {
347
+ ctrlCState.reset();
348
+ const input = line.trim();
349
+ if (!input) {
350
+ rl.prompt();
351
+ return;
352
+ }
353
+
354
+ if (input === "exit") {
355
+ console.log(`${C.dim}bye${C.reset}`);
356
+ rl.close();
357
+ return;
358
+ }
359
+
360
+ if (input === "/clear") {
361
+ session.reset();
362
+ console.log(`${C.dim}session cleared${C.reset}`);
363
+ rl.prompt();
364
+ return;
365
+ }
366
+
367
+ if (input === "/history") {
368
+ console.log(`${C.dim}${session.turnCount} conversation turns${C.reset}`);
369
+ rl.prompt();
370
+ return;
371
+ }
372
+
373
+ currentAbort = new AbortController();
374
+ const signal = currentAbort.signal;
375
+
376
+ try {
377
+ let lastAccumulated = "";
378
+ for await (const event of session.chat(input, signal)) {
379
+ if (event.type === "text") {
380
+ const newText = event.accumulated.slice(lastAccumulated.length);
381
+ process.stdout.write(newText);
382
+ lastAccumulated = event.accumulated;
383
+ } else if (event.type === "done") {
384
+ if (lastAccumulated) console.log("");
385
+ console.log(`${C.dim}[done]${C.reset}`);
386
+ } else if (event.type === "compact") {
387
+ console.log(`${C.dim}[context compacted: ${event.compactedMessages} old messages]${C.reset}`);
388
+ } else if (event.type === "tool_use") {
389
+ console.log(`\n${C.dim}[tool] ${event.name} ${stringifyConsoleArg(event.input)}${C.reset}`);
390
+ } else if (event.type === "tool_result") {
391
+ const status = event.is_error ? "error" : "ok";
392
+ console.log(`${C.dim}[tool result] ${event.name ?? event.tool_use_id} ${status}${C.reset}`);
393
+ } else if (event.type === "error") {
394
+ console.log(`\n${C.yellow}[error] ${event.message}${C.reset}`);
395
+ }
396
+ }
397
+ } catch (err) {
398
+ console.log(`\n${C.yellow}[error] ${(err as Error).message}${C.reset}`);
399
+ } finally {
400
+ currentAbort = null;
401
+ ctrlCState.reset();
402
+ }
403
+
404
+ rl.prompt();
405
+ });
406
+
407
+ rl.on("SIGINT", () => {
408
+ const action = ctrlCState.press(currentAbort !== null);
409
+
410
+ if (action === "exit") {
411
+ console.log(`\n${C.dim}bye${C.reset}`);
412
+ rl.close();
413
+ return;
414
+ }
415
+
416
+ if (action === "interrupt") {
417
+ console.log(`\n${C.yellow}[interrupting...]${C.reset}`);
418
+ currentAbort?.abort();
419
+ currentAbort = null;
420
+ return;
421
+ }
422
+
423
+ if (action === "arm-interrupt") {
424
+ console.log(`\n${C.dim}Press Ctrl+C again to interrupt current response${C.reset}`);
425
+ return;
426
+ }
427
+
428
+ if (action === "arm-exit") {
429
+ console.log(`\n${C.dim}Press Ctrl+C again to exit, or type exit${C.reset}`);
430
+ rl.prompt();
431
+ }
432
+ });
433
+
434
+ rl.on("close", () => {
435
+ console.log("");
436
+ process.exit(0);
437
+ });
438
+ }
439
+
440
+ async function main(): Promise<void> {
441
+ const args = parseArgs();
442
+
443
+ if (args.streamJson) {
444
+ const code = await runStreamJson(args);
445
+ process.exit(code);
446
+ }
447
+
448
+ if (args.help) {
449
+ const { appConfig } = await loadRuntime();
450
+ printHelp(appConfig);
451
+ return;
452
+ }
453
+
454
+ if (args.listSessions) {
455
+ printSessions();
456
+ return;
457
+ }
458
+
459
+ await runRepl(args);
460
+ }
461
+
462
+ function isDirectCliInvocation(): boolean {
463
+ const current = resolvePath(fileURLToPath(import.meta.url));
464
+ const invoked = process.argv[1] ? resolvePath(process.argv[1]) : "";
465
+ return current === invoked;
466
+ }
467
+
468
+ if (isDirectCliInvocation()) {
469
+ main().catch((err) => {
470
+ console.error(`${C.yellow}startup failed: ${(err as Error).message}${C.reset}`);
471
+ process.exit(1);
472
+ });
473
+ }