sysprom 1.16.1 → 1.18.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 (84) hide show
  1. package/README.md +147 -75
  2. package/dist/schema.json +2 -1
  3. package/dist/src/cli/commands/add.js +52 -36
  4. package/dist/src/cli/commands/graph.d.ts +15 -0
  5. package/dist/src/cli/commands/graph.js +51 -2
  6. package/dist/src/cli/commands/infer.js +44 -25
  7. package/dist/src/cli/commands/init.d.ts +1 -1
  8. package/dist/src/cli/commands/json2md.d.ts +30 -1
  9. package/dist/src/cli/commands/json2md.js +42 -1
  10. package/dist/src/cli/commands/md2json.d.ts +1 -1
  11. package/dist/src/cli/commands/query.js +35 -37
  12. package/dist/src/cli/commands/speckit.js +81 -77
  13. package/dist/src/cli/commands/stats.js +4 -4
  14. package/dist/src/cli/commands/sync.d.ts +1 -1
  15. package/dist/src/cli/commands/update.js +33 -20
  16. package/dist/src/cli/define-command.d.ts +1 -1
  17. package/dist/src/cli/define-command.js +176 -156
  18. package/dist/src/endpoint-types.js +13 -6
  19. package/dist/src/io.js +59 -8
  20. package/dist/src/json-to-md.d.ts +32 -2
  21. package/dist/src/json-to-md.js +145 -5
  22. package/dist/src/mcp/server.js +269 -112
  23. package/dist/src/md-to-json.js +7 -0
  24. package/dist/src/operations/add-node.d.ts +12 -9
  25. package/dist/src/operations/add-plan-task.d.ts +8 -6
  26. package/dist/src/operations/add-relationship.d.ts +11 -8
  27. package/dist/src/operations/check.d.ts +4 -3
  28. package/dist/src/operations/define-operation.d.ts +1 -1
  29. package/dist/src/operations/graph-decision.d.ts +329 -0
  30. package/dist/src/operations/graph-decision.js +96 -0
  31. package/dist/src/operations/graph-dependency.d.ts +329 -0
  32. package/dist/src/operations/graph-dependency.js +121 -0
  33. package/dist/src/operations/graph-refinement.d.ts +329 -0
  34. package/dist/src/operations/graph-refinement.js +97 -0
  35. package/dist/src/operations/graph-shared.d.ts +116 -0
  36. package/dist/src/operations/graph-shared.js +257 -0
  37. package/dist/src/operations/graph.d.ts +20 -4
  38. package/dist/src/operations/graph.js +129 -36
  39. package/dist/src/operations/index.d.ts +3 -0
  40. package/dist/src/operations/index.js +3 -0
  41. package/dist/src/operations/infer-completeness.d.ts +4 -3
  42. package/dist/src/operations/infer-derived.d.ts +4 -3
  43. package/dist/src/operations/infer-impact.d.ts +28 -21
  44. package/dist/src/operations/infer-lifecycle.d.ts +4 -3
  45. package/dist/src/operations/init-document.d.ts +4 -3
  46. package/dist/src/operations/json-to-markdown.d.ts +28 -3
  47. package/dist/src/operations/json-to-markdown.js +11 -1
  48. package/dist/src/operations/mark-task-done.d.ts +8 -6
  49. package/dist/src/operations/mark-task-undone.d.ts +8 -6
  50. package/dist/src/operations/markdown-to-json.d.ts +4 -3
  51. package/dist/src/operations/next-id.d.ts +4 -3
  52. package/dist/src/operations/node-history.d.ts +4 -3
  53. package/dist/src/operations/plan-add-task.d.ts +8 -6
  54. package/dist/src/operations/plan-gate.d.ts +4 -3
  55. package/dist/src/operations/plan-init.d.ts +4 -3
  56. package/dist/src/operations/plan-progress.d.ts +4 -3
  57. package/dist/src/operations/plan-status.d.ts +4 -3
  58. package/dist/src/operations/query-node.d.ts +24 -17
  59. package/dist/src/operations/query-nodes.d.ts +8 -6
  60. package/dist/src/operations/query-relationships.d.ts +7 -5
  61. package/dist/src/operations/remove-node.d.ts +12 -9
  62. package/dist/src/operations/remove-relationship.d.ts +10 -7
  63. package/dist/src/operations/rename.d.ts +8 -6
  64. package/dist/src/operations/search.d.ts +8 -6
  65. package/dist/src/operations/speckit-diff.d.ts +4 -3
  66. package/dist/src/operations/speckit-export.d.ts +4 -3
  67. package/dist/src/operations/speckit-import.d.ts +4 -3
  68. package/dist/src/operations/speckit-sync.d.ts +12 -9
  69. package/dist/src/operations/state-at.d.ts +4 -3
  70. package/dist/src/operations/stats.d.ts +4 -3
  71. package/dist/src/operations/sync.d.ts +12 -9
  72. package/dist/src/operations/task-list.d.ts +4 -3
  73. package/dist/src/operations/timeline.d.ts +4 -3
  74. package/dist/src/operations/trace-from-node.d.ts +12 -9
  75. package/dist/src/operations/update-metadata.d.ts +8 -6
  76. package/dist/src/operations/update-node.d.ts +11 -8
  77. package/dist/src/operations/update-plan-task.d.ts +8 -6
  78. package/dist/src/operations/validate.d.ts +4 -3
  79. package/dist/src/schema.d.ts +15 -10
  80. package/dist/src/schema.js +3 -11
  81. package/dist/src/utils/define-schema.d.ts +17 -0
  82. package/dist/src/utils/define-schema.js +21 -0
  83. package/package.json +98 -93
  84. package/schema.json +2 -1
@@ -1,6 +1,10 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { NODE_FILE_MAP, NODE_TYPE_LABELS, NodeType, RelationshipType, RELATIONSHIP_TYPE_LABELS, } from "./schema.js";
4
+ import { graphOp } from "./operations/graph.js";
5
+ import { graphRefinementOp } from "./operations/graph-refinement.js";
6
+ import { graphDecisionOp } from "./operations/graph-decision.js";
7
+ import { graphDependencyOp } from "./operations/graph-dependency.js";
4
8
  // ---------------------------------------------------------------------------
5
9
  // Text helpers
6
10
  // ---------------------------------------------------------------------------
@@ -25,7 +29,13 @@ function renderFrontMatter(fields) {
25
29
  // ---------------------------------------------------------------------------
26
30
  // Node location map (for hyperlinking)
27
31
  // ---------------------------------------------------------------------------
28
- /** GitHub-compatible heading anchor slug. */
32
+ /**
33
+ * GitHub-compatible heading anchor slug.
34
+ * @param text
35
+ * @example
36
+ * // Convert a heading into a GitHub-style slug
37
+ * // slugify('ID — Name') // 'id---name'
38
+ */
29
39
  function slugify(text) {
30
40
  return text
31
41
  .toLowerCase()
@@ -64,6 +74,10 @@ function fileForNodeType(type) {
64
74
  * In single-file mode: `[ID](#anchor)`
65
75
  * In multi-doc mode: `[ID](./FILE.md#anchor)`
66
76
  * Falls back to plain ID if the node isn't in the map.
77
+ * @param id
78
+ * @param nodeMap
79
+ * @param currentFile
80
+ * @example
67
81
  */
68
82
  function linkNodeId(id, nodeMap, currentFile) {
69
83
  const loc = nodeMap.get(id);
@@ -412,9 +426,82 @@ function generateDocFile(doc, fileName, types, fromIdx, nodeMap) {
412
426
  lines.push(...renderNodesGrouped(doc.nodes, types, fromIdx, 2, nodeMap, `${fileName}.md`));
413
427
  return lines.join("\n") + "\n";
414
428
  }
429
+ function generateDiagramsFile(doc, opts) {
430
+ const lines = [];
431
+ lines.push(renderFrontMatter({
432
+ title: "Diagrams",
433
+ doc_type: "diagrams",
434
+ }));
435
+ lines.push("");
436
+ lines.push("# Diagrams");
437
+ lines.push("");
438
+ lines.push("## Relationship Graph");
439
+ lines.push("");
440
+ lines.push("```mermaid");
441
+ lines.push(graphOp({
442
+ doc,
443
+ format: "mermaid",
444
+ layout: opts?.relationshipLayout ?? "TD",
445
+ labelMode: opts?.labelMode ?? "friendly",
446
+ cluster: true,
447
+ connectedOnly: false,
448
+ }));
449
+ lines.push("```");
450
+ lines.push("");
451
+ const refinement = graphRefinementOp({
452
+ doc,
453
+ format: "mermaid",
454
+ layout: opts?.refinementLayout ?? "TD",
455
+ labelMode: opts?.labelMode ?? "friendly",
456
+ });
457
+ if (refinement.includes("-->")) {
458
+ lines.push("## Refinement Chain");
459
+ lines.push("");
460
+ lines.push("```mermaid");
461
+ lines.push(refinement);
462
+ lines.push("```");
463
+ lines.push("");
464
+ }
465
+ const decisions = graphDecisionOp({
466
+ doc,
467
+ format: "mermaid",
468
+ layout: opts?.decisionLayout ?? "TD",
469
+ labelMode: opts?.labelMode ?? "friendly",
470
+ });
471
+ if (decisions.includes("-->")) {
472
+ lines.push("## Decision Map");
473
+ lines.push("");
474
+ lines.push("```mermaid");
475
+ lines.push(decisions);
476
+ lines.push("```");
477
+ lines.push("");
478
+ }
479
+ const dependencies = graphDependencyOp({
480
+ doc,
481
+ format: "mermaid",
482
+ layout: opts?.dependencyLayout ?? "LR",
483
+ labelMode: opts?.labelMode ?? "friendly",
484
+ });
485
+ if (dependencies.includes("-->") || dependencies.includes("-.->")) {
486
+ lines.push("## Dependency Graph");
487
+ lines.push("");
488
+ lines.push("```mermaid");
489
+ lines.push(dependencies);
490
+ lines.push("```");
491
+ lines.push("");
492
+ }
493
+ return lines.join("\n") + "\n";
494
+ }
415
495
  /**
416
496
  * Convert a SysProM document to a single Markdown string.
417
497
  * @param doc - The SysProM document to convert.
498
+ * @param options
499
+ * @param options.embedDiagrams
500
+ * @param options.labelMode
501
+ * @param options.relationshipLayout
502
+ * @param options.refinementLayout
503
+ * @param options.decisionLayout
504
+ * @param options.dependencyLayout
418
505
  * @returns The Markdown representation.
419
506
  * @example
420
507
  * ```ts
@@ -422,7 +509,7 @@ function generateDocFile(doc, fileName, types, fromIdx, nodeMap) {
422
509
  * writeFileSync("output.spm.md", md);
423
510
  * ```
424
511
  */
425
- export function jsonToMarkdownSingle(doc) {
512
+ export function jsonToMarkdownSingle(doc, options) {
426
513
  const fromIdx = indexRelationshipsFrom(doc.relationships ?? []);
427
514
  const nodeMap = buildNodeLocationMap(doc.nodes, "single-file");
428
515
  const lines = [];
@@ -449,6 +536,26 @@ export function jsonToMarkdownSingle(doc) {
449
536
  "version",
450
537
  ];
451
538
  lines.push(...renderNodesGrouped(doc.nodes, allTypes, fromIdx, 2, nodeMap));
539
+ // Diagrams section
540
+ if (options?.embedDiagrams &&
541
+ doc.relationships &&
542
+ doc.relationships.length > 0) {
543
+ lines.push("## Diagrams");
544
+ lines.push("");
545
+ lines.push("### Relationship Graph");
546
+ lines.push("");
547
+ lines.push("```mermaid");
548
+ lines.push(graphOp({
549
+ doc,
550
+ format: "mermaid",
551
+ layout: options?.relationshipLayout ?? "TD",
552
+ labelMode: options?.labelMode ?? "friendly",
553
+ cluster: true,
554
+ connectedOnly: false,
555
+ }));
556
+ lines.push("```");
557
+ lines.push("");
558
+ }
452
559
  // Relationships summary
453
560
  if (doc.relationships && doc.relationships.length > 0) {
454
561
  lines.push("## Relationships");
@@ -479,12 +586,19 @@ export function jsonToMarkdownSingle(doc) {
479
586
  * Convert a SysProM document to a multi-document Markdown folder.
480
587
  * @param doc - The SysProM document to convert.
481
588
  * @param outDir - Output directory path.
589
+ * @param options
590
+ * @param options.embedDiagrams
591
+ * @param options.labelMode
592
+ * @param options.relationshipLayout
593
+ * @param options.refinementLayout
594
+ * @param options.decisionLayout
595
+ * @param options.dependencyLayout
482
596
  * @example
483
597
  * ```ts
484
598
  * jsonToMarkdownMultiDoc(doc, "./SysProM");
485
599
  * ```
486
600
  */
487
- export function jsonToMarkdownMultiDoc(doc, outDir) {
601
+ export function jsonToMarkdownMultiDoc(doc, outDir, options) {
488
602
  mkdirSync(outDir, { recursive: true });
489
603
  const fromIdx = indexRelationshipsFrom(doc.relationships ?? []);
490
604
  const nodeMap = buildNodeLocationMap(doc.nodes, "multi-doc");
@@ -495,6 +609,18 @@ export function jsonToMarkdownMultiDoc(doc, outDir) {
495
609
  continue;
496
610
  writeFileSync(join(outDir, `${fileName}.md`), generateDocFile(doc, fileName, types, fromIdx, nodeMap));
497
611
  }
612
+ // Diagrams file
613
+ if (options?.embedDiagrams &&
614
+ doc.relationships &&
615
+ doc.relationships.length > 0) {
616
+ writeFileSync(join(outDir, "DIAGRAMS.md"), generateDiagramsFile(doc, {
617
+ labelMode: options?.labelMode ?? "friendly",
618
+ relationshipLayout: options?.relationshipLayout,
619
+ refinementLayout: options?.refinementLayout,
620
+ decisionLayout: options?.decisionLayout,
621
+ dependencyLayout: options?.dependencyLayout,
622
+ }));
623
+ }
498
624
  // Subsystem folders or single files
499
625
  const subsystemNodes = doc.nodes.filter((n) => n.subsystem);
500
626
  // Count subsystems per type to decide automatic grouping
@@ -556,9 +682,23 @@ export function jsonToMarkdownMultiDoc(doc, outDir) {
556
682
  */
557
683
  export function jsonToMarkdown(doc, output, options) {
558
684
  if (options.form === "single-file") {
559
- writeFileSync(output, jsonToMarkdownSingle(doc));
685
+ writeFileSync(output, jsonToMarkdownSingle(doc, {
686
+ embedDiagrams: options.embedDiagrams,
687
+ labelMode: options.labelMode,
688
+ relationshipLayout: options.relationshipLayout,
689
+ refinementLayout: options.refinementLayout,
690
+ decisionLayout: options.decisionLayout,
691
+ dependencyLayout: options.dependencyLayout,
692
+ }));
560
693
  }
561
694
  else {
562
- jsonToMarkdownMultiDoc(doc, output);
695
+ jsonToMarkdownMultiDoc(doc, output, {
696
+ embedDiagrams: options.embedDiagrams,
697
+ labelMode: options.labelMode,
698
+ relationshipLayout: options.relationshipLayout,
699
+ refinementLayout: options.refinementLayout,
700
+ decisionLayout: options.decisionLayout,
701
+ dependencyLayout: options.dependencyLayout,
702
+ });
563
703
  }
564
704
  }
@@ -5,11 +5,76 @@ import * as z from "zod";
5
5
  import { loadDocument, saveDocument } from "../io.js";
6
6
  import { NodeType, RelationshipType } from "../schema.js";
7
7
  import { validateOp, statsOp, queryNodesOp, queryNodeOp, queryRelationshipsOp, traceFromNodeOp, addNodeOp, removeNodeOp, updateNodeOp, addRelationshipOp, removeRelationshipOp, nextIdOp, inferCompletenessOp, inferLifecycleOp, inferImpactOp, impactSummaryOp, inferDerivedOp, } from "../operations/index.js";
8
+ /**
9
+ * Wrap an error with a descriptive prefix and attach the original as cause.
10
+ * @param prefix - The error prefix (e.g., "Failed to add node")
11
+ * @param error - The caught error
12
+ * @example
13
+ * ```ts
14
+ * try {
15
+ * someOperation();
16
+ * } catch (error) {
17
+ * wrapError("Failed to do X", error);
18
+ * }
19
+ * ```
20
+ */
21
+ function wrapError(prefix, error) {
22
+ throw new Error(`${prefix}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
23
+ }
8
24
  // Create MCP server instance
9
25
  const server = new McpServer({
10
26
  name: "sysprom-mcp",
11
27
  version: "1.0.0",
12
28
  });
29
+ // Register init-document tool
30
+ server.registerTool("init-document", {
31
+ description: "Initialise a new SysProM document with metadata and empty structure",
32
+ inputSchema: z.object({
33
+ path: z
34
+ .string()
35
+ .describe("Output path for the document (must end in .json)"),
36
+ title: z.string().describe("Document title"),
37
+ description: z.string().optional().describe("Document description"),
38
+ scope: z.string().optional().describe("Document scope"),
39
+ }),
40
+ }, ({ path, title, description, scope }) => {
41
+ try {
42
+ const now = new Date().toISOString().split("T")[0];
43
+ const doc = {
44
+ $schema: "https://sysprom.org/schema.json",
45
+ metadata: {
46
+ title,
47
+ ...(description && { description }),
48
+ ...(scope && { scope }),
49
+ version: "1.0.0",
50
+ created: now,
51
+ },
52
+ nodes: [],
53
+ relationships: [],
54
+ };
55
+ try {
56
+ saveDocument(doc, "json", path);
57
+ }
58
+ catch (error) {
59
+ wrapError("Failed to save document", error);
60
+ }
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text",
65
+ text: JSON.stringify({
66
+ message: "Document initialised",
67
+ path,
68
+ title,
69
+ }, null, 2),
70
+ },
71
+ ],
72
+ };
73
+ }
74
+ catch (error) {
75
+ wrapError("init-document", error);
76
+ }
77
+ });
13
78
  // Register validate tool
14
79
  server.registerTool("validate", {
15
80
  description: "Validate a SysProM document and return any validation issues",
@@ -145,34 +210,50 @@ server.registerTool("add-node", {
145
210
  description: z.string().optional().describe("Node description"),
146
211
  }),
147
212
  }, ({ path, type, id, name, description }) => {
148
- const loaded = loadDocument(path);
149
- const nodeType = NodeType.safeParse(type);
150
- if (!nodeType.success) {
151
- throw new Error(`Invalid node type: "${type}". Valid types: ${NodeType.options.join(", ")}`);
152
- }
153
- const nodeId = id ?? nextIdOp({ doc: loaded.doc, type: nodeType.data });
154
- const updated = addNodeOp({
155
- doc: loaded.doc,
156
- node: {
157
- id: nodeId,
158
- type: nodeType.data,
159
- name,
160
- ...(description && { description }),
161
- },
162
- });
163
- saveDocument(updated, loaded.format, loaded.path);
164
- return {
165
- content: [
166
- {
167
- type: "text",
168
- text: JSON.stringify({
169
- message: "Node added",
213
+ try {
214
+ const loaded = loadDocument(path);
215
+ const nodeType = NodeType.safeParse(type);
216
+ if (!nodeType.success) {
217
+ throw new Error(`Invalid node type: "${type}". Valid types: ${NodeType.options.join(", ")}`);
218
+ }
219
+ const nodeId = id ?? nextIdOp({ doc: loaded.doc, type: nodeType.data });
220
+ let updated;
221
+ try {
222
+ updated = addNodeOp({
223
+ doc: loaded.doc,
224
+ node: {
170
225
  id: nodeId,
171
- nodeCount: updated.nodes.length,
172
- }, null, 2),
173
- },
174
- ],
175
- };
226
+ type: nodeType.data,
227
+ name,
228
+ ...(description && { description }),
229
+ },
230
+ });
231
+ }
232
+ catch (error) {
233
+ wrapError("Failed to add node", error);
234
+ }
235
+ try {
236
+ saveDocument(updated, loaded.format, loaded.path);
237
+ }
238
+ catch (error) {
239
+ throw new Error(`Failed to save document: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
240
+ }
241
+ return {
242
+ content: [
243
+ {
244
+ type: "text",
245
+ text: JSON.stringify({
246
+ message: "Node added",
247
+ id: nodeId,
248
+ nodeCount: updated.nodes.length,
249
+ }, null, 2),
250
+ },
251
+ ],
252
+ };
253
+ }
254
+ catch (error) {
255
+ wrapError("add-node", error);
256
+ }
176
257
  });
177
258
  // Register remove-node tool
178
259
  server.registerTool("remove-node", {
@@ -182,21 +263,37 @@ server.registerTool("remove-node", {
182
263
  id: z.string().describe("Node ID"),
183
264
  }),
184
265
  }, ({ path, id }) => {
185
- const loaded = loadDocument(path);
186
- const result = removeNodeOp({ doc: loaded.doc, id });
187
- saveDocument(result.doc, loaded.format, loaded.path);
188
- return {
189
- content: [
190
- {
191
- type: "text",
192
- text: JSON.stringify({
193
- message: `Node ${id} removed`,
194
- nodeCount: result.doc.nodes.length,
195
- warnings: result.warnings,
196
- }, null, 2),
197
- },
198
- ],
199
- };
266
+ try {
267
+ const loaded = loadDocument(path);
268
+ let result;
269
+ try {
270
+ result = removeNodeOp({ doc: loaded.doc, id });
271
+ }
272
+ catch (error) {
273
+ wrapError("Failed to remove node", error);
274
+ }
275
+ try {
276
+ saveDocument(result.doc, loaded.format, loaded.path);
277
+ }
278
+ catch (error) {
279
+ throw new Error(`Failed to save document: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
280
+ }
281
+ return {
282
+ content: [
283
+ {
284
+ type: "text",
285
+ text: JSON.stringify({
286
+ message: `Node ${id} removed`,
287
+ nodeCount: result.doc.nodes.length,
288
+ warnings: result.warnings,
289
+ }, null, 2),
290
+ },
291
+ ],
292
+ };
293
+ }
294
+ catch (error) {
295
+ wrapError("remove-node", error);
296
+ }
200
297
  });
201
298
  // Register update-node tool
202
299
  server.registerTool("update-node", {
@@ -207,11 +304,10 @@ server.registerTool("update-node", {
207
304
  fields: z.record(z.string(), z.unknown()).describe("Fields to update"),
208
305
  }),
209
306
  }, ({ path, id, fields }) => {
210
- const loaded = loadDocument(path);
211
- // Validate fields are valid node property updates
212
- const validFields = Object.entries(fields).reduce((acc, [key, value]) => {
213
- // Allow common node fields; unknown fields are silently ignored
214
- if ([
307
+ try {
308
+ const loaded = loadDocument(path);
309
+ // Track which fields are valid
310
+ const allowedFields = [
215
311
  "name",
216
312
  "description",
217
313
  "status",
@@ -227,26 +323,55 @@ server.registerTool("update-node", {
227
323
  "input",
228
324
  "output",
229
325
  "external_references",
230
- ].includes(key)) {
231
- acc[key] = value;
326
+ ];
327
+ const droppedFields = [];
328
+ const validFields = Object.entries(fields).reduce((acc, [key, value]) => {
329
+ if (allowedFields.includes(key)) {
330
+ acc[key] = value;
331
+ }
332
+ else {
333
+ droppedFields.push(key);
334
+ }
335
+ return acc;
336
+ }, {});
337
+ let updated;
338
+ try {
339
+ updated = updateNodeOp({
340
+ doc: loaded.doc,
341
+ id,
342
+ fields: validFields,
343
+ });
232
344
  }
233
- return acc;
234
- }, {});
235
- const updated = updateNodeOp({
236
- doc: loaded.doc,
237
- id,
238
- fields: validFields,
239
- });
240
- saveDocument(updated, loaded.format, loaded.path);
241
- const node = updated.nodes.find((n) => n.id === id);
242
- return {
243
- content: [
244
- {
245
- type: "text",
246
- text: JSON.stringify({ message: "Node updated", node }, null, 2),
247
- },
248
- ],
249
- };
345
+ catch (error) {
346
+ wrapError("Failed to update node", error);
347
+ }
348
+ try {
349
+ saveDocument(updated, loaded.format, loaded.path);
350
+ }
351
+ catch (error) {
352
+ throw new Error(`Failed to save document: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
353
+ }
354
+ const node = updated.nodes.find((n) => n.id === id);
355
+ return {
356
+ content: [
357
+ {
358
+ type: "text",
359
+ text: JSON.stringify({
360
+ message: "Node updated",
361
+ node,
362
+ ...(droppedFields.length > 0 && {
363
+ warnings: [
364
+ `These fields are not updateable and were ignored: ${droppedFields.join(", ")}`,
365
+ ],
366
+ }),
367
+ }, null, 2),
368
+ },
369
+ ],
370
+ };
371
+ }
372
+ catch (error) {
373
+ wrapError("update-node", error);
374
+ }
250
375
  });
251
376
  // Register add-relationship tool
252
377
  server.registerTool("add-relationship", {
@@ -258,31 +383,47 @@ server.registerTool("add-relationship", {
258
383
  type: z.string().describe("Relationship type"),
259
384
  }),
260
385
  }, ({ path, from, to, type }) => {
261
- const loaded = loadDocument(path);
262
- const relType = RelationshipType.safeParse(type);
263
- if (!relType.success) {
264
- throw new Error(`Invalid relationship type: "${type}". Valid types: ${RelationshipType.options.join(", ")}`);
386
+ try {
387
+ const loaded = loadDocument(path);
388
+ const relType = RelationshipType.safeParse(type);
389
+ if (!relType.success) {
390
+ throw new Error(`Invalid relationship type: "${type}". Valid types: ${RelationshipType.options.join(", ")}`);
391
+ }
392
+ let updated;
393
+ try {
394
+ updated = addRelationshipOp({
395
+ doc: loaded.doc,
396
+ rel: {
397
+ from,
398
+ to,
399
+ type: relType.data,
400
+ },
401
+ });
402
+ }
403
+ catch (error) {
404
+ wrapError("Failed to add relationship", error);
405
+ }
406
+ try {
407
+ saveDocument(updated, loaded.format, loaded.path);
408
+ }
409
+ catch (error) {
410
+ throw new Error(`Failed to save document: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
411
+ }
412
+ return {
413
+ content: [
414
+ {
415
+ type: "text",
416
+ text: JSON.stringify({
417
+ message: "Relationship added",
418
+ relationshipCount: (updated.relationships ?? []).length,
419
+ }, null, 2),
420
+ },
421
+ ],
422
+ };
423
+ }
424
+ catch (error) {
425
+ wrapError("add-relationship", error);
265
426
  }
266
- const updated = addRelationshipOp({
267
- doc: loaded.doc,
268
- rel: {
269
- from,
270
- to,
271
- type: relType.data,
272
- },
273
- });
274
- saveDocument(updated, loaded.format, loaded.path);
275
- return {
276
- content: [
277
- {
278
- type: "text",
279
- text: JSON.stringify({
280
- message: "Relationship added",
281
- relationshipCount: (updated.relationships ?? []).length,
282
- }, null, 2),
283
- },
284
- ],
285
- };
286
427
  });
287
428
  // Register remove-relationship tool
288
429
  server.registerTool("remove-relationship", {
@@ -294,29 +435,45 @@ server.registerTool("remove-relationship", {
294
435
  type: z.string().describe("Relationship type"),
295
436
  }),
296
437
  }, ({ path, from, to, type }) => {
297
- const loaded = loadDocument(path);
298
- const relType = RelationshipType.safeParse(type);
299
- if (!relType.success) {
300
- throw new Error(`Invalid relationship type: "${type}". Valid types: ${RelationshipType.options.join(", ")}`);
438
+ try {
439
+ const loaded = loadDocument(path);
440
+ const relType = RelationshipType.safeParse(type);
441
+ if (!relType.success) {
442
+ throw new Error(`Invalid relationship type: "${type}". Valid types: ${RelationshipType.options.join(", ")}`);
443
+ }
444
+ let result;
445
+ try {
446
+ result = removeRelationshipOp({
447
+ doc: loaded.doc,
448
+ from,
449
+ to,
450
+ type: relType.data,
451
+ });
452
+ }
453
+ catch (error) {
454
+ wrapError("Failed to remove relationship", error);
455
+ }
456
+ try {
457
+ saveDocument(result.doc, loaded.format, loaded.path);
458
+ }
459
+ catch (error) {
460
+ throw new Error(`Failed to save document: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
461
+ }
462
+ return {
463
+ content: [
464
+ {
465
+ type: "text",
466
+ text: JSON.stringify({
467
+ message: "Relationship removed",
468
+ relationshipCount: (result.doc.relationships ?? []).length,
469
+ }, null, 2),
470
+ },
471
+ ],
472
+ };
473
+ }
474
+ catch (error) {
475
+ wrapError("remove-relationship", error);
301
476
  }
302
- const result = removeRelationshipOp({
303
- doc: loaded.doc,
304
- from,
305
- to,
306
- type: relType.data,
307
- });
308
- saveDocument(result.doc, loaded.format, loaded.path);
309
- return {
310
- content: [
311
- {
312
- type: "text",
313
- text: JSON.stringify({
314
- message: "Relationship removed",
315
- relationshipCount: (result.doc.relationships ?? []).length,
316
- }, null, 2),
317
- },
318
- ],
319
- };
320
477
  });
321
478
  // Register infer-completeness tool
322
479
  server.registerTool("infer-completeness", {
@@ -3,6 +3,13 @@ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
3
3
  import { join, basename } from "node:path";
4
4
  import { NODE_FILE_MAP, NODE_LABEL_TO_TYPE, RELATIONSHIP_TYPE_LABELS, RELATIONSHIP_LABEL_TO_TYPE, NodeType, RelationshipType, NodeStatus, ExternalReferenceRole, } from "./schema.js";
5
5
  /** Strip markdown link syntax `[text](url)` → `text`. */
6
+ /**
7
+ * Strip markdown link syntax `[text](url)` → `text`.
8
+ * @param s - Markdown text potentially containing links
9
+ * @returns Text with markdown links removed
10
+ * @example
11
+ * // stripMarkdownLink('[Hello](https://example.com)') // 'Hello'
12
+ */
6
13
  function stripMarkdownLink(s) {
7
14
  return s.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
8
15
  }