c0de-agent 1.12.0 → 1.13.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 +51 -23
- package/dist/cli/commands/acp.js +26 -8
- package/dist/cli/commands/chat.js +70 -6
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.js +46 -0
- package/dist/cli/commands/serve.js +11 -2
- package/dist/cli/commands/sessions.js +51 -4
- package/dist/cli/commands/trust.d.ts +11 -0
- package/dist/cli/commands/trust.js +31 -0
- package/dist/cli/commands/update.js +4 -0
- package/dist/cli/deps.d.ts +9 -6
- package/dist/cli/deps.js +66 -19
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +92 -12
- package/dist/cli/modes/acp.js +39 -8
- package/dist/cli/modes/print.d.ts +3 -0
- package/dist/cli/modes/print.js +71 -8
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/agent.js +9 -4
- package/dist/core/agents/builtin.js +2 -2
- package/dist/core/agents/index.d.ts +0 -1
- package/dist/core/agents/index.js +0 -1
- package/dist/core/config.d.ts +43 -1
- package/dist/core/config.js +163 -14
- package/dist/core/loop/segment.d.ts +3 -1
- package/dist/core/loop/segment.js +24 -1
- package/dist/core/loop/subagent.js +65 -32
- package/dist/core/loop.d.ts +7 -0
- package/dist/core/loop.js +90 -6
- package/dist/core/slash.d.ts +10 -0
- package/dist/core/slash.js +150 -20
- package/dist/core/types.d.ts +20 -1
- package/dist/core/workflows/context.js +44 -4
- package/dist/core/workflows/discovery.d.ts +2 -2
- package/dist/core/workflows/discovery.js +5 -7
- package/dist/core/workflows/index.d.ts +2 -1
- package/dist/core/workflows/index.js +2 -1
- package/dist/core/workflows/registry.d.ts +13 -3
- package/dist/core/workflows/registry.js +14 -9
- package/dist/core/workflows/resolve.d.ts +22 -0
- package/dist/core/workflows/resolve.js +23 -0
- package/dist/core/workflows/runtime.d.ts +6 -1
- package/dist/core/workflows/runtime.js +19 -2
- package/dist/db/schema.d.ts +356 -1
- package/dist/db/schema.js +57 -3
- package/dist/kanban/index.d.ts +2 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +84 -2
- package/dist/kanban/store.js +358 -25
- package/dist/llm/registry.d.ts +7 -1
- package/dist/llm/registry.js +7 -1
- package/dist/plugins/init.d.ts +7 -0
- package/dist/plugins/init.js +22 -1
- package/dist/plugins/loader.d.ts +7 -1
- package/dist/plugins/loader.js +12 -4
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/project.d.ts +23 -0
- package/dist/project/project.js +65 -0
- package/dist/project/trust.d.ts +65 -0
- package/dist/project/trust.js +328 -0
- package/dist/server/agent-manager.d.ts +6 -2
- package/dist/server/agent-manager.js +4 -2
- package/dist/server/app.js +45 -30
- package/dist/server/auth-manager.d.ts +15 -4
- package/dist/server/auth-manager.js +41 -24
- package/dist/server/context.js +11 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/middleware/auth.d.ts +11 -1
- package/dist/server/middleware/auth.js +25 -3
- package/dist/server/permission/interactive.d.ts +5 -0
- package/dist/server/permission/interactive.js +4 -1
- package/dist/server/permission/store.d.ts +2 -0
- package/dist/server/permission/store.js +8 -0
- package/dist/server/routes/auth.js +35 -9
- package/dist/server/routes/chat.d.ts +16 -7
- package/dist/server/routes/chat.js +483 -28
- package/dist/server/routes/commands.d.ts +2 -2
- package/dist/server/routes/commands.js +4 -6
- package/dist/server/routes/config.js +81 -4
- package/dist/server/routes/files.js +120 -24
- package/dist/server/routes/kanban.js +152 -22
- package/dist/server/routes/permissions.js +7 -0
- package/dist/server/routes/project.js +76 -22
- package/dist/server/routes/provider.js +6 -0
- package/dist/server/routes/session.js +177 -13
- package/dist/server/routes/tool.js +24 -5
- package/dist/server/routes/update.js +58 -2
- package/dist/server/routes/usage.d.ts +4 -0
- package/dist/server/routes/usage.js +133 -0
- package/dist/server/routes/workflows.js +476 -87
- package/dist/server/server.d.ts +12 -1
- package/dist/server/server.js +194 -33
- package/dist/server/terminal/pty-manager.d.ts +12 -0
- package/dist/server/terminal/pty-manager.js +63 -7
- package/dist/server/types.d.ts +9 -0
- package/dist/session/branch.d.ts +2 -1
- package/dist/session/branch.js +75 -10
- package/dist/session/context.js +28 -2
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/session.d.ts +67 -10
- package/dist/session/session.js +180 -67
- package/dist/session/types.d.ts +2 -0
- package/dist/session/usage.d.ts +62 -0
- package/dist/session/usage.js +194 -0
- package/dist/shared/types/agent.d.ts +19 -3
- package/dist/shared/types/config.d.ts +59 -4
- package/dist/shared/types/kanban.d.ts +4 -2
- package/dist/shared/types/message.d.ts +14 -0
- package/dist/tools/builtin/kanban.js +13 -2
- package/dist/tools/index.d.ts +14 -3
- package/dist/tools/index.js +23 -5
- package/dist/tools/websearch/index.d.ts +5 -1
- package/dist/tools/websearch/index.js +40 -4
- package/dist/tools/websearch/providers/brave.js +2 -2
- package/dist/tools/websearch/providers/duckduckgo.js +2 -1
- package/dist/tools/websearch/providers/tavily.js +2 -2
- package/dist/tools/websearch/types.d.ts +5 -1
- package/dist/tools/websearch/types.js +7 -1
- package/dist/update/scheduler.js +5 -2
- package/dist/update/snapshot.d.ts +17 -3
- package/dist/update/snapshot.js +3 -2
- package/dist/update/updater.d.ts +2 -0
- package/dist/update/updater.js +2 -1
- package/dist/update/version.d.ts +3 -0
- package/dist/update/version.js +9 -3
- package/drizzle/0010_usage_events_ledger.sql +17 -0
- package/drizzle/0011_slow_slapstick.sql +12 -0
- package/drizzle/0012_chief_timeslip.sql +1 -0
- package/drizzle/0013_swift_zeigeist.sql +1 -0
- package/drizzle/0014_flimsy_legion.sql +1 -0
- package/drizzle/meta/0010_snapshot.json +1010 -0
- package/drizzle/meta/0011_snapshot.json +1055 -0
- package/drizzle/meta/0012_snapshot.json +1062 -0
- package/drizzle/meta/0013_snapshot.json +1068 -0
- package/drizzle/meta/0014_snapshot.json +1075 -0
- package/drizzle/meta/_journal.json +35 -0
- package/package.json +24 -23
- package/dist/core/agents/discovery.d.ts +0 -6
- package/dist/core/agents/discovery.js +0 -96
package/dist/server/server.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ import { buildRegistryFromConfig, syncRegistryFromConfig } from './registry-conf
|
|
|
6
6
|
import type { ServerContext } from './types.js';
|
|
7
7
|
type StartServerOptions = {
|
|
8
8
|
port?: number;
|
|
9
|
+
/** 绑定地址(默认 127.0.0.1,仅本机可达)。显式配置非回环地址(0.0.0.0/局域网 IP)
|
|
10
|
+
* 时服务对网络可达,安全仅靠 token + allowedOrigins 保证——serve 命令会打印醒目警告。 */
|
|
11
|
+
host?: string;
|
|
9
12
|
cwd?: string;
|
|
10
13
|
/** 注入已有 DB(测试用)。 */
|
|
11
14
|
db?: DB;
|
|
@@ -31,6 +34,9 @@ type BootstrappedServer = {
|
|
|
31
34
|
ctx: ServerContext;
|
|
32
35
|
close(): Promise<void>;
|
|
33
36
|
};
|
|
37
|
+
/** serve 默认端口:避开 node 生态常用端口(3000/4000/5173/8000/8080…),
|
|
38
|
+
* 降低与其他本地服务撞端口的概率。显式 --port 始终优先。 */
|
|
39
|
+
export declare const DEFAULT_SERVE_PORT = 7310;
|
|
34
40
|
/** 解析 PGLite 持久化数据目录:优先 C0DE_DB_DIR,否则全局数据根下 pglite 子目录。
|
|
35
41
|
*
|
|
36
42
|
* 历史上数据库放在 <cwd>/.c0de/pglite(项目级),但项目 DB schema 通过 projectId
|
|
@@ -71,10 +77,15 @@ declare function buildServerContext(db: DB, opts?: StartServerOptions): Promise<
|
|
|
71
77
|
declare function acquireDevDbLock(dataDir: string): void;
|
|
72
78
|
/** Release the dev DB lock (best-effort, stale-detection covers missed calls). */
|
|
73
79
|
declare function releaseDevDbLock(dataDir: string): void;
|
|
80
|
+
/** 读取运行中 serve 的端口(CLI 锁冲突引导用)。文件缺失/损坏 → null。 */
|
|
81
|
+
declare function readServeInfo(dataDir: string): {
|
|
82
|
+
pid: number;
|
|
83
|
+
port: number;
|
|
84
|
+
} | null;
|
|
74
85
|
/** dev 专用:创建 + migrate PGLite,跨热重载复用(单写者,只建一次)。 */
|
|
75
86
|
declare function createDevDb(cwd: string): Promise<DB>;
|
|
76
87
|
/** 初始化 DB + 配置 + 注册表,返回 ServerContext + 清理函数(dev 与独立后端共用)。 */
|
|
77
88
|
declare function bootstrapServerContext(opts?: StartServerOptions): Promise<BootstrappedServer>;
|
|
78
89
|
declare function startServer(opts?: StartServerOptions): Promise<RunningServer>;
|
|
79
90
|
export type { BootstrappedServer, RunningServer, StartServerOptions };
|
|
80
|
-
export { acquireDevDbLock, bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, releaseDevDbLock, resolveAuthToken, resolveDbDir, startServer, syncRegistryFromConfig, };
|
|
91
|
+
export { acquireDevDbLock, bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, readServeInfo, releaseDevDbLock, resolveAuthToken, resolveDbDir, startServer, syncRegistryFromConfig, };
|
package/dist/server/server.js
CHANGED
|
@@ -7,12 +7,16 @@ import { dirname, join } from 'node:path';
|
|
|
7
7
|
import { serve } from '@hono/node-server';
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
9
|
import { BUILTIN_AGENTS, createAgentRegistry } from '../core/agents/index.js';
|
|
10
|
-
import { loadConfig } from '../core/config.js';
|
|
10
|
+
import { loadConfig, loadConfigScopes } from '../core/config.js';
|
|
11
11
|
import { createAndPopulateRegistry } from '../core/workflows/index.js';
|
|
12
12
|
import { createDB, migrateDB } from '../db/index.js';
|
|
13
|
+
import { purgeDeletedKanbanBoards } from '../kanban/index.js';
|
|
13
14
|
import { initPlugins } from '../plugins/index.js';
|
|
15
|
+
import { getByDirectory } from '../project/index.js';
|
|
16
|
+
import { projectTrustCurrent } from '../project/trust.js';
|
|
14
17
|
import { markDeadBackgroundJobs } from '../session/jobs.js';
|
|
15
18
|
import { purgeDeletedSessions, purgeEmptySessions, purgeTemporarySessions, TRASH_PURGE_GRACE_MS, TRASH_RETENTION_MS, } from '../session/session.js';
|
|
19
|
+
import { backfillUsageEventsOnce } from '../session/usage.js';
|
|
16
20
|
import { createDefaultRegistry, createDefaultURLRegistry } from '../tools/index.js';
|
|
17
21
|
import { checkForUpdate, confirmHandoff, createHandoffServer, createUpdateScheduler, requestHandoff, restoreSessions, } from '../update/index.js';
|
|
18
22
|
import { createAgentManager } from './agent-manager.js';
|
|
@@ -22,6 +26,9 @@ import { isAllowedOrigin } from './middleware/cors.js';
|
|
|
22
26
|
import { createPermissionStore } from './permission/store.js';
|
|
23
27
|
import { buildRegistryFromConfig, syncRegistryFromConfig } from './registry-config.js';
|
|
24
28
|
import { PTYManager } from './terminal/pty-manager.js';
|
|
29
|
+
/** serve 默认端口:避开 node 生态常用端口(3000/4000/5173/8000/8080…),
|
|
30
|
+
* 降低与其他本地服务撞端口的概率。显式 --port 始终优先。 */
|
|
31
|
+
export const DEFAULT_SERVE_PORT = 7310;
|
|
25
32
|
/** 全局数据根目录:XDG_DATA_HOME 优先,否则 ~/.local/share/c0de。
|
|
26
33
|
* 与 opencode (~/.local/share/opencode/)、oh-my-pi (~/.omp/agent/) 同约定——
|
|
27
34
|
* 数据库等运行时数据放全局,项目 .c0de/ 只留配置和扩展。 */
|
|
@@ -149,23 +156,51 @@ function migrateLegacyPglite(cwd, targetDir) {
|
|
|
149
156
|
*/
|
|
150
157
|
async function buildServerContext(db, opts = {}) {
|
|
151
158
|
const cwd = opts.cwd ?? process.cwd();
|
|
159
|
+
// P1:快照携带的终端元信息——新实例原位重建 shell(同 id)。提前解析出来,
|
|
160
|
+
// ptyManager 创建后立即恢复;旧快照无 terminals 字段时为空。
|
|
161
|
+
let restoredTerminals = [];
|
|
152
162
|
if (opts.restoreFrom) {
|
|
153
163
|
const snapshot = JSON.parse(readFileSync(opts.restoreFrom, 'utf8'));
|
|
154
164
|
await restoreSessions(db, snapshot);
|
|
165
|
+
restoredTerminals = Array.isArray(snapshot.terminals)
|
|
166
|
+
? snapshot.terminals.filter((t) => !!t &&
|
|
167
|
+
typeof t.id === 'string' &&
|
|
168
|
+
typeof t.shell === 'string' &&
|
|
169
|
+
typeof t.cwd === 'string')
|
|
170
|
+
: [];
|
|
155
171
|
}
|
|
156
172
|
const config = await loadConfig(cwd);
|
|
157
173
|
const toolRegistry = createDefaultRegistry(config);
|
|
158
174
|
const llmRegistry = buildRegistryFromConfig(config);
|
|
159
175
|
const urlRegistry = createDefaultURLRegistry();
|
|
176
|
+
// P0-2:项目插件只在项目被显式信任后加载(fail-closed)——克隆仓库自带的
|
|
177
|
+
// .c0de/plugins 不会在首次 serve 时静默执行;c0de trust / Web 信任后重启生效。
|
|
178
|
+
// 未注册项目(首次 serve、尚无项目记录)视为未信任。
|
|
179
|
+
// P0(代码面):已信任但指纹漂移(git pull 改了插件代码/MCP 参数/风险键)→
|
|
180
|
+
// 同样不加载——与聊天门禁同口径,杜绝「先重启、后门禁」窗口里执行漂移代码。
|
|
181
|
+
let projectTrusted = false;
|
|
182
|
+
try {
|
|
183
|
+
const p = await getByDirectory(db, cwd);
|
|
184
|
+
if (p?.trustedAt != null) {
|
|
185
|
+
projectTrusted = projectTrustCurrent(loadConfigScopes(cwd).project, p.trustedAt, p.riskFingerprint, cwd);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// 信任状态查询失败 → 保持未信任(宁可少加载插件)
|
|
190
|
+
}
|
|
160
191
|
const { pluginRegistry, hookRunner } = await initPlugins({
|
|
161
192
|
cwd,
|
|
162
193
|
config,
|
|
163
194
|
toolRegistry,
|
|
164
195
|
llmRegistry,
|
|
196
|
+
projectTrusted,
|
|
165
197
|
});
|
|
166
198
|
// 工作流注册表:三级发现(builtin → global → project),eager 初始化。
|
|
167
199
|
// 此前是惰性 getter 只注册 builtin,导致项目级 .c0de/workflows/*.js 永远不可见。
|
|
168
|
-
|
|
200
|
+
// P0 信任边界:项目级工作流与 .c0de/plugins 同为仓库自带任意代码执行面,
|
|
201
|
+
// 仅在 serve 目录项目当前可信任(projectTrusted)时 dynamic import——与上方
|
|
202
|
+
// 插件加载同口径,杜绝「克隆即执行」。
|
|
203
|
+
const workflowRegistry = await createAndPopulateRegistry(cwd, { projectTrusted });
|
|
169
204
|
const dataDir = resolveDbDir();
|
|
170
205
|
// 先解析/生成 bootstrap token(落盘),再创建 authManager 读取——
|
|
171
206
|
// 首启时文件由 resolveAuthToken 生成,顺序颠倒会导致 bootstrap 读到空值。
|
|
@@ -179,6 +214,34 @@ async function buildServerContext(db, opts = {}) {
|
|
|
179
214
|
? { firstDeviceTtlMs: config.security.firstDeviceTtlMs }
|
|
180
215
|
: {}),
|
|
181
216
|
});
|
|
217
|
+
const ptyManager = new PTYManager();
|
|
218
|
+
// P1:热更新恢复——按快照元信息原位重建终端 shell(同 id,前端布局无感重连)。
|
|
219
|
+
// 进程内状态(运行中的 dev server 等)无法续命,但用户在熟悉的布局里得到新 shell,
|
|
220
|
+
// 且 cwd/title/shell 保持一致。恢复失败(极端环境)不阻塞启动。
|
|
221
|
+
for (const t of restoredTerminals) {
|
|
222
|
+
try {
|
|
223
|
+
ptyManager.create({
|
|
224
|
+
id: t.id,
|
|
225
|
+
shell: t.shell,
|
|
226
|
+
cwd: t.cwd,
|
|
227
|
+
title: t.title || undefined,
|
|
228
|
+
...(t.projectId ? { projectId: t.projectId } : {}),
|
|
229
|
+
});
|
|
230
|
+
// P3-7:用户勾选的「更新后自动重启」——重建 shell 后把前台命令写入 stdin 重跑
|
|
231
|
+
// (如 `npm run dev`)。写入由 PTY 缓冲,shell 初始化完成后即执行;失败不阻塞启动。
|
|
232
|
+
if (typeof t.command === 'string' && t.command.length > 0) {
|
|
233
|
+
try {
|
|
234
|
+
ptyManager.write(t.id, `${t.command}\n`);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
console.warn(`[server] 终端命令重跑失败(${t.id}):${err instanceof Error ? err.message : String(err)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
console.warn(`[server] 终端恢复失败(${t.id}):${err instanceof Error ? err.message : String(err)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
182
245
|
const ctx = {
|
|
183
246
|
db,
|
|
184
247
|
config,
|
|
@@ -188,10 +251,21 @@ async function buildServerContext(db, opts = {}) {
|
|
|
188
251
|
hookRunner,
|
|
189
252
|
pluginRegistry,
|
|
190
253
|
agentManager: createAgentManager(),
|
|
191
|
-
|
|
254
|
+
// P3:权限双层超时可配置(config.permission.timeoutMs/expireGraceMs);
|
|
255
|
+
// 缺省回退 store 内置默认(5 分钟 + 25 分钟)。
|
|
256
|
+
permissionStore: createPermissionStore({
|
|
257
|
+
...(config.permission.timeoutMs !== undefined && config.permission.timeoutMs > 0
|
|
258
|
+
? { timeoutMs: config.permission.timeoutMs }
|
|
259
|
+
: {}),
|
|
260
|
+
...(config.permission.expireGraceMs !== undefined && config.permission.expireGraceMs > 0
|
|
261
|
+
? { expireGraceMs: config.permission.expireGraceMs }
|
|
262
|
+
: {}),
|
|
263
|
+
}),
|
|
192
264
|
permissionMode: config.permission.defaultMode,
|
|
193
265
|
sessionPermissionModes: new Map(),
|
|
194
266
|
sessionAlwaysAllow: new Map(),
|
|
267
|
+
workflowBusyBySession: new Map(),
|
|
268
|
+
workflowBusyByScope: new Map(),
|
|
195
269
|
authToken: resolvedToken,
|
|
196
270
|
authManager: config.security.authEnabled === false ? undefined : authManager,
|
|
197
271
|
// Agent 注册表:内置 4 个默认 agent;项目/用户自定义 agent 可在启动后补充加载。
|
|
@@ -209,7 +283,7 @@ async function buildServerContext(db, opts = {}) {
|
|
|
209
283
|
initialDelayMs: config.update.initialDelayMs,
|
|
210
284
|
}),
|
|
211
285
|
cwd,
|
|
212
|
-
ptyManager
|
|
286
|
+
ptyManager,
|
|
213
287
|
};
|
|
214
288
|
// handoff server 不在此创建:它需要持有主 HTTP server 引用以触发完整关停,
|
|
215
289
|
// 由 startServer 在 serve() 之后创建(dev 模式走 vite,不创建)。
|
|
@@ -285,6 +359,49 @@ function releaseDevDbLock(dataDir) {
|
|
|
285
359
|
/* best-effort */
|
|
286
360
|
}
|
|
287
361
|
}
|
|
362
|
+
const SERVE_INFO_FILE = 'serve-info.json';
|
|
363
|
+
/** serve 运行信息落盘(port + pid):CLI 在锁冲突(serve 占用持久库)时读取,
|
|
364
|
+
* 引导用户直接打开 Web 界面继续对话,而不是给出无从下手的报错。不落 token。 */
|
|
365
|
+
function writeServeInfo(dataDir, port) {
|
|
366
|
+
try {
|
|
367
|
+
if (!existsSync(dataDir))
|
|
368
|
+
mkdirSync(dataDir, { recursive: true });
|
|
369
|
+
writeFileSync(join(dataDir, SERVE_INFO_FILE), JSON.stringify({ pid: process.pid, port, startedAt: Date.now() }), 'utf8');
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
/* best-effort:仅影响 CLI 引导文案,不影响服务本身 */
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** 关闭时清除本进程写入的 serve 信息(pid 匹配才删,防止误删热更新新实例的信息)。 */
|
|
376
|
+
function removeServeInfo(dataDir) {
|
|
377
|
+
try {
|
|
378
|
+
const path = join(dataDir, SERVE_INFO_FILE);
|
|
379
|
+
if (!existsSync(path))
|
|
380
|
+
return;
|
|
381
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
382
|
+
if (parsed.pid === process.pid)
|
|
383
|
+
unlinkSync(path);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
/* best-effort */
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** 读取运行中 serve 的端口(CLI 锁冲突引导用)。文件缺失/损坏 → null。 */
|
|
390
|
+
function readServeInfo(dataDir) {
|
|
391
|
+
try {
|
|
392
|
+
const path = join(dataDir, SERVE_INFO_FILE);
|
|
393
|
+
if (!existsSync(path))
|
|
394
|
+
return null;
|
|
395
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
396
|
+
if (typeof parsed.pid === 'number' && typeof parsed.port === 'number') {
|
|
397
|
+
return { pid: parsed.pid, port: parsed.port };
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
288
405
|
/** dev 专用:创建 + migrate PGLite,跨热重载复用(单写者,只建一次)。 */
|
|
289
406
|
async function createDevDb(cwd) {
|
|
290
407
|
const dataDir = resolveDbDir();
|
|
@@ -329,6 +446,17 @@ async function bootstrapServerContext(opts = {}) {
|
|
|
329
446
|
db = await createDevDb(cwd);
|
|
330
447
|
}
|
|
331
448
|
const { ctx, dispose } = await buildServerContext(db, opts);
|
|
449
|
+
// P2 成本账本 backfill:升级后首次启动把遗留 segments 补齐进 usage_events
|
|
450
|
+
// (幂等,callId 唯一约束去重;app_meta 标记保证只执行一次——导入会话的
|
|
451
|
+
// segments 已剥离 call id,不产生账本行)。fire-and-forget。
|
|
452
|
+
void backfillUsageEventsOnce(db)
|
|
453
|
+
.then((added) => {
|
|
454
|
+
if (added > 0)
|
|
455
|
+
console.log(`[server] 成本账本补齐:新增 ${added} 条历史调用记录`);
|
|
456
|
+
})
|
|
457
|
+
.catch((err) => {
|
|
458
|
+
console.warn(`[server] 成本账本补齐失败:${err instanceof Error ? err.message : String(err)}`);
|
|
459
|
+
});
|
|
332
460
|
// P2-4 + A3:回收站两阶段清理——启动时清一次 + 每 24h 清一次。
|
|
333
461
|
// 阶段一标记到期条目进入宽限期(UI 可见可恢复),阶段二物理清除宽限期满的条目。
|
|
334
462
|
// fire-and-forget:清理失败不阻塞服务启动;结果落日志可查。
|
|
@@ -343,6 +471,18 @@ async function bootstrapServerContext(opts = {}) {
|
|
|
343
471
|
}
|
|
344
472
|
})
|
|
345
473
|
.catch(() => { });
|
|
474
|
+
// P2-5:回收站看板两阶段清理(与会话回收站同策略:到期先标记宽限,期满再物理清除,
|
|
475
|
+
// 杜绝「到期即静默清空」)。
|
|
476
|
+
void purgeDeletedKanbanBoards(db, TRASH_RETENTION_MS, TRASH_PURGE_GRACE_MS)
|
|
477
|
+
.then(({ marked, deleted }) => {
|
|
478
|
+
if (marked > 0) {
|
|
479
|
+
console.log(`[server] 回收站清理:${marked} 个看板已到期,进入 ${TRASH_PURGE_GRACE_MS / (24 * 60 * 60 * 1000)} 天宽限期(期间可恢复)`);
|
|
480
|
+
}
|
|
481
|
+
if (deleted > 0) {
|
|
482
|
+
console.log(`[server] 回收站清理:已物理清除 ${deleted} 个超过宽限期的看板`);
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
.catch(() => { });
|
|
346
486
|
};
|
|
347
487
|
runTrashPurge();
|
|
348
488
|
const purgeTimer = setInterval(() => {
|
|
@@ -476,7 +616,10 @@ function attachTerminalUpgrade(ctx, wss, server) {
|
|
|
476
616
|
});
|
|
477
617
|
}
|
|
478
618
|
async function startServer(opts = {}) {
|
|
479
|
-
const port = opts.port ??
|
|
619
|
+
const port = opts.port ?? DEFAULT_SERVE_PORT;
|
|
620
|
+
// 默认仅绑定回环地址(安全默认:本地工具不应默认暴露局域网/公网)。
|
|
621
|
+
// 需要容器/远程访问时显式 --host 0.0.0.0(serve 命令会打印非回环警告)。
|
|
622
|
+
const host = opts.host ?? '127.0.0.1';
|
|
480
623
|
// spec §18.3:新实例从 --handoff-port 拿到旧实例端口,请求优雅退出后绑端口。
|
|
481
624
|
// 握手 token:updater spawn 时经 C0DE_AUTH_TOKEN 环境变量传入,否则读 auth-token 文件。
|
|
482
625
|
if (opts.handoffPort && !opts.skipHandoff) {
|
|
@@ -508,8 +651,10 @@ async function startServer(opts = {}) {
|
|
|
508
651
|
// config.update.enabled 时启动后台调度器。
|
|
509
652
|
if (ctx.config.update.enabled)
|
|
510
653
|
ctx.updateScheduler.start();
|
|
511
|
-
const server = serve({ fetch: app.fetch, port });
|
|
654
|
+
const server = serve({ fetch: app.fetch, port, hostname: host });
|
|
512
655
|
ctx.port = port;
|
|
656
|
+
ctx.host = host;
|
|
657
|
+
writeServeInfo(resolveDbDir(), port);
|
|
513
658
|
// WebSocket:终端双向流。Hono v2 无原生 WS,用 ws 包直接挂载到 HTTP server。
|
|
514
659
|
const wss = new WebSocketServer({ noServer: true });
|
|
515
660
|
attachTerminalUpgrade(ctx, wss, server);
|
|
@@ -520,6 +665,7 @@ async function startServer(opts = {}) {
|
|
|
520
665
|
closed = true;
|
|
521
666
|
wss.close();
|
|
522
667
|
server.close();
|
|
668
|
+
removeServeInfo(resolveDbDir());
|
|
523
669
|
await closeCtx();
|
|
524
670
|
};
|
|
525
671
|
const close = async () => {
|
|
@@ -536,7 +682,40 @@ async function startServer(opts = {}) {
|
|
|
536
682
|
// config.update.enabled=false 或 skipHandoff 时跳过。
|
|
537
683
|
if (ctx.config.update.enabled && !opts.skipHandoff) {
|
|
538
684
|
const createHandoff = opts.createHandoffFn ?? createHandoffServer;
|
|
539
|
-
|
|
685
|
+
// P0 修复:确认窗口必须在「收到 POST /handoff(交接开始)」时才启动,
|
|
686
|
+
// 而非 bringUp 时无条件启动——否则任何运行超过 HANDOFF_CONFIRM_WINDOW_MS
|
|
687
|
+
// 的普通实例都会在窗口超时后误判「新实例未确认」,重新 bringUp 时第二
|
|
688
|
+
// listen 触发 EADDRINUSE(unhandled 'error')崩溃。窗口从交接实际发生起算。
|
|
689
|
+
const armConfirmWindow = () => {
|
|
690
|
+
clearConfirmTimer();
|
|
691
|
+
confirmTimer = setTimeout(() => {
|
|
692
|
+
void (async () => {
|
|
693
|
+
if (reviving)
|
|
694
|
+
return;
|
|
695
|
+
reviving = true;
|
|
696
|
+
try {
|
|
697
|
+
await bringUp();
|
|
698
|
+
console.warn('[handoff] 新实例未确认接管,旧实例已复活并重新接管端口');
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
if (err.code === 'EADDRINUSE') {
|
|
702
|
+
// 新实例已绑定端口(确认请求丢失)→ 它已接管,旧实例正常退出。
|
|
703
|
+
console.warn('[handoff] 端口已被新实例接管,旧实例退出');
|
|
704
|
+
process.exit(0);
|
|
705
|
+
}
|
|
706
|
+
console.error('[handoff] 旧实例复活失败,退出:', err);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
finally {
|
|
710
|
+
reviving = false;
|
|
711
|
+
}
|
|
712
|
+
})();
|
|
713
|
+
}, HANDOFF_CONFIRM_WINDOW_MS);
|
|
714
|
+
};
|
|
715
|
+
handoffServer = await createHandoff(async () => {
|
|
716
|
+
await closeMain();
|
|
717
|
+
armConfirmWindow();
|
|
718
|
+
}, {
|
|
540
719
|
// P2-16:token 轮换后新旧实例 bootstrap 可能不同,用 verifyHandoff 接受
|
|
541
720
|
// 当前/历史 bootstrap 与设备 token;无 authManager 回退字符串比较。
|
|
542
721
|
...(ctx.authManager
|
|
@@ -550,30 +729,6 @@ async function startServer(opts = {}) {
|
|
|
550
729
|
},
|
|
551
730
|
});
|
|
552
731
|
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
732
|
}
|
|
578
733
|
// 两阶段交接第二阶段:新实例绑定成功后确认旧实例可退出。
|
|
579
734
|
// 旧实例(无 handoffPort)跳过;确认失败静默(窗口超时兜底)。
|
|
@@ -591,9 +746,15 @@ async function startServer(opts = {}) {
|
|
|
591
746
|
close: async () => {
|
|
592
747
|
await currentClose?.();
|
|
593
748
|
},
|
|
594
|
-
|
|
749
|
+
// P:仅「首设备待注册」时回传 bootstrap 供 serve 命令拼 ?token= 注册 URL。
|
|
750
|
+
// 已有已注册设备时 auth-token 文件里是注册后被轮换的旧值——带进 URL 会让新浏览器
|
|
751
|
+
// 误点「注册链接」却得到 BOOTSTRAP_CONSUMED 引导配对(横幅文案误导)。此时不携带,
|
|
752
|
+
// serve 打印无 token 的 URL,新浏览器自然走配对流程。
|
|
753
|
+
...(currentAuthToken && !hasRegisteredDevices(resolveDbDir())
|
|
754
|
+
? { authToken: currentAuthToken }
|
|
755
|
+
: {}),
|
|
595
756
|
};
|
|
596
757
|
}
|
|
597
758
|
// P1-1:buildRegistryFromConfig/syncRegistryFromConfig 实现移入 registry-config.ts,
|
|
598
759
|
// 此处 re-export 保留 server.ts 历史导出兼容(测试/CLI 引用)。
|
|
599
|
-
export { acquireDevDbLock, bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, releaseDevDbLock, resolveAuthToken, resolveDbDir, startServer, syncRegistryFromConfig, };
|
|
760
|
+
export { acquireDevDbLock, bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, readServeInfo, releaseDevDbLock, resolveAuthToken, resolveDbDir, startServer, syncRegistryFromConfig, };
|
|
@@ -21,6 +21,8 @@ export interface CreatePTYOptions {
|
|
|
21
21
|
shell?: string;
|
|
22
22
|
/** 所属项目 id。 */
|
|
23
23
|
projectId?: string;
|
|
24
|
+
/** 指定 PTY id(热更新快照恢复时沿用原 id,前端布局无感重连)。 */
|
|
25
|
+
id?: string;
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* 检测当前平台默认 shell。
|
|
@@ -32,6 +34,16 @@ export interface CreatePTYOptions {
|
|
|
32
34
|
* /etc/passwd(getpwuid),是用户真实登录 shell 的可靠来源。
|
|
33
35
|
*/
|
|
34
36
|
export declare function detectShell(): string;
|
|
37
|
+
/** 把 argv 拼回可安全经 shell 重放的命令行(导出便于单测安全转义)。 */
|
|
38
|
+
export declare function argvToCommand(argv: string[]): string;
|
|
39
|
+
/**
|
|
40
|
+
* 尽力捕获 shell 当前前台命令(Linux 专有,最佳努力)——热更新后可选自动重跑。
|
|
41
|
+
* 读取 /proc/<shellPid>/task/<shellPid>/children:
|
|
42
|
+
* - 恰好 1 个直接子进程 → 返回其 cmdline(如 `npm run dev`)。
|
|
43
|
+
* - 0 个 / 多个(后台任务、管道、子 shell)→ 返回 null(不猜,回退默认不重跑)。
|
|
44
|
+
* 非 Linux 平台或任何读取异常 → null(能力不可用,优雅降级)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function captureForegroundCommand(shellPid: number): string | null;
|
|
35
47
|
/**
|
|
36
48
|
* PTY 生命周期管理器。
|
|
37
49
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/server/terminal/pty-manager.ts
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdirSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir, userInfo } from 'node:os';
|
|
5
5
|
import { basename, join } from 'node:path';
|
|
6
6
|
import { spawn } from 'node-pty';
|
|
@@ -86,6 +86,10 @@ export function detectShell() {
|
|
|
86
86
|
}
|
|
87
87
|
const DEFAULT_COLS = 80;
|
|
88
88
|
const DEFAULT_ROWS = 24;
|
|
89
|
+
/** P3-13:cols/rows 上限钳制——路由层只查 Number.isFinite,负值/超大值直接进
|
|
90
|
+
* spawn 可能打爆 node-pty 缓冲区或 UI 布局。 */
|
|
91
|
+
const MAX_COLS = 1000;
|
|
92
|
+
const MAX_ROWS = 500;
|
|
89
93
|
const MAX_TITLE_LEN = 100;
|
|
90
94
|
/** scrollback 环形缓冲最大字节数(约 50KB)。 */
|
|
91
95
|
const SCROLLBACK_MAX = 50_000;
|
|
@@ -93,6 +97,41 @@ function truncateTitle(title) {
|
|
|
93
97
|
const clean = title.replace(/[\r\n]/g, ' ').trim();
|
|
94
98
|
return clean.length > MAX_TITLE_LEN ? `${clean.slice(0, MAX_TITLE_LEN)}…` : clean;
|
|
95
99
|
}
|
|
100
|
+
/** POSIX 单引号转义:仅安全字符(字母数字与常见路径/别名符号)原样,否则单引号包裹。 */
|
|
101
|
+
function shellQuote(arg) {
|
|
102
|
+
if (/^[A-Za-z0-9_./:,+@%=-]+$/.test(arg))
|
|
103
|
+
return arg;
|
|
104
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
105
|
+
}
|
|
106
|
+
/** 把 argv 拼回可安全经 shell 重放的命令行(导出便于单测安全转义)。 */
|
|
107
|
+
export function argvToCommand(argv) {
|
|
108
|
+
return argv.map(shellQuote).join(' ');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 尽力捕获 shell 当前前台命令(Linux 专有,最佳努力)——热更新后可选自动重跑。
|
|
112
|
+
* 读取 /proc/<shellPid>/task/<shellPid>/children:
|
|
113
|
+
* - 恰好 1 个直接子进程 → 返回其 cmdline(如 `npm run dev`)。
|
|
114
|
+
* - 0 个 / 多个(后台任务、管道、子 shell)→ 返回 null(不猜,回退默认不重跑)。
|
|
115
|
+
* 非 Linux 平台或任何读取异常 → null(能力不可用,优雅降级)。
|
|
116
|
+
*/
|
|
117
|
+
export function captureForegroundCommand(shellPid) {
|
|
118
|
+
if (process.platform !== 'linux')
|
|
119
|
+
return null;
|
|
120
|
+
try {
|
|
121
|
+
const childrenRaw = readFileSync(`/proc/${shellPid}/task/${shellPid}/children`, 'utf8');
|
|
122
|
+
const children = childrenRaw.trim().split(/\s+/).filter(Boolean).map(Number);
|
|
123
|
+
if (children.length !== 1)
|
|
124
|
+
return null;
|
|
125
|
+
const cmdline = readFileSync(`/proc/${children[0]}/cmdline`, 'utf8');
|
|
126
|
+
const argv = cmdline.split('\0').filter((s) => s.length > 0);
|
|
127
|
+
if (argv.length === 0)
|
|
128
|
+
return null;
|
|
129
|
+
return argvToCommand(argv);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
96
135
|
/**
|
|
97
136
|
* PTY 生命周期管理器。
|
|
98
137
|
*
|
|
@@ -104,9 +143,15 @@ export class PTYManager {
|
|
|
104
143
|
entries = new Map();
|
|
105
144
|
/** 创建新 PTY 会话。 */
|
|
106
145
|
create(opts) {
|
|
107
|
-
const id = `pty_${randomUUID()}`;
|
|
108
|
-
|
|
109
|
-
const
|
|
146
|
+
const id = opts.id ?? `pty_${randomUUID()}`;
|
|
147
|
+
// 指定 id 已存在(重复 restore 快照)→ 返回既有条目,避免泄漏/覆盖进程。
|
|
148
|
+
const existing = this.entries.get(id);
|
|
149
|
+
if (existing)
|
|
150
|
+
return existing.info;
|
|
151
|
+
// P3-13:钳制到 [1, MAX]——负值/超大值不进入 spawn。
|
|
152
|
+
const clampDim = (v, dflt, max) => Math.min(max, Math.max(1, Number.isFinite(v) ? v : dflt));
|
|
153
|
+
const cols = clampDim(opts.cols, DEFAULT_COLS, MAX_COLS);
|
|
154
|
+
const rows = clampDim(opts.rows, DEFAULT_ROWS, MAX_ROWS);
|
|
110
155
|
const shell = opts.shell ?? detectShell();
|
|
111
156
|
const integration = setupShellIntegration(shell);
|
|
112
157
|
const pty = spawn(shell, integration.spawnArgs, {
|
|
@@ -158,6 +203,14 @@ export class PTYManager {
|
|
|
158
203
|
ws.close(1000, 'pty exited');
|
|
159
204
|
}
|
|
160
205
|
}
|
|
206
|
+
// P3-12 修复:自然退出同样清理 shell 注入脚本/目录(bash --init-file、
|
|
207
|
+
// zsh ZDOTDIR 临时文件)——此前仅 kill() 清理,自然退出残留 /tmp。
|
|
208
|
+
try {
|
|
209
|
+
entry.integrationCleanup?.();
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// 清理失败不影响退出流程
|
|
213
|
+
}
|
|
161
214
|
this.entries.delete(id);
|
|
162
215
|
});
|
|
163
216
|
this.entries.set(id, entry);
|
|
@@ -175,9 +228,12 @@ export class PTYManager {
|
|
|
175
228
|
const entry = this.entries.get(id);
|
|
176
229
|
if (!entry)
|
|
177
230
|
throw new Error(`PTY not found: ${id}`);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
231
|
+
// P3-13:与 create 同口径钳制
|
|
232
|
+
const c = Math.min(MAX_COLS, Math.max(1, cols));
|
|
233
|
+
const r = Math.min(MAX_ROWS, Math.max(1, rows));
|
|
234
|
+
entry.pty.resize(c, r);
|
|
235
|
+
entry.info.cols = c;
|
|
236
|
+
entry.info.rows = r;
|
|
181
237
|
}
|
|
182
238
|
/** 更新 PTY 标题。 */
|
|
183
239
|
setTitle(id, title) {
|
package/dist/server/types.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ type ServerContext = {
|
|
|
46
46
|
authManager?: AuthManager;
|
|
47
47
|
/** 主 HTTP 服务实际绑定端口(startServer 绑定后回填;热更新 spawn 新实例复用)。 */
|
|
48
48
|
port?: number;
|
|
49
|
+
/** 主 HTTP 服务绑定地址(startServer 回填;热更新 spawn 新实例须复用同一地址)。 */
|
|
50
|
+
host?: string;
|
|
49
51
|
/** P1-1:项目级 LLM 注册表缓存 Map<projectId, Registry>——按项目合并配置
|
|
50
52
|
* (global + 项目 .c0de/config.json 的 providers)构建,会话聊天按所属项目
|
|
51
53
|
* 解析路由。项目配置 PATCH 后失效重建。 */
|
|
@@ -54,6 +56,13 @@ type ServerContext = {
|
|
|
54
56
|
agentRegistry: AgentRegistry;
|
|
55
57
|
/** 工作流注册表(spec: dynamic-workflow-design)。注入 /workflow slash 命令和 workflowz steering。 */
|
|
56
58
|
workflowRegistry?: import('../core/workflows/registry.js').WorkflowRegistry;
|
|
59
|
+
/** 工作流运行占用:发起会话 id → 工作流会话 id(/workflow run 通道)。
|
|
60
|
+
* 发起会话在其工作流运行期间视为占用——主 run 与再次 /workflow run 均 409;
|
|
61
|
+
* abort/pause/resume 控制端点据此路由到实际的工作流 run。 */
|
|
62
|
+
workflowBusyBySession: Map<string, string>;
|
|
63
|
+
/** 工作流运行占用:执行作用域键 → 工作流会话 id。键为 `project:<projectId>`
|
|
64
|
+
* 或 `dir:<cwd>`(REST 无 projectId 时)。同一项目/目录不允许并发工作流执行。 */
|
|
65
|
+
workflowBusyByScope: Map<string, string>;
|
|
57
66
|
/** 后台版本检查调度器(spec §18.1);/api/update 读取其缓存结果。 */
|
|
58
67
|
updateScheduler: UpdateScheduler;
|
|
59
68
|
/** Handoff HTTP 端点(spec §18.3);热更新时新实例 POST /handoff 触发优雅退出。 undefined 表示未启用。 */
|
package/dist/session/branch.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ declare function forkSession(handle: DB, sessionId: string, messageIndex: number
|
|
|
14
14
|
declare function getBranches(handle: DB, sessionId: string): Promise<Session[]>;
|
|
15
15
|
/** Build a full session tree from root sessions down.
|
|
16
16
|
* 每层按 metadata.lastOpenedAt 降序(fallback updatedAt、createdAt)。
|
|
17
|
-
*
|
|
17
|
+
* 排除软删除会话与临时 CLI 会话(print/workflow);持久化 CLI 会话(--continue 续接)
|
|
18
|
+
* 与 Web 会话同树可见(CLI/Web 同库不同视图 → 同库同视图)。
|
|
18
19
|
*/
|
|
19
20
|
declare function getTree(handle: DB): Promise<SessionTreeNode[]>;
|
|
20
21
|
export { BranchPointOutOfRangeError, forkSession, getBranches, getTree };
|