chatccc 0.2.226 → 0.2.227

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 (64) hide show
  1. package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  2. package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  3. package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  4. package/README.md +90 -90
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-activity.test.ts +76 -76
  7. package/src/__tests__/builtin-chat-session.test.ts +350 -350
  8. package/src/__tests__/builtin-config.test.ts +26 -26
  9. package/src/__tests__/builtin-context.test.ts +163 -163
  10. package/src/__tests__/builtin-file-tools.test.ts +275 -275
  11. package/src/__tests__/builtin-permissions.test.ts +211 -211
  12. package/src/__tests__/builtin-session-select.test.ts +116 -116
  13. package/src/__tests__/builtin-skills.test.ts +185 -74
  14. package/src/__tests__/card-action-routing.test.ts +18 -18
  15. package/src/__tests__/ccc-adapter.test.ts +136 -136
  16. package/src/__tests__/claude-adapter.test.ts +614 -614
  17. package/src/__tests__/codex-adapter.test.ts +58 -58
  18. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  19. package/src/__tests__/cursor-adapter.test.ts +268 -268
  20. package/src/__tests__/feishu-avatar.test.ts +164 -164
  21. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  22. package/src/__tests__/package-files.test.ts +24 -24
  23. package/src/__tests__/progress-reducer.test.ts +110 -110
  24. package/src/__tests__/response-stall.test.ts +49 -49
  25. package/src/__tests__/sim-platform.test.ts +16 -16
  26. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  27. package/src/__tests__/stop-session.test.ts +34 -34
  28. package/src/__tests__/terminal-renderer.test.ts +247 -247
  29. package/src/__tests__/update-command-guard.test.ts +144 -144
  30. package/src/__tests__/web-ui.test.ts +326 -326
  31. package/src/adapters/adapter-interface.ts +18 -18
  32. package/src/adapters/ccc-adapter.ts +131 -131
  33. package/src/adapters/claude-adapter.ts +620 -620
  34. package/src/adapters/codex-adapter.ts +426 -426
  35. package/src/adapters/cursor-adapter.ts +681 -681
  36. package/src/agent-activity.ts +170 -170
  37. package/src/agent-delegate-task.ts +91 -91
  38. package/src/builtin/cli.ts +61 -2
  39. package/src/builtin/config.ts +84 -84
  40. package/src/builtin/context.ts +323 -323
  41. package/src/builtin/file-log.ts +38 -38
  42. package/src/builtin/index.ts +44 -24
  43. package/src/builtin/proc-tree-kill.ts +61 -61
  44. package/src/builtin/progress/cards-helpers.ts +76 -76
  45. package/src/builtin/progress/reducer.ts +108 -108
  46. package/src/builtin/progress/terminal-renderer.ts +294 -294
  47. package/src/builtin/progress/view.ts +77 -77
  48. package/src/builtin/raw-stream-log.ts +124 -124
  49. package/src/builtin/session-select.ts +48 -48
  50. package/src/builtin/skills.ts +126 -44
  51. package/src/card-action-routing.ts +14 -14
  52. package/src/feishu-api.ts +193 -193
  53. package/src/feishu-message-ingress.ts +195 -195
  54. package/src/index.ts +306 -306
  55. package/src/orchestrator.ts +2388 -2388
  56. package/src/platform-adapter.ts +6 -6
  57. package/src/progress/reducer.ts +108 -108
  58. package/src/progress/terminal-renderer.ts +294 -294
  59. package/src/progress/view.ts +77 -77
  60. package/src/response-stall.ts +28 -28
  61. package/src/session-chat-binding.ts +82 -82
  62. package/src/startup-lifecycle.ts +250 -250
  63. package/src/stream-state.ts +18 -18
  64. package/src/update-command-guard.ts +165 -165
@@ -1,77 +1,77 @@
1
- /**
2
- * progress/view.ts — 平台无关的"过程进度"视图模型
3
- *
4
- * 这是飞书过程卡片与终端过程区块共享的唯一数据模型:
5
- * - 飞书:buildProgressCard(view) 把 ProgressView 渲染为卡片 JSON
6
- * - 终端:TerminalProgressRenderer 把 ProgressView 渲染为 ANSI 区块
7
- * - 事件流:reduceProgress(prev, event) 把 ChatEvent 增量合并进 ProgressView
8
- *
9
- * 语义与飞书卡片对齐:
10
- * - headerTitle 是"生成中"阶段展示的标题(如"正在启动 Agent · 0秒")
11
- * - status 决定终态外观(完成 / 已停止 / 异常结束)
12
- * - text 是正文累积(对应卡片 main_content / 终端区块正文)
13
- * - tools 是本轮工具调用列表(飞书卡片暂不展示,终端折叠为单行)
14
- */
15
-
16
- export type ProgressStatus = "generating" | "done" | "stopped" | "error";
17
-
18
- export type ProgressToolStatus = "running" | "ok" | "error";
19
-
20
- export interface ProgressToolCall {
21
- /** 工具调用 ID(ChatEvent.tool_use.id,可能缺失) */
22
- id: string;
23
- /** 工具名,如 edit_file */
24
- name: string;
25
- status: ProgressToolStatus;
26
- /** 工具输入摘要(终端折叠行展示,截断保存) */
27
- detail?: string;
28
- /** 工具结果摘要(成功/失败,截断保存) */
29
- summary?: string;
30
- }
31
-
32
- export interface ProgressView {
33
- /** 当前阶段状态,决定终态外观 */
34
- status: ProgressStatus;
35
- /** 生成中阶段的头部标题(对应飞书卡片 header title) */
36
- headerTitle: string;
37
- /** 卡片头部颜色模板(飞书用),终端忽略 */
38
- headerTemplate: string;
39
- /** 正文累积内容 */
40
- text: string;
41
- /** 本轮工具调用列表 */
42
- tools: ProgressToolCall[];
43
- /** 是否展示停止按钮 / 停止提示 */
44
- showStop: boolean;
45
- /** 最近一次更新时间戳 */
46
- updatedAt: number;
47
- }
48
-
49
- export interface ProgressViewInit {
50
- status?: ProgressStatus;
51
- headerTitle?: string;
52
- headerTemplate?: string;
53
- text?: string;
54
- tools?: ProgressToolCall[];
55
- showStop?: boolean;
56
- }
57
-
58
- /** 创建 ProgressView,未提供的字段使用与飞书 buildProgressCard 一致的默认值 */
59
- export function progressView(init: ProgressViewInit = {}): ProgressView {
60
- return {
61
- status: init.status ?? "generating",
62
- headerTitle: init.headerTitle ?? "生成中...",
63
- headerTemplate: init.headerTemplate ?? "blue",
64
- text: init.text ?? "",
65
- tools: init.tools ?? [],
66
- showStop: init.showStop ?? true,
67
- updatedAt: Date.now(),
68
- };
69
- }
70
-
71
- /** 只读浅拷贝并覆盖部分字段,返回新视图(reducer 的不可变更新用) */
72
- export function withProgressView(
73
- prev: ProgressView,
74
- patch: Partial<ProgressView>,
75
- ): ProgressView {
76
- return { ...prev, ...patch, updatedAt: Date.now() };
77
- }
1
+ /**
2
+ * progress/view.ts — 平台无关的"过程进度"视图模型
3
+ *
4
+ * 这是飞书过程卡片与终端过程区块共享的唯一数据模型:
5
+ * - 飞书:buildProgressCard(view) 把 ProgressView 渲染为卡片 JSON
6
+ * - 终端:TerminalProgressRenderer 把 ProgressView 渲染为 ANSI 区块
7
+ * - 事件流:reduceProgress(prev, event) 把 ChatEvent 增量合并进 ProgressView
8
+ *
9
+ * 语义与飞书卡片对齐:
10
+ * - headerTitle 是"生成中"阶段展示的标题(如"正在启动 Agent · 0秒")
11
+ * - status 决定终态外观(完成 / 已停止 / 异常结束)
12
+ * - text 是正文累积(对应卡片 main_content / 终端区块正文)
13
+ * - tools 是本轮工具调用列表(飞书卡片暂不展示,终端折叠为单行)
14
+ */
15
+
16
+ export type ProgressStatus = "generating" | "done" | "stopped" | "error";
17
+
18
+ export type ProgressToolStatus = "running" | "ok" | "error";
19
+
20
+ export interface ProgressToolCall {
21
+ /** 工具调用 ID(ChatEvent.tool_use.id,可能缺失) */
22
+ id: string;
23
+ /** 工具名,如 edit_file */
24
+ name: string;
25
+ status: ProgressToolStatus;
26
+ /** 工具输入摘要(终端折叠行展示,截断保存) */
27
+ detail?: string;
28
+ /** 工具结果摘要(成功/失败,截断保存) */
29
+ summary?: string;
30
+ }
31
+
32
+ export interface ProgressView {
33
+ /** 当前阶段状态,决定终态外观 */
34
+ status: ProgressStatus;
35
+ /** 生成中阶段的头部标题(对应飞书卡片 header title) */
36
+ headerTitle: string;
37
+ /** 卡片头部颜色模板(飞书用),终端忽略 */
38
+ headerTemplate: string;
39
+ /** 正文累积内容 */
40
+ text: string;
41
+ /** 本轮工具调用列表 */
42
+ tools: ProgressToolCall[];
43
+ /** 是否展示停止按钮 / 停止提示 */
44
+ showStop: boolean;
45
+ /** 最近一次更新时间戳 */
46
+ updatedAt: number;
47
+ }
48
+
49
+ export interface ProgressViewInit {
50
+ status?: ProgressStatus;
51
+ headerTitle?: string;
52
+ headerTemplate?: string;
53
+ text?: string;
54
+ tools?: ProgressToolCall[];
55
+ showStop?: boolean;
56
+ }
57
+
58
+ /** 创建 ProgressView,未提供的字段使用与飞书 buildProgressCard 一致的默认值 */
59
+ export function progressView(init: ProgressViewInit = {}): ProgressView {
60
+ return {
61
+ status: init.status ?? "generating",
62
+ headerTitle: init.headerTitle ?? "生成中...",
63
+ headerTemplate: init.headerTemplate ?? "blue",
64
+ text: init.text ?? "",
65
+ tools: init.tools ?? [],
66
+ showStop: init.showStop ?? true,
67
+ updatedAt: Date.now(),
68
+ };
69
+ }
70
+
71
+ /** 只读浅拷贝并覆盖部分字段,返回新视图(reducer 的不可变更新用) */
72
+ export function withProgressView(
73
+ prev: ProgressView,
74
+ patch: Partial<ProgressView>,
75
+ ): ProgressView {
76
+ return { ...prev, ...patch, updatedAt: Date.now() };
77
+ }
@@ -1,124 +1,124 @@
1
- import { createWriteStream, type Dirent } from "node:fs";
2
- import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
3
- import { once } from "node:events";
4
- import { join } from "node:path";
5
- import { createGzip } from "node:zlib";
6
-
7
- export interface RawStreamLogOptions {
8
- enabled: boolean;
9
- rootDir: string;
10
- tool: string;
11
- sessionId: string;
12
- label: string;
13
- maxBytesPerTurn: number;
14
- retentionDays: number;
15
- }
16
-
17
- export interface RawStreamLogHandle {
18
- filePath: string;
19
- writeLine(line: string): void;
20
- close(options: { keep: boolean }): Promise<void>;
21
- }
22
-
23
- export function sanitizeLogPathSegment(value: string): string {
24
- const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^[._]+|_+$/g, "");
25
- return safe || "unknown";
26
- }
27
-
28
- async function cleanupOldRawStreamLogs(rootDir: string, retentionDays: number): Promise<void> {
29
- if (!Number.isFinite(retentionDays) || retentionDays <= 0) return;
30
- const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
31
-
32
- const visit = async (dir: string): Promise<void> => {
33
- let entries: Dirent[];
34
- try {
35
- entries = await readdir(dir, { withFileTypes: true });
36
- } catch {
37
- return;
38
- }
39
-
40
- await Promise.all(entries.map(async (entry) => {
41
- const path = join(dir, entry.name);
42
- if (entry.isDirectory()) {
43
- await visit(path);
44
- try {
45
- await rm(path, { recursive: false });
46
- } catch {
47
- // Directory is not empty or already gone.
48
- }
49
- return;
50
- }
51
-
52
- if (!entry.isFile()) return;
53
- try {
54
- const info = await stat(path);
55
- if (info.mtimeMs < cutoff) await rm(path, { force: true });
56
- } catch {
57
- // Best-effort cleanup only.
58
- }
59
- }));
60
- };
61
-
62
- await visit(rootDir);
63
- }
64
-
65
- export async function createRawStreamLog(options: RawStreamLogOptions): Promise<RawStreamLogHandle | null> {
66
- if (!options.enabled) return null;
67
-
68
- const maxBytes = Math.max(0, Math.floor(options.maxBytesPerTurn));
69
- const tool = sanitizeLogPathSegment(options.tool);
70
- const session = sanitizeLogPathSegment(options.sessionId);
71
- const label = sanitizeLogPathSegment(options.label);
72
- const dir = join(options.rootDir, tool, session);
73
- await mkdir(dir, { recursive: true });
74
- void cleanupOldRawStreamLogs(join(options.rootDir, tool), options.retentionDays);
75
-
76
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
77
- const filePath = join(dir, `${timestamp}-${label}.jsonl.gz`);
78
- const output = createWriteStream(filePath);
79
- const gzip = createGzip();
80
- gzip.pipe(output);
81
-
82
- let bytes = 0;
83
- let truncated = false;
84
- let ended = false;
85
-
86
- const writeLine = (line: string): void => {
87
- if (ended || truncated) return;
88
- const payload = `${line}\n`;
89
- const payloadBytes = Buffer.byteLength(payload, "utf-8");
90
- if (maxBytes > 0 && bytes + payloadBytes > maxBytes) {
91
- const marker = JSON.stringify({
92
- type: "deepccc_raw_stream_log_truncated",
93
- reason: "max_bytes_per_turn_exceeded",
94
- maxBytesPerTurn: maxBytes,
95
- writtenBytes: bytes,
96
- });
97
- gzip.write(`${marker}\n`);
98
- truncated = true;
99
- return;
100
- }
101
- bytes += payloadBytes;
102
- gzip.write(payload);
103
- };
104
-
105
- const close = async ({ keep }: { keep: boolean }): Promise<void> => {
106
- if (ended) return;
107
- ended = true;
108
- gzip.end();
109
- try {
110
- await once(output, "finish");
111
- } catch {
112
- // Ignore close errors; caller cannot recover from debug log failures.
113
- }
114
- if (!keep) {
115
- try {
116
- await unlink(filePath);
117
- } catch {
118
- // Best-effort cleanup only.
119
- }
120
- }
121
- };
122
-
123
- return { filePath, writeLine, close };
124
- }
1
+ import { createWriteStream, type Dirent } from "node:fs";
2
+ import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
3
+ import { once } from "node:events";
4
+ import { join } from "node:path";
5
+ import { createGzip } from "node:zlib";
6
+
7
+ export interface RawStreamLogOptions {
8
+ enabled: boolean;
9
+ rootDir: string;
10
+ tool: string;
11
+ sessionId: string;
12
+ label: string;
13
+ maxBytesPerTurn: number;
14
+ retentionDays: number;
15
+ }
16
+
17
+ export interface RawStreamLogHandle {
18
+ filePath: string;
19
+ writeLine(line: string): void;
20
+ close(options: { keep: boolean }): Promise<void>;
21
+ }
22
+
23
+ export function sanitizeLogPathSegment(value: string): string {
24
+ const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^[._]+|_+$/g, "");
25
+ return safe || "unknown";
26
+ }
27
+
28
+ async function cleanupOldRawStreamLogs(rootDir: string, retentionDays: number): Promise<void> {
29
+ if (!Number.isFinite(retentionDays) || retentionDays <= 0) return;
30
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
31
+
32
+ const visit = async (dir: string): Promise<void> => {
33
+ let entries: Dirent[];
34
+ try {
35
+ entries = await readdir(dir, { withFileTypes: true });
36
+ } catch {
37
+ return;
38
+ }
39
+
40
+ await Promise.all(entries.map(async (entry) => {
41
+ const path = join(dir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ await visit(path);
44
+ try {
45
+ await rm(path, { recursive: false });
46
+ } catch {
47
+ // Directory is not empty or already gone.
48
+ }
49
+ return;
50
+ }
51
+
52
+ if (!entry.isFile()) return;
53
+ try {
54
+ const info = await stat(path);
55
+ if (info.mtimeMs < cutoff) await rm(path, { force: true });
56
+ } catch {
57
+ // Best-effort cleanup only.
58
+ }
59
+ }));
60
+ };
61
+
62
+ await visit(rootDir);
63
+ }
64
+
65
+ export async function createRawStreamLog(options: RawStreamLogOptions): Promise<RawStreamLogHandle | null> {
66
+ if (!options.enabled) return null;
67
+
68
+ const maxBytes = Math.max(0, Math.floor(options.maxBytesPerTurn));
69
+ const tool = sanitizeLogPathSegment(options.tool);
70
+ const session = sanitizeLogPathSegment(options.sessionId);
71
+ const label = sanitizeLogPathSegment(options.label);
72
+ const dir = join(options.rootDir, tool, session);
73
+ await mkdir(dir, { recursive: true });
74
+ void cleanupOldRawStreamLogs(join(options.rootDir, tool), options.retentionDays);
75
+
76
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
77
+ const filePath = join(dir, `${timestamp}-${label}.jsonl.gz`);
78
+ const output = createWriteStream(filePath);
79
+ const gzip = createGzip();
80
+ gzip.pipe(output);
81
+
82
+ let bytes = 0;
83
+ let truncated = false;
84
+ let ended = false;
85
+
86
+ const writeLine = (line: string): void => {
87
+ if (ended || truncated) return;
88
+ const payload = `${line}\n`;
89
+ const payloadBytes = Buffer.byteLength(payload, "utf-8");
90
+ if (maxBytes > 0 && bytes + payloadBytes > maxBytes) {
91
+ const marker = JSON.stringify({
92
+ type: "deepccc_raw_stream_log_truncated",
93
+ reason: "max_bytes_per_turn_exceeded",
94
+ maxBytesPerTurn: maxBytes,
95
+ writtenBytes: bytes,
96
+ });
97
+ gzip.write(`${marker}\n`);
98
+ truncated = true;
99
+ return;
100
+ }
101
+ bytes += payloadBytes;
102
+ gzip.write(payload);
103
+ };
104
+
105
+ const close = async ({ keep }: { keep: boolean }): Promise<void> => {
106
+ if (ended) return;
107
+ ended = true;
108
+ gzip.end();
109
+ try {
110
+ await once(output, "finish");
111
+ } catch {
112
+ // Ignore close errors; caller cannot recover from debug log failures.
113
+ }
114
+ if (!keep) {
115
+ try {
116
+ await unlink(filePath);
117
+ } catch {
118
+ // Best-effort cleanup only.
119
+ }
120
+ }
121
+ };
122
+
123
+ return { filePath, writeLine, close };
124
+ }
@@ -1,48 +1,48 @@
1
- import {
2
- DEFAULT_BUILTIN_CONTEXT_DIR,
3
- getBuiltinContextSession,
4
- latestBuiltinSessionForCwd,
5
- newBuiltinSessionId,
6
- normalizeBuiltinSessionId,
7
- } from "./context.js";
8
-
9
- export type BuiltinResumeRequest = true | string | undefined;
10
-
11
- export interface ResolveBuiltinSessionOptions {
12
- cwd: string;
13
- contextDir?: string;
14
- resume?: BuiltinResumeRequest;
15
- now?: Date;
16
- randomSuffix?: string;
17
- }
18
-
19
- export interface ResolvedBuiltinSession {
20
- mode: "new" | "resumed";
21
- sessionId: string;
22
- }
23
-
24
- export function resolveBuiltinSession(options: ResolveBuiltinSessionOptions): ResolvedBuiltinSession {
25
- const contextDir = options.contextDir ?? DEFAULT_BUILTIN_CONTEXT_DIR;
26
-
27
- if (options.resume === true) {
28
- const latest = latestBuiltinSessionForCwd(options.cwd, contextDir);
29
- if (!latest) {
30
- throw new Error(`No resumable DeepCCC session found for cwd: ${options.cwd}`);
31
- }
32
- return { mode: "resumed", sessionId: latest.sessionId };
33
- }
34
-
35
- if (typeof options.resume === "string") {
36
- const sessionId = normalizeBuiltinSessionId(options.resume);
37
- const existing = getBuiltinContextSession(sessionId, contextDir);
38
- if (!existing) {
39
- throw new Error(`DeepCCC session not found: ${sessionId}`);
40
- }
41
- return { mode: "resumed", sessionId };
42
- }
43
-
44
- return {
45
- mode: "new",
46
- sessionId: newBuiltinSessionId(options.now, options.randomSuffix),
47
- };
48
- }
1
+ import {
2
+ DEFAULT_BUILTIN_CONTEXT_DIR,
3
+ getBuiltinContextSession,
4
+ latestBuiltinSessionForCwd,
5
+ newBuiltinSessionId,
6
+ normalizeBuiltinSessionId,
7
+ } from "./context.js";
8
+
9
+ export type BuiltinResumeRequest = true | string | undefined;
10
+
11
+ export interface ResolveBuiltinSessionOptions {
12
+ cwd: string;
13
+ contextDir?: string;
14
+ resume?: BuiltinResumeRequest;
15
+ now?: Date;
16
+ randomSuffix?: string;
17
+ }
18
+
19
+ export interface ResolvedBuiltinSession {
20
+ mode: "new" | "resumed";
21
+ sessionId: string;
22
+ }
23
+
24
+ export function resolveBuiltinSession(options: ResolveBuiltinSessionOptions): ResolvedBuiltinSession {
25
+ const contextDir = options.contextDir ?? DEFAULT_BUILTIN_CONTEXT_DIR;
26
+
27
+ if (options.resume === true) {
28
+ const latest = latestBuiltinSessionForCwd(options.cwd, contextDir);
29
+ if (!latest) {
30
+ throw new Error(`No resumable DeepCCC session found for cwd: ${options.cwd}`);
31
+ }
32
+ return { mode: "resumed", sessionId: latest.sessionId };
33
+ }
34
+
35
+ if (typeof options.resume === "string") {
36
+ const sessionId = normalizeBuiltinSessionId(options.resume);
37
+ const existing = getBuiltinContextSession(sessionId, contextDir);
38
+ if (!existing) {
39
+ throw new Error(`DeepCCC session not found: ${sessionId}`);
40
+ }
41
+ return { mode: "resumed", sessionId };
42
+ }
43
+
44
+ return {
45
+ mode: "new",
46
+ sessionId: newBuiltinSessionId(options.now, options.randomSuffix),
47
+ };
48
+ }