hive-am 1.1.0 → 1.2.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 (261) hide show
  1. package/CHANGELOG.md +83 -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 +103 -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.js +2 -5
  68. package/server/dist/providers/preamble.js +2 -0
  69. package/server/dist/providers/spawn.js +31 -3
  70. package/server/dist/runtime.js +8 -6
  71. package/server/dist/schedules.js +2 -21
  72. package/server/dist/seed.js +15 -11
  73. package/server/dist/services/agents.js +90 -0
  74. package/server/dist/services/colonies.js +38 -0
  75. package/server/dist/services/connections.js +76 -0
  76. package/server/dist/services/notebook.js +22 -0
  77. package/server/dist/services/providers.js +27 -0
  78. package/server/dist/services/schedules.js +18 -0
  79. package/server/dist/sessions-summary.js +51 -0
  80. package/server/dist/skills/defaults.js +37 -0
  81. package/server/dist/skills/notebook.js +3 -8
  82. package/server/dist/terminals.js +177 -0
  83. package/server/dist/wake.js +13 -16
  84. package/server/dist/watch.js +2 -8
  85. package/server/mcp/dispatch.mjs +70 -1
  86. package/web/.next/BUILD_ID +1 -1
  87. package/web/.next/app-build-manifest.json +150 -98
  88. package/web/.next/app-path-routes-manifest.json +7 -5
  89. package/web/.next/build-manifest.json +14 -14
  90. package/web/.next/prerender-manifest.json +67 -43
  91. package/web/.next/react-loadable-manifest.json +26 -1
  92. package/web/.next/required-server-files.json +13 -1
  93. package/web/.next/routes-manifest.json +26 -0
  94. package/web/.next/server/app/_not-found/page.js +2 -2
  95. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  96. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  97. package/web/.next/server/app/_not-found.html +1 -1
  98. package/web/.next/server/app/_not-found.rsc +12 -11
  99. package/web/.next/server/app/agents/[id]/page.js +4 -4
  100. package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
  101. package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
  102. package/web/.next/server/app/agents/page.js +2 -2
  103. package/web/.next/server/app/agents/page.js.nft.json +1 -1
  104. package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  105. package/web/.next/server/app/agents.html +1 -1
  106. package/web/.next/server/app/agents.rsc +14 -13
  107. package/web/.next/server/app/connections/page.js +2 -2
  108. package/web/.next/server/app/connections/page.js.nft.json +1 -1
  109. package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  110. package/web/.next/server/app/connections.html +1 -1
  111. package/web/.next/server/app/connections.rsc +14 -13
  112. package/web/.next/server/app/index.html +1 -1
  113. package/web/.next/server/app/index.rsc +14 -13
  114. package/web/.next/server/app/page.js +2 -2
  115. package/web/.next/server/app/page.js.nft.json +1 -1
  116. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  117. package/web/.next/server/app/relations/page.js +2 -2
  118. package/web/.next/server/app/relations/page.js.nft.json +1 -1
  119. package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
  120. package/web/.next/server/app/relations.html +1 -1
  121. package/web/.next/server/app/relations.rsc +14 -13
  122. package/web/.next/server/app/resources/[id]/page.js +2 -0
  123. package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -0
  124. package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -0
  125. package/web/.next/server/app/resources/page.js +2 -0
  126. package/web/.next/server/app/resources/page.js.nft.json +1 -0
  127. package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -0
  128. package/web/.next/server/app/resources.html +1 -0
  129. package/web/.next/server/app/resources.meta +15 -0
  130. package/web/.next/server/app/resources.rsc +25 -0
  131. package/web/.next/server/app/schedules/page.js +2 -2
  132. package/web/.next/server/app/schedules/page.js.nft.json +1 -1
  133. package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
  134. package/web/.next/server/app/schedules.html +1 -1
  135. package/web/.next/server/app/schedules.rsc +14 -13
  136. package/web/.next/server/app/sessions/page.js +2 -2
  137. package/web/.next/server/app/sessions/page.js.nft.json +1 -1
  138. package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  139. package/web/.next/server/app/sessions.html +1 -1
  140. package/web/.next/server/app/sessions.rsc +14 -13
  141. package/web/.next/server/app/skills/page.js +2 -2
  142. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  143. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  144. package/web/.next/server/app/skills.html +1 -1
  145. package/web/.next/server/app/skills.rsc +14 -13
  146. package/web/.next/server/app/types/page.js +2 -2
  147. package/web/.next/server/app/types/page.js.nft.json +1 -1
  148. package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
  149. package/web/.next/server/app/types.html +1 -1
  150. package/web/.next/server/app/types.rsc +14 -13
  151. package/web/.next/server/app-paths-manifest.json +7 -5
  152. package/web/.next/server/chunks/143.js +1 -1
  153. package/web/.next/server/chunks/274.js +1 -0
  154. package/web/.next/server/chunks/306.js +1 -0
  155. package/web/.next/server/chunks/423.js +1 -0
  156. package/web/.next/server/chunks/505.js +6 -0
  157. package/web/.next/server/chunks/555.js +1 -0
  158. package/web/.next/server/chunks/612.js +1 -0
  159. package/web/.next/server/chunks/638.js +1 -0
  160. package/web/.next/server/chunks/692.js +18 -0
  161. package/web/.next/server/chunks/716.js +2 -0
  162. package/web/.next/server/chunks/790.js +11 -0
  163. package/web/.next/server/chunks/802.js +1 -0
  164. package/web/.next/server/chunks/890.js +1 -1
  165. package/web/.next/server/chunks/919.js +1 -0
  166. package/web/.next/server/chunks/950.js +6 -0
  167. package/web/.next/server/middleware-build-manifest.js +1 -1
  168. package/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  169. package/web/.next/server/next-font-manifest.js +1 -1
  170. package/web/.next/server/next-font-manifest.json +1 -1
  171. package/web/.next/server/pages/404.html +1 -1
  172. package/web/.next/server/pages/500.html +1 -1
  173. package/web/.next/server/server-reference-manifest.json +1 -1
  174. package/web/.next/static/chunks/0937d497-114a2a3de515565e.js +1 -0
  175. package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +1 -0
  176. package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +1 -0
  177. package/web/.next/static/chunks/2b9ff8ee-969e65cc354b2150.js +1 -0
  178. package/web/.next/static/chunks/30-9adcbe92b15a6815.js +1 -0
  179. package/web/.next/static/chunks/316-26da9cc233ba7557.js +1 -0
  180. package/web/.next/static/chunks/350-56599dc3bf06066d.js +1 -0
  181. package/web/.next/static/chunks/486-13e08542f3a1cbac.js +1 -0
  182. package/web/.next/static/chunks/491-6b4e6201f5110dff.js +1 -0
  183. package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +1 -0
  184. package/web/.next/static/chunks/545.bfbd92b7632c09de.js +1 -0
  185. package/web/.next/static/chunks/{805-d946ebedb7e85f73.js → 562-7c2e33d838e869bf.js} +1 -1
  186. package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +1 -0
  187. package/web/.next/static/chunks/586-f7017d585a84738d.js +1 -0
  188. package/web/.next/static/chunks/657.b4c39e676c3e20dc.js +1 -0
  189. package/web/.next/static/chunks/701.6abcaf5e60463eef.js +6 -0
  190. package/web/.next/static/chunks/705-a16e1fe09be1ac7f.js +1 -0
  191. package/web/.next/static/chunks/706-fa2be0f10ffebe6a.js +1 -0
  192. package/web/.next/static/chunks/837-33c7480524f48577.js +1 -0
  193. package/web/.next/static/chunks/839-23c5508622fae942.js +1 -0
  194. package/web/.next/static/chunks/844-fe55292e0701f436.js +1 -0
  195. package/web/.next/static/chunks/89-b194fcffe5120c44.js +1 -0
  196. package/web/.next/static/chunks/97.594543c2196de524.js +1 -0
  197. package/web/.next/static/chunks/app/_not-found/page-238a7b80bda6b4ae.js +1 -0
  198. package/web/.next/static/chunks/app/agents/[id]/page-4af6c246c4ca7a97.js +1 -0
  199. package/web/.next/static/chunks/app/agents/page-f4c4c80f955f3967.js +1 -0
  200. package/web/.next/static/chunks/app/connections/page-0936beaf02d863c6.js +1 -0
  201. package/web/.next/static/chunks/app/layout-87ebb70bd9cb8443.js +1 -0
  202. package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +1 -0
  203. package/web/.next/static/chunks/app/relations/page-fba25393c8688714.js +1 -0
  204. package/web/.next/static/chunks/app/resources/[id]/page-d110d89bb8b5fca7.js +1 -0
  205. package/web/.next/static/chunks/app/resources/page-bbd0b450d3f3131d.js +1 -0
  206. package/web/.next/static/chunks/app/schedules/page-a1124397fc760413.js +1 -0
  207. package/web/.next/static/chunks/app/sessions/page-0e322811b0cde724.js +1 -0
  208. package/web/.next/static/chunks/app/skills/page-22ea0ef89d9f7a13.js +1 -0
  209. package/web/.next/static/chunks/app/types/page-18c4e1db2724d41d.js +1 -0
  210. package/web/.next/static/chunks/fcafe1ac.366e4950d3b0d4b7.js +18 -0
  211. package/web/.next/static/chunks/framework-bbac72adbbe2d328.js +1 -0
  212. package/web/.next/static/chunks/main-app-6086068e5fb5eacd.js +1 -0
  213. package/web/.next/static/chunks/main-c15f3fee674e0303.js +1 -0
  214. package/web/.next/static/chunks/pages/_app-d1670a2c6ce392fc.js +1 -0
  215. package/web/.next/static/chunks/pages/_error-95aac014d202fa7a.js +1 -0
  216. package/web/.next/static/chunks/webpack-ef515e41a83a16ac.js +1 -0
  217. package/web/.next/static/css/945795da867f5250.css +1 -0
  218. package/web/.next/static/css/dd4e32c285f75776.css +1 -0
  219. package/web/.next/static/p4S3i9kvO359GW91DQiHu/_buildManifest.js +1 -0
  220. package/web/.next/trace +29 -23
  221. package/web/.next/types/app/resources/[id]/page.ts +84 -0
  222. package/web/.next/types/app/resources/page.ts +84 -0
  223. package/web/.next/types/routes.d.ts +6 -2
  224. package/web/.next/types/validator.ts +18 -0
  225. package/web/next.config.mjs +4 -0
  226. package/web/.next/server/chunks/247.js +0 -1
  227. package/web/.next/server/chunks/484.js +0 -11
  228. package/web/.next/server/chunks/851.js +0 -1
  229. package/web/.next/server/chunks/932.js +0 -2
  230. package/web/.next/static/_z2hBOHhwiR5ytdI_8alh/_buildManifest.js +0 -1
  231. package/web/.next/static/chunks/0937d497-44abb0f9355ace92.js +0 -1
  232. package/web/.next/static/chunks/2b9ff8ee-c3b61ac1e608c57d.js +0 -1
  233. package/web/.next/static/chunks/463-6ae35cdefdc59169.js +0 -1
  234. package/web/.next/static/chunks/493-50d2adf9fd5ec680.js +0 -1
  235. package/web/.next/static/chunks/517-eb26c72f6059aa26.js +0 -1
  236. package/web/.next/static/chunks/589-8bf536f331de4269.js +0 -1
  237. package/web/.next/static/chunks/657.f1e03b7b65eba1d9.js +0 -1
  238. package/web/.next/static/chunks/687-3a4205f51be38602.js +0 -1
  239. package/web/.next/static/chunks/837-fd7623601dc78ed8.js +0 -1
  240. package/web/.next/static/chunks/850-318f32484989dd59.js +0 -1
  241. package/web/.next/static/chunks/89-ace2e1aa63babfdf.js +0 -1
  242. package/web/.next/static/chunks/951-e250b541b67da7ce.js +0 -1
  243. package/web/.next/static/chunks/app/_not-found/page-bbd54d7b9a55d7ae.js +0 -1
  244. package/web/.next/static/chunks/app/agents/[id]/page-a8b7a43d2c18483f.js +0 -1
  245. package/web/.next/static/chunks/app/agents/page-e9b17f1c3439ef98.js +0 -1
  246. package/web/.next/static/chunks/app/connections/page-477f36fcd5b13d78.js +0 -1
  247. package/web/.next/static/chunks/app/layout-a2d38e3de5911b1b.js +0 -1
  248. package/web/.next/static/chunks/app/page-6fc87fc5a128a204.js +0 -1
  249. package/web/.next/static/chunks/app/relations/page-0355a93a8eb67989.js +0 -1
  250. package/web/.next/static/chunks/app/schedules/page-39befd15b3b20586.js +0 -1
  251. package/web/.next/static/chunks/app/sessions/page-beae9a74a07d69e0.js +0 -1
  252. package/web/.next/static/chunks/app/skills/page-4f839ac913e26d32.js +0 -1
  253. package/web/.next/static/chunks/app/types/page-4f6b99af3232f861.js +0 -1
  254. package/web/.next/static/chunks/framework-0e68884e76d9a8d3.js +0 -1
  255. package/web/.next/static/chunks/main-app-d6f73ccd41f325c5.js +0 -1
  256. package/web/.next/static/chunks/main-cd22509db15764e2.js +0 -1
  257. package/web/.next/static/chunks/pages/_app-0f75d689c738a6fc.js +0 -1
  258. package/web/.next/static/chunks/pages/_error-d481f6d49fb4455d.js +0 -1
  259. package/web/.next/static/chunks/webpack-d40de72e52f65f79.js +0 -1
  260. package/web/.next/static/css/da22f0402b1c40ae.css +0 -1
  261. /package/web/.next/static/{_z2hBOHhwiR5ytdI_8alh → p4S3i9kvO359GW91DQiHu}/_ssgManifest.js +0 -0
@@ -0,0 +1,103 @@
1
+ import { createReadStream, realpathSync, statSync } from 'node:fs';
2
+ import { basename, extname, sep } from 'node:path';
3
+ import { agents, resolved } from '../../db.js';
4
+ import { inboxDir, saveUploads } from '../../connections/files.js';
5
+ import { uploadsBlock } from '../../connections/prompt.js';
6
+ import { readHistory } from '../../history/index.js';
7
+ import { liveTurn, notifyAgentsChanged, queueDepth, sendTurn, stopAgent } from '../../runtime.js';
8
+ import { sessionStats } from '../../stats.js';
9
+ import { summaryOf } from '../../sessions-summary.js';
10
+ import { emptyUsage } from '../../pricing.js';
11
+ import { agentOrFail, createAgent, removeAgent, resumeSession, sessionToRead, updateAgent } from '../../services/agents.js';
12
+ import { UPLOAD_LIMIT, body } from '../body.js';
13
+ import { HttpError, bad, notFound } from '../errors.js';
14
+ import { route } from '../router.js';
15
+ import { qpath, text, validateAgentInput } from '../validate.js';
16
+ route('GET', '/api/agents', () => agents.list().map((a) => ({ ...a, queued: queueDepth(a.id), live: !!liveTurn(a.id) })));
17
+ route('POST', '/api/agents', async ({ req }) => createAgent(validateAgentInput(await body(req))));
18
+ route('GET', '/api/agents/:id', ({ params }) => agentOrFail(params[0]));
19
+ route('PATCH', '/api/agents/:id', async ({ req, params }) => updateAgent(params[0], validateAgentInput(await body(req), true)));
20
+ route('DELETE', '/api/agents/:id', ({ params }) => { removeAgent(params[0]); return { ok: true }; });
21
+ route('POST', '/api/agents/:id/messages', async ({ req, params }) => {
22
+ const a = agentOrFail(params[0]);
23
+ const { prompt, files, images } = await body(req, UPLOAD_LIMIT); // `images` is the name this field had at first
24
+ let msg = text(prompt);
25
+ const sent = files ?? images, hasFiles = Array.isArray(sent) && sent.length > 0;
26
+ if (!msg.trim() && !hasFiles)
27
+ throw bad('Message is empty');
28
+ if (hasFiles) {
29
+ try {
30
+ const saved = saveUploads(a.id, sent);
31
+ msg = `${msg.trim() || '(no text)'}${uploadsBlock(saved)}`;
32
+ }
33
+ catch (e) {
34
+ throw bad(e instanceof Error ? e.message : 'The files could not be saved');
35
+ }
36
+ }
37
+ void sendTurn(a.id, msg).catch((e) => console.error('[turn]', e));
38
+ return { accepted: true, prompt: msg };
39
+ });
40
+ // A file that was sent from the chat, for its thumbnail or download: only from this agent's inbox; pictures inline, anything else only as a download.
41
+ route('GET', '/api/agents/:id/inbox', ({ params, url, res }) => {
42
+ const a = agentOrFail(params[0]);
43
+ let root, abs;
44
+ try {
45
+ root = realpathSync(inboxDir(a.id));
46
+ abs = realpathSync(qpath(url));
47
+ }
48
+ catch (e) {
49
+ if (e instanceof HttpError)
50
+ throw e;
51
+ throw notFound('File not found');
52
+ }
53
+ if (!abs.startsWith(root + sep) || !statSync(abs).isFile())
54
+ throw notFound('File not found');
55
+ const image = { '.png': 'image/png', '.jpg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' }[extname(abs).toLowerCase()];
56
+ const shown = basename(abs).replace(/^(?:web-)?\d+-\d+-/, '');
57
+ res.writeHead(200, {
58
+ 'content-type': image ?? 'application/octet-stream', 'content-length': statSync(abs).size, 'cache-control': 'private, max-age=3600', 'x-content-type-options': 'nosniff',
59
+ 'content-security-policy': "sandbox; default-src 'none'; style-src 'unsafe-inline'",
60
+ ...(image ? {} : { 'content-disposition': `attachment; filename*=UTF-8''${encodeURIComponent(shown)}` }),
61
+ });
62
+ createReadStream(abs).pipe(res);
63
+ });
64
+ route('POST', '/api/agents/:id/stop', ({ params }) => ({ stopped: stopAgent(params[0]) }));
65
+ route('POST', '/api/agents/:id/new-session', ({ params }) => {
66
+ const a = agentOrFail(params[0]);
67
+ stopAgent(a.id);
68
+ agents.setSession(a, null);
69
+ notifyAgentsChanged();
70
+ return { ok: true };
71
+ });
72
+ route('POST', '/api/agents/:id/resume-session', async ({ req, params }) => {
73
+ const a = agentOrFail(params[0]);
74
+ resumeSession(a, (await body(req)).session_id);
75
+ return { ok: true };
76
+ });
77
+ route('GET', '/api/agents/:id/history', ({ params, url }) => {
78
+ const a = agentOrFail(params[0]);
79
+ const sid = sessionToRead(a, url.searchParams.get('session'));
80
+ // A long conversation is sent in pieces: `limit` keeps only the newest messages (the browser would freeze rendering thousands).
81
+ const all = readHistory(resolved(a), sid), limit = Number(url.searchParams.get('limit'));
82
+ return { session_id: sid, total: all.length, messages: limit > 0 && all.length > limit ? all.slice(-limit) : all };
83
+ });
84
+ route('GET', '/api/agents/:id/stats', ({ params, url }) => {
85
+ const a = agentOrFail(params[0]);
86
+ return sessionStats(readHistory(resolved(a), sessionToRead(a, url.searchParams.get('session'))));
87
+ });
88
+ route('GET', '/api/agents/:id/live', ({ params }) => liveTurn(params[0]));
89
+ route('GET', '/api/agents/:id/sessions', ({ params }) => agents.sessions(params[0]));
90
+ // ---- sessions across all managed agents ----
91
+ route('GET', '/api/sessions', () => {
92
+ const byAgent = new Map(agents.list().map((a) => [a.id, a]));
93
+ // Never reads a conversation here: sizes and costs come from a cache that fills in the background (`pending` rows are still being read).
94
+ return agents.allSessions().map((s) => {
95
+ const a = byAgent.get(s.agent_id);
96
+ const { summary, fresh } = summaryOf({ provider: s.provider, cwd: s.cwd, session_id: s.session_id });
97
+ return {
98
+ ...s, current: a.session_id === s.session_id, pending: !fresh,
99
+ message_count: summary?.message_count ?? 0, usage: summary?.usage ?? emptyUsage(), cost: summary?.cost ?? null,
100
+ tool_calls: summary?.tool_calls ?? 0, model: summary?.model ?? null, preview: summary?.preview ?? '',
101
+ };
102
+ });
103
+ });
@@ -0,0 +1,16 @@
1
+ import { channelMute, channelReact, channelReply, channelSendFile } from '../../connections/manager.js';
2
+ import { scheduleWake } from '../../wake.js';
3
+ import { createWatch } from '../../watch.js';
4
+ import { body } from '../body.js';
5
+ import { route } from '../router.js';
6
+ import { text } from '../validate.js';
7
+ // What an agent does in the chat it is answering, and its timers (`from` is the agent id). The domain errors become 400 in http/errors.ts.
8
+ route('POST', '/api/channel/reply', async ({ req }) => { const { from, text: t } = await body(req); return channelReply(text(from), text(t)); });
9
+ route('POST', '/api/channel/send-file', async ({ req }) => {
10
+ const { from, path, caption } = await body(req);
11
+ return channelSendFile(text(from), text(path), caption === undefined ? undefined : String(caption));
12
+ });
13
+ route('POST', '/api/channel/react', async ({ req }) => { const { from, emoji } = await body(req); return channelReact(text(from), emoji); });
14
+ route('POST', '/api/channel/mute', async ({ req }) => { const { from, muted } = await body(req); return channelMute(text(from), muted !== false); });
15
+ route('POST', '/api/wake', async ({ req }) => { const { from, minutes, note } = await body(req); return scheduleWake(text(from), minutes, note); });
16
+ route('POST', '/api/wake-when-done', async ({ req }) => { const b = await body(req); return createWatch(text(b.from), b); });
@@ -0,0 +1,9 @@
1
+ import { colonies } from '../../db.js';
2
+ import { createColony, removeColony, updateColony } from '../../services/colonies.js';
3
+ import { body } from '../body.js';
4
+ import { route } from '../router.js';
5
+ import { validateColony } from '../validate.js';
6
+ route('GET', '/api/colonies', () => colonies.list());
7
+ route('POST', '/api/colonies', async ({ req }) => createColony(validateColony(await body(req))));
8
+ route('PATCH', '/api/colonies/:id', async ({ req, params }) => updateColony(params[0], validateColony(await body(req), true)));
9
+ route('DELETE', '/api/colonies/:id', ({ params }) => { removeColony(params[0]); return { ok: true }; });
@@ -0,0 +1,36 @@
1
+ import { connections, threads } from '../../connections/store.js';
2
+ import { publicConnection } from '../../connections/public.js';
3
+ import { testConnection } from '../../connections/manager.js';
4
+ import { createConnection, removeConnection, restartConnection, updateConnection } from '../../services/connections.js';
5
+ import { body } from '../body.js';
6
+ import { bad, notFound } from '../errors.js';
7
+ import { route } from '../router.js';
8
+ route('GET', '/api/connections', () => connections.list().map(publicConnection));
9
+ route('POST', '/api/connections', async ({ req }) => createConnection(await body(req)));
10
+ route('PATCH', '/api/connections/:id', async ({ req, params }) => updateConnection(params[0], await body(req)));
11
+ route('DELETE', '/api/connections/:id', async ({ params }) => { await removeConnection(params[0]); return { ok: true }; });
12
+ route('POST', '/api/connections/:id/restart', ({ params }) => restartConnection(params[0]));
13
+ route('POST', '/api/connections/:id/test', async ({ params }) => {
14
+ if (!connections.get(params[0]))
15
+ throw notFound('Connection not found');
16
+ try {
17
+ return { ok: true, detail: await testConnection(params[0]) };
18
+ }
19
+ catch (e) {
20
+ throw bad(e instanceof Error ? e.message : String(e));
21
+ }
22
+ });
23
+ route('PATCH', '/api/connections/:id/threads/:threadId', async ({ req, params }) => {
24
+ const th = threads.get(params[1]);
25
+ if (!th || th.connection_id !== params[0])
26
+ throw notFound('Thread not found');
27
+ const p = await body(req);
28
+ if (typeof p.muted === 'boolean')
29
+ threads.setMuted(th.id, p.muted);
30
+ return threads.get(th.id);
31
+ });
32
+ route('GET', '/api/connections/:id/threads', ({ params }) => {
33
+ if (!connections.get(params[0]))
34
+ throw notFound('Connection not found');
35
+ return threads.forConnection(params[0]);
36
+ });
@@ -0,0 +1,22 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { bad } from '../errors.js';
5
+ import { route } from '../router.js';
6
+ // ---- folder picker ----
7
+ route('GET', '/api/fs/dirs', ({ url }) => {
8
+ const p = resolve(url.searchParams.get('path') || homedir());
9
+ if (!existsSync(p) || !statSync(p).isDirectory())
10
+ throw bad('Not a folder');
11
+ // `files=yml,yaml` also lists the files with those extensions (to pick a file, e.g. a compose file).
12
+ const exts = (url.searchParams.get('files') ?? '').split(',').map((x) => x.trim().toLowerCase().replace(/^\./, '')).filter((x) => /^[a-z0-9]{1,8}$/.test(x));
13
+ let dirs = [], files = [];
14
+ try {
15
+ const all = readdirSync(p, { withFileTypes: true });
16
+ dirs = all.filter((d) => d.isDirectory() && !d.name.startsWith('.') && d.name !== 'node_modules').map((d) => d.name).sort((a, b) => a.localeCompare(b));
17
+ if (exts.length)
18
+ files = all.filter((d) => d.isFile() && exts.includes(d.name.split('.').pop().toLowerCase())).map((d) => d.name).sort((a, b) => a.localeCompare(b));
19
+ }
20
+ catch { /* unreadable */ }
21
+ return { path: p, parent: p === '/' ? null : dirname(p), dirs: dirs.map((n) => ({ name: n, path: join(p, n) })), files: files.map((n) => ({ name: n, path: join(p, n) })) };
22
+ });
@@ -0,0 +1,90 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { agents } from '../../db.js';
3
+ import { gitDiff, gitFile, gitImagePath, gitList, gitStatus, gitTree } from '../../git/repo.js';
4
+ import { listStashes, planSwitch, smartCancel, smartFinish, smartSwitch, stashApply, stashDetail, stashDrop, stashSave } from '../../git/switch.js';
5
+ import { gitCompare, gitCompareDiff, gitGrep, gitImageAt, gitRefs } from '../../git/browse.js';
6
+ import { gitBlame, gitBranchCreate, gitBranchDelete, gitRefFile, gitBranches, gitCommitDetail, gitCommitChanges, gitCommitDiff, gitDiscardAll, gitDiscardFile, gitDiscardHunk, gitDiscardLines, gitFetch, gitLog, gitMerge, gitMergeAbort, gitPull, gitPush, gitRebaseContinue, gitResolveContent, gitResolveSide, gitSwitch, gitTagFile, gitTagTree, gitTags, gitUnresolve } from '../../git/ops.js';
7
+ import { agentCwd, othersInRepo } from '../../services/agents.js';
8
+ import { body } from '../body.js';
9
+ import { requireLocal } from '../guards.js';
10
+ import { route } from '../router.js';
11
+ import { numList, qpath, text } from '../validate.js';
12
+ // Errors from git (GitOpError, PathError) become 400 in http/errors.ts. Everything here is scoped to the agent's effective folder.
13
+ const CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'";
14
+ const q = (url, key) => url.searchParams.get(key) ?? '';
15
+ const old = (url) => url.searchParams.get('old') ?? undefined;
16
+ // ---- read-only explorer ----
17
+ route('GET', '/api/agents/:id/git/status', ({ params }) => gitStatus(agentCwd(params[0])));
18
+ route('GET', '/api/agents/:id/git/ls', ({ params, url }) => gitList(agentCwd(params[0]), qpath(url)));
19
+ route('GET', '/api/agents/:id/git/tree', ({ params }) => gitTree(agentCwd(params[0])));
20
+ route('GET', '/api/agents/:id/git/diff', ({ params, url }) => gitDiff(agentCwd(params[0]), qpath(url), old(url)));
21
+ route('GET', '/api/agents/:id/git/file', ({ params, url }) => gitFile(agentCwd(params[0]), qpath(url)));
22
+ route('GET', '/api/agents/:id/git/raw', async ({ params, url, res }) => {
23
+ const img = await gitImagePath(agentCwd(params[0]), qpath(url));
24
+ // Served only as an inert image: no sniffing, and a sandbox policy so an SVG can never run scripts.
25
+ res.writeHead(200, { 'content-type': img.type, 'content-length': img.size, 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', 'content-security-policy': CSP });
26
+ createReadStream(img.abs).pipe(res);
27
+ });
28
+ // ---- history and actions (writes: commit, pull, push, fetch, branches) ----
29
+ /** The API allows any origin (it is a local tool), so writes also check that the request comes from a local page. */
30
+ const gitWrite = (path, fn) => route('POST', `/api/agents/:id/git/${path}`, async ({ req, params }) => {
31
+ requireLocal(req, 'Git actions are only allowed from the local app');
32
+ const b = await body(req);
33
+ return fn(agentCwd(params[0]), b);
34
+ });
35
+ route('GET', '/api/agents/:id/git/log', ({ params, url }) => {
36
+ const by = url.searchParams.get('by');
37
+ return gitLog(agentCwd(params[0]), Number(url.searchParams.get('skip') ?? 0), {
38
+ path: url.searchParams.get('path') ?? undefined, q: url.searchParams.get('q') ?? undefined, ref: url.searchParams.get('ref') ?? undefined,
39
+ by: by === 'author' || by === 'content' ? by : 'message',
40
+ });
41
+ });
42
+ route('GET', '/api/agents/:id/git/commit', ({ params, url }) => gitCommitDetail(agentCwd(params[0]), q(url, 'sha')));
43
+ route('GET', '/api/agents/:id/git/commit-diff', ({ params, url }) => gitCommitDiff(agentCwd(params[0]), q(url, 'sha'), qpath(url), old(url)));
44
+ route('GET', '/api/agents/:id/git/blame', ({ params, url }) => gitBlame(agentCwd(params[0]), qpath(url)));
45
+ // Smart switch: what would happen, plus which other agents are working in the same repository right now (a switch moves their files too).
46
+ route('GET', '/api/agents/:id/git/switch-plan', async ({ params, url }) => {
47
+ const cwd = agentCwd(params[0]);
48
+ const plan = await planSwitch(cwd, q(url, 'branch'));
49
+ const others = await othersInRepo(params[0], cwd);
50
+ return { ...plan, others, selfRunning: agents.get(params[0])?.status === 'running' };
51
+ });
52
+ route('GET', '/api/agents/:id/git/stashes', ({ params }) => listStashes(agentCwd(params[0])));
53
+ route('GET', '/api/agents/:id/git/stash', ({ params, url }) => stashDetail(agentCwd(params[0]), q(url, 'sha')));
54
+ route('GET', '/api/agents/:id/git/refs', ({ params }) => gitRefs(agentCwd(params[0])));
55
+ route('GET', '/api/agents/:id/git/compare', ({ params, url }) => gitCompare(agentCwd(params[0]), q(url, 'base'), q(url, 'head')));
56
+ route('GET', '/api/agents/:id/git/compare-diff', ({ params, url }) => gitCompareDiff(agentCwd(params[0]), q(url, 'base'), q(url, 'head'), qpath(url), old(url)));
57
+ route('GET', '/api/agents/:id/git/grep', ({ params, url }) => gitGrep(agentCwd(params[0]), q(url, 'q'), url.searchParams.get('case') === '1'));
58
+ route('GET', '/api/agents/:id/git/ref-file', ({ params, url }) => gitRefFile(agentCwd(params[0]), q(url, 'ref'), qpath(url)));
59
+ route('GET', '/api/agents/:id/git/raw-at', async ({ params, url, res }) => {
60
+ const img = await gitImageAt(agentCwd(params[0]), q(url, 'ref'), qpath(url));
61
+ res.writeHead(200, { 'content-type': img.type, 'content-length': img.bytes.length, 'cache-control': 'private, max-age=3600', 'x-content-type-options': 'nosniff', 'content-security-policy': CSP });
62
+ res.end(img.bytes);
63
+ });
64
+ route('GET', '/api/agents/:id/git/tags', ({ params }) => gitTags(agentCwd(params[0])));
65
+ route('GET', '/api/agents/:id/git/tag-tree', ({ params, url }) => gitTagTree(agentCwd(params[0]), q(url, 'tag')));
66
+ route('GET', '/api/agents/:id/git/tag-file', ({ params, url }) => gitTagFile(agentCwd(params[0]), q(url, 'tag'), qpath(url)));
67
+ route('GET', '/api/agents/:id/git/branches', ({ params }) => gitBranches(agentCwd(params[0])));
68
+ gitWrite('commit', (cwd, b) => gitCommitChanges(cwd, text(b.message), Array.isArray(b.paths) ? b.paths.map(String) : []));
69
+ gitWrite('fetch', (cwd) => gitFetch(cwd));
70
+ gitWrite('pull', (cwd, b) => gitPull(cwd, (['ff-only', 'merge', 'rebase'].includes(b.mode) ? b.mode : 'ff-only')));
71
+ gitWrite('push', (cwd) => gitPush(cwd));
72
+ gitWrite('switch', (cwd, b) => gitSwitch(cwd, text(b.branch), !!b.create));
73
+ gitWrite('branch-create', (cwd, b) => gitBranchCreate(cwd, text(b.branch), text(b.from)));
74
+ gitWrite('branch-delete', (cwd, b) => gitBranchDelete(cwd, text(b.branch), !!b.force));
75
+ gitWrite('merge', (cwd, b) => gitMerge(cwd, text(b.branch)));
76
+ gitWrite('merge-abort', (cwd) => gitMergeAbort(cwd));
77
+ gitWrite('switch-smart', (cwd, b) => smartSwitch(cwd, text(b.branch)));
78
+ gitWrite('smart-finish', (cwd) => smartFinish(cwd));
79
+ gitWrite('smart-cancel', (cwd) => smartCancel(cwd));
80
+ gitWrite('stash-save', (cwd, b) => stashSave(cwd, text(b.message)));
81
+ gitWrite('stash-apply', (cwd, b) => stashApply(cwd, text(b.sha), !!b.pop));
82
+ gitWrite('stash-drop', (cwd, b) => stashDrop(cwd, text(b.sha)));
83
+ gitWrite('discard', (cwd, b) => gitDiscardFile(cwd, text(b.path), b.oldPath ? String(b.oldPath) : undefined));
84
+ gitWrite('discard-hunk', (cwd, b) => gitDiscardHunk(cwd, text(b.path), Number(b.index), text(b.header)));
85
+ gitWrite('discard-lines', (cwd, b) => gitDiscardLines(cwd, text(b.path), Number(b.index), text(b.header), numList(b.lines)));
86
+ gitWrite('discard-all', (cwd) => gitDiscardAll(cwd));
87
+ gitWrite('rebase-continue', (cwd) => gitRebaseContinue(cwd));
88
+ gitWrite('resolve-side', (cwd, b) => gitResolveSide(cwd, text(b.path), b.side));
89
+ gitWrite('resolve', (cwd, b) => gitResolveContent(cwd, text(b.path), text(b.content), !!b.keepMarkers));
90
+ gitWrite('unresolve', (cwd, b) => gitUnresolve(cwd, text(b.path)));
@@ -0,0 +1,11 @@
1
+ import { listModels } from '../../models.js';
2
+ import { isProvider, providerStatus } from '../../services/providers.js';
3
+ import { notFound } from '../errors.js';
4
+ import { route } from '../router.js';
5
+ route('GET', '/api/health', () => ({ ok: true }));
6
+ route('GET', '/api/providers', () => providerStatus());
7
+ route('GET', '/api/providers/:p/models', async ({ params }) => {
8
+ if (!isProvider(params[0]))
9
+ throw notFound('Unknown provider');
10
+ return listModels(params[0]);
11
+ });
@@ -0,0 +1,34 @@
1
+ import { liveIsDirect } from '../../runtime.js';
2
+ import { notebooks } from '../../skills/notebook.js';
3
+ import { NOTEBOOK_SKILL_ID } from '../../skills/defaults.js';
4
+ import { nbView, noteSource, notebookAgent, notebookEnabled } from '../../services/notebook.js';
5
+ import { agentOrFail } from '../../services/agents.js';
6
+ import { body } from '../body.js';
7
+ import { requireLocal } from '../guards.js';
8
+ import { route } from '../router.js';
9
+ import { text } from '../validate.js';
10
+ // The agent's tools (`from` is its id) and the interface. NotebookError becomes a 400 in http/errors.ts.
11
+ route('POST', '/api/notebook/read', async ({ req }) => { const { from } = await body(req); return nbView(notebooks.get(notebookAgent(from).id)); });
12
+ route('POST', '/api/notebook/add', async ({ req }) => {
13
+ const { from, section, note } = await body(req);
14
+ const a = notebookAgent(from);
15
+ const r = notebooks.add(a.id, text(section), text(note), noteSource(a.id), liveIsDirect(a.id));
16
+ return { added: r.added, note: r.note, ...nbView(r.notebook) };
17
+ });
18
+ route('POST', '/api/notebook/rewrite', async ({ req }) => {
19
+ const { from, content, version } = await body(req);
20
+ const a = notebookAgent(from);
21
+ return { added: true, ...nbView(notebooks.rewrite(a.id, text(content), Number(version), liveIsDirect(a.id))) };
22
+ });
23
+ route('GET', '/api/agents/:id/notebook', ({ params }) => {
24
+ const a = agentOrFail(params[0]);
25
+ const n = notebooks.get(a.id);
26
+ return { ...nbView(n), updated_at: n.updated_at, updated_by: n.updated_by, enabled: notebookEnabled(a), skill_id: NOTEBOOK_SKILL_ID };
27
+ });
28
+ route('PUT', '/api/agents/:id/notebook', async ({ req, params }) => {
29
+ requireLocal(req, 'The notebook can only be edited from the local app');
30
+ const a = agentOrFail(params[0]);
31
+ const p = await body(req);
32
+ const n = notebooks.edit(a.id, text(p.content), typeof p.version === 'number' ? p.version : undefined);
33
+ return { ...nbView(n), updated_at: n.updated_at, updated_by: n.updated_by };
34
+ });
@@ -0,0 +1,32 @@
1
+ import { agents, dispatches } from '../../db.js';
2
+ import { dispatch, liveTurn, notifyAgentsChanged } from '../../runtime.js';
3
+ import { agentOrFail } from '../../services/agents.js';
4
+ import { body } from '../body.js';
5
+ import { bad } from '../errors.js';
6
+ import { route } from '../router.js';
7
+ import { strList } from '../validate.js';
8
+ route('GET', '/api/orchestrators/:id/workers', ({ params }) => {
9
+ const o = agentOrFail(params[0]);
10
+ return o.worker_ids.map((id) => agents.get(id)).filter((w) => !!w).map((w) => ({ name: w.name, role: w.role, description: w.description, provider: w.provider, busy: !!liveTurn(w.id) }));
11
+ });
12
+ route('PUT', '/api/orchestrators/:id/workers', async ({ req, params }) => {
13
+ const o = agentOrFail(params[0]);
14
+ if (o.role !== 'orchestrator')
15
+ throw bad('Only orchestrators can have subagents');
16
+ const { worker_ids } = await body(req);
17
+ agents.setWorkers(o.id, worker_ids === undefined || worker_ids === null ? [] : strList(worker_ids));
18
+ notifyAgentsChanged();
19
+ return agents.get(o.id);
20
+ });
21
+ route('POST', '/api/dispatch', async ({ req }) => {
22
+ const { from, agent, task } = await body(req);
23
+ if (!from || !agent || !task)
24
+ throw bad('from, agent and task are required');
25
+ try {
26
+ return await dispatch(from, agent, task);
27
+ }
28
+ catch (e) {
29
+ throw bad(e instanceof Error ? e.message : String(e));
30
+ }
31
+ });
32
+ route('GET', '/api/dispatches', () => dispatches.recent());
@@ -0,0 +1,68 @@
1
+ import { notifyAgentsChanged } from '../../runtime.js';
2
+ import { DRIVER_PROPS } from '../../objects/driver-props.js';
3
+ import { composeServices, listContainers, listImages } from '../../objects/docker-info.js';
4
+ import { agentObjectAction, agentObjectLogs, agentObjects } from '../../objects/agent.js';
5
+ import { dbColumnsOf, dbHistory, dbHistoryClear, dbRun, dbSavedAdd, dbSavedList, dbSavedRemove, dbSavedUpdate, dbSchemaOf, dbTestConfig, createObject, statsOf, httpDescribe, httpSession, httpSessionClear, httpSessionSet, httpInspect, httpExport, httpFileRead, httpFileWrite, httpFileCreate, httpRun, httpHistory, httpDocs, httpHistoryClear, httpScan, listObjects, setObjectManager, setAgentObjects, objectAction, objectServiceAction, listTemplates, createFromTemplate, objectInspect, objectLogs, removeObject, updateObject } from '../../objects/index.js';
6
+ import { body } from '../body.js';
7
+ import { notFound } from '../errors.js';
8
+ import { requireLocal } from '../guards.js';
9
+ import { route } from '../router.js';
10
+ import { text } from '../validate.js';
11
+ // ObjectError becomes a 400 in http/errors.ts. "Object" is the old name of "resource": the paths keep it.
12
+ const p = (url, key) => url.searchParams.get(key) ?? undefined;
13
+ /** Resources run commands and open connections with saved passwords, so, like the git actions, they only accept requests from the local app. */
14
+ const local = (req) => requireLocal(req, 'Resources can only be changed from the local app');
15
+ /** A route that changes things: local app only, with the JSON body already read. */
16
+ const write = (method, path, fn) => route(method, path, async (ctx) => { local(ctx.req); return fn(await body(ctx.req), ctx); });
17
+ /** A route that answers from a resource but is still local only (it reaches a database with its saved password). */
18
+ const guarded = (path, fn, method = 'GET') => route(method, path, (ctx) => { local(ctx.req); return fn(ctx); });
19
+ // The agents' side (tools `object_list`, `object_action`, `object_logs`): `from` is the agent; scope and permission are checked in objects/agent.ts.
20
+ route('POST', '/api/agent-objects/list', async ({ req }) => { const b = await body(req); return { objects: agentObjects(b.from) }; });
21
+ route('POST', '/api/agent-objects/action', async ({ req }) => { const b = await body(req); return agentObjectAction(b.from, b.name, b.action); });
22
+ route('POST', '/api/agent-objects/logs', async ({ req }) => { const b = await body(req); return agentObjectLogs(b.from, b.name, b.tail); });
23
+ route('GET', '/api/objects', () => listObjects());
24
+ write('POST', '/api/objects', (b) => createObject(b));
25
+ write('PATCH', '/api/objects/:id', (b, { params }) => updateObject(params[0], b));
26
+ route('DELETE', '/api/objects/:id', async ({ req, params }) => { local(req); if (!(await removeObject(params[0])))
27
+ throw notFound('Object not found'); return { ok: true }; });
28
+ route('GET', '/api/object-templates', () => listTemplates());
29
+ write('PUT', '/api/objects/:id/manager', (b, { params }) => { const r = setObjectManager(params[0], b.agent_id); if (r.skillAdded)
30
+ notifyAgentsChanged(); return r; });
31
+ write('PUT', '/api/agents/:id/resources', (b, { params }) => { const r = setAgentObjects(params[0], b.object_ids); if (r.skillAdded)
32
+ notifyAgentsChanged(); return r; });
33
+ write('POST', '/api/objects/from-template', (b) => createFromTemplate(b));
34
+ route('GET', '/api/objects/:id/inspect', ({ params, url }) => objectInspect(params[0], p(url, 'service')));
35
+ write('POST', '/api/objects/:id/service', (b, { params }) => objectServiceAction(params[0], b.service, b.action));
36
+ write('POST', '/api/objects/:id/action', (b, { params }) => objectAction(params[0], b.action));
37
+ // What Docker knows, to suggest in the forms (containers, images, the services of a compose file).
38
+ route('GET', '/api/docker/containers', () => listContainers());
39
+ route('GET', '/api/docker/images', () => listImages());
40
+ route('GET', '/api/docker/compose-services', ({ url }) => composeServices(url.searchParams.get('file') ?? ''));
41
+ route('GET', '/api/objects/:id/stats', ({ params }) => statsOf(params[0]));
42
+ route('GET', '/api/objects/:id/http', ({ params }) => httpScan(params[0]));
43
+ route('GET', '/api/objects/:id/http/export', ({ params, url }) => httpExport(params[0], url.searchParams.get('file') ?? '', Number(url.searchParams.get('index')), (url.searchParams.get('format') ?? 'curl'), p(url, 'env'), url.searchParams.get('redact') === '1'));
44
+ route('GET', '/api/objects/:id/http/variables', ({ params, url }) => httpInspect(params[0], p(url, 'file'), url.searchParams.has('index') ? Number(url.searchParams.get('index')) : undefined, p(url, 'env')));
45
+ route('GET', '/api/objects/:id/http/file', ({ params, url }) => httpFileRead(params[0], url.searchParams.get('file') ?? ''));
46
+ write('POST', '/api/objects/:id/http/file', (b, { params }) => httpFileCreate(params[0], text(b.file), text(b.content)));
47
+ write('PUT', '/api/objects/:id/http/file', (b, { params }) => httpFileWrite(params[0], text(b.file), text(b.content)));
48
+ route('GET', '/api/objects/:id/http/session', ({ params }) => httpSession(params[0]));
49
+ write('POST', '/api/objects/:id/http/session', (b, { params }) => httpSessionSet(params[0], text(b.name), text(b.value)));
50
+ guarded('/api/objects/:id/http/session', ({ params, url }) => httpSessionClear(params[0], p(url, 'name')), 'DELETE');
51
+ route('GET', '/api/objects/:id/http/describe', ({ params, url }) => httpDescribe(params[0], url.searchParams.get('file') ?? '', Number(url.searchParams.get('index') ?? 0), p(url, 'env')));
52
+ route('GET', '/api/objects/:id/http/docs', ({ params, url }) => httpDocs(params[0], url.searchParams.get('format') === 'postman' ? 'postman' : 'markdown', p(url, 'file')));
53
+ route('GET', '/api/objects/:id/http/history', ({ params }) => httpHistory(params[0]));
54
+ guarded('/api/objects/:id/http/history', ({ params }) => httpHistoryClear(params[0]), 'DELETE');
55
+ write('POST', '/api/objects/:id/http/run', (b, { params }) => httpRun(params[0], text(b.file), Number(b.index ?? 0), b.env ? String(b.env) : undefined));
56
+ // ---- database resources (a connection opens per request; local app only, because it uses the saved password) ----
57
+ route('GET', '/api/object-db/props', () => DRIVER_PROPS);
58
+ write('POST', '/api/object-db/test', (b) => dbTestConfig(b));
59
+ guarded('/api/objects/:id/db/schema', ({ params, url }) => dbSchemaOf(params[0], p(url, 'database')));
60
+ guarded('/api/objects/:id/db/columns', ({ params, url }) => dbColumnsOf(params[0], { database: p(url, 'database'), schema: url.searchParams.get('schema') ?? '', table: url.searchParams.get('table') ?? '' }));
61
+ write('POST', '/api/objects/:id/db/query', (b, { params }) => dbRun(params[0], { sql: text(b.sql), database: b.database ? String(b.database) : undefined }));
62
+ guarded('/api/objects/:id/db/history', ({ params }) => dbHistory(params[0]));
63
+ guarded('/api/objects/:id/db/history', ({ params }) => dbHistoryClear(params[0]), 'DELETE');
64
+ guarded('/api/objects/:id/db/saved', ({ params }) => dbSavedList(params[0]));
65
+ write('POST', '/api/objects/:id/db/saved', (b, { params }) => dbSavedAdd(params[0], b));
66
+ write('PATCH', '/api/objects/:id/db/saved/:sid', (b, { params }) => dbSavedUpdate(params[0], params[1], b));
67
+ guarded('/api/objects/:id/db/saved/:sid', ({ params }) => dbSavedRemove(params[0], params[1]), 'DELETE');
68
+ route('GET', '/api/objects/:id/logs', ({ params, url }) => objectLogs(params[0], { tail: url.searchParams.get('tail') ? Number(url.searchParams.get('tail')) : undefined, after: p(url, 'after') }));
@@ -0,0 +1,40 @@
1
+ import { cancelFor, createSchedule, listFor, removeSchedule, runsOf, setEnabled } from '../../schedules.js';
2
+ import { removeWakeup } from '../../wake.js';
3
+ import { WatchError, cancelWatchFor, removeWatch } from '../../watch.js';
4
+ import { allSchedules } from '../../services/schedules.js';
5
+ import { body } from '../body.js';
6
+ import { notFound } from '../errors.js';
7
+ import { route } from '../router.js';
8
+ import { text } from '../validate.js';
9
+ // Recurring schedules and pending wake-ups: the agent's tools (`from` is its id) and the interface. ScheduleError becomes a 400 in http/errors.ts.
10
+ route('POST', '/api/schedules/create', async ({ req }) => {
11
+ const b = await body(req);
12
+ const r = createSchedule(text(b.from), b);
13
+ return { id: r.schedule.id, schedule: r.schedule.kind === 'every' ? `every ${r.schedule.expr} min` : `${r.schedule.expr} (${r.schedule.tz})`, next: r.next, where: r.where };
14
+ });
15
+ route('POST', '/api/schedules/list', async ({ req }) => { const b = await body(req); return { schedules: listFor(text(b.from)) }; });
16
+ route('POST', '/api/schedules/cancel', async ({ req }) => {
17
+ const b = await body(req);
18
+ const from = text(b.from), id = text(b.id);
19
+ try {
20
+ cancelWatchFor(from, id);
21
+ return { cancelled: true };
22
+ }
23
+ catch (e) {
24
+ if (!(e instanceof WatchError))
25
+ throw e;
26
+ } // not one of its watches: a schedule then
27
+ cancelFor(from, id);
28
+ return { cancelled: true };
29
+ });
30
+ route('GET', '/api/schedules', () => allSchedules());
31
+ route('GET', '/api/schedules/:id/runs', ({ params }) => runsOf(params[0]));
32
+ route('PATCH', '/api/schedules/:id', async ({ req, params }) => {
33
+ const b = await body(req);
34
+ return setEnabled(params[0], !!b.enabled) ?? (() => { throw notFound('Schedule not found'); })();
35
+ });
36
+ route('DELETE', '/api/schedules/:id', ({ params }) => {
37
+ if (!removeSchedule(params[0]) && !removeWakeup(params[0]) && !removeWatch(params[0]))
38
+ throw notFound('Schedule not found');
39
+ return { ok: true };
40
+ });
@@ -0,0 +1,47 @@
1
+ import { agents, resolved, skills } from '../../db.js';
2
+ import { body } from '../body.js';
3
+ import { HttpError, bad, notFound } from '../errors.js';
4
+ import { route } from '../router.js';
5
+ import { requireName, text, trimmed } from '../validate.js';
6
+ const checkLoad = (v) => { if (v !== undefined && v !== 'always' && v !== 'on_demand')
7
+ throw bad('load must be always or on_demand'); };
8
+ route('GET', '/api/skills', () => skills.list());
9
+ route('GET', '/api/skills/usage', () => skills.usage());
10
+ route('POST', '/api/skills', async ({ req }) => {
11
+ const p = await body(req);
12
+ const name = requireName(p.name);
13
+ checkLoad(p.load);
14
+ try {
15
+ return skills.create({ name, description: p.description ?? '', content: p.content ?? '', load: p.load });
16
+ }
17
+ catch {
18
+ throw bad(`A skill named "${text(p.name)}" already exists`);
19
+ } // the name is unique: that is what the insert refuses
20
+ });
21
+ route('PATCH', '/api/skills/:id', async ({ req, params }) => {
22
+ const p = await body(req);
23
+ checkLoad(p.load);
24
+ try {
25
+ return skills.update(params[0], p) ?? (() => { throw notFound('Skill not found'); })();
26
+ }
27
+ catch (e) {
28
+ if (e instanceof HttpError)
29
+ throw e;
30
+ throw bad(`A skill named "${text(p.name)}" already exists`);
31
+ }
32
+ });
33
+ route('DELETE', '/api/skills/:id', ({ params }) => { if (!skills.remove(params[0]))
34
+ throw notFound('Skill not found'); return { ok: true }; });
35
+ // `skill_read`: the text of one of the skills the agent has, loaded when it needs it.
36
+ route('POST', '/api/skills/read', async ({ req }) => {
37
+ const { from, name } = await body(req);
38
+ const agent = agents.get(text(from));
39
+ if (!agent)
40
+ throw bad('Unknown agent');
41
+ const mine = resolved(agent).skill_ids.map((id) => skills.get(id)).filter((s) => !!s);
42
+ const want = trimmed(name).toLowerCase();
43
+ const s = mine.find((x) => x.name.toLowerCase() === want);
44
+ if (!s)
45
+ throw bad(`No skill named "${text(name)}". Your skills: ${mine.map((x) => x.name).join(', ') || '(none)'}.`);
46
+ return { name: s.name, content: s.content.trim() };
47
+ });
@@ -0,0 +1,18 @@
1
+ import { closeTerminal, createTerminal, listTerminals, terminalsEnabled } from '../../terminals.js';
2
+ import { body } from '../body.js';
3
+ import { notFound } from '../errors.js';
4
+ import { requireLocal } from '../guards.js';
5
+ import { route } from '../router.js';
6
+ // Real shells shown in the bottom panel (see terminals.ts). Strict: the Host must be loopback too. TerminalError becomes a 400 in http/errors.ts.
7
+ const MSG = 'Terminals can only be used from this machine';
8
+ route('GET', '/api/terminals', ({ req }) => { requireLocal(req, MSG, { strict: true }); return { enabled: terminalsEnabled(), terminals: listTerminals() }; });
9
+ route('POST', '/api/terminals', async ({ req }) => {
10
+ requireLocal(req, MSG, { strict: true });
11
+ return createTerminal(await body(req));
12
+ });
13
+ route('DELETE', '/api/terminals/:id', ({ req, params }) => {
14
+ requireLocal(req, MSG, { strict: true });
15
+ if (!closeTerminal(params[0]))
16
+ throw notFound('Terminal not found');
17
+ return { ok: true };
18
+ });
@@ -0,0 +1,36 @@
1
+ import { types } from '../../db.js';
2
+ import { spawnFromType } from '../../services/agents.js';
3
+ import { body } from '../body.js';
4
+ import { bad, notFound } from '../errors.js';
5
+ import { route } from '../router.js';
6
+ import { isProvider } from '../../services/providers.js';
7
+ import { requireName, text, validateAgentInput } from '../validate.js';
8
+ route('GET', '/api/types', () => types.list());
9
+ route('POST', '/api/types', async ({ req }) => {
10
+ const p = await body(req);
11
+ const name = requireName(p.name);
12
+ if (!isProvider(p.provider))
13
+ throw bad('Pick a provider');
14
+ try {
15
+ return types.create({ ...p, name });
16
+ }
17
+ catch {
18
+ throw bad(`A type named "${text(p.name)}" already exists`);
19
+ }
20
+ });
21
+ route('PATCH', '/api/types/:id', async ({ req, params }) => {
22
+ const t = types.update(params[0], await body(req));
23
+ if (!t)
24
+ throw notFound('Type not found');
25
+ return t;
26
+ });
27
+ route('DELETE', '/api/types/:id', ({ params }) => { if (!types.remove(params[0]))
28
+ throw notFound('Type not found'); return { ok: true }; });
29
+ route('POST', '/api/types/:id/spawn', async ({ req, params }) => {
30
+ const t = types.get(params[0]);
31
+ if (!t)
32
+ throw notFound('Type not found');
33
+ const p = await body(req);
34
+ validateAgentInput({ name: p.name, cwd: p.cwd, colony_id: p.colony_id });
35
+ return spawnFromType(t, p);
36
+ });