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
@@ -84,7 +84,9 @@ type LLMCall = {
84
84
  firstToken: number;
85
85
  total: number;
86
86
  };
87
- cost: number;
87
+ /** 估算成本(USD)。null = 价格未知(provider 未注册或模型无价目),
88
+ * 聚合时按 $0 计入并单独计数,UI 显式提示——不再静默记 0 造成预算漏报。 */
89
+ cost: number | null;
88
90
  thinking?: string;
89
91
  /** 模型回复文本(替代完整 responseChunks;前端只用文本拼接)。 */
90
92
  responseText: string;
@@ -149,19 +151,23 @@ type AgentEvent = {
149
151
  tool: string;
150
152
  input: unknown;
151
153
  }
152
- /** P1-6:权限确认超时(5 分钟)被自动拒绝时通知前端,提供「重新询问」入口。 */
154
+ /** P1-6:权限确认超时(5 分钟)被自动拒绝时通知前端,提供「重新询问」入口。
155
+ * timeoutAction 告知兜底动作(pause=拒绝后暂停 run / deny=拒绝后继续),前端据此渲染文案。 */
153
156
  | {
154
157
  _tag: 'permission_timeout';
155
158
  toolCallId: string;
156
159
  tool: string;
157
160
  input: unknown;
161
+ timeoutAction: 'pause' | 'deny';
158
162
  }
159
- /** P0 双层超时:提示后仍无响应,pending 已兜底自动拒绝(run 继续)——前端须清理弹窗状态。 */
163
+ /** P0 双层超时:提示后仍无响应,pending 已兜底自动拒绝——前端须清理弹窗状态。
164
+ * timeoutAction='pause' 时后端同时暂停 run(status_change(paused) 事件随后到达)。 */
160
165
  | {
161
166
  _tag: 'permission_expired';
162
167
  toolCallId: string;
163
168
  tool: string;
164
169
  input: unknown;
170
+ timeoutAction: 'pause' | 'deny';
165
171
  } | {
166
172
  _tag: 'error';
167
173
  error: AgentError;
@@ -169,6 +175,13 @@ type AgentEvent = {
169
175
  /** 通知前端:本轮 LLM 调用详情已持久化,应刷新调用详情面板。轻量通知,不带 payload。 */
170
176
  | {
171
177
  _tag: 'llm_detail';
178
+ }
179
+ /** 工作流执行进度(/workflow run 的 onProgress → SSE progress 事件):
180
+ * 前端以横幅展示当前阶段,不再对长工作流黑盒等待。 */
181
+ | {
182
+ _tag: 'progress';
183
+ message: string;
184
+ detail?: unknown;
172
185
  } | {
173
186
  _tag: 'subagent_start';
174
187
  childId: string;
@@ -248,6 +261,9 @@ type AgentState = {
248
261
  provider: string;
249
262
  model: string;
250
263
  };
264
+ /** P3 成本护栏:本 run 已因预算超支暂停过一次。恢复后不再重复暂停
265
+ * (用户点「恢复」即知情同意继续,反复暂停是骚扰而非护栏)。 */
266
+ budgetPauseTriggered?: boolean;
251
267
  /**
252
268
  * 压缩死锁标记:自动压缩成功后仍超阈值(如 keepRecentTokens 本身已超限)时置真,
253
269
  * 暂停后续自动压缩以防每轮重复触发(无限循环)。收到新用户消息(agentLoop 重入)时重置。
@@ -46,15 +46,30 @@ type WebSearchConfig = {
46
46
  };
47
47
  /** 多 agent 配置(spec: multi-agent-design §4.12)。 */
48
48
  type AgentsConfig = {
49
- /** agent markdown 目录(相对项目根),默认 '.c0de/agents'。 */
50
- dir: string;
51
49
  /** 并行子 agent 数上限,默认 3。 */
52
50
  subagentConcurrency: number;
53
51
  };
54
52
  /** 权限配置:控制工具执行授权的默认行为。 */
55
53
  type PermissionConfig = {
56
- /** 启动时的默认授权模式:'default' 逐个确认(默认);'auto' 自动放行 ask 工具(YOLO 自动授权)。 */
54
+ /**
55
+ * 启动时的默认授权模式:
56
+ * - 'default'(默认):只读工具自动放行;写/执行工具需确认。Web 为交互弹窗;
57
+ * CLI 非交互(`c0de chat` 未加 -y)下写/执行工具直接拒绝并提示加 -y 或改 auto。
58
+ * (`c0de acp` 恒为 full-auto,由编辑器侧自行控制执行授权,不受本项影响。)
59
+ * - 'auto':全部工具自动放行(YOLO)。克隆仓库前务必先审查其 .c0de/config.json。
60
+ */
57
61
  defaultMode: 'default' | 'auto';
62
+ /**
63
+ * 权限确认超时(提示后 25 分钟宽限期满)兜底拒绝后的动作:
64
+ * - 'pause'(默认):拒绝该工具并暂停 run,用户回来点「恢复」继续——
65
+ * 避免 default 模式下 agent 在用户缺席时继续自主执行后续工具;
66
+ * - 'deny':拒绝该工具,run 继续执行(旧行为,会话绝不挂起)。
67
+ */
68
+ timeoutAction?: 'pause' | 'deny';
69
+ /** 首层确认超时(毫秒)——超过仅提示、pending 保持;默认 5 分钟。 */
70
+ timeoutMs?: number;
71
+ /** 首层超时后到兜底自动拒绝的宽限期(毫秒);默认 25 分钟。 */
72
+ expireGraceMs?: number;
58
73
  };
59
74
  /** 自动升级配置(spec §18)。控制后台 npm registry 检查与无感知热更新行为。 */
60
75
  type UpdateConfig = {
@@ -87,6 +102,45 @@ type SecurityConfig = {
87
102
  * 用于共享主机等 URL 可能泄漏的场景,缩短「先到先得」竞态窗口。 */
88
103
  firstDeviceTtlMs?: number;
89
104
  };
105
+ /** 用量与成本统计配置。 */
106
+ type UsageConfig = {
107
+ /** 月度成本预算(USD,按配置价目估算)。0 = 不限制。
108
+ * 项目口径:当前项目聚合成本超过预算时告警/暂停。
109
+ * 当前月成本超过预算时,设置页「用量」面板给出醒目告警。 */
110
+ monthlyBudgetUsd: number;
111
+ /**
112
+ * 全局月度成本预算(USD,所有项目 + 未归属调用聚合)。0 = 不限制。
113
+ * P1-3:与项目预算并存,任一超支即触发 budgetAction——
114
+ * 未归属项目/未配置项目预算的花费至少受全局预算兜底。
115
+ * 仅 global 作用域生效:项目作用域写入会被剥离并忽略(作用域收敛)。 */
116
+ globalMonthlyBudgetUsd?: number;
117
+ /**
118
+ * 月度 token 预算(input + output + cacheRead tokens 之和),0 = 不限制。
119
+ * P0:价格独立护栏——自建网关/未登记模型的 cost 恒 $0,金额护栏拦不住,
120
+ * token 护栏按 token 量兜底(缓存读取亦按用量计费,故纳入口径)。
121
+ * 动作由 tokenBudgetAction 决定(缺省回退 budgetAction,向后兼容)。 */
122
+ monthlyTokenBudget?: number;
123
+ /**
124
+ * 全局月度 token 预算(所有项目 + 未归属调用聚合),0 = 不限制。
125
+ * 与 monthlyTokenBudget 并存,任一超支即按 tokenBudgetAction 触发。
126
+ * 仅 global 作用域生效(与 globalMonthlyBudgetUsd 同口径)。 */
127
+ globalMonthlyTokenBudget?: number;
128
+ /**
129
+ * 超支动作(P3 成本护栏),作用于**金额**预算:
130
+ * - 'warn'(默认):仅告警(顶栏徽标/用量面板),agent 继续执行;
131
+ * - 'pause':新一轮 LLM 请求前发现当月成本超预算 → 暂停 run(等同权限超时
132
+ * 暂停机制),用户点「恢复」后本 run 不再因预算重复暂停(用户已知情);
133
+ * - 'abort':新一轮 LLM 请求前发现超预算 → 中止 run(硬封顶,无「恢复」按钮,
134
+ * 需重新发送消息/调高预算后重试),适合需要「撞线即硬停、不可在界面原地放行」的场景。
135
+ */
136
+ budgetAction?: 'warn' | 'pause' | 'abort';
137
+ /**
138
+ * token 预算的超支动作,独立于金额预算(P:token 是兜底口径,用户可能希望
139
+ * 「金额超支暂停、token 超支只告警」或反之)。缺省回退 budgetAction。
140
+ * 语义同 budgetAction:'pause'/'abort' 硬性暂停/中止,'warn' 仅徽标/面板告警。
141
+ */
142
+ tokenBudgetAction?: 'warn' | 'pause' | 'abort';
143
+ };
90
144
  /** Global application configuration. */
91
145
  type Config = {
92
146
  providers: ProviderConfig[];
@@ -125,6 +179,7 @@ type Config = {
125
179
  agents: AgentsConfig;
126
180
  permission: PermissionConfig;
127
181
  update: UpdateConfig;
182
+ usage: UsageConfig;
128
183
  theme: 'light' | 'dark' | 'system';
129
184
  };
130
- export type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig, };
185
+ export type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, UsageConfig, WebSearchConfig, };
@@ -30,10 +30,10 @@ type KanbanCard = {
30
30
  createdAt: string;
31
31
  updatedAt: string;
32
32
  };
33
- /** A kanban board (one per project). */
33
+ /** A kanban board (one per project). P2-5:回收站(软删除)看板 projectId 为 null。 */
34
34
  type KanbanBoard = {
35
35
  id: string;
36
- projectId: string;
36
+ projectId: string | null;
37
37
  columns: KanbanColumnDef[];
38
38
  labels: KanbanLabelDef[];
39
39
  createdAt: string;
@@ -51,6 +51,8 @@ declare const KANBAN_LABEL_COLORS: readonly string[];
51
51
  * implementation, mirroring the todoState pattern). */
52
52
  interface KanbanStore {
53
53
  getBoard(): Promise<KanbanBoardWithCards>;
54
+ /** 只读获取板(不懒创建):纯读路径(导出等)用,无板返回 null。 */
55
+ peekBoard(): Promise<KanbanBoardWithCards | null>;
54
56
  addCard(input: {
55
57
  title: string;
56
58
  description?: string | null;
@@ -48,10 +48,18 @@ type SessionMetadata = {
48
48
  mainThreadId?: string;
49
49
  squashCount?: number;
50
50
  fileSnapshots?: string[];
51
+ /** 工作流运行会话对应的 /workflow 名称(创建时写入;中断恢复指引展示用)。 */
52
+ workflowName?: string;
51
53
  /** 本会话分段增量 LLM 调用记录,用于调用详情面板展示。 */
52
54
  segments?: LLMSegment[];
53
55
  /** 上次 agent run 状态;status='running' 且进程无活跃 run → 被中断。 */
54
56
  lastRun?: LastRun;
57
+ /** M3:中断 run 的半截轮次区间起点——最后一条含文本 user 消息的条目 id。
58
+ * 区间 (since, until] 内的条目属「未完成轮次」:构建上下文时剔除、时间线置灰。
59
+ * 与 unfinishedUntilEntryId 成对使用;条目被 /clear、压缩归档后区间自然失效(no-op)。 */
60
+ unfinishedSinceEntryId?: string;
61
+ /** M3:中断轮次区间终点——标记时数据库中最后一条条目的 id(含)。 */
62
+ unfinishedUntilEntryId?: string;
55
63
  /** 上次打开时间戳(ms),用于会话列表按最近打开排序。 */
56
64
  lastOpenedAt?: number;
57
65
  /** P2:会话级授权模式覆盖('auto'/'default'),跨重启持久化。 */
@@ -63,6 +71,12 @@ type SessionMetadata = {
63
71
  /** 回收站条目已到期、进入物理清除宽限期的时间戳(ms)。
64
72
  * 到期先标记,宽限期(默认 7 天)内可在 UI 恢复;期满后由 purgeDeletedSessions 物理清除。 */
65
73
  purgePendingAt?: number;
74
+ /**
75
+ * P2-3:最近一次预算超支暂停的原因(loop 暂停时写入)。热更新/重启后 run 重建,
76
+ * 内存 budgetPauseTriggered 标记丢失——新 run 启动时消费此字段还原标记,
77
+ * 避免用户已确认继续后又被同一超支原因二次暂停。消费即删除(单次语义)。
78
+ */
79
+ budgetPauseReason?: string;
66
80
  };
67
81
  /** A conversation session (may have a parent for branching). */
68
82
  type Session = {
@@ -31,6 +31,17 @@ function formatBoardSummary(board, errors = []) {
31
31
  if (cards.length === 0)
32
32
  lines.push(' (empty)');
33
33
  }
34
+ // P1 修复:历史幽灵卡片(列 id 不在当前列配置,旧版本脏数据)单独列出并附
35
+ // 完整 id——否则对 agent 完全不可见(且 Total 与列之和恒对不上),无法清理。
36
+ const colIds = new Set(board.columns.map((c) => c.id));
37
+ const orphans = board.cards.filter((c) => !colIds.has(c.columnId));
38
+ if (orphans.length > 0) {
39
+ lines.push(`## 未归属列 (${orphans.length})`);
40
+ for (const card of orphans) {
41
+ lines.push(` [${card.id}] "${card.title}"(引用已不存在的列 "${card.columnId}",` +
42
+ `可 delete 清理或 move 到现有列)`);
43
+ }
44
+ }
34
45
  const total = board.cards.length;
35
46
  lines.push(`\nTotal: ${total} card(s).`);
36
47
  if (total === 0)
@@ -84,12 +95,12 @@ export const kanbanTool = {
84
95
  name: 'kanban',
85
96
  description: `Manage the project's shared kanban task board. 5 operations:
86
97
  - view: list all columns and cards (read-only)
87
- - add: create a new task card (title required; optional description, columnId defaults to todo, priority defaults to medium, labels)
98
+ - add: create a new task card (title required; optional description, columnId defaults to the board's first column, priority defaults to medium, labels)
88
99
  - update: edit a card's title/description/priority/labels (id required)
89
100
  - move: move a card to a different column or reorder (id + columnId required; optional position)
90
101
  - delete: remove a card (id required)
91
102
 
92
- The board is shared across all sessions and agents in the same project — use it to coordinate work, track tasks visible to the user, and break down complex projects. Column ids are: todo, in_progress, in_review, done, cancelled (customizable by the user via the board config UI).`,
103
+ The board is shared across all sessions and agents in the same project — use it to coordinate work, track tasks visible to the user, and break down complex projects. Column ids are user-configurable: do NOT guess them — run \`view\` first to discover the current columns, then reuse those exact ids. Adding/moving to an unknown column id fails with the list of valid ids.`,
93
104
  parameters: kanbanParameters,
94
105
  permission: 'auto',
95
106
  execute: async (input, ctx) => {
@@ -34,10 +34,21 @@ import type { ToolRegistry } from './types.js';
34
34
  */
35
35
  export declare function createDefaultRegistry(config?: Config): ToolRegistry;
36
36
  /**
37
- * 解析本轮启用的工具名列表(P1-1 修复:config.tools.enabled/disabled 双入口统一生效)。
37
+ * 解析本轮启用的工具名列表(config.tools.enabled/disabled 双入口统一生效)。
38
38
  *
39
- * - explicit(前端显式选择)非 undefined → 以其为准,但过滤掉 disabled 与未注册名。
40
- * - 否则 enabled 非空 → enabled ∩ registered;enabled 空 → 全部 registered(空=全部)。
39
+ * 语义(P1-1 修复「空=全部」安全陷阱 + P2 交集收紧):
40
+ * - explicit(前端显式选择)非 undefined → 与 config.tools.enabled 取交集——
41
+ * enabled 含 '*' 或未配置时视为无上限(explicit 原样生效)。此前 explicit 完全
42
+ * 覆盖 enabled:config.tools.enabled=[] 的 fail-closed 语义可被请求级 tools
43
+ * 静默绕过,设置页「禁用全部」与实际行为矛盾。
44
+ * - 否则 config.tools.enabled:含 '*'(通配)→ 全部注册工具;
45
+ * 空数组 → 无工具(fail-closed);否则 → 显式名单。
41
46
  * - disabled 恒过滤(registry 已排除,此处兜底)。
42
47
  */
43
48
  export declare function resolveEnabledToolNames(registry: ToolRegistry, config: Pick<Config, 'tools'>, explicit?: string[]): string[];
49
+ /**
50
+ * 依赖「会话绑定项目」的工具:其运行时依赖(如 kanban 的 kanbanStore)由宿主按
51
+ * session.projectId 注入——无项目会话(CLI print 未注册 cwd 等)调用必失败,
52
+ * 只会浪费一轮工具调用。会话无 projectId 时从工具集中剔除。
53
+ */
54
+ export declare const PROJECT_BOUND_TOOLS: ReadonlySet<string>;
@@ -66,16 +66,34 @@ export function createDefaultRegistry(config = DEFAULT_CONFIG) {
66
66
  return reg;
67
67
  }
68
68
  /**
69
- * 解析本轮启用的工具名列表(P1-1 修复:config.tools.enabled/disabled 双入口统一生效)。
69
+ * 解析本轮启用的工具名列表(config.tools.enabled/disabled 双入口统一生效)。
70
70
  *
71
- * - explicit(前端显式选择)非 undefined → 以其为准,但过滤掉 disabled 与未注册名。
72
- * - 否则 enabled 非空 → enabled ∩ registered;enabled 空 → 全部 registered(空=全部)。
71
+ * 语义(P1-1 修复「空=全部」安全陷阱 + P2 交集收紧):
72
+ * - explicit(前端显式选择)非 undefined → 与 config.tools.enabled 取交集——
73
+ * enabled 含 '*' 或未配置时视为无上限(explicit 原样生效)。此前 explicit 完全
74
+ * 覆盖 enabled:config.tools.enabled=[] 的 fail-closed 语义可被请求级 tools
75
+ * 静默绕过,设置页「禁用全部」与实际行为矛盾。
76
+ * - 否则 config.tools.enabled:含 '*'(通配)→ 全部注册工具;
77
+ * 空数组 → 无工具(fail-closed);否则 → 显式名单。
73
78
  * - disabled 恒过滤(registry 已排除,此处兜底)。
74
79
  */
75
80
  export function resolveEnabledToolNames(registry, config, explicit) {
76
81
  const all = listTools(registry).map((t) => t.name);
77
82
  const disabled = new Set(config.tools?.disabled ?? []);
78
- const base = explicit ??
79
- (config.tools?.enabled && config.tools.enabled.length > 0 ? config.tools.enabled : all);
83
+ const enabled = config.tools?.enabled;
84
+ const enabledIsWildcard = enabled === undefined || (enabled.length > 0 && enabled.includes('*'));
85
+ const base = explicit !== undefined
86
+ ? enabledIsWildcard
87
+ ? explicit
88
+ : explicit.filter((n) => enabled.includes(n))
89
+ : enabledIsWildcard
90
+ ? all
91
+ : (enabled ?? []);
80
92
  return base.filter((n) => all.includes(n) && !disabled.has(n));
81
93
  }
94
+ /**
95
+ * 依赖「会话绑定项目」的工具:其运行时依赖(如 kanban 的 kanbanStore)由宿主按
96
+ * session.projectId 注入——无项目会话(CLI print 未注册 cwd 等)调用必失败,
97
+ * 只会浪费一轮工具调用。会话无 projectId 时从工具集中剔除。
98
+ */
99
+ export const PROJECT_BOUND_TOOLS = new Set(['kanban']);
@@ -10,7 +10,11 @@ export declare function resolveProvider(preference: 'auto' | WebSearchProviderId
10
10
  export declare function formatForLLM(response: WebSearchResponse): string;
11
11
  /** 测试钩子:覆盖默认 fetch(undici ProxyAgent 包装版)。 */
12
12
  export declare function setFetchOverride(f: typeof fetch | undefined): void;
13
- /** 工具入口:解析 provider → search → 返回 WebSearchResponse。 */
13
+ /** 工具入口:解析 provider → search → 返回 WebSearchResponse。
14
+ * 'auto' 模式带运行时降级链:候选后端按 key 可用性排列(tavily > brave >
15
+ * duckduckgo),当前后端因鉴权/限流/服务端/网络错误失败时依次降级——
16
+ * 此前 auto 只按 key **存在性**选一次,key 失效/过期即整体失败。
17
+ * 显式指定后端时尊重用户选择,单后端不降级。abort 立即上抛,不沿链继续。 */
14
18
  export declare function runWebSearch(input: {
15
19
  query: string;
16
20
  numResults?: number;
@@ -65,17 +65,53 @@ export function setFetchOverride(f) {
65
65
  function currentFetch() {
66
66
  return fetchOverride ?? createFetch();
67
67
  }
68
- /** 工具入口:解析 provider → search → 返回 WebSearchResponse。 */
68
+ /** 运行时降级判定:鉴权失效(key 过期/被撤)、限流、服务端错误可换后端重试;
69
+ * 4xx 参数错误(400 等)换后端无意义;网络层错误(无 status)也降级(该后端连通性
70
+ * 问题,duckduckgo 兜底)。 */
71
+ function isFallbackable(err) {
72
+ const status = err?.status;
73
+ return (typeof status !== 'number' ||
74
+ status >= 500 ||
75
+ status === 401 ||
76
+ status === 403 ||
77
+ status === 429);
78
+ }
79
+ /** 工具入口:解析 provider → search → 返回 WebSearchResponse。
80
+ * 'auto' 模式带运行时降级链:候选后端按 key 可用性排列(tavily > brave >
81
+ * duckduckgo),当前后端因鉴权/限流/服务端/网络错误失败时依次降级——
82
+ * 此前 auto 只按 key **存在性**选一次,key 失效/过期即整体失败。
83
+ * 显式指定后端时尊重用户选择,单后端不降级。abort 立即上抛,不沿链继续。 */
69
84
  export async function runWebSearch(input, config, abort) {
70
85
  const keys = resolveKeys(config);
71
- const provider = resolveProvider(config.provider, keys);
72
- return provider.search({
86
+ const apiKeyFor = (provider) => provider.id === 'tavily' ? keys.tavily : provider.id === 'brave' ? keys.brave : undefined;
87
+ const search = (provider) => provider.search({
73
88
  query: input.query,
74
89
  limit: clampNumResults(input.numResults),
75
90
  recency: input.recency,
76
91
  signal: abort,
77
- apiKey: provider.id === 'tavily' ? keys.tavily : provider.id === 'brave' ? keys.brave : undefined,
92
+ apiKey: apiKeyFor(provider),
78
93
  fetchImpl: currentFetch(),
79
94
  });
95
+ if (config.provider === 'auto') {
96
+ const chain = [tavilyProvider, braveProvider, duckduckgoProvider].filter((p) => p.isAvailable(apiKeyFor(p)));
97
+ let lastError = null;
98
+ for (const provider of chain) {
99
+ try {
100
+ return await search(provider);
101
+ }
102
+ catch (err) {
103
+ if (abort.aborted)
104
+ throw err;
105
+ if (!isFallbackable(err))
106
+ throw err;
107
+ lastError = err;
108
+ }
109
+ }
110
+ if (lastError instanceof Error)
111
+ throw lastError;
112
+ throw new Error(`websearch 无可用后端:${String(lastError)}`);
113
+ }
114
+ const provider = resolveProvider(config.provider, keys);
115
+ return search(provider);
80
116
  }
81
117
  export { clampNumResults, DEFAULT_NUM_RESULTS };
@@ -1,4 +1,4 @@
1
- import { DEFAULT_NUM_RESULTS } from '../types.js';
1
+ import { DEFAULT_NUM_RESULTS, httpStatusError } from '../types.js';
2
2
  const BRAVE_SEARCH_URL = 'https://api.search.brave.com/res/v1/web/search';
3
3
  /** 导出供测试:recency → Brave freshness 代码映射。 */
4
4
  export function recencyToFreshness(recency) {
@@ -39,7 +39,7 @@ export const braveProvider = {
39
39
  });
40
40
  if (!response.ok) {
41
41
  const text = await response.text().catch(() => response.statusText);
42
- throw new Error(`Brave API error (${response.status}): ${text}`);
42
+ throw httpStatusError(`Brave API error (${response.status}): ${text}`, response.status);
43
43
  }
44
44
  const data = (await response.json());
45
45
  const sources = [];
@@ -1,3 +1,4 @@
1
+ import { httpStatusError } from '../types.js';
1
2
  const DUCKDUCKGO_SEARCH_URL = 'https://api.duckduckgo.com/';
2
3
  const AGENT_TOKEN = 'c0de-agent';
3
4
  function cleanText(value) {
@@ -49,7 +50,7 @@ async function callDuckDuckGoSearch(params) {
49
50
  });
50
51
  if (!response.ok) {
51
52
  const text = await response.text().catch(() => response.statusText);
52
- throw new Error(`DuckDuckGo API error (${response.status}): ${text}`);
53
+ throw httpStatusError(`DuckDuckGo API error (${response.status}): ${text}`, response.status);
53
54
  }
54
55
  return (await response.json());
55
56
  }
@@ -1,4 +1,4 @@
1
- import { DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS, MIN_NUM_RESULTS } from '../types.js';
1
+ import { DEFAULT_NUM_RESULTS, httpStatusError, MAX_NUM_RESULTS, MIN_NUM_RESULTS } from '../types.js';
2
2
  const TAVILY_SEARCH_URL = 'https://api.tavily.com/search';
3
3
  function clamp(n) {
4
4
  if (n === undefined || Number.isNaN(n))
@@ -62,7 +62,7 @@ async function callTavilySearch(apiKey, params) {
62
62
  if (!response.ok) {
63
63
  const raw = await response.text().catch(() => response.statusText);
64
64
  const msg = getErrorMessage(safeParse(raw)) ?? (raw.trim() || response.statusText);
65
- throw new Error(`Tavily API error (${response.status}): ${msg}`);
65
+ throw httpStatusError(`Tavily API error (${response.status}): ${msg}`, response.status);
66
66
  }
67
67
  return (await response.json());
68
68
  }
@@ -47,5 +47,9 @@ declare const MAX_NUM_RESULTS = 20;
47
47
  declare const MIN_NUM_RESULTS = 1;
48
48
  /** clamp 结果数到 [MIN, MAX];undefined/NaN 回退到 fallback。 */
49
49
  declare function clampNumResults(n: number | undefined, fallback?: number): number;
50
+ /** 携带 HTTP 状态码的错误(降级链按 401/403/429/5xx 判定是否换后端重试)。 */
51
+ declare function httpStatusError(message: string, status: number): Error & {
52
+ status: number;
53
+ };
50
54
  export type { Recency, WebSearchParams, WebSearchProvider, WebSearchProviderId, WebSearchResponse, WebSearchSource, };
51
- export { clampNumResults, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS, MIN_NUM_RESULTS };
55
+ export { clampNumResults, DEFAULT_NUM_RESULTS, httpStatusError, MAX_NUM_RESULTS, MIN_NUM_RESULTS };
@@ -15,4 +15,10 @@ function clampNumResults(n, fallback = DEFAULT_NUM_RESULTS) {
15
15
  return fallback;
16
16
  return Math.min(MAX_NUM_RESULTS, Math.max(MIN_NUM_RESULTS, Math.trunc(n)));
17
17
  }
18
- export { clampNumResults, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS, MIN_NUM_RESULTS };
18
+ /** 携带 HTTP 状态码的错误(降级链按 401/403/429/5xx 判定是否换后端重试)。 */
19
+ function httpStatusError(message, status) {
20
+ const err = new Error(message);
21
+ err.status = status;
22
+ return err;
23
+ }
24
+ export { clampNumResults, DEFAULT_NUM_RESULTS, httpStatusError, MAX_NUM_RESULTS, MIN_NUM_RESULTS };
@@ -1,3 +1,4 @@
1
+ import { getCurrentVersion } from './version.js';
1
2
  const DEFAULT_INTERVAL_MS = 60 * 60 * 1000;
2
3
  const DEFAULT_INITIAL_DELAY_MS = 10_000;
3
4
  function createUpdateScheduler(opts) {
@@ -14,8 +15,10 @@ function createUpdateScheduler(opts) {
14
15
  r = await opts.checkFn();
15
16
  }
16
17
  catch {
17
- // 异常降级:缓存 hasUpdate:false,但不触发 onUpdate(避免误报)。
18
- lastResult = { hasUpdate: false, currentVersion: '0.0.0', latestVersion: '0.0.0' };
18
+ // 异常降级:缓存 hasUpdate:false + checkError,且保留真实当前版本
19
+ //(此前写死 '0.0.0',前端/API 会展示错误版本号)。不触发 onUpdate(避免误报)。
20
+ const v = getCurrentVersion();
21
+ lastResult = { hasUpdate: false, currentVersion: v, latestVersion: v, checkError: true };
19
22
  return lastResult;
20
23
  }
21
24
  lastResult = r;
@@ -26,6 +26,17 @@ type SerializedEntry = {
26
26
  tokenCount: number;
27
27
  createdAt: number;
28
28
  };
29
+ /** 序列化后的终端元信息(热更新后新实例按原 id 重建 shell)。 */
30
+ type SerializedTerminal = {
31
+ id: string;
32
+ shell: string;
33
+ cwd: string;
34
+ title: string;
35
+ projectId?: string;
36
+ /** P3-7:用户勾选「更新后自动重启」的前台命令(如 `npm run dev`);
37
+ * 新实例重建 shell 后把该命令写入 stdin 重跑。仅显式勾选才携带。 */
38
+ command?: string;
39
+ };
29
40
  /** 热更新迁移快照(spec §18.2)。 */
30
41
  type SessionSnapshot = {
31
42
  version: string;
@@ -33,12 +44,15 @@ type SessionSnapshot = {
33
44
  entries: SerializedEntry[];
34
45
  config: unknown;
35
46
  timestamp: number;
47
+ /** P1:活跃终端元信息——进程无法续命,新实例据此在原位重建 shell(同 id,
48
+ * 前端持久化布局可无感重连)。旧快照缺省视为无终端。 */
49
+ terminals?: SerializedTerminal[];
36
50
  };
37
51
  /** 按 parentId 拓扑序排列 sessions,使父会话先于子会话插入(满足自引用 FK)。 */
38
52
  declare function orderSessionsByParent(list: SerializedSession[]): SerializedSession[];
39
- /** 从 DB 导出所有会话与条目为可序列化快照。 */
40
- declare function serializeSessions(handle: DB, config?: unknown): Promise<SessionSnapshot>;
53
+ /** 从 DB 导出所有会话与条目为可序列化快照。terminals 为活跃终端元信息(可选)。 */
54
+ declare function serializeSessions(handle: DB, config?: unknown, terminals?: SerializedTerminal[]): Promise<SessionSnapshot>;
41
55
  /** 把快照导入 DB(保留原始 id 与时间戳;父会话先插入以满足 FK)。 */
42
56
  declare function restoreSessions(handle: DB, snapshot: SessionSnapshot): Promise<void>;
43
- export type { SerializedEntry, SerializedSession, SessionSnapshot };
57
+ export type { SerializedEntry, SerializedSession, SerializedTerminal, SessionSnapshot };
44
58
  export { orderSessionsByParent, restoreSessions, serializeSessions };
@@ -54,8 +54,8 @@ function orderSessionsByParent(list) {
54
54
  visit(s);
55
55
  return ordered;
56
56
  }
57
- /** 从 DB 导出所有会话与条目为可序列化快照。 */
58
- async function serializeSessions(handle, config) {
57
+ /** 从 DB 导出所有会话与条目为可序列化快照。terminals 为活跃终端元信息(可选)。 */
58
+ async function serializeSessions(handle, config, terminals) {
59
59
  const [sRows, eRows] = await Promise.all([
60
60
  handle.db.select().from(sessions),
61
61
  handle.db.select().from(sessionEntries),
@@ -66,6 +66,7 @@ async function serializeSessions(handle, config) {
66
66
  entries: eRows.map(toSerializedEntry),
67
67
  config: config ?? null,
68
68
  timestamp: Date.now(),
69
+ ...(terminals && terminals.length > 0 ? { terminals } : {}),
69
70
  };
70
71
  }
71
72
  /** 把快照导入 DB(保留原始 id 与时间戳;父会话先插入以满足 FK)。 */
@@ -51,6 +51,8 @@ type HotUpdateOptions = {
51
51
  handoffPort?: number;
52
52
  /** 旧实例主服务端口;新实例 --port 复用(保证接管同一地址)。 */
53
53
  port?: number;
54
+ /** 旧实例绑定地址;新实例 --host 复用(保证接管同一地址)。 */
55
+ host?: string;
54
56
  /** 传给新实例的认证 token(经环境变量 C0DE_AUTH_TOKEN),保证新旧实例握手同 token。 */
55
57
  authToken?: string;
56
58
  /** 手动安装等待程序文件变更的超时(ms);默认 10 分钟,超时放弃等待。 */
@@ -170,7 +170,7 @@ function defaultSpawn(_snapshotPath, argv, opts) {
170
170
  }
171
171
  });
172
172
  }
173
- /** 组装新实例 argv(serve --restore <snapshot> [--port] [--handoff-port])。 */
173
+ /** 组装新实例 argv(serve --restore <snapshot> [--port] [--host] [--handoff-port])。 */
174
174
  function buildSpawnArgv(snapshotPath, opts) {
175
175
  const restoreFlag = opts.restoreFlag ?? '--restore';
176
176
  const argv = [
@@ -178,6 +178,7 @@ function buildSpawnArgv(snapshotPath, opts) {
178
178
  restoreFlag,
179
179
  snapshotPath,
180
180
  ...(opts.port !== undefined ? ['--port', String(opts.port)] : []),
181
+ ...(opts.host ? ['--host', opts.host] : []),
181
182
  ];
182
183
  if (opts.handoffPort !== undefined) {
183
184
  argv.push('--handoff-port', String(opts.handoffPort));
@@ -3,6 +3,9 @@ type UpdateCheckResult = {
3
3
  currentVersion: string;
4
4
  latestVersion: string;
5
5
  releaseNotes?: string;
6
+ /** P3-8:检查本身失败(网络/registry 异常),与「已是最新」区分——
7
+ * 消费方据此给出「稍后重试」而非误导性的「无更新」。 */
8
+ checkError?: boolean;
6
9
  };
7
10
  type CheckOptions = {
8
11
  fetchImpl?: typeof fetch;
@@ -48,16 +48,22 @@ async function checkForUpdate(opts = {}) {
48
48
  };
49
49
  try {
50
50
  const res = await fetchImpl(`${registry}/${pkg}/latest`);
51
- if (!res.ok)
51
+ if (!res.ok) {
52
+ // 非 200(404/5xx/限流)→ 检查失败而非无更新
53
+ result.checkError = true;
52
54
  return result;
55
+ }
53
56
  const data = (await res.json());
54
- if (!data.version)
57
+ if (!data.version) {
58
+ result.checkError = true;
55
59
  return result;
60
+ }
56
61
  result.latestVersion = data.version;
57
62
  result.hasUpdate = compareSemver(current, data.version) < 0;
58
63
  }
59
64
  catch {
60
- // 离线/网络错误:保守地报告无更新
65
+ // 离线/网络错误:保守地报告无更新,但标记检查失败供上层区分提示
66
+ result.checkError = true;
61
67
  }
62
68
  return result;
63
69
  }
@@ -0,0 +1,17 @@
1
+ CREATE TABLE "usage_events" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "call_id" uuid NOT NULL,
4
+ "session_id" uuid,
5
+ "project_id" text,
6
+ "provider" text NOT NULL,
7
+ "model" text NOT NULL,
8
+ "input_tokens" bigint DEFAULT 0 NOT NULL,
9
+ "output_tokens" bigint DEFAULT 0 NOT NULL,
10
+ "cache_read" bigint DEFAULT 0,
11
+ "cost" real,
12
+ "timestamp" bigint NOT NULL
13
+ );
14
+ --> statement-breakpoint
15
+ CREATE UNIQUE INDEX "uq_usage_events_call" ON "usage_events" USING btree ("call_id");--> statement-breakpoint
16
+ CREATE INDEX "idx_usage_events_project_ts" ON "usage_events" USING btree ("project_id","timestamp");--> statement-breakpoint
17
+ CREATE INDEX "idx_usage_events_ts" ON "usage_events" USING btree ("timestamp");
@@ -0,0 +1,12 @@
1
+ CREATE TABLE "app_meta" (
2
+ "key" text PRIMARY KEY NOT NULL,
3
+ "value" text NOT NULL
4
+ );
5
+ --> statement-breakpoint
6
+ ALTER TABLE "kanban_boards" DROP CONSTRAINT "kanban_boards_project_id_projects_id_fk";
7
+ --> statement-breakpoint
8
+ ALTER TABLE "kanban_boards" ALTER COLUMN "project_id" DROP NOT NULL;--> statement-breakpoint
9
+ ALTER TABLE "kanban_boards" ADD COLUMN "deleted_at" timestamp with time zone DEFAULT null;--> statement-breakpoint
10
+ ALTER TABLE "kanban_boards" ADD COLUMN "deleted_project_name" text;--> statement-breakpoint
11
+ ALTER TABLE "projects" ADD COLUMN "trusted_at" timestamp with time zone DEFAULT null;--> statement-breakpoint
12
+ ALTER TABLE "kanban_boards" ADD CONSTRAINT "kanban_boards_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE set null ON UPDATE no action;
@@ -0,0 +1 @@
1
+ ALTER TABLE "kanban_boards" ADD COLUMN "purge_pending_at" timestamp with time zone DEFAULT null;
@@ -0,0 +1 @@
1
+ ALTER TABLE "kanban_boards" ADD COLUMN "deleted_project_worktree" text;
@@ -0,0 +1 @@
1
+ ALTER TABLE "projects" ADD COLUMN "risk_fingerprint" text DEFAULT null;