hive-am 1.1.0 → 1.2.1

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 (262) hide show
  1. package/CHANGELOG.md +100 -1
  2. package/README.md +9 -1
  3. package/package.json +13 -2
  4. package/server/dist/api.js +42 -762
  5. package/server/dist/connections/channel-skill.js +13 -1
  6. package/server/dist/connections/files.js +45 -1
  7. package/server/dist/connections/manager.js +41 -3
  8. package/server/dist/connections/outbound.js +2 -1
  9. package/server/dist/connections/prompt.js +4 -0
  10. package/server/dist/connections/router.js +3 -2
  11. package/server/dist/connections/store.js +4 -43
  12. package/server/dist/connections/telegram.js +43 -3
  13. package/server/dist/cron.js +2 -1
  14. package/server/dist/db/json.js +16 -0
  15. package/server/dist/db/migrations.js +199 -0
  16. package/server/dist/db.js +98 -196
  17. package/server/dist/errors.js +11 -0
  18. package/server/dist/git/browse.js +99 -0
  19. package/server/dist/git/ops.js +136 -19
  20. package/server/dist/git/repo.js +2 -1
  21. package/server/dist/history/claude.js +21 -21
  22. package/server/dist/history/index.js +17 -9
  23. package/server/dist/history/kiro.js +17 -12
  24. package/server/dist/history/lines.js +12 -0
  25. package/server/dist/history/opencode.js +18 -1
  26. package/server/dist/http/body.js +46 -0
  27. package/server/dist/http/errors.js +21 -0
  28. package/server/dist/http/guards.js +44 -0
  29. package/server/dist/http/router.js +24 -0
  30. package/server/dist/http/routes/agents.js +124 -0
  31. package/server/dist/http/routes/channel.js +16 -0
  32. package/server/dist/http/routes/colonies.js +9 -0
  33. package/server/dist/http/routes/connections.js +36 -0
  34. package/server/dist/http/routes/fs.js +22 -0
  35. package/server/dist/http/routes/git.js +90 -0
  36. package/server/dist/http/routes/meta.js +11 -0
  37. package/server/dist/http/routes/notebook.js +34 -0
  38. package/server/dist/http/routes/orchestration.js +32 -0
  39. package/server/dist/http/routes/resources.js +68 -0
  40. package/server/dist/http/routes/schedules.js +40 -0
  41. package/server/dist/http/routes/skills.js +47 -0
  42. package/server/dist/http/routes/terminals.js +18 -0
  43. package/server/dist/http/routes/types.js +36 -0
  44. package/server/dist/http/validate.js +65 -0
  45. package/server/dist/index.js +67 -5
  46. package/server/dist/instructions.js +8 -2
  47. package/server/dist/objects/agent.js +49 -0
  48. package/server/dist/objects/database.js +451 -0
  49. package/server/dist/objects/db-history.js +79 -0
  50. package/server/dist/objects/docker-info.js +75 -0
  51. package/server/dist/objects/docker.js +285 -0
  52. package/server/dist/objects/driver-props.js +196 -0
  53. package/server/dist/objects/exec.js +16 -0
  54. package/server/dist/objects/http-history.js +44 -0
  55. package/server/dist/objects/http-script.js +644 -0
  56. package/server/dist/objects/http.js +735 -0
  57. package/server/dist/objects/index.js +445 -0
  58. package/server/dist/objects/model.js +250 -0
  59. package/server/dist/objects/runner.js +211 -0
  60. package/server/dist/objects/stats.js +106 -0
  61. package/server/dist/objects/store.js +115 -0
  62. package/server/dist/objects/supervisor.js +111 -0
  63. package/server/dist/objects/templates.js +67 -0
  64. package/server/dist/providers/claude.js +3 -13
  65. package/server/dist/providers/index.js +7 -1
  66. package/server/dist/providers/kiro.js +2 -5
  67. package/server/dist/providers/opencode-server.js +45 -0
  68. package/server/dist/providers/opencode.js +52 -9
  69. package/server/dist/providers/preamble.js +2 -0
  70. package/server/dist/providers/spawn.js +31 -3
  71. package/server/dist/runtime.js +8 -6
  72. package/server/dist/schedules.js +2 -21
  73. package/server/dist/seed.js +15 -11
  74. package/server/dist/services/agents.js +90 -0
  75. package/server/dist/services/colonies.js +38 -0
  76. package/server/dist/services/connections.js +76 -0
  77. package/server/dist/services/notebook.js +22 -0
  78. package/server/dist/services/providers.js +27 -0
  79. package/server/dist/services/schedules.js +18 -0
  80. package/server/dist/sessions-summary.js +51 -0
  81. package/server/dist/skills/defaults.js +37 -0
  82. package/server/dist/skills/notebook.js +3 -8
  83. package/server/dist/terminals.js +185 -0
  84. package/server/dist/wake.js +13 -16
  85. package/server/dist/watch.js +2 -8
  86. package/server/mcp/dispatch.mjs +70 -1
  87. package/web/.next/BUILD_ID +1 -1
  88. package/web/.next/app-build-manifest.json +146 -94
  89. package/web/.next/app-path-routes-manifest.json +6 -4
  90. package/web/.next/build-manifest.json +14 -14
  91. package/web/.next/prerender-manifest.json +67 -43
  92. package/web/.next/react-loadable-manifest.json +26 -1
  93. package/web/.next/required-server-files.json +13 -1
  94. package/web/.next/routes-manifest.json +26 -0
  95. package/web/.next/server/app/_not-found/page.js +2 -2
  96. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  97. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  98. package/web/.next/server/app/_not-found.html +1 -1
  99. package/web/.next/server/app/_not-found.rsc +12 -11
  100. package/web/.next/server/app/agents/[id]/page.js +4 -4
  101. package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
  102. package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
  103. package/web/.next/server/app/agents/page.js +2 -2
  104. package/web/.next/server/app/agents/page.js.nft.json +1 -1
  105. package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  106. package/web/.next/server/app/agents.html +1 -1
  107. package/web/.next/server/app/agents.rsc +14 -13
  108. package/web/.next/server/app/connections/page.js +2 -2
  109. package/web/.next/server/app/connections/page.js.nft.json +1 -1
  110. package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  111. package/web/.next/server/app/connections.html +1 -1
  112. package/web/.next/server/app/connections.rsc +14 -13
  113. package/web/.next/server/app/index.html +1 -1
  114. package/web/.next/server/app/index.rsc +14 -13
  115. package/web/.next/server/app/page.js +2 -2
  116. package/web/.next/server/app/page.js.nft.json +1 -1
  117. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  118. package/web/.next/server/app/relations/page.js +2 -2
  119. package/web/.next/server/app/relations/page.js.nft.json +1 -1
  120. package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
  121. package/web/.next/server/app/relations.html +1 -1
  122. package/web/.next/server/app/relations.rsc +14 -13
  123. package/web/.next/server/app/resources/[id]/page.js +2 -0
  124. package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -0
  125. package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -0
  126. package/web/.next/server/app/resources/page.js +2 -0
  127. package/web/.next/server/app/resources/page.js.nft.json +1 -0
  128. package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -0
  129. package/web/.next/server/app/resources.html +1 -0
  130. package/web/.next/server/app/resources.meta +15 -0
  131. package/web/.next/server/app/resources.rsc +25 -0
  132. package/web/.next/server/app/schedules/page.js +2 -2
  133. package/web/.next/server/app/schedules/page.js.nft.json +1 -1
  134. package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
  135. package/web/.next/server/app/schedules.html +1 -1
  136. package/web/.next/server/app/schedules.rsc +14 -13
  137. package/web/.next/server/app/sessions/page.js +2 -2
  138. package/web/.next/server/app/sessions/page.js.nft.json +1 -1
  139. package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  140. package/web/.next/server/app/sessions.html +1 -1
  141. package/web/.next/server/app/sessions.rsc +14 -13
  142. package/web/.next/server/app/skills/page.js +2 -2
  143. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  144. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  145. package/web/.next/server/app/skills.html +1 -1
  146. package/web/.next/server/app/skills.rsc +14 -13
  147. package/web/.next/server/app/types/page.js +2 -2
  148. package/web/.next/server/app/types/page.js.nft.json +1 -1
  149. package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
  150. package/web/.next/server/app/types.html +1 -1
  151. package/web/.next/server/app/types.rsc +14 -13
  152. package/web/.next/server/app-paths-manifest.json +6 -4
  153. package/web/.next/server/chunks/143.js +1 -1
  154. package/web/.next/server/chunks/274.js +1 -0
  155. package/web/.next/server/chunks/306.js +1 -0
  156. package/web/.next/server/chunks/423.js +1 -0
  157. package/web/.next/server/chunks/505.js +6 -0
  158. package/web/.next/server/chunks/555.js +1 -0
  159. package/web/.next/server/chunks/612.js +1 -0
  160. package/web/.next/server/chunks/638.js +1 -0
  161. package/web/.next/server/chunks/692.js +18 -0
  162. package/web/.next/server/chunks/716.js +2 -0
  163. package/web/.next/server/chunks/790.js +11 -0
  164. package/web/.next/server/chunks/802.js +1 -0
  165. package/web/.next/server/chunks/890.js +1 -1
  166. package/web/.next/server/chunks/919.js +1 -0
  167. package/web/.next/server/chunks/950.js +6 -0
  168. package/web/.next/server/middleware-build-manifest.js +1 -1
  169. package/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  170. package/web/.next/server/next-font-manifest.js +1 -1
  171. package/web/.next/server/next-font-manifest.json +1 -1
  172. package/web/.next/server/pages/404.html +1 -1
  173. package/web/.next/server/pages/500.html +1 -1
  174. package/web/.next/server/server-reference-manifest.json +1 -1
  175. package/web/.next/static/Bz7onClw1BajMUCwqpU8o/_buildManifest.js +1 -0
  176. package/web/.next/static/chunks/0937d497-114a2a3de515565e.js +1 -0
  177. package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +1 -0
  178. package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +1 -0
  179. package/web/.next/static/chunks/2b9ff8ee-969e65cc354b2150.js +1 -0
  180. package/web/.next/static/chunks/30-9adcbe92b15a6815.js +1 -0
  181. package/web/.next/static/chunks/316-26da9cc233ba7557.js +1 -0
  182. package/web/.next/static/chunks/350-56599dc3bf06066d.js +1 -0
  183. package/web/.next/static/chunks/486-13e08542f3a1cbac.js +1 -0
  184. package/web/.next/static/chunks/491-6b4e6201f5110dff.js +1 -0
  185. package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +1 -0
  186. package/web/.next/static/chunks/545.bfbd92b7632c09de.js +1 -0
  187. package/web/.next/static/chunks/{805-d946ebedb7e85f73.js → 562-7c2e33d838e869bf.js} +1 -1
  188. package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +1 -0
  189. package/web/.next/static/chunks/586-f7017d585a84738d.js +1 -0
  190. package/web/.next/static/chunks/657.b4c39e676c3e20dc.js +1 -0
  191. package/web/.next/static/chunks/701.6abcaf5e60463eef.js +6 -0
  192. package/web/.next/static/chunks/705-c7e75806f77b880c.js +1 -0
  193. package/web/.next/static/chunks/706-fa2be0f10ffebe6a.js +1 -0
  194. package/web/.next/static/chunks/837-33c7480524f48577.js +1 -0
  195. package/web/.next/static/chunks/839-4aefe6e71be6e1ef.js +1 -0
  196. package/web/.next/static/chunks/844-fe55292e0701f436.js +1 -0
  197. package/web/.next/static/chunks/89-b194fcffe5120c44.js +1 -0
  198. package/web/.next/static/chunks/97.594543c2196de524.js +1 -0
  199. package/web/.next/static/chunks/app/_not-found/page-238a7b80bda6b4ae.js +1 -0
  200. package/web/.next/static/chunks/app/agents/[id]/page-3faf40800981a736.js +1 -0
  201. package/web/.next/static/chunks/app/agents/page-f4c4c80f955f3967.js +1 -0
  202. package/web/.next/static/chunks/app/connections/page-0936beaf02d863c6.js +1 -0
  203. package/web/.next/static/chunks/app/layout-99620746d028dda8.js +1 -0
  204. package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +1 -0
  205. package/web/.next/static/chunks/app/relations/page-fba25393c8688714.js +1 -0
  206. package/web/.next/static/chunks/app/resources/[id]/page-d110d89bb8b5fca7.js +1 -0
  207. package/web/.next/static/chunks/app/resources/page-bbd0b450d3f3131d.js +1 -0
  208. package/web/.next/static/chunks/app/schedules/page-a1124397fc760413.js +1 -0
  209. package/web/.next/static/chunks/app/sessions/page-0e322811b0cde724.js +1 -0
  210. package/web/.next/static/chunks/app/skills/page-22ea0ef89d9f7a13.js +1 -0
  211. package/web/.next/static/chunks/app/types/page-18c4e1db2724d41d.js +1 -0
  212. package/web/.next/static/chunks/fcafe1ac.366e4950d3b0d4b7.js +18 -0
  213. package/web/.next/static/chunks/framework-bbac72adbbe2d328.js +1 -0
  214. package/web/.next/static/chunks/main-app-6086068e5fb5eacd.js +1 -0
  215. package/web/.next/static/chunks/main-c15f3fee674e0303.js +1 -0
  216. package/web/.next/static/chunks/pages/_app-d1670a2c6ce392fc.js +1 -0
  217. package/web/.next/static/chunks/pages/_error-95aac014d202fa7a.js +1 -0
  218. package/web/.next/static/chunks/webpack-ef515e41a83a16ac.js +1 -0
  219. package/web/.next/static/css/945795da867f5250.css +1 -0
  220. package/web/.next/static/css/b7a03c62468f4263.css +1 -0
  221. package/web/.next/trace +29 -23
  222. package/web/.next/types/app/resources/[id]/page.ts +84 -0
  223. package/web/.next/types/app/resources/page.ts +84 -0
  224. package/web/.next/types/routes.d.ts +6 -2
  225. package/web/.next/types/validator.ts +18 -0
  226. package/web/next.config.mjs +4 -0
  227. package/web/.next/server/chunks/247.js +0 -1
  228. package/web/.next/server/chunks/484.js +0 -11
  229. package/web/.next/server/chunks/851.js +0 -1
  230. package/web/.next/server/chunks/932.js +0 -2
  231. package/web/.next/static/_z2hBOHhwiR5ytdI_8alh/_buildManifest.js +0 -1
  232. package/web/.next/static/chunks/0937d497-44abb0f9355ace92.js +0 -1
  233. package/web/.next/static/chunks/2b9ff8ee-c3b61ac1e608c57d.js +0 -1
  234. package/web/.next/static/chunks/463-6ae35cdefdc59169.js +0 -1
  235. package/web/.next/static/chunks/493-50d2adf9fd5ec680.js +0 -1
  236. package/web/.next/static/chunks/517-eb26c72f6059aa26.js +0 -1
  237. package/web/.next/static/chunks/589-8bf536f331de4269.js +0 -1
  238. package/web/.next/static/chunks/657.f1e03b7b65eba1d9.js +0 -1
  239. package/web/.next/static/chunks/687-3a4205f51be38602.js +0 -1
  240. package/web/.next/static/chunks/837-fd7623601dc78ed8.js +0 -1
  241. package/web/.next/static/chunks/850-318f32484989dd59.js +0 -1
  242. package/web/.next/static/chunks/89-ace2e1aa63babfdf.js +0 -1
  243. package/web/.next/static/chunks/951-e250b541b67da7ce.js +0 -1
  244. package/web/.next/static/chunks/app/_not-found/page-bbd54d7b9a55d7ae.js +0 -1
  245. package/web/.next/static/chunks/app/agents/[id]/page-a8b7a43d2c18483f.js +0 -1
  246. package/web/.next/static/chunks/app/agents/page-e9b17f1c3439ef98.js +0 -1
  247. package/web/.next/static/chunks/app/connections/page-477f36fcd5b13d78.js +0 -1
  248. package/web/.next/static/chunks/app/layout-a2d38e3de5911b1b.js +0 -1
  249. package/web/.next/static/chunks/app/page-6fc87fc5a128a204.js +0 -1
  250. package/web/.next/static/chunks/app/relations/page-0355a93a8eb67989.js +0 -1
  251. package/web/.next/static/chunks/app/schedules/page-39befd15b3b20586.js +0 -1
  252. package/web/.next/static/chunks/app/sessions/page-beae9a74a07d69e0.js +0 -1
  253. package/web/.next/static/chunks/app/skills/page-4f839ac913e26d32.js +0 -1
  254. package/web/.next/static/chunks/app/types/page-4f6b99af3232f861.js +0 -1
  255. package/web/.next/static/chunks/framework-0e68884e76d9a8d3.js +0 -1
  256. package/web/.next/static/chunks/main-app-d6f73ccd41f325c5.js +0 -1
  257. package/web/.next/static/chunks/main-cd22509db15764e2.js +0 -1
  258. package/web/.next/static/chunks/pages/_app-0f75d689c738a6fc.js +0 -1
  259. package/web/.next/static/chunks/pages/_error-d481f6d49fb4455d.js +0 -1
  260. package/web/.next/static/chunks/webpack-d40de72e52f65f79.js +0 -1
  261. package/web/.next/static/css/da22f0402b1c40ae.css +0 -1
  262. /package/web/.next/static/{_z2hBOHhwiR5ytdI_8alh → Bz7onClw1BajMUCwqpU8o}/_ssgManifest.js +0 -0
package/server/dist/db.js CHANGED
@@ -3,118 +3,28 @@ import { mkdirSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { randomUUID } from 'node:crypto';
6
+ import { migrate } from './db/migrations.js';
7
+ import { parseJson } from './db/json.js';
6
8
  export const DATA_DIR = process.env.HIVE_AM_HOME ?? join(homedir(), '.hive-am');
7
9
  mkdirSync(DATA_DIR, { recursive: true });
8
10
  export const db = new Database(join(DATA_DIR, 'hive-am.db'));
9
11
  db.pragma('journal_mode = WAL');
10
12
  db.pragma('foreign_keys = ON');
11
- db.exec(`
12
- CREATE TABLE IF NOT EXISTS skills (
13
- id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT NOT NULL DEFAULT '',
14
- content TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
15
- );
16
- CREATE TABLE IF NOT EXISTS agent_types (
17
- id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT NOT NULL DEFAULT '',
18
- role TEXT NOT NULL, provider TEXT NOT NULL, model TEXT NOT NULL DEFAULT '',
19
- system_prompt TEXT NOT NULL DEFAULT '', permission TEXT NOT NULL DEFAULT 'acceptEdits',
20
- color TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL
21
- );
22
- CREATE TABLE IF NOT EXISTS type_skills (
23
- type_id TEXT NOT NULL REFERENCES agent_types(id) ON DELETE CASCADE,
24
- skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
25
- PRIMARY KEY (type_id, skill_id)
26
- );
27
- CREATE TABLE IF NOT EXISTS agents (
28
- id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT NOT NULL DEFAULT '',
29
- role TEXT NOT NULL, type_id TEXT REFERENCES agent_types(id) ON DELETE SET NULL,
30
- provider TEXT NOT NULL, model TEXT NOT NULL DEFAULT '', system_prompt TEXT NOT NULL DEFAULT '',
31
- permission TEXT NOT NULL DEFAULT 'acceptEdits', cwd TEXT NOT NULL,
32
- session_id TEXT, status TEXT NOT NULL DEFAULT 'idle',
33
- created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
34
- );
35
- CREATE TABLE IF NOT EXISTS agent_skills (
36
- agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
37
- skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
38
- PRIMARY KEY (agent_id, skill_id)
39
- );
40
- CREATE TABLE IF NOT EXISTS assignments (
41
- orchestrator_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
42
- worker_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
43
- PRIMARY KEY (orchestrator_id, worker_id)
44
- );
45
- CREATE TABLE IF NOT EXISTS colonies (
46
- id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, color TEXT NOT NULL DEFAULT '', cwd TEXT NOT NULL DEFAULT '',
47
- permission TEXT NOT NULL DEFAULT 'acceptEdits', system_prompt TEXT NOT NULL DEFAULT '',
48
- inherit TEXT NOT NULL DEFAULT '{}', created_at INTEGER NOT NULL
49
- );
50
- CREATE TABLE IF NOT EXISTS colony_skills (
51
- colony_id TEXT NOT NULL REFERENCES colonies(id) ON DELETE CASCADE,
52
- skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
53
- PRIMARY KEY (colony_id, skill_id)
54
- );
55
- -- Pointer log of every native session an agent has ever used (for the Sessions view).
56
- CREATE TABLE IF NOT EXISTS agent_sessions (
57
- agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
58
- provider TEXT NOT NULL, session_id TEXT NOT NULL, cwd TEXT NOT NULL,
59
- first_seen INTEGER NOT NULL, last_seen INTEGER NOT NULL,
60
- PRIMARY KEY (agent_id, session_id)
61
- );
62
- -- Dispatch log: who asked whom to do what. Results live in the worker's own session.
63
- CREATE TABLE IF NOT EXISTS dispatches (
64
- id TEXT PRIMARY KEY, from_id TEXT NOT NULL, to_id TEXT NOT NULL, task TEXT NOT NULL,
65
- status TEXT NOT NULL, result TEXT, created_at INTEGER NOT NULL, finished_at INTEGER
66
- );
67
- `);
68
- // Lightweight migrations for databases created before colonies existed.
69
- const agentCols = db.prepare('PRAGMA table_info(agents)').all().map((c) => c.name);
70
- if (!agentCols.includes('colony_id'))
71
- db.exec('ALTER TABLE agents ADD COLUMN colony_id TEXT REFERENCES colonies(id) ON DELETE SET NULL');
72
- if (!agentCols.includes('overrides'))
73
- db.exec("ALTER TABLE agents ADD COLUMN overrides TEXT NOT NULL DEFAULT '[]'");
74
- if (!agentCols.includes('session_cwd'))
75
- db.exec('ALTER TABLE agents ADD COLUMN session_cwd TEXT');
76
- if (!agentCols.includes('instr_hash'))
77
- db.exec('ALTER TABLE agents ADD COLUMN instr_hash TEXT');
78
- // Skills briefly had a "default/personal" split; all skills are equal now.
79
- for (const col of ['kind', 'slug']) {
80
- try {
81
- db.exec(`ALTER TABLE skills DROP COLUMN ${col}`);
82
- }
83
- catch { /* already gone */ }
84
- }
85
- db.exec('CREATE TABLE IF NOT EXISTS seeded_skills (slug TEXT PRIMARY KEY)');
86
- // How a skill reaches the agent: `always` puts all its text in the instructions; `on_demand` lists name + description and the agent reads the text with `skill_read` when a task needs it.
87
- if (!db.prepare('PRAGMA table_info(skills)').all().some((c) => c.name === 'load')) {
88
- db.exec("ALTER TABLE skills ADD COLUMN load TEXT NOT NULL DEFAULT 'always'");
89
- // The skills that came with hive-am are long, situational guides (the notebook one must always be in view).
90
- db.exec("UPDATE skills SET load='on_demand' WHERE id LIKE 'default-%' AND id<>'default-notebook'");
91
- }
92
- // How each assignment loads the skill (see Skill.load, which is only the suggestion offered when assigning).
93
- for (const [table] of [['type_skills'], ['colony_skills'], ['agent_skills']]) {
94
- if (db.prepare(`PRAGMA table_info(${table})`).all().some((c) => c.name === 'load'))
95
- continue;
96
- db.exec(`ALTER TABLE ${table} ADD COLUMN load TEXT NOT NULL DEFAULT 'always'`);
97
- db.exec(`UPDATE ${table} SET load=COALESCE((SELECT load FROM skills WHERE skills.id=${table}.skill_id), 'always')`);
98
- }
99
- const sessCols = db.prepare('PRAGMA table_info(agent_sessions)').all().map((c) => c.name);
100
- if (!sessCols.includes('kind'))
101
- db.exec("ALTER TABLE agent_sessions ADD COLUMN kind TEXT NOT NULL DEFAULT 'direct'");
102
- if (!sessCols.includes('from_id'))
103
- db.exec('ALTER TABLE agent_sessions ADD COLUMN from_id TEXT');
104
- if (!sessCols.includes('task'))
105
- db.exec('ALTER TABLE agent_sessions ADD COLUMN task TEXT');
106
- const dispCols = db.prepare('PRAGMA table_info(dispatches)').all().map((c) => c.name);
107
- if (!dispCols.includes('session_id'))
108
- db.exec('ALTER TABLE dispatches ADD COLUMN session_id TEXT');
13
+ // Every table and column is created by the numbered migrations (db/migrations.ts); modules that own a table import `db` from here, so it is ready by then.
14
+ migrate(db);
109
15
  const now = () => Date.now();
110
16
  const uid = () => randomUUID();
17
+ // Statements are prepared once and reused (the SQL text is the key; the few dynamic ones only vary by table name).
18
+ const prepared = new Map();
19
+ const q = (sql) => { let st = prepared.get(sql); if (!st)
20
+ prepared.set(sql, (st = db.prepare(sql))); return st; };
111
21
  // ---- skills ----
112
22
  export const skills = {
113
- list: () => db.prepare('SELECT * FROM skills ORDER BY name').all(),
114
- get: (id) => db.prepare('SELECT * FROM skills WHERE id=?').get(id),
23
+ list: () => q('SELECT * FROM skills ORDER BY name').all(),
24
+ get: (id) => q('SELECT * FROM skills WHERE id=?').get(id),
115
25
  create(p) {
116
26
  const id = uid(), t = now();
117
- db.prepare('INSERT INTO skills (id,name,description,content,created_at,updated_at,load) VALUES (?,?,?,?,?,?,?)').run(id, p.name, p.description ?? '', p.content ?? '', t, t, p.load ?? ((p.content ?? '').length > 2000 ? 'on_demand' : 'always'));
27
+ q('INSERT INTO skills (id,name,description,content,created_at,updated_at,load) VALUES (?,?,?,?,?,?,?)').run(id, p.name, p.description ?? '', p.content ?? '', t, t, p.load ?? ((p.content ?? '').length > 2000 ? 'on_demand' : 'always'));
118
28
  return skills.get(id);
119
29
  },
120
30
  update(id, p) {
@@ -122,36 +32,38 @@ export const skills = {
122
32
  if (!cur)
123
33
  return;
124
34
  const n = { ...cur, ...p };
125
- db.prepare('UPDATE skills SET name=?, description=?, content=?, load=?, updated_at=? WHERE id=?').run(n.name, n.description, n.content, n.load, now(), id);
35
+ q('UPDATE skills SET name=?, description=?, content=?, load=?, updated_at=? WHERE id=?').run(n.name, n.description, n.content, n.load, now(), id);
126
36
  return skills.get(id);
127
37
  },
128
- remove: (id) => db.prepare('DELETE FROM skills WHERE id=?').run(id).changes > 0,
38
+ remove: (id) => q('DELETE FROM skills WHERE id=?').run(id).changes > 0,
129
39
  /**
130
40
  * Adds the skills that come with hive-am, once each: they are ordinary skills afterwards (edit them, delete them). A
131
41
  * skill that was seeded before is never added back, and one the user already has under that name is left alone.
132
42
  */
133
43
  seedDefaults(defs) {
134
44
  const t = now();
135
- for (const d of defs) {
136
- const id = `default-${d.slug}`;
137
- if (db.prepare('SELECT 1 FROM seeded_skills WHERE slug=?').get(d.slug))
138
- continue;
139
- db.prepare('INSERT OR IGNORE INTO seeded_skills (slug) VALUES (?)').run(d.slug);
140
- if (db.prepare('SELECT 1 FROM skills WHERE id=?').get(id))
141
- continue; // from an earlier build: keep it as it is
142
- const taken = !!db.prepare('SELECT 1 FROM skills WHERE lower(name)=lower(?)').get(d.name);
143
- db.prepare('INSERT INTO skills (id,name,description,content,created_at,updated_at,load) VALUES (?,?,?,?,?,?,?)').run(id, taken ? `${d.name} (hive-am)` : d.name, d.description, d.content, t, t, d.load ?? 'always');
144
- }
45
+ db.transaction(() => {
46
+ for (const d of defs) {
47
+ const id = `default-${d.slug}`;
48
+ if (q('SELECT 1 FROM seeded_skills WHERE slug=?').get(d.slug))
49
+ continue;
50
+ q('INSERT OR IGNORE INTO seeded_skills (slug) VALUES (?)').run(d.slug);
51
+ if (q('SELECT 1 FROM skills WHERE id=?').get(id))
52
+ continue; // from an earlier build: keep it as it is
53
+ const taken = !!q('SELECT 1 FROM skills WHERE lower(name)=lower(?)').get(d.name);
54
+ q('INSERT INTO skills (id,name,description,content,created_at,updated_at,load) VALUES (?,?,?,?,?,?,?)').run(id, taken ? `${d.name} (hive-am)` : d.name, d.description, d.content, t, t, d.load ?? 'always');
55
+ }
56
+ })();
145
57
  },
146
58
  /** Agents/types using each skill, for the library view. */
147
59
  usage() {
148
60
  const out = {};
149
61
  for (const s of skills.list())
150
62
  out[s.id] = { agents: 0, types: 0 };
151
- for (const r of db.prepare('SELECT skill_id, COUNT(*) c FROM agent_skills GROUP BY skill_id').all())
63
+ for (const r of q('SELECT skill_id, COUNT(*) c FROM agent_skills GROUP BY skill_id').all())
152
64
  if (out[r.skill_id])
153
65
  out[r.skill_id].agents = r.c;
154
- for (const r of db.prepare('SELECT skill_id, COUNT(*) c FROM type_skills GROUP BY skill_id').all())
66
+ for (const r of q('SELECT skill_id, COUNT(*) c FROM type_skills GROUP BY skill_id').all())
155
67
  if (out[r.skill_id])
156
68
  out[r.skill_id].types = r.c;
157
69
  return out;
@@ -166,10 +78,11 @@ const JOINS = {
166
78
  /** How each skill assigned to this type/colony/agent is loaded. */
167
79
  function loadsOf(kind, id) {
168
80
  const j = JOINS[kind], out = {};
169
- for (const r of db.prepare(`SELECT skill_id, load FROM ${j.table} WHERE ${j.owner}=?`).all(id))
81
+ for (const r of q(`SELECT skill_id, load FROM ${j.table} WHERE ${j.owner}=?`).all(id))
170
82
  out[r.skill_id] = r.load === 'on_demand' ? 'on_demand' : 'always';
171
83
  return out;
172
84
  }
85
+ const skillIdsOf = (kind, id) => { const j = JOINS[kind]; return q(`SELECT skill_id FROM ${j.table} WHERE ${j.owner}=?`).all(id).map((x) => x.skill_id); };
173
86
  /**
174
87
  * Replaces the skills of a type/colony/agent. A skill keeps the way it was loaded before; a new one takes what the caller
175
88
  * says, or else the skill's own suggestion.
@@ -177,98 +90,87 @@ function loadsOf(kind, id) {
177
90
  function assignSkills(kind, id, ids, loads) {
178
91
  const j = JOINS[kind], prev = loadsOf(kind, id);
179
92
  db.transaction(() => {
180
- db.prepare(`DELETE FROM ${j.table} WHERE ${j.owner}=?`).run(id);
93
+ q(`DELETE FROM ${j.table} WHERE ${j.owner}=?`).run(id);
181
94
  for (const s of ids) {
182
95
  const load = loads?.[s] ?? prev[s] ?? skills.get(s)?.load ?? 'always';
183
- db.prepare(`INSERT OR IGNORE INTO ${j.table} (${j.owner}, skill_id, load) VALUES (?,?,?)`).run(id, s, load === 'on_demand' ? 'on_demand' : 'always');
96
+ q(`INSERT OR IGNORE INTO ${j.table} (${j.owner}, skill_id, load) VALUES (?,?,?)`).run(id, s, load === 'on_demand' ? 'on_demand' : 'always');
184
97
  }
185
98
  })();
186
99
  }
187
100
  // ---- agent types ----
188
- function typeRow(r) {
189
- const skill_ids = db.prepare('SELECT skill_id FROM type_skills WHERE type_id=?').all(r.id).map((x) => x.skill_id);
190
- return { ...r, skill_ids, skill_loads: loadsOf('type', r.id) };
101
+ function typeOf(r) {
102
+ return { ...r, skill_ids: skillIdsOf('type', r.id), skill_loads: loadsOf('type', r.id) };
191
103
  }
192
104
  export const types = {
193
- list: () => db.prepare('SELECT * FROM agent_types ORDER BY role DESC, name').all().map(typeRow),
194
- get(id) { const r = db.prepare('SELECT * FROM agent_types WHERE id=?').get(id); return r ? typeRow(r) : undefined; },
195
- create(p) {
105
+ list: () => q('SELECT * FROM agent_types ORDER BY role DESC, name').all().map(typeOf),
106
+ get(id) { const r = q('SELECT * FROM agent_types WHERE id=?').get(id); return r ? typeOf(r) : undefined; },
107
+ create: db.transaction((p) => {
196
108
  const id = uid();
197
- db.prepare('INSERT INTO agent_types VALUES (?,?,?,?,?,?,?,?,?,?)').run(id, p.name, p.description ?? '', p.role, p.provider, p.model ?? '', p.system_prompt ?? '', (p.permission ?? 'acceptEdits'), p.color ?? '', now());
109
+ q('INSERT INTO agent_types (id,name,description,role,provider,model,system_prompt,permission,color,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)').run(id, p.name, p.description ?? '', p.role, p.provider, p.model ?? '', p.system_prompt ?? '', (p.permission ?? 'acceptEdits'), p.color ?? '', now());
198
110
  types.setSkills(id, p.skill_ids ?? [], p.skill_loads);
199
111
  return types.get(id);
200
- },
201
- update(id, p) {
112
+ }),
113
+ update: db.transaction((id, p) => {
202
114
  const cur = types.get(id);
203
115
  if (!cur)
204
116
  return;
205
117
  const n = { ...cur, ...p };
206
- db.prepare(`UPDATE agent_types SET name=?, description=?, role=?, provider=?, model=?, system_prompt=?, permission=?, color=? WHERE id=?`)
118
+ q('UPDATE agent_types SET name=?, description=?, role=?, provider=?, model=?, system_prompt=?, permission=?, color=? WHERE id=?')
207
119
  .run(n.name, n.description, n.role, n.provider, n.model, n.system_prompt, n.permission, n.color, id);
208
120
  if (p.skill_ids || p.skill_loads)
209
121
  types.setSkills(id, p.skill_ids ?? cur.skill_ids, p.skill_loads);
210
122
  return types.get(id);
211
- },
123
+ }),
212
124
  setSkills: (id, ids, loads) => assignSkills('type', id, ids, loads),
213
- remove: (id) => db.prepare('DELETE FROM agent_types WHERE id=?').run(id).changes > 0,
125
+ remove: (id) => q('DELETE FROM agent_types WHERE id=?').run(id).changes > 0,
214
126
  };
215
127
  // ---- colonies ----
216
128
  const DEFAULT_INHERIT = { cwd: true, permission: true, skills: true, prompt: true };
217
- function colonyRow(r) {
218
- const skill_ids = db.prepare('SELECT skill_id FROM colony_skills WHERE colony_id=?').all(r.id).map((x) => x.skill_id);
219
- const agent_ids = db.prepare('SELECT id FROM agents WHERE colony_id=? ORDER BY name').all(r.id).map((x) => x.id);
220
- let inherit = DEFAULT_INHERIT;
221
- try {
222
- inherit = { ...DEFAULT_INHERIT, ...JSON.parse(r.inherit) };
223
- }
224
- catch { /* keep defaults */ }
225
- return { ...r, inherit, skill_ids, skill_loads: loadsOf('colony', r.id), agent_ids };
129
+ function colonyOf(r) {
130
+ const agent_ids = q('SELECT id FROM agents WHERE colony_id=? ORDER BY name').all(r.id).map((x) => x.id);
131
+ const inherit = { ...DEFAULT_INHERIT, ...parseJson(r.inherit, {}, `colony ${r.id} inherit`) };
132
+ return { ...r, inherit, skill_ids: skillIdsOf('colony', r.id), skill_loads: loadsOf('colony', r.id), agent_ids };
226
133
  }
227
134
  export const colonies = {
228
- list: () => db.prepare('SELECT * FROM colonies ORDER BY name').all().map(colonyRow),
229
- get(id) { const r = db.prepare('SELECT * FROM colonies WHERE id=?').get(id); return r ? colonyRow(r) : undefined; },
230
- create(p) {
135
+ list: () => q('SELECT * FROM colonies ORDER BY name').all().map(colonyOf),
136
+ get(id) { const r = q('SELECT * FROM colonies WHERE id=?').get(id); return r ? colonyOf(r) : undefined; },
137
+ create: db.transaction((p) => {
231
138
  const id = uid();
232
- db.prepare('INSERT INTO colonies VALUES (?,?,?,?,?,?,?,?)').run(id, p.name, p.color ?? '', p.cwd ?? '', (p.permission ?? 'acceptEdits'), p.system_prompt ?? '', JSON.stringify({ ...DEFAULT_INHERIT, ...(p.inherit ?? {}) }), now());
139
+ q('INSERT INTO colonies (id,name,color,cwd,permission,system_prompt,inherit,created_at) VALUES (?,?,?,?,?,?,?,?)').run(id, p.name, p.color ?? '', p.cwd ?? '', (p.permission ?? 'acceptEdits'), p.system_prompt ?? '', JSON.stringify({ ...DEFAULT_INHERIT, ...(p.inherit ?? {}) }), now());
233
140
  colonies.setSkills(id, p.skill_ids ?? [], p.skill_loads);
234
141
  if (p.agent_ids)
235
142
  colonies.setMembers(id, p.agent_ids);
236
143
  return colonies.get(id);
237
- },
238
- update(id, p) {
144
+ }),
145
+ update: db.transaction((id, p) => {
239
146
  const cur = colonies.get(id);
240
147
  if (!cur)
241
148
  return;
242
149
  const n = { ...cur, ...p, inherit: { ...cur.inherit, ...(p.inherit ?? {}) } };
243
- db.prepare('UPDATE colonies SET name=?, color=?, cwd=?, permission=?, system_prompt=?, inherit=? WHERE id=?')
150
+ q('UPDATE colonies SET name=?, color=?, cwd=?, permission=?, system_prompt=?, inherit=? WHERE id=?')
244
151
  .run(n.name, n.color, n.cwd, n.permission, n.system_prompt, JSON.stringify(n.inherit), id);
245
152
  if (p.skill_ids || p.skill_loads)
246
153
  colonies.setSkills(id, p.skill_ids ?? cur.skill_ids, p.skill_loads);
247
154
  if (p.agent_ids)
248
155
  colonies.setMembers(id, p.agent_ids);
249
156
  return colonies.get(id);
250
- },
157
+ }),
251
158
  setSkills: (id, ids, loads) => assignSkills('colony', id, ids, loads),
252
159
  /** Replaces membership; an agent belongs to at most one colony. */
253
- setMembers(id, agentIds) {
254
- db.transaction(() => {
255
- db.prepare('UPDATE agents SET colony_id=NULL WHERE colony_id=?').run(id);
256
- for (const a of agentIds)
257
- db.prepare('UPDATE agents SET colony_id=? WHERE id=?').run(id, a);
258
- })();
259
- },
260
- remove: (id) => db.prepare('DELETE FROM colonies WHERE id=?').run(id).changes > 0,
160
+ setMembers: db.transaction((id, agentIds) => {
161
+ q('UPDATE agents SET colony_id=NULL WHERE colony_id=?').run(id);
162
+ for (const a of agentIds)
163
+ q('UPDATE agents SET colony_id=? WHERE id=?').run(id, a);
164
+ }),
165
+ /** Members stay (their colony becomes none through the foreign key); the colony's own skill assignments go with it. */
166
+ remove: (id) => q('DELETE FROM colonies WHERE id=?').run(id).changes > 0,
261
167
  };
262
168
  // ---- agents ----
263
169
  const FIELDS = ['cwd', 'permission', 'skills', 'prompt'];
264
- function agentRow(r) {
265
- const skill_ids = db.prepare('SELECT skill_id FROM agent_skills WHERE agent_id=?').all(r.id).map((x) => x.skill_id);
266
- const worker_ids = db.prepare('SELECT worker_id FROM assignments WHERE orchestrator_id=?').all(r.id).map((x) => x.worker_id);
267
- let overrides = [];
268
- try {
269
- overrides = JSON.parse(r.overrides ?? '[]').filter((f) => FIELDS.includes(f));
270
- }
271
- catch { /* none */ }
170
+ function agentOf(r) {
171
+ const skill_ids = skillIdsOf('agent', r.id);
172
+ const worker_ids = q('SELECT worker_id FROM assignments WHERE orchestrator_id=?').all(r.id).map((x) => x.worker_id);
173
+ const overrides = parseJson(r.overrides, [], `agent ${r.id} overrides`).filter((f) => FIELDS.includes(f));
272
174
  const col = r.colony_id ? colonies.get(r.colony_id) : undefined;
273
175
  const follows = (f) => !!col && col.inherit[f] && !overrides.includes(f);
274
176
  const inherited = FIELDS.filter(follows);
@@ -289,69 +191,69 @@ export function resolved(a) {
289
191
  return { ...a, cwd: a.effective.cwd, permission: a.effective.permission, system_prompt: a.effective.system_prompt, skill_ids: a.effective.skill_ids, skill_loads: a.effective.skill_loads };
290
192
  }
291
193
  export const agents = {
292
- list: () => db.prepare('SELECT * FROM agents ORDER BY role DESC, name').all().map(agentRow),
293
- get(id) { const r = db.prepare('SELECT * FROM agents WHERE id=?').get(id); return r ? agentRow(r) : undefined; },
294
- byName(name) { const r = db.prepare('SELECT * FROM agents WHERE lower(name)=lower(?)').get(name); return r ? agentRow(r) : undefined; },
295
- create(p) {
194
+ list: () => q('SELECT * FROM agents ORDER BY role DESC, name').all().map(agentOf),
195
+ get(id) { const r = q('SELECT * FROM agents WHERE id=?').get(id); return r ? agentOf(r) : undefined; },
196
+ byName(name) { const r = q('SELECT * FROM agents WHERE lower(name)=lower(?)').get(name); return r ? agentOf(r) : undefined; },
197
+ /** The agent, its skills and its workers are written together: either all of it exists afterwards or none. */
198
+ create: db.transaction((p) => {
296
199
  const id = uid(), t = now();
297
- db.prepare('INSERT INTO agents (id,name,description,role,type_id,provider,model,system_prompt,permission,cwd,session_id,status,created_at,updated_at,colony_id,overrides) VALUES (?,?,?,?,?,?,?,?,?,?,NULL,?,?,?,?,?)').run(id, p.name, p.description ?? '', p.role, p.type_id ?? null, p.provider, p.model ?? '', p.system_prompt ?? '', (p.permission ?? 'acceptEdits'), p.cwd ?? '', 'idle', t, t, p.colony_id ?? null, JSON.stringify(p.overrides ?? []));
200
+ q('INSERT INTO agents (id,name,description,role,type_id,provider,model,system_prompt,permission,cwd,session_id,status,created_at,updated_at,colony_id,overrides) VALUES (?,?,?,?,?,?,?,?,?,?,NULL,?,?,?,?,?)').run(id, p.name, p.description ?? '', p.role, p.type_id ?? null, p.provider, p.model ?? '', p.system_prompt ?? '', (p.permission ?? 'acceptEdits'), p.cwd ?? '', 'idle', t, t, p.colony_id ?? null, JSON.stringify(p.overrides ?? []));
298
201
  agents.setSkills(id, p.skill_ids ?? [], p.skill_loads);
299
202
  agents.setWorkers(id, p.worker_ids ?? []);
300
203
  return agents.get(id);
301
- },
302
- update(id, p) {
204
+ }),
205
+ update: db.transaction((id, p) => {
303
206
  const cur = agents.get(id);
304
207
  if (!cur)
305
208
  return;
306
209
  const n = { ...cur, ...p };
307
- db.prepare(`UPDATE agents SET name=?, description=?, role=?, type_id=?, provider=?, model=?, system_prompt=?, permission=?, cwd=?, colony_id=?, overrides=?, updated_at=? WHERE id=?`)
210
+ q('UPDATE agents SET name=?, description=?, role=?, type_id=?, provider=?, model=?, system_prompt=?, permission=?, cwd=?, colony_id=?, overrides=?, updated_at=? WHERE id=?')
308
211
  .run(n.name, n.description, n.role, n.type_id, n.provider, n.model, n.system_prompt, n.permission, n.cwd, n.colony_id ?? null, JSON.stringify(n.overrides ?? []), now(), id);
309
212
  if (p.skill_ids || p.skill_loads)
310
213
  agents.setSkills(id, p.skill_ids ?? cur.skill_ids, p.skill_loads);
311
214
  if (p.worker_ids)
312
215
  agents.setWorkers(id, p.worker_ids);
313
216
  return agents.get(id);
314
- },
315
- setInstrHash: (id, hash) => db.prepare('UPDATE agents SET instr_hash=? WHERE id=?').run(hash, id),
316
- setStatus: (id, status) => db.prepare('UPDATE agents SET status=?, updated_at=? WHERE id=?').run(status, now(), id),
317
- setSession(a, sessionId) {
318
- db.prepare('UPDATE agents SET session_id=?, session_cwd=?, instr_hash=NULL, updated_at=? WHERE id=?').run(sessionId, sessionId ? a.effective.cwd : null, now(), a.id);
217
+ }),
218
+ setInstrHash: (id, hash) => q('UPDATE agents SET instr_hash=? WHERE id=?').run(hash, id),
219
+ setStatus: (id, status) => q('UPDATE agents SET status=?, updated_at=? WHERE id=?').run(status, now(), id),
220
+ setSession: db.transaction((a, sessionId) => {
221
+ q('UPDATE agents SET session_id=?, session_cwd=?, instr_hash=NULL, updated_at=? WHERE id=?').run(sessionId, sessionId ? a.effective.cwd : null, now(), a.id);
319
222
  if (sessionId) {
320
- db.prepare(`INSERT INTO agent_sessions (agent_id, provider, session_id, cwd, first_seen, last_seen) VALUES (?,?,?,?,?,?)
223
+ q(`INSERT INTO agent_sessions (agent_id, provider, session_id, cwd, first_seen, last_seen) VALUES (?,?,?,?,?,?)
321
224
  ON CONFLICT(agent_id, session_id) DO UPDATE SET last_seen=excluded.last_seen`)
322
225
  .run(a.id, a.provider, sessionId, a.effective.cwd, now(), now());
323
226
  }
324
- },
227
+ }),
325
228
  /** Logs a native session without making it the agent's current conversation (used for delegations). */
326
229
  recordSession(a, sessionId, kind, fromId, task) {
327
- db.prepare(`INSERT INTO agent_sessions (agent_id, provider, session_id, cwd, first_seen, last_seen, kind, from_id, task) VALUES (?,?,?,?,?,?,?,?,?)
230
+ q(`INSERT INTO agent_sessions (agent_id, provider, session_id, cwd, first_seen, last_seen, kind, from_id, task) VALUES (?,?,?,?,?,?,?,?,?)
328
231
  ON CONFLICT(agent_id, session_id) DO UPDATE SET last_seen=excluded.last_seen`)
329
232
  .run(a.id, a.provider, sessionId, a.effective.cwd, now(), now(), kind, fromId ?? null, task ? task.slice(0, 500) : null);
330
233
  },
331
234
  setSkills: (id, ids, loads) => assignSkills('agent', id, ids, loads),
332
- setWorkers(id, ids) {
333
- db.transaction(() => {
334
- db.prepare('DELETE FROM assignments WHERE orchestrator_id=?').run(id);
335
- for (const w of ids)
336
- if (w !== id)
337
- db.prepare('INSERT OR IGNORE INTO assignments VALUES (?,?)').run(id, w);
338
- })();
339
- },
340
- remove: (id) => db.prepare('DELETE FROM agents WHERE id=?').run(id).changes > 0,
341
- sessions: (id) => db.prepare('SELECT s.*, f.name from_name FROM agent_sessions s LEFT JOIN agents f ON f.id=s.from_id WHERE s.agent_id=? ORDER BY s.last_seen DESC').all(id),
342
- allSessions: () => db.prepare(`SELECT s.*, a.name agent_name, a.role agent_role, f.name from_name FROM agent_sessions s JOIN agents a ON a.id=s.agent_id LEFT JOIN agents f ON f.id=s.from_id ORDER BY s.last_seen DESC`).all(),
235
+ /** Replaces the workers an orchestrator may dispatch to (never itself). */
236
+ setWorkers: db.transaction((id, ids) => {
237
+ q('DELETE FROM assignments WHERE orchestrator_id=?').run(id);
238
+ for (const w of ids)
239
+ if (w !== id)
240
+ q('INSERT OR IGNORE INTO assignments (orchestrator_id, worker_id) VALUES (?,?)').run(id, w);
241
+ }),
242
+ remove: (id) => q('DELETE FROM agents WHERE id=?').run(id).changes > 0,
243
+ sessions: (id) => q('SELECT s.*, f.name from_name FROM agent_sessions s LEFT JOIN agents f ON f.id=s.from_id WHERE s.agent_id=? ORDER BY s.last_seen DESC').all(id),
244
+ allSessions: () => q(`SELECT s.*, a.name agent_name, a.role agent_role, f.name from_name FROM agent_sessions s JOIN agents a ON a.id=s.agent_id LEFT JOIN agents f ON f.id=s.from_id ORDER BY s.last_seen DESC`).all(),
343
245
  };
344
246
  // ---- dispatch log ----
345
247
  export const dispatches = {
346
248
  start(from, to, task) {
347
249
  const id = uid();
348
- db.prepare('INSERT INTO dispatches (id, from_id, to_id, task, status, result, created_at, finished_at) VALUES (?,?,?,?,?,NULL,?,NULL)').run(id, from, to, task, 'running', now());
250
+ q('INSERT INTO dispatches (id, from_id, to_id, task, status, result, created_at, finished_at) VALUES (?,?,?,?,?,NULL,?,NULL)').run(id, from, to, task, 'running', now());
349
251
  return id;
350
252
  },
351
- setSession: (id, sessionId) => db.prepare('UPDATE dispatches SET session_id=? WHERE id=?').run(sessionId, id),
352
- finish: (id, status, result) => db.prepare('UPDATE dispatches SET status=?, result=?, finished_at=? WHERE id=?').run(status, result.slice(0, 20000), now(), id),
353
- recent: (limit = 50) => db.prepare('SELECT * FROM dispatches ORDER BY created_at DESC LIMIT ?').all(limit),
253
+ setSession: (id, sessionId) => q('UPDATE dispatches SET session_id=? WHERE id=?').run(sessionId, id),
254
+ finish: (id, status, result) => q('UPDATE dispatches SET status=?, result=?, finished_at=? WHERE id=?').run(status, result.slice(0, 20000), now(), id),
255
+ recent: (limit = 50) => q('SELECT * FROM dispatches ORDER BY created_at DESC LIMIT ?').all(limit),
354
256
  };
355
257
  // Agents left 'running' by a crash/blackout are simply idle again: their session is intact on disk.
356
- db.prepare("UPDATE agents SET status='idle' WHERE status='running'").run();
357
- db.prepare("UPDATE dispatches SET status='interrupted' WHERE status='running'").run();
258
+ q("UPDATE agents SET status='idle' WHERE status='running'").run();
259
+ q("UPDATE dispatches SET status='interrupted' WHERE status='running'").run();
@@ -0,0 +1,11 @@
1
+ export class DomainError extends Error {
2
+ kind;
3
+ code;
4
+ constructor(message, kind = 'invalid', code) {
5
+ super(message);
6
+ this.kind = kind;
7
+ this.code = code;
8
+ }
9
+ }
10
+ /** The HTTP status a kind maps to. */
11
+ export const STATUS_OF_KIND = { invalid: 400, forbidden: 403, not_found: 404, conflict: 409, unavailable: 503 };
@@ -0,0 +1,99 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { filesFromDiffTree, repoOf, resolveRef, run } from './ops.js';
3
+ import { gitEnv, imageType, PathError, safePath } from './repo.js';
4
+ const MAX_REFS = 1000;
5
+ export async function gitRefs(cwd) {
6
+ const { root } = await repoOf(cwd);
7
+ const [heads, tags] = await Promise.all([
8
+ run(root, ['for-each-ref', '--sort=-committerdate', `--count=${MAX_REFS}`, '--format=%(refname)%1f%(refname:short)%1f%(objectname)%1f%(committerdate:iso-strict)%1f%(contents:subject)%1f%(HEAD)', 'refs/heads', 'refs/remotes']),
9
+ // For an annotated tag %(*objectname) is its commit; for a lightweight one %(objectname) already is.
10
+ run(root, ['for-each-ref', '--sort=-creatordate', `--count=${MAX_REFS}`, '--format=%(refname:strip=2)%1f%(*objectname)%1f%(objectname)%1f%(creatordate:iso-strict)%1f%(contents:subject)', 'refs/tags']),
11
+ ]);
12
+ const res = { current: null, branches: [], remotes: [], tags: [] };
13
+ for (const line of heads.out.split('\n').filter(Boolean)) {
14
+ const [ref, name, sha, date, subject, head] = line.split('\x1f');
15
+ if (ref.startsWith('refs/heads/')) {
16
+ res.branches.push({ name, sha, date, subject });
17
+ if (head === '*')
18
+ res.current = name;
19
+ }
20
+ else if (!ref.endsWith('/HEAD'))
21
+ res.remotes.push({ name, sha, date, subject });
22
+ }
23
+ for (const line of tags.out.split('\n').filter(Boolean)) {
24
+ const [name, peeled, direct, date, subject] = line.split('\x1f');
25
+ res.tags.push({ name, sha: peeled || direct, date, subject });
26
+ }
27
+ return res;
28
+ }
29
+ const MAX_COMPARE_FILES = 500, MAX_COMPARE_COMMITS = 100;
30
+ /** What changed going from `base` to `head`: the files, and the commits in between. */
31
+ export async function gitCompare(cwd, base, head) {
32
+ const { root } = await repoOf(cwd);
33
+ const [b, h] = [await resolveRef(root, base), await resolveRef(root, head)];
34
+ const [names, nums, ahead, behind, log] = await Promise.all([
35
+ run(root, ['diff-tree', '-r', '-M', '--name-status', '-z', '--no-commit-id', b, h], { raw: true }),
36
+ run(root, ['diff-tree', '-r', '-M', '--histogram', '--numstat', '-z', '--no-commit-id', b, h], { raw: true }),
37
+ run(root, ['rev-list', '--count', `${b}..${h}`]),
38
+ run(root, ['rev-list', '--count', `${h}..${b}`]),
39
+ run(root, ['log', `--max-count=${MAX_COMPARE_COMMITS + 1}`, '--format=%H%x1f%h%x1f%an%x1f%aI%x1f%s%x1e', `${b}..${h}`]),
40
+ ]);
41
+ const files = filesFromDiffTree(names.out, nums.out);
42
+ const commits = log.out.split('\x1e').map((r) => r.trim()).filter(Boolean).map((r) => { const [sha, short, author, date, subject] = r.split('\x1f'); return { sha, short, author, date, subject }; });
43
+ return {
44
+ base: { ref: base, sha: b }, head: { ref: head, sha: h }, ahead: Number(ahead.out) || 0, behind: Number(behind.out) || 0,
45
+ commits: commits.slice(0, MAX_COMPARE_COMMITS), commitsTruncated: commits.length > MAX_COMPARE_COMMITS,
46
+ files: files.slice(0, MAX_COMPARE_FILES), truncated: files.length > MAX_COMPARE_FILES,
47
+ };
48
+ }
49
+ export async function gitCompareDiff(cwd, base, head, rel, oldRel) {
50
+ const { root } = await repoOf(cwd);
51
+ const [b, h] = [await resolveRef(root, base), await resolveRef(root, head)];
52
+ await safePath(cwd, root, rel, false);
53
+ if (oldRel)
54
+ await safePath(cwd, root, oldRel, false);
55
+ const { out } = await run(root, ['diff', '--histogram', '--no-color', '--no-ext-diff', '-M', '-U3', b, h, '--', ...(oldRel ? [oldRel, rel] : [rel])]);
56
+ const MAX = 600_000;
57
+ return { path: rel, diff: out.length > MAX ? out.slice(0, MAX) : out, truncated: out.length > MAX, binary: /^Binary files .* differ$/m.test(out) || /^GIT binary patch/m.test(out) };
58
+ }
59
+ const MAX_HITS = 300;
60
+ /** Lines of the working files (tracked and new, never ignored ones) that contain `q`. */
61
+ export async function gitGrep(cwd, q, caseSensitive = false) {
62
+ const { root, scope } = await repoOf(cwd);
63
+ const text = q.trim().slice(0, 200);
64
+ if (text.length < 2)
65
+ return { hits: [], truncated: false };
66
+ const { out } = await run(root, ['grep', '-n', '-I', '--null', '--fixed-strings', '--no-color', '--untracked', '--exclude-standard', ...(caseSensitive ? [] : ['-i']), '-e', text, '--', scope || '.'], { okCodes: [1], raw: true });
67
+ const hits = [];
68
+ let truncated = false;
69
+ for (const l of out.split('\n')) {
70
+ if (!l)
71
+ continue;
72
+ if (hits.length >= MAX_HITS) {
73
+ truncated = true;
74
+ break;
75
+ }
76
+ // With --null both the file name and the line number are followed by a NUL: `path NUL line NUL text`.
77
+ const [path, line, ...text] = l.split('\0');
78
+ if (!path || text.length === 0)
79
+ continue;
80
+ hits.push({ path, line: Number(line) || 0, text: text.join('\0').slice(0, 300) });
81
+ }
82
+ return { hits, truncated };
83
+ }
84
+ /* ------------------------------------------------------------------ pictures at a version */
85
+ const MAX_IMAGE = 20 * 1024 * 1024;
86
+ /** The bytes of a picture as it was at a commit, served only as an inert image (see the route). */
87
+ export async function gitImageAt(cwd, ref, rel) {
88
+ const { root } = await repoOf(cwd);
89
+ const type = imageType(rel);
90
+ if (!type)
91
+ throw new PathError('Not an image');
92
+ const sha = await resolveRef(root, ref);
93
+ if (!rel || rel.includes('\0') || rel.startsWith('/') || rel.split('/').includes('..'))
94
+ throw new PathError('Invalid path');
95
+ const bytes = await new Promise((resolve, reject) => {
96
+ execFile('git', ['cat-file', 'blob', `${sha}:${rel}`], { cwd: root, encoding: 'buffer', maxBuffer: MAX_IMAGE, env: gitEnv(), timeout: 30_000 }, (err, stdout) => (err ? reject(new PathError('Picture not found at that version')) : resolve(stdout)));
97
+ });
98
+ return { bytes, type };
99
+ }