c0de-agent 1.12.0 → 1.13.0

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 (142) hide show
  1. package/README.md +51 -23
  2. package/dist/cli/commands/acp.js +26 -8
  3. package/dist/cli/commands/chat.js +70 -6
  4. package/dist/cli/commands/config.d.ts +2 -0
  5. package/dist/cli/commands/config.js +46 -0
  6. package/dist/cli/commands/serve.js +11 -2
  7. package/dist/cli/commands/sessions.js +51 -4
  8. package/dist/cli/commands/trust.d.ts +11 -0
  9. package/dist/cli/commands/trust.js +31 -0
  10. package/dist/cli/commands/update.js +4 -0
  11. package/dist/cli/deps.d.ts +9 -6
  12. package/dist/cli/deps.js +66 -19
  13. package/dist/cli/index.d.ts +4 -1
  14. package/dist/cli/index.js +92 -12
  15. package/dist/cli/modes/acp.js +39 -8
  16. package/dist/cli/modes/print.d.ts +3 -0
  17. package/dist/cli/modes/print.js +71 -8
  18. package/dist/core/agent.d.ts +1 -1
  19. package/dist/core/agent.js +9 -4
  20. package/dist/core/agents/builtin.js +2 -2
  21. package/dist/core/agents/index.d.ts +0 -1
  22. package/dist/core/agents/index.js +0 -1
  23. package/dist/core/config.d.ts +43 -1
  24. package/dist/core/config.js +163 -14
  25. package/dist/core/loop/segment.d.ts +3 -1
  26. package/dist/core/loop/segment.js +24 -1
  27. package/dist/core/loop/subagent.js +65 -32
  28. package/dist/core/loop.d.ts +7 -0
  29. package/dist/core/loop.js +90 -6
  30. package/dist/core/slash.d.ts +10 -0
  31. package/dist/core/slash.js +150 -20
  32. package/dist/core/types.d.ts +20 -1
  33. package/dist/core/workflows/context.js +44 -4
  34. package/dist/core/workflows/discovery.d.ts +2 -2
  35. package/dist/core/workflows/discovery.js +5 -7
  36. package/dist/core/workflows/index.d.ts +2 -1
  37. package/dist/core/workflows/index.js +2 -1
  38. package/dist/core/workflows/registry.d.ts +13 -3
  39. package/dist/core/workflows/registry.js +14 -9
  40. package/dist/core/workflows/resolve.d.ts +22 -0
  41. package/dist/core/workflows/resolve.js +23 -0
  42. package/dist/core/workflows/runtime.d.ts +6 -1
  43. package/dist/core/workflows/runtime.js +19 -2
  44. package/dist/db/schema.d.ts +356 -1
  45. package/dist/db/schema.js +57 -3
  46. package/dist/kanban/index.d.ts +2 -1
  47. package/dist/kanban/index.js +1 -1
  48. package/dist/kanban/store.d.ts +84 -2
  49. package/dist/kanban/store.js +358 -25
  50. package/dist/llm/registry.d.ts +7 -1
  51. package/dist/llm/registry.js +7 -1
  52. package/dist/plugins/init.d.ts +7 -0
  53. package/dist/plugins/init.js +22 -1
  54. package/dist/plugins/loader.d.ts +7 -1
  55. package/dist/plugins/loader.js +12 -4
  56. package/dist/project/index.d.ts +1 -1
  57. package/dist/project/index.js +1 -1
  58. package/dist/project/project.d.ts +23 -0
  59. package/dist/project/project.js +65 -0
  60. package/dist/project/trust.d.ts +65 -0
  61. package/dist/project/trust.js +328 -0
  62. package/dist/server/agent-manager.d.ts +6 -2
  63. package/dist/server/agent-manager.js +4 -2
  64. package/dist/server/app.js +45 -30
  65. package/dist/server/auth-manager.d.ts +15 -4
  66. package/dist/server/auth-manager.js +41 -24
  67. package/dist/server/context.js +11 -1
  68. package/dist/server/index.d.ts +1 -1
  69. package/dist/server/index.js +1 -1
  70. package/dist/server/middleware/auth.d.ts +11 -1
  71. package/dist/server/middleware/auth.js +25 -3
  72. package/dist/server/permission/interactive.d.ts +5 -0
  73. package/dist/server/permission/interactive.js +4 -1
  74. package/dist/server/permission/store.d.ts +2 -0
  75. package/dist/server/permission/store.js +8 -0
  76. package/dist/server/routes/auth.js +35 -9
  77. package/dist/server/routes/chat.d.ts +16 -7
  78. package/dist/server/routes/chat.js +483 -28
  79. package/dist/server/routes/commands.d.ts +2 -2
  80. package/dist/server/routes/commands.js +4 -6
  81. package/dist/server/routes/config.js +81 -4
  82. package/dist/server/routes/files.js +120 -24
  83. package/dist/server/routes/kanban.js +152 -22
  84. package/dist/server/routes/permissions.js +7 -0
  85. package/dist/server/routes/project.js +76 -22
  86. package/dist/server/routes/provider.js +6 -0
  87. package/dist/server/routes/session.js +177 -13
  88. package/dist/server/routes/tool.js +24 -5
  89. package/dist/server/routes/update.js +58 -2
  90. package/dist/server/routes/usage.d.ts +4 -0
  91. package/dist/server/routes/usage.js +133 -0
  92. package/dist/server/routes/workflows.js +476 -87
  93. package/dist/server/server.d.ts +12 -1
  94. package/dist/server/server.js +194 -33
  95. package/dist/server/terminal/pty-manager.d.ts +12 -0
  96. package/dist/server/terminal/pty-manager.js +63 -7
  97. package/dist/server/types.d.ts +9 -0
  98. package/dist/session/branch.d.ts +2 -1
  99. package/dist/session/branch.js +75 -10
  100. package/dist/session/context.js +28 -2
  101. package/dist/session/index.d.ts +1 -1
  102. package/dist/session/index.js +1 -1
  103. package/dist/session/session.d.ts +67 -10
  104. package/dist/session/session.js +180 -67
  105. package/dist/session/types.d.ts +2 -0
  106. package/dist/session/usage.d.ts +62 -0
  107. package/dist/session/usage.js +194 -0
  108. package/dist/shared/types/agent.d.ts +19 -3
  109. package/dist/shared/types/config.d.ts +59 -4
  110. package/dist/shared/types/kanban.d.ts +4 -2
  111. package/dist/shared/types/message.d.ts +14 -0
  112. package/dist/tools/builtin/kanban.js +13 -2
  113. package/dist/tools/index.d.ts +14 -3
  114. package/dist/tools/index.js +23 -5
  115. package/dist/tools/websearch/index.d.ts +5 -1
  116. package/dist/tools/websearch/index.js +40 -4
  117. package/dist/tools/websearch/providers/brave.js +2 -2
  118. package/dist/tools/websearch/providers/duckduckgo.js +2 -1
  119. package/dist/tools/websearch/providers/tavily.js +2 -2
  120. package/dist/tools/websearch/types.d.ts +5 -1
  121. package/dist/tools/websearch/types.js +7 -1
  122. package/dist/update/scheduler.js +5 -2
  123. package/dist/update/snapshot.d.ts +17 -3
  124. package/dist/update/snapshot.js +3 -2
  125. package/dist/update/updater.d.ts +2 -0
  126. package/dist/update/updater.js +2 -1
  127. package/dist/update/version.d.ts +3 -0
  128. package/dist/update/version.js +9 -3
  129. package/drizzle/0010_usage_events_ledger.sql +17 -0
  130. package/drizzle/0011_slow_slapstick.sql +12 -0
  131. package/drizzle/0012_chief_timeslip.sql +1 -0
  132. package/drizzle/0013_swift_zeigeist.sql +1 -0
  133. package/drizzle/0014_flimsy_legion.sql +1 -0
  134. package/drizzle/meta/0010_snapshot.json +1010 -0
  135. package/drizzle/meta/0011_snapshot.json +1055 -0
  136. package/drizzle/meta/0012_snapshot.json +1062 -0
  137. package/drizzle/meta/0013_snapshot.json +1068 -0
  138. package/drizzle/meta/0014_snapshot.json +1075 -0
  139. package/drizzle/meta/_journal.json +35 -0
  140. package/package.json +24 -23
  141. package/dist/core/agents/discovery.d.ts +0 -6
  142. package/dist/core/agents/discovery.js +0 -96
package/README.md CHANGED
@@ -6,18 +6,21 @@
6
6
 
7
7
  - **Browser-Server 架构**:Hono 后端通过 SSE 推送 LLM 流式响应;React 前端单页应用。
8
8
  - **本地优先**:会话与索引存储在 PGLite(浏览器/进程内 PostgreSQL),数据随用随取。
9
- - **多项目**:工作区隔离的多项目管理,每个项目独立会话历史与配置。项目目录被**移动/重命名**后,顶栏「目录已失效」提示提供「重新定位」——输入新路径即可把会话与看板整体迁移过去(无需删除项目)。
10
- - **工具系统**:内置文件读写、bash、grep、glob、编辑等工具,可按需启用/禁用。
11
- - **权限模式**:`default` / `auto` 等授权模式,控制工具执行前是否需用户确认。权限确认双层超时:5 分钟未确认仅提示(可在弹窗中重新确认,不重发消息);再过 25 分钟仍未响应则自动拒绝(run 继续执行,会话不会无限挂起)。
9
+ - **多项目**:工作区隔离的多项目管理,每个项目独立会话历史与配置。项目目录被**移动/重命名**后,顶栏「目录已失效」提示提供「重新定位」——输入新路径即可把会话与看板整体迁移过去(无需删除项目)。设置页区分「项目/全局」两个配置作用域;**首次添加 Provider 且全局尚无任何 AI 服务时会提示一次保存到全局作用域(所有项目与 CLI 可见)还是仅当前项目**——项目作用域的 Provider 只在该项目生效,其他项目与 `c0de chat` 不可见。前端默认模型选择会校正失效组合:`defaultProvider`/`defaultModel` 不在已配置 provider 的模型清单时,自动回退到首个已配置 provider 与其首个声明模型。
10
+ - **工具系统**:内置文件读写、bash、grep、glob、编辑等工具,可按需启用/禁用(`tools.enabled` 与 `slashCommands.enabled` 语义统一 fail-closed:含 `'*'` = 全部,显式名单 = 仅名单内,**空数组 = 全部禁用**——旧版「空=全部启用」的配置升级后会收到迁移告警引导改 `['*']`)。
11
+ - **权限模式**:`default` / `auto` 等授权模式,控制工具执行前是否需用户确认。权限确认双层超时(`permission.timeoutMs` / `permission.expireGraceMs`,默认 5 分钟 + 25 分钟,设置页可调):首层超时仅提示(可在弹窗中重新确认,不重发消息);宽限期满仍未响应则自动拒绝。默认在拒绝后**暂停对话**(等你点击「恢复」继续,agent 不会在无人确认时继续自主执行);`permission.timeoutAction: 'deny'` 可改回旧行为(拒绝后对话继续,会话永不挂起)。
12
+ - **项目信任边界**:项目级 `.c0de/config.json` 与 `.c0de/plugins` 默认**不自动生效**——克隆一个携带这些文件的仓库后,若其配置把权限降级为 `auto`、要求启用项目插件、声明了 MCP 服务器、或把 `providers[].baseURL` 指向自定义端点(你的提示词将发往该地址),Web 聊天入口会弹出「信任此项目?」确认(明示每一项风险),显式信任后才放行;CLI(`c0de chat` / `c0de acp`)无确认弹窗,遇到未信任项目的风险配置会直接拒绝并引导 `c0de trust <dir> --yes`(`c0de trust` 会先打印将接受的风险清单,有风险时必须 `--yes` 确认,不盲信任)。信任**非一次性**——信任时落盘项目风险配置指纹,若仓库后续 `git pull` 新增 `auto`/插件/MCP/自定义 baseURL 等风险键,**或修改了 MCP 参数(如同名改 args)与插件代码(如同名改文件内容)**,指纹漂移会重新触发确认(可随时收回项目级配置);项目插件本就在项目未信任时不加载(`c0de trust` 或 Web 信任后重启 serve 生效),**已信任项目指纹漂移后同样不加载**(与聊天门禁同口径,重启也不会执行漂移后的插件代码)。**`security.*` 是服务端全局参数,不参与项目作用域合并**(加载时整体剥离:`authEnabled`/`token`/`allowedOrigins` 等只能在本机全局配置设置)——克隆仓库无法借 `.c0de/config.json` 静默关闭鉴权、植入已知弱 token 或放宽 CORS 来源;项目配置中遗留的 `security` 键会在设置页告警「已忽略」,Web 设置/`c0de config set` 对项目作用域写入 `security` 会直接拒绝并引导改全局。全局配置(`~/.c0de/config.json`)是你本机的显式编辑,其 `permission.defaultMode: auto` / `permission.timeoutAction: deny` 会直接作用于所有项目;为防「本机全局 auto 让无 `.c0de` 的克隆仓库跳过信任门禁全自动执行」,**未信任项目 + 全局权限风险同样触发一次性信任确认**(信任后全局不复检;全局插件/MCP 仍是本机显式安装、不参与门禁)。信任确认弹窗会把项目与全局风险一并列明供你知情。
12
13
  - **会话可恢复**:会话快照序列化/反序列化,支持进程重启后无缝恢复。
13
- - **无感知热更新**:后台定期检查新版本,安装成功后才暂停活跃会话 → 序列化会话快照 → npm 自更新 → 端口 handoff 接管。空闲会话零感知;进行中的对话会中断(界面提示后可一键重发继续),安装失败时不触碰任何会话状态。更新完成后请**刷新页面**加载新版界面(后续请求已自动落到新实例,不刷新则页面仍运行旧版前端)。
14
+ - **无感知热更新**:后台定期检查新版本,安装成功后才暂停活跃会话 → 序列化会话快照 → npm 自更新 → 端口 handoff 接管。空闲会话零感知;进行中的对话会中断(界面提示后可一键重发继续),中断前的半截回复与工具结果会在时间线置灰标记为「未完成轮次」并**从上下文剔除**(重发后不影响新对话),安装失败时不触碰任何会话状态。**终端面板**:应用更新前会列出将被关闭的终端(含标题/目录,分级确认弹层需输入版本号确认);快照携带终端元信息,新实例启动后按原 id 在**原位重建 shell**(刷新页面后终端布局无感重连;终端内正在运行的进程如 dev server 无法续命,会随旧实例停止)。应用更新确认层还会列出**等待确认的权限请求数**——更新会使其弹窗失效(该工具按拒绝处理),避免用户盯着弹窗却被静默终断;进行中的对话逐项标注运行态——**正在执行工具的 run 会先等待其完成,超时未暂停将被强制中止(其子进程会被终止)**,已暂停的 run 标为「安全点」。更新完成后请**刷新页面**加载新版界面(后续请求已自动落到新实例,不刷新则页面仍运行旧版前端)。
14
15
  - **追加指令**:对话进行中可注入「追加指令」(steering),立即影响下一轮模型输出,并作为会话历史的一部分持久化——刷新后不丢失。
15
16
  - **故障回退**:主 provider 失败时自动重试;启用后按配置的重试参数执行,并回退到声明了同一模型的其他 provider(设置 → 故障回退)。
16
- - **斜杠命令**:`/compact`、`/model`、`/clear` 等可配置的斜杠命令。
17
- - **会话回收站**:删除会话软删除入回收站,保留 60 天(自**首次在回收站看到该条目**起算——只标记一次,不随每次打开重置,倒计时稳定可预期)可恢复(按项目隔离,恢复时自动重新归属)。到期后先进入 **7 天宽限期**(回收站内明示「即将清除」,仍可恢复),宽限期满才物理清除,绝不静默清空。删除项目会将其会话移入回收站的「未归属项目」分组(跨项目可见,恢复时归属到某项目);该分组默认折叠,展开时才启动保留期倒计时。恢复会话时仅还原同一次删除操作中一并删除的分支,漏掉的分支会在提示中显式告知数量。
18
- - **会话导出/导入**:会话可导出为 JSON 备份,并在任意项目重新导入(会话列表「导入」按钮),跨机器迁移或本地备份均可用。导出含权限态(自动授权/始终允许白名单)时,导入前会明示风险并让你选择是否随迁——默认剥离,避免导入他人导出的会话时静默继承对危险工具的免确认放行。导出仅含本会话与归档;有派生分支时会先提示分支内容不会随迁。
19
- - **看板**:项目级任务看板(与会话内 agent 待办相互独立,可把待办导出为看板卡片),支持导出/导入整板 JSON 备份——注意:删除项目会**永久**级联删除看板(会话有回收站,看板没有),删除前请先导出备份;目录只是被移动/重命名时请用「重新定位」整体迁移,不要用删除。
20
- - **会话用量**:会话列表每行显示累计 token 徽标;会话页「会话信息」面板提供 token/成本/消息数等完整统计(token 为服务端上报用量,成本按配置价目估算)。
17
+ - **斜杠命令**:`/compact`、`/model`、`/clear` 等可配置的斜杠命令(`slashCommands.enabled` 与 `tools.enabled` 同为 fail-closed 语义)。
18
+ - **分支(fork)**:会话可在任意消息处分支为独立会话。分支继承分支点之前的完整上下文——包括压缩/清理归档(归档面板可查)、用量统计(token 徽标/会话信息面板显示继承成本;新调用按实际发生记账,不重复计费),以及权限态(会话级 `auto` 模式与「始终允许」白名单)与 CLI 会话的工作目录。
19
+ - **会话回收站**:删除会话软删除入回收站,保留 60 天(自**首次打开回收站看到该分组**起算——分组粒度、只标记一次,不随每次打开重置,倒计时稳定可预期;从未被查看的条目自删除起最长保留 365 天后进入宽限期,不会无限滞留)可恢复(按项目隔离,恢复时自动重新归属)。到期后先进入 **7 天宽限期**(回收站内明示「即将清除」,仍可恢复),宽限期满才物理清除,绝不静默清空。回收站列表按到期紧迫度排序(宽限期内条目置顶,其次按到期先后)。删除项目会将其会话移入回收站的「未归属项目」分组(跨项目可见),该分组默认折叠,展开时才启动保留期倒计时;恢复此类会话时可**二选一**:「恢复到当前项目」或「恢复并重建原项目」(原目录仍存在时重建项目记录,响应与界面明示重建的项目,不再静默复活已删除的项目)。恢复会话时仅还原同一次删除操作中一并删除的分支,漏掉的分支会在提示中显式告知数量;恢复前确认框会列出将连带还原的父会话。
20
+ - **会话导出/导入**:会话可导出为 JSON 备份,并在任意项目重新导入(会话列表「导入」按钮),跨机器迁移或本地备份均可用。导出含权限态(自动授权/始终允许白名单)时,导入前会明示风险并让你选择是否随迁——默认剥离,避免导入他人导出的会话时静默继承对危险工具的免确认放行;归档中的**文件快照(被压缩进上下文的文件内容)默认剥离**,导出对话框显式确认后才携带,分享导出 JSON 不泄露文件内容。用量统计(segments)随导入携带、会话信息面板/徽标保留历史成本与 token 口径,但**导入的调用不进入本机成本账本**(`usage_events`)——跨机器导入的历史消费不污染本地月度成本与预算护栏;且 segments 按导入消息时间窗裁剪。导出仅含本会话与归档;有派生分支时会先提示分支内容不会随迁。
21
+ - **看板**:项目级任务看板(与会话内 agent 待办相互独立,可把待办导出为看板卡片),支持导出/导入整板 JSON 备份。**删除项目不再永久销毁看板**——看板随项目软删除进入回收站「未归属看板」分组(删除后保留 60 天,到期先进入 7 天宽限期——明示「即将清除」仍可恢复——宽限期满才物理清除,绝不静默清空),可「恢复到当前项目」、「恢复并重建原项目」(原目录仍存在时重建项目记录)或彻底删除。目录只是被移动/重命名时请用「重新定位」整体迁移,不要用删除。
22
+ - **工作流**:内置/用户级(`~/.c0de/workflows`)/项目级(`.c0de/workflows`)三级工作流脚本,聊天中 `/workflow run <name>` 或 REST `POST /api/workflows/:name/run` 执行(信任门禁、预算护栏、权限确认与主对话同口径;`meta.timeout` 超时中止执行)。设置页「工作流」面板可查看源码、新建、编辑、删除(删除需输入名称确认)。
23
+ - **会话用量**:会话列表每行显示累计 token 徽标;会话页「会话信息」面板提供 token/成本/消息数等完整统计(token 为服务端上报用量,成本按配置价目估算)。设置页「用量与成本」面板提供**项目口径**聚合:总量、按月、按模型统计,并可设置月度成本预算(`usage.monthlyBudgetUsd`)与超支动作(`usage.budgetAction: 'warn' | 'pause'`)——`warn` 仅告警(当月成本达预算 80% 徽标变警示色、超预算变红),`pause` 在新一轮回复前暂停对话(等同权限超时暂停机制,点「恢复」继续,本 run 不再重复暂停);CLI(`c0de chat`)无「恢复」交互,`pause` 时在入口单次拒绝、并每轮 LLM 请求前复查、超支即中止 run 明示超支(`warn` 不变,仅告警);默认 `warn`。另可设置**全局月度预算**(`usage.globalMonthlyBudgetUsd`,所有项目+未归属调用聚合,仅全局作用域生效)作兜底护栏——与项目预算并存,任一超支即触发动作;另支持**月度 token 预算**(`usage.monthlyTokenBudget` / `usage.globalMonthlyTokenBudget`,input+output+cacheRead tokens 之和)——价格独立护栏,兜底「自建网关/未登记模型 cost 恒 $0、金额护栏拦不住」的场景(token 预算动作由 `usage.tokenBudgetAction` 独立控制,缺省回退 `budgetAction`;`pause` 时超支暂停/拒绝,`warn` 下仅顶栏徽标与「用量与成本」面板告警、不暂停对话);「未归属项目」的调用(CLI 未绑定会话/孤儿会话)单独统计展示,且**只受全局口径预算约束**——只设项目预算、未设全局预算时,这些调用没有任何护栏(设置页会告警提示补充全局预算)。**本月口径由服务端本地时区计算下发**,前端徽标/面板与预算暂停判定同源一致(远程访问/容器时区不同也不会错报)。成本是账本:写入独立的 `usage_events` 表,**会话彻底删除(含回收站到期物理清除)后已发生花费仍计入**;价格未知的调用(自建网关/未登记模型)按 $0 计入并单独计数提示,内置价目标注最近核价日期(估算可能随价格变动过期,实际费用以账单为准)。顶栏显示**本月成本徽标**(按当前项目口径),点击直达用量面板。
21
24
  - **主题**:亮/暗/跟随系统主题。
22
25
 
23
26
  ## 安装
@@ -32,18 +35,23 @@ pnpm add -g c0de-agent
32
35
 
33
36
  要求 Node.js >= 22.0.0。
34
37
 
38
+ > **首次使用前先配置 AI 服务**:项目默认不携带 `providers`(`providers: []`)。启动
39
+ > `c0de serve` 后,先在 Web 界面「设置 → Provider」添加 API 服务并「测试连接」,或在
40
+ > `~/.c0de/config.json` 中手动写入 `providers`(`c0de config set providers …`)。未配置
41
+ > provider 时发送消息会得到「未配置可用的 AI 服务」引导,而非模型响应。
42
+
35
43
  ## 安全
36
44
 
37
45
  - **密钥存储**:`config.json` 写入时强制 `chmod 600`;`providers[].apiKey` 与 `websearch.tavilyApiKey/braveApiKey` 落盘前自动加密(AES-256-GCM,机器绑定,`enc:` 前缀),无论经 Web 设置、`c0de config set` 还是 `/config` 斜杠命令写入均不落明文。例外:`security.token`(静态模式专用)以明文存储,仅建议 CI/脚本场景使用,交互使用请走设备配对。`/config` 与 `c0de config get` 展示配置时对 apiKey/token 等字段自动脱敏。
38
46
  - 项目级配置(`<项目>/.c0de/config.json`)含密钥且目录在 git 仓库内时,设置页会提示将 `.c0de/` 加入 `.gitignore`,防止误提交。
39
- - **认证**:`security.authEnabled` 默认开启。首次启动自动生成 bootstrap token(持久化于数据目录 `auth-token` 文件),浏览器首访凭启动打印的 URL `?token=` 注册为**首台设备**,服务端随即**轮换 bootstrap token**(旧 token 立即失效,杜绝 URL/shell 历史泄漏)并下发设备 token;后续 API 请求与终端 WebSocket 均携带设备 token。新增设备无 token 时进入**配对流程**:新设备生成 6 位配对码,由已授权设备在「设备配对」弹窗中核对并批准后下发新设备 token。显式配置 `security.token` 时为静态模式(不轮换、不配对,适合 CI/脚本)。
47
+ - **认证**:`security.authEnabled` 默认开启。首次启动自动生成 bootstrap token(持久化于数据目录 `auth-token` 文件),浏览器首访凭启动打印的 URL `?token=` 注册为**首台设备**,服务端随即**轮换 bootstrap token**(旧 token 立即失效,杜绝 URL/shell 历史泄漏)并下发设备 token;后续 API 请求与终端 WebSocket 均携带设备 token。新增设备无 token 时进入**配对流程**:新设备生成 6 位配对码,由已授权设备在「设备配对」弹窗中**输入对方屏幕显示的 6 位配对码**(服务端核对匹配,防多个请求并存时看错行误批)批准后下发新设备 token(设备**在 token 实际交付时**才登记——审批后、新设备取 token 前若服务重启,审批态随内存清空、重新配对即可,不会留下「已授权却永远拿不到 token」的僵尸设备条目)。显式配置 `security.token` 时为静态模式(不轮换、不配对,适合 CI/脚本)。**已有已注册设备后**,`c0de serve` 打印的启动 URL 不再携带 `?token=`(那已是轮换失效的旧值)——新浏览器直接走配对流程,已注册浏览器凭 localStorage 中的设备 token 无缝使用。
40
48
  - **首设备先到先得**:任何能看见启动打印 URL 的人(同机其他用户、终端输出被截图/共享)都能抢先注册为首台设备,随后真实用户只能经其配对审批。请勿在共享主机或会被录屏的场景让 URL 暴露。默认防护:bootstrap token 生成后 **5 分钟**内有效(`security.firstDeviceTtlMs` 默认值),超时需重启 `c0de serve`(无已注册设备时重启会重新生成 bootstrap,窗口随之刷新);共享主机/CI 等场景可显式配置更短的值。
41
49
  - token 解析优先级:`security.token` 配置 > 环境变量 `C0DE_AUTH_TOKEN` > 数据目录 token 文件 > 自动生成并持久化。
42
50
  - 显式关闭:`security.authEnabled: false`(本机单用户且端口仅本机可达时)。
43
51
  - **设备管理**:`c0de auth list` 列出已授权设备;`c0de auth revoke <id>` 撤销设备(运行中的服务立即生效);`c0de auth reset` 清除全部设备与 token。运行中的服务会热加载 `devices.json` 的变更。
44
- - **丢失唯一设备的恢复**:若唯一设备的浏览器数据被清空(设备 token 丢失),配对流程因无已授权设备可批准而无法完成。恢复步骤:`c0de auth reset` → 重启 `c0de serve` → 打开启动日志打印的带 `?token=` 的 URL 重新注册首台设备。
45
- - **CORS/Origin 校验**:仅放行本机回环 origin 与 `security.allowedOrigins` 中显式配置的 origin;WebSocket 升级在服务端独立校验 Origin(浏览器 WS 不受 CORS 约束)。
46
- - **监听地址**:默认绑定 `0.0.0.0`(便于容器/远程访问),安全性由 token + allowedOrigins 保证;需要时可用 `security.allowedOrigins` 收紧或自行反向代理限制。
52
+ - **丢失唯一设备的恢复**:若唯一设备的浏览器数据被清空(设备 token 丢失),配对流程因无已授权设备可批准而无法完成。恢复步骤:`c0de auth reset` → 重启 `c0de serve` → 打开启动日志打印的带 `?token=` 的 URL 重新注册首台设备。新设备的配对界面检测到服务端**零已授权设备**时会直接展示上述恢复指引(并停止无意义的审批轮询),不再干等一个不可能到来的批准。
53
+ - **CORS/Origin 校验**:仅放行本机回环 origin 与 `security.allowedOrigins` 中显式配置的 origin;WebSocket 升级在服务端独立校验 Origin(浏览器 WS 不受 CORS 约束)。**认证关闭(`authEnabled: false`)时另有跨域写防线**:携带非本地/未配置 Origin 的 POST/PUT/PATCH/DELETE 请求会被 403 拒绝(CORS 回显只拦「读」,恶意网页仍可对本地服务盲发副作用请求;带合法 token 时由 token 防线覆盖)。无 Origin 的请求(curl/CLI/脚本)不受影响。
54
+ - **监听地址**:默认绑定 `127.0.0.1`(仅本机可达,安全默认)。容器/远程访问需显式 `c0de serve --host 0.0.0.0`(或局域网 IP)——启动时会打印**醒目警告**,此时安全性由 token + allowedOrigins 保证;需要时可用 `security.allowedOrigins` 收紧或自行反向代理限制。
47
55
  - **热更新交接**:新实例经环境变量继承 token,请求旧实例 `/handoff` 时须携带匹配的 Bearer token,防止任意本地进程借 handoff 杀掉服务。
48
56
 
49
57
  ## 使用
@@ -52,8 +60,8 @@ pnpm add -g c0de-agent
52
60
  # 在项目目录启动
53
61
  c0de serve
54
62
 
55
- # 指定端口并自动打开浏览器
56
- c0de serve --port 3000 --open
63
+ # 指定端口并自动打开浏览器(默认 7310)
64
+ c0de serve --port 8080 --open
57
65
 
58
66
  # 从快照恢复会话
59
67
  c0de serve --restore snapshot.json
@@ -64,20 +72,33 @@ c0de chat "解释这段代码"
64
72
  # serve 运行时打印模式默认拒绝(对话无法持久化),显式接受临时模式:
65
73
  c0de chat --temp "解释这段代码"
66
74
 
75
+ # 打印模式默认拒绝写工具;--allow 定向放行指定写工具(或用 -y 全量放行)
76
+ c0de chat --allow write,bash "修复这个函数"
77
+
67
78
  # 更新检查
68
79
  c0de update --check
80
+
81
+ # 信任项目目录:启用其项目级配置与 .c0de/plugins(克隆仓库审查后显式执行;
82
+ # 含风险配置时需 --yes——不带 --yes 会先打印风险清单供审查)
83
+ c0de trust /path/to/cloned-repo --yes
69
84
  ```
70
85
 
71
- 启动后访问 `http://localhost:3000`。
86
+ 启动后访问 `http://localhost:7310`。
72
87
 
73
- > **CLI 与 Web 的会话隔离**:`c0de chat` 产生的会话标记为 `cli` 来源,不会出现在
74
- > Web 界面的会话树中(Web 会话树仅展示 web 会话)。CLI 侧 `c0de sessions list` 会列出
75
- > 全部会话(`[cli]`/`[web]` 标注),并可删除/恢复任意会话——这是 Web 会话的 CLI 恢复途径。
88
+ > **CLI 与 Web 的会话互通**:`c0de chat --continue` 续接过的 CLI 会话与 Web 会话在
89
+ > Web 会话树中**同树展示**(带 `CLI` 徽标;所在目录已注册为项目时归入该项目,否则在
90
+ > 列表底部的「CLI 会话(未绑定项目)」分组);一次性问答(未续接)仍是临时会话,
91
+ > 不进入 Web 树,30 天后自动移入回收站(走回收站保留期,可恢复)。`c0de sessions list` 会列出全部会话
92
+ > (`[cli]`/`[web]` 标注),并可删除/恢复/彻底删除任意会话(`c0de sessions purge <id> --yes`
93
+ > 永久删除、`--all --yes` 清空回收站;`deleted` 子命令列出回收站并启动 60 天保留期倒计时,
94
+ > 与 Web 打开回收站同口径;`--project <路径>` 可把「看到」标记与列表限定到单个项目)。
76
95
  > 注意:数据库为单写者,`c0de sessions` / `c0de chat`(非 `--temp`)在 serve 运行期间会报错拒绝
77
- > (CLI 管理需先停止 serve;运行中的服务请用 Web 界面管理)。
96
+ > (CLI 管理需先停止 serve)——报错会直接给出**运行中 serve 的 Web 地址**,打开浏览器即可继续。
97
+ > 已删除(在回收站中)的会话不可被 `--continue` 续接——报错会给出恢复途径
98
+ > (Web 回收站恢复,或停止 serve 后 `c0de sessions restore <id>`)。
78
99
  > 恢复的会话若原项目目录已不存在(孤儿会话),可用 `c0de sessions restore <id> --project <路径>`
79
100
  > 显式归属到项目。两者存于同一数据库。`c0de chat --continue` 续接过的 CLI 会话不会被自动清理;
80
- > 未续接的一次性问答会话 30 天后自动清除。
101
+ > 未续接的一次性问答会话 30 天后自动移入回收站。
81
102
 
82
103
  ## 开发
83
104
 
@@ -85,7 +106,7 @@ c0de update --check
85
106
  # 安装依赖
86
107
  pnpm install
87
108
 
88
- # 启动开发服务器(前端热更新)
109
+ # 启动开发服务器(前端热更新,端口 3020)
89
110
  pnpm dev
90
111
 
91
112
  # 类型检查
@@ -104,11 +125,18 @@ pnpm lint
104
125
  pnpm format
105
126
  ```
106
127
 
128
+ > **开发环境隔离**:`.env.example` 是开发隔离模板(复制为 `.env` 后生效,`.env` 不入 git)。
129
+ > 它把 dev server 的全局配置/工作流/插件(`C0DE_CONFIG_DIR`)与运行时数据 DB/设备认证
130
+ > (`C0DE_DB_DIR`)从安装版默认位置(`~/.c0de`、`~/.local/share/c0de/pglite`)重定向到
131
+ > 仓库内 `.c0de-dev/`(已 gitignore),与 `npm i -g c0de-agent` 安装版互不干扰、
132
+ > 可同时运行。两个变量经 `vite.config.ts` 在 dev server 启动时注入进程环境
133
+ > (vite 8 不再自动把 `.env` 合并进 `process.env`)。
134
+
107
135
  ## 架构
108
136
 
109
137
  ```
110
138
  src/
111
- ├── cli/ # CLI 入口与命令(serve / chat / acp / update / config / init / plugin)
139
+ ├── cli/ # CLI 入口与命令(serve / chat / acp / update / config / init / plugin / sessions / trust)
112
140
  ├── core/ # 配置、agent 注册与主循环(loop/ 子模块)、权限、prompt、工作流、斜杠命令
113
141
  ├── llm/ # LLM provider 抽象、registry 与重试策略
114
142
  ├── server/ # Hono HTTP 服务、SSE 流、路由、鉴权/CORS、交互式权限、终端(node-pty)
@@ -1,13 +1,20 @@
1
1
  import readline from 'node:readline/promises';
2
+ import { getByDirectory } from '../../project/index.js';
2
3
  import { createSession, listAllSessions } from '../../session/session.js';
3
4
  import { formatACPEvent, runAcpLoop } from '../modes/acp.js';
4
5
  import { runPrintMode } from '../modes/print.js';
5
6
  /** 构造 ACP method → handler 映射。chat 复用 Print 模式。 */
6
7
  function createAcpHandlers(config, deps, opts) {
8
+ // P2-4:当前在途 chat run 的中止控制器。abort 请求触发其 abort,
9
+ // runPrintMode 桥接信号后真中止 agent(此前 abort 是空操作)。
10
+ let currentAbort = null;
7
11
  return {
8
12
  'session/create': async (params) => {
9
13
  const title = params.title ?? 'acp-session';
10
- const session = await createSession(deps.db, title, undefined, undefined, 'cli');
14
+ // P1-2 CLI/Web 同树:cwd 已注册为项目时直接绑定;worktreePath 落盘保证
15
+ // Web 打开时工具在原目录执行。ACP 会话无 print 标记,永不自动清理。
16
+ const existing = await getByDirectory(deps.db, deps.cwd);
17
+ const session = await createSession(deps.db, title, existing?.id, undefined, 'cli', undefined, deps.cwd);
11
18
  return { sessionId: session.id };
12
19
  },
13
20
  'session/list': async () => {
@@ -19,14 +26,25 @@ function createAcpHandlers(config, deps, opts) {
19
26
  if (!message)
20
27
  throw new Error('chat: message is required');
21
28
  const sessionId = params.sessionId;
22
- const text = await runPrintMode(config, message, deps, {
23
- ...(sessionId ? { sessionId } : {}),
24
- onEvent: (e) => opts.onEvent('event', e),
25
- });
26
- return { text };
29
+ const controller = new AbortController();
30
+ currentAbort = controller;
31
+ try {
32
+ const text = await runPrintMode(config, message, deps, {
33
+ ...(sessionId ? { sessionId } : {}),
34
+ onEvent: (e) => opts.onEvent('event', e),
35
+ abortSignal: controller.signal,
36
+ });
37
+ return { text };
38
+ }
39
+ finally {
40
+ if (currentAbort === controller)
41
+ currentAbort = null;
42
+ }
43
+ },
44
+ abort: async () => {
45
+ currentAbort?.abort();
46
+ return { ok: true };
27
47
  },
28
- abort: async () => ({ ok: true }),
29
- 'tool/confirm': async () => ({ ok: true }),
30
48
  };
31
49
  }
32
50
  /** 写一行 ACP event 到给定 writer(命令层用于接 stdout)。 */
@@ -6,7 +6,7 @@ import { runPrintMode } from '../modes/print.js';
6
6
  * /compact 需要会话上下文,由消费方在此执行压缩并输出摘要。
7
7
  */
8
8
  async function runSlashCommand(message, continueId, ctx) {
9
- const { createSlashRegistry, parseSlashInput } = await import('../../core/slash.js');
9
+ const { createSlashRegistry, isSlashCommandEnabled, parseSlashInput } = await import('../../core/slash.js');
10
10
  const parsed = parseSlashInput(message);
11
11
  if (!parsed)
12
12
  return false;
@@ -16,15 +16,23 @@ async function runSlashCommand(message, continueId, ctx) {
16
16
  return false; // 未知斜杠命令:回退为正常消息发给 agent(与 Web 端一致)
17
17
  const out = ctx.stdout ?? process.stdout.write.bind(process.stdout);
18
18
  const err = ctx.stderr ?? process.stderr.write.bind(process.stderr);
19
- const enabledList = ctx.config.slashCommands?.enabled ?? [];
20
- const enabledSet = new Set(enabledList.map((n) => (n.startsWith('/') ? n.slice(1) : n)));
21
- if (enabledSet.size > 0 && !enabledSet.has(parsed.name)) {
19
+ if (!isSlashCommandEnabled(ctx.config.slashCommands?.enabled, parsed.name)) {
22
20
  err(`斜杠命令 /${parsed.name} 未启用(config.slashCommands.enabled)\n`);
23
21
  return true;
24
22
  }
25
23
  const { compactContext } = await import('../../core/loop.js');
26
24
  const { createAgent } = await import('../../core/agent.js');
27
25
  const { getSession } = await import('../../session/session.js');
26
+ // /workflow 命令注入三级注册表(内置 + 用户级 + 受信任的项目级):
27
+ // 此前 CLI 缺省回退仅内置注册表,~/.c0de/workflows 用户级工作流在 CLI
28
+ // 完全不可见/不可运行。项目级仅在 cwd 当前可信时 import(fail-closed)。
29
+ let workflowRegistry;
30
+ if (parsed.name === 'workflow') {
31
+ const { createAndPopulateRegistry } = await import('../../core/workflows/index.js');
32
+ const { resolveCwdProjectTrusted } = await import('../deps.js');
33
+ const projectTrusted = await resolveCwdProjectTrusted(ctx.deps.db, ctx.deps.cwd).catch(() => false);
34
+ workflowRegistry = await createAndPopulateRegistry(ctx.deps.cwd, { projectTrusted });
35
+ }
28
36
  const result = await cmd.execute(parsed.args, {
29
37
  cwd: ctx.deps.cwd,
30
38
  config: ctx.config,
@@ -32,6 +40,7 @@ async function runSlashCommand(message, continueId, ctx) {
32
40
  sessionId: continueId,
33
41
  // 消费渠道:/model 等命令按渠道给指引(CLI 无底部模型选择器)。
34
42
  channel: 'cli',
43
+ ...(workflowRegistry ? { workflowRegistry } : {}),
35
44
  });
36
45
  if (result._tag === 'compact') {
37
46
  // /compact:消费方执行压缩(Web 端由 chat 路由消费)。需要会话上下文。
@@ -89,6 +98,60 @@ async function runChatCommand(ctx) {
89
98
  // 斜杠命令拦截:命中则执行本地语义,不把命令文本发给 LLM。
90
99
  if (await runSlashCommand(message, continueId, ctx))
91
100
  return;
101
+ // P1-4:CLI 无可恢复的「暂停」交互,budgetAction='pause'/'abort' 时在单次询问前拦截——
102
+ // 当月金额/token 预算超支则明确拒绝并给出操作路径,而非静默放行继续烧钱。
103
+ // 与 Web 端暂停语义对齐(Web 暂停可恢复、CLI 直接拒绝本次执行)。
104
+ if (ctx.config.usage?.budgetAction === 'pause' ||
105
+ ctx.config.usage?.budgetAction === 'abort' ||
106
+ ctx.config.usage?.tokenBudgetAction === 'pause' ||
107
+ ctx.config.usage?.tokenBudgetAction === 'abort') {
108
+ const { budgetOverageParts } = await import('../../session/usage.js');
109
+ // 项目预算仅在 cwd 能解析到项目时检查;未归属目录的调用只受全局预算兜底。
110
+ const { getByDirectory } = await import('../../project/index.js');
111
+ let projectId = null;
112
+ try {
113
+ projectId = (await getByDirectory(ctx.deps.db, ctx.deps.cwd))?.id ?? null;
114
+ }
115
+ catch {
116
+ projectId = null;
117
+ }
118
+ const over = await budgetOverageParts(ctx.deps.db, ctx.config.usage, projectId);
119
+ if (over.parts.length > 0) {
120
+ throw new Error(`chat: 月度预算已超支(${over.parts.join(';')})。` +
121
+ 'CLI 无法暂停对话,已拒绝本次执行。请提升预算、改用更便宜的模型,或经 Web 界面调整后再试。');
122
+ }
123
+ if (over.warnings.length > 0) {
124
+ // P2-2:未超支但金额口径不可靠(价格未知调用无 token 兜底)——不拒绝,
125
+ // 但显式告知用户预算护栏可能低估,保持「静默失效」零容忍。
126
+ console.warn(`chat: 预算金额口径可能低估——${over.warnings.join(';')}`);
127
+ }
128
+ }
129
+ // P1-1:显式打印「权限水位」,消除 Web↔CLI 的静默授权差异——未经确认的写工具在
130
+ // 非交互下会被拒绝(default)或全自动放行(auto/-y),用户须在入口即知情。
131
+ const yes = ctx.args.options.yes === true;
132
+ const allowRaw = ctx.args.options.allow ? String(ctx.args.options.allow) : undefined;
133
+ const allowList = allowRaw
134
+ ? allowRaw
135
+ .split(',')
136
+ .map((s) => s.trim())
137
+ .filter(Boolean)
138
+ : [];
139
+ const defaultMode = ctx.config.permission?.defaultMode ?? 'default';
140
+ let waterLevel;
141
+ if (yes) {
142
+ waterLevel = '全部工具自动放行(-y)';
143
+ }
144
+ else if (defaultMode === 'auto') {
145
+ waterLevel = '全部工具自动放行(permission.defaultMode=auto)';
146
+ }
147
+ else if (allowList.length > 0) {
148
+ waterLevel = `只读自动放行,写/执行工具定向放行:${allowList.join(', ')}(其余写工具将被拒绝)`;
149
+ }
150
+ else {
151
+ waterLevel =
152
+ '只读工具自动放行,写/执行工具将被拒绝(非交互)。加 -y 全量放行或 --allow <工具> 定向放行';
153
+ }
154
+ err(`[c0de] 权限水位:${waterLevel}\n`);
92
155
  const text = await runPrintMode(ctx.config, message, ctx.deps, {
93
156
  ...(model ? { model } : {}),
94
157
  ...(continueId ? { sessionId: continueId } : {}),
@@ -113,10 +176,11 @@ async function runChatCommand(ctx) {
113
176
  else {
114
177
  out(`${text}\n`);
115
178
  }
116
- // P2-5:一次性 CLI 问答不进 Web 会话树,明确告知查看/续接途径,
179
+ // P2-5:一次性 CLI 问答(未续接)不进 Web 会话树,明确告知查看/续接途径,
117
180
  // 避免「我的会话去哪了」(--continue 续接时用户已知会话存在,不提示)。
118
181
  if (!continueId) {
119
- err('此问答会话不会出现在 Web 界面;可用 `c0de sessions list` 查看,或 `c0de chat --continue` 续接。\n');
182
+ err('此问答会话为一次性会话,暂不在 Web 会话树中;`c0de chat --continue <id>` 续接后即可在 Web 界面查看。\n' +
183
+ '可用 `c0de sessions list` 列出全部会话。\n');
120
184
  }
121
185
  }
122
186
  export { runChatCommand };
@@ -4,6 +4,8 @@ type ConfigCommandContext = {
4
4
  args: CommandArgs;
5
5
  cwd: string;
6
6
  write?: (s: string) => void;
7
+ /** 测试注入:覆盖指纹刷新实现(缺省走真实持久库路径)。 */
8
+ refreshTrust?: (cwd: string) => Promise<void>;
7
9
  };
8
10
  declare function runConfigCommand(ctx: ConfigCommandContext): Promise<void>;
9
11
  export type { ConfigCommandContext };
@@ -1,5 +1,40 @@
1
1
  import { applyScopedPatch, loadConfigScopes, mergeConfig, saveConfigScoped, } from '../../core/config.js';
2
2
  import { coerce, getByPath, setPathPatch } from '../../core/config-path.js';
3
+ /**
4
+ * P1:config set 写入项目作用域后刷新信任指纹(serve 未运行时生效)。
5
+ * config 命令无 agent deps 生命周期,此处独立打开持久库;serve 运行期间
6
+ * 持久库被占用(单写者)——跳过刷新,配置已落盘,信任复检的 fail-closed
7
+ * 路径兜底(下次聊天一次性确认,风险项即本次修改)。
8
+ */
9
+ async function refreshTrustFingerprint(cwd) {
10
+ try {
11
+ const { mkdirSync } = await import('node:fs');
12
+ const { createDB, migrateDB } = await import('../../db/index.js');
13
+ const { acquireDevDbLock, releaseDevDbLock, resolveDbDir } = await import('../../server/server.js');
14
+ const { getByDirectory, trustProject } = await import('../../project/index.js');
15
+ const dataDir = resolveDbDir();
16
+ mkdirSync(dataDir, { recursive: true });
17
+ acquireDevDbLock(dataDir);
18
+ try {
19
+ const db = await createDB({ driver: 'pglite', dataDir });
20
+ try {
21
+ await migrateDB(db);
22
+ const p = await getByDirectory(db, cwd);
23
+ if (p?.trustedAt != null)
24
+ await trustProject(db, p.id);
25
+ }
26
+ finally {
27
+ await db.close();
28
+ }
29
+ }
30
+ finally {
31
+ releaseDevDbLock(dataDir);
32
+ }
33
+ }
34
+ catch {
35
+ // serve 锁冲突/库错误等:跳过刷新,不阻塞配置写入结果
36
+ }
37
+ }
3
38
  async function runConfigCommand(ctx) {
4
39
  const write = ctx.write ?? process.stdout.write.bind(process.stdout);
5
40
  const sub = ctx.args.positionals[0];
@@ -38,12 +73,23 @@ async function runConfigCommand(ctx) {
38
73
  throw new Error(`config set: 未知的顶层配置键 "${topKey}"。有效键:${[...KNOWN_CONFIG_KEYS].join(', ')}`);
39
74
  }
40
75
  const scope = ctx.args.options.global ? 'global' : 'project';
76
+ // 作用域收敛:security 是服务端全局参数,项目作用域写入会被加载时剥离(不生效)。
77
+ // 与其写入一个永不生效的键,不如直接拒绝并给出正解路径(与 Web PATCH /api/config 同语义)。
78
+ if (scope === 'project' && topKey === 'security') {
79
+ throw new Error('config set: security 是服务端全局参数,仅在全局作用域生效。请加 --global 写入全局配置。');
80
+ }
41
81
  // 只把 patch 合并进目标作用域的原始文件,不写入默认值与其他作用域的配置(P2-3)。
42
82
  // 值为 null → 删除该键(作用域内取消覆盖,回落另一作用域/默认值)。
43
83
  const scopeCfg = scope === 'global' ? scopes.global : scopes.project;
44
84
  const value = coerce(rawArg);
45
85
  const next = applyScopedPatch(scopeCfg ?? {}, setPathPatch(key, value));
46
86
  await saveConfigScoped(scope, ctx.cwd, next);
87
+ // P1:项目作用域写入是用户显式操作——已信任项目刷新风险指纹(与 Web
88
+ // PATCH /api/config、/config 斜杠命令同口径),防「信任 → 改配置 → 再信任」
89
+ // 自锁复检循环。
90
+ if (scope === 'project') {
91
+ await (ctx.refreshTrust ?? refreshTrustFingerprint)(ctx.cwd);
92
+ }
47
93
  write(`${value === null ? '已取消设置' : '已设置'} ${key} (scope: ${scope})\n`);
48
94
  return;
49
95
  }
@@ -1,13 +1,15 @@
1
- import { startServer } from '../../server/index.js';
1
+ import { DEFAULT_SERVE_PORT, startServer } from '../../server/index.js';
2
2
  import { openBrowser, printStartupBanner } from '../utils/output.js';
3
3
  async function runServeCommand(ctx) {
4
- const port = ctx.args.options.port ?? 3000;
4
+ const port = ctx.args.options.port ?? DEFAULT_SERVE_PORT;
5
+ const host = ctx.args.options.host ?? '127.0.0.1';
5
6
  const shouldOpen = ctx.args.options.open ?? true;
6
7
  const starter = ctx.serverStarter ?? startServer;
7
8
  const restore = ctx.args.options.restore;
8
9
  const handoffPort = ctx.args.options['handoff-port'];
9
10
  const handle = await starter({
10
11
  port,
12
+ host,
11
13
  cwd: ctx.cwd,
12
14
  ...(restore ? { restoreFrom: restore } : {}),
13
15
  ...(handoffPort ? { handoffPort } : {}),
@@ -17,6 +19,13 @@ async function runServeCommand(ctx) {
17
19
  const tokenQuery = handle.authToken ? `?token=${encodeURIComponent(handle.authToken)}` : '';
18
20
  const url = `http://localhost:${handle.port}${tokenQuery}`;
19
21
  (ctx.banner ?? printStartupBanner)(url);
22
+ // 非回环监听警告:绑定 0.0.0.0/局域网 IP 时本机以外的主机均可访问,
23
+ // 安全性完全依赖 token + allowedOrigins——默认行为已收紧为 127.0.0.1。
24
+ const isLoopback = host === '127.0.0.1' || host === 'localhost' || host === '::1' || host.startsWith('127.');
25
+ if (!isLoopback) {
26
+ console.warn(`[c0de] ⚠ 监听地址为 ${host}(非回环):本机以外的主机均可访问此服务。\n` +
27
+ ` 请确认 security.authEnabled 已开启、token 未泄漏;仅在可信网络使用,或在防火墙限制访问。`);
28
+ }
20
29
  if (shouldOpen) {
21
30
  const opener = ctx.opener ?? ((u) => openBrowser(u));
22
31
  await opener(url);
@@ -7,7 +7,7 @@
7
7
  // P1 闭环修复:delete 移入回收站后必须有 CLI 恢复途径(此前文案承诺
8
8
  // 「60 天内可恢复」但 CLI 无 restore 子命令,恢复只能绕道 Web 界面)。
9
9
  import { fromDirectory } from '../../project/project.js';
10
- import { getSession, listAllSessions, listDeletedSessions, rebindSession, restoreSession, softDeleteSession, } from '../../session/session.js';
10
+ import { emptyTrash, getSession, listAllSessions, listDeletedSessions, permanentlyDeleteSession, rebindSession, restoreSession, softDeleteSession, touchTrashSeen, } from '../../session/session.js';
11
11
  async function runSessionsCommand(ctx) {
12
12
  const write = ctx.write ?? ((s) => process.stdout.write(s));
13
13
  const sub = ctx.args.positionals[0] ?? 'list';
@@ -76,19 +76,66 @@ async function runSessionsCommand(ctx) {
76
76
  write(`已恢复会话 ${id}。\n`);
77
77
  return;
78
78
  }
79
- // 回收站列表:软删除会话 + 剩余保留信息(辅助 restore)
79
+ // 回收站列表:软删除会话 + 剩余保留信息(辅助 restore)。
80
+ // P2:列出即「看到」——CLI-only 用户此前无 Web 回收站可打开,trashSeenAt 永不写入,
81
+ // 60 天保留期永不启动(条目只能等 365 天绝对上限)。此处与 Web 打开分组同口径
82
+ // 标记(仅首次、全量、含孤儿)。
83
+ // --project <path>:把「看到」标记与列表限定到单个项目(与 Web 按项目分组一致)——
84
+ // 缺省全库标记会启动所有项目回收站条目的倒计时,多项目用户应显式限定作用域。
80
85
  if (sub === 'deleted') {
81
- const sessions = await listDeletedSessions(ctx.db);
86
+ const projectPath = ctx.args.options.project;
87
+ let projectId;
88
+ if (projectPath) {
89
+ const { existsSync } = await import('node:fs');
90
+ if (!existsSync(projectPath)) {
91
+ throw new Error(`sessions deleted: 项目目录不存在:${projectPath}`);
92
+ }
93
+ projectId = (await fromDirectory(ctx.db, projectPath)).id;
94
+ }
95
+ await touchTrashSeen(ctx.db, projectId ? { projectId } : {});
96
+ const sessions = await listDeletedSessions(ctx.db, projectId);
82
97
  if (sessions.length === 0) {
83
98
  write('回收站为空。\n');
84
99
  return;
85
100
  }
101
+ if (!projectId) {
102
+ write('(全库回收站。多项目用户建议加 --project <路径> 限定作用域:全库「看到」标记会启动所有项目条目的保留期倒计时。)\n');
103
+ }
86
104
  for (const s of sessions) {
87
105
  const when = s.deletedAt ? new Date(s.deletedAt).toISOString() : '-';
88
106
  write(`- ${s.title} (id: ${s.id}, deleted ${when})\n`);
89
107
  }
90
108
  return;
91
109
  }
92
- throw new Error(`sessions: unknown subcommand "${sub}" (expected list|delete|restore|deleted)`);
110
+ // 彻底删除(不可恢复):purge <id> 单条;purge --all 清空回收站。
111
+ // P2:CLI 回收站闭环——此前 CLI 无永久删除途径,条目只能被动等 purge 任务。
112
+ // 永久操作需 --yes(与 Web 键入确认的「彻底删除/清空回收站」同强度)。
113
+ if (sub === 'purge') {
114
+ const all = ctx.args.options.all === true;
115
+ const yes = ctx.args.options.yes === true;
116
+ const id = ctx.args.positionals[1];
117
+ if (!all && !id) {
118
+ throw new Error('sessions purge: 需要会话 id(purge <id>)或 --all(清空回收站),两者均不可恢复,需 --yes 确认');
119
+ }
120
+ if (!yes) {
121
+ throw new Error(all
122
+ ? 'sessions purge --all 将永久删除回收站全部会话,不可恢复。确认执行请加 --yes。'
123
+ : `sessions purge ${id} 将永久删除该会话及其派生分支,不可恢复。确认执行请加 --yes。`);
124
+ }
125
+ const deleted = all
126
+ ? await emptyTrash(ctx.db)
127
+ : await permanentlyDeleteSession(ctx.db, id ?? '');
128
+ if (all) {
129
+ write(`已清空回收站(永久删除 ${deleted} 个会话)。\n`);
130
+ }
131
+ else if (deleted > 0) {
132
+ write(`已永久删除会话 ${id}(含 ${deleted - 1} 个派生会话)。\n`);
133
+ }
134
+ else {
135
+ throw new Error(`sessions purge: 会话不在回收站或不存在:${id}`);
136
+ }
137
+ return;
138
+ }
139
+ throw new Error(`sessions: unknown subcommand "${sub}" (expected list|delete|restore|deleted|purge)`);
93
140
  }
94
141
  export { runSessionsCommand };
@@ -0,0 +1,11 @@
1
+ import type { DB } from '../../db/client.js';
2
+ import type { CommandArgs } from '../parser.js';
3
+ type TrustCommandContext = {
4
+ args: CommandArgs;
5
+ db: DB;
6
+ cwd: string;
7
+ write?: (s: string) => void;
8
+ };
9
+ declare function runTrustCommand(ctx: TrustCommandContext): Promise<void>;
10
+ export type { TrustCommandContext };
11
+ export { runTrustCommand };
@@ -0,0 +1,31 @@
1
+ // src/cli/commands/trust.ts — 显式信任项目(P0-2 信任边界)。
2
+ //
3
+ // c0de trust [目录] [--yes]:把目录解析/注册为项目并落盘 trustedAt。
4
+ // 未信任项目携带风险配置时 Web 聊天入口被 409 门禁拦截;项目 .c0de/plugins
5
+ // 在未信任时不加载。本命令是无 Web 场景(headless/CI 前手工审查过仓库)的信任途径。
6
+ // P1:信任前展示即将接受的风险清单(项目风险 + 全局权限风险上下文),有风险时
7
+ // 必须 --yes 显式确认——Web 信任弹窗逐项明示,CLI 不得盲信任。
8
+ // 与 sessions 命令同约束:需要持久库,serve 运行期间报错引导走 Web 界面。
9
+ import { loadConfigScopes } from '../../core/config.js';
10
+ import { fromDirectory, trustProject } from '../../project/project.js';
11
+ import { enrichProjectRiskWithGlobal, summarizeProjectRisk } from '../../project/trust.js';
12
+ async function runTrustCommand(ctx) {
13
+ const write = ctx.write ?? ((s) => process.stdout.write(s));
14
+ const dir = ctx.args.positionals[0] ?? ctx.cwd;
15
+ const yes = ctx.args.options.yes === true;
16
+ // 信任前明示即将接受的风险(与 Web 信任弹窗同口径:项目风险 + 全局权限风险上下文)。
17
+ // dir 传入使 summarizeProjectRisk 检测 .c0de/workflows(纯文件系统风险面)。
18
+ const scopes = loadConfigScopes(dir);
19
+ const risks = enrichProjectRiskWithGlobal(summarizeProjectRisk(scopes.project, dir), scopes.global);
20
+ if (risks.length > 0 && !yes) {
21
+ const lines = risks.map((r) => ` - ${r.kind}: ${r.detail}`).join('\n');
22
+ throw new Error(`信任「${dir}」将接受以下风险项:\n${lines}\n` +
23
+ `确认已审查无误后执行 c0de trust ${dir !== ctx.cwd ? `${dir} ` : ''}--yes 显式信任。`);
24
+ }
25
+ const project = await fromDirectory(ctx.db, dir);
26
+ await trustProject(ctx.db, project.id);
27
+ const accepted = risks.length > 0 ? `(已确认 ${risks.length} 项风险)` : '(无风险项)';
28
+ write(`已信任项目「${project.name ?? project.worktree}」(${project.worktree})${accepted}。\n` +
29
+ `该项目作用域配置(.c0de/config.json)生效,项目插件与工作流在下次启动/刷新后加载。\n`);
30
+ }
31
+ export { runTrustCommand };
@@ -9,6 +9,10 @@ async function runUpdateCommand(ctx) {
9
9
  const out = ctx.out ?? ((s) => process.stdout.write(`${s}\n`));
10
10
  const check = ctx.checkFn ?? checkForUpdate;
11
11
  const result = await check();
12
+ if (result.checkError) {
13
+ out('检查更新失败:无法连接 npm registry,请检查网络后重试');
14
+ return;
15
+ }
12
16
  out(`当前版本 ${result.currentVersion},最新版本 ${result.latestVersion}`);
13
17
  if (!result.hasUpdate) {
14
18
  out('已是最新版本');
@@ -9,11 +9,7 @@ import type { PermissionChecker } from '../tools/types.js';
9
9
  type PermissionStrategy = 'full-auto' | 'safe';
10
10
  /** 真正非交互模式(print/acp):所有工具无条件放行(命令由用户显式触发)。 */
11
11
  declare const fullyAutoApproveChecker: PermissionChecker;
12
- /**
13
- * 非交互模式(print)安全策略(P1-6):ask 工具**直接拒绝**并给出可操作提示
14
- * (-y 或 serve),而非返回 permission_required 让 LLM 在无确认通道下无限重试。
15
- * 拒绝原因作为 tool result 交给模型,模型会向用户转述。
16
- */
12
+ /** 无白名单的非交互安全策略(保留导出兼容既有调用方)。 */
17
13
  declare const nonInteractiveSafeChecker: PermissionChecker;
18
14
  /** 把 config.providers 注册到新建的 LLM registry。 */
19
15
  declare function buildLLMRegistry(config: Config): Registry;
@@ -23,10 +19,17 @@ type BuildDepsOptions = {
23
19
  /** 权限策略:'full-auto' 全部放行;'safe' 只读放行、写操作 ask。不传时按
24
20
  * config.permission.defaultMode 决定('auto'→full-auto,'default'→safe)。 */
25
21
  permissionStrategy?: PermissionStrategy;
22
+ /** --allow 白名单(safe 模式下 ask 工具定向放行)。 */
23
+ allowTools?: string[];
26
24
  /** 测试注入 mock chatStream。 */
27
25
  chatStream?: LoopDeps['chatStream'];
28
26
  };
27
+ /**
28
+ * cwd 是否当前可信项目(trustedAt 非空且指纹未漂移)。与聊天门禁/插件加载同口径;
29
+ * CLI 工作流注册表(内置+用户级+项目级)的构建也用它决定是否 import 项目级文件。
30
+ */
31
+ declare function resolveCwdProjectTrusted(db: DB, cwd: string): Promise<boolean>;
29
32
  /** 组装完整 LoopDeps(默认 safe 放行 + 默认工具注册表)。 */
30
33
  declare function buildAgentDeps(config: Config, opts: BuildDepsOptions): Promise<LoopDeps>;
31
34
  export type { BuildDepsOptions, PermissionStrategy };
32
- export { buildAgentDeps, buildLLMRegistry, fullyAutoApproveChecker, nonInteractiveSafeChecker };
35
+ export { buildAgentDeps, buildLLMRegistry, fullyAutoApproveChecker, nonInteractiveSafeChecker, resolveCwdProjectTrusted, };