hive-am 1.0.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 (268) hide show
  1. package/CHANGELOG.md +114 -1
  2. package/README.md +34 -4
  3. package/package.json +13 -2
  4. package/server/dist/api.js +42 -677
  5. package/server/dist/connections/addressing.js +87 -0
  6. package/server/dist/connections/channel-skill.js +62 -0
  7. package/server/dist/connections/fake.js +1 -0
  8. package/server/dist/connections/files.js +45 -1
  9. package/server/dist/connections/format.js +9 -1
  10. package/server/dist/connections/manager.js +60 -3
  11. package/server/dist/connections/outbound.js +2 -1
  12. package/server/dist/connections/prompt.js +12 -3
  13. package/server/dist/connections/rate.js +20 -0
  14. package/server/dist/connections/router.js +55 -22
  15. package/server/dist/connections/secrets.js +52 -0
  16. package/server/dist/connections/store.js +6 -43
  17. package/server/dist/connections/telegram.js +105 -14
  18. package/server/dist/cron.js +138 -0
  19. package/server/dist/db/json.js +16 -0
  20. package/server/dist/db/migrations.js +199 -0
  21. package/server/dist/db.js +98 -196
  22. package/server/dist/errors.js +11 -0
  23. package/server/dist/git/browse.js +99 -0
  24. package/server/dist/git/ops.js +136 -19
  25. package/server/dist/git/repo.js +2 -1
  26. package/server/dist/history/claude.js +33 -20
  27. package/server/dist/history/index.js +17 -9
  28. package/server/dist/history/kiro.js +17 -12
  29. package/server/dist/history/lines.js +12 -0
  30. package/server/dist/history/opencode.js +18 -1
  31. package/server/dist/http/body.js +46 -0
  32. package/server/dist/http/errors.js +21 -0
  33. package/server/dist/http/guards.js +44 -0
  34. package/server/dist/http/router.js +24 -0
  35. package/server/dist/http/routes/agents.js +103 -0
  36. package/server/dist/http/routes/channel.js +16 -0
  37. package/server/dist/http/routes/colonies.js +9 -0
  38. package/server/dist/http/routes/connections.js +36 -0
  39. package/server/dist/http/routes/fs.js +22 -0
  40. package/server/dist/http/routes/git.js +90 -0
  41. package/server/dist/http/routes/meta.js +11 -0
  42. package/server/dist/http/routes/notebook.js +34 -0
  43. package/server/dist/http/routes/orchestration.js +32 -0
  44. package/server/dist/http/routes/resources.js +68 -0
  45. package/server/dist/http/routes/schedules.js +40 -0
  46. package/server/dist/http/routes/skills.js +47 -0
  47. package/server/dist/http/routes/terminals.js +18 -0
  48. package/server/dist/http/routes/types.js +36 -0
  49. package/server/dist/http/validate.js +65 -0
  50. package/server/dist/index.js +74 -4
  51. package/server/dist/instructions.js +23 -11
  52. package/server/dist/objects/agent.js +49 -0
  53. package/server/dist/objects/database.js +451 -0
  54. package/server/dist/objects/db-history.js +79 -0
  55. package/server/dist/objects/docker-info.js +75 -0
  56. package/server/dist/objects/docker.js +285 -0
  57. package/server/dist/objects/driver-props.js +196 -0
  58. package/server/dist/objects/exec.js +16 -0
  59. package/server/dist/objects/http-history.js +44 -0
  60. package/server/dist/objects/http-script.js +644 -0
  61. package/server/dist/objects/http.js +735 -0
  62. package/server/dist/objects/index.js +445 -0
  63. package/server/dist/objects/model.js +250 -0
  64. package/server/dist/objects/runner.js +211 -0
  65. package/server/dist/objects/stats.js +106 -0
  66. package/server/dist/objects/store.js +115 -0
  67. package/server/dist/objects/supervisor.js +111 -0
  68. package/server/dist/objects/templates.js +67 -0
  69. package/server/dist/providers/claude.js +5 -13
  70. package/server/dist/providers/index.js +7 -1
  71. package/server/dist/providers/kiro.js +4 -5
  72. package/server/dist/providers/opencode-server.js +164 -0
  73. package/server/dist/providers/opencode.js +60 -37
  74. package/server/dist/providers/preamble.js +2 -0
  75. package/server/dist/providers/spawn.js +31 -3
  76. package/server/dist/runtime.js +10 -7
  77. package/server/dist/schedules.js +222 -0
  78. package/server/dist/seed.js +15 -11
  79. package/server/dist/services/agents.js +90 -0
  80. package/server/dist/services/colonies.js +38 -0
  81. package/server/dist/services/connections.js +76 -0
  82. package/server/dist/services/notebook.js +22 -0
  83. package/server/dist/services/providers.js +27 -0
  84. package/server/dist/services/schedules.js +18 -0
  85. package/server/dist/sessions-summary.js +51 -0
  86. package/server/dist/skills/defaults.js +120 -0
  87. package/server/dist/skills/notebook.js +3 -8
  88. package/server/dist/terminals.js +177 -0
  89. package/server/dist/wake.js +149 -0
  90. package/server/dist/watch.js +201 -0
  91. package/server/mcp/dispatch.mjs +169 -1
  92. package/web/.next/BUILD_ID +1 -1
  93. package/web/.next/app-build-manifest.json +145 -85
  94. package/web/.next/app-path-routes-manifest.json +5 -2
  95. package/web/.next/build-manifest.json +14 -14
  96. package/web/.next/prerender-manifest.json +85 -37
  97. package/web/.next/react-loadable-manifest.json +26 -1
  98. package/web/.next/required-server-files.json +13 -1
  99. package/web/.next/routes-manifest.json +32 -0
  100. package/web/.next/server/app/_not-found/page.js +2 -2
  101. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  102. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  103. package/web/.next/server/app/_not-found.html +1 -1
  104. package/web/.next/server/app/_not-found.rsc +12 -11
  105. package/web/.next/server/app/agents/[id]/page.js +4 -4
  106. package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
  107. package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
  108. package/web/.next/server/app/agents/page.js +2 -2
  109. package/web/.next/server/app/agents/page.js.nft.json +1 -1
  110. package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  111. package/web/.next/server/app/agents.html +1 -1
  112. package/web/.next/server/app/agents.rsc +14 -13
  113. package/web/.next/server/app/connections/page.js +2 -2
  114. package/web/.next/server/app/connections/page.js.nft.json +1 -1
  115. package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  116. package/web/.next/server/app/connections.html +1 -1
  117. package/web/.next/server/app/connections.rsc +14 -13
  118. package/web/.next/server/app/index.html +1 -1
  119. package/web/.next/server/app/index.rsc +14 -13
  120. package/web/.next/server/app/page.js +2 -2
  121. package/web/.next/server/app/page.js.nft.json +1 -1
  122. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  123. package/web/.next/server/app/relations/page.js +2 -2
  124. package/web/.next/server/app/relations/page.js.nft.json +1 -1
  125. package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
  126. package/web/.next/server/app/relations.html +1 -1
  127. package/web/.next/server/app/relations.rsc +14 -13
  128. package/web/.next/server/app/resources/[id]/page.js +2 -0
  129. package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -0
  130. package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -0
  131. package/web/.next/server/app/resources/page.js +2 -0
  132. package/web/.next/server/app/resources/page.js.nft.json +1 -0
  133. package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -0
  134. package/web/.next/server/app/resources.html +1 -0
  135. package/web/.next/server/app/resources.meta +15 -0
  136. package/web/.next/server/app/resources.rsc +25 -0
  137. package/web/.next/server/app/schedules/page.js +2 -0
  138. package/web/.next/server/app/schedules/page.js.nft.json +1 -0
  139. package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -0
  140. package/web/.next/server/app/schedules.html +1 -0
  141. package/web/.next/server/app/schedules.meta +15 -0
  142. package/web/.next/server/app/schedules.rsc +25 -0
  143. package/web/.next/server/app/sessions/page.js +2 -2
  144. package/web/.next/server/app/sessions/page.js.nft.json +1 -1
  145. package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  146. package/web/.next/server/app/sessions.html +1 -1
  147. package/web/.next/server/app/sessions.rsc +14 -13
  148. package/web/.next/server/app/skills/page.js +2 -2
  149. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  150. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  151. package/web/.next/server/app/skills.html +1 -1
  152. package/web/.next/server/app/skills.rsc +14 -13
  153. package/web/.next/server/app/types/page.js +2 -2
  154. package/web/.next/server/app/types/page.js.nft.json +1 -1
  155. package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
  156. package/web/.next/server/app/types.html +1 -1
  157. package/web/.next/server/app/types.rsc +14 -13
  158. package/web/.next/server/app-paths-manifest.json +5 -2
  159. package/web/.next/server/chunks/143.js +1 -1
  160. package/web/.next/server/chunks/274.js +1 -0
  161. package/web/.next/server/chunks/306.js +1 -0
  162. package/web/.next/server/chunks/423.js +1 -0
  163. package/web/.next/server/chunks/505.js +6 -0
  164. package/web/.next/server/chunks/555.js +1 -0
  165. package/web/.next/server/chunks/612.js +1 -0
  166. package/web/.next/server/chunks/638.js +1 -0
  167. package/web/.next/server/chunks/692.js +18 -0
  168. package/web/.next/server/chunks/716.js +2 -0
  169. package/web/.next/server/chunks/790.js +11 -0
  170. package/web/.next/server/chunks/802.js +1 -0
  171. package/web/.next/server/chunks/890.js +1 -1
  172. package/web/.next/server/chunks/919.js +1 -0
  173. package/web/.next/server/chunks/950.js +6 -0
  174. package/web/.next/server/middleware-build-manifest.js +1 -1
  175. package/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  176. package/web/.next/server/next-font-manifest.js +1 -1
  177. package/web/.next/server/next-font-manifest.json +1 -1
  178. package/web/.next/server/pages/404.html +1 -1
  179. package/web/.next/server/pages/500.html +1 -1
  180. package/web/.next/server/server-reference-manifest.json +1 -1
  181. package/web/.next/static/chunks/0937d497-114a2a3de515565e.js +1 -0
  182. package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +1 -0
  183. package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +1 -0
  184. package/web/.next/static/chunks/2b9ff8ee-969e65cc354b2150.js +1 -0
  185. package/web/.next/static/chunks/30-9adcbe92b15a6815.js +1 -0
  186. package/web/.next/static/chunks/316-26da9cc233ba7557.js +1 -0
  187. package/web/.next/static/chunks/350-56599dc3bf06066d.js +1 -0
  188. package/web/.next/static/chunks/486-13e08542f3a1cbac.js +1 -0
  189. package/web/.next/static/chunks/491-6b4e6201f5110dff.js +1 -0
  190. package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +1 -0
  191. package/web/.next/static/chunks/545.bfbd92b7632c09de.js +1 -0
  192. package/web/.next/static/chunks/{805-d946ebedb7e85f73.js → 562-7c2e33d838e869bf.js} +1 -1
  193. package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +1 -0
  194. package/web/.next/static/chunks/586-f7017d585a84738d.js +1 -0
  195. package/web/.next/static/chunks/657.b4c39e676c3e20dc.js +1 -0
  196. package/web/.next/static/chunks/701.6abcaf5e60463eef.js +6 -0
  197. package/web/.next/static/chunks/705-a16e1fe09be1ac7f.js +1 -0
  198. package/web/.next/static/chunks/706-fa2be0f10ffebe6a.js +1 -0
  199. package/web/.next/static/chunks/837-33c7480524f48577.js +1 -0
  200. package/web/.next/static/chunks/839-23c5508622fae942.js +1 -0
  201. package/web/.next/static/chunks/844-fe55292e0701f436.js +1 -0
  202. package/web/.next/static/chunks/89-b194fcffe5120c44.js +1 -0
  203. package/web/.next/static/chunks/97.594543c2196de524.js +1 -0
  204. package/web/.next/static/chunks/app/_not-found/page-238a7b80bda6b4ae.js +1 -0
  205. package/web/.next/static/chunks/app/agents/[id]/page-4af6c246c4ca7a97.js +1 -0
  206. package/web/.next/static/chunks/app/agents/page-f4c4c80f955f3967.js +1 -0
  207. package/web/.next/static/chunks/app/connections/page-0936beaf02d863c6.js +1 -0
  208. package/web/.next/static/chunks/app/layout-87ebb70bd9cb8443.js +1 -0
  209. package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +1 -0
  210. package/web/.next/static/chunks/app/relations/page-fba25393c8688714.js +1 -0
  211. package/web/.next/static/chunks/app/resources/[id]/page-d110d89bb8b5fca7.js +1 -0
  212. package/web/.next/static/chunks/app/resources/page-bbd0b450d3f3131d.js +1 -0
  213. package/web/.next/static/chunks/app/schedules/page-a1124397fc760413.js +1 -0
  214. package/web/.next/static/chunks/app/sessions/page-0e322811b0cde724.js +1 -0
  215. package/web/.next/static/chunks/app/skills/page-22ea0ef89d9f7a13.js +1 -0
  216. package/web/.next/static/chunks/app/types/page-18c4e1db2724d41d.js +1 -0
  217. package/web/.next/static/chunks/fcafe1ac.366e4950d3b0d4b7.js +18 -0
  218. package/web/.next/static/chunks/framework-bbac72adbbe2d328.js +1 -0
  219. package/web/.next/static/chunks/main-app-6086068e5fb5eacd.js +1 -0
  220. package/web/.next/static/chunks/main-c15f3fee674e0303.js +1 -0
  221. package/web/.next/static/chunks/pages/_app-d1670a2c6ce392fc.js +1 -0
  222. package/web/.next/static/chunks/pages/_error-95aac014d202fa7a.js +1 -0
  223. package/web/.next/static/chunks/webpack-ef515e41a83a16ac.js +1 -0
  224. package/web/.next/static/css/945795da867f5250.css +1 -0
  225. package/web/.next/static/css/dd4e32c285f75776.css +1 -0
  226. package/web/.next/static/p4S3i9kvO359GW91DQiHu/_buildManifest.js +1 -0
  227. package/web/.next/trace +29 -23
  228. package/web/.next/types/app/resources/[id]/page.ts +84 -0
  229. package/web/.next/types/app/resources/page.ts +84 -0
  230. package/web/.next/types/app/schedules/page.ts +84 -0
  231. package/web/.next/types/routes.d.ts +7 -2
  232. package/web/.next/types/validator.ts +27 -0
  233. package/web/next.config.mjs +4 -0
  234. package/web/.next/server/chunks/149.js +0 -11
  235. package/web/.next/server/chunks/247.js +0 -1
  236. package/web/.next/server/chunks/851.js +0 -1
  237. package/web/.next/server/chunks/932.js +0 -2
  238. package/web/.next/static/84BpRSahg2BTwk3mX6AG8/_buildManifest.js +0 -1
  239. package/web/.next/static/chunks/0937d497-44abb0f9355ace92.js +0 -1
  240. package/web/.next/static/chunks/2b9ff8ee-c3b61ac1e608c57d.js +0 -1
  241. package/web/.next/static/chunks/351-65da8c1a6e25f93b.js +0 -1
  242. package/web/.next/static/chunks/463-6ae35cdefdc59169.js +0 -1
  243. package/web/.next/static/chunks/493-92266798be872b8f.js +0 -1
  244. package/web/.next/static/chunks/517-eb26c72f6059aa26.js +0 -1
  245. package/web/.next/static/chunks/657.f1e03b7b65eba1d9.js +0 -1
  246. package/web/.next/static/chunks/687-3a4205f51be38602.js +0 -1
  247. package/web/.next/static/chunks/837-fd7623601dc78ed8.js +0 -1
  248. package/web/.next/static/chunks/850-318f32484989dd59.js +0 -1
  249. package/web/.next/static/chunks/89-ace2e1aa63babfdf.js +0 -1
  250. package/web/.next/static/chunks/951-e250b541b67da7ce.js +0 -1
  251. package/web/.next/static/chunks/app/_not-found/page-bbd54d7b9a55d7ae.js +0 -1
  252. package/web/.next/static/chunks/app/agents/[id]/page-5eaf094fec246f1b.js +0 -1
  253. package/web/.next/static/chunks/app/agents/page-e9b17f1c3439ef98.js +0 -1
  254. package/web/.next/static/chunks/app/connections/page-c6afd90abd82d47a.js +0 -1
  255. package/web/.next/static/chunks/app/layout-c415d5a847cd94b3.js +0 -1
  256. package/web/.next/static/chunks/app/page-6fc87fc5a128a204.js +0 -1
  257. package/web/.next/static/chunks/app/relations/page-0355a93a8eb67989.js +0 -1
  258. package/web/.next/static/chunks/app/sessions/page-beae9a74a07d69e0.js +0 -1
  259. package/web/.next/static/chunks/app/skills/page-4f839ac913e26d32.js +0 -1
  260. package/web/.next/static/chunks/app/types/page-4f6b99af3232f861.js +0 -1
  261. package/web/.next/static/chunks/framework-0e68884e76d9a8d3.js +0 -1
  262. package/web/.next/static/chunks/main-app-d6f73ccd41f325c5.js +0 -1
  263. package/web/.next/static/chunks/main-cd22509db15764e2.js +0 -1
  264. package/web/.next/static/chunks/pages/_app-0f75d689c738a6fc.js +0 -1
  265. package/web/.next/static/chunks/pages/_error-d481f6d49fb4455d.js +0 -1
  266. package/web/.next/static/chunks/webpack-d40de72e52f65f79.js +0 -1
  267. package/web/.next/static/css/abe1a6216491f944.css +0 -1
  268. /package/web/.next/static/{84BpRSahg2BTwk3mX6AG8 → p4S3i9kvO359GW91DQiHu}/_ssgManifest.js +0 -0
@@ -0,0 +1,44 @@
1
+ import { forbidden } from './errors.js';
2
+ const LOOPBACK = ['localhost', '127.0.0.1', '[::1]', '::1'];
3
+ const loop = (h) => LOOPBACK.includes(h.replace(/:\d+$/, '').toLowerCase());
4
+ const hostOf = (origin) => { try {
5
+ const h = new URL(origin).hostname;
6
+ return h === '::1' ? '[::1]' : h;
7
+ }
8
+ catch {
9
+ return null;
10
+ } };
11
+ /** The browser's origin (when it sends one) is this machine, or the same name the request was addressed to. Programs (curl, the MCP) send no origin. */
12
+ export function originIsLocal(req) {
13
+ const origin = req.headers.origin;
14
+ if (!origin)
15
+ return true;
16
+ const host = hostOf(origin);
17
+ if (host === null)
18
+ return false;
19
+ const own = String(req.headers.host ?? '').replace(/:\d+$/, '');
20
+ return loop(host) || host === own;
21
+ }
22
+ /**
23
+ * Only a page or a program of this machine: the Host must be a loopback name (a page that reached us through another name, DNS rebinding,
24
+ * is refused) and a browser's origin must be loopback too. Used by terminals (REST and WebSocket).
25
+ */
26
+ export function isLocalRequest(req) {
27
+ if (!loop(String(req.headers.host ?? '')))
28
+ return false;
29
+ const origin = req.headers.origin;
30
+ if (origin) {
31
+ const h = hostOf(origin);
32
+ if (h === null || !loop(h))
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ /**
38
+ * The single guard for endpoints that act on the machine. The API answers any origin (`*`, it is a local tool), so these check the
39
+ * request itself. `strict` also pins the Host (terminals); otherwise only the origin is checked, as git actions and resources always did.
40
+ */
41
+ export function requireLocal(req, message, opts = {}) {
42
+ if (!(opts.strict ? isLocalRequest(req) : originIsLocal(req)))
43
+ throw forbidden(message);
44
+ }
@@ -0,0 +1,24 @@
1
+ import { bad } from './errors.js';
2
+ /** The route table: the modules in ./routes fill it when they are imported (see api.ts for the order). */
3
+ export const routes = [];
4
+ /** `:name` matches one path segment; its decoded text reaches the handler in `params`, in order. */
5
+ export const route = (method, pattern, handler) => {
6
+ routes.push({ method, pattern, re: new RegExp('^' + pattern.replace(/:[^/]+/g, '([^/]+)') + '$'), handler });
7
+ };
8
+ /** The first route that answers this method and path, with its (decoded) parameters. A malformed %-escape is a 400. */
9
+ export function match(method, pathname) {
10
+ for (const r of routes) {
11
+ if (r.method !== method)
12
+ continue;
13
+ const m = r.re.exec(pathname);
14
+ if (!m)
15
+ continue;
16
+ try {
17
+ return { route: r, params: m.slice(1).map(decodeURIComponent) };
18
+ }
19
+ catch {
20
+ throw bad('Malformed URL');
21
+ }
22
+ }
23
+ return null;
24
+ }
@@ -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
+ });