thebird 1.2.18 → 1.2.20

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 CHANGED
@@ -5,3 +5,16 @@
5
5
  ### Added
6
6
  - `wasi/cli.ts`: Deno CLI — Anthropic-format prompt → Gemini streaming via REST, flags: `--model`, `--system`
7
7
  - `deno.json`: tasks `cli` (run) and `cli:compile` (single binary)
8
+
9
+ ## [Unreleased - 2]
10
+
11
+ ### Added
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
+ - `examples/sdk-validate.js`: Anthropic SDK (`@anthropic-ai/sdk`) client pointing at local proxy, validates both streaming and non-streaming paths.
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.
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="https://cdn.jsdelivr.net/gh/gzuidhof/coi-serviceworker/coi-serviceworker.min.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
- :root { --bg: #0f1117; }
11
- html, body { height: 100%; background: var(--bg); }
12
- bird-chat { display: flex; flex-direction: column; height: 100dvh; }
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
- <bird-chat></bird-chat>
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>
@@ -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));
@@ -0,0 +1,31 @@
1
+ const Anthropic = require('@anthropic-ai/sdk').default;
2
+
3
+ const client = new Anthropic({
4
+ apiKey: 'placeholder',
5
+ baseURL: process.env.THEBIRD_URL || 'http://localhost:3456',
6
+ });
7
+
8
+ async function main() {
9
+ process.stdout.write('[streaming] ');
10
+ const stream = client.messages.stream({
11
+ model: 'gemini-2.5-flash',
12
+ max_tokens: 256,
13
+ messages: [{ role: 'user', content: 'Say exactly: thebird works' }],
14
+ });
15
+ for await (const ev of stream) {
16
+ if (ev.type === 'content_block_delta' && ev.delta.type === 'text_delta') {
17
+ process.stdout.write(ev.delta.text);
18
+ }
19
+ }
20
+ process.stdout.write('\n');
21
+
22
+ process.stdout.write('[non-streaming] ');
23
+ const msg = await client.messages.create({
24
+ model: 'gemini-2.5-flash',
25
+ max_tokens: 256,
26
+ messages: [{ role: 'user', content: 'Say exactly: thebird works' }],
27
+ });
28
+ console.log(msg.content[0].text);
29
+ }
30
+
31
+ main().catch(e => { process.stderr.write(e.message + '\n'); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.18",
3
+ "version": "1.2.20",
4
4
  "description": "Anthropic SDK to Gemini streaming bridge — drop-in proxy that translates Anthropic message format and tool calls to Google Gemini",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -35,9 +35,9 @@
35
35
  "url": "https://github.com/AnEntrypoint/thebird.git"
36
36
  },
37
37
  "dependencies": {
38
+ "@anthropic-ai/sdk": "^0.88.0",
38
39
  "@google/genai": "^1.0.0"
39
40
  },
40
- "peerDependencies": {},
41
41
  "engines": {
42
42
  "node": ">=18"
43
43
  }
package/server.js ADDED
@@ -0,0 +1,77 @@
1
+ const http = require('http');
2
+ const { streamGemini, generateGemini } = require('./index.js');
3
+
4
+ const PORT = process.env.PORT || 3456;
5
+ const state = { requests: 0, errors: 0, active: 0 };
6
+
7
+ const sse = (ev, data) => `event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`;
8
+
9
+ const msgId = () => 'msg_' + Math.random().toString(36).slice(2, 12);
10
+
11
+ async function handleMessages(req, res) {
12
+ let body = '';
13
+ for await (const chunk of req) body += chunk;
14
+ const { model, messages, system, stream, max_tokens } = JSON.parse(body);
15
+ const apiKey = process.env.GEMINI_API_KEY;
16
+ if (!apiKey) { res.writeHead(500); res.end(JSON.stringify({ error: 'GEMINI_API_KEY required' })); return; }
17
+ const params = { model: model || 'gemini-2.5-flash', messages, system, apiKey, maxOutputTokens: max_tokens || 8192 };
18
+
19
+ if (!stream) {
20
+ const result = await generateGemini(params);
21
+ res.writeHead(200, { 'Content-Type': 'application/json' });
22
+ res.end(JSON.stringify({
23
+ id: msgId(), type: 'message', role: 'assistant', model: params.model,
24
+ content: [{ type: 'text', text: result.text }],
25
+ stop_reason: 'end_turn', usage: { input_tokens: 0, output_tokens: 0 },
26
+ }));
27
+ return;
28
+ }
29
+
30
+ res.writeHead(200, {
31
+ 'Content-Type': 'text/event-stream',
32
+ 'Cache-Control': 'no-cache',
33
+ 'Connection': 'keep-alive',
34
+ });
35
+
36
+ const id = msgId();
37
+ res.write(sse('message_start', { type: 'message_start', message: { id, type: 'message', role: 'assistant', content: [], model: params.model, stop_reason: null, usage: { input_tokens: 0, output_tokens: 0 } } }));
38
+ res.write(sse('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }));
39
+ res.write(sse('ping', { type: 'ping' }));
40
+
41
+ let outputTokens = 0;
42
+ for await (const ev of streamGemini(params).fullStream) {
43
+ if (ev.type === 'text-delta') {
44
+ outputTokens += ev.textDelta.length;
45
+ res.write(sse('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: ev.textDelta } }));
46
+ }
47
+ }
48
+
49
+ res.write(sse('content_block_stop', { type: 'content_block_stop', index: 0 }));
50
+ res.write(sse('message_delta', { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: outputTokens } }));
51
+ res.write(sse('message_stop', { type: 'message_stop' }));
52
+ res.end();
53
+ }
54
+
55
+ http.createServer(async (req, res) => {
56
+ state.requests++;
57
+ state.active++;
58
+ try {
59
+ if (req.method === 'GET' && req.url === '/debug/server') {
60
+ res.writeHead(200, { 'Content-Type': 'application/json' });
61
+ res.end(JSON.stringify(state));
62
+ return;
63
+ }
64
+ if (req.method === 'POST' && req.url === '/v1/messages') {
65
+ await handleMessages(req, res);
66
+ return;
67
+ }
68
+ res.writeHead(404);
69
+ res.end(JSON.stringify({ error: 'not found' }));
70
+ } catch (err) {
71
+ state.errors++;
72
+ res.writeHead(500);
73
+ res.end(JSON.stringify({ error: err.message }));
74
+ } finally {
75
+ state.active--;
76
+ }
77
+ }).listen(PORT, () => process.stderr.write(`thebird proxy listening on ${PORT}\n`));