@neurynae/toolcairn-mcp 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 (102) hide show
  1. package/bin/toolpilot-mcp.js +5 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +45 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/middleware/event-logger.d.ts +19 -0
  7. package/dist/middleware/event-logger.d.ts.map +1 -0
  8. package/dist/middleware/event-logger.js +138 -0
  9. package/dist/middleware/event-logger.js.map +1 -0
  10. package/dist/schemas.d.ts +2 -0
  11. package/dist/schemas.d.ts.map +1 -0
  12. package/dist/schemas.js +3 -0
  13. package/dist/schemas.js.map +1 -0
  14. package/dist/server.d.ts +3 -0
  15. package/dist/server.d.ts.map +1 -0
  16. package/dist/server.js +116 -0
  17. package/dist/server.js.map +1 -0
  18. package/dist/server.prod.d.ts +14 -0
  19. package/dist/server.prod.d.ts.map +1 -0
  20. package/dist/server.prod.js +127 -0
  21. package/dist/server.prod.js.map +1 -0
  22. package/dist/templates/agent-instructions.d.ts +22 -0
  23. package/dist/templates/agent-instructions.d.ts.map +1 -0
  24. package/dist/templates/agent-instructions.js +155 -0
  25. package/dist/templates/agent-instructions.js.map +1 -0
  26. package/dist/tools/check-compatibility.d.ts +100 -0
  27. package/dist/tools/check-compatibility.d.ts.map +1 -0
  28. package/dist/tools/check-compatibility.js +103 -0
  29. package/dist/tools/check-compatibility.js.map +1 -0
  30. package/dist/tools/check-issue.d.ts +126 -0
  31. package/dist/tools/check-issue.d.ts.map +1 -0
  32. package/dist/tools/check-issue.js +248 -0
  33. package/dist/tools/check-issue.js.map +1 -0
  34. package/dist/tools/classify-prompt.d.ts +101 -0
  35. package/dist/tools/classify-prompt.d.ts.map +1 -0
  36. package/dist/tools/classify-prompt.js +64 -0
  37. package/dist/tools/classify-prompt.js.map +1 -0
  38. package/dist/tools/compare-tools.d.ts +102 -0
  39. package/dist/tools/compare-tools.d.ts.map +1 -0
  40. package/dist/tools/compare-tools.js +178 -0
  41. package/dist/tools/compare-tools.js.map +1 -0
  42. package/dist/tools/format-results.d.ts +44 -0
  43. package/dist/tools/format-results.d.ts.map +1 -0
  44. package/dist/tools/format-results.js +114 -0
  45. package/dist/tools/format-results.js.map +1 -0
  46. package/dist/tools/generate-tracker.d.ts +7 -0
  47. package/dist/tools/generate-tracker.d.ts.map +1 -0
  48. package/dist/tools/generate-tracker.js +408 -0
  49. package/dist/tools/generate-tracker.js.map +1 -0
  50. package/dist/tools/get-stack.d.ts +105 -0
  51. package/dist/tools/get-stack.d.ts.map +1 -0
  52. package/dist/tools/get-stack.js +156 -0
  53. package/dist/tools/get-stack.js.map +1 -0
  54. package/dist/tools/init-project-config.d.ts +107 -0
  55. package/dist/tools/init-project-config.d.ts.map +1 -0
  56. package/dist/tools/init-project-config.js +52 -0
  57. package/dist/tools/init-project-config.js.map +1 -0
  58. package/dist/tools/read-project-config.d.ts +99 -0
  59. package/dist/tools/read-project-config.d.ts.map +1 -0
  60. package/dist/tools/read-project-config.js +78 -0
  61. package/dist/tools/read-project-config.js.map +1 -0
  62. package/dist/tools/refine-requirement.d.ts +105 -0
  63. package/dist/tools/refine-requirement.d.ts.map +1 -0
  64. package/dist/tools/refine-requirement.js +77 -0
  65. package/dist/tools/refine-requirement.js.map +1 -0
  66. package/dist/tools/report-outcome.d.ts +104 -0
  67. package/dist/tools/report-outcome.d.ts.map +1 -0
  68. package/dist/tools/report-outcome.js +108 -0
  69. package/dist/tools/report-outcome.js.map +1 -0
  70. package/dist/tools/search-tools-respond.d.ts +103 -0
  71. package/dist/tools/search-tools-respond.d.ts.map +1 -0
  72. package/dist/tools/search-tools-respond.js +91 -0
  73. package/dist/tools/search-tools-respond.js.map +1 -0
  74. package/dist/tools/search-tools.d.ts +104 -0
  75. package/dist/tools/search-tools.d.ts.map +1 -0
  76. package/dist/tools/search-tools.js +77 -0
  77. package/dist/tools/search-tools.js.map +1 -0
  78. package/dist/tools/suggest-graph-update.d.ts +117 -0
  79. package/dist/tools/suggest-graph-update.d.ts.map +1 -0
  80. package/dist/tools/suggest-graph-update.js +177 -0
  81. package/dist/tools/suggest-graph-update.js.map +1 -0
  82. package/dist/tools/toolpilot-init.d.ts +103 -0
  83. package/dist/tools/toolpilot-init.d.ts.map +1 -0
  84. package/dist/tools/toolpilot-init.js +115 -0
  85. package/dist/tools/toolpilot-init.js.map +1 -0
  86. package/dist/tools/update-project-config.d.ts +104 -0
  87. package/dist/tools/update-project-config.d.ts.map +1 -0
  88. package/dist/tools/update-project-config.js +117 -0
  89. package/dist/tools/update-project-config.js.map +1 -0
  90. package/dist/tools/verify-suggestion.d.ts +113 -0
  91. package/dist/tools/verify-suggestion.d.ts.map +1 -0
  92. package/dist/tools/verify-suggestion.js +223 -0
  93. package/dist/tools/verify-suggestion.js.map +1 -0
  94. package/dist/transport.d.ts +5 -0
  95. package/dist/transport.d.ts.map +1 -0
  96. package/dist/transport.js +13 -0
  97. package/dist/transport.js.map +1 -0
  98. package/dist/utils.d.ts +4 -0
  99. package/dist/utils.d.ts.map +1 -0
  100. package/dist/utils.js +12 -0
  101. package/dist/utils.js.map +1 -0
  102. package/package.json +54 -0
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Generate the standalone tracker.html content.
3
+ * Called by toolpilot_init to produce the HTML file content.
4
+ * The agent writes the returned content to .toolpilot/tracker.html
5
+ */
6
+ export function generateTrackerHtml(eventsPath) {
7
+ return `<!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
12
+ <title>ToolPilot Tracker</title>
13
+ <style>
14
+ :root {
15
+ --bg: #0a0a0f;
16
+ --surface: #12121a;
17
+ --surface2: #1a1a26;
18
+ --border: #2a2a3a;
19
+ --accent: #7c5cfc;
20
+ --accent2: #5b8def;
21
+ --green: #22c55e;
22
+ --red: #ef4444;
23
+ --yellow: #f59e0b;
24
+ --text: #e2e8f0;
25
+ --muted: #64748b;
26
+ --mono: 'JetBrains Mono', 'Fira Code', monospace;
27
+ }
28
+ * { box-sizing: border-box; margin: 0; padding: 0; }
29
+ body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; font-size: 14px; min-height: 100vh; }
30
+
31
+ header { display: flex; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--border); background: var(--surface); }
32
+ header h1 { font-size: 16px; font-weight: 700; letter-spacing: -0.02em; }
33
+ header h1 span { color: var(--accent); }
34
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; margin-left: auto; }
35
+ .status-dot.paused { background: var(--yellow); animation: none; }
36
+ @keyframes pulse { 0%,100%{ opacity:1; } 50%{ opacity:0.4; } }
37
+
38
+ .controls { display: flex; gap: 8px; align-items: center; padding: 12px 24px; border-bottom: 1px solid var(--border); background: var(--surface); }
39
+ .btn { padding: 5px 12px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface2); color: var(--text); cursor: pointer; font-size: 12px; transition: border-color .15s; }
40
+ .btn:hover { border-color: var(--accent); }
41
+ .btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
42
+ input[type=range] { accent-color: var(--accent); }
43
+ .label { color: var(--muted); font-size: 12px; }
44
+
45
+ .metrics { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1px; background: var(--border); border-bottom: 1px solid var(--border); }
46
+ .metric { background: var(--surface); padding: 14px 18px; }
47
+ .metric-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px; }
48
+ .metric-value { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
49
+ .metric-value.green { color: var(--green); }
50
+ .metric-value.red { color: var(--red); }
51
+ .metric-value.accent { color: var(--accent); }
52
+ .metric-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
53
+
54
+ .layout { display: grid; grid-template-columns: 1fr 340px; height: calc(100vh - 140px); }
55
+ .feed { overflow-y: auto; border-right: 1px solid var(--border); }
56
+ .sidebar { overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
57
+
58
+ .event-row { display: grid; grid-template-columns: 80px 160px 1fr auto auto; gap: 12px; align-items: center; padding: 8px 16px; border-bottom: 1px solid #1a1a22; transition: background .1s; cursor: pointer; }
59
+ .event-row:hover { background: var(--surface2); }
60
+ .event-row.selected { background: #1e1a30; }
61
+ .event-row .time { font-family: var(--mono); font-size: 11px; color: var(--muted); }
62
+ .event-row .tool { font-family: var(--mono); font-size: 12px; color: var(--accent); font-weight: 600; }
63
+ .event-row .summary { font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
64
+ .event-row .dur { font-family: var(--mono); font-size: 11px; color: var(--muted); text-align: right; }
65
+ .badge { display: inline-flex; align-items: center; padding: 2px 7px; border-radius: 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
66
+ .badge.ok { background: rgba(34,197,94,.15); color: var(--green); }
67
+ .badge.error { background: rgba(239,68,68,.15); color: var(--red); }
68
+ .badge.warn { background: rgba(245,158,11,.15); color: var(--yellow); }
69
+
70
+ .detail-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px; }
71
+ .detail-card h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 10px; }
72
+ .kv { display: flex; justify-content: space-between; padding: 3px 0; border-bottom: 1px solid #1a1a22; font-size: 12px; }
73
+ .kv:last-child { border-bottom: none; }
74
+ .kv .k { color: var(--muted); }
75
+ .kv .v { font-family: var(--mono); color: var(--text); }
76
+ .kv .v.green { color: var(--green); }
77
+ .kv .v.red { color: var(--red); }
78
+ .kv .v.yellow { color: var(--yellow); }
79
+
80
+ .bar-chart { margin-top: 6px; }
81
+ .bar-row { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; font-size: 11px; }
82
+ .bar-label { width: 120px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }
83
+ .bar-track { flex: 1; height: 6px; background: var(--surface2); border-radius: 3px; }
84
+ .bar-fill { height: 100%; border-radius: 3px; background: var(--accent); transition: width .3s; }
85
+ .bar-count { width: 28px; text-align: right; color: var(--text); }
86
+
87
+ .empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--muted); gap: 8px; }
88
+ .empty svg { opacity: .3; }
89
+ .empty p { font-size: 13px; }
90
+ .empty code { font-family: var(--mono); font-size: 11px; background: var(--surface2); padding: 3px 8px; border-radius: 4px; color: var(--accent); }
91
+
92
+ .insights-list { list-style: none; display: flex; flex-direction: column; gap: 6px; }
93
+ .insight-item { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; font-size: 12px; }
94
+ .insight-item .i-tool { color: var(--accent); font-family: var(--mono); font-weight: 600; }
95
+ .insight-item .i-text { color: var(--muted); margin-top: 2px; }
96
+
97
+ ::-webkit-scrollbar { width: 4px; }
98
+ ::-webkit-scrollbar-track { background: transparent; }
99
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
100
+ </style>
101
+ </head>
102
+ <body>
103
+
104
+ <header>
105
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
106
+ <circle cx="10" cy="10" r="9" stroke="#7c5cfc" stroke-width="1.5"/>
107
+ <path d="M6 10h8M10 6v8" stroke="#7c5cfc" stroke-width="1.5" stroke-linecap="round"/>
108
+ </svg>
109
+ <h1><span>Tool</span>Pilot Tracker</h1>
110
+ <div id="statusText" style="font-size:12px; color:var(--muted);">Loading...</div>
111
+ <div id="statusDot" class="status-dot paused"></div>
112
+ </header>
113
+
114
+ <div class="controls">
115
+ <button class="btn active" id="btnLive" onclick="toggleLive()">⬤ Live</button>
116
+ <button class="btn" id="btnClear" onclick="clearEvents()">Clear</button>
117
+ <span class="label" style="margin-left:8px;">Interval:</span>
118
+ <input type="range" min="1" max="30" value="3" id="intervalSlider" onchange="setInterval_(this.value)" style="width:80px;" />
119
+ <span class="label" id="intervalLabel">3s</span>
120
+ <span style="margin-left:auto; font-size:11px; color:var(--muted);" id="lastRefresh">—</span>
121
+ </div>
122
+
123
+ <div class="metrics" id="metrics">
124
+ <div class="metric"><div class="metric-label">Total Calls</div><div class="metric-value accent" id="mTotal">0</div></div>
125
+ <div class="metric"><div class="metric-label">Success Rate</div><div class="metric-value green" id="mSuccess">—</div></div>
126
+ <div class="metric"><div class="metric-label">Avg Latency</div><div class="metric-value" id="mLatency">—</div></div>
127
+ <div class="metric"><div class="metric-label">Issues Caught</div><div class="metric-value yellow" id="mIssues">0</div><div class="metric-sub">check_issue calls</div></div>
128
+ <div class="metric"><div class="metric-label">Deprecation Warns</div><div class="metric-value yellow" id="mDeprecation">0</div></div>
129
+ <div class="metric"><div class="metric-label">Non-OSS Guided</div><div class="metric-value" id="mNonOss">0</div></div>
130
+ <div class="metric"><div class="metric-label">Graph Updates</div><div class="metric-value accent" id="mGraph">0</div></div>
131
+ </div>
132
+
133
+ <div class="layout">
134
+ <div class="feed" id="feed">
135
+ <div class="empty" id="emptyState">
136
+ <svg width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="18" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M13 20h14M20 13v14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
137
+ <p>Waiting for MCP tool calls...</p>
138
+ <code>Set TOOLPILOT_EVENTS_PATH in your MCP server env</code>
139
+ </div>
140
+ </div>
141
+ <div class="sidebar">
142
+ <div class="detail-card" id="detailPanel" style="display:none">
143
+ <h3>Event Detail</h3>
144
+ <div id="detailContent"></div>
145
+ </div>
146
+ <div class="detail-card">
147
+ <h3>Calls by Tool</h3>
148
+ <div id="toolChart" class="bar-chart"></div>
149
+ </div>
150
+ <div class="detail-card">
151
+ <h3>Recent Insights</h3>
152
+ <ul class="insights-list" id="insightsList"></ul>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <script>
158
+ // ─── Config ──────────────────────────────────────────────────────────────────
159
+ const EVENTS_PATH = ${JSON.stringify(eventsPath)};
160
+
161
+ // ─── State ───────────────────────────────────────────────────────────────────
162
+ let allEvents = [];
163
+ let selectedId = null;
164
+ let isLive = true;
165
+ let pollIntervalMs = 3000;
166
+ let pollHandle = null;
167
+ let lastByteOffset = 0;
168
+
169
+ // ─── Polling ──────────────────────────────────────────────────────────────────
170
+ async function fetchEvents() {
171
+ if (!EVENTS_PATH) return;
172
+ try {
173
+ // Fetch with range header to only get new bytes
174
+ const headers = lastByteOffset > 0 ? { 'Range': \`bytes=\${lastByteOffset}-\` } : {};
175
+ const res = await fetch(\`file://\${EVENTS_PATH}\`, { headers }).catch(() => null);
176
+ if (!res) return;
177
+
178
+ const text = await res.text();
179
+ if (!text.trim()) return;
180
+
181
+ const newLines = text.trim().split('\\n').filter(Boolean);
182
+ let added = 0;
183
+ for (const line of newLines) {
184
+ try {
185
+ const ev = JSON.parse(line);
186
+ if (!allEvents.find(e => e.id === ev.id)) {
187
+ allEvents.push(ev);
188
+ added++;
189
+ }
190
+ } catch {}
191
+ }
192
+
193
+ if (added > 0) {
194
+ allEvents.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
195
+ renderAll();
196
+ }
197
+
198
+ document.getElementById('lastRefresh').textContent = 'Updated ' + new Date().toLocaleTimeString();
199
+ document.getElementById('statusDot').className = 'status-dot' + (isLive ? '' : ' paused');
200
+ document.getElementById('statusText').textContent = \`\${allEvents.length} events\`;
201
+ } catch (e) {
202
+ console.warn('Fetch error', e);
203
+ }
204
+ }
205
+
206
+ function toggleLive() {
207
+ isLive = !isLive;
208
+ document.getElementById('btnLive').className = 'btn' + (isLive ? ' active' : '');
209
+ document.getElementById('statusDot').className = 'status-dot' + (isLive ? '' : ' paused');
210
+ if (isLive) startPolling(); else stopPolling();
211
+ }
212
+
213
+ function clearEvents() {
214
+ allEvents = [];
215
+ selectedId = null;
216
+ renderAll();
217
+ }
218
+
219
+ function setInterval_(v) {
220
+ pollIntervalMs = Number(v) * 1000;
221
+ document.getElementById('intervalLabel').textContent = v + 's';
222
+ if (isLive) { stopPolling(); startPolling(); }
223
+ }
224
+
225
+ function startPolling() {
226
+ if (pollHandle) clearInterval(pollHandle);
227
+ fetchEvents();
228
+ pollHandle = setInterval(fetchEvents, pollIntervalMs);
229
+ }
230
+
231
+ function stopPolling() {
232
+ if (pollHandle) { clearInterval(pollHandle); pollHandle = null; }
233
+ }
234
+
235
+ // ─── Render ───────────────────────────────────────────────────────────────────
236
+ function fmtTime(iso) {
237
+ return new Date(iso).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
238
+ }
239
+
240
+ function toolSummary(ev) {
241
+ const m = ev.metadata || {};
242
+ if (ev.tool_name === 'search_tools' || ev.tool_name === 'search_tools_respond') {
243
+ const parts = [];
244
+ if (m.is_two_option) parts.push('2-option result');
245
+ if (m.had_non_indexed_guidance) parts.push('non-OSS guidance');
246
+ if (m.had_deprecation_warning) parts.push('⚠ deprecated tool');
247
+ if (m.had_credibility_warning) parts.push('⚠ low-stars warning');
248
+ return parts.join(' · ') || m.status || '';
249
+ }
250
+ if (ev.tool_name === 'check_issue') return m.status ? \`status: \${m.status}\` : '';
251
+ if (ev.tool_name === 'suggest_graph_update') {
252
+ if (m.auto_graduated) return '✓ auto-graduated to graph';
253
+ if (m.staged) return 'staged for review';
254
+ return '';
255
+ }
256
+ if (ev.tool_name === 'compare_tools') return m.recommendation ? \`rec: \${m.recommendation}\` : '';
257
+ if (ev.tool_name === 'check_compatibility') return m.compatibility_signal ? m.compatibility_signal : '';
258
+ return m.status || '';
259
+ }
260
+
261
+ function renderFeed() {
262
+ const feed = document.getElementById('feed');
263
+ const empty = document.getElementById('emptyState');
264
+ if (allEvents.length === 0) {
265
+ empty.style.display = 'flex';
266
+ feed.querySelectorAll('.event-row').forEach(r => r.remove());
267
+ return;
268
+ }
269
+ empty.style.display = 'none';
270
+
271
+ // Remove rows not in allEvents
272
+ const existingIds = new Set(Array.from(feed.querySelectorAll('.event-row')).map(r => r.dataset.id));
273
+ const currentIds = new Set(allEvents.map(e => e.id));
274
+ existingIds.forEach(id => { if (!currentIds.has(id)) feed.querySelector(\`[data-id="\${id}"]\`)?.remove(); });
275
+
276
+ // Add new rows at top
277
+ for (const ev of allEvents) {
278
+ if (feed.querySelector(\`[data-id="\${ev.id}"]\`)) continue;
279
+ const row = document.createElement('div');
280
+ row.className = 'event-row' + (selectedId === ev.id ? ' selected' : '');
281
+ row.dataset.id = ev.id;
282
+ row.onclick = () => selectEvent(ev.id);
283
+
284
+ const badgeClass = ev.status === 'ok' ? 'ok' : 'error';
285
+ const summary = toolSummary(ev);
286
+ row.innerHTML = \`
287
+ <span class="time">\${fmtTime(ev.created_at)}</span>
288
+ <span class="tool">\${ev.tool_name}</span>
289
+ <span class="summary">\${summary}</span>
290
+ <span class="dur">\${ev.duration_ms}ms</span>
291
+ <span class="badge \${badgeClass}">\${ev.status}</span>
292
+ \`;
293
+
294
+ // Insert in chronological order (newest first)
295
+ const firstRow = feed.querySelector('.event-row');
296
+ if (firstRow) feed.insertBefore(row, firstRow);
297
+ else feed.appendChild(row);
298
+ }
299
+ }
300
+
301
+ function renderMetrics() {
302
+ const total = allEvents.length;
303
+ const okCount = allEvents.filter(e => e.status === 'ok').length;
304
+ const avgMs = total > 0 ? Math.round(allEvents.reduce((s, e) => s + e.duration_ms, 0) / total) : 0;
305
+ const issueCount = allEvents.filter(e => e.tool_name === 'check_issue').length;
306
+ const deprecCount = allEvents.filter(e => e.metadata?.had_deprecation_warning).length;
307
+ const nonOssCount = allEvents.filter(e => e.metadata?.had_non_indexed_guidance).length;
308
+ const graphCount = allEvents.filter(e => e.tool_name === 'suggest_graph_update').length;
309
+
310
+ document.getElementById('mTotal').textContent = total;
311
+ document.getElementById('mSuccess').textContent = total > 0 ? Math.round(okCount / total * 100) + '%' : '—';
312
+ document.getElementById('mLatency').textContent = total > 0 ? avgMs + 'ms' : '—';
313
+ document.getElementById('mIssues').textContent = issueCount;
314
+ document.getElementById('mDeprecation').textContent = deprecCount;
315
+ document.getElementById('mNonOss').textContent = nonOssCount;
316
+ document.getElementById('mGraph').textContent = graphCount;
317
+ }
318
+
319
+ function renderToolChart() {
320
+ const counts = {};
321
+ for (const ev of allEvents) counts[ev.tool_name] = (counts[ev.tool_name] || 0) + 1;
322
+ const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 8);
323
+ const max = sorted[0]?.[1] || 1;
324
+ const html = sorted.map(([tool, count]) => \`
325
+ <div class="bar-row">
326
+ <span class="bar-label">\${tool}</span>
327
+ <div class="bar-track"><div class="bar-fill" style="width:\${count/max*100}%"></div></div>
328
+ <span class="bar-count">\${count}</span>
329
+ </div>
330
+ \`).join('');
331
+ document.getElementById('toolChart').innerHTML = html || '<span style="color:var(--muted);font-size:12px">No data yet</span>';
332
+ }
333
+
334
+ function renderInsights() {
335
+ const insights = [];
336
+ for (const ev of allEvents.slice(0, 50)) {
337
+ const m = ev.metadata || {};
338
+ if (ev.tool_name === 'check_issue' && ev.status === 'ok') {
339
+ insights.push({ tool: ev.tool_name, text: 'Issue check ran — may have prevented a debug loop', time: ev.created_at });
340
+ }
341
+ if (m.had_deprecation_warning) {
342
+ insights.push({ tool: ev.tool_name, text: 'Deprecated/unmaintained tool detected in results', time: ev.created_at });
343
+ }
344
+ if (m.auto_graduated) {
345
+ insights.push({ tool: 'suggest_graph_update', text: 'New edge auto-graduated to graph (confidence ≥0.8)', time: ev.created_at });
346
+ }
347
+ if (m.had_non_indexed_guidance) {
348
+ insights.push({ tool: ev.tool_name, text: 'Non-indexed tool detected — non-OSS guidance provided', time: ev.created_at });
349
+ }
350
+ if (m.recommendation) {
351
+ insights.push({ tool: 'compare_tools', text: \`Tool comparison recommended: \${m.recommendation}\`, time: ev.created_at });
352
+ }
353
+ }
354
+ const list = document.getElementById('insightsList');
355
+ if (insights.length === 0) {
356
+ list.innerHTML = '<li style="color:var(--muted);font-size:12px">No insights yet</li>';
357
+ return;
358
+ }
359
+ list.innerHTML = insights.slice(0, 8).map(i => \`
360
+ <li class="insight-item">
361
+ <div class="i-tool">\${i.tool}</div>
362
+ <div class="i-text">\${i.text}</div>
363
+ </li>
364
+ \`).join('');
365
+ }
366
+
367
+ function selectEvent(id) {
368
+ selectedId = id;
369
+ document.querySelectorAll('.event-row').forEach(r => r.classList.toggle('selected', r.dataset.id === id));
370
+ const ev = allEvents.find(e => e.id === id);
371
+ if (!ev) return;
372
+ const panel = document.getElementById('detailPanel');
373
+ const content = document.getElementById('detailContent');
374
+ panel.style.display = 'block';
375
+ const m = ev.metadata || {};
376
+ const rows = [
377
+ ['Tool', ev.tool_name],
378
+ ['Status', ev.status],
379
+ ['Duration', ev.duration_ms + 'ms'],
380
+ ['Time', new Date(ev.created_at).toLocaleString()],
381
+ ev.query_id ? ['Session ID', ev.query_id.slice(0, 8) + '...'] : null,
382
+ ...Object.entries(m).filter(([k]) => k !== 'tool').map(([k, v]) => [k, String(v)])
383
+ ].filter(Boolean);
384
+ content.innerHTML = rows.map(([k, v]) => {
385
+ const cls = v === 'true' || v === 'ok' ? 'green' : v === 'false' || v === 'error' ? 'red' : '';
386
+ return \`<div class="kv"><span class="k">\${k}</span><span class="v \${cls}">\${v}</span></div>\`;
387
+ }).join('');
388
+ }
389
+
390
+ function renderAll() {
391
+ renderFeed();
392
+ renderMetrics();
393
+ renderToolChart();
394
+ renderInsights();
395
+ }
396
+
397
+ // ─── Boot ─────────────────────────────────────────────────────────────────────
398
+ if (!EVENTS_PATH || EVENTS_PATH === 'null') {
399
+ document.getElementById('statusText').textContent = 'No events path configured';
400
+ document.getElementById('emptyState').querySelector('p').textContent = 'TOOLPILOT_EVENTS_PATH not set in MCP server environment';
401
+ } else {
402
+ startPolling();
403
+ }
404
+ </script>
405
+ </body>
406
+ </html>`;
407
+ }
408
+ //# sourceMappingURL=generate-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-tracker.js","sourceRoot":"","sources":["../../src/tools/generate-tracker.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAkB;IACpD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAwJa,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAuPxC,CAAC;AACT,CAAC"}
@@ -0,0 +1,105 @@
1
+ export declare function handleGetStack(args: {
2
+ use_case: string;
3
+ constraints?: {
4
+ deployment_model?: 'self-hosted' | 'cloud' | 'embedded' | 'serverless';
5
+ language?: string;
6
+ license?: string;
7
+ };
8
+ limit: number;
9
+ }): Promise<{
10
+ [x: string]: unknown;
11
+ content: ({
12
+ type: "text";
13
+ text: string;
14
+ annotations?: {
15
+ audience?: ("user" | "assistant")[] | undefined;
16
+ priority?: number | undefined;
17
+ lastModified?: string | undefined;
18
+ } | undefined;
19
+ _meta?: {
20
+ [x: string]: unknown;
21
+ } | undefined;
22
+ } | {
23
+ type: "image";
24
+ data: string;
25
+ mimeType: string;
26
+ annotations?: {
27
+ audience?: ("user" | "assistant")[] | undefined;
28
+ priority?: number | undefined;
29
+ lastModified?: string | undefined;
30
+ } | undefined;
31
+ _meta?: {
32
+ [x: string]: unknown;
33
+ } | undefined;
34
+ } | {
35
+ type: "audio";
36
+ data: string;
37
+ mimeType: string;
38
+ annotations?: {
39
+ audience?: ("user" | "assistant")[] | undefined;
40
+ priority?: number | undefined;
41
+ lastModified?: string | undefined;
42
+ } | undefined;
43
+ _meta?: {
44
+ [x: string]: unknown;
45
+ } | undefined;
46
+ } | {
47
+ uri: string;
48
+ name: string;
49
+ type: "resource_link";
50
+ description?: string | undefined;
51
+ mimeType?: string | undefined;
52
+ annotations?: {
53
+ audience?: ("user" | "assistant")[] | undefined;
54
+ priority?: number | undefined;
55
+ lastModified?: string | undefined;
56
+ } | undefined;
57
+ _meta?: {
58
+ [x: string]: unknown;
59
+ } | undefined;
60
+ icons?: {
61
+ src: string;
62
+ mimeType?: string | undefined;
63
+ sizes?: string[] | undefined;
64
+ theme?: "light" | "dark" | undefined;
65
+ }[] | undefined;
66
+ title?: string | undefined;
67
+ } | {
68
+ type: "resource";
69
+ resource: {
70
+ uri: string;
71
+ text: string;
72
+ mimeType?: string | undefined;
73
+ _meta?: {
74
+ [x: string]: unknown;
75
+ } | undefined;
76
+ } | {
77
+ uri: string;
78
+ blob: string;
79
+ mimeType?: string | undefined;
80
+ _meta?: {
81
+ [x: string]: unknown;
82
+ } | undefined;
83
+ };
84
+ annotations?: {
85
+ audience?: ("user" | "assistant")[] | undefined;
86
+ priority?: number | undefined;
87
+ lastModified?: string | undefined;
88
+ } | undefined;
89
+ _meta?: {
90
+ [x: string]: unknown;
91
+ } | undefined;
92
+ })[];
93
+ _meta?: {
94
+ [x: string]: unknown;
95
+ progressToken?: string | number | undefined;
96
+ "io.modelcontextprotocol/related-task"?: {
97
+ taskId: string;
98
+ } | undefined;
99
+ } | undefined;
100
+ structuredContent?: {
101
+ [x: string]: unknown;
102
+ } | undefined;
103
+ isError?: boolean | undefined;
104
+ }>;
105
+ //# sourceMappingURL=get-stack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-stack.d.ts","sourceRoot":"","sources":["../../src/tools/get-stack.ts"],"names":[],"mappings":"AAiFA,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE;QACZ,gBAAgB,CAAC,EAAE,aAAa,GAAG,OAAO,GAAG,UAAU,GAAG,YAAY,CAAC;QACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,KAAK,EAAE,MAAM,CAAC;CACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+FA"}
@@ -0,0 +1,156 @@
1
+ import { MemgraphToolRepository } from '@toolpilot/graph';
2
+ import pino from 'pino';
3
+ import { errResult, okResult } from '../utils.js';
4
+ const logger = pino({ name: '@toolpilot/mcp-server:get-stack' });
5
+ const toolRepo = new MemgraphToolRepository();
6
+ /**
7
+ * Detect if a use_case string mentions multiple distinct needs.
8
+ * Maps need-name to list of indicator keywords (all lowercase).
9
+ */
10
+ const NEED_KEYWORDS = {
11
+ auth: ['auth', 'authentication', 'authorization', 'login', 'jwt', 'oauth', 'session'],
12
+ database: ['database', 'db', 'sql', 'orm', 'data', 'storage', 'postgres', 'mysql'],
13
+ queue: ['queue', 'job', 'background', 'worker', 'task', 'cron', 'schedule', 'message'],
14
+ cache: ['cache', 'caching', 'redis', 'memcache', 'fast'],
15
+ search: ['search', 'fulltext', 'index', 'elasticsearch', 'typesense'],
16
+ monitoring: ['monitor', 'log', 'trace', 'metric', 'observ'],
17
+ testing: ['test', 'spec', 'e2e', 'unit'],
18
+ realtime: ['realtime', 'websocket', 'socket', 'live', 'push'],
19
+ };
20
+ /** Stop-words to exclude from keyword extraction */
21
+ const STOP_WORDS = new Set([
22
+ 'a',
23
+ 'an',
24
+ 'the',
25
+ 'for',
26
+ 'to',
27
+ 'of',
28
+ 'in',
29
+ 'on',
30
+ 'at',
31
+ 'by',
32
+ 'i',
33
+ 'is',
34
+ 'it',
35
+ 'be',
36
+ 'as',
37
+ 'do',
38
+ 'so',
39
+ 'or',
40
+ 'and',
41
+ 'but',
42
+ 'not',
43
+ 'with',
44
+ 'that',
45
+ 'this',
46
+ 'from',
47
+ 'use',
48
+ 'need',
49
+ 'want',
50
+ 'build',
51
+ 'my',
52
+ 'me',
53
+ 'we',
54
+ 'our',
55
+ ]);
56
+ /**
57
+ * Split a use_case string into meaningful lowercase keywords,
58
+ * removing punctuation, stop-words, and words under 3 characters.
59
+ */
60
+ function extractKeywords(text) {
61
+ return text
62
+ .toLowerCase()
63
+ .split(/[\s,./\-+:;!?()[\]{}|'"]+/)
64
+ .filter((w) => w.length >= 3 && !STOP_WORDS.has(w));
65
+ }
66
+ /** Deduplicate ToolNode array by name, keeping first occurrence */
67
+ function deduplicateByName(tools) {
68
+ const seen = new Set();
69
+ return tools.filter((t) => {
70
+ if (seen.has(t.name))
71
+ return false;
72
+ seen.add(t.name);
73
+ return true;
74
+ });
75
+ }
76
+ export async function handleGetStack(args) {
77
+ try {
78
+ const { use_case, constraints, limit } = args;
79
+ const lcUseCase = use_case.toLowerCase();
80
+ // Step 1: Detect distinct needs (multi-branch composition)
81
+ const detectedNeeds = Object.entries(NEED_KEYWORDS)
82
+ .filter(([, keywords]) => keywords.some((k) => lcUseCase.includes(k)))
83
+ .map(([need]) => need);
84
+ logger.debug({ use_case, detectedNeeds }, 'Detected needs from use_case');
85
+ let rawTools = [];
86
+ if (detectedNeeds.length > 1) {
87
+ // Multi-branch: get top tools per detected need, then merge
88
+ logger.debug({ detectedNeeds }, 'Multi-branch composition');
89
+ const branchResults = await Promise.all(detectedNeeds.map((need) => toolRepo.findByUseCases([need], 3)));
90
+ const allBranchTools = branchResults.flatMap((r) => (r.ok ? r.data : []));
91
+ rawTools = deduplicateByName(allBranchTools);
92
+ // If too few results, supplement with a general keyword search
93
+ if (rawTools.length < 3) {
94
+ const keywords = extractKeywords(use_case);
95
+ const supplementResult = await toolRepo.findByUseCases(keywords, limit * 2);
96
+ if (supplementResult.ok) {
97
+ rawTools = deduplicateByName([...rawTools, ...supplementResult.data]);
98
+ }
99
+ }
100
+ }
101
+ else {
102
+ // Single need: general keyword extraction
103
+ const keywords = detectedNeeds.length === 1 ? [detectedNeeds[0]] : extractKeywords(use_case);
104
+ logger.debug({ keywords }, 'Single-need keyword search');
105
+ const graphResult = await toolRepo.findByUseCases(keywords, limit * 3);
106
+ if (!graphResult.ok) {
107
+ logger.error({ err: graphResult.error, use_case }, 'get_stack findByUseCases failed');
108
+ return errResult('db_error', graphResult.error.message);
109
+ }
110
+ rawTools = graphResult.data;
111
+ // Fallback: if graph returns fewer than 3 results, supplement with 'other' category
112
+ if (rawTools.length < 3) {
113
+ logger.debug({ count: rawTools.length }, 'Sparse graph results, falling back to category');
114
+ const fallbackResult = await toolRepo.findByCategories(['other']);
115
+ if (fallbackResult.ok) {
116
+ rawTools = deduplicateByName([...rawTools, ...fallbackResult.data]);
117
+ }
118
+ }
119
+ }
120
+ // Step 2: Apply constraints
121
+ let tools = rawTools;
122
+ if (constraints) {
123
+ if (constraints.deployment_model) {
124
+ const dm = constraints.deployment_model;
125
+ tools = tools.filter((t) => t.deployment_models.includes(dm));
126
+ }
127
+ if (constraints.language) {
128
+ const lang = constraints.language;
129
+ tools = tools.filter((t) => t.language === lang);
130
+ }
131
+ if (constraints.license) {
132
+ const lic = constraints.license;
133
+ tools = tools.filter((t) => t.license === lic);
134
+ }
135
+ }
136
+ // Step 3: Sort by maintenance_score, take top `limit`
137
+ const results = tools
138
+ .sort((a, b) => b.health.maintenance_score - a.health.maintenance_score)
139
+ .slice(0, limit)
140
+ .map((t) => ({
141
+ name: t.name,
142
+ display_name: t.display_name,
143
+ description: t.description,
144
+ category: t.category,
145
+ github_url: t.github_url,
146
+ maintenance_score: t.health.maintenance_score,
147
+ }));
148
+ logger.info({ use_case, detectedNeeds, resultCount: results.length }, 'get_stack complete');
149
+ return okResult({ use_case, tools: results });
150
+ }
151
+ catch (e) {
152
+ logger.error({ err: e }, 'get_stack threw');
153
+ return errResult('internal_error', e instanceof Error ? e.message : String(e));
154
+ }
155
+ }
156
+ //# sourceMappingURL=get-stack.js.map