chatccc 0.2.188 → 0.2.190

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 (67) hide show
  1. package/agent-prompts/claude_specific.md +45 -45
  2. package/agent-prompts/codex_specific.md +2 -2
  3. package/agent-prompts/cursor_specific.md +13 -13
  4. package/config.sample.json +14 -14
  5. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  6. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  7. package/im-skills/feishu-skill/skill.md +3 -3
  8. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  9. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  10. package/im-skills/wechat-file-skill/skill.md +10 -10
  11. package/im-skills/wechat-image-skill/skill.md +10 -10
  12. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  13. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  14. package/im-skills/wechat-video-skill/skill.md +10 -10
  15. package/package.json +1 -1
  16. package/scripts/postinstall-sharp-check.mjs +58 -58
  17. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  18. package/src/__tests__/builtin-config.test.ts +33 -33
  19. package/src/__tests__/card-plain-text.test.ts +5 -5
  20. package/src/__tests__/cardkit.test.ts +60 -60
  21. package/src/__tests__/cards.test.ts +77 -77
  22. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  23. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  24. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  25. package/src/__tests__/claude-adapter.test.ts +592 -592
  26. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  27. package/src/__tests__/config-reload.test.ts +4 -4
  28. package/src/__tests__/config-sample.test.ts +17 -17
  29. package/src/__tests__/feishu-api.test.ts +60 -60
  30. package/src/__tests__/feishu-platform.test.ts +22 -22
  31. package/src/__tests__/format-message.test.ts +47 -47
  32. package/src/__tests__/orchestrator.test.ts +116 -112
  33. package/src/__tests__/privacy.test.ts +198 -198
  34. package/src/__tests__/raw-stream-log.test.ts +106 -106
  35. package/src/__tests__/session.test.ts +17 -17
  36. package/src/__tests__/shared-prefix.test.ts +36 -36
  37. package/src/__tests__/stream-state.test.ts +42 -42
  38. package/src/__tests__/web-ui.test.ts +209 -130
  39. package/src/adapters/claude-adapter.ts +566 -566
  40. package/src/adapters/claude-session-meta-store.ts +120 -120
  41. package/src/adapters/codex-adapter.ts +27 -28
  42. package/src/adapters/cursor-adapter.ts +46 -46
  43. package/src/adapters/raw-stream-log.ts +124 -124
  44. package/src/adapters/resource-monitor.ts +140 -140
  45. package/src/agent-delegate-task-rpc.ts +153 -153
  46. package/src/agent-delegate-task.ts +81 -81
  47. package/src/agent-stop-stuck.ts +129 -129
  48. package/src/builtin/cli.ts +189 -189
  49. package/src/builtin/index.ts +170 -168
  50. package/src/cards.ts +137 -137
  51. package/src/chatgpt-subscription-rpc.ts +27 -27
  52. package/src/chatgpt-subscription.ts +299 -299
  53. package/src/chrome-devtools-guard.ts +318 -318
  54. package/src/codex-reset-actions.ts +184 -184
  55. package/src/config.ts +86 -86
  56. package/src/feishu-platform.ts +20 -20
  57. package/src/format-message.ts +293 -293
  58. package/src/litellm-proxy.ts +374 -374
  59. package/src/orchestrator.ts +143 -143
  60. package/src/privacy.ts +118 -118
  61. package/src/session-chat-binding.ts +6 -6
  62. package/src/session-name.ts +8 -8
  63. package/src/session.ts +98 -98
  64. package/src/shared-prefix.ts +29 -29
  65. package/src/sim-platform.ts +20 -20
  66. package/src/turn-cards.ts +117 -117
  67. package/src/web-ui.ts +142 -24
@@ -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" });
@@ -1,36 +1,36 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- ABD_APPEND_PROMPT,
5
- applySharedPrefix,
6
- } from "../shared-prefix.ts";
7
-
8
- describe("applySharedPrefix", () => {
9
- it("leaves normal messages unchanged", () => {
10
- expect(applySharedPrefix("帮我分析")).toEqual({
11
- matched: false,
12
- text: "帮我分析",
13
- body: "帮我分析",
14
- });
15
- });
16
-
17
- it("removes /abd without requiring a space", () => {
18
- const result = applySharedPrefix("/abd帮我分析");
19
-
20
- expect(result.matched).toBe(true);
21
- expect(result.body).toBe("帮我分析");
22
- expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
23
- });
24
-
25
- it("removes whitespace after /abd", () => {
26
- const result = applySharedPrefix("/abd 帮我分析");
27
-
28
- expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
29
- });
30
-
31
- it("keeps the appendix when the user body is empty", () => {
32
- const result = applySharedPrefix("/abd ");
33
-
34
- expect(result.text).toBe(`---\n${ABD_APPEND_PROMPT}`);
35
- });
36
- });
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ ABD_APPEND_PROMPT,
5
+ applySharedPrefix,
6
+ } from "../shared-prefix.ts";
7
+
8
+ describe("applySharedPrefix", () => {
9
+ it("leaves normal messages unchanged", () => {
10
+ expect(applySharedPrefix("帮我分析")).toEqual({
11
+ matched: false,
12
+ text: "帮我分析",
13
+ body: "帮我分析",
14
+ });
15
+ });
16
+
17
+ it("removes /abd without requiring a space", () => {
18
+ const result = applySharedPrefix("/abd帮我分析");
19
+
20
+ expect(result.matched).toBe(true);
21
+ expect(result.body).toBe("帮我分析");
22
+ expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
23
+ });
24
+
25
+ it("removes whitespace after /abd", () => {
26
+ const result = applySharedPrefix("/abd 帮我分析");
27
+
28
+ expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
29
+ });
30
+
31
+ it("keeps the appendix when the user body is empty", () => {
32
+ const result = applySharedPrefix("/abd ");
33
+
34
+ expect(result.text).toBe(`---\n${ABD_APPEND_PROMPT}`);
35
+ });
36
+ });
@@ -15,12 +15,12 @@ vi.mock("../config.ts", async () => {
15
15
  });
16
16
 
17
17
  const {
18
- writeStreamState,
19
- readStreamState,
20
- createEmptyStreamState,
21
- isFinalReplySentForTurn,
22
- markFinalReplySent,
23
- STREAMS_DIR,
18
+ writeStreamState,
19
+ readStreamState,
20
+ createEmptyStreamState,
21
+ isFinalReplySentForTurn,
22
+ markFinalReplySent,
23
+ STREAMS_DIR,
24
24
  _setRenameForTest,
25
25
  _resetRenameForTest,
26
26
  } = await import("../stream-state.ts");
@@ -119,36 +119,36 @@ describe("writeStreamState — atomic rename", () => {
119
119
  const got = await readStreamState("never-existed");
120
120
  expect(got).toBeNull();
121
121
  });
122
- it("marks final reply delivery for the matching terminal turn only", async () => {
123
- const state = createEmptyStreamState("sid-delivered", "/tmp", "claude", 2);
124
- state.status = "done";
125
- state.finalReply = "done";
126
- await writeStreamState(state);
127
-
128
- await markFinalReplySent("sid-delivered", 2, 12345);
129
-
130
- const got = await readStreamState("sid-delivered");
131
- expect(got).not.toBeNull();
132
- expect(got!.finalReplySentTurn).toBe(2);
133
- expect(got!.finalReplySentAt).toBe(12345);
134
- expect(isFinalReplySentForTurn(got!)).toBe(true);
135
- });
136
-
137
- it("does not mark a running state or a different turn as delivered", async () => {
138
- const state = createEmptyStreamState("sid-running", "/tmp", "claude", 3);
139
- await writeStreamState(state);
140
-
141
- await markFinalReplySent("sid-running", 2, 12345);
142
- await markFinalReplySent("sid-running", 3, 12345);
143
-
144
- const got = await readStreamState("sid-running");
145
- expect(got).not.toBeNull();
146
- expect(got!.finalReplySentTurn).toBeUndefined();
147
- expect(isFinalReplySentForTurn(got!)).toBe(false);
148
- });
149
- });
150
-
151
- describe("createEmptyStreamState", () => {
122
+ it("marks final reply delivery for the matching terminal turn only", async () => {
123
+ const state = createEmptyStreamState("sid-delivered", "/tmp", "claude", 2);
124
+ state.status = "done";
125
+ state.finalReply = "done";
126
+ await writeStreamState(state);
127
+
128
+ await markFinalReplySent("sid-delivered", 2, 12345);
129
+
130
+ const got = await readStreamState("sid-delivered");
131
+ expect(got).not.toBeNull();
132
+ expect(got!.finalReplySentTurn).toBe(2);
133
+ expect(got!.finalReplySentAt).toBe(12345);
134
+ expect(isFinalReplySentForTurn(got!)).toBe(true);
135
+ });
136
+
137
+ it("does not mark a running state or a different turn as delivered", async () => {
138
+ const state = createEmptyStreamState("sid-running", "/tmp", "claude", 3);
139
+ await writeStreamState(state);
140
+
141
+ await markFinalReplySent("sid-running", 2, 12345);
142
+ await markFinalReplySent("sid-running", 3, 12345);
143
+
144
+ const got = await readStreamState("sid-running");
145
+ expect(got).not.toBeNull();
146
+ expect(got!.finalReplySentTurn).toBeUndefined();
147
+ expect(isFinalReplySentForTurn(got!)).toBe(false);
148
+ });
149
+ });
150
+
151
+ describe("createEmptyStreamState", () => {
152
152
  it("生成的 state 字段正确且 status=running", () => {
153
153
  const s = createEmptyStreamState("sid-X", "/cwd", "cursor", 5);
154
154
  expect(s.sessionId).toBe("sid-X");
@@ -156,9 +156,9 @@ describe("createEmptyStreamState", () => {
156
156
  expect(s.tool).toBe("cursor");
157
157
  expect(s.turnCount).toBe(5);
158
158
  expect(s.status).toBe("running");
159
- expect(s.accumulatedContent).toBe("");
160
- expect(s.finalReply).toBe("");
161
- expect(s.finalReplySentTurn).toBeUndefined();
162
- expect(s.chunkCount).toBe(0);
163
- });
164
- });
159
+ expect(s.accumulatedContent).toBe("");
160
+ expect(s.finalReply).toBe("");
161
+ expect(s.finalReplySentTurn).toBeUndefined();
162
+ expect(s.chunkCount).toBe(0);
163
+ });
164
+ });