@cmgmyr/hive 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/claude-plugin/.claude-plugin/plugin.json +7 -0
  4. package/claude-plugin/README.md +47 -0
  5. package/claude-plugin/hooks/hooks.json +14 -0
  6. package/claude-plugin/kickoff.mjs +41 -0
  7. package/claude-plugin/skills/cleanup/SKILL.md +60 -0
  8. package/dist/abi.js +220 -0
  9. package/dist/abiProbe.js +9 -0
  10. package/dist/backgroundTasks.js +51 -0
  11. package/dist/backup.js +378 -0
  12. package/dist/brief.js +113 -0
  13. package/dist/build-info.json +1 -0
  14. package/dist/claudeDir.js +5 -0
  15. package/dist/cli.js +2901 -0
  16. package/dist/codexHome.js +297 -0
  17. package/dist/config.js +72 -0
  18. package/dist/context.js +256 -0
  19. package/dist/contextCheckpoint.js +55 -0
  20. package/dist/dashboard.js +1158 -0
  21. package/dist/dataDir.js +89 -0
  22. package/dist/db.js +421 -0
  23. package/dist/dispatcher.js +150 -0
  24. package/dist/firstPrompt.js +6 -0
  25. package/dist/harnesses.js +173 -0
  26. package/dist/help.js +353 -0
  27. package/dist/hook.js +197 -0
  28. package/dist/hooks.js +50 -0
  29. package/dist/index.js +34 -0
  30. package/dist/kickoff.js +191 -0
  31. package/dist/leadMessage.js +98 -0
  32. package/dist/mcpConfig.js +121 -0
  33. package/dist/processes.js +161 -0
  34. package/dist/profiles.js +278 -0
  35. package/dist/projectYml.js +254 -0
  36. package/dist/prompts.js +18 -0
  37. package/dist/ptys.js +146 -0
  38. package/dist/result.js +53 -0
  39. package/dist/scheduler.js +1622 -0
  40. package/dist/sessionProbe.js +131 -0
  41. package/dist/slug.js +29 -0
  42. package/dist/spawn.js +370 -0
  43. package/dist/stateProvenance.js +97 -0
  44. package/dist/statusline.js +82 -0
  45. package/dist/strictInput.js +52 -0
  46. package/dist/teardown.js +89 -0
  47. package/dist/tmux.js +1295 -0
  48. package/dist/tools/agents.js +1246 -0
  49. package/dist/tools/kv.js +89 -0
  50. package/dist/tools/leases.js +87 -0
  51. package/dist/tools/meta.js +183 -0
  52. package/dist/tools/pads.js +279 -0
  53. package/dist/tools/params.js +14 -0
  54. package/dist/tools/todos.js +382 -0
  55. package/dist/tools/wakes.js +466 -0
  56. package/dist/transcript.js +123 -0
  57. package/dist/version.js +59 -0
  58. package/package.json +62 -0
  59. package/profiles/orchestration/posture.md +27 -0
  60. package/profiles/orchestration/runbook.md +72 -0
  61. package/profiles/orchestration/worker.md +38 -0
  62. package/profiles/simple/posture.md +14 -0
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ import { db } from "../db.js";
3
+ import { currentActor, effectiveProjectId } from "../context.js";
4
+ import { run } from "../result.js";
5
+ import { idParam, projectIdParam } from "./params.js";
6
+ function purgeExpired(projectId) {
7
+ db.prepare("DELETE FROM kv WHERE project_id = ? AND expires_at IS NOT NULL AND expires_at < datetime('now')").run(projectId);
8
+ }
9
+ export function registerKv(server) {
10
+ server.registerTool("kv_set", {
11
+ description: "Set a small shared JSON value other sessions can discover. Optional TTL in seconds.",
12
+ inputSchema: {
13
+ key: z.string(),
14
+ value: z.any().describe("Any JSON value."),
15
+ ttl_seconds: z.number().int().positive().optional(),
16
+ project_id: projectIdParam,
17
+ },
18
+ outputSchema: { project_id: idParam, key: z.string() },
19
+ }, (args) => run(() => {
20
+ const projectId = effectiveProjectId(args.project_id);
21
+ purgeExpired(projectId);
22
+ const actor = currentActor();
23
+ db.prepare(`INSERT INTO kv (project_id, key, value, updated_by, expires_at)
24
+ VALUES (?, ?, ?, ?, CASE WHEN ? IS NULL THEN NULL ELSE datetime('now', printf('+%d seconds', ?)) END)
25
+ ON CONFLICT(project_id, key) DO UPDATE SET
26
+ value = excluded.value,
27
+ updated_by = excluded.updated_by,
28
+ updated_at = datetime('now'),
29
+ expires_at = excluded.expires_at`).run(projectId, args.key, JSON.stringify(args.value ?? null), actor, args.ttl_seconds ?? null, args.ttl_seconds ?? null);
30
+ return { project_id: projectId, key: args.key };
31
+ }));
32
+ server.registerTool("kv_get", {
33
+ description: "Get a shared JSON value by key.",
34
+ inputSchema: { key: z.string(), project_id: projectIdParam },
35
+ }, (args) => run(() => {
36
+ const projectId = effectiveProjectId(args.project_id);
37
+ purgeExpired(projectId);
38
+ const row = db
39
+ .prepare("SELECT * FROM kv WHERE project_id = ? AND key = ?")
40
+ .get(projectId, args.key);
41
+ if (!row)
42
+ return { project_id: projectId, key: args.key, found: false };
43
+ return {
44
+ project_id: projectId,
45
+ key: row.key,
46
+ found: true,
47
+ value: JSON.parse(row.value),
48
+ updated_by: row.updated_by,
49
+ updated_at: row.updated_at,
50
+ expires_at: row.expires_at,
51
+ };
52
+ }));
53
+ server.registerTool("kv_list", {
54
+ description: "List shared values, optionally filtered by key prefix.",
55
+ inputSchema: { prefix: z.string().optional(), project_id: projectIdParam },
56
+ }, (args) => run(() => {
57
+ const projectId = effectiveProjectId(args.project_id);
58
+ purgeExpired(projectId);
59
+ let sql = "SELECT * FROM kv WHERE project_id = ?";
60
+ const params = [projectId];
61
+ if (args.prefix) {
62
+ sql += " AND key LIKE ?";
63
+ params.push(`${args.prefix}%`);
64
+ }
65
+ sql += " ORDER BY key";
66
+ const rows = db.prepare(sql).all(...params);
67
+ return {
68
+ project_id: projectId,
69
+ entries: rows.map((r) => ({
70
+ key: r.key,
71
+ value: JSON.parse(r.value),
72
+ updated_by: r.updated_by,
73
+ updated_at: r.updated_at,
74
+ expires_at: r.expires_at,
75
+ })),
76
+ };
77
+ }));
78
+ server.registerTool("kv_delete", {
79
+ description: "Delete a shared value by key.",
80
+ inputSchema: { key: z.string(), project_id: projectIdParam },
81
+ outputSchema: { project_id: idParam, key: z.string(), deleted: z.boolean() },
82
+ }, (args) => run(() => {
83
+ const projectId = effectiveProjectId(args.project_id);
84
+ const info = db
85
+ .prepare("DELETE FROM kv WHERE project_id = ? AND key = ?")
86
+ .run(projectId, args.key);
87
+ return { project_id: projectId, key: args.key, deleted: info.changes > 0 };
88
+ }));
89
+ }
@@ -0,0 +1,87 @@
1
+ import { z } from "zod";
2
+ import { db } from "../db.js";
3
+ import { currentActor, effectiveProjectId } from "../context.js";
4
+ import { run } from "../result.js";
5
+ import { idParam, projectIdParam } from "./params.js";
6
+ function purgeExpired(projectId) {
7
+ db.prepare("DELETE FROM leases WHERE project_id = ? AND expires_at < datetime('now')").run(projectId);
8
+ }
9
+ export function readLease(projectId, key) {
10
+ return db.prepare("SELECT * FROM leases WHERE project_id = ? AND lock_key = ?").get(projectId, key);
11
+ }
12
+ export function extendOwnedLease(projectId, key, actor, ttlSeconds) {
13
+ const info = db
14
+ .prepare(`UPDATE leases SET expires_at = datetime('now', printf('+%d seconds', ?))
15
+ WHERE project_id = ? AND lock_key = ? AND owner = ?`)
16
+ .run(ttlSeconds, projectId, key, actor);
17
+ return info.changes > 0;
18
+ }
19
+ export function registerLeases(server) {
20
+ server.registerTool("lease_acquire", {
21
+ description: "Try to take a named lease on a shared work area (non-blocking). Re-taking your own lease extends it. Leases expire on their own.",
22
+ inputSchema: {
23
+ key: z.string().describe('Stable and specific, like "file:src/api/routes.ts".'),
24
+ ttl_seconds: z.number().int().positive(),
25
+ project_id: projectIdParam,
26
+ },
27
+ outputSchema: {
28
+ project_id: idParam,
29
+ key: z.string(),
30
+ acquired: z.boolean(),
31
+ expires_at: z.string().optional(),
32
+ extended: z.boolean().optional(),
33
+ held_by: z.string().optional(),
34
+ },
35
+ }, (args) => run(() => {
36
+ const projectId = effectiveProjectId(args.project_id);
37
+ purgeExpired(projectId);
38
+ const actor = currentActor();
39
+ const inserted = db
40
+ .prepare(`INSERT INTO leases (project_id, lock_key, owner, expires_at)
41
+ VALUES (?, ?, ?, datetime('now', printf('+%d seconds', ?)))
42
+ ON CONFLICT(project_id, lock_key) DO NOTHING
43
+ RETURNING expires_at`)
44
+ .get(projectId, args.key, actor, args.ttl_seconds);
45
+ if (inserted) {
46
+ return { project_id: projectId, key: args.key, acquired: true, expires_at: inserted.expires_at };
47
+ }
48
+ const row = readLease(projectId, args.key);
49
+ if (!row) {
50
+ return { project_id: projectId, key: args.key, acquired: false };
51
+ }
52
+ if (row.owner === actor) {
53
+ if (extendOwnedLease(projectId, args.key, actor, args.ttl_seconds)) {
54
+ return { project_id: projectId, key: args.key, acquired: true, extended: true };
55
+ }
56
+ const current = readLease(projectId, args.key);
57
+ if (!current) {
58
+ return { project_id: projectId, key: args.key, acquired: false };
59
+ }
60
+ return {
61
+ project_id: projectId,
62
+ key: args.key,
63
+ acquired: false,
64
+ held_by: current.owner,
65
+ expires_at: current.expires_at,
66
+ };
67
+ }
68
+ return {
69
+ project_id: projectId,
70
+ key: args.key,
71
+ acquired: false,
72
+ held_by: row.owner,
73
+ expires_at: row.expires_at,
74
+ };
75
+ }));
76
+ server.registerTool("lease_release", {
77
+ description: "Release a lease you own.",
78
+ inputSchema: { key: z.string(), project_id: projectIdParam },
79
+ outputSchema: { project_id: idParam, key: z.string(), released: z.boolean() },
80
+ }, (args) => run(() => {
81
+ const projectId = effectiveProjectId(args.project_id);
82
+ const info = db
83
+ .prepare("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND owner = ?")
84
+ .run(projectId, args.key, currentActor());
85
+ return { project_id: projectId, key: args.key, released: info.changes > 0 };
86
+ }));
87
+ }
@@ -0,0 +1,183 @@
1
+ import { z } from "zod";
2
+ import { db } from "../db.js";
3
+ import { addProject, currentActor, listProjects, resolveProject, selectProjectById, TOUCH_INTERVAL_MS, trySelectedProject, } from "../context.js";
4
+ import { errorMessage, run } from "../result.js";
5
+ import { HELP_TOPICS, helpOverview } from "../help.js";
6
+ import { idParam } from "./params.js";
7
+ function refuseIfLocked(tool) {
8
+ if (process.env.HIVE_PROJECT_LOCK === "1") {
9
+ throw new Error(`${tool} sweeps the whole store, across every project and every actor. This session is locked to one project (HIVE_PROJECT_LOCK=1) and cannot run it. Run it from an unlocked session (a lead).`);
10
+ }
11
+ }
12
+ export const PROJECT_OWNER_TABLES = [
13
+ "pads",
14
+ "todos",
15
+ "kv",
16
+ "leases",
17
+ "agents",
18
+ "wakes",
19
+ "command_trust",
20
+ ];
21
+ function existsWhere(table, column, value) {
22
+ return db.prepare(`SELECT 1 FROM ${table} WHERE ${column} = ? LIMIT 1`).get(value) !== undefined;
23
+ }
24
+ function projectOwnsRows(projectId) {
25
+ return PROJECT_OWNER_TABLES.some((table) => existsWhere(table, "project_id", projectId));
26
+ }
27
+ const pruneProjectIfEmpty = db.transaction((projectId) => {
28
+ if (projectOwnsRows(projectId))
29
+ return false;
30
+ return db.prepare("DELETE FROM projects WHERE id = ?").run(projectId).changes > 0;
31
+ });
32
+ export const ACTOR_OWNER_COLUMNS = [
33
+ ["agents", "actor_id"],
34
+ ["agents", "parent_actor_id"],
35
+ ["todos", "locked_by"],
36
+ ["todo_comments", "author"],
37
+ ["kv", "updated_by"],
38
+ ["leases", "owner"],
39
+ ["pads", "updated_by"],
40
+ ["wakes", "owner"],
41
+ ["wakes", "deliver_actor"],
42
+ ["agent_state_log", "actor_id"],
43
+ ];
44
+ function actorOwnsRows(actorId) {
45
+ return ACTOR_OWNER_COLUMNS.some(([table, column]) => existsWhere(table, column, actorId));
46
+ }
47
+ const ACTOR_LIVENESS_WINDOW_MS = TOUCH_INTERVAL_MS * 2;
48
+ function actorRecentlySeen(actorId) {
49
+ return (db
50
+ .prepare("SELECT 1 FROM actors WHERE id = ? AND last_seen_at >= datetime('now', ?) LIMIT 1")
51
+ .get(actorId, `-${Math.ceil(ACTOR_LIVENESS_WINDOW_MS / 1000)} seconds`) !== undefined);
52
+ }
53
+ const pruneActorIfInert = db.transaction((actorId) => {
54
+ if (actorRecentlySeen(actorId))
55
+ return "live";
56
+ if (actorOwnsRows(actorId))
57
+ return "kept";
58
+ return db.prepare("DELETE FROM actors WHERE id = ?").run(actorId).changes > 0 ? "deleted" : "kept";
59
+ });
60
+ export function registerMeta(server) {
61
+ server.registerTool("whoami", {
62
+ description: "Show this session's actor identity and effective project scope. Call this first in a new session.",
63
+ inputSchema: {},
64
+ }, () => run(() => {
65
+ const actorId = currentActor();
66
+ const actor = db.prepare("SELECT * FROM actors WHERE id = ?").get(actorId);
67
+ let project = null;
68
+ let projectError;
69
+ try {
70
+ project = resolveProject();
71
+ }
72
+ catch (e) {
73
+ projectError = e instanceof Error ? e.message : String(e);
74
+ }
75
+ return {
76
+ actor_id: actor.id,
77
+ actor_name: actor.name,
78
+ kind: actor.kind,
79
+ project: project ? { id: project.id, name: project.name, path: project.path } : null,
80
+ ...(projectError ? { project_error: projectError } : {}),
81
+ cwd: process.cwd(),
82
+ };
83
+ }));
84
+ server.registerTool("help", {
85
+ description: `Hive usage guidance. Omit topic for an overview, or pass one of: ${Object.keys(HELP_TOPICS).join(", ")}.`,
86
+ inputSchema: { topic: z.string().optional() },
87
+ }, ({ topic }) => run(() => {
88
+ if (!topic)
89
+ return helpOverview();
90
+ const text = HELP_TOPICS[topic.toLowerCase()];
91
+ if (!text) {
92
+ throw new Error(`Unknown topic "${topic}". Topics: ${Object.keys(HELP_TOPICS).join(", ")}`);
93
+ }
94
+ return text;
95
+ }));
96
+ server.registerTool("project_list", {
97
+ description: "List registered projects and the currently selected one.",
98
+ inputSchema: {},
99
+ }, () => run(() => ({
100
+ projects: listProjects(),
101
+ selected_project_id: trySelectedProject()?.id ?? null,
102
+ })));
103
+ server.registerTool("project_add", {
104
+ description: "Register a directory as a project. Defaults to the current working directory. Returns the existing project if the path is already registered.",
105
+ inputSchema: {
106
+ path: z.string().optional(),
107
+ name: z.string().optional(),
108
+ },
109
+ outputSchema: { id: idParam, name: z.string(), path: z.string(), created_at: z.string() },
110
+ }, ({ path, name }) => run(() => addProject(path, name)));
111
+ server.registerTool("project_select", {
112
+ description: "Set which project later tools act on in this session.",
113
+ inputSchema: { project_id: idParam },
114
+ }, ({ project_id }) => run(() => selectProjectById(project_id)));
115
+ server.registerTool("project_prune", {
116
+ description: "Delete every registered project that owns no rows anywhere in the store (pads, todos, kv, leases, agents, wakes, command_trust), verified individually before each delete. Never prunes the caller's own project. Refuses under HIVE_PROJECT_LOCK=1: this is a whole-store sweep, and a project-locked session may only touch its own project. Immediate, permanent: no dry-run mode.",
117
+ inputSchema: {},
118
+ outputSchema: {
119
+ deleted: z.array(z.object({ id: idParam, name: z.string() })),
120
+ held_back: z.object({ project_id: idParam, reason: z.string() }),
121
+ errors: z.array(z.object({ id: idParam, name: z.string(), error: z.string() })).optional(),
122
+ },
123
+ }, () => run(() => {
124
+ refuseIfLocked("project_prune");
125
+ const homeId = resolveProject().id;
126
+ const deleted = [];
127
+ const errors = [];
128
+ for (const project of listProjects()) {
129
+ if (project.id === homeId)
130
+ continue;
131
+ try {
132
+ if (pruneProjectIfEmpty.immediate(project.id)) {
133
+ deleted.push({ id: project.id, name: project.name });
134
+ }
135
+ }
136
+ catch (e) {
137
+ errors.push({ id: project.id, name: project.name, error: errorMessage(e) });
138
+ }
139
+ }
140
+ return {
141
+ deleted,
142
+ held_back: { project_id: homeId, reason: "caller's own project" },
143
+ ...(errors.length > 0 ? { errors } : {}),
144
+ };
145
+ }));
146
+ server.registerTool("actor_prune", {
147
+ description: "Delete every actor that owns no rows anywhere in the store and has not been active in the last minute: agents.actor_id, agents.parent_actor_id, todos.locked_by, todo_comments.author, kv.updated_by, leases.owner, pads.updated_by, wakes.owner, wakes.deliver_actor, agent_state_log.actor_id. The scan is global across every project, never scoped to the caller's: actors carry no project_id, so an actor can own rows in a project the caller cannot see, and a project-scoped scan would misread that actor as inert and delete it. Never prunes the caller's own actor. Refuses under HIVE_PROJECT_LOCK=1: this is a whole-store sweep. Run this after project_prune when sweeping the store: an empty project owns no agents rows either, so today the order cannot orphan an actor, but that stops being true the day project deletion ever covers a non-empty project, and this ordering is the one that stays safe if it does. Immediate, permanent: no dry-run mode.",
148
+ inputSchema: {},
149
+ outputSchema: {
150
+ deleted: z.array(z.object({ id: z.string(), name: z.string() })),
151
+ held_back: z.object({ actor_id: z.string(), reason: z.string() }),
152
+ held_back_live: z.array(z.object({ id: z.string(), name: z.string() })),
153
+ errors: z.array(z.object({ id: z.string(), name: z.string(), error: z.string() })).optional(),
154
+ },
155
+ }, () => run(() => {
156
+ refuseIfLocked("actor_prune");
157
+ const homeActor = currentActor();
158
+ const actors = db.prepare("SELECT id, name, kind FROM actors").all();
159
+ const deleted = [];
160
+ const heldBackLive = [];
161
+ const errors = [];
162
+ for (const actor of actors) {
163
+ if (actor.id === homeActor)
164
+ continue;
165
+ try {
166
+ const outcome = pruneActorIfInert.immediate(actor.id);
167
+ if (outcome === "deleted")
168
+ deleted.push({ id: actor.id, name: actor.name });
169
+ else if (outcome === "live")
170
+ heldBackLive.push({ id: actor.id, name: actor.name });
171
+ }
172
+ catch (e) {
173
+ errors.push({ id: actor.id, name: actor.name, error: errorMessage(e) });
174
+ }
175
+ }
176
+ return {
177
+ deleted,
178
+ held_back: { actor_id: homeActor, reason: "caller's own actor" },
179
+ held_back_live: heldBackLive,
180
+ ...(errors.length > 0 ? { errors } : {}),
181
+ };
182
+ }));
183
+ }
@@ -0,0 +1,279 @@
1
+ import { z } from "zod";
2
+ import { db } from "../db.js";
3
+ import { currentActor, effectiveProjectId, resolveProject } from "../context.js";
4
+ import { matchesAnyTag, parseTags, run } from "../result.js";
5
+ import { idParam, limitParam, offsetParam, projectIdParam } from "./params.js";
6
+ function selectPad(projectId, padId, columns) {
7
+ const row = db
8
+ .prepare(`SELECT ${columns} FROM pads WHERE project_id = ? AND id = ?`)
9
+ .get(projectId, padId);
10
+ if (!row)
11
+ throw new Error(`No pad with id ${padId} in project ${projectId}. Call pad_list.`);
12
+ return row;
13
+ }
14
+ function getPad(projectId, padId) {
15
+ return selectPad(projectId, padId, "*");
16
+ }
17
+ export function getPadMeta(projectId, padId) {
18
+ return selectPad(projectId, padId, "id, name, revision, archived");
19
+ }
20
+ function revisionMismatchError(padId, expected, current) {
21
+ if (current == null) {
22
+ return new Error(`Pad ${padId} no longer exists; it may have just been deleted by another write. Call pad_list to confirm.`);
23
+ }
24
+ return new Error(`Revision mismatch for pad ${padId}: expected ${expected}, current ${current}. Re-read with pad_read and retry.`);
25
+ }
26
+ function currentRevisionOrDeleted(padId) {
27
+ const row = db.prepare("SELECT revision FROM pads WHERE id = ?").get(padId);
28
+ return row?.revision ?? null;
29
+ }
30
+ function assertRowChanged(padId, predicateRevision, changed) {
31
+ if (!changed) {
32
+ throw revisionMismatchError(padId, predicateRevision, currentRevisionOrDeleted(padId));
33
+ }
34
+ }
35
+ function checkRevision(pad, expected, required) {
36
+ if (expected == null) {
37
+ if (required) {
38
+ throw new Error(`expected_revision is required to overwrite pad ${pad.id}. Current revision: ${pad.revision}. Read it first with pad_read.`);
39
+ }
40
+ return;
41
+ }
42
+ if (expected !== pad.revision) {
43
+ throw revisionMismatchError(pad.id, expected, pad.revision);
44
+ }
45
+ }
46
+ export function bumpPad(padId, predicateRevision, set, ...params) {
47
+ const where = predicateRevision != null ? "id = ? AND revision = ?" : "id = ?";
48
+ const whereParams = predicateRevision != null ? [padId, predicateRevision] : [padId];
49
+ const row = db
50
+ .prepare(`UPDATE pads SET ${set}, revision = revision + 1,
51
+ updated_by = ?, updated_at = datetime('now') WHERE ${where} RETURNING revision`)
52
+ .get(...params, currentActor(), ...whereParams);
53
+ assertRowChanged(padId, predicateRevision, row != null);
54
+ return row.revision;
55
+ }
56
+ export const APPEND_WITH_SEPARATOR_SET = "content = content || (CASE WHEN content = '' OR substr(content, -1) = char(10) THEN '' ELSE char(10) END) || ?";
57
+ export function getActivePadByName(projectId, name) {
58
+ return db
59
+ .prepare("SELECT * FROM pads WHERE project_id = ? AND name = ? AND archived = 0")
60
+ .get(projectId, name);
61
+ }
62
+ export function listActivePads(projectId) {
63
+ return db
64
+ .prepare(`SELECT name, revision, length(content) AS content_length, updated_by, updated_at
65
+ FROM pads WHERE project_id = ? AND archived = 0 ORDER BY name`)
66
+ .all(projectId);
67
+ }
68
+ export function overwritePadContent(projectId, padId, content, expectedRevision) {
69
+ const pad = getPadMeta(projectId, padId);
70
+ checkRevision(pad, expectedRevision, true);
71
+ return bumpPad(padId, pad.revision, "content = ?", content);
72
+ }
73
+ export function createPad(projectId, name, content, tags) {
74
+ const row = db
75
+ .prepare(`INSERT OR IGNORE INTO pads (project_id, name, content, tags, updated_by)
76
+ VALUES (?, ?, ?, ?, ?) RETURNING id`)
77
+ .get(projectId, name, content, JSON.stringify(tags), currentActor());
78
+ return row?.id ?? null;
79
+ }
80
+ export function registerPads(server) {
81
+ server.registerTool("pad_write", {
82
+ description: "Create a pad, or fully overwrite one by passing pad_id plus expected_revision. Pad names are unique per project. Prefer pad_append/pad_edit for targeted changes.",
83
+ inputSchema: {
84
+ name: z.string(),
85
+ content: z.string(),
86
+ tags: z.array(z.string()).optional(),
87
+ pad_id: idParam.optional().describe("Pass with expected_revision to overwrite."),
88
+ expected_revision: idParam.optional(),
89
+ project_id: projectIdParam,
90
+ },
91
+ outputSchema: { pad_id: idParam, revision: idParam },
92
+ }, (args) => run(() => {
93
+ const projectId = effectiveProjectId(args.project_id);
94
+ if (args.pad_id != null) {
95
+ const pad = getPadMeta(projectId, args.pad_id);
96
+ checkRevision(pad, args.expected_revision, true);
97
+ const revision = bumpPad(pad.id, pad.revision, "name = ?, content = ?, tags = COALESCE(?, tags)", args.name, args.content, args.tags ? JSON.stringify(args.tags) : null);
98
+ return { pad_id: pad.id, revision };
99
+ }
100
+ const padId = createPad(projectId, args.name, args.content, args.tags ?? []);
101
+ if (padId == null) {
102
+ throw new Error(`A pad named "${args.name}" already exists in this project. Read it with pad_read(name="${args.name}"), then overwrite with pad_id + expected_revision, or pick another name.`);
103
+ }
104
+ return { pad_id: padId, revision: 1 };
105
+ }));
106
+ server.registerTool("pad_read", {
107
+ description: "Read a pad's content, revision, and metadata by pad_id or name.",
108
+ inputSchema: {
109
+ pad_id: idParam.optional(),
110
+ name: z.string().optional(),
111
+ project_id: projectIdParam,
112
+ },
113
+ }, (args) => run(() => {
114
+ const projectId = effectiveProjectId(args.project_id);
115
+ let pad;
116
+ if (args.pad_id != null) {
117
+ pad = getPad(projectId, args.pad_id);
118
+ }
119
+ else if (args.name) {
120
+ const row = db
121
+ .prepare("SELECT * FROM pads WHERE project_id = ? AND name = ? AND archived = 0")
122
+ .get(projectId, args.name);
123
+ if (!row)
124
+ throw new Error(`No active pad named "${args.name}". Call pad_list.`);
125
+ pad = row;
126
+ }
127
+ else {
128
+ throw new Error("Pass pad_id or name.");
129
+ }
130
+ return {
131
+ pad_id: pad.id,
132
+ name: pad.name,
133
+ revision: pad.revision,
134
+ tags: parseTags(pad.tags),
135
+ updated_by: pad.updated_by,
136
+ updated_at: pad.updated_at,
137
+ content: pad.content,
138
+ };
139
+ }));
140
+ server.registerTool("pad_append", {
141
+ description: "Append content to the end of a pad. Optional expected_revision guards against concurrent writes.",
142
+ inputSchema: {
143
+ pad_id: idParam,
144
+ content: z.string(),
145
+ expected_revision: idParam.optional(),
146
+ project_id: projectIdParam,
147
+ },
148
+ outputSchema: { pad_id: idParam, revision: idParam },
149
+ }, (args) => run(() => {
150
+ const projectId = effectiveProjectId(args.project_id);
151
+ const pad = getPadMeta(projectId, args.pad_id);
152
+ checkRevision(pad, args.expected_revision, false);
153
+ const revision = bumpPad(pad.id, args.expected_revision, APPEND_WITH_SEPARATOR_SET, args.content);
154
+ return { pad_id: pad.id, revision };
155
+ }));
156
+ server.registerTool("pad_edit", {
157
+ description: "Replace one literal occurrence of old_text with new_text in a pad. old_text must match exactly once; include surrounding context to disambiguate.",
158
+ inputSchema: {
159
+ pad_id: idParam,
160
+ old_text: z.string(),
161
+ new_text: z.string(),
162
+ expected_revision: idParam.optional(),
163
+ project_id: projectIdParam,
164
+ },
165
+ outputSchema: { pad_id: idParam, revision: idParam },
166
+ }, (args) => run(() => {
167
+ const projectId = effectiveProjectId(args.project_id);
168
+ const pad = getPad(projectId, args.pad_id);
169
+ checkRevision(pad, args.expected_revision, false);
170
+ const parts = pad.content.split(args.old_text);
171
+ const occurrences = parts.length - 1;
172
+ if (occurrences === 0) {
173
+ throw new Error(`old_text not found in pad ${pad.id}. Re-read with pad_read.`);
174
+ }
175
+ if (occurrences > 1) {
176
+ throw new Error(`old_text matches ${occurrences} places in pad ${pad.id}. Include more surrounding context so it matches exactly once.`);
177
+ }
178
+ const revision = bumpPad(pad.id, pad.revision, "content = ?", parts.join(args.new_text));
179
+ return { pad_id: pad.id, revision };
180
+ }));
181
+ server.registerTool("pad_archive", {
182
+ description: "Archive a pad (or unarchive with archived=false). Archiving frees the name for a new active pad; the old content stays readable by pad_id.",
183
+ inputSchema: {
184
+ pad_id: idParam,
185
+ archived: z.boolean().optional().describe("Default true. Pass false to unarchive."),
186
+ project_id: projectIdParam,
187
+ },
188
+ outputSchema: { pad_id: idParam, archived: z.boolean(), revision: idParam },
189
+ }, (args) => run(() => {
190
+ const projectId = effectiveProjectId(args.project_id);
191
+ const pad = getPadMeta(projectId, args.pad_id);
192
+ const archived = args.archived ?? true;
193
+ if ((pad.archived === 1) === archived) {
194
+ return { pad_id: pad.id, archived, revision: pad.revision };
195
+ }
196
+ let revision;
197
+ try {
198
+ revision = bumpPad(pad.id, undefined, "archived = ?", archived ? 1 : 0);
199
+ }
200
+ catch (e) {
201
+ if (e instanceof Error && e.message.includes("UNIQUE")) {
202
+ throw new Error(`Cannot unarchive pad ${pad.id}: an active pad named "${pad.name}" already exists. Rename or archive that pad first.`);
203
+ }
204
+ throw e;
205
+ }
206
+ return { pad_id: pad.id, archived, revision };
207
+ }));
208
+ server.registerTool("pad_delete", {
209
+ description: "Permanently delete a pad. Irreversible; prefer pad_archive. Optional expected_revision guards against deleting a pad someone just updated.",
210
+ inputSchema: {
211
+ pad_id: idParam,
212
+ expected_revision: idParam.optional(),
213
+ project_id: projectIdParam,
214
+ },
215
+ outputSchema: { pad_id: idParam, deleted: z.boolean() },
216
+ }, (args) => run(() => {
217
+ const projectId = effectiveProjectId(args.project_id);
218
+ const pad = getPadMeta(projectId, args.pad_id);
219
+ checkRevision(pad, args.expected_revision, false);
220
+ const info = db.prepare("DELETE FROM pads WHERE id = ? AND revision = ?").run(pad.id, pad.revision);
221
+ assertRowChanged(pad.id, pad.revision, info.changes > 0);
222
+ return { pad_id: pad.id, deleted: true };
223
+ }));
224
+ server.registerTool("pad_list", {
225
+ description: "List pads without full content. query matches names and content (returns a snippet); tags matches any listed tag.",
226
+ inputSchema: {
227
+ query: z.string().optional(),
228
+ tags: z.array(z.string()).optional(),
229
+ include_archived: z.boolean().optional(),
230
+ limit: limitParam,
231
+ offset: offsetParam,
232
+ project_id: projectIdParam,
233
+ },
234
+ }, (args) => run(() => {
235
+ const project = resolveProject(args.project_id);
236
+ const limit = Math.min(args.limit ?? 50, 200);
237
+ const offset = args.offset ?? 0;
238
+ const columns = `id, name, revision, tags, archived, updated_by, updated_at,
239
+ length(content) AS content_length${args.query ? ", content" : ""}`;
240
+ let sql = `SELECT ${columns} FROM pads WHERE project_id = ?`;
241
+ const params = [project.id];
242
+ if (!args.include_archived)
243
+ sql += " AND archived = 0";
244
+ if (args.query) {
245
+ sql += " AND (name LIKE ? OR content LIKE ?)";
246
+ params.push(`%${args.query}%`, `%${args.query}%`);
247
+ }
248
+ sql += " ORDER BY updated_at DESC";
249
+ const rows = db.prepare(sql).all(...params).filter((r) => matchesAnyTag(r.tags, args.tags));
250
+ const page = rows.slice(offset, offset + limit);
251
+ return {
252
+ project_id: project.id,
253
+ project_name: project.name,
254
+ total_count: rows.length,
255
+ offset,
256
+ limit,
257
+ pads: page.map((r) => {
258
+ let snippet;
259
+ if (args.query && r.content != null) {
260
+ const idx = r.content.toLowerCase().indexOf(args.query.toLowerCase());
261
+ if (idx >= 0) {
262
+ snippet = r.content.slice(Math.max(0, idx - 40), idx + args.query.length + 80);
263
+ }
264
+ }
265
+ return {
266
+ pad_id: r.id,
267
+ name: r.name,
268
+ revision: r.revision,
269
+ tags: parseTags(r.tags),
270
+ archived: r.archived === 1,
271
+ content_length: r.content_length,
272
+ updated_by: r.updated_by,
273
+ updated_at: r.updated_at,
274
+ ...(snippet ? { snippet } : {}),
275
+ };
276
+ }),
277
+ };
278
+ }));
279
+ }
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ export const idParam = z.number().int().positive();
3
+ export const projectIdParam = idParam
4
+ .optional()
5
+ .describe("Different project override. Use ONLY when the user explicitly asks for another project by name; otherwise stay in the current scope, even when results are empty.");
6
+ export const agentNameParam = z
7
+ .string()
8
+ .optional()
9
+ .describe("The worker's name, e.g. \"impl\" or \"DEVX-123\". Prefer passing this rather than agent_id, but pass only one: if both arrive, agent_id wins and this is ignored. A partial name works when it matches exactly one running worker, so \"123\" finds DEVX-123.");
10
+ export const agentIdParam = idParam
11
+ .optional()
12
+ .describe("Numeric agent id. Use name instead unless you have the id to hand.");
13
+ export const limitParam = z.number().int().positive().optional();
14
+ export const offsetParam = z.number().int().nonnegative().optional();