chatccc 0.2.201 → 0.2.202

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 (92) hide show
  1. package/README.md +17 -17
  2. package/agent-prompts/claude_specific.md +45 -45
  3. package/agent-prompts/codex_specific.md +2 -2
  4. package/agent-prompts/cursor_specific.md +13 -13
  5. package/bin/cccagent.mjs +17 -17
  6. package/config.sample.json +27 -27
  7. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  8. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  9. package/im-skills/feishu-skill/skill.md +3 -3
  10. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  11. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  12. package/im-skills/wechat-file-skill/skill.md +10 -10
  13. package/im-skills/wechat-image-skill/skill.md +10 -10
  14. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  15. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  16. package/im-skills/wechat-video-skill/skill.md +10 -10
  17. package/package.json +1 -1
  18. package/scripts/postinstall-sharp-check.mjs +58 -58
  19. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  20. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  21. package/src/__tests__/builtin-chat-session.test.ts +277 -277
  22. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  23. package/src/__tests__/builtin-config.test.ts +33 -33
  24. package/src/__tests__/builtin-context.test.ts +163 -163
  25. package/src/__tests__/builtin-file-tools.test.ts +224 -224
  26. package/src/__tests__/builtin-session-select.test.ts +116 -116
  27. package/src/__tests__/builtin-sigint.test.ts +56 -56
  28. package/src/__tests__/card-plain-text.test.ts +5 -5
  29. package/src/__tests__/cardkit.test.ts +60 -60
  30. package/src/__tests__/cards.test.ts +133 -122
  31. package/src/__tests__/ccc-adapter.test.ts +114 -114
  32. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  33. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  34. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  35. package/src/__tests__/claude-adapter.test.ts +592 -592
  36. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  37. package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
  38. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  39. package/src/__tests__/config-reload.test.ts +10 -10
  40. package/src/__tests__/config-sample.test.ts +18 -18
  41. package/src/__tests__/cursor-adapter.test.ts +226 -226
  42. package/src/__tests__/feishu-api.test.ts +60 -60
  43. package/src/__tests__/feishu-avatar.test.ts +90 -90
  44. package/src/__tests__/feishu-platform.test.ts +22 -22
  45. package/src/__tests__/format-message.test.ts +47 -47
  46. package/src/__tests__/jsonl-stream.test.ts +79 -79
  47. package/src/__tests__/orchestrator.test.ts +464 -301
  48. package/src/__tests__/privacy.test.ts +198 -198
  49. package/src/__tests__/raw-stream-log.test.ts +106 -106
  50. package/src/__tests__/session.test.ts +46 -44
  51. package/src/__tests__/shared-prefix.test.ts +36 -36
  52. package/src/__tests__/sim-platform.test.ts +12 -12
  53. package/src/__tests__/stream-state.test.ts +42 -42
  54. package/src/__tests__/web-ui.test.ts +209 -209
  55. package/src/adapters/ccc-adapter.ts +121 -121
  56. package/src/adapters/claude-adapter.ts +603 -603
  57. package/src/adapters/claude-session-meta-store.ts +120 -120
  58. package/src/adapters/codex-adapter.ts +380 -380
  59. package/src/adapters/cursor-adapter.ts +673 -673
  60. package/src/adapters/jsonl-stream.ts +157 -157
  61. package/src/adapters/raw-stream-log.ts +124 -124
  62. package/src/adapters/resource-monitor.ts +140 -140
  63. package/src/agent-delegate-task-rpc.ts +153 -153
  64. package/src/agent-delegate-task.ts +81 -81
  65. package/src/agent-reload-config-rpc.ts +34 -34
  66. package/src/agent-stop-stuck.ts +129 -129
  67. package/src/builtin/cli.ts +473 -473
  68. package/src/builtin/context.ts +323 -323
  69. package/src/builtin/file-tools.ts +1072 -1072
  70. package/src/builtin/index.ts +404 -404
  71. package/src/builtin/session-select.ts +48 -48
  72. package/src/builtin/sigint.ts +50 -50
  73. package/src/cards.ts +222 -210
  74. package/src/chatgpt-subscription-rpc.ts +27 -27
  75. package/src/chatgpt-subscription.ts +299 -299
  76. package/src/chrome-devtools-guard.ts +318 -318
  77. package/src/codex-reset-actions.ts +184 -184
  78. package/src/config.ts +125 -125
  79. package/src/feishu-api.ts +118 -118
  80. package/src/feishu-platform.ts +20 -20
  81. package/src/format-message.ts +293 -293
  82. package/src/litellm-proxy.ts +374 -374
  83. package/src/orchestrator.ts +348 -329
  84. package/src/privacy.ts +118 -118
  85. package/src/runtime-reload.ts +34 -34
  86. package/src/session-chat-binding.ts +6 -6
  87. package/src/session-name.ts +8 -8
  88. package/src/session.ts +177 -169
  89. package/src/shared-prefix.ts +29 -29
  90. package/src/sim-platform.ts +20 -20
  91. package/src/turn-cards.ts +117 -117
  92. package/src/web-ui.ts +205 -205
@@ -1,18 +1,18 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import {
3
- buildProgressCard,
4
- buildHelpCard,
2
+ import {
3
+ buildProgressCard,
4
+ buildHelpCard,
5
5
  buildCdContent,
6
6
  buildCdCard,
7
7
  buildSessionsCard,
8
- buildStatusCard,
9
- buildCodexUsageCard,
10
- buildCodexResetConfirmCard,
11
- buildButtons,
12
- truncateContent,
13
- getToolEmoji,
14
- } from "../cards.ts";
15
- import { ABD_HELP_LINE } from "../shared-prefix.ts";
8
+ buildStatusCard,
9
+ buildCodexUsageCard,
10
+ buildCodexResetConfirmCard,
11
+ buildButtons,
12
+ truncateContent,
13
+ getToolEmoji,
14
+ } from "../cards.ts";
15
+ import { ABD_HELP_LINE } from "../shared-prefix.ts";
16
16
 
17
17
  // ---------------------------------------------------------------------------
18
18
  // truncateContent
@@ -154,32 +154,32 @@ describe("buildHelpCard", () => {
154
154
  expect(parsed.elements[0].text.content).toContain("你好");
155
155
  });
156
156
 
157
- it("includes action buttons", () => {
158
- const card = buildHelpCard("test");
159
- const parsed = JSON.parse(card);
160
- const action = parsed.elements[2];
161
- expect(action.tag).toBe("action");
162
- expect(action.actions).toHaveLength(7);
163
- });
164
-
165
- it("adds ABD prefix help as the final help line", () => {
166
- const card = buildHelpCard("test");
167
- const parsed = JSON.parse(card);
168
- const lines = parsed.elements[1].text.content.split("\n");
169
-
170
- expect(lines).toContain("发送 **/usage** 查看 Codex 实际存在的 5h/7天用量窗口,以及查询/使用主动重置卡");
171
- expect(lines.at(-1)).toBe(ABD_HELP_LINE);
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
- });
182
- });
157
+ it("includes action buttons", () => {
158
+ const card = buildHelpCard("test");
159
+ const parsed = JSON.parse(card);
160
+ const action = parsed.elements[2];
161
+ expect(action.tag).toBe("action");
162
+ expect(action.actions).toHaveLength(7);
163
+ });
164
+
165
+ it("adds ABD prefix help as the final help line", () => {
166
+ const card = buildHelpCard("test");
167
+ const parsed = JSON.parse(card);
168
+ const lines = parsed.elements[1].text.content.split("\n");
169
+
170
+ expect(lines).toContain("发送 **/usage** 查看 Codex 实际存在的 5h/7天用量窗口,以及查询/使用主动重置卡");
171
+ expect(lines.at(-1)).toBe(ABD_HELP_LINE);
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
+ });
182
+ });
183
183
 
184
184
  // ---------------------------------------------------------------------------
185
185
  // buildCdContent
@@ -258,7 +258,7 @@ describe("buildCdCard", () => {
258
258
  it("shows current working directory in markdown", () => {
259
259
  const card = buildCdCard("/home/project", entries, []);
260
260
  const parsed = JSON.parse(card);
261
- const cwdContent = mdContents(parsed).find((c) => c.includes("本会话默认工作路径"));
261
+ const cwdContent = mdContents(parsed).find((c) => c.includes("后续新建会话默认工作路径"));
262
262
  expect(cwdContent).toBeDefined();
263
263
  expect(cwdContent).toContain("/home/project");
264
264
  });
@@ -373,29 +373,29 @@ describe("buildSessionsCard", () => {
373
373
  expect(content).toContain("Cursor 会话");
374
374
  });
375
375
 
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", () => {
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", () => {
399
399
  const card = buildSessionsCard([
400
400
  { sessionId: "abc123", chatName: "帮我写代码-src", chatId: "oc_abc123", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
401
401
  ]);
@@ -403,19 +403,20 @@ describe("buildSessionsCard", () => {
403
403
  expect(parsed.elements[0].text.content).toContain("帮我写代码-src");
404
404
  });
405
405
 
406
- it("shows (群聊) tag for group chat sessions and not for private chats", () => {
407
- const card = buildSessionsCard([
408
- { sessionId: "g1", chatName: "group-chat", chatId: "oc_group1", active: false, turnCount: 1, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
409
- { sessionId: "p1", chatName: "private-chat", chatId: "ou_private1", active: false, turnCount: 1, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
410
- ]);
411
- const content: string = JSON.parse(card).elements[0].text.content;
412
- expect(content).toContain("(群聊)");
406
+ it("shows (群聊) tag for group chat sessions and not for private chats", () => {
407
+ const card = buildSessionsCard([
408
+ { sessionId: "g1", chatName: "group-chat", chatId: "oc_group1", chatType: "group", active: false, turnCount: 1, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
409
+ { sessionId: "p1", chatName: "private-chat", chatId: "oc_private1", chatType: "p2p", active: false, turnCount: 1, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
410
+ ]);
411
+ const content: string = JSON.parse(card).elements[0].text.content;
412
+ expect(content).toContain("(群聊)");
413
+ expect(content).toContain("(私聊)");
413
414
  // 群聊会话包含 (群聊)
414
415
  expect(content).toMatch(/group-chat.*\(群聊\)/);
415
416
  // 私聊会话不包含 (群聊)
416
417
  expect(content).toMatch(/private-chat/);
417
- const afterPrivateChat = content.split("private-chat")[1];
418
- expect(afterPrivateChat).not.toContain("(群聊)");
418
+ const afterPrivateChat = content.split("private-chat")[1];
419
+ expect(afterPrivateChat).not.toContain("(群聊)");
419
420
  });
420
421
 
421
422
  it("shows chat id missing for sessions without a chat binding", () => {
@@ -426,13 +427,23 @@ describe("buildSessionsCard", () => {
426
427
  expect(content).toContain("chat id缺失");
427
428
  });
428
429
 
429
- it("includes /session help text in non-empty card", () => {
430
+ it("includes /session help text in non-empty card", () => {
430
431
  const card = buildSessionsCard([
431
432
  { sessionId: "abc123", chatName: "", chatId: "oc_abc123", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
432
433
  ]);
433
434
  const parsed = JSON.parse(card);
434
- expect(parsed.elements[2].text.content).toContain("/session 数字");
435
- });
435
+ expect(parsed.elements[2].text.content).toContain("/session 数字");
436
+ });
437
+
438
+ it("explains fixed Feishu private-session behavior without advertising switching", () => {
439
+ const card = buildSessionsCard([
440
+ { sessionId: "abc123", chatName: "飞书私聊", chatId: "ou_private", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
441
+ ], { fixedPrivateSession: true });
442
+ const parsed = JSON.parse(card);
443
+ expect(parsed.elements[2].text.content).toContain("固定的专属会话");
444
+ expect(parsed.elements[2].text.content).toContain("不支持 **/session** 切换");
445
+ expect(parsed.elements[2].text.content).not.toContain("/session 数字");
446
+ });
436
447
 
437
448
  it("includes close button", () => {
438
449
  const card = buildSessionsCard([]);
@@ -446,7 +457,7 @@ describe("buildSessionsCard", () => {
446
457
  // buildStatusCard
447
458
  // ---------------------------------------------------------------------------
448
459
 
449
- describe("buildStatusCard", () => {
460
+ describe("buildStatusCard", () => {
450
461
  it("returns valid JSON with status text", () => {
451
462
  const card = buildStatusCard("一切正常");
452
463
  const parsed = JSON.parse(card);
@@ -466,54 +477,54 @@ describe("buildStatusCard", () => {
466
477
  const action = parsed.elements[2];
467
478
  expect(action.actions[0].text.content).toBe("收起");
468
479
  });
469
- });
470
-
471
- // ---------------------------------------------------------------------------
472
- // buildCodexUsageCard / buildCodexResetConfirmCard
473
- // ---------------------------------------------------------------------------
474
-
475
- describe("Codex usage reset cards", () => {
476
- it("shows the reset button only when reset credits are available", () => {
477
- const card = buildCodexUsageCard("Codex 用量", 2);
478
- const parsed = JSON.parse(card);
479
- const action = parsed.elements.find((element: any) => element.tag === "action");
480
- expect(action.actions[0].text.content).toBe("发起重置");
481
- expect(action.actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
482
-
483
- const noCreditCard = buildCodexUsageCard("Codex 用量", 0);
484
- const noCreditParsed = JSON.parse(noCreditCard);
485
- expect(noCreditParsed.elements.some((element: any) => element.tag === "action")).toBe(false);
486
- });
487
-
488
- it("builds yes/no confirmation buttons tied to the parent usage card", () => {
489
- const card = buildCodexResetConfirmCard({
490
- availableCount: 2,
491
- parentMessageId: "usage-message",
492
- requestId: "request-1",
493
- });
494
- const parsed = JSON.parse(card);
495
- const action = parsed.elements.find((element: any) => element.tag === "action");
496
-
497
- expect(action.actions[0].text.content).toBe("是,发起重置");
498
- expect(action.actions[0].value).toEqual({
499
- action: "codex_reset_confirm",
500
- decision: "yes",
501
- parentMessageId: "usage-message",
502
- requestId: "request-1",
503
- });
504
- expect(action.actions[1].text.content).toBe("否");
505
- expect(action.actions[1].value).toEqual({
506
- action: "codex_reset_confirm",
507
- decision: "no",
508
- parentMessageId: "usage-message",
509
- requestId: "request-1",
510
- });
511
- });
512
- });
513
-
514
- // ---------------------------------------------------------------------------
515
- // buildButtons
516
- // ---------------------------------------------------------------------------
480
+ });
481
+
482
+ // ---------------------------------------------------------------------------
483
+ // buildCodexUsageCard / buildCodexResetConfirmCard
484
+ // ---------------------------------------------------------------------------
485
+
486
+ describe("Codex usage reset cards", () => {
487
+ it("shows the reset button only when reset credits are available", () => {
488
+ const card = buildCodexUsageCard("Codex 用量", 2);
489
+ const parsed = JSON.parse(card);
490
+ const action = parsed.elements.find((element: any) => element.tag === "action");
491
+ expect(action.actions[0].text.content).toBe("发起重置");
492
+ expect(action.actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
493
+
494
+ const noCreditCard = buildCodexUsageCard("Codex 用量", 0);
495
+ const noCreditParsed = JSON.parse(noCreditCard);
496
+ expect(noCreditParsed.elements.some((element: any) => element.tag === "action")).toBe(false);
497
+ });
498
+
499
+ it("builds yes/no confirmation buttons tied to the parent usage card", () => {
500
+ const card = buildCodexResetConfirmCard({
501
+ availableCount: 2,
502
+ parentMessageId: "usage-message",
503
+ requestId: "request-1",
504
+ });
505
+ const parsed = JSON.parse(card);
506
+ const action = parsed.elements.find((element: any) => element.tag === "action");
507
+
508
+ expect(action.actions[0].text.content).toBe("是,发起重置");
509
+ expect(action.actions[0].value).toEqual({
510
+ action: "codex_reset_confirm",
511
+ decision: "yes",
512
+ parentMessageId: "usage-message",
513
+ requestId: "request-1",
514
+ });
515
+ expect(action.actions[1].text.content).toBe("否");
516
+ expect(action.actions[1].value).toEqual({
517
+ action: "codex_reset_confirm",
518
+ decision: "no",
519
+ parentMessageId: "usage-message",
520
+ requestId: "request-1",
521
+ });
522
+ });
523
+ });
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // buildButtons
527
+ // ---------------------------------------------------------------------------
517
528
 
518
529
  describe("buildButtons", () => {
519
530
  it("returns action with buttons", () => {
@@ -1,114 +1,114 @@
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
- isLoopFinished: vi.fn(() => ({ loopFinished: true })),
18
- stepCountIs: vi.fn((count: number) => ({ count })),
19
- jsonSchema: vi.fn((schema: unknown) => schema),
20
- tool: vi.fn((definition: unknown) => definition),
21
- }));
22
-
23
- async function* textStream(...chunks: string[]): AsyncIterable<string> {
24
- for (const chunk of chunks) yield chunk;
25
- }
26
-
27
- async function* fullStream(...parts: unknown[]): AsyncIterable<unknown> {
28
- for (const part of parts) yield part;
29
- }
30
-
31
- afterEach(() => {
32
- streamTextMock.mockReset();
33
- generateTextMock.mockReset();
34
- });
35
-
36
- describe("createCccAdapter", () => {
37
- it("creates a persisted ccc session and exposes model/cwd metadata", async () => {
38
- const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
39
- const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-meta-"));
40
- const adapter = createCccAdapter({
41
- apiKey: "sk-test",
42
- contextDir,
43
- model: "deepseek-v4-pro",
44
- });
45
-
46
- const created = await adapter.createSession("F:\\repo");
47
- const info = await adapter.getSessionInfo(created.sessionId);
48
-
49
- expect(created.sessionId).toMatch(/^session-\d{8}-\d{6}-[a-f0-9]{6}$/);
50
- expect(info).toEqual(expect.objectContaining({
51
- sessionId: created.sessionId,
52
- cwd: "F:\\repo",
53
- model: "deepseek-v4-pro",
54
- }));
55
- });
56
-
57
- it("maps ChatSession text chunks to unified assistant text blocks", async () => {
58
- const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
59
- const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-stream-"));
60
- const adapter = createCccAdapter({
61
- apiKey: "sk-test",
62
- contextDir,
63
- model: "deepseek-v4-flash",
64
- });
65
- const { sessionId } = await adapter.createSession("F:\\repo");
66
- streamTextMock.mockReturnValueOnce({ textStream: textStream("hello", " world") });
67
-
68
- const messages = [];
69
- for await (const message of adapter.prompt(sessionId, "hi", "F:\\repo")) {
70
- messages.push(message);
71
- }
72
-
73
- expect(messages).toEqual([
74
- { type: "assistant", blocks: [{ type: "text", text: "hello" }] },
75
- { type: "assistant", blocks: [{ type: "text", text: " world" }] },
76
- ]);
77
- expect(streamTextMock).toHaveBeenCalledWith(expect.objectContaining({
78
- model: { modelId: "deepseek-v4-flash" },
79
- }));
80
- });
81
-
82
- it("maps ChatSession tool events to unified tool blocks", async () => {
83
- const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
84
- const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-tools-"));
85
- const adapter = createCccAdapter({
86
- apiKey: "sk-test",
87
- contextDir,
88
- model: "deepseek-v4-flash",
89
- });
90
- const { sessionId } = await adapter.createSession("F:\\repo");
91
- streamTextMock.mockReturnValueOnce({
92
- fullStream: fullStream(
93
- { type: "tool-call", toolCallId: "call-1", toolName: "read_file", input: { path: "README.md" } },
94
- { type: "tool-result", toolCallId: "call-1", toolName: "read_file", output: { content: "hello" } },
95
- ),
96
- });
97
-
98
- const messages = [];
99
- for await (const message of adapter.prompt(sessionId, "read", "F:\\repo")) {
100
- messages.push(message);
101
- }
102
-
103
- expect(messages).toEqual([
104
- {
105
- type: "assistant",
106
- blocks: [{ type: "tool_use", id: "call-1", name: "read_file", input: { path: "README.md" } }],
107
- },
108
- {
109
- type: "assistant",
110
- blocks: [{ type: "tool_result", tool_use_id: "call-1", content: { content: "hello" }, is_error: false }],
111
- },
112
- ]);
113
- });
114
- });
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
+ isLoopFinished: vi.fn(() => ({ loopFinished: true })),
18
+ stepCountIs: vi.fn((count: number) => ({ count })),
19
+ jsonSchema: vi.fn((schema: unknown) => schema),
20
+ tool: vi.fn((definition: unknown) => definition),
21
+ }));
22
+
23
+ async function* textStream(...chunks: string[]): AsyncIterable<string> {
24
+ for (const chunk of chunks) yield chunk;
25
+ }
26
+
27
+ async function* fullStream(...parts: unknown[]): AsyncIterable<unknown> {
28
+ for (const part of parts) yield part;
29
+ }
30
+
31
+ afterEach(() => {
32
+ streamTextMock.mockReset();
33
+ generateTextMock.mockReset();
34
+ });
35
+
36
+ describe("createCccAdapter", () => {
37
+ it("creates a persisted ccc session and exposes model/cwd metadata", async () => {
38
+ const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
39
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-meta-"));
40
+ const adapter = createCccAdapter({
41
+ apiKey: "sk-test",
42
+ contextDir,
43
+ model: "deepseek-v4-pro",
44
+ });
45
+
46
+ const created = await adapter.createSession("F:\\repo");
47
+ const info = await adapter.getSessionInfo(created.sessionId);
48
+
49
+ expect(created.sessionId).toMatch(/^session-\d{8}-\d{6}-[a-f0-9]{6}$/);
50
+ expect(info).toEqual(expect.objectContaining({
51
+ sessionId: created.sessionId,
52
+ cwd: "F:\\repo",
53
+ model: "deepseek-v4-pro",
54
+ }));
55
+ });
56
+
57
+ it("maps ChatSession text chunks to unified assistant text blocks", async () => {
58
+ const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
59
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-stream-"));
60
+ const adapter = createCccAdapter({
61
+ apiKey: "sk-test",
62
+ contextDir,
63
+ model: "deepseek-v4-flash",
64
+ });
65
+ const { sessionId } = await adapter.createSession("F:\\repo");
66
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("hello", " world") });
67
+
68
+ const messages = [];
69
+ for await (const message of adapter.prompt(sessionId, "hi", "F:\\repo")) {
70
+ messages.push(message);
71
+ }
72
+
73
+ expect(messages).toEqual([
74
+ { type: "assistant", blocks: [{ type: "text", text: "hello" }] },
75
+ { type: "assistant", blocks: [{ type: "text", text: " world" }] },
76
+ ]);
77
+ expect(streamTextMock).toHaveBeenCalledWith(expect.objectContaining({
78
+ model: { modelId: "deepseek-v4-flash" },
79
+ }));
80
+ });
81
+
82
+ it("maps ChatSession tool events to unified tool blocks", async () => {
83
+ const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
84
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-tools-"));
85
+ const adapter = createCccAdapter({
86
+ apiKey: "sk-test",
87
+ contextDir,
88
+ model: "deepseek-v4-flash",
89
+ });
90
+ const { sessionId } = await adapter.createSession("F:\\repo");
91
+ streamTextMock.mockReturnValueOnce({
92
+ fullStream: fullStream(
93
+ { type: "tool-call", toolCallId: "call-1", toolName: "read_file", input: { path: "README.md" } },
94
+ { type: "tool-result", toolCallId: "call-1", toolName: "read_file", output: { content: "hello" } },
95
+ ),
96
+ });
97
+
98
+ const messages = [];
99
+ for await (const message of adapter.prompt(sessionId, "read", "F:\\repo")) {
100
+ messages.push(message);
101
+ }
102
+
103
+ expect(messages).toEqual([
104
+ {
105
+ type: "assistant",
106
+ blocks: [{ type: "tool_use", id: "call-1", name: "read_file", input: { path: "README.md" } }],
107
+ },
108
+ {
109
+ type: "assistant",
110
+ blocks: [{ type: "tool_result", tool_use_id: "call-1", content: { content: "hello" }, is_error: false }],
111
+ },
112
+ ]);
113
+ });
114
+ });