thebird 1.2.83 → 1.2.84

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.
@@ -29,13 +29,15 @@ jobs:
29
29
  - name: Bump patch version
30
30
  run: |
31
31
  git fetch --tags --force
32
- for i in 1 2 3 4 5; do
32
+ HIGHEST=$(git tag -l 'v[0-9]*' | sort -V | tail -1 | sed 's/^v//')
33
+ CURRENT=$(node -p "require('./package.json').version")
34
+ BASE="$HIGHEST"
35
+ if [ -z "$BASE" ] || [ "$(printf '%s\n%s' "$CURRENT" "$BASE" | sort -V | tail -1)" = "$CURRENT" ]; then BASE="$CURRENT"; fi
36
+ node -e "const p=require('./package.json');p.version='$BASE';require('fs').writeFileSync('./package.json', JSON.stringify(p,null,2)+'\n')"
37
+ NEW_VERSION=$(npm version patch --no-git-tag-version)
38
+ while git rev-parse "$NEW_VERSION" >/dev/null 2>&1; do
39
+ echo "tag $NEW_VERSION exists, bumping again"
33
40
  NEW_VERSION=$(npm version patch --no-git-tag-version)
34
- if git rev-parse "${NEW_VERSION}" >/dev/null 2>&1; then
35
- echo "Tag ${NEW_VERSION} already exists, bumping again (attempt $i)"
36
- continue
37
- fi
38
- break
39
41
  done
40
42
  git add package.json
41
43
  git commit -m "chore: bump to ${NEW_VERSION}"
@@ -91,8 +91,8 @@ function buildStream(provider) {
91
91
  if (provider.type === 'gemini') {
92
92
  return streamGemini({ model: provider.model, messages: provider.messages, tools: TOOLS, apiKey: provider.apiKey, maxOutputTokens: 8192 }).fullStream;
93
93
  }
94
- if (provider.type === 'kilo') {
95
- return streamKiloHTTP({ url: provider.baseUrl, model: provider.model, messages: provider.messages });
94
+ if (provider.type === 'kilo' || provider.type === 'opencode') {
95
+ return streamKiloHTTP({ url: provider.baseUrl, model: provider.model, messages: provider.messages, providerType: provider.type });
96
96
  }
97
97
  const url = (provider.baseUrl || '').replace(/\/$/, '') + '/chat/completions';
98
98
  return streamOpenAI({ url, apiKey: provider.apiKey, messages: provider.messages, model: provider.model, tools: TOOLS, maxOutputTokens: 8192 });
package/docs/app.js CHANGED
@@ -12,6 +12,7 @@ const PROVIDERS = {
12
12
  deepseek: { label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', keyPlaceholder: 'DEEPSEEK_API_KEY', models: ['deepseek-chat', 'deepseek-reasoner'] },
13
13
  cerebras: { label: 'Cerebras', baseUrl: 'https://api.cerebras.ai/v1', keyPlaceholder: 'CEREBRAS_API_KEY', models: ['gpt-oss-120b', 'llama3.1-8b'] },
14
14
  kilo: { label: 'Kilo Code', baseUrl: 'http://localhost:4780', keyPlaceholder: '(no key needed)', models: ['x-ai/grok-code-fast-1:optimized:free', 'openrouter/free', 'kilo-auto/free'] },
15
+ opencode: { label: 'opencode', baseUrl: 'http://localhost:4790', keyPlaceholder: '(no key needed)', models: ['x-ai/grok-code-fast-1:optimized:free', 'openrouter/free', 'kilo-auto/free'] },
15
16
  custom: { label: 'Custom (OpenAI-compat)', baseUrl: '', keyPlaceholder: 'API_KEY', models: [] },
16
17
  };
17
18
 
@@ -95,6 +96,21 @@ class BirdChat extends HTMLElement {
95
96
  this.setState({ providerType: type, baseUrl, model, models: [], apiKey: localStorage.getItem('provider_api_key') || '' });
96
97
  }
97
98
 
99
+ renderBaseUrlInput() {
100
+ const { providerType, baseUrl } = this.state;
101
+ if (providerType !== 'custom' && providerType !== 'kilo' && providerType !== 'opencode') return null;
102
+ const ph = providerType === 'kilo' ? 'http://localhost:4780' : (providerType === 'opencode' ? 'http://localhost:4790' : 'https://your-endpoint/v1');
103
+ return html`<input type="text" class="tui-input" style="flex:1;min-width:140px" placeholder=${ph} value=${baseUrl}
104
+ onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />`;
105
+ }
106
+ renderApiKeyInput() {
107
+ const { providerType, apiKey } = this.state;
108
+ if (providerType === 'kilo' || providerType === 'opencode') return null;
109
+ const provDef = PROVIDERS[providerType] || PROVIDERS.custom;
110
+ return html`<input id="api-key-input" type="password" class="tui-input" style="flex:1;min-width:120px" placeholder=${provDef.keyPlaceholder} value=${apiKey}
111
+ onchange=${e => { const v = e.target.value.trim(); localStorage.setItem('provider_api_key', v); this.setState({ apiKey: v }); if (v) this.loadModels(); }} />`;
112
+ }
113
+
98
114
  render() {
99
115
  const { messages, streaming, model, apiKey, models, modelsLoading, status, providerType, baseUrl, streamingText } = this.state;
100
116
  const provDef = PROVIDERS[providerType] || PROVIDERS.custom;
@@ -108,18 +124,8 @@ class BirdChat extends HTMLElement {
108
124
  <div class="tui-toolbar">
109
125
  <label>provider:</label>
110
126
  <select class="tui-select" onchange=${e => this.setProvider(e.target.value)}>${provOpts}</select>
111
- ${(providerType === 'custom' || providerType === 'kilo') ? html`
112
- <input type="text" class="tui-input" style="flex:1;min-width:140px"
113
- placeholder=${providerType === 'kilo' ? 'http://localhost:4780' : 'https://your-endpoint/v1'} value=${baseUrl}
114
- onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />` : ''}
115
- ${providerType !== 'kilo' ? html`<input id="api-key-input" type="password" class="tui-input" style="flex:1;min-width:120px"
116
- placeholder=${provDef.keyPlaceholder} value=${apiKey}
117
- onchange=${e => {
118
- const v = e.target.value.trim();
119
- localStorage.setItem('provider_api_key', v);
120
- this.setState({ apiKey: v });
121
- if (v) this.loadModels();
122
- }} />` : ''}
127
+ ${this.renderBaseUrlInput()}
128
+ ${this.renderApiKeyInput()}
123
129
  ${modelsLoading
124
130
  ? html`<span class="tui-spinner"></span>`
125
131
  : html`<select class="tui-select" value=${model}
@@ -129,7 +135,7 @@ class BirdChat extends HTMLElement {
129
135
 
130
136
  <div id="msg-list" class="tui-msglist">
131
137
  ${messages.map((m, i) => html`
132
- <div key=${i} class=${'tui-msg ' + m.role}>${m.content}</div>`)}
138
+ <div key=${i} class=${'tui-msg ' + m.role}>${typeof m.content === 'string' ? m.content : (m.content || []).filter(b => b.type === 'text').map(b => b.text).join('')}</div>`)}
133
139
  ${streaming && !streamingText && html`<div class="tui-msg assistant"><span class="tui-spinner"></span> thinking...</div>`}
134
140
  </div>
135
141
 
@@ -152,7 +158,7 @@ class BirdChat extends HTMLElement {
152
158
  const text = input?.value.trim();
153
159
  if (!text || this.state.streaming) return;
154
160
  const { apiKey, model, providerType, baseUrl } = this.state;
155
- if (!apiKey && providerType !== 'kilo') { this.setState({ status: 'Enter an API key above.' }); return; }
161
+ if (!apiKey && providerType !== 'kilo' && providerType !== 'opencode') { this.setState({ status: 'Enter an API key above.' }); return; }
156
162
  input.value = '';
157
163
  input.style.height = 'auto';
158
164
  const normalizedMessages = [...this.state.messages, { role: 'user', content: text }].map(m => ({
@@ -1,72 +1,67 @@
1
- import { mirrorFromSandbox } from './kilo-fs-mirror.js';
2
-
3
- export async function* streamKiloHTTP({ url, model, messages }) {
1
+ export async function* streamKiloHTTP({ url, model, messages, providerType }) {
4
2
  yield { type: 'start-step' };
5
3
  const base = (url || 'http://localhost:4780').replace(/\/$/, '');
6
- const fsBase = base.replace(/:\d+$/, ':4781');
4
+ const isOpencode = providerType === 'opencode';
5
+ const dbgKey = isOpencode ? 'opencode' : 'kilo';
7
6
  let sessRes;
8
7
  try { sessRes = await fetch(base + '/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); }
9
- catch (e) { throw new Error('kilo serve not reachable at ' + base + ' — start it with: node start-kilo.js --origin ' + location.origin); }
10
- if (!sessRes.ok) throw new Error('kilo /session ' + sessRes.status + ': ' + await sessRes.text());
8
+ catch (e) { throw new Error(dbgKey + ' serve not reachable at ' + base + ' — start it with: ' + dbgKey + ' serve --port ' + (new URL(base).port || (isOpencode ? 4790 : 4780)) + ' --cors ' + location.origin); }
9
+ if (!sessRes.ok) throw new Error('/session ' + sessRes.status + ': ' + await sessRes.text());
11
10
  const { id: sessionId } = await sessRes.json();
12
- Object.assign(window.__debug = window.__debug || {}, { kilo: { sessionId, url: base, fsBase, writes: [], toolCalls: [], lastStatus: null } });
13
-
14
- const queue = [];
15
- let resolveNext = null;
16
- let streamEnded = false;
17
- const push = ev => { queue.push(ev); if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } };
18
- const textSeen = new Set();
19
- const toolState = new Map();
20
-
21
- const es = new EventSource(base + '/event');
22
- es.onmessage = (ev) => {
23
- try {
24
- const msg = JSON.parse(ev.data);
25
- if (msg.type !== 'message.part.updated') return;
26
- const part = msg.properties?.part;
27
- if (!part) return;
28
- if (part.type === 'text' && part.messageID && !textSeen.has(part.id)) {
29
- const prior = toolState.get('__text_' + part.id) || '';
30
- const txt = part.text || '';
31
- if (txt.length > prior.length) { push({ type: 'text-delta', textDelta: txt.slice(prior.length) }); toolState.set('__text_' + part.id, txt); }
32
- } else if (part.type === 'tool') {
33
- const cid = part.callID;
34
- const st = part.state?.status;
35
- if (st === 'running' && !toolState.has(cid)) {
36
- toolState.set(cid, { name: part.tool, args: part.state.input || {} });
37
- push({ type: 'tool-call', toolCallId: cid, toolName: part.tool, args: part.state.input || {} });
38
- window.__debug.kilo.toolCalls.push({ id: cid, name: part.tool, args: part.state.input || {} });
39
- } else if (st === 'completed' && toolState.has(cid) && !toolState.get(cid).completed) {
40
- toolState.get(cid).completed = true;
41
- push({ type: 'tool-result', toolCallId: cid, toolName: part.tool, args: part.state.input || {}, result: part.state.output || '' });
42
- }
43
- }
44
- } catch (_) {}
45
- };
11
+ Object.assign(window.__debug = window.__debug || {}, { [dbgKey]: { sessionId, url: base, lastStatus: null } });
46
12
 
47
13
  const userText = messages.filter(m => m.role === 'user').map(m =>
48
14
  typeof m.content === 'string' ? m.content : (m.content || []).filter(b => b.type === 'text').map(b => b.text).join('')
49
15
  ).join('\n');
50
16
 
51
- const body = { parts: [{ type: 'text', text: userText }], providerID: 'kilo', modelID: model || 'x-ai/grok-code-fast-1:optimized:free' };
52
- const msgPromise = fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(async r => {
53
- window.__debug.kilo.lastStatus = r.status;
54
- const json = await r.json();
55
- window.__debug.kilo.lastResult = json;
56
- streamEnded = true;
57
- if (resolveNext) { const x = resolveNext; resolveNext = null; x(); }
58
- return json;
59
- });
17
+ const modelId = model || 'x-ai/grok-code-fast-1:optimized:free';
18
+ const body = { parts: [{ type: 'text', text: userText }], agent: 'hermes-llm' };
19
+ if (isOpencode) body.model = { providerID: 'kilo', modelID: modelId };
20
+ else { body.providerID = 'kilo'; body.modelID = modelId; }
60
21
 
61
- while (!streamEnded || queue.length) {
62
- if (queue.length) { yield queue.shift(); continue; }
63
- await new Promise(r => { resolveNext = r; });
22
+ let text = '';
23
+ if (isOpencode) {
24
+ const es = new EventSource(base + '/event');
25
+ const textByPart = new Map();
26
+ let done = false;
27
+ const pending = [];
28
+ let resolveNext = null;
29
+ const push = ev => { pending.push(ev); if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } };
30
+ es.onmessage = e => {
31
+ try {
32
+ const m = JSON.parse(e.data);
33
+ if (m.type === 'message.part.updated') {
34
+ const part = m.properties?.part;
35
+ if (part?.sessionID === sessionId && part.type === 'text') {
36
+ const prior = textByPart.get(part.id) || '';
37
+ const txt = part.text || '';
38
+ if (txt.length > prior.length) { push({ type:'text-delta', textDelta: txt.slice(prior.length) }); textByPart.set(part.id, txt); }
39
+ }
40
+ } else if (m.type === 'message.updated') {
41
+ const info = m.properties?.info;
42
+ if (info?.sessionID === sessionId && info.role === 'assistant' && info.time?.completed) {
43
+ done = true; if (resolveNext) { const r = resolveNext; resolveNext = null; r(); }
44
+ }
45
+ }
46
+ } catch (_) {}
47
+ };
48
+ const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
49
+ window.__debug[dbgKey].lastStatus = msgRes.status;
50
+ if (!msgRes.ok) { es.close(); throw new Error('message ' + msgRes.status + ': ' + await msgRes.text()); }
51
+ const deadline = Date.now() + 180000;
52
+ while (!done || pending.length) {
53
+ if (pending.length) { const ev = pending.shift(); if (ev.type === 'text-delta') text += ev.textDelta; yield ev; continue; }
54
+ if (Date.now() > deadline) break;
55
+ await new Promise(r => { resolveNext = r; setTimeout(r, 5000); });
56
+ }
57
+ es.close();
58
+ } else {
59
+ const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
60
+ window.__debug[dbgKey].lastStatus = msgRes.status;
61
+ if (!msgRes.ok) throw new Error('message ' + msgRes.status + ': ' + await msgRes.text());
62
+ const result = await msgRes.json();
63
+ window.__debug[dbgKey].lastResult = result;
64
+ for (const tp of (result.parts || []).filter(p => p.type === 'text')) { text += tp.text; yield { type: 'text-delta', textDelta: tp.text }; }
64
65
  }
65
- const result = await msgPromise;
66
- es.close();
67
- const touched = [...toolState.values()].filter(v => v.completed && (v.name === 'write' || v.name === 'edit')).map(v => v.args.filePath).filter(Boolean);
68
- const mirrored = await mirrorFromSandbox(fsBase, touched);
69
- window.__debug.kilo.writes = mirrored;
70
- if (mirrored.length) window.refreshPreview?.();
71
- yield { type: 'finish-step', finishReason: result.info?.finish || 'stop' };
66
+ yield { type: 'finish-step', finishReason: 'stop' };
72
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.83",
3
+ "version": "1.2.84",
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
  "scripts": {
6
6
  "start": "node serve.js"
package/start-kilo.js CHANGED
@@ -1,45 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  const { spawn } = require('child_process');
3
- const http = require('http');
4
3
  const fs = require('fs');
5
4
  const path = require('path');
6
5
  const os = require('os');
7
6
  const args = process.argv.slice(2);
8
7
  const get = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
9
- const kiloPort = get('--port', '4780');
10
- const fsPort = get('--fs-port', '4781');
8
+ const kiloPort = get('--kilo-port', '4780');
9
+ const ocPort = get('--opencode-port', '4790');
11
10
  const origin = get('--origin', 'http://localhost:8787');
12
11
  const sandbox = path.resolve(get('--sandbox', '.sandbox'));
13
12
  fs.mkdirSync(sandbox, { recursive: true });
14
- const kiloWin = process.env.USERPROFILE + '\\AppData\\Roaming\\npm\\node_modules\\@kilocode\\cli\\node_modules\\@kilocode\\cli-windows-x64\\bin\\kilo.exe';
15
- const bin = os.platform() === 'win32' && fs.existsSync(kiloWin) ? kiloWin : 'kilo';
16
- const kilo = spawn(bin, ['serve', '--port', kiloPort, '--hostname', '127.0.0.1', '--cors', origin], { stdio: 'inherit', env: process.env, cwd: sandbox });
17
- const cors = { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Methods': 'GET,OPTIONS', 'Access-Control-Allow-Headers': 'content-type' };
18
- const srv = http.createServer((req, res) => {
19
- if (req.method === 'OPTIONS') { res.writeHead(204, cors); res.end(); return; }
20
- const rel = decodeURIComponent(req.url.replace(/^\/+/, '').split('?')[0]);
21
- if (rel === '__list') {
22
- const out = [];
23
- const walk = d => { for (const e of fs.readdirSync(d, { withFileTypes: true })) {
24
- if (e.name.startsWith('.')) continue;
25
- const full = path.join(d, e.name);
26
- if (e.isDirectory()) walk(full);
27
- else out.push(path.relative(sandbox, full).replace(/\\/g, '/'));
28
- }};
29
- try { walk(sandbox); } catch (e) {}
30
- res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
31
- res.end(JSON.stringify(out));
32
- return;
33
- }
34
- const full = path.resolve(path.join(sandbox, rel));
35
- if (!full.startsWith(sandbox)) { res.writeHead(403, cors); res.end('forbidden'); return; }
36
- fs.readFile(full, (err, data) => {
37
- if (err) { res.writeHead(404, cors); res.end('not found'); return; }
38
- const ct = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.md': 'text/plain' }[path.extname(rel)] || 'application/octet-stream';
39
- res.writeHead(200, { ...cors, 'Content-Type': ct });
40
- res.end(data);
41
- });
42
- });
43
- srv.listen(fsPort, '127.0.0.1', () => console.log('[fs-bridge] sandbox=' + sandbox + ' serving http://127.0.0.1:' + fsPort));
44
- kilo.on('exit', c => { srv.close(); process.exit(c || 0); });
45
- process.on('SIGINT', () => { kilo.kill(); srv.close(); });
13
+ try { fs.writeFileSync(path.join(sandbox, '.gitignore'), '*\n!.gitignore\n'); } catch (e) {}
14
+
15
+ const isWin = os.platform() === 'win32';
16
+ const kiloBin = isWin ? process.env.USERPROFILE + '\\AppData\\Roaming\\npm\\node_modules\\@kilocode\\cli\\node_modules\\@kilocode\\cli-windows-x64\\bin\\kilo.exe' : 'kilo';
17
+ const ocBin = isWin ? process.env.USERPROFILE + '\\AppData\\Roaming\\npm\\node_modules\\opencode-windows-x64\\bin\\opencode.exe' : 'opencode';
18
+
19
+ const procs = [];
20
+ const launch = (name, bin, port) => {
21
+ if (!args.includes('--no-' + name) && fs.existsSync(bin)) {
22
+ const p = spawn(bin, ['serve', '--port', port, '--hostname', '127.0.0.1', '--cors', origin], { stdio: 'inherit', env: process.env, cwd: sandbox });
23
+ procs.push(p);
24
+ console.log(`[${name}] serve --port ${port} pid ${p.pid}`);
25
+ } else if (!fs.existsSync(bin)) console.log(`[${name}] skip (${bin} not found)`);
26
+ };
27
+ launch('kilo', kiloBin, kiloPort);
28
+ launch('opencode', ocBin, ocPort);
29
+
30
+ const stop = () => { for (const p of procs) { try { p.kill(); } catch (e) {} } process.exit(0); };
31
+ process.on('SIGINT', stop);
32
+ process.on('SIGTERM', stop);
33
+ Promise.all(procs.map(p => new Promise(r => p.on('exit', r)))).then(() => process.exit(0));
@@ -1,15 +0,0 @@
1
- export async function mirrorFromSandbox(fsBase, _touchedPaths) {
2
- const listRes = await fetch(fsBase + '/__list');
3
- if (!listRes.ok) return [];
4
- const relFiles = await listRes.json();
5
- const snap = window.__debug.idbSnapshot || (window.__debug.idbSnapshot = {});
6
- const mirrored = [];
7
- for (const rel of relFiles) {
8
- const r = await fetch(fsBase + '/' + rel);
9
- if (!r.ok) continue;
10
- const content = await r.text();
11
- if (snap[rel] !== content) { snap[rel] = content; mirrored.push(rel); }
12
- }
13
- if (mirrored.length) { window.__debug.idbPersist?.(); window.__debug.shell?.onPreviewWrite?.(); }
14
- return mirrored;
15
- }