c0de-agent 1.9.0 → 1.11.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 (109) hide show
  1. package/README.md +25 -8
  2. package/dist/cli/commands/acp.js +5 -3
  3. package/dist/cli/commands/auth.d.ts +17 -0
  4. package/dist/cli/commands/auth.js +73 -0
  5. package/dist/cli/commands/chat.js +10 -0
  6. package/dist/cli/commands/config.d.ts +2 -9
  7. package/dist/cli/commands/config.js +15 -54
  8. package/dist/cli/commands/init.js +4 -2
  9. package/dist/cli/commands/plugin.js +8 -4
  10. package/dist/cli/commands/serve.d.ts +1 -0
  11. package/dist/cli/commands/serve.js +4 -1
  12. package/dist/cli/commands/sessions.d.ts +10 -0
  13. package/dist/cli/commands/sessions.js +35 -0
  14. package/dist/cli/commands/update.js +18 -10
  15. package/dist/cli/deps.d.ts +7 -5
  16. package/dist/cli/deps.js +23 -7
  17. package/dist/cli/index.d.ts +1 -1
  18. package/dist/cli/index.js +96 -37
  19. package/dist/cli/modes/print.d.ts +3 -1
  20. package/dist/cli/modes/print.js +31 -4
  21. package/dist/core/agent.js +3 -4
  22. package/dist/core/config-path.d.ts +9 -0
  23. package/dist/core/config-path.js +48 -0
  24. package/dist/core/config.d.ts +27 -2
  25. package/dist/core/config.js +83 -13
  26. package/dist/core/index.d.ts +1 -1
  27. package/dist/core/index.js +1 -1
  28. package/dist/core/loop.js +14 -4
  29. package/dist/core/slash.js +92 -30
  30. package/dist/core/title.js +15 -0
  31. package/dist/core/types.d.ts +2 -0
  32. package/dist/core/workflows/discovery.d.ts +1 -1
  33. package/dist/core/workflows/index.d.ts +1 -1
  34. package/dist/db/client.d.ts +4 -0
  35. package/dist/db/client.js +5 -0
  36. package/dist/db/schema.d.ts +51 -0
  37. package/dist/db/schema.js +7 -1
  38. package/dist/server/agent-manager.d.ts +20 -0
  39. package/dist/server/agent-manager.js +63 -11
  40. package/dist/server/app.js +12 -1
  41. package/dist/server/auth-manager.d.ts +79 -0
  42. package/dist/server/auth-manager.js +270 -0
  43. package/dist/server/context.js +3 -0
  44. package/dist/server/middleware/auth.d.ts +4 -3
  45. package/dist/server/middleware/auth.js +5 -5
  46. package/dist/server/middleware/cors.d.ts +11 -2
  47. package/dist/server/middleware/cors.js +15 -12
  48. package/dist/server/permission/interactive.d.ts +2 -0
  49. package/dist/server/permission/interactive.js +5 -1
  50. package/dist/server/permission/store.d.ts +4 -0
  51. package/dist/server/permission/store.js +1 -0
  52. package/dist/server/routes/auth.d.ts +4 -0
  53. package/dist/server/routes/auth.js +94 -0
  54. package/dist/server/routes/chat.d.ts +6 -1
  55. package/dist/server/routes/chat.js +294 -153
  56. package/dist/server/routes/config.js +60 -8
  57. package/dist/server/routes/permissions.d.ts +4 -1
  58. package/dist/server/routes/permissions.js +27 -4
  59. package/dist/server/routes/project.js +56 -2
  60. package/dist/server/routes/session.js +37 -9
  61. package/dist/server/routes/terminal.js +29 -2
  62. package/dist/server/routes/update.d.ts +1 -0
  63. package/dist/server/routes/update.js +33 -4
  64. package/dist/server/routes/workflows.js +2 -2
  65. package/dist/server/server.d.ts +24 -1
  66. package/dist/server/server.js +149 -29
  67. package/dist/server/types.d.ts +17 -2
  68. package/dist/session/archive.d.ts +1 -1
  69. package/dist/session/branch.d.ts +11 -2
  70. package/dist/session/branch.js +107 -31
  71. package/dist/session/compaction.js +1 -0
  72. package/dist/session/context.d.ts +3 -2
  73. package/dist/session/context.js +36 -3
  74. package/dist/session/index.d.ts +1 -1
  75. package/dist/session/index.js +1 -1
  76. package/dist/session/session.d.ts +25 -5
  77. package/dist/session/session.js +129 -10
  78. package/dist/session/snapshot.d.ts +9 -3
  79. package/dist/session/snapshot.js +45 -4
  80. package/dist/session/squash.js +1 -0
  81. package/dist/session/types.d.ts +3 -1
  82. package/dist/shared/types/agent.d.ts +11 -0
  83. package/dist/shared/types/config.d.ts +12 -6
  84. package/dist/shared/types/message.d.ts +6 -2
  85. package/dist/shared/utils/path.d.ts +2 -3
  86. package/dist/shared/utils/path.js +20 -4
  87. package/dist/tools/builtin/todo.js +5 -4
  88. package/dist/tools/index.d.ts +14 -3
  89. package/dist/tools/index.js +35 -14
  90. package/dist/update/index.d.ts +2 -2
  91. package/dist/update/index.js +2 -2
  92. package/dist/update/ipc.d.ts +38 -5
  93. package/dist/update/ipc.js +56 -7
  94. package/dist/update/snapshot.d.ts +4 -0
  95. package/dist/update/snapshot.js +8 -0
  96. package/dist/update/updater.d.ts +61 -10
  97. package/dist/update/updater.js +236 -25
  98. package/dist/update/version.d.ts +6 -1
  99. package/dist/update/version.js +18 -3
  100. package/drizzle/0005_snapshot_mtime.sql +1 -0
  101. package/drizzle/0006_sour_susan_delgado.sql +2 -0
  102. package/drizzle/0007_same_wilson_fisk.sql +2 -0
  103. package/drizzle/meta/0005_snapshot.json +855 -0
  104. package/drizzle/meta/0006_snapshot.json +867 -0
  105. package/drizzle/meta/0007_snapshot.json +869 -0
  106. package/drizzle/meta/_journal.json +21 -0
  107. package/package.json +1 -7
  108. package/dist/mcp/index.d.ts +0 -1
  109. package/dist/mcp/index.js +0 -3
package/README.md CHANGED
@@ -8,12 +8,13 @@
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 工具,支持多搜索引擎。
15
15
  - **斜杠命令**:`/compact`、`/model`、`/clear` 等可配置的斜杠命令。
16
- - **主题与多语言**:亮/暗主题,简体中文 / English。
16
+ - **会话回收站**:删除会话软删除入回收站,30 天内可恢复。
17
+ - **主题**:亮/暗/跟随系统主题。
17
18
 
18
19
  ## 安装
19
20
 
@@ -27,6 +28,17 @@ pnpm add -g c0de-agent
27
28
 
28
29
  要求 Node.js >= 22.0.0。
29
30
 
31
+ ## 安全
32
+
33
+ - **认证**:`security.authEnabled` 默认开启。首次启动自动生成 bootstrap token(持久化于数据目录 `auth-token` 文件),浏览器首访凭启动打印的 URL `?token=` 注册为**首台设备**,服务端随即**轮换 bootstrap token**(旧 token 立即失效,杜绝 URL/shell 历史泄漏)并下发设备 token;后续 API 请求与终端 WebSocket 均携带设备 token。新增设备无 token 时进入**配对流程**:新设备生成 6 位配对码,由已授权设备在「设备配对」弹窗中核对并批准后下发新设备 token。显式配置 `security.token` 时为静态模式(不轮换、不配对,适合 CI/脚本)。
34
+ - token 解析优先级:`security.token` 配置 > 环境变量 `C0DE_AUTH_TOKEN` > 数据目录 token 文件 > 自动生成并持久化。
35
+ - 显式关闭:`security.authEnabled: false`(本机单用户且端口仅本机可达时)。
36
+ - **设备管理**:`c0de auth list` 列出已授权设备;`c0de auth revoke <id>` 撤销设备(运行中的服务立即生效);`c0de auth reset` 清除全部设备与 token。运行中的服务会热加载 `devices.json` 的变更。
37
+ - **丢失唯一设备的恢复**:若唯一设备的浏览器数据被清空(设备 token 丢失),配对流程因无已授权设备可批准而无法完成。恢复步骤:`c0de auth reset` → 重启 `c0de serve` → 打开启动日志打印的带 `?token=` 的 URL 重新注册首台设备。
38
+ - **CORS/Origin 校验**:仅放行本机回环 origin 与 `security.allowedOrigins` 中显式配置的 origin;WebSocket 升级在服务端独立校验 Origin(浏览器 WS 不受 CORS 约束)。
39
+ - **监听地址**:默认绑定 `0.0.0.0`(便于容器/远程访问),安全性由 token + allowedOrigins 保证;需要时可用 `security.allowedOrigins` 收紧或自行反向代理限制。
40
+ - **热更新交接**:新实例经环境变量继承 token,请求旧实例 `/handoff` 时须携带匹配的 Bearer token,防止任意本地进程借 handoff 杀掉服务。
41
+
30
42
  ## 使用
31
43
 
32
44
  ```bash
@@ -77,14 +89,19 @@ pnpm format
77
89
 
78
90
  ```
79
91
  src/
80
- ├── cli/ # CLI 入口与命令(serve / chat / update)
81
- ├── core/ # 配置、agent 注册、权限、工具注册
82
- ├── llm/ # LLM provider 抽象与调用
83
- ├── server/ # Hono HTTP 服务、SSE 流、路由、handoff
84
- ├── session/ # 会话上下文与消息转换
92
+ ├── cli/ # CLI 入口与命令(serve / chat / acp / update / config / init / plugin
93
+ ├── core/ # 配置、agent 注册与主循环(loop/ 子模块)、权限、prompt、工作流、斜杠命令
94
+ ├── llm/ # LLM provider 抽象、registry 与重试策略
95
+ ├── server/ # Hono HTTP 服务、SSE 流、路由、鉴权/CORS、交互式权限、终端(node-pty)
96
+ ├── session/ # 会话上下文、消息、compaction、branch/squash、快照
97
+ ├── project/ # 多项目解析与检测(.git、package.json 等)
98
+ ├── db/ # Drizzle schema 与迁移(PGLite)
99
+ ├── tools/ # 内置工具实现(read/write/edit/bash/grep/glob/todo/kanban/dap/websearch…)
100
+ ├── plugins/ # 插件系统(12 个生命周期钩子)
101
+ ├── dap/ # Debug Adapter Protocol 会话与协议编解码
85
102
  ├── shared/ # 前后端共享类型
86
- ├── tools/ # 内置工具实现
87
103
  ├── update/ # 版本检查、热更新调度器、会话快照
104
+ ├── kanban/ # 看板状态存储
88
105
  └── web/ # React 前端(组件、视图、服务、hooks)
89
106
  ```
90
107
 
@@ -1,5 +1,5 @@
1
1
  import readline from 'node:readline/promises';
2
- import { createSession, listSessions } from '../../session/session.js';
2
+ import { createSession, listAllSessions } from '../../session/session.js';
3
3
  import { formatACPEvent, runAcpLoop } from '../modes/acp.js';
4
4
  import { runPrintMode } from '../modes/print.js';
5
5
  /** 构造 ACP method → handler 映射。chat 复用 Print 模式。 */
@@ -7,18 +7,20 @@ function createAcpHandlers(config, deps, opts) {
7
7
  return {
8
8
  'session/create': async (params) => {
9
9
  const title = params.title ?? 'acp-session';
10
- const session = await createSession(deps.db, title);
10
+ const session = await createSession(deps.db, title, undefined, undefined, 'cli');
11
11
  return { sessionId: session.id };
12
12
  },
13
13
  'session/list': async () => {
14
- const sessions = await listSessions(deps.db);
14
+ const sessions = await listAllSessions(deps.db);
15
15
  return { sessions };
16
16
  },
17
17
  chat: async (params) => {
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 };
@@ -0,0 +1,17 @@
1
+ import type { CommandArgs } from '../parser.js';
2
+ type DeviceRecord = {
3
+ id: string;
4
+ name: string;
5
+ tokenHash: string;
6
+ createdAt: number;
7
+ };
8
+ type AuthCommandContext = {
9
+ args: CommandArgs;
10
+ write?: (s: string) => void;
11
+ };
12
+ declare function devicesPath(): string;
13
+ declare function readDevices(): DeviceRecord[];
14
+ declare function writeDevices(devices: DeviceRecord[]): void;
15
+ declare function runAuthCommand(ctx: AuthCommandContext): Promise<void>;
16
+ export type { AuthCommandContext };
17
+ export { devicesPath, readDevices, runAuthCommand, writeDevices };
@@ -0,0 +1,73 @@
1
+ // src/cli/commands/auth.ts — 设备管理与认证恢复(P1-4/P2-5)。
2
+ //
3
+ // 本地终端是可信信道:唯一设备丢失(浏览器数据清空)导致配对死锁时,
4
+ // 用户可通过 `c0de auth reset` 清除设备注册表,重启 serve 后用启动日志
5
+ // 打印的带 token URL 重新注册首台设备。
6
+ // 运行中的 serve 通过 authManager 的 devices.json watcher 热加载 revoke/reset 变更。
7
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { resolveDbDir } from '../../server/server.js';
10
+ function devicesPath() {
11
+ return join(resolveDbDir(), 'devices.json');
12
+ }
13
+ function authTokenPath() {
14
+ return join(resolveDbDir(), 'auth-token');
15
+ }
16
+ function readDevices() {
17
+ try {
18
+ if (!existsSync(devicesPath()))
19
+ return [];
20
+ const raw = JSON.parse(readFileSync(devicesPath(), 'utf-8'));
21
+ return Array.isArray(raw.devices) ? raw.devices : [];
22
+ }
23
+ catch {
24
+ throw new Error('auth: 无法读取设备注册表(devices.json 损坏?)');
25
+ }
26
+ }
27
+ function writeDevices(devices) {
28
+ const file = { version: 1, devices };
29
+ writeFileSync(devicesPath(), JSON.stringify(file, null, 2), 'utf-8');
30
+ }
31
+ async function runAuthCommand(ctx) {
32
+ const write = ctx.write ?? ((s) => process.stdout.write(s));
33
+ const sub = ctx.args.positionals[0] ?? 'list';
34
+ if (sub === 'list') {
35
+ const devices = readDevices();
36
+ if (devices.length === 0) {
37
+ write('无已授权设备。\n');
38
+ return;
39
+ }
40
+ for (const d of devices) {
41
+ write(`- ${d.name} (id: ${d.id}, 注册于 ${new Date(d.createdAt).toISOString()})\n`);
42
+ }
43
+ return;
44
+ }
45
+ if (sub === 'revoke') {
46
+ const id = ctx.args.positionals[1];
47
+ if (!id)
48
+ throw new Error('auth revoke: a device id is required (use `c0de auth list`)');
49
+ const devices = readDevices();
50
+ const target = devices.find((d) => d.id === id);
51
+ if (!target)
52
+ throw new Error(`auth revoke: device not found: ${id}`);
53
+ writeDevices(devices.filter((d) => d.id !== id));
54
+ write(`已撤销设备 "${target.name}"(${id})。运行中的 serve 将立即生效。\n`);
55
+ return;
56
+ }
57
+ if (sub === 'reset') {
58
+ // 清除设备注册表 + bootstrap:恢复到「未注册」状态。
59
+ // 运行中的 serve 经 watcher 热清空内存设备;bootstrap 在重启后重新生成并打印。
60
+ try {
61
+ rmSync(devicesPath(), { force: true });
62
+ rmSync(authTokenPath(), { force: true });
63
+ }
64
+ catch (err) {
65
+ throw new Error(`auth reset: ${err instanceof Error ? err.message : String(err)}`);
66
+ }
67
+ write('已清除全部已授权设备与认证 token。\n');
68
+ write('恢复步骤:重启 c0de serve,然后打开启动日志中打印的带 ?token= 的 URL 注册首台设备。\n');
69
+ return;
70
+ }
71
+ throw new Error(`auth: unknown subcommand "${sub}" (expected list|revoke|reset)`);
72
+ }
73
+ export { devicesPath, readDevices, runAuthCommand, writeDevices };
@@ -7,13 +7,23 @@ 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`);
15
17
  else if (e._tag === 'thinking')
16
18
  err(`[thinking] ${e.text}\n`);
19
+ else if (e._tag === 'tool_call_end') {
20
+ // P2-17:非交互模式被拒绝的工具调用,把拒绝原因直接可见地输出给用户
21
+ //(拒绝原因本身含 -y / serve 指引),而不是只留给模型转述。
22
+ const result = e.result;
23
+ if (result?._tag === 'deny') {
24
+ err(`[tool] 工具调用已拒绝:${result.reason ?? '需要确认'}\n`);
25
+ }
26
+ }
17
27
  },
18
28
  });
19
29
  if (format === 'json') {
@@ -1,17 +1,10 @@
1
+ import { coerce, getByPath, setPathPatch } from '../../core/config-path.js';
1
2
  import type { CommandArgs } from '../parser.js';
2
3
  type ConfigCommandContext = {
3
4
  args: CommandArgs;
4
5
  cwd: string;
5
6
  write?: (s: string) => void;
6
7
  };
7
- /** 按 a.b.c 点路径从对象取值;找不到抛错。 */
8
- declare function getByPath(obj: unknown, path: string): unknown;
9
- /** 按 a.b.c 点路径写值(创建中间对象)。 */
10
- declare function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void;
11
- /** 把字符串解析为合适的标量类型。 */
12
- declare function coerce(value: string): unknown;
13
- /** 构造 a.b.c → { a: { b: { c: value } } } 的 patch 对象。 */
14
- declare function setPathPatch(path: string, value: unknown): Record<string, unknown>;
15
8
  declare function runConfigCommand(ctx: ConfigCommandContext): Promise<void>;
16
9
  export type { ConfigCommandContext };
17
- export { coerce, getByPath, runConfigCommand, setByPath, setPathPatch };
10
+ export { coerce, getByPath, runConfigCommand, setPathPatch };
@@ -1,54 +1,11 @@
1
- import { loadConfig, mergeConfig, saveConfig } from '../../core/config.js';
2
- /** a.b.c 点路径从对象取值;找不到抛错。 */
3
- function getByPath(obj, path) {
4
- const parts = path.split('.');
5
- let cur = obj;
6
- for (const p of parts) {
7
- if (cur === null || typeof cur !== 'object')
8
- throw new Error(`config: key "${path}" not found`);
9
- cur = cur[p];
10
- if (cur === undefined)
11
- throw new Error(`config: key "${path}" not found`);
12
- }
13
- return cur;
14
- }
15
- /** 按 a.b.c 点路径写值(创建中间对象)。 */
16
- function setByPath(obj, path, value) {
17
- const parts = path.split('.');
18
- let cur = obj;
19
- for (let i = 0; i < parts.length - 1; i++) {
20
- const k = parts[i];
21
- const next = cur[k];
22
- cur[k] = (next !== null && typeof next === 'object' ? next : {});
23
- cur = cur[k];
24
- }
25
- cur[parts[parts.length - 1]] = value;
26
- }
27
- /** 把字符串解析为合适的标量类型。 */
28
- function coerce(value) {
29
- if (value === 'true')
30
- return true;
31
- if (value === 'false')
32
- return false;
33
- if (/^-?\d+(\.\d+)?$/.test(value))
34
- return Number(value);
35
- try {
36
- return JSON.parse(value);
37
- }
38
- catch {
39
- return value;
40
- }
41
- }
42
- /** 构造 a.b.c → { a: { b: { c: value } } } 的 patch 对象。 */
43
- function setPathPatch(path, value) {
44
- const root = {};
45
- setByPath(root, path, value);
46
- return root;
47
- }
1
+ import { applyScopedPatch, loadConfigScopes, mergeConfig, saveConfigScoped, } from '../../core/config.js';
2
+ import { coerce, getByPath, setPathPatch } from '../../core/config-path.js';
48
3
  async function runConfigCommand(ctx) {
49
4
  const write = ctx.write ?? process.stdout.write.bind(process.stdout);
50
5
  const sub = ctx.args.positionals[0];
51
- const config = await loadConfig(ctx.cwd);
6
+ const scopes = loadConfigScopes(ctx.cwd);
7
+ // get 展示用合并视图(global ← project,含默认值),与 loadConfig 语义一致。
8
+ const config = mergeConfig(scopes.global, scopes.project);
52
9
  if (sub === 'get') {
53
10
  const key = ctx.args.positionals[1];
54
11
  if (!key) {
@@ -61,17 +18,21 @@ async function runConfigCommand(ctx) {
61
18
  }
62
19
  if (sub === 'set') {
63
20
  const key = ctx.args.positionals[1];
64
- const rawValue = ctx.args.positionals[2];
21
+ const rawArg = ctx.args.positionals[2];
65
22
  if (!key)
66
23
  throw new Error('config set: a key is required');
67
- if (rawValue === undefined)
24
+ if (rawArg === undefined)
68
25
  throw new Error('config set: a value is required');
69
26
  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
- write(`Set ${key} (scope: ${scope})\n`);
27
+ // 只把 patch 合并进目标作用域的原始文件,不写入默认值与其他作用域的配置(P2-3)。
28
+ // 值为 null → 删除该键(作用域内取消覆盖,回落另一作用域/默认值)。
29
+ const scopeCfg = scope === 'global' ? scopes.global : scopes.project;
30
+ const value = coerce(rawArg);
31
+ const next = applyScopedPatch(scopeCfg ?? {}, setPathPatch(key, value));
32
+ await saveConfigScoped(scope, ctx.cwd, next);
33
+ write(`${value === null ? 'Unset' : 'Set'} ${key} (scope: ${scope})\n`);
73
34
  return;
74
35
  }
75
36
  throw new Error(`config: unknown subcommand "${sub ?? ''}" (expected get|set)`);
76
37
  }
77
- export { coerce, getByPath, runConfigCommand, setByPath, setPathPatch };
38
+ export { coerce, getByPath, runConfigCommand, setPathPatch };
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { DEFAULT_CONFIG, saveConfig } from '../../core/config.js';
3
+ import { saveConfigScoped } from '../../core/config.js';
4
4
  async function runInitCommand(ctx) {
5
5
  const log = ctx.log ?? process.stdout.write.bind(process.stdout);
6
6
  const force = ctx.args.options.force ?? false;
@@ -8,7 +8,9 @@ async function runInitCommand(ctx) {
8
8
  if (existsSync(path) && !force) {
9
9
  throw new Error(`init: config already exists at ${path} (use --force to overwrite)`);
10
10
  }
11
- await saveConfig(DEFAULT_CONFIG, 'project', ctx.cwd);
11
+ // P1-3:只写空配置——写入 DEFAULT_CONFIG 会把 providers: [] 等默认值固化进项目文件,
12
+ // 数组整体替换语义会覆盖全局 providers,使项目内 AI 服务失效。
13
+ await saveConfigScoped('project', ctx.cwd, {});
12
14
  log(`Created ${path}\n`);
13
15
  }
14
16
  export { runInitCommand };
@@ -1,4 +1,4 @@
1
- import { loadConfig, saveConfig } from '../../core/config.js';
1
+ import { applyScopedPatch, loadConfigScopes, mergeConfig } from '../../core/config.js';
2
2
  async function runPluginCommand(ctx) {
3
3
  const write = ctx.write ?? process.stdout.write.bind(process.stdout);
4
4
  const discover = ctx.discover ?? (async () => []);
@@ -18,11 +18,15 @@ async function runPluginCommand(ctx) {
18
18
  const name = ctx.args.positionals[1];
19
19
  if (!name)
20
20
  throw new Error('plugin install: a plugin name is required');
21
- const config = await loadConfig(ctx.cwd);
22
- const enabled = config.plugins.enabled;
21
+ // P1-2:只把 plugins.enabled 合并进 project 作用域文件,不整体落盘
22
+ //(saveConfig 全量写入会把默认值与全局配置固化进项目文件)。
23
+ const scopes = loadConfigScopes(ctx.cwd);
24
+ const enabled = [...(mergeConfig(scopes.global, scopes.project).plugins?.enabled ?? [])];
23
25
  if (!enabled.includes(name))
24
26
  enabled.push(name);
25
- await saveConfig(config, 'project', ctx.cwd);
27
+ const next = applyScopedPatch(scopes.project ?? {}, { plugins: { enabled } });
28
+ const { saveConfigScoped } = await import('../../core/config.js');
29
+ await saveConfigScoped('project', ctx.cwd, next);
26
30
  write(`Enabled plugin "${name}".\n`);
27
31
  return;
28
32
  }
@@ -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));
@@ -0,0 +1,10 @@
1
+ import type { DB } from '../../db/client.js';
2
+ import type { CommandArgs } from '../parser.js';
3
+ type SessionsCommandContext = {
4
+ args: CommandArgs;
5
+ db: DB;
6
+ write?: (s: string) => void;
7
+ };
8
+ declare function runSessionsCommand(ctx: SessionsCommandContext): Promise<void>;
9
+ export type { SessionsCommandContext };
10
+ export { runSessionsCommand };
@@ -0,0 +1,35 @@
1
+ // src/cli/commands/sessions.ts — CLI 会话管理(P3:清理 print 模式积累的 CLI 会话)。
2
+ //
3
+ // c0de chat 每次运行都创建 source='cli' 会话且被 Web 会话树排除,
4
+ // 此前无任何 CLI 途径列出/清理,垃圾数据只增不减。
5
+ // 复用 withAgentDeps 的持久库生命周期(serve 占用时退化为内存库则明确报错)。
6
+ import { listAllSessions, softDeleteSession } from '../../session/session.js';
7
+ async function runSessionsCommand(ctx) {
8
+ const write = ctx.write ?? ((s) => process.stdout.write(s));
9
+ const sub = ctx.args.positionals[0] ?? 'list';
10
+ if (sub === 'list') {
11
+ const sessions = await listAllSessions(ctx.db);
12
+ if (sessions.length === 0) {
13
+ write('无会话。\n');
14
+ return;
15
+ }
16
+ for (const s of sessions) {
17
+ const source = s.source === 'cli' ? 'cli' : 'web';
18
+ const when = new Date(s.updatedAt).toISOString();
19
+ write(`- [${source}] ${s.title} (id: ${s.id}, updated ${when})\n`);
20
+ }
21
+ return;
22
+ }
23
+ if (sub === 'delete') {
24
+ const id = ctx.args.positionals[1];
25
+ if (!id)
26
+ throw new Error('sessions delete: a session id is required (use `c0de sessions list`)');
27
+ const ok = await softDeleteSession(ctx.db, id);
28
+ if (!ok)
29
+ throw new Error(`sessions delete: session not found or already deleted: ${id}`);
30
+ write(`已删除会话 ${id}(移入回收站,30 天内可恢复)。\n`);
31
+ return;
32
+ }
33
+ throw new Error(`sessions: unknown subcommand "${sub}" (expected list|delete)`);
34
+ }
35
+ export { runSessionsCommand };
@@ -1,4 +1,4 @@
1
- import { checkForUpdate, performHotUpdate, serializeSessions, } from '../../update/index.js';
1
+ import { checkForUpdate, performHotUpdate, performInstall, serializeSessions, } from '../../update/index.js';
2
2
  /**
3
3
  * `c0de update`:检查 npm registry 新版本。
4
4
  * - 默认:check-only,报告有无更新。
@@ -19,16 +19,24 @@ async function runUpdateCommand(ctx) {
19
19
  out(`发现新版本 ${result.latestVersion}。加 --apply 执行热更新。`);
20
20
  return;
21
21
  }
22
+ // P2-7:无 db(独立 CLI,无活跃 serve)→ 仅安装,不 spawn `serve`。
23
+ // 此前会拉起一个用户未请求的常驻服务;活跃 serve 的热更新走 /api/update/apply。
24
+ if (!ctx.db) {
25
+ out(`发现新版本 ${result.latestVersion},开始安装…`);
26
+ const r = await performInstall(ctx.updateOpts);
27
+ if (r._tag === 'success') {
28
+ out('安装完成。若 c0de serve 正在运行,请在页面顶部的更新横幅中应用热更新,或重启 serve。');
29
+ }
30
+ else if (r._tag === 'install_failed') {
31
+ out(`安装失败:${r.error}`);
32
+ }
33
+ else {
34
+ out(`无法自动安装(${r.error})。请手动执行:${r.command}`);
35
+ }
36
+ return;
37
+ }
22
38
  out(`发现新版本 ${result.latestVersion},开始热更新…`);
23
- const snapshot = ctx.db
24
- ? await serializeSessions(ctx.db, ctx.config)
25
- : {
26
- version: result.currentVersion,
27
- sessions: [],
28
- entries: [],
29
- config: ctx.config ?? null,
30
- timestamp: Date.now(),
31
- };
39
+ const snapshot = await serializeSessions(ctx.db, ctx.config);
32
40
  const r = await performHotUpdate(snapshot, ctx.updateOpts);
33
41
  if (r._tag === 'success') {
34
42
  out(`热更新完成,快照写入 ${r.snapshotPath}。新实例将接管。`);
@@ -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>;