@batalabs/virlow-mcp-core 3.84.2 → 3.85.1

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,457 @@
1
+ /** The MCP Apps resource type. ChatGPT reads it too. */
2
+ export const WIDGET_MIME_TYPE = 'text/html;profile=mcp-app';
3
+ /**
4
+ * Versioned, because a host treats the URI as the cache key for the page:
5
+ * ChatGPT keeps what it fetched for a URI, so changed HTML under the same URI
6
+ * may never be seen. Bump the version whenever the page changes.
7
+ * v2: the card performs the MCP Apps handshake even when window.openai exists.
8
+ */
9
+ export const SAVED_MEMORY_URI = 'ui://widget/saved-memory-v2.html';
10
+ export const MEMORY_CONTEXT_URI = 'ui://widget/memory-context-v2.html';
11
+ /** Where "Open in Virlow" goes. The web app has no per-memory route, so it
12
+ * opens the app itself. */
13
+ export const VIRLOW_APP_URL = 'https://app.virlow.com';
14
+ /**
15
+ * The thirteen kind colours, light and dark. A copy of KIND_COLORS in the
16
+ * web app (src/lib/entityKinds.ts), which cannot be imported here: that
17
+ * module pulls in lucide-react. A test holds the two to the same values.
18
+ */
19
+ export const KIND_COLORS = {
20
+ person: { light: '#4F6D8F', dark: '#8FB0D0' },
21
+ organization: { light: '#C2603E', dark: '#E08A67' },
22
+ project: { light: '#A27B17', dark: '#F2C66D' },
23
+ tool: { light: '#5E7B4A', dark: '#9DBB85' },
24
+ place: { light: '#7A5A8C', dark: '#B79BC8' },
25
+ event: { light: '#CB2A61', dark: '#F6638A' },
26
+ topic: { light: '#0C9184', dark: '#82DCCF' },
27
+ product: { light: '#9E2185', dark: '#EA78CD' },
28
+ media: { light: '#6E4A2E', dark: '#C8A07E' },
29
+ pet: { light: '#C16B10', dark: '#F59C4D' },
30
+ group: { light: '#10953A', dark: '#6AD177' },
31
+ goal: { light: '#5D47B2', dark: '#AC8FFA' },
32
+ other: { light: '#6B665C', dark: '#9A9488' },
33
+ };
34
+ /** How many facts the context card shows; the text result keeps them all. */
35
+ export const CONTEXT_CARD_FACTS = 5;
36
+ const VIEW_URI = {
37
+ saved: SAVED_MEMORY_URI,
38
+ context: MEMORY_CONTEXT_URI,
39
+ };
40
+ /** Which tools show which card. Tools not named here have no UI. */
41
+ export const TOOL_WIDGETS = {
42
+ add_memory: 'saved',
43
+ update_memory: 'saved',
44
+ memory_context: 'context',
45
+ };
46
+ /** The tool-side `_meta` linking a tool to its card: the MCP Apps key, the
47
+ * flat `ui/resourceUri` spelling older MCP Apps hosts read (the ext-apps
48
+ * SDK's registerAppTool writes both), and ChatGPT's alias. Visibility is
49
+ * stated rather than left to the default: the model calls these tools and
50
+ * the card may call tools too (add_memory's Undo calls delete_memory, and
51
+ * `openai/widgetAccessible` is ChatGPT's older spelling of that). */
52
+ export function widgetToolMeta(tool) {
53
+ const view = TOOL_WIDGETS[tool];
54
+ if (!view)
55
+ return undefined;
56
+ const uri = VIEW_URI[view];
57
+ return {
58
+ ui: { resourceUri: uri, visibility: ['model', 'app'] },
59
+ 'ui/resourceUri': uri,
60
+ 'openai/outputTemplate': uri,
61
+ ...(tool === 'add_memory' && { 'openai/widgetAccessible': true }),
62
+ };
63
+ }
64
+ /**
65
+ * The resource-side `_meta`. Empty CSP lists are the secure default (no
66
+ * network); they are stated rather than omitted so a reviewer reads the
67
+ * decision. `openai/widgetCSP` is ChatGPT's older spelling of the same
68
+ * thing; redirect_domains lets "Open in Virlow" go straight to the app.
69
+ */
70
+ export function resourceMeta(view) {
71
+ return {
72
+ ui: {
73
+ prefersBorder: true,
74
+ csp: { connectDomains: [], resourceDomains: [], frameDomains: [] },
75
+ },
76
+ 'openai/widgetDescription': view === 'saved'
77
+ ? 'Shows the memory just saved to Virlow, with Undo and a link to the app. The card already shows the fact, so a one-line confirmation is enough.'
78
+ : 'Shows a short summary of what Virlow has saved about this user or project.',
79
+ 'openai/widgetCSP': {
80
+ connect_domains: [],
81
+ resource_domains: [],
82
+ redirect_domains: [VIRLOW_APP_URL],
83
+ },
84
+ 'openai/widgetPrefersBorder': true,
85
+ };
86
+ }
87
+ /** Everything the server needs to list and serve the two resources. */
88
+ export const WIDGET_RESOURCES = [
89
+ {
90
+ name: 'saved-memory-card',
91
+ uri: SAVED_MEMORY_URI,
92
+ title: 'Saved to Virlow',
93
+ description: 'Inline card shown after a memory is saved or updated.',
94
+ view: 'saved',
95
+ },
96
+ {
97
+ name: 'memory-context-card',
98
+ uri: MEMORY_CONTEXT_URI,
99
+ title: 'What Virlow knows',
100
+ description: 'Inline card summarising the memories returned by memory_context.',
101
+ view: 'context',
102
+ },
103
+ ];
104
+ /** A `resources/read` result for one card. */
105
+ export function readWidget(view) {
106
+ const uri = VIEW_URI[view];
107
+ return {
108
+ contents: [{ uri, mimeType: WIDGET_MIME_TYPE, text: widgetHtml(view), _meta: resourceMeta(view) }],
109
+ };
110
+ }
111
+ // ---------- the page ----------
112
+ /** The Virlow mark from public/favicon.svg: the italic v and its dot. */
113
+ const MARK_SVG = '<svg class="mark" viewBox="0 0 64 64" width="20" height="20" aria-hidden="true" focusable="false">' +
114
+ '<rect width="64" height="64" rx="12" fill="#fbfaf7"/>' +
115
+ '<g transform="translate(6.172854 51.207268) scale(2.201335896 -2.201335896)">' +
116
+ '<path d="M7.8 -0.4Q6.8 -0.4 6.4 -0.2Q6.1 0.1 6.1 0.4Q6.1 0.6 6.1 0.8Q6.1 0.9 6.1 1.1Q6.1 1.3 6.1 1.7Q6 3.9 5.7 6.3Q5.3 8.7 4.6 10.8Q3.9 12.9 3.1 14.3Q2.9 14.5 2.7 14.7Q2.5 14.8 2.3 14.8Q2 14.8 1.7 14.7Q1.4 14.5 1 14.2Q0.8 13.9 0.5 13.9Q0.4 13.9 0.1 14.1Q-0.1 14.3 -0.1 14.7Q-0.1 14.9 0 15.1Q1 16.5 2.3 17.2Q3.6 17.8 5.1 17.8Q7.1 17.8 8.2 15.9Q9 14.5 9.5 12.6Q9.9 10.8 10.1 8.7Q10.3 6.6 10.4 4.5Q10.4 4.1 10 3.1Q9.7 2.2 9.3 1.7Q10.3 2.3 11.4 3.3Q12.5 4.3 13.4 5.6Q14.4 6.9 15 8.3Q15.6 9.7 15.6 11.1Q15.6 12 15.4 12.6Q15.2 13.3 14.9 14Q14.7 14.7 14.7 15.5Q14.7 16.6 15.4 17.2Q16.1 17.8 17.2 17.8Q18.2 17.8 18.9 17.2Q19.5 16.6 19.8 15.8Q20.1 14.9 20.1 13.9Q20.1 12.4 19.4 10.7Q18.7 9 17.5 7.3Q16.2 5.7 14.8 4.2Q13.3 2.7 11.8 1.5Q10.3 0.4 9 -0.3Q9 -0.4 8.8 -0.4Q8.6 -0.4 7.8 -0.4Z" fill="#1f1d1a"/>' +
117
+ '<path d="M17.7 2.6Q17.7 3.9 18.5 4.9Q19.4 5.8 20.8 5.8Q22.1 5.8 22.8 5Q23.5 4.2 23.5 3Q23.5 1.6 22.6 0.6Q21.7 -0.4 20.4 -0.4Q19 -0.4 18.3 0.5Q17.7 1.4 17.7 2.6Z" fill="#775825"/>' +
118
+ '</g></svg>';
119
+ function kindVars(mode) {
120
+ return Object.entries(KIND_COLORS)
121
+ .map(([kind, c]) => `--k-${kind}:${c[mode]};`)
122
+ .join('');
123
+ }
124
+ const LIGHT = 'color-scheme:light;--ink:#1F1D1A;--ink-2:#5E5A52;--ink-3:#6B665C;--rule:rgba(0,0,0,0.12);' +
125
+ '--chip:rgba(0,0,0,0.05);--btn:#4F6D8F;--btn-ink:#FFFFFF;--btn-hover:#33506E;--ghost-hover:rgba(0,0,0,0.05);' +
126
+ '--ring:#33506E;--danger:#B3402E;--ok:#4C783C;' +
127
+ kindVars('light');
128
+ const DARK = 'color-scheme:dark;--ink:#EDE7DD;--ink-2:#B8B0A4;--ink-3:#9A9488;--rule:rgba(255,255,255,0.16);' +
129
+ '--chip:rgba(255,255,255,0.07);--btn:#8FB0D0;--btn-ink:#14181D;--btn-hover:#B8D1E8;--ghost-hover:rgba(255,255,255,0.08);' +
130
+ '--ring:#B8D1E8;--danger:#E07A68;--ok:#9DBB85;' +
131
+ kindVars('dark');
132
+ /** Kept to body and body-small sizes, system fonts, no inner scrolling: the
133
+ * Apps SDK rules for an inline card. Long text is clamped, not scrolled. */
134
+ const CSS = `
135
+ :root{${LIGHT}}
136
+ :root[data-theme="dark"]{${DARK}}
137
+ @media (prefers-color-scheme: dark){:root:not([data-theme="light"]){${DARK}}}
138
+ *{box-sizing:border-box}
139
+ html,body{margin:0;padding:0;background:transparent;overflow:hidden}
140
+ body{font:14px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:var(--ink);-webkit-font-smoothing:antialiased}
141
+ .card{padding:14px 16px;display:flex;flex-direction:column;gap:8px}
142
+ .head{display:flex;align-items:center;gap:8px;min-width:0}
143
+ .mark{flex:none;border-radius:5px;box-shadow:0 0 0 1px var(--rule)}
144
+ .status{font-size:13px;font-weight:600;color:var(--ink-2);outline:none}
145
+ .status.removed{color:var(--ink-3)}
146
+ .ns{margin-left:auto;font-size:12px;color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:45%}
147
+ .label{font-weight:600;margin:0;overflow-wrap:anywhere}
148
+ .fact{margin:0;color:var(--ink-2);overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden}
149
+ .removed-body .label,.removed-body .fact{text-decoration:line-through;color:var(--ink-3)}
150
+ .chips{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}
151
+ .chip{display:inline-flex;align-items:center;gap:6px;max-width:100%;padding:2px 9px 2px 7px;border-radius:999px;background:var(--chip);font-size:12px;color:var(--ink)}
152
+ .chip span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
153
+ .dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--k-other)}
154
+ ${Object.keys(KIND_COLORS)
155
+ .map((k) => `.chip[data-kind="${k}"] .dot{background:var(--k-${k})}`)
156
+ .join('\n')}
157
+ .facts{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:6px}
158
+ .facts li{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;overflow-wrap:anywhere;color:var(--ink-2)}
159
+ .facts b{color:var(--ink);font-weight:600}
160
+ .note{margin:0;font-size:12px;color:var(--ink-3)}
161
+ .error{margin:0;font-size:12px;color:var(--danger)}
162
+ .actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:2px}
163
+ button{font:inherit;font-size:13px;font-weight:600;border-radius:8px;padding:6px 12px;cursor:pointer;border:1px solid transparent}
164
+ button:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
165
+ button[disabled]{opacity:.6;cursor:default}
166
+ .primary{background:var(--btn);color:var(--btn-ink)}
167
+ .primary:hover:not([disabled]){background:var(--btn-hover)}
168
+ .secondary{background:transparent;color:var(--ink);border-color:var(--rule)}
169
+ .secondary:hover:not([disabled]){background:var(--ghost-hover)}
170
+ .empty{margin:0;color:var(--ink-3)}
171
+ `;
172
+ /**
173
+ * The card's script. Plain ES2017 in a string, because it ships inside the
174
+ * HTML rather than through the package build. It speaks to both bridges at
175
+ * once, never one instead of the other:
176
+ *
177
+ * - The MCP Apps bridge, JSON-RPC over postMessage, whenever the page is in a
178
+ * frame: the card sends ui/initialize, then ui/notifications/initialized,
179
+ * receives ui/notifications/tool-result, reports its height with
180
+ * ui/notifications/size-changed, and calls tools/call and ui/open-link.
181
+ * Every MCP Apps host needs this, ChatGPT included: it delivers the tool
182
+ * result to an mcp-app frame only after the handshake.
183
+ * - ChatGPT's `window.openai`, when present, as an extension: toolOutput
184
+ * (the structuredContent, possibly before the handshake completes),
185
+ * widgetState / setWidgetState for the Undo state, theme, the
186
+ * `openai:set_globals` event, and callTool / openExternal as the fallback
187
+ * while the standard bridge is not up.
188
+ */
189
+ const SCRIPT = `
190
+ (function () {
191
+ 'use strict';
192
+ var APP_URL = ${JSON.stringify(VIRLOW_APP_URL)};
193
+ var KIND_LABEL = {person:'person',organization:'organization',project:'project',tool:'tool',place:'place',event:'event',topic:'topic',product:'product',media:'media',pet:'pet',group:'group',goal:'goal',other:'other'};
194
+ var root = document.getElementById('root');
195
+ var view = document.body.getAttribute('data-view') === 'context' ? 'context' : 'saved';
196
+ var state = { data: null, removed: false, busy: false, error: '' };
197
+ var bridge = null;
198
+
199
+ function openai() { return window.openai || null; }
200
+
201
+ function el(tag, cls, text) {
202
+ var node = document.createElement(tag);
203
+ if (cls) node.className = cls;
204
+ if (text != null) node.textContent = String(text);
205
+ return node;
206
+ }
207
+
208
+ function applyTheme(theme) {
209
+ if (theme === 'light' || theme === 'dark') document.documentElement.setAttribute('data-theme', theme);
210
+ }
211
+
212
+ function header(status, ns, removed) {
213
+ var head = el('div', 'head');
214
+ var mark = document.createElement('span');
215
+ mark.innerHTML = ${JSON.stringify(MARK_SVG)};
216
+ head.appendChild(mark.firstChild);
217
+ var s = el('span', 'status' + (removed ? ' removed' : ''), status);
218
+ s.id = 'status';
219
+ s.setAttribute('tabindex', '-1');
220
+ s.setAttribute('role', 'status');
221
+ s.setAttribute('aria-live', 'polite');
222
+ head.appendChild(s);
223
+ if (ns) {
224
+ var n = el('span', 'ns', ns);
225
+ n.title = 'Project: ' + ns;
226
+ head.appendChild(n);
227
+ }
228
+ return head;
229
+ }
230
+
231
+ function chips(entities, max) {
232
+ var list = el('ul', 'chips');
233
+ list.setAttribute('aria-label', 'About');
234
+ var shown = entities.slice(0, max);
235
+ for (var i = 0; i < shown.length; i++) {
236
+ var e = shown[i];
237
+ var kind = KIND_LABEL[e.kind] ? e.kind : 'other';
238
+ var li = el('li', 'chip');
239
+ li.setAttribute('data-kind', kind);
240
+ li.title = e.name + ' (' + kind + ')';
241
+ li.appendChild(el('span', 'dot')).setAttribute('aria-hidden', 'true');
242
+ var name = el('span', null, e.name);
243
+ li.appendChild(name);
244
+ var sr = el('span', 'sr', ', ' + kind);
245
+ sr.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)';
246
+ li.appendChild(sr);
247
+ list.appendChild(li);
248
+ }
249
+ if (entities.length > max) list.appendChild(el('li', 'chip', '+' + (entities.length - max)));
250
+ return list;
251
+ }
252
+
253
+ function button(label, cls, onClick, aria) {
254
+ var b = el('button', cls, label);
255
+ b.type = 'button';
256
+ if (aria) b.setAttribute('aria-label', aria);
257
+ b.addEventListener('click', onClick);
258
+ return b;
259
+ }
260
+
261
+ // The standard bridge first once it is up, window.openai while it is not
262
+ // (or in a ChatGPT that never answers ui/initialize).
263
+ function openApp() {
264
+ if (bridge && bridge.ready) { bridge.request('ui/open-link', { url: APP_URL }).catch(function () {}); return; }
265
+ var host = openai();
266
+ if (host && typeof host.openExternal === 'function') { host.openExternal({ href: APP_URL }); return; }
267
+ window.open(APP_URL, '_blank', 'noopener');
268
+ }
269
+
270
+ function callTool(name, args) {
271
+ if (bridge && bridge.ready) return bridge.request('tools/call', { name: name, arguments: args });
272
+ var host = openai();
273
+ if (host && typeof host.callTool === 'function') return Promise.resolve(host.callTool(name, args));
274
+ return Promise.reject(new Error('This host cannot run tools from a card.'));
275
+ }
276
+
277
+ function persist() {
278
+ var host = openai();
279
+ if (host && typeof host.setWidgetState === 'function') {
280
+ try { host.setWidgetState({ removed: state.removed }); } catch (e) { /* the card still shows it */ }
281
+ }
282
+ }
283
+
284
+ function undo() {
285
+ var d = state.data;
286
+ if (!d || !d.id || state.busy || state.removed) return;
287
+ state.busy = true;
288
+ state.error = '';
289
+ render();
290
+ callTool('delete_memory', { id: d.id })
291
+ .then(function (result) {
292
+ if (result && result.isError) throw new Error('refused');
293
+ state.removed = true;
294
+ persist();
295
+ })
296
+ .catch(function () { state.error = 'Could not remove it. Try again.'; })
297
+ .then(function () {
298
+ state.busy = false;
299
+ render();
300
+ var s = document.getElementById('status');
301
+ if (state.removed && s) s.focus();
302
+ });
303
+ }
304
+
305
+ function renderSaved(d) {
306
+ var card = el('div', 'card' + (state.removed ? ' removed-body' : ''));
307
+ var status = state.removed ? 'Removed from Virlow' : d.updated ? 'Updated in Virlow' : 'Saved to Virlow';
308
+ card.appendChild(header(status, d.namespace, state.removed));
309
+ card.appendChild(el('p', 'label', d.label));
310
+ card.appendChild(el('p', 'fact', d.text));
311
+ if (d.entities && d.entities.length) card.appendChild(chips(d.entities, 8));
312
+ if (state.removed) card.appendChild(el('p', 'note', 'Moved to the Trash. You can restore it in the Virlow app.'));
313
+ if (state.error) card.appendChild(el('p', 'error', state.error));
314
+ var actions = el('div', 'actions');
315
+ // Undo deletes the memory, so it is offered only for a new one: on an
316
+ // update it would delete what was there before, not reverse the change.
317
+ if (!d.updated && !state.removed) {
318
+ var u = button(state.busy ? 'Removing...' : 'Undo', 'primary', undo, 'Undo: remove this memory from Virlow');
319
+ if (state.busy) u.disabled = true;
320
+ actions.appendChild(u);
321
+ }
322
+ actions.appendChild(button('Open in Virlow', 'secondary', openApp, 'Open Virlow in a new tab'));
323
+ card.appendChild(actions);
324
+ return card;
325
+ }
326
+
327
+ function renderContext(d) {
328
+ var card = el('div', 'card');
329
+ card.appendChild(header('What Virlow knows', d.namespace, false));
330
+ var facts = d.facts || [];
331
+ if (facts.length) {
332
+ var list = el('ul', 'facts');
333
+ for (var i = 0; i < facts.length; i++) {
334
+ var li = el('li');
335
+ li.appendChild(el('b', null, facts[i].label));
336
+ li.appendChild(document.createTextNode(': ' + facts[i].text));
337
+ list.appendChild(li);
338
+ }
339
+ card.appendChild(list);
340
+ } else {
341
+ card.appendChild(el('p', 'empty', 'Nothing saved yet.'));
342
+ }
343
+ var people = (d.entities || []).filter(function (e) { return e.kind === 'person' || e.kind === 'project'; });
344
+ if (people.length) card.appendChild(chips(people, 6));
345
+ if (d.paused) card.appendChild(el('p', 'note', 'Saving new memories is paused.'));
346
+ var actions = el('div', 'actions');
347
+ actions.appendChild(button('Open in Virlow', 'secondary', openApp, 'Open Virlow in a new tab'));
348
+ card.appendChild(actions);
349
+ return card;
350
+ }
351
+
352
+ function render() {
353
+ root.textContent = '';
354
+ var d = state.data;
355
+ if (!d || typeof d !== 'object') return;
356
+ root.appendChild(view === 'context' ? renderContext(d) : renderSaved(d));
357
+ if (bridge) bridge.resized();
358
+ }
359
+
360
+ function fromOpenai() {
361
+ var host = openai();
362
+ if (!host) return;
363
+ applyTheme(host.theme);
364
+ if (host.toolOutput && typeof host.toolOutput === 'object') state.data = host.toolOutput;
365
+ var ws = host.widgetState;
366
+ if (ws && ws.removed === true) state.removed = true;
367
+ render();
368
+ }
369
+
370
+ function startBridge() {
371
+ var nextId = 1;
372
+ var pending = {};
373
+ var lastHeight = 0;
374
+ function post(msg) { window.parent.postMessage(msg, '*'); }
375
+ bridge = {
376
+ request: function (method, params) {
377
+ return new Promise(function (resolve, reject) {
378
+ var id = nextId++;
379
+ pending[id] = { resolve: resolve, reject: reject };
380
+ post({ jsonrpc: '2.0', id: id, method: method, params: params });
381
+ });
382
+ },
383
+ notify: function (method, params) { post({ jsonrpc: '2.0', method: method, params: params || {} }); },
384
+ // Set once the host answers ui/initialize. Nothing but that request
385
+ // goes to the host before then, as the MCP Apps lifecycle requires.
386
+ ready: false,
387
+ resized: function () {
388
+ if (!bridge.ready) return;
389
+ var h = Math.ceil(document.documentElement.getBoundingClientRect().height);
390
+ if (h && h !== lastHeight) { lastHeight = h; bridge.notify('ui/notifications/size-changed', { height: h }); }
391
+ }
392
+ };
393
+ window.addEventListener('message', function (event) {
394
+ if (event.source !== window.parent) return;
395
+ var m = event.data;
396
+ if (!m || m.jsonrpc !== '2.0') return;
397
+ if (m.method === undefined && m.id != null && pending[m.id]) {
398
+ var p = pending[m.id];
399
+ delete pending[m.id];
400
+ if (m.error) p.reject(new Error(m.error.message || 'error')); else p.resolve(m.result);
401
+ return;
402
+ }
403
+ var params = m.params || {};
404
+ if (m.method === 'ui/notifications/tool-result') {
405
+ if (params.structuredContent) { state.data = params.structuredContent; render(); }
406
+ } else if (m.method === 'ui/notifications/host-context-changed') {
407
+ applyTheme(params.theme);
408
+ } else if ((m.method === 'ui/resource-teardown' || m.method === 'ping') && m.id != null) {
409
+ post({ jsonrpc: '2.0', id: m.id, result: {} });
410
+ } else if (m.id != null) {
411
+ post({ jsonrpc: '2.0', id: m.id, error: { code: -32601, message: 'Method not found' } });
412
+ }
413
+ });
414
+ bridge.request('ui/initialize', {
415
+ appInfo: { name: 'virlow-card', version: '1' },
416
+ appCapabilities: {},
417
+ protocolVersion: '2026-01-26'
418
+ }).then(function (result) {
419
+ if (result && result.hostContext) applyTheme(result.hostContext.theme);
420
+ bridge.ready = true;
421
+ bridge.notify('ui/notifications/initialized');
422
+ // The host sizes the frame from these reports alone: without one an
423
+ // inline card can stay at zero height.
424
+ bridge.resized();
425
+ if (typeof ResizeObserver === 'function') {
426
+ new ResizeObserver(function () { bridge.resized(); }).observe(document.documentElement);
427
+ }
428
+ }).catch(function () {});
429
+ }
430
+
431
+ // Both, not either. ChatGPT injects window.openai into an MCP Apps
432
+ // (text/html;profile=mcp-app) frame too, but as an extension: the tool
433
+ // result reaches the card over the bridge, and only after the card has
434
+ // sent ui/initialize. A card that saw window.openai and skipped the
435
+ // bridge never initialized, never received its data and never reported a
436
+ // height, so ChatGPT showed nothing at all.
437
+ if (openai()) {
438
+ window.addEventListener('openai:set_globals', fromOpenai);
439
+ fromOpenai();
440
+ }
441
+ if (window.parent && window.parent !== window) {
442
+ startBridge();
443
+ }
444
+ })();
445
+ `;
446
+ /** The whole page for one card. */
447
+ export function widgetHtml(view) {
448
+ return ('<!doctype html><html lang="en"><head><meta charset="utf-8">' +
449
+ '<meta name="viewport" content="width=device-width,initial-scale=1">' +
450
+ `<title>${view === 'saved' ? 'Saved to Virlow' : 'What Virlow knows'}</title>` +
451
+ `<style>${CSS}</style></head>` +
452
+ `<body data-view="${view}"><div id="root"></div><script>${SCRIPT}</script></body></html>`);
453
+ }
454
+ /** The pieces, for the widget tests: they run the script against the page
455
+ * body in jsdom without a host. */
456
+ export const WIDGET_PARTS = { css: CSS, script: SCRIPT };
457
+ //# sourceMappingURL=widgets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widgets.js","sourceRoot":"","sources":["../src/widgets.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@batalabs/virlow-mcp-core",
3
- "version": "3.84.2",
3
+ "version": "3.85.1",
4
4
  "type": "module",
5
5
  "description": "Virlow MCP connector core: encrypted note and memory access, folder exposure, and the memories opt-in. Shared by the local virlow-mcp CLI and the hosted connector.",
6
6
  "license": "UNLICENSED",
@@ -60,8 +60,8 @@
60
60
  "@modelcontextprotocol/sdk": "^1.12.0",
61
61
  "yaml": ">=2.8.3 <3",
62
62
  "zod": "^3.24.0",
63
- "@batalabs/virlow-crypto": "3.84.2",
64
- "@batalabs/virlow-memory": "3.84.2"
63
+ "@batalabs/virlow-crypto": "3.85.1",
64
+ "@batalabs/virlow-memory": "3.85.1"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^22.10.0",