c0de-agent 1.11.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 (177) hide show
  1. package/README.md +63 -16
  2. package/dist/cli/commands/acp.js +26 -8
  3. package/dist/cli/commands/chat.js +151 -1
  4. package/dist/cli/commands/config.d.ts +2 -0
  5. package/dist/cli/commands/config.js +63 -3
  6. package/dist/cli/commands/serve.js +11 -2
  7. package/dist/cli/commands/sessions.js +109 -3
  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 +110 -11
  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 +78 -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/compact.d.ts +2 -0
  24. package/dist/core/compact.js +1 -0
  25. package/dist/core/config.d.ts +53 -2
  26. package/dist/core/config.js +235 -19
  27. package/dist/core/loop/compaction.d.ts +2 -1
  28. package/dist/core/loop/compaction.js +19 -6
  29. package/dist/core/loop/segment.d.ts +3 -1
  30. package/dist/core/loop/segment.js +24 -1
  31. package/dist/core/loop/stream-collect.js +9 -1
  32. package/dist/core/loop/subagent.js +88 -37
  33. package/dist/core/loop.d.ts +7 -0
  34. package/dist/core/loop.js +90 -6
  35. package/dist/core/redact.d.ts +11 -0
  36. package/dist/core/redact.js +49 -0
  37. package/dist/core/slash.d.ts +10 -0
  38. package/dist/core/slash.js +237 -65
  39. package/dist/core/title.js +7 -1
  40. package/dist/core/types.d.ts +22 -1
  41. package/dist/core/workflows/context.js +44 -4
  42. package/dist/core/workflows/discovery.d.ts +2 -2
  43. package/dist/core/workflows/discovery.js +5 -7
  44. package/dist/core/workflows/index.d.ts +2 -1
  45. package/dist/core/workflows/index.js +2 -1
  46. package/dist/core/workflows/registry.d.ts +13 -3
  47. package/dist/core/workflows/registry.js +14 -9
  48. package/dist/core/workflows/resolve.d.ts +22 -0
  49. package/dist/core/workflows/resolve.js +23 -0
  50. package/dist/core/workflows/runtime.d.ts +6 -1
  51. package/dist/core/workflows/runtime.js +19 -2
  52. package/dist/db/schema.d.ts +373 -1
  53. package/dist/db/schema.js +60 -4
  54. package/dist/kanban/index.d.ts +2 -1
  55. package/dist/kanban/index.js +1 -1
  56. package/dist/kanban/store.d.ts +87 -1
  57. package/dist/kanban/store.js +429 -22
  58. package/dist/llm/index.d.ts +1 -1
  59. package/dist/llm/index.js +1 -1
  60. package/dist/llm/provider.js +1 -1
  61. package/dist/llm/registry.d.ts +7 -1
  62. package/dist/llm/registry.js +7 -1
  63. package/dist/llm/retry.d.ts +5 -2
  64. package/dist/llm/retry.js +6 -5
  65. package/dist/llm/routing.d.ts +24 -1
  66. package/dist/llm/routing.js +29 -1
  67. package/dist/plugins/init.d.ts +7 -0
  68. package/dist/plugins/init.js +22 -1
  69. package/dist/plugins/loader.d.ts +7 -1
  70. package/dist/plugins/loader.js +12 -4
  71. package/dist/project/index.d.ts +1 -1
  72. package/dist/project/index.js +1 -1
  73. package/dist/project/project.d.ts +37 -0
  74. package/dist/project/project.js +142 -1
  75. package/dist/project/trust.d.ts +65 -0
  76. package/dist/project/trust.js +328 -0
  77. package/dist/server/agent-manager.d.ts +15 -2
  78. package/dist/server/agent-manager.js +18 -3
  79. package/dist/server/app.js +45 -30
  80. package/dist/server/auth-manager.d.ts +26 -9
  81. package/dist/server/auth-manager.js +78 -42
  82. package/dist/server/context.js +14 -1
  83. package/dist/server/index.d.ts +1 -1
  84. package/dist/server/index.js +1 -1
  85. package/dist/server/middleware/auth.d.ts +11 -1
  86. package/dist/server/middleware/auth.js +25 -3
  87. package/dist/server/permission/interactive.d.ts +11 -3
  88. package/dist/server/permission/interactive.js +23 -4
  89. package/dist/server/permission/store.d.ts +14 -2
  90. package/dist/server/permission/store.js +39 -3
  91. package/dist/server/registry-config.d.ts +14 -0
  92. package/dist/server/registry-config.js +44 -0
  93. package/dist/server/routes/auth.js +66 -10
  94. package/dist/server/routes/chat.d.ts +21 -0
  95. package/dist/server/routes/chat.js +685 -52
  96. package/dist/server/routes/commands.d.ts +4 -2
  97. package/dist/server/routes/commands.js +9 -4
  98. package/dist/server/routes/config.js +180 -23
  99. package/dist/server/routes/files.js +142 -28
  100. package/dist/server/routes/kanban.js +218 -26
  101. package/dist/server/routes/permissions.d.ts +0 -4
  102. package/dist/server/routes/permissions.js +140 -7
  103. package/dist/server/routes/project.js +132 -29
  104. package/dist/server/routes/provider.js +54 -3
  105. package/dist/server/routes/session.js +463 -23
  106. package/dist/server/routes/tool.js +24 -5
  107. package/dist/server/routes/update.d.ts +11 -5
  108. package/dist/server/routes/update.js +145 -29
  109. package/dist/server/routes/usage.d.ts +4 -0
  110. package/dist/server/routes/usage.js +133 -0
  111. package/dist/server/routes/workflows.js +476 -87
  112. package/dist/server/server.d.ts +16 -13
  113. package/dist/server/server.js +364 -107
  114. package/dist/server/terminal/pty-manager.d.ts +12 -0
  115. package/dist/server/terminal/pty-manager.js +63 -7
  116. package/dist/server/types.d.ts +16 -0
  117. package/dist/session/archive.d.ts +3 -1
  118. package/dist/session/archive.js +13 -1
  119. package/dist/session/branch.d.ts +2 -1
  120. package/dist/session/branch.js +92 -9
  121. package/dist/session/context.js +28 -2
  122. package/dist/session/import.d.ts +44 -0
  123. package/dist/session/import.js +83 -0
  124. package/dist/session/index.d.ts +1 -1
  125. package/dist/session/index.js +1 -1
  126. package/dist/session/jobs.d.ts +12 -0
  127. package/dist/session/jobs.js +53 -0
  128. package/dist/session/message.d.ts +7 -1
  129. package/dist/session/message.js +21 -1
  130. package/dist/session/session.d.ts +186 -12
  131. package/dist/session/session.js +577 -33
  132. package/dist/session/snapshot.d.ts +5 -2
  133. package/dist/session/snapshot.js +10 -2
  134. package/dist/session/types.d.ts +13 -1
  135. package/dist/session/usage.d.ts +62 -0
  136. package/dist/session/usage.js +194 -0
  137. package/dist/shared/types/agent.d.ts +29 -2
  138. package/dist/shared/types/config.d.ts +64 -5
  139. package/dist/shared/types/kanban.d.ts +17 -2
  140. package/dist/shared/types/message.d.ts +23 -0
  141. package/dist/tools/builtin/kanban.js +13 -2
  142. package/dist/tools/index.d.ts +14 -3
  143. package/dist/tools/index.js +23 -5
  144. package/dist/tools/websearch/index.d.ts +5 -1
  145. package/dist/tools/websearch/index.js +46 -7
  146. package/dist/tools/websearch/providers/brave.js +2 -2
  147. package/dist/tools/websearch/providers/duckduckgo.js +2 -1
  148. package/dist/tools/websearch/providers/tavily.js +2 -2
  149. package/dist/tools/websearch/types.d.ts +5 -1
  150. package/dist/tools/websearch/types.js +7 -1
  151. package/dist/update/index.d.ts +3 -3
  152. package/dist/update/index.js +2 -2
  153. package/dist/update/ipc.d.ts +13 -2
  154. package/dist/update/ipc.js +36 -2
  155. package/dist/update/scheduler.js +5 -2
  156. package/dist/update/snapshot.d.ts +18 -3
  157. package/dist/update/snapshot.js +5 -2
  158. package/dist/update/updater.d.ts +17 -1
  159. package/dist/update/updater.js +47 -18
  160. package/dist/update/version.d.ts +3 -0
  161. package/dist/update/version.js +9 -3
  162. package/drizzle/0008_tranquil_lily_hollister.sql +1 -0
  163. package/drizzle/0010_usage_events_ledger.sql +17 -0
  164. package/drizzle/0011_slow_slapstick.sql +12 -0
  165. package/drizzle/0012_chief_timeslip.sql +1 -0
  166. package/drizzle/0013_swift_zeigeist.sql +1 -0
  167. package/drizzle/0014_flimsy_legion.sql +1 -0
  168. package/drizzle/meta/0008_snapshot.json +875 -0
  169. package/drizzle/meta/0010_snapshot.json +1010 -0
  170. package/drizzle/meta/0011_snapshot.json +1055 -0
  171. package/drizzle/meta/0012_snapshot.json +1062 -0
  172. package/drizzle/meta/0013_snapshot.json +1068 -0
  173. package/drizzle/meta/0014_snapshot.json +1075 -0
  174. package/drizzle/meta/_journal.json +42 -0
  175. package/package.json +42 -42
  176. package/dist/core/agents/discovery.d.ts +0 -6
  177. package/dist/core/agents/discovery.js +0 -96
@@ -3,7 +3,11 @@
3
3
  // c0de chat 每次运行都创建 source='cli' 会话且被 Web 会话树排除,
4
4
  // 此前无任何 CLI 途径列出/清理,垃圾数据只增不减。
5
5
  // 复用 withAgentDeps 的持久库生命周期(serve 占用时退化为内存库则明确报错)。
6
- import { listAllSessions, softDeleteSession } from '../../session/session.js';
6
+ //
7
+ // P1 闭环修复:delete 移入回收站后必须有 CLI 恢复途径(此前文案承诺
8
+ // 「60 天内可恢复」但 CLI 无 restore 子命令,恢复只能绕道 Web 界面)。
9
+ import { fromDirectory } from '../../project/project.js';
10
+ import { emptyTrash, getSession, listAllSessions, listDeletedSessions, permanentlyDeleteSession, rebindSession, restoreSession, softDeleteSession, touchTrashSeen, } from '../../session/session.js';
7
11
  async function runSessionsCommand(ctx) {
8
12
  const write = ctx.write ?? ((s) => process.stdout.write(s));
9
13
  const sub = ctx.args.positionals[0] ?? 'list';
@@ -27,9 +31,111 @@ async function runSessionsCommand(ctx) {
27
31
  const ok = await softDeleteSession(ctx.db, id);
28
32
  if (!ok)
29
33
  throw new Error(`sessions delete: session not found or already deleted: ${id}`);
30
- write(`已删除会话 ${id}(移入回收站,30 天内可恢复)。\n`);
34
+ write(`已删除会话 ${id}(移入回收站,60 天内可用 \`c0de sessions restore ${id}\` 恢复)。\n`);
35
+ return;
36
+ }
37
+ if (sub === 'restore') {
38
+ const id = ctx.args.positionals[1];
39
+ if (!id)
40
+ throw new Error('sessions restore: a session id is required (use `c0de sessions list`)');
41
+ const ok = await restoreSession(ctx.db, id);
42
+ if (!ok)
43
+ throw new Error(`sessions restore: session not found or not deleted: ${id}`);
44
+ // P2-2:与 Web restore 对齐——会话项目已删除(FK set null)时按 worktreePath
45
+ // 重建归属,避免恢复成功但在 Web 各项目视图不可达。
46
+ // P2 修复:--project <path> 提供显式归属出口(Web 有「归属到当前项目」,
47
+ // CLI 此前只有警告、用户自己修不了)。
48
+ const projectPath = ctx.args.options.project;
49
+ const session = await getSession(ctx.db, id);
50
+ if (session && !session.projectId && (session.worktreePath || projectPath)) {
51
+ try {
52
+ const { existsSync } = await import('node:fs');
53
+ const target = projectPath ?? session.worktreePath;
54
+ if (target && existsSync(target)) {
55
+ const project = await fromDirectory(ctx.db, target);
56
+ await rebindSession(ctx.db, id, project);
57
+ write(`已恢复会话 ${id}(已重新归属到项目 ${project.id})。\n`);
58
+ return;
59
+ }
60
+ if (projectPath) {
61
+ throw new Error(`项目目录不存在:${projectPath}`);
62
+ }
63
+ write(`已恢复会话 ${id}(警告:原项目目录不存在,会话未归属任何项目,Web 不可见)。\n`);
64
+ write(`提示:c0de sessions restore ${id} --project <项目路径> 可显式归属。\n`);
65
+ return;
66
+ }
67
+ catch (error) {
68
+ write(`已恢复会话 ${id}(警告:项目归属失败——${error instanceof Error ? error.message : String(error)},会话可能不可见)。\n`);
69
+ return;
70
+ }
71
+ }
72
+ if (session?.projectId && projectPath) {
73
+ write(`已恢复会话 ${id}(会话已有项目归属,忽略 --project)。\n`);
74
+ return;
75
+ }
76
+ write(`已恢复会话 ${id}。\n`);
77
+ return;
78
+ }
79
+ // 回收站列表:软删除会话 + 剩余保留信息(辅助 restore)。
80
+ // P2:列出即「看到」——CLI-only 用户此前无 Web 回收站可打开,trashSeenAt 永不写入,
81
+ // 60 天保留期永不启动(条目只能等 365 天绝对上限)。此处与 Web 打开分组同口径
82
+ // 标记(仅首次、全量、含孤儿)。
83
+ // --project <path>:把「看到」标记与列表限定到单个项目(与 Web 按项目分组一致)——
84
+ // 缺省全库标记会启动所有项目回收站条目的倒计时,多项目用户应显式限定作用域。
85
+ if (sub === 'deleted') {
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);
97
+ if (sessions.length === 0) {
98
+ write('回收站为空。\n');
99
+ return;
100
+ }
101
+ if (!projectId) {
102
+ write('(全库回收站。多项目用户建议加 --project <路径> 限定作用域:全库「看到」标记会启动所有项目条目的保留期倒计时。)\n');
103
+ }
104
+ for (const s of sessions) {
105
+ const when = s.deletedAt ? new Date(s.deletedAt).toISOString() : '-';
106
+ write(`- ${s.title} (id: ${s.id}, deleted ${when})\n`);
107
+ }
108
+ return;
109
+ }
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
+ }
31
137
  return;
32
138
  }
33
- throw new Error(`sessions: unknown subcommand "${sub}" (expected list|delete)`);
139
+ throw new Error(`sessions: unknown subcommand "${sub}" (expected list|delete|restore|deleted|purge)`);
34
140
  }
35
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, };
package/dist/cli/deps.js CHANGED
@@ -1,6 +1,9 @@
1
+ import { loadConfigScopes } from '../core/config.js';
1
2
  import { decryptSecret } from '../core/secret.js';
2
3
  import { createRegistry, overrideToCapabilities, registerProvider } from '../llm/index.js';
3
4
  import { initPlugins } from '../plugins/index.js';
5
+ import { getByDirectory } from '../project/index.js';
6
+ import { projectTrustCurrent } from '../project/trust.js';
4
7
  import { createDefaultRegistry, createDefaultURLRegistry } from '../tools/index.js';
5
8
  import { autoAllowChecker } from '../tools/permission.js';
6
9
  /** 真正非交互模式(print/acp):所有工具无条件放行(命令由用户显式触发)。 */
@@ -14,20 +17,29 @@ const fullyAutoApproveChecker = {
14
17
  * 非交互模式(print)安全策略(P1-6):ask 工具**直接拒绝**并给出可操作提示
15
18
  * (-y 或 serve),而非返回 permission_required 让 LLM 在无确认通道下无限重试。
16
19
  * 拒绝原因作为 tool result 交给模型,模型会向用户转述。
20
+ * `allowTools`(--allow 白名单)内的 ask 工具改为放行,提供「全拒 / 全放行」之间的
21
+ * 逐工具粒度;未列出的 ask 工具仍拒绝。
17
22
  */
18
- const nonInteractiveSafeChecker = {
19
- check: async (tool, _input, _ctx) => {
20
- const result = await autoAllowChecker.check(tool, _input, _ctx);
21
- if (result._tag === 'ask') {
22
- return {
23
- _tag: 'deny',
24
- reason: `非交互模式:工具 "${tool.name}" 需要确认。请加 -y 放行写操作,或使用 c0de serve 交互确认`,
25
- };
26
- }
27
- return result;
28
- },
29
- confirm: autoAllowChecker.confirm,
30
- };
23
+ function createNonInteractiveChecker(allowTools) {
24
+ const allowSet = new Set(allowTools ?? []);
25
+ return {
26
+ check: async (tool, _input, _ctx) => {
27
+ const result = await autoAllowChecker.check(tool, _input, _ctx);
28
+ if (result._tag === 'ask') {
29
+ if (allowSet.has(tool.name))
30
+ return { _tag: 'allow' };
31
+ return {
32
+ _tag: 'deny',
33
+ reason: `非交互模式:工具 "${tool.name}" 需要确认。请加 -y 全量放行、--allow <工具名> 定向放行,或使用 c0de serve 交互确认`,
34
+ };
35
+ }
36
+ return result;
37
+ },
38
+ confirm: autoAllowChecker.confirm,
39
+ };
40
+ }
41
+ /** 无白名单的非交互安全策略(保留导出兼容既有调用方)。 */
42
+ const nonInteractiveSafeChecker = createNonInteractiveChecker();
31
43
  /** 把 config.providers 注册到新建的 LLM registry。 */
32
44
  function buildLLMRegistry(config) {
33
45
  const registry = createRegistry();
@@ -53,25 +65,52 @@ function registerProviderFromConfig(registry, p) {
53
65
  ...(p.models ? { models: overrideToCapabilities(p.models) } : {}),
54
66
  });
55
67
  }
56
- /** 解析权限 checker:显式策略优先,否则回退到 config.permission.defaultMode。 */
57
- function resolvePermissionChecker(config, strategy) {
68
+ /** 解析权限 checker:显式策略优先,否则回退到 config.permission.defaultMode。
69
+ * 无 --allow 白名单时返回单例 nonInteractiveSafeChecker(保持对象同一性);有白名单
70
+ * 时按白名单新建 checker(--allow 只影响 ask 工具的定向放行)。 */
71
+ function resolvePermissionChecker(config, strategy, allowTools) {
58
72
  if (strategy === 'full-auto')
59
73
  return fullyAutoApproveChecker;
74
+ const listed = allowTools !== undefined && allowTools.length > 0;
60
75
  if (strategy === 'safe')
61
- return nonInteractiveSafeChecker;
76
+ return listed ? createNonInteractiveChecker(allowTools) : nonInteractiveSafeChecker;
62
77
  return config.permission.defaultMode === 'auto'
63
78
  ? fullyAutoApproveChecker
64
- : nonInteractiveSafeChecker;
79
+ : listed
80
+ ? createNonInteractiveChecker(allowTools)
81
+ : nonInteractiveSafeChecker;
82
+ }
83
+ /**
84
+ * cwd 是否当前可信项目(trustedAt 非空且指纹未漂移)。与聊天门禁/插件加载同口径;
85
+ * CLI 工作流注册表(内置+用户级+项目级)的构建也用它决定是否 import 项目级文件。
86
+ */
87
+ async function resolveCwdProjectTrusted(db, cwd) {
88
+ try {
89
+ const p = await getByDirectory(db, cwd);
90
+ if (p?.trustedAt != null) {
91
+ return projectTrustCurrent(loadConfigScopes(cwd).project, p.trustedAt, p.riskFingerprint, cwd);
92
+ }
93
+ }
94
+ catch {
95
+ // 查询失败 → 未信任
96
+ }
97
+ return false;
65
98
  }
66
99
  /** 组装完整 LoopDeps(默认 safe 放行 + 默认工具注册表)。 */
67
100
  async function buildAgentDeps(config, opts) {
68
101
  const llmRegistry = buildLLMRegistry(config);
69
102
  const toolRegistry = createDefaultRegistry(config);
103
+ // P0-2:项目插件仅在项目被显式信任后加载(c0de trust <dir>);内存库
104
+ // (--temp/锁冲突降级)不含项目记录 → 未信任,项目插件不加载。
105
+ // P0(代码面):已信任但指纹漂移(插件代码/MCP 参数/风险键变更)→ 不加载,
106
+ // 与聊天门禁同口径。
107
+ const projectTrusted = await resolveCwdProjectTrusted(opts.db, opts.cwd);
70
108
  const { hookRunner } = await initPlugins({
71
109
  cwd: opts.cwd,
72
110
  config,
73
111
  toolRegistry,
74
112
  llmRegistry,
113
+ projectTrusted,
75
114
  });
76
115
  const deps = {
77
116
  db: opts.db,
@@ -79,11 +118,19 @@ async function buildAgentDeps(config, opts) {
79
118
  toolRegistry,
80
119
  urlRegistry: createDefaultURLRegistry(),
81
120
  hookRunner,
82
- permission: resolvePermissionChecker(config, opts.permissionStrategy),
121
+ permission: resolvePermissionChecker(config, opts.permissionStrategy, opts.allowTools),
83
122
  config,
84
123
  cwd: opts.cwd,
85
124
  ...(opts.chatStream ? { chatStream: opts.chatStream } : {}),
125
+ // P:预算护栏 CLI 变体——print/acp 无恢复 UI,超支中止 run(产出 error)而非
126
+ // 暂停永久挂起。金额与 token 任一动作='pause'/'abort' 即启用(每轮 LLM 请求前检查)。
127
+ ...(config.usage?.budgetAction === 'pause' ||
128
+ config.usage?.budgetAction === 'abort' ||
129
+ config.usage?.tokenBudgetAction === 'pause' ||
130
+ config.usage?.tokenBudgetAction === 'abort'
131
+ ? { budgetAbort: true }
132
+ : {}),
86
133
  };
87
134
  return deps;
88
135
  }
89
- export { buildAgentDeps, buildLLMRegistry, fullyAutoApproveChecker, nonInteractiveSafeChecker };
136
+ export { buildAgentDeps, buildLLMRegistry, fullyAutoApproveChecker, nonInteractiveSafeChecker, resolveCwdProjectTrusted, };
@@ -4,5 +4,8 @@ declare const COMMANDS: CommandSpec[];
4
4
  type DispatchOverrides = {
5
5
  runServe?: () => Promise<void>;
6
6
  };
7
+ /** P1-2:解析 --budget-action 覆盖值。CLI 无「恢复」UI,config 的 pause 在此等效 abort;
8
+ * 接受 warn|abort(pause 兼容映射到 abort)。非法值抛错并提示可取值。 */
9
+ declare function parseBudgetActionOption(value: string | undefined): 'warn' | 'abort' | undefined;
7
10
  declare function dispatch(argv: string[], overrides?: DispatchOverrides): Promise<void>;
8
- export { COMMANDS, dispatch };
11
+ export { COMMANDS, dispatch, parseBudgetActionOption };
package/dist/cli/index.js CHANGED
@@ -4,7 +4,8 @@ import { mkdirSync } from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { loadConfig } from '../core/config.js';
6
6
  import { createDB, migrateDB } from '../db/index.js';
7
- import { acquireDevDbLock, releaseDevDbLock, resolveDbDir } from '../server/server.js';
7
+ import { enforceProjectTrust } from '../project/index.js';
8
+ import { acquireDevDbLock, readServeInfo, releaseDevDbLock, resolveDbDir, } from '../server/server.js';
8
9
  import { runAcpCommand } from './commands/acp.js';
9
10
  import { runAuthCommand } from './commands/auth.js';
10
11
  import { runChatCommand } from './commands/chat.js';
@@ -13,6 +14,7 @@ import { runInitCommand } from './commands/init.js';
13
14
  import { runPluginCommand } from './commands/plugin.js';
14
15
  import { runServeCommand } from './commands/serve.js';
15
16
  import { runSessionsCommand } from './commands/sessions.js';
17
+ import { runTrustCommand } from './commands/trust.js';
16
18
  import { runUpdateCommand } from './commands/update.js';
17
19
  import { buildAgentDeps } from './deps.js';
18
20
  import { parseCommand } from './parser.js';
@@ -24,7 +26,13 @@ const COMMANDS = [
24
26
  { name: 'model', type: 'string' },
25
27
  { name: 'format', type: 'string' },
26
28
  { name: 'yes', type: 'boolean', short: 'y' },
29
+ { name: 'allow', type: 'string' },
27
30
  { name: 'continue', type: 'string' },
31
+ // P2 修复:serve 运行时持久库被占用,此前静默退化为内存库(会话不保存,
32
+ // 仅 stderr 一行提示极易错过)。现须显式 --temp 才允许临时模式。
33
+ { name: 'temp', type: 'boolean' },
34
+ // P1-2:CLI 无「恢复」交互,预算 pause 在此等效「超支中止」;允许本 run 覆盖。
35
+ { name: 'budget-action', type: 'string' },
28
36
  ],
29
37
  },
30
38
  {
@@ -32,6 +40,7 @@ const COMMANDS = [
32
40
  description: 'Start the HTTP server (default when no command given).',
33
41
  options: [
34
42
  { name: 'port', type: 'string' },
43
+ { name: 'host', type: 'string' },
35
44
  { name: 'open', type: 'boolean' },
36
45
  { name: 'restore', type: 'string' },
37
46
  { name: 'handoff-port', type: 'string' },
@@ -59,13 +68,27 @@ const COMMANDS = [
59
68
  },
60
69
  {
61
70
  name: 'sessions',
62
- description: 'Manage CLI/web sessions (list / delete).',
63
- options: [],
71
+ description: 'Manage CLI/web sessions (list / delete / restore / deleted / purge).',
72
+ options: [
73
+ { name: 'project', type: 'string' },
74
+ { name: 'yes', type: 'boolean', short: 'y' },
75
+ { name: 'all', type: 'boolean' },
76
+ ],
77
+ },
78
+ {
79
+ name: 'trust',
80
+ description: 'Trust a project directory: enable its project-scope config and plugins.',
81
+ options: [{ name: 'yes', type: 'boolean', short: 'y' }],
64
82
  },
65
83
  {
66
84
  name: 'acp',
67
85
  description: 'Run in Agent Client Protocol mode (editor integration).',
68
- options: [],
86
+ options: [
87
+ // serve 占用持久库时的显式临时模式开关(与 chat --temp 同语义)。
88
+ { name: 'temp', type: 'boolean' },
89
+ // P1-2:ACP 同样无恢复 UI(超支中止),允许本 run 覆盖 warn/abort。
90
+ { name: 'budget-action', type: 'string' },
91
+ ],
69
92
  },
70
93
  {
71
94
  name: 'update',
@@ -84,6 +107,17 @@ function isDbLockConflict(err) {
84
107
  return true;
85
108
  return err.name === 'RuntimeError' && /abort/i.test(err.message);
86
109
  }
110
+ /** P1-2:解析 --budget-action 覆盖值。CLI 无「恢复」UI,config 的 pause 在此等效 abort;
111
+ * 接受 warn|abort(pause 兼容映射到 abort)。非法值抛错并提示可取值。 */
112
+ function parseBudgetActionOption(value) {
113
+ if (value === undefined)
114
+ return undefined;
115
+ if (value === 'warn')
116
+ return 'warn';
117
+ if (value === 'abort' || value === 'pause')
118
+ return 'abort';
119
+ throw new Error(`--budget-action 仅支持 warn|abort(CLI 无恢复 UI,config 的 pause 在此等效 abort)`);
120
+ }
87
121
  /** 封装 agent 依赖生命周期:加载配置 → 建库迁移 → 组装 deps → 使用后关库。
88
122
  * chat / acp 两个命令复用同一套逻辑(此前在 dispatch 中各写一份)。
89
123
  * 数据库优先用全局 dataDir 持久库(会话可恢复,--continue 可用);
@@ -92,6 +126,31 @@ function isDbLockConflict(err) {
92
126
  * 带 --continue 时内存库不含历史会话,直接报错退出而非误导性的 session not found。 */
93
127
  async function withAgentDeps(cwd, opts, fn) {
94
128
  const config = await loadConfig(cwd);
129
+ // P1-2:--budget-action 覆盖(warn=不中止;abort=超支中止)。
130
+ // 直接映射为 config 的 abort 动作(不再借 'pause' 中转):
131
+ // 预算预检、buildAgentDeps 的 budgetAbort 注入、超支判定文案三处口径一致。
132
+ // CLI 无「恢复」UI,config 的 pause 在此等效 abort。
133
+ if (opts.budgetAction) {
134
+ const action = opts.budgetAction === 'abort' ? 'abort' : 'warn';
135
+ config.usage = { ...config.usage, budgetAction: action, tokenBudgetAction: action };
136
+ }
137
+ const budgetWillAbort = config.usage?.budgetAction === 'pause' ||
138
+ config.usage?.budgetAction === 'abort' ||
139
+ config.usage?.tokenBudgetAction === 'pause' ||
140
+ config.usage?.tokenBudgetAction === 'abort';
141
+ const budgetConfigured = (config.usage?.monthlyBudgetUsd ?? 0) > 0 ||
142
+ (config.usage?.globalMonthlyBudgetUsd ?? 0) > 0 ||
143
+ (config.usage?.monthlyTokenBudget ?? 0) > 0 ||
144
+ (config.usage?.globalMonthlyTokenBudget ?? 0) > 0;
145
+ if (opts.budgetAction === undefined && budgetWillAbort && budgetConfigured) {
146
+ // 文案按配置实际动作区分:pause 与 abort 在 CLI 下都等价于中止本次 run,
147
+ // 但用户应知道自己配置的是什么。
148
+ const effective = config.usage?.budgetAction === 'abort' || config.usage?.tokenBudgetAction === 'abort'
149
+ ? 'abort'
150
+ : 'pause';
151
+ process.stderr.write(`[c0de] ⚠ 预算动作为 ${effective}:CLI 无「恢复」交互,超预算将中止本次 run(无断点续跑)。` +
152
+ '如需仅告警不中止,加 `--budget-action warn`。\n');
153
+ }
95
154
  const dataDir = resolveDbDir();
96
155
  let db;
97
156
  let holdLock = false;
@@ -108,15 +167,28 @@ async function withAgentDeps(cwd, opts, fn) {
108
167
  holdLock = false;
109
168
  if (!isDbLockConflict(err))
110
169
  throw err;
170
+ // serve 运行时引导:读取运行中实例的端口(serve-info.json),直接给出可点击的
171
+ // Web 地址,而不是让用户面对「单写者限制」无从下手。
172
+ const info = readServeInfo(dataDir);
173
+ const webUrl = info && info.port > 0 ? `http://localhost:${info.port}/` : '启动日志中打印的地址';
111
174
  if (opts.requirePersistent) {
112
- throw new Error('c0de serve 正在运行并占用会话库。请通过 Web UI 管理会话,或停止 serve 后重试。');
175
+ throw new Error(`c0de serve 正在运行并占用会话库,CLI 会话管理暂不可用(单写者限制)。\n` +
176
+ ` 1) 在浏览器打开 ${webUrl} 管理会话;\n` +
177
+ ` 2) 或停止 serve(Ctrl+C)后重试本命令。`);
113
178
  }
114
179
  if (opts.continueSessionId) {
115
180
  throw new Error(`无法续聊会话 ${opts.continueSessionId}:持久库被 c0de serve 占用,内存模式无法续聊。\n` +
116
- `请停止 serve 后重试,或去掉 --continue 开启新会话。`);
181
+ `请在浏览器打开 ${webUrl} 找到该会话继续对话;或停止 serve 后重试。`);
182
+ }
183
+ if (!opts.allowTemp) {
184
+ // P2 修复:锁冲突不再静默降级为不保存会话的内存模式(用户极易错过 stderr
185
+ // 提示导致对话丢失)。必须显式 --temp 确认放弃持久化。
186
+ throw new Error(`c0de serve 正在运行并占用会话库,本次对话将无法保存。\n` +
187
+ ` 1) 在浏览器打开 ${webUrl} 继续对话(推荐,会话自动保存);\n` +
188
+ ` 2) 或停止 serve 后重试(对话可持久化);\n` +
189
+ ` 3) 或加 --temp 显式接受临时模式(消息与工具调用不会保存)。`);
117
190
  }
118
- process.stderr.write('[c0de] ⚠ c0de serve 正在运行:本次对话为临时模式,消息与工具调用不会保存。\n' +
119
- ' 停止 serve 后重试可持久化,或直接使用浏览器界面。\n');
191
+ process.stderr.write('[c0de] ⚠ 临时模式:消息与工具调用不会保存。停止 serve 后重试可持久化,或直接使用浏览器界面。\n');
120
192
  db = await createDB({ driver: 'pglite' });
121
193
  await migrateDB(db);
122
194
  }
@@ -125,7 +197,13 @@ async function withAgentDeps(cwd, opts, fn) {
125
197
  db,
126
198
  cwd,
127
199
  ...(opts.strategy ? { permissionStrategy: opts.strategy } : {}),
200
+ ...(opts.allowTools ? { allowTools: opts.allowTools } : {}),
128
201
  });
202
+ // P0:agent 执行路径(chat/acp)在无 Web 确认弹窗下硬拦未信任项目的风险配置,
203
+ // 引导 c0de trust;trust/sessions 等非 agent 命令经 skipTrustGate 跳过。
204
+ if (opts.skipTrustGate !== true) {
205
+ await enforceProjectTrust(db, cwd);
206
+ }
129
207
  await fn(config, deps);
130
208
  }
131
209
  finally {
@@ -154,10 +232,21 @@ async function dispatch(argv, overrides = {}) {
154
232
  case 'chat': {
155
233
  // --yes / -y 显式放行写操作;否则按 config.permission.defaultMode 决定(默认 safe)。
156
234
  const strategy = args.options.yes ? 'full-auto' : undefined;
235
+ const allowTools = args.options.allow
236
+ ? String(args.options.allow)
237
+ .split(',')
238
+ .map((s) => s.trim())
239
+ .filter((s) => s.length > 0)
240
+ : undefined;
157
241
  const continueId = args.options.continue;
242
+ const allowTemp = args.options.temp === true;
243
+ const budgetAction = parseBudgetActionOption(args.options['budget-action']);
158
244
  await withAgentDeps(cwd, {
159
245
  ...(strategy ? { strategy } : {}),
246
+ ...(allowTools && allowTools.length > 0 ? { allowTools } : {}),
160
247
  ...(continueId ? { continueSessionId: continueId } : {}),
248
+ ...(allowTemp ? { allowTemp } : {}),
249
+ ...(budgetAction ? { budgetAction } : {}),
161
250
  }, (config, deps) => runChatCommand({ args, config, deps }));
162
251
  return;
163
252
  }
@@ -179,12 +268,22 @@ async function dispatch(argv, overrides = {}) {
179
268
  }
180
269
  case 'sessions': {
181
270
  // 需要持久库列出/清理会话;serve 运行时内存库不含数据,直接失败而不是列空表。
182
- await withAgentDeps(cwd, { requirePersistent: true }, (_config, deps) => runSessionsCommand({ args, db: deps.db }));
271
+ await withAgentDeps(cwd, { requirePersistent: true, skipTrustGate: true }, (_config, deps) => runSessionsCommand({ args, db: deps.db }));
272
+ return;
273
+ }
274
+ case 'trust': {
275
+ // 与 sessions 同约束:持久库落盘 trustedAt;serve 运行时引导走 Web 信任弹窗。
276
+ await withAgentDeps(cwd, { requirePersistent: true, skipTrustGate: true }, (_config, deps) => runTrustCommand({ args, db: deps.db, cwd }));
183
277
  return;
184
278
  }
185
279
  case 'acp': {
186
280
  // ACP 非交互:所有工具放行(编辑器侧自行控制执行授权)。
187
- await withAgentDeps(cwd, { strategy: 'full-auto' }, (config, deps) => runAcpCommand({ config, deps }));
281
+ const budgetAction = parseBudgetActionOption(args.options['budget-action']);
282
+ await withAgentDeps(cwd, {
283
+ strategy: 'full-auto',
284
+ ...(args.options.temp === true ? { allowTemp: true } : {}),
285
+ ...(budgetAction ? { budgetAction } : {}),
286
+ }, (config, deps) => runAcpCommand({ config, deps }));
188
287
  return;
189
288
  }
190
289
  case 'update': {
@@ -206,7 +305,7 @@ async function main() {
206
305
  process.exit(1);
207
306
  }
208
307
  }
209
- export { COMMANDS, dispatch };
308
+ export { COMMANDS, dispatch, parseBudgetActionOption };
210
309
  // bin 入口:仅在直接执行时运行 main(非被 import)。
211
310
  const isMain = process.argv[1] === fileURLToPath(import.meta.url);
212
311
  if (isMain) {
@@ -27,31 +27,62 @@ function formatACPEvent(method, params) {
27
27
  return JSON.stringify({ jsonrpc: '2.0', method, params });
28
28
  }
29
29
  async function runAcpLoop(opts) {
30
+ // P2-4:chat 在后台单飞执行——loop 继续读取后续请求(abort 等)而不被阻塞。
31
+ // 此前串行 await 使 abort 请求永远排在 chat 完成之后,中止形同虚设。
32
+ let chatInFlight = null;
33
+ const writer = opts.writer;
30
34
  for await (const line of opts.reader) {
31
35
  const trimmed = line.trim();
32
36
  if (!trimmed)
33
37
  continue;
34
38
  const req = parseACPRequest(trimmed);
35
39
  if (!req) {
36
- opts.writer(formatACPError(null, -32700, 'Parse error'));
40
+ writer(formatACPError(null, -32700, 'Parse error'));
37
41
  continue;
38
42
  }
39
43
  const handler = opts.handlers[req.method];
40
44
  if (!handler) {
41
45
  if (req.id !== null)
42
- opts.writer(formatACPError(req.id, -32601, `Method not found: ${req.method}`));
46
+ writer(formatACPError(req.id, -32601, `Method not found: ${req.method}`));
43
47
  continue;
44
48
  }
45
- try {
46
- const result = await handler(req.params);
49
+ const respond = (result) => {
47
50
  if (req.id !== null)
48
- opts.writer(formatACPResponse(req.id, result));
49
- }
50
- catch (err) {
51
+ writer(formatACPResponse(req.id, result));
52
+ };
53
+ const fail = (err) => {
51
54
  if (req.id !== null) {
52
- opts.writer(formatACPError(req.id, -32603, err instanceof Error ? err.message : String(err)));
55
+ writer(formatACPError(req.id, -32603, err instanceof Error ? err.message : String(err)));
53
56
  }
57
+ };
58
+ if (req.method === 'chat') {
59
+ if (chatInFlight) {
60
+ fail(new Error('chat: another chat is already in progress (abort it first)'));
61
+ continue;
62
+ }
63
+ chatInFlight = (async () => {
64
+ try {
65
+ respond(await handler(req.params));
66
+ }
67
+ catch (err) {
68
+ fail(err);
69
+ }
70
+ finally {
71
+ chatInFlight = null;
72
+ }
73
+ })();
74
+ void chatInFlight;
75
+ continue;
76
+ }
77
+ try {
78
+ respond(await handler(req.params));
79
+ }
80
+ catch (err) {
81
+ fail(err);
54
82
  }
55
83
  }
84
+ // 流结束前等最后一个在途 chat 收尾,保证响应写全。
85
+ if (chatInFlight)
86
+ await chatInFlight;
56
87
  }
57
88
  export { formatACPError, formatACPEvent, formatACPResponse, parseACPRequest, runAcpLoop };