chatccc 0.2.6 → 0.2.8

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.
package/README.md CHANGED
@@ -63,24 +63,44 @@ npm run dev
63
63
 
64
64
  #### Cursor Agent CLI(使用 Cursor 会话时需要)
65
65
 
66
- ChatCCC **不捆绑** Cursor Agent CLI,需要用户自行安装。Cursor Agent CLI 目前主要有两种来源:
66
+ ChatCCC **不捆绑** Cursor Agent CLI,需要用户自行安装。
67
+
68
+ **推荐安装方式(Windows):**
69
+
70
+ 在 PowerShell 中执行:
71
+
72
+ ```powershell
73
+ irm 'https://cursor.com/install?win32=true' | iex
74
+ ```
75
+
76
+ 安装完成后 `agent` 命令会出现在系统 PATH 中。
77
+
78
+ **其他安装方式:**
67
79
 
68
80
  1. **Cursor IDE 自带**:安装 [Cursor IDE](https://cursor.com) 后,部分版本会自带 `agent` 或 `cursor-agent` 命令
69
- 2. **独立安装**:Cursor 正在逐步提供独立的 CLI 安装方式,安装后 `agent` 命令会出现在系统 PATH 中
81
+ 2. **独立安装**:Cursor 正在逐步提供独立的 CLI 安装方式
70
82
 
71
- 验证是否已安装:
83
+ 验证是否已安装(任一可用即可):
72
84
 
73
85
  ```bash
74
86
  agent --version
87
+ cursor-agent --version
75
88
  ```
76
89
 
77
- `agent` 不在 PATH 中,可通过环境变量指定自定义命令:
90
+ ChatCCC 启动时会按以下顺序探测 Cursor Agent CLI:
91
+
92
+ 1. 环境变量 `CHATCCC_CURSOR_COMMAND` 指定的路径(若已设置)
93
+ 2. Windows 上 Cursor IDE 默认安装位置 `%LOCALAPPDATA%\cursor-agent\agent.cmd`(自动识别)
94
+ 3. PATH 中的 `agent` 命令
95
+
96
+ 若以上都找不到,或你想用一个非默认的可执行文件(例如 `cursor-agent`),在 `.env` 中显式指定:
78
97
 
79
98
  ```env
80
99
  CHATCCC_CURSOR_COMMAND=/path/to/agent
81
- CHATCCC_CURSOR_ARGS=-p --output-format stream-json --stream-partial-output
82
100
  ```
83
101
 
102
+ > 高级用户如需覆盖默认 CLI 参数,可设置 `CHATCCC_CURSOR_ARGS`,一般无需修改。
103
+
84
104
  > **说明**:只使用 Claude Code(`/new claude` 或 `/new`)的用户无需安装 Cursor CLI。
85
105
 
86
106
  ### 2. 创建飞书应用
@@ -132,14 +152,25 @@ CHATCCC_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxx
132
152
 
133
153
  若你曾使用旧版变量名 `FEISHU_CLAUDER_APP_ID` / `FEISHU_CLAUDER_APP_SECRET`,请改为上表中的 `CHATCCC_APP_ID` / `CHATCCC_APP_SECRET`。
134
154
 
135
- 可选:Claude 模型与思考深度(**默认均为 `default`**):
155
+ 可选:Claude 模型与思考深度:
136
156
 
137
157
  ```env
138
- # 网关或服务商文档中的 model;设为 default(任意大小写)或不写则交给 SDK/CLI 默认,且 /status 中显示为 default
158
+ # 网关或服务商文档中的 model;设为 default(任意大小写)或不写则交给 SDK/CLI 默认
139
159
  CHATCCC_ANTHROPIC_MODEL=default
140
160
 
141
- # 如 low / medium / high / max;设为 default(任意大小写)或不写则不向 SDK 传入 effort,/status 显示 default
142
- CHATCCC_ANTHROPIC_EFFORT=default
161
+ # 如 low / medium / high / max;设为 default(任意大小写)或不写则不向 SDK 传入 effort
162
+ CHATCCC_ANTHROPIC_EFFORT=max
163
+ ```
164
+
165
+ Cursor Agent CLI 模型(仅 Cursor 会话相关,省略或 `default` 时不传 `--model`):
166
+
167
+ ```env
168
+ # Cursor 会话实际使用的模型 ID(如 claude-opus-4-7-max / claude-opus-4-7-thinking-max 等)
169
+ # 可用 `agent --list-models` 查看所有支持的模型 ID
170
+ CHATCCC_CURSOR_MODEL=claude-opus-4-7-max
171
+
172
+ # 高级用户可覆盖默认 CLI 参数(一般无需修改)
173
+ # CHATCCC_CURSOR_ARGS=-p --force --output-format stream-json --stream-partial-output
143
174
  ```
144
175
 
145
176
  #### 注意!第三方 API(如 DeepSeek)与鉴权 403
@@ -172,7 +203,7 @@ CHATCCC_ANTHROPIC_EFFORT=default
172
203
 
173
204
  > **说明**:项目根目录的 `.env` 若通过 `tsx --env-file=.env` 加载,其中的变量会进入 Node 进程环境,**多数情况下**会随进程继承给 SDK 子进程;但各家网关与 Claude Code 版本组合较多,**仍推荐**将 **`ANTHROPIC_API_KEY`** 与 **`ANTHROPIC_BASE_URL`** 放在系统/用户环境变量中,与 `settings.json` 形成双保险,避免仅依赖 `.claude/settings.json` 时在 SDK 场景下踩坑。
174
205
 
175
- `**CHATCCC_PORT`(可选)**:默认端口为 `18080`。若在一台机器上同时运行多个 ChatCCC 实例,可在各自的 `.env` 中设置不同端口:
206
+ **`CHATCCC_PORT`(可选)**:默认端口为 `18080`。若在一台机器上同时运行多个 ChatCCC 实例,可在各自的 `.env` 中设置不同端口:
176
207
 
177
208
  ```env
178
209
  # 实例 A(项目1)的 .env
@@ -184,6 +215,13 @@ CHATCCC_PORT=18081
184
215
 
185
216
  不同端口的实例互不冲突,启动时会自动清理各自端口上的旧进程。
186
217
 
218
+ **`CHATCCC_GIT_TIMEOUT_SECONDS`(可选)**:`/git <子命令>` 在会话工作目录执行 git 命令时的单次超时秒数,默认 `180`,允许范围 `1–3600`。配置非法(非整数、越界等)时启动日志会标红提示并回退为默认值;命令超时则会被 `SIGKILL` 强制终止,并把已收集到的输出(带「⏱️ 命令超时被强制终止」标记)回发到群里。
219
+
220
+ ```env
221
+ # 调高到 600 秒,适合 git fsck、git gc 等耗时操作
222
+ CHATCCC_GIT_TIMEOUT_SECONDS=600
223
+ ```
224
+
187
225
  > **权限说明**:目前 ChatCCC 以 `bypassPermissions` 模式运行,即跳过所有权限确认、允许所有操作。后续会考虑引入细粒度权限控制,让你可以按需放行特定操作。
188
226
 
189
227
  > **Linux 用户注意**:不能将项目装在 `/root` 目录下运行。
@@ -204,6 +242,7 @@ CHATCCC_PORT=18081
204
242
  | `/status` | 查看当前会话的状态(轮数、模型、上下文 token 等) |
205
243
  | `/cd` | 查看/切换工作目录 |
206
244
  | `/sessions` | 查看所有会话状态 |
245
+ | `/git <子命令>` | 在**当前会话工作目录**执行 `git ...` 并把 stdout/stderr 回发到群里(仅会话群内可用,超时见 `CHATCCC_GIT_TIMEOUT_SECONDS`) |
207
246
  | `/restart` | 重启机器人进程 |
208
247
 
209
248
 
@@ -211,4 +250,4 @@ CHATCCC_PORT=18081
211
250
 
212
251
  ## 技术栈
213
252
 
214
- TypeScript / Node.js >= 20 / tsx / Anthropic Claude Agent SDK / 飞书 WebSocket API / CardKit
253
+ TypeScript / Node.js >= 20 / tsx / Anthropic Claude Agent SDK / Cursor Agent CLI / 飞书 WebSocket API / CardKit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,152 +1,152 @@
1
- import { describe, it, expect } from "vitest";
2
- import type {
3
- UnifiedBlock,
4
- UnifiedStreamMessage,
5
- UnifiedThinkingBlock,
6
- UnifiedTextBlock,
7
- UnifiedToolUseBlock,
8
- UnifiedToolResultBlock,
9
- UnifiedRedactedThinkingBlock,
10
- UnifiedSearchResultBlock,
11
- UnifiedCompactBoundaryBlock,
12
- CreateSessionResult,
13
- SessionInfo,
14
- ToolAdapter,
15
- } from "../adapters/adapter-interface.ts";
16
-
17
- // ---------------------------------------------------------------------------
18
- // 编译期类型验证:确保 UnifiedBlock 联合类型覆盖了所有预期的 block 形状
19
- // ---------------------------------------------------------------------------
20
-
21
- describe("UnifiedBlock union type coverage", () => {
22
- it("accepts thinking block", () => {
23
- const b: UnifiedBlock = { type: "thinking", thinking: "Let me think..." };
24
- expect(b.type).toBe("thinking");
25
- });
26
-
27
- it("accepts text block", () => {
28
- const b: UnifiedBlock = { type: "text", text: "Hello" };
29
- expect(b.type).toBe("text");
30
- });
31
-
32
- it("accepts tool_use block", () => {
33
- const b: UnifiedBlock = { type: "tool_use", name: "Read", input: { file_path: "/tmp" } };
34
- expect(b.type).toBe("tool_use");
35
- });
36
-
37
- it("accepts tool_result block with string content", () => {
38
- const b: UnifiedBlock = { type: "tool_result", tool_use_id: "abc123", content: "result" };
39
- expect(b.type).toBe("tool_result");
40
- });
41
-
42
- it("accepts tool_result block with error flag", () => {
43
- const b: UnifiedBlock = { type: "tool_result", tool_use_id: "abc123", content: "error", is_error: true };
44
- expect(b.is_error).toBe(true);
45
- });
46
-
47
- it("accepts redacted_thinking block", () => {
48
- const b: UnifiedBlock = { type: "redacted_thinking" };
49
- expect(b.type).toBe("redacted_thinking");
50
- });
51
-
52
- it("accepts search_result block", () => {
53
- const b: UnifiedBlock = { type: "search_result", query: "what is TypeScript" };
54
- expect(b.type).toBe("search_result");
55
- });
56
-
57
- it("accepts compact_boundary block", () => {
58
- const b: UnifiedBlock = { type: "compact_boundary", trigger: "auto", pre_tokens: 10000, post_tokens: 5000 };
59
- expect(b.type).toBe("compact_boundary");
60
- });
61
-
62
- it("accepts compact_boundary block without post_tokens", () => {
63
- const b: UnifiedBlock = { type: "compact_boundary", trigger: "manual", pre_tokens: 20000 };
64
- expect(b.type).toBe("compact_boundary");
65
- });
66
- });
67
-
68
- // ---------------------------------------------------------------------------
69
- // UnifiedStreamMessage
70
- // ---------------------------------------------------------------------------
71
-
72
- describe("UnifiedStreamMessage", () => {
73
- it("accepts assistant message with blocks", () => {
74
- const m: UnifiedStreamMessage = {
75
- type: "assistant",
76
- blocks: [
77
- { type: "thinking", thinking: "..." },
78
- { type: "text", text: "answer" },
79
- ],
80
- };
81
- expect(m.type).toBe("assistant");
82
- expect(m.blocks).toHaveLength(2);
83
- });
84
-
85
- it("accepts user message with tool_result blocks", () => {
86
- const m: UnifiedStreamMessage = {
87
- type: "user",
88
- blocks: [{ type: "tool_result", tool_use_id: "abc", content: "ok" }],
89
- };
90
- expect(m.type).toBe("user");
91
- });
92
-
93
- it("accepts system message", () => {
94
- const m: UnifiedStreamMessage = {
95
- type: "system",
96
- blocks: [{ type: "compact_boundary", trigger: "auto", pre_tokens: 100 }],
97
- };
98
- expect(m.type).toBe("system");
99
- });
100
- });
101
-
102
- // ---------------------------------------------------------------------------
103
- // CreateSessionResult
104
- // ---------------------------------------------------------------------------
105
-
106
- describe("CreateSessionResult", () => {
107
- it("accepts valid result", () => {
108
- const r: CreateSessionResult = { sessionId: "uuid-12345" };
109
- expect(r.sessionId).toBe("uuid-12345");
110
- });
111
- });
112
-
113
- // ---------------------------------------------------------------------------
114
- // SessionInfo
115
- // ---------------------------------------------------------------------------
116
-
117
- describe("SessionInfo", () => {
118
- it("accepts full info", () => {
119
- const info: SessionInfo = {
120
- sessionId: "abc",
121
- cwd: "/home/user",
122
- summary: "A session",
123
- lastModified: 1234567890,
124
- };
125
- expect(info.sessionId).toBe("abc");
126
- });
127
-
128
- it("accepts minimal info (only sessionId)", () => {
129
- const info: SessionInfo = { sessionId: "minimal" };
130
- expect(info.cwd).toBeUndefined();
131
- });
132
- });
133
-
134
- // ---------------------------------------------------------------------------
135
- // ToolAdapter interface 结构断言
136
- // ---------------------------------------------------------------------------
137
-
138
- describe("ToolAdapter interface structure", () => {
139
- it("has correct property names via mock object", () => {
140
- // 如果 ToolAdapter 接口的字段名或函数签名变更,该对象字面量将无法编译
141
- const mock: ToolAdapter = {
142
- displayName: "Test",
143
- sessionDescPrefix: "Test Session:",
144
- createSession: async () => ({ sessionId: "s" }),
145
- prompt: async function* () {},
146
- getSessionInfo: async () => undefined,
147
- closeSession: async () => {},
148
- };
149
- expect(mock.displayName).toBe("Test");
150
- expect(mock.sessionDescPrefix).toBe("Test Session:");
151
- });
1
+ import { describe, it, expect } from "vitest";
2
+ import type {
3
+ UnifiedBlock,
4
+ UnifiedStreamMessage,
5
+ UnifiedThinkingBlock,
6
+ UnifiedTextBlock,
7
+ UnifiedToolUseBlock,
8
+ UnifiedToolResultBlock,
9
+ UnifiedRedactedThinkingBlock,
10
+ UnifiedSearchResultBlock,
11
+ UnifiedCompactBoundaryBlock,
12
+ CreateSessionResult,
13
+ SessionInfo,
14
+ ToolAdapter,
15
+ } from "../adapters/adapter-interface.ts";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // 编译期类型验证:确保 UnifiedBlock 联合类型覆盖了所有预期的 block 形状
19
+ // ---------------------------------------------------------------------------
20
+
21
+ describe("UnifiedBlock union type coverage", () => {
22
+ it("accepts thinking block", () => {
23
+ const b: UnifiedBlock = { type: "thinking", thinking: "Let me think..." };
24
+ expect(b.type).toBe("thinking");
25
+ });
26
+
27
+ it("accepts text block", () => {
28
+ const b: UnifiedBlock = { type: "text", text: "Hello" };
29
+ expect(b.type).toBe("text");
30
+ });
31
+
32
+ it("accepts tool_use block", () => {
33
+ const b: UnifiedBlock = { type: "tool_use", name: "Read", input: { file_path: "/tmp" } };
34
+ expect(b.type).toBe("tool_use");
35
+ });
36
+
37
+ it("accepts tool_result block with string content", () => {
38
+ const b: UnifiedBlock = { type: "tool_result", tool_use_id: "abc123", content: "result" };
39
+ expect(b.type).toBe("tool_result");
40
+ });
41
+
42
+ it("accepts tool_result block with error flag", () => {
43
+ const b: UnifiedBlock = { type: "tool_result", tool_use_id: "abc123", content: "error", is_error: true };
44
+ expect(b.is_error).toBe(true);
45
+ });
46
+
47
+ it("accepts redacted_thinking block", () => {
48
+ const b: UnifiedBlock = { type: "redacted_thinking" };
49
+ expect(b.type).toBe("redacted_thinking");
50
+ });
51
+
52
+ it("accepts search_result block", () => {
53
+ const b: UnifiedBlock = { type: "search_result", query: "what is TypeScript" };
54
+ expect(b.type).toBe("search_result");
55
+ });
56
+
57
+ it("accepts compact_boundary block", () => {
58
+ const b: UnifiedBlock = { type: "compact_boundary", trigger: "auto", pre_tokens: 10000, post_tokens: 5000 };
59
+ expect(b.type).toBe("compact_boundary");
60
+ });
61
+
62
+ it("accepts compact_boundary block without post_tokens", () => {
63
+ const b: UnifiedBlock = { type: "compact_boundary", trigger: "manual", pre_tokens: 20000 };
64
+ expect(b.type).toBe("compact_boundary");
65
+ });
66
+ });
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // UnifiedStreamMessage
70
+ // ---------------------------------------------------------------------------
71
+
72
+ describe("UnifiedStreamMessage", () => {
73
+ it("accepts assistant message with blocks", () => {
74
+ const m: UnifiedStreamMessage = {
75
+ type: "assistant",
76
+ blocks: [
77
+ { type: "thinking", thinking: "..." },
78
+ { type: "text", text: "answer" },
79
+ ],
80
+ };
81
+ expect(m.type).toBe("assistant");
82
+ expect(m.blocks).toHaveLength(2);
83
+ });
84
+
85
+ it("accepts user message with tool_result blocks", () => {
86
+ const m: UnifiedStreamMessage = {
87
+ type: "user",
88
+ blocks: [{ type: "tool_result", tool_use_id: "abc", content: "ok" }],
89
+ };
90
+ expect(m.type).toBe("user");
91
+ });
92
+
93
+ it("accepts system message", () => {
94
+ const m: UnifiedStreamMessage = {
95
+ type: "system",
96
+ blocks: [{ type: "compact_boundary", trigger: "auto", pre_tokens: 100 }],
97
+ };
98
+ expect(m.type).toBe("system");
99
+ });
100
+ });
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // CreateSessionResult
104
+ // ---------------------------------------------------------------------------
105
+
106
+ describe("CreateSessionResult", () => {
107
+ it("accepts valid result", () => {
108
+ const r: CreateSessionResult = { sessionId: "uuid-12345" };
109
+ expect(r.sessionId).toBe("uuid-12345");
110
+ });
111
+ });
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // SessionInfo
115
+ // ---------------------------------------------------------------------------
116
+
117
+ describe("SessionInfo", () => {
118
+ it("accepts full info", () => {
119
+ const info: SessionInfo = {
120
+ sessionId: "abc",
121
+ cwd: "/home/user",
122
+ summary: "A session",
123
+ lastModified: 1234567890,
124
+ };
125
+ expect(info.sessionId).toBe("abc");
126
+ });
127
+
128
+ it("accepts minimal info (only sessionId)", () => {
129
+ const info: SessionInfo = { sessionId: "minimal" };
130
+ expect(info.cwd).toBeUndefined();
131
+ });
132
+ });
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // ToolAdapter interface 结构断言
136
+ // ---------------------------------------------------------------------------
137
+
138
+ describe("ToolAdapter interface structure", () => {
139
+ it("has correct property names via mock object", () => {
140
+ // 如果 ToolAdapter 接口的字段名或函数签名变更,该对象字面量将无法编译
141
+ const mock: ToolAdapter = {
142
+ displayName: "Test",
143
+ sessionDescPrefix: "Test Session:",
144
+ createSession: async () => ({ sessionId: "s" }),
145
+ prompt: async function* () {},
146
+ getSessionInfo: async () => undefined,
147
+ closeSession: async () => {},
148
+ };
149
+ expect(mock.displayName).toBe("Test");
150
+ expect(mock.sessionDescPrefix).toBe("Test Session:");
151
+ });
152
152
  });