@rune-kit/rune 2.4.0 → 2.7.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 (49) hide show
  1. package/README.md +47 -17
  2. package/compiler/__tests__/executive-dashboards.test.js +285 -0
  3. package/compiler/__tests__/inject.test.js +128 -0
  4. package/compiler/__tests__/orchestrators.test.js +151 -0
  5. package/compiler/__tests__/org-templates.test.js +447 -0
  6. package/compiler/__tests__/pack-split.test.js +141 -1
  7. package/compiler/__tests__/parser.test.js +147 -1
  8. package/compiler/__tests__/scripts-bundling.test.js +10 -11
  9. package/compiler/__tests__/skill-index.test.js +218 -0
  10. package/compiler/__tests__/status.test.js +336 -0
  11. package/compiler/__tests__/templates.test.js +245 -0
  12. package/compiler/__tests__/visualizer.test.js +325 -0
  13. package/compiler/adapters/antigravity.js +18 -4
  14. package/compiler/bin/rune.js +90 -1
  15. package/compiler/doctor.js +283 -2
  16. package/compiler/emitter.js +490 -17
  17. package/compiler/parser.js +255 -4
  18. package/compiler/status.js +342 -0
  19. package/compiler/visualizer.js +622 -0
  20. package/hooks/hooks.json +12 -0
  21. package/hooks/intent-router/index.cjs +108 -0
  22. package/hooks/pre-tool-guard/index.cjs +177 -68
  23. package/package.json +63 -63
  24. package/skills/autopsy/SKILL.md +48 -1
  25. package/skills/brainstorm/SKILL.md +2 -0
  26. package/skills/completion-gate/SKILL.md +26 -1
  27. package/skills/context-engine/SKILL.md +93 -2
  28. package/skills/cook/SKILL.md +794 -648
  29. package/skills/debug/SKILL.md +409 -392
  30. package/skills/deploy/SKILL.md +2 -0
  31. package/skills/docs/SKILL.md +28 -3
  32. package/skills/fix/SKILL.md +284 -281
  33. package/skills/mcp-builder/SKILL.md +53 -1
  34. package/skills/onboard/SKILL.md +58 -1
  35. package/skills/perf/SKILL.md +34 -1
  36. package/skills/plan/SKILL.md +372 -342
  37. package/skills/preflight/SKILL.md +396 -360
  38. package/skills/retro/SKILL.md +95 -1
  39. package/skills/review/SKILL.md +535 -489
  40. package/skills/scope-guard/SKILL.md +1 -0
  41. package/skills/scout/SKILL.md +1 -0
  42. package/skills/sentinel/SKILL.md +353 -299
  43. package/skills/sentinel/references/policy-driven-constraints.md +424 -0
  44. package/skills/session-bridge/SKILL.md +58 -2
  45. package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
  46. package/skills/team/SKILL.md +16 -1
  47. package/skills/test/SKILL.md +587 -585
  48. package/skills/verification/SKILL.md +1 -0
  49. package/skills/watchdog/SKILL.md +2 -0
@@ -0,0 +1,622 @@
1
+ /**
2
+ * Visualizer — Interactive Mesh Graph
3
+ *
4
+ * Generates a self-contained HTML file with a force-directed graph
5
+ * of all skills, connections, and signals. No CDN dependencies.
6
+ */
7
+
8
+ import { existsSync } from 'node:fs';
9
+ import { readdir, readFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { parseSkill } from './parser.js';
12
+
13
+ // ─── Data Collection ───
14
+
15
+ export async function collectGraphData(runeRoot, tierSources = {}) {
16
+ const skillsDir = path.join(runeRoot, 'skills');
17
+ const extDir = path.join(runeRoot, 'extensions');
18
+
19
+ const nodes = [];
20
+ const edges = [];
21
+ const signalEdges = [];
22
+ const signalMap = {};
23
+
24
+ // Parse core skills
25
+ if (existsSync(skillsDir)) {
26
+ const entries = await readdir(skillsDir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (!entry.isDirectory()) continue;
29
+ const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
30
+ if (!existsSync(skillFile)) continue;
31
+
32
+ const content = await readFile(skillFile, 'utf-8');
33
+ const parsed = parseSkill(content, skillFile);
34
+
35
+ nodes.push({
36
+ id: parsed.name,
37
+ layer: parsed.layer || 'L3',
38
+ group: parsed.group || '',
39
+ description: (parsed.description || '').slice(0, 120),
40
+ tier: 'free',
41
+ connections: [...new Set(parsed.crossRefs.map((r) => r.skillName))],
42
+ signals: parsed.signals || null,
43
+ });
44
+
45
+ // Cross-ref edges (skip self-references)
46
+ const refs = [...new Set(parsed.crossRefs.map((r) => r.skillName))];
47
+ for (const target of refs) {
48
+ if (target !== parsed.name) {
49
+ edges.push({ source: parsed.name, target, type: 'crossref' });
50
+ }
51
+ }
52
+
53
+ // Signal edges
54
+ if (parsed.signals) {
55
+ for (const sig of parsed.signals.emit) {
56
+ if (!signalMap[sig]) signalMap[sig] = { emitters: [], listeners: [] };
57
+ signalMap[sig].emitters.push(parsed.name);
58
+ }
59
+ for (const sig of parsed.signals.listen) {
60
+ if (!signalMap[sig]) signalMap[sig] = { emitters: [], listeners: [] };
61
+ signalMap[sig].listeners.push(parsed.name);
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ // Scan extension packs as L4 nodes
68
+ const packDirs = [
69
+ { dir: extDir, tier: 'free' },
70
+ { dir: tierSources.pro, tier: 'pro' },
71
+ { dir: tierSources.business, tier: 'business' },
72
+ ];
73
+
74
+ for (const { dir, tier } of packDirs) {
75
+ if (!dir || !existsSync(dir)) continue;
76
+ const entries = await readdir(dir, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ if (!entry.isDirectory()) continue;
79
+ const packFile = path.join(dir, entry.name, 'PACK.md');
80
+ if (!existsSync(packFile)) continue;
81
+
82
+ const baseName = entry.name.replace(/^(pro|business)-/, '');
83
+ const nodeId = `pack:${baseName}`;
84
+
85
+ // Avoid duplicate pack nodes (tier override)
86
+ if (!nodes.find((n) => n.id === nodeId)) {
87
+ nodes.push({
88
+ id: nodeId,
89
+ layer: 'L4',
90
+ group: 'extension',
91
+ description: `${tier} pack: ${baseName}`,
92
+ tier,
93
+ connections: [],
94
+ signals: null,
95
+ });
96
+ }
97
+ }
98
+ }
99
+
100
+ // Build signal edges from signal map
101
+ for (const [sigName, { emitters, listeners }] of Object.entries(signalMap)) {
102
+ for (const emitter of emitters) {
103
+ for (const listener of listeners) {
104
+ signalEdges.push({ source: emitter, target: listener, signal: sigName });
105
+ }
106
+ }
107
+ }
108
+
109
+ return {
110
+ nodes,
111
+ edges,
112
+ signalEdges,
113
+ signals: Object.keys(signalMap),
114
+ stats: {
115
+ nodeCount: nodes.length,
116
+ edgeCount: edges.length,
117
+ signalEdgeCount: signalEdges.length,
118
+ signalCount: Object.keys(signalMap).length,
119
+ },
120
+ };
121
+ }
122
+
123
+ // ─── HTML Generation ───
124
+
125
+ export function generateMeshHTML(graphData) {
126
+ const dataJson = JSON.stringify(graphData);
127
+
128
+ return `<!DOCTYPE html>
129
+ <html lang="en">
130
+ <head>
131
+ <meta charset="UTF-8">
132
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
133
+ <title>Rune Mesh Visualizer</title>
134
+ <style>
135
+ * { margin: 0; padding: 0; box-sizing: border-box; }
136
+ body {
137
+ background: #0a0a1a;
138
+ color: #e2e8f0;
139
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
140
+ overflow: hidden;
141
+ height: 100vh;
142
+ }
143
+
144
+ /* ─── Top Bar ─── */
145
+ .topbar {
146
+ position: fixed; top: 0; left: 0; right: 0; z-index: 10;
147
+ display: flex; align-items: center; gap: 12px;
148
+ padding: 10px 16px;
149
+ background: rgba(10,10,26,0.92);
150
+ backdrop-filter: blur(8px);
151
+ border-bottom: 1px solid rgba(255,255,255,0.06);
152
+ }
153
+ .topbar h1 { font-size: 14px; font-weight: 600; white-space: nowrap; }
154
+ .topbar .stats { font-size: 12px; color: #64748b; margin-left: 8px; }
155
+ .search-box {
156
+ padding: 5px 10px; border-radius: 6px; border: 1px solid #334155;
157
+ background: #1e293b; color: #e2e8f0; font-size: 12px; width: 180px;
158
+ outline: none;
159
+ }
160
+ .search-box:focus { border-color: #6366f1; }
161
+ .filter-group { display: flex; gap: 4px; margin-left: auto; }
162
+ .filter-btn {
163
+ padding: 3px 10px; border-radius: 12px; border: 1px solid #334155;
164
+ background: transparent; color: #94a3b8; font-size: 11px; cursor: pointer;
165
+ transition: all 0.15s;
166
+ }
167
+ .filter-btn:hover { border-color: #6366f1; color: #e2e8f0; }
168
+ .filter-btn.active { background: var(--btn-color, #6366f1); border-color: var(--btn-color, #6366f1); color: #fff; }
169
+
170
+ /* ─── Canvas ─── */
171
+ #canvas {
172
+ position: fixed; top: 44px; left: 0; right: 0; bottom: 0;
173
+ cursor: grab;
174
+ }
175
+ #canvas:active { cursor: grabbing; }
176
+
177
+ /* ─── Detail Panel ─── */
178
+ .detail-panel {
179
+ position: fixed; right: -320px; top: 44px; bottom: 0; width: 300px;
180
+ background: rgba(15,23,42,0.95); backdrop-filter: blur(12px);
181
+ border-left: 1px solid rgba(255,255,255,0.08);
182
+ padding: 20px; overflow-y: auto;
183
+ transition: right 0.25s ease;
184
+ z-index: 5;
185
+ }
186
+ .detail-panel.open { right: 0; }
187
+ .detail-panel h2 { font-size: 16px; margin-bottom: 4px; }
188
+ .detail-panel .layer-badge {
189
+ display: inline-block; padding: 2px 8px; border-radius: 8px;
190
+ font-size: 11px; font-weight: 600; margin-bottom: 12px;
191
+ }
192
+ .detail-panel .desc { font-size: 13px; color: #94a3b8; line-height: 1.5; margin-bottom: 16px; }
193
+ .detail-panel h3 { font-size: 12px; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; margin: 12px 0 6px; }
194
+ .detail-panel .conn-list { list-style: none; }
195
+ .detail-panel .conn-list li {
196
+ font-size: 13px; padding: 3px 0; color: #cbd5e1; cursor: pointer;
197
+ }
198
+ .detail-panel .conn-list li:hover { color: #6366f1; }
199
+ .detail-panel .signal-tag {
200
+ display: inline-block; padding: 2px 8px; border-radius: 4px;
201
+ font-size: 11px; margin: 2px; background: rgba(99,102,241,0.15); color: #a5b4fc;
202
+ }
203
+ .detail-panel .close-btn {
204
+ position: absolute; top: 12px; right: 12px; background: none; border: none;
205
+ color: #64748b; font-size: 18px; cursor: pointer;
206
+ }
207
+
208
+ /* ─── Legend ─── */
209
+ .legend {
210
+ position: fixed; bottom: 12px; left: 12px; z-index: 5;
211
+ display: flex; gap: 12px; font-size: 11px; color: #64748b;
212
+ }
213
+ .legend-item { display: flex; align-items: center; gap: 4px; }
214
+ .legend-dot { width: 8px; height: 8px; border-radius: 50%; }
215
+ .legend-line { width: 20px; height: 2px; }
216
+ .legend-line.dashed { background: repeating-linear-gradient(90deg, #f59e0b 0 4px, transparent 4px 8px); }
217
+
218
+ @media print { body { background: #fff; color: #000; } }
219
+ </style>
220
+ </head>
221
+ <body>
222
+
223
+ <div class="topbar">
224
+ <h1>Rune Mesh</h1>
225
+ <span class="stats" id="stats"></span>
226
+ <input class="search-box" id="search" placeholder="Search skills..." autocomplete="off">
227
+ <div class="filter-group" id="filters"></div>
228
+ </div>
229
+
230
+ <canvas id="canvas"></canvas>
231
+
232
+ <div class="detail-panel" id="detail">
233
+ <button class="close-btn" id="detail-close">&times;</button>
234
+ <div id="detail-content"></div>
235
+ </div>
236
+
237
+ <div class="legend">
238
+ <div class="legend-item"><div class="legend-dot" style="background:#f59e0b"></div> L0 Router</div>
239
+ <div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div> L1 Orchestrator</div>
240
+ <div class="legend-item"><div class="legend-dot" style="background:#6366f1"></div> L2 Workflow</div>
241
+ <div class="legend-item"><div class="legend-dot" style="background:#10b981"></div> L3 Utility</div>
242
+ <div class="legend-item"><div class="legend-dot" style="background:#8b5cf6"></div> L4 Extension</div>
243
+ <div class="legend-item"><div class="legend-line" style="background:#475569"></div> Cross-ref</div>
244
+ <div class="legend-item"><div class="legend-line dashed"></div> Signal</div>
245
+ </div>
246
+
247
+ <script>
248
+ // ─── Data ───
249
+ const DATA = ${dataJson};
250
+
251
+ // ─── Config ───
252
+ const LAYER_COLORS = {
253
+ L0: '#f59e0b', L1: '#ef4444', L2: '#6366f1', L3: '#10b981', L4: '#8b5cf6'
254
+ };
255
+ const LAYER_RADIUS = { L0: 0, L1: 110, L2: 220, L3: 320, L4: 410 };
256
+ const TIER_BORDER = { free: null, pro: '#f59e0b', business: '#8b5cf6' };
257
+ const NODE_SIZES = { L0: 28, L1: 18, L2: 14, L3: 11, L4: 9 };
258
+
259
+ // ─── State ───
260
+ let nodes = [];
261
+ let cam = { x: 0, y: 0, zoom: 1 };
262
+ let drag = null;
263
+ let hoveredNode = null;
264
+ let selectedNode = null;
265
+ let activeFilters = new Set(['L0','L1','L2','L3','L4']);
266
+ let searchTerm = '';
267
+
268
+ // ─── Init ───
269
+ const canvas = document.getElementById('canvas');
270
+ const ctx = canvas.getContext('2d');
271
+ const dpr = window.devicePixelRatio || 1;
272
+
273
+ function resize() {
274
+ canvas.width = window.innerWidth * dpr;
275
+ canvas.height = (window.innerHeight - 44) * dpr;
276
+ canvas.style.width = window.innerWidth + 'px';
277
+ canvas.style.height = (window.innerHeight - 44) + 'px';
278
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
279
+ draw();
280
+ }
281
+ window.addEventListener('resize', resize);
282
+
283
+ // ─── Layout: Concentric Rings ───
284
+ function layoutNodes() {
285
+ const cx = window.innerWidth / 2;
286
+ const cy = (window.innerHeight - 44) / 2;
287
+
288
+ const byLayer = {};
289
+ for (const n of DATA.nodes) {
290
+ const l = n.layer || 'L3';
291
+ if (!byLayer[l]) byLayer[l] = [];
292
+ byLayer[l].push(n);
293
+ }
294
+
295
+ nodes = [];
296
+ for (const [layer, layerNodes] of Object.entries(byLayer)) {
297
+ const r = LAYER_RADIUS[layer] || 350;
298
+ const count = layerNodes.length;
299
+ const angleStep = (2 * Math.PI) / Math.max(count, 1);
300
+ const startAngle = -Math.PI / 2;
301
+
302
+ layerNodes.forEach((n, i) => {
303
+ const angle = startAngle + i * angleStep;
304
+ nodes.push({
305
+ ...n,
306
+ x: cx + r * Math.cos(angle),
307
+ y: cy + r * Math.sin(angle),
308
+ r: NODE_SIZES[layer] || 12,
309
+ color: LAYER_COLORS[layer] || '#6366f1',
310
+ visible: true,
311
+ highlighted: false,
312
+ });
313
+ });
314
+ }
315
+ }
316
+
317
+ function getNode(id) { return nodes.find(n => n.id === id); }
318
+
319
+ // ─── Filtering ───
320
+ function applyFilters() {
321
+ const term = searchTerm.toLowerCase();
322
+ for (const n of nodes) {
323
+ const layerMatch = activeFilters.has(n.layer);
324
+ const searchMatch = !term || n.id.toLowerCase().includes(term) ||
325
+ (n.description && n.description.toLowerCase().includes(term));
326
+ n.visible = layerMatch && searchMatch;
327
+ }
328
+ draw();
329
+ }
330
+
331
+ // ─── Drawing ───
332
+ function screenX(x) { return (x + cam.x) * cam.zoom; }
333
+ function screenY(y) { return (y + cam.y) * cam.zoom; }
334
+
335
+ function draw() {
336
+ const w = window.innerWidth;
337
+ const h = window.innerHeight - 44;
338
+ ctx.clearRect(0, 0, w, h);
339
+
340
+ ctx.save();
341
+ ctx.translate(cam.x * cam.zoom, cam.y * cam.zoom);
342
+ ctx.scale(cam.zoom, cam.zoom);
343
+
344
+ // Draw cross-ref edges
345
+ for (const e of DATA.edges) {
346
+ const src = getNode(e.source);
347
+ const tgt = getNode(e.target);
348
+ if (!src || !tgt || !src.visible || !tgt.visible) continue;
349
+
350
+ const isHighlighted = hoveredNode &&
351
+ (hoveredNode.id === e.source || hoveredNode.id === e.target);
352
+
353
+ ctx.beginPath();
354
+ ctx.moveTo(src.x, src.y);
355
+ // Quadratic bezier toward center for curvature
356
+ const mx = (src.x + tgt.x) / 2;
357
+ const my = (src.y + tgt.y) / 2;
358
+ const cx2 = mx * 0.95 + w / 2 * 0.05;
359
+ const cy2 = my * 0.95 + h / 2 * 0.05;
360
+ ctx.quadraticCurveTo(cx2, cy2, tgt.x, tgt.y);
361
+ ctx.strokeStyle = isHighlighted ? 'rgba(99,102,241,0.5)' : 'rgba(71,85,105,0.15)';
362
+ ctx.lineWidth = isHighlighted ? 1.5 : 0.5;
363
+ ctx.stroke();
364
+ }
365
+
366
+ // Draw signal edges (dashed)
367
+ for (const e of DATA.signalEdges) {
368
+ const src = getNode(e.source);
369
+ const tgt = getNode(e.target);
370
+ if (!src || !tgt || !src.visible || !tgt.visible) continue;
371
+
372
+ const isHighlighted = hoveredNode &&
373
+ (hoveredNode.id === e.source || hoveredNode.id === e.target);
374
+ if (!isHighlighted) continue; // Only show signal edges on hover
375
+
376
+ ctx.beginPath();
377
+ ctx.setLineDash([4, 4]);
378
+ ctx.moveTo(src.x, src.y);
379
+ ctx.lineTo(tgt.x, tgt.y);
380
+ ctx.strokeStyle = 'rgba(245,158,11,0.6)';
381
+ ctx.lineWidth = 1.5;
382
+ ctx.stroke();
383
+ ctx.setLineDash([]);
384
+ }
385
+
386
+ // Draw nodes
387
+ for (const n of nodes) {
388
+ if (!n.visible) continue;
389
+
390
+ const isDimmed = hoveredNode && !n.highlighted && n.id !== hoveredNode.id;
391
+ const alpha = isDimmed ? 0.15 : 1;
392
+ const isSelected = selectedNode && selectedNode.id === n.id;
393
+
394
+ // Glow for hovered/selected
395
+ if ((n.id === hoveredNode?.id || isSelected) && !isDimmed) {
396
+ ctx.beginPath();
397
+ ctx.arc(n.x, n.y, n.r + 6, 0, Math.PI * 2);
398
+ ctx.fillStyle = n.color + '22';
399
+ ctx.fill();
400
+ }
401
+
402
+ // Node circle
403
+ ctx.beginPath();
404
+ ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
405
+ ctx.fillStyle = '#0f172a';
406
+ ctx.fill();
407
+ ctx.strokeStyle = isDimmed ? n.color + '33' : n.color;
408
+ ctx.lineWidth = isSelected ? 3 : (n.tier !== 'free' ? 2.5 : 1.5);
409
+ ctx.stroke();
410
+ ctx.globalAlpha = alpha;
411
+
412
+ // Tier indicator (double ring for paid)
413
+ if (TIER_BORDER[n.tier] && !isDimmed) {
414
+ ctx.beginPath();
415
+ ctx.arc(n.x, n.y, n.r + 3, 0, Math.PI * 2);
416
+ ctx.strokeStyle = TIER_BORDER[n.tier] + '88';
417
+ ctx.lineWidth = 1;
418
+ ctx.setLineDash([3, 3]);
419
+ ctx.stroke();
420
+ ctx.setLineDash([]);
421
+ }
422
+
423
+ // Label
424
+ ctx.fillStyle = isDimmed ? '#475569' : '#e2e8f0';
425
+ ctx.font = (n.r >= 14 ? '10px' : '8px') + ' -apple-system, sans-serif';
426
+ ctx.textAlign = 'center';
427
+ ctx.textBaseline = 'middle';
428
+
429
+ const label = n.id.replace('pack:', '');
430
+ if (n.r >= 14) {
431
+ ctx.fillText(label, n.x, n.y);
432
+ } else {
433
+ // Small nodes: label below
434
+ ctx.fillText(label, n.x, n.y + n.r + 10);
435
+ }
436
+
437
+ ctx.globalAlpha = 1;
438
+ }
439
+
440
+ ctx.restore();
441
+ }
442
+
443
+ // ─── Interaction ───
444
+ function worldPos(e) {
445
+ const rect = canvas.getBoundingClientRect();
446
+ return {
447
+ x: (e.clientX - rect.left) / cam.zoom - cam.x,
448
+ y: (e.clientY - rect.top) / cam.zoom - cam.y
449
+ };
450
+ }
451
+
452
+ function hitTest(wx, wy) {
453
+ for (let i = nodes.length - 1; i >= 0; i--) {
454
+ const n = nodes[i];
455
+ if (!n.visible) continue;
456
+ const dx = wx - n.x, dy = wy - n.y;
457
+ if (dx * dx + dy * dy < (n.r + 4) * (n.r + 4)) return n;
458
+ }
459
+ return null;
460
+ }
461
+
462
+ function highlightConnected(node) {
463
+ const connected = new Set();
464
+ if (!node) { nodes.forEach(n => n.highlighted = false); return; }
465
+
466
+ connected.add(node.id);
467
+ for (const e of DATA.edges) {
468
+ if (e.source === node.id) connected.add(e.target);
469
+ if (e.target === node.id) connected.add(e.source);
470
+ }
471
+ for (const e of DATA.signalEdges) {
472
+ if (e.source === node.id) connected.add(e.target);
473
+ if (e.target === node.id) connected.add(e.source);
474
+ }
475
+ nodes.forEach(n => n.highlighted = connected.has(n.id));
476
+ }
477
+
478
+ canvas.addEventListener('mousemove', (e) => {
479
+ if (drag) {
480
+ cam.x += (e.clientX - drag.x) / cam.zoom;
481
+ cam.y += (e.clientY - drag.y) / cam.zoom;
482
+ drag = { x: e.clientX, y: e.clientY };
483
+ draw();
484
+ return;
485
+ }
486
+
487
+ const { x, y } = worldPos(e);
488
+ const hit = hitTest(x, y);
489
+ if (hit !== hoveredNode) {
490
+ hoveredNode = hit;
491
+ highlightConnected(hit);
492
+ canvas.style.cursor = hit ? 'pointer' : 'grab';
493
+ draw();
494
+ }
495
+ });
496
+
497
+ canvas.addEventListener('mousedown', (e) => {
498
+ const { x, y } = worldPos(e);
499
+ const hit = hitTest(x, y);
500
+ if (hit) {
501
+ selectedNode = hit;
502
+ showDetail(hit);
503
+ draw();
504
+ } else {
505
+ drag = { x: e.clientX, y: e.clientY };
506
+ }
507
+ });
508
+
509
+ canvas.addEventListener('mouseup', () => { drag = null; });
510
+ canvas.addEventListener('mouseleave', () => {
511
+ drag = null;
512
+ hoveredNode = null;
513
+ highlightConnected(null);
514
+ draw();
515
+ });
516
+
517
+ canvas.addEventListener('wheel', (e) => {
518
+ e.preventDefault();
519
+ const factor = e.deltaY > 0 ? 0.9 : 1.1;
520
+ cam.zoom = Math.max(0.3, Math.min(3, cam.zoom * factor));
521
+ draw();
522
+ }, { passive: false });
523
+
524
+ // ─── Detail Panel ───
525
+ const detailPanel = document.getElementById('detail');
526
+ const detailContent = document.getElementById('detail-content');
527
+
528
+ function showDetail(node) {
529
+ const layerColor = LAYER_COLORS[node.layer] || '#6366f1';
530
+
531
+ const connections = [];
532
+ for (const e of DATA.edges) {
533
+ if (e.source === node.id && getNode(e.target)) connections.push({ dir: '→', target: e.target });
534
+ if (e.target === node.id && getNode(e.source)) connections.push({ dir: '←', target: e.source });
535
+ }
536
+
537
+ const signals = [];
538
+ for (const e of DATA.signalEdges) {
539
+ if (e.source === node.id) signals.push({ dir: 'emit', signal: e.signal, target: e.target });
540
+ if (e.target === node.id) signals.push({ dir: 'listen', signal: e.signal, target: e.source });
541
+ }
542
+
543
+ let html = '<h2>' + esc(node.id.replace('pack:', '')) + '</h2>';
544
+ html += '<span class="layer-badge" style="background:' + layerColor + '22;color:' + layerColor + '">' + node.layer + (node.tier !== 'free' ? ' · ' + node.tier : '') + '</span>';
545
+ if (node.description) html += '<p class="desc">' + esc(node.description) + '</p>';
546
+
547
+ if (connections.length > 0) {
548
+ html += '<h3>Connections (' + connections.length + ')</h3><ul class="conn-list">';
549
+ for (const c of connections) {
550
+ html += '<li onclick="focusNode(\\'' + esc(c.target) + '\\')">' + c.dir + ' ' + esc(c.target) + '</li>';
551
+ }
552
+ html += '</ul>';
553
+ }
554
+
555
+ if (signals.length > 0) {
556
+ const uniqueSignals = [...new Set(signals.map(s => s.signal))];
557
+ html += '<h3>Signals (' + uniqueSignals.length + ')</h3>';
558
+ for (const sig of uniqueSignals) {
559
+ html += '<span class="signal-tag">' + esc(sig) + '</span>';
560
+ }
561
+ }
562
+
563
+ detailContent.innerHTML = html;
564
+ detailPanel.classList.add('open');
565
+ }
566
+
567
+ document.getElementById('detail-close').addEventListener('click', () => {
568
+ detailPanel.classList.remove('open');
569
+ selectedNode = null;
570
+ draw();
571
+ });
572
+
573
+ window.focusNode = function(id) {
574
+ const n = getNode(id);
575
+ if (!n) return;
576
+ selectedNode = n;
577
+ hoveredNode = n;
578
+ highlightConnected(n);
579
+ showDetail(n);
580
+ draw();
581
+ };
582
+
583
+ function esc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
584
+
585
+ // ─── Search & Filters ───
586
+ document.getElementById('search').addEventListener('input', (e) => {
587
+ searchTerm = e.target.value;
588
+ applyFilters();
589
+ });
590
+
591
+ const filtersEl = document.getElementById('filters');
592
+ for (const [layer, color] of Object.entries(LAYER_COLORS)) {
593
+ const btn = document.createElement('button');
594
+ btn.className = 'filter-btn active';
595
+ btn.style.setProperty('--btn-color', color);
596
+ btn.textContent = layer;
597
+ btn.addEventListener('click', () => {
598
+ if (activeFilters.has(layer)) {
599
+ activeFilters.delete(layer);
600
+ btn.classList.remove('active');
601
+ } else {
602
+ activeFilters.add(layer);
603
+ btn.classList.add('active');
604
+ }
605
+ applyFilters();
606
+ });
607
+ filtersEl.appendChild(btn);
608
+ }
609
+
610
+ // ─── Stats ───
611
+ document.getElementById('stats').textContent =
612
+ DATA.stats.nodeCount + ' nodes · ' +
613
+ DATA.stats.edgeCount + ' edges · ' +
614
+ DATA.stats.signalCount + ' signals';
615
+
616
+ // ─── Boot ───
617
+ layoutNodes();
618
+ resize();
619
+ </script>
620
+ </body>
621
+ </html>`;
622
+ }
package/hooks/hooks.json CHANGED
@@ -12,6 +12,18 @@
12
12
  ]
13
13
  }
14
14
  ],
15
+ "UserPromptSubmit": [
16
+ {
17
+ "matcher": ".*",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cjs\" intent-router",
22
+ "async": true
23
+ }
24
+ ]
25
+ }
26
+ ],
15
27
  "PreToolUse": [
16
28
  {
17
29
  "matcher": "Read|Write|Edit",