chatccc 0.2.195 → 0.2.197

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 (51) 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 -0
  6. package/src/__tests__/builtin-chat-session.test.ts +277 -181
  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 -196
  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 -113
  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 -0
  19. package/src/__tests__/codex-raw-stream-log.test.ts +120 -0
  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 +99 -2
  23. package/src/__tests__/feishu-avatar.test.ts +40 -40
  24. package/src/__tests__/orchestrator.test.ts +181 -154
  25. package/src/__tests__/raw-stream-log.test.ts +106 -106
  26. package/src/__tests__/session.test.ts +40 -40
  27. package/src/__tests__/sim-platform.test.ts +12 -12
  28. package/src/__tests__/web-ui.test.ts +209 -209
  29. package/src/adapters/ccc-adapter.ts +121 -119
  30. package/src/adapters/claude-adapter.ts +603 -566
  31. package/src/adapters/codex-adapter.ts +57 -32
  32. package/src/adapters/cursor-adapter.ts +182 -57
  33. package/src/adapters/raw-stream-log.ts +124 -124
  34. package/src/agent-reload-config-rpc.ts +34 -0
  35. package/src/builtin/cli.ts +473 -461
  36. package/src/builtin/context.ts +323 -323
  37. package/src/builtin/file-tools.ts +1072 -915
  38. package/src/builtin/index.ts +404 -353
  39. package/src/builtin/session-select.ts +48 -48
  40. package/src/builtin/sigint.ts +50 -50
  41. package/src/cards.ts +195 -195
  42. package/src/chatgpt-subscription-rpc.ts +27 -27
  43. package/src/chatgpt-subscription.ts +299 -299
  44. package/src/chrome-devtools-guard.ts +318 -318
  45. package/src/config.ts +125 -125
  46. package/src/feishu-api.ts +49 -49
  47. package/src/index.ts +8 -13
  48. package/src/orchestrator.ts +166 -145
  49. package/src/runtime-reload.ts +34 -0
  50. package/src/session.ts +132 -130
  51. package/src/web-ui.ts +205 -205
@@ -1,106 +1,106 @@
1
- import { gunzipSync } from "node:zlib";
2
- import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
-
6
- import { describe, expect, it } from "vitest";
7
-
8
- import {
9
- createRawStreamLog,
10
- sanitizeLogPathSegment,
11
- } from "../adapters/raw-stream-log.ts";
12
-
13
- describe("raw stream log", () => {
14
- it("does nothing when disabled", async () => {
15
- const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
16
- try {
17
- const log = await createRawStreamLog({
18
- enabled: false,
19
- rootDir: root,
20
- tool: "cursor",
21
- sessionId: "sid",
22
- label: "turn",
23
- maxBytesPerTurn: 1024,
24
- retentionDays: 7,
25
- });
26
- expect(log).toBeNull();
27
- } finally {
28
- await rm(root, { recursive: true, force: true });
29
- }
30
- });
31
-
32
- it("writes gzipped JSONL and can keep the file", async () => {
33
- const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
34
- try {
35
- const log = await createRawStreamLog({
36
- enabled: true,
37
- rootDir: root,
38
- tool: "cursor",
39
- sessionId: "sid/unsafe",
40
- label: "turn:1",
41
- maxBytesPerTurn: 1024,
42
- retentionDays: 7,
43
- });
44
- expect(log).not.toBeNull();
45
- log!.writeLine('{"type":"assistant","text":"hello"}');
46
- await log!.close({ keep: true });
47
-
48
- const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
49
- expect(raw).toBe('{"type":"assistant","text":"hello"}\n');
50
- expect(await stat(log!.filePath)).toBeTruthy();
51
- } finally {
52
- await rm(root, { recursive: true, force: true });
53
- }
54
- });
55
-
56
- it("writes a truncation marker once maxBytesPerTurn is exceeded", async () => {
57
- const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
58
- try {
59
- const log = await createRawStreamLog({
60
- enabled: true,
61
- rootDir: root,
62
- tool: "cursor",
63
- sessionId: "sid",
64
- label: "turn",
65
- maxBytesPerTurn: 12,
66
- retentionDays: 7,
67
- });
68
- log!.writeLine('{"a":1}');
69
- log!.writeLine('{"too":"large"}');
70
- log!.writeLine('{"ignored":true}');
71
- await log!.close({ keep: true });
72
-
73
- const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
74
- expect(raw).toContain('{"a":1}');
75
- expect(raw).toContain("chatccc_raw_stream_log_truncated");
76
- expect(raw).not.toContain("ignored");
77
- } finally {
78
- await rm(root, { recursive: true, force: true });
79
- }
80
- });
81
-
82
- it("removes the file when keep is false", async () => {
83
- const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
84
- try {
85
- const log = await createRawStreamLog({
86
- enabled: true,
87
- rootDir: root,
88
- tool: "cursor",
89
- sessionId: "sid",
90
- label: "turn",
91
- maxBytesPerTurn: 1024,
92
- retentionDays: 7,
93
- });
94
- log!.writeLine('{"type":"result"}');
95
- await log!.close({ keep: false });
96
- await expect(stat(log!.filePath)).rejects.toThrow();
97
- } finally {
98
- await rm(root, { recursive: true, force: true });
99
- }
100
- });
101
-
102
- it("sanitizes path segments", () => {
103
- expect(sanitizeLogPathSegment("../sid:1/2")).toBe("sid_1_2");
104
- expect(sanitizeLogPathSegment("")).toBe("unknown");
105
- });
106
- });
1
+ import { gunzipSync } from "node:zlib";
2
+ import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ import {
9
+ createRawStreamLog,
10
+ sanitizeLogPathSegment,
11
+ } from "../adapters/raw-stream-log.ts";
12
+
13
+ describe("raw stream log", () => {
14
+ it("does nothing when disabled", async () => {
15
+ const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
16
+ try {
17
+ const log = await createRawStreamLog({
18
+ enabled: false,
19
+ rootDir: root,
20
+ tool: "cursor",
21
+ sessionId: "sid",
22
+ label: "turn",
23
+ maxBytesPerTurn: 1024,
24
+ retentionDays: 7,
25
+ });
26
+ expect(log).toBeNull();
27
+ } finally {
28
+ await rm(root, { recursive: true, force: true });
29
+ }
30
+ });
31
+
32
+ it("writes gzipped JSONL and can keep the file", async () => {
33
+ const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
34
+ try {
35
+ const log = await createRawStreamLog({
36
+ enabled: true,
37
+ rootDir: root,
38
+ tool: "cursor",
39
+ sessionId: "sid/unsafe",
40
+ label: "turn:1",
41
+ maxBytesPerTurn: 1024,
42
+ retentionDays: 7,
43
+ });
44
+ expect(log).not.toBeNull();
45
+ log!.writeLine('{"type":"assistant","text":"hello"}');
46
+ await log!.close({ keep: true });
47
+
48
+ const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
49
+ expect(raw).toBe('{"type":"assistant","text":"hello"}\n');
50
+ expect(await stat(log!.filePath)).toBeTruthy();
51
+ } finally {
52
+ await rm(root, { recursive: true, force: true });
53
+ }
54
+ });
55
+
56
+ it("writes a truncation marker once maxBytesPerTurn is exceeded", async () => {
57
+ const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
58
+ try {
59
+ const log = await createRawStreamLog({
60
+ enabled: true,
61
+ rootDir: root,
62
+ tool: "cursor",
63
+ sessionId: "sid",
64
+ label: "turn",
65
+ maxBytesPerTurn: 12,
66
+ retentionDays: 7,
67
+ });
68
+ log!.writeLine('{"a":1}');
69
+ log!.writeLine('{"too":"large"}');
70
+ log!.writeLine('{"ignored":true}');
71
+ await log!.close({ keep: true });
72
+
73
+ const raw = gunzipSync(await readFile(log!.filePath)).toString("utf-8");
74
+ expect(raw).toContain('{"a":1}');
75
+ expect(raw).toContain("chatccc_raw_stream_log_truncated");
76
+ expect(raw).not.toContain("ignored");
77
+ } finally {
78
+ await rm(root, { recursive: true, force: true });
79
+ }
80
+ });
81
+
82
+ it("removes the file when keep is false", async () => {
83
+ const root = await mkdtemp(join(tmpdir(), "chatccc-raw-log-"));
84
+ try {
85
+ const log = await createRawStreamLog({
86
+ enabled: true,
87
+ rootDir: root,
88
+ tool: "cursor",
89
+ sessionId: "sid",
90
+ label: "turn",
91
+ maxBytesPerTurn: 1024,
92
+ retentionDays: 7,
93
+ });
94
+ log!.writeLine('{"type":"result"}');
95
+ await log!.close({ keep: false });
96
+ await expect(stat(log!.filePath)).rejects.toThrow();
97
+ } finally {
98
+ await rm(root, { recursive: true, force: true });
99
+ }
100
+ });
101
+
102
+ it("sanitizes path segments", () => {
103
+ expect(sanitizeLogPathSegment("../sid:1/2")).toBe("sid_1_2");
104
+ expect(sanitizeLogPathSegment("")).toBe("unknown");
105
+ });
106
+ });
@@ -91,12 +91,12 @@ import {
91
91
  stopSession,
92
92
  startUnifiedDisplayLoop,
93
93
  stopUnifiedDisplayLoop,
94
- _setProcessAliveForTest,
95
- _resetProcessAliveForTest,
96
- _setProcessMonitorIntervalForTest,
97
- _resetProcessMonitorIntervalForTest,
98
- setSessionEffortOverride,
99
- } from "../session.ts";
94
+ _setProcessAliveForTest,
95
+ _resetProcessAliveForTest,
96
+ _setProcessMonitorIntervalForTest,
97
+ _resetProcessMonitorIntervalForTest,
98
+ setSessionEffortOverride,
99
+ } from "../session.ts";
100
100
  import {
101
101
  activePrompts,
102
102
  bindChatToSession,
@@ -896,17 +896,17 @@ describe("getSessionStatus", () => {
896
896
  expect(status!.effort).not.toBeNull();
897
897
  // model 必为字符串(留空时显示 '(留空)',否则为环境变量值);不应是占位符
898
898
  expect(typeof status!.model).toBe("string");
899
- expect(status!.model.length).toBeGreaterThan(0);
900
- });
901
-
902
- it("Codex session status uses the per-session effort override", async () => {
903
- mockSessionInfo("chat-codex", { sessionId: "sid-codex-effort", tool: "codex" });
904
- setSessionEffortOverride("sid-codex-effort", "xhigh");
905
-
906
- const status = await getSessionStatus("chat-codex");
907
-
908
- expect(status!.effort).toBe("xhigh");
909
- });
899
+ expect(status!.model.length).toBeGreaterThan(0);
900
+ });
901
+
902
+ it("Codex session status uses the per-session effort override", async () => {
903
+ mockSessionInfo("chat-codex", { sessionId: "sid-codex-effort", tool: "codex" });
904
+ setSessionEffortOverride("sid-codex-effort", "xhigh");
905
+
906
+ const status = await getSessionStatus("chat-codex");
907
+
908
+ expect(status!.effort).toBe("xhigh");
909
+ });
910
910
 
911
911
  it("Cursor 会话:effort 恒为 null(卡片渲染时隐藏该行,避免显示无意义的 effort)", async () => {
912
912
  mockSessionInfo("chat-cursor", { sessionId: "sid-cur", tool: "cursor" });
@@ -942,29 +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
-
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
- });
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
+ });
968
968
 
969
969
  describe("getAllSessionsStatus", () => {
970
970
  let registryFile = "";
@@ -62,21 +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("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 正常工作", () => {
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 正常工作", () => {
76
76
  const notice = SimulatedPlatform.formatDelayNotice(Date.now() - 20 * 60 * 1000, "测试消息");
77
77
  expect(notice).toBeDefined();
78
78
  expect(notice).toContain("延迟送达");
79
79
  // 近期消息不触发
80
80
  expect(SimulatedPlatform.formatDelayNotice(Date.now(), "test")).toBeNull();
81
81
  });
82
- });
82
+ });