@fingerskier/augment 0.2.0 → 0.3.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.
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Dashboard snippets for MCP Apps (SEP-1865, extension `io.modelcontextprotocol/ui`,
3
+ * spec version 2026-01-26). Each snippet is a self-contained HTML5 document served
4
+ * as a predeclared `ui://` resource; UI-capable hosts render it in a sandboxed
5
+ * iframe and stream the tool result in over postMessage JSON-RPC. Templates are
6
+ * compiled-in strings (same no-frontend-build approach as src/dashboard/assets.ts)
7
+ * and work offline under the spec's default CSP (inline script/style only, no
8
+ * network), so the postMessage bridge below is hand-rolled rather than pulled from
9
+ * `@modelcontextprotocol/ext-apps` — the snippet must be a single inline document.
10
+ */
11
+ /** Spec-mandated mime type for MCP Apps HTML templates (mirrors ext-apps RESOURCE_MIME_TYPE). */
12
+ export const UI_MIME_TYPE = "text/html;profile=mcp-app";
13
+ /** Reverse-DNS extension id hosts advertise in initialize capabilities.extensions. */
14
+ export const UI_EXTENSION_ID = "io.modelcontextprotocol/ui";
15
+ export const INSIGHTS_RESOURCE_URI = "ui://augment/insights.html";
16
+ export const GRAPH_RESOURCE_URI = "ui://augment/graph.html";
17
+ /**
18
+ * JSON destined for an inline <script> block. `<` is escaped so memory content
19
+ * containing `</script>` (or `<!--`) cannot break out of the data island;
20
+ * U+2028/U+2029 are escaped because they terminate lines in JS source.
21
+ */
22
+ export function escapeInlineJson(value) {
23
+ return JSON.stringify(value)
24
+ .replace(/</g, "\\u003c")
25
+ .replace(/\u2028/g, "\\u2028")
26
+ .replace(/\u2029/g, "\\u2029");
27
+ }
28
+ /**
29
+ * Makes bundled JS source safe to inline in a <script> element. `<\/` parses
30
+ * identically to `</` inside JS string and regex literals — the only forms a
31
+ * real minified bundle emits — so behavior is preserved while an embedded
32
+ * `</script` can no longer close the tag. (A bare relational-division
33
+ * expression like `a</script/.test(x)` would be corrupted, but no bundler
34
+ * produces that shape.)
35
+ */
36
+ export function escapeInlineScript(js) {
37
+ return js.replace(/<\/script/gi, "<\\/script");
38
+ }
39
+ const SNIPPET_CSS = `
40
+ :root {
41
+ --bg: #0f1419; --panel: #161b22; --line: #2a313c; --fg: #e6edf3; --muted: #8b949e;
42
+ --accent: #58a6ff; --danger: #f85149; --ok: #3fb950;
43
+ --spec: #58a6ff; --arch: #bc8cff; --issue: #f85149; --todo: #d29922; --work: #3fb950;
44
+ }
45
+ :root[data-theme="light"] {
46
+ --bg: #ffffff; --panel: #f6f8fa; --line: #d0d7de; --fg: #1f2328; --muted: #59636e;
47
+ --accent: #0969da; --danger: #d1242f; --ok: #1a7f37;
48
+ }
49
+ * { box-sizing: border-box; }
50
+ html, body { margin: 0; }
51
+ body {
52
+ background: var(--color-background-primary, var(--bg));
53
+ color: var(--color-text-primary, var(--fg));
54
+ font: 13px/1.5 var(--font-sans, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
55
+ padding: 12px 14px;
56
+ }
57
+ h1 { font-size: 15px; margin: 0; }
58
+ h2 { font-size: 12px; margin: 14px 0 6px; color: var(--color-text-secondary, var(--muted));
59
+ text-transform: uppercase; letter-spacing: .4px; }
60
+ .head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
61
+ .head .brand { color: var(--accent); font-weight: 700; letter-spacing: .5px; font-size: 12px; }
62
+ .head .totals { color: var(--color-text-secondary, var(--muted)); font-size: 12px; margin-left: auto; }
63
+ .muted { color: var(--color-text-secondary, var(--muted)); }
64
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 4px 18px; }
65
+ .bar { display: flex; height: 12px; border-radius: 6px; overflow: hidden;
66
+ border: 1px solid var(--color-border-primary, var(--line)); background: var(--color-background-secondary, var(--panel)); }
67
+ .bar > span { display: block; height: 100%; }
68
+ .legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 6px; font-size: 12px; }
69
+ .dot { display: inline-block; width: 8px; height: 8px; border-radius: 4px; margin-right: 5px; }
70
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; }
71
+ .chip { border: 1px solid var(--color-border-primary, var(--line)); border-radius: 10px;
72
+ padding: 1px 9px; font-size: 12px; background: var(--color-background-secondary, var(--panel)); }
73
+ .chip b { color: var(--accent); font-weight: 600; margin-left: 4px; }
74
+ .rows .row { display: flex; gap: 8px; align-items: center; padding: 3px 0; font-size: 12px; }
75
+ .rows .row .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
76
+ .kind-pill { display: inline-block; padding: 0 7px; border-radius: 10px; font-size: 11px;
77
+ font-weight: 600; color: #04121f; }
78
+ .spark { width: 100%; height: 56px; display: block; }
79
+ .footer { margin-top: 14px; display: flex; align-items: center; gap: 10px; }
80
+ .footer .stamp { color: var(--color-text-secondary, var(--muted)); font-size: 11px; }
81
+ button {
82
+ font: inherit; color: var(--color-text-primary, var(--fg));
83
+ background: var(--color-background-secondary, var(--panel));
84
+ border: 1px solid var(--color-border-primary, var(--line)); border-radius: 6px;
85
+ padding: 3px 10px; cursor: pointer;
86
+ }
87
+ button:hover { border-color: var(--accent); }
88
+ button[hidden] { display: none; }
89
+ #status { color: var(--color-text-secondary, var(--muted)); }
90
+ `;
91
+ /**
92
+ * Shared snippet runtime: XSS-safe DOM helpers plus a minimal MCP Apps bridge.
93
+ * `augmentApp(name, render)` wires the SEP-1865 lifecycle — `ui/initialize`
94
+ * request, `ui/notifications/initialized`, then renders on each
95
+ * `ui/notifications/tool-result` — and falls back to the inline `#augment-data`
96
+ * JSON island (used by the classic MCP-UI embedded variant) when no host bridge
97
+ * answers. All dynamic values reach the DOM via textContent, never markup.
98
+ */
99
+ const SNIPPET_RUNTIME_JS = String.raw `
100
+ var KIND_COLORS = { SPEC: "#58a6ff", ARCH: "#bc8cff", ISSUE: "#f85149", TODO: "#d29922", WORK: "#3fb950" };
101
+ var VERDICT_COLORS = { helpful: "var(--ok)", harmless: "var(--muted)", harmful: "var(--danger)", "correct-abstention": "var(--accent)" };
102
+
103
+ function $(sel) { return document.querySelector(sel); }
104
+
105
+ function h(tag, props) {
106
+ var node = document.createElement(tag);
107
+ applyProps(node, props);
108
+ for (var i = 2; i < arguments.length; i += 1) appendKid(node, arguments[i]);
109
+ return node;
110
+ }
111
+
112
+ function s(tag, props) {
113
+ var node = document.createElementNS("http://www.w3.org/2000/svg", tag);
114
+ for (var key in (props || {})) node.setAttribute(key, props[key]);
115
+ for (var i = 2; i < arguments.length; i += 1) appendKid(node, arguments[i]);
116
+ return node;
117
+ }
118
+
119
+ function applyProps(node, props) {
120
+ for (var key in (props || {})) {
121
+ var value = props[key];
122
+ if (key === "class") node.className = value;
123
+ else if (key === "text") node.textContent = value;
124
+ else if (key.indexOf("on") === 0 && typeof value === "function") node.addEventListener(key.slice(2), value);
125
+ else if (value !== undefined && value !== null) node.setAttribute(key, value);
126
+ }
127
+ }
128
+
129
+ function appendKid(node, kid) {
130
+ node.append(kid && kid.nodeType ? kid : document.createTextNode(kid == null ? "" : kid));
131
+ }
132
+
133
+ function kindPill(kind) {
134
+ return h("span", { class: "kind-pill", style: "background:" + (KIND_COLORS[kind] || "#8b949e") }, kind);
135
+ }
136
+
137
+ function augmentApp(appName, render) {
138
+ var nextId = 1;
139
+ var pending = {};
140
+ // Renderers that paint to a canvas (cytoscape) can't read live CSS
141
+ // variables, so they register a callback here to recompute colors and
142
+ // repaint when the host theme changes. Reset on every render.
143
+ var themeHandlers = [];
144
+ var bridge = {
145
+ connected: false,
146
+ callTool: null,
147
+ onThemeChange: function (handler) { themeHandlers.push(handler); },
148
+ };
149
+ var lastData = null;
150
+ // Top-level (no embedding frame) means window.parent === window: any
151
+ // handshake we posted would loop straight back to ourselves.
152
+ var topLevel = window.parent === window;
153
+ // Hold outbound notifications until the handshake settles (spec ordering:
154
+ // nothing before ui/notifications/initialized) or we give up on a host.
155
+ var canNotify = false;
156
+
157
+ function post(message) { window.parent.postMessage(message, "*"); }
158
+ function notify(method, params) {
159
+ var message = { jsonrpc: "2.0", method: method };
160
+ if (params) message.params = params;
161
+ post(message);
162
+ }
163
+ function request(method, params, timeoutMs) {
164
+ return new Promise(function (resolve, reject) {
165
+ var id = nextId; nextId += 1;
166
+ var timer = timeoutMs
167
+ ? setTimeout(function () { delete pending[id]; reject(new Error(method + " timed out")); }, timeoutMs)
168
+ : null;
169
+ pending[id] = function (result, error) {
170
+ if (timer) clearTimeout(timer);
171
+ if (error) reject(new Error(error.message || "host error"));
172
+ else resolve(result);
173
+ };
174
+ post({ jsonrpc: "2.0", id: id, method: method, params: params || {} });
175
+ });
176
+ }
177
+
178
+ function applyHostContext(context) {
179
+ if (!context) return;
180
+ if (context.theme) document.documentElement.setAttribute("data-theme", context.theme);
181
+ var variables = context.styles && context.styles.variables;
182
+ if (variables) {
183
+ for (var name in variables) {
184
+ if (name.indexOf("--") === 0) document.documentElement.style.setProperty(name, String(variables[name]));
185
+ }
186
+ }
187
+ // Now that the effective colors are updated, let canvas renderers repaint
188
+ // (CSS variables never reach a <canvas>, so they must do it themselves).
189
+ for (var i = 0; i < themeHandlers.length; i += 1) {
190
+ try { themeHandlers[i](); } catch (error) { /* a broken repaint must not wedge the frame */ }
191
+ }
192
+ }
193
+
194
+ function reportSize() {
195
+ if (!canNotify) return;
196
+ notify("ui/notifications/size-changed", {
197
+ width: document.documentElement.scrollWidth,
198
+ height: document.documentElement.scrollHeight,
199
+ });
200
+ }
201
+
202
+ function setStatus(text) {
203
+ var status = $("#status");
204
+ if (status) status.textContent = text;
205
+ }
206
+
207
+ function show(data) {
208
+ lastData = data;
209
+ themeHandlers = []; // the previous render's handlers reference stale nodes
210
+ var app = $("#app");
211
+ app.replaceChildren();
212
+ try {
213
+ render(app, data, bridge);
214
+ } catch (error) {
215
+ app.replaceChildren(h("p", { class: "muted" }, "Failed to render: " + (error && error.message)));
216
+ }
217
+ requestAnimationFrame(reportSize);
218
+ }
219
+
220
+ function showMessage(text) {
221
+ var app = $("#app");
222
+ app.replaceChildren(h("p", { class: "muted", id: "status" }, text));
223
+ requestAnimationFrame(reportSize);
224
+ }
225
+
226
+ window.addEventListener("message", function (event) {
227
+ // Only the embedding host may drive this view: drop self-echoes when
228
+ // rendered top-level and anything a co-resident frame tries to inject.
229
+ if (topLevel || event.source !== window.parent) return;
230
+ var message = event.data;
231
+ if (!message || message.jsonrpc !== "2.0") return;
232
+ if (message.id !== undefined && message.method === undefined) {
233
+ var handler = pending[message.id];
234
+ if (handler) { delete pending[message.id]; handler(message.result, message.error); }
235
+ return;
236
+ }
237
+ if (message.method === "ui/notifications/tool-result") {
238
+ var result = message.params || {};
239
+ if (result.structuredContent) {
240
+ show(result.structuredContent);
241
+ } else {
242
+ // Text-only results (e.g. "no such project") must still surface
243
+ // instead of leaving the frame on "Waiting for tool result…".
244
+ var texts = [];
245
+ var blocks = result.content || [];
246
+ for (var i = 0; i < blocks.length; i += 1) {
247
+ if (blocks[i] && blocks[i].type === "text") texts.push(blocks[i].text);
248
+ }
249
+ showMessage(texts.length ? texts.join("\n") : "Tool returned no renderable data.");
250
+ }
251
+ return;
252
+ }
253
+ if (message.method === "ui/notifications/host-context-changed") {
254
+ applyHostContext(message.params);
255
+ requestAnimationFrame(reportSize);
256
+ return;
257
+ }
258
+ if (message.id !== undefined && message.method !== undefined) {
259
+ if (message.method === "ping" || message.method === "ui/resource-teardown") {
260
+ post({ jsonrpc: "2.0", id: message.id, result: {} });
261
+ } else {
262
+ post({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Method not found" } });
263
+ }
264
+ }
265
+ });
266
+
267
+ var inlineNode = document.getElementById("augment-data");
268
+ var inlineData = null;
269
+ if (inlineNode) {
270
+ try { inlineData = JSON.parse(inlineNode.textContent); } catch (error) { inlineData = null; }
271
+ }
272
+ var hasInlineData = Boolean(inlineData);
273
+ if (inlineData) show(inlineData);
274
+
275
+ if (topLevel) {
276
+ if (!lastData) setStatus("No MCP Apps host detected and no inline data.");
277
+ } else {
278
+ // No hard deadline on the handshake: a slow host may still answer after
279
+ // the status flips, and the pending entry stays live so a late response
280
+ // completes the connection instead of being dropped.
281
+ var statusTimer = setTimeout(function () {
282
+ if (bridge.connected) return;
283
+ if (hasInlineData) {
284
+ // Classic embedded variant only: no SEP-1865 handshake occurs, yet
285
+ // the inline content is already shown, so a size hint is safe and
286
+ // useful. A conformant host completes ui/initialize first (clearing
287
+ // this timer), so it never receives an out-of-lifecycle notification.
288
+ canNotify = true;
289
+ reportSize();
290
+ } else {
291
+ setStatus("No MCP Apps host detected and no inline data.");
292
+ }
293
+ }, 3000);
294
+ request("ui/initialize", {
295
+ protocolVersion: "2026-01-26",
296
+ appInfo: { name: appName, version: "1" },
297
+ appCapabilities: {},
298
+ }).then(function (result) {
299
+ clearTimeout(statusTimer);
300
+ bridge.connected = true;
301
+ bridge.callTool = function (name, args) {
302
+ return request("tools/call", { name: name, arguments: args || {} }, 120000);
303
+ };
304
+ notify("ui/notifications/initialized");
305
+ canNotify = true;
306
+ if (result) applyHostContext(result.hostContext);
307
+ reportSize();
308
+ if (!lastData) setStatus("Waiting for tool result…");
309
+ });
310
+ }
311
+
312
+ if (window.ResizeObserver) {
313
+ var scheduled = false;
314
+ new ResizeObserver(function () {
315
+ if (scheduled) return;
316
+ scheduled = true;
317
+ requestAnimationFrame(function () { scheduled = false; reportSize(); });
318
+ }).observe(document.body);
319
+ }
320
+ }
321
+ `;
322
+ const INSIGHTS_RENDER_JS = String.raw `
323
+ function formatChars(count) {
324
+ if (count >= 1000000) return (count / 1000000).toFixed(1) + "M";
325
+ if (count >= 1000) return (count / 1000).toFixed(1) + "k";
326
+ return String(count);
327
+ }
328
+
329
+ function section(title) {
330
+ var kids = [h("h2", {}, title)];
331
+ for (var i = 1; i < arguments.length; i += 1) kids.push(arguments[i]);
332
+ var wrap = h("div", {});
333
+ for (var j = 0; j < kids.length; j += 1) wrap.append(kids[j]);
334
+ return wrap;
335
+ }
336
+
337
+ function kindBar(byKind, total) {
338
+ var bar = h("div", { class: "bar" });
339
+ var legend = h("div", { class: "legend" });
340
+ for (var i = 0; i < byKind.length; i += 1) {
341
+ var entry = byKind[i];
342
+ if (entry.count > 0 && total > 0) {
343
+ bar.append(h("span", {
344
+ style: "width:" + (100 * entry.count / total) + "%;background:" + (KIND_COLORS[entry.kind] || "#8b949e"),
345
+ title: entry.kind + ": " + entry.count,
346
+ }));
347
+ }
348
+ legend.append(h("span", {},
349
+ h("span", { class: "dot", style: "background:" + (KIND_COLORS[entry.kind] || "#8b949e") }),
350
+ entry.kind + " " + entry.count));
351
+ }
352
+ return h("div", {}, bar, legend);
353
+ }
354
+
355
+ function sparkline(weeks) {
356
+ var width = 300, height = 56, pad = 2;
357
+ var max = 1;
358
+ for (var i = 0; i < weeks.length; i += 1) max = Math.max(max, weeks[i].created + weeks[i].updated);
359
+ var slot = (width - pad * 2) / weeks.length;
360
+ var svg = s("svg", { class: "spark", viewBox: "0 0 " + width + " " + height, preserveAspectRatio: "none" });
361
+ for (var j = 0; j < weeks.length; j += 1) {
362
+ var week = weeks[j];
363
+ var x = pad + j * slot;
364
+ var createdH = Math.round((height - 14) * week.created / max);
365
+ var updatedH = Math.round((height - 14) * week.updated / max);
366
+ var base = height - 12;
367
+ var title = week.start + ": " + week.created + " created, " + week.updated + " updated";
368
+ var group = s("g", {});
369
+ group.append(s("title", {}, title));
370
+ if (updatedH > 0) group.append(s("rect", { x: x + 1, y: base - createdH - updatedH, width: Math.max(1, slot - 2), height: updatedH, fill: "var(--muted)", opacity: "0.55" }));
371
+ if (createdH > 0) group.append(s("rect", { x: x + 1, y: base - createdH, width: Math.max(1, slot - 2), height: createdH, fill: "var(--accent)" }));
372
+ group.append(s("rect", { x: x + 1, y: base + 2, width: Math.max(1, slot - 2), height: 2, fill: "var(--line)" }));
373
+ svg.append(group);
374
+ }
375
+ return svg;
376
+ }
377
+
378
+ function tallyRows(tally) {
379
+ var rows = h("div", { class: "rows" });
380
+ for (var verdict in tally.by_verdict) {
381
+ rows.append(h("div", { class: "row" },
382
+ h("span", { class: "dot", style: "background:" + (VERDICT_COLORS[verdict] || "var(--muted)") }),
383
+ h("span", { class: "grow" }, verdict),
384
+ h("span", { class: "muted" }, String(tally.by_verdict[verdict]))));
385
+ }
386
+ rows.append(h("div", { class: "row muted" }, tally.total + " rated of " + tally.target + " target"));
387
+ return rows;
388
+ }
389
+
390
+ function renderInsights(app, data, bridge) {
391
+ var totals = data.totals || { memories: 0, links: 0, distinct_tags: 0, total_chars: 0 };
392
+ app.append(h("div", { class: "head" },
393
+ h("span", { class: "brand" }, "augment"),
394
+ h("h1", {}, "Memory insights — " + data.project.name),
395
+ h("span", { class: "totals" },
396
+ totals.memories + " memories · " + totals.links + " links · " + formatChars(totals.total_chars) + " chars")));
397
+
398
+ app.append(section("Memories by kind", kindBar(data.by_kind || [], totals.memories)));
399
+
400
+ var grid = h("div", { class: "grid" });
401
+ if ((data.links_by_type || []).length > 0) {
402
+ var linkRows = h("div", { class: "rows" });
403
+ for (var i = 0; i < data.links_by_type.length; i += 1) {
404
+ var entry = data.links_by_type[i];
405
+ linkRows.append(h("div", { class: "row" },
406
+ h("span", { class: "grow" }, entry.type),
407
+ h("span", { class: "muted" }, String(entry.count))));
408
+ }
409
+ linkRows.append(h("div", { class: "row muted" }, data.orphan_count + " memories with no links"));
410
+ grid.append(section("Links", linkRows));
411
+ }
412
+ if ((data.top_connected || []).length > 0) {
413
+ var hubRows = h("div", { class: "rows" });
414
+ for (var j = 0; j < data.top_connected.length; j += 1) {
415
+ var hub = data.top_connected[j];
416
+ hubRows.append(h("div", { class: "row" },
417
+ kindPill(hub.kind),
418
+ h("span", { class: "grow", title: hub.name }, hub.name),
419
+ h("span", { class: "muted" }, "×" + hub.degree)));
420
+ }
421
+ grid.append(section("Most connected", hubRows));
422
+ }
423
+ if (data.tally && data.tally.total > 0) {
424
+ grid.append(section("Recall quality (tally)", tallyRows(data.tally)));
425
+ }
426
+ if (grid.childNodes.length > 0) app.append(grid);
427
+
428
+ if ((data.top_tags || []).length > 0) {
429
+ var chips = h("div", { class: "chips" });
430
+ for (var k = 0; k < data.top_tags.length; k += 1) {
431
+ var tag = data.top_tags[k];
432
+ chips.append(h("span", { class: "chip" }, tag.tag, h("b", {}, String(tag.count))));
433
+ }
434
+ app.append(section("Top tags", chips));
435
+ }
436
+
437
+ app.append(section("Activity (created vs updated, last 12 weeks)", sparkline(data.activity || [])));
438
+
439
+ var refresh = h("button", { hidden: "", onclick: function () {
440
+ refresh.disabled = true;
441
+ bridge.callTool("memory_insights", { project_name: data.project.name }).then(function (result) {
442
+ refresh.disabled = false;
443
+ if (result && result.structuredContent) {
444
+ app.replaceChildren();
445
+ renderInsights(app, result.structuredContent, bridge);
446
+ }
447
+ }).catch(function () { refresh.disabled = false; });
448
+ } }, "Refresh");
449
+ if (bridge.connected) refresh.removeAttribute("hidden");
450
+ app.append(h("div", { class: "footer" }, refresh,
451
+ h("span", { class: "stamp" }, "generated " + String(data.generated_at || "").replace("T", " ").slice(0, 16))));
452
+ }
453
+
454
+ augmentApp("augment-memory-insights", renderInsights);
455
+ `;
456
+ const GRAPH_RENDER_JS = String.raw `
457
+ function renderGraph(app, data, bridge) {
458
+ app.append(h("div", { class: "head" },
459
+ h("span", { class: "brand" }, "augment"),
460
+ h("h1", {}, "Memory graph — " + data.project.name),
461
+ h("span", { class: "totals" }, data.total_memories + " memories · " + data.total_links + " links")));
462
+ if (data.truncated) {
463
+ app.append(h("p", { class: "muted", style: "margin:4px 0 0" },
464
+ "Showing the " + data.nodes.length + " most recently updated memories."));
465
+ }
466
+
467
+ var stage = h("div", { style: "position:relative;margin-top:8px" });
468
+ var cyHost = h("div", { style: "height:380px;border:1px solid var(--color-border-primary, var(--line));border-radius:6px" });
469
+ var info = h("div", { class: "muted", style: "padding:6px 2px;min-height:24px;font-size:12px" },
470
+ "Tap a node to inspect it.");
471
+ stage.append(cyHost);
472
+ app.append(stage, info);
473
+
474
+ var elements = [];
475
+ for (var i = 0; i < data.nodes.length; i += 1) {
476
+ var node = data.nodes[i];
477
+ elements.push({ data: { id: "m" + node.id, label: node.name, kind: node.kind,
478
+ color: KIND_COLORS[node.kind] || "#8b949e", tags: node.tags, degree: node.degree } });
479
+ }
480
+ for (var j = 0; j < data.edges.length; j += 1) {
481
+ var edge = data.edges[j];
482
+ elements.push({ data: { id: "l" + edge.id, source: "m" + edge.source, target: "m" + edge.target, label: edge.type } });
483
+ }
484
+
485
+ // Cytoscape paints to canvas, so CSS custom properties don't resolve —
486
+ // read the effective theme colors off the document instead.
487
+ var fgColor = getComputedStyle(document.body).color || "#e6edf3";
488
+ var cy = cytoscape({
489
+ container: cyHost,
490
+ elements: elements,
491
+ minZoom: 0.2,
492
+ maxZoom: 2.5,
493
+ style: [
494
+ { selector: "node", style: {
495
+ "label": "data(label)", "color": fgColor, "font-size": 10, "text-wrap": "wrap",
496
+ "text-max-width": 110, "text-valign": "bottom", "text-margin-y": 4,
497
+ "background-color": "data(color)", "border-color": "data(color)",
498
+ "width": 20, "height": 20, "border-width": 2 } },
499
+ { selector: "node:selected", style: { "border-color": "#ffffff", "border-width": 3 } },
500
+ { selector: "edge", style: {
501
+ "label": "data(label)", "font-size": 8, "color": "#8b949e", "curve-style": "bezier",
502
+ "width": 1.2, "line-color": "#484f58", "target-arrow-color": "#484f58",
503
+ "target-arrow-shape": "triangle", "text-rotation": "autorotate" } },
504
+ ],
505
+ });
506
+ cy.layout({ name: "cose", animate: false, padding: 24, nodeRepulsion: 9000, idealEdgeLength: 80 }).run();
507
+
508
+ // Node label color is baked onto the canvas at render time; a later host
509
+ // theme switch would leave it stale (e.g. light text on a light panel).
510
+ // Re-read the effective color and restyle in place — positions/zoom/pan
511
+ // are preserved (no relayout).
512
+ bridge.onThemeChange(function () {
513
+ cy.nodes().style("color", getComputedStyle(document.body).color || "#e6edf3");
514
+ });
515
+
516
+ cy.on("tap", "node", function (event) {
517
+ var d = event.target.data();
518
+ info.replaceChildren(
519
+ kindPill(d.kind), " ",
520
+ h("b", {}, d.label),
521
+ h("span", { class: "muted" }, " ×" + d.degree + " links" +
522
+ (d.tags && d.tags.length ? " · " + d.tags.join(", ") : "")));
523
+ });
524
+ }
525
+
526
+ augmentApp("augment-memory-graph", renderGraph);
527
+ `;
528
+ function snippetDocument(options) {
529
+ const dataIsland = options.inlineData === undefined
530
+ ? ""
531
+ : `\n <script type="application/json" id="augment-data">${escapeInlineJson(options.inlineData)}</script>`;
532
+ return `<!DOCTYPE html>
533
+ <html lang="en">
534
+ <head>
535
+ <meta charset="utf-8" />
536
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
537
+ <title>${options.title}</title>
538
+ <style>${SNIPPET_CSS}</style>${options.headExtra ?? ""}
539
+ </head>
540
+ <body>
541
+ <div id="app"><p id="status" class="muted">Connecting to host…</p></div>${dataIsland}
542
+ <script>
543
+ (() => {
544
+ "use strict";
545
+ ${SNIPPET_RUNTIME_JS}
546
+ ${options.bodyJs}
547
+ })();
548
+ </script>
549
+ </body>
550
+ </html>
551
+ `;
552
+ }
553
+ /**
554
+ * The insights dashboard snippet. With no argument this is the `ui://` template
555
+ * (data arrives via ui/notifications/tool-result); with inline data it is the
556
+ * self-contained classic MCP-UI embedded variant.
557
+ */
558
+ export function insightsSnippetHtml(inlineData) {
559
+ return snippetDocument({
560
+ title: "Augment — Memory Insights",
561
+ bodyJs: INSIGHTS_RENDER_JS,
562
+ inlineData,
563
+ });
564
+ }
565
+ /**
566
+ * The memory-graph snippet. Cytoscape is inlined (resolved from node_modules at
567
+ * read time, like the dashboard route) so the document stays offline-safe under
568
+ * the default MCP Apps CSP.
569
+ */
570
+ export function graphSnippetHtml(cytoscapeJs, inlineData) {
571
+ return snippetDocument({
572
+ title: "Augment — Memory Graph",
573
+ headExtra: `\n <script>${escapeInlineScript(cytoscapeJs)}</script>`,
574
+ bodyJs: GRAPH_RENDER_JS,
575
+ inlineData,
576
+ });
577
+ }
578
+ //# sourceMappingURL=apps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apps.js","sourceRoot":"","sources":["../../src/mcp/apps.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,iGAAiG;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,2BAA2B,CAAC;AACxD,sFAAsF;AACtF,MAAM,CAAC,MAAM,eAAe,GAAG,4BAA4B,CAAC;AAE5D,MAAM,CAAC,MAAM,qBAAqB,GAAG,4BAA4B,CAAC;AAClE,MAAM,CAAC,MAAM,kBAAkB,GAAG,yBAAyB,CAAC;AAE5D;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SACzB,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC;SACxB,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC;SAC7B,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAU;IAC3C,OAAO,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDnB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8NpC,CAAC;AAEF,MAAM,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqIpC,CAAC;AAEF,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuEjC,CAAC;AAEF,SAAS,eAAe,CAAC,OAKxB;IACC,MAAM,UAAU,GACd,OAAO,CAAC,UAAU,KAAK,SAAS;QAC9B,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,yDAAyD,gBAAgB,CAAC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC;IAC/G,OAAO;;;;;WAKE,OAAO,CAAC,KAAK;WACb,WAAW,WAAW,OAAO,CAAC,SAAS,IAAI,EAAE;;;4EAGoB,UAAU;;;;EAIpF,kBAAkB;EAClB,OAAO,CAAC,MAAM;;;;;CAKf,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAA2B;IAC7D,OAAO,eAAe,CAAC;QACrB,KAAK,EAAE,2BAA2B;QAClC,MAAM,EAAE,kBAAkB;QAC1B,UAAU;KACX,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,UAAyB;IAC7E,OAAO,eAAe,CAAC;QACrB,KAAK,EAAE,wBAAwB;QAC/B,SAAS,EAAE,eAAe,kBAAkB,CAAC,WAAW,CAAC,WAAW;QACpE,MAAM,EAAE,eAAe;QACvB,UAAU;KACX,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,91 @@
1
+ import { type LinkType, type MemoryKind } from "../constants.js";
2
+ import type { TallySummary } from "../tally.js";
3
+ import type { ProjectGraph } from "../types.js";
4
+ /**
5
+ * Pure aggregation over a project's memory graph (plus the optional tally
6
+ * summary) into the payload the dashboard snippets render. Kept free of I/O so
7
+ * the numbers the MCP Apps iframe shows are unit-testable without a daemon.
8
+ */
9
+ export declare const ACTIVITY_WEEKS = 12;
10
+ export declare const TOP_TAG_LIMIT = 12;
11
+ export declare const TOP_CONNECTED_LIMIT = 5;
12
+ /** Node cap for the graph snippet payload — keeps structuredContent bounded. */
13
+ export declare const GRAPH_NODE_LIMIT = 300;
14
+ export interface ActivityWeek {
15
+ /** ISO date (UTC midnight) of the first day of the bucket. */
16
+ start: string;
17
+ created: number;
18
+ updated: number;
19
+ }
20
+ export interface MemoryInsights {
21
+ project: {
22
+ id: number;
23
+ name: string;
24
+ };
25
+ generated_at: string;
26
+ totals: {
27
+ memories: number;
28
+ links: number;
29
+ distinct_tags: number;
30
+ total_chars: number;
31
+ };
32
+ by_kind: Array<{
33
+ kind: MemoryKind;
34
+ count: number;
35
+ }>;
36
+ links_by_type: Array<{
37
+ type: LinkType;
38
+ count: number;
39
+ }>;
40
+ top_tags: Array<{
41
+ tag: string;
42
+ count: number;
43
+ }>;
44
+ /**
45
+ * Oldest week first; 12 seven-day buckets aligned to UTC midnights, the
46
+ * window ending at the first UTC midnight after `generated_at`.
47
+ */
48
+ activity: ActivityWeek[];
49
+ top_connected: Array<{
50
+ id: number;
51
+ name: string;
52
+ kind: MemoryKind;
53
+ degree: number;
54
+ }>;
55
+ orphan_count: number;
56
+ freshest?: {
57
+ id: number;
58
+ name: string;
59
+ kind: MemoryKind;
60
+ updated_at: string;
61
+ };
62
+ tally?: TallySummary;
63
+ }
64
+ export interface GraphPayload {
65
+ project: {
66
+ id: number;
67
+ name: string;
68
+ };
69
+ nodes: Array<{
70
+ id: number;
71
+ name: string;
72
+ kind: MemoryKind;
73
+ tags: string[];
74
+ degree: number;
75
+ }>;
76
+ edges: Array<{
77
+ id: number;
78
+ source: number;
79
+ target: number;
80
+ type: LinkType;
81
+ }>;
82
+ total_memories: number;
83
+ total_links: number;
84
+ truncated: boolean;
85
+ }
86
+ export declare function computeInsights(graph: ProjectGraph, tally: TallySummary | undefined, now: Date): MemoryInsights;
87
+ export declare function toGraphPayload(graph: ProjectGraph, nodeLimit?: number): GraphPayload;
88
+ /** Text fallback: what the model (and any non-UI host) sees for memory_insights. */
89
+ export declare function renderInsightsText(insights: MemoryInsights): string;
90
+ /** Text fallback for memory_graph — a summary, never the full node/edge dump. */
91
+ export declare function renderGraphText(payload: GraphPayload): string;