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 +0 -0
- package/package.json +1 -1
- package/serve.mjs +60 -2
package/bin/mnfst-run.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
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:
|
|
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
|
-
|
|
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
|
|