@graph-tl/graph 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.
@@ -0,0 +1,1476 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getLicenseTier
4
+ } from "./chunk-WKOEKYTF.js";
5
+ import {
6
+ EngineError,
7
+ ValidationError,
8
+ closeDb,
9
+ createNode,
10
+ getAncestors,
11
+ getChildren,
12
+ getDb,
13
+ getEvents,
14
+ getNode,
15
+ getNodeOrThrow,
16
+ getProjectRoot,
17
+ getProjectSummary,
18
+ initDb,
19
+ listProjects,
20
+ logEvent,
21
+ optionalBoolean,
22
+ optionalNumber,
23
+ optionalString,
24
+ requireArray,
25
+ requireString,
26
+ updateNode
27
+ } from "./chunk-RVM33A4A.js";
28
+
29
+ // src/server.ts
30
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
31
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
32
+ import {
33
+ CallToolRequestSchema,
34
+ ListToolsRequestSchema
35
+ } from "@modelcontextprotocol/sdk/types.js";
36
+
37
+ // src/tools/open.ts
38
+ function handleOpen(input, agent) {
39
+ const project = optionalString(input?.project, "project");
40
+ const goal = optionalString(input?.goal, "goal");
41
+ if (!project) {
42
+ return { projects: listProjects() };
43
+ }
44
+ let root = getProjectRoot(project);
45
+ if (!root) {
46
+ root = createNode({
47
+ project,
48
+ summary: goal ?? project,
49
+ agent
50
+ });
51
+ }
52
+ const summary = getProjectSummary(project);
53
+ return { project, root, summary };
54
+ }
55
+
56
+ // src/edges.ts
57
+ import { nanoid } from "nanoid";
58
+ function wouldCreateCycle(from, to) {
59
+ const db = getDb();
60
+ const visited = /* @__PURE__ */ new Set();
61
+ const stack = [to];
62
+ while (stack.length > 0) {
63
+ const current = stack.pop();
64
+ if (current === from) return true;
65
+ if (visited.has(current)) continue;
66
+ visited.add(current);
67
+ const deps = db.prepare(
68
+ `SELECT to_node FROM edges WHERE from_node = ? AND type = 'depends_on'`
69
+ ).all(current);
70
+ for (const dep of deps) {
71
+ stack.push(dep.to_node);
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+ function addEdge(input) {
77
+ const db = getDb();
78
+ const fromExists = db.prepare("SELECT id FROM nodes WHERE id = ?").get(input.from);
79
+ const toExists = db.prepare("SELECT id FROM nodes WHERE id = ?").get(input.to);
80
+ if (!fromExists) {
81
+ return { edge: null, rejected: true, reason: "node_not_found: " + input.from };
82
+ }
83
+ if (!toExists) {
84
+ return { edge: null, rejected: true, reason: "node_not_found: " + input.to };
85
+ }
86
+ const existing = db.prepare(
87
+ "SELECT id FROM edges WHERE from_node = ? AND to_node = ? AND type = ?"
88
+ ).get(input.from, input.to, input.type);
89
+ if (existing) {
90
+ return { edge: null, rejected: true, reason: "duplicate_edge" };
91
+ }
92
+ if (input.type === "depends_on") {
93
+ if (wouldCreateCycle(input.from, input.to)) {
94
+ return { edge: null, rejected: true, reason: "cycle_detected" };
95
+ }
96
+ }
97
+ const edge = {
98
+ id: nanoid(),
99
+ from_node: input.from,
100
+ to_node: input.to,
101
+ type: input.type,
102
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
103
+ };
104
+ db.prepare(`
105
+ INSERT INTO edges (id, from_node, to_node, type, created_at)
106
+ VALUES (?, ?, ?, ?, ?)
107
+ `).run(edge.id, edge.from_node, edge.to_node, edge.type, edge.created_at);
108
+ logEvent(input.from, input.agent, "edge_added", [
109
+ { field: "edge", before: null, after: { to: input.to, type: input.type } }
110
+ ]);
111
+ return { edge, rejected: false };
112
+ }
113
+ function removeEdge(from, to, type, agent) {
114
+ const db = getDb();
115
+ const result = db.prepare(
116
+ "DELETE FROM edges WHERE from_node = ? AND to_node = ? AND type = ?"
117
+ ).run(from, to, type);
118
+ if (result.changes > 0) {
119
+ logEvent(from, agent, "edge_removed", [
120
+ { field: "edge", before: { to, type }, after: null }
121
+ ]);
122
+ return true;
123
+ }
124
+ return false;
125
+ }
126
+ function getEdgesFrom(nodeId, type) {
127
+ const db = getDb();
128
+ if (type) {
129
+ return db.prepare("SELECT * FROM edges WHERE from_node = ? AND type = ?").all(nodeId, type);
130
+ }
131
+ return db.prepare("SELECT * FROM edges WHERE from_node = ?").all(nodeId);
132
+ }
133
+ function getEdgesTo(nodeId, type) {
134
+ const db = getDb();
135
+ if (type) {
136
+ return db.prepare("SELECT * FROM edges WHERE to_node = ? AND type = ?").all(nodeId, type);
137
+ }
138
+ return db.prepare("SELECT * FROM edges WHERE to_node = ?").all(nodeId);
139
+ }
140
+ function findNewlyActionable(project, resolvedNodeIds) {
141
+ const db = getDb();
142
+ if (resolvedNodeIds && resolvedNodeIds.length > 0) {
143
+ const placeholders = resolvedNodeIds.map(() => "?").join(",");
144
+ const rows2 = db.prepare(
145
+ `SELECT DISTINCT n.id, n.summary FROM nodes n
146
+ WHERE n.resolved = 0 AND n.project = ?
147
+ AND (
148
+ -- nodes that had a depends_on edge to one of the resolved nodes
149
+ n.id IN (
150
+ SELECT e.from_node FROM edges e
151
+ WHERE e.type = 'depends_on' AND e.to_node IN (${placeholders})
152
+ )
153
+ OR
154
+ -- parents of resolved nodes (might now be leaf if all children resolved)
155
+ n.id IN (SELECT parent FROM nodes WHERE id IN (${placeholders}) AND parent IS NOT NULL)
156
+ )
157
+ -- is a leaf (no unresolved children)
158
+ AND NOT EXISTS (
159
+ SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0
160
+ )
161
+ -- all deps resolved
162
+ AND NOT EXISTS (
163
+ SELECT 1 FROM edges e
164
+ JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0
165
+ WHERE e.from_node = n.id AND e.type = 'depends_on'
166
+ )`
167
+ ).all(project, ...resolvedNodeIds, ...resolvedNodeIds);
168
+ return rows2;
169
+ }
170
+ const rows = db.prepare(
171
+ `SELECT n.id, n.summary FROM nodes n
172
+ WHERE n.project = ? AND n.resolved = 0
173
+ AND NOT EXISTS (
174
+ SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0
175
+ )
176
+ AND NOT EXISTS (
177
+ SELECT 1 FROM edges e
178
+ JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0
179
+ WHERE e.from_node = n.id AND e.type = 'depends_on'
180
+ )`
181
+ ).all(project);
182
+ return rows;
183
+ }
184
+
185
+ // src/tools/plan.ts
186
+ function handlePlan(input, agent) {
187
+ const db = getDb();
188
+ const nodes = requireArray(input?.nodes, "nodes");
189
+ for (let i = 0; i < nodes.length; i++) {
190
+ const n = nodes[i];
191
+ requireString(n.ref, `nodes[${i}].ref`);
192
+ requireString(n.summary, `nodes[${i}].summary`);
193
+ }
194
+ const refMap = /* @__PURE__ */ new Map();
195
+ const created = [];
196
+ const refs = /* @__PURE__ */ new Set();
197
+ for (const node of nodes) {
198
+ if (refs.has(node.ref)) {
199
+ throw new Error(`Duplicate ref in batch: ${node.ref}`);
200
+ }
201
+ refs.add(node.ref);
202
+ }
203
+ const transaction = db.transaction(() => {
204
+ for (const nodeInput of nodes) {
205
+ let parentId;
206
+ if (nodeInput.parent_ref) {
207
+ parentId = refMap.get(nodeInput.parent_ref);
208
+ if (!parentId) {
209
+ const existing = getNode(nodeInput.parent_ref);
210
+ if (existing) {
211
+ parentId = existing.id;
212
+ } else {
213
+ throw new Error(
214
+ `parent_ref "${nodeInput.parent_ref}" is neither a batch ref nor an existing node ID`
215
+ );
216
+ }
217
+ }
218
+ }
219
+ let project;
220
+ if (parentId) {
221
+ const parentNode = getNode(parentId);
222
+ project = parentNode.project;
223
+ } else {
224
+ throw new Error(
225
+ `Node "${nodeInput.ref}" has no parent_ref. All planned nodes must have a parent (an existing node or a batch ref).`
226
+ );
227
+ }
228
+ const node = createNode({
229
+ project,
230
+ parent: parentId,
231
+ summary: nodeInput.summary,
232
+ context_links: nodeInput.context_links,
233
+ properties: nodeInput.properties,
234
+ agent
235
+ });
236
+ refMap.set(nodeInput.ref, node.id);
237
+ created.push({ ref: nodeInput.ref, id: node.id });
238
+ }
239
+ for (const nodeInput of nodes) {
240
+ if (!nodeInput.depends_on || nodeInput.depends_on.length === 0) continue;
241
+ const fromId = refMap.get(nodeInput.ref);
242
+ for (const dep of nodeInput.depends_on) {
243
+ let toId = refMap.get(dep);
244
+ if (!toId) {
245
+ const existing = getNode(dep);
246
+ if (existing) {
247
+ toId = existing.id;
248
+ } else {
249
+ throw new Error(
250
+ `depends_on "${dep}" in node "${nodeInput.ref}" is neither a batch ref nor an existing node ID`
251
+ );
252
+ }
253
+ }
254
+ const result = addEdge({
255
+ from: fromId,
256
+ to: toId,
257
+ type: "depends_on",
258
+ agent
259
+ });
260
+ if (result.rejected) {
261
+ throw new Error(
262
+ `Dependency edge from "${nodeInput.ref}" to "${dep}" rejected: ${result.reason}`
263
+ );
264
+ }
265
+ }
266
+ }
267
+ });
268
+ transaction();
269
+ return { created };
270
+ }
271
+
272
+ // src/tools/update.ts
273
+ function handleUpdate(input, agent) {
274
+ const updates = requireArray(input?.updates, "updates");
275
+ for (let i = 0; i < updates.length; i++) {
276
+ requireString(updates[i].node_id, `updates[${i}].node_id`);
277
+ if (updates[i].add_evidence) {
278
+ for (let j = 0; j < updates[i].add_evidence.length; j++) {
279
+ requireString(updates[i].add_evidence[j].type, `updates[${i}].add_evidence[${j}].type`);
280
+ requireString(updates[i].add_evidence[j].ref, `updates[${i}].add_evidence[${j}].ref`);
281
+ }
282
+ }
283
+ }
284
+ const updated = [];
285
+ const resolvedIds = [];
286
+ let project = null;
287
+ for (const entry of updates) {
288
+ const node = updateNode({
289
+ node_id: entry.node_id,
290
+ agent,
291
+ resolved: entry.resolved,
292
+ state: entry.state,
293
+ summary: entry.summary,
294
+ properties: entry.properties,
295
+ add_context_links: entry.add_context_links,
296
+ remove_context_links: entry.remove_context_links,
297
+ add_evidence: entry.add_evidence
298
+ });
299
+ updated.push({ node_id: node.id, rev: node.rev });
300
+ if (entry.resolved === true) {
301
+ resolvedIds.push(node.id);
302
+ project = node.project;
303
+ }
304
+ }
305
+ const result = { updated };
306
+ if (resolvedIds.length > 0 && project) {
307
+ result.newly_actionable = findNewlyActionable(project, resolvedIds);
308
+ }
309
+ return result;
310
+ }
311
+
312
+ // src/tools/connect.ts
313
+ function handleConnect(input, agent) {
314
+ const edges = requireArray(input?.edges, "edges");
315
+ for (let i = 0; i < edges.length; i++) {
316
+ requireString(edges[i].from, `edges[${i}].from`);
317
+ requireString(edges[i].to, `edges[${i}].to`);
318
+ requireString(edges[i].type, `edges[${i}].type`);
319
+ }
320
+ let applied = 0;
321
+ const rejected = [];
322
+ for (const edge of edges) {
323
+ if (edge.type === "parent") {
324
+ rejected.push({
325
+ from: edge.from,
326
+ to: edge.to,
327
+ reason: "parent_edges_not_allowed: use graph_restructure to reparent"
328
+ });
329
+ continue;
330
+ }
331
+ if (edge.remove) {
332
+ const removed = removeEdge(edge.from, edge.to, edge.type, agent);
333
+ if (removed) {
334
+ applied++;
335
+ } else {
336
+ rejected.push({
337
+ from: edge.from,
338
+ to: edge.to,
339
+ reason: "edge_not_found"
340
+ });
341
+ }
342
+ } else {
343
+ const result2 = addEdge({
344
+ from: edge.from,
345
+ to: edge.to,
346
+ type: edge.type,
347
+ agent
348
+ });
349
+ if (result2.rejected) {
350
+ rejected.push({
351
+ from: edge.from,
352
+ to: edge.to,
353
+ reason: result2.reason
354
+ });
355
+ } else {
356
+ applied++;
357
+ }
358
+ }
359
+ }
360
+ const result = { applied };
361
+ if (rejected.length > 0) {
362
+ result.rejected = rejected;
363
+ }
364
+ return result;
365
+ }
366
+
367
+ // src/tools/context.ts
368
+ function buildNodeTree(nodeId, currentDepth, maxDepth) {
369
+ const node = getNodeOrThrow(nodeId);
370
+ const children = getChildren(nodeId);
371
+ const tree = {
372
+ id: node.id,
373
+ summary: node.summary,
374
+ resolved: node.resolved,
375
+ state: node.state
376
+ };
377
+ if (children.length === 0) {
378
+ return tree;
379
+ }
380
+ if (currentDepth < maxDepth) {
381
+ tree.children = children.map(
382
+ (child) => buildNodeTree(child.id, currentDepth + 1, maxDepth)
383
+ );
384
+ } else {
385
+ tree.child_count = children.length;
386
+ }
387
+ return tree;
388
+ }
389
+ function handleContext(input) {
390
+ const nodeId = requireString(input?.node_id, "node_id");
391
+ const depth = optionalNumber(input?.depth, "depth", 0, 10) ?? 2;
392
+ const node = getNodeOrThrow(nodeId);
393
+ const ancestors = getAncestors(nodeId);
394
+ const children = buildNodeTree(nodeId, 0, depth);
395
+ const depsOut = getEdgesFrom(nodeId, "depends_on");
396
+ const depsIn = getEdgesTo(nodeId, "depends_on");
397
+ const depends_on = depsOut.map((edge) => {
398
+ const target = getNode(edge.to_node);
399
+ return {
400
+ node: target,
401
+ satisfied: target?.resolved ?? false
402
+ };
403
+ });
404
+ const depended_by = depsIn.map((edge) => {
405
+ const source = getNode(edge.from_node);
406
+ return {
407
+ node: source,
408
+ satisfied: node.resolved
409
+ };
410
+ });
411
+ return { node, ancestors, children, depends_on, depended_by };
412
+ }
413
+
414
+ // src/tools/query.ts
415
+ function getDescendantIds(nodeId) {
416
+ const db = getDb();
417
+ const rows = db.prepare(
418
+ `WITH RECURSIVE descendants(id) AS (
419
+ SELECT id FROM nodes WHERE parent = ?
420
+ UNION ALL
421
+ SELECT n.id FROM nodes n JOIN descendants d ON n.parent = d.id
422
+ )
423
+ SELECT id FROM descendants`
424
+ ).all(nodeId);
425
+ return rows.map((r) => r.id);
426
+ }
427
+ function handleQuery(input) {
428
+ const project = requireString(input?.project, "project");
429
+ const db = getDb();
430
+ const limit = Math.min(optionalNumber(input?.limit, "limit", 1, 100) ?? 20, 100);
431
+ const filter = input?.filter;
432
+ const cursor = optionalString(input?.cursor, "cursor");
433
+ const conditions = ["n.project = ?"];
434
+ const params = [project];
435
+ if (filter?.resolved !== void 0) {
436
+ conditions.push("n.resolved = ?");
437
+ params.push(filter.resolved ? 1 : 0);
438
+ }
439
+ if (filter?.text) {
440
+ conditions.push("n.summary LIKE ?");
441
+ params.push(`%${filter.text}%`);
442
+ }
443
+ if (filter?.ancestor) {
444
+ const descendantIds = getDescendantIds(filter.ancestor);
445
+ if (descendantIds.length === 0) {
446
+ return { nodes: [], total: 0 };
447
+ }
448
+ conditions.push(`n.id IN (${descendantIds.map(() => "?").join(",")})`);
449
+ params.push(...descendantIds);
450
+ }
451
+ if (filter?.has_evidence_type) {
452
+ conditions.push("n.evidence LIKE ?");
453
+ params.push(`%"type":"${filter.has_evidence_type}"%`);
454
+ }
455
+ if (filter?.is_leaf) {
456
+ conditions.push(
457
+ "NOT EXISTS (SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0)"
458
+ );
459
+ }
460
+ if (filter?.is_actionable) {
461
+ conditions.push("n.resolved = 0");
462
+ conditions.push(
463
+ "NOT EXISTS (SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0)"
464
+ );
465
+ conditions.push(
466
+ `NOT EXISTS (
467
+ SELECT 1 FROM edges e
468
+ JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0
469
+ WHERE e.from_node = n.id AND e.type = 'depends_on'
470
+ )`
471
+ );
472
+ }
473
+ if (filter?.is_blocked) {
474
+ conditions.push("n.resolved = 0");
475
+ conditions.push(
476
+ `EXISTS (
477
+ SELECT 1 FROM edges e
478
+ JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0
479
+ WHERE e.from_node = n.id AND e.type = 'depends_on'
480
+ )`
481
+ );
482
+ }
483
+ if (filter?.properties) {
484
+ for (const [key, value] of Object.entries(filter.properties)) {
485
+ conditions.push("json_extract(n.properties, ?) = ?");
486
+ params.push(`$.${key}`, value);
487
+ }
488
+ }
489
+ if (filter?.claimed_by !== void 0) {
490
+ if (filter.claimed_by === null) {
491
+ conditions.push(
492
+ "(json_extract(n.properties, '$._claimed_by') IS NULL)"
493
+ );
494
+ } else {
495
+ conditions.push("json_extract(n.properties, '$._claimed_by') = ?");
496
+ params.push(filter.claimed_by);
497
+ }
498
+ }
499
+ if (cursor) {
500
+ const [cursorTime, cursorId] = cursor.split("|");
501
+ conditions.push("(n.created_at > ? OR (n.created_at = ? AND n.id > ?))");
502
+ params.push(cursorTime, cursorTime, cursorId);
503
+ }
504
+ const whereClause = conditions.join(" AND ");
505
+ let orderBy;
506
+ switch (input.sort) {
507
+ case "depth":
508
+ orderBy = "n.created_at ASC, n.id ASC";
509
+ break;
510
+ case "recent":
511
+ orderBy = "n.updated_at DESC, n.id ASC";
512
+ break;
513
+ case "created":
514
+ orderBy = "n.created_at ASC, n.id ASC";
515
+ break;
516
+ case "readiness":
517
+ default:
518
+ orderBy = "n.updated_at ASC, n.id ASC";
519
+ break;
520
+ }
521
+ const countConditions = cursor ? conditions.slice(0, -1) : conditions;
522
+ const countParams = cursor ? params.slice(0, -3) : [...params];
523
+ const total = db.prepare(`SELECT COUNT(*) as count FROM nodes n WHERE ${countConditions.join(" AND ")}`).get(...countParams).count;
524
+ params.push(limit + 1);
525
+ const query = `SELECT * FROM nodes n WHERE ${whereClause} ORDER BY ${orderBy} LIMIT ?`;
526
+ const rows = db.prepare(query).all(...params);
527
+ const hasMore = rows.length > limit;
528
+ const slice = hasMore ? rows.slice(0, limit) : rows;
529
+ const nodes = slice.map((row) => ({
530
+ id: row.id,
531
+ summary: row.summary,
532
+ resolved: row.resolved === 1,
533
+ state: row.state ? JSON.parse(row.state) : null,
534
+ parent: row.parent,
535
+ depth: row.depth,
536
+ properties: JSON.parse(row.properties)
537
+ }));
538
+ const result = { nodes, total };
539
+ if (hasMore) {
540
+ const last = slice[slice.length - 1];
541
+ result.next_cursor = `${last.created_at}|${last.id}`;
542
+ }
543
+ return result;
544
+ }
545
+
546
+ // src/tools/next.ts
547
+ function handleNext(input, agent, claimTtlMinutes = 60) {
548
+ const project = requireString(input?.project, "project");
549
+ const scope = optionalString(input?.scope, "scope");
550
+ const count = optionalNumber(input?.count, "count", 1, 50) ?? 1;
551
+ const claim = optionalBoolean(input?.claim, "claim") ?? false;
552
+ const db = getDb();
553
+ let scopeFilter = "";
554
+ const scopeParams = [];
555
+ if (scope) {
556
+ const descendantIds = db.prepare(
557
+ `WITH RECURSIVE descendants(id) AS (
558
+ SELECT id FROM nodes WHERE parent = ?
559
+ UNION ALL
560
+ SELECT n.id FROM nodes n JOIN descendants d ON n.parent = d.id
561
+ )
562
+ SELECT id FROM descendants`
563
+ ).all(scope);
564
+ if (descendantIds.length === 0) {
565
+ return { nodes: [] };
566
+ }
567
+ scopeFilter = `AND n.id IN (${descendantIds.map(() => "?").join(",")})`;
568
+ scopeParams.push(...descendantIds.map((d) => d.id));
569
+ }
570
+ let query = `
571
+ SELECT n.* FROM nodes n
572
+ WHERE n.project = ? AND n.resolved = 0
573
+ ${scopeFilter}
574
+ AND NOT EXISTS (
575
+ SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0
576
+ )
577
+ AND NOT EXISTS (
578
+ SELECT 1 FROM edges e
579
+ JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0
580
+ WHERE e.from_node = n.id AND e.type = 'depends_on'
581
+ )
582
+ `;
583
+ const params = [project, ...scopeParams];
584
+ const claimCutoff = new Date(
585
+ Date.now() - claimTtlMinutes * 60 * 1e3
586
+ ).toISOString();
587
+ query += `
588
+ AND (
589
+ json_extract(n.properties, '$._claimed_by') IS NULL
590
+ OR json_extract(n.properties, '$._claimed_by') = ?
591
+ OR json_extract(n.properties, '$._claimed_at') <= ?
592
+ )
593
+ `;
594
+ params.push(agent, claimCutoff);
595
+ if (input.filter) {
596
+ for (const [key, value] of Object.entries(input.filter)) {
597
+ query += " AND json_extract(n.properties, ?) = ?";
598
+ params.push(`$.${key}`, value);
599
+ }
600
+ }
601
+ query += `
602
+ ORDER BY
603
+ COALESCE(CAST(json_extract(n.properties, '$.priority') AS REAL), 0) DESC,
604
+ n.depth DESC,
605
+ n.updated_at ASC
606
+ LIMIT ?
607
+ `;
608
+ params.push(count);
609
+ const rows = db.prepare(query).all(...params);
610
+ const selected = rows.map((row) => ({ row }));
611
+ const results = selected.map(({ row }) => {
612
+ const node = getNode(row.id);
613
+ const ancestors = getAncestors(row.id);
614
+ const inherited = [];
615
+ for (const anc of ancestors) {
616
+ const ancNode = getNode(anc.id);
617
+ if (ancNode && ancNode.context_links.length > 0) {
618
+ inherited.push({ node_id: anc.id, links: ancNode.context_links });
619
+ }
620
+ }
621
+ const depEdges = getEdgesFrom(row.id, "depends_on");
622
+ const resolved_deps = depEdges.map((edge) => {
623
+ const depNode = getNode(edge.to_node);
624
+ if (!depNode || !depNode.resolved) return null;
625
+ return {
626
+ id: depNode.id,
627
+ summary: depNode.summary,
628
+ evidence: depNode.evidence
629
+ };
630
+ }).filter(Boolean);
631
+ if (claim) {
632
+ updateNode({
633
+ node_id: node.id,
634
+ agent,
635
+ properties: {
636
+ _claimed_by: agent,
637
+ _claimed_at: (/* @__PURE__ */ new Date()).toISOString()
638
+ }
639
+ });
640
+ }
641
+ return {
642
+ node: claim ? getNode(row.id) : node,
643
+ ancestors: ancestors.map((a) => ({ id: a.id, summary: a.summary })),
644
+ context_links: {
645
+ self: node.context_links,
646
+ inherited
647
+ },
648
+ resolved_deps
649
+ };
650
+ });
651
+ return { nodes: results };
652
+ }
653
+
654
+ // src/tools/restructure.ts
655
+ function wouldCreateParentCycle(nodeId, newParentId) {
656
+ const db = getDb();
657
+ let current = newParentId;
658
+ while (current) {
659
+ if (current === nodeId) return true;
660
+ const row = db.prepare("SELECT parent FROM nodes WHERE id = ?").get(current);
661
+ current = row?.parent ?? null;
662
+ }
663
+ return false;
664
+ }
665
+ function getAllDescendants(nodeId) {
666
+ const ids = [];
667
+ const stack = [nodeId];
668
+ while (stack.length > 0) {
669
+ const current = stack.pop();
670
+ const children = getChildren(current);
671
+ for (const child of children) {
672
+ ids.push(child.id);
673
+ stack.push(child.id);
674
+ }
675
+ }
676
+ return ids;
677
+ }
678
+ function recomputeSubtreeDepth(nodeId, newDepth) {
679
+ const db = getDb();
680
+ db.prepare("UPDATE nodes SET depth = ? WHERE id = ?").run(newDepth, nodeId);
681
+ const children = db.prepare("SELECT id FROM nodes WHERE parent = ?").all(nodeId);
682
+ for (const child of children) {
683
+ recomputeSubtreeDepth(child.id, newDepth + 1);
684
+ }
685
+ }
686
+ function handleMove(op, agent) {
687
+ const db = getDb();
688
+ const node = getNodeOrThrow(op.node_id);
689
+ const newParent = getNodeOrThrow(op.new_parent);
690
+ if (wouldCreateParentCycle(op.node_id, op.new_parent)) {
691
+ throw new Error(
692
+ `Move would create cycle: ${op.node_id} cannot be moved under ${op.new_parent}`
693
+ );
694
+ }
695
+ const oldParent = node.parent;
696
+ const now = (/* @__PURE__ */ new Date()).toISOString();
697
+ db.prepare("UPDATE nodes SET parent = ?, updated_at = ? WHERE id = ?").run(
698
+ op.new_parent,
699
+ now,
700
+ op.node_id
701
+ );
702
+ recomputeSubtreeDepth(op.node_id, newParent.depth + 1);
703
+ logEvent(op.node_id, agent, "moved", [
704
+ { field: "parent", before: oldParent, after: op.new_parent }
705
+ ]);
706
+ return { node_id: op.node_id, result: `moved under ${op.new_parent}` };
707
+ }
708
+ function handleMerge(op, agent) {
709
+ const db = getDb();
710
+ const source = getNodeOrThrow(op.source);
711
+ const target = getNodeOrThrow(op.target);
712
+ const movedChildren = db.prepare("SELECT id FROM nodes WHERE parent = ?").all(op.source);
713
+ db.prepare("UPDATE nodes SET parent = ?, updated_at = ? WHERE parent = ?").run(
714
+ op.target,
715
+ (/* @__PURE__ */ new Date()).toISOString(),
716
+ op.source
717
+ );
718
+ for (const child of movedChildren) {
719
+ recomputeSubtreeDepth(child.id, target.depth + 1);
720
+ }
721
+ const targetEvidence = [...target.evidence, ...source.evidence];
722
+ db.prepare("UPDATE nodes SET evidence = ?, updated_at = ? WHERE id = ?").run(
723
+ JSON.stringify(targetEvidence),
724
+ (/* @__PURE__ */ new Date()).toISOString(),
725
+ op.target
726
+ );
727
+ const sourceOutEdges = getEdgesFrom(op.source);
728
+ const sourceInEdges = getEdgesTo(op.source);
729
+ for (const edge of sourceOutEdges) {
730
+ const existing = db.prepare(
731
+ "SELECT id FROM edges WHERE from_node = ? AND to_node = ? AND type = ?"
732
+ ).get(op.target, edge.to_node, edge.type);
733
+ if (!existing) {
734
+ db.prepare(
735
+ "UPDATE edges SET from_node = ? WHERE id = ?"
736
+ ).run(op.target, edge.id);
737
+ } else {
738
+ db.prepare("DELETE FROM edges WHERE id = ?").run(edge.id);
739
+ }
740
+ }
741
+ for (const edge of sourceInEdges) {
742
+ const existing = db.prepare(
743
+ "SELECT id FROM edges WHERE from_node = ? AND to_node = ? AND type = ?"
744
+ ).get(edge.from_node, op.target, edge.type);
745
+ if (!existing) {
746
+ db.prepare(
747
+ "UPDATE edges SET to_node = ? WHERE id = ?"
748
+ ).run(op.target, edge.id);
749
+ } else {
750
+ db.prepare("DELETE FROM edges WHERE id = ?").run(edge.id);
751
+ }
752
+ }
753
+ logEvent(op.target, agent, "merged", [
754
+ { field: "merged_from", before: null, after: op.source }
755
+ ]);
756
+ logEvent(op.source, agent, "merged_into", [
757
+ { field: "merged_into", before: null, after: op.target }
758
+ ]);
759
+ db.prepare("DELETE FROM edges WHERE from_node = ? OR to_node = ?").run(
760
+ op.source,
761
+ op.source
762
+ );
763
+ db.prepare("DELETE FROM nodes WHERE id = ?").run(op.source);
764
+ return { node_id: op.target, result: `merged ${op.source} into ${op.target}` };
765
+ }
766
+ function handleDrop(op, agent) {
767
+ const now = (/* @__PURE__ */ new Date()).toISOString();
768
+ const descendants = getAllDescendants(op.node_id);
769
+ const allIds = [op.node_id, ...descendants];
770
+ for (const id of allIds) {
771
+ const node = getNode(id);
772
+ if (!node || node.resolved) continue;
773
+ updateNode({
774
+ node_id: id,
775
+ agent,
776
+ resolved: true,
777
+ add_evidence: [{ type: "dropped", ref: op.reason }]
778
+ });
779
+ logEvent(id, agent, "dropped", [
780
+ { field: "resolved", before: false, after: true },
781
+ { field: "reason", before: null, after: op.reason }
782
+ ]);
783
+ }
784
+ return {
785
+ node_id: op.node_id,
786
+ result: `dropped ${allIds.length} node(s): ${op.reason}`
787
+ };
788
+ }
789
+ function handleRestructure(input, agent) {
790
+ const operations = requireArray(input?.operations, "operations");
791
+ for (let i = 0; i < operations.length; i++) {
792
+ const op = operations[i];
793
+ requireString(op.op, `operations[${i}].op`);
794
+ if (op.op === "move") {
795
+ requireString(op.node_id, `operations[${i}].node_id`);
796
+ requireString(op.new_parent, `operations[${i}].new_parent`);
797
+ } else if (op.op === "merge") {
798
+ requireString(op.source, `operations[${i}].source`);
799
+ requireString(op.target, `operations[${i}].target`);
800
+ } else if (op.op === "drop") {
801
+ requireString(op.node_id, `operations[${i}].node_id`);
802
+ requireString(op.reason, `operations[${i}].reason`);
803
+ } else {
804
+ throw new Error(`Unknown operation: ${op.op}`);
805
+ }
806
+ }
807
+ const db = getDb();
808
+ let applied = 0;
809
+ const details = [];
810
+ let project = null;
811
+ const transaction = db.transaction(() => {
812
+ for (const op of operations) {
813
+ let detail;
814
+ switch (op.op) {
815
+ case "move":
816
+ detail = handleMove(op, agent);
817
+ project = getNode(op.node_id)?.project ?? project;
818
+ break;
819
+ case "merge":
820
+ detail = handleMerge(op, agent);
821
+ project = getNode(op.target)?.project ?? project;
822
+ break;
823
+ case "drop":
824
+ detail = handleDrop(op, agent);
825
+ project = getNode(op.node_id)?.project ?? project;
826
+ break;
827
+ default:
828
+ throw new Error(`Unknown operation: ${op.op}`);
829
+ }
830
+ details.push({ op: op.op, ...detail });
831
+ applied++;
832
+ }
833
+ });
834
+ transaction();
835
+ const result = { applied, details };
836
+ if (project) {
837
+ const actionable = findNewlyActionable(project);
838
+ if (actionable.length > 0) {
839
+ result.newly_actionable = actionable;
840
+ }
841
+ }
842
+ return result;
843
+ }
844
+
845
+ // src/tools/history.ts
846
+ function handleHistory(input) {
847
+ const nodeId = requireString(input?.node_id, "node_id");
848
+ const limit = optionalNumber(input?.limit, "limit", 1, 100) ?? 20;
849
+ const cursor = optionalString(input?.cursor, "cursor");
850
+ getNodeOrThrow(nodeId);
851
+ const { events, next_cursor } = getEvents(nodeId, limit, cursor);
852
+ const result = {
853
+ events: events.map((e) => ({
854
+ timestamp: e.timestamp,
855
+ agent: e.agent,
856
+ action: e.action,
857
+ changes: e.changes
858
+ }))
859
+ };
860
+ if (next_cursor) {
861
+ result.next_cursor = next_cursor;
862
+ }
863
+ return result;
864
+ }
865
+
866
+ // src/tools/onboard.ts
867
+ function handleOnboard(input) {
868
+ const project = requireString(input?.project, "project");
869
+ const evidenceLimit = optionalNumber(input?.evidence_limit, "evidence_limit", 1, 50) ?? 20;
870
+ const db = getDb();
871
+ const root = getProjectRoot(project);
872
+ if (!root) {
873
+ throw new EngineError("project_not_found", `Project not found: ${project}`);
874
+ }
875
+ const summary = getProjectSummary(project);
876
+ const topChildren = db.prepare("SELECT * FROM nodes WHERE parent = ? ORDER BY created_at ASC").all(root.id);
877
+ const tree = topChildren.map((child) => {
878
+ const grandchildren = db.prepare(
879
+ `SELECT id, summary, resolved,
880
+ (SELECT COUNT(*) FROM nodes gc WHERE gc.parent = n.id) as child_count
881
+ FROM nodes n WHERE parent = ? ORDER BY created_at ASC`
882
+ ).all(child.id);
883
+ return {
884
+ id: child.id,
885
+ summary: child.summary,
886
+ resolved: child.resolved === 1,
887
+ children: grandchildren.map((gc) => ({
888
+ id: gc.id,
889
+ summary: gc.summary,
890
+ resolved: gc.resolved === 1,
891
+ child_count: gc.child_count
892
+ }))
893
+ };
894
+ });
895
+ const allNodes = db.prepare("SELECT id, summary, evidence FROM nodes WHERE project = ? AND resolved = 1 AND evidence != '[]'").all(project);
896
+ const allEvidence = [];
897
+ for (const node of allNodes) {
898
+ const evidence = JSON.parse(node.evidence);
899
+ for (const ev of evidence) {
900
+ allEvidence.push({
901
+ node_id: node.id,
902
+ node_summary: node.summary,
903
+ type: ev.type,
904
+ ref: ev.ref,
905
+ agent: ev.agent,
906
+ timestamp: ev.timestamp
907
+ });
908
+ }
909
+ }
910
+ allEvidence.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
911
+ const recent_evidence = allEvidence.slice(0, evidenceLimit);
912
+ const linkRows = db.prepare("SELECT context_links FROM nodes WHERE project = ? AND context_links != '[]'").all(project);
913
+ const linkSet = /* @__PURE__ */ new Set();
914
+ for (const row of linkRows) {
915
+ const links = JSON.parse(row.context_links);
916
+ for (const link of links) {
917
+ linkSet.add(link);
918
+ }
919
+ }
920
+ const context_links = [...linkSet].sort();
921
+ const actionableRows = db.prepare(
922
+ `SELECT n.id, n.summary, n.properties FROM nodes n
923
+ WHERE n.project = ? AND n.resolved = 0
924
+ AND NOT EXISTS (
925
+ SELECT 1 FROM nodes child WHERE child.parent = n.id AND child.resolved = 0
926
+ )
927
+ AND NOT EXISTS (
928
+ SELECT 1 FROM edges e
929
+ JOIN nodes dep ON dep.id = e.to_node AND dep.resolved = 0
930
+ WHERE e.from_node = n.id AND e.type = 'depends_on'
931
+ )
932
+ ORDER BY
933
+ COALESCE(CAST(json_extract(n.properties, '$.priority') AS REAL), 0) DESC,
934
+ n.depth DESC,
935
+ n.updated_at ASC
936
+ LIMIT 10`
937
+ ).all(project);
938
+ const actionable = actionableRows.map((row) => ({
939
+ id: row.id,
940
+ summary: row.summary,
941
+ properties: JSON.parse(row.properties)
942
+ }));
943
+ return {
944
+ project,
945
+ summary,
946
+ tree,
947
+ recent_evidence,
948
+ context_links,
949
+ actionable
950
+ };
951
+ }
952
+
953
+ // src/tools/agent-config.ts
954
+ var AGENT_PROMPT = `---
955
+ name: graph
956
+ description: Use this agent for tasks tracked in Graph. Enforces the claim-work-resolve workflow \u2014 always checks graph_next before working, adds new work to the graph before executing, and resolves with evidence.
957
+ tools: Read, Edit, Write, Bash, Glob, Grep, Task(Explore)
958
+ model: sonnet
959
+ ---
960
+
961
+ You are a graph-optimized agent. You execute tasks tracked in a Graph project. Follow this workflow strictly.
962
+
963
+ # Workflow
964
+
965
+ ## 1. ORIENT
966
+ On your first call, orient yourself:
967
+ \`\`\`
968
+ graph_onboard({ project: "<project-name>" })
969
+ \`\`\`
970
+ Read the summary, recent evidence, context links, and actionable tasks. Understand what was done and what's left.
971
+
972
+ ## 2. CLAIM
973
+ Get your next task:
974
+ \`\`\`
975
+ graph_next({ project: "<project-name>", claim: true })
976
+ \`\`\`
977
+ Read the task summary, ancestor chain (for scope), resolved dependencies (for context on what was done before you), and context links (for files to look at).
978
+
979
+ ## 3. PLAN
980
+ If you discover work that isn't in the graph, add it BEFORE executing:
981
+ \`\`\`
982
+ graph_plan({ nodes: [{ ref: "new-work", parent_ref: "<parent-id>", summary: "..." }] })
983
+ \`\`\`
984
+ Never execute ad-hoc work. The graph is the source of truth.
985
+
986
+ ## 4. WORK
987
+ Execute the claimed task. While working:
988
+ - Annotate key code changes with \`// [sl:nodeId]\` where nodeId is the task you're working on
989
+ - This creates a traceable link from code back to the task, its evidence, and its history
990
+
991
+ ## 5. RESOLVE
992
+ When done, resolve the task with evidence:
993
+ \`\`\`
994
+ graph_update({ updates: [{
995
+ node_id: "<task-id>",
996
+ resolved: true,
997
+ add_evidence: [
998
+ { type: "note", ref: "What you did and why" },
999
+ { type: "git", ref: "<commit-hash> \u2014 <summary>" },
1000
+ { type: "test", ref: "Test results" }
1001
+ ],
1002
+ add_context_links: ["path/to/files/you/touched"]
1003
+ }] })
1004
+ \`\`\`
1005
+ Evidence is mandatory. At minimum, include one note explaining what you did.
1006
+
1007
+ ## 6. LOOP
1008
+ Check the response for \`newly_actionable\` tasks. Then call \`graph_next\` again for your next task. Repeat until no actionable tasks remain.
1009
+
1010
+ # Rules
1011
+
1012
+ - NEVER start work without a claimed task
1013
+ - NEVER resolve without evidence
1014
+ - NEVER execute ad-hoc work \u2014 add it to the graph first via graph_plan
1015
+ - ALWAYS include context_links for files you modified when resolving
1016
+ - If a parent task becomes actionable (all children resolved), resolve it with a summary of what its children accomplished
1017
+ - If you're approaching context limits, ensure your current task's state is captured (update with evidence even if not fully resolved) so the next agent can pick up where you left off
1018
+ `;
1019
+ function handleAgentConfig(dbPath) {
1020
+ const tier = getLicenseTier(dbPath);
1021
+ if (tier !== "pro") {
1022
+ throw new EngineError(
1023
+ "free_tier_limit",
1024
+ "The graph-optimized agent configuration is a pro feature. Activate a license key to unlock it."
1025
+ );
1026
+ }
1027
+ return {
1028
+ agent_file: AGENT_PROMPT,
1029
+ install_path: ".claude/agents/graph.md",
1030
+ instructions: "Save the agent_file content to .claude/agents/graph.md in your project root. Claude Code will automatically discover it and use it when tasks match the agent description."
1031
+ };
1032
+ }
1033
+
1034
+ // src/gates.ts
1035
+ var FREE_LIMITS = {
1036
+ maxProjects: 1,
1037
+ maxNodesPerProject: 50,
1038
+ onboardEvidenceLimit: 5,
1039
+ scopeEnabled: false
1040
+ };
1041
+ function checkNodeLimit(tier, project, adding) {
1042
+ if (tier === "pro") return;
1043
+ const db = getDb();
1044
+ const { count } = db.prepare("SELECT COUNT(*) as count FROM nodes WHERE project = ?").get(project);
1045
+ if (count + adding > FREE_LIMITS.maxNodesPerProject) {
1046
+ throw new EngineError(
1047
+ "free_tier_limit",
1048
+ `Free tier is limited to ${FREE_LIMITS.maxNodesPerProject} nodes per project. Current: ${count}, adding: ${adding}. Activate a license key to remove this limit.`
1049
+ );
1050
+ }
1051
+ }
1052
+ function checkProjectLimit(tier) {
1053
+ if (tier === "pro") return;
1054
+ const db = getDb();
1055
+ const { count } = db.prepare("SELECT COUNT(*) as count FROM nodes WHERE parent IS NULL").get();
1056
+ if (count >= FREE_LIMITS.maxProjects) {
1057
+ throw new EngineError(
1058
+ "free_tier_limit",
1059
+ `Free tier is limited to ${FREE_LIMITS.maxProjects} project. Activate a license key to create unlimited projects.`
1060
+ );
1061
+ }
1062
+ }
1063
+ function capEvidenceLimit(tier, requested) {
1064
+ const max = tier === "pro" ? requested ?? 20 : FREE_LIMITS.onboardEvidenceLimit;
1065
+ return Math.min(requested ?? max, tier === "pro" ? 50 : FREE_LIMITS.onboardEvidenceLimit);
1066
+ }
1067
+ function checkScope(tier, scope) {
1068
+ if (tier === "pro") return scope;
1069
+ return void 0;
1070
+ }
1071
+
1072
+ // src/server.ts
1073
+ var AGENT_IDENTITY = process.env.GRAPH_AGENT ?? "default-agent";
1074
+ var DB_PATH = process.env.GRAPH_DB ?? "./graph.db";
1075
+ var CLAIM_TTL = parseInt(process.env.GRAPH_CLAIM_TTL ?? "60", 10);
1076
+ var TOOLS = [
1077
+ {
1078
+ name: "graph_open",
1079
+ description: "Open an existing project or create a new one. Omit 'project' to list all projects. Returns project root node and summary stats (total, resolved, unresolved, blocked, actionable counts).",
1080
+ inputSchema: {
1081
+ type: "object",
1082
+ properties: {
1083
+ project: {
1084
+ type: "string",
1085
+ description: "Project name (e.g. 'my-project'). Omit to list all projects."
1086
+ },
1087
+ goal: {
1088
+ type: "string",
1089
+ description: "Project goal/description. Used on creation only."
1090
+ }
1091
+ }
1092
+ }
1093
+ },
1094
+ {
1095
+ name: "graph_plan",
1096
+ description: "Batch create nodes with parent-child and dependency relationships in one atomic call. Use for decomposing work into subtrees. Each node needs a temp 'ref' for intra-batch references. parent_ref and depends_on can reference batch refs or existing node IDs.",
1097
+ inputSchema: {
1098
+ type: "object",
1099
+ properties: {
1100
+ nodes: {
1101
+ type: "array",
1102
+ items: {
1103
+ type: "object",
1104
+ properties: {
1105
+ ref: {
1106
+ type: "string",
1107
+ description: "Temp ID for referencing within this batch"
1108
+ },
1109
+ parent_ref: {
1110
+ type: "string",
1111
+ description: "Parent: a ref from this batch OR an existing node ID"
1112
+ },
1113
+ summary: { type: "string" },
1114
+ context_links: {
1115
+ type: "array",
1116
+ items: { type: "string" },
1117
+ description: "Pointers to files, commits, URLs"
1118
+ },
1119
+ depends_on: {
1120
+ type: "array",
1121
+ items: { type: "string" },
1122
+ description: "Refs within batch OR existing node IDs this depends on"
1123
+ },
1124
+ properties: {
1125
+ type: "object",
1126
+ description: "Freeform key-value properties"
1127
+ }
1128
+ },
1129
+ required: ["ref", "summary"]
1130
+ },
1131
+ description: "Nodes to create"
1132
+ }
1133
+ },
1134
+ required: ["nodes"]
1135
+ }
1136
+ },
1137
+ {
1138
+ name: "graph_next",
1139
+ description: "Get the next actionable node \u2014 an unresolved leaf with all dependencies resolved. Ranked by priority (from properties), depth, and least-recently-updated. Returns the node with ancestor chain, context links, and resolved dependency info. Use claim=true to soft-lock the node. When modifying code for this task, annotate key changes with // [sl:nodeId] so future agents can trace code back to this task.",
1140
+ inputSchema: {
1141
+ type: "object",
1142
+ properties: {
1143
+ project: { type: "string", description: "Project name (e.g. 'my-project'), not a node ID" },
1144
+ scope: {
1145
+ type: "string",
1146
+ description: "Node ID to scope results to. Only returns actionable descendants of this node."
1147
+ },
1148
+ filter: {
1149
+ type: "object",
1150
+ description: "Match against node properties"
1151
+ },
1152
+ count: {
1153
+ type: "number",
1154
+ description: "Return top N nodes (default 1)"
1155
+ },
1156
+ claim: {
1157
+ type: "boolean",
1158
+ description: "If true, soft-lock returned nodes with agent identity"
1159
+ }
1160
+ },
1161
+ required: ["project"]
1162
+ }
1163
+ },
1164
+ {
1165
+ name: "graph_context",
1166
+ description: "Deep-read a node and its neighborhood: ancestors (scope chain), children tree (to configurable depth), dependency graph (what it depends on, what depends on it). Look for // [sl:nodeId] annotations in source files to find code tied to specific tasks.",
1167
+ inputSchema: {
1168
+ type: "object",
1169
+ properties: {
1170
+ node_id: { type: "string", description: "Node ID to inspect" },
1171
+ depth: {
1172
+ type: "number",
1173
+ description: "Levels of children to return (default 2)"
1174
+ }
1175
+ },
1176
+ required: ["node_id"]
1177
+ }
1178
+ },
1179
+ {
1180
+ name: "graph_update",
1181
+ description: "Update one or more nodes. Can change resolved, state, summary, properties (merged), context_links, and add evidence. When resolving nodes, returns newly_actionable \u2014 nodes that became unblocked. ENFORCED: Resolving a node requires evidence \u2014 the engine rejects resolved=true if the node has no existing evidence and no add_evidence in the call. Include at least one add_evidence entry (type: 'git' for commits, 'note' for what was done and why, 'test' for results). Also add context_links to files you modified.",
1182
+ inputSchema: {
1183
+ type: "object",
1184
+ properties: {
1185
+ updates: {
1186
+ type: "array",
1187
+ items: {
1188
+ type: "object",
1189
+ properties: {
1190
+ node_id: { type: "string" },
1191
+ resolved: { type: "boolean" },
1192
+ state: { description: "Agent-defined state, any type" },
1193
+ summary: { type: "string" },
1194
+ properties: {
1195
+ type: "object",
1196
+ description: "Merged into existing. Set a key to null to delete it."
1197
+ },
1198
+ add_context_links: {
1199
+ type: "array",
1200
+ items: { type: "string" },
1201
+ description: "Files modified or created for this task. Add when resolving so future agents know what was touched."
1202
+ },
1203
+ remove_context_links: {
1204
+ type: "array",
1205
+ items: { type: "string" }
1206
+ },
1207
+ add_evidence: {
1208
+ type: "array",
1209
+ description: "Evidence of work done. Always add when resolving. Types: 'git' (commit hash + summary), 'note' (what was implemented and why), 'test' (test results).",
1210
+ items: {
1211
+ type: "object",
1212
+ properties: {
1213
+ type: { type: "string", description: "Evidence type: git, note, test, or custom" },
1214
+ ref: { type: "string", description: "The evidence content \u2014 commit ref, implementation note, test result" }
1215
+ },
1216
+ required: ["type", "ref"]
1217
+ }
1218
+ }
1219
+ },
1220
+ required: ["node_id"]
1221
+ }
1222
+ }
1223
+ },
1224
+ required: ["updates"]
1225
+ }
1226
+ },
1227
+ {
1228
+ name: "graph_connect",
1229
+ description: "Add or remove edges between nodes. Types: 'depends_on' (with cycle detection), 'relates_to', or custom. Parent edges not allowed \u2014 use graph_restructure for reparenting.",
1230
+ inputSchema: {
1231
+ type: "object",
1232
+ properties: {
1233
+ edges: {
1234
+ type: "array",
1235
+ items: {
1236
+ type: "object",
1237
+ properties: {
1238
+ from: { type: "string", description: "Source node ID" },
1239
+ to: { type: "string", description: "Target node ID" },
1240
+ type: {
1241
+ type: "string",
1242
+ description: "'depends_on', 'relates_to', or custom"
1243
+ },
1244
+ remove: {
1245
+ type: "boolean",
1246
+ description: "True to remove this edge"
1247
+ }
1248
+ },
1249
+ required: ["from", "to", "type"]
1250
+ }
1251
+ }
1252
+ },
1253
+ required: ["edges"]
1254
+ }
1255
+ },
1256
+ {
1257
+ name: "graph_query",
1258
+ description: "Search and filter nodes. Filters: resolved, properties, text, ancestor (descendants of), is_leaf, is_actionable, is_blocked, claimed_by. Supports sorting and cursor pagination.",
1259
+ inputSchema: {
1260
+ type: "object",
1261
+ properties: {
1262
+ project: { type: "string", description: "Project name (e.g. 'my-project'), not a node ID" },
1263
+ filter: {
1264
+ type: "object",
1265
+ properties: {
1266
+ resolved: { type: "boolean" },
1267
+ properties: { type: "object" },
1268
+ text: { type: "string", description: "Substring match on summary" },
1269
+ ancestor: {
1270
+ type: "string",
1271
+ description: "Return all descendants of this node"
1272
+ },
1273
+ has_evidence_type: { type: "string" },
1274
+ is_leaf: { type: "boolean" },
1275
+ is_actionable: { type: "boolean" },
1276
+ is_blocked: { type: "boolean" },
1277
+ claimed_by: {
1278
+ type: ["string", "null"],
1279
+ description: "Filter by claim. null = unclaimed."
1280
+ }
1281
+ }
1282
+ },
1283
+ sort: {
1284
+ type: "string",
1285
+ enum: ["readiness", "depth", "recent", "created"]
1286
+ },
1287
+ limit: { type: "number", description: "Max results (default 20, max 100)" },
1288
+ cursor: { type: "string", description: "Pagination cursor" }
1289
+ },
1290
+ required: ["project"]
1291
+ }
1292
+ },
1293
+ {
1294
+ name: "graph_restructure",
1295
+ description: "Modify graph structure: move (reparent), merge (combine two nodes), drop (resolve node + subtree with reason). Atomic. Reports newly_actionable nodes.",
1296
+ inputSchema: {
1297
+ type: "object",
1298
+ properties: {
1299
+ operations: {
1300
+ type: "array",
1301
+ items: {
1302
+ type: "object",
1303
+ properties: {
1304
+ op: {
1305
+ type: "string",
1306
+ enum: ["move", "merge", "drop"]
1307
+ },
1308
+ node_id: { type: "string", description: "For move and drop" },
1309
+ new_parent: { type: "string", description: "For move" },
1310
+ source: { type: "string", description: "For merge: node to absorb" },
1311
+ target: {
1312
+ type: "string",
1313
+ description: "For merge: node that survives"
1314
+ },
1315
+ reason: { type: "string", description: "For drop: why" }
1316
+ },
1317
+ required: ["op"]
1318
+ }
1319
+ }
1320
+ },
1321
+ required: ["operations"]
1322
+ }
1323
+ },
1324
+ {
1325
+ name: "graph_history",
1326
+ description: "Read the audit trail for a node. Shows who changed what, when, and why. Useful for understanding past decisions across sessions.",
1327
+ inputSchema: {
1328
+ type: "object",
1329
+ properties: {
1330
+ node_id: { type: "string" },
1331
+ limit: { type: "number", description: "Max events (default 20)" },
1332
+ cursor: { type: "string", description: "Pagination cursor" }
1333
+ },
1334
+ required: ["node_id"]
1335
+ }
1336
+ },
1337
+ {
1338
+ name: "graph_onboard",
1339
+ description: "Single-call orientation for new agents joining a project. Returns project summary, tree structure (depth 2), recent evidence from resolved nodes (knowledge transfer), all context links, and actionable tasks. Use this as your first call when starting work on an existing project.",
1340
+ inputSchema: {
1341
+ type: "object",
1342
+ properties: {
1343
+ project: { type: "string", description: "Project name (e.g. 'my-project')" },
1344
+ evidence_limit: {
1345
+ type: "number",
1346
+ description: "Max evidence entries to return (default 20, max 50)"
1347
+ }
1348
+ },
1349
+ required: ["project"]
1350
+ }
1351
+ },
1352
+ {
1353
+ name: "graph_agent_config",
1354
+ description: "Returns the graph-optimized agent configuration file for Claude Code. Pro tier only. Save the returned content to .claude/agents/graph.md to enable the graph workflow agent.",
1355
+ inputSchema: {
1356
+ type: "object",
1357
+ properties: {}
1358
+ }
1359
+ }
1360
+ ];
1361
+ async function startServer() {
1362
+ initDb(DB_PATH);
1363
+ const tier = getLicenseTier(DB_PATH);
1364
+ const server = new Server(
1365
+ { name: "graph", version: "0.1.0" },
1366
+ { capabilities: { tools: {} } }
1367
+ );
1368
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1369
+ tools: TOOLS
1370
+ }));
1371
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1372
+ const { name, arguments: args } = request.params;
1373
+ try {
1374
+ let result;
1375
+ switch (name) {
1376
+ case "graph_open": {
1377
+ const openArgs = args;
1378
+ if (openArgs?.project) {
1379
+ const { getProjectRoot: getProjectRoot2 } = await import("./nodes-75II37NJ.js");
1380
+ if (!getProjectRoot2(openArgs.project)) {
1381
+ checkProjectLimit(tier);
1382
+ }
1383
+ }
1384
+ result = handleOpen(openArgs, AGENT_IDENTITY);
1385
+ break;
1386
+ }
1387
+ case "graph_plan": {
1388
+ const planArgs = args;
1389
+ if (planArgs?.nodes?.length > 0) {
1390
+ const { getNode: getNode2 } = await import("./nodes-75II37NJ.js");
1391
+ const firstParent = planArgs.nodes[0]?.parent_ref;
1392
+ if (firstParent && typeof firstParent === "string" && !planArgs.nodes.some((n) => n.ref === firstParent)) {
1393
+ const parentNode = getNode2(firstParent);
1394
+ if (parentNode) {
1395
+ checkNodeLimit(tier, parentNode.project, planArgs.nodes.length);
1396
+ }
1397
+ }
1398
+ }
1399
+ result = handlePlan(planArgs, AGENT_IDENTITY);
1400
+ break;
1401
+ }
1402
+ case "graph_next": {
1403
+ const nextArgs = args;
1404
+ if (nextArgs?.scope) {
1405
+ nextArgs.scope = checkScope(tier, nextArgs.scope);
1406
+ }
1407
+ result = handleNext(nextArgs, AGENT_IDENTITY, CLAIM_TTL);
1408
+ break;
1409
+ }
1410
+ case "graph_context":
1411
+ result = handleContext(args);
1412
+ break;
1413
+ case "graph_update":
1414
+ result = handleUpdate(args, AGENT_IDENTITY);
1415
+ break;
1416
+ case "graph_connect":
1417
+ result = handleConnect(args, AGENT_IDENTITY);
1418
+ break;
1419
+ case "graph_query":
1420
+ result = handleQuery(args);
1421
+ break;
1422
+ case "graph_restructure":
1423
+ result = handleRestructure(args, AGENT_IDENTITY);
1424
+ break;
1425
+ case "graph_history":
1426
+ result = handleHistory(args);
1427
+ break;
1428
+ case "graph_onboard": {
1429
+ const onboardArgs = args;
1430
+ onboardArgs.evidence_limit = capEvidenceLimit(tier, onboardArgs?.evidence_limit);
1431
+ result = handleOnboard(onboardArgs);
1432
+ break;
1433
+ }
1434
+ case "graph_agent_config":
1435
+ result = handleAgentConfig(DB_PATH);
1436
+ break;
1437
+ default:
1438
+ return {
1439
+ content: [
1440
+ { type: "text", text: JSON.stringify({ error: `Unknown tool: ${name}` }) }
1441
+ ],
1442
+ isError: true
1443
+ };
1444
+ }
1445
+ return {
1446
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1447
+ };
1448
+ } catch (error) {
1449
+ const message = error instanceof Error ? error.message : String(error);
1450
+ const code = error instanceof ValidationError ? "validation_error" : error instanceof EngineError ? error.code : "error";
1451
+ return {
1452
+ content: [
1453
+ {
1454
+ type: "text",
1455
+ text: JSON.stringify({ error: message, code })
1456
+ }
1457
+ ],
1458
+ isError: true
1459
+ };
1460
+ }
1461
+ });
1462
+ const transport = new StdioServerTransport();
1463
+ await server.connect(transport);
1464
+ process.on("SIGINT", () => {
1465
+ closeDb();
1466
+ process.exit(0);
1467
+ });
1468
+ process.on("SIGTERM", () => {
1469
+ closeDb();
1470
+ process.exit(0);
1471
+ });
1472
+ }
1473
+ export {
1474
+ startServer
1475
+ };
1476
+ //# sourceMappingURL=server-7IB2VIMK.js.map