thebird 1.2.75 → 1.2.77
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 +5 -0
- package/docs/app.js +35 -43
- package/docs/index.html +22 -26
- package/docs/tui.css +131 -0
- package/index.js +2 -2
- package/lib/errors.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
- `lib/circuit-breaker.js`: `createCircuitBreaker()` — per-provider failure tracking with auto-recovery after cooldown
|
|
9
9
|
- `lib/capabilities.js`: `getCapabilities()` / `stripUnsupported()` — provider capability metadata with automatic feature stripping and warnings
|
|
10
10
|
- `lib/router-stream.js`: Router logic extracted from index.js — circuit breaker and capability checks integrated
|
|
11
|
+
- `docs/tui.css`: TUI (text user interface) theme — monospace, green-on-black, box-drawing borders, scanline overlay, ASCII spinner
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `docs/index.html`: Restyled to TUI aesthetic — ASCII art header, bracket-style tabs, removed Tailwind/RippleUI dependencies
|
|
15
|
+
- `docs/app.js`: Chat UI uses TUI-styled classes — monospace messages with `> ` / `< ` prefixes, bracket buttons
|
|
11
16
|
|
|
12
17
|
### Changed
|
|
13
18
|
- `index.js`: Trimmed from 177 to 104 lines by extracting router logic to lib/router-stream.js
|
package/docs/app.js
CHANGED
|
@@ -103,51 +103,44 @@ class BirdChat extends HTMLElement {
|
|
|
103
103
|
html`<option value=${id} selected=${id === providerType}>${p.label}</option>`);
|
|
104
104
|
|
|
105
105
|
applyDiff(this, html`
|
|
106
|
-
<div
|
|
107
|
-
<
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<button class="btn btn-sm btn-ghost" onclick=${() => this.setState({ messages: [], status: '' })}>Clear</button>
|
|
131
|
-
</div>
|
|
132
|
-
</header>
|
|
133
|
-
|
|
134
|
-
<div id="msg-list" class="flex-1 overflow-y-auto flex flex-col gap-3 p-4">
|
|
106
|
+
<div style="display:flex;flex-direction:column;height:100%">
|
|
107
|
+
<div class="tui-toolbar">
|
|
108
|
+
<label>provider:</label>
|
|
109
|
+
<select class="tui-select" onchange=${e => this.setProvider(e.target.value)}>${provOpts}</select>
|
|
110
|
+
${(providerType === 'custom' || providerType === 'acp') ? html`
|
|
111
|
+
<input type="text" class="tui-input" style="flex:1;min-width:140px"
|
|
112
|
+
placeholder=${providerType === 'acp' ? 'ws://localhost:3000' : 'https://your-endpoint/v1'} value=${baseUrl}
|
|
113
|
+
onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />` : ''}
|
|
114
|
+
${providerType !== 'acp' ? html`<input id="api-key-input" type="password" class="tui-input" style="flex:1;min-width:120px"
|
|
115
|
+
placeholder=${provDef.keyPlaceholder} value=${apiKey}
|
|
116
|
+
onchange=${e => {
|
|
117
|
+
const v = e.target.value.trim();
|
|
118
|
+
localStorage.setItem('provider_api_key', v);
|
|
119
|
+
this.setState({ apiKey: v });
|
|
120
|
+
if (v) this.loadModels();
|
|
121
|
+
}} />` : ''}
|
|
122
|
+
${modelsLoading
|
|
123
|
+
? html`<span class="tui-spinner"></span>`
|
|
124
|
+
: html`<select class="tui-select" value=${model}
|
|
125
|
+
onchange=${e => { localStorage.setItem('provider_model', e.target.value); this.setState({ model: e.target.value }); }}>${opts}</select>`}
|
|
126
|
+
<button class="tui-btn" onclick=${() => this.setState({ messages: [], status: '' })}>[clear]</button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div id="msg-list" class="tui-msglist">
|
|
135
130
|
${messages.map((m, i) => html`
|
|
136
|
-
<div key=${i} class=${'
|
|
137
|
-
|
|
138
|
-
</div>`)}
|
|
139
|
-
${streaming && !streamingText && html`<div class="flex justify-start"><div class="card bg-base-200 px-4 py-3"><span class="loading loading-dots loading-sm"></span></div></div>`}
|
|
131
|
+
<div key=${i} class=${'tui-msg ' + m.role}>${m.content}</div>`)}
|
|
132
|
+
${streaming && !streamingText && html`<div class="tui-msg assistant"><span class="tui-spinner"></span> thinking...</div>`}
|
|
140
133
|
</div>
|
|
141
134
|
|
|
142
|
-
${status && html`<div class="
|
|
135
|
+
${status && html`<div class="tui-error-text" style="padding:0 1ch">${status}</div>`}
|
|
143
136
|
|
|
144
|
-
<form class="
|
|
145
|
-
<textarea id="chat-input" class="textarea
|
|
146
|
-
placeholder="
|
|
137
|
+
<form class="tui-compose" onsubmit=${e => { e.preventDefault(); this.send(); }}>
|
|
138
|
+
<textarea id="chat-input" class="tui-textarea" style="flex:1;resize:none;min-height:24px;max-height:120px"
|
|
139
|
+
placeholder="type message... (shift+enter for newline)" rows="1" disabled=${streaming}
|
|
147
140
|
onkeydown=${e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.send(); } }}
|
|
148
141
|
oninput=${e => { e.target.style.height = 'auto'; e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; }}></textarea>
|
|
149
|
-
<button type="submit" class="btn
|
|
150
|
-
${streaming ? html`<span class="
|
|
142
|
+
<button type="submit" class="tui-btn primary" disabled=${streaming}>
|
|
143
|
+
${streaming ? html`<span class="tui-spinner"></span>` : '[send]'}
|
|
151
144
|
</button>
|
|
152
145
|
</form>
|
|
153
146
|
</div>`);
|
|
@@ -167,12 +160,11 @@ class BirdChat extends HTMLElement {
|
|
|
167
160
|
try {
|
|
168
161
|
let full = '';
|
|
169
162
|
const streamEl = document.createElement('div');
|
|
170
|
-
streamEl.className = 'msg
|
|
163
|
+
streamEl.className = 'tui-msg assistant';
|
|
171
164
|
const cursor = document.createElement('span');
|
|
172
|
-
cursor.
|
|
173
|
-
cursor.textContent = '
|
|
165
|
+
cursor.style.cssText = 'animation:blink 0.5s step-end infinite';
|
|
166
|
+
cursor.textContent = '█';
|
|
174
167
|
const wrap = document.createElement('div');
|
|
175
|
-
wrap.className = 'flex justify-start';
|
|
176
168
|
wrap.appendChild(streamEl);
|
|
177
169
|
wrap.appendChild(cursor);
|
|
178
170
|
const list = this.querySelector('#msg-list');
|
package/docs/index.html
CHANGED
|
@@ -1,43 +1,38 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<meta name="version" content="1.2.23">
|
|
7
|
-
<title>thebird —
|
|
7
|
+
<title>thebird — TUI</title>
|
|
8
8
|
<script>window.coi = { coepDegrade: () => false };</script>
|
|
9
9
|
<script src="coi-serviceworker.js"></script>
|
|
10
|
-
<link rel="stylesheet" href="vendor/tailwind.css" />
|
|
11
|
-
<link rel="stylesheet" href="vendor/rippleui.css" />
|
|
12
10
|
<link rel="stylesheet" href="vendor/xterm.css" />
|
|
13
|
-
<
|
|
14
|
-
html, body { height: 100%; background: #0f1117; }
|
|
15
|
-
.tab-active { border-bottom: 2px solid oklch(var(--p)); }
|
|
16
|
-
bird-chat { display: flex; flex-direction: column; height: 100%; }
|
|
17
|
-
.msg-bubble { max-width: 680px; white-space: pre-wrap; word-break: break-word; }
|
|
18
|
-
#msg-list { scroll-behavior: smooth; }
|
|
19
|
-
</style>
|
|
11
|
+
<link rel="stylesheet" href="tui.css" />
|
|
20
12
|
</head>
|
|
21
|
-
<body
|
|
22
|
-
<div
|
|
23
|
-
<div class="
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<button id="tab-
|
|
13
|
+
<body>
|
|
14
|
+
<div style="display:flex;flex-direction:column;height:100%">
|
|
15
|
+
<div class="tui-header"><pre class="logo" style="margin:0;padding:2px 1ch"> ▀█▀ █░█ █▀▀ █▄▄ █ █▀█ █▀▄ <span class="ver">v1.2</span>
|
|
16
|
+
█ █▀█ ██▄ █▄█ █ █▀▄ █▄▀ <span style="color:#1a9a1a">anthropic → gemini/openai bridge</span></pre></div>
|
|
17
|
+
<div class="tui-tabs">
|
|
18
|
+
<button id="tab-chat" class="tui-tab active" onclick="switchTab('chat')">Chat</button>
|
|
19
|
+
<button id="tab-term" class="tui-tab" onclick="switchTab('term')">Terminal</button>
|
|
20
|
+
<button id="tab-preview" class="tui-tab" onclick="switchTab('preview')">Preview</button>
|
|
27
21
|
</div>
|
|
28
|
-
<div id="pane-chat"
|
|
22
|
+
<div id="pane-chat" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
|
|
29
23
|
<bird-chat></bird-chat>
|
|
30
24
|
</div>
|
|
31
|
-
<div id="pane-term" class="
|
|
32
|
-
<div id="term-container"
|
|
25
|
+
<div id="pane-term" class="hidden" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
|
|
26
|
+
<div id="term-container" style="flex:1"></div>
|
|
33
27
|
</div>
|
|
34
|
-
<div id="pane-preview" class="
|
|
35
|
-
<div class="
|
|
36
|
-
<button class="
|
|
28
|
+
<div id="pane-preview" class="hidden" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
|
|
29
|
+
<div class="tui-toolbar">
|
|
30
|
+
<button class="tui-btn" onclick="refreshPreview()">[reload]</button>
|
|
37
31
|
</div>
|
|
38
|
-
<iframe id="preview-frame"
|
|
32
|
+
<iframe id="preview-frame" style="width:100%;flex:1;border:0;background:#000" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
|
|
39
33
|
</div>
|
|
40
34
|
</div>
|
|
35
|
+
<div class="tui-scanline"></div>
|
|
41
36
|
<script>
|
|
42
37
|
function callExpressRoute(path, method) {
|
|
43
38
|
const handlers = window.__debug?.shell?.httpHandlers || {};
|
|
@@ -64,12 +59,13 @@ async function refreshPreview() {
|
|
|
64
59
|
if (result) { iframe.srcdoc = result.body; return; }
|
|
65
60
|
const snap = window.__debug?.idbSnapshot || {};
|
|
66
61
|
if (snap['index.html']) { iframe.srcdoc = snap['index.html']; return; }
|
|
67
|
-
iframe.srcdoc = '<
|
|
62
|
+
iframe.srcdoc = '<pre style="color:#33ff33;padding:1ch;font-family:monospace">No express app running.\nRun node app.js in terminal.</pre>';
|
|
68
63
|
}
|
|
69
64
|
function switchTab(t) {
|
|
70
65
|
['chat', 'term', 'preview'].forEach(id => {
|
|
71
66
|
document.getElementById('pane-' + id).classList.toggle('hidden', id !== t);
|
|
72
|
-
document.getElementById('tab-' + id)
|
|
67
|
+
const tab = document.getElementById('tab-' + id);
|
|
68
|
+
tab.classList.toggle('active', id === t);
|
|
73
69
|
});
|
|
74
70
|
if (t === 'preview') refreshPreview();
|
|
75
71
|
}
|
package/docs/tui.css
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--tui-fg: #33ff33;
|
|
3
|
+
--tui-fg-dim: #1a9a1a;
|
|
4
|
+
--tui-fg-bright: #66ff66;
|
|
5
|
+
--tui-bg: #0a0a0a;
|
|
6
|
+
--tui-bg-alt: #111311;
|
|
7
|
+
--tui-border: #33ff33;
|
|
8
|
+
--tui-border-dim: #1a5a1a;
|
|
9
|
+
--tui-accent: #ffcc00;
|
|
10
|
+
--tui-error: #ff3333;
|
|
11
|
+
--tui-user: #00ccff;
|
|
12
|
+
}
|
|
13
|
+
*, *::before, *::after { font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'SF Mono', 'Consolas', monospace !important; }
|
|
14
|
+
html, body { background: var(--tui-bg); color: var(--tui-fg); height: 100%; margin: 0; font-size: 14px; line-height: 1.4; }
|
|
15
|
+
::selection { background: var(--tui-fg); color: var(--tui-bg); }
|
|
16
|
+
.tui-header {
|
|
17
|
+
border-bottom: 1px solid var(--tui-border-dim);
|
|
18
|
+
background: var(--tui-bg);
|
|
19
|
+
padding: 0;
|
|
20
|
+
white-space: pre;
|
|
21
|
+
font-size: 11px;
|
|
22
|
+
line-height: 1.2;
|
|
23
|
+
color: var(--tui-fg-dim);
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
26
|
+
.tui-header .logo { color: var(--tui-fg-bright); }
|
|
27
|
+
.tui-header .ver { color: var(--tui-accent); }
|
|
28
|
+
.tui-tabs {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 0;
|
|
31
|
+
background: var(--tui-bg);
|
|
32
|
+
border-bottom: 1px solid var(--tui-border-dim);
|
|
33
|
+
padding: 0 1ch;
|
|
34
|
+
}
|
|
35
|
+
.tui-tab {
|
|
36
|
+
background: none;
|
|
37
|
+
border: 1px solid var(--tui-border-dim);
|
|
38
|
+
border-bottom: none;
|
|
39
|
+
color: var(--tui-fg-dim);
|
|
40
|
+
padding: 2px 2ch;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
font-size: 13px;
|
|
43
|
+
margin-bottom: -1px;
|
|
44
|
+
position: relative;
|
|
45
|
+
}
|
|
46
|
+
.tui-tab:hover { color: var(--tui-fg); }
|
|
47
|
+
.tui-tab.active {
|
|
48
|
+
color: var(--tui-accent);
|
|
49
|
+
border-color: var(--tui-border);
|
|
50
|
+
background: var(--tui-bg-alt);
|
|
51
|
+
border-bottom: 1px solid var(--tui-bg-alt);
|
|
52
|
+
}
|
|
53
|
+
.tui-input, .tui-select, .tui-textarea {
|
|
54
|
+
background: var(--tui-bg);
|
|
55
|
+
border: 1px solid var(--tui-border-dim);
|
|
56
|
+
color: var(--tui-fg);
|
|
57
|
+
padding: 2px 1ch;
|
|
58
|
+
font-size: 13px;
|
|
59
|
+
outline: none;
|
|
60
|
+
}
|
|
61
|
+
.tui-input:focus, .tui-select:focus, .tui-textarea:focus {
|
|
62
|
+
border-color: var(--tui-border);
|
|
63
|
+
box-shadow: 0 0 4px rgba(51, 255, 51, 0.15);
|
|
64
|
+
}
|
|
65
|
+
.tui-select { appearance: none; padding-right: 2ch; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%2333ff33'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.5ch center; }
|
|
66
|
+
.tui-btn {
|
|
67
|
+
background: var(--tui-bg);
|
|
68
|
+
border: 1px solid var(--tui-border-dim);
|
|
69
|
+
color: var(--tui-fg);
|
|
70
|
+
padding: 2px 2ch;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
font-size: 13px;
|
|
73
|
+
}
|
|
74
|
+
.tui-btn:hover { border-color: var(--tui-border); color: var(--tui-fg-bright); }
|
|
75
|
+
.tui-btn.primary { border-color: var(--tui-accent); color: var(--tui-accent); }
|
|
76
|
+
.tui-btn.primary:hover { background: var(--tui-accent); color: var(--tui-bg); }
|
|
77
|
+
.tui-msg {
|
|
78
|
+
max-width: 72ch;
|
|
79
|
+
padding: 4px 1ch;
|
|
80
|
+
white-space: pre-wrap;
|
|
81
|
+
word-break: break-word;
|
|
82
|
+
font-size: 13px;
|
|
83
|
+
line-height: 1.4;
|
|
84
|
+
}
|
|
85
|
+
.tui-msg.user { color: var(--tui-user); }
|
|
86
|
+
.tui-msg.user::before { content: '> '; color: var(--tui-user); }
|
|
87
|
+
.tui-msg.assistant { color: var(--tui-fg); }
|
|
88
|
+
.tui-msg.assistant::before { content: '< '; color: var(--tui-fg-dim); }
|
|
89
|
+
.tui-status { color: var(--tui-accent); font-size: 12px; }
|
|
90
|
+
.tui-error-text { color: var(--tui-error); font-size: 12px; }
|
|
91
|
+
.tui-spinner::after { content: '⠋'; animation: tui-spin 0.6s steps(6) infinite; }
|
|
92
|
+
@keyframes tui-spin {
|
|
93
|
+
0% { content: '⠋'; } 16% { content: '⠙'; } 33% { content: '⠹'; }
|
|
94
|
+
50% { content: '⠸'; } 66% { content: '⠼'; } 83% { content: '⠴'; }
|
|
95
|
+
}
|
|
96
|
+
.tui-toolbar {
|
|
97
|
+
display: flex;
|
|
98
|
+
gap: 1ch;
|
|
99
|
+
padding: 2px 1ch;
|
|
100
|
+
border-bottom: 1px solid var(--tui-border-dim);
|
|
101
|
+
background: var(--tui-bg);
|
|
102
|
+
align-items: center;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
font-size: 13px;
|
|
105
|
+
}
|
|
106
|
+
.tui-toolbar label { color: var(--tui-fg-dim); font-size: 12px; }
|
|
107
|
+
.tui-msglist {
|
|
108
|
+
flex: 1;
|
|
109
|
+
overflow-y: auto;
|
|
110
|
+
padding: 1ch;
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
gap: 4px;
|
|
114
|
+
}
|
|
115
|
+
.tui-compose {
|
|
116
|
+
display: flex;
|
|
117
|
+
gap: 1ch;
|
|
118
|
+
padding: 4px 1ch;
|
|
119
|
+
border-top: 1px solid var(--tui-border-dim);
|
|
120
|
+
background: var(--tui-bg);
|
|
121
|
+
}
|
|
122
|
+
.tui-scanline {
|
|
123
|
+
pointer-events: none;
|
|
124
|
+
position: fixed;
|
|
125
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
126
|
+
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px);
|
|
127
|
+
z-index: 9999;
|
|
128
|
+
}
|
|
129
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
130
|
+
#pane-term { background: #000; }
|
|
131
|
+
.hidden { display: none !important; }
|
package/index.js
CHANGED
|
@@ -98,6 +98,6 @@ async function generateGemini({ model, system, messages, tools, apiKey, temperat
|
|
|
98
98
|
const { streamRouter, generateRouter, createRouter } = require('./lib/router-stream');
|
|
99
99
|
const { cloudGenerate, streamCloud, cloudStream } = require('./lib/cloud-generate');
|
|
100
100
|
const { ensureAuth, login: oauthLogin } = require('./lib/oauth');
|
|
101
|
-
const { BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError, classifyError } = require('./lib/errors');
|
|
101
|
+
const { BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError, classifyError, redactKeys } = require('./lib/errors');
|
|
102
102
|
|
|
103
|
-
module.exports = { streamGemini, createFullStream, generateGemini, streamRouter, generateRouter, createRouter, convertMessages, convertTools, cleanSchema, GeminiError, BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError, classifyError, cloudGenerate, streamCloud, cloudStream, ensureAuth, oauthLogin };
|
|
103
|
+
module.exports = { streamGemini, createFullStream, generateGemini, streamRouter, generateRouter, createRouter, convertMessages, convertTools, cleanSchema, GeminiError, BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError, classifyError, redactKeys, cloudGenerate, streamCloud, cloudStream, ensureAuth, oauthLogin };
|
package/lib/errors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const KEY_PATTERNS = [
|
|
2
2
|
/\b(AIza[A-Za-z0-9_-]{20,})/g,
|
|
3
|
-
/\b(sk-[A-Za-z0-
|
|
4
|
-
/\b(key-[A-Za-z0-
|
|
3
|
+
/\b(sk-[A-Za-z0-9_-]{20,})/g,
|
|
4
|
+
/\b(key-[A-Za-z0-9_-]{20,})/g,
|
|
5
5
|
/((?:api[_-]?key|token|secret|authorization|bearer)[=:\s"']+)([A-Za-z0-9_-]{20,})/gi,
|
|
6
6
|
];
|
|
7
7
|
|
package/package.json
CHANGED