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.
- package/bin/cccagent.mjs +17 -0
- package/package.json +3 -2
- package/src/__tests__/builtin-chat-session.test.ts +76 -0
- package/src/__tests__/builtin-cli-json.test.ts +39 -0
- package/src/__tests__/builtin-context.test.ts +163 -0
- package/src/__tests__/builtin-session-select.test.ts +116 -0
- package/src/__tests__/builtin-sigint.test.ts +56 -0
- package/src/__tests__/cards.test.ts +32 -11
- package/src/__tests__/ccc-adapter.test.ts +73 -0
- package/src/__tests__/orchestrator.test.ts +34 -9
- package/src/__tests__/session.test.ts +23 -13
- package/src/__tests__/sim-platform.test.ts +12 -7
- package/src/adapters/ccc-adapter.ts +99 -0
- package/src/builtin/cli.ts +316 -79
- package/src/builtin/context.ts +323 -0
- package/src/builtin/index.ts +73 -10
- package/src/builtin/session-select.ts +48 -0
- package/src/builtin/sigint.ts +50 -0
- package/src/cards.ts +58 -35
- package/src/config.ts +24 -18
- package/src/feishu-api.ts +14 -12
- package/src/orchestrator.ts +1 -1
- package/src/session.ts +31 -15
|
@@ -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("
|
|
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
|
+
}
|
package/src/builtin/cli.ts
CHANGED
|
@@ -1,101 +1,305 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* builtin
|
|
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-
|
|
7
|
-
* 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
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as readline from "node:readline";
|
|
11
11
|
import * as process from "node:process";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
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 <
|
|
25
|
-
const arg =
|
|
26
|
-
const next =
|
|
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
|
-
|
|
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
|
|
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
|
|
96
|
+
"ChatCCC builtin Agent terminal REPL",
|
|
51
97
|
"",
|
|
52
|
-
"
|
|
98
|
+
"Usage: npx tsx src/builtin/cli.ts [options]",
|
|
53
99
|
"",
|
|
54
|
-
"
|
|
55
|
-
` --model <name>
|
|
56
|
-
` --base-url <url>
|
|
57
|
-
" --api-key <key>
|
|
58
|
-
" --cwd <path>
|
|
59
|
-
" --
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// 主程序
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
276
|
+
async function runRepl(args: ParsedArgs): Promise<void> {
|
|
277
|
+
const { ChatSession, appConfig } = await loadRuntime();
|
|
84
278
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
315
|
+
const ctrlCState = createCtrlCState();
|
|
113
316
|
|
|
114
317
|
rl.prompt();
|
|
115
318
|
|
|
116
319
|
rl.on("line", async (line: string) => {
|
|
117
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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}[
|
|
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}[
|
|
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}[
|
|
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
|
|
175
|
-
const shouldExit = now - lastCtrlCAt <= DOUBLE_CTRL_C_EXIT_WINDOW_MS;
|
|
376
|
+
const action = ctrlCState.press(currentAbort !== null);
|
|
176
377
|
|
|
177
|
-
if (
|
|
178
|
-
console.log(`\n${C.dim}
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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()
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|