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