thebird 1.2.83 → 1.2.85

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
@@ -95,6 +95,21 @@ class BirdChat extends HTMLElement {
95
95
  this.setState({ providerType: type, baseUrl, model, models: [], apiKey: localStorage.getItem('provider_api_key') || '' });
96
96
  }
97
97
 
98
+ renderBaseUrlInput() {
99
+ const { providerType, baseUrl } = this.state;
100
+ if (providerType !== 'custom' && providerType !== 'kilo' && providerType !== 'opencode') return null;
101
+ const ph = providerType === 'kilo' ? 'http://localhost:4780' : (providerType === 'opencode' ? 'http://localhost:4790' : 'https://your-endpoint/v1');
102
+ return html`<input type="text" class="tui-input" style="flex:1;min-width:140px" placeholder=${ph} value=${baseUrl}
103
+ onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />`;
104
+ }
105
+ renderApiKeyInput() {
106
+ const { providerType, apiKey } = this.state;
107
+ if (providerType === 'kilo' || providerType === 'opencode') return null;
108
+ const provDef = PROVIDERS[providerType] || PROVIDERS.custom;
109
+ return html`<input id="api-key-input" type="password" class="tui-input" style="flex:1;min-width:120px" placeholder=${provDef.keyPlaceholder} value=${apiKey}
110
+ onchange=${e => { const v = e.target.value.trim(); localStorage.setItem('provider_api_key', v); this.setState({ apiKey: v }); if (v) this.loadModels(); }} />`;
111
+ }
112
+
98
113
  render() {
99
114
  const { messages, streaming, model, apiKey, models, modelsLoading, status, providerType, baseUrl, streamingText } = this.state;
100
115
  const provDef = PROVIDERS[providerType] || PROVIDERS.custom;
@@ -108,18 +123,8 @@ class BirdChat extends HTMLElement {
108
123
  <div class="tui-toolbar">
109
124
  <label>provider:</label>
110
125
  <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
- }} />` : ''}
126
+ ${this.renderBaseUrlInput()}
127
+ ${this.renderApiKeyInput()}
123
128
  ${modelsLoading
124
129
  ? html`<span class="tui-spinner"></span>`
125
130
  : html`<select class="tui-select" value=${model}
@@ -129,7 +134,7 @@ class BirdChat extends HTMLElement {
129
134
 
130
135
  <div id="msg-list" class="tui-msglist">
131
136
  ${messages.map((m, i) => html`
132
- <div key=${i} class=${'tui-msg ' + m.role}>${m.content}</div>`)}
137
+ <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
138
  ${streaming && !streamingText && html`<div class="tui-msg assistant"><span class="tui-spinner"></span> thinking...</div>`}
134
139
  </div>
135
140
 
@@ -152,7 +157,7 @@ class BirdChat extends HTMLElement {
152
157
  const text = input?.value.trim();
153
158
  if (!text || this.state.streaming) return;
154
159
  const { apiKey, model, providerType, baseUrl } = this.state;
155
- if (!apiKey && providerType !== 'kilo') { this.setState({ status: 'Enter an API key above.' }); return; }
160
+ if (!apiKey && providerType !== 'kilo' && providerType !== 'opencode') { this.setState({ status: 'Enter an API key above.' }); return; }
156
161
  input.value = '';
157
162
  input.style.height = 'auto';
158
163
  const normalizedMessages = [...this.state.messages, { role: 'user', content: text }].map(m => ({
@@ -1,15 +1,17 @@
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;
1
+ export async function mirrorFromSandbox(fsBase) {
2
+ try {
3
+ const listRes = await fetch(fsBase + '/__list');
4
+ if (!listRes.ok) return [];
5
+ const relFiles = await listRes.json();
6
+ const snap = window.__debug.idbSnapshot || (window.__debug.idbSnapshot = {});
7
+ const mirrored = [];
8
+ for (const rel of relFiles) {
9
+ const r = await fetch(fsBase + '/' + rel);
10
+ if (!r.ok) continue;
11
+ const content = await r.text();
12
+ if (snap[rel] !== content) { snap[rel] = content; mirrored.push(rel); }
13
+ }
14
+ if (mirrored.length) { window.__debug.idbPersist?.(); window.__debug.shell?.onPreviewWrite?.(); }
15
+ return mirrored;
16
+ } catch (e) { return []; }
15
17
  }
@@ -1,72 +1,76 @@
1
1
  import { mirrorFromSandbox } from './kilo-fs-mirror.js';
2
2
 
3
- export async function* streamKiloHTTP({ url, model, messages }) {
3
+ export async function* streamKiloHTTP({ url, model, messages, providerType, agent }) {
4
4
  yield { type: 'start-step' };
5
5
  const base = (url || 'http://localhost:4780').replace(/\/$/, '');
6
6
  const fsBase = base.replace(/:\d+$/, ':4781');
7
+ const isOpencode = providerType === 'opencode';
8
+ const dbgKey = isOpencode ? 'opencode' : 'kilo';
7
9
  let sessRes;
8
10
  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());
11
+ catch (e) { throw new Error(dbgKey + ' serve not reachable at ' + base + ' — start it with: node start-kilo.js --origin ' + location.origin); }
12
+ if (!sessRes.ok) throw new Error('/session ' + sessRes.status + ': ' + await sessRes.text());
11
13
  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
- };
14
+ Object.assign(window.__debug = window.__debug || {}, { [dbgKey]: { sessionId, url: base, fsBase, lastStatus: null } });
46
15
 
47
16
  const userText = messages.filter(m => m.role === 'user').map(m =>
48
17
  typeof m.content === 'string' ? m.content : (m.content || []).filter(b => b.type === 'text').map(b => b.text).join('')
49
18
  ).join('\n');
50
19
 
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
- });
20
+ const modelId = model || 'x-ai/grok-code-fast-1:optimized:free';
21
+ const codingIntent = /\b(write|create|make|build|generate|save|file|html|css|script|app|page|code)\b/i.test(userText);
22
+ const agentName = agent || (codingIntent ? 'build' : 'hermes-llm');
23
+ const body = { parts: [{ type: 'text', text: userText }], agent: agentName };
24
+ if (isOpencode) body.model = { providerID: 'kilo', modelID: modelId };
25
+ else { body.providerID = 'kilo'; body.modelID = modelId; }
60
26
 
61
- while (!streamEnded || queue.length) {
62
- if (queue.length) { yield queue.shift(); continue; }
63
- await new Promise(r => { resolveNext = r; });
27
+ let text = '';
28
+ if (isOpencode) {
29
+ const es = new EventSource(base + '/event');
30
+ const textByPart = new Map();
31
+ const assistantMsgs = new Set();
32
+ let done = false;
33
+ const pending = [];
34
+ let resolveNext = null;
35
+ const push = ev => { pending.push(ev); if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } };
36
+ es.onmessage = e => {
37
+ try {
38
+ const m = JSON.parse(e.data);
39
+ if (m.type === 'message.updated') {
40
+ const info = m.properties?.info;
41
+ if (info?.sessionID === sessionId && info.role === 'assistant') {
42
+ assistantMsgs.add(info.id);
43
+ if (info.time?.completed) { done = true; if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } }
44
+ }
45
+ } else if (m.type === 'message.part.updated') {
46
+ const part = m.properties?.part;
47
+ if (part?.sessionID === sessionId && part.type === 'text' && assistantMsgs.has(part.messageID)) {
48
+ const prior = textByPart.get(part.id) || '';
49
+ const txt = part.text || '';
50
+ if (txt.length > prior.length) { push({ type:'text-delta', textDelta: txt.slice(prior.length) }); textByPart.set(part.id, txt); }
51
+ }
52
+ }
53
+ } catch (_) {}
54
+ };
55
+ const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
56
+ window.__debug[dbgKey].lastStatus = msgRes.status;
57
+ if (!msgRes.ok) { es.close(); throw new Error('message ' + msgRes.status + ': ' + await msgRes.text()); }
58
+ const deadline = Date.now() + 180000;
59
+ while (!done || pending.length) {
60
+ if (pending.length) { const ev = pending.shift(); if (ev.type === 'text-delta') text += ev.textDelta; yield ev; continue; }
61
+ if (Date.now() > deadline) break;
62
+ await new Promise(r => { resolveNext = r; setTimeout(r, 5000); });
63
+ }
64
+ es.close();
65
+ } else {
66
+ const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
67
+ window.__debug[dbgKey].lastStatus = msgRes.status;
68
+ if (!msgRes.ok) throw new Error('message ' + msgRes.status + ': ' + await msgRes.text());
69
+ const result = await msgRes.json();
70
+ window.__debug[dbgKey].lastResult = result;
71
+ for (const tp of (result.parts || []).filter(p => p.type === 'text')) { text += tp.text; yield { type: 'text-delta', textDelta: tp.text }; }
64
72
  }
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' };
73
+ const mirrored = await mirrorFromSandbox(fsBase);
74
+ if (mirrored.length) { window.__debug[dbgKey].writes = mirrored; window.refreshPreview?.(); }
75
+ yield { type: 'finish-step', finishReason: 'stop' };
72
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.83",
3
+ "version": "1.2.85",
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
@@ -6,14 +6,29 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const args = process.argv.slice(2);
8
8
  const get = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
9
- const kiloPort = get('--port', '4780');
9
+ const kiloPort = get('--kilo-port', '4780');
10
+ const ocPort = get('--opencode-port', '4790');
10
11
  const fsPort = get('--fs-port', '4781');
11
12
  const origin = get('--origin', 'http://localhost:8787');
12
13
  const sandbox = path.resolve(get('--sandbox', '.sandbox'));
13
14
  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 });
15
+ try { fs.writeFileSync(path.join(sandbox, '.gitignore'), '*\n!.gitignore\n'); } catch (e) {}
16
+
17
+ const isWin = os.platform() === 'win32';
18
+ const kiloBin = isWin ? process.env.USERPROFILE + '\\AppData\\Roaming\\npm\\node_modules\\@kilocode\\cli\\node_modules\\@kilocode\\cli-windows-x64\\bin\\kilo.exe' : 'kilo';
19
+ const ocBin = isWin ? process.env.USERPROFILE + '\\AppData\\Roaming\\npm\\node_modules\\opencode-windows-x64\\bin\\opencode.exe' : 'opencode';
20
+
21
+ const procs = [];
22
+ const launch = (name, bin, port) => {
23
+ if (args.includes('--no-' + name)) return;
24
+ if (!fs.existsSync(bin)) { console.log(`[${name}] skip (${bin} not found)`); return; }
25
+ const p = spawn(bin, ['serve', '--port', port, '--hostname', '127.0.0.1', '--cors', origin], { stdio: 'inherit', env: process.env, cwd: sandbox });
26
+ procs.push(p);
27
+ console.log(`[${name}] serve --port ${port} pid ${p.pid}`);
28
+ };
29
+ launch('kilo', kiloBin, kiloPort);
30
+ launch('opencode', ocBin, ocPort);
31
+
17
32
  const cors = { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Methods': 'GET,OPTIONS', 'Access-Control-Allow-Headers': 'content-type' };
18
33
  const srv = http.createServer((req, res) => {
19
34
  if (req.method === 'OPTIONS') { res.writeHead(204, cors); res.end(); return; }
@@ -27,19 +42,18 @@ const srv = http.createServer((req, res) => {
27
42
  else out.push(path.relative(sandbox, full).replace(/\\/g, '/'));
28
43
  }};
29
44
  try { walk(sandbox); } catch (e) {}
30
- res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
31
- res.end(JSON.stringify(out));
32
- return;
45
+ res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify(out)); return;
33
46
  }
34
47
  const full = path.resolve(path.join(sandbox, rel));
35
- if (!full.startsWith(sandbox)) { res.writeHead(403, cors); res.end('forbidden'); return; }
48
+ if (!full.startsWith(sandbox)) { res.writeHead(403, cors); res.end(); return; }
36
49
  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);
50
+ if (err) { res.writeHead(404, cors); res.end(); return; }
51
+ const ct = { '.html':'text/html','.js':'application/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.md':'text/plain','.txt':'text/plain' }[path.extname(rel)] || 'application/octet-stream';
52
+ res.writeHead(200, { ...cors, 'Content-Type': ct }); res.end(data);
41
53
  });
42
54
  });
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(); });
55
+ srv.listen(fsPort, '127.0.0.1', () => console.log(`[fs-bridge] sandbox=${sandbox} serving http://127.0.0.1:${fsPort}`));
56
+
57
+ const stop = () => { try { srv.close(); } catch (e) {} for (const p of procs) { try { p.kill(); } catch (e) {} } process.exit(0); };
58
+ process.on('SIGINT', stop);
59
+ process.on('SIGTERM', stop);