c0de-agent 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -7
- package/dist/cli/commands/acp.js +2 -0
- package/dist/cli/commands/chat.js +2 -0
- package/dist/cli/commands/config.js +8 -4
- package/dist/cli/commands/serve.d.ts +1 -0
- package/dist/cli/commands/serve.js +4 -1
- 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 +68 -37
- package/dist/cli/modes/print.d.ts +3 -1
- package/dist/cli/modes/print.js +21 -4
- package/dist/core/agent.js +3 -4
- package/dist/core/config.d.ts +18 -1
- package/dist/core/config.js +55 -4
- package/dist/core/loop/compaction.d.ts +31 -0
- package/dist/core/loop/compaction.js +137 -0
- package/dist/core/loop/persist.d.ts +11 -0
- package/dist/core/loop/persist.js +107 -0
- package/dist/core/loop/segment.d.ts +8 -0
- package/dist/core/loop/segment.js +72 -0
- package/dist/core/loop/stream-collect.d.ts +47 -0
- package/dist/core/loop/stream-collect.js +183 -0
- package/dist/core/loop/subagent.d.ts +12 -0
- package/dist/core/loop/subagent.js +171 -0
- package/dist/core/loop/todo.d.ts +5 -0
- package/dist/core/loop/todo.js +26 -0
- package/dist/core/loop.d.ts +2 -20
- package/dist/core/loop.js +13 -659
- package/dist/core/prompt-registry.d.ts +1 -1
- package/dist/core/prompt-registry.js +10 -0
- package/dist/core/slash.js +1 -1
- package/dist/core/todo-tags.d.ts +46 -0
- package/dist/core/todo-tags.js +192 -0
- package/dist/core/workflows/builtins.js +4 -4
- package/dist/core/workflows/discovery.d.ts +1 -1
- package/dist/core/workflows/discovery.js +4 -1
- package/dist/core/workflows/index.d.ts +1 -1
- package/dist/db/schema.d.ts +17 -0
- package/dist/db/schema.js +3 -1
- package/dist/llm/registry.d.ts +30 -3
- package/dist/llm/registry.js +25 -7
- package/dist/llm/retry.d.ts +4 -2
- package/dist/llm/retry.js +7 -6
- package/dist/llm/schema/errors.d.ts +13 -3
- package/dist/llm/schema/errors.js +35 -3
- package/dist/llm/transport.js +5 -1
- package/dist/project/resolve.js +2 -2
- package/dist/server/agent-manager.d.ts +15 -0
- package/dist/server/agent-manager.js +35 -11
- package/dist/server/app.js +2 -1
- package/dist/server/context.js +2 -0
- package/dist/server/middleware/cors.d.ts +11 -2
- package/dist/server/middleware/cors.js +15 -12
- package/dist/server/routes/chat.js +212 -145
- package/dist/server/routes/config.js +7 -3
- package/dist/server/routes/session.js +6 -2
- package/dist/server/routes/terminal.js +12 -0
- package/dist/server/routes/todo.js +5 -2
- package/dist/server/routes/update.d.ts +1 -0
- package/dist/server/routes/update.js +11 -4
- package/dist/server/routes/workflows.js +2 -2
- package/dist/server/server.d.ts +28 -3
- package/dist/server/server.js +127 -38
- package/dist/server/types.d.ts +9 -1
- package/dist/session/branch.d.ts +10 -2
- package/dist/session/branch.js +98 -29
- 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/snapshot.d.ts +9 -3
- package/dist/session/snapshot.js +45 -4
- package/dist/session/squash.js +32 -24
- package/dist/session/types.d.ts +2 -0
- package/dist/shared/types/agent.d.ts +11 -0
- package/dist/shared/types/config.d.ts +7 -6
- package/dist/shared/utils/path.d.ts +2 -3
- package/dist/shared/utils/path.js +20 -4
- package/dist/tools/builtin/todo.d.ts +7 -0
- package/dist/tools/builtin/todo.js +28 -20
- package/dist/tools/index.d.ts +14 -3
- package/dist/tools/index.js +36 -14
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/ipc.d.ts +32 -5
- package/dist/update/ipc.js +53 -7
- package/dist/update/updater.d.ts +7 -3
- package/dist/update/updater.js +14 -6
- 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/meta/0005_snapshot.json +855 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -7
- package/dist/mcp/index.d.ts +0 -1
- package/dist/mcp/index.js +0 -3
package/dist/session/snapshot.js
CHANGED
|
@@ -13,10 +13,15 @@ function rowToSnapshot(row) {
|
|
|
13
13
|
tokenCount: row.tokenCount ?? 0,
|
|
14
14
|
version: row.version ?? 1,
|
|
15
15
|
createdAt,
|
|
16
|
+
...(row.mtimeMs != null ? { mtimeMs: Number(row.mtimeMs) } : {}),
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
/** Create or update a file snapshot. Always creates a new version. Returns the snapshot id.
|
|
19
|
-
|
|
19
|
+
/** Create or update a file snapshot. Always creates a new version. Returns the snapshot id.
|
|
20
|
+
* 未传 mtimeMs 时从同文件已有最新行透传(而非置空):compaction/squash 等从
|
|
21
|
+
* 工具结果重建快照的调用点拿不到磁盘 mtime,透传旧值可避免每次压缩后热文件
|
|
22
|
+
* 被误判过期而多一轮冗余重读(自愈无害但浪费;旧值 ≤ 内容实际新鲜度,
|
|
23
|
+
* 绝不会掩盖磁盘真实变更)。 */
|
|
24
|
+
async function upsertFileSnapshot(handle, sessionId, filePath, content, mtimeMs) {
|
|
20
25
|
const contentHash = createHash('sha256').update(content).digest('hex');
|
|
21
26
|
const tokenCount = estimateTokens(content);
|
|
22
27
|
const [existing] = await handle.db
|
|
@@ -26,9 +31,20 @@ async function upsertFileSnapshot(handle, sessionId, filePath, content) {
|
|
|
26
31
|
.orderBy(desc(fileSnapshots.version))
|
|
27
32
|
.limit(1);
|
|
28
33
|
const version = (existing?.version ?? 0) + 1;
|
|
34
|
+
// mtimeMs 是浮点(stat 返回微秒精度),bigint 列不接受小数;取整存储。
|
|
35
|
+
// 调用方未显式提供时继承已有行的 mtimeMs(见函数头注释)。
|
|
36
|
+
const effectiveMtimeMs = mtimeMs ?? (existing?.mtimeMs != null ? Number(existing.mtimeMs) : null);
|
|
29
37
|
const [row] = await handle.db
|
|
30
38
|
.insert(fileSnapshots)
|
|
31
|
-
.values({
|
|
39
|
+
.values({
|
|
40
|
+
sessionId,
|
|
41
|
+
filePath,
|
|
42
|
+
content,
|
|
43
|
+
contentHash,
|
|
44
|
+
tokenCount,
|
|
45
|
+
version,
|
|
46
|
+
...(effectiveMtimeMs != null ? { mtimeMs: Math.round(effectiveMtimeMs) } : {}),
|
|
47
|
+
})
|
|
32
48
|
.returning();
|
|
33
49
|
if (!row)
|
|
34
50
|
throw new Error('Failed to insert file snapshot');
|
|
@@ -57,4 +73,29 @@ async function checkFileSnapshot(handle, sessionId, filePath) {
|
|
|
57
73
|
const snapshot = await getLatestFileSnapshot(handle, sessionId, filePath);
|
|
58
74
|
return snapshot?.content ?? null;
|
|
59
75
|
}
|
|
60
|
-
|
|
76
|
+
/** 把源会话每个文件的最新快照复制到目标会话(fork 分支用,P1-4)。 */
|
|
77
|
+
async function copyFileSnapshots(handle, fromSessionId, toSessionId) {
|
|
78
|
+
const rows = await handle.db
|
|
79
|
+
.select()
|
|
80
|
+
.from(fileSnapshots)
|
|
81
|
+
.where(eq(fileSnapshots.sessionId, fromSessionId));
|
|
82
|
+
const latest = new Map();
|
|
83
|
+
for (const r of rows) {
|
|
84
|
+
const prev = latest.get(r.filePath);
|
|
85
|
+
if (!prev || (r.version ?? 0) > (prev.version ?? 0))
|
|
86
|
+
latest.set(r.filePath, r);
|
|
87
|
+
}
|
|
88
|
+
for (const r of latest.values()) {
|
|
89
|
+
await handle.db.insert(fileSnapshots).values({
|
|
90
|
+
sessionId: toSessionId,
|
|
91
|
+
entryId: r.entryId,
|
|
92
|
+
filePath: r.filePath,
|
|
93
|
+
content: r.content,
|
|
94
|
+
contentHash: r.contentHash,
|
|
95
|
+
tokenCount: r.tokenCount,
|
|
96
|
+
version: r.version,
|
|
97
|
+
mtimeMs: r.mtimeMs,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export { checkFileSnapshot, copyFileSnapshots, getFileSnapshots, getLatestFileSnapshot, upsertFileSnapshot, };
|
package/dist/session/squash.js
CHANGED
|
@@ -50,31 +50,39 @@ async function squashRecent(handle, sessionId, count, summarizer, config) {
|
|
|
50
50
|
${history}`;
|
|
51
51
|
const summary = await summarizer(prompt);
|
|
52
52
|
const squashEntryId = generateId();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const fileSnapshotIds =
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
// Atomic rewrite: archive originals → upsert snapshots → delete originals →
|
|
54
|
+
// insert summary. If any step throws, the whole transaction rolls back and
|
|
55
|
+
// the original message history is left intact. (Mirrors compactSession.)
|
|
56
|
+
const { archiveId, fileSnapshotIds } = await handle.db.transaction(async (tx) => {
|
|
57
|
+
const txHandle = { db: tx, close: handle.close };
|
|
58
|
+
const archiveId = cfg.archiveOriginal
|
|
59
|
+
? await archiveOriginalEntries(txHandle, sessionId, toSquash, 'squash', summary, squashEntryId)
|
|
60
|
+
: generateId();
|
|
61
|
+
const fileSnapshotIds = [];
|
|
62
|
+
if (cfg.preserveFileSnapshots) {
|
|
63
|
+
const hotFiles = extractHotFiles(toSquash);
|
|
64
|
+
for (const file of hotFiles) {
|
|
65
|
+
// 不传 mtimeMs:由 upsertFileSnapshot 从已有行透传,压缩后热文件不误判过期。
|
|
66
|
+
const id = await upsertFileSnapshot(txHandle, sessionId, file.path, file.content);
|
|
67
|
+
fileSnapshotIds.push(id);
|
|
68
|
+
}
|
|
62
69
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
await deleteEntriesByIds(txHandle, toSquash.map((m) => m.id));
|
|
71
|
+
await insertEntry(txHandle, {
|
|
72
|
+
id: squashEntryId,
|
|
73
|
+
sessionId,
|
|
74
|
+
tag: 'squash',
|
|
75
|
+
content: {
|
|
76
|
+
summary,
|
|
77
|
+
squashedEntryIds: toSquash.map((m) => m.id),
|
|
78
|
+
archiveId,
|
|
79
|
+
},
|
|
80
|
+
tokenCount: estimateTokens(summary),
|
|
81
|
+
// Position at the first squashed message's timestamp so it sorts between
|
|
82
|
+
// the prefix and the kept tail under createdAt-ascending order.
|
|
83
|
+
createdAt: toSquash[0] ? new Date(toSquash[0].createdAt) : new Date(),
|
|
84
|
+
});
|
|
85
|
+
return { archiveId, fileSnapshotIds };
|
|
78
86
|
});
|
|
79
87
|
return {
|
|
80
88
|
compacted: true,
|
package/dist/session/types.d.ts
CHANGED
|
@@ -117,6 +117,8 @@ type FileSnapshot = {
|
|
|
117
117
|
contentHash: string;
|
|
118
118
|
tokenCount: number;
|
|
119
119
|
version: number;
|
|
120
|
+
/** 快照创建时文件磁盘 mtime(毫秒);旧行无此字段(迁移前写入) */
|
|
121
|
+
mtimeMs?: number;
|
|
120
122
|
createdAt: number;
|
|
121
123
|
};
|
|
122
124
|
export type { ArchiveRef, BranchSummaryEntry, CompactionArchive, CompactionConfig, CompactionEntry, CompactionResult, FileSnapshot, HotFile, MessageInput, SessionEntry, SessionTreeNode, SquashConfig, SquashEntry, SteeringEntry, Summarizer, };
|
|
@@ -184,6 +184,17 @@ type AgentEvent = {
|
|
|
184
184
|
archiveId?: string;
|
|
185
185
|
compactedCount: number;
|
|
186
186
|
keptCount: number;
|
|
187
|
+
}
|
|
188
|
+
/** Tag-based todo 操作成功后发射,前端据此刷新 TodoPanel。 */
|
|
189
|
+
| {
|
|
190
|
+
_tag: 'todo_update';
|
|
191
|
+
phases: {
|
|
192
|
+
name: string;
|
|
193
|
+
tasks: {
|
|
194
|
+
content: string;
|
|
195
|
+
status: string;
|
|
196
|
+
}[];
|
|
197
|
+
}[];
|
|
187
198
|
} | {
|
|
188
199
|
_tag: 'done';
|
|
189
200
|
};
|
|
@@ -64,16 +64,17 @@ type UpdateConfig = {
|
|
|
64
64
|
intervalMs: number;
|
|
65
65
|
/** 启动后首次检查的延迟(毫秒),默认 10 秒。 */
|
|
66
66
|
initialDelayMs: number;
|
|
67
|
-
/** 发现新版本后自动应用(无感知),false 则仅前端提示手动确认(默认 false)。 */
|
|
68
|
-
autoApply: boolean;
|
|
69
67
|
};
|
|
70
|
-
/** Server security configuration (spec §24.2)
|
|
68
|
+
/** Server security configuration (spec §24.2)。 */
|
|
71
69
|
type SecurityConfig = {
|
|
72
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* 是否启用 Bearer token 认证(默认 true)。
|
|
72
|
+
* 关闭为显式选择(可信网络/无人值守场景),会取消一切 API 鉴权。
|
|
73
|
+
*/
|
|
73
74
|
authEnabled: boolean;
|
|
74
|
-
/** Bearer token
|
|
75
|
+
/** Bearer token;未提供时服务端自动生成并持久化到全局数据目录(跨重启/热更新稳定)。 */
|
|
75
76
|
token?: string;
|
|
76
|
-
/** 额外允许的 CORS origin
|
|
77
|
+
/** 额外允许的 CORS origin(本地回环始终允许;局域网/远程访问需显式添加)。 */
|
|
77
78
|
allowedOrigins: string[];
|
|
78
79
|
};
|
|
79
80
|
/** Global application configuration. */
|
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
* - `resolve(root, requestPath)` 解析后,若结果不在 root 子树内则返回 null。
|
|
6
6
|
* - 判定逻辑:`relative(root, resolved)` 以 `..` 开头即越界;
|
|
7
7
|
* 再用 `resolve(root, rel) === resolved` 兜底,防止相对段恰好名为 root 的边角情况。
|
|
8
|
+
* - symlink 防逃逸(P2-5):root 与目标(或其最近已存在祖先)经 realpath 解析后,
|
|
9
|
+
* 目标必须仍落在 root 实路径内;否则返回 null。
|
|
8
10
|
*
|
|
9
11
|
* 返回解析后的绝对路径;越界返回 null。
|
|
10
|
-
*
|
|
11
|
-
* 注意:本函数基于 `relative`,不解析符号链接(symlink)。若 root 内存在指向
|
|
12
|
-
* root 外的符号链接,本函数无法阻止经由该链接的逃逸——symlink 逃逸不在本次范围内。
|
|
13
12
|
*/
|
|
14
13
|
declare function safeResolve(root: string, requestPath: string): string | null;
|
|
15
14
|
export { safeResolve };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, relative, resolve, sep } from 'node:path';
|
|
2
3
|
/**
|
|
3
4
|
* 安全路径解析:确保 requestPath 解析后落在 root 内,防止路径穿越
|
|
4
5
|
* (如 `../etc/passwd`、绝对路径 `/etc/passwd`)。
|
|
@@ -6,11 +7,10 @@ import { relative, resolve } from 'node:path';
|
|
|
6
7
|
* - `resolve(root, requestPath)` 解析后,若结果不在 root 子树内则返回 null。
|
|
7
8
|
* - 判定逻辑:`relative(root, resolved)` 以 `..` 开头即越界;
|
|
8
9
|
* 再用 `resolve(root, rel) === resolved` 兜底,防止相对段恰好名为 root 的边角情况。
|
|
10
|
+
* - symlink 防逃逸(P2-5):root 与目标(或其最近已存在祖先)经 realpath 解析后,
|
|
11
|
+
* 目标必须仍落在 root 实路径内;否则返回 null。
|
|
9
12
|
*
|
|
10
13
|
* 返回解析后的绝对路径;越界返回 null。
|
|
11
|
-
*
|
|
12
|
-
* 注意:本函数基于 `relative`,不解析符号链接(symlink)。若 root 内存在指向
|
|
13
|
-
* root 外的符号链接,本函数无法阻止经由该链接的逃逸——symlink 逃逸不在本次范围内。
|
|
14
14
|
*/
|
|
15
15
|
function safeResolve(root, requestPath) {
|
|
16
16
|
const resolved = resolve(root, requestPath);
|
|
@@ -18,6 +18,22 @@ function safeResolve(root, requestPath) {
|
|
|
18
18
|
if (rel.startsWith('..') || resolve(root, rel) !== resolved) {
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
|
+
try {
|
|
22
|
+
const rootReal = realpathSync(root);
|
|
23
|
+
let probe = resolved;
|
|
24
|
+
while (!existsSync(probe)) {
|
|
25
|
+
const parent = dirname(probe);
|
|
26
|
+
if (parent === probe)
|
|
27
|
+
break;
|
|
28
|
+
probe = parent;
|
|
29
|
+
}
|
|
30
|
+
const probeReal = realpathSync(probe);
|
|
31
|
+
if (probeReal !== rootReal && !probeReal.startsWith(rootReal + sep))
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// realpath 失败(root 不存在等):保持原有 relative 判定结果
|
|
36
|
+
}
|
|
21
37
|
return resolved;
|
|
22
38
|
}
|
|
23
39
|
export { safeResolve };
|
|
@@ -47,6 +47,11 @@ export declare function nextActionableTask(phases: readonly TodoPhase[]): TodoIt
|
|
|
47
47
|
* `descriptions`. Normalize-then-equal first, with a substring fallback
|
|
48
48
|
* in either direction (≥6 char overlap on the contained side). */
|
|
49
49
|
export declare function todoMatchesAnyDescription(content: string, descriptions: readonly string[]): boolean;
|
|
50
|
+
/** Apply a single todo op to existing phases. Returns new phases + errors. */
|
|
51
|
+
declare function applyParams(phases: TodoPhase[], params: TodoInput): {
|
|
52
|
+
phases: TodoPhase[];
|
|
53
|
+
errors: string[];
|
|
54
|
+
};
|
|
50
55
|
/** Render todo phases as a Markdown checklist suitable for editing/copying. */
|
|
51
56
|
export declare function phasesToMarkdown(phases: TodoPhase[]): string;
|
|
52
57
|
/** Parse a Markdown checklist back into todo phases. */
|
|
@@ -54,6 +59,7 @@ export declare function markdownToPhases(md: string): {
|
|
|
54
59
|
phases: TodoPhase[];
|
|
55
60
|
errors: string[];
|
|
56
61
|
};
|
|
62
|
+
export declare function formatSummary(phases: TodoPhase[], errors: string[], readOnly?: boolean): string;
|
|
57
63
|
/** Extract the latest todo phases from stored messages (tool results).
|
|
58
64
|
* Scans backwards for the most recent `todo` tool result with phases metadata. */
|
|
59
65
|
export declare function getLatestTodoPhasesFromMessages(messages: {
|
|
@@ -65,3 +71,4 @@ export declare function getLatestTodoPhasesFromMessages(messages: {
|
|
|
65
71
|
* State is held in-memory via ctx.todoState hook (dependency-reversal). */
|
|
66
72
|
export declare const todoTool: ToolDef;
|
|
67
73
|
export type { TodoInput, TodoItem, TodoPhase, TodoStatus };
|
|
74
|
+
export { applyParams };
|
|
@@ -293,10 +293,9 @@ export function phasesToMarkdown(phases) {
|
|
|
293
293
|
if (phases.length === 0)
|
|
294
294
|
return '# Todos\n';
|
|
295
295
|
const out = [];
|
|
296
|
-
for (
|
|
296
|
+
for (const [i, phase] of phases.entries()) {
|
|
297
297
|
if (i > 0)
|
|
298
298
|
out.push('');
|
|
299
|
-
const phase = phases[i];
|
|
300
299
|
out.push(`# ${phase.name}`);
|
|
301
300
|
for (const task of phase.tasks) {
|
|
302
301
|
out.push(`- [${STATUS_TO_MARKER[task.status]}] ${task.content}`);
|
|
@@ -320,14 +319,13 @@ export function markdownToPhases(md) {
|
|
|
320
319
|
const phases = [];
|
|
321
320
|
let currentPhase;
|
|
322
321
|
const lines = md.split(/\r?\n/);
|
|
323
|
-
for (
|
|
324
|
-
const raw = lines[lineNum];
|
|
322
|
+
for (const [lineNum, raw] of lines.entries()) {
|
|
325
323
|
const trimmed = raw.trim();
|
|
326
324
|
if (!trimmed)
|
|
327
325
|
continue;
|
|
328
326
|
const headingMatch = /^#{1,6}\s+(.+?)\s*$/.exec(trimmed);
|
|
329
327
|
if (headingMatch) {
|
|
330
|
-
currentPhase = { name: headingMatch[1].trim(), tasks: [] };
|
|
328
|
+
currentPhase = { name: (headingMatch[1] ?? '').trim(), tasks: [] };
|
|
331
329
|
phases.push(currentPhase);
|
|
332
330
|
continue;
|
|
333
331
|
}
|
|
@@ -343,7 +341,7 @@ export function markdownToPhases(md) {
|
|
|
343
341
|
errors.push(`Line ${lineNum + 1}: unknown status marker "[${marker}]" (use [ ], [x], [/], [-])`);
|
|
344
342
|
continue;
|
|
345
343
|
}
|
|
346
|
-
currentPhase.tasks.push({ content: taskMatch[2].trim(), status });
|
|
344
|
+
currentPhase.tasks.push({ content: (taskMatch[2] ?? '').trim(), status });
|
|
347
345
|
continue;
|
|
348
346
|
}
|
|
349
347
|
errors.push(`Line ${lineNum + 1}: unrecognized syntax "${trimmed}"`);
|
|
@@ -354,7 +352,7 @@ export function markdownToPhases(md) {
|
|
|
354
352
|
// =============================================================================
|
|
355
353
|
// Summary formatter
|
|
356
354
|
// =============================================================================
|
|
357
|
-
function formatSummary(phases, errors, readOnly = false) {
|
|
355
|
+
export function formatSummary(phases, errors, readOnly = false) {
|
|
358
356
|
const tasks = phases.flatMap((phase) => phase.tasks);
|
|
359
357
|
if (tasks.length === 0) {
|
|
360
358
|
if (errors.length > 0)
|
|
@@ -362,16 +360,20 @@ function formatSummary(phases, errors, readOnly = false) {
|
|
|
362
360
|
return readOnly ? 'Todo list is empty.' : 'Todo list cleared.';
|
|
363
361
|
}
|
|
364
362
|
const remainingByPhase = phases
|
|
365
|
-
.map((phase) => ({
|
|
363
|
+
.map((phase, pi) => ({
|
|
366
364
|
name: phase.name,
|
|
367
|
-
tasks: phase.tasks
|
|
365
|
+
tasks: phase.tasks
|
|
366
|
+
.map((task, ti) => ({ task, seq: `${pi + 1}-${ti + 1}` }))
|
|
367
|
+
.filter(({ task }) => task.status === 'pending' || task.status === 'in_progress'),
|
|
368
368
|
}))
|
|
369
369
|
.filter((phase) => phase.tasks.length > 0);
|
|
370
|
-
const remainingTasks = remainingByPhase.flatMap((phase) => phase.tasks.map((task) => ({ ...task, phase: phase.name })));
|
|
370
|
+
const remainingTasks = remainingByPhase.flatMap((phase) => phase.tasks.map(({ task, seq }) => ({ ...task, seq, phase: phase.name })));
|
|
371
371
|
let currentIdx = phases.findIndex((phase) => phase.tasks.some((task) => task.status === 'pending' || task.status === 'in_progress'));
|
|
372
372
|
if (currentIdx === -1)
|
|
373
373
|
currentIdx = phases.length - 1;
|
|
374
374
|
const current = phases[currentIdx];
|
|
375
|
+
if (!current)
|
|
376
|
+
return errors.length > 0 ? `Errors: ${errors.join('; ')}` : 'Todo list cleared.';
|
|
375
377
|
const done = current.tasks.filter((task) => task.status === 'completed' || task.status === 'abandoned').length;
|
|
376
378
|
const lines = [];
|
|
377
379
|
if (errors.length > 0)
|
|
@@ -382,7 +384,7 @@ function formatSummary(phases, errors, readOnly = false) {
|
|
|
382
384
|
else {
|
|
383
385
|
lines.push(`Remaining items (${remainingTasks.length}):`);
|
|
384
386
|
for (const task of remainingTasks) {
|
|
385
|
-
lines.push(` - ${task.content} [${task.status}] (${task.phase})`);
|
|
387
|
+
lines.push(` - ${task.seq}: ${task.content} [${task.status}] (${task.phase})`);
|
|
386
388
|
}
|
|
387
389
|
}
|
|
388
390
|
const closedAll = tasks.filter((task) => task.status === 'completed' || task.status === 'abandoned').length;
|
|
@@ -392,16 +394,17 @@ function formatSummary(phases, errors, readOnly = false) {
|
|
|
392
394
|
lines.push(`Active phase ${currentIdx + 1}/${phases.length} "${current.name}" (${done}/${current.tasks.length})${workedAhead
|
|
393
395
|
? ' — earliest phase with open tasks; the in-progress pointer auto-advances to the earliest open task on each completion, so it can sit behind out-of-order work (nothing was un-completed).'
|
|
394
396
|
: '.'}`);
|
|
395
|
-
for (const phase of phases) {
|
|
397
|
+
for (const [pi, phase] of phases.entries()) {
|
|
396
398
|
lines.push(` ${phase.name}:`);
|
|
397
|
-
for (const task of phase.tasks) {
|
|
399
|
+
for (const [ti, task] of phase.tasks.entries()) {
|
|
400
|
+
const seq = `${pi + 1}-${ti + 1}`;
|
|
398
401
|
const checkbox = task.status === 'completed' ? '[X]' : '[ ]';
|
|
399
402
|
const tag = task.status === 'in_progress'
|
|
400
403
|
? ' (in progress)'
|
|
401
404
|
: task.status === 'abandoned'
|
|
402
405
|
? ' (dropped)'
|
|
403
406
|
: '';
|
|
404
|
-
lines.push(` - ${checkbox} ${task.content}${tag}`);
|
|
407
|
+
lines.push(` - ${checkbox} ${seq}: ${task.content}${tag}`);
|
|
405
408
|
}
|
|
406
409
|
}
|
|
407
410
|
return lines.join('\n');
|
|
@@ -414,20 +417,21 @@ function formatSummary(phases, errors, readOnly = false) {
|
|
|
414
417
|
export function getLatestTodoPhasesFromMessages(messages) {
|
|
415
418
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
416
419
|
const msg = messages[i];
|
|
417
|
-
if (msg
|
|
420
|
+
if (msg?.role !== 'tool')
|
|
418
421
|
continue;
|
|
419
422
|
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
420
423
|
const part = msg.content[j];
|
|
421
|
-
if (
|
|
424
|
+
if (part?._tag !== 'tool_result')
|
|
422
425
|
continue;
|
|
423
426
|
if (part.tool !== 'todo')
|
|
424
427
|
continue;
|
|
425
428
|
const output = part.output;
|
|
426
|
-
if (
|
|
429
|
+
if (output?._tag !== 'success')
|
|
427
430
|
continue;
|
|
428
431
|
const metadata = output.metadata;
|
|
429
|
-
|
|
430
|
-
|
|
432
|
+
const phases = metadata?.phases;
|
|
433
|
+
if (Array.isArray(phases)) {
|
|
434
|
+
return clonePhases(phases);
|
|
431
435
|
}
|
|
432
436
|
}
|
|
433
437
|
}
|
|
@@ -463,7 +467,10 @@ const todoParameters = {
|
|
|
463
467
|
},
|
|
464
468
|
},
|
|
465
469
|
task: { type: 'string', description: 'Task content (for start/done/drop/rm)' },
|
|
466
|
-
phase: {
|
|
470
|
+
phase: {
|
|
471
|
+
type: 'string',
|
|
472
|
+
description: 'Phase name (for done/drop/rm/append, or init flat mode)',
|
|
473
|
+
},
|
|
467
474
|
items: {
|
|
468
475
|
type: 'array',
|
|
469
476
|
items: { type: 'string' },
|
|
@@ -515,3 +522,4 @@ export const todoTool = {
|
|
|
515
522
|
return { _tag: 'success', output, metadata };
|
|
516
523
|
},
|
|
517
524
|
};
|
|
525
|
+
export { applyParams };
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -6,11 +6,11 @@ export { dapTools } from './builtin/dap.js';
|
|
|
6
6
|
export { editTool } from './builtin/edit.js';
|
|
7
7
|
export { globTool, globToRegex } from './builtin/glob.js';
|
|
8
8
|
export { grepTool } from './builtin/grep.js';
|
|
9
|
+
export { kanbanTool } from './builtin/kanban.js';
|
|
9
10
|
export { readTool } from './builtin/read.js';
|
|
10
11
|
export { createDefaultURLRegistry, createFileResolver, createSkillResolver, } from './builtin/resolvers.js';
|
|
11
|
-
export { kanbanTool } from './builtin/kanban.js';
|
|
12
|
-
export { todoTool } from './builtin/todo.js';
|
|
13
12
|
export type { TodoInput, TodoItem, TodoPhase, TodoStatus } from './builtin/todo.js';
|
|
13
|
+
export { todoTool } from './builtin/todo.js';
|
|
14
14
|
export { writeTool } from './builtin/write.js';
|
|
15
15
|
export { yieldTool } from './builtin/yield.js';
|
|
16
16
|
export { executeTool } from './executor.js';
|
|
@@ -23,10 +23,21 @@ export { formatForLLM, resolveProvider, runWebSearch } from './websearch/index.j
|
|
|
23
23
|
export type { WebSearchProvider, WebSearchProviderId, WebSearchResponse, WebSearchSource, } from './websearch/types.js';
|
|
24
24
|
export { createWebSearchTool } from './websearch/websearch.js';
|
|
25
25
|
import type { Config } from '../shared/types/config.js';
|
|
26
|
+
import type { ToolRegistry } from './types.js';
|
|
26
27
|
/**
|
|
27
28
|
* Create a registry pre-loaded with all builtin tools:
|
|
28
29
|
* read, write, edit, glob, grep, bash, task, websearch, and the debug_* set.
|
|
29
30
|
*
|
|
31
|
+
* config.tools.disabled 中的工具**不注册**——registry 层过滤保证两个入口
|
|
32
|
+
* (CLI print / Web chat / /api/tools 列表)统一生效,禁用的工具对 agent 完全不可见。
|
|
30
33
|
* @param config 可选配置;websearch 工具按 config.websearch 构造。省略时用 DEFAULT_CONFIG。
|
|
31
34
|
*/
|
|
32
|
-
export declare function createDefaultRegistry(config?: Config):
|
|
35
|
+
export declare function createDefaultRegistry(config?: Config): ToolRegistry;
|
|
36
|
+
/**
|
|
37
|
+
* 解析本轮启用的工具名列表(P1-1 修复:config.tools.enabled/disabled 双入口统一生效)。
|
|
38
|
+
*
|
|
39
|
+
* - explicit(前端显式选择)非 undefined → 以其为准,但过滤掉 disabled 与未注册名。
|
|
40
|
+
* - 否则 enabled 非空 → enabled ∩ registered;enabled 空 → 全部 registered(空=全部)。
|
|
41
|
+
* - disabled 恒过滤(registry 已排除,此处兜底)。
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveEnabledToolNames(registry: ToolRegistry, config: Pick<Config, 'tools'>, explicit?: string[]): string[];
|
package/dist/tools/index.js
CHANGED
|
@@ -4,10 +4,10 @@ export { dapTools } from './builtin/dap.js';
|
|
|
4
4
|
export { editTool } from './builtin/edit.js';
|
|
5
5
|
export { globTool, globToRegex } from './builtin/glob.js';
|
|
6
6
|
export { grepTool } from './builtin/grep.js';
|
|
7
|
+
export { kanbanTool } from './builtin/kanban.js';
|
|
7
8
|
export { readTool } from './builtin/read.js';
|
|
8
9
|
// ── Builtin tools ───────────────────────────────────────────
|
|
9
10
|
export { createDefaultURLRegistry, createFileResolver, createSkillResolver, } from './builtin/resolvers.js';
|
|
10
|
-
export { kanbanTool } from './builtin/kanban.js';
|
|
11
11
|
export { todoTool } from './builtin/todo.js';
|
|
12
12
|
export { writeTool } from './builtin/write.js';
|
|
13
13
|
export { yieldTool } from './builtin/yield.js';
|
|
@@ -33,28 +33,50 @@ import { taskTool } from './builtin/task.js';
|
|
|
33
33
|
import { todoTool } from './builtin/todo.js';
|
|
34
34
|
import { writeTool } from './builtin/write.js';
|
|
35
35
|
import { yieldTool } from './builtin/yield.js';
|
|
36
|
-
import { createToolRegistry, registerTool } from './registry.js';
|
|
36
|
+
import { createToolRegistry, listTools, registerTool } from './registry.js';
|
|
37
37
|
import { createWebSearchTool } from './websearch/websearch.js';
|
|
38
38
|
/**
|
|
39
39
|
* Create a registry pre-loaded with all builtin tools:
|
|
40
40
|
* read, write, edit, glob, grep, bash, task, websearch, and the debug_* set.
|
|
41
41
|
*
|
|
42
|
+
* config.tools.disabled 中的工具**不注册**——registry 层过滤保证两个入口
|
|
43
|
+
* (CLI print / Web chat / /api/tools 列表)统一生效,禁用的工具对 agent 完全不可见。
|
|
42
44
|
* @param config 可选配置;websearch 工具按 config.websearch 构造。省略时用 DEFAULT_CONFIG。
|
|
43
45
|
*/
|
|
44
46
|
export function createDefaultRegistry(config = DEFAULT_CONFIG) {
|
|
45
47
|
const reg = createToolRegistry();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
const disabled = new Set(config.tools?.disabled ?? []);
|
|
49
|
+
const register = (tool) => {
|
|
50
|
+
if (!disabled.has(tool.name))
|
|
51
|
+
registerTool(reg, tool);
|
|
52
|
+
};
|
|
53
|
+
register(readTool);
|
|
54
|
+
register(writeTool);
|
|
55
|
+
register(editTool);
|
|
56
|
+
register(globTool);
|
|
57
|
+
register(grepTool);
|
|
58
|
+
register(bashTool);
|
|
59
|
+
register(kanbanTool);
|
|
60
|
+
register(taskTool);
|
|
61
|
+
register(todoTool);
|
|
62
|
+
register(yieldTool);
|
|
63
|
+
register(createWebSearchTool(config.websearch));
|
|
57
64
|
for (const tool of dapTools)
|
|
58
|
-
|
|
65
|
+
register(tool);
|
|
59
66
|
return reg;
|
|
60
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* 解析本轮启用的工具名列表(P1-1 修复:config.tools.enabled/disabled 双入口统一生效)。
|
|
70
|
+
*
|
|
71
|
+
* - explicit(前端显式选择)非 undefined → 以其为准,但过滤掉 disabled 与未注册名。
|
|
72
|
+
* - 否则 enabled 非空 → enabled ∩ registered;enabled 空 → 全部 registered(空=全部)。
|
|
73
|
+
* - disabled 恒过滤(registry 已排除,此处兜底)。
|
|
74
|
+
*/
|
|
75
|
+
export function resolveEnabledToolNames(registry, config, explicit) {
|
|
76
|
+
const all = listTools(registry).map((t) => t.name);
|
|
77
|
+
const disabled = new Set(config.tools?.disabled ?? []);
|
|
78
|
+
const base = explicit ?? (config.tools?.enabled && config.tools.enabled.length > 0
|
|
79
|
+
? config.tools.enabled
|
|
80
|
+
: all);
|
|
81
|
+
return base.filter((n) => all.includes(n) && !disabled.has(n));
|
|
82
|
+
}
|
package/dist/update/index.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export { orderSessionsByParent, restoreSessions, serializeSessions } from './sna
|
|
|
7
7
|
export type { HotUpdateOptions, HotUpdateResult, SpawnFn } from './updater.js';
|
|
8
8
|
export { cleanupSnapshot, performHotUpdate } from './updater.js';
|
|
9
9
|
export type { UpdateCheckResult } from './version.js';
|
|
10
|
-
export { checkForUpdate, compareSemver } from './version.js';
|
|
10
|
+
export { checkForUpdate, compareSemver, getCurrentVersion } from './version.js';
|
package/dist/update/index.js
CHANGED
|
@@ -3,4 +3,4 @@ export { createHandoffServer, requestHandoff } from './ipc.js';
|
|
|
3
3
|
export { createUpdateScheduler } from './scheduler.js';
|
|
4
4
|
export { orderSessionsByParent, restoreSessions, serializeSessions } from './snapshot.js';
|
|
5
5
|
export { cleanupSnapshot, performHotUpdate } from './updater.js';
|
|
6
|
-
export { checkForUpdate, compareSemver } from './version.js';
|
|
6
|
+
export { checkForUpdate, compareSemver, getCurrentVersion } from './version.js';
|
package/dist/update/ipc.d.ts
CHANGED
|
@@ -7,9 +7,36 @@ type HandoffServer = {
|
|
|
7
7
|
port: number;
|
|
8
8
|
close: () => Promise<void>;
|
|
9
9
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
type HandoffOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* 期望的 Bearer token。设置后 /handoff 必须携带匹配的 Authorization 头,
|
|
13
|
+
* 否则 401——防止任意本地进程用 POST /handoff 杀掉服务。
|
|
14
|
+
* 旧实例与新实例读同一 auth-token 文件(或同一 config token),天然匹配。
|
|
15
|
+
*/
|
|
16
|
+
expectedToken?: string;
|
|
17
|
+
/**
|
|
18
|
+
* 响应发出后退出进程(旧实例完整让渡:主服务与资源已在 onHandoff 中关闭)。
|
|
19
|
+
* 退出延迟 250ms 保证 HTTP 响应刷出;不设时仅响应不退出(测试用)。
|
|
20
|
+
*/
|
|
21
|
+
exitAfterResponse?: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* requestHandoff 抛出的可区分错误(调用方据此分流处理):
|
|
25
|
+
* - kind='connect':连接层失败(ECONNREFUSED 等)——旧实例未启 handoff 端点,
|
|
26
|
+
* 调用方应直接尝试绑端口;
|
|
27
|
+
* - kind='http':收到 HTTP 响应但非 2xx(status 为状态码)——如 401 token
|
|
28
|
+
* 不匹配、500 onHandoff 失败,旧实例存在但拒绝/未能让渡。
|
|
29
|
+
*/
|
|
30
|
+
type HandoffError = Error & {
|
|
31
|
+
kind: 'connect' | 'http';
|
|
32
|
+
status?: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* 旧实例启动时创建 handoff 端点。收到 POST /handoff 即触发 onHandoff
|
|
36
|
+
* (关闭主服务与资源),响应 200;exitAfterResponse 时随后退出进程。
|
|
37
|
+
*/
|
|
38
|
+
declare function createHandoffServer(onHandoff: () => Promise<void>, opts?: HandoffOptions): Promise<HandoffServer>;
|
|
39
|
+
/** 新实例请求旧实例让出端口(graceful shutdown)。失败抛可区分的 HandoffError。 */
|
|
40
|
+
declare function requestHandoff(port: number, host?: string, token?: string): Promise<void>;
|
|
41
|
+
export type { HandoffError, HandoffOptions, HandoffServer };
|
|
15
42
|
export { createHandoffServer, requestHandoff };
|