c0de-agent 0.0.0 → 1.0.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.
Files changed (330) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -0
  3. package/dist/cli/commands/acp.d.ts +19 -0
  4. package/dist/cli/commands/acp.js +50 -0
  5. package/dist/cli/commands/chat.d.ts +13 -0
  6. package/dist/cli/commands/chat.js +26 -0
  7. package/dist/cli/commands/config.d.ts +17 -0
  8. package/dist/cli/commands/config.js +77 -0
  9. package/dist/cli/commands/init.d.ts +9 -0
  10. package/dist/cli/commands/init.js +14 -0
  11. package/dist/cli/commands/plugin.d.ts +11 -0
  12. package/dist/cli/commands/plugin.js +31 -0
  13. package/dist/cli/commands/serve.d.ts +18 -0
  14. package/dist/cli/commands/serve.js +35 -0
  15. package/dist/cli/commands/update.d.ts +24 -0
  16. package/dist/cli/commands/update.js +40 -0
  17. package/dist/cli/deps.d.ts +19 -0
  18. package/dist/cli/deps.js +57 -0
  19. package/dist/cli/index.d.ts +8 -0
  20. package/dist/cli/index.js +149 -0
  21. package/dist/cli/modes/acp.d.ts +18 -0
  22. package/dist/cli/modes/acp.js +57 -0
  23. package/dist/cli/modes/print.d.ts +16 -0
  24. package/dist/cli/modes/print.js +30 -0
  25. package/dist/cli/parser.d.ts +19 -0
  26. package/dist/cli/parser.js +28 -0
  27. package/dist/cli/utils/output.d.ts +11 -0
  28. package/dist/cli/utils/output.js +23 -0
  29. package/dist/cli/utils/prompt.d.ts +12 -0
  30. package/dist/cli/utils/prompt.js +18 -0
  31. package/dist/core/agent.d.ts +12 -0
  32. package/dist/core/agent.js +95 -0
  33. package/dist/core/agents/builtin.d.ts +6 -0
  34. package/dist/core/agents/builtin.js +76 -0
  35. package/dist/core/agents/discovery.d.ts +6 -0
  36. package/dist/core/agents/discovery.js +96 -0
  37. package/dist/core/agents/index.d.ts +6 -0
  38. package/dist/core/agents/index.js +4 -0
  39. package/dist/core/agents/parallel.d.ts +13 -0
  40. package/dist/core/agents/parallel.js +37 -0
  41. package/dist/core/agents/registry.d.ts +4 -0
  42. package/dist/core/agents/registry.js +23 -0
  43. package/dist/core/agents/types.d.ts +37 -0
  44. package/dist/core/agents/types.js +1 -0
  45. package/dist/core/compact.d.ts +11 -0
  46. package/dist/core/compact.js +31 -0
  47. package/dist/core/config.d.ts +7 -0
  48. package/dist/core/config.js +84 -0
  49. package/dist/core/context.d.ts +23 -0
  50. package/dist/core/context.js +93 -0
  51. package/dist/core/index.d.ts +13 -0
  52. package/dist/core/index.js +11 -0
  53. package/dist/core/loop.d.ts +25 -0
  54. package/dist/core/loop.js +651 -0
  55. package/dist/core/metrics.d.ts +27 -0
  56. package/dist/core/metrics.js +94 -0
  57. package/dist/core/prompt-registry.d.ts +21 -0
  58. package/dist/core/prompt-registry.js +200 -0
  59. package/dist/core/prompt.d.ts +11 -0
  60. package/dist/core/prompt.js +13 -0
  61. package/dist/core/secret.d.ts +12 -0
  62. package/dist/core/secret.js +51 -0
  63. package/dist/core/slash.d.ts +15 -0
  64. package/dist/core/slash.js +114 -0
  65. package/dist/core/steering.d.ts +8 -0
  66. package/dist/core/steering.js +15 -0
  67. package/dist/core/title.d.ts +42 -0
  68. package/dist/core/title.js +115 -0
  69. package/dist/core/tool-exec.d.ts +20 -0
  70. package/dist/core/tool-exec.js +64 -0
  71. package/dist/core/types.d.ts +95 -0
  72. package/dist/core/types.js +1 -0
  73. package/dist/core/worktree.d.ts +24 -0
  74. package/dist/core/worktree.js +68 -0
  75. package/dist/dap/index.d.ts +6 -0
  76. package/dist/dap/index.js +4 -0
  77. package/dist/dap/protocol.d.ts +40 -0
  78. package/dist/dap/protocol.js +132 -0
  79. package/dist/dap/session.d.ts +23 -0
  80. package/dist/dap/session.js +130 -0
  81. package/dist/dap/transport.d.ts +8 -0
  82. package/dist/dap/transport.js +30 -0
  83. package/dist/dap/types.d.ts +50 -0
  84. package/dist/dap/types.js +2 -0
  85. package/dist/db/client.d.ts +22 -0
  86. package/dist/db/client.js +39 -0
  87. package/dist/db/index.d.ts +6 -0
  88. package/dist/db/index.js +4 -0
  89. package/dist/db/migrate.d.ts +9 -0
  90. package/dist/db/migrate.js +15 -0
  91. package/dist/db/schema.d.ts +965 -0
  92. package/dist/db/schema.js +112 -0
  93. package/dist/db/types.d.ts +11 -0
  94. package/dist/db/types.js +1 -0
  95. package/dist/llm/index.d.ts +13 -0
  96. package/dist/llm/index.js +8 -0
  97. package/dist/llm/protocols/openai-compat.d.ts +139 -0
  98. package/dist/llm/protocols/openai-compat.js +214 -0
  99. package/dist/llm/protocols/utils/index.d.ts +2 -0
  100. package/dist/llm/protocols/utils/index.js +2 -0
  101. package/dist/llm/protocols/utils/lifecycle.d.ts +23 -0
  102. package/dist/llm/protocols/utils/lifecycle.js +46 -0
  103. package/dist/llm/protocols/utils/tool-stream.d.ts +46 -0
  104. package/dist/llm/protocols/utils/tool-stream.js +94 -0
  105. package/dist/llm/provider-error.d.ts +5 -0
  106. package/dist/llm/provider-error.js +33 -0
  107. package/dist/llm/provider.d.ts +32 -0
  108. package/dist/llm/provider.js +191 -0
  109. package/dist/llm/registry.d.ts +44 -0
  110. package/dist/llm/registry.js +107 -0
  111. package/dist/llm/retry.d.ts +37 -0
  112. package/dist/llm/retry.js +82 -0
  113. package/dist/llm/routing.d.ts +36 -0
  114. package/dist/llm/routing.js +53 -0
  115. package/dist/llm/schema/errors.d.ts +88 -0
  116. package/dist/llm/schema/errors.js +33 -0
  117. package/dist/llm/schema/events.d.ts +135 -0
  118. package/dist/llm/schema/events.js +35 -0
  119. package/dist/llm/schema/ids.d.ts +22 -0
  120. package/dist/llm/schema/ids.js +3 -0
  121. package/dist/llm/schema/index.d.ts +5 -0
  122. package/dist/llm/schema/index.js +5 -0
  123. package/dist/llm/schema/messages.d.ts +97 -0
  124. package/dist/llm/schema/messages.js +18 -0
  125. package/dist/llm/schema/options.d.ts +54 -0
  126. package/dist/llm/schema/options.js +54 -0
  127. package/dist/llm/token.d.ts +2 -0
  128. package/dist/llm/token.js +13 -0
  129. package/dist/llm/transport.d.ts +31 -0
  130. package/dist/llm/transport.js +165 -0
  131. package/dist/mcp/index.d.ts +1 -0
  132. package/dist/mcp/index.js +3 -0
  133. package/dist/plugins/builtin.d.ts +6 -0
  134. package/dist/plugins/builtin.js +54 -0
  135. package/dist/plugins/hooks.d.ts +3 -0
  136. package/dist/plugins/hooks.js +79 -0
  137. package/dist/plugins/index.d.ts +9 -0
  138. package/dist/plugins/index.js +8 -0
  139. package/dist/plugins/init.d.ts +23 -0
  140. package/dist/plugins/init.js +30 -0
  141. package/dist/plugins/lifecycle.d.ts +6 -0
  142. package/dist/plugins/lifecycle.js +74 -0
  143. package/dist/plugins/loader.d.ts +16 -0
  144. package/dist/plugins/loader.js +68 -0
  145. package/dist/plugins/logger.d.ts +4 -0
  146. package/dist/plugins/logger.js +31 -0
  147. package/dist/plugins/registry.d.ts +7 -0
  148. package/dist/plugins/registry.js +24 -0
  149. package/dist/plugins/types.d.ts +113 -0
  150. package/dist/plugins/types.js +1 -0
  151. package/dist/project/detect.d.ts +4 -0
  152. package/dist/project/detect.js +131 -0
  153. package/dist/project/index.d.ts +4 -0
  154. package/dist/project/index.js +2 -0
  155. package/dist/project/project.d.ts +23 -0
  156. package/dist/project/project.js +87 -0
  157. package/dist/project/resolve.d.ts +8 -0
  158. package/dist/project/resolve.js +81 -0
  159. package/dist/server/agent-manager.d.ts +32 -0
  160. package/dist/server/agent-manager.js +54 -0
  161. package/dist/server/app.d.ts +5 -0
  162. package/dist/server/app.js +75 -0
  163. package/dist/server/context.d.ts +20 -0
  164. package/dist/server/context.js +47 -0
  165. package/dist/server/dev.d.ts +11 -0
  166. package/dist/server/dev.js +83 -0
  167. package/dist/server/index.d.ts +11 -0
  168. package/dist/server/index.js +7 -0
  169. package/dist/server/middleware/auth.d.ts +15 -0
  170. package/dist/server/middleware/auth.js +29 -0
  171. package/dist/server/middleware/cors.d.ts +16 -0
  172. package/dist/server/middleware/cors.js +44 -0
  173. package/dist/server/middleware/error.d.ts +15 -0
  174. package/dist/server/middleware/error.js +11 -0
  175. package/dist/server/permission/interactive.d.ts +24 -0
  176. package/dist/server/permission/interactive.js +56 -0
  177. package/dist/server/permission/store.d.ts +30 -0
  178. package/dist/server/permission/store.js +47 -0
  179. package/dist/server/routes/agent.d.ts +4 -0
  180. package/dist/server/routes/agent.js +19 -0
  181. package/dist/server/routes/catalog.d.ts +42 -0
  182. package/dist/server/routes/catalog.js +194 -0
  183. package/dist/server/routes/chat.d.ts +8 -0
  184. package/dist/server/routes/chat.js +282 -0
  185. package/dist/server/routes/commands.d.ts +5 -0
  186. package/dist/server/routes/commands.js +17 -0
  187. package/dist/server/routes/config.d.ts +4 -0
  188. package/dist/server/routes/config.js +27 -0
  189. package/dist/server/routes/files.d.ts +4 -0
  190. package/dist/server/routes/files.js +183 -0
  191. package/dist/server/routes/filesystem.d.ts +29 -0
  192. package/dist/server/routes/filesystem.js +108 -0
  193. package/dist/server/routes/health.d.ts +3 -0
  194. package/dist/server/routes/health.js +13 -0
  195. package/dist/server/routes/permissions.d.ts +5 -0
  196. package/dist/server/routes/permissions.js +23 -0
  197. package/dist/server/routes/project.d.ts +4 -0
  198. package/dist/server/routes/project.js +51 -0
  199. package/dist/server/routes/provider.d.ts +16 -0
  200. package/dist/server/routes/provider.js +66 -0
  201. package/dist/server/routes/session.d.ts +4 -0
  202. package/dist/server/routes/session.js +130 -0
  203. package/dist/server/routes/tool.d.ts +4 -0
  204. package/dist/server/routes/tool.js +28 -0
  205. package/dist/server/routes/update.d.ts +14 -0
  206. package/dist/server/routes/update.js +49 -0
  207. package/dist/server/server.d.ts +43 -0
  208. package/dist/server/server.js +197 -0
  209. package/dist/server/types.d.ts +73 -0
  210. package/dist/server/types.js +2 -0
  211. package/dist/server/util/safe-path.d.ts +7 -0
  212. package/dist/server/util/safe-path.js +7 -0
  213. package/dist/session/archive.d.ts +15 -0
  214. package/dist/session/archive.js +129 -0
  215. package/dist/session/branch.d.ts +9 -0
  216. package/dist/session/branch.js +63 -0
  217. package/dist/session/compaction.d.ts +27 -0
  218. package/dist/session/compaction.js +192 -0
  219. package/dist/session/context.d.ts +16 -0
  220. package/dist/session/context.js +173 -0
  221. package/dist/session/index.d.ts +10 -0
  222. package/dist/session/index.js +10 -0
  223. package/dist/session/message.d.ts +22 -0
  224. package/dist/session/message.js +145 -0
  225. package/dist/session/session.d.ts +37 -0
  226. package/dist/session/session.js +144 -0
  227. package/dist/session/snapshot.d.ts +11 -0
  228. package/dist/session/snapshot.js +60 -0
  229. package/dist/session/squash.d.ts +9 -0
  230. package/dist/session/squash.js +88 -0
  231. package/dist/session/token.d.ts +10 -0
  232. package/dist/session/token.js +33 -0
  233. package/dist/session/types.d.ts +122 -0
  234. package/dist/session/types.js +1 -0
  235. package/dist/shared/index.d.ts +2 -0
  236. package/dist/shared/index.js +1 -0
  237. package/dist/shared/types/agent.d.ts +202 -0
  238. package/dist/shared/types/agent.js +1 -0
  239. package/dist/shared/types/base.d.ts +32 -0
  240. package/dist/shared/types/base.js +1 -0
  241. package/dist/shared/types/config.d.ts +104 -0
  242. package/dist/shared/types/config.js +1 -0
  243. package/dist/shared/types/index.d.ts +6 -0
  244. package/dist/shared/types/index.js +1 -0
  245. package/dist/shared/types/llm.d.ts +122 -0
  246. package/dist/shared/types/llm.js +1 -0
  247. package/dist/shared/types/message.d.ts +71 -0
  248. package/dist/shared/types/message.js +1 -0
  249. package/dist/shared/types/tool.d.ts +134 -0
  250. package/dist/shared/types/tool.js +1 -0
  251. package/dist/shared/utils/path.d.ts +15 -0
  252. package/dist/shared/utils/path.js +23 -0
  253. package/dist/shared/utils.d.ts +5 -0
  254. package/dist/shared/utils.js +10 -0
  255. package/dist/tools/builtin/bash.d.ts +12 -0
  256. package/dist/tools/builtin/bash.js +121 -0
  257. package/dist/tools/builtin/dap.d.ts +23 -0
  258. package/dist/tools/builtin/dap.js +231 -0
  259. package/dist/tools/builtin/edit.d.ts +12 -0
  260. package/dist/tools/builtin/edit.js +151 -0
  261. package/dist/tools/builtin/glob.d.ts +11 -0
  262. package/dist/tools/builtin/glob.js +120 -0
  263. package/dist/tools/builtin/grep.d.ts +6 -0
  264. package/dist/tools/builtin/grep.js +141 -0
  265. package/dist/tools/builtin/read.d.ts +6 -0
  266. package/dist/tools/builtin/read.js +73 -0
  267. package/dist/tools/builtin/resolvers.d.ts +12 -0
  268. package/dist/tools/builtin/resolvers.js +79 -0
  269. package/dist/tools/builtin/task.d.ts +9 -0
  270. package/dist/tools/builtin/task.js +100 -0
  271. package/dist/tools/builtin/write.d.ts +6 -0
  272. package/dist/tools/builtin/write.js +36 -0
  273. package/dist/tools/builtin/yield.d.ts +9 -0
  274. package/dist/tools/builtin/yield.js +40 -0
  275. package/dist/tools/executor.d.ts +9 -0
  276. package/dist/tools/executor.js +54 -0
  277. package/dist/tools/hashline/index.d.ts +2 -0
  278. package/dist/tools/hashline/index.js +3 -0
  279. package/dist/tools/hashline/patch.d.ts +48 -0
  280. package/dist/tools/hashline/patch.js +201 -0
  281. package/dist/tools/index.d.ts +30 -0
  282. package/dist/tools/index.js +55 -0
  283. package/dist/tools/permission.d.ts +13 -0
  284. package/dist/tools/permission.js +39 -0
  285. package/dist/tools/registry.d.ts +15 -0
  286. package/dist/tools/registry.js +54 -0
  287. package/dist/tools/resolver.d.ts +15 -0
  288. package/dist/tools/resolver.js +47 -0
  289. package/dist/tools/truncate.d.ts +8 -0
  290. package/dist/tools/truncate.js +50 -0
  291. package/dist/tools/types.d.ts +101 -0
  292. package/dist/tools/types.js +1 -0
  293. package/dist/tools/validate.d.ts +8 -0
  294. package/dist/tools/validate.js +130 -0
  295. package/dist/tools/websearch/fetch.d.ts +11 -0
  296. package/dist/tools/websearch/fetch.js +21 -0
  297. package/dist/tools/websearch/index.d.ts +19 -0
  298. package/dist/tools/websearch/index.js +78 -0
  299. package/dist/tools/websearch/providers/brave.d.ts +5 -0
  300. package/dist/tools/websearch/providers/brave.js +53 -0
  301. package/dist/tools/websearch/providers/duckduckgo.d.ts +3 -0
  302. package/dist/tools/websearch/providers/duckduckgo.js +75 -0
  303. package/dist/tools/websearch/providers/tavily.d.ts +11 -0
  304. package/dist/tools/websearch/providers/tavily.js +89 -0
  305. package/dist/tools/websearch/types.d.ts +51 -0
  306. package/dist/tools/websearch/types.js +18 -0
  307. package/dist/tools/websearch/websearch.d.ts +10 -0
  308. package/dist/tools/websearch/websearch.js +48 -0
  309. package/dist/update/index.d.ts +10 -0
  310. package/dist/update/index.js +6 -0
  311. package/dist/update/ipc.d.ts +15 -0
  312. package/dist/update/ipc.js +41 -0
  313. package/dist/update/scheduler.d.ts +34 -0
  314. package/dist/update/scheduler.js +57 -0
  315. package/dist/update/snapshot.d.ts +39 -0
  316. package/dist/update/snapshot.js +99 -0
  317. package/dist/update/updater.d.ts +48 -0
  318. package/dist/update/updater.js +83 -0
  319. package/dist/update/version.d.ts +18 -0
  320. package/dist/update/version.js +49 -0
  321. package/drizzle/0000_magical_nightshade.sql +55 -0
  322. package/drizzle/0001_chemical_doctor_faustus.sql +13 -0
  323. package/drizzle/0002_amusing_micromacro.sql +12 -0
  324. package/drizzle/0003_regular_purifiers.sql +2 -0
  325. package/drizzle/meta/0000_snapshot.json +448 -0
  326. package/drizzle/meta/0001_snapshot.json +539 -0
  327. package/drizzle/meta/0002_snapshot.json +633 -0
  328. package/drizzle/meta/0003_snapshot.json +645 -0
  329. package/drizzle/meta/_journal.json +34 -0
  330. package/package.json +100 -8
@@ -0,0 +1,28 @@
1
+ import { Hono } from 'hono';
2
+ import { listTools } from '../../tools/registry.js';
3
+ import { apiError } from '../middleware/error.js';
4
+ function createToolRoute(ctx) {
5
+ const app = new Hono();
6
+ // 列出可用工具(不含 execute 函数)
7
+ app.get('/', (c) => {
8
+ const tools = listTools(ctx.toolRegistry, { config: {}, cwd: ctx.cwd });
9
+ const serializable = tools.map((t) => ({
10
+ name: t.name,
11
+ description: t.description,
12
+ parameters: t.parameters,
13
+ permission: t.permission,
14
+ }));
15
+ return c.json(serializable);
16
+ });
17
+ // 确认工具执行权限
18
+ app.post('/confirm', async (c) => {
19
+ const body = await c.req.json();
20
+ const ok = ctx.permissionStore.resolve(body.toolCallId, body.approved);
21
+ if (!ok) {
22
+ return apiError(c, 404, 'NOT_FOUND', 'No pending permission for this tool call');
23
+ }
24
+ return c.json({ confirmed: true });
25
+ });
26
+ return app;
27
+ }
28
+ export { createToolRoute };
@@ -0,0 +1,14 @@
1
+ import { Hono } from 'hono';
2
+ import type { ServerContext } from '../types.js';
3
+ /**
4
+ * GET /api/update — 返回后台调度器缓存的版本检查结果(spec §18.1)。
5
+ * 路由本身不触发 npm registry 请求;调度器周期性 checkNow 写入缓存,
6
+ * 路由读缓存(避免每个前端轮询都打外网)。
7
+ *
8
+ * POST /api/update/apply — 手动触发热更新(spec §18.2):
9
+ * 序列化当前会话 → performHotUpdate(npm install -g + spawn 新实例 + handoffPort)。
10
+ * 旧实例通过 handoff 端点优雅退出,新实例 restore 快照并接管端口。
11
+ * 响应立即返回(不阻塞 spawn),实际接管由 handoff IPC 异步完成。
12
+ */
13
+ declare function createUpdateRoute(ctx: ServerContext): Hono;
14
+ export { createUpdateRoute };
@@ -0,0 +1,49 @@
1
+ import { Hono } from 'hono';
2
+ import { performHotUpdate, serializeSessions } from '../../update/index.js';
3
+ import { apiError } from '../middleware/error.js';
4
+ /**
5
+ * GET /api/update — 返回后台调度器缓存的版本检查结果(spec §18.1)。
6
+ * 路由本身不触发 npm registry 请求;调度器周期性 checkNow 写入缓存,
7
+ * 路由读缓存(避免每个前端轮询都打外网)。
8
+ *
9
+ * POST /api/update/apply — 手动触发热更新(spec §18.2):
10
+ * 序列化当前会话 → performHotUpdate(npm install -g + spawn 新实例 + handoffPort)。
11
+ * 旧实例通过 handoff 端点优雅退出,新实例 restore 快照并接管端口。
12
+ * 响应立即返回(不阻塞 spawn),实际接管由 handoff IPC 异步完成。
13
+ */
14
+ function createUpdateRoute(ctx) {
15
+ const app = new Hono();
16
+ app.get('/', (c) => {
17
+ const cached = ctx.updateScheduler.getLastResult();
18
+ if (cached)
19
+ return c.json(cached);
20
+ // 无缓存(首次启动延迟未到):同步触发一次,避免前端首屏空。
21
+ // 不 await——保持 GET 语义非阻塞;前端下次轮询拿到结果。
22
+ void ctx.updateScheduler.checkNow();
23
+ return c.json({
24
+ hasUpdate: false,
25
+ currentVersion: '0.0.0',
26
+ latestVersion: '0.0.0',
27
+ });
28
+ });
29
+ app.post('/apply', async (c) => {
30
+ const result = await ctx.updateScheduler.checkNow();
31
+ if (!result.hasUpdate) {
32
+ return apiError(c, 409, 'NO_UPDATE', '已是最新版本,无需热更新');
33
+ }
34
+ const snapshot = await serializeSessions(ctx.db, ctx.config);
35
+ const r = await performHotUpdate(snapshot, {
36
+ handoffPort: ctx.handoff?.port,
37
+ });
38
+ if (r._tag === 'success') {
39
+ return c.json({
40
+ ok: true,
41
+ snapshotPath: r.snapshotPath,
42
+ latestVersion: result.latestVersion,
43
+ });
44
+ }
45
+ return apiError(c, 500, 'HOT_UPDATE_FAILED', `${r._tag}: ${'error' in r ? r.error : 'unknown'}`);
46
+ });
47
+ return app;
48
+ }
49
+ export { createUpdateRoute };
@@ -0,0 +1,43 @@
1
+ import type { Hono } from 'hono';
2
+ import type { DB } from '../db/client.js';
3
+ import type { Registry } from '../llm/registry.js';
4
+ import type { Config } from '../shared/types/config.js';
5
+ import { checkForUpdate, createHandoffServer } from '../update/index.js';
6
+ import type { ServerContext } from './types.js';
7
+ type StartServerOptions = {
8
+ port?: number;
9
+ cwd?: string;
10
+ /** 注入已有 DB(测试用)。 */
11
+ db?: DB;
12
+ /** 热更新新实例启动时从此快照文件恢复会话状态。 */
13
+ restoreFrom?: string;
14
+ /** 新实例启动时提供:请求旧实例 handoff 后再绑端口(spec §18.3)。 */
15
+ handoffPort?: number;
16
+ /** 测试注入:跳过 handoff 请求与重试绑端口。 */
17
+ skipHandoff?: boolean;
18
+ /** 测试注入:覆盖 checkForUpdate(默认走真实 npm registry)。 */
19
+ checkForUpdateFn?: typeof checkForUpdate;
20
+ /** 测试注入:覆盖 createHandoffServer。 */
21
+ createHandoffFn?: typeof createHandoffServer;
22
+ };
23
+ type RunningServer = {
24
+ app: Hono;
25
+ port: number;
26
+ close(): Promise<void>;
27
+ };
28
+ type BootstrappedServer = {
29
+ ctx: ServerContext;
30
+ close(): Promise<void>;
31
+ };
32
+ /** 把 config.providers 注册到新建的 LLM registry(修复此前空 registry 的遗漏)。 */
33
+ declare function buildRegistryFromConfig(config: Config): Registry;
34
+ /**
35
+ * config 变更后原地同步 registry(清空旧路由 + 重新注册),
36
+ * 使运行中的 ServerContext 立即生效,无需重启。
37
+ */
38
+ declare function syncRegistryFromConfig(registry: Registry, config: Config): void;
39
+ /** 初始化 DB + 配置 + 注册表,返回 ServerContext + 清理函数(dev 与独立后端共用)。 */
40
+ declare function bootstrapServerContext(opts?: StartServerOptions): Promise<BootstrappedServer>;
41
+ declare function startServer(opts?: StartServerOptions): Promise<RunningServer>;
42
+ export type { BootstrappedServer, RunningServer, StartServerOptions };
43
+ export { bootstrapServerContext, buildRegistryFromConfig, startServer, syncRegistryFromConfig };
@@ -0,0 +1,197 @@
1
+ // src/server/server.ts
2
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
3
+ import { connect as tcpConnect } from 'node:net';
4
+ import { join } from 'node:path';
5
+ import { serve } from '@hono/node-server';
6
+ import { BUILTIN_AGENTS, createAgentRegistry } from '../core/agents/index.js';
7
+ import { loadConfig } from '../core/config.js';
8
+ import { decryptSecret } from '../core/secret.js';
9
+ import { createDB, migrateDB } from '../db/index.js';
10
+ import { createRegistry, registerProvider } from '../llm/registry.js';
11
+ import { initPlugins } from '../plugins/index.js';
12
+ import { createDefaultRegistry, createDefaultURLRegistry } from '../tools/index.js';
13
+ import { checkForUpdate, createHandoffServer, createUpdateScheduler, requestHandoff, restoreSessions, } from '../update/index.js';
14
+ import { createAgentManager } from './agent-manager.js';
15
+ import { createApp } from './app.js';
16
+ import { createPermissionStore } from './permission/store.js';
17
+ /** 把 config.providers 注册到新建的 LLM registry(修复此前空 registry 的遗漏)。 */
18
+ function buildRegistryFromConfig(config) {
19
+ const registry = createRegistry();
20
+ for (const p of config.providers) {
21
+ registerProviderFromConfig(registry, p);
22
+ }
23
+ return registry;
24
+ }
25
+ function registerProviderFromConfig(registry, p) {
26
+ // 兼容 config.json 中以 _tag 标识 provider 的格式(name 缺失时回退到 _tag)
27
+ const name = p.name || p._tag;
28
+ if (!name || !p.baseURL)
29
+ return;
30
+ // baseURL 已含 /v1 时用 /chat/completions,避免 /v1/v1 双重前缀
31
+ const path = p.baseURL.replace(/\/+$/, '').endsWith('/v1') ? '/chat/completions' : undefined;
32
+ registerProvider(registry, {
33
+ name,
34
+ baseURL: p.baseURL,
35
+ apiKey: p.apiKey ? decryptSecret(p.apiKey) : p.apiKey,
36
+ ...(path ? { path } : {}),
37
+ });
38
+ }
39
+ /**
40
+ * config 变更后原地同步 registry(清空旧路由 + 重新注册),
41
+ * 使运行中的 ServerContext 立即生效,无需重启。
42
+ */
43
+ function syncRegistryFromConfig(registry, config) {
44
+ registry.routes.clear();
45
+ registry.roles.clear();
46
+ for (const p of config.providers) {
47
+ registerProviderFromConfig(registry, p);
48
+ }
49
+ }
50
+ /** 解析 PGLite 持久化数据目录:优先 C0DE_DB_DIR,否则 <cwd>/.c0de/pglite。 */
51
+ function resolveDbDir(cwd) {
52
+ const envDir = process.env.C0DE_DB_DIR;
53
+ if (envDir && envDir.trim() !== '')
54
+ return envDir;
55
+ return join(cwd, '.c0de', 'pglite');
56
+ }
57
+ /** 初始化 DB + 配置 + 注册表,返回 ServerContext + 清理函数(dev 与独立后端共用)。 */
58
+ async function bootstrapServerContext(opts = {}) {
59
+ const cwd = opts.cwd ?? process.cwd();
60
+ const ownsDb = !opts.db;
61
+ // 持久化 PGLite 数据:默认 <cwd>/.c0de/pglite(与 .c0de/cache、.c0de/config.json 同约定),
62
+ // 可用 C0DE_DB_DIR 覆盖。此前默认 in-memory,进程重启即丢全部会话/消息/调用详情。
63
+ // 测试注入 opts.db 时跳过(保持 in-memory 隔离)。
64
+ let db;
65
+ if (opts.db) {
66
+ db = opts.db;
67
+ }
68
+ else {
69
+ const dataDir = resolveDbDir(cwd);
70
+ if (!existsSync(dataDir))
71
+ mkdirSync(dataDir, { recursive: true });
72
+ db = await createDB({ driver: 'pglite', dataDir });
73
+ }
74
+ // migrateDB 失败时必须 close db,否则 PGLite WASM 实例泄漏并锁住 dataDir,
75
+ // 导致后续重试全部 abort(RuntimeError: Aborted())。
76
+ try {
77
+ await migrateDB(db);
78
+ }
79
+ catch (err) {
80
+ if (ownsDb)
81
+ await db.close().catch(() => { });
82
+ throw err;
83
+ }
84
+ if (opts.restoreFrom) {
85
+ const snapshot = JSON.parse(readFileSync(opts.restoreFrom, 'utf8'));
86
+ await restoreSessions(db, snapshot);
87
+ }
88
+ const config = await loadConfig(cwd);
89
+ const toolRegistry = createDefaultRegistry(config);
90
+ const llmRegistry = buildRegistryFromConfig(config);
91
+ const urlRegistry = createDefaultURLRegistry();
92
+ const { pluginRegistry, hookRunner } = await initPlugins({
93
+ cwd,
94
+ config,
95
+ toolRegistry,
96
+ llmRegistry,
97
+ });
98
+ const ctx = {
99
+ db,
100
+ config,
101
+ toolRegistry,
102
+ llmRegistry,
103
+ urlRegistry,
104
+ hookRunner,
105
+ pluginRegistry,
106
+ agentManager: createAgentManager(),
107
+ permissionStore: createPermissionStore(),
108
+ permissionMode: config.permission.defaultMode,
109
+ // Agent 注册表:内置 4 个默认 agent;项目/用户自定义 agent 可在启动后补充加载。
110
+ agentRegistry: (() => {
111
+ const reg = createAgentRegistry();
112
+ for (const def of BUILTIN_AGENTS)
113
+ reg.register(def);
114
+ return reg;
115
+ })(),
116
+ // spec §18.1 后台版本检查调度器;config.update.enabled 控制是否启动。
117
+ updateScheduler: createUpdateScheduler({
118
+ checkFn: opts.checkForUpdateFn ?? checkForUpdate,
119
+ intervalMs: config.update.intervalMs,
120
+ initialDelayMs: config.update.initialDelayMs,
121
+ }),
122
+ cwd,
123
+ };
124
+ // spec §18.3 handoff server:旧实例监听随机端口,收到 POST /handoff 时
125
+ // 序列化当前会话状态 + 优雅关闭,让新实例接管。config.update.enabled=false
126
+ // 或测试 skipHandoff 时跳过(减少后台资源占用)。
127
+ let handoffServer;
128
+ if (config.update.enabled && !opts.skipHandoff) {
129
+ const createHandoff = opts.createHandoffFn ?? createHandoffServer;
130
+ handoffServer = await createHandoff(async () => {
131
+ // 序列化在热更新主链路(performHotUpdate)已完成;handoff 回调只需释放资源。
132
+ // 这里提前停 scheduler 以避免 close 期间还在跑检查。
133
+ ctx.updateScheduler.stop();
134
+ });
135
+ ctx.handoff = { port: handoffServer.port, server: handoffServer };
136
+ }
137
+ return {
138
+ ctx,
139
+ close: async () => {
140
+ ctx.updateScheduler.stop();
141
+ if (handoffServer)
142
+ await handoffServer.close();
143
+ if (ownsDb)
144
+ await db.close();
145
+ },
146
+ };
147
+ }
148
+ /**
149
+ * 请求旧实例 handoff 后轮询端口释放(spec §18.3)。旧实例收到 /handoff 后
150
+ * 优雅退出释放端口;新实例轮询 TCP 连接直到被拒(端口已释放)或超时报错。
151
+ */
152
+ async function requestHandoffWithRetry(port, maxAttempts = 30, delayMs = 100) {
153
+ try {
154
+ await requestHandoff(port);
155
+ }
156
+ catch {
157
+ // 旧实例未启 handoff 端点(update 未启用);直接尝试绑端口。
158
+ return;
159
+ }
160
+ for (let i = 0; i < maxAttempts; i++) {
161
+ const released = await new Promise((resolve) => {
162
+ const sock = tcpConnect({ host: '127.0.0.1', port });
163
+ sock.once('connect', () => {
164
+ sock.destroy();
165
+ resolve(false); // 连上 → 旧实例还在
166
+ });
167
+ sock.once('error', () => resolve(true)); // 连接拒绝 → 端口已释放
168
+ });
169
+ if (released)
170
+ return;
171
+ await new Promise((r) => setTimeout(r, delayMs));
172
+ }
173
+ throw new Error(`handoff: old instance on port ${port} did not release after ${maxAttempts} attempts`);
174
+ }
175
+ async function startServer(opts = {}) {
176
+ const port = opts.port ?? 3000;
177
+ // spec §18.3:新实例从 --handoff-port 拿到旧实例端口,请求优雅退出后绑端口。
178
+ if (opts.handoffPort && !opts.skipHandoff) {
179
+ await requestHandoffWithRetry(opts.handoffPort);
180
+ }
181
+ const { ctx, close: closeCtx } = await bootstrapServerContext(opts);
182
+ const app = createApp(ctx);
183
+ // config.update.enabled 时启动后台调度器。
184
+ if (ctx.config.update.enabled)
185
+ ctx.updateScheduler.start();
186
+ const server = serve({ fetch: app.fetch, port });
187
+ let closed = false;
188
+ const close = async () => {
189
+ if (closed)
190
+ return;
191
+ closed = true;
192
+ server.close();
193
+ await closeCtx();
194
+ };
195
+ return { app, port, close };
196
+ }
197
+ export { bootstrapServerContext, buildRegistryFromConfig, startServer, syncRegistryFromConfig };
@@ -0,0 +1,73 @@
1
+ import type { AgentRegistry } from '../core/agents/types.js';
2
+ import type { Config } from '../core/config.js';
3
+ import type { DB } from '../db/client.js';
4
+ import type { chatStream as chatStreamFn } from '../llm/provider.js';
5
+ import type { Registry } from '../llm/registry.js';
6
+ import type { HookRunner, PluginRegistry } from '../plugins/types.js';
7
+ import type { Config as SharedConfig } from '../shared/types/config.js';
8
+ import type { URLRegistry } from '../shared/types/tool.js';
9
+ import type { ToolRegistry } from '../tools/types.js';
10
+ import type { HandoffServer, UpdateScheduler } from '../update/index.js';
11
+ import type { AgentManager } from './agent-manager.js';
12
+ import type { PermissionStore } from './permission/store.js';
13
+ /** 持有所有服务依赖的不可变上下文(config 字段可变用于 PATCH 更新)。 */
14
+ type ServerContext = {
15
+ db: DB;
16
+ config: Config;
17
+ toolRegistry: ToolRegistry;
18
+ llmRegistry: Registry;
19
+ /** 内置 URL 解析器注册表(file://, skill://);注入 agent loop 的 ToolContext。 */
20
+ urlRegistry: URLRegistry;
21
+ /** 插件 hook runner;接入 agent loop 的 tool/provider/message hook。 */
22
+ hookRunner: HookRunner;
23
+ /** 插件注册表(builtin + 已加载外部插件);供插件管理 API 查询。 */
24
+ pluginRegistry: PluginRegistry;
25
+ agentManager: AgentManager;
26
+ /** 全局权限确认 store(单例),独立于 agent run 生命周期。 */
27
+ permissionStore: PermissionStore;
28
+ /** 全局授权模式(运行时可临时切换):'default' 逐个确认;'auto' 自动放行 ask 工具(YOLO)。启动时取 config.permission.defaultMode,PUT /api/permissions 临时切换不回写 config。 */
29
+ permissionMode: 'default' | 'auto';
30
+ /** Agent 类型注册表(spec: multi-agent-design)。注入 agent loop 的 runSubAgent。 */
31
+ agentRegistry: AgentRegistry;
32
+ /** 后台版本检查调度器(spec §18.1);/api/update 读取其缓存结果。 */
33
+ updateScheduler: UpdateScheduler;
34
+ /** Handoff HTTP 端点(spec §18.3);热更新时新实例 POST /handoff 触发优雅退出。 undefined 表示未启用。 */
35
+ handoff?: {
36
+ port: number;
37
+ server: HandoffServer;
38
+ };
39
+ cwd: string;
40
+ /** 测试注入:覆盖 LLM chat stream。生产环境为 undefined。 */
41
+ chatStream?: typeof chatStreamFn;
42
+ };
43
+ /** POST /api/chat 请求体。 */
44
+ type ChatRequest = {
45
+ sessionId: string;
46
+ message: string;
47
+ provider?: string;
48
+ model?: string;
49
+ tools?: string[];
50
+ };
51
+ /** Agent 控制请求(abort/pause/resume)。 */
52
+ type ControlRequest = {
53
+ sessionId: string;
54
+ };
55
+ /** POST /api/chat/steer 请求体。 */
56
+ type SteerRequest = {
57
+ sessionId: string;
58
+ message: string;
59
+ };
60
+ /** POST /api/tools/confirm 请求体。 */
61
+ type ConfirmRequest = {
62
+ toolCallId: string;
63
+ approved: boolean;
64
+ };
65
+ /** 统一错误响应体。 */
66
+ type APIErrorBody = {
67
+ error: {
68
+ code: string;
69
+ message: string;
70
+ details?: Record<string, unknown>;
71
+ };
72
+ };
73
+ export type { APIErrorBody, ChatRequest, Config, ConfirmRequest, ControlRequest, HandoffServer, ServerContext, SharedConfig, SteerRequest, };
@@ -0,0 +1,2 @@
1
+ // src/server/types.ts
2
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * safeResolve 已下沉到共享层(src/shared/utils/path.ts),供 tools / server 等
3
+ * 所有层统一引用,避免 tools 层反向依赖 server 层。
4
+ *
5
+ * 本文件仅做 re-export,保持 server 层调用方(chat/files 路由)的对外 API 不变。
6
+ */
7
+ export { safeResolve } from '../../shared/utils/path.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * safeResolve 已下沉到共享层(src/shared/utils/path.ts),供 tools / server 等
3
+ * 所有层统一引用,避免 tools 层反向依赖 server 层。
4
+ *
5
+ * 本文件仅做 re-export,保持 server 层调用方(chat/files 路由)的对外 API 不变。
6
+ */
7
+ export { safeResolve } from '../../shared/utils/path.js';
@@ -0,0 +1,15 @@
1
+ import type { DB } from '../db/client.js';
2
+ import type { ArchiveRef, CompactionArchive, SessionEntry } from './types.js';
3
+ /** Archive original entries before compaction/squash. Returns the archive id. */
4
+ declare function archiveOriginalEntries(handle: DB, sessionId: string, entries: SessionEntry[], archiveType: 'compaction' | 'squash', summary: string, compactionId: string): Promise<string>;
5
+ /** Get an archive by id. Returns null for invalid or non-existent ids. */
6
+ declare function getArchive(handle: DB, id: string): Promise<CompactionArchive | null>;
7
+ /** Get the original entries stored in an archive. */
8
+ declare function getArchiveOriginalEntries(handle: DB, archiveId: string): Promise<SessionEntry[]>;
9
+ /** Search archives by keyword (case-insensitive substring on searchable text). */
10
+ declare function searchArchives(handle: DB, sessionId: string, query: string): Promise<CompactionArchive[]>;
11
+ /** Parse a `@[archive:<id>]` or `@[squash:<n>]` reference from text. Returns null if none. */
12
+ declare function parseArchiveReference(text: string): ArchiveRef | null;
13
+ /** Resolve an archive reference to displayable text. */
14
+ declare function resolveArchiveReference(handle: DB, sessionId: string, ref: ArchiveRef): Promise<string | null>;
15
+ export { archiveOriginalEntries, getArchive, getArchiveOriginalEntries, parseArchiveReference, resolveArchiveReference, searchArchives, };
@@ -0,0 +1,129 @@
1
+ import { and, desc, eq, ilike } from 'drizzle-orm';
2
+ import { compactionArchives } from '../db/schema.js';
3
+ import { generateId } from '../shared/index.js';
4
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
+ function toEpochMs(value) {
6
+ if (value instanceof Date)
7
+ return value.getTime();
8
+ if (typeof value === 'number')
9
+ return value;
10
+ return new Date(value).getTime();
11
+ }
12
+ function rowToArchive(row) {
13
+ return {
14
+ id: row.id,
15
+ sessionId: row.sessionId,
16
+ compactionId: row.compactionId,
17
+ archiveType: row.archiveType,
18
+ originalEntries: row.originalEntries,
19
+ summary: row.summary,
20
+ tokenCount: row.tokenCount ?? 0,
21
+ searchableText: row.searchableText ?? '',
22
+ createdAt: toEpochMs(row.createdAt),
23
+ };
24
+ }
25
+ /** Convert an entry to searchable plain text. */
26
+ function entryToSearchableText(entry) {
27
+ if ('_tag' in entry) {
28
+ switch (entry._tag) {
29
+ case 'compaction':
30
+ case 'squash':
31
+ case 'branch_summary':
32
+ return entry.summary;
33
+ case 'steering':
34
+ return entry.content;
35
+ }
36
+ }
37
+ return entry.content
38
+ .map((part) => {
39
+ switch (part._tag) {
40
+ case 'text':
41
+ case 'thinking':
42
+ case 'steering':
43
+ return part.text;
44
+ case 'tool_call':
45
+ return `${part.tool}: ${JSON.stringify(part.input)}`;
46
+ case 'tool_result':
47
+ return JSON.stringify(part.output);
48
+ default:
49
+ return '';
50
+ }
51
+ })
52
+ .join(' ');
53
+ }
54
+ /** Archive original entries before compaction/squash. Returns the archive id. */
55
+ async function archiveOriginalEntries(handle, sessionId, entries, archiveType, summary, compactionId) {
56
+ const id = generateId();
57
+ const searchableText = entries.map(entryToSearchableText).join('\n');
58
+ const tokenCount = entries.reduce((sum, e) => sum + ('tokenCount' in e ? e.tokenCount : 0), 0);
59
+ await handle.db.insert(compactionArchives).values({
60
+ id,
61
+ sessionId,
62
+ compactionId,
63
+ archiveType,
64
+ originalEntries: entries,
65
+ summary,
66
+ tokenCount,
67
+ searchableText,
68
+ });
69
+ return id;
70
+ }
71
+ /** Get an archive by id. Returns null for invalid or non-existent ids. */
72
+ async function getArchive(handle, id) {
73
+ // PGLite rejects non-UUID strings at query time; validate first.
74
+ if (!UUID_RE.test(id))
75
+ return null;
76
+ const [row] = await handle.db
77
+ .select()
78
+ .from(compactionArchives)
79
+ .where(eq(compactionArchives.id, id));
80
+ return row ? rowToArchive(row) : null;
81
+ }
82
+ /** Get the original entries stored in an archive. */
83
+ async function getArchiveOriginalEntries(handle, archiveId) {
84
+ const archive = await getArchive(handle, archiveId);
85
+ return archive?.originalEntries ?? [];
86
+ }
87
+ /** Escape LIKE/ILIKE metacharacters so the query is treated as a literal substring. */
88
+ function escapeLikePattern(text) {
89
+ return text.replace(/[%_\\]/g, '\\$&');
90
+ }
91
+ /** Search archives by keyword (case-insensitive substring on searchable text). */
92
+ async function searchArchives(handle, sessionId, query) {
93
+ const pattern = `%${escapeLikePattern(query)}%`;
94
+ const rows = await handle.db
95
+ .select()
96
+ .from(compactionArchives)
97
+ .where(and(eq(compactionArchives.sessionId, sessionId), ilike(compactionArchives.searchableText, pattern)))
98
+ .orderBy(desc(compactionArchives.createdAt));
99
+ return rows.map(rowToArchive);
100
+ }
101
+ /** Parse a `@[archive:<id>]` or `@[squash:<n>]` reference from text. Returns null if none. */
102
+ function parseArchiveReference(text) {
103
+ const archiveId = text.match(/@\[archive:([^\]]+)\]/)?.[1];
104
+ if (archiveId)
105
+ return { type: 'archive', id: archiveId };
106
+ const squashId = text.match(/@\[squash:(\d+|last)\]/)?.[1];
107
+ if (squashId)
108
+ return { type: 'squash', id: squashId };
109
+ return null;
110
+ }
111
+ /** Resolve an archive reference to displayable text. */
112
+ async function resolveArchiveReference(handle, sessionId, ref) {
113
+ if (ref.type === 'archive') {
114
+ const archive = await getArchive(handle, ref.id);
115
+ return archive ? `[Referenced Archive]\n${archive.summary}` : null;
116
+ }
117
+ // squash reference: nth most recent squash archive
118
+ const archives = await handle.db
119
+ .select()
120
+ .from(compactionArchives)
121
+ .where(and(eq(compactionArchives.sessionId, sessionId), eq(compactionArchives.archiveType, 'squash')))
122
+ .orderBy(desc(compactionArchives.createdAt));
123
+ const index = ref.id === 'last' ? 0 : Number.parseInt(ref.id, 10) - 1;
124
+ const row = archives[index];
125
+ if (!row)
126
+ return null;
127
+ return `[Referenced Squash #${index + 1}]\n${row.summary}`;
128
+ }
129
+ export { archiveOriginalEntries, getArchive, getArchiveOriginalEntries, parseArchiveReference, resolveArchiveReference, searchArchives, };
@@ -0,0 +1,9 @@
1
+ import type { DB } from '../db/client.js';
2
+ import type { Session, SessionTreeNode } from './types.js';
3
+ /** Fork a session at a message index — copies messages 0..index into a new child session. */
4
+ declare function forkSession(handle: DB, sessionId: string, messageIndex: number): Promise<Session>;
5
+ /** Get direct child sessions (branches) of a session. */
6
+ declare function getBranches(handle: DB, sessionId: string): Promise<Session[]>;
7
+ /** Build a full session tree from root sessions down. */
8
+ declare function getTree(handle: DB): Promise<SessionTreeNode[]>;
9
+ export { forkSession, getBranches, getTree };
@@ -0,0 +1,63 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { sessions } from '../db/schema.js';
3
+ import { generateId } from '../shared/index.js';
4
+ import { getMessages, insertEntry } from './message.js';
5
+ import { createSession, getSession, rowToSession } from './session.js';
6
+ /** Fork a session at a message index — copies messages 0..index into a new child session. */
7
+ async function forkSession(handle, sessionId, messageIndex) {
8
+ const source = await getSession(handle, sessionId);
9
+ if (!source)
10
+ throw new Error(`Session not found: ${sessionId}`);
11
+ const messages = await getMessages(handle, sessionId);
12
+ const toCopy = messages.slice(0, messageIndex + 1);
13
+ const forked = await createSession(handle, `Branch of ${source.title}`, source.projectId ?? undefined);
14
+ await handle.db
15
+ .update(sessions)
16
+ .set({ parentId: sessionId, branchPoint: messageIndex })
17
+ .where(eq(sessions.id, forked.id));
18
+ for (const msg of toCopy) {
19
+ await insertEntry(handle, {
20
+ id: generateId(),
21
+ sessionId: forked.id,
22
+ tag: 'message',
23
+ role: msg.role,
24
+ content: msg.content,
25
+ tokenCount: msg.tokenCount,
26
+ });
27
+ }
28
+ await insertEntry(handle, {
29
+ id: generateId(),
30
+ sessionId: forked.id,
31
+ tag: 'branch_summary',
32
+ content: {
33
+ summary: `Branched from session ${sessionId} at message ${messageIndex}`,
34
+ sourceSessionId: sessionId,
35
+ },
36
+ });
37
+ const updated = await getSession(handle, forked.id);
38
+ if (!updated)
39
+ throw new Error('Forked session not found after creation');
40
+ return updated;
41
+ }
42
+ /** Get direct child sessions (branches) of a session. */
43
+ async function getBranches(handle, sessionId) {
44
+ const rows = await handle.db.select().from(sessions).where(eq(sessions.parentId, sessionId));
45
+ return rows.map(rowToSession);
46
+ }
47
+ /** Build a full session tree from root sessions down. */
48
+ async function getTree(handle) {
49
+ const rows = await handle.db.select().from(sessions);
50
+ const byParent = new Map();
51
+ for (const row of rows) {
52
+ const session = rowToSession(row);
53
+ const list = byParent.get(session.parentId) ?? [];
54
+ list.push(session);
55
+ byParent.set(session.parentId, list);
56
+ }
57
+ const build = (parentId) => (byParent.get(parentId) ?? []).map((session) => ({
58
+ session,
59
+ children: build(session.id),
60
+ }));
61
+ return build(null);
62
+ }
63
+ export { forkSession, getBranches, getTree };