mnfst 0.5.165 → 0.5.166

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.
@@ -455,6 +455,106 @@
455
455
  })();
456
456
 
457
457
 
458
+ /* Manifest Chat — optional LLM (Claude) adapter
459
+ * By Andrew Matlock under MIT license · https://manifestx.dev
460
+ *
461
+ * Reference `claude` adapter: replies stream from a backend proxy holding the
462
+ * API key (tools/chat-llm-proxy.mjs). $chat never calls the LLM — this adapter
463
+ * does, behind the same contract. Optional, loaded separately from the bundle.
464
+ * Attachments ride draft.body.media[] as base64 → image/document blocks.
465
+ */
466
+
467
+ (function () {
468
+ 'use strict';
469
+
470
+ function ready(fn) {
471
+ if (window.ManifestChatAdapters) return fn();
472
+ const t = setInterval(() => { if (window.ManifestChatAdapters) { clearInterval(t); fn(); } }, 20);
473
+ setTimeout(() => clearInterval(t), 5000);
474
+ }
475
+
476
+ ready(function () {
477
+ const USER = { id: 'you', kind: 'human', role: 'user', displayName: 'You', color: '#7c3aed' };
478
+ const BOT = { id: 'claude', kind: 'agent', role: 'assistant', displayName: 'Claude', color: '#d97706' };
479
+
480
+ function claudeAdapter(opts) {
481
+ opts = opts || {};
482
+ // Same-origin relay mnfst-run serves for an `ai` block; override via
483
+ // opts.endpoint / window.CHAT_LLM_ENDPOINT.
484
+ const endpoint = opts.endpoint || window.CHAT_LLM_ENDPOINT || '/_ai/chat';
485
+ const system = opts.system || 'You are a helpful assistant for the Manifest framework docs. Answer in concise markdown.';
486
+ const handlers = {}; // conversationId -> subscribe handlers
487
+ let seq = 0;
488
+ const id = (p) => p + '_' + Date.now().toString(36) + '_' + (++seq);
489
+ const lsKey = (cid) => 'mnfst.chat.' + cid;
490
+
491
+ // text-only persistence so docs sessions survive reload (attachments stay ephemeral)
492
+ function loadStore(cid) { try { return JSON.parse(localStorage.getItem(lsKey(cid)) || '[]'); } catch { return []; } }
493
+ function saveMsg(cid, m) {
494
+ const all = loadStore(cid);
495
+ all.push({ id: m.id, role: m.author.kind === 'agent' ? 'assistant' : 'user', text: m.body.text, ts: m.ts });
496
+ try { localStorage.setItem(lsKey(cid), JSON.stringify(all.slice(-200))); } catch { }
497
+ }
498
+ const toMsg = (r) => ({ id: r.id, conversationId: null, author: r.role === 'assistant' ? BOT : USER, body: { text: r.text }, ts: r.ts, status: 'delivered' });
499
+
500
+ // Build the Anthropic messages array; attachments → image/document blocks.
501
+ function apiMessages(cid, draft) {
502
+ const hist = loadStore(cid).map(r => ({ role: r.role, content: r.text }));
503
+ const media = (draft.body && draft.body.media) || [];
504
+ const blocks = media.map(a => a.kind === 'image'
505
+ ? { type: 'image', source: { type: 'base64', media_type: a.mediaType, data: a.data } }
506
+ : { type: 'document', source: { type: 'base64', media_type: a.mediaType || 'application/pdf', data: a.data } });
507
+ const text = (draft.body && draft.body.text) || draft.text || '';
508
+ const last = blocks.length ? { role: 'user', content: [...blocks, { type: 'text', text }] } : { role: 'user', content: text };
509
+ return [...hist, last];
510
+ }
511
+
512
+ async function streamReply(cid) {
513
+ const h = handlers[cid]; if (!h) return;
514
+ const messages = streamReply._pending; streamReply._pending = null;
515
+ const mid = id('m');
516
+ h.onMessage && h.onMessage({ id: mid, conversationId: cid, author: BOT, body: { text: '' }, ts: Date.now(), status: 'streaming', meta: { model: opts.model } });
517
+ let full = '';
518
+ try {
519
+ const resp = await fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ system, model: opts.model, messages }) });
520
+ const reader = resp.body.getReader(); const dec = new TextDecoder(); let buf = '';
521
+ for (; ;) {
522
+ const { done, value } = await reader.read(); if (done) break;
523
+ buf += dec.decode(value, { stream: true });
524
+ let i;
525
+ while ((i = buf.indexOf('\n\n')) >= 0) {
526
+ const frame = buf.slice(0, i); buf = buf.slice(i + 2);
527
+ const line = frame.split('\n').find(l => l.startsWith('data:')); if (!line) continue;
528
+ let evt; try { evt = JSON.parse(line.slice(5).trim()); } catch { continue; }
529
+ if (evt.type === 'content_block_delta' && evt.delta && evt.delta.type === 'text_delta') {
530
+ full += evt.delta.text; h.onMessagePart && h.onMessagePart(mid, { text: evt.delta.text });
531
+ } else if (evt.type === 'error') { full += '\n\n_(error: ' + (evt.error && evt.error.message) + ')_'; h.onMessagePart && h.onMessagePart(mid, { text: '\n\n_(error)_' }); }
532
+ }
533
+ }
534
+ } catch (e) { h.onMessagePart && h.onMessagePart(mid, { text: '\n\n_(proxy unreachable — is tools/chat-llm-proxy.mjs running?)_' }); full += ' (proxy unreachable)'; }
535
+ h.onMessagePart && h.onMessagePart(mid, { text: '', done: true });
536
+ saveMsg(cid, { id: mid, author: BOT, body: { text: full }, ts: Date.now() });
537
+ }
538
+
539
+ return {
540
+ identity: () => USER,
541
+ async load(cid) { return { messages: loadStore(cid).map(toMsg), participants: [USER, BOT] }; },
542
+ subscribe(cid, h) { handlers[cid] = h; setTimeout(() => h.onConnection && h.onConnection(true), 0); return () => { delete handlers[cid]; }; },
543
+ async send(cid, draft) {
544
+ const mid = id('m'); const ts = Date.now();
545
+ saveMsg(cid, { id: mid, author: USER, body: { text: (draft.body && draft.body.text) || draft.text || '' }, ts });
546
+ streamReply._pending = apiMessages(cid, draft);
547
+ setTimeout(() => streamReply(cid), 30); // assistant reply streams in as a separate inbound
548
+ return { id: mid, ts };
549
+ }
550
+ };
551
+ }
552
+
553
+ window.ManifestChatAdapters.register('claude', claudeAdapter);
554
+ });
555
+ })();
556
+
557
+
458
558
  /* Manifest Chat — magic + init
459
559
  /* By Andrew Matlock under MIT license
460
560
  /* https://manifestx.dev
@@ -3,7 +3,7 @@
3
3
  "manifest.appwrite.data.js": "sha384-KFKVo5TQESLJB7wgk+C8Zb/sAl43QAMdioWW3tQRWYq5umfdpGCZ5Hld9y9f6/Np",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.charts.js": "sha384-wTQlB3hJ9V9lmisCgm8l6YkpJV+nA4+Vci3dI0x1nBtWnS9TFr+eXaJelP7AusyD",
6
- "manifest.chat.js": "sha384-HDyzPvNxqUOKVcLC3HNSFd5w83Flgm58LHy1BDLB9ejUsbYHAmr34G4X3FwKXokW",
6
+ "manifest.chat.js": "sha384-lBsqJdnSC4Gclt+te4Yh5V1LXZSGn3CCeCKPwtOk5gp6UlCROigBepvaAY3NKNZp",
7
7
  "manifest.code.js": "sha384-G3MNfvQRWWY+gm4SGGrSsgQbDeil9MP+ON2DTk6Rcq9Nsex8woHMDt5EJHrFH0Nj",
8
8
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
9
9
  "manifest.colorpicker.js": "sha384-N5ezEwgKrDodV4YdURxhdyZKmg1n/QhqvmAC4SEG03mw1pEAZAA8Gphp+NLAOxts",
@@ -29,5 +29,5 @@
29
29
  "manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
30
30
  "manifest.utilities.js": "sha384-MtzNaVOrgyepjlY5nz38pAJk67yiFPU1H5WoPNfcNbUZTXy+oin1lrtKCIcobEOJ",
31
31
  "manifest.virtual.js": "sha384-c194pXD0Ld48QJ+HPVNibvbn54/ETJRuYhUOWesEBjtOIQcQMIKo0DnCyQMvDlLg",
32
- "manifest.js": "sha384-VshJgT9QAxsLpwNmQZfrxw/EAxoyLIklo0DsmhxLhjVmmT70fLmn5eNRuq98o6zs"
32
+ "manifest.js": "sha384-RusFaByZu31LkLZCAhjgtJC8iWksvmjTKS8D5Xj9/4N7B/Xr4DDyFGmiL23kajgK"
33
33
  }
package/lib/manifest.js CHANGED
@@ -202,11 +202,29 @@
202
202
 
203
203
  // Configuration
204
204
  const DEFAULT_VERSION = 'latest';
205
- const ALPINE_CDN_URL = 'https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js';
205
+
206
+ // CDN fallback chain (first-party first). Each origin serves the npm scheme
207
+ // `<origin>/<pkg>@<version>/<path>`. Override order with `data-cdn`
208
+ // (comma-separated origins).
209
+ let CDN_HOSTS = [
210
+ 'https://cdn.manifestx.dev/npm',
211
+ 'https://cdn.jsdelivr.net/npm',
212
+ 'https://unpkg.com'
213
+ ];
214
+ function setCdnHosts(value) {
215
+ if (!value) return;
216
+ const hosts = value.split(',').map(s => s.trim().replace(/\/$/, '')).filter(Boolean);
217
+ if (hosts.length) CDN_HOSTS = hosts;
218
+ }
219
+
220
+ // unpkg serves packages as published (no auto-minify); mnfst ships unminified .js.
221
+ function hostFile(host, file) {
222
+ return host.includes('unpkg.com') ? file.replace(/\.min\.js$/, '.js') : file;
223
+ }
206
224
 
207
225
  // Get base URL for a given version
208
- function getBaseUrl(version = DEFAULT_VERSION) {
209
- return `https://cdn.jsdelivr.net/npm/mnfst@${version}/lib`;
226
+ function getBaseUrl(version = DEFAULT_VERSION, host = CDN_HOSTS[0]) {
227
+ return `${host}/mnfst@${version}/lib`;
210
228
  }
211
229
 
212
230
  // Available core plugins (auto-loaded if no data-plugins specified)
@@ -279,26 +297,29 @@
279
297
  });
280
298
  }
281
299
 
282
- // Plugin URL: `data-plugin-base` override → unminified `.js`; else the
283
- // jsDelivr CDN as `.min.js`.
300
+ // Plugin URLs: `data-plugin-base` override → single unminified `.js` (no
301
+ // fallback an explicit base, e.g. local dev, must fail loudly); else one
302
+ // candidate per CDN host, tried in order.
284
303
  let _pluginBase = null;
285
304
  function setPluginBase(b) { _pluginBase = b || null; }
286
- function getPluginUrl(pluginName, version = DEFAULT_VERSION) {
305
+ function getPluginUrlCandidates(pluginName, version = DEFAULT_VERSION) {
287
306
  // Hyphenated API name → dotted file name (`appwrite-auth` → appwrite.auth).
288
307
  const fileName = pluginName.replace(/-/g, '.');
289
308
  if (_pluginBase) {
290
309
  const base = _pluginBase.replace(/\/$/, '');
291
- return `${base}/manifest.${fileName}.js`;
310
+ return [`${base}/manifest.${fileName}.js`];
292
311
  }
293
- const base = getBaseUrl(version);
294
- return `${base}/manifest.${fileName}.min.js`;
312
+ return CDN_HOSTS.map(h => `${getBaseUrl(version, h)}/${hostFile(h, `manifest.${fileName}.min.js`)}`);
313
+ }
314
+ function getPluginUrl(pluginName, version = DEFAULT_VERSION) {
315
+ return getPluginUrlCandidates(pluginName, version)[0];
295
316
  }
296
317
 
297
- // Resolve Alpine CDN URL from a data-alpine value (version tag or full URL)
298
- function resolveAlpineUrl(dataAlpine) {
299
- if (!dataAlpine) return ALPINE_CDN_URL;
300
- if (dataAlpine.startsWith('http')) return dataAlpine;
301
- return `https://cdn.jsdelivr.net/npm/alpinejs@${dataAlpine}/dist/cdn.min.js`;
318
+ // Alpine URL candidates from a data-alpine value (version tag or full URL)
319
+ function alpineUrlCandidates(dataAlpine) {
320
+ if (dataAlpine && dataAlpine.startsWith('http')) return [dataAlpine];
321
+ const v = dataAlpine || '3';
322
+ return CDN_HOSTS.map(h => `${h}/alpinejs@${v}/dist/cdn.min.js`);
302
323
  }
303
324
 
304
325
  // Has DOMContentLoaded fired? readyState can't tell ('interactive' spans
@@ -331,7 +352,7 @@
331
352
  // fire `alpine:init` before the page's registrations exist. DCL fires only
332
353
  // after every deferred script runs, making the order deterministic (and cold
333
354
  // cache is already past DCL, so no added delay).
334
- function loadAlpine(alpineUrl = ALPINE_CDN_URL) {
355
+ function loadAlpine(alpineUrls) {
335
356
  whenDomReady(() => {
336
357
  if (window.Alpine) {
337
358
  return;
@@ -343,24 +364,30 @@
343
364
  return;
344
365
  }
345
366
 
346
- const script = document.createElement('script');
347
- script.src = alpineUrl;
348
- // No `defer` — past DCL, so execute as soon as it arrives.
349
- document.head.appendChild(script);
367
+ // Past DCL, so each candidate executes as soon as it arrives; fall
368
+ // through the CDN chain on error.
369
+ (async () => {
370
+ for (const url of alpineUrls) {
371
+ try {
372
+ return await injectScript(url);
373
+ } catch (_) {
374
+ console.warn(`[Manifest Loader] Alpine failed from ${url} — trying fallback CDN`);
375
+ }
376
+ }
377
+ console.error('[Manifest Loader] Alpine.js failed to load from all CDNs.');
378
+ })();
350
379
  });
351
380
  }
352
381
 
353
- // Add a script tag to the head and wait for it to load and execute
354
- function addScript(pluginName, version = DEFAULT_VERSION) {
382
+ // Inject one script URL and wait for it to load and execute
383
+ function injectScript(url) {
355
384
  return new Promise((resolve, reject) => {
356
- const url = getPluginUrl(pluginName, version);
357
-
358
385
  // Skip if script with same src already in DOM (e.g. prerendered HTML or second loader run)
359
386
  const existing = document.querySelector(`script[src="${url}"]`);
360
387
  if (existing) {
361
388
  if (existing.complete) return resolve();
362
389
  existing.addEventListener('load', () => resolve());
363
- existing.addEventListener('error', () => reject(new Error(`Failed to load ${pluginName} from ${url}`)));
390
+ existing.addEventListener('error', () => reject(new Error(`Failed to load ${url}`)));
364
391
  return;
365
392
  }
366
393
 
@@ -368,11 +395,28 @@
368
395
  script.src = url;
369
396
  script.async = false; // Ensure scripts execute in order
370
397
  script.onload = () => resolve();
371
- script.onerror = () => reject(new Error(`Failed to load ${pluginName} from ${url}`));
398
+ script.onerror = () => { script.remove(); reject(new Error(`Failed to load ${url}`)); };
372
399
  document.head.appendChild(script);
373
400
  });
374
401
  }
375
402
 
403
+ // Load a plugin, falling through the CDN chain on error. A fallback insert
404
+ // lands after already-inserted scripts, so strict cross-plugin execution
405
+ // order is traded for availability in the (already degraded) fallback case.
406
+ async function addScript(pluginName, version = DEFAULT_VERSION) {
407
+ const urls = getPluginUrlCandidates(pluginName, version);
408
+ let lastErr = null;
409
+ for (const url of urls) {
410
+ try {
411
+ return await injectScript(url);
412
+ } catch (e) {
413
+ lastErr = e;
414
+ if (urls.length > 1) console.warn(`[Manifest Loader] ${url} failed — trying fallback CDN`);
415
+ }
416
+ }
417
+ throw lastErr || new Error(`Failed to load ${pluginName}`);
418
+ }
419
+
376
420
  // Resolve plugin dependencies (auto-inject required dependencies)
377
421
  function resolveDependencies(pluginList) {
378
422
  const resolved = [];
@@ -435,6 +479,15 @@
435
479
  return [];
436
480
  }
437
481
 
482
+ // Detect the chat plugin from manifest.json content.
483
+ // Opt-in / auto-loaded when an `ai` (or `chat`) config block is present.
484
+ function detectChatPlugins(manifest) {
485
+ if (!manifest || typeof manifest !== 'object') return [];
486
+ if ((manifest.ai && typeof manifest.ai === 'object') ||
487
+ (manifest.chat && typeof manifest.chat === 'object')) return ['chat'];
488
+ return [];
489
+ }
490
+
438
491
  // Parse data attributes
439
492
  function parseDataAttributes() {
440
493
  // Try to get current script first, then fall back to querySelector
@@ -455,6 +508,8 @@
455
508
  // Override: resolve plugin URLs against this base (dir serving
456
509
  // `manifest.<name>.js`, relative or absolute) instead of the CDN.
457
510
  const pluginBase = script.getAttribute('data-plugin-base');
511
+ // Override: custom CDN fallback chain (comma-separated origins).
512
+ const cdn = script.getAttribute('data-cdn');
458
513
 
459
514
  let pluginList = [];
460
515
  const deriveFromManifest = !plugins;
@@ -483,37 +538,29 @@
483
538
  version,
484
539
  alpine,
485
540
  pluginBase,
541
+ cdn,
486
542
  };
487
543
  }
488
544
 
489
- // Load custom Tailwind CDN script
490
- function loadTailwind(version = DEFAULT_VERSION) {
491
- return new Promise((resolve, reject) => {
492
- const base = getBaseUrl(version);
493
- const tailwindUrl = `${base}/manifest.tailwind.min.js`;
494
-
495
- // Check if already loaded
496
- const existing = document.querySelector(`script[src="${tailwindUrl}"]`);
497
- if (existing && existing.complete) {
498
- return resolve();
545
+ // Load custom Tailwind CDN script, falling through the CDN chain on error
546
+ async function loadTailwind(version = DEFAULT_VERSION) {
547
+ const urls = CDN_HOSTS.map(h => `${getBaseUrl(version, h)}/${hostFile(h, 'manifest.tailwind.min.js')}`);
548
+ let lastErr = null;
549
+ for (const url of urls) {
550
+ try {
551
+ return await injectScript(url);
552
+ } catch (e) {
553
+ lastErr = e;
499
554
  }
500
-
501
- const script = document.createElement('script');
502
- script.src = tailwindUrl;
503
- script.async = false;
504
- script.onload = () => resolve();
505
- script.onerror = () => {
506
- console.warn(`[Manifest Loader] Tailwind plugin not yet published to CDN. Load it directly: <script src="/scripts/tailwind.v4.3.1.js"></script>`);
507
- reject(new Error(`Tailwind plugin not available from CDN. Load it directly from your project.`));
508
- };
509
- document.head.appendChild(script);
510
- });
555
+ }
556
+ console.warn(`[Manifest Loader] Tailwind plugin not available from any CDN. Load it directly: <script src="/scripts/tailwind.v4.3.1.js"></script>`);
557
+ throw lastErr || new Error(`Tailwind plugin not available from CDN.`);
511
558
  }
512
559
 
513
560
  // Expose API
514
561
  window.Manifest = {
515
562
  loadPlugin: function (pluginName, version = DEFAULT_VERSION) {
516
- const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, 'payments'];
563
+ const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, 'payments', 'chat'];
517
564
  if (!allPlugins.includes(pluginName)) {
518
565
  console.warn(`[Manifest Loader] Unknown plugin: ${pluginName}`);
519
566
  return Promise.reject(new Error(`Unknown plugin: ${pluginName}`));
@@ -532,6 +579,7 @@
532
579
  // Parse config and load plugins
533
580
  const config = parseDataAttributes();
534
581
  if (config && config.pluginBase) setPluginBase(config.pluginBase);
582
+ if (config && config.cdn) setCdnHosts(config.cdn);
535
583
 
536
584
  if (config && config.plugins.length > 0) {
537
585
  if (window.__manifestLoaderStarted) {
@@ -605,7 +653,8 @@
605
653
  const corePlugins = getDefaultPluginsFromManifest(manifest);
606
654
  const appwritePlugins = detectAppwritePlugins(manifest);
607
655
  const paymentsPlugins = detectPaymentsPlugins(manifest);
608
- pluginsToLoad = resolveDependencies([...corePlugins, ...appwritePlugins, ...paymentsPlugins]);
656
+ const chatPlugins = detectChatPlugins(manifest);
657
+ pluginsToLoad = resolveDependencies([...corePlugins, ...appwritePlugins, ...paymentsPlugins, ...chatPlugins]);
609
658
  } else {
610
659
  const needsManifest = config.plugins.some(p => MANIFEST_DEPENDENT_PLUGINS.includes(p));
611
660
  if (needsManifest) {
@@ -634,7 +683,7 @@
634
683
  window.ManifestComponentsRegistry.manifest = manifest;
635
684
  }
636
685
  }
637
- loadAlpine(resolveAlpineUrl(config.alpine));
686
+ loadAlpine(alpineUrlCandidates(config.alpine));
638
687
  };
639
688
 
640
689
  loadPlugins();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.165",
3
+ "version": "0.5.166",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",
@@ -27,7 +27,7 @@
27
27
  "render": "node src/scripts/manifest.render.mjs --root src",
28
28
  "prerender:docs": "node src/scripts/manifest.render.mjs --root docs",
29
29
  "prerender:starter": "node src/scripts/manifest.render.mjs --root templates/starter",
30
- "release": "node scripts/release-bump.mjs . && npm publish",
30
+ "release": "node scripts/release-bump.mjs . && npm publish && node scripts/cdn-warm.mjs",
31
31
  "release:run": "node scripts/release-bump.mjs packages/run && cd packages/run && npm publish --auth-type=web",
32
32
  "release:render": "node scripts/release-bump.mjs packages/render && cd packages/render && npm publish --auth-type=web",
33
33
  "release:types": "node scripts/release-bump.mjs packages/types && cd packages/types && npm publish --auth-type=web",