chatccc 0.2.296 → 0.2.298

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
@@ -229,7 +229,7 @@ Claude Code、Cursor 和 Codex 需要对应的本地工具;CCC Agent 内置于
229
229
 
230
230
  CCC Agent 是 ChatCCC 内置的编程 Agent,不需要额外安装 CLI,开箱即用。在首次配置向导或 Web 管理页中启用后,填写 API Key、Base URL 和模型即可使用;它可以设为 `/new` 的默认 Agent,也可以通过 `/new ccc` 显式创建会话。
231
231
 
232
- CCC Agent 支持**协作式让位**:任务运行期间你可以继续在群里发消息,ChatCCC 会在 Agent 的每个模型步骤边界把新消息注入当前任务,不打断正在执行的工具步骤、不新开一轮对话。已完成的中间工作会保留,Agent 收到插话后会调整后续方向。运行中的消息进入独立的注入队列(深度 50),与整轮排队(深度 1)分离。
232
+ CCC Agent 支持**协作式让位**:任务运行期间你可以继续在群里发消息,ChatCCC 会在 Agent 的每个模型步骤边界把新消息注入当前任务,不打断正在执行的工具步骤、不新开一轮对话。已完成的中间工作会保留,Agent 收到插话后会调整后续方向。运行中的消息进入独立的注入队列(深度 50),与整轮排队(深度 1)分离。飞书中每条消息实际注入后,机器人会发送一条最多 200 字的回执,冻结注入前的过程卡片,并用新的“生成中”卡片展示后续过程。
233
233
 
234
234
  DeepCCC 同时提供独立端口的本地 Web UI。ChatCCC 管理页顶部点击 **DeepCCC Web** 会复用或按需启动该服务;只全局安装 `deepccc` 的用户直接运行 `deepccc` 即可启动并打开网页版,终端模式改用 `deepccc-cli`。默认地址为 `http://127.0.0.1:28080/`,端口可通过 `~/.deepccc/config.json` 的 `web.port` 修改。网页版支持多会话并发、持久化历史、会话级 model/effort、API 设置和会话内高风险操作审批。
235
235
 
@@ -290,7 +290,7 @@ codex --version
290
290
 
291
291
  Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理,也可以在 `config.json` 中覆盖。
292
292
 
293
- ChatCCC 通过 Codex 的 **app-server 模式**(一个常驻进程 + WebSocket JSON-RPC)驱动 Codex,而不是一次性 `codex exec` 子进程。因此 Codex 与 CCC Agent 一样支持**协作式让位**:任务运行期间继续发消息,ChatCCC 会在 Codex 的工具步骤边界把消息注入当前 turn(`turn/steer`),不打断正在执行的命令、不丢已完成的工作;`/stop` 通过 `turn/interrupt` 只打断当前 turn,不影响同进程内的其他会话。权限策略与旧 exec 的 `--dangerously-bypass-approvals-and-sandbox` 等价(零审批、完整沙箱权限),`/plan`、`/ask` 退化为只读沙箱。要求 Codex CLI >= 0.60.0(建议保持最新版)。
293
+ ChatCCC 通过 Codex 的 **app-server 模式**(一个常驻进程 + WebSocket JSON-RPC)驱动 Codex,而不是一次性 `codex exec` 子进程。因此 Codex 与 CCC Agent 一样支持**协作式让位**:任务运行期间继续发消息,ChatCCC 会在 Codex 的工具步骤边界把消息注入当前 turn(`turn/steer`),不打断正在执行的命令、不丢已完成的工作;飞书会在 `turn/steer` 成功后发送注入回执,并从新的过程卡片继续展示。`/stop` 通过 `turn/interrupt` 只打断当前 turn,不影响同进程内的其他会话。权限策略与旧 exec 的 `--dangerously-bypass-approvals-and-sandbox` 等价(零审批、完整沙箱权限),`/plan`、`/ask` 退化为只读沙箱。要求 Codex CLI >= 0.60.0(建议保持最新版)。
294
294
 
295
295
  #### 可选:Chrome CDP
296
296
 
@@ -165,7 +165,7 @@ Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以
165
165
 
166
166
  API 设置采用单一 Provider 配置,支持 OpenAI-compatible 与 Anthropic Messages。完整 API Key 只保存在本机 `~/.deepccc/config.json`,浏览器读取设置时仅返回掩码。危险命令会在会话中暂停并请求“拒绝、允许一次、本会话允许、永久允许”,浏览器断开或审批超时默认拒绝。
167
167
 
168
- `git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_command` 创建 Git 提交时会保留用户为
168
+ `git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_process` 或 `run_command` 创建 Git 提交时会保留用户为
169
169
  主 Author,并追加 `Co-authored-by: DeepCCC <20184052+wzj998@users.noreply.github.com>`。
170
170
  可设为 `false` 或用 `DEEPCCC_GIT_COAUTHOR=false` 全局关闭。ChatCCC 的
171
171
  `ccc.gitCoAuthor` 是三态 override:`null`/缺失跟随这里,`true` 强制开启,`false` 强制关闭。
@@ -276,7 +276,7 @@ deepccc-cli --max-output-tokens 8192
276
276
 
277
277
  ## 权限机制
278
278
 
279
- `deepccc` 内置轻量权限机制,对标主流 agent 的审批体验:**只拦截有副作用的操作**(`run_command` 与文件写操作),只读工具(`read_file` / `list_dir` / `search_code`)永不拦截,常规文件编辑默认放行不打断工作流。
279
+ `deepccc` 内置轻量权限机制,对标主流 agent 的审批体验:**只拦截有副作用的操作**(`run_process` / `run_script` / `run_command` 与文件写操作),只读工具(`read_file` / `list_dir` / `search_code`)永不拦截,常规文件编辑默认放行不打断工作流。
280
280
 
281
281
  默认模式(`ask`)下,只有**命中内置危险命令库**的高危命令才会询问,例如:
282
282
 
@@ -307,11 +307,13 @@ deepccc-cli --max-output-tokens 8192
307
307
  ```json
308
308
  {
309
309
  "allow": [
310
+ "run_process:git status*",
310
311
  "run_command:git status*",
311
312
  "run_command:git push --force origin release*"
312
313
  ],
313
314
  "deny": [
314
315
  "edit_file:node_modules/**",
316
+ "run_process:npm publish*",
315
317
  "run_command:npm publish*"
316
318
  ]
317
319
  }
@@ -433,11 +435,13 @@ deepccc 的内核主战场在 ChatCCC 仓库的 `deepccc-agent/` 子目录;本
433
435
  - 用 ripgrep 搜索代码
434
436
  - 编辑、创建、删除、移动文件
435
437
  - 应用 unified diff patch
436
- - 运行非交互式 shell 命令,并返回 stdout、stderr、exitCode 和超时状态
438
+ - 用 `run_process` 以结构化 argv、`shell:false` 运行单个程序,避免参数被 shell 二次解析
439
+ - 用 `run_script` 通过 stdin 运行多行 Python/Node 代码,避免复杂 `python -c` / `node -e` 引号
440
+ - 仅在需要 `&&`、管道或重定向时用 `run_command` 运行非交互式 shell 命令
437
441
  - 联网搜索(`websearch`:DuckDuckGo,免 API key,返回标题 + URL + 摘要)
438
442
  - 抓取网页并转纯文本(`webfetch`:仅 http/https,自动去 HTML 标签、控制长度与超时)
439
443
 
440
- 命令返回非零退出码时不会直接被当成工具异常;模型可以读取结构化结果,继续判断下一步。
444
+ 三个命令工具都支持独立 `cwd`、超时、取消、输出限长和运行中消息让位;返回非零退出码时不会直接被当成工具异常,模型可以读取结构化结果继续判断。Windows 下 `run_command` 会拒绝多行或嵌套同类引号的复杂 Python/Node 内联代码,并提示改用 `run_script`。
441
445
 
442
446
  ## License
443
447
 
@@ -1,11 +1,11 @@
1
1
  ## Windows 命令行提示
2
2
 
3
- 你运行在 Windows 上。run_command 通过 cmd.exe 执行,而不是 bash。cmd 的引号规则与 bash 不同,容易踩坑:
3
+ 你运行在 Windows 上。优先绕过 shell,只有需要 shell 运算符时才进入 cmd.exe:
4
4
 
5
- - 双引号不会被剥离:`echo "hello world"` 会原样打印 `"hello world"`(含引号),`"a b" "c"` 会把带引号的字面量 `"a b"` 和 `"c"` 传给程序。
6
- - 单引号在 cmd.exe 中不是引用字符:`'a b'` 会被解析成两个参数(`'a` 和 `b'`)。
7
- - 多行或引号密集的内联脚本(python -c "...\n...", ssh host "bash -c '...'")在 cmd 引号规则下经常失败;把脚本写入临时文件再执行该文件。
8
- - PowerShell 专用语法(Get-Item、2>$null、Select-Object)不可用;除非显式调用 powershell,否则 shell 是 cmd.exe。
9
- - 传含空格参数时用双引号,并预期引号会原样到达程序;当目标接受文件输入时,优先把值写入文件。
10
- - cmd.exe 跨盘 `cd` 需要 `/d`:`cd /d D:\repo` 才真正切换盘符和目录;裸 `cd D:\repo` 只打印目标路径、停留在原盘,后续命令会在错误的目录执行。
11
- - npm 11.x 在 Windows 上忽略 `--prefix` 对 `npm publish` 的作用:`npm --prefix D:/repo publish` 发布的是**当前目录**的包,而不是 `--prefix` 指向的目录。务必先 `cd /d` 进入包目录,再执行 `npm publish`。
5
+ - 单个程序优先用 `run_process`,把可执行文件和每个参数分别放进 `executable` / `args`。它使用 `shell:false`,不会让 cmd.exe 再解释引号、反斜杠、`&`、`%VAR%` 等内容。
6
+ - 多行或引号密集的 Python/Node 代码必须用 `run_script`。不要塞进 `python -c` / `node -e`;Windows 下复杂内联代码会被 `run_command` 拒绝,避免命令返回 0 却实际执行了不同代码。
7
+ - `run_command` 仅用于 `&&`、管道、重定向等必须由 shell 解析的场景;它通过 cmd.exe 执行,不是 bash,也不是 PowerShell。
8
+ - 双引号在 cmd.exe、Node 的 shell 封装和目标程序的 argv 解析之间会经历多层处理;是否保留或剥离取决于具体调用链,不能假设会“原样到达”。单引号在 cmd.exe 中不是可靠的引用字符。
9
+ - 使用各命令工具的 `cwd` 参数切换工作目录,不要前置 `cd /d ... &&`。只有手工编写纯 cmd 命令时,跨盘 `cd` 才需要 `/d`。
10
+ - PowerShell 专用语法(Get-Item、2>$null、Select-Object)不能直接写进 `run_command`;确需使用时,用 `run_process` 显式执行 `powershell.exe` 并逐项传参。
11
+ - npm 11.x 在 Windows 上忽略 `--prefix` 对 `npm publish` 的作用。发布时把工具的 `cwd` 参数设为目标包目录,再执行 `npm publish`。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -4,7 +4,7 @@ import { createReadStream } from "node:fs";
4
4
  import { copyFile, mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
5
5
  import { createRequire } from "node:module";
6
6
  import { homedir } from "node:os";
7
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { createInterface } from "node:readline";
9
9
  import { jsonSchema, tool } from "ai";
10
10
  import { isDangerousCommand } from "./permissions.js";
@@ -25,6 +25,7 @@ const SEARCH_TIMEOUT_MS = 15_000;
25
25
  const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
26
26
  const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
27
27
  const MAX_COMMAND_TIMEOUT_MS = 900_000;
28
+ const MAX_SCRIPT_BYTES = 2 * 1024 * 1024;
28
29
  /**
29
30
  * 让位注入的轮询间隔:run_command 执行期以此频率检查是否有待注入的用户消息。
30
31
  * 取 1s 而非更小值:轮询本身不花 token(只读内存队列,无模型调用),但更密的
@@ -54,6 +55,22 @@ export function withGitCoAuthor(command, coAuthor) {
54
55
  const trailer = ` --trailer "Co-authored-by: ${coAuthor.name} <${coAuthor.email}>"`;
55
56
  return command.replace(/(^|(?:&&|\|\||;|\|)\s*)(git\s+commit)\b/g, (_match, prefix, gitCommit) => `${prefix}${gitCommit}${trailer}`);
56
57
  }
58
+ /** Structured equivalent of withGitCoAuthor for run_process({ executable: "git", args: [...] }). */
59
+ export function withGitCoAuthorArgs(executable, args, coAuthor) {
60
+ const executableName = basename(executable).replace(/\.(?:exe|cmd|bat)$/i, "").toLowerCase();
61
+ if (!coAuthor?.enabled
62
+ || executableName !== "git"
63
+ || args[0]?.toLowerCase() !== "commit"
64
+ || args.some((arg) => arg.includes(coAuthor.email))) {
65
+ return [...args];
66
+ }
67
+ return [
68
+ args[0],
69
+ "--trailer",
70
+ `Co-authored-by: ${coAuthor.name} <${coAuthor.email}>`,
71
+ ...args.slice(1),
72
+ ];
73
+ }
57
74
  /**
58
75
  * 把 `~` / `~/x`(含反斜杠 `~\\x`)展开为用户主目录绝对路径。
59
76
  * `~user/x` 形式不展开(Node 无内置支持),保持原样交给后续解析。
@@ -775,10 +792,91 @@ function pruneBackgroundCommands() {
775
792
  return;
776
793
  }
777
794
  }
778
- export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shouldYield) {
779
- const command = withGitCoAuthor(input.command?.trim(), coAuthor);
780
- if (!command)
781
- throw new Error("command is required");
795
+ const WINDOWS_INLINE_SCRIPT = /\b(?:python(?:\d+(?:\.\d+)?)?|py|node)(?:\.exe)?\s+(?:-c|-e|--eval)\s+/gi;
796
+ /**
797
+ * cmd.exe cannot reliably preserve nested quotes/backslashes in complex inline
798
+ * Python/Node programs. Keep simple one-line snippets working, but reject the
799
+ * cases for which a zero exit code can still mean that different code ran.
800
+ */
801
+ export function isUnsafeWindowsInlineScript(command) {
802
+ WINDOWS_INLINE_SCRIPT.lastIndex = 0;
803
+ let match;
804
+ while ((match = WINDOWS_INLINE_SCRIPT.exec(command)) !== null) {
805
+ const tail = command.slice(WINDOWS_INLINE_SCRIPT.lastIndex);
806
+ if (/[\r\n]/.test(tail))
807
+ return true;
808
+ const shellBoundary = tail.search(/\s(?:&&|\|\||[|>])/);
809
+ const argument = (shellBoundary >= 0 ? tail.slice(0, shellBoundary) : tail).trim();
810
+ const quote = argument[0];
811
+ if (quote !== "\"" && quote !== "'")
812
+ continue;
813
+ // cmd.exe does not treat single quotes as argument delimiters.
814
+ if (quote === "'")
815
+ return true;
816
+ const closingQuote = argument.lastIndexOf(quote);
817
+ if (closingQuote <= 0)
818
+ return true;
819
+ const body = argument.slice(1, closingQuote);
820
+ if (body.includes(quote))
821
+ return true;
822
+ }
823
+ return false;
824
+ }
825
+ function assertStringArgs(args) {
826
+ if (args === undefined)
827
+ return [];
828
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
829
+ throw new Error("args must be an array of strings");
830
+ }
831
+ return [...args];
832
+ }
833
+ function resolveProcessExecutable(commandCwd, value) {
834
+ const executable = value.trim();
835
+ if (!executable)
836
+ throw new Error("executable is required");
837
+ const expanded = expandHomePath(executable);
838
+ if (isAbsolute(expanded))
839
+ return resolve(expanded);
840
+ if (expanded.includes("/") || expanded.includes("\\") || expanded.startsWith(".")) {
841
+ return resolve(commandCwd, expanded);
842
+ }
843
+ return executable;
844
+ }
845
+ function formatProcessCommand(executable, args) {
846
+ return [executable, ...args].map((part) => JSON.stringify(part)).join(" ");
847
+ }
848
+ async function resolveWindowsNodeShim(executable, args) {
849
+ if (process.platform !== "win32" || extname(executable).toLowerCase() === ".exe") {
850
+ return { executable, args };
851
+ }
852
+ const toolName = basename(executable).replace(/\.(?:cmd|bat)$/i, "").toLowerCase();
853
+ if (toolName !== "npm" && toolName !== "npx")
854
+ return { executable, args };
855
+ const cliName = toolName === "npm" ? "npm-cli.js" : "npx-cli.js";
856
+ const candidates = new Set();
857
+ const npmExecPath = process.env.npm_execpath?.trim();
858
+ if (npmExecPath) {
859
+ candidates.add(toolName === "npm" ? npmExecPath : join(dirname(npmExecPath), cliName));
860
+ }
861
+ const addInstallRoot = (root) => {
862
+ if (root)
863
+ candidates.add(join(root, "node_modules", "npm", "bin", cliName));
864
+ };
865
+ addInstallRoot(dirname(process.execPath));
866
+ if (executable.includes("/") || executable.includes("\\"))
867
+ addInstallRoot(dirname(executable));
868
+ for (const pathEntry of (process.env.PATH ?? "").split(delimiter)) {
869
+ addInstallRoot(pathEntry.replace(/^"(.*)"$/, "$1"));
870
+ }
871
+ for (const candidate of candidates) {
872
+ if (await pathExists(candidate)) {
873
+ return { executable: process.execPath, args: [candidate, ...args] };
874
+ }
875
+ }
876
+ throw new Error(`无法在 shell:false 下定位 Windows ${toolName} CLI;请用 run_command 执行 ${toolName},`
877
+ + "或用 run_process 显式调用 node 并把 npm CLI JS 路径放入 args。");
878
+ }
879
+ async function spawnForTool(cwd, input, abortSignal, shouldYield) {
782
880
  const commandCwd = resolveToolPath(cwd, input.cwd);
783
881
  const cwdInfo = await stat(commandCwd);
784
882
  if (!cwdInfo.isDirectory()) {
@@ -788,6 +886,7 @@ export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shoul
788
886
  const startedAt = Date.now();
789
887
  const stdout = { chunks: [], bytes: 0, truncated: false };
790
888
  const stderr = { chunks: [], bytes: 0, truncated: false };
889
+ const command = input.command;
791
890
  return new Promise((resolvePromise, reject) => {
792
891
  let settled = false;
793
892
  let timedOut = false;
@@ -795,11 +894,11 @@ export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shoul
795
894
  let fallbackTimer;
796
895
  let yieldPoll;
797
896
  let backgroundEntry;
798
- const child = spawn(command, {
897
+ const child = spawn(input.executable, input.args, {
799
898
  cwd: commandCwd,
800
- shell: true,
899
+ shell: input.shell,
801
900
  windowsHide: true,
802
- stdio: ["ignore", "pipe", "pipe"],
901
+ stdio: [input.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
803
902
  detached: process.platform !== "win32",
804
903
  });
805
904
  const cleanup = () => {
@@ -934,6 +1033,10 @@ export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shoul
934
1033
  child.stderr?.on("data", (chunk) => {
935
1034
  appendLimitedOutput(stderr, chunk);
936
1035
  });
1036
+ child.stdin?.on("error", () => {
1037
+ // The child error/close event is authoritative; ignore a secondary EPIPE
1038
+ // when an interpreter exits before consuming all script input.
1039
+ });
937
1040
  child.once("error", (err) => {
938
1041
  if (backgroundEntry) {
939
1042
  settle(null, "error");
@@ -948,8 +1051,76 @@ export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shoul
948
1051
  child.once("close", (code, signal) => {
949
1052
  settle(code, signal);
950
1053
  });
1054
+ if (input.stdin !== undefined) {
1055
+ child.stdin?.end(input.stdin, "utf8");
1056
+ }
951
1057
  });
952
1058
  }
1059
+ export async function runCommandForTool(cwd, input, abortSignal, coAuthor, shouldYield) {
1060
+ const command = withGitCoAuthor(input.command?.trim(), coAuthor);
1061
+ if (!command)
1062
+ throw new Error("command is required");
1063
+ if (process.platform === "win32" && isUnsafeWindowsInlineScript(command)) {
1064
+ throw new Error("Windows CMD 无法可靠传递复杂的 python -c/node -e 内联代码;请改用 run_script,"
1065
+ + "单程序参数请用 run_process。run_command 仅保留给 &&、管道和重定向等 shell 操作。");
1066
+ }
1067
+ return spawnForTool(cwd, {
1068
+ executable: command,
1069
+ args: [],
1070
+ command,
1071
+ cwd: input.cwd,
1072
+ timeoutMs: input.timeoutMs,
1073
+ shell: true,
1074
+ }, abortSignal, shouldYield);
1075
+ }
1076
+ export async function runProcessForTool(cwd, input, abortSignal, coAuthor, shouldYield) {
1077
+ const commandCwd = resolveToolPath(cwd, input.cwd);
1078
+ const requestedExecutable = resolveProcessExecutable(commandCwd, input.executable ?? "");
1079
+ const requestedArgs = withGitCoAuthorArgs(requestedExecutable, assertStringArgs(input.args), coAuthor);
1080
+ const invocation = await resolveWindowsNodeShim(requestedExecutable, requestedArgs);
1081
+ return spawnForTool(cwd, {
1082
+ executable: invocation.executable,
1083
+ args: invocation.args,
1084
+ command: formatProcessCommand(requestedExecutable, requestedArgs),
1085
+ cwd: commandCwd,
1086
+ timeoutMs: input.timeoutMs,
1087
+ shell: false,
1088
+ }, abortSignal, shouldYield);
1089
+ }
1090
+ async function defaultScriptExecutable(commandCwd, language) {
1091
+ if (language === "node")
1092
+ return process.execPath;
1093
+ const venvPython = process.platform === "win32"
1094
+ ? join(commandCwd, ".venv", "Scripts", "python.exe")
1095
+ : join(commandCwd, ".venv", "bin", "python");
1096
+ if (await pathExists(venvPython))
1097
+ return venvPython;
1098
+ return process.platform === "win32" ? "python" : "python3";
1099
+ }
1100
+ export async function runScriptForTool(cwd, input, abortSignal, shouldYield) {
1101
+ if (input.language !== "node" && input.language !== "python") {
1102
+ throw new Error("language must be node or python");
1103
+ }
1104
+ if (typeof input.code !== "string" || !input.code.trim()) {
1105
+ throw new Error("code is required");
1106
+ }
1107
+ if (Buffer.byteLength(input.code, "utf8") > MAX_SCRIPT_BYTES) {
1108
+ throw new Error(`script exceeds ${MAX_SCRIPT_BYTES} bytes`);
1109
+ }
1110
+ const commandCwd = resolveToolPath(cwd, input.cwd);
1111
+ const configuredExecutable = input.executable?.trim();
1112
+ const executable = resolveProcessExecutable(commandCwd, configuredExecutable || await defaultScriptExecutable(commandCwd, input.language));
1113
+ const args = ["-", ...assertStringArgs(input.args)];
1114
+ return spawnForTool(cwd, {
1115
+ executable,
1116
+ args,
1117
+ command: `${formatProcessCommand(executable, args)} <${input.language} script via stdin>`,
1118
+ cwd: commandCwd,
1119
+ timeoutMs: input.timeoutMs,
1120
+ shell: false,
1121
+ stdin: input.code,
1122
+ }, abortSignal, shouldYield);
1123
+ }
953
1124
  /** 读取后台命令的最新输出;waitMs > 0 时最多等待该时长(等待期间仍可让位)。 */
954
1125
  export async function commandOutputForTool(taskId, input, shouldYield) {
955
1126
  const entry = backgroundCommands.get(taskId);
@@ -1407,14 +1578,72 @@ export function createBuiltinFileTools(cwd, options = {}) {
1407
1578
  }),
1408
1579
  execute: (input) => presentFileForTool(cwd, input),
1409
1580
  }),
1581
+ run_process: tool({
1582
+ description: "优先使用:以结构化 executable + args 直接运行单个非交互式程序(shell:false),参数不会经过平台 shell 展开,适合测试、git、npm 和含空格/引号的参数。需要 &&、管道或重定向时才用 run_command。",
1583
+ inputSchema: jsonSchema({
1584
+ type: "object",
1585
+ additionalProperties: false,
1586
+ properties: {
1587
+ executable: { type: "string", description: "可执行文件名或路径,例如 git、npm、node、.venv/Scripts/python.exe。" },
1588
+ args: {
1589
+ type: "array",
1590
+ items: { type: "string" },
1591
+ description: "逐项传递给程序的参数;不要自行拼接或 shell 转义。",
1592
+ },
1593
+ cwd: { type: "string", description: "可选工作目录。请用此字段切换目录,不要在命令前拼接 cd /d。" },
1594
+ timeoutMs: { type: "number", description: `可选超时(毫秒),上限为 ${MAX_COMMAND_TIMEOUT_MS}。` },
1595
+ },
1596
+ required: ["executable"],
1597
+ }),
1598
+ execute: async (input, execOptions) => {
1599
+ const action = [input.executable, ...(input.args ?? [])].join(" ");
1600
+ await guard({
1601
+ tool: "run_process",
1602
+ action,
1603
+ reason: isDangerousCommand(action) ? "high-risk" : "rule",
1604
+ detail: `运行程序: ${action}`,
1605
+ });
1606
+ return runProcessForTool(cwd, input, execOptions.abortSignal, options.gitCoAuthor, options.shouldYieldToInjection);
1607
+ },
1608
+ }),
1609
+ run_script: tool({
1610
+ description: "运行多行 Node.js 或 Python 脚本,代码通过 stdin 传入解释器(shell:false),避免 python -c/node -e 的多层引号问题。Python 默认优先使用 cwd 下的 .venv,也可显式指定 executable。",
1611
+ inputSchema: jsonSchema({
1612
+ type: "object",
1613
+ additionalProperties: false,
1614
+ properties: {
1615
+ language: { type: "string", enum: ["node", "python"], description: "脚本语言。" },
1616
+ code: { type: "string", description: "要执行的完整多行代码,不需要 shell 引号。" },
1617
+ args: {
1618
+ type: "array",
1619
+ items: { type: "string" },
1620
+ description: "可选脚本参数,逐项原样传递。",
1621
+ },
1622
+ executable: { type: "string", description: "可选解释器路径,例如 .venv/Scripts/python.exe。" },
1623
+ cwd: { type: "string", description: "可选工作目录。请用此字段切换目录,不要在命令前拼接 cd /d。" },
1624
+ timeoutMs: { type: "number", description: `可选超时(毫秒),上限为 ${MAX_COMMAND_TIMEOUT_MS}。` },
1625
+ },
1626
+ required: ["language", "code"],
1627
+ }),
1628
+ execute: async (input, execOptions) => {
1629
+ const runtime = input.executable?.trim() || input.language;
1630
+ await guard({
1631
+ tool: "run_script",
1632
+ action: `${runtime} script`,
1633
+ reason: "rule",
1634
+ detail: `运行 ${input.language} 脚本`,
1635
+ });
1636
+ return runScriptForTool(cwd, input, execOptions.abortSignal, options.shouldYieldToInjection);
1637
+ },
1638
+ }),
1410
1639
  run_command: tool({
1411
- description: "在本地工作区运行非交互式 shell 命令。用于测试、git 和包脚本。返回 stdout/stderr 和 exitCode;非零退出码不是工具错误。命令运行期间若用户发来新消息,会立即转入后台并返回 taskId(backgrounded:true),命令继续运行,用 command_output 取后续输出。",
1640
+ description: "仅在需要 &&、管道、重定向或其他 shell 运算符时运行非交互式 shell 命令;单程序优先用 run_process,多行 Python/Node 优先用 run_script。请用 cwd 字段切换目录,不要前置 cd /d。返回 stdout/stderr 和 exitCode;非零退出码不是工具错误。运行期间若有新消息会转入后台并返回 taskId。",
1412
1641
  inputSchema: jsonSchema({
1413
1642
  type: "object",
1414
1643
  additionalProperties: false,
1415
1644
  properties: {
1416
- command: { type: "string", description: "要在平台 shell 中运行的命令行。" },
1417
- cwd: { type: "string", description: "可选的工作目录。默认为会话工作目录。" },
1645
+ command: { type: "string", description: "确实需要 shell 解析的命令行。Windows 下复杂 python -c/node -e 会被拒绝。" },
1646
+ cwd: { type: "string", description: "可选工作目录。请用此字段切换目录,不要在命令前拼接 cd /d。" },
1418
1647
  timeoutMs: { type: "number", description: `可选超时(毫秒),上限为 ${MAX_COMMAND_TIMEOUT_MS}。` },
1419
1648
  },
1420
1649
  required: ["command"],
@@ -1430,12 +1659,12 @@ export function createBuiltinFileTools(cwd, options = {}) {
1430
1659
  },
1431
1660
  }),
1432
1661
  command_output: tool({
1433
- description: "读取被让位到后台的命令的最新输出。立即返回当前状态;waitMs 可最多等待 30 秒。任务结束后可反复读取完整 stdout/stderr。",
1662
+ description: "读取 run_process、run_script 或 run_command 让位到后台后的最新输出。立即返回当前状态;waitMs 可最多等待 30 秒。",
1434
1663
  inputSchema: jsonSchema({
1435
1664
  type: "object",
1436
1665
  additionalProperties: false,
1437
1666
  properties: {
1438
- taskId: { type: "string", description: "run_command 返回的 taskId(backgrounded:true 时)。" },
1667
+ taskId: { type: "string", description: "命令工具返回的 taskId(backgrounded:true 时)。" },
1439
1668
  waitMs: { type: "number", description: `可选等待毫秒数;0 或省略立即返回,上限 ${MAX_COMMAND_OUTPUT_WAIT_MS}。` },
1440
1669
  },
1441
1670
  required: ["taskId"],
@@ -1443,12 +1672,12 @@ export function createBuiltinFileTools(cwd, options = {}) {
1443
1672
  execute: (input) => commandOutputForTool(input.taskId, input, options.shouldYieldToInjection),
1444
1673
  }),
1445
1674
  command_kill: tool({
1446
- description: "终止一个仍在后台运行的命令(run_command 让位后转入后台的)。",
1675
+ description: "终止一个由 run_process、run_script 或 run_command 转入后台的进程。",
1447
1676
  inputSchema: jsonSchema({
1448
1677
  type: "object",
1449
1678
  additionalProperties: false,
1450
1679
  properties: {
1451
- taskId: { type: "string", description: "run_command 返回的 taskId。" },
1680
+ taskId: { type: "string", description: "命令工具返回的 taskId。" },
1452
1681
  },
1453
1682
  required: ["taskId"],
1454
1683
  }),
@@ -153,8 +153,8 @@ function readProjectInstructionFiles(cwd) {
153
153
  function buildRuntimeWorkspacePrompt(cwd) {
154
154
  return [
155
155
  `当前工作目录:${cwd}`,
156
- "需要理解代码、配置、项目结构、测试或 git 状态时,主动使用 read_file、list_dir、search_code 和 run_command。",
157
- "使用 run_command 执行非交互式 shell 命令,如 npm test、类型检查、git status、git add、git commit 和 git push。先检查 exitCode、stdout 和 stderr 再决定下一步。",
156
+ "需要理解代码、配置、项目结构、测试或 git 状态时,主动使用 read_file、list_dir、search_code 和命令执行工具。",
157
+ "单个程序优先使用 run_process 的结构化 executable/args;多行 Python/Node 使用 run_script;只有 &&、管道、重定向等 shell 场景才使用 run_command。用 cwd 参数切换目录,不要前置 cd。先检查 exitCode、stdout 和 stderr 再决定下一步。",
158
158
  "编辑前先阅读相关文件范围。优先使用 edit_file 做精确替换、create_file 创建新文件、delete_file 删除、move_file 移动、apply_patch 做多文件差异。",
159
159
  "文件工具通过 DeepCCC 在本地执行。可行时优先使用带 SHA-256 前置条件的受保护编辑,避免覆盖并发用户修改。",
160
160
  ].join("\n");
@@ -288,7 +288,7 @@ export class ChatSession {
288
288
  context;
289
289
  compactionTimeoutMs;
290
290
  maxSteps;
291
- /** 透传给 run_command 的让位注入判定(见 ChatSessionOptions.shouldYieldToInjection)。 */
291
+ /** 透传给命令工具的让位注入判定(见 ChatSessionOptions.shouldYieldToInjection)。 */
292
292
  shouldYieldToInjection;
293
293
  effort;
294
294
  maxOutputTokens;
@@ -146,7 +146,7 @@ export class PermissionGate {
146
146
  return "deny";
147
147
  if (rules.allow.some((r) => keys.some((k) => matchRule(r, k))))
148
148
  return "allow";
149
- // 只有命中内置危险命令库的 run_command 才询问;文件编辑等常规操作默认放行
149
+ // 只有命令执行工具标记为 high-risk 才询问;文件编辑等常规操作默认放行
150
150
  if (request.reason !== "high-risk")
151
151
  return "allow";
152
152
  // 高危且无交互能力(非 TTY / JSONL / 程序化调用)→ 安全默认拒绝
@@ -50,6 +50,8 @@ const TOOL_EMOJI_MAP = {
50
50
  read_file: "\u{1F4D6}", // 📖
51
51
  list_dir: "\u{1F4C2}", // 📂
52
52
  search_code: "\u{1F50E}", // 🔎
53
+ run_process: "\u{1F5A5}\u{FE0F}", // 🖥️
54
+ run_script: "\u{1F5A5}\u{FE0F}", // 🖥️
53
55
  run_command: "\u{1F5A5}\u{FE0F}", // 🖥️
54
56
  edit_file: "\u{270F}\u{FE0F}", // ✏️
55
57
  create_file: "\u{270D}\u{FE0F}", // ✍️
@@ -2,6 +2,8 @@ export const WEB_TOOL_SUMMARY_RULES = {
2
2
  read_file: { emoji: "📖", inputFields: ["path"] },
3
3
  list_dir: { emoji: "📂", inputFields: ["path"] },
4
4
  search_code: { emoji: "🔎", inputFields: ["query", "path"] },
5
+ run_process: { emoji: "🖥️", inputFields: ["executable", "args", "cwd"] },
6
+ run_script: { emoji: "🖥️", inputFields: ["language", "cwd"] },
5
7
  run_command: { emoji: "🖥️", inputFields: ["command"] },
6
8
  edit_file: { emoji: "✏️", inputFields: ["path"] },
7
9
  create_file: { emoji: "✍️", inputFields: ["path"] },
package/dist/src/cards.js CHANGED
@@ -61,6 +61,8 @@ const TOOL_EMOJI_MAP = {
61
61
  read_file: "\u{1F4D6}", // 📖
62
62
  list_dir: "\u{1F4C2}", // 📂
63
63
  search_code: "\u{1F50E}", // 🔎
64
+ run_process: "\u{1F5A5}\u{FE0F}", // 🖥️
65
+ run_script: "\u{1F5A5}\u{FE0F}", // 🖥️
64
66
  run_command: "\u{1F5A5}\u{FE0F}", // 🖥️
65
67
  edit_file: "\u{270F}\u{FE0F}", // ✏️
66
68
  create_file: "\u{270D}\u{FE0F}", // ✍️
@@ -397,7 +399,7 @@ export function buildQueueFullCard() {
397
399
  ],
398
400
  });
399
401
  }
400
- // 协作式让位卡片(仅 ccc):消息进入注入队列,将在 step 边界吸收进本轮。
402
+ // 协作式让位卡片(ccc/codex):消息进入注入队列,将在 step/item 边界吸收进本轮。
401
403
  // 与上面的整轮队列卡片区分,强调“无需等待整轮结束”。
402
404
  export function buildInjectionQueuedCard(text) {
403
405
  const preview = text.length > 100 ? text.slice(0, 100) + "…" : text;
@@ -417,7 +419,7 @@ export function buildInjectionQueuedCard(text) {
417
419
  ],
418
420
  });
419
421
  }
420
- // 注入队列满卡片(仅 ccc)
422
+ // 注入队列满卡片(ccc/codex)
421
423
  export function buildInjectionQueueFullCard() {
422
424
  return JSON.stringify({
423
425
  config: { wide_screen_mode: true },
@@ -267,6 +267,28 @@ function formatTerminalCardContent(state) {
267
267
  ? `${content}\n\n${reason}\n以上内容可能不完整。`
268
268
  : reason;
269
269
  }
270
+ function formatTerminalCardContentForDisplay(state, display) {
271
+ if (display.rotationAccLen === undefined)
272
+ return formatTerminalCardContent(state);
273
+ const accumulatedDelta = state.accumulatedContent.slice(display.rotationAccLen);
274
+ const previousFinalReply = display.rotationFinalReply ?? "";
275
+ const finalReplyDelta = previousFinalReply && state.finalReply.startsWith(previousFinalReply)
276
+ ? state.finalReply.slice(previousFinalReply.length)
277
+ : state.finalReply;
278
+ const content = accumulatedDelta + finalReplyDelta;
279
+ if (state.status !== "error")
280
+ return content;
281
+ const error = state.terminalError ?? {
282
+ kind: "unknown",
283
+ title: "原因未记录",
284
+ message: "当前状态中没有可用的错误详情,请查看运行日志。",
285
+ occurredAt: Date.now(),
286
+ };
287
+ const reason = formatTerminalErrorReason(error);
288
+ return content.trim()
289
+ ? `${content}\n\n${reason}\n以上内容可能不完整。`
290
+ : reason;
291
+ }
270
292
  function isCardKitSequenceConflict(err) {
271
293
  return err instanceof Error && err.message.includes("300317");
272
294
  }
@@ -729,6 +751,85 @@ export function _resetSessionRegistryFileForTest() {
729
751
  export function pickFinalReply(state) {
730
752
  return state.finalCompleteText || state.finalText;
731
753
  }
754
+ const APPLIED_INJECTION_PREVIEW_CHARS = 200;
755
+ export function unwrapInjectedUserMessage(text) {
756
+ const match = text.match(/^\[User message\]\r?\n([\s\S]*)\r?\n\[\/User message\]$/);
757
+ return match?.[1] ?? text;
758
+ }
759
+ export function formatAppliedInjectionReceipt(text) {
760
+ const normalized = text.trim() || "(空消息)";
761
+ const chars = Array.from(normalized);
762
+ const preview = chars.length > APPLIED_INJECTION_PREVIEW_CHARS
763
+ ? `${chars.slice(0, APPLIED_INJECTION_PREVIEW_CHARS).join("")}…`
764
+ : normalized;
765
+ return `已收到注入信息:${preview}`;
766
+ }
767
+ async function rotateFeishuCardForAppliedInjection(sessionId, turnCount, tool, injectedText, state, nextHeaderTitle) {
768
+ if (tool !== "codex" && tool !== "ccc")
769
+ return;
770
+ const chatId = pickDisplayChat(sessionId);
771
+ if (!chatId)
772
+ return;
773
+ const platform = platformForChat(chatId);
774
+ if (platform?.kind !== "feishu")
775
+ return;
776
+ let previousDisplay = displayCards.get(chatId);
777
+ if (previousDisplay
778
+ && (previousDisplay.sessionId !== sessionId || previousDisplay.turnCount !== turnCount)) {
779
+ previousDisplay = undefined;
780
+ }
781
+ if (previousDisplay) {
782
+ while (previousDisplay.cardBusy)
783
+ await new Promise((resolve) => setTimeout(resolve, 20));
784
+ if (displayCards.get(chatId) !== previousDisplay)
785
+ previousDisplay = undefined;
786
+ }
787
+ if (previousDisplay) {
788
+ previousDisplay.cardBusy = true;
789
+ const nextSequence = previousDisplay.sequence + 1;
790
+ const oldContent = state.accumulatedContent + pickFinalReply(state);
791
+ const oldCard = buildProgressCard(progressView({
792
+ text: truncateContent(oldContent) || "等待 Agent 输出...",
793
+ status: "done",
794
+ showStop: false,
795
+ headerTitle: "已收到注入,后续转入新卡片",
796
+ headerTemplate: "blue",
797
+ }));
798
+ await platform.cardUpdate(previousDisplay.cardId, oldCard, nextSequence).then(() => {
799
+ previousDisplay.sequence = nextSequence;
800
+ }).catch((err) => {
801
+ console.error(`[${ts()}] [INJECT] freeze old card failed: ${err.message}`);
802
+ if (isCardKitSequenceConflict(err))
803
+ previousDisplay.sequence = nextSequence;
804
+ });
805
+ }
806
+ await platform.sendText(chatId, formatAppliedInjectionReceipt(injectedText)).catch((err) => {
807
+ console.error(`[${ts()}] [INJECT] receipt send failed: ${err.message}`);
808
+ });
809
+ const newCardId = await createVisibleProgressCard(platform, chatId, sessionId, turnCount, "新生成卡片发送失败,结果将继续更新在上一张卡片中。", nextHeaderTitle);
810
+ if (!newCardId) {
811
+ if (previousDisplay)
812
+ previousDisplay.cardBusy = false;
813
+ return;
814
+ }
815
+ if (previousDisplay) {
816
+ await markCardDone(sessionId, turnCount, previousDisplay.cardId);
817
+ }
818
+ displayCards.set(chatId, {
819
+ cardId: newCardId,
820
+ sequence: 1,
821
+ cardBusy: false,
822
+ cardCreatedAt: Date.now(),
823
+ lastSentContent: "",
824
+ lastSentHeaderTitle: nextHeaderTitle,
825
+ streamErrorNotified: false,
826
+ sessionId,
827
+ turnCount,
828
+ rotationAccLen: state.accumulatedContent.length,
829
+ rotationFinalReply: pickFinalReply(state),
830
+ dotCount: 0,
831
+ });
832
+ }
732
833
  export function accumulateBlockContent(block, state, toolCallMap) {
733
834
  if (state.transcript !== undefined || (block.type !== "agent_progress" && block.type !== "text_reset")) {
734
835
  const transcriptState = { transcript: state.transcript ?? [] };
@@ -1123,7 +1224,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
1123
1224
  else {
1124
1225
  const nextSeq = display.sequence + 1;
1125
1226
  const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
1126
- const cardContent = truncateContent(formatTerminalCardContent(prevState)) || " ";
1227
+ const cardContent = truncateContent(formatTerminalCardContentForDisplay(prevState, display)) || " ";
1127
1228
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
1128
1229
  await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
1129
1230
  console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${err.message}`);
@@ -1289,7 +1390,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
1289
1390
  }
1290
1391
  try {
1291
1392
  // 供 onInjectionRejected 退回的“最后一条已取出但未成功注入”的消息
1292
- let lastShiftedInjection = null;
1393
+ const injectionState = { lastShifted: null };
1293
1394
  for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
1294
1395
  onProcessStart: (processInfo) => {
1295
1396
  startPromptProcessMonitor(sessionId, processInfo);
@@ -1312,16 +1413,16 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
1312
1413
  const injected = shiftInjection(sessionId);
1313
1414
  if (!injected)
1314
1415
  return undefined;
1315
- lastShiftedInjection = injected;
1416
+ injectionState.lastShifted = injected;
1316
1417
  // 与首次消息保持一致的结构化包装;imSkillsPrompt 已在首次消息中,
1317
1418
  // 此处不重复注入,避免多轮注入导致上下文膨胀。
1318
1419
  return `[User message]\n${injected.text}\n[/User message]`;
1319
1420
  } : undefined,
1320
1421
  // 注入未被当前 turn 吸收(如 turn 恰好已结束)时,退回队列头部,避免丢消息。
1321
1422
  onInjectionRejected: tool === "ccc" || tool === "codex" ? () => {
1322
- if (lastShiftedInjection) {
1323
- unshiftInjection(sessionId, lastShiftedInjection);
1324
- lastShiftedInjection = null;
1423
+ if (injectionState.lastShifted) {
1424
+ unshiftInjection(sessionId, injectionState.lastShifted);
1425
+ injectionState.lastShifted = null;
1325
1426
  }
1326
1427
  } : undefined,
1327
1428
  })) {
@@ -1339,7 +1440,15 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
1339
1440
  let activityChanged = false;
1340
1441
  let progressHeartbeat = false;
1341
1442
  let outputReset = false;
1342
- for (const block of unifiedMsg.blocks) {
1443
+ let injectionBoundaryObserved = false;
1444
+ for (const originalBlock of unifiedMsg.blocks) {
1445
+ let block = originalBlock;
1446
+ if (originalBlock.type === "input_injected") {
1447
+ const injectedText = injectionState.lastShifted?.text
1448
+ ?? unwrapInjectedUserMessage(originalBlock.text);
1449
+ injectionState.lastShifted = null;
1450
+ block = { ...originalBlock, text: injectedText };
1451
+ }
1343
1452
  if (block.type === "agent_progress")
1344
1453
  progressHeartbeat = true;
1345
1454
  if (block.type === "text_reset") {
@@ -1349,6 +1458,10 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
1349
1458
  if (updateAgentActivity(activityTracker, block))
1350
1459
  activityChanged = true;
1351
1460
  accumulateBlockContent(block, state, toolCallMap);
1461
+ if (block.type === "input_injected") {
1462
+ injectionBoundaryObserved = true;
1463
+ await rotateFeishuCardForAppliedInjection(sessionId, nextTurnCount, tool, block.text, state, formatAgentActivityTitle(activityTracker.activity));
1464
+ }
1352
1465
  if (block.type === "compact_boundary" && block.post_tokens) {
1353
1466
  for (const cid of getChatsForSession(sessionId)) {
1354
1467
  const sinfo = sessionInfoMap.get(cid);
@@ -1374,7 +1487,10 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
1374
1487
  }
1375
1488
  // 定时写入文件
1376
1489
  const now2 = Date.now();
1377
- if (activityChanged || outputReset || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
1490
+ if (activityChanged
1491
+ || outputReset
1492
+ || injectionBoundaryObserved
1493
+ || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
1378
1494
  lastFileWrite = now2;
1379
1495
  await writeStreamState({
1380
1496
  sessionId,
@@ -1814,7 +1930,7 @@ export function startUnifiedDisplayLoop() {
1814
1930
  if (!terminalCardAlreadyUpdated) {
1815
1931
  const nextSeq = display.sequence + 1;
1816
1932
  const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
1817
- const cardContent = truncateContent(formatTerminalCardContent(state)) || " ";
1933
+ const cardContent = truncateContent(formatTerminalCardContentForDisplay(state, display)) || " ";
1818
1934
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
1819
1935
  await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
1820
1936
  display.sequence = nextSeq;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.296",
3
+ "version": "0.2.298",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",