@hugobatist/smartcode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +292 -0
  3. package/dist/cli.js +4324 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/index.d.ts +374 -0
  6. package/dist/index.js +1167 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/static/annotations-panel.js +133 -0
  9. package/dist/static/annotations-svg.js +108 -0
  10. package/dist/static/annotations.css +367 -0
  11. package/dist/static/annotations.js +367 -0
  12. package/dist/static/app-init.js +497 -0
  13. package/dist/static/breakpoints.css +69 -0
  14. package/dist/static/breakpoints.js +197 -0
  15. package/dist/static/clipboard.js +94 -0
  16. package/dist/static/collapse-ui.js +325 -0
  17. package/dist/static/command-history.js +89 -0
  18. package/dist/static/context-menu.js +334 -0
  19. package/dist/static/custom-renderer.js +201 -0
  20. package/dist/static/dagre-layout.js +291 -0
  21. package/dist/static/diagram-dom.js +241 -0
  22. package/dist/static/diagram-editor.js +368 -0
  23. package/dist/static/editor-panel.js +107 -0
  24. package/dist/static/editor-popovers.js +187 -0
  25. package/dist/static/event-bus.js +57 -0
  26. package/dist/static/export.js +181 -0
  27. package/dist/static/file-tree.js +470 -0
  28. package/dist/static/ghost-paths.js +397 -0
  29. package/dist/static/heatmap.css +116 -0
  30. package/dist/static/heatmap.js +308 -0
  31. package/dist/static/icons.js +66 -0
  32. package/dist/static/inline-edit.js +294 -0
  33. package/dist/static/interaction-state.js +155 -0
  34. package/dist/static/interaction-tracker.js +93 -0
  35. package/dist/static/live.html +239 -0
  36. package/dist/static/main-layout.css +220 -0
  37. package/dist/static/main.css +334 -0
  38. package/dist/static/mcp-sessions.js +202 -0
  39. package/dist/static/modal.css +109 -0
  40. package/dist/static/modal.js +171 -0
  41. package/dist/static/node-drag.js +293 -0
  42. package/dist/static/pan-zoom.js +199 -0
  43. package/dist/static/renderer.js +280 -0
  44. package/dist/static/search.css +103 -0
  45. package/dist/static/search.js +304 -0
  46. package/dist/static/selection.js +353 -0
  47. package/dist/static/session-player.css +137 -0
  48. package/dist/static/session-player.js +411 -0
  49. package/dist/static/sidebar.css +248 -0
  50. package/dist/static/svg-renderer.js +313 -0
  51. package/dist/static/svg-shapes.js +218 -0
  52. package/dist/static/tokens.css +76 -0
  53. package/dist/static/vendor/dagre-bundle.js +43 -0
  54. package/dist/static/vendor/dagre.min.js +3 -0
  55. package/dist/static/vendor/graphlib.min.js +2 -0
  56. package/dist/static/viewport-transform.js +107 -0
  57. package/dist/static/workspace-switcher.js +202 -0
  58. package/dist/static/ws-client.js +71 -0
  59. package/dist/static/ws-handler.js +125 -0
  60. package/package.json +74 -0
package/dist/cli.js ADDED
@@ -0,0 +1,4324 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/utils/version.ts
13
+ import { readFileSync } from "fs";
14
+ import { join, dirname } from "path";
15
+ import { fileURLToPath } from "url";
16
+ function getVersion() {
17
+ if (cachedVersion) return cachedVersion;
18
+ const startDir = dirname(fileURLToPath(import.meta.url));
19
+ let dir = startDir;
20
+ for (let i = 0; i < 5; i++) {
21
+ try {
22
+ const pkgPath = join(dir, "package.json");
23
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
24
+ if (pkg.version) {
25
+ cachedVersion = pkg.version;
26
+ return cachedVersion;
27
+ }
28
+ } catch {
29
+ }
30
+ dir = dirname(dir);
31
+ }
32
+ cachedVersion = "0.0.0";
33
+ return cachedVersion;
34
+ }
35
+ var cachedVersion;
36
+ var init_version = __esm({
37
+ "src/utils/version.ts"() {
38
+ "use strict";
39
+ }
40
+ });
41
+
42
+ // src/utils/logger.ts
43
+ import pc from "picocolors";
44
+ var log;
45
+ var init_logger = __esm({
46
+ "src/utils/logger.ts"() {
47
+ "use strict";
48
+ log = {
49
+ info: (...args) => console.error(pc.blue("[smartcode]"), ...args),
50
+ warn: (...args) => console.error(pc.yellow("[smartcode]"), ...args),
51
+ error: (...args) => console.error(pc.red("[smartcode]"), ...args),
52
+ debug: (...args) => {
53
+ if (process.env["DEBUG"]) console.error(pc.dim("[smartcode]"), ...args);
54
+ }
55
+ };
56
+ }
57
+ });
58
+
59
+ // src/diagram/annotations.ts
60
+ function isAnnotationStart(line) {
61
+ return line === ANNOTATION_START || line === ANNOTATION_START_LEGACY;
62
+ }
63
+ function parseAllAnnotations(content) {
64
+ const flags = /* @__PURE__ */ new Map();
65
+ const statuses = /* @__PURE__ */ new Map();
66
+ const breakpoints = /* @__PURE__ */ new Set();
67
+ const risks = /* @__PURE__ */ new Map();
68
+ const ghosts = [];
69
+ const lines = content.split("\n");
70
+ let inBlock = false;
71
+ for (const line of lines) {
72
+ const trimmed = line.trim();
73
+ if (isAnnotationStart(trimmed)) {
74
+ inBlock = true;
75
+ continue;
76
+ }
77
+ if (trimmed === ANNOTATION_END) {
78
+ inBlock = false;
79
+ continue;
80
+ }
81
+ if (!inBlock || trimmed === "") continue;
82
+ let match = FLAG_REGEX.exec(trimmed);
83
+ if (match) {
84
+ flags.set(match[1], { nodeId: match[1], message: match[2] });
85
+ continue;
86
+ }
87
+ match = STATUS_REGEX.exec(trimmed);
88
+ if (match) {
89
+ const statusValue = match[2];
90
+ if (VALID_STATUSES.includes(statusValue)) {
91
+ statuses.set(match[1], statusValue);
92
+ } else {
93
+ log.debug(`Skipping invalid status value: ${statusValue}`);
94
+ }
95
+ continue;
96
+ }
97
+ match = BREAKPOINT_REGEX.exec(trimmed);
98
+ if (match) {
99
+ breakpoints.add(match[1]);
100
+ continue;
101
+ }
102
+ match = RISK_REGEX.exec(trimmed);
103
+ if (match) {
104
+ risks.set(match[1], { nodeId: match[1], level: match[2], reason: match[3] });
105
+ continue;
106
+ }
107
+ match = GHOST_REGEX.exec(trimmed);
108
+ if (match) {
109
+ ghosts.push({ fromNodeId: match[1], toNodeId: match[2], label: match[3] });
110
+ continue;
111
+ }
112
+ log.debug(`Skipping unrecognized annotation line: ${trimmed}`);
113
+ }
114
+ return { flags, statuses, breakpoints, risks, ghosts };
115
+ }
116
+ function parseFlags(content) {
117
+ return parseAllAnnotations(content).flags;
118
+ }
119
+ function parseStatuses(content) {
120
+ return parseAllAnnotations(content).statuses;
121
+ }
122
+ function stripAnnotations(content) {
123
+ const lines = content.split("\n");
124
+ const result = [];
125
+ let inBlock = false;
126
+ for (const line of lines) {
127
+ const trimmed = line.trim();
128
+ if (isAnnotationStart(trimmed)) {
129
+ inBlock = true;
130
+ continue;
131
+ }
132
+ if (trimmed === ANNOTATION_END) {
133
+ inBlock = false;
134
+ continue;
135
+ }
136
+ if (!inBlock) {
137
+ result.push(line);
138
+ }
139
+ }
140
+ while (result.length > 0 && result[result.length - 1].trim() === "") {
141
+ result.pop();
142
+ }
143
+ return result.join("\n") + "\n";
144
+ }
145
+ function injectAnnotations(content, flags, statuses, breakpoints, risks, ghosts) {
146
+ const clean = stripAnnotations(content);
147
+ const hasFlags = flags.size > 0;
148
+ const hasStatuses = statuses !== void 0 && statuses.size > 0;
149
+ const hasBreakpoints = breakpoints !== void 0 && breakpoints.size > 0;
150
+ const hasRisks = risks !== void 0 && risks.size > 0;
151
+ const hasGhosts = ghosts !== void 0 && ghosts.length > 0;
152
+ if (!hasFlags && !hasStatuses && !hasBreakpoints && !hasRisks && !hasGhosts) {
153
+ return clean;
154
+ }
155
+ const lines = [
156
+ "",
157
+ ANNOTATION_START
158
+ ];
159
+ for (const [nodeId, flag] of flags) {
160
+ const escapedMessage = flag.message.replace(/"/g, "''");
161
+ lines.push(`%% @flag ${nodeId} "${escapedMessage}"`);
162
+ }
163
+ if (hasStatuses) {
164
+ for (const [nodeId, status] of statuses) {
165
+ lines.push(`%% @status ${nodeId} ${status}`);
166
+ }
167
+ }
168
+ if (hasBreakpoints) {
169
+ for (const nodeId of breakpoints) {
170
+ lines.push(`%% @breakpoint ${nodeId}`);
171
+ }
172
+ }
173
+ if (hasRisks) {
174
+ for (const [nodeId, risk] of risks) {
175
+ const escapedReason = risk.reason.replace(/"/g, "''");
176
+ lines.push(`%% @risk ${nodeId} ${risk.level} "${escapedReason}"`);
177
+ }
178
+ }
179
+ if (hasGhosts) {
180
+ for (const ghost of ghosts) {
181
+ const escapedLabel = ghost.label.replace(/"/g, "''");
182
+ lines.push(`%% @ghost ${ghost.fromNodeId} ${ghost.toNodeId} "${escapedLabel}"`);
183
+ }
184
+ }
185
+ lines.push(ANNOTATION_END);
186
+ lines.push("");
187
+ return clean.trimEnd() + "\n" + lines.join("\n");
188
+ }
189
+ var ANNOTATION_START, ANNOTATION_START_LEGACY, ANNOTATION_END, FLAG_REGEX, STATUS_REGEX, BREAKPOINT_REGEX, RISK_REGEX, GHOST_REGEX, VALID_STATUSES;
190
+ var init_annotations = __esm({
191
+ "src/diagram/annotations.ts"() {
192
+ "use strict";
193
+ init_logger();
194
+ ANNOTATION_START = "%% --- ANNOTATIONS (auto-managed by SmartCode) ---";
195
+ ANNOTATION_START_LEGACY = "%% --- ANNOTATIONS (auto-managed by SmartB Diagrams) ---";
196
+ ANNOTATION_END = "%% --- END ANNOTATIONS ---";
197
+ FLAG_REGEX = /^%%\s*@flag\s+(\S+)\s+"([^"]*)"$/;
198
+ STATUS_REGEX = /^%%\s*@status\s+(\S+)\s+(\S+)$/;
199
+ BREAKPOINT_REGEX = /^%%\s*@breakpoint\s+(\S+)$/;
200
+ RISK_REGEX = /^%%\s*@risk\s+(\S+)\s+(high|medium|low)\s+"([^"]*)"$/;
201
+ GHOST_REGEX = /^%%\s*@ghost\s+(\S+)\s+(\S+)\s+"([^"]*)"$/;
202
+ VALID_STATUSES = ["ok", "problem", "in-progress", "discarded"];
203
+ }
204
+ });
205
+
206
+ // src/diagram/constants.ts
207
+ var KNOWN_DIAGRAM_TYPES, SUBGRAPH_START, SUBGRAPH_END;
208
+ var init_constants = __esm({
209
+ "src/diagram/constants.ts"() {
210
+ "use strict";
211
+ KNOWN_DIAGRAM_TYPES = [
212
+ "flowchart",
213
+ "graph",
214
+ "sequenceDiagram",
215
+ "classDiagram",
216
+ "stateDiagram",
217
+ "erDiagram",
218
+ "gantt",
219
+ "pie",
220
+ "gitgraph",
221
+ "mindmap",
222
+ "timeline"
223
+ ];
224
+ SUBGRAPH_START = /^\s*subgraph\s+([^\s\[]+)(?:\s*\["([^"]+)"\])?/;
225
+ SUBGRAPH_END = /^\s*end\s*$/;
226
+ }
227
+ });
228
+
229
+ // src/diagram/parser.ts
230
+ function parseDiagramType(content) {
231
+ const lines = content.split("\n");
232
+ for (const line of lines) {
233
+ const trimmed = line.trim();
234
+ if (trimmed === "" || trimmed.startsWith("%%")) continue;
235
+ for (const diagramType of KNOWN_DIAGRAM_TYPES) {
236
+ if (trimmed === diagramType || trimmed.startsWith(diagramType + " ")) {
237
+ return diagramType;
238
+ }
239
+ }
240
+ return void 0;
241
+ }
242
+ return void 0;
243
+ }
244
+ function parseDiagramContent(rawContent) {
245
+ const mermaidContent = stripAnnotations(rawContent);
246
+ const flags = parseFlags(rawContent);
247
+ const diagramType = parseDiagramType(mermaidContent);
248
+ return { mermaidContent, flags, diagramType };
249
+ }
250
+ var init_parser = __esm({
251
+ "src/diagram/parser.ts"() {
252
+ "use strict";
253
+ init_annotations();
254
+ init_constants();
255
+ }
256
+ });
257
+
258
+ // src/diagram/validator.ts
259
+ function validateMermaidSyntax(content) {
260
+ const errors = [];
261
+ const trimmedContent = content.trim();
262
+ if (trimmedContent === "") {
263
+ return { valid: false, errors: [{ message: "Empty diagram content" }] };
264
+ }
265
+ const diagramType = parseDiagramType(content);
266
+ if (!diagramType) {
267
+ const firstLine = trimmedContent.split("\n")[0]?.trim() ?? "";
268
+ errors.push({
269
+ message: `Unknown diagram type. First line: "${firstLine}". Expected one of: ${KNOWN_DIAGRAM_TYPES.join(", ")}`,
270
+ line: 1
271
+ });
272
+ return { valid: false, errors, diagramType: void 0 };
273
+ }
274
+ const bracketErrors = checkBracketMatching(content);
275
+ errors.push(...bracketErrors);
276
+ const danglingErrors = checkDanglingArrows(content);
277
+ errors.push(...danglingErrors);
278
+ return {
279
+ valid: errors.length === 0,
280
+ errors,
281
+ diagramType
282
+ };
283
+ }
284
+ function checkBracketMatching(content) {
285
+ const errors = [];
286
+ const lines = content.split("\n");
287
+ const pairs = [["[", "]"], ["(", ")"], ["{", "}"]];
288
+ for (let i = 0; i < lines.length; i++) {
289
+ const line = lines[i];
290
+ const lineNum = i + 1;
291
+ if (line.trim().startsWith("%%")) continue;
292
+ for (const [open2, close] of pairs) {
293
+ let depth = 0;
294
+ for (const char of line) {
295
+ if (char === open2) depth++;
296
+ if (char === close) depth--;
297
+ if (depth < 0) {
298
+ errors.push({
299
+ message: `Unexpected closing '${close}' without matching '${open2}'`,
300
+ line: lineNum
301
+ });
302
+ break;
303
+ }
304
+ }
305
+ if (depth > 0) {
306
+ errors.push({
307
+ message: `Unclosed '${open2}' -- missing '${close}'`,
308
+ line: lineNum
309
+ });
310
+ }
311
+ }
312
+ }
313
+ return errors;
314
+ }
315
+ function checkDanglingArrows(content) {
316
+ const errors = [];
317
+ const lines = content.split("\n");
318
+ const danglingArrowPattern = /-->\s*$/;
319
+ for (let i = 0; i < lines.length; i++) {
320
+ const line = lines[i];
321
+ const lineNum = i + 1;
322
+ const trimmed = line.trim();
323
+ if (trimmed === "" || trimmed.startsWith("%%")) continue;
324
+ if (danglingArrowPattern.test(trimmed)) {
325
+ errors.push({
326
+ message: `Dangling arrow -- line ends with '-->' but no target node`,
327
+ line: lineNum
328
+ });
329
+ }
330
+ }
331
+ return errors;
332
+ }
333
+ var init_validator = __esm({
334
+ "src/diagram/validator.ts"() {
335
+ "use strict";
336
+ init_parser();
337
+ init_constants();
338
+ }
339
+ });
340
+
341
+ // src/diagram/graph-types.ts
342
+ var SHAPE_PATTERNS;
343
+ var init_graph_types = __esm({
344
+ "src/diagram/graph-types.ts"() {
345
+ "use strict";
346
+ SHAPE_PATTERNS = [
347
+ { open: "([", close: "])", shape: "stadium" },
348
+ { open: "[[", close: "]]", shape: "subroutine" },
349
+ { open: "[(", close: ")]", shape: "cylinder" },
350
+ { open: "((", close: "))", shape: "circle" },
351
+ { open: "{{", close: "}}", shape: "hexagon" },
352
+ { open: "[/", close: "\\]", shape: "trapezoid" },
353
+ { open: "[\\", close: "/]", shape: "trapezoid-alt" },
354
+ { open: "[/", close: "/]", shape: "parallelogram" },
355
+ { open: "[\\", close: "\\]", shape: "parallelogram-alt" },
356
+ { open: ">", close: "]", shape: "asymmetric" },
357
+ { open: "{", close: "}", shape: "diamond" },
358
+ { open: "(", close: ")", shape: "rounded" },
359
+ { open: "[", close: "]", shape: "rect" }
360
+ ];
361
+ }
362
+ });
363
+
364
+ // src/diagram/graph-edge-parser.ts
365
+ function stripInlineClass(ref) {
366
+ const classIdx = ref.indexOf(":::");
367
+ if (classIdx === -1) return { id: ref };
368
+ return {
369
+ id: ref.substring(0, classIdx),
370
+ className: ref.substring(classIdx + 3)
371
+ };
372
+ }
373
+ function parseNodeShape(definition) {
374
+ const trimmed = definition.trim();
375
+ if (!trimmed) return null;
376
+ const { id: withShape, className } = stripInlineClass(trimmed);
377
+ for (const sp of SHAPE_PATTERNS) {
378
+ const openIdx = withShape.indexOf(sp.open);
379
+ if (openIdx === -1) continue;
380
+ const nodeId = withShape.substring(0, openIdx).trim();
381
+ if (!nodeId || !/^[\w][\w\d_-]*$/.test(nodeId)) continue;
382
+ const afterOpen = withShape.substring(openIdx + sp.open.length);
383
+ if (!afterOpen.endsWith(sp.close)) continue;
384
+ const labelRaw = afterOpen.substring(0, afterOpen.length - sp.close.length);
385
+ let label = labelRaw;
386
+ if (label.startsWith('"') && label.endsWith('"')) {
387
+ label = label.substring(1, label.length - 1);
388
+ }
389
+ return { id: nodeId, label, shape: sp.shape, className };
390
+ }
391
+ return null;
392
+ }
393
+ function extractNodeSegments(line) {
394
+ let work = line;
395
+ for (const op of EDGE_OPS) {
396
+ work = work.replace(op.pattern, " \0 ");
397
+ }
398
+ return work.split("\0").map((s) => s.trim()).filter(Boolean);
399
+ }
400
+ function parseEdgesFromLine(line) {
401
+ const result = [];
402
+ let remaining = line;
403
+ let lastNode = null;
404
+ while (remaining.trim()) {
405
+ let earliest = null;
406
+ for (const op of EDGE_OPS) {
407
+ const match = op.pattern.exec(remaining);
408
+ if (match && (earliest === null || match.index < earliest.index)) {
409
+ earliest = {
410
+ index: match.index,
411
+ matchLen: match[0].length,
412
+ type: op.type,
413
+ bidirectional: op.bidirectional,
414
+ label: match[1]
415
+ };
416
+ }
417
+ }
418
+ if (!earliest) break;
419
+ const beforeOp = remaining.substring(0, earliest.index).trim();
420
+ remaining = remaining.substring(earliest.index + earliest.matchLen);
421
+ if (lastNode === null) {
422
+ lastNode = extractNodeId(beforeOp);
423
+ if (!lastNode) break;
424
+ }
425
+ const afterTrimmed = remaining.trim();
426
+ const nextNodeId = extractNodeId(afterTrimmed, "left");
427
+ if (!nextNodeId) break;
428
+ result.push({
429
+ from: lastNode,
430
+ to: nextNodeId,
431
+ type: earliest.type,
432
+ label: earliest.label,
433
+ bidirectional: earliest.bidirectional
434
+ });
435
+ lastNode = nextNodeId;
436
+ remaining = advancePastNode(remaining.trim(), nextNodeId);
437
+ }
438
+ return result;
439
+ }
440
+ function extractNodeId(text, direction = "right") {
441
+ if (!text.trim()) return null;
442
+ const { id } = stripInlineClass(text.trim());
443
+ const shaped = parseNodeShape(text.trim());
444
+ if (shaped) return shaped.id;
445
+ if (direction === "left") {
446
+ const match2 = /^([\w][\w\d_-]*)/.exec(id);
447
+ return match2 ? match2[1] : null;
448
+ }
449
+ const match = /([\w][\w\d_-]*)$/.exec(id);
450
+ return match ? match[1] : null;
451
+ }
452
+ function advancePastNode(text, nodeId) {
453
+ const { id: cleanText } = stripInlineClass(text);
454
+ for (const sp of SHAPE_PATTERNS) {
455
+ const expectedStart = nodeId + sp.open;
456
+ if (cleanText.startsWith(expectedStart)) {
457
+ const closeIdx = cleanText.indexOf(sp.close, expectedStart.length);
458
+ if (closeIdx !== -1) {
459
+ const afterClose = closeIdx + sp.close.length;
460
+ const afterWithClass = text.substring(afterClose);
461
+ const classMatch = /^:::\S+/.exec(afterWithClass);
462
+ return classMatch ? afterWithClass.substring(classMatch[0].length) : afterWithClass;
463
+ }
464
+ }
465
+ }
466
+ const safeId = regexSafe(nodeId);
467
+ const simplePattern = new RegExp(`^${safeId}(?::::\\S+)?`);
468
+ const simpleMatch = simplePattern.exec(text);
469
+ if (simpleMatch) {
470
+ return text.substring(simpleMatch[0].length);
471
+ }
472
+ return text.substring(nodeId.length);
473
+ }
474
+ function regexSafe(s) {
475
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
476
+ }
477
+ var EDGE_OPS;
478
+ var init_graph_edge_parser = __esm({
479
+ "src/diagram/graph-edge-parser.ts"() {
480
+ "use strict";
481
+ init_graph_types();
482
+ EDGE_OPS = [
483
+ // Bidirectional variants (must come before unidirectional)
484
+ { pattern: /\s*<==>\s*/, type: "thick", bidirectional: true },
485
+ { pattern: /\s*<-\.->\s*/, type: "dotted", bidirectional: true },
486
+ { pattern: /\s*<-->\s*/, type: "arrow", bidirectional: true },
487
+ // Labeled edges: pipe syntax -->|"label"| or -->|label|
488
+ { pattern: /\s*-->\|"([^"]*)"\|\s*/, type: "arrow", bidirectional: false },
489
+ { pattern: /\s*-->\|([^|]*)\|\s*/, type: "arrow", bidirectional: false },
490
+ // Labeled edges: inline syntax -- "label" -->
491
+ { pattern: /\s*--\s*"([^"]*)"\s*-->\s*/, type: "arrow", bidirectional: false },
492
+ // Unlabeled operators (order: longest first)
493
+ { pattern: /\s*~~~\s*/, type: "invisible", bidirectional: false },
494
+ { pattern: /\s*==>\s*/, type: "thick", bidirectional: false },
495
+ { pattern: /\s*-\.->\s*/, type: "dotted", bidirectional: false },
496
+ { pattern: /\s*---\s*/, type: "open", bidirectional: false },
497
+ { pattern: /\s*-->\s*/, type: "arrow", bidirectional: false }
498
+ ];
499
+ }
500
+ });
501
+
502
+ // src/diagram/graph-parser.ts
503
+ function parseMermaidToGraph(rawContent, filePath) {
504
+ const flags = parseFlags(rawContent);
505
+ const statuses = parseStatuses(rawContent);
506
+ const mermaidContent = stripAnnotations(rawContent);
507
+ const lines = mermaidContent.split("\n");
508
+ let diagramType = "flowchart";
509
+ let direction = "TB";
510
+ for (const line of lines) {
511
+ const trimmed = line.trim();
512
+ if (!trimmed || trimmed.startsWith("%%")) continue;
513
+ const dirMatch = /^(flowchart|graph)\s+(TB|TD|BT|LR|RL)/.exec(trimmed);
514
+ if (dirMatch) {
515
+ diagramType = dirMatch[1];
516
+ const rawDir = dirMatch[2];
517
+ direction = rawDir === "TD" ? "TB" : rawDir;
518
+ }
519
+ break;
520
+ }
521
+ const classDefs = /* @__PURE__ */ new Map();
522
+ const nodeStyles = /* @__PURE__ */ new Map();
523
+ const linkStyles = /* @__PURE__ */ new Map();
524
+ const classAssignments = /* @__PURE__ */ new Map();
525
+ const directiveLineIndices = /* @__PURE__ */ new Set();
526
+ parseStyleDirectives(
527
+ lines,
528
+ classDefs,
529
+ nodeStyles,
530
+ linkStyles,
531
+ classAssignments,
532
+ directiveLineIndices
533
+ );
534
+ const subgraphs = /* @__PURE__ */ new Map();
535
+ const lineToSubgraph = /* @__PURE__ */ new Map();
536
+ const subgraphLineIndices = /* @__PURE__ */ new Set();
537
+ parseSubgraphStructure(lines, subgraphs, lineToSubgraph, subgraphLineIndices);
538
+ const nodes = /* @__PURE__ */ new Map();
539
+ parseNodeDefinitions(
540
+ lines,
541
+ nodes,
542
+ classAssignments,
543
+ lineToSubgraph,
544
+ directiveLineIndices,
545
+ subgraphLineIndices
546
+ );
547
+ const edges = [];
548
+ parseEdgeDefinitions(
549
+ lines,
550
+ edges,
551
+ nodes,
552
+ subgraphs,
553
+ lineToSubgraph,
554
+ directiveLineIndices,
555
+ subgraphLineIndices
556
+ );
557
+ for (const [nodeId, flag] of flags) {
558
+ const node = nodes.get(nodeId);
559
+ if (node) node.flag = flag;
560
+ }
561
+ for (const [nodeId, status] of statuses) {
562
+ const node = nodes.get(nodeId);
563
+ if (node) node.status = status;
564
+ }
565
+ const validation = validateMermaidSyntax(mermaidContent);
566
+ return {
567
+ diagramType,
568
+ direction,
569
+ nodes,
570
+ edges,
571
+ subgraphs,
572
+ classDefs,
573
+ nodeStyles,
574
+ linkStyles,
575
+ classAssignments,
576
+ filePath,
577
+ flags,
578
+ statuses,
579
+ validation
580
+ };
581
+ }
582
+ function parseStyleDirectives(lines, classDefs, nodeStyles, linkStyles, classAssignments, directiveLineIndices) {
583
+ for (let i = 0; i < lines.length; i++) {
584
+ const trimmed = lines[i].trim();
585
+ const classDefMatch = /^classDef\s+(\S+)\s+(.+);?\s*$/.exec(trimmed);
586
+ if (classDefMatch) {
587
+ const name = classDefMatch[1];
588
+ classDefs.set(name, classDefMatch[2].replace(/;\s*$/, ""));
589
+ directiveLineIndices.add(i);
590
+ continue;
591
+ }
592
+ const styleMatch = /^style\s+(\S+)\s+(.+);?\s*$/.exec(trimmed);
593
+ if (styleMatch) {
594
+ nodeStyles.set(styleMatch[1], styleMatch[2].replace(/;\s*$/, ""));
595
+ directiveLineIndices.add(i);
596
+ continue;
597
+ }
598
+ const linkStyleMatch = /^linkStyle\s+(\d+)\s+(.+);?\s*$/.exec(trimmed);
599
+ if (linkStyleMatch) {
600
+ linkStyles.set(
601
+ parseInt(linkStyleMatch[1], 10),
602
+ linkStyleMatch[2].replace(/;\s*$/, "")
603
+ );
604
+ directiveLineIndices.add(i);
605
+ continue;
606
+ }
607
+ const classDirectiveMatch = /^class\s+(.+?)\s+(\S+);?\s*$/.exec(trimmed);
608
+ if (classDirectiveMatch) {
609
+ const className = classDirectiveMatch[2].replace(/;\s*$/, "");
610
+ const nodeIds = classDirectiveMatch[1].split(",").map((s) => s.trim());
611
+ for (const nid of nodeIds) {
612
+ classAssignments.set(nid, className);
613
+ }
614
+ directiveLineIndices.add(i);
615
+ continue;
616
+ }
617
+ }
618
+ }
619
+ function parseSubgraphStructure(lines, subgraphs, lineToSubgraph, subgraphLineIndices) {
620
+ const subgraphStack = [];
621
+ for (let i = 0; i < lines.length; i++) {
622
+ const line = lines[i];
623
+ const startMatch = SUBGRAPH_START.exec(line);
624
+ if (startMatch) {
625
+ const id = startMatch[1];
626
+ const label = startMatch[2] || id;
627
+ const parentId = subgraphStack.length > 0 ? subgraphStack[subgraphStack.length - 1] : null;
628
+ const sg = {
629
+ id,
630
+ label,
631
+ parentId,
632
+ nodeIds: [],
633
+ childSubgraphIds: []
634
+ };
635
+ subgraphs.set(id, sg);
636
+ if (parentId) {
637
+ const parent = subgraphs.get(parentId);
638
+ if (parent) parent.childSubgraphIds.push(id);
639
+ }
640
+ subgraphStack.push(id);
641
+ subgraphLineIndices.add(i);
642
+ continue;
643
+ }
644
+ if (SUBGRAPH_END.test(line) && subgraphStack.length > 0) {
645
+ subgraphStack.pop();
646
+ subgraphLineIndices.add(i);
647
+ continue;
648
+ }
649
+ if (subgraphStack.length > 0) {
650
+ lineToSubgraph.set(i, subgraphStack[subgraphStack.length - 1]);
651
+ }
652
+ }
653
+ }
654
+ function isSkippableLine(trimmed, lineIdx, directiveIndices, subgraphIndices) {
655
+ if (directiveIndices.has(lineIdx)) return true;
656
+ if (subgraphIndices.has(lineIdx)) return true;
657
+ if (!trimmed || trimmed.startsWith("%%")) return true;
658
+ if (DIRECTION_LINE.test(trimmed)) return true;
659
+ return false;
660
+ }
661
+ function parseNodeDefinitions(lines, nodes, classAssignments, lineToSubgraph, directiveLineIndices, subgraphLineIndices) {
662
+ for (let i = 0; i < lines.length; i++) {
663
+ const trimmed = lines[i].trim();
664
+ if (isSkippableLine(trimmed, i, directiveLineIndices, subgraphLineIndices)) continue;
665
+ const nodeSegments = extractNodeSegments(trimmed);
666
+ for (const segment of nodeSegments) {
667
+ const parsed = parseNodeShape(segment);
668
+ if (parsed && !nodes.has(parsed.id)) {
669
+ const subgraphId = lineToSubgraph.get(i);
670
+ const cssClass = parsed.className || classAssignments.get(parsed.id);
671
+ nodes.set(parsed.id, {
672
+ id: parsed.id,
673
+ label: parsed.label,
674
+ shape: parsed.shape,
675
+ subgraphId,
676
+ cssClass
677
+ });
678
+ if (parsed.className) {
679
+ classAssignments.set(parsed.id, parsed.className);
680
+ }
681
+ } else if (!parsed) {
682
+ const { id: bareId, className } = stripInlineClass(segment.trim());
683
+ if (className && /^[\w][\w\d_-]*$/.test(bareId)) {
684
+ classAssignments.set(bareId, className);
685
+ if (!nodes.has(bareId)) {
686
+ nodes.set(bareId, {
687
+ id: bareId,
688
+ label: bareId,
689
+ shape: "rect",
690
+ subgraphId: lineToSubgraph.get(i),
691
+ cssClass: className
692
+ });
693
+ } else {
694
+ nodes.get(bareId).cssClass = className;
695
+ }
696
+ }
697
+ }
698
+ }
699
+ }
700
+ }
701
+ function parseEdgeDefinitions(lines, edges, nodes, subgraphs, lineToSubgraph, directiveLineIndices, subgraphLineIndices) {
702
+ const edgeIdCounts = /* @__PURE__ */ new Map();
703
+ for (let i = 0; i < lines.length; i++) {
704
+ const trimmed = lines[i].trim();
705
+ if (isSkippableLine(trimmed, i, directiveLineIndices, subgraphLineIndices)) continue;
706
+ const lineEdges = parseEdgesFromLine(trimmed);
707
+ for (const edgeInfo of lineEdges) {
708
+ const { id: fromId } = stripInlineClass(edgeInfo.from);
709
+ const { id: toId } = stripInlineClass(edgeInfo.to);
710
+ ensureNode(nodes, fromId, lineToSubgraph.get(i));
711
+ ensureNode(nodes, toId, lineToSubgraph.get(i));
712
+ const sgId = lineToSubgraph.get(i);
713
+ if (sgId) {
714
+ const sg = subgraphs.get(sgId);
715
+ if (sg) {
716
+ if (!sg.nodeIds.includes(fromId) && !subgraphs.has(fromId)) {
717
+ sg.nodeIds.push(fromId);
718
+ }
719
+ if (!sg.nodeIds.includes(toId) && !subgraphs.has(toId)) {
720
+ sg.nodeIds.push(toId);
721
+ }
722
+ }
723
+ }
724
+ const baseId = `${fromId}->${toId}`;
725
+ const count = edgeIdCounts.get(baseId) || 0;
726
+ const edgeId = count === 0 ? baseId : `${baseId}#${count}`;
727
+ edgeIdCounts.set(baseId, count + 1);
728
+ edges.push({
729
+ id: edgeId,
730
+ from: fromId,
731
+ to: toId,
732
+ type: edgeInfo.type,
733
+ label: edgeInfo.label,
734
+ bidirectional: edgeInfo.bidirectional || void 0
735
+ });
736
+ }
737
+ }
738
+ }
739
+ function ensureNode(nodes, id, subgraphId) {
740
+ if (nodes.has(id)) return;
741
+ nodes.set(id, { id, label: id, shape: "rect", subgraphId });
742
+ }
743
+ var DIRECTION_LINE;
744
+ var init_graph_parser = __esm({
745
+ "src/diagram/graph-parser.ts"() {
746
+ "use strict";
747
+ init_annotations();
748
+ init_validator();
749
+ init_constants();
750
+ init_graph_edge_parser();
751
+ DIRECTION_LINE = /^(flowchart|graph)\s+(TB|TD|BT|LR|RL)/;
752
+ }
753
+ });
754
+
755
+ // src/utils/paths.ts
756
+ import path from "path";
757
+ import { existsSync } from "fs";
758
+ import { fileURLToPath as fileURLToPath2 } from "url";
759
+ function getStaticDir() {
760
+ const prodPath = path.join(__dirname, "..", "static");
761
+ if (existsSync(prodPath)) return prodPath;
762
+ const devPath = path.join(__dirname, "..", "..", "static");
763
+ if (existsSync(devPath)) return devPath;
764
+ return prodPath;
765
+ }
766
+ function resolveProjectPath(projectRoot, filePath) {
767
+ const resolvedRoot = path.resolve(projectRoot);
768
+ const resolved = path.resolve(resolvedRoot, filePath);
769
+ if (!resolved.startsWith(resolvedRoot + path.sep) && resolved !== resolvedRoot) {
770
+ throw new Error(`Path traversal detected: ${filePath}`);
771
+ }
772
+ return resolved;
773
+ }
774
+ var __dirname;
775
+ var init_paths = __esm({
776
+ "src/utils/paths.ts"() {
777
+ "use strict";
778
+ __dirname = path.dirname(fileURLToPath2(import.meta.url));
779
+ }
780
+ });
781
+
782
+ // src/project/discovery.ts
783
+ import { readdir } from "fs/promises";
784
+ import { join as join2 } from "path";
785
+ async function discoverMmdFiles(directory) {
786
+ const entries = await readdir(directory, { recursive: true, withFileTypes: true });
787
+ const files = [];
788
+ for (const entry of entries) {
789
+ if (!entry.isFile() || !entry.name.endsWith(".mmd")) continue;
790
+ const parentPath = entry.parentPath ?? "";
791
+ const relativePath = parentPath ? join2(parentPath, entry.name).slice(directory.length + 1) : entry.name;
792
+ const segments = relativePath.split("/");
793
+ const excluded = segments.some((seg) => EXCLUDED_DIRS.has(seg));
794
+ if (!excluded) {
795
+ files.push(relativePath);
796
+ }
797
+ }
798
+ return files.sort();
799
+ }
800
+ var EXCLUDED_DIRS;
801
+ var init_discovery = __esm({
802
+ "src/project/discovery.ts"() {
803
+ "use strict";
804
+ EXCLUDED_DIRS = /* @__PURE__ */ new Set([
805
+ "node_modules",
806
+ ".git",
807
+ "test",
808
+ "dist",
809
+ ".planning",
810
+ ".smartcode"
811
+ ]);
812
+ }
813
+ });
814
+
815
+ // src/diagram/service.ts
816
+ var service_exports = {};
817
+ __export(service_exports, {
818
+ DiagramService: () => DiagramService
819
+ });
820
+ import { readFile, writeFile, mkdir } from "fs/promises";
821
+ import { dirname as dirname2 } from "path";
822
+ var DiagramService;
823
+ var init_service = __esm({
824
+ "src/diagram/service.ts"() {
825
+ "use strict";
826
+ init_parser();
827
+ init_annotations();
828
+ init_validator();
829
+ init_graph_parser();
830
+ init_paths();
831
+ init_discovery();
832
+ DiagramService = class {
833
+ constructor(projectRoot) {
834
+ this.projectRoot = projectRoot;
835
+ }
836
+ /** Per-file write locks to serialize concurrent write operations */
837
+ writeLocks = /* @__PURE__ */ new Map();
838
+ /**
839
+ * Serialize write operations on a given file path.
840
+ * Each call waits for the previous write on the same file to finish before running.
841
+ * Cleans up lock entry when no further writes are queued.
842
+ */
843
+ async withWriteLock(filePath, fn) {
844
+ const prev = this.writeLocks.get(filePath) ?? Promise.resolve();
845
+ const current = prev.then(fn, fn);
846
+ const settled = current.then(() => {
847
+ }, () => {
848
+ });
849
+ this.writeLocks.set(filePath, settled);
850
+ const result = await current;
851
+ if (this.writeLocks.get(filePath) === settled) {
852
+ this.writeLocks.delete(filePath);
853
+ }
854
+ return result;
855
+ }
856
+ /**
857
+ * Read a .mmd file and parse all annotations + mermaid content in one pass.
858
+ */
859
+ async readAllAnnotations(filePath) {
860
+ const resolved = this.resolvePath(filePath);
861
+ const raw = await readFile(resolved, "utf-8");
862
+ const { mermaidContent } = parseDiagramContent(raw);
863
+ const { flags, statuses, breakpoints, risks, ghosts } = parseAllAnnotations(raw);
864
+ return { raw, mermaidContent, flags, statuses, breakpoints, risks, ghosts };
865
+ }
866
+ /**
867
+ * Read-modify-write cycle for annotation mutations.
868
+ * Acquires the write lock, reads all annotations, calls modifyFn to mutate them,
869
+ * then writes the result back.
870
+ */
871
+ async modifyAnnotation(filePath, modifyFn) {
872
+ return this.withWriteLock(filePath, async () => {
873
+ const data = await this.readAllAnnotations(filePath);
874
+ modifyFn(data);
875
+ await this._writeDiagramInternal(
876
+ filePath,
877
+ data.mermaidContent,
878
+ data.flags,
879
+ data.statuses,
880
+ data.breakpoints,
881
+ data.risks,
882
+ data.ghosts
883
+ );
884
+ });
885
+ }
886
+ /**
887
+ * Read and parse a .mmd file.
888
+ * Resolves path with traversal protection, parses content, and validates syntax.
889
+ */
890
+ async readDiagram(filePath) {
891
+ const resolved = this.resolvePath(filePath);
892
+ const raw = await readFile(resolved, "utf-8");
893
+ const { mermaidContent, diagramType } = parseDiagramContent(raw);
894
+ const { flags, statuses, breakpoints, risks, ghosts } = parseAllAnnotations(raw);
895
+ const validation = validateMermaidSyntax(mermaidContent);
896
+ if (diagramType && !validation.diagramType) {
897
+ validation.diagramType = diagramType;
898
+ }
899
+ return { raw, mermaidContent, flags, statuses, breakpoints, risks, ghosts, validation, filePath };
900
+ }
901
+ /**
902
+ * Read a .mmd file and parse it into a structured GraphModel.
903
+ */
904
+ async readGraph(filePath) {
905
+ const resolved = this.resolvePath(filePath);
906
+ const raw = await readFile(resolved, "utf-8");
907
+ return parseMermaidToGraph(raw, filePath);
908
+ }
909
+ /**
910
+ * Write a .mmd file. If flags or statuses are provided, injects annotation block.
911
+ * Creates parent directories if they don't exist.
912
+ */
913
+ async writeDiagram(filePath, content, flags, statuses, breakpoints, risks, ghosts) {
914
+ return this.withWriteLock(
915
+ filePath,
916
+ () => this._writeDiagramInternal(filePath, content, flags, statuses, breakpoints, risks, ghosts)
917
+ );
918
+ }
919
+ /**
920
+ * Write diagram content while preserving existing developer-owned annotations (flags, breakpoints).
921
+ * Reads existing annotations first, merges caller-provided statuses/risks/ghosts on top, preserves flags
922
+ * and breakpoints unconditionally, then writes the merged result atomically under the write lock.
923
+ *
924
+ * Merge semantics:
925
+ * - `content`: always replaces the Mermaid diagram body
926
+ * - `statuses`: if provided, replaces all statuses; if undefined, preserves existing
927
+ * - `risks`: if provided, replaces all risks; if undefined, preserves existing
928
+ * - `ghosts`: if provided, replaces all ghosts; if undefined, preserves existing
929
+ * - `flags`: always preserved from the file (developer-owned, never touched by MCP)
930
+ * - `breakpoints`: always preserved from the file (developer-owned, never touched by MCP)
931
+ */
932
+ async writeDiagramPreserving(filePath, content, statuses, risks, ghosts) {
933
+ return this.withWriteLock(filePath, async () => {
934
+ let existingFlags = /* @__PURE__ */ new Map();
935
+ let existingBreakpoints = /* @__PURE__ */ new Set();
936
+ let existingStatuses = /* @__PURE__ */ new Map();
937
+ let existingRisks = /* @__PURE__ */ new Map();
938
+ let existingGhosts = [];
939
+ try {
940
+ const data = await this.readAllAnnotations(filePath);
941
+ existingFlags = data.flags;
942
+ existingBreakpoints = data.breakpoints;
943
+ existingStatuses = data.statuses;
944
+ existingRisks = data.risks;
945
+ existingGhosts = data.ghosts;
946
+ } catch {
947
+ }
948
+ await this._writeDiagramInternal(
949
+ filePath,
950
+ content,
951
+ existingFlags,
952
+ // always preserve
953
+ statuses ?? existingStatuses,
954
+ // replace if provided, else preserve
955
+ existingBreakpoints,
956
+ // always preserve
957
+ risks ?? existingRisks,
958
+ // replace if provided, else preserve
959
+ ghosts ?? existingGhosts
960
+ // replace if provided, else preserve
961
+ );
962
+ });
963
+ }
964
+ /**
965
+ * Write raw content to a .mmd file under the write lock.
966
+ * Does NOT process annotations -- writes content as-is.
967
+ * Used by /save endpoint which receives pre-formatted content from the editor.
968
+ */
969
+ async writeRaw(filePath, content) {
970
+ return this.withWriteLock(filePath, async () => {
971
+ const resolved = this.resolvePath(filePath);
972
+ await mkdir(dirname2(resolved), { recursive: true });
973
+ await writeFile(resolved, content, "utf-8");
974
+ });
975
+ }
976
+ /**
977
+ * Internal write without acquiring the lock.
978
+ * Used by methods that already hold the write lock for the same file.
979
+ */
980
+ async _writeDiagramInternal(filePath, content, flags, statuses, breakpoints, risks, ghosts) {
981
+ const resolved = this.resolvePath(filePath);
982
+ let output = content;
983
+ if (flags || statuses || breakpoints || risks || ghosts && ghosts.length > 0) {
984
+ output = injectAnnotations(content, flags ?? /* @__PURE__ */ new Map(), statuses, breakpoints, risks, ghosts);
985
+ }
986
+ await mkdir(dirname2(resolved), { recursive: true });
987
+ await writeFile(resolved, output, "utf-8");
988
+ }
989
+ /** Get all flags from a .mmd file as an array. */
990
+ async getFlags(filePath) {
991
+ const diagram = await this.readDiagram(filePath);
992
+ return Array.from(diagram.flags.values());
993
+ }
994
+ /** Set (add or update) a flag on a specific node. */
995
+ async setFlag(filePath, nodeId, message) {
996
+ return this.modifyAnnotation(filePath, (data) => {
997
+ data.flags.set(nodeId, { nodeId, message });
998
+ });
999
+ }
1000
+ /** Remove a flag from a specific node. */
1001
+ async removeFlag(filePath, nodeId) {
1002
+ return this.modifyAnnotation(filePath, (data) => {
1003
+ data.flags.delete(nodeId);
1004
+ });
1005
+ }
1006
+ /** Get all statuses from a .mmd file. */
1007
+ async getStatuses(filePath) {
1008
+ const diagram = await this.readDiagram(filePath);
1009
+ return diagram.statuses;
1010
+ }
1011
+ /** Set (add or update) a status on a specific node. */
1012
+ async setStatus(filePath, nodeId, status) {
1013
+ return this.modifyAnnotation(filePath, (data) => {
1014
+ data.statuses.set(nodeId, status);
1015
+ });
1016
+ }
1017
+ /** Remove a status from a specific node. */
1018
+ async removeStatus(filePath, nodeId) {
1019
+ return this.modifyAnnotation(filePath, (data) => {
1020
+ data.statuses.delete(nodeId);
1021
+ });
1022
+ }
1023
+ /** Get all breakpoints from a .mmd file. */
1024
+ async getBreakpoints(filePath) {
1025
+ const resolved = this.resolvePath(filePath);
1026
+ const raw = await readFile(resolved, "utf-8");
1027
+ return parseAllAnnotations(raw).breakpoints;
1028
+ }
1029
+ /** Set (add) a breakpoint on a specific node. */
1030
+ async setBreakpoint(filePath, nodeId) {
1031
+ return this.modifyAnnotation(filePath, (data) => {
1032
+ data.breakpoints.add(nodeId);
1033
+ });
1034
+ }
1035
+ /** Remove a breakpoint from a specific node. */
1036
+ async removeBreakpoint(filePath, nodeId) {
1037
+ return this.modifyAnnotation(filePath, (data) => {
1038
+ data.breakpoints.delete(nodeId);
1039
+ });
1040
+ }
1041
+ /** Get all risk annotations from a .mmd file. */
1042
+ async getRisks(filePath) {
1043
+ const resolved = this.resolvePath(filePath);
1044
+ const raw = await readFile(resolved, "utf-8");
1045
+ return parseAllAnnotations(raw).risks;
1046
+ }
1047
+ /** Set (add or update) a risk annotation on a specific node. */
1048
+ async setRisk(filePath, nodeId, level, reason) {
1049
+ return this.modifyAnnotation(filePath, (data) => {
1050
+ data.risks.set(nodeId, { nodeId, level, reason });
1051
+ });
1052
+ }
1053
+ /** Remove a risk annotation from a specific node. */
1054
+ async removeRisk(filePath, nodeId) {
1055
+ return this.modifyAnnotation(filePath, (data) => {
1056
+ data.risks.delete(nodeId);
1057
+ });
1058
+ }
1059
+ /** Get all ghost path annotations from a .mmd file. */
1060
+ async getGhosts(filePath) {
1061
+ const resolved = this.resolvePath(filePath);
1062
+ const raw = await readFile(resolved, "utf-8");
1063
+ return parseAllAnnotations(raw).ghosts;
1064
+ }
1065
+ /** Add a ghost path annotation to a .mmd file. */
1066
+ async addGhost(filePath, fromNodeId, toNodeId, label) {
1067
+ return this.modifyAnnotation(filePath, (data) => {
1068
+ data.ghosts.push({ fromNodeId, toNodeId, label });
1069
+ });
1070
+ }
1071
+ /** Remove a specific ghost path annotation (by from+to+label exact match). */
1072
+ async removeGhost(filePath, fromNodeId, toNodeId) {
1073
+ return this.modifyAnnotation(filePath, (data) => {
1074
+ data.ghosts = data.ghosts.filter(
1075
+ (g) => !(g.fromNodeId === fromNodeId && g.toNodeId === toNodeId)
1076
+ );
1077
+ });
1078
+ }
1079
+ /** Clear all ghost path annotations from a .mmd file. */
1080
+ async clearGhosts(filePath) {
1081
+ return this.modifyAnnotation(filePath, (data) => {
1082
+ data.ghosts = [];
1083
+ });
1084
+ }
1085
+ /** Validate the Mermaid syntax of a .mmd file. */
1086
+ async validate(filePath) {
1087
+ const diagram = await this.readDiagram(filePath);
1088
+ return diagram.validation;
1089
+ }
1090
+ /** List all .mmd files in the project root. */
1091
+ async listFiles() {
1092
+ return discoverMmdFiles(this.projectRoot);
1093
+ }
1094
+ /**
1095
+ * Resolve a relative file path against the project root.
1096
+ * Single chokepoint for path security -- rejects path traversal.
1097
+ */
1098
+ resolvePath(filePath) {
1099
+ return resolveProjectPath(this.projectRoot, filePath);
1100
+ }
1101
+ };
1102
+ }
1103
+ });
1104
+
1105
+ // src/server/static.ts
1106
+ import { readFile as readFile2 } from "fs/promises";
1107
+ import { extname } from "path";
1108
+ async function serveStaticFile(res, filePath) {
1109
+ try {
1110
+ const content = await readFile2(filePath);
1111
+ const ext = extname(filePath);
1112
+ const mime = MIME_TYPES[ext] ?? "application/octet-stream";
1113
+ res.writeHead(200, { "Content-Type": mime });
1114
+ res.end(content);
1115
+ return true;
1116
+ } catch {
1117
+ return false;
1118
+ }
1119
+ }
1120
+ var MIME_TYPES;
1121
+ var init_static = __esm({
1122
+ "src/server/static.ts"() {
1123
+ "use strict";
1124
+ MIME_TYPES = {
1125
+ ".html": "text/html; charset=utf-8",
1126
+ ".js": "application/javascript; charset=utf-8",
1127
+ ".css": "text/css; charset=utf-8",
1128
+ ".json": "application/json; charset=utf-8",
1129
+ ".svg": "image/svg+xml",
1130
+ ".png": "image/png",
1131
+ ".mmd": "text/plain; charset=utf-8"
1132
+ };
1133
+ }
1134
+ });
1135
+
1136
+ // src/diagram/collapser-parser.ts
1137
+ function parseSubgraphs(content) {
1138
+ const subgraphs = /* @__PURE__ */ new Map();
1139
+ const lines = content.split("\n");
1140
+ const stack = [];
1141
+ for (let i = 0; i < lines.length; i++) {
1142
+ const line = lines[i];
1143
+ const startMatch = line.match(SUBGRAPH_START);
1144
+ if (startMatch) {
1145
+ const id = startMatch[1];
1146
+ const label = startMatch[2] || id;
1147
+ const parent = stack.length > 0 ? stack[stack.length - 1].id : null;
1148
+ const info = {
1149
+ id,
1150
+ label,
1151
+ startLine: i,
1152
+ endLine: -1,
1153
+ nodeIds: [],
1154
+ childSubgraphs: [],
1155
+ parent
1156
+ };
1157
+ stack.push(info);
1158
+ if (parent) {
1159
+ const parentInfo = subgraphs.get(parent) || stack.find((s) => s.id === parent);
1160
+ if (parentInfo) parentInfo.childSubgraphs.push(id);
1161
+ }
1162
+ continue;
1163
+ }
1164
+ if (SUBGRAPH_END.test(line) && stack.length > 0) {
1165
+ const completed = stack.pop();
1166
+ completed.endLine = i;
1167
+ subgraphs.set(completed.id, completed);
1168
+ continue;
1169
+ }
1170
+ if (stack.length > 0) {
1171
+ const current = stack[stack.length - 1];
1172
+ const nodeMatch = line.match(NODE_DEF);
1173
+ const edgeMatch = line.match(EDGE_LINE);
1174
+ if (nodeMatch && !current.nodeIds.includes(nodeMatch[1])) {
1175
+ current.nodeIds.push(nodeMatch[1]);
1176
+ } else if (edgeMatch && !current.nodeIds.includes(edgeMatch[1])) {
1177
+ current.nodeIds.push(edgeMatch[1]);
1178
+ }
1179
+ }
1180
+ }
1181
+ while (stack.length > 0) {
1182
+ const incomplete = stack.pop();
1183
+ incomplete.endLine = lines.length - 1;
1184
+ subgraphs.set(incomplete.id, incomplete);
1185
+ }
1186
+ return subgraphs;
1187
+ }
1188
+ function countAllNodes(content) {
1189
+ const seen = /* @__PURE__ */ new Set();
1190
+ const lines = content.split("\n");
1191
+ for (const line of lines) {
1192
+ const nodeMatch = line.match(NODE_DEF);
1193
+ const edgeMatch = line.match(EDGE_LINE);
1194
+ if (nodeMatch) seen.add(nodeMatch[1]);
1195
+ if (edgeMatch) seen.add(edgeMatch[1]);
1196
+ }
1197
+ return seen.size;
1198
+ }
1199
+ function countNodesInSubgraph(info, subgraphs) {
1200
+ let count = info.nodeIds.length;
1201
+ for (const childId of info.childSubgraphs) {
1202
+ const child = subgraphs.get(childId);
1203
+ if (child) count += countNodesInSubgraph(child, subgraphs);
1204
+ }
1205
+ return count;
1206
+ }
1207
+ function countVisibleNodes(content, subgraphs, state) {
1208
+ let total = countAllNodes(content);
1209
+ for (const subgraphId of state.collapsed) {
1210
+ const info = subgraphs.get(subgraphId);
1211
+ if (!info) continue;
1212
+ if (info.parent && state.collapsed.has(info.parent)) continue;
1213
+ total -= countNodesInSubgraph(info, subgraphs);
1214
+ total += 1;
1215
+ }
1216
+ return Math.max(0, total);
1217
+ }
1218
+ function getLeafSubgraphs(subgraphs) {
1219
+ return [...subgraphs.values()].filter((s) => s.childSubgraphs.length === 0);
1220
+ }
1221
+ function getAllNodesInSubgraph(info, subgraphs) {
1222
+ const nodes = [...info.nodeIds];
1223
+ for (const childId of info.childSubgraphs) {
1224
+ const child = subgraphs.get(childId);
1225
+ if (child) nodes.push(...getAllNodesInSubgraph(child, subgraphs));
1226
+ }
1227
+ return nodes;
1228
+ }
1229
+ function findContainingSubgraph(nodeId, subgraphs) {
1230
+ let deepest = null;
1231
+ for (const info of subgraphs.values()) {
1232
+ if (info.nodeIds.includes(nodeId)) {
1233
+ if (!deepest || info.parent && info.parent === deepest.id) {
1234
+ deepest = info;
1235
+ }
1236
+ }
1237
+ }
1238
+ return deepest?.id || null;
1239
+ }
1240
+ function getPathToRoot(subgraphId, subgraphs) {
1241
+ const path6 = [];
1242
+ let current = subgraphs.get(subgraphId);
1243
+ while (current) {
1244
+ path6.unshift(current.id);
1245
+ current = current.parent ? subgraphs.get(current.parent) : void 0;
1246
+ }
1247
+ return path6;
1248
+ }
1249
+ var NODE_DEF, EDGE_LINE;
1250
+ var init_collapser_parser = __esm({
1251
+ "src/diagram/collapser-parser.ts"() {
1252
+ "use strict";
1253
+ init_constants();
1254
+ NODE_DEF = /^\s*(\w[\w\d_-]*)(?:\s*\[|\s*\(|\s*\{|\s*\[\[|\s*>)/;
1255
+ EDGE_LINE = /^\s*(\w[\w\d_-]*)\s*(?:-->|---|-\.-|-.->|==>|-.->)/;
1256
+ }
1257
+ });
1258
+
1259
+ // src/diagram/collapser-transform.ts
1260
+ function createEmptyState() {
1261
+ return {
1262
+ collapsed: /* @__PURE__ */ new Set(),
1263
+ focusPath: [],
1264
+ focusedSubgraph: null
1265
+ };
1266
+ }
1267
+ function autoCollapseToLimit(content, subgraphs, state, config) {
1268
+ if (!config.autoCollapse) return state;
1269
+ const newCollapsed = new Set(state.collapsed);
1270
+ let visibleNodes = countVisibleNodes(content, subgraphs, { ...state, collapsed: newCollapsed });
1271
+ while (visibleNodes > config.maxVisibleNodes) {
1272
+ const leaves = getLeafSubgraphs(subgraphs).filter((s) => !newCollapsed.has(s.id)).sort((a, b) => countNodesInSubgraph(b, subgraphs) - countNodesInSubgraph(a, subgraphs));
1273
+ if (leaves.length === 0) break;
1274
+ const largest = leaves[0];
1275
+ newCollapsed.add(largest.id);
1276
+ visibleNodes = countVisibleNodes(content, subgraphs, { ...state, collapsed: newCollapsed });
1277
+ }
1278
+ return { ...state, collapsed: newCollapsed };
1279
+ }
1280
+ function escapeRegExp(s) {
1281
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1282
+ }
1283
+ function generateCollapsedView(content, subgraphs, state, config = DEFAULT_CONFIG) {
1284
+ const autoState = autoCollapseToLimit(content, subgraphs, state, config);
1285
+ const autoCollapsed = [...autoState.collapsed].filter((id) => !state.collapsed.has(id));
1286
+ const manualCollapsed = [...state.collapsed];
1287
+ const lines = content.split("\n");
1288
+ const result = [];
1289
+ const edgeRedirects = /* @__PURE__ */ new Map();
1290
+ const skipRanges = [];
1291
+ for (const subgraphId of autoState.collapsed) {
1292
+ const info = subgraphs.get(subgraphId);
1293
+ if (!info) continue;
1294
+ if (info.parent && autoState.collapsed.has(info.parent)) continue;
1295
+ skipRanges.push({ start: info.startLine, end: info.endLine, id: subgraphId });
1296
+ const summaryId = `${config.collapsedNodePrefix}${subgraphId}`;
1297
+ for (const nodeId of getAllNodesInSubgraph(info, subgraphs)) {
1298
+ edgeRedirects.set(nodeId, summaryId);
1299
+ }
1300
+ }
1301
+ skipRanges.sort((a, b) => a.start - b.start);
1302
+ const compiledRedirects = [];
1303
+ for (const [from, to] of edgeRedirects) {
1304
+ compiledRedirects.push({
1305
+ regex: new RegExp(`\\b${escapeRegExp(from)}\\b`, "g"),
1306
+ replacement: to
1307
+ });
1308
+ }
1309
+ let skipIndex = 0;
1310
+ for (let i = 0; i < lines.length; i++) {
1311
+ if (skipIndex < skipRanges.length && i >= skipRanges[skipIndex].start) {
1312
+ if (i === skipRanges[skipIndex].start) {
1313
+ const range = skipRanges[skipIndex];
1314
+ const summaryId = `${config.collapsedNodePrefix}${range.id}`;
1315
+ const info = subgraphs.get(range.id);
1316
+ const nodeCount = countNodesInSubgraph(info, subgraphs);
1317
+ result.push(` ${summaryId}["[+]${info.label} (${nodeCount} nodes)"]`);
1318
+ }
1319
+ if (i <= skipRanges[skipIndex].end) {
1320
+ if (i === skipRanges[skipIndex].end) skipIndex++;
1321
+ continue;
1322
+ }
1323
+ }
1324
+ let line = lines[i];
1325
+ for (const { regex, replacement } of compiledRedirects) {
1326
+ regex.lastIndex = 0;
1327
+ line = line.replace(regex, replacement);
1328
+ }
1329
+ result.push(line);
1330
+ }
1331
+ const collapsedContent = result.join("\n");
1332
+ const visibleNodes = countVisibleNodes(content, subgraphs, autoState);
1333
+ return {
1334
+ content: collapsedContent,
1335
+ visibleNodes,
1336
+ autoCollapsed,
1337
+ manualCollapsed
1338
+ };
1339
+ }
1340
+ function focusOnNode(nodeId, subgraphs, currentState) {
1341
+ const containingSubgraph = findContainingSubgraph(nodeId, subgraphs);
1342
+ if (!containingSubgraph) return currentState;
1343
+ const focusPath = getPathToRoot(containingSubgraph, subgraphs);
1344
+ const newCollapsed = /* @__PURE__ */ new Set();
1345
+ for (const info of subgraphs.values()) {
1346
+ if (!focusPath.includes(info.id)) {
1347
+ if (info.parent == null || focusPath.includes(info.parent)) {
1348
+ newCollapsed.add(info.id);
1349
+ }
1350
+ }
1351
+ }
1352
+ return {
1353
+ collapsed: newCollapsed,
1354
+ focusPath,
1355
+ focusedSubgraph: containingSubgraph
1356
+ };
1357
+ }
1358
+ function navigateToBreadcrumb(breadcrumbId, _subgraphs, currentState) {
1359
+ if (breadcrumbId === "root") {
1360
+ return exitFocus();
1361
+ }
1362
+ const index = currentState.focusPath.indexOf(breadcrumbId);
1363
+ if (index === -1) return currentState;
1364
+ const newFocusPath = currentState.focusPath.slice(0, index + 1);
1365
+ const focusedSubgraph = newFocusPath[newFocusPath.length - 1] || null;
1366
+ return {
1367
+ ...currentState,
1368
+ focusPath: newFocusPath,
1369
+ focusedSubgraph
1370
+ };
1371
+ }
1372
+ function exitFocus() {
1373
+ return {
1374
+ collapsed: /* @__PURE__ */ new Set(),
1375
+ focusPath: [],
1376
+ focusedSubgraph: null
1377
+ };
1378
+ }
1379
+ function getBreadcrumbs(state, subgraphs) {
1380
+ const crumbs = [{ id: "root", label: "Overview" }];
1381
+ for (const id of state.focusPath) {
1382
+ const info = subgraphs.get(id);
1383
+ if (info) crumbs.push({ id, label: info.label });
1384
+ }
1385
+ return crumbs;
1386
+ }
1387
+ var DEFAULT_CONFIG;
1388
+ var init_collapser_transform = __esm({
1389
+ "src/diagram/collapser-transform.ts"() {
1390
+ "use strict";
1391
+ init_collapser_parser();
1392
+ DEFAULT_CONFIG = {
1393
+ collapsedNodePrefix: "__collapsed__",
1394
+ maxVisibleNodes: 50,
1395
+ autoCollapse: true
1396
+ };
1397
+ }
1398
+ });
1399
+
1400
+ // src/diagram/collapser.ts
1401
+ var init_collapser = __esm({
1402
+ "src/diagram/collapser.ts"() {
1403
+ "use strict";
1404
+ init_collapser_parser();
1405
+ init_collapser_transform();
1406
+ }
1407
+ });
1408
+
1409
+ // src/diagram/graph-serializer.ts
1410
+ function serializeGraphModel(graph) {
1411
+ return {
1412
+ diagramType: graph.diagramType,
1413
+ direction: graph.direction,
1414
+ nodes: Object.fromEntries(graph.nodes),
1415
+ edges: graph.edges,
1416
+ subgraphs: Object.fromEntries(graph.subgraphs),
1417
+ classDefs: Object.fromEntries(graph.classDefs),
1418
+ nodeStyles: Object.fromEntries(graph.nodeStyles),
1419
+ linkStyles: Object.fromEntries(graph.linkStyles),
1420
+ classAssignments: Object.fromEntries(graph.classAssignments),
1421
+ filePath: graph.filePath,
1422
+ flags: Object.fromEntries(graph.flags),
1423
+ statuses: Object.fromEntries(graph.statuses)
1424
+ };
1425
+ }
1426
+ var SHAPE_BRACKETS;
1427
+ var init_graph_serializer = __esm({
1428
+ "src/diagram/graph-serializer.ts"() {
1429
+ "use strict";
1430
+ init_graph_types();
1431
+ SHAPE_BRACKETS = /* @__PURE__ */ new Map();
1432
+ for (const sp of SHAPE_PATTERNS) {
1433
+ if (!SHAPE_BRACKETS.has(sp.shape)) {
1434
+ SHAPE_BRACKETS.set(sp.shape, { open: sp.open, close: sp.close });
1435
+ }
1436
+ }
1437
+ }
1438
+ });
1439
+
1440
+ // src/server/session-routes.ts
1441
+ function registerSessionRoutes(routes, sessionStore) {
1442
+ routes.push({
1443
+ method: "GET",
1444
+ pattern: new RegExp("^/api/sessions/(?<file>.+)$"),
1445
+ handler: async (_req, res, params) => {
1446
+ try {
1447
+ const file = decodeURIComponent(params["file"]);
1448
+ const sessionIds = await sessionStore.listSessions(file);
1449
+ const sessions = await Promise.all(
1450
+ sessionIds.map(async (sessionId) => {
1451
+ try {
1452
+ const events = await sessionStore.readSession(sessionId);
1453
+ const startEvent = events.find((e) => e.type === "session:start");
1454
+ const endEvent = events.find((e) => e.type === "session:end");
1455
+ const startTs = startEvent?.ts ?? 0;
1456
+ const endTs = endEvent?.ts ?? (events.length > 0 ? events[events.length - 1].ts : 0);
1457
+ return {
1458
+ sessionId,
1459
+ totalEvents: events.length,
1460
+ duration: endTs - startTs,
1461
+ startedAt: startTs
1462
+ };
1463
+ } catch {
1464
+ return { sessionId, totalEvents: 0, duration: 0, startedAt: 0 };
1465
+ }
1466
+ })
1467
+ );
1468
+ sendJson(res, { sessions });
1469
+ } catch (err) {
1470
+ const message = err instanceof Error ? err.message : "Unknown error";
1471
+ sendJson(res, { error: message }, 500);
1472
+ }
1473
+ }
1474
+ });
1475
+ routes.push({
1476
+ method: "GET",
1477
+ pattern: new RegExp("^/api/session/(?<id>[^/]+)$"),
1478
+ handler: async (_req, res, params) => {
1479
+ try {
1480
+ const id = decodeURIComponent(params["id"]);
1481
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) {
1482
+ sendJson(res, { error: "Invalid session ID" }, 400);
1483
+ return;
1484
+ }
1485
+ const events = await sessionStore.readSession(id);
1486
+ if (events.length === 0) {
1487
+ sendJson(res, { error: "Session not found" }, 404);
1488
+ return;
1489
+ }
1490
+ sendJson(res, { events });
1491
+ } catch (err) {
1492
+ const message = err instanceof Error ? err.message : "Unknown error";
1493
+ sendJson(res, { error: message }, 500);
1494
+ }
1495
+ }
1496
+ });
1497
+ }
1498
+ var init_session_routes = __esm({
1499
+ "src/server/session-routes.ts"() {
1500
+ "use strict";
1501
+ init_server();
1502
+ }
1503
+ });
1504
+
1505
+ // src/server/file-tree.ts
1506
+ function buildFileTree(files) {
1507
+ const root = [];
1508
+ for (const filePath of files) {
1509
+ const parts = filePath.split("/");
1510
+ let current = root;
1511
+ for (let i = 0; i < parts.length; i++) {
1512
+ const part = parts[i];
1513
+ const isFile = i === parts.length - 1;
1514
+ if (isFile) {
1515
+ current.push({ type: "file", name: part, path: filePath });
1516
+ } else {
1517
+ let folder = current.find(
1518
+ (n) => n.type === "folder" && n.name === part
1519
+ );
1520
+ if (!folder) {
1521
+ folder = { type: "folder", name: part, children: [] };
1522
+ current.push(folder);
1523
+ }
1524
+ current = folder.children;
1525
+ }
1526
+ }
1527
+ }
1528
+ return root;
1529
+ }
1530
+ var init_file_tree = __esm({
1531
+ "src/server/file-tree.ts"() {
1532
+ "use strict";
1533
+ }
1534
+ });
1535
+
1536
+ // src/server/file-routes.ts
1537
+ import { mkdir as mkdir2, unlink, rename, rm } from "fs/promises";
1538
+ import path2 from "path";
1539
+ function registerFileRoutes(routes, service, projectDir) {
1540
+ routes.push({
1541
+ method: "GET",
1542
+ pattern: new RegExp("^/tree\\.json$"),
1543
+ handler: async (_req, res) => {
1544
+ try {
1545
+ const files = await service.listFiles();
1546
+ const tree = buildFileTree(files);
1547
+ sendJson(res, tree);
1548
+ } catch (err) {
1549
+ const message = err instanceof Error ? err.message : "Unknown error";
1550
+ sendJson(res, { error: message }, 500);
1551
+ }
1552
+ }
1553
+ });
1554
+ routes.push({
1555
+ method: "POST",
1556
+ pattern: new RegExp("^/save$"),
1557
+ handler: async (req, res) => {
1558
+ try {
1559
+ const body = await readJsonBody(req);
1560
+ if (!body.filename || body.content === void 0) {
1561
+ sendJson(res, { error: "Missing filename or content" }, 400);
1562
+ return;
1563
+ }
1564
+ await service.writeRaw(body.filename, body.content);
1565
+ sendJson(res, { ok: true });
1566
+ } catch (err) {
1567
+ const message = err instanceof Error ? err.message : "Unknown error";
1568
+ if (message === "Payload too large") {
1569
+ sendJson(res, { error: message }, 413);
1570
+ return;
1571
+ }
1572
+ const code = err?.code;
1573
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1574
+ }
1575
+ }
1576
+ });
1577
+ routes.push({
1578
+ method: "POST",
1579
+ pattern: new RegExp("^/delete$"),
1580
+ handler: async (req, res) => {
1581
+ try {
1582
+ const body = await readJsonBody(req);
1583
+ if (!body.filename) {
1584
+ sendJson(res, { error: "Missing filename" }, 400);
1585
+ return;
1586
+ }
1587
+ const resolved = resolveProjectPath(projectDir, body.filename);
1588
+ await unlink(resolved);
1589
+ sendJson(res, { ok: true });
1590
+ } catch (err) {
1591
+ const message = err instanceof Error ? err.message : "Unknown error";
1592
+ if (message === "Payload too large") {
1593
+ sendJson(res, { error: message }, 413);
1594
+ return;
1595
+ }
1596
+ const code = err?.code;
1597
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1598
+ }
1599
+ }
1600
+ });
1601
+ routes.push({
1602
+ method: "POST",
1603
+ pattern: new RegExp("^/mkdir$"),
1604
+ handler: async (req, res) => {
1605
+ try {
1606
+ const body = await readJsonBody(req);
1607
+ if (!body.folder) {
1608
+ sendJson(res, { error: "Missing folder" }, 400);
1609
+ return;
1610
+ }
1611
+ const resolved = resolveProjectPath(projectDir, body.folder);
1612
+ await mkdir2(resolved, { recursive: true });
1613
+ sendJson(res, { ok: true });
1614
+ } catch (err) {
1615
+ const message = err instanceof Error ? err.message : "Unknown error";
1616
+ if (message === "Payload too large") {
1617
+ sendJson(res, { error: message }, 413);
1618
+ return;
1619
+ }
1620
+ sendJson(res, { error: message }, 500);
1621
+ }
1622
+ }
1623
+ });
1624
+ routes.push({
1625
+ method: "POST",
1626
+ pattern: new RegExp("^/move$"),
1627
+ handler: async (req, res) => {
1628
+ try {
1629
+ const body = await readJsonBody(req);
1630
+ if (!body.from || !body.to) {
1631
+ sendJson(res, { error: "Missing from or to" }, 400);
1632
+ return;
1633
+ }
1634
+ const resolvedFrom = resolveProjectPath(projectDir, body.from);
1635
+ const resolvedTo = resolveProjectPath(projectDir, body.to);
1636
+ await mkdir2(path2.dirname(resolvedTo), { recursive: true });
1637
+ await rename(resolvedFrom, resolvedTo);
1638
+ sendJson(res, { ok: true });
1639
+ } catch (err) {
1640
+ const message = err instanceof Error ? err.message : "Unknown error";
1641
+ if (message === "Payload too large") {
1642
+ sendJson(res, { error: message }, 413);
1643
+ return;
1644
+ }
1645
+ const code = err?.code;
1646
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1647
+ }
1648
+ }
1649
+ });
1650
+ routes.push({
1651
+ method: "POST",
1652
+ pattern: new RegExp("^/rmdir$"),
1653
+ handler: async (req, res) => {
1654
+ try {
1655
+ const body = await readJsonBody(req);
1656
+ if (!body.folder) {
1657
+ sendJson(res, { error: "Missing folder" }, 400);
1658
+ return;
1659
+ }
1660
+ const resolved = resolveProjectPath(projectDir, body.folder);
1661
+ await rm(resolved, { recursive: true });
1662
+ sendJson(res, { ok: true });
1663
+ } catch (err) {
1664
+ const message = err instanceof Error ? err.message : "Unknown error";
1665
+ if (message === "Payload too large") {
1666
+ sendJson(res, { error: message }, 413);
1667
+ return;
1668
+ }
1669
+ const code = err?.code;
1670
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1671
+ }
1672
+ }
1673
+ });
1674
+ }
1675
+ var init_file_routes = __esm({
1676
+ "src/server/file-routes.ts"() {
1677
+ "use strict";
1678
+ init_paths();
1679
+ init_server();
1680
+ init_file_tree();
1681
+ }
1682
+ });
1683
+
1684
+ // src/server/breakpoint-routes.ts
1685
+ function registerBreakpointRoutes(routes, service, wsManager, breakpointContinueSignals) {
1686
+ routes.push({
1687
+ method: "POST",
1688
+ pattern: new RegExp("^/api/breakpoints/(?<file>.+)/continue$"),
1689
+ handler: async (req, res, params) => {
1690
+ try {
1691
+ const file = decodeURIComponent(params["file"]);
1692
+ const body = await readJsonBody(req);
1693
+ if (!body.nodeId) {
1694
+ sendJson(res, { error: "Missing nodeId" }, 400);
1695
+ return;
1696
+ }
1697
+ if (breakpointContinueSignals) {
1698
+ if (breakpointContinueSignals.size >= 500) {
1699
+ const firstKey = breakpointContinueSignals.keys().next().value;
1700
+ if (firstKey !== void 0) breakpointContinueSignals.delete(firstKey);
1701
+ }
1702
+ breakpointContinueSignals.set(`${file}:${body.nodeId}`, true);
1703
+ }
1704
+ if (wsManager) {
1705
+ wsManager.broadcastAll({ type: "breakpoint:continue", file, nodeId: body.nodeId });
1706
+ }
1707
+ sendJson(res, { ok: true });
1708
+ } catch (err) {
1709
+ const message = err instanceof Error ? err.message : "Unknown error";
1710
+ if (message === "Payload too large") {
1711
+ sendJson(res, { error: message }, 413);
1712
+ return;
1713
+ }
1714
+ sendJson(res, { error: message }, 500);
1715
+ }
1716
+ }
1717
+ });
1718
+ routes.push({
1719
+ method: "GET",
1720
+ pattern: new RegExp("^/api/breakpoints/(?<file>.+)$"),
1721
+ handler: async (_req, res, params) => {
1722
+ try {
1723
+ const file = decodeURIComponent(params["file"]);
1724
+ const breakpoints = await service.getBreakpoints(file);
1725
+ sendJson(res, { breakpoints: Array.from(breakpoints) });
1726
+ } catch (err) {
1727
+ const message = err instanceof Error ? err.message : "Unknown error";
1728
+ const code = err?.code;
1729
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1730
+ }
1731
+ }
1732
+ });
1733
+ routes.push({
1734
+ method: "POST",
1735
+ pattern: new RegExp("^/api/breakpoints/(?<file>.+)$"),
1736
+ handler: async (req, res, params) => {
1737
+ try {
1738
+ const file = decodeURIComponent(params["file"]);
1739
+ const body = await readJsonBody(req);
1740
+ if (!body.nodeId || !body.action) {
1741
+ sendJson(res, { error: "Missing nodeId or action" }, 400);
1742
+ return;
1743
+ }
1744
+ if (body.action === "set") {
1745
+ await service.setBreakpoint(file, body.nodeId);
1746
+ if (wsManager) {
1747
+ wsManager.broadcastAll({ type: "breakpoint:hit", file, nodeId: body.nodeId });
1748
+ }
1749
+ } else {
1750
+ await service.removeBreakpoint(file, body.nodeId);
1751
+ }
1752
+ sendJson(res, { ok: true });
1753
+ } catch (err) {
1754
+ const message = err instanceof Error ? err.message : "Unknown error";
1755
+ if (message === "Payload too large") {
1756
+ sendJson(res, { error: message }, 413);
1757
+ return;
1758
+ }
1759
+ const code = err?.code;
1760
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1761
+ }
1762
+ }
1763
+ });
1764
+ }
1765
+ var init_breakpoint_routes = __esm({
1766
+ "src/server/breakpoint-routes.ts"() {
1767
+ "use strict";
1768
+ init_server();
1769
+ }
1770
+ });
1771
+
1772
+ // src/server/ghost-path-routes.ts
1773
+ function registerGhostPathRoutes(routes, service, wsManager) {
1774
+ routes.push({
1775
+ method: "GET",
1776
+ pattern: new RegExp("^/api/ghost-paths/(?<file>.+)$"),
1777
+ handler: async (_req, res, params) => {
1778
+ try {
1779
+ const file = decodeURIComponent(params["file"]);
1780
+ const ghosts = await service.getGhosts(file);
1781
+ sendJson(res, { ghostPaths: ghosts });
1782
+ } catch (err) {
1783
+ const message = err instanceof Error ? err.message : "Unknown error";
1784
+ const code = err?.code;
1785
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1786
+ }
1787
+ }
1788
+ });
1789
+ routes.push({
1790
+ method: "POST",
1791
+ pattern: new RegExp("^/api/ghost-paths/(?<file>.+)$"),
1792
+ handler: async (req, res, params) => {
1793
+ try {
1794
+ const file = decodeURIComponent(params["file"]);
1795
+ const body = await readJsonBody(req);
1796
+ if (!body.fromNodeId || !body.toNodeId) {
1797
+ sendJson(res, { error: "Missing fromNodeId or toNodeId" }, 400);
1798
+ return;
1799
+ }
1800
+ await service.addGhost(file, body.fromNodeId, body.toNodeId, body.label ?? "");
1801
+ const ghostPaths = await service.getGhosts(file);
1802
+ if (wsManager) {
1803
+ wsManager.broadcastAll({ type: "ghost:update", file, ghostPaths });
1804
+ }
1805
+ sendJson(res, { ok: true });
1806
+ } catch (err) {
1807
+ const message = err instanceof Error ? err.message : "Unknown error";
1808
+ if (message === "Payload too large") {
1809
+ sendJson(res, { error: message }, 413);
1810
+ return;
1811
+ }
1812
+ sendJson(res, { error: message }, 500);
1813
+ }
1814
+ }
1815
+ });
1816
+ routes.push({
1817
+ method: "DELETE",
1818
+ pattern: new RegExp("^/api/ghost-paths/(?<file>.+)$"),
1819
+ handler: async (_req, res, params) => {
1820
+ try {
1821
+ const file = decodeURIComponent(params["file"]);
1822
+ await service.clearGhosts(file);
1823
+ if (wsManager) {
1824
+ wsManager.broadcastAll({ type: "ghost:update", file, ghostPaths: [] });
1825
+ }
1826
+ sendJson(res, { ok: true });
1827
+ } catch (err) {
1828
+ const message = err instanceof Error ? err.message : "Unknown error";
1829
+ sendJson(res, { error: message }, 500);
1830
+ }
1831
+ }
1832
+ });
1833
+ }
1834
+ var init_ghost_path_routes = __esm({
1835
+ "src/server/ghost-path-routes.ts"() {
1836
+ "use strict";
1837
+ init_server();
1838
+ }
1839
+ });
1840
+
1841
+ // src/server/annotation-routes.ts
1842
+ function registerAnnotationRoutes(routes, service) {
1843
+ routes.push({
1844
+ method: "GET",
1845
+ pattern: new RegExp("^/api/annotations/(?<file>.+)/risks$"),
1846
+ handler: async (_req, res, params) => {
1847
+ try {
1848
+ const file = decodeURIComponent(params["file"]);
1849
+ const risks = await service.getRisks(file);
1850
+ const entries = [];
1851
+ for (const [, risk] of risks) {
1852
+ entries.push({ nodeId: risk.nodeId, level: risk.level, reason: risk.reason });
1853
+ }
1854
+ sendJson(res, { risks: entries });
1855
+ } catch (err) {
1856
+ const message = err instanceof Error ? err.message : "Unknown error";
1857
+ const code = err?.code;
1858
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1859
+ }
1860
+ }
1861
+ });
1862
+ routes.push({
1863
+ method: "POST",
1864
+ pattern: new RegExp("^/api/annotations/(?<file>.+)/risk$"),
1865
+ handler: async (req, res, params) => {
1866
+ try {
1867
+ const file = decodeURIComponent(params["file"]);
1868
+ const body = await readJsonBody(req);
1869
+ if (!body.nodeId || !body.level || !body.reason) {
1870
+ sendJson(res, { error: "Missing nodeId, level, or reason" }, 400);
1871
+ return;
1872
+ }
1873
+ if (!VALID_RISK_LEVELS.has(body.level)) {
1874
+ sendJson(res, { error: `Invalid level: must be high, medium, or low` }, 400);
1875
+ return;
1876
+ }
1877
+ await service.setRisk(file, body.nodeId, body.level, body.reason);
1878
+ sendJson(res, { ok: true });
1879
+ } catch (err) {
1880
+ const message = err instanceof Error ? err.message : "Unknown error";
1881
+ if (message === "Payload too large") {
1882
+ sendJson(res, { error: message }, 413);
1883
+ return;
1884
+ }
1885
+ const code = err?.code;
1886
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1887
+ }
1888
+ }
1889
+ });
1890
+ routes.push({
1891
+ method: "DELETE",
1892
+ pattern: new RegExp("^/api/annotations/(?<file>.+)/risk$"),
1893
+ handler: async (req, res, params) => {
1894
+ try {
1895
+ const file = decodeURIComponent(params["file"]);
1896
+ const body = await readJsonBody(req);
1897
+ if (!body.nodeId) {
1898
+ sendJson(res, { error: "Missing nodeId" }, 400);
1899
+ return;
1900
+ }
1901
+ await service.removeRisk(file, body.nodeId);
1902
+ sendJson(res, { ok: true });
1903
+ } catch (err) {
1904
+ const message = err instanceof Error ? err.message : "Unknown error";
1905
+ if (message === "Payload too large") {
1906
+ sendJson(res, { error: message }, 413);
1907
+ return;
1908
+ }
1909
+ const code = err?.code;
1910
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
1911
+ }
1912
+ }
1913
+ });
1914
+ }
1915
+ var VALID_RISK_LEVELS;
1916
+ var init_annotation_routes = __esm({
1917
+ "src/server/annotation-routes.ts"() {
1918
+ "use strict";
1919
+ init_server();
1920
+ VALID_RISK_LEVELS = /* @__PURE__ */ new Set(["high", "medium", "low"]);
1921
+ }
1922
+ });
1923
+
1924
+ // src/registry/mcp-session-registry.ts
1925
+ import { readFile as readFile3, writeFile as writeFile2, readdir as readdir2, mkdir as mkdir3, rename as rename2, unlink as unlink2 } from "fs/promises";
1926
+ import { join as join3 } from "path";
1927
+ import { tmpdir } from "os";
1928
+ import { randomBytes, randomUUID } from "crypto";
1929
+ function isProcessAlive(pid) {
1930
+ try {
1931
+ process.kill(pid, 0);
1932
+ return true;
1933
+ } catch {
1934
+ return false;
1935
+ }
1936
+ }
1937
+ var McpSessionRegistry;
1938
+ var init_mcp_session_registry = __esm({
1939
+ "src/registry/mcp-session-registry.ts"() {
1940
+ "use strict";
1941
+ init_logger();
1942
+ McpSessionRegistry = class _McpSessionRegistry {
1943
+ manifestDir;
1944
+ projectRoot;
1945
+ sessions = /* @__PURE__ */ new Map();
1946
+ activeSessionId = null;
1947
+ constructor(projectRoot) {
1948
+ this.projectRoot = projectRoot;
1949
+ this.manifestDir = join3(projectRoot, ".smartcode", "mcp-sessions");
1950
+ }
1951
+ /** Register the registry by ensuring the manifest directory exists */
1952
+ async register() {
1953
+ await mkdir3(this.manifestDir, { recursive: true });
1954
+ if (this.sessions.size === 0) {
1955
+ await this.createSession();
1956
+ }
1957
+ log.debug(`MCP session registry registered (pid ${process.pid})`);
1958
+ }
1959
+ /**
1960
+ * Create a new session and make it the active one.
1961
+ * Returns the new session ID.
1962
+ */
1963
+ async createSession(label) {
1964
+ await mkdir3(this.manifestDir, { recursive: true });
1965
+ const sessionId = randomUUID();
1966
+ const shortId = sessionId.substring(0, 8);
1967
+ const session = {
1968
+ sessionId,
1969
+ label: label || `Session ${shortId}`,
1970
+ startedAt: Date.now(),
1971
+ diagrams: /* @__PURE__ */ new Map()
1972
+ };
1973
+ this.sessions.set(sessionId, session);
1974
+ this.activeSessionId = sessionId;
1975
+ await this.writeManifest(session);
1976
+ log.debug(`MCP session created: ${sessionId} (label: ${session.label})`);
1977
+ return sessionId;
1978
+ }
1979
+ /** Switch the active session to an existing session ID */
1980
+ setActiveSession(sessionId) {
1981
+ if (!this.sessions.has(sessionId)) {
1982
+ throw new Error(`Session not found: ${sessionId}`);
1983
+ }
1984
+ this.activeSessionId = sessionId;
1985
+ }
1986
+ /** Get the current active session ID (or null) */
1987
+ getActiveSessionId() {
1988
+ return this.activeSessionId;
1989
+ }
1990
+ /** Rename a session's label and persist to disk */
1991
+ async renameSession(sessionId, label) {
1992
+ const session = this.sessions.get(sessionId);
1993
+ if (session) {
1994
+ session.label = label;
1995
+ await this.writeManifest(session);
1996
+ }
1997
+ await _McpSessionRegistry.renameOnDisk(this.projectRoot, sessionId, label);
1998
+ }
1999
+ /**
2000
+ * Track a diagram file in the active session.
2001
+ * Backward-compat: auto-creates a default session if none exists.
2002
+ */
2003
+ async trackDiagram(filePath) {
2004
+ if (!this.activeSessionId || !this.sessions.has(this.activeSessionId)) {
2005
+ await this.createSession();
2006
+ }
2007
+ const session = this.sessions.get(this.activeSessionId);
2008
+ const now = Date.now();
2009
+ const existing = session.diagrams.get(filePath);
2010
+ if (existing) {
2011
+ existing.lastUpdated = now;
2012
+ } else {
2013
+ session.diagrams.set(filePath, { filePath, firstSeen: now, lastUpdated: now });
2014
+ if (session.label.startsWith("Session ")) {
2015
+ const base = filePath.includes("/") ? filePath.split("/").pop() : filePath;
2016
+ session.label = base.replace(".mmd", "");
2017
+ }
2018
+ }
2019
+ await this.writeManifest(session);
2020
+ }
2021
+ /** Deregister all sessions owned by this process */
2022
+ async deregister() {
2023
+ for (const session of this.sessions.values()) {
2024
+ try {
2025
+ await unlink2(this.manifestPath(session.sessionId));
2026
+ } catch {
2027
+ }
2028
+ }
2029
+ this.sessions.clear();
2030
+ this.activeSessionId = null;
2031
+ log.debug(`MCP session registry deregistered (pid ${process.pid})`);
2032
+ }
2033
+ /** List all sessions owned by this process instance */
2034
+ listSessions() {
2035
+ return Array.from(this.sessions.values()).map((s) => this.buildManifest(s));
2036
+ }
2037
+ // ── Backward-compat getters ──
2038
+ /** Get the session ID (returns active session for backward compat) */
2039
+ get sessionId() {
2040
+ return this.activeSessionId ?? "";
2041
+ }
2042
+ // ── Static methods (read from disk, cross-process) ──
2043
+ /** List all active MCP sessions for a project (filters out dead PIDs, cleans stale) */
2044
+ static async listActive(projectRoot) {
2045
+ const dir = join3(projectRoot, ".smartcode", "mcp-sessions");
2046
+ let entries;
2047
+ try {
2048
+ entries = await readdir2(dir);
2049
+ } catch {
2050
+ return [];
2051
+ }
2052
+ const manifests = [];
2053
+ const stale = [];
2054
+ for (const entry of entries) {
2055
+ if (!entry.endsWith(".json")) continue;
2056
+ try {
2057
+ const raw = await readFile3(join3(dir, entry), "utf-8");
2058
+ const manifest = JSON.parse(raw);
2059
+ if (isProcessAlive(manifest.pid)) {
2060
+ manifests.push(manifest);
2061
+ } else {
2062
+ stale.push(entry);
2063
+ }
2064
+ } catch {
2065
+ stale.push(entry);
2066
+ }
2067
+ }
2068
+ for (const s of stale) {
2069
+ unlink2(join3(dir, s)).catch(() => {
2070
+ });
2071
+ }
2072
+ return manifests;
2073
+ }
2074
+ /** Get a specific session manifest by ID */
2075
+ static async getSession(projectRoot, sessionId) {
2076
+ const filePath = join3(projectRoot, ".smartcode", "mcp-sessions", `${sessionId}.json`);
2077
+ try {
2078
+ const raw = await readFile3(filePath, "utf-8");
2079
+ const manifest = JSON.parse(raw);
2080
+ return isProcessAlive(manifest.pid) ? manifest : null;
2081
+ } catch {
2082
+ return null;
2083
+ }
2084
+ }
2085
+ /** Rename a session on disk (works for any process's sessions) */
2086
+ static async renameOnDisk(projectRoot, sessionId, label) {
2087
+ const filePath = join3(projectRoot, ".smartcode", "mcp-sessions", `${sessionId}.json`);
2088
+ try {
2089
+ const raw = await readFile3(filePath, "utf-8");
2090
+ const manifest = JSON.parse(raw);
2091
+ manifest.label = label;
2092
+ const data = JSON.stringify(manifest, null, 2);
2093
+ const tempPath = join3(tmpdir(), `smartcode-mcp-${randomBytes(4).toString("hex")}.json`);
2094
+ await writeFile2(tempPath, data, "utf-8");
2095
+ try {
2096
+ await rename2(tempPath, filePath);
2097
+ } catch {
2098
+ await writeFile2(filePath, data, "utf-8");
2099
+ await unlink2(tempPath).catch(() => {
2100
+ });
2101
+ }
2102
+ return true;
2103
+ } catch {
2104
+ return false;
2105
+ }
2106
+ }
2107
+ // ── Private helpers ──
2108
+ manifestPath(sessionId) {
2109
+ return join3(this.manifestDir, `${sessionId}.json`);
2110
+ }
2111
+ buildManifest(session) {
2112
+ return {
2113
+ sessionId: session.sessionId,
2114
+ pid: process.pid,
2115
+ startedAt: session.startedAt,
2116
+ label: session.label,
2117
+ diagrams: Array.from(session.diagrams.values())
2118
+ };
2119
+ }
2120
+ /** Atomically write manifest (temp file + rename) */
2121
+ async writeManifest(session) {
2122
+ const data = JSON.stringify(this.buildManifest(session), null, 2);
2123
+ const tempPath = join3(tmpdir(), `smartcode-mcp-${randomBytes(4).toString("hex")}.json`);
2124
+ await writeFile2(tempPath, data, "utf-8");
2125
+ try {
2126
+ await rename2(tempPath, this.manifestPath(session.sessionId));
2127
+ } catch {
2128
+ await writeFile2(this.manifestPath(session.sessionId), data, "utf-8");
2129
+ await unlink2(tempPath).catch(() => {
2130
+ });
2131
+ }
2132
+ }
2133
+ };
2134
+ }
2135
+ });
2136
+
2137
+ // src/server/mcp-session-routes.ts
2138
+ function registerMcpSessionRoutes(routes, projectDir, wsManager) {
2139
+ routes.push({
2140
+ method: "GET",
2141
+ pattern: new RegExp("^/api/mcp-sessions$"),
2142
+ handler: async (_req, res) => {
2143
+ try {
2144
+ const sessions = await McpSessionRegistry.listActive(projectDir);
2145
+ sendJson(res, { sessions });
2146
+ } catch (err) {
2147
+ const message = err instanceof Error ? err.message : "Unknown error";
2148
+ sendJson(res, { error: message }, 500);
2149
+ }
2150
+ }
2151
+ });
2152
+ routes.push({
2153
+ method: "GET",
2154
+ pattern: new RegExp("^/api/mcp-sessions/(?<id>[^/]+)$"),
2155
+ handler: async (_req, res, params) => {
2156
+ try {
2157
+ const id = decodeURIComponent(params["id"]);
2158
+ const session = await McpSessionRegistry.getSession(projectDir, id);
2159
+ if (!session) {
2160
+ sendJson(res, { error: "Session not found" }, 404);
2161
+ return;
2162
+ }
2163
+ sendJson(res, session);
2164
+ } catch (err) {
2165
+ const message = err instanceof Error ? err.message : "Unknown error";
2166
+ sendJson(res, { error: message }, 500);
2167
+ }
2168
+ }
2169
+ });
2170
+ routes.push({
2171
+ method: "PATCH",
2172
+ pattern: new RegExp("^/api/mcp-sessions/(?<id>[^/]+)$"),
2173
+ handler: async (req, res, params) => {
2174
+ try {
2175
+ const id = decodeURIComponent(params["id"]);
2176
+ const body = await readJsonBody(req);
2177
+ const label = body?.label;
2178
+ if (!label || typeof label !== "string" || !label.trim()) {
2179
+ sendJson(res, { error: "label is required" }, 400);
2180
+ return;
2181
+ }
2182
+ const ok = await McpSessionRegistry.renameOnDisk(projectDir, id, label.trim());
2183
+ if (!ok) {
2184
+ sendJson(res, { error: "Session not found" }, 404);
2185
+ return;
2186
+ }
2187
+ if (wsManager) {
2188
+ wsManager.broadcastAll({ type: "mcp-session:updated" });
2189
+ }
2190
+ sendJson(res, { ok: true, sessionId: id, label: label.trim() });
2191
+ } catch (err) {
2192
+ const message = err instanceof Error ? err.message : "Unknown error";
2193
+ sendJson(res, { error: message }, 500);
2194
+ }
2195
+ }
2196
+ });
2197
+ }
2198
+ var init_mcp_session_routes = __esm({
2199
+ "src/server/mcp-session-routes.ts"() {
2200
+ "use strict";
2201
+ init_mcp_session_registry();
2202
+ init_server();
2203
+ }
2204
+ });
2205
+
2206
+ // src/server/heatmap-routes.ts
2207
+ function mergeCounts(a, b) {
2208
+ const result = { ...a };
2209
+ for (const [key, value] of Object.entries(b)) {
2210
+ result[key] = (result[key] ?? 0) + value;
2211
+ }
2212
+ return result;
2213
+ }
2214
+ function registerHeatmapRoutes(routes, heatmapStore, sessionStore, wsManager) {
2215
+ routes.push({
2216
+ method: "GET",
2217
+ pattern: new RegExp("^/api/heatmap/(?<file>.+)$"),
2218
+ handler: async (_req, res, params) => {
2219
+ try {
2220
+ const file = decodeURIComponent(params["file"]);
2221
+ const clickCounts = heatmapStore.getCounts(file);
2222
+ let sessionCounts = {};
2223
+ if (sessionStore) {
2224
+ try {
2225
+ sessionCounts = await sessionStore.getHeatmapData(file);
2226
+ } catch {
2227
+ }
2228
+ }
2229
+ const merged = mergeCounts(clickCounts, sessionCounts);
2230
+ sendJson(res, merged);
2231
+ } catch (err) {
2232
+ const message = err instanceof Error ? err.message : "Unknown error";
2233
+ sendJson(res, { error: message }, 500);
2234
+ }
2235
+ }
2236
+ });
2237
+ routes.push({
2238
+ method: "POST",
2239
+ pattern: new RegExp("^/api/heatmap/(?<file>.+)/increment$"),
2240
+ handler: async (req, res, params) => {
2241
+ try {
2242
+ const file = decodeURIComponent(params["file"]);
2243
+ const body = await readJsonBody(req);
2244
+ if (!body.counts || typeof body.counts !== "object") {
2245
+ sendJson(res, { error: 'Missing or invalid "counts" field' }, 400);
2246
+ return;
2247
+ }
2248
+ for (const [key, value] of Object.entries(body.counts)) {
2249
+ if (typeof key !== "string" || typeof value !== "number" || value < 0) {
2250
+ sendJson(res, { error: "Invalid count entry" }, 400);
2251
+ return;
2252
+ }
2253
+ }
2254
+ heatmapStore.increment(file, body.counts);
2255
+ if (wsManager) {
2256
+ const allCounts = heatmapStore.getCounts(file);
2257
+ let sessionCounts = {};
2258
+ if (sessionStore) {
2259
+ try {
2260
+ sessionCounts = await sessionStore.getHeatmapData(file);
2261
+ } catch {
2262
+ }
2263
+ }
2264
+ const merged = mergeCounts(allCounts, sessionCounts);
2265
+ wsManager.broadcastAll({
2266
+ type: "heatmap:update",
2267
+ file,
2268
+ data: merged
2269
+ });
2270
+ }
2271
+ sendJson(res, { ok: true });
2272
+ } catch (err) {
2273
+ const message = err instanceof Error ? err.message : "Unknown error";
2274
+ if (message === "Invalid JSON" || message === "Payload too large") {
2275
+ sendJson(res, { error: message }, 400);
2276
+ return;
2277
+ }
2278
+ sendJson(res, { error: message }, 500);
2279
+ }
2280
+ }
2281
+ });
2282
+ }
2283
+ var HeatmapStore;
2284
+ var init_heatmap_routes = __esm({
2285
+ "src/server/heatmap-routes.ts"() {
2286
+ "use strict";
2287
+ init_server();
2288
+ HeatmapStore = class {
2289
+ data = /* @__PURE__ */ new Map();
2290
+ /** Merge incoming counts into the store for a given file */
2291
+ increment(file, counts) {
2292
+ let fileMap = this.data.get(file);
2293
+ if (!fileMap) {
2294
+ fileMap = /* @__PURE__ */ new Map();
2295
+ this.data.set(file, fileMap);
2296
+ }
2297
+ for (const [nodeId, count] of Object.entries(counts)) {
2298
+ if (typeof count !== "number" || count < 0) continue;
2299
+ fileMap.set(nodeId, (fileMap.get(nodeId) ?? 0) + count);
2300
+ }
2301
+ }
2302
+ /** Get click counts for a file as a plain object */
2303
+ getCounts(file) {
2304
+ const fileMap = this.data.get(file);
2305
+ if (!fileMap) return {};
2306
+ const result = {};
2307
+ for (const [nodeId, count] of fileMap) {
2308
+ result[nodeId] = count;
2309
+ }
2310
+ return result;
2311
+ }
2312
+ /** Clear counts for a specific file (used in testing) */
2313
+ clear(file) {
2314
+ this.data.delete(file);
2315
+ }
2316
+ /** Clear all data (used in testing) */
2317
+ clearAll() {
2318
+ this.data.clear();
2319
+ }
2320
+ };
2321
+ }
2322
+ });
2323
+
2324
+ // src/registry/workspace-registry.ts
2325
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir4, rename as rename3, unlink as unlink3 } from "fs/promises";
2326
+ import { join as join4, basename } from "path";
2327
+ import { homedir, tmpdir as tmpdir2 } from "os";
2328
+ import { randomBytes as randomBytes2 } from "crypto";
2329
+ async function readRegistry() {
2330
+ try {
2331
+ const raw = await readFile4(REGISTRY_PATH, "utf-8");
2332
+ const data = JSON.parse(raw);
2333
+ return Array.isArray(data) ? data : [];
2334
+ } catch {
2335
+ return [];
2336
+ }
2337
+ }
2338
+ async function writeRegistry(entries) {
2339
+ await mkdir4(SMARTCODE_DIR, { recursive: true });
2340
+ const tempPath = join4(tmpdir2(), `smartcode-registry-${randomBytes2(4).toString("hex")}.json`);
2341
+ await writeFile3(tempPath, JSON.stringify(entries, null, 2), "utf-8");
2342
+ try {
2343
+ await rename3(tempPath, REGISTRY_PATH);
2344
+ } catch {
2345
+ await writeFile3(REGISTRY_PATH, JSON.stringify(entries, null, 2), "utf-8");
2346
+ await unlink3(tempPath).catch(() => {
2347
+ });
2348
+ }
2349
+ }
2350
+ function isProcessAlive2(pid) {
2351
+ try {
2352
+ process.kill(pid, 0);
2353
+ return true;
2354
+ } catch {
2355
+ return false;
2356
+ }
2357
+ }
2358
+ function filterAlive(entries) {
2359
+ return entries.filter((e) => isProcessAlive2(e.pid));
2360
+ }
2361
+ async function register(dir, port) {
2362
+ const name = basename(dir) || dir;
2363
+ const pid = process.pid;
2364
+ const entries = filterAlive(await readRegistry());
2365
+ const cleaned = entries.filter((e) => e.port !== port && e.dir !== dir);
2366
+ cleaned.push({ name, dir, port, pid });
2367
+ await writeRegistry(cleaned);
2368
+ log.debug(`Workspace registered: ${name} (port ${port}, pid ${pid})`);
2369
+ }
2370
+ async function deregister(port) {
2371
+ const entries = await readRegistry();
2372
+ const filtered = entries.filter((e) => e.port !== port);
2373
+ await writeRegistry(filtered);
2374
+ log.debug(`Workspace deregistered (port ${port})`);
2375
+ }
2376
+ async function list() {
2377
+ const entries = await readRegistry();
2378
+ const alive = filterAlive(entries);
2379
+ if (alive.length !== entries.length) {
2380
+ await writeRegistry(alive).catch(() => {
2381
+ });
2382
+ }
2383
+ return alive;
2384
+ }
2385
+ var SMARTCODE_DIR, REGISTRY_PATH;
2386
+ var init_workspace_registry = __esm({
2387
+ "src/registry/workspace-registry.ts"() {
2388
+ "use strict";
2389
+ init_logger();
2390
+ SMARTCODE_DIR = join4(homedir(), ".smartcode");
2391
+ REGISTRY_PATH = join4(SMARTCODE_DIR, "workspaces.json");
2392
+ }
2393
+ });
2394
+
2395
+ // src/server/routes.ts
2396
+ import { readFile as readFile5 } from "fs/promises";
2397
+ function registerRoutes(service, projectDir, wsManager, breakpointContinueSignals, sessionStore, heatmapStore) {
2398
+ const routes = [];
2399
+ registerFileRoutes(routes, service, projectDir);
2400
+ routes.push({
2401
+ method: "GET",
2402
+ pattern: new RegExp("^/api/status$"),
2403
+ handler: async (_req, res) => {
2404
+ try {
2405
+ const files = await service.listFiles();
2406
+ const activeFlags = [];
2407
+ for (const file of files) {
2408
+ try {
2409
+ const flags = await service.getFlags(file);
2410
+ for (const flag of flags) {
2411
+ activeFlags.push({ file, nodeId: flag.nodeId, message: flag.message });
2412
+ }
2413
+ } catch {
2414
+ }
2415
+ }
2416
+ sendJson(res, {
2417
+ status: "running",
2418
+ uptime: process.uptime(),
2419
+ port: null,
2420
+ projectDir,
2421
+ files: files.length,
2422
+ connectedClients: wsManager?.getClientCount() ?? 0,
2423
+ activeFlags
2424
+ });
2425
+ } catch (err) {
2426
+ const message = err instanceof Error ? err.message : "Unknown error";
2427
+ sendJson(res, { error: message }, 500);
2428
+ }
2429
+ }
2430
+ });
2431
+ routes.push({
2432
+ method: "GET",
2433
+ pattern: new RegExp("^/api/diagrams$"),
2434
+ handler: async (_req, res) => {
2435
+ try {
2436
+ const files = await service.listFiles();
2437
+ sendJson(res, { files });
2438
+ } catch (err) {
2439
+ const message = err instanceof Error ? err.message : "Unknown error";
2440
+ sendJson(res, { error: message }, 500);
2441
+ }
2442
+ }
2443
+ });
2444
+ routes.push({
2445
+ method: "GET",
2446
+ pattern: new RegExp("^/api/diagrams/(?<file>.+)$"),
2447
+ handler: async (req, res, params) => {
2448
+ try {
2449
+ const file = decodeURIComponent(params["file"]);
2450
+ const diagram = await service.readDiagram(file);
2451
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
2452
+ const collapsedParam = url.searchParams.get("collapsed");
2453
+ const configParam = url.searchParams.get("collapseConfig");
2454
+ const focusParam = url.searchParams.get("focus");
2455
+ const breadcrumbParam = url.searchParams.get("breadcrumb");
2456
+ let collapseConfig = { ...DEFAULT_CONFIG };
2457
+ if (configParam) {
2458
+ try {
2459
+ const userConfig = JSON.parse(configParam);
2460
+ collapseConfig = { ...DEFAULT_CONFIG, ...userConfig };
2461
+ } catch {
2462
+ }
2463
+ }
2464
+ const subgraphs = parseSubgraphs(diagram.mermaidContent);
2465
+ let userCollapsed = [];
2466
+ if (collapsedParam) {
2467
+ try {
2468
+ const parsed = JSON.parse(collapsedParam);
2469
+ if (Array.isArray(parsed)) userCollapsed = parsed;
2470
+ } catch {
2471
+ }
2472
+ }
2473
+ let state = {
2474
+ ...createEmptyState(),
2475
+ collapsed: new Set(userCollapsed)
2476
+ };
2477
+ if (focusParam) {
2478
+ state = focusOnNode(focusParam, subgraphs, state);
2479
+ } else if (breadcrumbParam) {
2480
+ state = navigateToBreadcrumb(breadcrumbParam, subgraphs, state);
2481
+ }
2482
+ const result = generateCollapsedView(
2483
+ diagram.mermaidContent,
2484
+ subgraphs,
2485
+ state,
2486
+ collapseConfig
2487
+ );
2488
+ const breadcrumbs = getBreadcrumbs(state, subgraphs);
2489
+ sendJson(res, {
2490
+ filePath: diagram.filePath,
2491
+ mermaidContent: result.content,
2492
+ rawContent: diagram.mermaidContent,
2493
+ flags: Object.fromEntries(diagram.flags),
2494
+ validation: {
2495
+ valid: diagram.validation.valid,
2496
+ errors: diagram.validation.errors,
2497
+ diagramType: diagram.validation.diagramType
2498
+ },
2499
+ collapse: {
2500
+ visibleNodes: result.visibleNodes,
2501
+ autoCollapsed: result.autoCollapsed,
2502
+ manualCollapsed: result.manualCollapsed,
2503
+ config: collapseConfig,
2504
+ breadcrumbs,
2505
+ focusedSubgraph: state.focusedSubgraph
2506
+ }
2507
+ });
2508
+ } catch (err) {
2509
+ const message = err instanceof Error ? err.message : "Unknown error";
2510
+ const code = err?.code;
2511
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
2512
+ }
2513
+ }
2514
+ });
2515
+ routes.push({
2516
+ method: "GET",
2517
+ pattern: new RegExp("^/api/graph/(?<file>.+)$"),
2518
+ handler: async (_req, res, params) => {
2519
+ try {
2520
+ const file = decodeURIComponent(params["file"]);
2521
+ const graph = await service.readGraph(file);
2522
+ const json = serializeGraphModel(graph);
2523
+ sendJson(res, json);
2524
+ } catch (err) {
2525
+ const message = err instanceof Error ? err.message : "Unknown error";
2526
+ const code = err?.code;
2527
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
2528
+ }
2529
+ }
2530
+ });
2531
+ registerBreakpointRoutes(routes, service, wsManager, breakpointContinueSignals);
2532
+ registerGhostPathRoutes(routes, service, wsManager);
2533
+ registerAnnotationRoutes(routes, service);
2534
+ if (sessionStore) {
2535
+ registerSessionRoutes(routes, sessionStore);
2536
+ }
2537
+ if (heatmapStore) {
2538
+ registerHeatmapRoutes(routes, heatmapStore, sessionStore, wsManager);
2539
+ }
2540
+ registerMcpSessionRoutes(routes, projectDir, wsManager);
2541
+ routes.push({
2542
+ method: "GET",
2543
+ pattern: new RegExp("^/api/workspaces$"),
2544
+ handler: async (_req, res) => {
2545
+ try {
2546
+ const workspaces = await list();
2547
+ sendJson(res, workspaces);
2548
+ } catch (err) {
2549
+ const message = err instanceof Error ? err.message : "Unknown error";
2550
+ sendJson(res, { error: message }, 500);
2551
+ }
2552
+ }
2553
+ });
2554
+ routes.push({
2555
+ method: "GET",
2556
+ pattern: new RegExp("^/(?<mmdPath>.+\\.mmd)$"),
2557
+ handler: async (_req, res, params) => {
2558
+ try {
2559
+ const filePath = params["mmdPath"];
2560
+ const resolved = resolveProjectPath(projectDir, filePath);
2561
+ const content = await readFile5(resolved, "utf-8");
2562
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
2563
+ res.end(content);
2564
+ } catch (err) {
2565
+ const message = err instanceof Error ? err.message : "Unknown error";
2566
+ const code = err?.code;
2567
+ sendJson(res, { error: message }, code === "ENOENT" ? 404 : 500);
2568
+ }
2569
+ }
2570
+ });
2571
+ return routes;
2572
+ }
2573
+ var init_routes = __esm({
2574
+ "src/server/routes.ts"() {
2575
+ "use strict";
2576
+ init_paths();
2577
+ init_server();
2578
+ init_collapser();
2579
+ init_graph_serializer();
2580
+ init_session_routes();
2581
+ init_file_routes();
2582
+ init_breakpoint_routes();
2583
+ init_ghost_path_routes();
2584
+ init_annotation_routes();
2585
+ init_mcp_session_routes();
2586
+ init_heatmap_routes();
2587
+ init_workspace_registry();
2588
+ }
2589
+ });
2590
+
2591
+ // src/server/websocket.ts
2592
+ import { WebSocketServer, WebSocket as WsWebSocket } from "ws";
2593
+ var WebSocketManager;
2594
+ var init_websocket = __esm({
2595
+ "src/server/websocket.ts"() {
2596
+ "use strict";
2597
+ init_logger();
2598
+ WebSocketManager = class {
2599
+ namespaces = /* @__PURE__ */ new Map();
2600
+ constructor(httpServer) {
2601
+ httpServer.on("upgrade", (request2, socket, head) => {
2602
+ const { pathname } = new URL(request2.url, `http://${request2.headers.host}`);
2603
+ let projectName = "default";
2604
+ if (pathname.startsWith("/ws/")) {
2605
+ projectName = decodeURIComponent(pathname.slice(4)) || "default";
2606
+ } else if (pathname !== "/ws") {
2607
+ socket.destroy();
2608
+ return;
2609
+ }
2610
+ const wss = this.getOrCreateNamespace(projectName);
2611
+ wss.handleUpgrade(request2, socket, head, (ws) => {
2612
+ wss.emit("connection", ws, request2);
2613
+ });
2614
+ });
2615
+ }
2616
+ /** Get or create a WebSocketServer for the given project namespace */
2617
+ getOrCreateNamespace(name) {
2618
+ let wss = this.namespaces.get(name);
2619
+ if (wss) return wss;
2620
+ wss = new WebSocketServer({ noServer: true });
2621
+ wss.on("connection", (ws) => {
2622
+ ws.on("error", (err) => log.error(`WebSocket error [${name}]:`, err.message));
2623
+ ws.send(JSON.stringify({ type: "connected", project: name }));
2624
+ });
2625
+ this.namespaces.set(name, wss);
2626
+ return wss;
2627
+ }
2628
+ /** Ensure a namespace exists for the given project name */
2629
+ addProject(name) {
2630
+ this.getOrCreateNamespace(name);
2631
+ }
2632
+ /** Broadcast a JSON message to all clients in a specific project namespace */
2633
+ broadcast(projectName, message) {
2634
+ const wss = this.namespaces.get(projectName);
2635
+ if (!wss) return;
2636
+ const data = JSON.stringify(message);
2637
+ wss.clients.forEach((client) => {
2638
+ if (client.readyState === WsWebSocket.OPEN) {
2639
+ client.send(data);
2640
+ }
2641
+ });
2642
+ }
2643
+ /** Broadcast a JSON message to all clients across all namespaces */
2644
+ broadcastAll(message) {
2645
+ const data = JSON.stringify(message);
2646
+ for (const wss of this.namespaces.values()) {
2647
+ wss.clients.forEach((client) => {
2648
+ if (client.readyState === WsWebSocket.OPEN) {
2649
+ client.send(data);
2650
+ }
2651
+ });
2652
+ }
2653
+ }
2654
+ /** Count connected clients with OPEN readyState */
2655
+ getClientCount(namespace) {
2656
+ if (namespace) {
2657
+ const wss = this.namespaces.get(namespace);
2658
+ if (!wss) return 0;
2659
+ let count = 0;
2660
+ wss.clients.forEach((client) => {
2661
+ if (client.readyState === WsWebSocket.OPEN) count++;
2662
+ });
2663
+ return count;
2664
+ }
2665
+ let total = 0;
2666
+ for (const wss of this.namespaces.values()) {
2667
+ wss.clients.forEach((client) => {
2668
+ if (client.readyState === WsWebSocket.OPEN) total++;
2669
+ });
2670
+ }
2671
+ return total;
2672
+ }
2673
+ /** Close all WebSocketServer instances */
2674
+ close() {
2675
+ for (const wss of this.namespaces.values()) {
2676
+ wss.close();
2677
+ }
2678
+ this.namespaces.clear();
2679
+ }
2680
+ };
2681
+ }
2682
+ });
2683
+
2684
+ // src/watcher/file-watcher.ts
2685
+ import { watch, existsSync as existsSync2 } from "fs";
2686
+ import path3 from "path";
2687
+ var FileWatcher;
2688
+ var init_file_watcher = __esm({
2689
+ "src/watcher/file-watcher.ts"() {
2690
+ "use strict";
2691
+ init_discovery();
2692
+ init_logger();
2693
+ FileWatcher = class _FileWatcher {
2694
+ constructor(projectDir, onFileChanged, onFileAdded, onFileRemoved) {
2695
+ this.projectDir = projectDir;
2696
+ this.onFileChanged = onFileChanged;
2697
+ this.onFileAdded = onFileAdded;
2698
+ this.onFileRemoved = onFileRemoved;
2699
+ this.ready = discoverMmdFiles(projectDir).then((files) => {
2700
+ for (const f of files) this.knownFiles.add(f);
2701
+ log.debug(`FileWatcher pre-populated ${files.length} known files`);
2702
+ }).catch(() => {
2703
+ log.warn("FileWatcher: failed to discover existing files, first events may misclassify");
2704
+ });
2705
+ this.watcher = watch(
2706
+ projectDir,
2707
+ { recursive: true },
2708
+ (_eventType, filename) => {
2709
+ if (!filename) return;
2710
+ const relative = filename.split(path3.sep).join("/");
2711
+ if (!relative.endsWith(".mmd")) return;
2712
+ if (relative.includes("node_modules/") || relative.includes(".git/")) return;
2713
+ const existing = this.debounceTimers.get(relative);
2714
+ if (existing) clearTimeout(existing);
2715
+ this.debounceTimers.set(relative, setTimeout(() => {
2716
+ this.debounceTimers.delete(relative);
2717
+ this.ready.then(() => this.handleEvent(relative));
2718
+ }, _FileWatcher.DEBOUNCE_MS));
2719
+ }
2720
+ );
2721
+ log.debug(`FileWatcher started for ${projectDir} (native fs.watch recursive)`);
2722
+ }
2723
+ watcher;
2724
+ /** Debounce map to avoid duplicate events for the same file */
2725
+ debounceTimers = /* @__PURE__ */ new Map();
2726
+ static DEBOUNCE_MS = 80;
2727
+ /** Tracks known files so we can distinguish adds from changes */
2728
+ knownFiles = /* @__PURE__ */ new Set();
2729
+ /** Resolves when initial file discovery is complete */
2730
+ ready;
2731
+ /** Check if file exists and route to appropriate callback */
2732
+ handleEvent(relative) {
2733
+ const absolute = path3.join(this.projectDir, relative);
2734
+ const exists = existsSync2(absolute);
2735
+ if (exists) {
2736
+ if (this.knownFiles.has(relative)) {
2737
+ log.debug(`File changed: ${relative}`);
2738
+ this.onFileChanged(relative);
2739
+ } else {
2740
+ log.debug(`File added: ${relative}`);
2741
+ this.knownFiles.add(relative);
2742
+ this.onFileAdded(relative);
2743
+ }
2744
+ } else {
2745
+ log.debug(`File removed: ${relative}`);
2746
+ this.knownFiles.delete(relative);
2747
+ this.onFileRemoved(relative);
2748
+ }
2749
+ }
2750
+ /** Wait for initial file discovery to complete. Useful for tests. */
2751
+ whenReady() {
2752
+ return this.ready;
2753
+ }
2754
+ /** Close the file watcher and release OS file handles */
2755
+ async close() {
2756
+ this.watcher.close();
2757
+ for (const timer of this.debounceTimers.values()) {
2758
+ clearTimeout(timer);
2759
+ }
2760
+ this.debounceTimers.clear();
2761
+ log.debug("FileWatcher closed");
2762
+ }
2763
+ };
2764
+ }
2765
+ });
2766
+
2767
+ // src/session/session-store.ts
2768
+ import { appendFile, readFile as readFile6, readdir as readdir3, mkdir as mkdir5, open as fsOpen } from "fs/promises";
2769
+ import { join as join5 } from "path";
2770
+ import { randomUUID as randomUUID2 } from "crypto";
2771
+ var SessionStore;
2772
+ var init_session_store = __esm({
2773
+ "src/session/session-store.ts"() {
2774
+ "use strict";
2775
+ SessionStore = class _SessionStore {
2776
+ sessionsDir;
2777
+ activeSessions = /* @__PURE__ */ new Map();
2778
+ writeLocks = /* @__PURE__ */ new Map();
2779
+ constructor(projectRoot) {
2780
+ this.sessionsDir = join5(projectRoot, ".smartcode", "sessions");
2781
+ }
2782
+ /** Valid session ID pattern (UUID format) */
2783
+ static SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
2784
+ /** Validate a session ID to prevent path traversal */
2785
+ validateSessionId(sessionId) {
2786
+ if (!_SessionStore.SESSION_ID_RE.test(sessionId)) {
2787
+ throw new Error(`Invalid session ID: ${sessionId}`);
2788
+ }
2789
+ }
2790
+ /** Ensure the sessions directory exists */
2791
+ async ensureDir() {
2792
+ await mkdir5(this.sessionsDir, { recursive: true });
2793
+ }
2794
+ /** Serialize write operations on a given session to prevent JSONL corruption */
2795
+ async withWriteLock(sessionId, fn) {
2796
+ const prev = this.writeLocks.get(sessionId) ?? Promise.resolve();
2797
+ const current = prev.then(fn, fn);
2798
+ const settled = current.then(() => {
2799
+ }, () => {
2800
+ });
2801
+ this.writeLocks.set(sessionId, settled);
2802
+ const result = await current;
2803
+ if (this.writeLocks.get(sessionId) === settled) {
2804
+ this.writeLocks.delete(sessionId);
2805
+ }
2806
+ return result;
2807
+ }
2808
+ /** Get the file path for a session's JSONL file */
2809
+ filePath(sessionId) {
2810
+ this.validateSessionId(sessionId);
2811
+ return join5(this.sessionsDir, `${sessionId}.jsonl`);
2812
+ }
2813
+ /**
2814
+ * Start a new session for a diagram file.
2815
+ * Creates the sessions directory if needed, generates a UUID, and writes the start event.
2816
+ * Returns the session ID.
2817
+ */
2818
+ async startSession(diagramFile) {
2819
+ await this.ensureDir();
2820
+ const id = randomUUID2();
2821
+ const startedAt = Date.now();
2822
+ const meta = { id, diagramFile, startedAt };
2823
+ this.activeSessions.set(id, meta);
2824
+ const startEvent = {
2825
+ ts: startedAt,
2826
+ type: "session:start",
2827
+ payload: { diagramFile }
2828
+ };
2829
+ await appendFile(this.filePath(id), JSON.stringify(startEvent) + "\n", "utf-8");
2830
+ return id;
2831
+ }
2832
+ /**
2833
+ * Record a step (event) in an active session.
2834
+ * Uses a write lock to prevent concurrent JSONL corruption.
2835
+ */
2836
+ async recordStep(sessionId, event) {
2837
+ return this.withWriteLock(sessionId, async () => {
2838
+ await appendFile(this.filePath(sessionId), JSON.stringify(event) + "\n", "utf-8");
2839
+ });
2840
+ }
2841
+ /**
2842
+ * End an active session.
2843
+ * Appends a session:end event, computes summary statistics, and removes from active sessions.
2844
+ */
2845
+ async endSession(sessionId) {
2846
+ const meta = this.activeSessions.get(sessionId);
2847
+ if (!meta) {
2848
+ throw new Error(`Session ${sessionId} is not active`);
2849
+ }
2850
+ const diagramFile = meta.diagramFile;
2851
+ const endEvent = {
2852
+ ts: Date.now(),
2853
+ type: "session:end",
2854
+ payload: {}
2855
+ };
2856
+ await this.withWriteLock(sessionId, async () => {
2857
+ await appendFile(this.filePath(sessionId), JSON.stringify(endEvent) + "\n", "utf-8");
2858
+ });
2859
+ const events = await this.readSession(sessionId);
2860
+ this.activeSessions.delete(sessionId);
2861
+ this.writeLocks.delete(sessionId);
2862
+ return this.computeSummary(events, sessionId, diagramFile);
2863
+ }
2864
+ /**
2865
+ * Read all events from a session's JSONL file.
2866
+ * Each line is parsed as a JSON object. Empty lines are filtered.
2867
+ */
2868
+ async readSession(sessionId) {
2869
+ try {
2870
+ const content = await readFile6(this.filePath(sessionId), "utf-8");
2871
+ return content.split("\n").filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
2872
+ } catch (err) {
2873
+ if (err?.code === "ENOENT") {
2874
+ return [];
2875
+ }
2876
+ throw err;
2877
+ }
2878
+ }
2879
+ /**
2880
+ * List session IDs for a specific diagram file.
2881
+ * Reads the first line of each .jsonl file to check the diagramFile in the session:start payload.
2882
+ */
2883
+ async listSessions(diagramFile) {
2884
+ await this.ensureDir();
2885
+ let entries;
2886
+ try {
2887
+ entries = await readdir3(this.sessionsDir);
2888
+ } catch {
2889
+ return [];
2890
+ }
2891
+ const matching = [];
2892
+ for (const entry of entries) {
2893
+ if (!entry.endsWith(".jsonl")) continue;
2894
+ const sessionId = entry.replace(".jsonl", "");
2895
+ try {
2896
+ const firstLine = await this.readFirstLine(join5(this.sessionsDir, entry));
2897
+ if (!firstLine) continue;
2898
+ const event = JSON.parse(firstLine);
2899
+ if (event.type === "session:start" && event.payload.diagramFile === diagramFile) {
2900
+ matching.push(sessionId);
2901
+ }
2902
+ } catch {
2903
+ }
2904
+ }
2905
+ return matching;
2906
+ }
2907
+ /**
2908
+ * Get heatmap data for a diagram file: counts of node:visited events by nodeId.
2909
+ * Aggregates across all sessions for the given file.
2910
+ */
2911
+ async getHeatmapData(diagramFile) {
2912
+ const sessionIds = await this.listSessions(diagramFile);
2913
+ const counts = {};
2914
+ for (const sessionId of sessionIds) {
2915
+ const events = await this.readSession(sessionId);
2916
+ for (const event of events) {
2917
+ if (event.type === "node:visited" && typeof event.payload.nodeId === "string") {
2918
+ const nodeId = event.payload.nodeId;
2919
+ counts[nodeId] = (counts[nodeId] ?? 0) + 1;
2920
+ }
2921
+ }
2922
+ }
2923
+ return counts;
2924
+ }
2925
+ /** Read only the first line of a file without loading the entire file into memory */
2926
+ async readFirstLine(filePath) {
2927
+ const fh = await fsOpen(filePath, "r");
2928
+ try {
2929
+ const buf = Buffer.alloc(4096);
2930
+ const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
2931
+ if (bytesRead === 0) return null;
2932
+ const text = buf.toString("utf-8", 0, bytesRead);
2933
+ const newlineIdx = text.indexOf("\n");
2934
+ return newlineIdx === -1 ? text : text.substring(0, newlineIdx);
2935
+ } finally {
2936
+ await fh.close();
2937
+ }
2938
+ }
2939
+ /** Get metadata for an active session, or undefined if not active */
2940
+ getActiveSession(sessionId) {
2941
+ return this.activeSessions.get(sessionId);
2942
+ }
2943
+ /** Compute summary statistics from a list of session events */
2944
+ computeSummary(events, sessionId, diagramFile) {
2945
+ const startEvent = events.find((e) => e.type === "session:start");
2946
+ const endEvent = events.find((e) => e.type === "session:end");
2947
+ const startTs = startEvent?.ts ?? 0;
2948
+ const endTs = endEvent?.ts ?? Date.now();
2949
+ const duration = endTs - startTs;
2950
+ let nodesVisited = 0;
2951
+ const uniqueNodes = /* @__PURE__ */ new Set();
2952
+ let edgesTraversed = 0;
2953
+ for (const event of events) {
2954
+ if (event.type === "node:visited") {
2955
+ nodesVisited++;
2956
+ if (typeof event.payload.nodeId === "string") {
2957
+ uniqueNodes.add(event.payload.nodeId);
2958
+ }
2959
+ } else if (event.type === "edge:traversed") {
2960
+ edgesTraversed++;
2961
+ }
2962
+ }
2963
+ return {
2964
+ sessionId,
2965
+ diagramFile,
2966
+ duration,
2967
+ totalEvents: events.length,
2968
+ nodesVisited,
2969
+ uniqueNodesVisited: uniqueNodes.size,
2970
+ edgesTraversed
2971
+ };
2972
+ }
2973
+ };
2974
+ }
2975
+ });
2976
+
2977
+ // src/server/server.ts
2978
+ var server_exports = {};
2979
+ __export(server_exports, {
2980
+ createHttpServer: () => createHttpServer,
2981
+ readJsonBody: () => readJsonBody,
2982
+ sendJson: () => sendJson,
2983
+ startServer: () => startServer
2984
+ });
2985
+ import { createServer } from "http";
2986
+ import { readFile as readFile7 } from "fs/promises";
2987
+ import path4 from "path";
2988
+ import { detect } from "detect-port";
2989
+ import open from "open";
2990
+ function setCorsHeaders(req, res) {
2991
+ const origin = req.headers.origin;
2992
+ if (origin && LOCALHOST_PATTERNS.some((p) => p.test(origin))) {
2993
+ res.setHeader("Access-Control-Allow-Origin", origin);
2994
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
2995
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
2996
+ }
2997
+ }
2998
+ function sendJson(res, data, status = 200) {
2999
+ const body = JSON.stringify(data);
3000
+ res.writeHead(status, {
3001
+ "Content-Type": "application/json; charset=utf-8",
3002
+ "Content-Length": Buffer.byteLength(body)
3003
+ });
3004
+ res.end(body);
3005
+ }
3006
+ async function readJsonBody(req) {
3007
+ return new Promise((resolve, reject) => {
3008
+ const chunks = [];
3009
+ let size = 0;
3010
+ let aborted = false;
3011
+ req.on("data", (chunk) => {
3012
+ size += chunk.length;
3013
+ if (size > MAX_BODY_SIZE) {
3014
+ aborted = true;
3015
+ req.destroy();
3016
+ reject(new Error("Payload too large"));
3017
+ return;
3018
+ }
3019
+ chunks.push(chunk);
3020
+ });
3021
+ req.on("end", () => {
3022
+ if (aborted) return;
3023
+ try {
3024
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
3025
+ } catch {
3026
+ reject(new Error("Invalid JSON"));
3027
+ }
3028
+ });
3029
+ req.on("error", (err) => {
3030
+ if (!aborted) reject(err);
3031
+ });
3032
+ });
3033
+ }
3034
+ function createHandler(routes, staticDir) {
3035
+ return async (req, res) => {
3036
+ try {
3037
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
3038
+ setCorsHeaders(req, res);
3039
+ if (req.method === "OPTIONS") {
3040
+ res.writeHead(204);
3041
+ res.end();
3042
+ return;
3043
+ }
3044
+ for (const route of routes) {
3045
+ if (req.method === route.method) {
3046
+ const match = route.pattern.exec(url.pathname);
3047
+ if (match) {
3048
+ const params = match.groups ?? {};
3049
+ await route.handler(req, res, params);
3050
+ return;
3051
+ }
3052
+ }
3053
+ }
3054
+ if (url.pathname === "/" || url.pathname === "/index.html") {
3055
+ const served = await serveStaticFile(res, path4.join(staticDir, "live.html"));
3056
+ if (served) return;
3057
+ }
3058
+ const safePath = path4.normalize(url.pathname).replace(/^(\.\.[/\\])+/, "");
3059
+ const staticFilePath = path4.join(staticDir, safePath);
3060
+ if (staticFilePath === staticDir || staticFilePath.startsWith(staticDir + path4.sep)) {
3061
+ const served = await serveStaticFile(res, staticFilePath);
3062
+ if (served) return;
3063
+ }
3064
+ sendJson(res, { error: "Not Found" }, 404);
3065
+ } catch (err) {
3066
+ const message = err instanceof Error ? err.message : "Internal Server Error";
3067
+ log.error("Request error:", message);
3068
+ sendJson(res, { error: message }, 500);
3069
+ }
3070
+ };
3071
+ }
3072
+ function createHttpServer(projectDir, existingService) {
3073
+ const resolvedDir = path4.resolve(projectDir);
3074
+ const service = existingService ?? new DiagramService(resolvedDir);
3075
+ const staticDir = getStaticDir();
3076
+ const httpServer = createServer();
3077
+ const wsManager = new WebSocketManager(httpServer);
3078
+ const breakpointContinueSignals = /* @__PURE__ */ new Map();
3079
+ const sessionStore = new SessionStore(resolvedDir);
3080
+ const heatmapStore = new HeatmapStore();
3081
+ const routes = registerRoutes(service, resolvedDir, wsManager, breakpointContinueSignals, sessionStore, heatmapStore);
3082
+ const handler = createHandler(routes, staticDir);
3083
+ httpServer.on("request", (req, res) => {
3084
+ handler(req, res).catch((err) => {
3085
+ log.error("Unhandled error:", err);
3086
+ if (!res.headersSent) {
3087
+ sendJson(res, { error: "Internal Server Error" }, 500);
3088
+ }
3089
+ });
3090
+ });
3091
+ const watchers = /* @__PURE__ */ new Map();
3092
+ function createProjectWatcher(projectName, projectDir2, projectService) {
3093
+ return new FileWatcher(
3094
+ projectDir2,
3095
+ async (file) => {
3096
+ const content = await readFile7(
3097
+ path4.join(projectDir2, file),
3098
+ "utf-8"
3099
+ ).catch(() => null);
3100
+ if (content !== null) {
3101
+ wsManager.broadcast(projectName, { type: "file:changed", file, content });
3102
+ }
3103
+ try {
3104
+ const graph = await projectService.readGraph(file);
3105
+ const graphJson = serializeGraphModel(graph);
3106
+ wsManager.broadcast(projectName, { type: "graph:update", file, graph: graphJson });
3107
+ } catch {
3108
+ }
3109
+ },
3110
+ (file) => {
3111
+ wsManager.broadcast(projectName, { type: "file:added", file });
3112
+ projectService.listFiles().then((files) => {
3113
+ wsManager.broadcast(projectName, { type: "tree:updated", files });
3114
+ }).catch((err) => {
3115
+ log.error(`Failed to list files for ${projectName} after add:`, err);
3116
+ });
3117
+ },
3118
+ (file) => {
3119
+ wsManager.broadcast(projectName, { type: "file:removed", file });
3120
+ projectService.listFiles().then((files) => {
3121
+ wsManager.broadcast(projectName, { type: "tree:updated", files });
3122
+ }).catch((err) => {
3123
+ log.error(`Failed to list files for ${projectName} after remove:`, err);
3124
+ });
3125
+ }
3126
+ );
3127
+ }
3128
+ const fileWatcher = createProjectWatcher("default", resolvedDir, service);
3129
+ watchers.set("default", fileWatcher);
3130
+ function addProject(name, dir) {
3131
+ const resolvedProjectDir = path4.resolve(dir);
3132
+ const projectService = new DiagramService(resolvedProjectDir);
3133
+ wsManager.addProject(name);
3134
+ watchers.set(name, createProjectWatcher(name, resolvedProjectDir, projectService));
3135
+ }
3136
+ async function closeAllWatchers() {
3137
+ for (const w of watchers.values()) {
3138
+ await w.close();
3139
+ }
3140
+ watchers.clear();
3141
+ }
3142
+ return { httpServer, wsManager, fileWatcher, breakpointContinueSignals, sessionStore, heatmapStore, addProject, closeAllWatchers };
3143
+ }
3144
+ async function startServer(options) {
3145
+ const projectDir = path4.resolve(options.dir);
3146
+ const actualPort = await detect(options.port);
3147
+ if (actualPort !== options.port) {
3148
+ log.warn(`Port ${options.port} is in use, using port ${actualPort}`);
3149
+ }
3150
+ const service = new DiagramService(projectDir);
3151
+ const { httpServer, wsManager, closeAllWatchers } = createHttpServer(projectDir, service);
3152
+ const mmdFiles = await service.listFiles();
3153
+ if (mmdFiles.length === 0) {
3154
+ log.warn("No .mmd files found in " + projectDir);
3155
+ log.warn("Run 'smartcode init' to create a sample diagram, or create a .mmd file manually.");
3156
+ }
3157
+ httpServer.listen(actualPort, () => {
3158
+ const url = `http://localhost:${actualPort}`;
3159
+ log.info(`Server running at ${url}`);
3160
+ log.info(`Serving diagrams from ${projectDir}`);
3161
+ if (options.openBrowser) {
3162
+ open(url).catch(() => {
3163
+ log.warn("Could not open browser automatically");
3164
+ });
3165
+ }
3166
+ });
3167
+ await register(projectDir, actualPort).catch((err) => {
3168
+ log.warn("Failed to register workspace:", err instanceof Error ? err.message : err);
3169
+ });
3170
+ process.once("SIGINT", async () => {
3171
+ log.info("Shutting down...");
3172
+ const shutdownTimeout = setTimeout(() => {
3173
+ log.warn("Shutdown timed out after 5s, forcing exit");
3174
+ process.exit(1);
3175
+ }, 5e3);
3176
+ try {
3177
+ await deregister(actualPort).catch(() => {
3178
+ });
3179
+ await closeAllWatchers().catch(() => {
3180
+ });
3181
+ wsManager.close();
3182
+ await new Promise((resolve) => httpServer.close(() => resolve()));
3183
+ } finally {
3184
+ clearTimeout(shutdownTimeout);
3185
+ process.exit(0);
3186
+ }
3187
+ });
3188
+ }
3189
+ var LOCALHOST_PATTERNS, MAX_BODY_SIZE;
3190
+ var init_server = __esm({
3191
+ "src/server/server.ts"() {
3192
+ "use strict";
3193
+ init_service();
3194
+ init_paths();
3195
+ init_logger();
3196
+ init_static();
3197
+ init_routes();
3198
+ init_websocket();
3199
+ init_file_watcher();
3200
+ init_graph_serializer();
3201
+ init_session_store();
3202
+ init_heatmap_routes();
3203
+ init_workspace_registry();
3204
+ LOCALHOST_PATTERNS = [
3205
+ /^https?:\/\/localhost(:\d+)?$/,
3206
+ /^https?:\/\/127\.0\.0\.1(:\d+)?$/,
3207
+ /^https?:\/\/\[::1\](:\d+)?$/
3208
+ ];
3209
+ MAX_BODY_SIZE = 1 * 1024 * 1024;
3210
+ }
3211
+ });
3212
+
3213
+ // src/cli/init.ts
3214
+ var init_exports = {};
3215
+ __export(init_exports, {
3216
+ initProject: () => initProject
3217
+ });
3218
+ import { writeFile as writeFile4, access } from "fs/promises";
3219
+ import path5 from "path";
3220
+ import pc2 from "picocolors";
3221
+ async function initProject(dir, force) {
3222
+ const resolvedDir = path5.resolve(dir);
3223
+ const configPath = path5.join(resolvedDir, ".smartcode.json");
3224
+ const diagramPath = path5.join(resolvedDir, "reasoning.mmd");
3225
+ const exists = await access(configPath).then(() => true).catch(() => false);
3226
+ if (exists && !force) {
3227
+ throw new Error(
3228
+ "Already initialized: .smartcode.json exists. Use --force to reinitialize."
3229
+ );
3230
+ }
3231
+ await writeFile4(configPath, JSON.stringify(DEFAULT_CONFIG2, null, 2) + "\n", "utf-8");
3232
+ const mcpPath = path5.join(resolvedDir, ".mcp.json");
3233
+ const mcpExists = await access(mcpPath).then(() => true).catch(() => false);
3234
+ if (!mcpExists || force) {
3235
+ await writeFile4(mcpPath, JSON.stringify(MCP_CONFIG, null, 2) + "\n", "utf-8");
3236
+ }
3237
+ const diagramExists = await access(diagramPath).then(() => true).catch(() => false);
3238
+ if (!diagramExists || force) {
3239
+ await writeFile4(diagramPath, SAMPLE_DIAGRAM, "utf-8");
3240
+ }
3241
+ log.info(pc2.green("Initialized SmartCode"));
3242
+ log.info(pc2.dim(` ${configPath}`));
3243
+ log.info(pc2.dim(` ${mcpPath}`));
3244
+ log.info(pc2.dim(` ${diagramPath}`));
3245
+ log.info("");
3246
+ log.info(`Open this project in ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} \u2014 SmartCode starts automatically.`);
3247
+ log.info(`Or run ${pc2.cyan("smartcode serve")} to start the viewer manually.`);
3248
+ }
3249
+ var DEFAULT_CONFIG2, MCP_CONFIG, SAMPLE_DIAGRAM;
3250
+ var init_init = __esm({
3251
+ "src/cli/init.ts"() {
3252
+ "use strict";
3253
+ init_logger();
3254
+ DEFAULT_CONFIG2 = {
3255
+ version: 1,
3256
+ diagramDir: ".",
3257
+ port: 3333
3258
+ };
3259
+ MCP_CONFIG = {
3260
+ mcpServers: {
3261
+ "smartcode": {
3262
+ command: "smartcode",
3263
+ args: ["mcp", "--serve"]
3264
+ }
3265
+ }
3266
+ };
3267
+ SAMPLE_DIAGRAM = `flowchart LR
3268
+ Start["Problem Statement"] --> Analyze["Analyze Requirements"]
3269
+ Analyze --> Plan["Create Plan"]
3270
+ Plan --> Implement["Implement Solution"]
3271
+ Implement --> Verify["Verify Results"]
3272
+ Verify --> Done["Complete"]
3273
+ `;
3274
+ }
3275
+ });
3276
+
3277
+ // src/cli/status.ts
3278
+ var status_exports = {};
3279
+ __export(status_exports, {
3280
+ formatUptime: () => formatUptime,
3281
+ showStatus: () => showStatus
3282
+ });
3283
+ import { request } from "http";
3284
+ import pc3 from "picocolors";
3285
+ function formatUptime(seconds) {
3286
+ const hrs = Math.floor(seconds / 3600);
3287
+ const mins = Math.floor(seconds % 3600 / 60);
3288
+ const secs = Math.floor(seconds % 60);
3289
+ const parts = [];
3290
+ if (hrs > 0) parts.push(`${hrs}h`);
3291
+ if (mins > 0) parts.push(`${mins}m`);
3292
+ if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
3293
+ return parts.join(" ");
3294
+ }
3295
+ async function showStatus(port) {
3296
+ try {
3297
+ const data = await fetchStatus(port);
3298
+ log.info(pc3.green("Server is running"));
3299
+ log.info(` Port: ${port}`);
3300
+ log.info(` Uptime: ${formatUptime(data.uptime)}`);
3301
+ log.info(` Project: ${data.projectDir}`);
3302
+ log.info(` Files: ${data.files}`);
3303
+ log.info(` Clients: ${data.connectedClients}`);
3304
+ log.info(` Flags: ${data.activeFlags.length}`);
3305
+ if (data.activeFlags.length > 0) {
3306
+ log.info("");
3307
+ log.info(pc3.yellow("Active Flags:"));
3308
+ for (const flag of data.activeFlags) {
3309
+ log.info(` ${pc3.dim(flag.file)} ${flag.nodeId}: ${flag.message}`);
3310
+ }
3311
+ }
3312
+ } catch {
3313
+ log.info(pc3.red("Server is not running"));
3314
+ log.info(` Tried port: ${port}`);
3315
+ log.info(` Run ${pc3.cyan("smartcode serve")} to start the server`);
3316
+ }
3317
+ }
3318
+ function fetchStatus(port) {
3319
+ return new Promise((resolve, reject) => {
3320
+ const req = request(
3321
+ {
3322
+ hostname: "localhost",
3323
+ port,
3324
+ path: "/api/status",
3325
+ method: "GET",
3326
+ timeout: 3e3
3327
+ },
3328
+ (res) => {
3329
+ const chunks = [];
3330
+ res.on("data", (chunk) => chunks.push(chunk));
3331
+ res.on("end", () => {
3332
+ try {
3333
+ const body = Buffer.concat(chunks).toString("utf-8");
3334
+ resolve(JSON.parse(body));
3335
+ } catch (err) {
3336
+ reject(err);
3337
+ }
3338
+ });
3339
+ }
3340
+ );
3341
+ req.on("error", reject);
3342
+ req.on("timeout", () => {
3343
+ req.destroy();
3344
+ reject(new Error("Request timed out"));
3345
+ });
3346
+ req.end();
3347
+ });
3348
+ }
3349
+ var init_status = __esm({
3350
+ "src/cli/status.ts"() {
3351
+ "use strict";
3352
+ init_logger();
3353
+ }
3354
+ });
3355
+
3356
+ // src/mcp/schemas.ts
3357
+ import { z } from "zod";
3358
+ var UpdateDiagramInput, ReadFlagsInput, GetDiagramContextInput, UpdateNodeStatusInput, GetCorrectionContextInput, CheckBreakpointsInput, RecordGhostPathInput, CreateMcpSessionInput, StartSessionInput, RecordStepInput, EndSessionInput, SetRiskLevelInput;
3359
+ var init_schemas = __esm({
3360
+ "src/mcp/schemas.ts"() {
3361
+ "use strict";
3362
+ UpdateDiagramInput = {
3363
+ filePath: z.string().describe(
3364
+ 'Relative path to the .mmd file (e.g., "reasoning.mmd")'
3365
+ ),
3366
+ content: z.string().describe("Full Mermaid diagram content"),
3367
+ nodeStatuses: z.record(
3368
+ z.string(),
3369
+ z.enum(["ok", "problem", "in-progress", "discarded"])
3370
+ ).optional().describe(
3371
+ 'Node status colors. Map of nodeId to status. Example: {"ANALYZE": "ok", "PLAN": "in-progress", "DEPLOY": "problem"}'
3372
+ ),
3373
+ riskLevels: z.record(
3374
+ z.string(),
3375
+ z.object({
3376
+ level: z.enum(["high", "medium", "low"]),
3377
+ reason: z.string()
3378
+ })
3379
+ ).optional().describe(
3380
+ 'Risk assessments per node. Example: {"DEPLOY": {"level": "high", "reason": "touches production DB"}}'
3381
+ ),
3382
+ ghostPaths: z.array(
3383
+ z.object({
3384
+ from: z.string(),
3385
+ to: z.string(),
3386
+ label: z.string().optional()
3387
+ })
3388
+ ).optional().describe(
3389
+ 'Rejected alternatives. Example: [{"from": "A", "to": "C", "label": "Skipped: too risky"}]'
3390
+ )
3391
+ };
3392
+ ReadFlagsInput = {
3393
+ filePath: z.string().describe(
3394
+ "Relative path to the .mmd file to read flags from"
3395
+ )
3396
+ };
3397
+ GetDiagramContextInput = {
3398
+ filePath: z.string().describe(
3399
+ "Relative path to the .mmd file to get context for"
3400
+ )
3401
+ };
3402
+ UpdateNodeStatusInput = {
3403
+ filePath: z.string().describe("Relative path to the .mmd file"),
3404
+ nodeId: z.string().describe("ID of the node to update status for"),
3405
+ status: z.enum(["ok", "problem", "in-progress", "discarded"]).describe("Status to set on the node")
3406
+ };
3407
+ GetCorrectionContextInput = {
3408
+ filePath: z.string().describe("Relative path to the .mmd file"),
3409
+ nodeId: z.string().describe("ID of the flagged node to get correction context for")
3410
+ };
3411
+ CheckBreakpointsInput = {
3412
+ filePath: z.string().describe("Relative path to the .mmd file"),
3413
+ currentNodeId: z.string().describe("Node ID the AI is currently processing")
3414
+ };
3415
+ RecordGhostPathInput = {
3416
+ filePath: z.string().describe("Relative path to the .mmd file"),
3417
+ fromNodeId: z.string().describe("Source node of the discarded path"),
3418
+ toNodeId: z.string().describe("Target node of the discarded path"),
3419
+ label: z.string().optional().describe("Optional reason for discarding this path")
3420
+ };
3421
+ CreateMcpSessionInput = {
3422
+ label: z.string().optional().describe(
3423
+ 'Human-readable label for this session (e.g., "Debug auth bug"). Auto-generated if not provided.'
3424
+ )
3425
+ };
3426
+ StartSessionInput = {
3427
+ filePath: z.string().describe("Relative path to the .mmd file to record a session for")
3428
+ };
3429
+ RecordStepInput = {
3430
+ sessionId: z.string().describe("ID of the active session to record a step in"),
3431
+ nodeId: z.string().describe("ID of the node being visited"),
3432
+ action: z.string().describe('Description of the action taken (e.g., "analyzed", "modified", "flagged")'),
3433
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Optional metadata about this step")
3434
+ };
3435
+ EndSessionInput = {
3436
+ sessionId: z.string().describe("ID of the active session to end")
3437
+ };
3438
+ SetRiskLevelInput = {
3439
+ filePath: z.string().describe("Relative path to the .mmd file"),
3440
+ nodeId: z.string().describe("ID of the node to set risk level on"),
3441
+ level: z.enum(["high", "medium", "low"]).describe("Risk level: high, medium, or low"),
3442
+ reason: z.string().describe("Reason for the risk assessment")
3443
+ };
3444
+ }
3445
+ });
3446
+
3447
+ // src/mcp/session-tools.ts
3448
+ function registerSessionTools(server, service, options) {
3449
+ server.registerTool(
3450
+ "start_session",
3451
+ {
3452
+ description: "Start a new recording session for a diagram file. Returns a session ID used by record_step and end_session. Sessions are persisted as JSONL files.",
3453
+ inputSchema: StartSessionInput
3454
+ },
3455
+ async ({ filePath }) => {
3456
+ try {
3457
+ const sessionStore = options?.sessionStore;
3458
+ if (!sessionStore) {
3459
+ return {
3460
+ content: [
3461
+ {
3462
+ type: "text",
3463
+ text: "Session recording unavailable: no session store (requires --serve mode)"
3464
+ }
3465
+ ],
3466
+ isError: true
3467
+ };
3468
+ }
3469
+ const sessionId = await sessionStore.startSession(filePath);
3470
+ if (options?.mcpSessionRegistry) {
3471
+ options.mcpSessionRegistry.trackDiagram(filePath).catch(() => {
3472
+ });
3473
+ if (options?.wsManager) {
3474
+ options.wsManager.broadcastAll({ type: "mcp-session:updated" });
3475
+ }
3476
+ }
3477
+ return {
3478
+ content: [
3479
+ {
3480
+ type: "text",
3481
+ text: JSON.stringify({ sessionId, message: "Session started" })
3482
+ }
3483
+ ]
3484
+ };
3485
+ } catch (error) {
3486
+ const message = error instanceof Error ? error.message : "Unknown error";
3487
+ return {
3488
+ content: [{ type: "text", text: message }],
3489
+ isError: true
3490
+ };
3491
+ }
3492
+ }
3493
+ );
3494
+ server.registerTool(
3495
+ "record_step",
3496
+ {
3497
+ description: "Record a step in an active session. Each step represents a node visit with an action description. The event is persisted to the session JSONL file and optionally broadcast via WebSocket.",
3498
+ inputSchema: RecordStepInput
3499
+ },
3500
+ async ({ sessionId, nodeId, action, metadata }) => {
3501
+ try {
3502
+ const sessionStore = options?.sessionStore;
3503
+ if (!sessionStore) {
3504
+ return {
3505
+ content: [
3506
+ {
3507
+ type: "text",
3508
+ text: "Session recording unavailable: no session store (requires --serve mode)"
3509
+ }
3510
+ ],
3511
+ isError: true
3512
+ };
3513
+ }
3514
+ const event = {
3515
+ ts: Date.now(),
3516
+ type: "node:visited",
3517
+ payload: { nodeId, action, ...metadata }
3518
+ };
3519
+ await sessionStore.recordStep(sessionId, event);
3520
+ if (options?.wsManager) {
3521
+ options.wsManager.broadcastAll({
3522
+ type: "session:event",
3523
+ sessionId,
3524
+ event: { ts: event.ts, type: event.type, payload: event.payload }
3525
+ });
3526
+ const activeMeta = sessionStore.getActiveSession(sessionId);
3527
+ if (activeMeta?.diagramFile) {
3528
+ options.wsManager.broadcastAll({
3529
+ type: "heatmap:update",
3530
+ file: activeMeta.diagramFile,
3531
+ data: { [nodeId]: 1 }
3532
+ });
3533
+ }
3534
+ }
3535
+ return {
3536
+ content: [
3537
+ {
3538
+ type: "text",
3539
+ text: JSON.stringify({ recorded: true })
3540
+ }
3541
+ ]
3542
+ };
3543
+ } catch (error) {
3544
+ const message = error instanceof Error ? error.message : "Unknown error";
3545
+ return {
3546
+ content: [{ type: "text", text: message }],
3547
+ isError: true
3548
+ };
3549
+ }
3550
+ }
3551
+ );
3552
+ server.registerTool(
3553
+ "end_session",
3554
+ {
3555
+ description: "End an active recording session. Returns a summary with statistics (duration, nodes visited, edges traversed). If a WebSocket manager is available, broadcasts updated heatmap data.",
3556
+ inputSchema: EndSessionInput
3557
+ },
3558
+ async ({ sessionId }) => {
3559
+ try {
3560
+ const sessionStore = options?.sessionStore;
3561
+ if (!sessionStore) {
3562
+ return {
3563
+ content: [
3564
+ {
3565
+ type: "text",
3566
+ text: "Session recording unavailable: no session store (requires --serve mode)"
3567
+ }
3568
+ ],
3569
+ isError: true
3570
+ };
3571
+ }
3572
+ const summary = await sessionStore.endSession(sessionId);
3573
+ if (options?.wsManager && summary.diagramFile) {
3574
+ const heatmapData = await sessionStore.getHeatmapData(summary.diagramFile);
3575
+ options.wsManager.broadcastAll({
3576
+ type: "heatmap:update",
3577
+ file: summary.diagramFile,
3578
+ data: heatmapData
3579
+ });
3580
+ }
3581
+ return {
3582
+ content: [
3583
+ {
3584
+ type: "text",
3585
+ text: JSON.stringify(summary, null, 2)
3586
+ }
3587
+ ]
3588
+ };
3589
+ } catch (error) {
3590
+ const message = error instanceof Error ? error.message : "Unknown error";
3591
+ return {
3592
+ content: [{ type: "text", text: message }],
3593
+ isError: true
3594
+ };
3595
+ }
3596
+ }
3597
+ );
3598
+ server.registerTool(
3599
+ "set_risk_level",
3600
+ {
3601
+ description: "Set a risk level annotation on a diagram node. Risk levels (high, medium, low) are persisted as @risk annotations in the .mmd file and rendered in the browser viewer.",
3602
+ inputSchema: SetRiskLevelInput
3603
+ },
3604
+ async ({ filePath, nodeId, level, reason }) => {
3605
+ try {
3606
+ await service.setRisk(filePath, nodeId, level, reason);
3607
+ if (options?.mcpSessionRegistry) {
3608
+ options.mcpSessionRegistry.trackDiagram(filePath).catch(() => {
3609
+ });
3610
+ if (options?.wsManager) {
3611
+ options.wsManager.broadcastAll({ type: "mcp-session:updated" });
3612
+ }
3613
+ }
3614
+ return {
3615
+ content: [
3616
+ {
3617
+ type: "text",
3618
+ text: JSON.stringify({ set: true, nodeId, level })
3619
+ }
3620
+ ]
3621
+ };
3622
+ } catch (error) {
3623
+ const message = error instanceof Error ? error.message : "Unknown error";
3624
+ return {
3625
+ content: [{ type: "text", text: message }],
3626
+ isError: true
3627
+ };
3628
+ }
3629
+ }
3630
+ );
3631
+ }
3632
+ var init_session_tools = __esm({
3633
+ "src/mcp/session-tools.ts"() {
3634
+ "use strict";
3635
+ init_schemas();
3636
+ }
3637
+ });
3638
+
3639
+ // src/mcp/tools.ts
3640
+ function registerTools(server, service, options) {
3641
+ server.registerTool(
3642
+ "update_diagram",
3643
+ {
3644
+ description: "Create or update a Mermaid diagram (.mmd file) with optional annotations \u2014 all in ONE call. Pass nodeStatuses to color nodes (ok=green, problem=red, in-progress=yellow, discarded=gray). Pass riskLevels to flag risky nodes with reasons. Pass ghostPaths to show rejected alternatives as dashed edges. Changes appear in the browser viewer within 100ms via WebSocket.",
3645
+ inputSchema: UpdateDiagramInput
3646
+ },
3647
+ async ({ filePath, content, nodeStatuses, riskLevels, ghostPaths }) => {
3648
+ try {
3649
+ const statusMap = nodeStatuses ? new Map(Object.entries(nodeStatuses)) : void 0;
3650
+ const riskMap = riskLevels ? new Map(
3651
+ Object.entries(riskLevels).map(([nodeId, r]) => [
3652
+ nodeId,
3653
+ { nodeId, level: r.level, reason: r.reason }
3654
+ ])
3655
+ ) : void 0;
3656
+ const ghostAnnotations = ghostPaths && ghostPaths.length > 0 ? ghostPaths.map((gp) => ({
3657
+ fromNodeId: gp.from,
3658
+ toNodeId: gp.to,
3659
+ label: gp.label ?? ""
3660
+ })) : void 0;
3661
+ await service.writeDiagramPreserving(filePath, content, statusMap, riskMap, ghostAnnotations);
3662
+ if (options?.mcpSessionRegistry) {
3663
+ options.mcpSessionRegistry.trackDiagram(filePath).catch(() => {
3664
+ });
3665
+ if (options?.wsManager) {
3666
+ options.wsManager.broadcastAll({ type: "mcp-session:updated" });
3667
+ }
3668
+ }
3669
+ if (ghostAnnotations && ghostAnnotations.length > 0 && options?.wsManager) {
3670
+ const allGhosts = await service.getGhosts(filePath);
3671
+ options.wsManager.broadcastAll({
3672
+ type: "ghost:update",
3673
+ file: filePath,
3674
+ ghostPaths: allGhosts
3675
+ });
3676
+ }
3677
+ const parts = [`Diagram updated: ${filePath}`];
3678
+ if (statusMap && statusMap.size > 0) parts.push(`${statusMap.size} node statuses set`);
3679
+ if (riskMap && riskMap.size > 0) parts.push(`${riskMap.size} risk levels set`);
3680
+ if (ghostPaths && ghostPaths.length > 0) parts.push(`${ghostPaths.length} ghost paths recorded`);
3681
+ return {
3682
+ content: [{ type: "text", text: parts.join(". ") + "." }]
3683
+ };
3684
+ } catch (error) {
3685
+ const message = error instanceof Error ? error.message : "Unknown error";
3686
+ return {
3687
+ content: [{ type: "text", text: message }],
3688
+ isError: true
3689
+ };
3690
+ }
3691
+ }
3692
+ );
3693
+ server.registerTool(
3694
+ "read_flags",
3695
+ {
3696
+ description: "Read all active developer flags from a .mmd diagram file. Flags are annotations added by developers to signal issues or requests to the AI.",
3697
+ inputSchema: ReadFlagsInput
3698
+ },
3699
+ async ({ filePath }) => {
3700
+ try {
3701
+ const flags = await service.getFlags(filePath);
3702
+ const result = flags.map((f) => ({
3703
+ nodeId: f.nodeId,
3704
+ message: f.message
3705
+ }));
3706
+ return {
3707
+ content: [
3708
+ {
3709
+ type: "text",
3710
+ text: JSON.stringify(result, null, 2)
3711
+ }
3712
+ ]
3713
+ };
3714
+ } catch (error) {
3715
+ const message = error instanceof Error ? error.message : "Unknown error";
3716
+ return {
3717
+ content: [{ type: "text", text: message }],
3718
+ isError: true
3719
+ };
3720
+ }
3721
+ }
3722
+ );
3723
+ server.registerTool(
3724
+ "get_diagram_context",
3725
+ {
3726
+ description: "Get the current state of a diagram including Mermaid content, flags, node statuses, and validation info. Use this to understand the diagram before making changes.",
3727
+ inputSchema: GetDiagramContextInput
3728
+ },
3729
+ async ({ filePath }) => {
3730
+ try {
3731
+ const diagram = await service.readDiagram(filePath);
3732
+ const context = {
3733
+ filePath: diagram.filePath,
3734
+ mermaidContent: diagram.mermaidContent,
3735
+ flags: Array.from(diagram.flags.values()).map((f) => ({
3736
+ nodeId: f.nodeId,
3737
+ message: f.message
3738
+ })),
3739
+ statuses: Object.fromEntries(diagram.statuses),
3740
+ breakpoints: Array.from(diagram.breakpoints),
3741
+ risks: Array.from(diagram.risks.values()).map((r) => ({
3742
+ nodeId: r.nodeId,
3743
+ level: r.level,
3744
+ reason: r.reason
3745
+ })),
3746
+ ghostPaths: diagram.ghosts.map((gp) => ({
3747
+ fromNodeId: gp.fromNodeId,
3748
+ toNodeId: gp.toNodeId,
3749
+ label: gp.label
3750
+ })),
3751
+ validation: {
3752
+ valid: diagram.validation.valid,
3753
+ errors: diagram.validation.errors,
3754
+ diagramType: diagram.validation.diagramType
3755
+ }
3756
+ };
3757
+ return {
3758
+ content: [
3759
+ {
3760
+ type: "text",
3761
+ text: JSON.stringify(context, null, 2)
3762
+ }
3763
+ ]
3764
+ };
3765
+ } catch (error) {
3766
+ const message = error instanceof Error ? error.message : "Unknown error";
3767
+ return {
3768
+ content: [{ type: "text", text: message }],
3769
+ isError: true
3770
+ };
3771
+ }
3772
+ }
3773
+ );
3774
+ server.registerTool(
3775
+ "update_node_status",
3776
+ {
3777
+ description: 'Set the status of a specific node in a diagram. Status values: "ok" (green), "problem" (red), "in-progress" (yellow), "discarded" (gray). Status renders as color in the browser viewer.',
3778
+ inputSchema: UpdateNodeStatusInput
3779
+ },
3780
+ async ({ filePath, nodeId, status }) => {
3781
+ try {
3782
+ await service.setStatus(filePath, nodeId, status);
3783
+ if (options?.mcpSessionRegistry) {
3784
+ options.mcpSessionRegistry.trackDiagram(filePath).catch(() => {
3785
+ });
3786
+ if (options?.wsManager) {
3787
+ options.wsManager.broadcastAll({ type: "mcp-session:updated" });
3788
+ }
3789
+ }
3790
+ return {
3791
+ content: [
3792
+ {
3793
+ type: "text",
3794
+ text: `Node "${nodeId}" status set to "${status}" in ${filePath}`
3795
+ }
3796
+ ]
3797
+ };
3798
+ } catch (error) {
3799
+ const message = error instanceof Error ? error.message : "Unknown error";
3800
+ return {
3801
+ content: [{ type: "text", text: message }],
3802
+ isError: true
3803
+ };
3804
+ }
3805
+ }
3806
+ );
3807
+ server.registerTool(
3808
+ "get_correction_context",
3809
+ {
3810
+ description: "Get structured correction context for a flagged diagram node. Returns the flag message, node context (statuses, other flags), the full diagram content, and a natural language instruction for making corrections. Use this when a developer has flagged a node for correction.",
3811
+ inputSchema: GetCorrectionContextInput
3812
+ },
3813
+ async ({ filePath, nodeId }) => {
3814
+ try {
3815
+ const diagram = await service.readDiagram(filePath);
3816
+ const flag = diagram.flags.get(nodeId);
3817
+ if (!flag) {
3818
+ return {
3819
+ content: [
3820
+ {
3821
+ type: "text",
3822
+ text: `No flag found on node "${nodeId}" in ${filePath}`
3823
+ }
3824
+ ],
3825
+ isError: true
3826
+ };
3827
+ }
3828
+ const context = {
3829
+ correction: {
3830
+ nodeId: flag.nodeId,
3831
+ flagMessage: flag.message
3832
+ },
3833
+ diagramState: {
3834
+ filePath,
3835
+ mermaidContent: diagram.mermaidContent,
3836
+ allFlags: Array.from(diagram.flags.values()).map((f) => ({
3837
+ nodeId: f.nodeId,
3838
+ message: f.message
3839
+ })),
3840
+ statuses: Object.fromEntries(diagram.statuses)
3841
+ },
3842
+ instruction: `The developer flagged node "${nodeId}" with the message: "${flag.message}". Review the diagram and update it to address this feedback. Use the update_diagram tool to write the corrected Mermaid content.`
3843
+ };
3844
+ return {
3845
+ content: [
3846
+ {
3847
+ type: "text",
3848
+ text: JSON.stringify(context, null, 2)
3849
+ }
3850
+ ]
3851
+ };
3852
+ } catch (error) {
3853
+ const message = error instanceof Error ? error.message : "Unknown error";
3854
+ return {
3855
+ content: [{ type: "text", text: message }],
3856
+ isError: true
3857
+ };
3858
+ }
3859
+ }
3860
+ );
3861
+ server.registerTool(
3862
+ "check_breakpoints",
3863
+ {
3864
+ description: 'Check if the current node has a breakpoint set. Returns "pause" if a breakpoint exists and no continue signal is pending, "continue" otherwise. The AI should respect the pause signal by waiting and re-checking.',
3865
+ inputSchema: CheckBreakpointsInput
3866
+ },
3867
+ async ({ filePath, currentNodeId }) => {
3868
+ try {
3869
+ const breakpoints = await service.getBreakpoints(filePath);
3870
+ if (breakpoints.has(currentNodeId)) {
3871
+ const signalKey = `${filePath}:${currentNodeId}`;
3872
+ const continueSignals = options?.breakpointContinueSignals;
3873
+ if (continueSignals && continueSignals.has(signalKey)) {
3874
+ continueSignals.delete(signalKey);
3875
+ if (options?.wsManager) {
3876
+ options.wsManager.broadcastAll({
3877
+ type: "breakpoint:continue",
3878
+ file: filePath,
3879
+ nodeId: currentNodeId
3880
+ });
3881
+ }
3882
+ return {
3883
+ content: [{ type: "text", text: "continue" }]
3884
+ };
3885
+ }
3886
+ if (options?.wsManager) {
3887
+ options.wsManager.broadcastAll({
3888
+ type: "breakpoint:hit",
3889
+ file: filePath,
3890
+ nodeId: currentNodeId
3891
+ });
3892
+ }
3893
+ return {
3894
+ content: [{ type: "text", text: "pause" }]
3895
+ };
3896
+ }
3897
+ return {
3898
+ content: [{ type: "text", text: "continue" }]
3899
+ };
3900
+ } catch (error) {
3901
+ const message = error instanceof Error ? error.message : "Unknown error";
3902
+ return {
3903
+ content: [{ type: "text", text: message }],
3904
+ isError: true
3905
+ };
3906
+ }
3907
+ }
3908
+ );
3909
+ server.registerTool(
3910
+ "record_ghost_path",
3911
+ {
3912
+ description: "Record a discarded reasoning branch as a ghost path. Ghost paths are displayed as dashed translucent edges in the browser viewer.",
3913
+ inputSchema: RecordGhostPathInput
3914
+ },
3915
+ async ({ filePath, fromNodeId, toNodeId, label }) => {
3916
+ try {
3917
+ if (options?.mcpSessionRegistry) {
3918
+ options.mcpSessionRegistry.trackDiagram(filePath).catch(() => {
3919
+ });
3920
+ if (options?.wsManager) {
3921
+ options.wsManager.broadcastAll({ type: "mcp-session:updated" });
3922
+ }
3923
+ }
3924
+ await service.addGhost(filePath, fromNodeId, toNodeId, label ?? "");
3925
+ if (options?.wsManager) {
3926
+ const allGhosts = await service.getGhosts(filePath);
3927
+ options.wsManager.broadcastAll({
3928
+ type: "ghost:update",
3929
+ file: filePath,
3930
+ ghostPaths: allGhosts
3931
+ });
3932
+ }
3933
+ return {
3934
+ content: [
3935
+ {
3936
+ type: "text",
3937
+ text: `Ghost path recorded: ${fromNodeId} -> ${toNodeId}`
3938
+ }
3939
+ ]
3940
+ };
3941
+ } catch (error) {
3942
+ const message = error instanceof Error ? error.message : "Unknown error";
3943
+ return {
3944
+ content: [{ type: "text", text: message }],
3945
+ isError: true
3946
+ };
3947
+ }
3948
+ }
3949
+ );
3950
+ server.registerTool(
3951
+ "create_mcp_session",
3952
+ {
3953
+ description: "Start a new MCP session to group diagrams for this conversation. Call at the start of each conversation to keep diagrams organized.",
3954
+ inputSchema: CreateMcpSessionInput
3955
+ },
3956
+ async ({ label }) => {
3957
+ try {
3958
+ if (!options?.mcpSessionRegistry) {
3959
+ return {
3960
+ content: [{ type: "text", text: "MCP sessions require --serve mode" }],
3961
+ isError: true
3962
+ };
3963
+ }
3964
+ const sessionId = await options.mcpSessionRegistry.createSession(label);
3965
+ if (options?.wsManager) {
3966
+ options.wsManager.broadcastAll({ type: "mcp-session:updated" });
3967
+ }
3968
+ return {
3969
+ content: [{ type: "text", text: `Session created: ${sessionId} (label: ${label || "auto"})` }]
3970
+ };
3971
+ } catch (error) {
3972
+ const message = error instanceof Error ? error.message : "Unknown error";
3973
+ return {
3974
+ content: [{ type: "text", text: message }],
3975
+ isError: true
3976
+ };
3977
+ }
3978
+ }
3979
+ );
3980
+ registerSessionTools(server, service, {
3981
+ sessionStore: options?.sessionStore,
3982
+ wsManager: options?.wsManager,
3983
+ mcpSessionRegistry: options?.mcpSessionRegistry
3984
+ });
3985
+ }
3986
+ var init_tools = __esm({
3987
+ "src/mcp/tools.ts"() {
3988
+ "use strict";
3989
+ init_schemas();
3990
+ init_session_tools();
3991
+ }
3992
+ });
3993
+
3994
+ // src/mcp/resources.ts
3995
+ import {
3996
+ ResourceTemplate
3997
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
3998
+ function registerResources(server, service) {
3999
+ server.registerResource(
4000
+ "diagram-list",
4001
+ "smartcode://diagrams",
4002
+ {
4003
+ title: "Available Diagrams",
4004
+ description: "List of all .mmd diagram files in the project",
4005
+ mimeType: "application/json"
4006
+ },
4007
+ async (uri) => {
4008
+ const files = await service.listFiles();
4009
+ return {
4010
+ contents: [
4011
+ {
4012
+ uri: uri.href,
4013
+ mimeType: "application/json",
4014
+ text: JSON.stringify({ files })
4015
+ }
4016
+ ]
4017
+ };
4018
+ }
4019
+ );
4020
+ const diagramTemplate = new ResourceTemplate(
4021
+ "smartcode://diagrams/{filePath}",
4022
+ {
4023
+ list: async () => {
4024
+ const files = await service.listFiles();
4025
+ return {
4026
+ resources: files.map((f) => ({
4027
+ uri: `smartcode://diagrams/${encodeURIComponent(f)}`,
4028
+ name: f
4029
+ }))
4030
+ };
4031
+ }
4032
+ }
4033
+ );
4034
+ server.registerResource(
4035
+ "diagram-content",
4036
+ diagramTemplate,
4037
+ {
4038
+ title: "Diagram Content",
4039
+ description: "Content of a specific .mmd diagram file",
4040
+ mimeType: "text/plain"
4041
+ },
4042
+ async (_uri, variables) => {
4043
+ try {
4044
+ const filePath = decodeURIComponent(
4045
+ String(variables.filePath)
4046
+ );
4047
+ const diagram = await service.readDiagram(filePath);
4048
+ return {
4049
+ contents: [
4050
+ {
4051
+ uri: _uri.href,
4052
+ mimeType: "text/plain",
4053
+ text: diagram.mermaidContent
4054
+ }
4055
+ ]
4056
+ };
4057
+ } catch {
4058
+ return { contents: [] };
4059
+ }
4060
+ }
4061
+ );
4062
+ }
4063
+ var init_resources = __esm({
4064
+ "src/mcp/resources.ts"() {
4065
+ "use strict";
4066
+ }
4067
+ });
4068
+
4069
+ // src/mcp/instructions.ts
4070
+ var MCP_INSTRUCTIONS;
4071
+ var init_instructions = __esm({
4072
+ "src/mcp/instructions.ts"() {
4073
+ "use strict";
4074
+ MCP_INSTRUCTIONS = `
4075
+ # SmartCode \u2014 Show Your Reasoning Visually
4076
+
4077
+ You have SmartCode. Use it to show the developer your reasoning in real-time
4078
+ as a visual flowchart in their browser. ONE tool call = complete diagram with colors,
4079
+ risks, and rejected alternatives.
4080
+
4081
+ ## Session Management
4082
+
4083
+ At the START of each conversation, call \`create_mcp_session\` with a descriptive label:
4084
+ \`\`\`json
4085
+ { "label": "Debug auth bug" }
4086
+ \`\`\`
4087
+ This groups all diagrams from this conversation into one session visible in the browser.
4088
+ If you forget, diagrams still work but will go into a default session.
4089
+
4090
+ ## When to Use (PROACTIVELY, without being asked)
4091
+
4092
+ - Planning any implementation with 3+ steps
4093
+ - Debugging: show your hypothesis tree
4094
+ - Refactoring: show before/after or decision points
4095
+ - Architecture analysis: show components and data flow
4096
+ - Any task where visualizing your plan prevents wasted effort
4097
+
4098
+ Do NOT use for: one-line fixes, simple answers, obvious tasks.
4099
+
4100
+ ## How to Use: ONE Call Does Everything
4101
+
4102
+ \`update_diagram\` accepts everything at once \u2014 diagram + annotations:
4103
+
4104
+ \`\`\`json
4105
+ {
4106
+ "filePath": "reasoning.mmd",
4107
+ "content": "flowchart TD\\n ANALYZE[\\"Analyze Code\\"] --> PLAN[\\"Create Plan\\"]\\n PLAN --> IMPL[\\"Implement\\"]\\n IMPL --> TEST[\\"Run Tests\\"]",
4108
+ "nodeStatuses": {
4109
+ "ANALYZE": "ok",
4110
+ "PLAN": "in-progress",
4111
+ "IMPL": "problem"
4112
+ },
4113
+ "riskLevels": {
4114
+ "IMPL": { "level": "high", "reason": "Touches auth module, could break login flow" }
4115
+ },
4116
+ "ghostPaths": [
4117
+ { "from": "ANALYZE", "to": "IMPL", "label": "Skip planning: rejected, too complex" }
4118
+ ]
4119
+ }
4120
+ \`\`\`
4121
+
4122
+ ### Status Colors (nodeStatuses)
4123
+ - **"ok"** (green) = verified, working, done
4124
+ - **"in-progress"** (yellow) = currently working on this
4125
+ - **"problem"** (red) = found issue, needs attention
4126
+ - **"discarded"** (gray) = ruled out, not pursuing
4127
+
4128
+ ### Risk Levels (riskLevels)
4129
+ - **"high"** = likely bugs, edge cases, or failures \u2014 ALWAYS explain why
4130
+ - **"medium"** = moderate complexity, worth watching
4131
+ - **"low"** = straightforward
4132
+
4133
+ ### Ghost Paths (ghostPaths)
4134
+ Alternatives you considered but rejected. Include WHY you rejected them.
4135
+
4136
+ ## Diagram Design Rules
4137
+
4138
+ - **Max 15 nodes** \u2014 be concise, not comprehensive
4139
+ - **Short labels** \u2014 "Validate Input" not "Validate all user input fields and return errors"
4140
+ - **Meaningful IDs** \u2014 use \`VALIDATE\` not \`A\` or \`node1\`
4141
+ - **Use subgraphs** to group phases
4142
+ - **TD** for sequential flows, **LR** for comparison/parallel
4143
+
4144
+ ## CRITICAL: Never Block Work for Diagrams
4145
+
4146
+ **NEVER stop your workflow just to update a diagram.** Diagram calls must NOT interrupt your task flow.
4147
+
4148
+ Rules:
4149
+ 1. **Always batch diagram calls with other tool calls** \u2014 if you're about to run a command, edit a file, or read something, include the \`update_diagram\` call in the SAME parallel tool call batch. Never make a dedicated turn just for a diagram.
4150
+ 2. **If you have nothing else to do in this turn**, then a standalone diagram call is OK (e.g., initial plan before starting work, or final summary).
4151
+ 3. **Prefer fewer, bigger updates** over many small ones. Update the diagram 2-3 times max per task: plan, mid-progress, done.
4152
+
4153
+ Good pattern:
4154
+ \`\`\`
4155
+ Turn 1: [update_diagram (plan)] + [read_file] \u2014 parallel
4156
+ Turn 2: [edit_file] + [update_diagram (progress)] \u2014 parallel
4157
+ Turn 3: [run_tests] + [update_diagram (final)] \u2014 parallel
4158
+ \`\`\`
4159
+
4160
+ Bad pattern (NEVER do this):
4161
+ \`\`\`
4162
+ Turn 1: [read_file]
4163
+ Turn 2: [update_diagram] \u2190 BLOCKING! Wasted turn
4164
+ Turn 3: [edit_file]
4165
+ Turn 4: [update_diagram] \u2190 BLOCKING again!
4166
+ \`\`\`
4167
+
4168
+ ## Workflow During a Task
4169
+
4170
+ 1. **Before coding**: Create diagram showing your plan \u2014 batch it with your first read/search call
4171
+ 2. **As you work**: Update statuses \u2014 batch it with your next edit/command call
4172
+ 3. **When done**: Final update with all nodes green (ok) or red (problem) \u2014 batch with summary
4173
+
4174
+ Each update is ONE \`update_diagram\` call \u2014 fast and fluid.
4175
+
4176
+ ## Incremental Updates
4177
+
4178
+ For small changes to an existing diagram, you can also use:
4179
+ - \`update_node_status\` \u2014 change one node's color
4180
+ - \`set_risk_level\` \u2014 add/change one risk assessment
4181
+ - \`record_ghost_path\` \u2014 add one rejected alternative
4182
+
4183
+ But prefer \`update_diagram\` for initial creation and major updates.
4184
+
4185
+ ## Developer Feedback (Flags)
4186
+
4187
+ Use \`read_flags\` to check if the developer flagged any nodes for correction.
4188
+ If flags exist, use \`get_correction_context\` to understand what they want.
4189
+
4190
+ ## Sessions (Optional, for long tasks)
4191
+
4192
+ For tasks spanning many steps, use sessions to generate heatmaps:
4193
+ - \`start_session\` \u2192 \`record_step\` per node \u2192 \`end_session\`
4194
+
4195
+ ## Breakpoints
4196
+
4197
+ If \`check_breakpoints\` returns "pause", STOP and wait for the developer.
4198
+ `.trim();
4199
+ }
4200
+ });
4201
+
4202
+ // src/mcp/server.ts
4203
+ var server_exports2 = {};
4204
+ __export(server_exports2, {
4205
+ createMcpServer: () => createMcpServer,
4206
+ startMcpServer: () => startMcpServer
4207
+ });
4208
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
4209
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4210
+ function createMcpServer(service, deps) {
4211
+ const server = new McpServer2(
4212
+ { name: "smartcode", version: getVersion() },
4213
+ { instructions: MCP_INSTRUCTIONS }
4214
+ );
4215
+ registerTools(server, service, deps);
4216
+ registerResources(server, service);
4217
+ log.debug("MCP server created");
4218
+ return server;
4219
+ }
4220
+ async function startMcpServer(options) {
4221
+ const { resolve } = await import("path");
4222
+ const { DiagramService: DiagramService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
4223
+ const resolvedDir = resolve(options.dir);
4224
+ const service = new DiagramService2(resolvedDir);
4225
+ const transport = new StdioServerTransport();
4226
+ let httpCleanup;
4227
+ let deps;
4228
+ if (options.serve) {
4229
+ const { createHttpServer: createHttpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4230
+ const { detect: detect2 } = await import("detect-port");
4231
+ const preferredPort = options.port ?? 3333;
4232
+ const actualPort = await detect2(preferredPort);
4233
+ if (actualPort !== preferredPort) {
4234
+ log.warn(`Port ${preferredPort} is in use, using port ${actualPort}`);
4235
+ }
4236
+ const { httpServer, wsManager, breakpointContinueSignals, sessionStore, closeAllWatchers } = createHttpServer2(resolvedDir, service);
4237
+ deps = { wsManager, breakpointContinueSignals, sessionStore };
4238
+ await new Promise((resolvePromise) => {
4239
+ httpServer.listen(actualPort, () => {
4240
+ log.info(`HTTP+WS server running at http://localhost:${actualPort}`);
4241
+ resolvePromise();
4242
+ });
4243
+ });
4244
+ await register(resolvedDir, actualPort).catch((err) => {
4245
+ log.warn("Failed to register workspace:", err instanceof Error ? err.message : err);
4246
+ });
4247
+ const mcpRegistry = new McpSessionRegistry(resolvedDir);
4248
+ await mcpRegistry.register().catch((err) => {
4249
+ log.warn("Failed to register MCP session:", err instanceof Error ? err.message : err);
4250
+ });
4251
+ deps.mcpSessionRegistry = mcpRegistry;
4252
+ httpCleanup = async () => {
4253
+ log.info("Shutting down HTTP+WS server...");
4254
+ await deregister(actualPort).catch(() => {
4255
+ });
4256
+ if (deps?.mcpSessionRegistry) {
4257
+ await deps.mcpSessionRegistry.deregister().catch(() => {
4258
+ });
4259
+ }
4260
+ await closeAllWatchers();
4261
+ wsManager.close();
4262
+ await new Promise((resolveClose) => {
4263
+ httpServer.close(() => resolveClose());
4264
+ });
4265
+ };
4266
+ }
4267
+ const server = createMcpServer(service, deps);
4268
+ await server.connect(transport);
4269
+ log.info(`MCP server running on stdio (project: ${resolvedDir})`);
4270
+ const shutdown = async () => {
4271
+ log.info("Shutting down MCP server...");
4272
+ if (httpCleanup) {
4273
+ await httpCleanup();
4274
+ }
4275
+ process.exit(0);
4276
+ };
4277
+ process.stdin.on("end", shutdown);
4278
+ process.on("SIGINT", shutdown);
4279
+ process.on("SIGTERM", shutdown);
4280
+ }
4281
+ var init_server2 = __esm({
4282
+ "src/mcp/server.ts"() {
4283
+ "use strict";
4284
+ init_tools();
4285
+ init_resources();
4286
+ init_instructions();
4287
+ init_logger();
4288
+ init_workspace_registry();
4289
+ init_mcp_session_registry();
4290
+ init_version();
4291
+ }
4292
+ });
4293
+
4294
+ // src/cli.ts
4295
+ init_version();
4296
+ import { Command } from "commander";
4297
+ var program = new Command();
4298
+ program.name("smartcode").description("AI observability diagrams -- see what your AI is thinking").version(getVersion());
4299
+ program.command("serve").description("Start the diagram viewer server").option("-p, --port <number>", "port number", "3333").option("-d, --dir <path>", "project directory", ".").option("--no-open", "do not open browser automatically").action(async (options) => {
4300
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4301
+ await startServer2({
4302
+ port: parseInt(options.port, 10),
4303
+ dir: options.dir,
4304
+ openBrowser: options.open
4305
+ });
4306
+ });
4307
+ program.command("init").description("Initialize a SmartCode project").option("-d, --dir <path>", "project directory", ".").option("-f, --force", "overwrite existing config").action(async (options) => {
4308
+ const { initProject: initProject2 } = await Promise.resolve().then(() => (init_init(), init_exports));
4309
+ await initProject2(options.dir, options.force);
4310
+ });
4311
+ program.command("status").description("Show status of the running SmartCode server").option("-p, --port <number>", "server port to check", "3333").action(async (options) => {
4312
+ const { showStatus: showStatus2 } = await Promise.resolve().then(() => (init_status(), status_exports));
4313
+ await showStatus2(parseInt(options.port, 10));
4314
+ });
4315
+ program.command("mcp").description("Start the MCP server for AI tool integration (stdio transport)").option("-d, --dir <path>", "project directory", ".").option("-s, --serve", "also start HTTP+WS server for browser viewing").option("-p, --port <number>", "HTTP server port (requires --serve)", "3333").action(async (options) => {
4316
+ const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server2(), server_exports2));
4317
+ await startMcpServer2({
4318
+ dir: options.dir,
4319
+ serve: options.serve,
4320
+ port: parseInt(options.port, 10)
4321
+ });
4322
+ });
4323
+ program.parse();
4324
+ //# sourceMappingURL=cli.js.map