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.
- package/README.md +147 -0
- package/package.json +65 -0
- package/plugin/.claude-plugin/plugin.json +13 -0
- package/plugin/.mcp.json +12 -0
- package/plugin/CLAUDE.md +10 -0
- package/plugin/commands/recall.md +55 -0
- package/plugin/commands/remember.md +34 -0
- package/plugin/commands/resume.md +45 -0
- package/plugin/commands/stash.md +34 -0
- package/plugin/commands/status.md +33 -0
- package/plugin/dist/analysis/worker.d.ts +1 -0
- package/plugin/dist/analysis/worker.js +233 -0
- package/plugin/dist/analysis/worker.js.map +1 -0
- package/plugin/dist/config-t8LZeB-u.mjs +90 -0
- package/plugin/dist/config-t8LZeB-u.mjs.map +1 -0
- package/plugin/dist/hooks/handler.d.ts +286 -0
- package/plugin/dist/hooks/handler.d.ts.map +1 -0
- package/plugin/dist/hooks/handler.js +2413 -0
- package/plugin/dist/hooks/handler.js.map +1 -0
- package/plugin/dist/index.d.ts +447 -0
- package/plugin/dist/index.d.ts.map +1 -0
- package/plugin/dist/index.js +7334 -0
- package/plugin/dist/index.js.map +1 -0
- package/plugin/dist/observations-CorAAc1A.d.mts +192 -0
- package/plugin/dist/observations-CorAAc1A.d.mts.map +1 -0
- package/plugin/dist/tool-registry-e710BvXq.mjs +3574 -0
- package/plugin/dist/tool-registry-e710BvXq.mjs.map +1 -0
- package/plugin/hooks/hooks.json +78 -0
- package/plugin/laminark.db +0 -0
- package/plugin/package.json +17 -0
- package/plugin/scripts/README.md +65 -0
- package/plugin/scripts/bump-version.sh +42 -0
- package/plugin/scripts/dev-sync.sh +58 -0
- package/plugin/scripts/ensure-deps.sh +15 -0
- package/plugin/scripts/install.sh +139 -0
- package/plugin/scripts/local-install.sh +138 -0
- package/plugin/scripts/uninstall.sh +133 -0
- package/plugin/scripts/update.sh +39 -0
- package/plugin/scripts/verify-install.sh +87 -0
- package/plugin/skills/status/SKILL.md +6 -0
- package/plugin/ui/activity.js +197 -0
- package/plugin/ui/app.js +1612 -0
- package/plugin/ui/graph.js +2560 -0
- package/plugin/ui/help/activity-feed.png +0 -0
- package/plugin/ui/help/analysis-panel.png +0 -0
- package/plugin/ui/help/graph-toolbar.png +0 -0
- package/plugin/ui/help/graph-view.png +0 -0
- package/plugin/ui/help/settings.png +0 -0
- package/plugin/ui/help/timeline.png +0 -0
- package/plugin/ui/help.js +932 -0
- package/plugin/ui/index.html +756 -0
- package/plugin/ui/settings.js +1414 -0
- package/plugin/ui/styles.css +3856 -0
- package/plugin/ui/timeline.js +652 -0
- 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
|
+
})();
|