thebird 1.2.19 → 1.2.21
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/CHANGELOG.md +6 -0
- package/docs/coi-serviceworker.js +2 -0
- package/docs/index.html +29 -6
- package/docs/terminal.js +104 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,3 +12,9 @@
|
|
|
12
12
|
- `server.js`: HTTP proxy on port 3456, serves Anthropic Messages API wire format (streaming SSE + non-streaming JSON), backed by thebird → Gemini. Observability at `GET /debug/server`.
|
|
13
13
|
- `examples/sdk-validate.js`: Anthropic SDK (`@anthropic-ai/sdk`) client pointing at local proxy, validates both streaming and non-streaming paths.
|
|
14
14
|
- `@anthropic-ai/sdk` added to dependencies.
|
|
15
|
+
|
|
16
|
+
## [Unreleased - 3]
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `docs/terminal.js`: WebContainer-powered in-browser terminal with xterm.js, IndexedDB FS persistence, npm install on boot, @anthropic-ai/sdk pre-installed. `window.__debug.container` and `window.__debug.term` live.
|
|
20
|
+
- `docs/index.html`: tabs (Chat / Terminal), coi-serviceworker shim for SharedArrayBuffer on GitHub Pages, xterm CSS.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
|
|
2
|
+
let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o="coepdegrade"==e,s={shouldRegister:()=>!e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator,t=r.serviceWorker&&r.serviceWorker.controller;t&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const i=window.sessionStorage.getItem("coiCoepHasFailed");if(t){const e=s.coepDegrade()&&!(o||window.crossOriginIsolated);r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||i&&s.coepDegrade())&&s.coepCredentialless()}),e&&(!s.quiet&&console.log("Reloading page to degrade COEP."),window.sessionStorage.setItem("coiReloadedBySelf","coepdegrade"),s.doReload("coepdegrade")),s.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&s.shouldRegister()&&(window.isSecureContext?r.serviceWorker?r.serviceWorker.register(window.document.currentScript.src).then((e=>{!s.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),e.addEventListener("updatefound",(()=>{!s.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","updatefound"),s.doReload()})),e.active&&!r.serviceWorker.controller&&(!s.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","notcontrolling"),s.doReload())}),(e=>{!s.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!s.quiet&&console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."):!s.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})();
|
package/docs/index.html
CHANGED
|
@@ -3,19 +3,42 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>thebird — Gemini chat</title>
|
|
6
|
+
<title>thebird — Gemini chat + terminal</title>
|
|
7
|
+
<script src="coi-serviceworker.js"></script>
|
|
7
8
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
9
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rippleui@1.12.1/dist/css/styles.css" />
|
|
10
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.css" />
|
|
9
11
|
<style>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
html, body { height: 100%; background: #0f1117; }
|
|
13
|
+
.tab-active { border-bottom: 2px solid oklch(var(--p)); }
|
|
14
|
+
#term-container { height: 100%; }
|
|
15
|
+
bird-chat { display: flex; flex-direction: column; height: 100%; }
|
|
13
16
|
.msg-bubble { max-width: 680px; white-space: pre-wrap; word-break: break-word; }
|
|
14
17
|
#msg-list { scroll-behavior: smooth; }
|
|
15
18
|
</style>
|
|
16
19
|
</head>
|
|
17
|
-
<body class="bg-base-100 text-base-content">
|
|
18
|
-
<
|
|
20
|
+
<body class="bg-base-100 text-base-content h-full">
|
|
21
|
+
<div class="flex flex-col h-full">
|
|
22
|
+
<div class="flex border-b border-base-300 bg-base-200 gap-2 px-4 pt-2 shrink-0">
|
|
23
|
+
<button id="tab-chat" class="px-3 py-1 text-sm tab-active" onclick="switchTab('chat')">Chat</button>
|
|
24
|
+
<button id="tab-term" class="px-3 py-1 text-sm" onclick="switchTab('term')">Terminal</button>
|
|
25
|
+
</div>
|
|
26
|
+
<div id="pane-chat" class="flex-1 overflow-hidden">
|
|
27
|
+
<bird-chat></bird-chat>
|
|
28
|
+
</div>
|
|
29
|
+
<div id="pane-term" class="flex-1 overflow-hidden hidden p-2 bg-black">
|
|
30
|
+
<div id="term-container" style="height:100%"></div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<script>
|
|
34
|
+
function switchTab(t) {
|
|
35
|
+
document.getElementById('pane-chat').classList.toggle('hidden', t !== 'chat');
|
|
36
|
+
document.getElementById('pane-term').classList.toggle('hidden', t !== 'term');
|
|
37
|
+
document.getElementById('tab-chat').classList.toggle('tab-active', t === 'chat');
|
|
38
|
+
document.getElementById('tab-term').classList.toggle('tab-active', t === 'term');
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
19
41
|
<script type="module" src="app.js"></script>
|
|
42
|
+
<script type="module" src="terminal.js"></script>
|
|
20
43
|
</body>
|
|
21
44
|
</html>
|
package/docs/terminal.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { WebContainer } from 'https://esm.sh/@webcontainer/api';
|
|
2
|
+
import { Terminal } from 'https://esm.sh/@xterm/xterm';
|
|
3
|
+
import { FitAddon } from 'https://esm.sh/@xterm/addon-fit';
|
|
4
|
+
|
|
5
|
+
const IDB_KEY = 'thebird_fs';
|
|
6
|
+
const DEFAULT_FILES = {
|
|
7
|
+
'package.json': JSON.stringify({ name: 'app', dependencies: { '@anthropic-ai/sdk': '^0.88.0' } }, null, 2),
|
|
8
|
+
'index.js': 'const Anthropic = require("@anthropic-ai/sdk");\nconsole.log("sdk loaded:", typeof Anthropic);\n',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function idbLoad() {
|
|
12
|
+
return new Promise((res, rej) => {
|
|
13
|
+
const req = indexedDB.open('thebird', 1);
|
|
14
|
+
req.onupgradeneeded = e => e.target.result.createObjectStore('fs');
|
|
15
|
+
req.onsuccess = e => {
|
|
16
|
+
const tx = e.target.result.transaction('fs', 'readonly');
|
|
17
|
+
const get = tx.objectStore('fs').get(IDB_KEY);
|
|
18
|
+
get.onsuccess = () => res(get.result || null);
|
|
19
|
+
get.onerror = rej;
|
|
20
|
+
};
|
|
21
|
+
req.onerror = rej;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function idbSave(data) {
|
|
26
|
+
return new Promise((res, rej) => {
|
|
27
|
+
const req = indexedDB.open('thebird', 1);
|
|
28
|
+
req.onsuccess = e => {
|
|
29
|
+
const tx = e.target.result.transaction('fs', 'readwrite');
|
|
30
|
+
tx.objectStore('fs').put(data, IDB_KEY);
|
|
31
|
+
tx.oncomplete = res;
|
|
32
|
+
tx.onerror = rej;
|
|
33
|
+
};
|
|
34
|
+
req.onerror = rej;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function snapshotToIDB(container, files) {
|
|
39
|
+
const snap = {};
|
|
40
|
+
await Promise.all(Object.keys(files).map(async p => {
|
|
41
|
+
try { snap[p] = await container.fs.readFile(p, 'utf-8'); } catch {}
|
|
42
|
+
}));
|
|
43
|
+
await idbSave(JSON.stringify(snap));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function boot() {
|
|
47
|
+
const el = document.getElementById('term-container');
|
|
48
|
+
if (!el) return;
|
|
49
|
+
|
|
50
|
+
const term = new Terminal({ theme: { background: '#000000' }, convertEol: true });
|
|
51
|
+
const fit = new FitAddon();
|
|
52
|
+
term.loadAddon(fit);
|
|
53
|
+
term.open(el);
|
|
54
|
+
fit.fit();
|
|
55
|
+
window.addEventListener('resize', () => fit.fit());
|
|
56
|
+
|
|
57
|
+
const saved = await idbLoad();
|
|
58
|
+
const files = saved ? JSON.parse(saved) : DEFAULT_FILES;
|
|
59
|
+
const mountTree = Object.fromEntries(
|
|
60
|
+
Object.entries(files).map(([p, c]) => [p, { file: { contents: c } }])
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
term.write('Booting WebContainer...\r\n');
|
|
64
|
+
let container;
|
|
65
|
+
try {
|
|
66
|
+
container = await WebContainer.boot();
|
|
67
|
+
} catch (e) {
|
|
68
|
+
term.write('\x1b[31mWebContainer boot failed: ' + e.message + '\x1b[0m\r\n');
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
await container.mount(mountTree);
|
|
72
|
+
term.write('Installing dependencies...\r\n');
|
|
73
|
+
|
|
74
|
+
const install = await container.spawn('npm', ['install']);
|
|
75
|
+
install.output.pipeTo(new WritableStream({ write: d => term.write(d) }));
|
|
76
|
+
const code = await install.exit;
|
|
77
|
+
if (code !== 0) throw new Error('npm install failed with code ' + code);
|
|
78
|
+
|
|
79
|
+
term.write('\x1b[32mReady.\x1b[0m Run commands below.\r\n$ ');
|
|
80
|
+
|
|
81
|
+
const shell = await container.spawn('sh', ['-c', 'while true; do read -r line && sh -c "$line" && printf "$ "; done']);
|
|
82
|
+
shell.output.pipeTo(new WritableStream({ write: d => term.write(d) }));
|
|
83
|
+
|
|
84
|
+
let buf = '';
|
|
85
|
+
term.onData(async data => {
|
|
86
|
+
if (data === '\r') {
|
|
87
|
+
term.write('\r\n');
|
|
88
|
+
await shell.input.write(buf + '\n');
|
|
89
|
+
await snapshotToIDB(container, files);
|
|
90
|
+
buf = '';
|
|
91
|
+
} else if (data === '\x7f') {
|
|
92
|
+
if (buf.length > 0) { buf = buf.slice(0, -1); term.write('\b \b'); }
|
|
93
|
+
} else {
|
|
94
|
+
buf += data;
|
|
95
|
+
term.write(data);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
window.__debug = window.__debug || {};
|
|
100
|
+
window.__debug.container = container;
|
|
101
|
+
window.__debug.term = term;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
boot().catch(e => console.error('[terminal] boot error:', e));
|
package/package.json
CHANGED