c0de-agent 1.10.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.
- package/README.md +5 -2
- package/dist/cli/commands/acp.js +3 -3
- package/dist/cli/commands/auth.d.ts +17 -0
- package/dist/cli/commands/auth.js +73 -0
- package/dist/cli/commands/chat.js +8 -0
- package/dist/cli/commands/config.d.ts +2 -9
- package/dist/cli/commands/config.js +9 -52
- package/dist/cli/commands/init.js +4 -2
- package/dist/cli/commands/plugin.js +8 -4
- package/dist/cli/commands/sessions.d.ts +10 -0
- package/dist/cli/commands/sessions.js +35 -0
- package/dist/cli/commands/update.js +18 -10
- package/dist/cli/index.js +30 -2
- package/dist/cli/modes/print.js +11 -1
- package/dist/core/config-path.d.ts +9 -0
- package/dist/core/config-path.js +48 -0
- package/dist/core/config.d.ts +10 -2
- package/dist/core/config.js +32 -10
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/loop.js +13 -3
- package/dist/core/slash.js +92 -30
- package/dist/core/title.js +15 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/db/client.d.ts +4 -0
- package/dist/db/client.js +5 -0
- package/dist/db/schema.d.ts +34 -0
- package/dist/db/schema.js +4 -0
- package/dist/server/agent-manager.d.ts +5 -0
- package/dist/server/agent-manager.js +28 -0
- package/dist/server/app.js +12 -2
- package/dist/server/auth-manager.d.ts +79 -0
- package/dist/server/auth-manager.js +270 -0
- package/dist/server/context.js +1 -0
- package/dist/server/middleware/auth.d.ts +4 -3
- package/dist/server/middleware/auth.js +5 -5
- package/dist/server/permission/interactive.d.ts +2 -0
- package/dist/server/permission/interactive.js +5 -1
- package/dist/server/permission/store.d.ts +4 -0
- package/dist/server/permission/store.js +1 -0
- package/dist/server/routes/auth.d.ts +4 -0
- package/dist/server/routes/auth.js +94 -0
- package/dist/server/routes/chat.d.ts +6 -1
- package/dist/server/routes/chat.js +87 -7
- package/dist/server/routes/config.js +59 -11
- package/dist/server/routes/permissions.d.ts +4 -1
- package/dist/server/routes/permissions.js +27 -4
- package/dist/server/routes/project.js +56 -2
- package/dist/server/routes/session.js +31 -7
- package/dist/server/routes/terminal.js +17 -2
- package/dist/server/routes/update.js +23 -1
- package/dist/server/server.js +47 -8
- package/dist/server/types.d.ts +8 -1
- package/dist/session/archive.d.ts +1 -1
- package/dist/session/branch.d.ts +1 -0
- package/dist/session/branch.js +12 -5
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/session.d.ts +25 -5
- package/dist/session/session.js +129 -10
- package/dist/session/types.d.ts +1 -1
- package/dist/shared/types/agent.d.ts +11 -0
- package/dist/shared/types/config.d.ts +5 -0
- package/dist/shared/types/message.d.ts +6 -2
- package/dist/tools/index.js +2 -3
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/ipc.d.ts +6 -0
- package/dist/update/ipc.js +7 -4
- package/dist/update/snapshot.d.ts +4 -0
- package/dist/update/snapshot.js +8 -0
- package/dist/update/updater.d.ts +55 -8
- package/dist/update/updater.js +230 -27
- package/drizzle/0006_sour_susan_delgado.sql +2 -0
- package/drizzle/0007_same_wilson_fisk.sql +2 -0
- package/drizzle/meta/0006_snapshot.json +867 -0
- package/drizzle/meta/0007_snapshot.json +869 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
- **无感知热更新**:后台定期检查新版本,序列化当前会话 → npm 自更新 → 端口 handoff 接管,用户无感升级。
|
|
14
14
|
- **Web 搜索**:内置 websearch 工具,支持多搜索引擎。
|
|
15
15
|
- **斜杠命令**:`/compact`、`/model`、`/clear` 等可配置的斜杠命令。
|
|
16
|
-
-
|
|
16
|
+
- **会话回收站**:删除会话软删除入回收站,30 天内可恢复。
|
|
17
|
+
- **主题**:亮/暗/跟随系统主题。
|
|
17
18
|
|
|
18
19
|
## 安装
|
|
19
20
|
|
|
@@ -29,9 +30,11 @@ pnpm add -g c0de-agent
|
|
|
29
30
|
|
|
30
31
|
## 安全
|
|
31
32
|
|
|
32
|
-
- **认证**:`security.authEnabled`
|
|
33
|
+
- **认证**:`security.authEnabled` 默认开启。首次启动自动生成 bootstrap token(持久化于数据目录 `auth-token` 文件),浏览器首访凭启动打印的 URL `?token=` 注册为**首台设备**,服务端随即**轮换 bootstrap token**(旧 token 立即失效,杜绝 URL/shell 历史泄漏)并下发设备 token;后续 API 请求与终端 WebSocket 均携带设备 token。新增设备无 token 时进入**配对流程**:新设备生成 6 位配对码,由已授权设备在「设备配对」弹窗中核对并批准后下发新设备 token。显式配置 `security.token` 时为静态模式(不轮换、不配对,适合 CI/脚本)。
|
|
33
34
|
- token 解析优先级:`security.token` 配置 > 环境变量 `C0DE_AUTH_TOKEN` > 数据目录 token 文件 > 自动生成并持久化。
|
|
34
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 重新注册首台设备。
|
|
35
38
|
- **CORS/Origin 校验**:仅放行本机回环 origin 与 `security.allowedOrigins` 中显式配置的 origin;WebSocket 升级在服务端独立校验 Origin(浏览器 WS 不受 CORS 约束)。
|
|
36
39
|
- **监听地址**:默认绑定 `0.0.0.0`(便于容器/远程访问),安全性由 token + allowedOrigins 保证;需要时可用 `security.allowedOrigins` 收紧或自行反向代理限制。
|
|
37
40
|
- **热更新交接**:新实例经环境变量继承 token,请求旧实例 `/handoff` 时须携带匹配的 Bearer token,防止任意本地进程借 handoff 杀掉服务。
|
package/dist/cli/commands/acp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
|
-
import { createSession,
|
|
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,11 +7,11 @@ 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
|
|
14
|
+
const sessions = await listAllSessions(deps.db);
|
|
15
15
|
return { sessions };
|
|
16
16
|
},
|
|
17
17
|
chat: async (params) => {
|
|
@@ -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 };
|
|
@@ -16,6 +16,14 @@ async function runChatCommand(ctx) {
|
|
|
16
16
|
err(`[tool] ${e.tool}\n`);
|
|
17
17
|
else if (e._tag === 'thinking')
|
|
18
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
|
+
}
|
|
19
27
|
},
|
|
20
28
|
});
|
|
21
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,
|
|
10
|
+
export { coerce, getByPath, runConfigCommand, setPathPatch };
|
|
@@ -1,50 +1,5 @@
|
|
|
1
|
-
import { loadConfigScopes, mergeConfig,
|
|
2
|
-
|
|
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];
|
|
@@ -63,19 +18,21 @@ async function runConfigCommand(ctx) {
|
|
|
63
18
|
}
|
|
64
19
|
if (sub === 'set') {
|
|
65
20
|
const key = ctx.args.positionals[1];
|
|
66
|
-
const
|
|
21
|
+
const rawArg = ctx.args.positionals[2];
|
|
67
22
|
if (!key)
|
|
68
23
|
throw new Error('config set: a key is required');
|
|
69
|
-
if (
|
|
24
|
+
if (rawArg === undefined)
|
|
70
25
|
throw new Error('config set: a value is required');
|
|
71
26
|
const scope = ctx.args.options.global ? 'global' : 'project';
|
|
72
27
|
// 只把 patch 合并进目标作用域的原始文件,不写入默认值与其他作用域的配置(P2-3)。
|
|
28
|
+
// 值为 null → 删除该键(作用域内取消覆盖,回落另一作用域/默认值)。
|
|
73
29
|
const scopeCfg = scope === 'global' ? scopes.global : scopes.project;
|
|
74
|
-
const
|
|
30
|
+
const value = coerce(rawArg);
|
|
31
|
+
const next = applyScopedPatch(scopeCfg ?? {}, setPathPatch(key, value));
|
|
75
32
|
await saveConfigScoped(scope, ctx.cwd, next);
|
|
76
|
-
write(
|
|
33
|
+
write(`${value === null ? 'Unset' : 'Set'} ${key} (scope: ${scope})\n`);
|
|
77
34
|
return;
|
|
78
35
|
}
|
|
79
36
|
throw new Error(`config: unknown subcommand "${sub ?? ''}" (expected get|set)`);
|
|
80
37
|
}
|
|
81
|
-
export { coerce, getByPath, runConfigCommand,
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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}。新实例将接管。`);
|
package/dist/cli/index.js
CHANGED
|
@@ -6,11 +6,13 @@ import { loadConfig } from '../core/config.js';
|
|
|
6
6
|
import { createDB, migrateDB } from '../db/index.js';
|
|
7
7
|
import { acquireDevDbLock, releaseDevDbLock, resolveDbDir } from '../server/server.js';
|
|
8
8
|
import { runAcpCommand } from './commands/acp.js';
|
|
9
|
+
import { runAuthCommand } from './commands/auth.js';
|
|
9
10
|
import { runChatCommand } from './commands/chat.js';
|
|
10
11
|
import { runConfigCommand } from './commands/config.js';
|
|
11
12
|
import { runInitCommand } from './commands/init.js';
|
|
12
13
|
import { runPluginCommand } from './commands/plugin.js';
|
|
13
14
|
import { runServeCommand } from './commands/serve.js';
|
|
15
|
+
import { runSessionsCommand } from './commands/sessions.js';
|
|
14
16
|
import { runUpdateCommand } from './commands/update.js';
|
|
15
17
|
import { buildAgentDeps } from './deps.js';
|
|
16
18
|
import { parseCommand } from './parser.js';
|
|
@@ -50,6 +52,16 @@ const COMMANDS = [
|
|
|
50
52
|
description: 'List or enable plugins.',
|
|
51
53
|
options: [],
|
|
52
54
|
},
|
|
55
|
+
{
|
|
56
|
+
name: 'auth',
|
|
57
|
+
description: 'Manage authorized devices (list / revoke / reset).',
|
|
58
|
+
options: [],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'sessions',
|
|
62
|
+
description: 'Manage CLI/web sessions (list / delete).',
|
|
63
|
+
options: [],
|
|
64
|
+
},
|
|
53
65
|
{
|
|
54
66
|
name: 'acp',
|
|
55
67
|
description: 'Run in Agent Client Protocol mode (editor integration).',
|
|
@@ -96,11 +108,15 @@ async function withAgentDeps(cwd, opts, fn) {
|
|
|
96
108
|
holdLock = false;
|
|
97
109
|
if (!isDbLockConflict(err))
|
|
98
110
|
throw err;
|
|
111
|
+
if (opts.requirePersistent) {
|
|
112
|
+
throw new Error('c0de serve 正在运行并占用会话库。请通过 Web UI 管理会话,或停止 serve 后重试。');
|
|
113
|
+
}
|
|
99
114
|
if (opts.continueSessionId) {
|
|
100
115
|
throw new Error(`无法续聊会话 ${opts.continueSessionId}:持久库被 c0de serve 占用,内存模式无法续聊。\n` +
|
|
101
116
|
`请停止 serve 后重试,或去掉 --continue 开启新会话。`);
|
|
102
117
|
}
|
|
103
|
-
process.stderr.write('[c0de] c0de serve
|
|
118
|
+
process.stderr.write('[c0de] ⚠ c0de serve 正在运行:本次对话为临时模式,消息与工具调用不会保存。\n' +
|
|
119
|
+
' 停止 serve 后重试可持久化,或直接使用浏览器界面。\n');
|
|
104
120
|
db = await createDB({ driver: 'pglite' });
|
|
105
121
|
await migrateDB(db);
|
|
106
122
|
}
|
|
@@ -139,7 +155,10 @@ async function dispatch(argv, overrides = {}) {
|
|
|
139
155
|
// --yes / -y 显式放行写操作;否则按 config.permission.defaultMode 决定(默认 safe)。
|
|
140
156
|
const strategy = args.options.yes ? 'full-auto' : undefined;
|
|
141
157
|
const continueId = args.options.continue;
|
|
142
|
-
await withAgentDeps(cwd, {
|
|
158
|
+
await withAgentDeps(cwd, {
|
|
159
|
+
...(strategy ? { strategy } : {}),
|
|
160
|
+
...(continueId ? { continueSessionId: continueId } : {}),
|
|
161
|
+
}, (config, deps) => runChatCommand({ args, config, deps }));
|
|
143
162
|
return;
|
|
144
163
|
}
|
|
145
164
|
case 'init': {
|
|
@@ -154,6 +173,15 @@ async function dispatch(argv, overrides = {}) {
|
|
|
154
173
|
await runPluginCommand({ args, cwd });
|
|
155
174
|
return;
|
|
156
175
|
}
|
|
176
|
+
case 'auth': {
|
|
177
|
+
await runAuthCommand({ args });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
case 'sessions': {
|
|
181
|
+
// 需要持久库列出/清理会话;serve 运行时内存库不含数据,直接失败而不是列空表。
|
|
182
|
+
await withAgentDeps(cwd, { requirePersistent: true }, (_config, deps) => runSessionsCommand({ args, db: deps.db }));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
157
185
|
case 'acp': {
|
|
158
186
|
// ACP 非交互:所有工具放行(编辑器侧自行控制执行授权)。
|
|
159
187
|
await withAgentDeps(cwd, { strategy: 'full-auto' }, (config, deps) => runAcpCommand({ config, deps }));
|
package/dist/cli/modes/print.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createAgent, runAgent } from '../../core/agent.js';
|
|
2
|
+
import { resolveRoute } from '../../llm/registry.js';
|
|
2
3
|
import { createSession, getSession } from '../../session/session.js';
|
|
3
4
|
import { resolveEnabledToolNames } from '../../tools/index.js';
|
|
4
5
|
/** 从事件流累积 assistant 文本。纯函数。 */
|
|
@@ -26,7 +27,7 @@ async function runPrintMode(config, message, deps, opts = {}) {
|
|
|
26
27
|
session = existing;
|
|
27
28
|
}
|
|
28
29
|
else {
|
|
29
|
-
session = await createSession(deps.db, 'cli-print');
|
|
30
|
+
session = await createSession(deps.db, 'cli-print', undefined, undefined, 'cli');
|
|
30
31
|
}
|
|
31
32
|
const agentConfig = {
|
|
32
33
|
provider: config.defaultProvider,
|
|
@@ -36,6 +37,15 @@ async function runPrintMode(config, message, deps, opts = {}) {
|
|
|
36
37
|
plugins: config.plugins.enabled,
|
|
37
38
|
...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
|
|
38
39
|
};
|
|
40
|
+
// P3:首跑友好报错——未配置 provider 时不再让底层 NoRoute 异常裸抛,
|
|
41
|
+
// 给出与 Web 端一致的引导(c0de serve → 设置 → Provider)。
|
|
42
|
+
try {
|
|
43
|
+
resolveRoute(deps.llmRegistry, agentConfig.provider, agentConfig.model);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error('未配置可用的 AI 服务。请运行 `c0de serve` 并在「设置 → Provider」中添加 API 服务并测试连接,' +
|
|
47
|
+
'或使用 `c0de config set` 配置 providers。');
|
|
48
|
+
}
|
|
39
49
|
const state = await createAgent(session, agentConfig, deps);
|
|
40
50
|
const events = [];
|
|
41
51
|
for await (const event of runAgent(state, [{ _tag: 'text', text: message }], deps)) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 按 a.b.c 点路径从对象取值;找不到抛错。 */
|
|
2
|
+
declare function getByPath(obj: unknown, path: string): unknown;
|
|
3
|
+
/** 按 a.b.c 点路径写值(创建中间对象)。 */
|
|
4
|
+
declare function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void;
|
|
5
|
+
/** 把字符串解析为合适的标量类型。 */
|
|
6
|
+
declare function coerce(value: string): unknown;
|
|
7
|
+
/** 构造 a.b.c → { a: { b: { c: value } } } 的 patch 对象。 */
|
|
8
|
+
declare function setPathPatch(path: string, value: unknown): Record<string, unknown>;
|
|
9
|
+
export { coerce, getByPath, setByPath, setPathPatch };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// 配置点路径(a.b.c)读写工具:CLI `config set/get` 与斜杠 `/config` 共用。
|
|
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
|
+
}
|
|
48
|
+
export { coerce, getByPath, setByPath, setPathPatch };
|
package/dist/core/config.d.ts
CHANGED
|
@@ -10,6 +10,15 @@ declare function loadConfigScopes(projectDir?: string): {
|
|
|
10
10
|
global: Partial<Config> | undefined;
|
|
11
11
|
project: Partial<Config> | undefined;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* 把 patch 应用到某个作用域的原始配置(scoped patch,null=删除):
|
|
15
|
+
* - 深合并:嵌套普通对象递归合并,数组整体替换(providers 等列表语义);
|
|
16
|
+
* - 值为 undefined 的键跳过;
|
|
17
|
+
* - 值为 null 的键从结果中**删除**——作用域内取消覆盖,回落到另一作用域/默认值。
|
|
18
|
+
* Config 各字段均无合法的 null 值(可选字段用 undefined),null 作为「unset」标记是安全的。
|
|
19
|
+
* 不注入默认值。CLI config set / 服务端 PATCH /api/config 共用。
|
|
20
|
+
*/
|
|
21
|
+
declare function applyScopedPatch(base: Record<string, unknown> | undefined, patch: Record<string, unknown>): Record<string, unknown>;
|
|
13
22
|
/**
|
|
14
23
|
* 不注入默认值的深合并:仅合并传入对象的自有键。
|
|
15
24
|
* 数组整体替换(providers 等列表语义);嵌套普通对象递归合并。
|
|
@@ -19,6 +28,5 @@ declare function mergeRaw(...cfgs: (Record<string, unknown> | undefined)[]): Rec
|
|
|
19
28
|
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。 */
|
|
20
29
|
declare function saveConfigScoped(scope: 'global' | 'project', projectDir: string | undefined, data: Record<string, unknown>): Promise<void>;
|
|
21
30
|
declare function loadConfig(projectDir?: string): Promise<Config>;
|
|
22
|
-
declare function saveConfig(config: Config, scope: 'global' | 'project', projectDir?: string): Promise<void>;
|
|
23
31
|
export type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig, };
|
|
24
|
-
export { DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw,
|
|
32
|
+
export { applyScopedPatch, DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfigScoped, };
|
package/dist/core/config.js
CHANGED
|
@@ -78,6 +78,37 @@ function loadConfigScopes(projectDir) {
|
|
|
78
78
|
const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
|
|
79
79
|
return { global: readJsonIfExists(globalPath), project: readJsonIfExists(projectPath) };
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* 把 patch 应用到某个作用域的原始配置(scoped patch,null=删除):
|
|
83
|
+
* - 深合并:嵌套普通对象递归合并,数组整体替换(providers 等列表语义);
|
|
84
|
+
* - 值为 undefined 的键跳过;
|
|
85
|
+
* - 值为 null 的键从结果中**删除**——作用域内取消覆盖,回落到另一作用域/默认值。
|
|
86
|
+
* Config 各字段均无合法的 null 值(可选字段用 undefined),null 作为「unset」标记是安全的。
|
|
87
|
+
* 不注入默认值。CLI config set / 服务端 PATCH /api/config 共用。
|
|
88
|
+
*/
|
|
89
|
+
function applyScopedPatch(base, patch) {
|
|
90
|
+
const result = { ...(base ?? {}) };
|
|
91
|
+
for (const [key, val] of Object.entries(patch)) {
|
|
92
|
+
if (val === undefined)
|
|
93
|
+
continue;
|
|
94
|
+
if (val === null) {
|
|
95
|
+
delete result[key];
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const current = result[key];
|
|
99
|
+
if (typeof val === 'object' &&
|
|
100
|
+
!Array.isArray(val) &&
|
|
101
|
+
current !== null &&
|
|
102
|
+
typeof current === 'object' &&
|
|
103
|
+
!Array.isArray(current)) {
|
|
104
|
+
result[key] = applyScopedPatch(current, val);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
result[key] = val;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
81
112
|
/**
|
|
82
113
|
* 不注入默认值的深合并:仅合并传入对象的自有键。
|
|
83
114
|
* 数组整体替换(providers 等列表语义);嵌套普通对象递归合并。
|
|
@@ -124,13 +155,4 @@ async function loadConfig(projectDir) {
|
|
|
124
155
|
const project = readJsonIfExists(projectPath);
|
|
125
156
|
return mergeConfig(global, project);
|
|
126
157
|
}
|
|
127
|
-
|
|
128
|
-
const dir = scope === 'global'
|
|
129
|
-
? join(homedir(), GLOBAL_CONFIG_DIR)
|
|
130
|
-
: join(projectDir ?? process.cwd(), '.c0de');
|
|
131
|
-
if (!existsSync(dir))
|
|
132
|
-
mkdirSync(dir, { recursive: true });
|
|
133
|
-
const path = join(dir, CONFIG_FILENAME);
|
|
134
|
-
writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8');
|
|
135
|
-
}
|
|
136
|
-
export { DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfig, saveConfigScoped, };
|
|
158
|
+
export { applyScopedPatch, DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfigScoped, };
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { abortAgent, createAgent, getAgentStatus, isAgentPaused, pauseAgent, resumeAgent, runAgent, } from './agent.js';
|
|
2
2
|
export { createSummarizer, runCompaction } from './compact.js';
|
|
3
3
|
export type { CompactionConfig, Config, MCPServerConfig } from './config.js';
|
|
4
|
-
export { DEFAULT_CONFIG, loadConfig, mergeConfig
|
|
4
|
+
export { DEFAULT_CONFIG, loadConfig, mergeConfig } from './config.js';
|
|
5
5
|
export { calibrateEstimate, createTokenBudget, estimateBudget, fitToBudget, shouldCompact, } from './context.js';
|
|
6
6
|
export { agentLoop } from './loop.js';
|
|
7
7
|
export { buildSystemPrompt } from './prompt.js';
|