@mneme-ai/core 1.90.0 → 1.92.0

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,72 @@
1
+ /**
2
+ * v1.92.0 -- RAINBOW: PHOENIX (tunnel watchdog + URL push).
3
+ *
4
+ * Quick tunnels (`cloudflared tunnel --url http://localhost:PORT`) die.
5
+ * They die when the process exits, when idle ~30 min, or randomly when
6
+ * the Cloudflare edge garbage-collects them. The user scans a QR five
7
+ * minutes later and gets HTTP 404.
8
+ *
9
+ * PHOENIX:
10
+ * 1. Periodically probes the tunnel URL with HEAD / GET.
11
+ * 2. If the probe fails N consecutive times -> respawn cloudflared.
12
+ * 3. Calls every onUrlChange listener with the new URL.
13
+ * 4. Exposes a /events SSE endpoint so the served PC + mobile pages
14
+ * can subscribe and re-render their QR LIVE when the URL changes
15
+ * -- without the user reopening the page.
16
+ *
17
+ * The page on the phone never has to be reloaded. The wizard never has
18
+ * to confess "the URL died."
19
+ */
20
+ export interface TunnelProbeResult {
21
+ url: string;
22
+ ok: boolean;
23
+ status: number | null;
24
+ elapsedMs: number;
25
+ /** Reason for failure (network, http status, etc). */
26
+ reason?: string;
27
+ }
28
+ export interface PhoenixOptions {
29
+ /** Initial tunnel URL (already-spawned tunnel from startQuickTunnel). */
30
+ initialUrl: string;
31
+ /** Probe interval in ms. Default 30_000. */
32
+ probeIntervalMs?: number;
33
+ /** Consecutive failures before respawn. Default 2. */
34
+ failuresBeforeRespawn?: number;
35
+ /** Function to probe the URL. Default: fetch HEAD. */
36
+ probeFn?: (url: string) => Promise<TunnelProbeResult>;
37
+ /** Function that respawns the tunnel + returns the new URL. */
38
+ respawnFn: () => Promise<string | null>;
39
+ /** Optional logger. */
40
+ log?: (msg: string) => void;
41
+ }
42
+ export interface PhoenixHandle {
43
+ getUrl(): string;
44
+ /** Subscribe to URL changes. Returns an unsubscribe function. */
45
+ onUrlChange(cb: (newUrl: string, oldUrl: string) => void): () => void;
46
+ /** History of every probe + respawn (newest last). */
47
+ getHistory(): Array<{
48
+ when: number;
49
+ kind: "probe" | "respawn";
50
+ ok: boolean;
51
+ url: string;
52
+ note?: string;
53
+ }>;
54
+ /** Stop the watchdog. Tunnel itself is NOT killed -- caller owns it. */
55
+ stop(): void;
56
+ }
57
+ export declare function createPhoenix(opts: PhoenixOptions): PhoenixHandle;
58
+ /** Inline JS for the served page so it can SUBSCRIBE to URL changes via
59
+ * Server-Sent Events. The page polls /events; when the server emits a
60
+ * url-change event, the page replaces its QR <img> src + URL text.
61
+ *
62
+ * Server side: open SSE response, send `event: url-change\ndata: NEW_URL\n\n`
63
+ * each time PHOENIX fires onUrlChange. */
64
+ export declare function renderPhoenixSubscriberScript(input: {
65
+ eventsUrl: string;
66
+ qrImgId: string;
67
+ urlTextId: string;
68
+ }): string;
69
+ /** Format the SSE wire payload for a single url-change event. Server
70
+ * writes this byte sequence to the response body. */
71
+ export declare function formatUrlChangeSseFrame(newUrl: string): string;
72
+ //# sourceMappingURL=phoenix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phoenix.d.ts","sourceRoot":"","sources":["../../src/rainbow/phoenix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACtD,+DAA+D;IAC/D,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,uBAAuB;IACvB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,IAAI,MAAM,CAAC;IACjB,iEAAiE;IACjE,WAAW,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACtE,sDAAsD;IACtD,UAAU,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1G,wEAAwE;IACxE,IAAI,IAAI,IAAI,CAAC;CACd;AA8BD,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,aAAa,CAsEjE;AAED;;;;;2CAK2C;AAC3C,wBAAgB,6BAA6B,CAAC,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAgBtH;AAED;sDACsD;AACtD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK9D"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * v1.92.0 -- RAINBOW: PHOENIX (tunnel watchdog + URL push).
3
+ *
4
+ * Quick tunnels (`cloudflared tunnel --url http://localhost:PORT`) die.
5
+ * They die when the process exits, when idle ~30 min, or randomly when
6
+ * the Cloudflare edge garbage-collects them. The user scans a QR five
7
+ * minutes later and gets HTTP 404.
8
+ *
9
+ * PHOENIX:
10
+ * 1. Periodically probes the tunnel URL with HEAD / GET.
11
+ * 2. If the probe fails N consecutive times -> respawn cloudflared.
12
+ * 3. Calls every onUrlChange listener with the new URL.
13
+ * 4. Exposes a /events SSE endpoint so the served PC + mobile pages
14
+ * can subscribe and re-render their QR LIVE when the URL changes
15
+ * -- without the user reopening the page.
16
+ *
17
+ * The page on the phone never has to be reloaded. The wizard never has
18
+ * to confess "the URL died."
19
+ */
20
+ import { setTimeout as nodeSetTimeout, clearTimeout as nodeClearTimeout } from "node:timers";
21
+ const HISTORY_MAX = 50;
22
+ /** Default probe: HEAD request with 8s timeout. A 5xx still counts as
23
+ * "tunnel alive" because the LAN server is reachable through it.
24
+ * A 4xx from cloudflared edge (specifically 530/521/404) means the
25
+ * tunnel itself is dead. */
26
+ async function defaultProbe(url) {
27
+ const t0 = Date.now();
28
+ const controller = new AbortController();
29
+ const timer = nodeSetTimeout(() => controller.abort(), 8000);
30
+ try {
31
+ const r = await fetch(url, { method: "HEAD", signal: controller.signal });
32
+ nodeClearTimeout(timer);
33
+ const elapsedMs = Date.now() - t0;
34
+ const isDead = r.status === 404 || r.status === 530 || r.status === 521 || r.status === 502;
35
+ return {
36
+ url,
37
+ ok: !isDead,
38
+ status: r.status,
39
+ elapsedMs,
40
+ reason: isDead ? `tunnel edge returned ${r.status}` : undefined,
41
+ };
42
+ }
43
+ catch (e) {
44
+ nodeClearTimeout(timer);
45
+ return { url, ok: false, status: null, elapsedMs: Date.now() - t0, reason: e.message };
46
+ }
47
+ }
48
+ export function createPhoenix(opts) {
49
+ let url = opts.initialUrl;
50
+ const probeIntervalMs = opts.probeIntervalMs ?? 30_000;
51
+ const failuresBeforeRespawn = opts.failuresBeforeRespawn ?? 2;
52
+ const probe = opts.probeFn ?? defaultProbe;
53
+ const log = opts.log ?? (() => undefined);
54
+ const listeners = [];
55
+ const history = [];
56
+ let consecutiveFailures = 0;
57
+ let stopped = false;
58
+ let timer = null;
59
+ function record(entry) {
60
+ history.push(entry);
61
+ if (history.length > HISTORY_MAX)
62
+ history.shift();
63
+ }
64
+ async function tick() {
65
+ if (stopped)
66
+ return;
67
+ try {
68
+ const r = await probe(url);
69
+ record({ when: Date.now(), kind: "probe", ok: r.ok, url, note: r.reason });
70
+ log(`probe ${url} ok=${r.ok} status=${r.status} elapsed=${r.elapsedMs}ms`);
71
+ if (r.ok) {
72
+ consecutiveFailures = 0;
73
+ }
74
+ else {
75
+ consecutiveFailures++;
76
+ if (consecutiveFailures >= failuresBeforeRespawn) {
77
+ consecutiveFailures = 0;
78
+ log(`tunnel dead (${r.reason ?? "unknown"}) -- respawning`);
79
+ const newUrl = await opts.respawnFn();
80
+ if (newUrl && newUrl !== url) {
81
+ const oldUrl = url;
82
+ url = newUrl;
83
+ record({ when: Date.now(), kind: "respawn", ok: true, url: newUrl, note: `replaced ${oldUrl}` });
84
+ log(`tunnel respawned: ${oldUrl} -> ${newUrl}`);
85
+ for (const cb of [...listeners]) {
86
+ try {
87
+ cb(newUrl, oldUrl);
88
+ }
89
+ catch { /* swallow */ }
90
+ }
91
+ }
92
+ else {
93
+ record({ when: Date.now(), kind: "respawn", ok: false, url, note: "respawnFn returned null" });
94
+ log(`respawn failed -- staying on ${url}`);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ catch (e) {
100
+ record({ when: Date.now(), kind: "probe", ok: false, url, note: e.message });
101
+ }
102
+ if (!stopped)
103
+ timer = nodeSetTimeout(tick, probeIntervalMs);
104
+ }
105
+ // First probe happens after one interval, not immediately, so the
106
+ // initial tunnel has time to settle.
107
+ timer = nodeSetTimeout(tick, probeIntervalMs);
108
+ return {
109
+ getUrl: () => url,
110
+ onUrlChange: (cb) => {
111
+ listeners.push(cb);
112
+ return () => {
113
+ const i = listeners.indexOf(cb);
114
+ if (i >= 0)
115
+ listeners.splice(i, 1);
116
+ };
117
+ },
118
+ getHistory: () => history.slice(),
119
+ stop: () => {
120
+ stopped = true;
121
+ if (timer) {
122
+ nodeClearTimeout(timer);
123
+ timer = null;
124
+ }
125
+ },
126
+ };
127
+ }
128
+ /** Inline JS for the served page so it can SUBSCRIBE to URL changes via
129
+ * Server-Sent Events. The page polls /events; when the server emits a
130
+ * url-change event, the page replaces its QR <img> src + URL text.
131
+ *
132
+ * Server side: open SSE response, send `event: url-change\ndata: NEW_URL\n\n`
133
+ * each time PHOENIX fires onUrlChange. */
134
+ export function renderPhoenixSubscriberScript(input) {
135
+ const eventsUrl = JSON.stringify(input.eventsUrl);
136
+ const qrId = JSON.stringify(input.qrImgId);
137
+ const urlId = JSON.stringify(input.urlTextId);
138
+ return `(function(){
139
+ if (typeof EventSource === "undefined") return;
140
+ var es = new EventSource(${eventsUrl});
141
+ es.addEventListener("url-change", function(ev) {
142
+ var newUrl = ev.data;
143
+ var img = document.getElementById(${qrId});
144
+ if (img) img.src = "https://api.qrserver.com/v1/create-qr-code/?size=360x360&format=svg&margin=10&data=" + encodeURIComponent(newUrl);
145
+ var txt = document.getElementById(${urlId});
146
+ if (txt) txt.textContent = newUrl;
147
+ });
148
+ es.addEventListener("error", function() { /* auto-reconnects */ });
149
+ })();`;
150
+ }
151
+ /** Format the SSE wire payload for a single url-change event. Server
152
+ * writes this byte sequence to the response body. */
153
+ export function formatUrlChangeSseFrame(newUrl) {
154
+ // Strip newlines from URL just in case (URL spec doesn't allow them
155
+ // but be defensive).
156
+ const safe = String(newUrl).replace(/\r?\n/g, "");
157
+ return `event: url-change\ndata: ${safe}\n\n`;
158
+ }
159
+ //# sourceMappingURL=phoenix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phoenix.js","sourceRoot":"","sources":["../../src/rainbow/phoenix.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * v1.92.0 -- RAINBOW: SAME-SHELL (same-machine 1-click clone).
3
+ *
4
+ * The forgotten case: when the user is on ONE machine running editor AI
5
+ * (Cursor / Claude Code) AND wants to continue with Web AI (ChatGPT.com,
6
+ * Gemini.com) in a browser ON THE SAME machine, the existing RAINBOW
7
+ * path forces them through QR → tunnel → phone scan → page → copy →
8
+ * back to PC → paste. Eight steps for a zero-network handoff.
9
+ *
10
+ * SAME-SHELL collapses this to two steps:
11
+ *
12
+ * 1. Editor AI: mneme.rainbow.show_local()
13
+ * → opens default browser at http://localhost:PORT
14
+ * → page auto-copies soul to clipboard on load
15
+ *
16
+ * 2. User opens chatgpt.com/gemini.com/claude.ai (link on page),
17
+ * Ctrl+V, done.
18
+ *
19
+ * Zero QR. Zero tunnel. Zero dpaste. Zero risk of 404 because no
20
+ * external network is involved at all. THIS IS THE FASTEST PATH AND
21
+ * THE LEAST FRAGILE -- but we never told the user it existed.
22
+ */
23
+ export interface SameShellPageInput {
24
+ soulText: string;
25
+ /** Port the local HTTP server is on. Used in the URL displayed for context. */
26
+ port: number;
27
+ defaultLang?: "en" | "th";
28
+ /** If set, the page shows a return-pad textarea posting to `${returnEndpoint}`. */
29
+ returnEndpoint?: string;
30
+ }
31
+ /** Render the same-machine page. Single big COPY button + auto-clipboard
32
+ * on load + four AI deep-links. */
33
+ export declare function renderSameShellPage(input: SameShellPageInput): string;
34
+ //# sourceMappingURL=same_shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"same_shell.d.ts","sourceRoot":"","sources":["../../src/rainbow/same_shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAeD;oCACoC;AACpC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAmIrE"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * v1.92.0 -- RAINBOW: SAME-SHELL (same-machine 1-click clone).
3
+ *
4
+ * The forgotten case: when the user is on ONE machine running editor AI
5
+ * (Cursor / Claude Code) AND wants to continue with Web AI (ChatGPT.com,
6
+ * Gemini.com) in a browser ON THE SAME machine, the existing RAINBOW
7
+ * path forces them through QR → tunnel → phone scan → page → copy →
8
+ * back to PC → paste. Eight steps for a zero-network handoff.
9
+ *
10
+ * SAME-SHELL collapses this to two steps:
11
+ *
12
+ * 1. Editor AI: mneme.rainbow.show_local()
13
+ * → opens default browser at http://localhost:PORT
14
+ * → page auto-copies soul to clipboard on load
15
+ *
16
+ * 2. User opens chatgpt.com/gemini.com/claude.ai (link on page),
17
+ * Ctrl+V, done.
18
+ *
19
+ * Zero QR. Zero tunnel. Zero dpaste. Zero risk of 404 because no
20
+ * external network is involved at all. THIS IS THE FASTEST PATH AND
21
+ * THE LEAST FRAGILE -- but we never told the user it existed.
22
+ */
23
+ function jsSafe(s) {
24
+ return JSON.stringify(s).replace(/<\/(script)/gi, "<\\/$1");
25
+ }
26
+ function escapeHtml(s) {
27
+ return s
28
+ .replace(/&/g, "&amp;")
29
+ .replace(/</g, "&lt;")
30
+ .replace(/>/g, "&gt;")
31
+ .replace(/"/g, "&quot;")
32
+ .replace(/'/g, "&#39;");
33
+ }
34
+ /** Render the same-machine page. Single big COPY button + auto-clipboard
35
+ * on load + four AI deep-links. */
36
+ export function renderSameShellPage(input) {
37
+ const lang = input.defaultLang ?? "en";
38
+ const port = Math.max(1, Math.floor(input.port));
39
+ const returnEndpoint = input.returnEndpoint ?? null;
40
+ return `<!doctype html><html lang="${lang}"><head>
41
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
42
+ <title>Mneme — Clone to another AI on this PC</title>
43
+ <style>
44
+ *{box-sizing:border-box;margin:0;padding:0}
45
+ body{font-family:-apple-system,"Segoe UI",sans-serif;background:linear-gradient(135deg,#16a085 0%,#2c3e50 100%);color:#fff;min-height:100vh;padding:32px 24px}
46
+ .wrap{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:22px}
47
+ .lang-btn{position:fixed;top:14px;right:14px;background:rgba(0,0,0,0.4);border:0;color:#fff;padding:10px 18px;border-radius:8px;font-weight:700;cursor:pointer}
48
+ h1{font-size:38px;text-align:center;font-weight:800}
49
+ .tagline{font-size:19px;opacity:0.92;text-align:center;line-height:1.5}
50
+ .copy-btn{background:#fff;color:#16a085;border:0;padding:28px;border-radius:18px;font-weight:800;font-size:24px;cursor:pointer;box-shadow:0 14px 30px rgba(0,0,0,0.4);width:100%}
51
+ .copy-btn:active{transform:scale(0.98)}
52
+ .copy-btn.done{background:#27ae60;color:#fff}
53
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
54
+ .grid a{display:flex;flex-direction:column;align-items:center;gap:6px;background:rgba(255,255,255,0.18);padding:22px 14px;border-radius:14px;text-decoration:none;color:#fff;font-weight:700;font-size:17px;border:2px solid rgba(255,255,255,0.1)}
55
+ .grid a:hover{background:rgba(255,255,255,0.28);border-color:rgba(255,255,255,0.4)}
56
+ .grid a span.hint{font-size:12px;opacity:0.7;font-weight:500}
57
+ .matrix{background:rgba(0,0,0,0.3);padding:22px;border-radius:14px;font-size:15px;line-height:1.6}
58
+ .matrix h3{font-size:19px;margin-bottom:12px}
59
+ .matrix table{width:100%;border-collapse:collapse;font-size:14px}
60
+ .matrix td{padding:8px 4px;border-bottom:1px solid rgba(255,255,255,0.15)}
61
+ .matrix td:last-child{text-align:center;font-weight:700;width:42px}
62
+ .return-pad{background:rgba(0,0,0,0.4);padding:22px;border-radius:14px;display:flex;flex-direction:column;gap:14px}
63
+ .return-pad h3{font-size:20px}
64
+ .return-pad textarea{width:100%;min-height:140px;padding:14px;border-radius:10px;border:0;font-family:monospace;font-size:13px;background:rgba(255,255,255,0.95);color:#222}
65
+ .return-pad button{background:#9b59b6;color:#fff;border:0;padding:18px;border-radius:12px;font-weight:800;font-size:18px;cursor:pointer}
66
+ .return-pad button:active{transform:scale(0.98)}
67
+ .return-pad .status{font-size:14px;opacity:0.9;text-align:center}
68
+ footer{font-size:13px;opacity:0.7;text-align:center;line-height:1.6}
69
+ </style></head><body>
70
+ <button class="lang-btn" id="lang">ไทย</button>
71
+ <div class="wrap">
72
+ <h1 data-en="🧬 Clone to another AI on this PC" data-th="🧬 Clone ไป AI ตัวอื่นบนเครื่องนี้"></h1>
73
+ <div class="tagline" data-en="Brain already copied to clipboard. Click an AI below, paste (Ctrl+V), continue." data-th="ความจำก๊อปลง clipboard แล้ว. กด AI ข้างล่าง, paste (Ctrl+V), คุยต่อได้เลย"></div>
74
+ <button class="copy-btn" id="c" data-en="📋 Copy brain again" data-th="📋 ก๊อปสมองอีกครั้ง"></button>
75
+ <div class="grid">
76
+ <a href="https://chatgpt.com/" target="_blank" id="lk-cg">🟢 <span>ChatGPT</span><span class="hint" data-en="opens in new tab" data-th="เปิด tab ใหม่"></span></a>
77
+ <a href="https://gemini.google.com/app" target="_blank" id="lk-gm">🔵 <span>Gemini</span><span class="hint" data-en="opens in new tab" data-th="เปิด tab ใหม่"></span></a>
78
+ <a href="https://claude.ai/new" target="_blank" id="lk-cl">🟣 <span>Claude</span><span class="hint" data-en="opens in new tab" data-th="เปิด tab ใหม่"></span></a>
79
+ <a href="https://www.perplexity.ai/" target="_blank" id="lk-pp">⚪ <span>Perplexity</span><span class="hint" data-en="opens in new tab" data-th="เปิด tab ใหม่"></span></a>
80
+ </div>
81
+ <div class="matrix">
82
+ <h3 data-en="🎯 Paste-only — what works / what doesn't" data-th="🎯 Paste-only ใช้ทำอะไรได้/ไม่ได้"></h3>
83
+ <table>
84
+ <tr><td data-en="On a train, only have your phone" data-th="อยู่บนรถไฟ มีแค่มือถือ"></td><td>✅</td></tr>
85
+ <tr><td data-en="Switch models for a second opinion" data-th="อยากเปลี่ยน model ดู second opinion"></td><td>✅</td></tr>
86
+ <tr><td data-en="Share with a teammate (PM, designer)" data-th="ส่งให้เพื่อน/PM/designer คุยต่อ"></td><td>✅</td></tr>
87
+ <tr><td data-en="Backup the whole conversation" data-th="Backup บทสนทนา"></td><td>✅</td></tr>
88
+ <tr><td data-en="Brainstorm with the best model per task" data-th="Brainstorm กับ model ที่เก่งสุดต่อ task"></td><td>✅</td></tr>
89
+ <tr><td data-en="Call Mneme tools (apoptosis / scan / audit)" data-th="เรียก Mneme tools (apoptosis / scan / audit)"></td><td>❌</td></tr>
90
+ <tr><td data-en="Read .mneme/ files on your PC" data-th="อ่าน .mneme/ บน PC"></td><td>❌</td></tr>
91
+ <tr><td data-en="Upgrade Mneme / install" data-th="อัปเกรด Mneme / install"></td><td>❌</td></tr>
92
+ <tr><td data-en="Modify your code" data-th="แก้ code ของคุณ"></td><td>❌</td></tr>
93
+ </table>
94
+ <div style="margin-top:14px;padding:12px;background:rgba(255,255,255,0.1);border-radius:8px;font-size:13px;line-height:1.6">
95
+ <span data-en="✱ The Web AI can SUGGEST next actions back via HOMUNCULUS RETURN block. Paste that back below, your editor AI executes it. Web AI = brain. Editor AI = hands. You = courier (2 paste ops)." data-th="✱ Web AI สามารถ SUGGEST next actions กลับมาเป็น HOMUNCULUS RETURN block ได้. paste กลับช่องล่าง editor AI ของคุณจะ execute. Web AI = สมอง. Editor AI = มือ. คุณ = courier (paste 2 ครั้ง)"></span>
96
+ </div>
97
+ </div>
98
+ ${returnEndpoint ? `<div class="return-pad">
99
+ <h3 data-en="🪃 BOOMERANG — paste the Web AI reply back here" data-th="🪃 BOOMERANG — paste คำตอบ Web AI กลับมาตรงนี้"></h3>
100
+ <div style="font-size:14px;opacity:0.92;line-height:1.5" data-en="Paste the Web AI's full reply (must include the HOMUNCULUS RETURN block). Your editor AI on this PC will pick it up via Mneme MCP and surface next_actions for execution." data-th="paste คำตอบ Web AI ทั้งหมด (ต้องมี HOMUNCULUS RETURN block). editor AI บนเครื่องนี้จะรับผ่าน Mneme MCP แล้วโชว์ next_actions ให้ execute"></div>
101
+ <textarea id="rp" placeholder="# HOMUNCULUS RETURN&#10;originator: claude-opus-4-7&#10;returning_from: gemini-2.5-pro&#10;..."></textarea>
102
+ <button id="rb" data-en="🪃 Send back to editor AI" data-th="🪃 ส่งกลับ editor AI"></button>
103
+ <div class="status" id="rs"></div>
104
+ </div>` : ""}
105
+ <footer>
106
+ <span data-en="localhost:${port} · same-machine handoff · no QR, no tunnel, no network needed" data-th="localhost:${port} · ส่งผ่านเครื่องเดียวกัน · ไม่ต้องใช้ QR / tunnel / network"></span>
107
+ </footer>
108
+ </div>
109
+ <script>
110
+ const SOUL = ${jsSafe(input.soulText)};
111
+ let lang = ${JSON.stringify(lang)};
112
+ function applyLang() {
113
+ document.querySelectorAll("[data-en]").forEach(el => { el.textContent = el.dataset[lang] || el.dataset.en; });
114
+ document.getElementById("lang").textContent = lang === "en" ? "ไทย" : "EN";
115
+ document.documentElement.lang = lang;
116
+ }
117
+ applyLang();
118
+ document.getElementById("lang").onclick = () => { lang = lang === "en" ? "th" : "en"; applyLang(); };
119
+
120
+ async function copySoul(showToast) {
121
+ try {
122
+ if (navigator.clipboard && window.isSecureContext) {
123
+ await navigator.clipboard.writeText(SOUL);
124
+ } else { throw new Error("no clipboard api"); }
125
+ const c = document.getElementById("c");
126
+ c.classList.add("done");
127
+ c.textContent = lang === "th" ? "✓ ก๊อปแล้ว — paste ได้เลย" : "✓ Copied — ready to paste";
128
+ if (showToast === false) return;
129
+ setTimeout(() => {
130
+ c.classList.remove("done");
131
+ c.textContent = lang === "th" ? "📋 ก๊อปสมองอีกครั้ง" : "📋 Copy brain again";
132
+ }, 3500);
133
+ } catch (e) {
134
+ const c = document.getElementById("c");
135
+ c.textContent = lang === "th" ? "❌ ก๊อปไม่ได้ — กดที่ textarea ข้างล่าง → Select All → Copy" : "❌ Copy failed — use the textarea below";
136
+ }
137
+ }
138
+ copySoul(false);
139
+ document.getElementById("c").onclick = () => copySoul(true);
140
+ ${["cg", "gm", "cl", "pp"].map(k => `document.getElementById("lk-${k}").addEventListener("click", () => copySoul(false));`).join("\n")}
141
+ ${returnEndpoint ? `
142
+ const RETURN_URL = ${jsSafe(returnEndpoint)};
143
+ document.getElementById("rb").onclick = async () => {
144
+ const body = document.getElementById("rp").value;
145
+ if (!body.trim()) {
146
+ document.getElementById("rs").textContent = lang === "th" ? "⚠ ใส่ HOMUNCULUS RETURN block ก่อน" : "⚠ paste a HOMUNCULUS RETURN block first";
147
+ return;
148
+ }
149
+ document.getElementById("rs").textContent = lang === "th" ? "⏳ กำลังส่ง..." : "⏳ sending...";
150
+ try {
151
+ const r = await fetch(RETURN_URL, { method: "POST", headers: { "content-type": "text/plain;charset=utf-8" }, body });
152
+ const j = await r.json().catch(() => ({}));
153
+ if (r.ok && j.ok) {
154
+ document.getElementById("rs").textContent = lang === "th" ? ("✓ ส่งถึง editor AI แล้ว — id=" + j.id) : ("✓ delivered to editor AI — id=" + j.id);
155
+ document.getElementById("rp").value = "";
156
+ } else {
157
+ document.getElementById("rs").textContent = lang === "th" ? ("✗ ไม่สำเร็จ: " + (j.error || r.status)) : ("✗ failed: " + (j.error || r.status));
158
+ }
159
+ } catch (e) {
160
+ document.getElementById("rs").textContent = lang === "th" ? ("✗ network: " + e.message) : ("✗ network: " + e.message);
161
+ }
162
+ };
163
+ ` : ""}
164
+ </script>
165
+ </body></html>`;
166
+ }
167
+ //# sourceMappingURL=same_shell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"same_shell.js","sourceRoot":"","sources":["../../src/rainbow/same_shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAWH,SAAS,MAAM,CAAC,CAAS;IACvB,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,CAAC;SACL,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED;oCACoC;AACpC,MAAM,UAAU,mBAAmB,CAAC,KAAyB;IAC3D,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACjD,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,IAAI,IAAI,CAAC;IAEpD,OAAO,8BAA8B,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0DzC,cAAc,CAAC,CAAC,CAAC;;;;;;OAMZ,CAAC,CAAC,CAAC,EAAE;;6BAEiB,IAAI,qFAAqF,IAAI;;;;eAI3G,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;aACxB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6B/B,CAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,+BAA+B,CAAC,sDAAsD,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;EACjI,cAAc,CAAC,CAAC,CAAC;qBACE,MAAM,CAAC,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;CAqB1C,CAAC,CAAC,CAAC,EAAE;;eAES,CAAC;AAChB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=v1_91.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v1_91.test.d.ts","sourceRoot":"","sources":["../../src/rainbow/v1_91.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderMobilePage, renderPcPage } from "./page_renderer.js";
3
+ describe("v1.91 RAINBOW · mobile page renderer", () => {
4
+ it("emits a complete HTML document", () => {
5
+ const html = renderMobilePage({ soulText: "# SOUL\nbody" });
6
+ expect(html.startsWith("<!doctype html>")).toBe(true);
7
+ expect(html).toContain("</html>");
8
+ expect(html).toContain("Mneme handoff");
9
+ });
10
+ it("includes Web Share API + clipboard + textarea fallback chain", () => {
11
+ const html = renderMobilePage({ soulText: "x" });
12
+ expect(html).toContain("navigator.share");
13
+ expect(html).toContain("navigator.clipboard");
14
+ expect(html).toContain("execCommand");
15
+ });
16
+ it("XSS-safe: closing </script> in soul is escaped", () => {
17
+ const html = renderMobilePage({ soulText: 'evil </script><img onerror=alert(1)>' });
18
+ expect(html).toContain("<\\/script>");
19
+ // Original (raw) </script> token must NOT appear in the JS string section
20
+ const scriptStart = html.indexOf("const SOUL = ");
21
+ const scriptEnd = html.indexOf(";", scriptStart);
22
+ const soulLiteral = html.slice(scriptStart, scriptEnd);
23
+ expect(soulLiteral).not.toContain("</script>");
24
+ });
25
+ it("ships bilingual data-en/data-th attributes for UI strings", () => {
26
+ const html = renderMobilePage({ soulText: "x" });
27
+ expect(html).toContain('data-en="Mneme handoff"');
28
+ expect(html).toContain('data-th="ส่งสมอง Mneme"');
29
+ });
30
+ it("includes the honest capability matrix (paste vs MCP truth)", () => {
31
+ const html = renderMobilePage({ soulText: "x" });
32
+ expect(html).toContain("Read this conversation");
33
+ expect(html).toContain("Call Mneme MCP tools");
34
+ expect(html).toContain("HOMUNCULUS RETURN block");
35
+ });
36
+ it("includes 4-vendor quick links (ChatGPT/Gemini/Claude/Perplexity)", () => {
37
+ const html = renderMobilePage({ soulText: "x" });
38
+ expect(html).toContain("chatgpt.com");
39
+ expect(html).toContain("gemini.google.com");
40
+ expect(html).toContain("claude.ai");
41
+ expect(html).toContain("perplexity.ai");
42
+ });
43
+ it("respects defaultLang option", () => {
44
+ const en = renderMobilePage({ soulText: "x", defaultLang: "en" });
45
+ const th = renderMobilePage({ soulText: "x", defaultLang: "th" });
46
+ expect(en).toContain('let lang = "en"');
47
+ expect(th).toContain('let lang = "th"');
48
+ });
49
+ });
50
+ describe("v1.91 RAINBOW · PC page renderer", () => {
51
+ const baseInput = {
52
+ primaryUrl: "http://192.168.1.10:7741",
53
+ label: { en: "Same Wi-Fi · 1 tap", th: "WiFi เดียวกัน · 1 แตะ" },
54
+ note: { en: "Phone MUST be on the same WiFi", th: "มือถือต้องอยู่ WiFi เดียวกับ PC" },
55
+ soulTokens: 1234,
56
+ hasTunnel: false,
57
+ hasPaste: true,
58
+ };
59
+ it("emits a complete HTML page", () => {
60
+ const html = renderPcPage(baseInput);
61
+ expect(html.startsWith("<!doctype html>")).toBe(true);
62
+ expect(html).toContain("</html>");
63
+ expect(html).toContain("Send brain to phone");
64
+ });
65
+ it("renders ONE primary QR pointing at the QR rendering service", () => {
66
+ const html = renderPcPage(baseInput);
67
+ expect(html).toContain("api.qrserver.com");
68
+ expect(html).toContain(encodeURIComponent("http://192.168.1.10:7741"));
69
+ });
70
+ it("includes STOP button (no Ctrl+C jargon)", () => {
71
+ const html = renderPcPage(baseInput);
72
+ expect(html).toContain("🛑 STOP");
73
+ expect(html).toContain('id="stop"');
74
+ expect(html).not.toContain("Ctrl+C in the terminal");
75
+ expect(html).not.toContain("Press Ctrl+C");
76
+ });
77
+ it("STOP button confirms before stopping (in selected language)", () => {
78
+ const html = renderPcPage(baseInput);
79
+ expect(html).toContain("confirm(");
80
+ expect(html).toContain("Server stopped");
81
+ expect(html).toContain("Server หยุดแล้ว");
82
+ });
83
+ it("footer reflects tunnel + paste availability", () => {
84
+ const noTunnel = renderPcPage({ ...baseInput, hasTunnel: false, hasPaste: false });
85
+ expect(noTunnel).not.toContain("Cloudflare tunnel");
86
+ const withTunnel = renderPcPage({ ...baseInput, hasTunnel: true, hasPaste: true });
87
+ expect(withTunnel).toContain("Cloudflare tunnel");
88
+ expect(withTunnel).toContain("paste fallback");
89
+ });
90
+ it("escapes HTML in label/note (XSS-safe)", () => {
91
+ const evil = {
92
+ ...baseInput,
93
+ label: { en: '<script>alert(1)</script>', th: 'ก' },
94
+ note: { en: 'a"b', th: 'b' },
95
+ };
96
+ const html = renderPcPage(evil);
97
+ expect(html).not.toContain("<script>alert(1)</script>");
98
+ expect(html).toContain("&lt;script&gt;alert(1)&lt;/script&gt;");
99
+ });
100
+ it("primary URL appears as displayed text + inside the QR encoder URL", () => {
101
+ const html = renderPcPage({ ...baseInput, primaryUrl: "https://abc.trycloudflare.com" });
102
+ expect(html).toContain("https://abc.trycloudflare.com");
103
+ expect(html).toContain(encodeURIComponent("https://abc.trycloudflare.com"));
104
+ });
105
+ it("STOP fetch URL is the primaryUrl + /stop, JSON-encoded for safety", () => {
106
+ const html = renderPcPage({ ...baseInput, primaryUrl: 'http://x.com/"</script>' });
107
+ // </script> in fetch arg must be escaped
108
+ expect(html).toContain("<\\/script>");
109
+ });
110
+ });
111
+ //# sourceMappingURL=v1_91.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v1_91.test.js","sourceRoot":"","sources":["../../src/rainbow/v1_91.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=v1_92.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v1_92.test.d.ts","sourceRoot":"","sources":["../../src/rainbow/v1_92.test.ts"],"names":[],"mappings":""}