@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.
- package/README.md +47 -17
- package/compiler/__tests__/executive-dashboards.test.js +285 -0
- package/compiler/__tests__/inject.test.js +128 -0
- package/compiler/__tests__/orchestrators.test.js +151 -0
- package/compiler/__tests__/org-templates.test.js +447 -0
- package/compiler/__tests__/pack-split.test.js +141 -1
- package/compiler/__tests__/parser.test.js +147 -1
- package/compiler/__tests__/scripts-bundling.test.js +10 -11
- package/compiler/__tests__/skill-index.test.js +218 -0
- package/compiler/__tests__/status.test.js +336 -0
- package/compiler/__tests__/templates.test.js +245 -0
- package/compiler/__tests__/visualizer.test.js +325 -0
- package/compiler/adapters/antigravity.js +18 -4
- package/compiler/bin/rune.js +90 -1
- package/compiler/doctor.js +283 -2
- package/compiler/emitter.js +490 -17
- package/compiler/parser.js +255 -4
- package/compiler/status.js +342 -0
- package/compiler/visualizer.js +622 -0
- package/hooks/hooks.json +12 -0
- package/hooks/intent-router/index.cjs +108 -0
- package/hooks/pre-tool-guard/index.cjs +177 -68
- package/package.json +63 -63
- package/skills/autopsy/SKILL.md +48 -1
- package/skills/brainstorm/SKILL.md +2 -0
- package/skills/completion-gate/SKILL.md +26 -1
- package/skills/context-engine/SKILL.md +93 -2
- package/skills/cook/SKILL.md +794 -648
- package/skills/debug/SKILL.md +409 -392
- package/skills/deploy/SKILL.md +2 -0
- package/skills/docs/SKILL.md +28 -3
- package/skills/fix/SKILL.md +284 -281
- package/skills/mcp-builder/SKILL.md +53 -1
- package/skills/onboard/SKILL.md +58 -1
- package/skills/perf/SKILL.md +34 -1
- package/skills/plan/SKILL.md +372 -342
- package/skills/preflight/SKILL.md +396 -360
- package/skills/retro/SKILL.md +95 -1
- package/skills/review/SKILL.md +535 -489
- package/skills/scope-guard/SKILL.md +1 -0
- package/skills/scout/SKILL.md +1 -0
- package/skills/sentinel/SKILL.md +353 -299
- package/skills/sentinel/references/policy-driven-constraints.md +424 -0
- package/skills/session-bridge/SKILL.md +58 -2
- package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
- package/skills/team/SKILL.md +16 -1
- package/skills/test/SKILL.md +587 -585
- package/skills/verification/SKILL.md +1 -0
- 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">×</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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
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",
|