laminark 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 (55) hide show
  1. package/README.md +147 -0
  2. package/package.json +65 -0
  3. package/plugin/.claude-plugin/plugin.json +13 -0
  4. package/plugin/.mcp.json +12 -0
  5. package/plugin/CLAUDE.md +10 -0
  6. package/plugin/commands/recall.md +55 -0
  7. package/plugin/commands/remember.md +34 -0
  8. package/plugin/commands/resume.md +45 -0
  9. package/plugin/commands/stash.md +34 -0
  10. package/plugin/commands/status.md +33 -0
  11. package/plugin/dist/analysis/worker.d.ts +1 -0
  12. package/plugin/dist/analysis/worker.js +233 -0
  13. package/plugin/dist/analysis/worker.js.map +1 -0
  14. package/plugin/dist/config-t8LZeB-u.mjs +90 -0
  15. package/plugin/dist/config-t8LZeB-u.mjs.map +1 -0
  16. package/plugin/dist/hooks/handler.d.ts +286 -0
  17. package/plugin/dist/hooks/handler.d.ts.map +1 -0
  18. package/plugin/dist/hooks/handler.js +2413 -0
  19. package/plugin/dist/hooks/handler.js.map +1 -0
  20. package/plugin/dist/index.d.ts +447 -0
  21. package/plugin/dist/index.d.ts.map +1 -0
  22. package/plugin/dist/index.js +7334 -0
  23. package/plugin/dist/index.js.map +1 -0
  24. package/plugin/dist/observations-CorAAc1A.d.mts +192 -0
  25. package/plugin/dist/observations-CorAAc1A.d.mts.map +1 -0
  26. package/plugin/dist/tool-registry-e710BvXq.mjs +3574 -0
  27. package/plugin/dist/tool-registry-e710BvXq.mjs.map +1 -0
  28. package/plugin/hooks/hooks.json +78 -0
  29. package/plugin/laminark.db +0 -0
  30. package/plugin/package.json +17 -0
  31. package/plugin/scripts/README.md +65 -0
  32. package/plugin/scripts/bump-version.sh +42 -0
  33. package/plugin/scripts/dev-sync.sh +58 -0
  34. package/plugin/scripts/ensure-deps.sh +15 -0
  35. package/plugin/scripts/install.sh +139 -0
  36. package/plugin/scripts/local-install.sh +138 -0
  37. package/plugin/scripts/uninstall.sh +133 -0
  38. package/plugin/scripts/update.sh +39 -0
  39. package/plugin/scripts/verify-install.sh +87 -0
  40. package/plugin/skills/status/SKILL.md +6 -0
  41. package/plugin/ui/activity.js +197 -0
  42. package/plugin/ui/app.js +1612 -0
  43. package/plugin/ui/graph.js +2560 -0
  44. package/plugin/ui/help/activity-feed.png +0 -0
  45. package/plugin/ui/help/analysis-panel.png +0 -0
  46. package/plugin/ui/help/graph-toolbar.png +0 -0
  47. package/plugin/ui/help/graph-view.png +0 -0
  48. package/plugin/ui/help/settings.png +0 -0
  49. package/plugin/ui/help/timeline.png +0 -0
  50. package/plugin/ui/help.js +932 -0
  51. package/plugin/ui/index.html +756 -0
  52. package/plugin/ui/settings.js +1414 -0
  53. package/plugin/ui/styles.css +3856 -0
  54. package/plugin/ui/timeline.js +652 -0
  55. package/plugin/ui/tools.js +826 -0
@@ -0,0 +1,826 @@
1
+ /**
2
+ * Laminark Tool Topology Visualization (D3.js)
3
+ *
4
+ * Force-directed graph of tool relationships clustered by server/plugin.
5
+ * Nodes sized by usage, edges from routing patterns and session co-occurrence.
6
+ *
7
+ * @module tools
8
+ */
9
+
10
+ (function () {
11
+ 'use strict';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // State
15
+ // ---------------------------------------------------------------------------
16
+
17
+ var initialized = false;
18
+ var svg, svgGroup, simulation;
19
+ var toolNodes = [];
20
+ var flowEdges = [];
21
+ var clusterHulls = [];
22
+ var showClusters = true;
23
+ var currentLayout = 'force';
24
+ var filterType = '';
25
+ var filterServer = '';
26
+ var selectedTool = null;
27
+
28
+ // Color palette for server clusters
29
+ var CLUSTER_COLORS = [
30
+ '#58a6ff', '#3fb950', '#d2a8ff', '#f0883e', '#f85149',
31
+ '#79c0ff', '#d29922', '#7ee787', '#f778ba', '#a5d6ff',
32
+ ];
33
+
34
+ // Tool type shapes (map to D3 symbols)
35
+ var TOOL_TYPE_ICONS = {
36
+ mcp_server: d3.symbolSquare,
37
+ mcp_tool: d3.symbolCircle,
38
+ slash_command: d3.symbolDiamond,
39
+ skill: d3.symbolStar,
40
+ plugin: d3.symbolTriangle,
41
+ builtin: d3.symbolCross,
42
+ unknown: d3.symbolCircle,
43
+ };
44
+
45
+ var serverColorMap = {};
46
+ var serverColorIdx = 0;
47
+
48
+ function getServerColor(serverName) {
49
+ var key = serverName || '__none__';
50
+ if (!serverColorMap[key]) {
51
+ serverColorMap[key] = CLUSTER_COLORS[serverColorIdx % CLUSTER_COLORS.length];
52
+ serverColorIdx++;
53
+ }
54
+ return serverColorMap[key];
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Display name helper
59
+ // ---------------------------------------------------------------------------
60
+
61
+ function shortName(name) {
62
+ // Strip mcp__ prefix and server name prefix for readability
63
+ return name.replace(/^mcp__[^_]+__/, '').replace(/^mcp__/, '');
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Data fetching
68
+ // ---------------------------------------------------------------------------
69
+
70
+ function fetchTools() {
71
+ return fetch('/api/tools').then(function (r) { return r.json(); }).catch(function () { return { tools: [] }; });
72
+ }
73
+
74
+ function fetchFlows() {
75
+ var params = new URLSearchParams();
76
+ if (window.laminarkState && window.laminarkState.currentProject) {
77
+ params.set('project', window.laminarkState.currentProject);
78
+ }
79
+ var qs = params.toString();
80
+ return fetch('/api/tools/flows' + (qs ? '?' + qs : '')).then(function (r) { return r.json(); }).catch(function () { return { edges: [] }; });
81
+ }
82
+
83
+ function fetchToolStats(name) {
84
+ var params = new URLSearchParams();
85
+ if (window.laminarkState && window.laminarkState.currentProject) {
86
+ params.set('project', window.laminarkState.currentProject);
87
+ }
88
+ var qs = params.toString();
89
+ return fetch('/api/tools/' + encodeURIComponent(name) + '/stats' + (qs ? '?' + qs : ''))
90
+ .then(function (r) { return r.json(); })
91
+ .catch(function () { return null; });
92
+ }
93
+
94
+ function fetchToolSessions() {
95
+ var params = new URLSearchParams();
96
+ if (window.laminarkState && window.laminarkState.currentProject) {
97
+ params.set('project', window.laminarkState.currentProject);
98
+ }
99
+ params.set('limit', '10');
100
+ var qs = params.toString();
101
+ return fetch('/api/tools/sessions' + (qs ? '?' + qs : ''))
102
+ .then(function (r) { return r.json(); })
103
+ .catch(function () { return { sessions: [] }; });
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Init
108
+ // ---------------------------------------------------------------------------
109
+
110
+ function initTools(containerId) {
111
+ if (initialized) return;
112
+ initialized = true;
113
+
114
+ var container = document.getElementById(containerId);
115
+ if (!container) return;
116
+
117
+ var graphArea = container.querySelector('.tools-graph-area');
118
+ if (!graphArea) return;
119
+
120
+ svg = d3.select('#tools-svg');
121
+ svgGroup = svg.append('g').attr('class', 'tools-zoom-group');
122
+
123
+ // Zoom behavior
124
+ var zoom = d3.zoom()
125
+ .scaleExtent([0.1, 6])
126
+ .on('zoom', function (event) {
127
+ svgGroup.attr('transform', event.transform);
128
+ });
129
+ svg.call(zoom);
130
+
131
+ // Layer ordering
132
+ svgGroup.append('g').attr('class', 'hull-layer');
133
+ svgGroup.append('g').attr('class', 'edge-layer');
134
+ svgGroup.append('g').attr('class', 'node-layer');
135
+ svgGroup.append('g').attr('class', 'label-layer');
136
+
137
+ // Resize handler
138
+ function resize() {
139
+ var rect = graphArea.getBoundingClientRect();
140
+ svg.attr('width', rect.width).attr('height', rect.height);
141
+ }
142
+ resize();
143
+ window.addEventListener('resize', resize);
144
+
145
+ // Toolbar event bindings
146
+ initToolbar(container);
147
+
148
+ // Load data
149
+ loadToolData();
150
+ }
151
+
152
+ function initToolbar(container) {
153
+ // Layout buttons
154
+ var layoutBtns = container.querySelectorAll('.tools-layout-btn');
155
+ layoutBtns.forEach(function (btn) {
156
+ btn.addEventListener('click', function () {
157
+ layoutBtns.forEach(function (b) { b.classList.remove('active'); });
158
+ btn.classList.add('active');
159
+ currentLayout = btn.getAttribute('data-layout');
160
+ updateSimulation();
161
+ });
162
+ });
163
+
164
+ // Cluster toggle
165
+ var clusterToggle = document.getElementById('tools-cluster-toggle');
166
+ if (clusterToggle) {
167
+ clusterToggle.addEventListener('change', function () {
168
+ showClusters = clusterToggle.checked;
169
+ renderHulls();
170
+ });
171
+ }
172
+
173
+ // Filter by type
174
+ var typeSelect = document.getElementById('tools-filter-type');
175
+ if (typeSelect) {
176
+ typeSelect.addEventListener('change', function () {
177
+ filterType = typeSelect.value;
178
+ applyFilters();
179
+ });
180
+ }
181
+
182
+ // Filter by server
183
+ var serverSelect = document.getElementById('tools-filter-server');
184
+ if (serverSelect) {
185
+ serverSelect.addEventListener('change', function () {
186
+ filterServer = serverSelect.value;
187
+ applyFilters();
188
+ });
189
+ }
190
+
191
+ // Detail panel close
192
+ var closeBtn = document.getElementById('tools-detail-close');
193
+ if (closeBtn) {
194
+ closeBtn.addEventListener('click', function () {
195
+ var panel = document.getElementById('tools-detail-panel');
196
+ if (panel) panel.classList.add('hidden');
197
+ selectedTool = null;
198
+ // Remove selection highlight
199
+ svgGroup.selectAll('.tool-node').classed('selected', false);
200
+ });
201
+ }
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Data loading and rendering
206
+ // ---------------------------------------------------------------------------
207
+
208
+ function loadToolData() {
209
+ Promise.all([fetchTools(), fetchFlows(), fetchToolSessions()])
210
+ .then(function (results) {
211
+ var toolsData = results[0];
212
+ var flowsData = results[1];
213
+ var sessionsData = results[2];
214
+
215
+ toolNodes = (toolsData.tools || []).map(function (t) {
216
+ return {
217
+ id: t.name,
218
+ name: t.name,
219
+ shortName: shortName(t.name),
220
+ toolType: t.toolType,
221
+ scope: t.scope,
222
+ status: t.status,
223
+ usageCount: t.usageCount || 0,
224
+ serverName: t.serverName,
225
+ description: t.description,
226
+ x: 0,
227
+ y: 0,
228
+ };
229
+ });
230
+
231
+ // Build a name set for filtering edges
232
+ var nameSet = new Set(toolNodes.map(function (n) { return n.id; }));
233
+
234
+ flowEdges = (flowsData.edges || [])
235
+ .filter(function (e) { return nameSet.has(e.source) && nameSet.has(e.target); })
236
+ .map(function (e) {
237
+ return {
238
+ source: e.source,
239
+ target: e.target,
240
+ frequency: e.frequency,
241
+ edgeType: e.edgeType,
242
+ };
243
+ });
244
+
245
+ // Populate server filter dropdown
246
+ populateServerFilter();
247
+
248
+ // Update stats
249
+ updateStats();
250
+
251
+ // Render if non-empty
252
+ if (toolNodes.length === 0) {
253
+ showEmptyState();
254
+ } else {
255
+ renderGraph();
256
+ }
257
+
258
+ // Render session strip
259
+ renderSessionStrip(sessionsData.sessions || []);
260
+ });
261
+ }
262
+
263
+ function populateServerFilter() {
264
+ var serverSelect = document.getElementById('tools-filter-server');
265
+ if (!serverSelect) return;
266
+
267
+ var servers = new Set();
268
+ toolNodes.forEach(function (n) {
269
+ if (n.serverName) servers.add(n.serverName);
270
+ });
271
+
272
+ // Clear existing options except first
273
+ while (serverSelect.options.length > 1) {
274
+ serverSelect.remove(1);
275
+ }
276
+
277
+ Array.from(servers).sort().forEach(function (s) {
278
+ var opt = document.createElement('option');
279
+ opt.value = s;
280
+ opt.textContent = s;
281
+ serverSelect.appendChild(opt);
282
+ });
283
+ }
284
+
285
+ function updateStats() {
286
+ var statsEl = document.getElementById('tools-stats');
287
+ if (statsEl) {
288
+ var visibleNodes = getFilteredNodes();
289
+ var visibleEdges = getFilteredEdges(visibleNodes);
290
+ statsEl.textContent = visibleNodes.length + ' tools, ' + visibleEdges.length + ' flows';
291
+ }
292
+ }
293
+
294
+ function showEmptyState() {
295
+ svgGroup.selectAll('*').remove();
296
+ var rect = svg.node().getBoundingClientRect();
297
+ svgGroup.append('text')
298
+ .attr('x', rect.width / 2)
299
+ .attr('y', rect.height / 2)
300
+ .attr('text-anchor', 'middle')
301
+ .attr('fill', '#8b949e')
302
+ .attr('font-size', '16px')
303
+ .text('No tools discovered yet. Use Claude Code tools to populate this view.');
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // Filtering
308
+ // ---------------------------------------------------------------------------
309
+
310
+ function getFilteredNodes() {
311
+ return toolNodes.filter(function (n) {
312
+ if (filterType && n.toolType !== filterType) return false;
313
+ if (filterServer && n.serverName !== filterServer) return false;
314
+ return true;
315
+ });
316
+ }
317
+
318
+ function getFilteredEdges(nodes) {
319
+ var nodeSet = new Set(nodes.map(function (n) { return n.id; }));
320
+ return flowEdges.filter(function (e) {
321
+ var srcId = typeof e.source === 'object' ? e.source.id : e.source;
322
+ var tgtId = typeof e.target === 'object' ? e.target.id : e.target;
323
+ return nodeSet.has(srcId) && nodeSet.has(tgtId);
324
+ });
325
+ }
326
+
327
+ function applyFilters() {
328
+ renderGraph();
329
+ updateStats();
330
+ }
331
+
332
+ // ---------------------------------------------------------------------------
333
+ // Graph rendering
334
+ // ---------------------------------------------------------------------------
335
+
336
+ function renderGraph() {
337
+ var nodes = getFilteredNodes();
338
+ var edges = getFilteredEdges(nodes);
339
+
340
+ // Size scale
341
+ var maxUsage = d3.max(nodes, function (d) { return d.usageCount; }) || 1;
342
+ var sizeScale = d3.scaleSqrt().domain([0, maxUsage]).range([6, 28]).clamp(true);
343
+
344
+ // Edge thickness scale
345
+ var maxFreq = d3.max(edges, function (d) { return d.frequency; }) || 1;
346
+ var edgeScale = d3.scaleLinear().domain([1, maxFreq]).range([1, 5]).clamp(true);
347
+
348
+ var rect = svg.node().getBoundingClientRect();
349
+ var width = rect.width || 800;
350
+ var height = rect.height || 600;
351
+
352
+ // Clear previous
353
+ svgGroup.select('.hull-layer').selectAll('*').remove();
354
+ svgGroup.select('.edge-layer').selectAll('*').remove();
355
+ svgGroup.select('.node-layer').selectAll('*').remove();
356
+ svgGroup.select('.label-layer').selectAll('*').remove();
357
+
358
+ // Edges
359
+ var edgeSel = svgGroup.select('.edge-layer')
360
+ .selectAll('line')
361
+ .data(edges, function (d) {
362
+ var s = typeof d.source === 'object' ? d.source.id : d.source;
363
+ var t = typeof d.target === 'object' ? d.target.id : d.target;
364
+ return s + '->' + t;
365
+ })
366
+ .join('line')
367
+ .attr('class', 'tool-edge')
368
+ .attr('stroke', function (d) { return d.edgeType === 'pattern' ? '#58a6ff' : '#30363d'; })
369
+ .attr('stroke-width', function (d) { return edgeScale(d.frequency); })
370
+ .attr('stroke-opacity', 0.4)
371
+ .attr('marker-end', 'url(#tool-arrow)');
372
+
373
+ // Arrow marker
374
+ svg.selectAll('defs').remove();
375
+ var defs = svg.append('defs');
376
+ defs.append('marker')
377
+ .attr('id', 'tool-arrow')
378
+ .attr('viewBox', '0 0 10 10')
379
+ .attr('refX', 20)
380
+ .attr('refY', 5)
381
+ .attr('markerWidth', 6)
382
+ .attr('markerHeight', 6)
383
+ .attr('orient', 'auto')
384
+ .append('path')
385
+ .attr('d', 'M0,0 L10,5 L0,10 Z')
386
+ .attr('fill', '#8b949e')
387
+ .attr('fill-opacity', 0.5);
388
+
389
+ // Nodes
390
+ var nodeSel = svgGroup.select('.node-layer')
391
+ .selectAll('path')
392
+ .data(nodes, function (d) { return d.id; })
393
+ .join('path')
394
+ .attr('class', 'tool-node')
395
+ .attr('d', function (d) {
396
+ var size = sizeScale(d.usageCount);
397
+ var symbolType = TOOL_TYPE_ICONS[d.toolType] || TOOL_TYPE_ICONS.unknown;
398
+ return d3.symbol().type(symbolType).size(size * size * 2)();
399
+ })
400
+ .attr('fill', function (d) { return getServerColor(d.serverName); })
401
+ .attr('stroke', function (d) {
402
+ return d.status === 'demoted' ? '#f85149' : d.status === 'stale' ? '#d29922' : 'rgba(255,255,255,0.2)';
403
+ })
404
+ .attr('stroke-width', function (d) {
405
+ return d.status !== 'active' ? 2 : 1;
406
+ })
407
+ .attr('cursor', 'pointer')
408
+ .on('click', function (event, d) {
409
+ event.stopPropagation();
410
+ selectToolNode(d);
411
+ })
412
+ .call(d3.drag()
413
+ .on('start', function (event, d) {
414
+ if (!event.active) simulation.alphaTarget(0.3).restart();
415
+ d.fx = d.x;
416
+ d.fy = d.y;
417
+ })
418
+ .on('drag', function (event, d) {
419
+ d.fx = event.x;
420
+ d.fy = event.y;
421
+ })
422
+ .on('end', function (event, d) {
423
+ if (!event.active) simulation.alphaTarget(0);
424
+ d.fx = null;
425
+ d.fy = null;
426
+ }));
427
+
428
+ // Tooltip on hover
429
+ nodeSel
430
+ .on('mouseenter', function (event, d) {
431
+ d3.select(this).attr('stroke-width', 3).attr('stroke', '#ffffff');
432
+ // Show tooltip
433
+ var tooltip = d3.select('#tools-svg').selectAll('.tool-tooltip').data([d]);
434
+ var tooltipEnter = tooltip.enter().append('g').attr('class', 'tool-tooltip');
435
+ tooltipEnter.append('rect');
436
+ tooltipEnter.append('text');
437
+ var g = tooltip.merge(tooltipEnter);
438
+ var text = g.select('text')
439
+ .text(d.name + (d.usageCount > 0 ? ' (' + d.usageCount + ' uses)' : ''))
440
+ .attr('x', 0).attr('y', 0)
441
+ .attr('fill', '#c9d1d9')
442
+ .attr('font-size', '11px')
443
+ .attr('text-anchor', 'middle');
444
+ var bbox = text.node().getBBox();
445
+ g.select('rect')
446
+ .attr('x', bbox.x - 4).attr('y', bbox.y - 2)
447
+ .attr('width', bbox.width + 8).attr('height', bbox.height + 4)
448
+ .attr('fill', '#161b22').attr('stroke', '#30363d').attr('rx', 3);
449
+ text.raise();
450
+ g.attr('transform', 'translate(' + d.x + ',' + (d.y - sizeScale(d.usageCount) - 12) + ')');
451
+ })
452
+ .on('mouseleave', function (event, d) {
453
+ d3.select(this)
454
+ .attr('stroke-width', d.status !== 'active' ? 2 : 1)
455
+ .attr('stroke', d.status === 'demoted' ? '#f85149' : d.status === 'stale' ? '#d29922' : 'rgba(255,255,255,0.2)');
456
+ svg.selectAll('.tool-tooltip').remove();
457
+ });
458
+
459
+ // Labels
460
+ var labelSel = svgGroup.select('.label-layer')
461
+ .selectAll('text')
462
+ .data(nodes, function (d) { return d.id; })
463
+ .join('text')
464
+ .attr('class', 'tool-label')
465
+ .text(function (d) { return d.shortName; })
466
+ .attr('fill', '#8b949e')
467
+ .attr('font-size', '10px')
468
+ .attr('text-anchor', 'middle')
469
+ .attr('dy', function (d) { return sizeScale(d.usageCount) + 12; })
470
+ .attr('pointer-events', 'none');
471
+
472
+ // Simulation
473
+ simulation = d3.forceSimulation(nodes)
474
+ .force('link', d3.forceLink(edges).id(function (d) { return d.id; }).distance(80).strength(0.3))
475
+ .force('charge', d3.forceManyBody().strength(-120))
476
+ .force('center', d3.forceCenter(width / 2, height / 2))
477
+ .force('collision', d3.forceCollide().radius(function (d) { return sizeScale(d.usageCount) + 8; }))
478
+ .on('tick', function () {
479
+ edgeSel
480
+ .attr('x1', function (d) { return d.source.x; })
481
+ .attr('y1', function (d) { return d.source.y; })
482
+ .attr('x2', function (d) { return d.target.x; })
483
+ .attr('y2', function (d) { return d.target.y; });
484
+
485
+ nodeSel
486
+ .attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; });
487
+
488
+ labelSel
489
+ .attr('x', function (d) { return d.x; })
490
+ .attr('y', function (d) { return d.y; });
491
+
492
+ // Update tooltip position if visible
493
+ svg.selectAll('.tool-tooltip').each(function () {
494
+ var g = d3.select(this);
495
+ var d = g.datum();
496
+ if (d) g.attr('transform', 'translate(' + d.x + ',' + (d.y - sizeScale(d.usageCount) - 12) + ')');
497
+ });
498
+
499
+ // Render cluster hulls
500
+ renderHulls();
501
+ });
502
+
503
+ // Apply cluster forces if layout is 'cluster'
504
+ updateSimulation();
505
+
506
+ // Click on SVG background to deselect
507
+ svg.on('click', function () {
508
+ var panel = document.getElementById('tools-detail-panel');
509
+ if (panel) panel.classList.add('hidden');
510
+ selectedTool = null;
511
+ svgGroup.selectAll('.tool-node').classed('selected', false);
512
+ });
513
+ }
514
+
515
+ function updateSimulation() {
516
+ if (!simulation) return;
517
+ var rect = svg.node().getBoundingClientRect();
518
+ var width = rect.width || 800;
519
+ var height = rect.height || 600;
520
+
521
+ if (currentLayout === 'cluster') {
522
+ // Compute cluster centers per serverName
523
+ var servers = {};
524
+ var nodes = getFilteredNodes();
525
+ nodes.forEach(function (n) {
526
+ var key = n.serverName || '__none__';
527
+ if (!servers[key]) servers[key] = [];
528
+ servers[key].push(n);
529
+ });
530
+ var serverKeys = Object.keys(servers);
531
+ var cols = Math.ceil(Math.sqrt(serverKeys.length));
532
+ var clusterCenters = {};
533
+ serverKeys.forEach(function (key, i) {
534
+ var col = i % cols;
535
+ var row = Math.floor(i / cols);
536
+ clusterCenters[key] = {
537
+ x: (col + 0.5) * (width / cols),
538
+ y: (row + 0.5) * (height / Math.ceil(serverKeys.length / cols)),
539
+ };
540
+ });
541
+
542
+ simulation
543
+ .force('x', d3.forceX(function (d) {
544
+ var key = d.serverName || '__none__';
545
+ return clusterCenters[key] ? clusterCenters[key].x : width / 2;
546
+ }).strength(0.4))
547
+ .force('y', d3.forceY(function (d) {
548
+ var key = d.serverName || '__none__';
549
+ return clusterCenters[key] ? clusterCenters[key].y : height / 2;
550
+ }).strength(0.4))
551
+ .force('center', null);
552
+ } else {
553
+ simulation
554
+ .force('x', null)
555
+ .force('y', null)
556
+ .force('center', d3.forceCenter(width / 2, height / 2));
557
+ }
558
+
559
+ simulation.alpha(0.6).restart();
560
+ }
561
+
562
+ // ---------------------------------------------------------------------------
563
+ // Cluster hulls
564
+ // ---------------------------------------------------------------------------
565
+
566
+ function renderHulls() {
567
+ var hullLayer = svgGroup.select('.hull-layer');
568
+ hullLayer.selectAll('*').remove();
569
+
570
+ if (!showClusters) return;
571
+
572
+ var nodes = getFilteredNodes();
573
+ var groups = {};
574
+ nodes.forEach(function (n) {
575
+ var key = n.serverName || '__none__';
576
+ if (!groups[key]) groups[key] = [];
577
+ groups[key].push([n.x, n.y]);
578
+ });
579
+
580
+ Object.keys(groups).forEach(function (key) {
581
+ var points = groups[key];
582
+ if (points.length < 3) return; // Need at least 3 points for a hull
583
+
584
+ var hull = d3.polygonHull(points);
585
+ if (!hull) return;
586
+
587
+ // Expand hull slightly for padding
588
+ var cx = d3.mean(points, function (p) { return p[0]; });
589
+ var cy = d3.mean(points, function (p) { return p[1]; });
590
+ var expandedHull = hull.map(function (p) {
591
+ var dx = p[0] - cx;
592
+ var dy = p[1] - cy;
593
+ var dist = Math.sqrt(dx * dx + dy * dy);
594
+ var expand = 25;
595
+ return [
596
+ p[0] + (dx / (dist || 1)) * expand,
597
+ p[1] + (dy / (dist || 1)) * expand,
598
+ ];
599
+ });
600
+
601
+ hullLayer.append('path')
602
+ .attr('d', 'M' + expandedHull.join('L') + 'Z')
603
+ .attr('fill', getServerColor(key === '__none__' ? null : key))
604
+ .attr('fill-opacity', 0.06)
605
+ .attr('stroke', getServerColor(key === '__none__' ? null : key))
606
+ .attr('stroke-opacity', 0.15)
607
+ .attr('stroke-width', 1.5)
608
+ .attr('rx', 8);
609
+
610
+ // Cluster label
611
+ hullLayer.append('text')
612
+ .attr('x', cx)
613
+ .attr('y', d3.min(hull, function (p) { return p[1]; }) - 12)
614
+ .attr('text-anchor', 'middle')
615
+ .attr('fill', getServerColor(key === '__none__' ? null : key))
616
+ .attr('fill-opacity', 0.5)
617
+ .attr('font-size', '11px')
618
+ .attr('font-weight', '500')
619
+ .text(key === '__none__' ? 'Other' : key);
620
+ });
621
+ }
622
+
623
+ // ---------------------------------------------------------------------------
624
+ // Detail panel
625
+ // ---------------------------------------------------------------------------
626
+
627
+ function selectToolNode(d) {
628
+ selectedTool = d;
629
+
630
+ // Highlight selected node
631
+ svgGroup.selectAll('.tool-node')
632
+ .classed('selected', function (n) { return n.id === d.id; });
633
+
634
+ // Show panel with loading state
635
+ var panel = document.getElementById('tools-detail-panel');
636
+ var title = document.getElementById('tools-detail-title');
637
+ var body = document.getElementById('tools-detail-body');
638
+ if (!panel || !title || !body) return;
639
+
640
+ title.textContent = d.shortName;
641
+ body.innerHTML = '<p class="empty-state">Loading stats...</p>';
642
+ panel.classList.remove('hidden');
643
+
644
+ // Fetch detailed stats
645
+ fetchToolStats(d.name).then(function (data) {
646
+ if (!data || !data.tool) {
647
+ body.innerHTML = '<p class="empty-state">No stats available</p>';
648
+ return;
649
+ }
650
+
651
+ renderToolDetail(body, data);
652
+ });
653
+ }
654
+
655
+ function renderToolDetail(container, data) {
656
+ container.innerHTML = '';
657
+ var tool = data.tool;
658
+
659
+ // Info section
660
+ var infoSection = document.createElement('div');
661
+ infoSection.className = 'detail-section';
662
+
663
+ var fields = [
664
+ { label: 'Full name', value: tool.name },
665
+ { label: 'Type', value: tool.toolType },
666
+ { label: 'Scope', value: tool.scope },
667
+ { label: 'Status', value: tool.status },
668
+ { label: 'Server', value: tool.serverName || 'N/A' },
669
+ { label: 'Usage count', value: String(tool.usageCount) },
670
+ { label: 'Success rate', value: data.successRate != null ? (data.successRate * 100).toFixed(0) + '%' : 'N/A' },
671
+ { label: 'Sessions used in', value: String(data.sessionsUsedIn) },
672
+ { label: 'Last used', value: tool.lastUsedAt ? new Date(tool.lastUsedAt).toLocaleString() : 'Never' },
673
+ { label: 'Discovered', value: new Date(tool.discoveredAt).toLocaleString() },
674
+ ];
675
+
676
+ fields.forEach(function (f) {
677
+ var row = document.createElement('div');
678
+ row.className = 'detail-field';
679
+ var lbl = document.createElement('span');
680
+ lbl.className = 'field-label';
681
+ lbl.textContent = f.label + ': ';
682
+ var val = document.createElement('span');
683
+ val.className = 'field-value';
684
+ val.textContent = f.value;
685
+ if (f.label === 'Status') {
686
+ val.className = 'tool-status-badge ' + tool.status;
687
+ }
688
+ row.appendChild(lbl);
689
+ row.appendChild(val);
690
+ infoSection.appendChild(row);
691
+ });
692
+
693
+ container.appendChild(infoSection);
694
+
695
+ // Description
696
+ if (tool.description) {
697
+ var descSection = document.createElement('div');
698
+ descSection.className = 'detail-section';
699
+ var descTitle = document.createElement('div');
700
+ descTitle.className = 'detail-section-title';
701
+ descTitle.textContent = 'Description';
702
+ descSection.appendChild(descTitle);
703
+ var descText = document.createElement('p');
704
+ descText.className = 'tool-description';
705
+ descText.textContent = tool.description;
706
+ descSection.appendChild(descText);
707
+ container.appendChild(descSection);
708
+ }
709
+
710
+ // Co-occurring tools
711
+ if (data.coOccurring && data.coOccurring.length > 0) {
712
+ var coSection = document.createElement('div');
713
+ coSection.className = 'detail-section';
714
+ var coTitle = document.createElement('div');
715
+ coTitle.className = 'detail-section-title';
716
+ coTitle.textContent = 'Top co-occurring tools';
717
+ coSection.appendChild(coTitle);
718
+
719
+ data.coOccurring.forEach(function (co) {
720
+ var item = document.createElement('div');
721
+ item.className = 'tool-co-occurring-item';
722
+ item.style.cursor = 'pointer';
723
+
724
+ var name = document.createElement('span');
725
+ name.className = 'tool-co-name';
726
+ name.textContent = shortName(co.name);
727
+
728
+ var count = document.createElement('span');
729
+ count.className = 'tool-co-count';
730
+ count.textContent = co.count + 'x';
731
+
732
+ item.appendChild(name);
733
+ item.appendChild(count);
734
+
735
+ item.addEventListener('click', function () {
736
+ var node = toolNodes.find(function (n) { return n.id === co.name; });
737
+ if (node) selectToolNode(node);
738
+ });
739
+
740
+ coSection.appendChild(item);
741
+ });
742
+
743
+ container.appendChild(coSection);
744
+ }
745
+ }
746
+
747
+ // ---------------------------------------------------------------------------
748
+ // Session flow strip
749
+ // ---------------------------------------------------------------------------
750
+
751
+ function renderSessionStrip(sessions) {
752
+ var content = document.getElementById('tools-session-strip-content');
753
+ if (!content) return;
754
+
755
+ content.innerHTML = '';
756
+
757
+ if (!sessions || sessions.length === 0) {
758
+ content.innerHTML = '<span class="tools-session-empty">No recent sessions with tool usage</span>';
759
+ return;
760
+ }
761
+
762
+ sessions.forEach(function (session) {
763
+ var strip = document.createElement('div');
764
+ strip.className = 'tools-session-item';
765
+
766
+ var label = document.createElement('span');
767
+ label.className = 'tools-session-id';
768
+ label.textContent = session.sessionId.substring(0, 8);
769
+ strip.appendChild(label);
770
+
771
+ var toolsDiv = document.createElement('div');
772
+ toolsDiv.className = 'tools-session-tools';
773
+
774
+ // Deduplicate consecutive same-tool calls and show flow
775
+ var prev = '';
776
+ var displayTools = [];
777
+ session.tools.forEach(function (t) {
778
+ if (t.name !== prev) {
779
+ displayTools.push(t);
780
+ prev = t.name;
781
+ }
782
+ });
783
+
784
+ // Limit display to 15 tools
785
+ var maxDisplay = 15;
786
+ var showing = displayTools.slice(0, maxDisplay);
787
+
788
+ showing.forEach(function (t, i) {
789
+ if (i > 0) {
790
+ var arrow = document.createElement('span');
791
+ arrow.className = 'tools-session-arrow';
792
+ arrow.textContent = '\u2192';
793
+ toolsDiv.appendChild(arrow);
794
+ }
795
+
796
+ var chip = document.createElement('span');
797
+ chip.className = 'tools-session-chip';
798
+ chip.textContent = shortName(t.name);
799
+ chip.style.borderColor = getServerColor(
800
+ toolNodes.find(function (n) { return n.id === t.name; })?.serverName || null
801
+ );
802
+ chip.title = t.name;
803
+ toolsDiv.appendChild(chip);
804
+ });
805
+
806
+ if (displayTools.length > maxDisplay) {
807
+ var more = document.createElement('span');
808
+ more.className = 'tools-session-more';
809
+ more.textContent = '+' + (displayTools.length - maxDisplay) + ' more';
810
+ toolsDiv.appendChild(more);
811
+ }
812
+
813
+ strip.appendChild(toolsDiv);
814
+ content.appendChild(strip);
815
+ });
816
+ }
817
+
818
+ // ---------------------------------------------------------------------------
819
+ // Exports
820
+ // ---------------------------------------------------------------------------
821
+
822
+ window.laminarkTools = {
823
+ initTools: initTools,
824
+ loadToolData: loadToolData,
825
+ };
826
+ })();