loom-spec 0.3.0 → 0.5.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 (44) hide show
  1. package/dist/cli/exportHtml.d.ts +22 -0
  2. package/dist/cli/exportHtml.js +182 -0
  3. package/dist/cli/exportHtml.js.map +1 -0
  4. package/dist/cli/index.js +40 -34
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/mcp/server.js +3 -207
  7. package/dist/mcp/server.js.map +1 -1
  8. package/dist/server/app.js +2 -50
  9. package/dist/server/app.js.map +1 -1
  10. package/dist/server/exportConfig.d.ts +40 -0
  11. package/dist/server/exportConfig.js +78 -0
  12. package/dist/server/exportConfig.js.map +1 -0
  13. package/dist/server/exportFilter.d.ts +52 -0
  14. package/dist/server/exportFilter.js +138 -0
  15. package/dist/server/exportFilter.js.map +1 -0
  16. package/dist/server/fileOps.d.ts +0 -12
  17. package/dist/server/fileOps.js +0 -51
  18. package/dist/server/fileOps.js.map +1 -1
  19. package/dist/server/watch.d.ts +1 -1
  20. package/dist/server/watch.js +0 -5
  21. package/dist/server/watch.js.map +1 -1
  22. package/dist/validate.d.ts +1 -3
  23. package/dist/validate.js +0 -15
  24. package/dist/validate.js.map +1 -1
  25. package/dist/view/assets/index-D8qr-jiw.css +1 -0
  26. package/dist/view/assets/index-DI0VS0HQ.js +205 -0
  27. package/dist/view/index.html +2 -2
  28. package/dist/{view/assets/index-CvyHnPjR.css → view-export/assets/bundle.css} +1 -1
  29. package/dist/{view/assets/index-Du05xzao.js → view-export/assets/bundle.js} +44 -49
  30. package/dist/view-export/index.html +24 -0
  31. package/package.json +3 -2
  32. package/templates/.claude/skills/loom-spec/SKILL.md +91 -76
  33. package/templates/.loom/README.md +1 -1
  34. package/dist/cli/importTrace.d.ts +0 -15
  35. package/dist/cli/importTrace.js +0 -188
  36. package/dist/cli/importTrace.js.map +0 -1
  37. package/dist/server/otel.d.ts +0 -32
  38. package/dist/server/otel.js +0 -98
  39. package/dist/server/otel.js.map +0 -1
  40. package/dist/types/timeline.d.ts +0 -97
  41. package/dist/types/timeline.js +0 -7
  42. package/dist/types/timeline.js.map +0 -1
  43. package/dist/view/assets/TimelineView-DEfpV9mL.js +0 -16
  44. package/schema/timeline.schema.json +0 -135
@@ -0,0 +1,22 @@
1
+ export interface ExportHtmlArgs {
2
+ /** Output file path. Default: "loom.html" in cwd. */
3
+ out: string;
4
+ /** Working directory root (walked up to find .loom/). */
5
+ root: string;
6
+ /** If set, only this diagram. */
7
+ diagram?: string;
8
+ /** Only export nodes carrying at least one of these tags. */
9
+ includeTags?: string[];
10
+ /** Drop nodes carrying any of these tags. */
11
+ excludeTags?: string[];
12
+ /** Named bundle from .loom/exports.json. Settings from the bundle become
13
+ * defaults; explicit CLI flags override. */
14
+ bundle?: string;
15
+ }
16
+ /**
17
+ * Used by the bundle-merge logic to tell "user passed --out" from "we are
18
+ * using the default". Exported so the CLI dispatcher can default-init the
19
+ * field once and stay consistent with this module's notion of "default".
20
+ */
21
+ export declare const DEFAULT_OUT = "loom.html";
22
+ export declare function runExportHtml(args: ExportHtmlArgs): Promise<void>;
@@ -0,0 +1,182 @@
1
+ import { readFile, writeFile, stat } from "node:fs/promises";
2
+ import { resolve, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { findLoomRoot } from "../server/findLoomRoot.js";
5
+ import { listDiagrams, readDiagram, readNodeTypes, } from "../server/fileOps.js";
6
+ import { applyFilter } from "../server/exportFilter.js";
7
+ import { loadExportsConfig } from "../server/exportConfig.js";
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ async function fileExists(p) {
10
+ try {
11
+ await stat(p);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ /**
19
+ * Locate the export-mode view bundle. Tries both layouts:
20
+ * - Production: alongside cli/ in dist/ → ../view-export/
21
+ * - Dev (tsx running from src/cli/): ../../dist/view-export/
22
+ */
23
+ async function findExportBundle() {
24
+ const candidates = [
25
+ resolve(here, "../view-export"),
26
+ resolve(here, "../../dist/view-export"),
27
+ ];
28
+ for (const c of candidates) {
29
+ if (await fileExists(resolve(c, "index.html")))
30
+ return c;
31
+ }
32
+ throw new Error(`Could not find the export view bundle. Looked in:\n - ${candidates.join("\n - ")}\n` +
33
+ `Did you run 'pnpm build' (or 'pnpm build:export') to produce dist/view-export?`);
34
+ }
35
+ /**
36
+ * Used by the bundle-merge logic to tell "user passed --out" from "we are
37
+ * using the default". Exported so the CLI dispatcher can default-init the
38
+ * field once and stay consistent with this module's notion of "default".
39
+ */
40
+ export const DEFAULT_OUT = "loom.html";
41
+ async function loadAllData(args) {
42
+ const loomRoot = await findLoomRoot(args.root);
43
+ const nodeTypes = await readNodeTypes(loomRoot.loomPath);
44
+ // Diagrams — either one specific or all
45
+ const diagrams = {};
46
+ if (args.diagram) {
47
+ diagrams[args.diagram] = await readDiagram(loomRoot.loomPath, args.diagram);
48
+ }
49
+ else {
50
+ const summaries = await listDiagrams(loomRoot.loomPath);
51
+ for (const s of summaries) {
52
+ diagrams[s.id] = await readDiagram(loomRoot.loomPath, s.id);
53
+ }
54
+ }
55
+ return {
56
+ generatedAt: new Date().toISOString(),
57
+ diagrams,
58
+ nodeTypes,
59
+ };
60
+ }
61
+ /**
62
+ * Inline the view bundle (HTML + CSS + JS) and the exported data into a
63
+ * single self-contained HTML string.
64
+ *
65
+ * The export bundle's index.html looks roughly like:
66
+ * <html><head><link rel="stylesheet" href="/assets/bundle.css">
67
+ * <script type="module" crossorigin src="/assets/bundle.js"></script>
68
+ * </head><body><div id="root"></div></body></html>
69
+ *
70
+ * We rewrite the link/script tags to inline contents and inject a
71
+ * <script>window.__LOOM_DATA__ = {...}</script> before the bundle.
72
+ */
73
+ function buildHtml(bundleHtml, bundleCss, bundleJs, data, meta) {
74
+ // Sanitize the JSON for inlining inside a <script> tag.
75
+ // Replace </script> sequences and <!-- inside string values so they can't
76
+ // close the surrounding script element or confuse parsers.
77
+ const safeJson = JSON.stringify(data)
78
+ .replace(/<\/script/gi, "<\\/script")
79
+ .replace(/<!--/g, "<\\!--");
80
+ const inlineDataTag = `<script>window.__LOOM_DATA__ = ${safeJson};</script>`;
81
+ const inlineCssTag = `<style>${bundleCss}</style>`;
82
+ const inlineJsTag = `<script type="module">${bundleJs}</script>`;
83
+ let out = bundleHtml;
84
+ // CRITICAL: pass replacement as a function rather than a string. With a
85
+ // string second arg, JS treats `$$` as a literal `$`, which mangles any
86
+ // `$$typeof` / `$&` / `$'` in the JS bundle (React's reconciler uses
87
+ // `$$typeof` heavily). Function form bypasses the pattern processing.
88
+ out = out.replace(/<link\s+rel=["']stylesheet["'][^>]*>/i, () => inlineCssTag);
89
+ out = out.replace(/<script\s+type=["']module["'][^>]*><\/script>/i, () => inlineDataTag + inlineJsTag);
90
+ out = out.replace(/<title>[^<]*<\/title>/i, () => `<title>loom-spec export · ${escapeHtml(meta.sourceRoot)}</title>`);
91
+ return out;
92
+ }
93
+ function escapeHtml(s) {
94
+ return s
95
+ .replace(/&/g, "&amp;")
96
+ .replace(/</g, "&lt;")
97
+ .replace(/>/g, "&gt;")
98
+ .replace(/"/g, "&quot;");
99
+ }
100
+ export async function runExportHtml(args) {
101
+ // If a bundle name was given, resolve its settings from .loom/exports.json
102
+ // first. Explicit CLI args take precedence (for ad-hoc overrides).
103
+ let effective = args;
104
+ if (args.bundle) {
105
+ const loomRoot = await findLoomRoot(args.root);
106
+ const config = await loadExportsConfig(loomRoot.loomPath);
107
+ if (!config) {
108
+ console.error(`export-html: no .loom/exports.json found, but '${args.bundle}' looks like a named bundle.\n` +
109
+ ` Create .loom/exports.json with an 'exports.${args.bundle}' entry, or pass ad-hoc flags instead.`);
110
+ process.exit(1);
111
+ }
112
+ const bundle = config.exports[args.bundle];
113
+ if (!bundle) {
114
+ const available = Object.keys(config.exports).sort().join(", ") || "(none)";
115
+ console.error(`export-html: bundle '${args.bundle}' not found in .loom/exports.json. ` +
116
+ `Available: ${available}.`);
117
+ process.exit(1);
118
+ }
119
+ // Merge: explicit CLI args (in `args`) override config-supplied values.
120
+ effective = {
121
+ ...args,
122
+ out: args.out !== DEFAULT_OUT ? args.out : bundle.out ?? args.out,
123
+ diagram: args.diagram ?? bundle.diagram,
124
+ includeTags: args.includeTags ?? bundle.includeTags,
125
+ excludeTags: args.excludeTags ?? bundle.excludeTags,
126
+ };
127
+ }
128
+ const bundleDir = await findExportBundle();
129
+ const [bundleHtml, bundleCss, bundleJs] = await Promise.all([
130
+ readFile(resolve(bundleDir, "index.html"), "utf8"),
131
+ readFile(resolve(bundleDir, "assets/bundle.css"), "utf8"),
132
+ readFile(resolve(bundleDir, "assets/bundle.js"), "utf8"),
133
+ ]);
134
+ const data = await loadAllData(effective);
135
+ if (Object.keys(data.diagrams).length === 0) {
136
+ console.error("export-html: no diagrams found — refusing to write an empty export.");
137
+ process.exit(1);
138
+ }
139
+ const filterSpec = {
140
+ includeTags: effective.includeTags,
141
+ excludeTags: effective.excludeTags,
142
+ };
143
+ const { payload: filtered, summary } = applyFilter(data, filterSpec);
144
+ // After filtering, if everything's gone, bail. Better to fail loud than
145
+ // silently emit a blank HTML the user will wonder about.
146
+ const survivingNodes = Object.values(filtered.diagrams).reduce((n, d) => n + d.nodes.length, 0);
147
+ if (survivingNodes === 0) {
148
+ console.error("export-html: filter matched 0 nodes — refusing to write an empty export.");
149
+ if (filterSpec.includeTags?.length || filterSpec.excludeTags?.length) {
150
+ console.error(` filter was: include=[${(filterSpec.includeTags ?? []).join(", ")}], ` +
151
+ `exclude=[${(filterSpec.excludeTags ?? []).join(", ")}]`);
152
+ }
153
+ process.exit(1);
154
+ }
155
+ const finalData = {
156
+ ...data,
157
+ diagrams: filtered.diagrams,
158
+ };
159
+ const html = buildHtml(bundleHtml, bundleCss, bundleJs, finalData, {
160
+ sourceRoot: effective.root,
161
+ });
162
+ const outPath = resolve(effective.out);
163
+ await writeFile(outPath, html, "utf8");
164
+ const sizeKb = Math.round(Buffer.byteLength(html, "utf8") / 1024);
165
+ const diagramCount = Object.keys(finalData.diagrams).length;
166
+ console.log(`Wrote ${outPath} (${sizeKb} kB): ` +
167
+ `${diagramCount} diagram${diagramCount === 1 ? "" : "s"}.`);
168
+ const droppedParts = [];
169
+ if (summary.nodesDropped > 0)
170
+ droppedParts.push(`${summary.nodesDropped} nodes`);
171
+ if (summary.edgesDropped > 0)
172
+ droppedParts.push(`${summary.edgesDropped} edges`);
173
+ if (summary.groupsDropped > 0)
174
+ droppedParts.push(`${summary.groupsDropped} groups`);
175
+ if (summary.drillDownsCleared > 0)
176
+ droppedParts.push(`${summary.drillDownsCleared} drill-down refs`);
177
+ if (droppedParts.length > 0) {
178
+ console.log(`Filter dropped: ${droppedParts.join(", ")}.`);
179
+ }
180
+ console.log(`Open it directly in a browser, or drop it into any docs / wiki / GitHub-Pages site.`);
181
+ }
182
+ //# sourceMappingURL=exportHtml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exportHtml.js","sourceRoot":"","sources":["../../src/cli/exportHtml.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -4,7 +4,7 @@ import { runView } from "./view.js";
4
4
  import { runValidate } from "./validate.js";
5
5
  import { runMcp } from "./mcp.js";
6
6
  import { runInstallMcp } from "./installMcp.js";
7
- import { runImportTrace } from "./importTrace.js";
7
+ import { runExportHtml, DEFAULT_OUT as EXPORT_DEFAULT_OUT } from "./exportHtml.js";
8
8
  const HELP = `loom-spec — node-based architecture spec for your repo
9
9
 
10
10
  Usage:
@@ -28,24 +28,32 @@ Usage:
28
28
  pre-commit hook.
29
29
 
30
30
  loom-spec mcp [--root <dir>]
31
- Start a Model Context Protocol server on stdio. Exposes 15 tools
32
- for diagrams (loom_list_diagrams, loom_add_node, loom_add_edge, …)
33
- and timelines (loom_list_timelines, loom_add_event, …) wire it
34
- into Claude Code's mcp.json (or any MCP-capable client).
31
+ Start a Model Context Protocol server on stdio. Exposes 10 tools
32
+ for editing diagrams (loom_list_diagrams, loom_add_node,
33
+ loom_add_edge, loom_validate, …). Wire it into Claude Code's
34
+ mcp.json (or any MCP-capable client).
35
35
 
36
- loom-spec import-trace <trace.json> --as <timeline-id> --diagram <diagram-id>
37
- [--map <mapping.json>] [--append] [--root <dir>]
38
- Read an OpenTelemetry OTLP-JSON trace and generate a timeline that
39
- mirrors the actual spans on the named diagram. Each span becomes
40
- an event; parent/child relationships preserve as triggered_by;
41
- service.name becomes the track. Spans whose service or name can't
42
- be matched to a node are skipped pass --map to override.
36
+ loom-spec export-html [<bundle-name>] [--out <path>] [--diagram <id>]
37
+ [--include-tag <comma-list>] [--exclude-tag <comma-list>]
38
+ [--root <dir>]
39
+ Build a standalone interactive HTML file from the spec pan/zoom,
40
+ drill-down, switch diagrams. Single self-contained file, no server
41
+ needed. Drop it into a manual, wiki, GitHub Pages site, anywhere.
42
+ Output defaults to ./loom.html. With --diagram, only that diagram
43
+ ships. With --include-tag / --exclude-tag, only nodes whose 'tags'
44
+ match survive — edges, groups, and drill-down chevrons that point
45
+ at dropped nodes are cleaned up automatically.
46
+
47
+ Pass a <bundle-name> as the first positional arg to read settings
48
+ from a named bundle in .loom/exports.json. Explicit flags override
49
+ the bundle's values.
43
50
 
44
51
  loom-spec --help
45
52
  Print this help.
46
53
  `;
47
54
  function parseFlags(argv) {
48
55
  const flags = {};
56
+ const positional = [];
49
57
  for (let i = 0; i < argv.length; i++) {
50
58
  const a = argv[i];
51
59
  if (!a)
@@ -61,12 +69,15 @@ function parseFlags(argv) {
61
69
  flags[key] = true;
62
70
  }
63
71
  }
72
+ else {
73
+ positional.push(a);
74
+ }
64
75
  }
65
- return flags;
76
+ return { flags, positional };
66
77
  }
67
78
  async function main() {
68
79
  const [, , subcommand, ...rest] = process.argv;
69
- const flags = parseFlags(rest);
80
+ const { flags, positional } = parseFlags(rest);
70
81
  if (!subcommand || subcommand === "--help" || subcommand === "-h") {
71
82
  console.log(HELP);
72
83
  return;
@@ -106,27 +117,22 @@ async function main() {
106
117
  });
107
118
  return;
108
119
  }
109
- if (subcommand === "import-trace") {
110
- // Positional arg: the trace file path (first non-flag in rest).
111
- const trace = rest.find((a) => a && !a.startsWith("--"));
112
- if (!trace) {
113
- console.error("import-trace: missing trace file path");
114
- console.log(HELP);
115
- process.exit(1);
116
- }
117
- const asId = flags.as;
118
- const diagramId = flags.diagram;
119
- if (!asId || !diagramId) {
120
- console.error("import-trace: --as <timeline-id> and --diagram <diagram-id> are required");
121
- process.exit(1);
122
- }
123
- await runImportTrace({
124
- trace,
125
- asId,
126
- diagramId,
127
- map: typeof flags.map === "string" ? flags.map : undefined,
128
- append: Boolean(flags.append),
120
+ if (subcommand === "export-html") {
121
+ const splitTags = (v) => {
122
+ if (typeof v !== "string" || !v.trim())
123
+ return undefined;
124
+ return v.split(",").map((s) => s.trim()).filter(Boolean);
125
+ };
126
+ // First positional arg, if any, is a named-bundle key from
127
+ // .loom/exports.json (e.g. `loom-spec export-html user-manual`).
128
+ const bundle = positional[0];
129
+ await runExportHtml({
130
+ out: flags.out ?? EXPORT_DEFAULT_OUT,
129
131
  root: flags.root ?? process.cwd(),
132
+ diagram: typeof flags.diagram === "string" ? flags.diagram : undefined,
133
+ includeTags: splitTags(flags["include-tag"]),
134
+ excludeTags: splitTags(flags["exclude-tag"]),
135
+ bundle,
130
136
  });
131
137
  return;
132
138
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from "zod";
4
- import { listDiagrams, readDiagram, writeDiagram, readNodeTypes, listTimelines, readTimeline, writeTimeline, } from "../server/fileOps.js";
5
- import { validateDiagram, validateTimeline } from "../validate.js";
4
+ import { listDiagrams, readDiagram, writeDiagram, readNodeTypes, } from "../server/fileOps.js";
5
+ import { validateDiagram } from "../validate.js";
6
6
  import { runDriftCheck } from "../server/drift.js";
7
7
  const STATUSES = ["planned", "implemented", "stale", "deprecated"];
8
8
  const EDGE_KINDS = [
@@ -14,7 +14,6 @@ const EDGE_KINDS = [
14
14
  "dependency",
15
15
  "control",
16
16
  ];
17
- const EVENT_KINDS = ["compute", "io", "wait", "error"];
18
17
  const codeRefSchema = z.object({
19
18
  path: z.string(),
20
19
  symbol: z.string().optional(),
@@ -34,13 +33,6 @@ function uniqueEdgeId(d) {
34
33
  i++;
35
34
  return `e${i}`;
36
35
  }
37
- function uniqueEventId(tl) {
38
- const ids = new Set(tl.events.map((e) => e.id));
39
- let i = tl.events.length + 1;
40
- while (ids.has(`ev${i}`))
41
- i++;
42
- return `ev${i}`;
43
- }
44
36
  function jsonText(obj) {
45
37
  return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
46
38
  }
@@ -68,28 +60,8 @@ async function persist(loomPath, id, d) {
68
60
  await writeDiagram(loomPath, id, d);
69
61
  return null;
70
62
  }
71
- async function readTimelineOrError(loomPath, id) {
72
- try {
73
- return { ok: true, timeline: await readTimeline(loomPath, id) };
74
- }
75
- catch (e) {
76
- const code = e.code;
77
- if (code === "ENOENT") {
78
- return { ok: false, error: errorText(`timeline '${id}' not found`) };
79
- }
80
- return { ok: false, error: errorText(`read failed: ${e.message}`) };
81
- }
82
- }
83
- async function persistTimeline(loomPath, id, tl) {
84
- const result = await validateTimeline(tl);
85
- if (!result.ok) {
86
- return errorText("Validation failed:", result.errors);
87
- }
88
- await writeTimeline(loomPath, id, tl);
89
- return null;
90
- }
91
63
  export function createMcpServer(loomRoot) {
92
- const server = new McpServer({ name: "loom-spec", version: "0.0.1" }, { instructions: `Use these tools to maintain the architecture spec in ${loomRoot.loomPath}. Diagram tools (loom_*_node, loom_*_edge) edit .loom/diagrams/*.flow.json; timeline tools (loom_*_event) edit .loom/timelines/*.timeline.json. Prefer them over editing the JSON files directly they validate against the schema before writing.` });
64
+ const server = new McpServer({ name: "loom-spec", version: "0.0.1" }, { instructions: `Use these tools to maintain the architecture spec in ${loomRoot.loomPath}. They edit .loom/diagrams/*.flow.json prefer them over editing the JSON files directly because they validate against the schema before writing.` });
93
65
  server.registerTool("loom_list_diagrams", {
94
66
  title: "List diagrams",
95
67
  description: "List all diagrams in the spec with title and node/edge counts.",
@@ -303,182 +275,6 @@ export function createMcpServer(loomRoot) {
303
275
  return err;
304
276
  return jsonText({ ok: true });
305
277
  });
306
- // ─── Timelines ───────────────────────────────────────────────────────
307
- server.registerTool("loom_list_timelines", {
308
- title: "List timelines",
309
- description: "List all timelines in the spec with title, referenced diagram, event count, and total duration in ms.",
310
- inputSchema: {},
311
- }, async () => {
312
- const summaries = await listTimelines(loomRoot.loomPath);
313
- return jsonText(summaries);
314
- });
315
- server.registerTool("loom_read_timeline", {
316
- title: "Read timeline",
317
- description: "Read a specific timeline's full JSON by id.",
318
- inputSchema: { id: z.string().describe("Timeline id (e.g. 'todo-completion')") },
319
- }, async ({ id }) => {
320
- const r = await readTimelineOrError(loomRoot.loomPath, id);
321
- if (!r.ok)
322
- return r.error;
323
- return jsonText(r.timeline);
324
- });
325
- server.registerTool("loom_add_event", {
326
- title: "Add timeline event",
327
- description: "Append a new event (clip) to a timeline. The referenced node must exist in the timeline's diagram. Returns the auto-generated event id.",
328
- inputSchema: {
329
- timeline: z.string().describe("Timeline id"),
330
- node: z
331
- .string()
332
- .describe("Node id from the timeline's diagram. The event lights up this node when the playhead enters its interval."),
333
- start_ms: z.number().min(0).describe("Start time in ms from t=0"),
334
- duration_ms: z
335
- .number()
336
- .min(0)
337
- .describe("Duration in ms (may be 0 for instantaneous events)"),
338
- track: z
339
- .string()
340
- .optional()
341
- .describe("Track to render this event on. Omit to auto-assign one track per node."),
342
- label: z.string().max(60).optional().describe("Short text shown inside the clip"),
343
- description: z.string().optional(),
344
- kind: z.enum(EVENT_KINDS).optional().describe("compute | io | wait | error"),
345
- code_refs: z.array(codeRefSchema).optional(),
346
- triggered_by: z
347
- .string()
348
- .optional()
349
- .describe("Id of another event in this timeline that caused this one"),
350
- tags: z.array(z.string()).optional(),
351
- id: z
352
- .string()
353
- .optional()
354
- .describe("Optional explicit id (lowercase kebab). Defaults to ev<n>."),
355
- },
356
- }, async ({ timeline, node, start_ms, duration_ms, track, label, description, kind, code_refs, triggered_by, tags, id, }) => {
357
- const tlRes = await readTimelineOrError(loomRoot.loomPath, timeline);
358
- if (!tlRes.ok)
359
- return tlRes.error;
360
- const tl = tlRes.timeline;
361
- // Verify the referenced node exists in the underlying diagram.
362
- const dRes = await readDiagramOrError(loomRoot.loomPath, tl.diagram);
363
- if (!dRes.ok) {
364
- return errorText(`timeline '${timeline}' references diagram '${tl.diagram}', which could not be read`);
365
- }
366
- if (!dRes.diagram.nodes.some((n) => n.id === node)) {
367
- return errorText(`node '${node}' does not exist in diagram '${tl.diagram}'. Available: ${dRes.diagram.nodes
368
- .map((n) => n.id)
369
- .join(", ")}`);
370
- }
371
- // Verify triggered_by, if set, references an existing event.
372
- if (triggered_by && !tl.events.some((e) => e.id === triggered_by)) {
373
- return errorText(`triggered_by event '${triggered_by}' not found in timeline`);
374
- }
375
- const newId = id ?? uniqueEventId(tl);
376
- if (tl.events.some((e) => e.id === newId)) {
377
- return errorText(`event with id '${newId}' already exists`);
378
- }
379
- const event = {
380
- id: newId,
381
- node,
382
- start_ms,
383
- duration_ms,
384
- track,
385
- label,
386
- description,
387
- kind,
388
- code_refs,
389
- triggered_by,
390
- tags,
391
- };
392
- tl.events.push(event);
393
- const err = await persistTimeline(loomRoot.loomPath, timeline, tl);
394
- if (err)
395
- return err;
396
- return jsonText({ ok: true, id: newId });
397
- });
398
- server.registerTool("loom_update_event", {
399
- title: "Update timeline event",
400
- description: "Patch fields on an existing event. Only the fields you pass are changed. If you change 'node', the new node must exist in the timeline's diagram.",
401
- inputSchema: {
402
- timeline: z.string(),
403
- id: z.string().describe("Event id"),
404
- patch: z
405
- .object({
406
- node: z.string().optional(),
407
- start_ms: z.number().min(0).optional(),
408
- duration_ms: z.number().min(0).optional(),
409
- track: z.string().nullable().optional(),
410
- label: z.string().max(60).nullable().optional(),
411
- description: z.string().nullable().optional(),
412
- kind: z.enum(EVENT_KINDS).nullable().optional(),
413
- code_refs: z.array(codeRefSchema).optional(),
414
- triggered_by: z.string().nullable().optional(),
415
- tags: z.array(z.string()).optional(),
416
- })
417
- .describe("Fields to merge. Pass null to clear an optional field."),
418
- },
419
- }, async ({ timeline, id, patch }) => {
420
- const tlRes = await readTimelineOrError(loomRoot.loomPath, timeline);
421
- if (!tlRes.ok)
422
- return tlRes.error;
423
- const tl = tlRes.timeline;
424
- const idx = tl.events.findIndex((e) => e.id === id);
425
- if (idx < 0)
426
- return errorText(`event '${id}' not found in timeline '${timeline}'`);
427
- // If node is being changed, verify the new node exists in the diagram.
428
- if (patch.node !== undefined && patch.node !== tl.events[idx].node) {
429
- const dRes = await readDiagramOrError(loomRoot.loomPath, tl.diagram);
430
- if (!dRes.ok)
431
- return dRes.error;
432
- if (!dRes.diagram.nodes.some((n) => n.id === patch.node)) {
433
- return errorText(`node '${patch.node}' does not exist in diagram '${tl.diagram}'`);
434
- }
435
- }
436
- // If triggered_by is being set (non-null), verify the target exists.
437
- if (patch.triggered_by && !tl.events.some((e) => e.id === patch.triggered_by)) {
438
- return errorText(`triggered_by event '${patch.triggered_by}' not found`);
439
- }
440
- const merged = { ...tl.events[idx] };
441
- for (const [k, v] of Object.entries(patch)) {
442
- if (v === undefined)
443
- continue;
444
- if (v === null) {
445
- // Clear the optional field.
446
- delete merged[k];
447
- }
448
- else {
449
- merged[k] = v;
450
- }
451
- }
452
- tl.events[idx] = merged;
453
- const err = await persistTimeline(loomRoot.loomPath, timeline, tl);
454
- if (err)
455
- return err;
456
- return jsonText({ ok: true });
457
- });
458
- server.registerTool("loom_delete_event", {
459
- title: "Delete timeline event",
460
- description: "Remove an event by id. Also drops any triggered_by references pointing at it so the timeline stays internally consistent.",
461
- inputSchema: {
462
- timeline: z.string(),
463
- id: z.string(),
464
- },
465
- }, async ({ timeline, id }) => {
466
- const tlRes = await readTimelineOrError(loomRoot.loomPath, timeline);
467
- if (!tlRes.ok)
468
- return tlRes.error;
469
- const tl = tlRes.timeline;
470
- const before = tl.events.length;
471
- tl.events = tl.events.filter((e) => e.id !== id);
472
- if (tl.events.length === before) {
473
- return errorText(`event '${id}' not found in timeline '${timeline}'`);
474
- }
475
- // Scrub dangling triggered_by refs.
476
- tl.events = tl.events.map((e) => e.triggered_by === id ? { ...e, triggered_by: undefined } : e);
477
- const err = await persistTimeline(loomRoot.loomPath, timeline, tl);
478
- if (err)
479
- return err;
480
- return jsonText({ ok: true });
481
- });
482
278
  server.registerTool("loom_validate", {
483
279
  title: "Validate spec",
484
280
  description: "Run the full drift + schema check on every diagram. Reports schema errors, missing code-ref files, and missing symbols.",