c0de-agent 1.9.0 → 1.10.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 (65) hide show
  1. package/README.md +21 -7
  2. package/dist/cli/commands/acp.js +2 -0
  3. package/dist/cli/commands/chat.js +2 -0
  4. package/dist/cli/commands/config.js +8 -4
  5. package/dist/cli/commands/serve.d.ts +1 -0
  6. package/dist/cli/commands/serve.js +4 -1
  7. package/dist/cli/deps.d.ts +7 -5
  8. package/dist/cli/deps.js +23 -7
  9. package/dist/cli/index.d.ts +1 -1
  10. package/dist/cli/index.js +68 -37
  11. package/dist/cli/modes/print.d.ts +3 -1
  12. package/dist/cli/modes/print.js +21 -4
  13. package/dist/core/agent.js +3 -4
  14. package/dist/core/config.d.ts +18 -1
  15. package/dist/core/config.js +55 -7
  16. package/dist/core/loop.js +1 -1
  17. package/dist/core/workflows/discovery.d.ts +1 -1
  18. package/dist/core/workflows/index.d.ts +1 -1
  19. package/dist/db/schema.d.ts +17 -0
  20. package/dist/db/schema.js +3 -1
  21. package/dist/server/agent-manager.d.ts +15 -0
  22. package/dist/server/agent-manager.js +35 -11
  23. package/dist/server/app.js +2 -1
  24. package/dist/server/context.js +2 -0
  25. package/dist/server/middleware/cors.d.ts +11 -2
  26. package/dist/server/middleware/cors.js +15 -12
  27. package/dist/server/routes/chat.js +211 -150
  28. package/dist/server/routes/config.js +7 -3
  29. package/dist/server/routes/session.js +6 -2
  30. package/dist/server/routes/terminal.js +12 -0
  31. package/dist/server/routes/update.d.ts +1 -0
  32. package/dist/server/routes/update.js +11 -4
  33. package/dist/server/routes/workflows.js +2 -2
  34. package/dist/server/server.d.ts +24 -1
  35. package/dist/server/server.js +107 -26
  36. package/dist/server/types.d.ts +9 -1
  37. package/dist/session/branch.d.ts +10 -2
  38. package/dist/session/branch.js +98 -29
  39. package/dist/session/compaction.js +1 -0
  40. package/dist/session/context.d.ts +3 -2
  41. package/dist/session/context.js +36 -3
  42. package/dist/session/snapshot.d.ts +9 -3
  43. package/dist/session/snapshot.js +45 -4
  44. package/dist/session/squash.js +1 -0
  45. package/dist/session/types.d.ts +2 -0
  46. package/dist/shared/types/config.d.ts +7 -6
  47. package/dist/shared/utils/path.d.ts +2 -3
  48. package/dist/shared/utils/path.js +20 -4
  49. package/dist/tools/builtin/todo.js +5 -4
  50. package/dist/tools/index.d.ts +14 -3
  51. package/dist/tools/index.js +36 -14
  52. package/dist/update/index.d.ts +1 -1
  53. package/dist/update/index.js +1 -1
  54. package/dist/update/ipc.d.ts +32 -5
  55. package/dist/update/ipc.js +53 -7
  56. package/dist/update/updater.d.ts +7 -3
  57. package/dist/update/updater.js +14 -6
  58. package/dist/update/version.d.ts +6 -1
  59. package/dist/update/version.js +18 -3
  60. package/drizzle/0005_snapshot_mtime.sql +1 -0
  61. package/drizzle/meta/0005_snapshot.json +855 -0
  62. package/drizzle/meta/_journal.json +7 -0
  63. package/package.json +1 -7
  64. package/dist/mcp/index.d.ts +0 -1
  65. package/dist/mcp/index.js +0 -3
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  - **本地优先**:会话与索引存储在 PGLite(浏览器/进程内 PostgreSQL),数据随用随取。
9
9
  - **多项目**:工作区隔离的多项目管理,每个项目独立会话历史与配置。
10
10
  - **工具系统**:内置文件读写、bash、grep、glob、编辑等工具,可按需启用/禁用。
11
- - **权限模式**:`default` / `yolo` 等授权模式,控制工具执行前是否需用户确认。
11
+ - **权限模式**:`default` / `auto` 等授权模式,控制工具执行前是否需用户确认。
12
12
  - **会话可恢复**:会话快照序列化/反序列化,支持进程重启后无缝恢复。
13
13
  - **无感知热更新**:后台定期检查新版本,序列化当前会话 → npm 自更新 → 端口 handoff 接管,用户无感升级。
14
14
  - **Web 搜索**:内置 websearch 工具,支持多搜索引擎。
@@ -27,6 +27,15 @@ pnpm add -g c0de-agent
27
27
 
28
28
  要求 Node.js >= 22.0.0。
29
29
 
30
+ ## 安全
31
+
32
+ - **认证**:`security.authEnabled` 默认开启。首次启动自动生成认证 token(持久化于数据目录 `auth-token` 文件),浏览器访问时经启动打印的 URL `?token=` 参数自动保存到 localStorage 并从地址栏移除;后续 API 请求与终端 WebSocket 均携带该 token,服务端校验不通过返回 401。
33
+ - token 解析优先级:`security.token` 配置 > 环境变量 `C0DE_AUTH_TOKEN` > 数据目录 token 文件 > 自动生成并持久化。
34
+ - 显式关闭:`security.authEnabled: false`(本机单用户且端口仅本机可达时)。
35
+ - **CORS/Origin 校验**:仅放行本机回环 origin 与 `security.allowedOrigins` 中显式配置的 origin;WebSocket 升级在服务端独立校验 Origin(浏览器 WS 不受 CORS 约束)。
36
+ - **监听地址**:默认绑定 `0.0.0.0`(便于容器/远程访问),安全性由 token + allowedOrigins 保证;需要时可用 `security.allowedOrigins` 收紧或自行反向代理限制。
37
+ - **热更新交接**:新实例经环境变量继承 token,请求旧实例 `/handoff` 时须携带匹配的 Bearer token,防止任意本地进程借 handoff 杀掉服务。
38
+
30
39
  ## 使用
31
40
 
32
41
  ```bash
@@ -77,14 +86,19 @@ pnpm format
77
86
 
78
87
  ```
79
88
  src/
80
- ├── cli/ # CLI 入口与命令(serve / chat / update)
81
- ├── core/ # 配置、agent 注册、权限、工具注册
82
- ├── llm/ # LLM provider 抽象与调用
83
- ├── server/ # Hono HTTP 服务、SSE 流、路由、handoff
84
- ├── session/ # 会话上下文与消息转换
89
+ ├── cli/ # CLI 入口与命令(serve / chat / acp / update / config / init / plugin
90
+ ├── core/ # 配置、agent 注册与主循环(loop/ 子模块)、权限、prompt、工作流、斜杠命令
91
+ ├── llm/ # LLM provider 抽象、registry 与重试策略
92
+ ├── server/ # Hono HTTP 服务、SSE 流、路由、鉴权/CORS、交互式权限、终端(node-pty)
93
+ ├── session/ # 会话上下文、消息、compaction、branch/squash、快照
94
+ ├── project/ # 多项目解析与检测(.git、package.json 等)
95
+ ├── db/ # Drizzle schema 与迁移(PGLite)
96
+ ├── tools/ # 内置工具实现(read/write/edit/bash/grep/glob/todo/kanban/dap/websearch…)
97
+ ├── plugins/ # 插件系统(12 个生命周期钩子)
98
+ ├── dap/ # Debug Adapter Protocol 会话与协议编解码
85
99
  ├── shared/ # 前后端共享类型
86
- ├── tools/ # 内置工具实现
87
100
  ├── update/ # 版本检查、热更新调度器、会话快照
101
+ ├── kanban/ # 看板状态存储
88
102
  └── web/ # React 前端(组件、视图、服务、hooks)
89
103
  ```
90
104
 
@@ -18,7 +18,9 @@ function createAcpHandlers(config, deps, opts) {
18
18
  const message = params.message;
19
19
  if (!message)
20
20
  throw new Error('chat: message is required');
21
+ const sessionId = params.sessionId;
21
22
  const text = await runPrintMode(config, message, deps, {
23
+ ...(sessionId ? { sessionId } : {}),
22
24
  onEvent: (e) => opts.onEvent('event', e),
23
25
  });
24
26
  return { text };
@@ -7,8 +7,10 @@ async function runChatCommand(ctx) {
7
7
  const err = ctx.stderr ?? process.stderr.write.bind(process.stderr);
8
8
  const format = ctx.args.options.format ?? 'text';
9
9
  const model = ctx.args.options.model;
10
+ const continueId = ctx.args.options.continue;
10
11
  const text = await runPrintMode(ctx.config, message, ctx.deps, {
11
12
  ...(model ? { model } : {}),
13
+ ...(continueId ? { sessionId: continueId } : {}),
12
14
  onEvent: (e) => {
13
15
  if (e._tag === 'tool_call_start')
14
16
  err(`[tool] ${e.tool}\n`);
@@ -1,4 +1,4 @@
1
- import { loadConfig, mergeConfig, saveConfig } from '../../core/config.js';
1
+ import { loadConfigScopes, mergeConfig, mergeRaw, saveConfigScoped, } from '../../core/config.js';
2
2
  /** 按 a.b.c 点路径从对象取值;找不到抛错。 */
3
3
  function getByPath(obj, path) {
4
4
  const parts = path.split('.');
@@ -48,7 +48,9 @@ function setPathPatch(path, value) {
48
48
  async function runConfigCommand(ctx) {
49
49
  const write = ctx.write ?? process.stdout.write.bind(process.stdout);
50
50
  const sub = ctx.args.positionals[0];
51
- const config = await loadConfig(ctx.cwd);
51
+ const scopes = loadConfigScopes(ctx.cwd);
52
+ // get 展示用合并视图(global ← project,含默认值),与 loadConfig 语义一致。
53
+ const config = mergeConfig(scopes.global, scopes.project);
52
54
  if (sub === 'get') {
53
55
  const key = ctx.args.positionals[1];
54
56
  if (!key) {
@@ -67,8 +69,10 @@ async function runConfigCommand(ctx) {
67
69
  if (rawValue === undefined)
68
70
  throw new Error('config set: a value is required');
69
71
  const scope = ctx.args.options.global ? 'global' : 'project';
70
- const merged = mergeConfig(config, setPathPatch(key, coerce(rawValue)));
71
- await saveConfig(merged, scope, ctx.cwd);
72
+ // 只把 patch 合并进目标作用域的原始文件,不写入默认值与其他作用域的配置(P2-3)。
73
+ const scopeCfg = scope === 'global' ? scopes.global : scopes.project;
74
+ const next = mergeRaw(scopeCfg ?? {}, setPathPatch(key, coerce(rawValue)));
75
+ await saveConfigScoped(scope, ctx.cwd, next);
72
76
  write(`Set ${key} (scope: ${scope})\n`);
73
77
  return;
74
78
  }
@@ -2,6 +2,7 @@ import type { StartServerOptions } from '../../server/index.js';
2
2
  import type { CommandArgs } from '../parser.js';
3
3
  type RunningHandle = {
4
4
  port: number;
5
+ authToken?: string;
5
6
  close(): Promise<void>;
6
7
  };
7
8
  type ServeCommandContext = {
@@ -12,7 +12,10 @@ async function runServeCommand(ctx) {
12
12
  ...(restore ? { restoreFrom: restore } : {}),
13
13
  ...(handoffPort ? { handoffPort } : {}),
14
14
  });
15
- const url = `http://localhost:${handle.port}`;
15
+ // P0-3:认证 token(authEnabled 显式关闭时为 undefined)挂到 URL,
16
+ // 浏览器首访存入 localStorage 并从地址栏移除。
17
+ const tokenQuery = handle.authToken ? `?token=${encodeURIComponent(handle.authToken)}` : '';
18
+ const url = `http://localhost:${handle.port}${tokenQuery}`;
16
19
  (ctx.banner ?? printStartupBanner)(url);
17
20
  if (shouldOpen) {
18
21
  const opener = ctx.opener ?? ((u) => openBrowser(u));
@@ -9,10 +9,12 @@ 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
- /** 安全策略:只读工具(permission: 'auto',如 read/grep/glob/websearch)自动放行;
13
- * 写/执行工具(permission: 'ask',如 bash/write/edit)返回 ask 需确认;
14
- * permission: 'deny' 拒绝。复用 tools autoAllowChecker(按工具自身声明的权限分级)。 */
15
- declare const readOnlySafeChecker: PermissionChecker;
12
+ /**
13
+ * 非交互模式(print)安全策略(P1-6):ask 工具**直接拒绝**并给出可操作提示
14
+ * (-y serve),而非返回 permission_required LLM 在无确认通道下无限重试。
15
+ * 拒绝原因作为 tool result 交给模型,模型会向用户转述。
16
+ */
17
+ declare const nonInteractiveSafeChecker: PermissionChecker;
16
18
  /** 把 config.providers 注册到新建的 LLM registry。 */
17
19
  declare function buildLLMRegistry(config: Config): Registry;
18
20
  type BuildDepsOptions = {
@@ -27,4 +29,4 @@ type BuildDepsOptions = {
27
29
  /** 组装完整 LoopDeps(默认 safe 放行 + 默认工具注册表)。 */
28
30
  declare function buildAgentDeps(config: Config, opts: BuildDepsOptions): Promise<LoopDeps>;
29
31
  export type { BuildDepsOptions, PermissionStrategy };
30
- export { buildAgentDeps, buildLLMRegistry, fullyAutoApproveChecker, readOnlySafeChecker };
32
+ export { buildAgentDeps, buildLLMRegistry, fullyAutoApproveChecker, nonInteractiveSafeChecker };
package/dist/cli/deps.js CHANGED
@@ -10,10 +10,24 @@ const fullyAutoApproveChecker = {
10
10
  },
11
11
  confirm: (_toolCallId, _approved) => { },
12
12
  };
13
- /** 安全策略:只读工具(permission: 'auto',如 read/grep/glob/websearch)自动放行;
14
- * 写/执行工具(permission: 'ask',如 bash/write/edit)返回 ask 需确认;
15
- * permission: 'deny' 拒绝。复用 tools autoAllowChecker(按工具自身声明的权限分级)。 */
16
- const readOnlySafeChecker = autoAllowChecker;
13
+ /**
14
+ * 非交互模式(print)安全策略(P1-6):ask 工具**直接拒绝**并给出可操作提示
15
+ * (-y serve),而非返回 permission_required LLM 在无确认通道下无限重试。
16
+ * 拒绝原因作为 tool result 交给模型,模型会向用户转述。
17
+ */
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
+ };
17
31
  /** 把 config.providers 注册到新建的 LLM registry。 */
18
32
  function buildLLMRegistry(config) {
19
33
  const registry = createRegistry();
@@ -44,8 +58,10 @@ function resolvePermissionChecker(config, strategy) {
44
58
  if (strategy === 'full-auto')
45
59
  return fullyAutoApproveChecker;
46
60
  if (strategy === 'safe')
47
- return readOnlySafeChecker;
48
- return config.permission.defaultMode === 'auto' ? fullyAutoApproveChecker : readOnlySafeChecker;
61
+ return nonInteractiveSafeChecker;
62
+ return config.permission.defaultMode === 'auto'
63
+ ? fullyAutoApproveChecker
64
+ : nonInteractiveSafeChecker;
49
65
  }
50
66
  /** 组装完整 LoopDeps(默认 safe 放行 + 默认工具注册表)。 */
51
67
  async function buildAgentDeps(config, opts) {
@@ -70,4 +86,4 @@ async function buildAgentDeps(config, opts) {
70
86
  };
71
87
  return deps;
72
88
  }
73
- export { buildAgentDeps, buildLLMRegistry, fullyAutoApproveChecker, readOnlySafeChecker };
89
+ export { buildAgentDeps, buildLLMRegistry, fullyAutoApproveChecker, nonInteractiveSafeChecker };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import type { CommandSpec } from './parser.js';
2
+ import { type CommandSpec } from './parser.js';
3
3
  declare const COMMANDS: CommandSpec[];
4
4
  type DispatchOverrides = {
5
5
  runServe?: () => Promise<void>;
package/dist/cli/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // src/cli/index.ts — c0de CLI bin entry.
3
+ import { mkdirSync } from 'node:fs';
3
4
  import { fileURLToPath } from 'node:url';
4
- import { parseArgs } from 'node:util';
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
8
  import { runAcpCommand } from './commands/acp.js';
8
9
  import { runChatCommand } from './commands/chat.js';
9
10
  import { runConfigCommand } from './commands/config.js';
@@ -12,6 +13,7 @@ import { runPluginCommand } from './commands/plugin.js';
12
13
  import { runServeCommand } from './commands/serve.js';
13
14
  import { runUpdateCommand } from './commands/update.js';
14
15
  import { buildAgentDeps } from './deps.js';
16
+ import { parseCommand } from './parser.js';
15
17
  const COMMANDS = [
16
18
  {
17
19
  name: 'chat',
@@ -20,6 +22,7 @@ const COMMANDS = [
20
22
  { name: 'model', type: 'string' },
21
23
  { name: 'format', type: 'string' },
22
24
  { name: 'yes', type: 'boolean', short: 'y' },
25
+ { name: 'continue', type: 'string' },
23
26
  ],
24
27
  },
25
28
  {
@@ -58,6 +61,63 @@ const COMMANDS = [
58
61
  options: [{ name: 'apply', type: 'boolean' }],
59
62
  },
60
63
  ];
64
+ /** 判别持久库错误是否为「库被其它 c0de 进程占用」的锁冲突特征(读 server.ts 确认):
65
+ * - acquireDevDbLock 对 live lock 抛出的 "Database is locked by another c0de process (PID …)";
66
+ * - PGLite 单写者 WASM 崩溃 RuntimeError "Aborted()"(serve 未持锁但占用 dataDir 时的兜底特征)。
67
+ * 其余错误(磁盘故障、迁移失败等)不属于锁冲突,不得误报 serve。 */
68
+ function isDbLockConflict(err) {
69
+ if (!(err instanceof Error))
70
+ return false;
71
+ if (err.message.includes('Database is locked by another c0de process'))
72
+ return true;
73
+ return err.name === 'RuntimeError' && /abort/i.test(err.message);
74
+ }
75
+ /** 封装 agent 依赖生命周期:加载配置 → 建库迁移 → 组装 deps → 使用后关库。
76
+ * chat / acp 两个命令复用同一套逻辑(此前在 dispatch 中各写一份)。
77
+ * 数据库优先用全局 dataDir 持久库(会话可恢复,--continue 可用);
78
+ * 仅当错误是锁冲突特征(dataDir 被 serve 实例占用)时退化为内存库并 stderr 提示,
79
+ * 其它持久库错误如实上抛(main 统一打印 + 非零退出);
80
+ * 带 --continue 时内存库不含历史会话,直接报错退出而非误导性的 session not found。 */
81
+ async function withAgentDeps(cwd, opts, fn) {
82
+ const config = await loadConfig(cwd);
83
+ const dataDir = resolveDbDir();
84
+ let db;
85
+ let holdLock = false;
86
+ try {
87
+ mkdirSync(dataDir, { recursive: true });
88
+ acquireDevDbLock(dataDir);
89
+ holdLock = true;
90
+ db = await createDB({ driver: 'pglite', dataDir });
91
+ await migrateDB(db);
92
+ }
93
+ catch (err) {
94
+ if (holdLock)
95
+ releaseDevDbLock(dataDir);
96
+ holdLock = false;
97
+ if (!isDbLockConflict(err))
98
+ throw err;
99
+ if (opts.continueSessionId) {
100
+ throw new Error(`无法续聊会话 ${opts.continueSessionId}:持久库被 c0de serve 占用,内存模式无法续聊。\n` +
101
+ `请停止 serve 后重试,或去掉 --continue 开启新会话。`);
102
+ }
103
+ process.stderr.write('[c0de] c0de serve 正在运行,本次会话不持久化(内存模式)\n');
104
+ db = await createDB({ driver: 'pglite' });
105
+ await migrateDB(db);
106
+ }
107
+ try {
108
+ const deps = await buildAgentDeps(config, {
109
+ db,
110
+ cwd,
111
+ ...(opts.strategy ? { permissionStrategy: opts.strategy } : {}),
112
+ });
113
+ await fn(config, deps);
114
+ }
115
+ finally {
116
+ await db.close();
117
+ if (holdLock)
118
+ releaseDevDbLock(dataDir);
119
+ }
120
+ }
61
121
  async function dispatch(argv, overrides = {}) {
62
122
  const [command, ...rest] = argv;
63
123
  const name = command ?? 'serve';
@@ -68,16 +128,7 @@ async function dispatch(argv, overrides = {}) {
68
128
  const spec = COMMANDS.find((c) => c.name === name);
69
129
  if (!spec)
70
130
  throw new Error(`cli: unknown command "${name}"`);
71
- const { values, positionals } = parseArgs({
72
- options: Object.fromEntries((spec.options ?? []).map((o) => [
73
- o.name,
74
- { type: o.type, ...(o.short ? { short: o.short } : {}) },
75
- ])),
76
- args: rest,
77
- allowPositionals: true,
78
- strict: true,
79
- });
80
- const args = { options: values, positionals };
131
+ const args = parseCommand(spec, rest);
81
132
  const cwd = process.cwd();
82
133
  switch (name) {
83
134
  case 'serve': {
@@ -85,21 +136,10 @@ async function dispatch(argv, overrides = {}) {
85
136
  return;
86
137
  }
87
138
  case 'chat': {
88
- const config = await loadConfig(cwd);
89
- const db = await createDB({ driver: 'pglite' });
90
- await migrateDB(db);
91
- try {
92
- // --yes / -y 显式放行写操作;否则按 config.permission.defaultMode 决定(默认 safe)。
93
- const deps = await buildAgentDeps(config, {
94
- db,
95
- cwd,
96
- ...(args.options.yes ? { permissionStrategy: 'full-auto' } : {}),
97
- });
98
- await runChatCommand({ args, config, deps });
99
- }
100
- finally {
101
- await db.close();
102
- }
139
+ // --yes / -y 显式放行写操作;否则按 config.permission.defaultMode 决定(默认 safe)。
140
+ const strategy = args.options.yes ? 'full-auto' : undefined;
141
+ const continueId = args.options.continue;
142
+ await withAgentDeps(cwd, { ...(strategy ? { strategy } : {}), ...(continueId ? { continueSessionId: continueId } : {}) }, (config, deps) => runChatCommand({ args, config, deps }));
103
143
  return;
104
144
  }
105
145
  case 'init': {
@@ -115,17 +155,8 @@ async function dispatch(argv, overrides = {}) {
115
155
  return;
116
156
  }
117
157
  case 'acp': {
118
- const config = await loadConfig(cwd);
119
- const db = await createDB({ driver: 'pglite' });
120
- await migrateDB(db);
121
- try {
122
- // ACP 非交互:所有工具放行(编辑器侧自行控制执行授权)。
123
- const deps = await buildAgentDeps(config, { db, cwd, permissionStrategy: 'full-auto' });
124
- await runAcpCommand({ config, deps });
125
- }
126
- finally {
127
- await db.close();
128
- }
158
+ // ACP 非交互:所有工具放行(编辑器侧自行控制执行授权)。
159
+ await withAgentDeps(cwd, { strategy: 'full-auto' }, (config, deps) => runAcpCommand({ config, deps }));
129
160
  return;
130
161
  }
131
162
  case 'update': {
@@ -5,12 +5,14 @@ type PrintOptions = {
5
5
  model?: string;
6
6
  format?: 'text' | 'json';
7
7
  maxTokens?: number;
8
+ /** 续接已有会话(--continue):加载该会话历史作为上下文。 */
9
+ sessionId?: string;
8
10
  /** 事件观察回调(CLI 用于把 tool/thinking 写到 stderr)。 */
9
11
  onEvent?: (event: AgentEvent) => void;
10
12
  };
11
13
  /** 从事件流累积 assistant 文本。纯函数。 */
12
14
  declare function collectAssistantText(events: AgentEvent[]): string;
13
- /** Print 模式:临时会话 agent → 收集文本 → 返回。 */
15
+ /** Print 模式:临时会话(或 --continue 指定会话)→ agent → 收集文本 → 返回。 */
14
16
  declare function runPrintMode(config: Config, message: string, deps: LoopDeps, opts?: PrintOptions): Promise<string>;
15
17
  export type { PrintOptions };
16
18
  export { collectAssistantText, runPrintMode };
@@ -1,5 +1,6 @@
1
1
  import { createAgent, runAgent } from '../../core/agent.js';
2
- import { createSession } from '../../session/session.js';
2
+ import { createSession, getSession } from '../../session/session.js';
3
+ import { resolveEnabledToolNames } from '../../tools/index.js';
3
4
  /** 从事件流累积 assistant 文本。纯函数。 */
4
5
  function collectAssistantText(events) {
5
6
  let text = '';
@@ -9,13 +10,29 @@ function collectAssistantText(events) {
9
10
  }
10
11
  return text;
11
12
  }
12
- /** Print 模式:临时会话 agent 收集文本 返回。 */
13
+ /** sessions.id uuid 列:非 uuid --continue 输入会让 PG 直接抛原始
14
+ * "invalid input syntax for type uuid" SQL 错误。前置校验统一转译为
15
+ * 「session not found」(与不存在的会话同语义),不泄漏 SQL 原文。 */
16
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+ /** Print 模式:临时会话(或 --continue 指定会话)→ agent → 收集文本 → 返回。 */
13
18
  async function runPrintMode(config, message, deps, opts = {}) {
14
- const session = await createSession(deps.db, 'cli-print');
19
+ let session;
20
+ if (opts.sessionId) {
21
+ if (!UUID_PATTERN.test(opts.sessionId))
22
+ throw new Error(`session not found: ${opts.sessionId}`);
23
+ const existing = await getSession(deps.db, opts.sessionId);
24
+ if (!existing)
25
+ throw new Error(`session not found: ${opts.sessionId}`);
26
+ session = existing;
27
+ }
28
+ else {
29
+ session = await createSession(deps.db, 'cli-print');
30
+ }
15
31
  const agentConfig = {
16
32
  provider: config.defaultProvider,
17
33
  model: opts.model ?? config.defaultModel,
18
- tools: config.tools.enabled,
34
+ // 工具集:enabled 非空 → enabled ∩ registered;空 → 全部 registered(disabled 已在 registry 层过滤)。
35
+ tools: resolveEnabledToolNames(deps.toolRegistry, config),
19
36
  plugins: config.plugins.enabled,
20
37
  ...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
21
38
  };
@@ -1,12 +1,11 @@
1
+ import { resolveRoute } from '../llm/registry.js';
1
2
  import { appendMessage, getMessages } from '../session/message.js';
2
3
  import { getLLMSegments } from '../session/session.js';
3
4
  import { generateId } from '../shared/index.js';
4
- import { listTools } from '../tools/registry.js';
5
5
  import { getLatestTodoPhasesFromMessages } from '../tools/builtin/todo.js';
6
- import { estimateBudget } from './context.js';
7
- import { createTokenBudget } from './context.js';
6
+ import { listTools } from '../tools/registry.js';
7
+ import { createTokenBudget, estimateBudget } from './context.js';
8
8
  import { agentLoop } from './loop.js';
9
- import { resolveRoute } from '../llm/registry.js';
10
9
  import { DEFAULT_SESSION_TITLE, generateSessionTitle } from './title.js';
11
10
  async function createAgent(session, config, deps) {
12
11
  const messages = await getMessages(deps.db, session.id);
@@ -1,7 +1,24 @@
1
1
  import type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig } from '../shared/types/config.js';
2
2
  declare const DEFAULT_CONFIG: Config;
3
3
  declare function mergeConfig(...configs: (Partial<Config> | undefined)[]): Config;
4
+ /**
5
+ * 读取 global/project 两个作用域的**原始文件内容**(不经 DEFAULT 合并)。
6
+ * 供配置持久化使用:写回某个作用域时只落该作用域应有的键,
7
+ * 避免把合并结果(含默认值与另一作用域的配置)整体序列化进文件。
8
+ */
9
+ declare function loadConfigScopes(projectDir?: string): {
10
+ global: Partial<Config> | undefined;
11
+ project: Partial<Config> | undefined;
12
+ };
13
+ /**
14
+ * 不注入默认值的深合并:仅合并传入对象的自有键。
15
+ * 数组整体替换(providers 等列表语义);嵌套普通对象递归合并。
16
+ * 用于「patch 合并进某个作用域的原始文件」后再写盘。
17
+ */
18
+ declare function mergeRaw(...cfgs: (Record<string, unknown> | undefined)[]): Record<string, unknown>;
19
+ /** 把 raw JSON 写回指定作用域配置文件(不含默认值)。 */
20
+ declare function saveConfigScoped(scope: 'global' | 'project', projectDir: string | undefined, data: Record<string, unknown>): Promise<void>;
4
21
  declare function loadConfig(projectDir?: string): Promise<Config>;
5
22
  declare function saveConfig(config: Config, scope: 'global' | 'project', projectDir?: string): Promise<void>;
6
23
  export type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig, };
7
- export { DEFAULT_CONFIG, loadConfig, mergeConfig, saveConfig };
24
+ export { DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfig, saveConfigScoped, };
@@ -16,19 +16,18 @@ const DEFAULT_CONFIG = {
16
16
  keepRecentTokens: 4000,
17
17
  midTurnEnabled: false,
18
18
  },
19
- tools: {
20
- enabled: ['read', 'write', 'edit', 'glob', 'grep', 'bash', 'todo', 'websearch'],
21
- disabled: [],
22
- },
19
+ // enabled 为空 = 启用全部注册工具(registry 层过滤 disabled)。
20
+ // 非空 = 默认工具集。CLI print Web chat 统一经 resolveEnabledToolNames 解析。
21
+ tools: { enabled: [], disabled: [] },
23
22
  plugins: { enabled: [] },
24
23
  mcpServers: [],
25
24
  slashCommands: { enabled: ['/compact', '/model', '/clear', '/help', '/fork', '/config'] },
26
25
  toolMetrics: { enabled: true, threshold: 0.8, minSamples: 5 },
27
- security: { authEnabled: false, allowedOrigins: [] },
26
+ security: { authEnabled: true, allowedOrigins: [] },
28
27
  websearch: { provider: 'auto' },
29
28
  agents: { dir: '.c0de/agents', subagentConcurrency: 3 },
30
29
  permission: { defaultMode: 'default' },
31
- update: { enabled: true, intervalMs: 60 * 60 * 1000, initialDelayMs: 10_000, autoApply: false },
30
+ update: { enabled: true, intervalMs: 60 * 60 * 1000, initialDelayMs: 10_000 },
32
31
  theme: 'system',
33
32
  locale: 'en',
34
33
  };
@@ -69,6 +68,55 @@ function readJsonIfExists(path) {
69
68
  return undefined;
70
69
  }
71
70
  }
71
+ /**
72
+ * 读取 global/project 两个作用域的**原始文件内容**(不经 DEFAULT 合并)。
73
+ * 供配置持久化使用:写回某个作用域时只落该作用域应有的键,
74
+ * 避免把合并结果(含默认值与另一作用域的配置)整体序列化进文件。
75
+ */
76
+ function loadConfigScopes(projectDir) {
77
+ const globalPath = join(homedir(), GLOBAL_CONFIG_DIR, CONFIG_FILENAME);
78
+ const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
79
+ return { global: readJsonIfExists(globalPath), project: readJsonIfExists(projectPath) };
80
+ }
81
+ /**
82
+ * 不注入默认值的深合并:仅合并传入对象的自有键。
83
+ * 数组整体替换(providers 等列表语义);嵌套普通对象递归合并。
84
+ * 用于「patch 合并进某个作用域的原始文件」后再写盘。
85
+ */
86
+ function mergeRaw(...cfgs) {
87
+ const result = {};
88
+ for (const cfg of cfgs) {
89
+ if (!cfg)
90
+ continue;
91
+ for (const [key, val] of Object.entries(cfg)) {
92
+ if (val === undefined)
93
+ continue;
94
+ const current = result[key];
95
+ if (val !== null &&
96
+ typeof val === 'object' &&
97
+ !Array.isArray(val) &&
98
+ current !== null &&
99
+ typeof current === 'object' &&
100
+ !Array.isArray(current)) {
101
+ result[key] = mergeRaw(current, val);
102
+ }
103
+ else {
104
+ result[key] = val;
105
+ }
106
+ }
107
+ }
108
+ return result;
109
+ }
110
+ /** 把 raw JSON 写回指定作用域配置文件(不含默认值)。 */
111
+ async function saveConfigScoped(scope, projectDir, data) {
112
+ const dir = scope === 'global'
113
+ ? join(homedir(), GLOBAL_CONFIG_DIR)
114
+ : join(projectDir ?? process.cwd(), '.c0de');
115
+ if (!existsSync(dir))
116
+ mkdirSync(dir, { recursive: true });
117
+ const path = join(dir, CONFIG_FILENAME);
118
+ writeFileSync(path, JSON.stringify(data, null, 2), 'utf-8');
119
+ }
72
120
  async function loadConfig(projectDir) {
73
121
  const globalPath = join(homedir(), GLOBAL_CONFIG_DIR, CONFIG_FILENAME);
74
122
  const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
@@ -85,4 +133,4 @@ async function saveConfig(config, scope, projectDir) {
85
133
  const path = join(dir, CONFIG_FILENAME);
86
134
  writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8');
87
135
  }
88
- export { DEFAULT_CONFIG, loadConfig, mergeConfig, saveConfig };
136
+ export { DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfig, saveConfigScoped, };
package/dist/core/loop.js CHANGED
@@ -77,7 +77,7 @@ export async function* agentLoop(state, deps) {
77
77
  }
78
78
  state.status = { _tag: 'running', turnCount: turn };
79
79
  const steering = drainSteering(state);
80
- const { entries, snapshots } = await getSessionContext(deps.db, state.session.id);
80
+ const { entries, snapshots } = await getSessionContext(deps.db, state.session.id, deps.cwd);
81
81
  let chatMessages = entriesToChatMessages(entries, snapshots);
82
82
  // steering 消息必须插入到最后一条 user 消息之前(而非末尾 push),
83
83
  // 使 notice 在相关 user 消息的同一 turn 生效。
@@ -28,5 +28,5 @@ type SaveResult = {
28
28
  * - 返回 { ok, filePath, meta } 或 { ok: false, error }
29
29
  */
30
30
  declare function saveWorkflow(name: string, source: string, target?: SaveTarget, projectDir?: string): Promise<SaveResult>;
31
- export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow };
32
31
  export type { SaveResult, SaveTarget };
32
+ export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow };
@@ -1,7 +1,7 @@
1
1
  export { BUILTIN_WORKFLOWS, createBuiltinWorkflows } from './builtins.js';
2
2
  export { buildWorkflowContext } from './context.js';
3
- export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow } from './discovery.js';
4
3
  export type { SaveResult, SaveTarget } from './discovery.js';
4
+ export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow } from './discovery.js';
5
5
  export type { WorkflowRegistry } from './registry.js';
6
6
  export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry } from './registry.js';
7
7
  export { executeWorkflow } from './runtime.js';
@@ -783,6 +783,23 @@ export declare const fileSnapshots: import("drizzle-orm/pg-core").PgTableWithCol
783
783
  identity: undefined;
784
784
  generated: undefined;
785
785
  }, {}, {}>;
786
+ mtimeMs: import("drizzle-orm/pg-core").PgColumn<{
787
+ name: "mtime_ms";
788
+ tableName: "file_snapshots";
789
+ dataType: "number";
790
+ columnType: "PgBigInt53";
791
+ data: number;
792
+ driverParam: string | number;
793
+ notNull: false;
794
+ hasDefault: false;
795
+ isPrimaryKey: false;
796
+ isAutoincrement: false;
797
+ hasRuntimeDefault: false;
798
+ enumValues: undefined;
799
+ baseColumn: never;
800
+ identity: undefined;
801
+ generated: undefined;
802
+ }, {}, {}>;
786
803
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
787
804
  name: "created_at";
788
805
  tableName: "file_snapshots";
package/dist/db/schema.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { sql } from 'drizzle-orm';
2
- import { index, integer, jsonb, pgTable, real, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
2
+ import { bigint, index, integer, jsonb, pgTable, real, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
3
3
  /**
4
4
  * Projects table — git repos or plain directories tracked for agent workspaces.
5
5
  * Identity: sha256(worktree)[:16](git 仓库用仓库根,普通目录用绝对路径)。gitRemote 仅作
@@ -89,6 +89,8 @@ export const fileSnapshots = pgTable('file_snapshots', {
89
89
  contentHash: text('content_hash').notNull(),
90
90
  tokenCount: integer('token_count').default(0),
91
91
  version: integer('version').default(1),
92
+ /** 快照创建时文件磁盘 mtime(毫秒)。注入前比对,过期自动重读(P1-5)。 */
93
+ mtimeMs: bigint('mtime_ms', { mode: 'number' }),
92
94
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
93
95
  }, (table) => [
94
96
  index('idx_snapshots_session_path').on(table.sessionId, table.filePath),