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.
- package/README.md +25 -8
- package/dist/cli/commands/acp.js +5 -3
- package/dist/cli/commands/auth.d.ts +17 -0
- package/dist/cli/commands/auth.js +73 -0
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/config.d.ts +2 -9
- package/dist/cli/commands/config.js +15 -54
- package/dist/cli/commands/init.js +4 -2
- package/dist/cli/commands/plugin.js +8 -4
- package/dist/cli/commands/serve.d.ts +1 -0
- package/dist/cli/commands/serve.js +4 -1
- 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/deps.d.ts +7 -5
- package/dist/cli/deps.js +23 -7
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +96 -37
- package/dist/cli/modes/print.d.ts +3 -1
- package/dist/cli/modes/print.js +31 -4
- package/dist/core/agent.js +3 -4
- package/dist/core/config-path.d.ts +9 -0
- package/dist/core/config-path.js +48 -0
- package/dist/core/config.d.ts +27 -2
- package/dist/core/config.js +83 -13
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/loop.js +14 -4
- package/dist/core/slash.js +92 -30
- package/dist/core/title.js +15 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/workflows/discovery.d.ts +1 -1
- package/dist/core/workflows/index.d.ts +1 -1
- package/dist/db/client.d.ts +4 -0
- package/dist/db/client.js +5 -0
- package/dist/db/schema.d.ts +51 -0
- package/dist/db/schema.js +7 -1
- package/dist/server/agent-manager.d.ts +20 -0
- package/dist/server/agent-manager.js +63 -11
- package/dist/server/app.js +12 -1
- package/dist/server/auth-manager.d.ts +79 -0
- package/dist/server/auth-manager.js +270 -0
- package/dist/server/context.js +3 -0
- package/dist/server/middleware/auth.d.ts +4 -3
- package/dist/server/middleware/auth.js +5 -5
- package/dist/server/middleware/cors.d.ts +11 -2
- package/dist/server/middleware/cors.js +15 -12
- 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 +294 -153
- package/dist/server/routes/config.js +60 -8
- 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 +37 -9
- package/dist/server/routes/terminal.js +29 -2
- package/dist/server/routes/update.d.ts +1 -0
- package/dist/server/routes/update.js +33 -4
- package/dist/server/routes/workflows.js +2 -2
- package/dist/server/server.d.ts +24 -1
- package/dist/server/server.js +149 -29
- package/dist/server/types.d.ts +17 -2
- package/dist/session/archive.d.ts +1 -1
- package/dist/session/branch.d.ts +11 -2
- package/dist/session/branch.js +107 -31
- package/dist/session/compaction.js +1 -0
- package/dist/session/context.d.ts +3 -2
- package/dist/session/context.js +36 -3
- 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/snapshot.d.ts +9 -3
- package/dist/session/snapshot.js +45 -4
- package/dist/session/squash.js +1 -0
- package/dist/session/types.d.ts +3 -1
- package/dist/shared/types/agent.d.ts +11 -0
- package/dist/shared/types/config.d.ts +12 -6
- package/dist/shared/types/message.d.ts +6 -2
- package/dist/shared/utils/path.d.ts +2 -3
- package/dist/shared/utils/path.js +20 -4
- package/dist/tools/builtin/todo.js +5 -4
- package/dist/tools/index.d.ts +14 -3
- package/dist/tools/index.js +35 -14
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/ipc.d.ts +38 -5
- package/dist/update/ipc.js +56 -7
- package/dist/update/snapshot.d.ts +4 -0
- package/dist/update/snapshot.js +8 -0
- package/dist/update/updater.d.ts +61 -10
- package/dist/update/updater.js +236 -25
- package/dist/update/version.d.ts +6 -1
- package/dist/update/version.js +18 -3
- package/drizzle/0005_snapshot_mtime.sql +1 -0
- package/drizzle/0006_sour_susan_delgado.sql +2 -0
- package/drizzle/0007_same_wilson_fisk.sql +2 -0
- package/drizzle/meta/0005_snapshot.json +855 -0
- package/drizzle/meta/0006_snapshot.json +867 -0
- package/drizzle/meta/0007_snapshot.json +869 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +1 -7
- package/dist/mcp/index.d.ts +0 -1
- package/dist/mcp/index.js +0 -3
package/dist/cli/index.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
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';
|
|
9
|
+
import { runAuthCommand } from './commands/auth.js';
|
|
8
10
|
import { runChatCommand } from './commands/chat.js';
|
|
9
11
|
import { runConfigCommand } from './commands/config.js';
|
|
10
12
|
import { runInitCommand } from './commands/init.js';
|
|
11
13
|
import { runPluginCommand } from './commands/plugin.js';
|
|
12
14
|
import { runServeCommand } from './commands/serve.js';
|
|
15
|
+
import { runSessionsCommand } from './commands/sessions.js';
|
|
13
16
|
import { runUpdateCommand } from './commands/update.js';
|
|
14
17
|
import { buildAgentDeps } from './deps.js';
|
|
18
|
+
import { parseCommand } from './parser.js';
|
|
15
19
|
const COMMANDS = [
|
|
16
20
|
{
|
|
17
21
|
name: 'chat',
|
|
@@ -20,6 +24,7 @@ const COMMANDS = [
|
|
|
20
24
|
{ name: 'model', type: 'string' },
|
|
21
25
|
{ name: 'format', type: 'string' },
|
|
22
26
|
{ name: 'yes', type: 'boolean', short: 'y' },
|
|
27
|
+
{ name: 'continue', type: 'string' },
|
|
23
28
|
],
|
|
24
29
|
},
|
|
25
30
|
{
|
|
@@ -47,6 +52,16 @@ const COMMANDS = [
|
|
|
47
52
|
description: 'List or enable plugins.',
|
|
48
53
|
options: [],
|
|
49
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
|
+
},
|
|
50
65
|
{
|
|
51
66
|
name: 'acp',
|
|
52
67
|
description: 'Run in Agent Client Protocol mode (editor integration).',
|
|
@@ -58,6 +73,67 @@ const COMMANDS = [
|
|
|
58
73
|
options: [{ name: 'apply', type: 'boolean' }],
|
|
59
74
|
},
|
|
60
75
|
];
|
|
76
|
+
/** 判别持久库错误是否为「库被其它 c0de 进程占用」的锁冲突特征(读 server.ts 确认):
|
|
77
|
+
* - acquireDevDbLock 对 live lock 抛出的 "Database is locked by another c0de process (PID …)";
|
|
78
|
+
* - PGLite 单写者 WASM 崩溃 RuntimeError "Aborted()"(serve 未持锁但占用 dataDir 时的兜底特征)。
|
|
79
|
+
* 其余错误(磁盘故障、迁移失败等)不属于锁冲突,不得误报 serve。 */
|
|
80
|
+
function isDbLockConflict(err) {
|
|
81
|
+
if (!(err instanceof Error))
|
|
82
|
+
return false;
|
|
83
|
+
if (err.message.includes('Database is locked by another c0de process'))
|
|
84
|
+
return true;
|
|
85
|
+
return err.name === 'RuntimeError' && /abort/i.test(err.message);
|
|
86
|
+
}
|
|
87
|
+
/** 封装 agent 依赖生命周期:加载配置 → 建库迁移 → 组装 deps → 使用后关库。
|
|
88
|
+
* chat / acp 两个命令复用同一套逻辑(此前在 dispatch 中各写一份)。
|
|
89
|
+
* 数据库优先用全局 dataDir 持久库(会话可恢复,--continue 可用);
|
|
90
|
+
* 仅当错误是锁冲突特征(dataDir 被 serve 实例占用)时退化为内存库并 stderr 提示,
|
|
91
|
+
* 其它持久库错误如实上抛(main 统一打印 + 非零退出);
|
|
92
|
+
* 带 --continue 时内存库不含历史会话,直接报错退出而非误导性的 session not found。 */
|
|
93
|
+
async function withAgentDeps(cwd, opts, fn) {
|
|
94
|
+
const config = await loadConfig(cwd);
|
|
95
|
+
const dataDir = resolveDbDir();
|
|
96
|
+
let db;
|
|
97
|
+
let holdLock = false;
|
|
98
|
+
try {
|
|
99
|
+
mkdirSync(dataDir, { recursive: true });
|
|
100
|
+
acquireDevDbLock(dataDir);
|
|
101
|
+
holdLock = true;
|
|
102
|
+
db = await createDB({ driver: 'pglite', dataDir });
|
|
103
|
+
await migrateDB(db);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (holdLock)
|
|
107
|
+
releaseDevDbLock(dataDir);
|
|
108
|
+
holdLock = false;
|
|
109
|
+
if (!isDbLockConflict(err))
|
|
110
|
+
throw err;
|
|
111
|
+
if (opts.requirePersistent) {
|
|
112
|
+
throw new Error('c0de serve 正在运行并占用会话库。请通过 Web UI 管理会话,或停止 serve 后重试。');
|
|
113
|
+
}
|
|
114
|
+
if (opts.continueSessionId) {
|
|
115
|
+
throw new Error(`无法续聊会话 ${opts.continueSessionId}:持久库被 c0de serve 占用,内存模式无法续聊。\n` +
|
|
116
|
+
`请停止 serve 后重试,或去掉 --continue 开启新会话。`);
|
|
117
|
+
}
|
|
118
|
+
process.stderr.write('[c0de] ⚠ c0de serve 正在运行:本次对话为临时模式,消息与工具调用不会保存。\n' +
|
|
119
|
+
' 停止 serve 后重试可持久化,或直接使用浏览器界面。\n');
|
|
120
|
+
db = await createDB({ driver: 'pglite' });
|
|
121
|
+
await migrateDB(db);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const deps = await buildAgentDeps(config, {
|
|
125
|
+
db,
|
|
126
|
+
cwd,
|
|
127
|
+
...(opts.strategy ? { permissionStrategy: opts.strategy } : {}),
|
|
128
|
+
});
|
|
129
|
+
await fn(config, deps);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await db.close();
|
|
133
|
+
if (holdLock)
|
|
134
|
+
releaseDevDbLock(dataDir);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
61
137
|
async function dispatch(argv, overrides = {}) {
|
|
62
138
|
const [command, ...rest] = argv;
|
|
63
139
|
const name = command ?? 'serve';
|
|
@@ -68,16 +144,7 @@ async function dispatch(argv, overrides = {}) {
|
|
|
68
144
|
const spec = COMMANDS.find((c) => c.name === name);
|
|
69
145
|
if (!spec)
|
|
70
146
|
throw new Error(`cli: unknown command "${name}"`);
|
|
71
|
-
const
|
|
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 };
|
|
147
|
+
const args = parseCommand(spec, rest);
|
|
81
148
|
const cwd = process.cwd();
|
|
82
149
|
switch (name) {
|
|
83
150
|
case 'serve': {
|
|
@@ -85,21 +152,13 @@ async function dispatch(argv, overrides = {}) {
|
|
|
85
152
|
return;
|
|
86
153
|
}
|
|
87
154
|
case 'chat': {
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
cwd,
|
|
96
|
-
...(args.options.yes ? { permissionStrategy: 'full-auto' } : {}),
|
|
97
|
-
});
|
|
98
|
-
await runChatCommand({ args, config, deps });
|
|
99
|
-
}
|
|
100
|
-
finally {
|
|
101
|
-
await db.close();
|
|
102
|
-
}
|
|
155
|
+
// --yes / -y 显式放行写操作;否则按 config.permission.defaultMode 决定(默认 safe)。
|
|
156
|
+
const strategy = args.options.yes ? 'full-auto' : undefined;
|
|
157
|
+
const continueId = args.options.continue;
|
|
158
|
+
await withAgentDeps(cwd, {
|
|
159
|
+
...(strategy ? { strategy } : {}),
|
|
160
|
+
...(continueId ? { continueSessionId: continueId } : {}),
|
|
161
|
+
}, (config, deps) => runChatCommand({ args, config, deps }));
|
|
103
162
|
return;
|
|
104
163
|
}
|
|
105
164
|
case 'init': {
|
|
@@ -114,18 +173,18 @@ async function dispatch(argv, overrides = {}) {
|
|
|
114
173
|
await runPluginCommand({ args, cwd });
|
|
115
174
|
return;
|
|
116
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
|
+
}
|
|
117
185
|
case 'acp': {
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
}
|
|
186
|
+
// ACP 非交互:所有工具放行(编辑器侧自行控制执行授权)。
|
|
187
|
+
await withAgentDeps(cwd, { strategy: 'full-auto' }, (config, deps) => runAcpCommand({ config, deps }));
|
|
129
188
|
return;
|
|
130
189
|
}
|
|
131
190
|
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
|
|
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 };
|
package/dist/cli/modes/print.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createAgent, runAgent } from '../../core/agent.js';
|
|
2
|
-
import {
|
|
2
|
+
import { resolveRoute } from '../../llm/registry.js';
|
|
3
|
+
import { createSession, getSession } from '../../session/session.js';
|
|
4
|
+
import { resolveEnabledToolNames } from '../../tools/index.js';
|
|
3
5
|
/** 从事件流累积 assistant 文本。纯函数。 */
|
|
4
6
|
function collectAssistantText(events) {
|
|
5
7
|
let text = '';
|
|
@@ -9,16 +11,41 @@ function collectAssistantText(events) {
|
|
|
9
11
|
}
|
|
10
12
|
return text;
|
|
11
13
|
}
|
|
12
|
-
/**
|
|
14
|
+
/** sessions.id 为 uuid 列:非 uuid 的 --continue 输入会让 PG 直接抛原始
|
|
15
|
+
* "invalid input syntax for type uuid" SQL 错误。前置校验统一转译为
|
|
16
|
+
* 「session not found」(与不存在的会话同语义),不泄漏 SQL 原文。 */
|
|
17
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
|
+
/** Print 模式:临时会话(或 --continue 指定会话)→ agent → 收集文本 → 返回。 */
|
|
13
19
|
async function runPrintMode(config, message, deps, opts = {}) {
|
|
14
|
-
|
|
20
|
+
let session;
|
|
21
|
+
if (opts.sessionId) {
|
|
22
|
+
if (!UUID_PATTERN.test(opts.sessionId))
|
|
23
|
+
throw new Error(`session not found: ${opts.sessionId}`);
|
|
24
|
+
const existing = await getSession(deps.db, opts.sessionId);
|
|
25
|
+
if (!existing)
|
|
26
|
+
throw new Error(`session not found: ${opts.sessionId}`);
|
|
27
|
+
session = existing;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
session = await createSession(deps.db, 'cli-print', undefined, undefined, 'cli');
|
|
31
|
+
}
|
|
15
32
|
const agentConfig = {
|
|
16
33
|
provider: config.defaultProvider,
|
|
17
34
|
model: opts.model ?? config.defaultModel,
|
|
18
|
-
|
|
35
|
+
// 工具集:enabled 非空 → enabled ∩ registered;空 → 全部 registered(disabled 已在 registry 层过滤)。
|
|
36
|
+
tools: resolveEnabledToolNames(deps.toolRegistry, config),
|
|
19
37
|
plugins: config.plugins.enabled,
|
|
20
38
|
...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
|
|
21
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
|
+
}
|
|
22
49
|
const state = await createAgent(session, agentConfig, deps);
|
|
23
50
|
const events = [];
|
|
24
51
|
for await (const event of runAgent(state, [{ _tag: 'text', text: message }], deps)) {
|
package/dist/core/agent.js
CHANGED
|
@@ -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 {
|
|
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);
|
|
@@ -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
|
@@ -1,7 +1,32 @@
|
|
|
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
|
+
* 把 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>;
|
|
22
|
+
/**
|
|
23
|
+
* 不注入默认值的深合并:仅合并传入对象的自有键。
|
|
24
|
+
* 数组整体替换(providers 等列表语义);嵌套普通对象递归合并。
|
|
25
|
+
* 用于「patch 合并进某个作用域的原始文件」后再写盘。
|
|
26
|
+
*/
|
|
27
|
+
declare function mergeRaw(...cfgs: (Record<string, unknown> | undefined)[]): Record<string, unknown>;
|
|
28
|
+
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。 */
|
|
29
|
+
declare function saveConfigScoped(scope: 'global' | 'project', projectDir: string | undefined, data: Record<string, unknown>): Promise<void>;
|
|
4
30
|
declare function loadConfig(projectDir?: string): Promise<Config>;
|
|
5
|
-
declare function saveConfig(config: Config, scope: 'global' | 'project', projectDir?: string): Promise<void>;
|
|
6
31
|
export type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig, };
|
|
7
|
-
export { DEFAULT_CONFIG, loadConfig, mergeConfig,
|
|
32
|
+
export { applyScopedPatch, DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfigScoped, };
|
package/dist/core/config.js
CHANGED
|
@@ -16,19 +16,18 @@ const DEFAULT_CONFIG = {
|
|
|
16
16
|
keepRecentTokens: 4000,
|
|
17
17
|
midTurnEnabled: false,
|
|
18
18
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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:
|
|
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
|
|
30
|
+
update: { enabled: true, intervalMs: 60 * 60 * 1000, initialDelayMs: 10_000 },
|
|
32
31
|
theme: 'system',
|
|
33
32
|
locale: 'en',
|
|
34
33
|
};
|
|
@@ -69,20 +68,91 @@ function readJsonIfExists(path) {
|
|
|
69
68
|
return undefined;
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
|
-
|
|
71
|
+
/**
|
|
72
|
+
* 读取 global/project 两个作用域的**原始文件内容**(不经 DEFAULT 合并)。
|
|
73
|
+
* 供配置持久化使用:写回某个作用域时只落该作用域应有的键,
|
|
74
|
+
* 避免把合并结果(含默认值与另一作用域的配置)整体序列化进文件。
|
|
75
|
+
*/
|
|
76
|
+
function loadConfigScopes(projectDir) {
|
|
73
77
|
const globalPath = join(homedir(), GLOBAL_CONFIG_DIR, CONFIG_FILENAME);
|
|
74
78
|
const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
return { global: readJsonIfExists(globalPath), project: readJsonIfExists(projectPath) };
|
|
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;
|
|
78
111
|
}
|
|
79
|
-
|
|
112
|
+
/**
|
|
113
|
+
* 不注入默认值的深合并:仅合并传入对象的自有键。
|
|
114
|
+
* 数组整体替换(providers 等列表语义);嵌套普通对象递归合并。
|
|
115
|
+
* 用于「patch 合并进某个作用域的原始文件」后再写盘。
|
|
116
|
+
*/
|
|
117
|
+
function mergeRaw(...cfgs) {
|
|
118
|
+
const result = {};
|
|
119
|
+
for (const cfg of cfgs) {
|
|
120
|
+
if (!cfg)
|
|
121
|
+
continue;
|
|
122
|
+
for (const [key, val] of Object.entries(cfg)) {
|
|
123
|
+
if (val === undefined)
|
|
124
|
+
continue;
|
|
125
|
+
const current = result[key];
|
|
126
|
+
if (val !== null &&
|
|
127
|
+
typeof val === 'object' &&
|
|
128
|
+
!Array.isArray(val) &&
|
|
129
|
+
current !== null &&
|
|
130
|
+
typeof current === 'object' &&
|
|
131
|
+
!Array.isArray(current)) {
|
|
132
|
+
result[key] = mergeRaw(current, val);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
result[key] = val;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。 */
|
|
142
|
+
async function saveConfigScoped(scope, projectDir, data) {
|
|
80
143
|
const dir = scope === 'global'
|
|
81
144
|
? join(homedir(), GLOBAL_CONFIG_DIR)
|
|
82
145
|
: join(projectDir ?? process.cwd(), '.c0de');
|
|
83
146
|
if (!existsSync(dir))
|
|
84
147
|
mkdirSync(dir, { recursive: true });
|
|
85
148
|
const path = join(dir, CONFIG_FILENAME);
|
|
86
|
-
writeFileSync(path, JSON.stringify(
|
|
149
|
+
writeFileSync(path, JSON.stringify(data, null, 2), 'utf-8');
|
|
150
|
+
}
|
|
151
|
+
async function loadConfig(projectDir) {
|
|
152
|
+
const globalPath = join(homedir(), GLOBAL_CONFIG_DIR, CONFIG_FILENAME);
|
|
153
|
+
const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
|
|
154
|
+
const global = readJsonIfExists(globalPath);
|
|
155
|
+
const project = readJsonIfExists(projectPath);
|
|
156
|
+
return mergeConfig(global, project);
|
|
87
157
|
}
|
|
88
|
-
export { DEFAULT_CONFIG, loadConfig, mergeConfig,
|
|
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';
|
package/dist/core/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// steering, slash commands, and compaction bridge.
|
|
3
3
|
export { abortAgent, createAgent, getAgentStatus, isAgentPaused, pauseAgent, resumeAgent, runAgent, } from './agent.js';
|
|
4
4
|
export { createSummarizer, runCompaction } from './compact.js';
|
|
5
|
-
export { DEFAULT_CONFIG, loadConfig, mergeConfig
|
|
5
|
+
export { DEFAULT_CONFIG, loadConfig, mergeConfig } from './config.js';
|
|
6
6
|
export { calibrateEstimate, createTokenBudget, estimateBudget, fitToBudget, shouldCompact, } from './context.js';
|
|
7
7
|
export { agentLoop } from './loop.js';
|
|
8
8
|
export { buildSystemPrompt } from './prompt.js';
|
package/dist/core/loop.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveRoute } from '../llm/registry.js';
|
|
|
3
3
|
import { detectProjectInfo } from '../project/detect.js';
|
|
4
4
|
import { entriesToChatMessages, getSessionContext } from '../session/context.js';
|
|
5
5
|
import { getMessages } from '../session/message.js';
|
|
6
|
+
import { updateSessionLastRun } from '../session/session.js';
|
|
6
7
|
import { estimateTokens } from '../session/token.js';
|
|
7
8
|
import { calibrateEstimate, createTokenBudget, estimateBudget } from './context.js';
|
|
8
9
|
import { runCompactionIfNeeded } from './loop/compaction.js';
|
|
@@ -69,15 +70,24 @@ export async function* agentLoop(state, deps) {
|
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
72
|
if (state.status._tag === 'paused') {
|
|
72
|
-
|
|
73
|
+
// 暂停点:上一轮原子操作(工具执行批次)已完成。
|
|
74
|
+
// 持久化 paused 状态供热更新/重启后恢复,随后阻塞等待 resume。
|
|
75
|
+
state.pauseYielded = false;
|
|
73
76
|
yield { _tag: 'status_change', status: state.status };
|
|
77
|
+
await updateSessionLastRun(deps.db, state.session.id, {
|
|
78
|
+
status: 'paused',
|
|
79
|
+
agentName: state.config.agentName,
|
|
80
|
+
provider: state.config.provider,
|
|
81
|
+
model: state.config.model,
|
|
82
|
+
startedAt: state.lastRunStartedAt ?? Date.now(),
|
|
83
|
+
}).catch(() => { });
|
|
74
84
|
await waitForResume(state);
|
|
75
|
-
if (state.status._tag
|
|
76
|
-
return;
|
|
85
|
+
if (state.status._tag === 'paused')
|
|
86
|
+
return; // 等待期间被 abort 等终结
|
|
77
87
|
}
|
|
78
88
|
state.status = { _tag: 'running', turnCount: turn };
|
|
79
89
|
const steering = drainSteering(state);
|
|
80
|
-
const { entries, snapshots } = await getSessionContext(deps.db, state.session.id);
|
|
90
|
+
const { entries, snapshots } = await getSessionContext(deps.db, state.session.id, deps.cwd);
|
|
81
91
|
let chatMessages = entriesToChatMessages(entries, snapshots);
|
|
82
92
|
// steering 消息必须插入到最后一条 user 消息之前(而非末尾 push),
|
|
83
93
|
// 使 notice 在相关 user 消息的同一 turn 生效。
|