thebird 1.2.23 → 1.2.25

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
@@ -3,6 +3,12 @@
3
3
  ## [Unreleased]
4
4
 
5
5
  ### Added
6
+ - `docs/defaults.json`: JSON blob of all thebird lib files + `server.js` + `agent.js` fetched by terminal.js on first boot
7
+ - `docs/terminal.js`: fetches `defaults.json` instead of hardcoded DEFAULT_FILES; jsh PTY shell with resize; `server-ready` wires iframe src + `window.__debug.previewUrl`; all debug keys registered
8
+ - `docs/index.html`: COEP fix via `window.coi = { coepDegrade: () => false }` (prevents Tailwind CDN block); Preview iframe `allow` attribute removed (invalid Feature Policy); `window.__debug` observability for container, term, shell, srv, previewUrl
9
+ - `agent.js` (in container): agentic loop using `@anthropic-ai/sdk` pointing at `http://localhost:3000` (thebird proxy), tools: `read_file`, `write_file`, `run_command`
10
+
11
+ ### Added (prev)
6
12
  - `wasi/cli.ts`: Deno CLI — Anthropic-format prompt → Gemini streaming via REST, flags: `--model`, `--system`
7
13
  - `deno.json`: tasks `cli` (run) and `cli:compile` (single binary)
8
14
 
package/CLAUDE.md CHANGED
@@ -122,8 +122,21 @@ Run examples against real Gemini API to validate message translation.
122
122
  - `wasi/cli.ts`: Deno streaming CLI — `deno run --allow-net --allow-env wasi/cli.ts [--model M] [--system S] <prompt>`
123
123
  - `deno.json`: tasks `cli` (run) and `cli:compile` (→ `dist/thebird` binary)
124
124
 
125
+ ## WebContainer Terminal in docs/
126
+
127
+ Interactive terminal in docs/index.html runs thebird + Node.js server in WebContainer API.
128
+
129
+ ### Architecture
130
+
131
+ - **defaults.json**: docs/defaults.json is a 46KB single-line JSON blob containing all container files (package.json, lib/*.js, index.js, server.js, agent.js). Fetched by terminal.js on first boot instead of hardcoding DEFAULT_FILES inline (avoids 200-line limit).
132
+ - **Flat mount object**: WebContainer accepts `{'lib/client.js': ...}` directly — no nested directory tree needed.
133
+ - **COEP window.coi fix**: Add `<script>window.coi = { coepDegrade: () => false };</script>` BEFORE coi-serviceworker.js. Prevents degradation from credentialless to require-corp, which blocks Tailwind CDN. Key is `window.coi` (not `window.__coi_serviceworker`).
134
+ - **iframe allow attribute**: Remove `allow="cross-origin-isolated"` — not a valid Feature Policy keyword. WebContainer iframes work without it.
135
+ - **agent.js routing**: Inside container, agent.js uses `@anthropic-ai/sdk` with `baseURL: "http://localhost:3000"` pointing at thebird proxy (server.js), which translates Anthropic format → Gemini.
136
+
125
137
  ## Environment Notes
126
138
 
127
139
  - Repo remote: `https://github.com/AnEntrypoint/thebird.git` (capital A)
128
140
  - Deno 2.1.3 available; `exec:bash` uses PowerShell — use `exec:cmd` with `set KEY=val && cmd` syntax for env vars
129
141
  - Windows `KEY=val cmd` inline env syntax fails in PowerShell
142
+ - CI workflow commits version bump after every push to main — always `git pull --rebase origin main` before pushing to avoid fast-forward rejection
package/README.md CHANGED
@@ -156,21 +156,6 @@ Pass options as a nested array: `["maxtoken", { "max_tokens": 16384 }]`.
156
156
 
157
157
  `streamGemini` / `generateGemini` bypass routing and call Gemini natively via `@google/genai`. Requires `GEMINI_API_KEY`.
158
158
 
159
- ### Params
160
-
161
- | Param | Type | Default | Description |
162
- |---|---|---|---|
163
- | `model` | `string \| { id }` | `'gemini-2.0-flash'` | Model id |
164
- | `messages` | `Message[]` | required | Conversation history |
165
- | `system` | `string` | — | System instruction |
166
- | `tools` | `Tools` | — | Tool definitions |
167
- | `apiKey` | `string` | `GEMINI_API_KEY` | Override API key |
168
- | `temperature` | `number` | `0.5` | Sampling temperature |
169
- | `maxOutputTokens` | `number` | `8192` | Max tokens |
170
- | `topP` | `number` | `0.95` | Top-p |
171
- | `topK` | `number` | — | Top-k |
172
- | `safetySettings` | `SafetySetting[]` | — | Safety thresholds |
173
-
174
159
  ## Message Format
175
160
 
176
161
  Messages follow the Anthropic SDK format. All image block variants are supported:
@@ -193,18 +178,22 @@ Messages follow the Anthropic SDK format. All image block variants are supported
193
178
  | `finish-step` | `finishReason` | Step completed |
194
179
  | `error` | `error` | Error during step |
195
180
 
196
- ## TypeScript
181
+ ## Browser Demo
197
182
 
198
- ```ts
199
- import { createRouter, streamRouter, generateGemini, RouterConfiguration, ProviderConfig, RouterConfig } from 'thebird';
200
- ```
183
+ Live at **[anentrypoint.github.io/thebird](https://anentrypoint.github.io/thebird/)**
201
184
 
202
- ## Utilities
185
+ - **Chat tab** — Gemini chat via direct API (Gemini API key stored in localStorage)
186
+ - **Terminal tab** — WebContainer (in-browser Node.js) booting thebird's full stack: `npm install`, `node server.js` (Anthropic→Gemini proxy on port 3000), then a `jsh` shell
187
+ - **Preview tab** — iframe pointed at the WebContainer's HTTP server, live-updated when the server starts
203
188
 
204
- ```js
205
- const { convertMessages, convertTools, cleanSchema } = require('thebird');
189
+ Run the agentic CLI inside the terminal tab:
190
+
191
+ ```
192
+ GEMINI_API_KEY=<key> node agent.js "your task"
206
193
  ```
207
194
 
195
+ `agent.js` uses `@anthropic-ai/sdk` pointing at `http://localhost:3000` (thebird proxy) with tools: `read_file`, `write_file`, `run_command`.
196
+
208
197
  ## License
209
198
 
210
199
  MIT
@@ -0,0 +1 @@
1
+ {"package.json":"{\n \"name\": \"app\",\n \"dependencies\": {\n \"@anthropic-ai/sdk\": \"^0.88.0\",\n \"@google/genai\": \"^1.0.0\"\n }\n}","lib/client.js":"const { GoogleGenAI } = require('@google/genai');\r\n\r\nlet _client = null;\r\n\r\nfunction getClient(apiKey) {\r\n if (!_client || apiKey) _client = new GoogleGenAI({ apiKey: apiKey || process.env.GEMINI_API_KEY });\r\n return _client;\r\n}\r\n\r\nmodule.exports = { getClient };\r\n","lib/errors.js":"class GeminiError extends Error {\r\n constructor(message, { status, code, retryable = false } = {}) {\r\n super(message);\r\n this.name = 'GeminiError';\r\n this.status = status;\r\n this.code = code;\r\n this.retryable = retryable;\r\n }\r\n}\r\n\r\nfunction isRetryable(err) {\r\n if (err instanceof GeminiError) return err.retryable;\r\n const status = err?.status ?? err?.code;\r\n if (status === 429) return true;\r\n if (typeof status === 'number' && status >= 500) return true;\r\n const msg = err?.message ?? '';\r\n return /quota|rate.?limit|overloaded|unavailable/i.test(msg);\r\n}\r\n\r\nasync function withRetry(fn, maxRetries = 3) {\r\n let lastErr;\r\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\r\n try {\r\n return await fn();\r\n } catch (err) {\r\n lastErr = err;\r\n if (!isRetryable(err) || attempt === maxRetries) throw err;\r\n const delay = Math.min(1000 * 2 ** attempt + Math.random() * 200, 16000);\r\n await new Promise(r => setTimeout(r, delay));\r\n }\r\n }\r\n throw lastErr;\r\n}\r\n\r\nmodule.exports = { GeminiError, isRetryable, withRetry };\r\n","lib/convert.js":"function cleanSchema(schema) {\r\n if (!schema || typeof schema !== 'object') return schema;\r\n if (Array.isArray(schema)) return schema.map(cleanSchema);\r\n const out = {};\r\n for (const [k, v] of Object.entries(schema)) {\r\n if (k === 'additionalProperties' || k === '$schema') continue;\r\n out[k] = cleanSchema(v);\r\n }\r\n return out;\r\n}\r\n\r\nfunction convertTools(tools) {\r\n if (!tools || typeof tools !== 'object') return [];\r\n return Object.entries(tools).map(([name, t]) => ({\r\n name,\r\n description: t.description || '',\r\n parameters: cleanSchema(t.parameters?.jsonSchema || t.parameters || { type: 'object' })\r\n }));\r\n}\r\n\r\nfunction convertImageBlock(b) {\r\n // Handle inlineData: { mimeType, data } (base64)\r\n if (b.inlineData || b.type === 'image') {\r\n const src = b.inlineData || b.source;\r\n if (src?.data) return { inlineData: { mimeType: src.mimeType || 'image/jpeg', data: src.data } };\r\n if (src?.url) return { fileData: { mimeType: src.mimeType || 'image/jpeg', fileUri: src.url } };\r\n }\r\n // Handle fileData: { mimeType, fileUri }\r\n if (b.fileData) return { fileData: { mimeType: b.fileData.mimeType, fileUri: b.fileData.fileUri } };\r\n // Anthropic-style image block\r\n if (b.type === 'image' && b.source) {\r\n if (b.source.type === 'base64') return { inlineData: { mimeType: b.source.media_type, data: b.source.data } };\r\n if (b.source.type === 'url') return { fileData: { mimeType: b.source.media_type || 'image/jpeg', fileUri: b.source.url } };\r\n }\r\n return null;\r\n}\r\n\r\nfunction convertMessages(messages) {\r\n const contents = [];\r\n for (const m of messages) {\r\n const role = m.role === 'assistant' ? 'model' : 'user';\r\n if (typeof m.content === 'string') {\r\n if (m.content) contents.push({ role, parts: [{ text: m.content }] });\r\n continue;\r\n }\r\n if (Array.isArray(m.content)) {\r\n const parts = m.content.map(b => {\r\n if (b.type === 'text' && b.text) return { text: b.text };\r\n if (b.type === 'image' || b.inlineData || b.fileData) return convertImageBlock(b);\r\n if (b.type === 'tool_use') return { functionCall: { name: b.name, args: b.input || {} } };\r\n if (b.type === 'tool_result') {\r\n let resp;\r\n try { resp = typeof b.content === 'string' ? JSON.parse(b.content) : (b.content || {}); }\r\n catch { resp = { result: b.content }; }\r\n return { functionResponse: { name: b.name || 'unknown', response: resp } };\r\n }\r\n return null;\r\n }).filter(Boolean);\r\n if (parts.length) contents.push({ role, parts });\r\n }\r\n }\r\n return contents;\r\n}\r\n\r\nfunction extractModelId(model) {\r\n if (typeof model === 'string') return model;\r\n if (model?.modelId) return model.modelId;\r\n if (model?.id) return model.id;\r\n return 'gemini-2.0-flash';\r\n}\r\n\r\nfunction buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities } = {}) {\r\n const geminiTools = convertTools(tools);\r\n const config = {\r\n maxOutputTokens: maxOutputTokens ?? 8192,\r\n temperature: temperature ?? 0.5,\r\n topP: topP ?? 0.95\r\n };\r\n if (topK != null) config.topK = topK;\r\n if (system) config.systemInstruction = system;\r\n if (geminiTools.length > 0) config.tools = [{ functionDeclarations: geminiTools }];\r\n if (safetySettings) config.safetySettings = safetySettings;\r\n if (responseModalities) config.responseModalities = responseModalities;\r\n return { config, geminiTools };\r\n}\r\n\r\nmodule.exports = { cleanSchema, convertTools, convertMessages, extractModelId, buildConfig, convertImageBlock };\r\n","lib/config.js":"const fs = require('fs');\r\nconst path = require('path');\r\nconst os = require('os');\r\n\r\nfunction interpolateEnv(val) {\r\n if (typeof val === 'string') return val.replace(/\\$\\{([^}]+)\\}|\\$([A-Z_][A-Z0-9_]*)/g, (_, a, b) => process.env[a || b] || '');\r\n if (Array.isArray(val)) return val.map(interpolateEnv);\r\n if (val && typeof val === 'object') {\r\n const out = {};\r\n for (const [k, v] of Object.entries(val)) out[k] = interpolateEnv(v);\r\n return out;\r\n }\r\n return val;\r\n}\r\n\r\nfunction loadConfig(configPath) {\r\n const fp = configPath || process.env.THEBIRD_CONFIG || path.join(os.homedir(), '.thebird', 'config.json');\r\n try {\r\n const raw = JSON.parse(fs.readFileSync(fp, 'utf8'));\r\n return interpolateEnv(raw);\r\n } catch { return {}; }\r\n}\r\n\r\nmodule.exports = { loadConfig, interpolateEnv };\r\n","lib/router.js":"const { loadConfig } = require('./config');\r\n\r\nconst SUBAGENT_RE = /<CCR-SUBAGENT-MODEL>([^<]+)<\\/CCR-SUBAGENT-MODEL>/;\r\n\r\nfunction estimateTokens(messages, system) {\r\n let chars = typeof system === 'string' ? system.length : (system ? JSON.stringify(system).length : 0);\r\n for (const m of (messages || [])) {\r\n chars += typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content || '').length;\r\n }\r\n return Math.ceil(chars / 4);\r\n}\r\n\r\nfunction extractSubagentModel(messages) {\r\n const first = messages?.[0];\r\n if (!first) return null;\r\n const text = typeof first.content === 'string' ? first.content :\r\n (Array.isArray(first.content) ? first.content.map(b => b.text || '').join('') : '');\r\n const m = SUBAGENT_RE.exec(text);\r\n return m ? m[1].trim() : null;\r\n}\r\n\r\nfunction parseProviderModel(str) {\r\n const idx = str.indexOf(',');\r\n if (idx === -1) return { providerName: null, modelName: str };\r\n return { providerName: str.slice(0, idx), modelName: str.slice(idx + 1) };\r\n}\r\n\r\nasync function route(params, routerCfg, customRouterFn) {\r\n const { messages, system, taskType } = params;\r\n\r\n if (customRouterFn) {\r\n const custom = await customRouterFn(params, routerCfg);\r\n if (custom) return parseProviderModel(custom);\r\n }\r\n\r\n const subagent = extractSubagentModel(messages);\r\n if (subagent) return parseProviderModel(subagent);\r\n\r\n if (taskType === 'background' && routerCfg.background) return parseProviderModel(routerCfg.background);\r\n if (taskType === 'think' && routerCfg.think) return parseProviderModel(routerCfg.think);\r\n if (taskType === 'webSearch' && routerCfg.webSearch) return parseProviderModel(routerCfg.webSearch);\r\n if (taskType === 'image' && routerCfg.image) return parseProviderModel(routerCfg.image);\r\n\r\n const threshold = routerCfg.longContextThreshold || 60000;\r\n if (routerCfg.longContext && estimateTokens(messages, system) > threshold) return parseProviderModel(routerCfg.longContext);\r\n\r\n if (routerCfg.default) return parseProviderModel(routerCfg.default);\r\n return { providerName: null, modelName: null };\r\n}\r\n\r\nmodule.exports = { route, estimateTokens, parseProviderModel };\r\n","lib/transformers.js":"function removeCacheControl(obj) {\r\n if (!obj || typeof obj !== 'object') return obj;\r\n if (Array.isArray(obj)) return obj.map(removeCacheControl);\r\n const out = {};\r\n for (const [k, v] of Object.entries(obj)) {\r\n if (k === 'cache_control') continue;\r\n out[k] = removeCacheControl(v);\r\n }\r\n return out;\r\n}\r\n\r\nconst BUILT_IN = {\r\n cleancache: {\r\n request(req) { return { ...req, messages: removeCacheControl(req.messages), system: removeCacheControl(req.system) }; }\r\n },\r\n deepseek: {\r\n request(req) {\r\n const r = removeCacheControl(req);\r\n if (r.system && typeof r.system !== 'string') {\r\n r.system = (Array.isArray(r.system) ? r.system : [r.system]).map(b => b.text || '').join('\\n');\r\n }\r\n return r;\r\n }\r\n },\r\n openrouter: {\r\n options: {},\r\n request(req, opts) {\r\n const headers = { 'HTTP-Referer': 'https://github.com/AnEntrypoint/thebird', 'X-Title': 'thebird', ...(opts || {}).headers };\r\n if ((opts || {}).provider) req = { ...req, provider: (opts || {}).provider };\r\n return { ...req, _extraHeaders: { ...(req._extraHeaders || {}), ...headers } };\r\n }\r\n },\r\n maxtoken: {\r\n request(req, opts) { return { ...req, max_tokens: (opts || {}).max_tokens || req.max_tokens }; }\r\n },\r\n tooluse: {\r\n request(req) {\r\n if (req.tools && req.tools.length > 0) return { ...req, tool_choice: { type: 'required' } };\r\n return req;\r\n }\r\n },\r\n reasoning: {\r\n request(req) { return req; },\r\n response(res) {\r\n if (!res.choices) return res;\r\n return {\r\n ...res,\r\n choices: res.choices.map(c => {\r\n if (!c.message) return c;\r\n const msg = { ...c.message };\r\n if (msg.reasoning_content) { msg._reasoning = msg.reasoning_content; delete msg.reasoning_content; }\r\n return { ...c, message: msg };\r\n })\r\n };\r\n }\r\n },\r\n sampling: {\r\n request(req) {\r\n const r = { ...req };\r\n delete r.top_k;\r\n delete r.repetition_penalty;\r\n return r;\r\n }\r\n },\r\n groq: {\r\n request(req) {\r\n const r = { ...req };\r\n delete r.top_k;\r\n return r;\r\n }\r\n }\r\n};\r\n\r\nfunction resolveTransformers(useList, customMap) {\r\n if (!useList) return [];\r\n return useList.map(entry => {\r\n const name = Array.isArray(entry) ? entry[0] : entry;\r\n const opts = Array.isArray(entry) ? entry[1] : undefined;\r\n const t = (customMap && customMap[name]) || BUILT_IN[name];\r\n if (!t) { console.warn('[thebird] unknown transformer:', name); return null; }\r\n return { transformer: t, opts };\r\n }).filter(Boolean);\r\n}\r\n\r\nfunction applyRequestTransformers(req, transformers) {\r\n return transformers.reduce((r, { transformer, opts }) => transformer.request ? transformer.request(r, opts) : r, req);\r\n}\r\n\r\nfunction applyResponseTransformers(res, transformers) {\r\n return transformers.reduce((r, { transformer, opts }) => transformer.response ? transformer.response(r, opts) : r, res);\r\n}\r\n\r\nmodule.exports = { resolveTransformers, applyRequestTransformers, applyResponseTransformers, BUILT_IN };\r\n","lib/providers/openai.js":"const { GeminiError } = require('../errors');\r\n\r\nfunction convertMessages(messages, system) {\r\n const result = [];\r\n if (system) result.push({ role: 'system', content: typeof system === 'string' ? system : JSON.stringify(system) });\r\n for (const m of messages) {\r\n if (typeof m.content === 'string') { result.push({ role: m.role, content: m.content }); continue; }\r\n if (!Array.isArray(m.content)) continue;\r\n const toolCalls = m.content.filter(b => b.type === 'tool_use');\r\n const toolResults = m.content.filter(b => b.type === 'tool_result');\r\n if (toolResults.length) {\r\n for (const b of toolResults) {\r\n const c = typeof b.content === 'string' ? b.content : JSON.stringify(b.content || '');\r\n result.push({ role: 'tool', tool_call_id: b.tool_use_id || b.id || b.name, content: c });\r\n }\r\n continue;\r\n }\r\n const textParts = m.content.filter(b => b.type === 'text').map(b => b.text).join('');\r\n if (toolCalls.length) {\r\n result.push({ role: 'assistant', content: textParts || null,\r\n tool_calls: toolCalls.map(b => ({ id: b.id || ('call_' + Math.random().toString(36).slice(2,8)), type: 'function',\r\n function: { name: b.name, arguments: JSON.stringify(b.input || {}) } })) });\r\n } else {\r\n result.push({ role: m.role, content: textParts });\r\n }\r\n }\r\n return result;\r\n}\r\n\r\nfunction convertTools(tools) {\r\n if (!tools || typeof tools !== 'object') return undefined;\r\n const list = Object.entries(tools).map(([name, t]) => ({\r\n type: 'function', function: { name, description: t.description || '',\r\n parameters: t.parameters?.jsonSchema || t.parameters || { type: 'object' } }\r\n }));\r\n return list.length ? list : undefined;\r\n}\r\n\r\nasync function callOpenAI({ url, apiKey, headers, body }) {\r\n const res = await fetch(url, { method: 'POST',\r\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, ...(headers || {}) },\r\n body: JSON.stringify(body) });\r\n if (!res.ok) { const t = await res.text(); throw new GeminiError(t, { status: res.status, retryable: res.status === 429 || res.status >= 500 }); }\r\n return res;\r\n}\r\n\r\nasync function* streamOpenAI({ url, apiKey, headers, body, tools, onStepFinish }) {\r\n while (true) {\r\n yield { type: 'start-step' };\r\n const res = await callOpenAI({ url, apiKey, headers, body: { ...body, stream: true } });\r\n const reader = res.body.getReader();\r\n const dec = new TextDecoder();\r\n let buf = '', toolCallsMap = {};\r\n try {\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n buf += dec.decode(value, { stream: true });\r\n const lines = buf.split('\\n');\r\n buf = lines.pop();\r\n for (const line of lines) {\r\n if (!line.startsWith('data: ')) continue;\r\n const d = line.slice(6).trim();\r\n if (d === '[DONE]') break;\r\n let chunk; try { chunk = JSON.parse(d); } catch { continue; }\r\n const delta = chunk.choices?.[0]?.delta;\r\n if (!delta) continue;\r\n if (delta.content) yield { type: 'text-delta', textDelta: delta.content };\r\n if (delta.tool_calls) {\r\n for (const tc of delta.tool_calls) {\r\n const idx = tc.index ?? 0;\r\n if (!toolCallsMap[idx]) toolCallsMap[idx] = { id: tc.id || '', name: '', args: '' };\r\n if (tc.id) toolCallsMap[idx].id = tc.id;\r\n if (tc.function?.name) toolCallsMap[idx].name += tc.function.name;\r\n if (tc.function?.arguments) toolCallsMap[idx].args += tc.function.arguments;\r\n }\r\n }\r\n }\r\n }\r\n } finally { reader.releaseLock(); }\r\n\r\n const pending = Object.values(toolCallsMap);\r\n if (!pending.length) {\r\n yield { type: 'finish-step', finishReason: 'stop' };\r\n if (onStepFinish) await onStepFinish();\r\n return;\r\n }\r\n const toolResultMsgs = [];\r\n for (const tc of pending) {\r\n let args; try { args = JSON.parse(tc.args || '{}'); } catch { args = {}; }\r\n const toolDef = tools?.[tc.name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + tc.name };\r\n if (toolDef?.execute) try { result = await toolDef.execute(args, { toolCallId: tc.id }); } catch(e) { result = { error: true, message: e.message }; }\r\n yield { type: 'tool-call', toolCallId: tc.id, toolName: tc.name, args };\r\n yield { type: 'tool-result', toolCallId: tc.id, toolName: tc.name, args, result };\r\n toolResultMsgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result ?? '') });\r\n }\r\n yield { type: 'finish-step', finishReason: 'tool-calls' };\r\n if (onStepFinish) await onStepFinish();\r\n body = { ...body, messages: [...body.messages,\r\n { role: 'assistant', content: null, tool_calls: pending.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: tc.args } })) },\r\n ...toolResultMsgs\r\n ]};\r\n toolCallsMap = {};\r\n }\r\n}\r\n\r\nasync function generateOpenAI({ url, apiKey, headers, body, tools }) {\r\n while (true) {\r\n const res = await callOpenAI({ url, apiKey, headers, body: { ...body, stream: false } });\r\n const data = await res.json();\r\n const msg = data.choices?.[0]?.message;\r\n if (!msg) throw new GeminiError('No message in response', { retryable: false });\r\n if (!msg.tool_calls?.length) return { text: msg.content || '', response: data };\r\n const toolResultMsgs = [];\r\n for (const tc of msg.tool_calls) {\r\n let args; try { args = JSON.parse(tc.function?.arguments || '{}'); } catch { args = {}; }\r\n const toolDef = tools?.[tc.function?.name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + tc.function?.name };\r\n if (toolDef?.execute) try { result = await toolDef.execute(args); } catch(e) { result = { error: true, message: e.message }; }\r\n toolResultMsgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result ?? '') });\r\n }\r\n body = { ...body, messages: [...body.messages, msg, ...toolResultMsgs] };\r\n }\r\n}\r\n\r\nmodule.exports = { streamOpenAI, generateOpenAI, convertMessages, convertTools };\r\n","lib/cloud-generate.js":"const { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./convert');\r\nconst { ensureAuth, CODE_ASSIST_BASE, CODE_ASSIST_HEADERS } = require('./oauth');\r\nconst crypto = require('crypto');\r\n\r\nfunction buildUserAgent(model) {\r\n return `gemini-cli/0.30.0 (node; ${process.platform}) model/${model || 'unknown'}`;\r\n}\r\n\r\nasync function cloudGenerate({ model, system, messages, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, authPort }) {\r\n const tokens = await ensureAuth(authPort);\r\n const modelId = extractModelId(model);\r\n const contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n\r\n const request = { contents };\r\n if (config.systemInstruction) request.systemInstruction = { parts: [{ text: config.systemInstruction }] };\r\n if (config.tools) request.tools = config.tools;\r\n const genConfig = {};\r\n if (config.maxOutputTokens) genConfig.maxOutputTokens = config.maxOutputTokens;\r\n if (config.temperature != null) genConfig.temperature = config.temperature;\r\n if (config.topP != null) genConfig.topP = config.topP;\r\n if (config.topK != null) genConfig.topK = config.topK;\r\n if (config.responseModalities) genConfig.responseModalities = config.responseModalities;\r\n if (Object.keys(genConfig).length) request.generationConfig = genConfig;\r\n\r\n const envelope = { project: tokens.projectId, model: modelId, user_prompt_id: crypto.randomUUID(), request };\r\n\r\n const res = await fetch(`${CODE_ASSIST_BASE}:generateContent`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n Authorization: `Bearer ${tokens.accessToken}`,\r\n 'User-Agent': buildUserAgent(modelId),\r\n 'x-activity-request-id': crypto.randomUUID(),\r\n ...CODE_ASSIST_HEADERS\r\n },\r\n body: JSON.stringify(envelope)\r\n });\r\n\r\n if (!res.ok) throw new Error(`Cloud generate failed (${res.status}): ${await res.text()}`);\r\n const data = await res.json();\r\n const inner = data.response || data;\r\n const candidate = inner.candidates?.[0];\r\n if (!candidate) throw new Error('No candidates returned');\r\n const allParts = candidate.content?.parts || [];\r\n const text = allParts.filter(p => p.text && !p.thought).map(p => p.text).join('');\r\n return { text, parts: allParts, response: inner };\r\n}\r\n\r\nasync function* cloudStream({ model, system, messages, tools, onStepFinish, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities, authPort }) {\r\n const tokens = await ensureAuth(authPort);\r\n const modelId = extractModelId(model);\r\n const contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n\r\n const request = { contents };\r\n if (config.systemInstruction) request.systemInstruction = { parts: [{ text: config.systemInstruction }] };\r\n if (config.tools) request.tools = config.tools;\r\n const genConfig = {};\r\n if (config.maxOutputTokens) genConfig.maxOutputTokens = config.maxOutputTokens;\r\n if (config.temperature != null) genConfig.temperature = config.temperature;\r\n if (config.topP != null) genConfig.topP = config.topP;\r\n if (config.topK != null) genConfig.topK = config.topK;\r\n if (config.responseModalities) genConfig.responseModalities = config.responseModalities;\r\n if (Object.keys(genConfig).length) request.generationConfig = genConfig;\r\n\r\n const envelope = { project: tokens.projectId, model: modelId, user_prompt_id: crypto.randomUUID(), request };\r\n\r\n const res = await fetch(`${CODE_ASSIST_BASE}:streamGenerateContent?alt=sse`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n Authorization: `Bearer ${tokens.accessToken}`,\r\n 'User-Agent': buildUserAgent(modelId),\r\n 'x-activity-request-id': crypto.randomUUID(),\r\n Accept: 'text/event-stream',\r\n ...CODE_ASSIST_HEADERS\r\n },\r\n body: JSON.stringify(envelope)\r\n });\r\n\r\n if (!res.ok) throw new Error(`Cloud stream failed (${res.status}): ${await res.text()}`);\r\n\r\n yield { type: 'start-step' };\r\n const reader = res.body.getReader();\r\n const decoder = new TextDecoder();\r\n let buffer = '';\r\n\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n buffer += decoder.decode(value, { stream: true });\r\n const lines = buffer.split('\\n');\r\n buffer = lines.pop() || '';\r\n for (const line of lines) {\r\n const trimmed = line.trim();\r\n if (!trimmed.startsWith('data:')) continue;\r\n const json = trimmed.slice(5).trim();\r\n if (!json || json === '[DONE]') continue;\r\n try {\r\n const parsed = JSON.parse(json);\r\n const inner = parsed.response || parsed;\r\n const parts = inner.candidates?.[0]?.content?.parts || [];\r\n for (const part of parts) {\r\n if (part.text && !part.thought) yield { type: 'text-delta', textDelta: part.text };\r\n if (part.inlineData) yield { type: 'image-data', inlineData: part.inlineData };\r\n }\r\n } catch {}\r\n }\r\n }\r\n yield { type: 'finish-step', finishReason: 'stop' };\r\n if (onStepFinish) await onStepFinish();\r\n}\r\n\r\nfunction streamCloud(params) {\r\n return { fullStream: cloudStream(params), warnings: Promise.resolve([]) };\r\n}\r\n\r\nmodule.exports = { cloudGenerate, cloudStream, streamCloud };\r\n","lib/oauth.js":"const http = require('http');\r\nconst crypto = require('crypto');\r\nconst fs = require('fs');\r\nconst path = require('path');\r\n\r\nconst CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID || '';\r\nconst CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET || '';\r\nconst SCOPES = 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile';\r\nconst AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';\r\nconst TOKEN_URL = 'https://oauth2.googleapis.com/token';\r\nconst CODE_ASSIST_BASE = 'https://cloudcode-pa.googleapis.com/v1internal';\r\nconst CODE_ASSIST_HEADERS = { 'X-Goog-Api-Client': 'gl-node/22.17.0', 'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI' };\r\nconst TOKEN_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thebird', 'oauth-tokens.json');\r\n\r\nfunction base64url(buf) {\r\n return buf.toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\r\n}\r\n\r\nfunction generatePkce() {\r\n const verifier = base64url(crypto.randomBytes(32));\r\n const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());\r\n return { verifier, challenge };\r\n}\r\n\r\nfunction readTokens() {\r\n try { return JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8')); } catch { return null; }\r\n}\r\n\r\nfunction writeTokens(tokens) {\r\n fs.mkdirSync(path.dirname(TOKEN_PATH), { recursive: true });\r\n fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));\r\n}\r\n\r\nasync function refreshAccessToken(refreshToken) {\r\n const res = await fetch(TOKEN_URL, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: CLIENT_ID, client_secret: CLIENT_SECRET })\r\n });\r\n if (!res.ok) throw new Error('Token refresh failed: ' + await res.text());\r\n const data = await res.json();\r\n return { accessToken: data.access_token, refreshToken: data.refresh_token || refreshToken, expiresAt: Date.now() + data.expires_in * 1000 };\r\n}\r\n\r\nasync function getValidToken() {\r\n const tokens = readTokens();\r\n if (!tokens?.refreshToken) return null;\r\n if (tokens.expiresAt && tokens.expiresAt > Date.now() + 60000) return tokens;\r\n const refreshed = await refreshAccessToken(tokens.refreshToken);\r\n const updated = { ...tokens, ...refreshed };\r\n writeTokens(updated);\r\n return updated;\r\n}\r\n\r\nasync function resolveProject(accessToken) {\r\n const res = await fetch(`${CODE_ASSIST_BASE}:loadCodeAssist`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS },\r\n body: JSON.stringify({ metadata: { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } })\r\n });\r\n if (!res.ok) throw new Error('Failed to load Code Assist project');\r\n const data = await res.json();\r\n const proj = data.cloudaicompanionProject;\r\n if (proj) return typeof proj === 'string' ? proj : proj.id;\r\n const tier = data.allowedTiers?.find(t => t.id === 'free-tier') || data.allowedTiers?.[0];\r\n if (!tier) throw new Error('No eligible tier: ' + (data.ineligibleTiers?.[0]?.reasonMessage || 'unknown'));\r\n const obRes = await fetch(`${CODE_ASSIST_BASE}:onboardUser`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS },\r\n body: JSON.stringify({ tierId: tier.id || 'legacy-tier', metadata: { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } })\r\n });\r\n if (!obRes.ok) throw new Error('Onboarding failed');\r\n let op = await obRes.json();\r\n for (let i = 0; i < 10 && !op.done && op.name; i++) {\r\n await new Promise(r => setTimeout(r, 5000));\r\n const pollRes = await fetch(`${CODE_ASSIST_BASE}/${op.name}`, { headers: { Authorization: `Bearer ${accessToken}`, ...CODE_ASSIST_HEADERS } });\r\n if (pollRes.ok) op = await pollRes.json();\r\n }\r\n return op.response?.cloudaicompanionProject?.id;\r\n}\r\n\r\nfunction login(port) {\r\n return new Promise((resolve, reject) => {\r\n const { verifier, challenge } = generatePkce();\r\n const state = crypto.randomBytes(32).toString('hex');\r\n const callbackUrl = `http://localhost:${port}/callback`;\r\n const url = new URL(AUTH_URL);\r\n url.searchParams.set('client_id', CLIENT_ID);\r\n url.searchParams.set('response_type', 'code');\r\n url.searchParams.set('redirect_uri', callbackUrl);\r\n url.searchParams.set('scope', SCOPES);\r\n url.searchParams.set('code_challenge', challenge);\r\n url.searchParams.set('code_challenge_method', 'S256');\r\n url.searchParams.set('state', state);\r\n url.searchParams.set('access_type', 'offline');\r\n url.searchParams.set('prompt', 'consent');\r\n\r\n const server = http.createServer(async (req, res) => {\r\n const u = new URL(req.url, `http://localhost:${port}`);\r\n if (!u.pathname.startsWith('/callback')) { res.end('waiting...'); return; }\r\n if (u.searchParams.get('state') !== state) { res.end('Invalid state'); server.close(); reject(new Error('Invalid state')); return; }\r\n const code = u.searchParams.get('code');\r\n try {\r\n const tokRes = await fetch(TOKEN_URL, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, code, grant_type: 'authorization_code', redirect_uri: callbackUrl, code_verifier: verifier })\r\n });\r\n if (!tokRes.ok) throw new Error('Token exchange failed: ' + await tokRes.text());\r\n const payload = await tokRes.json();\r\n if (!payload.refresh_token) throw new Error('No refresh token — ensure prompt=consent');\r\n const projectId = await resolveProject(payload.access_token);\r\n const tokens = { accessToken: payload.access_token, refreshToken: payload.refresh_token, expiresAt: Date.now() + payload.expires_in * 1000, projectId };\r\n writeTokens(tokens);\r\n res.end('Authenticated! You can close this tab.');\r\n server.close();\r\n resolve(tokens);\r\n } catch (e) { res.end('Error: ' + e.message); server.close(); reject(e); }\r\n });\r\n server.listen(port, () => {\r\n console.log(`Open this URL to authenticate:\\n${url.toString()}\\n`);\r\n try { const { exec } = require('child_process'); exec(`start \"\" \"${url.toString()}\"`); } catch {}\r\n });\r\n });\r\n}\r\n\r\nasync function ensureAuth(port) {\r\n const existing = await getValidToken();\r\n if (existing?.accessToken && existing?.projectId) return existing;\r\n return login(port || 8585);\r\n}\r\n\r\nmodule.exports = { login, ensureAuth, getValidToken, readTokens, writeTokens, resolveProject, CODE_ASSIST_BASE, CODE_ASSIST_HEADERS };\r\n","index.js":"const { getClient } = require('./lib/client');\r\nconst { GeminiError, withRetry } = require('./lib/errors');\r\nconst { convertMessages, convertTools, cleanSchema, extractModelId, buildConfig } = require('./lib/convert');\r\nconst { loadConfig } = require('./lib/config');\r\nconst { route } = require('./lib/router');\r\nconst { resolveTransformers, applyRequestTransformers } = require('./lib/transformers');\r\nconst openaiProv = require('./lib/providers/openai');\r\n\r\nfunction streamGemini({ model, system, messages, tools, onStepFinish, apiKey,\r\n temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {\r\n return {\r\n fullStream: createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }),\r\n warnings: Promise.resolve([])\r\n };\r\n}\r\n\r\nasync function* createFullStream({ model, system, messages, tools, onStepFinish, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {\r\n const client = getClient(apiKey);\r\n const modelId = extractModelId(model);\r\n let contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n while (true) {\r\n yield { type: 'start-step' };\r\n try {\r\n const stream = await withRetry(() => client.models.generateContentStream({ model: modelId, contents, config }));\r\n const allParts = [];\r\n for await (const chunk of stream) {\r\n for (const candidate of (chunk.candidates || [])) {\r\n for (const part of (candidate.content?.parts || [])) {\r\n allParts.push(part);\r\n if (part.text && !part.thought) yield { type: 'text-delta', textDelta: part.text };\r\n }\r\n }\r\n }\r\n const fcParts = allParts.filter(p => p.functionCall);\r\n if (fcParts.length === 0) {\r\n yield { type: 'finish-step', finishReason: 'stop' };\r\n if (onStepFinish) await onStepFinish();\r\n return;\r\n }\r\n const toolResultParts = [];\r\n for (const part of fcParts) {\r\n const name = part.functionCall.name;\r\n const args = part.functionCall.args || {};\r\n const toolId = 'toolu_' + Math.random().toString(36).slice(2, 10);\r\n yield { type: 'tool-call', toolCallId: toolId, toolName: name, args };\r\n const toolDef = tools?.[name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };\r\n if (toolDef?.execute) {\r\n try { result = await toolDef.execute(args, { toolCallId: toolId }); }\r\n catch (e) { result = { error: true, message: e.message }; }\r\n }\r\n yield { type: 'tool-result', toolCallId: toolId, toolName: name, args, result };\r\n toolResultParts.push({ functionResponse: { name, response: result || {} } });\r\n }\r\n yield { type: 'finish-step', finishReason: 'tool-calls' };\r\n if (onStepFinish) await onStepFinish();\r\n contents.push({ role: 'model', parts: allParts });\r\n contents.push({ role: 'user', parts: toolResultParts });\r\n } catch (err) {\r\n yield { type: 'error', error: err };\r\n yield { type: 'finish-step', finishReason: 'error' };\r\n if (onStepFinish) await onStepFinish();\r\n return;\r\n }\r\n }\r\n}\r\n\r\nasync function generateGemini({ model, system, messages, tools, apiKey, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities }) {\r\n const client = getClient(apiKey);\r\n const modelId = extractModelId(model);\r\n let contents = convertMessages(messages);\r\n const { config } = buildConfig({ system, tools, temperature, maxOutputTokens, topP, topK, safetySettings, responseModalities });\r\n while (true) {\r\n const response = await withRetry(() => client.models.generateContent({ model: modelId, contents, config }));\r\n const candidate = response.candidates?.[0];\r\n if (!candidate) throw new GeminiError('No candidates returned', { retryable: false });\r\n const allParts = candidate.content?.parts || [];\r\n const fcParts = allParts.filter(p => p.functionCall);\r\n if (fcParts.length === 0) {\r\n const text = allParts.filter(p => p.text && !p.thought).map(p => p.text).join('');\r\n return { text, parts: allParts, response };\r\n }\r\n const toolResultParts = [];\r\n for (const part of fcParts) {\r\n const name = part.functionCall.name;\r\n const args = part.functionCall.args || {};\r\n const toolDef = tools?.[name];\r\n let result = toolDef ? null : { error: true, message: 'Tool not found: ' + name };\r\n if (toolDef?.execute) {\r\n try { result = await toolDef.execute(args); }\r\n catch (e) { result = { error: true, message: e.message }; }\r\n }\r\n toolResultParts.push({ functionResponse: { name, response: result || {} } });\r\n }\r\n contents.push({ role: 'model', parts: allParts });\r\n contents.push({ role: 'user', parts: toolResultParts });\r\n }\r\n}\r\n\r\nfunction isGeminiProvider(p) {\r\n return p.name === 'gemini' || (p.api_base_url || '').includes('generativelanguage.googleapis.com');\r\n}\r\n\r\nfunction findProvider(providers, providerName, modelName) {\r\n if (providerName) return providers.find(p => p.name === providerName);\r\n if (modelName) return providers.find(p => (p.models || []).includes(modelName));\r\n return providers[0];\r\n}\r\n\r\nfunction buildOpenAIUrl(base) {\r\n const clean = (base || '').replace(/\\/$/g, '');\r\n return clean.includes('/completions') ? clean : clean + '/chat/completions';\r\n}\r\n\r\nfunction resolveForProvider(provider, model, customMap) {\r\n const useList = provider.transformer?.[model]?.use || provider.transformer?.use || [];\r\n return resolveTransformers(useList, customMap);\r\n}\r\n\r\nasync function* routerStream(params, resolver) {\r\n const { provider, actualModel, transformers } = await resolver(params);\r\n if (isGeminiProvider(provider)) {\r\n yield* createFullStream({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });\r\n } else {\r\n const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);\r\n const oaiTools = openaiProv.convertTools(params.tools);\r\n let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };\r\n if (oaiTools) req.tools = oaiTools;\r\n req = applyRequestTransformers(req, transformers);\r\n yield* openaiProv.streamOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools, onStepFinish: params.onStepFinish });\r\n }\r\n}\r\n\r\nfunction createRouter(config) {\r\n const providers = config.Providers || config.providers || [];\r\n const routerCfg = config.Router || {};\r\n async function resolve(params) {\r\n const { providerName, modelName } = await route(params, routerCfg, config.customRouter);\r\n const provider = findProvider(providers, providerName, modelName) || providers[0];\r\n if (!provider) throw new Error('[thebird] no provider configured');\r\n const actualModel = modelName || (provider.models || [])[0] || extractModelId(params.model) || 'gemini-2.0-flash';\r\n const transformers = resolveForProvider(provider, actualModel, config._transformers);\r\n return { provider, actualModel, transformers };\r\n }\r\n return {\r\n stream(params) { return { fullStream: routerStream(params, resolve), warnings: Promise.resolve([]) }; },\r\n async generate(params) {\r\n const { provider, actualModel, transformers } = await resolve(params);\r\n if (isGeminiProvider(provider)) return generateGemini({ ...params, model: actualModel, apiKey: provider.api_key || params.apiKey });\r\n const oaiMsgs = openaiProv.convertMessages(params.messages, params.system);\r\n const oaiTools = openaiProv.convertTools(params.tools);\r\n let req = { messages: oaiMsgs, model: actualModel, max_tokens: params.maxOutputTokens || 8192, temperature: params.temperature ?? 0.5 };\r\n if (oaiTools) req.tools = oaiTools;\r\n req = applyRequestTransformers(req, transformers);\r\n return openaiProv.generateOpenAI({ url: buildOpenAIUrl(provider.api_base_url), apiKey: provider.api_key, headers: req._extraHeaders, body: req, tools: params.tools });\r\n }\r\n };\r\n}\r\n\r\nfunction streamRouter(params) {\r\n const config = loadConfig(params.configPath);\r\n if (!(config.Providers || config.providers)?.length) return streamGemini(params);\r\n return createRouter(config).stream(params);\r\n}\r\n\r\nasync function generateRouter(params) {\r\n const config = loadConfig(params.configPath);\r\n if (!(config.Providers || config.providers)?.length) return generateGemini(params);\r\n return createRouter(config).generate(params);\r\n}\r\n\r\nconst { cloudGenerate, streamCloud, cloudStream } = require('./lib/cloud-generate');\r\nconst { ensureAuth, login: oauthLogin } = require('./lib/oauth');\r\n\r\nmodule.exports = { streamGemini, generateGemini, streamRouter, generateRouter, createRouter, convertMessages, convertTools, cleanSchema, GeminiError, cloudGenerate, streamCloud, cloudStream, ensureAuth, oauthLogin };\r\n","server.js":"const http = require('http');\r\nconst { streamGemini, generateGemini } = require('./index.js');\r\n\r\nconst PORT = process.env.PORT || 3456;\r\nconst state = { requests: 0, errors: 0, active: 0 };\r\n\r\nconst sse = (ev, data) => `event: ${ev}\\ndata: ${JSON.stringify(data)}\\n\\n`;\r\n\r\nconst msgId = () => 'msg_' + Math.random().toString(36).slice(2, 12);\r\n\r\nasync function handleMessages(req, res) {\r\n let body = '';\r\n for await (const chunk of req) body += chunk;\r\n const { model, messages, system, stream, max_tokens } = JSON.parse(body);\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) { res.writeHead(500); res.end(JSON.stringify({ error: 'GEMINI_API_KEY required' })); return; }\r\n const params = { model: model || 'gemini-2.5-flash', messages, system, apiKey, maxOutputTokens: max_tokens || 8192 };\r\n\r\n if (!stream) {\r\n const result = await generateGemini(params);\r\n res.writeHead(200, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify({\r\n id: msgId(), type: 'message', role: 'assistant', model: params.model,\r\n content: [{ type: 'text', text: result.text }],\r\n stop_reason: 'end_turn', usage: { input_tokens: 0, output_tokens: 0 },\r\n }));\r\n return;\r\n }\r\n\r\n res.writeHead(200, {\r\n 'Content-Type': 'text/event-stream',\r\n 'Cache-Control': 'no-cache',\r\n 'Connection': 'keep-alive',\r\n });\r\n\r\n const id = msgId();\r\n 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 } } }));\r\n res.write(sse('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }));\r\n res.write(sse('ping', { type: 'ping' }));\r\n\r\n let outputTokens = 0;\r\n for await (const ev of streamGemini(params).fullStream) {\r\n if (ev.type === 'text-delta') {\r\n outputTokens += ev.textDelta.length;\r\n res.write(sse('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: ev.textDelta } }));\r\n }\r\n }\r\n\r\n res.write(sse('content_block_stop', { type: 'content_block_stop', index: 0 }));\r\n res.write(sse('message_delta', { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: outputTokens } }));\r\n res.write(sse('message_stop', { type: 'message_stop' }));\r\n res.end();\r\n}\r\n\r\nhttp.createServer(async (req, res) => {\r\n state.requests++;\r\n state.active++;\r\n try {\r\n if (req.method === 'GET' && req.url === '/debug/server') {\r\n res.writeHead(200, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify(state));\r\n return;\r\n }\r\n if (req.method === 'POST' && req.url === '/v1/messages') {\r\n await handleMessages(req, res);\r\n return;\r\n }\r\n res.writeHead(404);\r\n res.end(JSON.stringify({ error: 'not found' }));\r\n } catch (err) {\r\n state.errors++;\r\n res.writeHead(500);\r\n res.end(JSON.stringify({ error: err.message }));\r\n } finally {\r\n state.active--;\r\n }\r\n}).listen(PORT, () => process.stderr.write(`thebird proxy listening on ${PORT}\\n`));\r\n","agent.js":"const Anthropic = require(\"@anthropic-ai/sdk\");\nconst { execSync } = require(\"child_process\");\nconst fs = require(\"fs\");\n\nconst apiKey = process.env.GEMINI_API_KEY;\nif (!apiKey) throw new Error(\"GEMINI_API_KEY not set\");\n\nconst client = new Anthropic({ apiKey, baseURL: \"http://localhost:3000\" });\n\nconst tools = [\n { name: \"read_file\", description: \"Read a file from the filesystem\", input_schema: { type: \"object\", properties: { path: { type: \"string\" } }, required: [\"path\"] } },\n { name: \"write_file\", description: \"Write content to a file\", input_schema: { type: \"object\", properties: { path: { type: \"string\" }, content: { type: \"string\" } }, required: [\"path\", \"content\"] } },\n { name: \"run_command\", description: \"Run a shell command and return stdout\", input_schema: { type: \"object\", properties: { command: { type: \"string\" } }, required: [\"command\"] } },\n];\n\nconst toolHandlers = {\n read_file: ({ path }) => fs.readFileSync(path, \"utf-8\"),\n write_file: ({ path, content }) => {\n fs.mkdirSync(require(\"path\").dirname(path), { recursive: true });\n fs.writeFileSync(path, content);\n return \"written: \" + path;\n },\n run_command: ({ command }) => {\n try { return execSync(command, { encoding: \"utf-8\", timeout: 10000 }); }\n catch (e) { return \"error: \" + e.message; }\n },\n};\n\nasync function agent(task) {\n const messages = [{ role: \"user\", content: task }];\n while (true) {\n const res = await client.messages.create({ model: \"gemini-2.5-flash\", max_tokens: 4096, tools, messages });\n console.log(\"[agent] stop_reason:\", res.stop_reason);\n messages.push({ role: \"assistant\", content: res.content });\n if (res.stop_reason === \"end_turn\") { console.log(\"[agent] done\"); break; }\n if (res.stop_reason !== \"tool_use\") break;\n const results = res.content\n .filter(b => b.type === \"tool_use\")\n .map(b => {\n console.log(\"[tool]\", b.name, JSON.stringify(b.input));\n let out;\n try { out = String(toolHandlers[b.name](b.input)); }\n catch (e) { out = \"error: \" + e.message; }\n console.log(\"[result]\", out.slice(0, 200));\n return { type: \"tool_result\", tool_use_id: b.id, content: out };\n });\n messages.push({ role: \"user\", content: results });\n }\n}\n\nconst task = process.argv.slice(2).join(\" \") || \"List all files, read package.json, then write a file called hello.txt with the content: hello world\";\nagent(task).catch(e => { console.error(\"[agent error]\", e.message); process.exit(1); });\n"}
package/docs/index.html CHANGED
@@ -3,8 +3,9 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="version" content="1.2.22">
6
+ <meta name="version" content="1.2.23">
7
7
  <title>thebird — Gemini chat + terminal</title>
8
+ <script>window.coi = { coepDegrade: () => false };</script>
8
9
  <script src="coi-serviceworker.js"></script>
9
10
  <script src="https://cdn.tailwindcss.com"></script>
10
11
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rippleui@1.12.1/dist/css/styles.css" />
@@ -32,7 +33,7 @@
32
33
  <div id="term-container" style="height:100%"></div>
33
34
  </div>
34
35
  <div id="pane-preview" class="flex-1 overflow-hidden hidden">
35
- <iframe id="preview-frame" class="w-full h-full border-0" src="about:blank" allow="cross-origin-isolated *"></iframe>
36
+ <iframe id="preview-frame" class="w-full h-full border-0" src="about:blank"></iframe>
36
37
  </div>
37
38
  </div>
38
39
  <script>
package/docs/terminal.js CHANGED
@@ -4,35 +4,6 @@ import { FitAddon } from 'https://esm.sh/@xterm/addon-fit';
4
4
 
5
5
  const IDB_KEY = 'thebird_fs';
6
6
 
7
- const SERVER_JS = [
8
- 'const http = require("http");',
9
- 'const state = { requests: 0, start: Date.now() };',
10
- 'http.createServer((req, res) => {',
11
- ' state.requests++;',
12
- ' res.setHeader("Content-Type", "application/json");',
13
- ' res.setHeader("Access-Control-Allow-Origin", "*");',
14
- ' res.end(JSON.stringify({ ok: true, path: req.url, requests: state.requests, uptime: Date.now() - state.start }));',
15
- '}).listen(3000, () => console.log("server ready on :3000"));',
16
- ].join('\n') + '\n';
17
-
18
- const INDEX_JS = [
19
- 'const { default: Anthropic } = require("@anthropic-ai/sdk");',
20
- 'const http = require("http");',
21
- 'const client = new Anthropic({ apiKey: "x", baseURL: "http://localhost:3000" });',
22
- 'console.log("sdk:", client.constructor.name);',
23
- 'http.get("http://localhost:3000/status", r => {',
24
- ' let d = "";',
25
- ' r.on("data", c => d += c);',
26
- ' r.on("end", () => console.log("server:", d));',
27
- '});',
28
- ].join('\n') + '\n';
29
-
30
- const DEFAULT_FILES = {
31
- 'package.json': JSON.stringify({ name: 'app', dependencies: { '@anthropic-ai/sdk': '^0.88.0' } }, null, 2),
32
- 'server.js': SERVER_JS,
33
- 'index.js': INDEX_JS,
34
- };
35
-
36
7
  async function idbLoad() {
37
8
  return new Promise((res, rej) => {
38
9
  const req = indexedDB.open('thebird', 1);
@@ -60,9 +31,9 @@ async function idbSave(data) {
60
31
  });
61
32
  }
62
33
 
63
- async function snapshotToIDB(container, files) {
34
+ async function snapshotToIDB(container, keys) {
64
35
  const snap = {};
65
- await Promise.all(Object.keys(files).map(async p => {
36
+ await Promise.all(keys.map(async p => {
66
37
  try { snap[p] = await container.fs.readFile(p, 'utf-8'); } catch {}
67
38
  }));
68
39
  await idbSave(JSON.stringify(snap));
@@ -80,9 +51,20 @@ async function boot() {
80
51
  window.addEventListener('resize', () => fit.fit());
81
52
 
82
53
  const saved = await idbLoad();
83
- const files = saved ? JSON.parse(saved) : DEFAULT_FILES;
54
+ let files;
55
+ if (saved) {
56
+ files = JSON.parse(saved);
57
+ } else {
58
+ const r = await fetch('./defaults.json');
59
+ files = await r.json();
60
+ }
61
+
84
62
  const mountTree = Object.fromEntries(
85
- Object.entries(files).map(([p, c]) => [p, { file: { contents: c } }])
63
+ Object.entries(files).map(([p, c]) => {
64
+ const parts = p.split('/');
65
+ if (parts.length === 1) return [p, { file: { contents: c } }];
66
+ return [p, { file: { contents: c } }];
67
+ })
86
68
  );
87
69
 
88
70
  term.write('Booting WebContainer...\r\n');
@@ -112,7 +94,7 @@ async function boot() {
112
94
  const srv = await container.spawn('node', ['server.js']);
113
95
  srv.output.pipeTo(new WritableStream({ write: d => term.write(d) }));
114
96
 
115
- term.write('\x1b[32mReady.\x1b[0m\r\n');
97
+ term.write('\x1b[32mReady. Run: GEMINI_API_KEY=<key> node agent.js\x1b[0m\r\n');
116
98
 
117
99
  const shell = await container.spawn('jsh', [], {
118
100
  terminal: { cols: term.cols, rows: term.rows },
@@ -122,7 +104,7 @@ async function boot() {
122
104
  const writer = shell.input.getWriter();
123
105
  term.onData(data => writer.write(data));
124
106
 
125
- await snapshotToIDB(container, files);
107
+ await snapshotToIDB(container, Object.keys(files));
126
108
 
127
109
  window.__debug = window.__debug || {};
128
110
  window.__debug.container = container;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.23",
3
+ "version": "1.2.25",
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",