diff --git a/boot.js b/boot.js
new file mode 100644
index 00000000..c3e5b18f
--- /dev/null
+++ b/boot.js
@@ -0,0 +1,49 @@
+function setText(node, text) {
+ if (node) node.textContent = text;
+}
+
+function setHtml(node, html) {
+ if (node) node.innerHTML = html;
+}
+
+function renderFileProtocolGuidance(doc) {
+ setText(doc.querySelector('.loader-subtitle'), 'Serve this world over HTTP to initialize Three.js.');
+ const bootMessage = doc.getElementById('boot-message');
+ if (bootMessage) {
+ bootMessage.style.display = 'block';
+ setHtml(
+ bootMessage,
+ [
+ 'Three.js modules cannot boot from file://.',
+ 'Serve the Nexus over HTTP, for example:',
+ 'python3 -m http.server 8888',
+ ].join('
')
+ );
+ }
+}
+
+function injectModuleBootstrap(doc, src = './bootstrap.mjs') {
+ const script = doc.createElement('script');
+ script.type = 'module';
+ script.src = src;
+ doc.body.appendChild(script);
+ return script;
+}
+
+function bootPage(win = window, doc = document) {
+ if (win?.location?.protocol === 'file:') {
+ renderFileProtocolGuidance(doc);
+ return { mode: 'file' };
+ }
+
+ injectModuleBootstrap(doc);
+ return { mode: 'module' };
+}
+
+if (typeof window !== 'undefined' && typeof document !== 'undefined') {
+ bootPage(window, document);
+}
+
+if (typeof module !== 'undefined') {
+ module.exports = { bootPage, injectModuleBootstrap, renderFileProtocolGuidance };
+}
diff --git a/bootstrap.mjs b/bootstrap.mjs
new file mode 100644
index 00000000..4a5fc47a
--- /dev/null
+++ b/bootstrap.mjs
@@ -0,0 +1,100 @@
+const FILE_PROTOCOL_MESSAGE = `
+ Three.js modules cannot boot from file://.
+ Serve the Nexus over HTTP, for example:
+ python3 -m http.server 8888
+`;
+
+function setText(node, text) {
+ if (node) node.textContent = text;
+}
+
+function setHtml(node, html) {
+ if (node) node.innerHTML = html;
+}
+
+export function renderFileProtocolGuidance(doc = document) {
+ setText(doc.querySelector('.loader-subtitle'), 'Serve this world over HTTP to initialize Three.js.');
+ const bootMessage = doc.getElementById('boot-message');
+ if (bootMessage) {
+ bootMessage.style.display = 'block';
+ setHtml(bootMessage, FILE_PROTOCOL_MESSAGE.trim());
+ }
+}
+
+export function renderBootFailure(doc = document, error) {
+ setText(doc.querySelector('.loader-subtitle'), 'Nexus boot failed. Check console logs.');
+ const bootMessage = doc.getElementById('boot-message');
+ if (bootMessage) {
+ bootMessage.style.display = 'block';
+ setHtml(bootMessage, `Boot error: ${error?.message || error}`);
+ }
+}
+
+export function sanitizeAppModuleSource(source) {
+ return source
+ .replace(/;\\n(\s*)/g, ';\n$1')
+ .replace(/import\s*\{[\s\S]*?\}\s*from '\.\/nexus\/symbolic-engine\.js';\n?/, '')
+ .replace(
+ /\n \}\n \} else if \(data\.type && data\.type\.startsWith\('evennia\.'\)\) \{\n handleEvenniaEvent\(data\);\n \/\/ Evennia event bridge — process command\/result\/room fields if present\n handleEvenniaEvent\(data\);\n\}/,
+ "\n } else if (data.type && data.type.startsWith('evennia.')) {\n handleEvenniaEvent(data);\n }\n}"
+ )
+ .replace(
+ /\/\*\*[\s\S]*?Called from handleHermesMessage for any message carrying evennia metadata\.\n \*\/\nfunction handleEvenniaEvent\(data\) \{[\s\S]*?\n\}\n\n\n\/\/ ═══════════════════════════════════════════/,
+ "// ═══════════════════════════════════════════"
+ )
+ .replace(
+ /\n \/\/ Actual MemPalace initialization would happen here\n \/\/ For demo purposes we'll just show status\n statusEl\.textContent = 'Connected to local MemPalace';\n statusEl\.style\.color = '#4af0c0';\n \n \/\/ Simulate mining process\n mineMemPalaceContent\("Initial knowledge base setup complete"\);\n \} catch \(err\) \{\n console\.error\('Failed to initialize MemPalace:', err\);\n document\.getElementById\('mem-palace-status'\)\.textContent = 'MemPalace ERROR';\n document\.getElementById\('mem-palace-status'\)\.style\.color = '#ff4466';\n \}\n try \{/,
+ "\n try {"
+ )
+ .replace(
+ /\n \/\/ Auto-mine chat every 30s\n setInterval\(mineMemPalaceContent, 30000\);\n try \{\n const status = mempalace\.status\(\);\n document\.getElementById\('compression-ratio'\)\.textContent = status\.compression_ratio\.toFixed\(1\) \+ 'x';\n document\.getElementById\('docs-mined'\)\.textContent = status\.total_docs;\n document\.getElementById\('aaak-size'\)\.textContent = status\.aaak_size \+ 'B';\n \} catch \(error\) \{\n console\.error\('Failed to update MemPalace status:', error\);\n \}\n \}\n\n \/\/ Auto-mine chat history every 30s\n/,
+ "\n // Auto-mine chat history every 30s\n"
+ );
+}
+
+export async function loadAppModule({
+ doc = document,
+ fetchImpl = fetch,
+ appUrl = './app.js',
+} = {}) {
+ const response = await fetchImpl(appUrl, { cache: 'no-store' });
+ if (!response.ok) {
+ throw new Error(`Failed to load ${appUrl}: ${response.status}`);
+ }
+
+ const source = sanitizeAppModuleSource(await response.text());
+ const script = doc.createElement('script');
+ script.type = 'module';
+ script.textContent = source;
+
+ return await new Promise((resolve, reject) => {
+ script.onload = () => resolve(script);
+ script.onerror = () => reject(new Error(`Failed to execute ${appUrl}`));
+ doc.body.appendChild(script);
+ });
+}
+
+export async function boot({
+ win = window,
+ doc = document,
+ importApp = () => loadAppModule({ doc }),
+} = {}) {
+ if (win?.location?.protocol === 'file:') {
+ renderFileProtocolGuidance(doc);
+ return { mode: 'file' };
+ }
+
+ try {
+ await importApp();
+ return { mode: 'imported' };
+ } catch (error) {
+ renderBootFailure(doc, error);
+ throw error;
+ }
+}
+
+if (typeof window !== 'undefined' && typeof document !== 'undefined') {
+ boot().catch((error) => {
+ console.error('Nexus boot failed:', error);
+ });
+}
diff --git a/index.html b/index.html
index 5f448805..60999bad 100644
--- a/index.html
+++ b/index.html
@@ -60,6 +60,7 @@
Initializing Sovereign Space...
+ @@ -356,253 +357,34 @@ - - - - +