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.
- package/CHANGELOG.md +114 -1
- package/README.md +34 -4
- package/package.json +13 -2
- package/server/dist/api.js +42 -677
- package/server/dist/connections/addressing.js +87 -0
- package/server/dist/connections/channel-skill.js +62 -0
- package/server/dist/connections/fake.js +1 -0
- package/server/dist/connections/files.js +45 -1
- package/server/dist/connections/format.js +9 -1
- package/server/dist/connections/manager.js +60 -3
- package/server/dist/connections/outbound.js +2 -1
- package/server/dist/connections/prompt.js +12 -3
- package/server/dist/connections/rate.js +20 -0
- package/server/dist/connections/router.js +55 -22
- package/server/dist/connections/secrets.js +52 -0
- package/server/dist/connections/store.js +6 -43
- package/server/dist/connections/telegram.js +105 -14
- package/server/dist/cron.js +138 -0
- package/server/dist/db/json.js +16 -0
- package/server/dist/db/migrations.js +199 -0
- package/server/dist/db.js +98 -196
- package/server/dist/errors.js +11 -0
- package/server/dist/git/browse.js +99 -0
- package/server/dist/git/ops.js +136 -19
- package/server/dist/git/repo.js +2 -1
- package/server/dist/history/claude.js +33 -20
- package/server/dist/history/index.js +17 -9
- package/server/dist/history/kiro.js +17 -12
- package/server/dist/history/lines.js +12 -0
- package/server/dist/history/opencode.js +18 -1
- package/server/dist/http/body.js +46 -0
- package/server/dist/http/errors.js +21 -0
- package/server/dist/http/guards.js +44 -0
- package/server/dist/http/router.js +24 -0
- package/server/dist/http/routes/agents.js +103 -0
- package/server/dist/http/routes/channel.js +16 -0
- package/server/dist/http/routes/colonies.js +9 -0
- package/server/dist/http/routes/connections.js +36 -0
- package/server/dist/http/routes/fs.js +22 -0
- package/server/dist/http/routes/git.js +90 -0
- package/server/dist/http/routes/meta.js +11 -0
- package/server/dist/http/routes/notebook.js +34 -0
- package/server/dist/http/routes/orchestration.js +32 -0
- package/server/dist/http/routes/resources.js +68 -0
- package/server/dist/http/routes/schedules.js +40 -0
- package/server/dist/http/routes/skills.js +47 -0
- package/server/dist/http/routes/terminals.js +18 -0
- package/server/dist/http/routes/types.js +36 -0
- package/server/dist/http/validate.js +65 -0
- package/server/dist/index.js +74 -4
- package/server/dist/instructions.js +23 -11
- package/server/dist/objects/agent.js +49 -0
- package/server/dist/objects/database.js +451 -0
- package/server/dist/objects/db-history.js +79 -0
- package/server/dist/objects/docker-info.js +75 -0
- package/server/dist/objects/docker.js +285 -0
- package/server/dist/objects/driver-props.js +196 -0
- package/server/dist/objects/exec.js +16 -0
- package/server/dist/objects/http-history.js +44 -0
- package/server/dist/objects/http-script.js +644 -0
- package/server/dist/objects/http.js +735 -0
- package/server/dist/objects/index.js +445 -0
- package/server/dist/objects/model.js +250 -0
- package/server/dist/objects/runner.js +211 -0
- package/server/dist/objects/stats.js +106 -0
- package/server/dist/objects/store.js +115 -0
- package/server/dist/objects/supervisor.js +111 -0
- package/server/dist/objects/templates.js +67 -0
- package/server/dist/providers/claude.js +5 -13
- package/server/dist/providers/index.js +7 -1
- package/server/dist/providers/kiro.js +4 -5
- package/server/dist/providers/opencode-server.js +164 -0
- package/server/dist/providers/opencode.js +60 -37
- package/server/dist/providers/preamble.js +2 -0
- package/server/dist/providers/spawn.js +31 -3
- package/server/dist/runtime.js +10 -7
- package/server/dist/schedules.js +222 -0
- package/server/dist/seed.js +15 -11
- package/server/dist/services/agents.js +90 -0
- package/server/dist/services/colonies.js +38 -0
- package/server/dist/services/connections.js +76 -0
- package/server/dist/services/notebook.js +22 -0
- package/server/dist/services/providers.js +27 -0
- package/server/dist/services/schedules.js +18 -0
- package/server/dist/sessions-summary.js +51 -0
- package/server/dist/skills/defaults.js +120 -0
- package/server/dist/skills/notebook.js +3 -8
- package/server/dist/terminals.js +177 -0
- package/server/dist/wake.js +149 -0
- package/server/dist/watch.js +201 -0
- package/server/mcp/dispatch.mjs +169 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-build-manifest.json +145 -85
- package/web/.next/app-path-routes-manifest.json +5 -2
- package/web/.next/build-manifest.json +14 -14
- package/web/.next/prerender-manifest.json +85 -37
- package/web/.next/react-loadable-manifest.json +26 -1
- package/web/.next/required-server-files.json +13 -1
- package/web/.next/routes-manifest.json +32 -0
- package/web/.next/server/app/_not-found/page.js +2 -2
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/server/app/_not-found.rsc +12 -11
- package/web/.next/server/app/agents/[id]/page.js +4 -4
- package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents/page.js +2 -2
- package/web/.next/server/app/agents/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents.html +1 -1
- package/web/.next/server/app/agents.rsc +14 -13
- package/web/.next/server/app/connections/page.js +2 -2
- package/web/.next/server/app/connections/page.js.nft.json +1 -1
- package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/connections.html +1 -1
- package/web/.next/server/app/connections.rsc +14 -13
- package/web/.next/server/app/index.html +1 -1
- package/web/.next/server/app/index.rsc +14 -13
- package/web/.next/server/app/page.js +2 -2
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations/page.js +2 -2
- package/web/.next/server/app/relations/page.js.nft.json +1 -1
- package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations.html +1 -1
- package/web/.next/server/app/relations.rsc +14 -13
- package/web/.next/server/app/resources/[id]/page.js +2 -0
- package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -0
- package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/resources/page.js +2 -0
- package/web/.next/server/app/resources/page.js.nft.json +1 -0
- package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/resources.html +1 -0
- package/web/.next/server/app/resources.meta +15 -0
- package/web/.next/server/app/resources.rsc +25 -0
- package/web/.next/server/app/schedules/page.js +2 -0
- package/web/.next/server/app/schedules/page.js.nft.json +1 -0
- package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/schedules.html +1 -0
- package/web/.next/server/app/schedules.meta +15 -0
- package/web/.next/server/app/schedules.rsc +25 -0
- package/web/.next/server/app/sessions/page.js +2 -2
- package/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/sessions.html +1 -1
- package/web/.next/server/app/sessions.rsc +14 -13
- package/web/.next/server/app/skills/page.js +2 -2
- package/web/.next/server/app/skills/page.js.nft.json +1 -1
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills.html +1 -1
- package/web/.next/server/app/skills.rsc +14 -13
- package/web/.next/server/app/types/page.js +2 -2
- package/web/.next/server/app/types/page.js.nft.json +1 -1
- package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/types.html +1 -1
- package/web/.next/server/app/types.rsc +14 -13
- package/web/.next/server/app-paths-manifest.json +5 -2
- package/web/.next/server/chunks/143.js +1 -1
- package/web/.next/server/chunks/274.js +1 -0
- package/web/.next/server/chunks/306.js +1 -0
- package/web/.next/server/chunks/423.js +1 -0
- package/web/.next/server/chunks/505.js +6 -0
- package/web/.next/server/chunks/555.js +1 -0
- package/web/.next/server/chunks/612.js +1 -0
- package/web/.next/server/chunks/638.js +1 -0
- package/web/.next/server/chunks/692.js +18 -0
- package/web/.next/server/chunks/716.js +2 -0
- package/web/.next/server/chunks/790.js +11 -0
- package/web/.next/server/chunks/802.js +1 -0
- package/web/.next/server/chunks/890.js +1 -1
- package/web/.next/server/chunks/919.js +1 -0
- package/web/.next/server/chunks/950.js +6 -0
- package/web/.next/server/middleware-build-manifest.js +1 -1
- package/web/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/web/.next/server/next-font-manifest.js +1 -1
- package/web/.next/server/next-font-manifest.json +1 -1
- package/web/.next/server/pages/404.html +1 -1
- package/web/.next/server/pages/500.html +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/static/chunks/0937d497-114a2a3de515565e.js +1 -0
- package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +1 -0
- package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +1 -0
- package/web/.next/static/chunks/2b9ff8ee-969e65cc354b2150.js +1 -0
- package/web/.next/static/chunks/30-9adcbe92b15a6815.js +1 -0
- package/web/.next/static/chunks/316-26da9cc233ba7557.js +1 -0
- package/web/.next/static/chunks/350-56599dc3bf06066d.js +1 -0
- package/web/.next/static/chunks/486-13e08542f3a1cbac.js +1 -0
- package/web/.next/static/chunks/491-6b4e6201f5110dff.js +1 -0
- package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +1 -0
- package/web/.next/static/chunks/545.bfbd92b7632c09de.js +1 -0
- package/web/.next/static/chunks/{805-d946ebedb7e85f73.js → 562-7c2e33d838e869bf.js} +1 -1
- package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +1 -0
- package/web/.next/static/chunks/586-f7017d585a84738d.js +1 -0
- package/web/.next/static/chunks/657.b4c39e676c3e20dc.js +1 -0
- package/web/.next/static/chunks/701.6abcaf5e60463eef.js +6 -0
- package/web/.next/static/chunks/705-a16e1fe09be1ac7f.js +1 -0
- package/web/.next/static/chunks/706-fa2be0f10ffebe6a.js +1 -0
- package/web/.next/static/chunks/837-33c7480524f48577.js +1 -0
- package/web/.next/static/chunks/839-23c5508622fae942.js +1 -0
- package/web/.next/static/chunks/844-fe55292e0701f436.js +1 -0
- package/web/.next/static/chunks/89-b194fcffe5120c44.js +1 -0
- package/web/.next/static/chunks/97.594543c2196de524.js +1 -0
- package/web/.next/static/chunks/app/_not-found/page-238a7b80bda6b4ae.js +1 -0
- package/web/.next/static/chunks/app/agents/[id]/page-4af6c246c4ca7a97.js +1 -0
- package/web/.next/static/chunks/app/agents/page-f4c4c80f955f3967.js +1 -0
- package/web/.next/static/chunks/app/connections/page-0936beaf02d863c6.js +1 -0
- package/web/.next/static/chunks/app/layout-87ebb70bd9cb8443.js +1 -0
- package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +1 -0
- package/web/.next/static/chunks/app/relations/page-fba25393c8688714.js +1 -0
- package/web/.next/static/chunks/app/resources/[id]/page-d110d89bb8b5fca7.js +1 -0
- package/web/.next/static/chunks/app/resources/page-bbd0b450d3f3131d.js +1 -0
- package/web/.next/static/chunks/app/schedules/page-a1124397fc760413.js +1 -0
- package/web/.next/static/chunks/app/sessions/page-0e322811b0cde724.js +1 -0
- package/web/.next/static/chunks/app/skills/page-22ea0ef89d9f7a13.js +1 -0
- package/web/.next/static/chunks/app/types/page-18c4e1db2724d41d.js +1 -0
- package/web/.next/static/chunks/fcafe1ac.366e4950d3b0d4b7.js +18 -0
- package/web/.next/static/chunks/framework-bbac72adbbe2d328.js +1 -0
- package/web/.next/static/chunks/main-app-6086068e5fb5eacd.js +1 -0
- package/web/.next/static/chunks/main-c15f3fee674e0303.js +1 -0
- package/web/.next/static/chunks/pages/_app-d1670a2c6ce392fc.js +1 -0
- package/web/.next/static/chunks/pages/_error-95aac014d202fa7a.js +1 -0
- package/web/.next/static/chunks/webpack-ef515e41a83a16ac.js +1 -0
- package/web/.next/static/css/945795da867f5250.css +1 -0
- package/web/.next/static/css/dd4e32c285f75776.css +1 -0
- package/web/.next/static/p4S3i9kvO359GW91DQiHu/_buildManifest.js +1 -0
- package/web/.next/trace +29 -23
- package/web/.next/types/app/resources/[id]/page.ts +84 -0
- package/web/.next/types/app/resources/page.ts +84 -0
- package/web/.next/types/app/schedules/page.ts +84 -0
- package/web/.next/types/routes.d.ts +7 -2
- package/web/.next/types/validator.ts +27 -0
- package/web/next.config.mjs +4 -0
- package/web/.next/server/chunks/149.js +0 -11
- package/web/.next/server/chunks/247.js +0 -1
- package/web/.next/server/chunks/851.js +0 -1
- package/web/.next/server/chunks/932.js +0 -2
- package/web/.next/static/84BpRSahg2BTwk3mX6AG8/_buildManifest.js +0 -1
- package/web/.next/static/chunks/0937d497-44abb0f9355ace92.js +0 -1
- package/web/.next/static/chunks/2b9ff8ee-c3b61ac1e608c57d.js +0 -1
- package/web/.next/static/chunks/351-65da8c1a6e25f93b.js +0 -1
- package/web/.next/static/chunks/463-6ae35cdefdc59169.js +0 -1
- package/web/.next/static/chunks/493-92266798be872b8f.js +0 -1
- package/web/.next/static/chunks/517-eb26c72f6059aa26.js +0 -1
- package/web/.next/static/chunks/657.f1e03b7b65eba1d9.js +0 -1
- package/web/.next/static/chunks/687-3a4205f51be38602.js +0 -1
- package/web/.next/static/chunks/837-fd7623601dc78ed8.js +0 -1
- package/web/.next/static/chunks/850-318f32484989dd59.js +0 -1
- package/web/.next/static/chunks/89-ace2e1aa63babfdf.js +0 -1
- package/web/.next/static/chunks/951-e250b541b67da7ce.js +0 -1
- package/web/.next/static/chunks/app/_not-found/page-bbd54d7b9a55d7ae.js +0 -1
- package/web/.next/static/chunks/app/agents/[id]/page-5eaf094fec246f1b.js +0 -1
- package/web/.next/static/chunks/app/agents/page-e9b17f1c3439ef98.js +0 -1
- package/web/.next/static/chunks/app/connections/page-c6afd90abd82d47a.js +0 -1
- package/web/.next/static/chunks/app/layout-c415d5a847cd94b3.js +0 -1
- package/web/.next/static/chunks/app/page-6fc87fc5a128a204.js +0 -1
- package/web/.next/static/chunks/app/relations/page-0355a93a8eb67989.js +0 -1
- package/web/.next/static/chunks/app/sessions/page-beae9a74a07d69e0.js +0 -1
- package/web/.next/static/chunks/app/skills/page-4f839ac913e26d32.js +0 -1
- package/web/.next/static/chunks/app/types/page-4f6b99af3232f861.js +0 -1
- package/web/.next/static/chunks/framework-0e68884e76d9a8d3.js +0 -1
- package/web/.next/static/chunks/main-app-d6f73ccd41f325c5.js +0 -1
- package/web/.next/static/chunks/main-cd22509db15764e2.js +0 -1
- package/web/.next/static/chunks/pages/_app-0f75d689c738a6fc.js +0 -1
- package/web/.next/static/chunks/pages/_error-d481f6d49fb4455d.js +0 -1
- package/web/.next/static/chunks/webpack-d40de72e52f65f79.js +0 -1
- package/web/.next/static/css/abe1a6216491f944.css +0 -1
- /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
|
+
});
|