c0de-agent 1.11.0 → 1.12.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 +24 -5
- package/dist/cli/commands/chat.js +87 -1
- package/dist/cli/commands/config.js +17 -3
- package/dist/cli/commands/sessions.js +62 -3
- package/dist/cli/index.js +26 -7
- package/dist/cli/modes/print.js +9 -2
- package/dist/core/compact.d.ts +2 -0
- package/dist/core/compact.js +1 -0
- package/dist/core/config.d.ts +11 -2
- package/dist/core/config.js +73 -6
- package/dist/core/loop/compaction.d.ts +2 -1
- package/dist/core/loop/compaction.js +19 -6
- package/dist/core/loop/stream-collect.js +9 -1
- package/dist/core/loop/subagent.js +23 -5
- package/dist/core/redact.d.ts +11 -0
- package/dist/core/redact.js +49 -0
- package/dist/core/slash.js +93 -51
- package/dist/core/title.js +7 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/db/schema.d.ts +17 -0
- package/dist/db/schema.js +3 -1
- package/dist/kanban/index.d.ts +1 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +5 -1
- package/dist/kanban/store.js +76 -2
- package/dist/llm/index.d.ts +1 -1
- package/dist/llm/index.js +1 -1
- package/dist/llm/provider.js +1 -1
- package/dist/llm/retry.d.ts +5 -2
- package/dist/llm/retry.js +6 -5
- package/dist/llm/routing.d.ts +24 -1
- package/dist/llm/routing.js +29 -1
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/project.d.ts +14 -0
- package/dist/project/project.js +77 -1
- package/dist/server/agent-manager.d.ts +10 -1
- package/dist/server/agent-manager.js +14 -1
- package/dist/server/auth-manager.d.ts +11 -5
- package/dist/server/auth-manager.js +39 -20
- package/dist/server/context.js +3 -0
- package/dist/server/permission/interactive.d.ts +6 -3
- package/dist/server/permission/interactive.js +20 -4
- package/dist/server/permission/store.d.ts +12 -2
- package/dist/server/permission/store.js +31 -3
- package/dist/server/registry-config.d.ts +14 -0
- package/dist/server/registry-config.js +44 -0
- package/dist/server/routes/auth.js +32 -2
- package/dist/server/routes/chat.d.ts +12 -0
- package/dist/server/routes/chat.js +216 -38
- package/dist/server/routes/commands.d.ts +4 -2
- package/dist/server/routes/commands.js +10 -3
- package/dist/server/routes/config.js +103 -23
- package/dist/server/routes/files.js +22 -4
- package/dist/server/routes/kanban.js +68 -6
- package/dist/server/routes/permissions.d.ts +0 -4
- package/dist/server/routes/permissions.js +133 -7
- package/dist/server/routes/project.js +59 -10
- package/dist/server/routes/provider.js +48 -3
- package/dist/server/routes/session.js +298 -22
- package/dist/server/routes/update.d.ts +11 -5
- package/dist/server/routes/update.js +88 -28
- package/dist/server/server.d.ts +4 -12
- package/dist/server/server.js +198 -102
- package/dist/server/types.d.ts +7 -0
- package/dist/session/archive.d.ts +3 -1
- package/dist/session/archive.js +13 -1
- package/dist/session/branch.js +20 -2
- package/dist/session/import.d.ts +44 -0
- package/dist/session/import.js +83 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/jobs.d.ts +12 -0
- package/dist/session/jobs.js +53 -0
- package/dist/session/message.d.ts +7 -1
- package/dist/session/message.js +21 -1
- package/dist/session/session.d.ts +128 -11
- package/dist/session/session.js +461 -30
- package/dist/session/snapshot.d.ts +5 -2
- package/dist/session/snapshot.js +10 -2
- package/dist/session/types.d.ts +11 -1
- package/dist/shared/types/agent.d.ts +11 -0
- package/dist/shared/types/config.d.ts +5 -1
- package/dist/shared/types/kanban.d.ts +13 -0
- package/dist/shared/types/message.d.ts +9 -0
- package/dist/tools/websearch/index.js +6 -3
- package/dist/update/index.d.ts +3 -3
- package/dist/update/index.js +2 -2
- package/dist/update/ipc.d.ts +13 -2
- package/dist/update/ipc.js +36 -2
- package/dist/update/snapshot.d.ts +1 -0
- package/dist/update/snapshot.js +2 -0
- package/dist/update/updater.d.ts +15 -1
- package/dist/update/updater.js +45 -17
- package/drizzle/0008_tranquil_lily_hollister.sql +1 -0
- package/drizzle/meta/0008_snapshot.json +875 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +41 -42
|
@@ -1,29 +1,64 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import {
|
|
2
|
+
import { getSession } from '../../session/session.js';
|
|
3
|
+
import { getCurrentVersion, manualInstallCommand, performHandoff, performInstall, serializeSessions, } from '../../update/index.js';
|
|
3
4
|
import { apiError } from '../middleware/error.js';
|
|
4
5
|
/**
|
|
5
6
|
* GET /api/update — 返回后台调度器缓存的版本检查结果(spec §18.1)。
|
|
6
7
|
* 路由本身不触发 npm registry 请求;调度器周期性 checkNow 写入缓存,
|
|
7
8
|
* 路由读缓存(避免每个前端轮询都打外网)。
|
|
8
9
|
*
|
|
9
|
-
* POST /api/update/apply — 手动触发热更新(spec §18.2
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* POST /api/update/apply — 手动触发热更新(spec §18.2),P0 修复后顺序:
|
|
11
|
+
* 1. checkNow 确认有更新;
|
|
12
|
+
* 2. performInstall(仅安装新版本)——失败时**尚未暂停任何会话**,
|
|
13
|
+
* 直接返回可操作错误(409 + 手动更新命令),用户工作零损失;
|
|
14
|
+
* 3. 安装成功 → pauseAll 暂停活跃 run 至安全点(原子操作完成);
|
|
15
|
+
* 4. serializeSessions → performHandoff(写快照 + spawn 新实例);
|
|
16
|
+
* 5. spawn 失败 → resumeAll 回滚暂停的会话,返回明确错误。
|
|
17
|
+
*
|
|
18
|
+
* 旧实例通过 handoff 端点优雅退出,新实例 restore 快照并接管端口。
|
|
19
|
+
* 响应立即返回(不阻塞 spawn),实际接管由 handoff IPC 异步完成。
|
|
20
|
+
* dev(vite)模式无 handoff server,返回 409——热更新仅在独立 serve 进程可用。
|
|
14
21
|
*/
|
|
15
22
|
function createUpdateRoute(ctx) {
|
|
16
23
|
const app = new Hono();
|
|
17
|
-
app.get('/', (c) => {
|
|
24
|
+
app.get('/', async (c) => {
|
|
25
|
+
// 热更新影响面(P2-11):apply 前由前端确认框逐项展示受影响对象。
|
|
26
|
+
// 顶层 run(主 agent)+ 终端数;子 agent 归父会话,不重复列出。
|
|
27
|
+
const active = ctx.agentManager.listActive();
|
|
28
|
+
const topRuns = active.filter((r) => !r.parentSessionId);
|
|
29
|
+
const runs = [];
|
|
30
|
+
for (const r of topRuns) {
|
|
31
|
+
let title = r.sessionId.slice(0, 8);
|
|
32
|
+
try {
|
|
33
|
+
const session = await getSession(ctx.db, r.sessionId);
|
|
34
|
+
if (session)
|
|
35
|
+
title = session.title;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// 会话查询失败:回退 id 前缀展示
|
|
39
|
+
}
|
|
40
|
+
runs.push({
|
|
41
|
+
sessionId: r.sessionId,
|
|
42
|
+
title,
|
|
43
|
+
...(r.agentType ? { agentType: r.agentType } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const impact = { runs, terminalCount: ctx.ptyManager.list().length };
|
|
18
47
|
// P2-8:update.enabled=false 时无 handoff server,apply 必然 409。
|
|
19
48
|
// 不再触发 checkNow,也不返回 hasUpdate,避免横幅出现一个点了必失败的应用按钮。
|
|
20
49
|
if (ctx.config.update.enabled === false) {
|
|
21
50
|
const v = getCurrentVersion();
|
|
22
|
-
return c.json({
|
|
51
|
+
return c.json({
|
|
52
|
+
hasUpdate: false,
|
|
53
|
+
disabled: true,
|
|
54
|
+
currentVersion: v,
|
|
55
|
+
latestVersion: v,
|
|
56
|
+
impact,
|
|
57
|
+
});
|
|
23
58
|
}
|
|
24
59
|
const cached = ctx.updateScheduler.getLastResult();
|
|
25
60
|
if (cached)
|
|
26
|
-
return c.json(cached);
|
|
61
|
+
return c.json({ ...cached, impact });
|
|
27
62
|
// 无缓存(首次启动延迟未到):同步触发一次,避免前端首屏空。
|
|
28
63
|
// 不 await——保持 GET 语义非阻塞;前端下次轮询拿到结果。
|
|
29
64
|
void ctx.updateScheduler.checkNow();
|
|
@@ -32,6 +67,7 @@ function createUpdateRoute(ctx) {
|
|
|
32
67
|
hasUpdate: false,
|
|
33
68
|
currentVersion: v,
|
|
34
69
|
latestVersion: v,
|
|
70
|
+
impact,
|
|
35
71
|
});
|
|
36
72
|
});
|
|
37
73
|
app.post('/apply', async (c) => {
|
|
@@ -42,36 +78,60 @@ function createUpdateRoute(ctx) {
|
|
|
42
78
|
if (!result.hasUpdate) {
|
|
43
79
|
return apiError(c, 409, 'NO_UPDATE', '已是最新版本,无需热更新');
|
|
44
80
|
}
|
|
45
|
-
//
|
|
46
|
-
//
|
|
81
|
+
// P0 修复:install 先行。失败(网络/权限/unknown 安装方式)时未触碰任何
|
|
82
|
+
// 会话状态,直接返回可操作错误——旧流程先 pause 后 install,失败时
|
|
83
|
+
// 已暂停/强杀的会话无回滚路径。
|
|
84
|
+
const install = await performInstall({});
|
|
85
|
+
if (install._tag === 'install_failed') {
|
|
86
|
+
return apiError(c, 409, 'INSTALL_FAILED', install.error, {
|
|
87
|
+
command: manualInstallCommand('c0de-agent'),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (install._tag === 'manual_install_required') {
|
|
91
|
+
return apiError(c, 409, 'MANUAL_UPDATE_REQUIRED', install.error, {
|
|
92
|
+
command: install.command,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const method = { kind: install.installMethod };
|
|
96
|
+
// 安装已成功 → 暂停所有活跃 run,等其到达安全暂停点(原子操作完成)。
|
|
97
|
+
// 注意:暂停不是「可恢复快照」——新实例只有 DB 状态、无内存 agent run,
|
|
98
|
+
// 进行中的对话会以 interrupted 呈现,用户重发上一条消息即可继续(幂等
|
|
99
|
+
// 检查跳过重复 append)。工具副作用不回滚。
|
|
47
100
|
const pauseTimeoutMs = ctx.config.update.pauseTimeoutMs ?? 30_000;
|
|
48
101
|
const pauseResult = await ctx.agentManager.pauseAll(pauseTimeoutMs);
|
|
49
102
|
const snapshot = await serializeSessions(ctx.db, ctx.config);
|
|
50
|
-
const r = await
|
|
103
|
+
const r = await performHandoff(snapshot, method, {
|
|
51
104
|
handoffPort: ctx.handoff.port,
|
|
52
105
|
port: ctx.port,
|
|
53
106
|
// P2-16:传当前 bootstrap(轮换后 ctx.authToken 可能已过期);
|
|
54
107
|
// 旧实例 handoff 端点经 verifyHandoff 接受当前/历史 bootstrap 与设备 token。
|
|
55
108
|
authToken: ctx.authManager?.bootstrap ?? ctx.authToken,
|
|
56
109
|
});
|
|
57
|
-
if (r._tag
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
110
|
+
if (r._tag !== 'success') {
|
|
111
|
+
// P0 回滚:spawn 失败(含程序文件未就绪等)→ 恢复被暂停的会话。
|
|
112
|
+
// forcedAbort 的无法恢复(已中止),但这是超时兜底,属可接受损失。
|
|
113
|
+
for (const id of pauseResult.pausedIds) {
|
|
114
|
+
ctx.agentManager.resume(id);
|
|
115
|
+
}
|
|
116
|
+
if (r._tag === 'manual_install_required') {
|
|
117
|
+
return apiError(c, 409, 'MANUAL_UPDATE_REQUIRED', r.error, {
|
|
118
|
+
command: r.command,
|
|
119
|
+
snapshotPath: r.snapshotPath,
|
|
120
|
+
resumedSessions: pauseResult.pausedIds.length,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return apiError(c, 500, 'HOT_UPDATE_FAILED', `${r._tag}: ${r.error}`, {
|
|
124
|
+
resumedSessions: pauseResult.pausedIds.length,
|
|
65
125
|
});
|
|
66
126
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
127
|
+
return c.json({
|
|
128
|
+
ok: true,
|
|
129
|
+
snapshotPath: r.snapshotPath,
|
|
130
|
+
latestVersion: result.latestVersion,
|
|
131
|
+
installMethod: r.installMethod,
|
|
132
|
+
pausedSessions: pauseResult.paused,
|
|
133
|
+
forcedAbort: pauseResult.forcedAbort,
|
|
134
|
+
});
|
|
75
135
|
});
|
|
76
136
|
return app;
|
|
77
137
|
}
|
package/dist/server/server.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import type { DB } from '../db/client.js';
|
|
3
|
-
import type { Registry } from '../llm/registry.js';
|
|
4
3
|
import type { Config } from '../shared/types/config.js';
|
|
5
4
|
import { checkForUpdate, createHandoffServer } from '../update/index.js';
|
|
5
|
+
import { buildRegistryFromConfig, syncRegistryFromConfig } from './registry-config.js';
|
|
6
6
|
import type { ServerContext } from './types.js';
|
|
7
7
|
type StartServerOptions = {
|
|
8
8
|
port?: number;
|
|
@@ -31,15 +31,6 @@ type BootstrappedServer = {
|
|
|
31
31
|
ctx: ServerContext;
|
|
32
32
|
close(): Promise<void>;
|
|
33
33
|
};
|
|
34
|
-
/** 把 config.providers 注册到新建的 LLM registry(修复此前空 registry 的遗漏)。 */
|
|
35
|
-
declare function buildRegistryFromConfig(config: Config): Registry;
|
|
36
|
-
/**
|
|
37
|
-
* config 变更后原子地同步 registry:在隔离的 next registry 上重建全部路由,
|
|
38
|
-
* 完成后一次性替换 registry 内部 table 引用。运行中的 resolveRoute 任何时刻
|
|
39
|
-
* 看到的都是完整的旧表或完整的新表,不会读到「已清空但未注册完」的半状态,
|
|
40
|
-
* 因此不会把本可用的 provider 误判为 NoRoute。ServerContext 立即生效,无需重启。
|
|
41
|
-
*/
|
|
42
|
-
declare function syncRegistryFromConfig(registry: Registry, config: Config): void;
|
|
43
34
|
/** 解析 PGLite 持久化数据目录:优先 C0DE_DB_DIR,否则全局数据根下 pglite 子目录。
|
|
44
35
|
*
|
|
45
36
|
* 历史上数据库放在 <cwd>/.c0de/pglite(项目级),但项目 DB schema 通过 projectId
|
|
@@ -50,8 +41,9 @@ declare function resolveDbDir(): string;
|
|
|
50
41
|
* 1. security.authEnabled 显式为 false → 无认证(用户显式选择)。
|
|
51
42
|
* 2. 用户配置 security.token → 使用。
|
|
52
43
|
* 3. 环境变量 C0DE_AUTH_TOKEN(热更新新实例经 updater 传入)→ 采用并落盘。
|
|
53
|
-
* 4.
|
|
54
|
-
* 5.
|
|
44
|
+
* 4. 已有已注册设备 → 复用 auth-token 文件(跨重启稳定;设备 token 存 devices.json)。
|
|
45
|
+
* 5. 无已注册设备(首设备待注册态)→ 每次 serve 都重新生成 bootstrap,刷新
|
|
46
|
+
* firstDeviceTtlMs 窗口。此前「复用旧文件」会让过期 bootstrap 无法通过重启恢复。
|
|
55
47
|
*/
|
|
56
48
|
declare function resolveAuthToken(config: Config, dataDir: string): string | undefined;
|
|
57
49
|
/**
|
package/dist/server/server.js
CHANGED
|
@@ -8,58 +8,20 @@ import { serve } from '@hono/node-server';
|
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
9
|
import { BUILTIN_AGENTS, createAgentRegistry } from '../core/agents/index.js';
|
|
10
10
|
import { loadConfig } from '../core/config.js';
|
|
11
|
-
import { decryptSecret } from '../core/secret.js';
|
|
12
11
|
import { createAndPopulateRegistry } from '../core/workflows/index.js';
|
|
13
12
|
import { createDB, migrateDB } from '../db/index.js';
|
|
14
|
-
import { createRegistry, overrideToCapabilities, rebuildRegistry, registerProvider, } from '../llm/registry.js';
|
|
15
13
|
import { initPlugins } from '../plugins/index.js';
|
|
16
|
-
import {
|
|
14
|
+
import { markDeadBackgroundJobs } from '../session/jobs.js';
|
|
15
|
+
import { purgeDeletedSessions, purgeEmptySessions, purgeTemporarySessions, TRASH_PURGE_GRACE_MS, TRASH_RETENTION_MS, } from '../session/session.js';
|
|
17
16
|
import { createDefaultRegistry, createDefaultURLRegistry } from '../tools/index.js';
|
|
18
|
-
import { checkForUpdate, createHandoffServer, createUpdateScheduler, requestHandoff, restoreSessions, } from '../update/index.js';
|
|
17
|
+
import { checkForUpdate, confirmHandoff, createHandoffServer, createUpdateScheduler, requestHandoff, restoreSessions, } from '../update/index.js';
|
|
19
18
|
import { createAgentManager } from './agent-manager.js';
|
|
20
19
|
import { createApp } from './app.js';
|
|
21
20
|
import { createAuthManager } from './auth-manager.js';
|
|
22
21
|
import { isAllowedOrigin } from './middleware/cors.js';
|
|
23
22
|
import { createPermissionStore } from './permission/store.js';
|
|
23
|
+
import { buildRegistryFromConfig, syncRegistryFromConfig } from './registry-config.js';
|
|
24
24
|
import { PTYManager } from './terminal/pty-manager.js';
|
|
25
|
-
/** 把 config.providers 注册到新建的 LLM registry(修复此前空 registry 的遗漏)。 */
|
|
26
|
-
function buildRegistryFromConfig(config) {
|
|
27
|
-
const registry = createRegistry();
|
|
28
|
-
for (const p of config.providers) {
|
|
29
|
-
registerProviderFromConfig(registry, p);
|
|
30
|
-
}
|
|
31
|
-
return registry;
|
|
32
|
-
}
|
|
33
|
-
function registerProviderFromConfig(registry, p) {
|
|
34
|
-
// 兼容 config.json 中以 _tag 标识 provider 的格式(name 缺失时回退到 _tag)
|
|
35
|
-
const name = p.name || p._tag;
|
|
36
|
-
if (!name || !p.baseURL)
|
|
37
|
-
return;
|
|
38
|
-
// baseURL 已含 /v1 时用 /chat/completions,避免 /v1/v1 双重前缀
|
|
39
|
-
const path = p.baseURL.replace(/\/+$/, '').endsWith('/v1') ? '/chat/completions' : undefined;
|
|
40
|
-
registerProvider(registry, {
|
|
41
|
-
name,
|
|
42
|
-
baseURL: p.baseURL,
|
|
43
|
-
apiKey: p.apiKey ? decryptSecret(p.apiKey) : p.apiKey,
|
|
44
|
-
...(path ? { path } : {}),
|
|
45
|
-
// 传递用户配置的 per-model capabilities(contextWindow 等),
|
|
46
|
-
// 否则 resolveRoute 回退到 DEFAULT_MODEL_CAPABILITIES,可能导致预算过小。
|
|
47
|
-
...(p.models ? { models: overrideToCapabilities(p.models) } : {}),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* config 变更后原子地同步 registry:在隔离的 next registry 上重建全部路由,
|
|
52
|
-
* 完成后一次性替换 registry 内部 table 引用。运行中的 resolveRoute 任何时刻
|
|
53
|
-
* 看到的都是完整的旧表或完整的新表,不会读到「已清空但未注册完」的半状态,
|
|
54
|
-
* 因此不会把本可用的 provider 误判为 NoRoute。ServerContext 立即生效,无需重启。
|
|
55
|
-
*/
|
|
56
|
-
function syncRegistryFromConfig(registry, config) {
|
|
57
|
-
rebuildRegistry(registry, (next) => {
|
|
58
|
-
for (const p of config.providers) {
|
|
59
|
-
registerProviderFromConfig(next, p);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
25
|
/** 全局数据根目录:XDG_DATA_HOME 优先,否则 ~/.local/share/c0de。
|
|
64
26
|
* 与 opencode (~/.local/share/opencode/)、oh-my-pi (~/.omp/agent/) 同约定——
|
|
65
27
|
* 数据库等运行时数据放全局,项目 .c0de/ 只留配置和扩展。 */
|
|
@@ -84,8 +46,9 @@ function resolveDbDir() {
|
|
|
84
46
|
* 1. security.authEnabled 显式为 false → 无认证(用户显式选择)。
|
|
85
47
|
* 2. 用户配置 security.token → 使用。
|
|
86
48
|
* 3. 环境变量 C0DE_AUTH_TOKEN(热更新新实例经 updater 传入)→ 采用并落盘。
|
|
87
|
-
* 4.
|
|
88
|
-
* 5.
|
|
49
|
+
* 4. 已有已注册设备 → 复用 auth-token 文件(跨重启稳定;设备 token 存 devices.json)。
|
|
50
|
+
* 5. 无已注册设备(首设备待注册态)→ 每次 serve 都重新生成 bootstrap,刷新
|
|
51
|
+
* firstDeviceTtlMs 窗口。此前「复用旧文件」会让过期 bootstrap 无法通过重启恢复。
|
|
89
52
|
*/
|
|
90
53
|
function resolveAuthToken(config, dataDir) {
|
|
91
54
|
if (config.security.authEnabled === false)
|
|
@@ -104,13 +67,16 @@ function resolveAuthToken(config, dataDir) {
|
|
|
104
67
|
}
|
|
105
68
|
return envToken;
|
|
106
69
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
70
|
+
// 已有设备 → 复用 auth-token 文件,保持跨重启稳定(已授权设备经 devices.json 认 token)。
|
|
71
|
+
if (hasRegisteredDevices(dataDir)) {
|
|
72
|
+
try {
|
|
73
|
+
const existing = readFileSync(tokenFile, 'utf-8').trim();
|
|
74
|
+
if (existing)
|
|
75
|
+
return existing;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// 文件不存在,走生成分支
|
|
79
|
+
}
|
|
114
80
|
}
|
|
115
81
|
const generated = randomBytes(24).toString('hex');
|
|
116
82
|
try {
|
|
@@ -122,6 +88,19 @@ function resolveAuthToken(config, dataDir) {
|
|
|
122
88
|
}
|
|
123
89
|
return generated;
|
|
124
90
|
}
|
|
91
|
+
/** dataDir 下是否存在已授权设备(devices.json 非空)。 */
|
|
92
|
+
function hasRegisteredDevices(dataDir) {
|
|
93
|
+
try {
|
|
94
|
+
const path = join(dataDir, 'devices.json');
|
|
95
|
+
if (!existsSync(path))
|
|
96
|
+
return false;
|
|
97
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
98
|
+
return Array.isArray(parsed.devices) && parsed.devices.length > 0;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
125
104
|
/** 只读 dataDir 下已存在的 auth-token(新实例 handoff 握手用;不生成)。 */
|
|
126
105
|
function readAuthTokenFile(dataDir) {
|
|
127
106
|
try {
|
|
@@ -196,6 +175,9 @@ async function buildServerContext(db, opts = {}) {
|
|
|
196
175
|
...(config.security.token && config.security.token.length > 0
|
|
197
176
|
? { staticToken: config.security.token }
|
|
198
177
|
: {}),
|
|
178
|
+
...(config.security.firstDeviceTtlMs !== undefined
|
|
179
|
+
? { firstDeviceTtlMs: config.security.firstDeviceTtlMs }
|
|
180
|
+
: {}),
|
|
199
181
|
});
|
|
200
182
|
const ctx = {
|
|
201
183
|
db,
|
|
@@ -209,6 +191,7 @@ async function buildServerContext(db, opts = {}) {
|
|
|
209
191
|
permissionStore: createPermissionStore(),
|
|
210
192
|
permissionMode: config.permission.defaultMode,
|
|
211
193
|
sessionPermissionModes: new Map(),
|
|
194
|
+
sessionAlwaysAllow: new Map(),
|
|
212
195
|
authToken: resolvedToken,
|
|
213
196
|
authManager: config.security.authEnabled === false ? undefined : authManager,
|
|
214
197
|
// Agent 注册表:内置 4 个默认 agent;项目/用户自定义 agent 可在启动后补充加载。
|
|
@@ -346,13 +329,37 @@ async function bootstrapServerContext(opts = {}) {
|
|
|
346
329
|
db = await createDevDb(cwd);
|
|
347
330
|
}
|
|
348
331
|
const { ctx, dispose } = await buildServerContext(db, opts);
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
332
|
+
// P2-4 + A3:回收站两阶段清理——启动时清一次 + 每 24h 清一次。
|
|
333
|
+
// 阶段一标记到期条目进入宽限期(UI 可见可恢复),阶段二物理清除宽限期满的条目。
|
|
334
|
+
// fire-and-forget:清理失败不阻塞服务启动;结果落日志可查。
|
|
335
|
+
const runTrashPurge = () => {
|
|
336
|
+
void purgeDeletedSessions(db)
|
|
337
|
+
.then(({ marked, deleted }) => {
|
|
338
|
+
if (marked > 0) {
|
|
339
|
+
console.log(`[server] 回收站清理:${marked} 个会话已到期,进入 ${TRASH_PURGE_GRACE_MS / (24 * 60 * 60 * 1000)} 天宽限期(期间可恢复)`);
|
|
340
|
+
}
|
|
341
|
+
if (deleted > 0) {
|
|
342
|
+
console.log(`[server] 回收站清理:已物理清除 ${deleted} 个超过宽限期的会话(保留期 ${TRASH_RETENTION_MS / (24 * 60 * 60 * 1000)} 天)`);
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
.catch(() => { });
|
|
346
|
+
};
|
|
347
|
+
runTrashPurge();
|
|
352
348
|
const purgeTimer = setInterval(() => {
|
|
353
|
-
|
|
349
|
+
runTrashPurge();
|
|
350
|
+
// P2:CLI print / 工作流临时会话同样每日清理(30 天保留)
|
|
351
|
+
void purgeTemporarySessions(db).catch(() => { });
|
|
352
|
+
// C4:长期空置的 web 空会话每日清理(7 天保留)
|
|
353
|
+
void purgeEmptySessions(db).catch(() => { });
|
|
354
354
|
}, 24 * 60 * 60 * 1000);
|
|
355
355
|
purgeTimer.unref();
|
|
356
|
+
// P2:启动时同步清一次临时会话(每日定时之外的兜底)
|
|
357
|
+
void purgeTemporarySessions(db).catch(() => { });
|
|
358
|
+
// C4:启动时同步清一次空会话(兜底)
|
|
359
|
+
void purgeEmptySessions(db).catch(() => { });
|
|
360
|
+
// P2:标记崩溃遗留的后台子 agent 任务(lastRun=running 的 subagent 会话)
|
|
361
|
+
// 并给父会话注入失败通知。fire-and-forget:失败不阻塞启动。
|
|
362
|
+
void markDeadBackgroundJobs(db).catch(() => { });
|
|
356
363
|
// P2-15:PGLite WAL 周期刷盘(30s),缩短 kill -9 等突然停机时的已提交数据丢失窗口。
|
|
357
364
|
const syncTimer = db.sync
|
|
358
365
|
? setInterval(() => {
|
|
@@ -367,8 +374,15 @@ async function bootstrapServerContext(opts = {}) {
|
|
|
367
374
|
if (syncTimer)
|
|
368
375
|
clearInterval(syncTimer);
|
|
369
376
|
await dispose();
|
|
370
|
-
if (ownsDb)
|
|
377
|
+
if (ownsDb) {
|
|
371
378
|
await db.close();
|
|
379
|
+
// P0 修复:优雅关闭(含热更新 handoff)时显式释放 .dev.lock。
|
|
380
|
+
// 旧流程依赖新实例的 stale-PID 检测清理锁文件,而旧进程在 handoff
|
|
381
|
+
// 响应后延迟 250ms 才退出——新实例若在该窗口内做锁检查(PID 仍存活)
|
|
382
|
+
// 会抛「Database is locked」崩溃,且旧实例已退出,双输。显式释放后
|
|
383
|
+
// 锁文件在进程存活期就已移除,新实例直接重建锁,无竞态窗口。
|
|
384
|
+
releaseDevDbLock(resolveDbDir());
|
|
385
|
+
}
|
|
372
386
|
},
|
|
373
387
|
};
|
|
374
388
|
}
|
|
@@ -410,24 +424,15 @@ async function requestHandoffWithRetry(port, token, maxAttempts = 30, delayMs =
|
|
|
410
424
|
}
|
|
411
425
|
throw new Error(`handoff: old instance on port ${port} did not release after ${maxAttempts} attempts`);
|
|
412
426
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const app = createApp(ctx);
|
|
423
|
-
// config.update.enabled 时启动后台调度器。
|
|
424
|
-
if (ctx.config.update.enabled)
|
|
425
|
-
ctx.updateScheduler.start();
|
|
426
|
-
const server = serve({ fetch: app.fetch, port });
|
|
427
|
-
ctx.port = port;
|
|
428
|
-
// WebSocket:终端双向流。Hono v2 无原生 WS,用 ws 包直接挂载到 HTTP server。
|
|
429
|
-
// 匹配 /api/terminal/:id/ws → ptyManager.attachWebSocket
|
|
430
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
427
|
+
/**
|
|
428
|
+
* 两阶段 handoff 的确认窗口(P3 回滚修复):旧实例响应 POST /handoff 后不退出,
|
|
429
|
+
* 等待新实例绑定端口成功后 POST /handoff-confirm 再退。窗口内未收到确认
|
|
430
|
+
* (新实例启动失败)→ 旧实例原地复活重新接管端口,用户工作零损失。
|
|
431
|
+
* 窗口需覆盖新实例 bootstrap(DB 迁移/快照恢复)耗时,取 60s 留裕量。
|
|
432
|
+
*/
|
|
433
|
+
const HANDOFF_CONFIRM_WINDOW_MS = 60_000;
|
|
434
|
+
/** 组装终端 WebSocket upgrade 处理器(bringUp 与复活共用)。 */
|
|
435
|
+
function attachTerminalUpgrade(ctx, wss, server) {
|
|
431
436
|
server.on('upgrade', (req, socket, head) => {
|
|
432
437
|
const url = new URL(req.url ?? '', `http://${req.headers.host ?? 'localhost'}`);
|
|
433
438
|
const match = url.pathname.match(/^\/api\/terminal\/([^/]+)\/ws$/);
|
|
@@ -469,35 +474,126 @@ async function startServer(opts = {}) {
|
|
|
469
474
|
}
|
|
470
475
|
});
|
|
471
476
|
});
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
await
|
|
477
|
+
}
|
|
478
|
+
async function startServer(opts = {}) {
|
|
479
|
+
const port = opts.port ?? 3000;
|
|
480
|
+
// spec §18.3:新实例从 --handoff-port 拿到旧实例端口,请求优雅退出后绑端口。
|
|
481
|
+
// 握手 token:updater spawn 时经 C0DE_AUTH_TOKEN 环境变量传入,否则读 auth-token 文件。
|
|
482
|
+
if (opts.handoffPort && !opts.skipHandoff) {
|
|
483
|
+
const pendingToken = process.env.C0DE_AUTH_TOKEN ?? readAuthTokenFile(resolveDbDir());
|
|
484
|
+
await requestHandoffWithRetry(opts.handoffPort, pendingToken);
|
|
485
|
+
}
|
|
486
|
+
// 两阶段 handoff 状态(旧实例视角):确认窗口定时器与复活互斥锁。
|
|
487
|
+
let confirmTimer = null;
|
|
488
|
+
let reviving = false;
|
|
489
|
+
let handoffServer;
|
|
490
|
+
let currentClose = null;
|
|
491
|
+
// RunningServer 暴露的稳定句柄(复活后更新;app/authToken 每次 bringUp 重建)。
|
|
492
|
+
let currentApp = null;
|
|
493
|
+
let currentAuthToken;
|
|
494
|
+
const clearConfirmTimer = () => {
|
|
495
|
+
if (confirmTimer) {
|
|
496
|
+
clearTimeout(confirmTimer);
|
|
497
|
+
confirmTimer = null;
|
|
498
|
+
}
|
|
480
499
|
};
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
500
|
+
/**
|
|
501
|
+
* 组装完整服务栈(bootstrap → app → 调度器 → 绑定端口 → WS → handoff server)。
|
|
502
|
+
* 首次启动与超时复活共用。复活时新实例可能已绑定端口:serve() 抛 EADDRINUSE
|
|
503
|
+
* 说明新实例实际上已接管(确认请求丢失),旧实例退出即可。
|
|
504
|
+
*/
|
|
505
|
+
const bringUp = async () => {
|
|
506
|
+
const { ctx, close: closeCtx } = await bootstrapServerContext(opts);
|
|
507
|
+
const app = createApp(ctx);
|
|
508
|
+
// config.update.enabled 时启动后台调度器。
|
|
509
|
+
if (ctx.config.update.enabled)
|
|
510
|
+
ctx.updateScheduler.start();
|
|
511
|
+
const server = serve({ fetch: app.fetch, port });
|
|
512
|
+
ctx.port = port;
|
|
513
|
+
// WebSocket:终端双向流。Hono v2 无原生 WS,用 ws 包直接挂载到 HTTP server。
|
|
514
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
515
|
+
attachTerminalUpgrade(ctx, wss, server);
|
|
516
|
+
let closed = false;
|
|
517
|
+
const closeMain = async () => {
|
|
518
|
+
if (closed)
|
|
519
|
+
return;
|
|
520
|
+
closed = true;
|
|
521
|
+
wss.close();
|
|
522
|
+
server.close();
|
|
523
|
+
await closeCtx();
|
|
524
|
+
};
|
|
525
|
+
const close = async () => {
|
|
526
|
+
clearConfirmTimer();
|
|
527
|
+
await closeMain();
|
|
528
|
+
await handoffServer?.close();
|
|
529
|
+
};
|
|
530
|
+
currentClose = close;
|
|
531
|
+
currentApp = app;
|
|
532
|
+
currentAuthToken = ctx.authToken;
|
|
533
|
+
// spec §18.3 handoff server:旧实例收到 POST /handoff 后关闭主服务与资源,
|
|
534
|
+
// 响应 200。两阶段交接:不再立即退出——等待新实例 POST /handoff-confirm
|
|
535
|
+
// (onConfirm)确认接管;窗口超时未确认则 revive 原地复活。
|
|
536
|
+
// config.update.enabled=false 或 skipHandoff 时跳过。
|
|
537
|
+
if (ctx.config.update.enabled && !opts.skipHandoff) {
|
|
538
|
+
const createHandoff = opts.createHandoffFn ?? createHandoffServer;
|
|
539
|
+
handoffServer = await createHandoff(closeMain, {
|
|
540
|
+
// P2-16:token 轮换后新旧实例 bootstrap 可能不同,用 verifyHandoff 接受
|
|
541
|
+
// 当前/历史 bootstrap 与设备 token;无 authManager 回退字符串比较。
|
|
542
|
+
...(ctx.authManager
|
|
543
|
+
? { verify: (t) => ctx.authManager?.verifyHandoff(t) ?? false }
|
|
544
|
+
: { expectedToken: ctx.authToken }),
|
|
545
|
+
exitAfterResponse: false,
|
|
546
|
+
onConfirm: () => {
|
|
547
|
+
// 新实例确认已接管端口 → 旧实例退出(延迟保证确认响应刷出)。
|
|
548
|
+
clearConfirmTimer();
|
|
549
|
+
setTimeout(() => process.exit(0), 250);
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
ctx.handoff = { port: handoffServer.port, server: handoffServer };
|
|
553
|
+
// 确认窗口:新实例启动失败时旧实例复活(保留进程句柄,定时器不可 unref)。
|
|
554
|
+
confirmTimer = setTimeout(() => {
|
|
555
|
+
void (async () => {
|
|
556
|
+
if (reviving)
|
|
557
|
+
return;
|
|
558
|
+
reviving = true;
|
|
559
|
+
try {
|
|
560
|
+
await bringUp();
|
|
561
|
+
console.warn('[handoff] 新实例未确认接管,旧实例已复活并重新接管端口');
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
if (err.code === 'EADDRINUSE') {
|
|
565
|
+
// 新实例已绑定端口(确认请求丢失)→ 它已接管,旧实例正常退出。
|
|
566
|
+
console.warn('[handoff] 端口已被新实例接管,旧实例退出');
|
|
567
|
+
process.exit(0);
|
|
568
|
+
}
|
|
569
|
+
console.error('[handoff] 旧实例复活失败,退出:', err);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
reviving = false;
|
|
574
|
+
}
|
|
575
|
+
})();
|
|
576
|
+
}, HANDOFF_CONFIRM_WINDOW_MS);
|
|
577
|
+
}
|
|
578
|
+
// 两阶段交接第二阶段:新实例绑定成功后确认旧实例可退出。
|
|
579
|
+
// 旧实例(无 handoffPort)跳过;确认失败静默(窗口超时兜底)。
|
|
580
|
+
if (opts.handoffPort) {
|
|
581
|
+
const confirmToken = process.env.C0DE_AUTH_TOKEN ?? readAuthTokenFile(resolveDbDir());
|
|
582
|
+
void confirmHandoff(opts.handoffPort, '127.0.0.1', confirmToken);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
await bringUp();
|
|
586
|
+
if (!currentApp)
|
|
587
|
+
throw new Error('server failed to start: app not built');
|
|
588
|
+
return {
|
|
589
|
+
app: currentApp,
|
|
590
|
+
port,
|
|
591
|
+
close: async () => {
|
|
592
|
+
await currentClose?.();
|
|
593
|
+
},
|
|
594
|
+
...(currentAuthToken ? { authToken: currentAuthToken } : {}),
|
|
484
595
|
};
|
|
485
|
-
// spec §18.3 handoff server:旧实例收到 POST /handoff 后关闭主服务与资源,
|
|
486
|
-
// 响应 200,随后退出进程(exitAfterResponse),端口让渡给新实例。
|
|
487
|
-
// config.update.enabled=false 或 skipHandoff 时跳过。
|
|
488
|
-
let handoffServer;
|
|
489
|
-
if (ctx.config.update.enabled && !opts.skipHandoff) {
|
|
490
|
-
const createHandoff = opts.createHandoffFn ?? createHandoffServer;
|
|
491
|
-
handoffServer = await createHandoff(closeMain, {
|
|
492
|
-
// P2-16:token 轮换后新旧实例 bootstrap 可能不同,用 verifyHandoff 接受
|
|
493
|
-
// 当前/历史 bootstrap 与设备 token;无 authManager 回退字符串比较。
|
|
494
|
-
...(ctx.authManager
|
|
495
|
-
? { verify: (t) => ctx.authManager?.verifyHandoff(t) ?? false }
|
|
496
|
-
: { expectedToken: ctx.authToken }),
|
|
497
|
-
exitAfterResponse: true,
|
|
498
|
-
});
|
|
499
|
-
ctx.handoff = { port: handoffServer.port, server: handoffServer };
|
|
500
|
-
}
|
|
501
|
-
return { app, port, close, ...(ctx.authToken ? { authToken: ctx.authToken } : {}) };
|
|
502
596
|
}
|
|
597
|
+
// P1-1:buildRegistryFromConfig/syncRegistryFromConfig 实现移入 registry-config.ts,
|
|
598
|
+
// 此处 re-export 保留 server.ts 历史导出兼容(测试/CLI 引用)。
|
|
503
599
|
export { acquireDevDbLock, bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, releaseDevDbLock, resolveAuthToken, resolveDbDir, startServer, syncRegistryFromConfig, };
|
package/dist/server/types.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ type ServerContext = {
|
|
|
32
32
|
/** 会话级授权模式覆盖:Map<sessionId, mode>。会话未覆盖时回退 permissionMode。
|
|
33
33
|
* P1-5:auto 是高风险状态,按会话隔离避免一个标签页切换影响全部会话。 */
|
|
34
34
|
sessionPermissionModes: Map<string, 'default' | 'auto'>;
|
|
35
|
+
/** 会话级「始终允许」工具白名单:Map<sessionId, string[]>。持久化于
|
|
36
|
+
* session.metadata.alwaysAllow;checker 经 getter 实时读取(run 中可追加)。 */
|
|
37
|
+
sessionAlwaysAllow: Map<string, string[]>;
|
|
35
38
|
/**
|
|
36
39
|
* API Bearer token(认证未显式关闭时必存在)。
|
|
37
40
|
* 用户配置 security.token 优先;否则自动生成并持久化到数据目录 auth-token 文件
|
|
@@ -43,6 +46,10 @@ type ServerContext = {
|
|
|
43
46
|
authManager?: AuthManager;
|
|
44
47
|
/** 主 HTTP 服务实际绑定端口(startServer 绑定后回填;热更新 spawn 新实例复用)。 */
|
|
45
48
|
port?: number;
|
|
49
|
+
/** P1-1:项目级 LLM 注册表缓存 Map<projectId, Registry>——按项目合并配置
|
|
50
|
+
* (global + 项目 .c0de/config.json 的 providers)构建,会话聊天按所属项目
|
|
51
|
+
* 解析路由。项目配置 PATCH 后失效重建。 */
|
|
52
|
+
projectRegistries?: Map<string, Registry>;
|
|
46
53
|
/** Agent 类型注册表(spec: multi-agent-design)。注入 agent loop 的 runSubAgent。 */
|
|
47
54
|
agentRegistry: AgentRegistry;
|
|
48
55
|
/** 工作流注册表(spec: dynamic-workflow-design)。注入 /workflow slash 命令和 workflowz steering。 */
|
|
@@ -8,8 +8,10 @@ declare function getArchive(handle: DB, id: string): Promise<CompactionArchive |
|
|
|
8
8
|
declare function getArchiveOriginalEntries(handle: DB, archiveId: string): Promise<SessionEntry[]>;
|
|
9
9
|
/** Search archives by keyword (case-insensitive substring on searchable text). */
|
|
10
10
|
declare function searchArchives(handle: DB, sessionId: string, query: string): Promise<CompactionArchive[]>;
|
|
11
|
+
/** List archives for a session(按创建时间倒序)。可选 query 走搜索过滤。 */
|
|
12
|
+
declare function listArchives(handle: DB, sessionId: string, query?: string): Promise<CompactionArchive[]>;
|
|
11
13
|
/** Parse a `@[archive:<id>]` or `@[squash:<n>]` reference from text. Returns null if none. */
|
|
12
14
|
declare function parseArchiveReference(text: string): ArchiveRef | null;
|
|
13
15
|
/** Resolve an archive reference to displayable text. */
|
|
14
16
|
declare function resolveArchiveReference(handle: DB, sessionId: string, ref: ArchiveRef): Promise<string | null>;
|
|
15
|
-
export { archiveOriginalEntries, getArchive, getArchiveOriginalEntries, parseArchiveReference, resolveArchiveReference, searchArchives, };
|
|
17
|
+
export { archiveOriginalEntries, getArchive, getArchiveOriginalEntries, listArchives, parseArchiveReference, resolveArchiveReference, searchArchives, };
|
package/dist/session/archive.js
CHANGED
|
@@ -98,6 +98,18 @@ async function searchArchives(handle, sessionId, query) {
|
|
|
98
98
|
.orderBy(desc(compactionArchives.createdAt));
|
|
99
99
|
return rows.map(rowToArchive);
|
|
100
100
|
}
|
|
101
|
+
/** List archives for a session(按创建时间倒序)。可选 query 走搜索过滤。 */
|
|
102
|
+
async function listArchives(handle, sessionId, query) {
|
|
103
|
+
if (query && query.trim().length > 0) {
|
|
104
|
+
return searchArchives(handle, sessionId, query.trim());
|
|
105
|
+
}
|
|
106
|
+
const rows = await handle.db
|
|
107
|
+
.select()
|
|
108
|
+
.from(compactionArchives)
|
|
109
|
+
.where(eq(compactionArchives.sessionId, sessionId))
|
|
110
|
+
.orderBy(desc(compactionArchives.createdAt));
|
|
111
|
+
return rows.map(rowToArchive);
|
|
112
|
+
}
|
|
101
113
|
/** Parse a `@[archive:<id>]` or `@[squash:<n>]` reference from text. Returns null if none. */
|
|
102
114
|
function parseArchiveReference(text) {
|
|
103
115
|
const archiveId = text.match(/@\[archive:([^\]]+)\]/)?.[1];
|
|
@@ -126,4 +138,4 @@ async function resolveArchiveReference(handle, sessionId, ref) {
|
|
|
126
138
|
return null;
|
|
127
139
|
return `[Referenced Squash #${index + 1}]\n${row.summary}`;
|
|
128
140
|
}
|
|
129
|
-
export { archiveOriginalEntries, getArchive, getArchiveOriginalEntries, parseArchiveReference, resolveArchiveReference, searchArchives, };
|
|
141
|
+
export { archiveOriginalEntries, getArchive, getArchiveOriginalEntries, listArchives, parseArchiveReference, resolveArchiveReference, searchArchives, };
|