chatccc 0.2.192 → 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.
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join } from "node:path";
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const pkgRoot = dirname(require.resolve("../package.json"));
8
+ const cliTs = join(pkgRoot, "src", "builtin", "cli.ts");
9
+ const tsxCli = require.resolve("tsx/cli");
10
+
11
+ const result = spawnSync(process.execPath, [tsxCli, cliTs, ...process.argv.slice(2)], {
12
+ stdio: "inherit",
13
+ cwd: process.cwd(),
14
+ env: process.env,
15
+ });
16
+
17
+ process.exit(result.status === null ? 1 : result.status);
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.192",
3
+ "version": "0.2.193",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "./src/index.ts",
8
8
  "bin": {
9
- "chatccc": "bin/chatccc.mjs"
9
+ "chatccc": "bin/chatccc.mjs",
10
+ "cccagent": "bin/cccagent.mjs"
10
11
  },
11
12
  "files": [
12
13
  "src/",
@@ -0,0 +1,39 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ describe("builtin cli --stream-json", () => {
9
+ it("writes only JSON lines to stdout when startup fails", async () => {
10
+ let caught: unknown;
11
+ try {
12
+ await execFileAsync(process.execPath, [
13
+ "bin/cccagent.mjs",
14
+ "--stream-json",
15
+ "--prompt",
16
+ "hello",
17
+ "--api-key",
18
+ "",
19
+ ], {
20
+ cwd: process.cwd(),
21
+ timeout: 10_000,
22
+ windowsHide: true,
23
+ });
24
+ } catch (err) {
25
+ caught = err;
26
+ }
27
+
28
+ expect(caught).toMatchObject({ code: 1 });
29
+ const stdout = (caught as { stdout?: string }).stdout ?? "";
30
+ const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
31
+ expect(lines.length).toBeGreaterThan(0);
32
+ for (const line of lines) {
33
+ expect(() => JSON.parse(line)).not.toThrow();
34
+ }
35
+ expect(JSON.parse(lines.at(-1)!)).toEqual(expect.objectContaining({
36
+ type: "error",
37
+ }));
38
+ });
39
+ });
@@ -7,6 +7,8 @@ import { describe, expect, it } from "vitest";
7
7
  import {
8
8
  BuiltinContextManager,
9
9
  estimateBuiltinContextTokens,
10
+ listBuiltinContextSessions,
11
+ newBuiltinSessionId,
10
12
  serializeMessagesForSummary,
11
13
  } from "../builtin/context.ts";
12
14
 
@@ -109,9 +111,44 @@ describe("BuiltinContextManager", () => {
109
111
  expect(persisted.messages).toEqual([]);
110
112
  expect(persisted.totalMessages).toBe(0);
111
113
  });
114
+
115
+ it("persists cwd metadata and lists saved sessions newest first", async () => {
116
+ const dir = await mkdtemp(join(tmpdir(), "chatccc-builtin-context-list-"));
117
+
118
+ const first = new BuiltinContextManager({
119
+ persist: true,
120
+ contextDir: dir,
121
+ sessionId: "older",
122
+ cwd: "C:\\repo-a",
123
+ });
124
+ first.appendMessage({ role: "user", content: "old" });
125
+
126
+ const second = new BuiltinContextManager({
127
+ persist: true,
128
+ contextDir: dir,
129
+ sessionId: "newer",
130
+ cwd: "C:\\repo-b",
131
+ });
132
+ second.appendMessage({ role: "user", content: "new" });
133
+
134
+ const sessions = listBuiltinContextSessions(dir);
135
+
136
+ expect(sessions.map((s) => s.sessionId)).toEqual(["newer", "older"]);
137
+ expect(sessions[0]).toEqual(expect.objectContaining({
138
+ cwd: "C:\\repo-b",
139
+ totalMessages: 1,
140
+ hasSummary: false,
141
+ }));
142
+ });
112
143
  });
113
144
 
114
145
  describe("builtin context helpers", () => {
146
+ it("creates readable timestamp-based session ids", () => {
147
+ const id = newBuiltinSessionId(new Date(2026, 6, 2, 12, 15, 30), "a1b2c3");
148
+
149
+ expect(id).toBe("session-20260702-121530-a1b2c3");
150
+ });
151
+
115
152
  it("estimates tokens from summary and messages", () => {
116
153
  expect(estimateBuiltinContextTokens("abc", [{ role: "user", content: "abcdef" }]))
117
154
  .toBeGreaterThanOrEqual(3);
@@ -0,0 +1,116 @@
1
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { describe, expect, it } from "vitest";
6
+
7
+ import { defaultBuiltinSessionId } from "../builtin/context.ts";
8
+ import { resolveBuiltinSession } from "../builtin/session-select.ts";
9
+
10
+ async function writeSession(
11
+ contextDir: string,
12
+ sessionId: string,
13
+ options: { cwd?: string; updatedAt: number; totalMessages?: number },
14
+ ): Promise<void> {
15
+ const dir = join(contextDir, sessionId);
16
+ await mkdir(dir, { recursive: true });
17
+ await writeFile(
18
+ join(dir, "context.json"),
19
+ JSON.stringify({
20
+ version: 1,
21
+ createdAt: options.updatedAt,
22
+ updatedAt: options.updatedAt,
23
+ sessionId,
24
+ ...(options.cwd ? { cwd: options.cwd } : {}),
25
+ summary: "",
26
+ messages: [],
27
+ totalMessages: options.totalMessages ?? 0,
28
+ compactedMessages: 0,
29
+ }),
30
+ "utf8",
31
+ );
32
+ }
33
+
34
+ describe("resolveBuiltinSession", () => {
35
+ it("creates a fresh timestamp session by default", async () => {
36
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-session-select-new-"));
37
+
38
+ const result = resolveBuiltinSession({
39
+ cwd: "C:\\repo",
40
+ contextDir,
41
+ now: new Date(2026, 6, 2, 12, 15, 30),
42
+ randomSuffix: "a1b2c3",
43
+ });
44
+
45
+ expect(result).toEqual({
46
+ mode: "new",
47
+ sessionId: "session-20260702-121530-a1b2c3",
48
+ });
49
+ });
50
+
51
+ it("resumes an explicit existing session id", async () => {
52
+ const contextDir = join(tmpdir(), `chatccc-session-select-explicit-${Date.now()}`);
53
+ await writeSession(contextDir, "manual-session", { updatedAt: 1 });
54
+
55
+ expect(resolveBuiltinSession({
56
+ cwd: "C:\\repo",
57
+ contextDir,
58
+ resume: "manual-session",
59
+ })).toEqual({
60
+ mode: "resumed",
61
+ sessionId: "manual-session",
62
+ });
63
+ });
64
+
65
+ it("fails when an explicit session id does not exist", async () => {
66
+ const contextDir = join(tmpdir(), `chatccc-session-select-missing-${Date.now()}`);
67
+
68
+ expect(() => resolveBuiltinSession({
69
+ cwd: "C:\\repo",
70
+ contextDir,
71
+ resume: "missing",
72
+ })).toThrow("未找到 ccc 会话: missing");
73
+ });
74
+
75
+ it("resumes the newest session for cwd when resume has no id", async () => {
76
+ const contextDir = join(tmpdir(), `chatccc-session-select-cwd-${Date.now()}`);
77
+ await writeSession(contextDir, "old-match", { cwd: "C:\\repo", updatedAt: 1_000 });
78
+ await writeSession(contextDir, "new-match", { cwd: "C:\\repo", updatedAt: 2_000 });
79
+ await writeSession(contextDir, "other-cwd", { cwd: "C:\\other", updatedAt: 3_000 });
80
+
81
+ expect(resolveBuiltinSession({
82
+ cwd: "C:\\repo",
83
+ contextDir,
84
+ resume: true,
85
+ })).toEqual({
86
+ mode: "resumed",
87
+ sessionId: "new-match",
88
+ });
89
+ });
90
+
91
+ it("resumes legacy cwd-hash sessions when resume has no id", async () => {
92
+ const contextDir = join(tmpdir(), `chatccc-session-select-legacy-${Date.now()}`);
93
+ const cwd = "C:\\repo";
94
+ const legacySessionId = defaultBuiltinSessionId(cwd);
95
+ await writeSession(contextDir, legacySessionId, { updatedAt: 1_000 });
96
+
97
+ expect(resolveBuiltinSession({
98
+ cwd,
99
+ contextDir,
100
+ resume: true,
101
+ })).toEqual({
102
+ mode: "resumed",
103
+ sessionId: legacySessionId,
104
+ });
105
+ });
106
+
107
+ it("fails when there is no cwd session to resume", async () => {
108
+ const contextDir = join(tmpdir(), `chatccc-session-select-empty-${Date.now()}`);
109
+
110
+ expect(() => resolveBuiltinSession({
111
+ cwd: "C:\\repo",
112
+ contextDir,
113
+ resume: true,
114
+ })).toThrow("未找到当前目录可恢复的 ccc 会话");
115
+ });
116
+ });
@@ -170,6 +170,15 @@ describe("buildHelpCard", () => {
170
170
  expect(lines).toContain("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡");
171
171
  expect(lines.at(-1)).toBe(ABD_HELP_LINE);
172
172
  });
173
+
174
+ it("does not advertise the hidden ccc agent", () => {
175
+ const card = buildHelpCard("test");
176
+ const parsed = JSON.parse(card);
177
+ const text = JSON.stringify(parsed);
178
+
179
+ expect(text).not.toContain("/new ccc");
180
+ expect(text).not.toContain("CCC Agent");
181
+ });
173
182
  });
174
183
 
175
184
  // ---------------------------------------------------------------------------
@@ -364,17 +373,29 @@ describe("buildSessionsCard", () => {
364
373
  expect(content).toContain("Cursor 会话");
365
374
  });
366
375
 
367
- it("omits Cursor section when no Cursor sessions", () => {
368
- const card = buildSessionsCard([
369
- { sessionId: "c1", chatName: "", chatId: "oc_c1", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
370
- ]);
371
- const parsed = JSON.parse(card);
372
- const content: string = parsed.elements[0].text.content;
373
- expect(content).toContain("Claude Code 会话");
374
- expect(content).not.toContain("Cursor 会话");
375
- });
376
-
377
- it("displays chatName when provided", () => {
376
+ it("omits Cursor section when no Cursor sessions", () => {
377
+ const card = buildSessionsCard([
378
+ { sessionId: "c1", chatName: "", chatId: "oc_c1", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
379
+ ]);
380
+ const parsed = JSON.parse(card);
381
+ const content: string = parsed.elements[0].text.content;
382
+ expect(content).toContain("Claude Code 会话");
383
+ expect(content).not.toContain("Cursor 会话");
384
+ });
385
+
386
+ it("labels hidden ccc sessions without grouping them as Claude Code", () => {
387
+ const card = buildSessionsCard([
388
+ { sessionId: "session-20260702-121530-a1b2c3", chatName: "", chatId: "oc_ccc", active: false, turnCount: 1, elapsedSeconds: null, model: "deepseek-v4-pro", tool: "ccc" },
389
+ ]);
390
+ const parsed = JSON.parse(card);
391
+ const content: string = parsed.elements[0].text.content;
392
+
393
+ expect(content).toContain("CCC Agent 会话");
394
+ expect(content).toContain("工具: CCC Agent");
395
+ expect(content).not.toContain("Claude Code 会话");
396
+ });
397
+
398
+ it("displays chatName when provided", () => {
378
399
  const card = buildSessionsCard([
379
400
  { sessionId: "abc123", chatName: "帮我写代码-src", chatId: "oc_abc123", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
380
401
  ]);
@@ -0,0 +1,73 @@
1
+ import { mkdtemp } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+
7
+ const streamTextMock = vi.fn();
8
+ const generateTextMock = vi.fn();
9
+
10
+ vi.mock("@ai-sdk/openai-compatible", () => ({
11
+ createOpenAICompatible: vi.fn(() => (modelId: string) => ({ modelId })),
12
+ }));
13
+
14
+ vi.mock("ai", () => ({
15
+ streamText: streamTextMock,
16
+ generateText: generateTextMock,
17
+ }));
18
+
19
+ async function* textStream(...chunks: string[]): AsyncIterable<string> {
20
+ for (const chunk of chunks) yield chunk;
21
+ }
22
+
23
+ afterEach(() => {
24
+ streamTextMock.mockReset();
25
+ generateTextMock.mockReset();
26
+ });
27
+
28
+ describe("createCccAdapter", () => {
29
+ it("creates a persisted ccc session and exposes model/cwd metadata", async () => {
30
+ const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
31
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-meta-"));
32
+ const adapter = createCccAdapter({
33
+ apiKey: "sk-test",
34
+ contextDir,
35
+ model: "deepseek-v4-pro",
36
+ });
37
+
38
+ const created = await adapter.createSession("F:\\repo");
39
+ const info = await adapter.getSessionInfo(created.sessionId);
40
+
41
+ expect(created.sessionId).toMatch(/^session-\d{8}-\d{6}-[a-f0-9]{6}$/);
42
+ expect(info).toEqual(expect.objectContaining({
43
+ sessionId: created.sessionId,
44
+ cwd: "F:\\repo",
45
+ model: "deepseek-v4-pro",
46
+ }));
47
+ });
48
+
49
+ it("maps ChatSession text chunks to unified assistant text blocks", async () => {
50
+ const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
51
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-stream-"));
52
+ const adapter = createCccAdapter({
53
+ apiKey: "sk-test",
54
+ contextDir,
55
+ model: "deepseek-v4-flash",
56
+ });
57
+ const { sessionId } = await adapter.createSession("F:\\repo");
58
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("hello", " world") });
59
+
60
+ const messages = [];
61
+ for await (const message of adapter.prompt(sessionId, "hi", "F:\\repo")) {
62
+ messages.push(message);
63
+ }
64
+
65
+ expect(messages).toEqual([
66
+ { type: "assistant", blocks: [{ type: "text", text: "hello" }] },
67
+ { type: "assistant", blocks: [{ type: "text", text: " world" }] },
68
+ ]);
69
+ expect(streamTextMock).toHaveBeenCalledWith(expect.objectContaining({
70
+ model: { modelId: "deepseek-v4-flash" },
71
+ }));
72
+ });
73
+ });
@@ -626,9 +626,9 @@ describe("handleCommand WeChat processing ack", () => {
626
626
  );
627
627
  });
628
628
 
629
- it("advertises /usage in new Codex and Cursor session ready messages", async () => {
630
- const codexPlatform = mockPlatform("feishu");
631
- _setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
629
+ it("advertises /usage in new Codex and Cursor session ready messages", async () => {
630
+ const codexPlatform = mockPlatform("feishu");
631
+ _setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
632
632
 
633
633
  await handleCommand(codexPlatform, "/new codex", "feishu-p2p", "ou-user", Date.now(), "p2p");
634
634
 
@@ -652,9 +652,34 @@ describe("handleCommand WeChat processing ack", () => {
652
652
  const claudePlatform = mockPlatform("feishu");
653
653
  await handleCommand(claudePlatform, "/new claude", "feishu-p2p-2", "ou-user", Date.now(), "p2p");
654
654
 
655
- const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
656
- ([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
657
- );
658
- expect(claudeReadyCall?.[2]).not.toContain("/usage");
659
- });
660
- });
655
+ const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
656
+ ([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
657
+ );
658
+ expect(claudeReadyCall?.[2]).not.toContain("/usage");
659
+ });
660
+
661
+ it("allows the hidden /new ccc entry without advertising it in normal help", async () => {
662
+ const platform = mockPlatform("feishu");
663
+ _setAdapterForToolForTest("ccc", mockAdapter("session-20260702-121530-a1b2c3"));
664
+
665
+ await handleCommand(platform, "/new ccc", "feishu-p2p-ccc", "ou-user", Date.now(), "p2p");
666
+
667
+ expect(platform.updateChatInfo).toHaveBeenCalledWith(
668
+ "feishu-group",
669
+ expect.any(String),
670
+ "CCC Session: session-20260702-121530-a1b2c3",
671
+ );
672
+ expect(platform.sendCard).toHaveBeenCalledWith(
673
+ "feishu-group",
674
+ "CCC Agent Session Ready",
675
+ expect.stringContaining("CCC Agent"),
676
+ "green",
677
+ );
678
+
679
+ const helpPlatform = mockPlatform("feishu");
680
+ await handleCommand(helpPlatform, "hello", "feishu-group-help", "ou-user", Date.now(), "group");
681
+ const helpCard = vi.mocked(helpPlatform.sendRawCard).mock.calls.at(-1)?.[1] as string;
682
+ expect(helpCard).not.toContain("/new ccc");
683
+ expect(helpCard).not.toContain("CCC Agent");
684
+ });
685
+ });
@@ -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
+ }