hive-am 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -1
- package/README.md +9 -1
- package/package.json +13 -2
- package/server/dist/api.js +42 -762
- package/server/dist/connections/channel-skill.js +13 -1
- package/server/dist/connections/files.js +45 -1
- package/server/dist/connections/manager.js +41 -3
- package/server/dist/connections/outbound.js +2 -1
- package/server/dist/connections/prompt.js +4 -0
- package/server/dist/connections/router.js +3 -2
- package/server/dist/connections/store.js +4 -43
- package/server/dist/connections/telegram.js +43 -3
- package/server/dist/cron.js +2 -1
- 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 +21 -21
- 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 +124 -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 +67 -5
- package/server/dist/instructions.js +8 -2
- 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 +3 -13
- package/server/dist/providers/index.js +7 -1
- package/server/dist/providers/kiro.js +2 -5
- package/server/dist/providers/opencode-server.js +45 -0
- package/server/dist/providers/opencode.js +52 -9
- package/server/dist/providers/preamble.js +2 -0
- package/server/dist/providers/spawn.js +31 -3
- package/server/dist/runtime.js +8 -6
- package/server/dist/schedules.js +2 -21
- 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 +37 -0
- package/server/dist/skills/notebook.js +3 -8
- package/server/dist/terminals.js +185 -0
- package/server/dist/wake.js +13 -16
- package/server/dist/watch.js +2 -8
- package/server/mcp/dispatch.mjs +70 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-build-manifest.json +146 -94
- package/web/.next/app-path-routes-manifest.json +6 -4
- package/web/.next/build-manifest.json +14 -14
- package/web/.next/prerender-manifest.json +67 -43
- package/web/.next/react-loadable-manifest.json +26 -1
- package/web/.next/required-server-files.json +13 -1
- package/web/.next/routes-manifest.json +26 -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 -2
- package/web/.next/server/app/schedules/page.js.nft.json +1 -1
- package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/schedules.html +1 -1
- package/web/.next/server/app/schedules.rsc +14 -13
- 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 +6 -4
- 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/Bz7onClw1BajMUCwqpU8o/_buildManifest.js +1 -0
- 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-c7e75806f77b880c.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-4aefe6e71be6e1ef.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-3faf40800981a736.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-99620746d028dda8.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/b7a03c62468f4263.css +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/routes.d.ts +6 -2
- package/web/.next/types/validator.ts +18 -0
- package/web/next.config.mjs +4 -0
- package/web/.next/server/chunks/247.js +0 -1
- package/web/.next/server/chunks/484.js +0 -11
- package/web/.next/server/chunks/851.js +0 -1
- package/web/.next/server/chunks/932.js +0 -2
- package/web/.next/static/_z2hBOHhwiR5ytdI_8alh/_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/463-6ae35cdefdc59169.js +0 -1
- package/web/.next/static/chunks/493-50d2adf9fd5ec680.js +0 -1
- package/web/.next/static/chunks/517-eb26c72f6059aa26.js +0 -1
- package/web/.next/static/chunks/589-8bf536f331de4269.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-a8b7a43d2c18483f.js +0 -1
- package/web/.next/static/chunks/app/agents/page-e9b17f1c3439ef98.js +0 -1
- package/web/.next/static/chunks/app/connections/page-477f36fcd5b13d78.js +0 -1
- package/web/.next/static/chunks/app/layout-a2d38e3de5911b1b.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/schedules/page-39befd15b3b20586.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/da22f0402b1c40ae.css +0 -1
- /package/web/.next/static/{_z2hBOHhwiR5ytdI_8alh → Bz7onClw1BajMUCwqpU8o}/_ssgManifest.js +0 -0
package/server/dist/api.js
CHANGED
|
@@ -1,747 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import { readHistory } from './history/index.js';
|
|
21
|
-
import { listModels } from './models.js';
|
|
22
|
-
import { sessionStats } from './stats.js';
|
|
23
|
-
import { createReadStream } from 'node:fs';
|
|
24
|
-
import { findRepo, gitDiff, gitFile, gitImagePath, gitList, gitStatus, gitTree, isPathError } from './git/repo.js';
|
|
25
|
-
import { listStashes, planSwitch, smartCancel, smartFinish, smartSwitch, stashApply, stashDetail, stashDrop, stashSave } from './git/switch.js';
|
|
26
|
-
import { GitOpError, gitBlame, gitBranches, gitCommitDetail, gitCommitChanges, gitCommitDiff, gitDiscardAll, gitDiscardFile, gitDiscardHunk, gitDiscardLines, gitFetch, gitLog, gitMerge, gitMergeAbort, gitPull, gitPush, gitRebaseContinue, gitResolveContent, gitResolveSide, gitSwitch, gitUnresolve } from './git/ops.js';
|
|
27
|
-
const exec = promisify(execFile);
|
|
28
|
-
const PROVIDERS = {
|
|
29
|
-
claude: { bin: 'claude', label: 'Claude Code' },
|
|
30
|
-
opencode: { bin: 'opencode', label: 'OpenCode' },
|
|
31
|
-
kiro: { bin: 'kiro-cli', label: 'Kiro' },
|
|
32
|
-
};
|
|
33
|
-
class HttpError extends Error {
|
|
34
|
-
status;
|
|
35
|
-
constructor(status, msg) {
|
|
36
|
-
super(msg);
|
|
37
|
-
this.status = status;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const bad = (m) => new HttpError(400, m);
|
|
41
|
-
const notFound = (m = 'Not found') => new HttpError(404, m);
|
|
42
|
-
async function body(req) {
|
|
43
|
-
const chunks = [];
|
|
44
|
-
for await (const c of req)
|
|
45
|
-
chunks.push(c);
|
|
46
|
-
if (!chunks.length)
|
|
47
|
-
return {};
|
|
48
|
-
try {
|
|
49
|
-
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
throw bad('Request body is not valid JSON');
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function json(res, status, data) {
|
|
56
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1
|
+
import { toHttpError } from './http/errors.js';
|
|
2
|
+
import { match } from './http/router.js';
|
|
3
|
+
// Each module registers its routes when imported (see http/router.ts). Within a method and a segment count no pattern overlaps, so the order is only for reading.
|
|
4
|
+
import './http/routes/meta.js';
|
|
5
|
+
import './http/routes/agents.js';
|
|
6
|
+
import './http/routes/git.js';
|
|
7
|
+
import './http/routes/resources.js';
|
|
8
|
+
import './http/routes/terminals.js';
|
|
9
|
+
import './http/routes/skills.js';
|
|
10
|
+
import './http/routes/types.js';
|
|
11
|
+
import './http/routes/colonies.js';
|
|
12
|
+
import './http/routes/connections.js';
|
|
13
|
+
import './http/routes/orchestration.js';
|
|
14
|
+
import './http/routes/notebook.js';
|
|
15
|
+
import './http/routes/channel.js';
|
|
16
|
+
import './http/routes/schedules.js';
|
|
17
|
+
import './http/routes/fs.js';
|
|
18
|
+
function json(res, status, data, headers = {}) {
|
|
19
|
+
res.writeHead(status, { 'content-type': 'application/json', ...headers });
|
|
57
20
|
res.end(JSON.stringify(data));
|
|
58
21
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return { id, label: PROVIDERS[id].label, installed: true, version: stdout.trim().split('\n')[0].slice(0, 60) };
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return { id, label: PROVIDERS[id].label, installed: false, version: '' };
|
|
70
|
-
}
|
|
71
|
-
}));
|
|
72
|
-
providerCache = { at: Date.now(), data };
|
|
73
|
-
return data;
|
|
74
|
-
}
|
|
75
|
-
function validateAgentInput(p, partial = false) {
|
|
76
|
-
if (!partial || p.name !== undefined) {
|
|
77
|
-
if (!String(p.name ?? '').trim())
|
|
78
|
-
throw bad('Name is required');
|
|
79
|
-
}
|
|
80
|
-
if (p.provider !== undefined && !(p.provider in PROVIDERS))
|
|
81
|
-
throw bad(`Unknown provider "${p.provider}"`);
|
|
82
|
-
if (p.role !== undefined && !['orchestrator', 'worker'].includes(p.role))
|
|
83
|
-
throw bad('Role must be orchestrator or worker');
|
|
84
|
-
if (p.cwd && !existsSync(p.cwd))
|
|
85
|
-
throw bad(`Folder does not exist: ${p.cwd}`);
|
|
86
|
-
if (p.colony_id && !colonies.get(p.colony_id))
|
|
87
|
-
throw bad('That colony no longer exists');
|
|
88
|
-
}
|
|
89
|
-
const routes = [];
|
|
90
|
-
const route = (method, path, h) => routes.push([method, new RegExp('^' + path.replace(/:[^/]+/g, '([^/]+)') + '$'), h]);
|
|
91
|
-
route('GET', '/api/health', () => ({ ok: true }));
|
|
92
|
-
route('GET', '/api/providers', () => providerStatus());
|
|
93
|
-
route('GET', '/api/providers/:p/models', async ({ params }) => {
|
|
94
|
-
if (!(params[0] in PROVIDERS))
|
|
95
|
-
throw notFound('Unknown provider');
|
|
96
|
-
return listModels(params[0]);
|
|
97
|
-
});
|
|
98
|
-
// ---- agents ----
|
|
99
|
-
route('GET', '/api/agents', () => agents.list().map((a) => ({ ...a, queued: queueDepth(a.id), live: !!liveTurn(a.id) })));
|
|
100
|
-
route('POST', '/api/agents', async ({ req }) => {
|
|
101
|
-
const p = await body(req);
|
|
102
|
-
validateAgentInput(p);
|
|
103
|
-
if (agents.byName(p.name))
|
|
104
|
-
throw bad(`An agent named "${p.name}" already exists`);
|
|
105
|
-
const a = agents.create({ ...p, name: p.name.trim(), cwd: p.cwd ?? '' });
|
|
106
|
-
if (!a.effective.cwd) {
|
|
107
|
-
agents.remove(a.id);
|
|
108
|
-
throw bad('Choose a working folder, or put the agent in a colony that provides one.');
|
|
109
|
-
}
|
|
110
|
-
notifyAgentsChanged();
|
|
111
|
-
return a;
|
|
112
|
-
});
|
|
113
|
-
route('GET', '/api/agents/:id', ({ params }) => agents.get(params[0]) ?? (() => { throw notFound('Agent not found'); })());
|
|
114
|
-
route('PATCH', '/api/agents/:id', async ({ req, params }) => {
|
|
115
|
-
const p = await body(req);
|
|
116
|
-
validateAgentInput(p, true);
|
|
117
|
-
const cur = agents.get(params[0]);
|
|
118
|
-
if (!cur)
|
|
119
|
-
throw notFound('Agent not found');
|
|
120
|
-
if (p.name && p.name.toLowerCase() !== cur.name.toLowerCase() && agents.byName(p.name))
|
|
121
|
-
throw bad(`An agent named "${p.name}" already exists`);
|
|
122
|
-
// Changing provider invalidates the native session pointer. (A changed folder is detected when the next turn starts.)
|
|
123
|
-
// All-or-nothing: a change that would leave a linked agent in Plan mode is rolled back.
|
|
124
|
-
const a = db.transaction(() => {
|
|
125
|
-
const u = agents.update(params[0], p);
|
|
126
|
-
const why = channelPermissionError(u.id);
|
|
127
|
-
if (why)
|
|
128
|
-
throw bad(why);
|
|
129
|
-
return u;
|
|
130
|
-
})();
|
|
131
|
-
if (p.provider && p.provider !== cur.provider)
|
|
132
|
-
agents.setSession(a, null);
|
|
133
|
-
if (!agents.get(a.id).effective.cwd)
|
|
134
|
-
throw bad('Choose a working folder, or keep inheriting the colony’s folder.');
|
|
135
|
-
notifyAgentsChanged();
|
|
136
|
-
return agents.get(params[0]);
|
|
137
|
-
});
|
|
138
|
-
route('DELETE', '/api/agents/:id', ({ params }) => {
|
|
139
|
-
stopAgent(params[0]);
|
|
140
|
-
if (!agents.remove(params[0]))
|
|
141
|
-
throw notFound('Agent not found');
|
|
142
|
-
notifyAgentsChanged();
|
|
143
|
-
return { ok: true };
|
|
144
|
-
});
|
|
145
|
-
route('POST', '/api/agents/:id/messages', async ({ req, params }) => {
|
|
146
|
-
const a = agents.get(params[0]);
|
|
147
|
-
if (!a)
|
|
148
|
-
throw notFound('Agent not found');
|
|
149
|
-
const { prompt } = await body(req);
|
|
150
|
-
if (!String(prompt ?? '').trim())
|
|
151
|
-
throw bad('Message is empty');
|
|
152
|
-
void sendTurn(a.id, String(prompt)).catch((e) => console.error('[turn]', e));
|
|
153
|
-
return { accepted: true };
|
|
154
|
-
});
|
|
155
|
-
route('POST', '/api/agents/:id/stop', ({ params }) => ({ stopped: stopAgent(params[0]) }));
|
|
156
|
-
route('POST', '/api/agents/:id/new-session', ({ params }) => {
|
|
157
|
-
const a = agents.get(params[0]);
|
|
158
|
-
if (!a)
|
|
159
|
-
throw notFound('Agent not found');
|
|
160
|
-
stopAgent(a.id);
|
|
161
|
-
agents.setSession(a, null);
|
|
162
|
-
notifyAgentsChanged();
|
|
163
|
-
return { ok: true };
|
|
164
|
-
});
|
|
165
|
-
route('POST', '/api/agents/:id/resume-session', async ({ req, params }) => {
|
|
166
|
-
const a = agents.get(params[0]);
|
|
167
|
-
if (!a)
|
|
168
|
-
throw notFound('Agent not found');
|
|
169
|
-
const { session_id } = await body(req);
|
|
170
|
-
if (!agents.sessions(a.id).some((s) => s.session_id === session_id && s.kind !== 'delegation'))
|
|
171
|
-
throw bad('Only the agent’s own conversations can be made current. Delegated sessions are read-only.');
|
|
172
|
-
agents.setSession(a, session_id);
|
|
173
|
-
notifyAgentsChanged();
|
|
174
|
-
return { ok: true };
|
|
175
|
-
});
|
|
176
|
-
route('GET', '/api/agents/:id/history', ({ params, url }) => {
|
|
177
|
-
const a = agents.get(params[0]);
|
|
178
|
-
if (!a)
|
|
179
|
-
throw notFound('Agent not found');
|
|
180
|
-
const sid = url.searchParams.get('session') ?? a.session_id;
|
|
181
|
-
if (sid && sid !== a.session_id && !agents.sessions(a.id).some((s) => s.session_id === sid))
|
|
182
|
-
throw bad('That session does not belong to this agent');
|
|
183
|
-
// A long conversation is sent in pieces: `limit` keeps only the newest messages (the browser would freeze rendering thousands).
|
|
184
|
-
const all = readHistory(resolved(a), sid), limit = Number(url.searchParams.get('limit'));
|
|
185
|
-
return { session_id: sid, total: all.length, messages: limit > 0 && all.length > limit ? all.slice(-limit) : all };
|
|
186
|
-
});
|
|
187
|
-
route('GET', '/api/agents/:id/stats', ({ params, url }) => {
|
|
188
|
-
const a = agents.get(params[0]);
|
|
189
|
-
if (!a)
|
|
190
|
-
throw notFound('Agent not found');
|
|
191
|
-
const sid = url.searchParams.get('session') ?? a.session_id;
|
|
192
|
-
if (sid && sid !== a.session_id && !agents.sessions(a.id).some((s) => s.session_id === sid))
|
|
193
|
-
throw bad('That session does not belong to this agent');
|
|
194
|
-
return sessionStats(readHistory(resolved(a), sid));
|
|
195
|
-
});
|
|
196
|
-
route('GET', '/api/agents/:id/live', ({ params }) => liveTurn(params[0]));
|
|
197
|
-
route('GET', '/api/agents/:id/sessions', ({ params }) => agents.sessions(params[0]));
|
|
198
|
-
// ---- read-only git explorer (scoped to the agent's effective folder) ----
|
|
199
|
-
const agentCwd = (id) => { const a = agents.get(id); if (!a)
|
|
200
|
-
throw notFound('Agent not found'); return resolved(a).cwd; };
|
|
201
|
-
const gitSafe = async (fn) => { try {
|
|
202
|
-
return await fn();
|
|
203
|
-
}
|
|
204
|
-
catch (e) {
|
|
205
|
-
if (isPathError(e) || e instanceof GitOpError)
|
|
206
|
-
throw bad(e.message);
|
|
207
|
-
throw e;
|
|
208
|
-
} };
|
|
209
|
-
const qpath = (url) => { const p = url.searchParams.get('path'); if (!p)
|
|
210
|
-
throw bad('path is required'); return p; };
|
|
211
|
-
route('GET', '/api/agents/:id/git/status', ({ params }) => gitStatus(agentCwd(params[0])));
|
|
212
|
-
route('GET', '/api/agents/:id/git/ls', ({ params, url }) => gitSafe(() => gitList(agentCwd(params[0]), qpath(url))));
|
|
213
|
-
route('GET', '/api/agents/:id/git/tree', ({ params }) => gitTree(agentCwd(params[0])));
|
|
214
|
-
route('GET', '/api/agents/:id/git/diff', ({ params, url }) => gitSafe(() => gitDiff(agentCwd(params[0]), qpath(url), url.searchParams.get('old') ?? undefined)));
|
|
215
|
-
route('GET', '/api/agents/:id/git/file', ({ params, url }) => gitSafe(() => gitFile(agentCwd(params[0]), qpath(url))));
|
|
216
|
-
route('GET', '/api/agents/:id/git/raw', async ({ params, url, res }) => {
|
|
217
|
-
const img = await gitSafe(() => gitImagePath(agentCwd(params[0]), qpath(url)));
|
|
218
|
-
// Served only as an inert image: no sniffing, and a sandbox policy so an SVG can never run scripts.
|
|
219
|
-
res.writeHead(200, { 'content-type': img.type, 'content-length': img.size, 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', 'content-security-policy': "sandbox; default-src 'none'; style-src 'unsafe-inline'" });
|
|
220
|
-
createReadStream(img.abs).pipe(res);
|
|
221
|
-
});
|
|
222
|
-
// ---- git history and actions (writes: commit, pull, push, fetch, branches) ----
|
|
223
|
-
/** The API allows any origin (it is a local tool), so writes also check that the request comes from a local page. */
|
|
224
|
-
function localOnly(req) {
|
|
225
|
-
const origin = req.headers.origin;
|
|
226
|
-
if (!origin)
|
|
227
|
-
return;
|
|
228
|
-
let host = '';
|
|
229
|
-
try {
|
|
230
|
-
host = new URL(origin).hostname;
|
|
231
|
-
}
|
|
232
|
-
catch { /* invalid */ }
|
|
233
|
-
const own = String(req.headers.host ?? '').replace(/:\d+$/, '');
|
|
234
|
-
if (!['localhost', '127.0.0.1', '[::1]', '::1'].includes(host) && host !== own)
|
|
235
|
-
throw new HttpError(403, 'Git actions are only allowed from the local app');
|
|
236
|
-
}
|
|
237
|
-
const gitWrite = (path, fn) => route('POST', `/api/agents/:id/git/${path}`, async ({ req, params }) => { localOnly(req); const b = await body(req); return gitSafe(() => fn(agentCwd(params[0]), b)); });
|
|
238
|
-
route('GET', '/api/agents/:id/git/log', ({ params, url }) => gitSafe(() => gitLog(agentCwd(params[0]), Number(url.searchParams.get('skip') ?? 0))));
|
|
239
|
-
route('GET', '/api/agents/:id/git/commit', ({ params, url }) => gitSafe(() => gitCommitDetail(agentCwd(params[0]), url.searchParams.get('sha') ?? '')));
|
|
240
|
-
route('GET', '/api/agents/:id/git/commit-diff', ({ params, url }) => gitSafe(() => gitCommitDiff(agentCwd(params[0]), url.searchParams.get('sha') ?? '', qpath(url), url.searchParams.get('old') ?? undefined)));
|
|
241
|
-
route('GET', '/api/agents/:id/git/blame', ({ params, url }) => gitSafe(() => gitBlame(agentCwd(params[0]), qpath(url))));
|
|
242
|
-
// Smart switch: what would happen, plus which other agents are working in the same repository right now (a switch
|
|
243
|
-
// moves their files too).
|
|
244
|
-
route('GET', '/api/agents/:id/git/switch-plan', async ({ params, url }) => {
|
|
245
|
-
const cwd = agentCwd(params[0]);
|
|
246
|
-
const plan = await gitSafe(() => planSwitch(cwd, url.searchParams.get('branch') ?? ''));
|
|
247
|
-
const here = await findRepo(cwd);
|
|
248
|
-
const others = [];
|
|
249
|
-
if (here.ok)
|
|
250
|
-
for (const a of agents.list()) {
|
|
251
|
-
if (a.id === params[0] || !(a.status === 'running' || liveTurn(a.id)))
|
|
252
|
-
continue;
|
|
253
|
-
const there = await findRepo(resolved(a).cwd);
|
|
254
|
-
if (there.ok && there.repo.root === here.repo.root)
|
|
255
|
-
others.push({ id: a.id, name: a.name });
|
|
256
|
-
}
|
|
257
|
-
return { ...plan, others, selfRunning: agents.get(params[0])?.status === 'running' };
|
|
258
|
-
});
|
|
259
|
-
route('GET', '/api/agents/:id/git/stashes', ({ params }) => gitSafe(() => listStashes(agentCwd(params[0]))));
|
|
260
|
-
route('GET', '/api/agents/:id/git/stash', ({ params, url }) => gitSafe(() => stashDetail(agentCwd(params[0]), url.searchParams.get('sha') ?? '')));
|
|
261
|
-
route('GET', '/api/agents/:id/git/branches', ({ params }) => gitSafe(() => gitBranches(agentCwd(params[0]))));
|
|
262
|
-
gitWrite('commit', (cwd, b) => gitCommitChanges(cwd, String(b.message ?? ''), Array.isArray(b.paths) ? b.paths.map(String) : []));
|
|
263
|
-
gitWrite('fetch', (cwd) => gitFetch(cwd));
|
|
264
|
-
gitWrite('pull', (cwd, b) => gitPull(cwd, (['ff-only', 'merge', 'rebase'].includes(b.mode) ? b.mode : 'ff-only')));
|
|
265
|
-
gitWrite('push', (cwd) => gitPush(cwd));
|
|
266
|
-
gitWrite('switch', (cwd, b) => gitSwitch(cwd, String(b.branch ?? ''), !!b.create));
|
|
267
|
-
gitWrite('merge', (cwd, b) => gitMerge(cwd, String(b.branch ?? '')));
|
|
268
|
-
gitWrite('merge-abort', (cwd) => gitMergeAbort(cwd));
|
|
269
|
-
gitWrite('switch-smart', (cwd, b) => smartSwitch(cwd, String(b.branch ?? '')));
|
|
270
|
-
gitWrite('smart-finish', (cwd) => smartFinish(cwd));
|
|
271
|
-
gitWrite('smart-cancel', (cwd) => smartCancel(cwd));
|
|
272
|
-
gitWrite('stash-save', (cwd, b) => stashSave(cwd, String(b.message ?? '')));
|
|
273
|
-
gitWrite('stash-apply', (cwd, b) => stashApply(cwd, String(b.sha ?? ''), !!b.pop));
|
|
274
|
-
gitWrite('stash-drop', (cwd, b) => stashDrop(cwd, String(b.sha ?? '')));
|
|
275
|
-
gitWrite('discard', (cwd, b) => gitDiscardFile(cwd, String(b.path ?? ''), b.oldPath ? String(b.oldPath) : undefined));
|
|
276
|
-
gitWrite('discard-hunk', (cwd, b) => gitDiscardHunk(cwd, String(b.path ?? ''), Number(b.index), String(b.header ?? '')));
|
|
277
|
-
gitWrite('discard-lines', (cwd, b) => gitDiscardLines(cwd, String(b.path ?? ''), Number(b.index), String(b.header ?? ''), Array.isArray(b.lines) ? b.lines.map(Number) : []));
|
|
278
|
-
gitWrite('discard-all', (cwd) => gitDiscardAll(cwd));
|
|
279
|
-
gitWrite('rebase-continue', (cwd) => gitRebaseContinue(cwd));
|
|
280
|
-
gitWrite('resolve-side', (cwd, b) => gitResolveSide(cwd, String(b.path ?? ''), b.side));
|
|
281
|
-
gitWrite('resolve', (cwd, b) => gitResolveContent(cwd, String(b.path ?? ''), String(b.content ?? ''), !!b.keepMarkers));
|
|
282
|
-
gitWrite('unresolve', (cwd, b) => gitUnresolve(cwd, String(b.path ?? '')));
|
|
283
|
-
// ---- sessions across all managed agents ----
|
|
284
|
-
route('GET', '/api/sessions', () => {
|
|
285
|
-
const byAgent = new Map(agents.list().map((a) => [a.id, a]));
|
|
286
|
-
return agents.allSessions().map((s) => {
|
|
287
|
-
const a = byAgent.get(s.agent_id);
|
|
288
|
-
const msgs = readHistory({ provider: s.provider, cwd: s.cwd, session_id: s.session_id }, s.session_id);
|
|
289
|
-
const first = msgs.find((m) => m.role === 'user')?.blocks.find((b) => b.type === 'text');
|
|
290
|
-
const st = sessionStats(msgs);
|
|
291
|
-
return {
|
|
292
|
-
...s, current: a.session_id === s.session_id, message_count: msgs.length,
|
|
293
|
-
usage: st.usage, cost: st.cost, tool_calls: st.toolCalls, model: st.models[0]?.model ?? null,
|
|
294
|
-
preview: first && first.type === 'text' ? first.text.slice(0, 160) : '',
|
|
295
|
-
};
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
// ---- skills ----
|
|
299
|
-
const checkLoad = (v) => { if (v !== undefined && v !== 'always' && v !== 'on_demand')
|
|
300
|
-
throw bad('load must be always or on_demand'); };
|
|
301
|
-
route('GET', '/api/skills', () => skills.list());
|
|
302
|
-
route('GET', '/api/skills/usage', () => skills.usage());
|
|
303
|
-
route('POST', '/api/skills', async ({ req }) => {
|
|
304
|
-
const p = await body(req);
|
|
305
|
-
if (!String(p.name ?? '').trim())
|
|
306
|
-
throw bad('Name is required');
|
|
307
|
-
checkLoad(p.load);
|
|
308
|
-
try {
|
|
309
|
-
return skills.create({ name: p.name.trim(), description: p.description ?? '', content: p.content ?? '', load: p.load });
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
throw bad(`A skill named "${p.name}" already exists`);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
route('PATCH', '/api/skills/:id', async ({ req, params }) => {
|
|
316
|
-
const p = await body(req);
|
|
317
|
-
checkLoad(p.load);
|
|
318
|
-
try {
|
|
319
|
-
return skills.update(params[0], p) ?? (() => { throw notFound('Skill not found'); })();
|
|
320
|
-
}
|
|
321
|
-
catch (e) {
|
|
322
|
-
if (e instanceof HttpError)
|
|
323
|
-
throw e;
|
|
324
|
-
throw bad(`A skill named "${p.name}" already exists`);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
route('DELETE', '/api/skills/:id', ({ params }) => { if (!skills.remove(params[0]))
|
|
328
|
-
throw notFound('Skill not found'); return { ok: true }; });
|
|
329
|
-
// ---- agent types ----
|
|
330
|
-
route('GET', '/api/types', () => types.list());
|
|
331
|
-
route('POST', '/api/types', async ({ req }) => {
|
|
332
|
-
const p = await body(req);
|
|
333
|
-
if (!String(p.name ?? '').trim())
|
|
334
|
-
throw bad('Name is required');
|
|
335
|
-
if (!(p.provider in PROVIDERS))
|
|
336
|
-
throw bad('Pick a provider');
|
|
337
|
-
try {
|
|
338
|
-
return types.create({ ...p, name: p.name.trim() });
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
throw bad(`A type named "${p.name}" already exists`);
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
route('PATCH', '/api/types/:id', async ({ req, params }) => {
|
|
345
|
-
const t = types.update(params[0], await body(req));
|
|
346
|
-
if (!t)
|
|
347
|
-
throw notFound('Type not found');
|
|
348
|
-
return t;
|
|
349
|
-
});
|
|
350
|
-
route('DELETE', '/api/types/:id', ({ params }) => { if (!types.remove(params[0]))
|
|
351
|
-
throw notFound('Type not found'); return { ok: true }; });
|
|
352
|
-
route('POST', '/api/types/:id/spawn', async ({ req, params }) => {
|
|
353
|
-
const t = types.get(params[0]);
|
|
354
|
-
if (!t)
|
|
355
|
-
throw notFound('Type not found');
|
|
356
|
-
const p = await body(req);
|
|
357
|
-
validateAgentInput({ name: p.name, cwd: p.cwd, colony_id: p.colony_id });
|
|
358
|
-
if (agents.byName(p.name))
|
|
359
|
-
throw bad(`An agent named "${p.name}" already exists`);
|
|
360
|
-
const a = agents.create({
|
|
361
|
-
name: p.name.trim(), description: p.description ?? t.description, role: t.role, type_id: t.id, provider: t.provider, model: t.model,
|
|
362
|
-
system_prompt: t.system_prompt, permission: t.permission, cwd: p.cwd ?? '', skill_ids: t.skill_ids, skill_loads: t.skill_loads, colony_id: p.colony_id ?? null,
|
|
363
|
-
});
|
|
364
|
-
if (!a.effective.cwd) {
|
|
365
|
-
agents.remove(a.id);
|
|
366
|
-
throw bad('Choose a working folder, or put the agent in a colony that provides one.');
|
|
367
|
-
}
|
|
368
|
-
notifyAgentsChanged();
|
|
369
|
-
return a;
|
|
370
|
-
});
|
|
371
|
-
// ---- colonies ----
|
|
372
|
-
function validateColony(p, partial = false) {
|
|
373
|
-
if (!partial || p.name !== undefined) {
|
|
374
|
-
if (!String(p.name ?? '').trim())
|
|
375
|
-
throw bad('Name is required');
|
|
376
|
-
}
|
|
377
|
-
if (p.cwd && !existsSync(p.cwd))
|
|
378
|
-
throw bad(`Folder does not exist: ${p.cwd}`);
|
|
379
|
-
}
|
|
380
|
-
route('GET', '/api/colonies', () => colonies.list());
|
|
381
|
-
route('POST', '/api/colonies', async ({ req }) => {
|
|
382
|
-
const p = await body(req);
|
|
383
|
-
validateColony(p);
|
|
384
|
-
if (colonies.list().some((c) => c.name.toLowerCase() === p.name.trim().toLowerCase()))
|
|
385
|
-
throw bad(`A colony named "${p.name}" already exists`);
|
|
386
|
-
const c = colonies.create({ ...p, name: p.name.trim() });
|
|
387
|
-
notifyAgentsChanged();
|
|
388
|
-
return c;
|
|
389
|
-
});
|
|
390
|
-
route('PATCH', '/api/colonies/:id', async ({ req, params }) => {
|
|
391
|
-
const p = await body(req);
|
|
392
|
-
validateColony(p, true);
|
|
393
|
-
const cur = colonies.get(params[0]);
|
|
394
|
-
if (!cur)
|
|
395
|
-
throw notFound('Colony not found');
|
|
396
|
-
if (p.name && p.name.toLowerCase() !== cur.name.toLowerCase() && colonies.list().some((c) => c.name.toLowerCase() === p.name.trim().toLowerCase()))
|
|
397
|
-
throw bad(`A colony named "${p.name}" already exists`);
|
|
398
|
-
const c = db.transaction(() => {
|
|
399
|
-
const u = colonies.update(params[0], p.name ? { ...p, name: p.name.trim() } : p);
|
|
400
|
-
for (const id of u.agent_ids) {
|
|
401
|
-
const why = channelPermissionError(id);
|
|
402
|
-
if (why)
|
|
403
|
-
throw bad(why);
|
|
404
|
-
}
|
|
405
|
-
return u;
|
|
406
|
-
})();
|
|
407
|
-
notifyAgentsChanged();
|
|
408
|
-
return c;
|
|
409
|
-
});
|
|
410
|
-
route('DELETE', '/api/colonies/:id', ({ params }) => {
|
|
411
|
-
if (!colonies.remove(params[0]))
|
|
412
|
-
throw notFound('Colony not found');
|
|
413
|
-
notifyAgentsChanged();
|
|
414
|
-
return { ok: true };
|
|
415
|
-
});
|
|
416
|
-
// ---- external connections ----
|
|
417
|
-
const KINDS = ['telegram', 'slack'];
|
|
418
|
-
const REQUIRED = { telegram: ['token'], slack: ['bot_token', 'app_token'] };
|
|
419
|
-
function validateConnection(kind, config, agentId, enabled) {
|
|
420
|
-
const missing = (REQUIRED[kind] ?? []).filter((k) => typeof config[k] !== 'string' || !config[k].trim());
|
|
421
|
-
if (missing.length)
|
|
422
|
-
throw bad(`Missing: ${missing.join(', ')}`);
|
|
423
|
-
if (config.effort !== undefined && !["", "low", "medium", "high"].includes(config.effort))
|
|
424
|
-
throw bad('effort must be empty, low, medium or high');
|
|
425
|
-
if (config.on_silent !== undefined && !['notice', 'send_text', 'ignore'].includes(config.on_silent))
|
|
426
|
-
throw bad('on_silent must be notice, send_text or ignore');
|
|
427
|
-
if (config.group_mode !== undefined && !['mention', 'open'].includes(config.group_mode))
|
|
428
|
-
throw bad('group_mode must be mention or open');
|
|
429
|
-
if (config.vision !== undefined && config.vision !== null && !validVision(config.vision))
|
|
430
|
-
throw bad('vision must be { provider, model } or empty');
|
|
431
|
-
if (config.chats !== undefined && (!Array.isArray(config.chats) || config.chats.length > 50))
|
|
432
|
-
throw bad('chats must be a list of at most 50 groups');
|
|
433
|
-
if (config.aliases !== undefined && (!Array.isArray(config.aliases) || config.aliases.length > 10))
|
|
434
|
-
throw bad('aliases must be a list of at most 10 names');
|
|
435
|
-
if (agentId) {
|
|
436
|
-
const a = agents.get(agentId);
|
|
437
|
-
if (!a)
|
|
438
|
-
throw bad('That agent no longer exists');
|
|
439
|
-
if (enabled && a.effective.permission === 'plan')
|
|
440
|
-
throw bad(`"${a.name}" is in Plan mode and cannot reply. Give it "Edit files" permission first.`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
/** Group ids and alias names from the form: trimmed, de-duplicated, blanks dropped. */
|
|
444
|
-
function cleanGroupConfig(config) {
|
|
445
|
-
if (Array.isArray(config.chats)) {
|
|
446
|
-
const seen = new Set();
|
|
447
|
-
config.chats = config.chats.map((c) => ({ id: String(c?.id ?? '').trim(), name: c?.name ? String(c.name).trim() : undefined })).filter((c) => c.id && !seen.has(c.id) && seen.add(c.id));
|
|
448
|
-
}
|
|
449
|
-
if (Array.isArray(config.aliases))
|
|
450
|
-
config.aliases = [...new Set(config.aliases.map((x) => String(x).trim()).filter(Boolean))];
|
|
451
|
-
return config;
|
|
452
|
-
}
|
|
453
|
-
const cleanAllowed = (v) => (Array.isArray(v) ? v : []).map((u) => ({ id: String(u?.id ?? '').trim(), name: u?.name ? String(u.name) : undefined, admin: !!u?.admin })).filter((u) => u.id);
|
|
454
|
-
route('GET', '/api/connections', () => connections.list().map(publicConnection));
|
|
455
|
-
route('POST', '/api/connections', async ({ req }) => {
|
|
456
|
-
const p = await body(req);
|
|
457
|
-
if (!KINDS.includes(p.kind))
|
|
458
|
-
throw bad(`Kind must be one of: ${KINDS.join(', ')}`);
|
|
459
|
-
const name = String(p.name ?? '').trim();
|
|
460
|
-
if (!name)
|
|
461
|
-
throw bad('Name is required');
|
|
462
|
-
if (connections.list().some((c) => c.name.toLowerCase() === name.toLowerCase()))
|
|
463
|
-
throw bad(`A connection named "${name}" already exists`);
|
|
464
|
-
const config = cleanGroupConfig(mergeConfig({}, p.config));
|
|
465
|
-
const enabled = p.enabled !== false;
|
|
466
|
-
validateConnection(p.kind, config, p.agent_id, enabled);
|
|
467
|
-
const c = connections.create({ kind: p.kind, name, agent_id: p.agent_id || null, config, allowed: cleanAllowed(p.allowed), enabled });
|
|
468
|
-
syncChannelSkill(c.agent_id);
|
|
469
|
-
await startConnection(c.id);
|
|
470
|
-
notifyAgentsChanged();
|
|
471
|
-
return publicConnection(connections.get(c.id));
|
|
472
|
-
});
|
|
473
|
-
route('PATCH', '/api/connections/:id', async ({ req, params }) => {
|
|
474
|
-
const p = await body(req);
|
|
475
|
-
const cur = connections.get(params[0]);
|
|
476
|
-
if (!cur)
|
|
477
|
-
throw notFound('Connection not found');
|
|
478
|
-
const name = p.name !== undefined ? String(p.name).trim() : cur.name;
|
|
479
|
-
if (!name)
|
|
480
|
-
throw bad('Name is required');
|
|
481
|
-
if (name.toLowerCase() !== cur.name.toLowerCase() && connections.list().some((c) => c.name.toLowerCase() === name.toLowerCase()))
|
|
482
|
-
throw bad(`A connection named "${name}" already exists`);
|
|
483
|
-
const config = cleanGroupConfig(mergeConfig(cur.config, p.config));
|
|
484
|
-
const agent_id = p.agent_id !== undefined ? (p.agent_id || null) : cur.agent_id;
|
|
485
|
-
const enabled = p.enabled !== undefined ? !!p.enabled : cur.enabled;
|
|
486
|
-
validateConnection(cur.kind, config, agent_id, enabled);
|
|
487
|
-
connections.update(cur.id, { name, config, agent_id, enabled, allowed: p.allowed !== undefined ? cleanAllowed(p.allowed) : cur.allowed });
|
|
488
|
-
syncChannelSkill(cur.agent_id, agent_id);
|
|
489
|
-
await startConnection(cur.id);
|
|
490
|
-
notifyAgentsChanged();
|
|
491
|
-
return publicConnection(connections.get(cur.id));
|
|
492
|
-
});
|
|
493
|
-
route('DELETE', '/api/connections/:id', async ({ params }) => {
|
|
494
|
-
await stopConnection(params[0]);
|
|
495
|
-
const gone = connections.get(params[0]);
|
|
496
|
-
if (!connections.remove(params[0]))
|
|
497
|
-
throw notFound('Connection not found');
|
|
498
|
-
syncChannelSkill(gone?.agent_id);
|
|
499
|
-
notifyAgentsChanged();
|
|
500
|
-
return { ok: true };
|
|
501
|
-
});
|
|
502
|
-
route('POST', '/api/connections/:id/restart', async ({ params }) => {
|
|
503
|
-
if (!connections.get(params[0]))
|
|
504
|
-
throw notFound('Connection not found');
|
|
505
|
-
await startConnection(params[0]);
|
|
506
|
-
return { status: connectionStatus(params[0]) };
|
|
507
|
-
});
|
|
508
|
-
route('POST', '/api/connections/:id/test', async ({ params }) => {
|
|
509
|
-
if (!connections.get(params[0]))
|
|
510
|
-
throw notFound('Connection not found');
|
|
511
|
-
try {
|
|
512
|
-
return { ok: true, detail: await testConnection(params[0]) };
|
|
513
|
-
}
|
|
514
|
-
catch (e) {
|
|
515
|
-
throw bad(e instanceof Error ? e.message : String(e));
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
route('PATCH', '/api/connections/:id/threads/:threadId', async ({ req, params }) => {
|
|
519
|
-
const th = threads.get(params[1]);
|
|
520
|
-
if (!th || th.connection_id !== params[0])
|
|
521
|
-
throw notFound('Thread not found');
|
|
522
|
-
const p = await body(req);
|
|
523
|
-
if (typeof p.muted === 'boolean')
|
|
524
|
-
threads.setMuted(th.id, p.muted);
|
|
525
|
-
return threads.get(th.id);
|
|
526
|
-
});
|
|
527
|
-
route('GET', '/api/connections/:id/threads', ({ params }) => {
|
|
528
|
-
if (!connections.get(params[0]))
|
|
529
|
-
throw notFound('Connection not found');
|
|
530
|
-
return threads.forConnection(params[0]);
|
|
531
|
-
});
|
|
532
|
-
// ---- orchestration ----
|
|
533
|
-
route('GET', '/api/orchestrators/:id/workers', ({ params }) => {
|
|
534
|
-
const o = agents.get(params[0]);
|
|
535
|
-
if (!o)
|
|
536
|
-
throw notFound('Agent not found');
|
|
537
|
-
return o.worker_ids.map((id) => agents.get(id)).filter(Boolean).map((w) => ({ name: w.name, role: w.role, description: w.description, provider: w.provider, busy: !!liveTurn(w.id) }));
|
|
538
|
-
});
|
|
539
|
-
route('PUT', '/api/orchestrators/:id/workers', async ({ req, params }) => {
|
|
540
|
-
const o = agents.get(params[0]);
|
|
541
|
-
if (!o)
|
|
542
|
-
throw notFound('Agent not found');
|
|
543
|
-
if (o.role !== 'orchestrator')
|
|
544
|
-
throw bad('Only orchestrators can have subagents');
|
|
545
|
-
const { worker_ids } = await body(req);
|
|
546
|
-
agents.setWorkers(o.id, worker_ids ?? []);
|
|
547
|
-
notifyAgentsChanged();
|
|
548
|
-
return agents.get(o.id);
|
|
549
|
-
});
|
|
550
|
-
route('POST', '/api/dispatch', async ({ req }) => {
|
|
551
|
-
const { from, agent, task } = await body(req);
|
|
552
|
-
if (!from || !agent || !task)
|
|
553
|
-
throw bad('from, agent and task are required');
|
|
554
|
-
try {
|
|
555
|
-
return await dispatch(from, agent, task);
|
|
556
|
-
}
|
|
557
|
-
catch (e) {
|
|
558
|
-
throw bad(e instanceof Error ? e.message : String(e));
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
// `skill_read`: the text of one of the skills the agent has, loaded when it needs it.
|
|
562
|
-
route('POST', '/api/skills/read', async ({ req }) => {
|
|
563
|
-
const { from, name } = await body(req);
|
|
564
|
-
const agent = agents.get(String(from ?? ''));
|
|
565
|
-
if (!agent)
|
|
566
|
-
throw bad('Unknown agent');
|
|
567
|
-
const mine = resolved(agent).skill_ids.map((id) => skills.get(id)).filter((s) => !!s);
|
|
568
|
-
const want = String(name ?? '').trim().toLowerCase();
|
|
569
|
-
const s = mine.find((x) => x.name.toLowerCase() === want);
|
|
570
|
-
if (!s)
|
|
571
|
-
throw bad(`No skill named "${name}". Your skills: ${mine.map((x) => x.name).join(', ') || '(none)'}.`);
|
|
572
|
-
return { name: s.name, content: s.content.trim() };
|
|
573
|
-
});
|
|
574
|
-
// ---- notebook ----
|
|
575
|
-
/** Who a note came from, so it can be audited: the date plus, for chat turns, where and who. */
|
|
576
|
-
function noteSource(agentId) {
|
|
577
|
-
const day = new Date().toISOString().slice(0, 10), o = liveOrigin(agentId);
|
|
578
|
-
return o ? `${day} · ${o.platform}: ${o.userName}` : day;
|
|
579
|
-
}
|
|
580
|
-
function notebookAgent(from) {
|
|
581
|
-
const agent = agents.get(String(from ?? ''));
|
|
582
|
-
if (!agent)
|
|
583
|
-
throw bad('Unknown agent');
|
|
584
|
-
if (!mcpCaps(resolved(agent)).includes('memory'))
|
|
585
|
-
throw bad('This agent does not have the Notebook skill.');
|
|
586
|
-
return agent;
|
|
587
|
-
}
|
|
588
|
-
const nbSafe = (fn) => { try {
|
|
589
|
-
return fn();
|
|
590
|
-
}
|
|
591
|
-
catch (e) {
|
|
592
|
-
if (e instanceof NotebookError)
|
|
593
|
-
throw bad(e.message);
|
|
594
|
-
throw e;
|
|
595
|
-
} };
|
|
596
|
-
const nbView = (n) => ({ content: n.content, version: n.version, size: n.content.length, max: NOTEBOOK_MAX });
|
|
597
|
-
route('POST', '/api/notebook/read', async ({ req }) => { const { from } = await body(req); return nbView(notebooks.get(notebookAgent(from).id)); });
|
|
598
|
-
route('POST', '/api/notebook/add', async ({ req }) => {
|
|
599
|
-
const { from, section, note } = await body(req);
|
|
600
|
-
const a = notebookAgent(from);
|
|
601
|
-
const r = nbSafe(() => notebooks.add(a.id, String(section ?? ''), String(note ?? ''), noteSource(a.id), liveIsDirect(a.id)));
|
|
602
|
-
return { added: r.added, note: r.note, ...nbView(r.notebook) };
|
|
603
|
-
});
|
|
604
|
-
route('POST', '/api/notebook/rewrite', async ({ req }) => {
|
|
605
|
-
const { from, content, version } = await body(req);
|
|
606
|
-
const a = notebookAgent(from);
|
|
607
|
-
return { added: true, ...nbView(nbSafe(() => notebooks.rewrite(a.id, String(content ?? ''), Number(version), liveIsDirect(a.id)))) };
|
|
608
|
-
});
|
|
609
|
-
route('GET', '/api/agents/:id/notebook', ({ params }) => {
|
|
610
|
-
const a = agents.get(params[0]);
|
|
611
|
-
if (!a)
|
|
612
|
-
throw notFound('Agent not found');
|
|
613
|
-
const n = notebooks.get(a.id);
|
|
614
|
-
return { ...nbView(n), updated_at: n.updated_at, updated_by: n.updated_by, enabled: resolved(a).skill_ids.includes(NOTEBOOK_SKILL_ID), skill_id: NOTEBOOK_SKILL_ID };
|
|
615
|
-
});
|
|
616
|
-
route('PUT', '/api/agents/:id/notebook', async ({ req, params }) => {
|
|
617
|
-
localOnly(req);
|
|
618
|
-
const a = agents.get(params[0]);
|
|
619
|
-
if (!a)
|
|
620
|
-
throw notFound('Agent not found');
|
|
621
|
-
const p = await body(req);
|
|
622
|
-
const n = nbSafe(() => notebooks.edit(a.id, String(p.content ?? ''), typeof p.version === 'number' ? p.version : undefined));
|
|
623
|
-
return { ...nbView(n), updated_at: n.updated_at, updated_by: n.updated_by };
|
|
624
|
-
});
|
|
625
|
-
route('POST', '/api/channel/reply', async ({ req }) => {
|
|
626
|
-
const { from, text } = await body(req);
|
|
627
|
-
try {
|
|
628
|
-
return await channelReply(String(from ?? ''), String(text ?? ''));
|
|
629
|
-
}
|
|
630
|
-
catch (e) {
|
|
631
|
-
if (e instanceof ChannelError)
|
|
632
|
-
throw bad(e.message);
|
|
633
|
-
throw e;
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
route('POST', '/api/channel/send-file', async ({ req }) => {
|
|
637
|
-
const { from, path, caption } = await body(req);
|
|
638
|
-
try {
|
|
639
|
-
return await channelSendFile(String(from ?? ''), String(path ?? ''), caption === undefined ? undefined : String(caption));
|
|
640
|
-
}
|
|
641
|
-
catch (e) {
|
|
642
|
-
if (e instanceof ChannelError || e instanceof SendError)
|
|
643
|
-
throw bad(e.message);
|
|
644
|
-
throw e;
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
route('POST', '/api/channel/mute', async ({ req }) => {
|
|
648
|
-
const { from, muted } = await body(req);
|
|
649
|
-
try {
|
|
650
|
-
return channelMute(String(from ?? ''), muted !== false);
|
|
651
|
-
}
|
|
652
|
-
catch (e) {
|
|
653
|
-
if (e instanceof ChannelError)
|
|
654
|
-
throw bad(e.message);
|
|
655
|
-
throw e;
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
route('POST', '/api/wake', async ({ req }) => {
|
|
659
|
-
const { from, minutes, note } = await body(req);
|
|
660
|
-
try {
|
|
661
|
-
return scheduleWake(String(from ?? ''), minutes, note);
|
|
662
|
-
}
|
|
663
|
-
catch (e) {
|
|
664
|
-
if (e instanceof WakeError)
|
|
665
|
-
throw bad(e.message);
|
|
666
|
-
throw e;
|
|
667
|
-
}
|
|
668
|
-
});
|
|
669
|
-
// Recurring schedules and pending wake-ups: the agent's tools (`from` is its id) and the interface.
|
|
670
|
-
const scheduleGuard = (fn) => { try {
|
|
671
|
-
return fn();
|
|
672
|
-
}
|
|
673
|
-
catch (e) {
|
|
674
|
-
if (e instanceof ScheduleError)
|
|
675
|
-
throw bad(e.message);
|
|
676
|
-
throw e;
|
|
677
|
-
} };
|
|
678
|
-
route('POST', '/api/wake-when-done', async ({ req }) => {
|
|
679
|
-
const b = await body(req);
|
|
680
|
-
try {
|
|
681
|
-
const r = createWatch(String(b.from ?? ''), b);
|
|
682
|
-
return r;
|
|
683
|
-
}
|
|
684
|
-
catch (e) {
|
|
685
|
-
if (e instanceof WatchError)
|
|
686
|
-
throw bad(e.message);
|
|
687
|
-
throw e;
|
|
688
|
-
}
|
|
689
|
-
});
|
|
690
|
-
route('POST', '/api/schedules/create', async ({ req }) => {
|
|
691
|
-
const b = await body(req);
|
|
692
|
-
return scheduleGuard(() => { const r = createSchedule(String(b.from ?? ''), b); 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 }; });
|
|
693
|
-
});
|
|
694
|
-
route('POST', '/api/schedules/list', async ({ req }) => { const b = await body(req); return { schedules: listFor(String(b.from ?? '')) }; });
|
|
695
|
-
route('POST', '/api/schedules/cancel', async ({ req }) => {
|
|
696
|
-
const b = await body(req);
|
|
697
|
-
const from = String(b.from ?? ''), id = String(b.id ?? '');
|
|
698
|
-
try {
|
|
699
|
-
cancelWatchFor(from, id);
|
|
700
|
-
return { cancelled: true };
|
|
701
|
-
}
|
|
702
|
-
catch (e) {
|
|
703
|
-
if (!(e instanceof WatchError))
|
|
704
|
-
throw e;
|
|
705
|
-
} // not one of its watches: a schedule then
|
|
706
|
-
scheduleGuard(() => cancelFor(from, id));
|
|
707
|
-
return { cancelled: true };
|
|
708
|
-
});
|
|
709
|
-
route('GET', '/api/schedules', () => {
|
|
710
|
-
const once = listWakeups().map((w) => ({
|
|
711
|
-
id: w.id, agent_id: w.agent_id, connection_id: w.connection_id, thread_id: w.thread_id, kind: 'once', expr: '', tz: '', note: w.note, enabled: true, next_due: w.due_at, created_at: w.created_at,
|
|
712
|
-
last_fired_at: null, fire_count: 0, last_status: null, last_detail: null, agent_name: agents.get(w.agent_id)?.name ?? '?', place: '', description: 'once',
|
|
713
|
-
}));
|
|
714
|
-
const watching = pendingWatches().map((w) => ({
|
|
715
|
-
id: w.id, agent_id: w.agent_id, connection_id: w.connection_id, thread_id: w.thread_id, kind: 'watch', expr: String(w.pid), tz: '', note: w.note, enabled: true, next_due: w.deadline, created_at: w.started_at,
|
|
716
|
-
last_fired_at: null, fire_count: 0, last_status: null, last_detail: null, agent_name: agents.get(w.agent_id)?.name ?? '?', place: '',
|
|
717
|
-
description: `process ${w.pid}${w.file ? ` · file ${w.file}` : ''}${w.log ? ` · log ${w.log}` : ''}`,
|
|
718
|
-
}));
|
|
719
|
-
return [...listAll(), ...once, ...watching];
|
|
720
|
-
});
|
|
721
|
-
route('GET', '/api/schedules/:id/runs', ({ params }) => runsOf(params[0]));
|
|
722
|
-
route('PATCH', '/api/schedules/:id', async ({ req, params }) => {
|
|
723
|
-
const b = await body(req);
|
|
724
|
-
return setEnabled(params[0], !!b.enabled) ?? (() => { throw notFound('Schedule not found'); })();
|
|
725
|
-
});
|
|
726
|
-
route('DELETE', '/api/schedules/:id', ({ params }) => {
|
|
727
|
-
if (!removeSchedule(params[0]) && !removeWakeup(params[0]) && !removeWatch(params[0]))
|
|
728
|
-
throw notFound('Schedule not found');
|
|
729
|
-
return { ok: true };
|
|
730
|
-
});
|
|
731
|
-
route('GET', '/api/dispatches', () => dispatches.recent());
|
|
732
|
-
// ---- folder picker ----
|
|
733
|
-
route('GET', '/api/fs/dirs', ({ url }) => {
|
|
734
|
-
const p = resolve(url.searchParams.get('path') || homedir());
|
|
735
|
-
if (!existsSync(p) || !statSync(p).isDirectory())
|
|
736
|
-
throw bad('Not a folder');
|
|
737
|
-
let entries = [];
|
|
738
|
-
try {
|
|
739
|
-
entries = readdirSync(p, { withFileTypes: true })
|
|
740
|
-
.filter((d) => d.isDirectory() && !d.name.startsWith('.') && d.name !== 'node_modules').map((d) => d.name).sort((a, b) => a.localeCompare(b));
|
|
741
|
-
}
|
|
742
|
-
catch { /* unreadable */ }
|
|
743
|
-
return { path: p, parent: p === '/' ? null : dirname(p), dirs: entries.map((n) => ({ name: n, path: join(p, n) })) };
|
|
744
|
-
});
|
|
22
|
+
/**
|
|
23
|
+
* TODO(security): the API answers any origin (`*`) and the endpoints that act on the machine (git writes, resources, notebook edits,
|
|
24
|
+
* terminals) only check the browser's Origin (see http/guards.ts). Pinning Host/Origin for every route (DNS rebinding, a foreign page
|
|
25
|
+
* reading GETs) is NOT applied: it has to be proven against the Next rewrite (/api/* on the UI's origin, forwarded with whatever Host
|
|
26
|
+
* and Origin Next sends) and the MCP clients in server/mcp before it ships. Candidate: refuse a Host that is not loopback or the
|
|
27
|
+
* configured UI host, and reply `access-control-allow-origin` only to loopback origins.
|
|
28
|
+
*/
|
|
745
29
|
export async function handle(req, res) {
|
|
746
30
|
res.setHeader('access-control-allow-origin', '*');
|
|
747
31
|
res.setHeader('access-control-allow-methods', 'GET,POST,PATCH,PUT,DELETE,OPTIONS');
|
|
@@ -750,25 +34,21 @@ export async function handle(req, res) {
|
|
|
750
34
|
res.writeHead(204);
|
|
751
35
|
return res.end();
|
|
752
36
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
console.error('[api]', e);
|
|
770
|
-
return json(res, 500, { error: 'Unexpected server error' });
|
|
771
|
-
}
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
39
|
+
const hit = match(req.method ?? '', url.pathname);
|
|
40
|
+
if (!hit)
|
|
41
|
+
return json(res, 404, { error: 'No such route' });
|
|
42
|
+
const out = await hit.route.handler({ req, res, params: hit.params, url });
|
|
43
|
+
if (res.headersSent)
|
|
44
|
+
return; // the handler streamed its own response
|
|
45
|
+
return json(res, 200, out ?? null);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
const h = toHttpError(e);
|
|
49
|
+
if (h)
|
|
50
|
+
return res.headersSent ? res.end() : json(res, h.status, { error: h.message }, h.status === 413 ? { connection: 'close' } : {});
|
|
51
|
+
console.error('[api]', e);
|
|
52
|
+
return res.headersSent ? res.end() : json(res, 500, { error: 'Unexpected server error' });
|
|
772
53
|
}
|
|
773
|
-
json(res, 404, { error: 'No such route' });
|
|
774
54
|
}
|