mnfst-run 1.0.22 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/mnfst-run.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-run",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "Zero-dependency dev server for Manifest projects",
5
5
  "type": "module",
6
6
  "bin": {
package/serve.mjs CHANGED
@@ -101,6 +101,7 @@ const MIME = {
101
101
  // server the tab is gone.
102
102
  const LIVE_RELOAD_SCRIPT = `<script>
103
103
  (function () {
104
+ window.__mnfstRun = true; // dev marker: the framework loader skips its service worker here
104
105
  var tabId = (window.crypto && crypto.randomUUID)
105
106
  ? crypto.randomUUID()
106
107
  : (Math.random().toString(36).slice(2) + Date.now().toString(36));
@@ -396,6 +397,32 @@ const aiConfig = (() => {
396
397
  try { return JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8')).ai || null; }
397
398
  catch { return null; }
398
399
  })();
400
+
401
+ // Doc-grounding: `ai.system` (persona/instructions) + optional `ai.grounding`
402
+ // (URL or project-relative file whose text is appended to the system prompt).
403
+ // Resolved ONCE at startup so the combined prompt is byte-identical across
404
+ // requests — the precondition for the server-side prompt cache to hit.
405
+ let aiSystemResolved = (aiConfig && aiConfig.system) || '';
406
+ async function resolveAiGrounding() {
407
+ const src = aiConfig && aiConfig.grounding;
408
+ if (!src) return;
409
+ try {
410
+ let text;
411
+ if (/^https?:\/\//.test(src)) {
412
+ const r = await fetch(src);
413
+ if (!r.ok) throw new Error(`http ${r.status}`);
414
+ text = await r.text();
415
+ } else {
416
+ text = readFileSync(join(root, src), 'utf8');
417
+ }
418
+ aiSystemResolved = (aiSystemResolved ? aiSystemResolved + '\n\n' : '')
419
+ + '<reference_documentation>\n' + text + '\n</reference_documentation>';
420
+ console.log(`[mnfst-run] AI grounding loaded from ${src} (${text.length} bytes)`);
421
+ } catch (e) {
422
+ console.warn(`[mnfst-run] AI grounding failed to load from ${src}: ${e.message} — continuing without it`);
423
+ }
424
+ }
425
+ if (aiConfig) resolveAiGrounding();
399
426
  const aiKey = process.env.ANTHROPIC_API_KEY || privateEnv.ANTHROPIC_API_KEY || '';
400
427
  if (publicEnv.PUBLIC_ANTHROPIC_API_KEY) {
401
428
  // The one footgun: a PUBLIC_-prefixed LLM key would ship to every visitor.
@@ -420,6 +447,12 @@ function streamMockAi(res) {
420
447
  res.on('close', () => clearInterval(tick));
421
448
  }
422
449
  async function streamRealAi(res, payload) {
450
+ // System prompt as a cache_control block: the (instructions + grounding) text
451
+ // is byte-identical every request, so Anthropic serves it from the prompt
452
+ // cache (~0.1× input price) after the first request — long doc-grounded
453
+ // prompts cost near-nothing per message. Per-request payload.system overrides
454
+ // (adapter opts) still work but won't share the project-level cache entry.
455
+ const systemText = payload.system || aiSystemResolved;
423
456
  const upstream = await fetch('https://api.anthropic.com/v1/messages', {
424
457
  method: 'POST',
425
458
  headers: { 'content-type': 'application/json', 'x-api-key': aiKey, 'anthropic-version': '2023-06-01' },
@@ -427,7 +460,7 @@ async function streamRealAi(res, payload) {
427
460
  model: payload.model || aiConfig.model || 'claude-haiku-4-5',
428
461
  max_tokens: payload.max_tokens || aiConfig.maxTokens || 1024,
429
462
  stream: true,
430
- system: payload.system || aiConfig.system || undefined,
463
+ system: systemText ? [{ type: 'text', text: systemText, cache_control: { type: 'ephemeral' } }] : undefined,
431
464
  messages: payload.messages || []
432
465
  })
433
466
  });
@@ -764,7 +797,12 @@ function navigateToNode(html, path) { // path indices are relative t
764
797
  }
765
798
  function resolveComponentFile(manifest, name) {
766
799
  const all = [...(manifest.preloadedComponents || []), ...(manifest.components || [])];
767
- return all.find(p => String(p).split('/').pop().replace('.html', '') === name) || null;
800
+ const listed = all.find(p => String(p).split('/').pop().replace('.html', '') === name);
801
+ if (listed) return listed;
802
+ // Convention component: components/<name>.html needs no manifest entry.
803
+ const rel = 'components/' + name + '.html';
804
+ const file = safeResolve('/' + rel);
805
+ return file && isFile(file) ? rel : null;
768
806
  }
769
807
 
770
808
  // Theme var write: rewrite (or append) a single `--var: value;` in the target CSS file.
@@ -865,6 +903,20 @@ function writeComponentInstance(file, key, overrides, removals) {
865
903
  return { region: key, status: 'written', file: basename(file), applied: Object.keys(overrides || {}), removed: removals || [] };
866
904
  }
867
905
 
906
+ // No-op service worker served at /sw.js when the project has none: a stale
907
+ // production worker (cached shell from a previous deploy) can never hold a
908
+ // dev session. It replaces that worker at the next update check, clears the
909
+ // framework's caches and unregisters itself.
910
+ const NOOP_SW = `// mnfst-run: no-op service worker (unregisters itself, clears Manifest caches)
911
+ self.addEventListener('install', function () { self.skipWaiting(); });
912
+ self.addEventListener('activate', function (e) {
913
+ e.waitUntil((async function () {
914
+ try { var keys = await caches.keys(); await Promise.all(keys.filter(function (k) { return k.indexOf('mnfst-sw:') === 0; }).map(function (k) { return caches.delete(k); })); } catch (_) {}
915
+ try { await self.registration.unregister(); } catch (_) {}
916
+ })());
917
+ });
918
+ `;
919
+
868
920
  // Resolve a request path against `root` and refuse anything that escapes.
869
921
  // `path.join` does NOT prevent `..` traversal — `join('/a/b', '/../../etc/passwd')`
870
922
  // returns `/etc/passwd`. Use `path.resolve` + an explicit prefix check.
@@ -1158,6 +1210,12 @@ const server = createServer((req, res) => {
1158
1210
  const exact = safeResolve(urlPath);
1159
1211
  if (exact && isFile(exact)) return serveFile(res, exact);
1160
1212
 
1213
+ if (urlPath === '/sw.js') {
1214
+ res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store' });
1215
+ res.end(NOOP_SW);
1216
+ return;
1217
+ }
1218
+
1161
1219
  const indexPath = safeResolve(urlPath.replace(/\/$/, '') + '/index.html');
1162
1220
  if (indexPath && isFile(indexPath)) return serveFile(res, indexPath);
1163
1221