bloby-bot 0.46.3 → 0.47.1
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/bin/cli.js +42 -8
- package/dist-bloby/assets/{bloby-BnHElaWD.js → bloby-E-QLmQDW.js} +4 -4
- package/dist-bloby/assets/globals-Ci0CEj1X.js +18 -0
- package/dist-bloby/assets/globals-DriF_8Q_.css +2 -0
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-B4IKFiNq.js → highlighted-body-OFNGDK62-CTiboTVa.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-CgVqYCFU.js +1 -0
- package/dist-bloby/assets/{onboard-DoRN5jiz.js → onboard-C1uMxuk2.js} +1 -1
- package/dist-bloby/bloby.html +3 -3
- package/dist-bloby/onboard.html +3 -3
- package/package.json +3 -2
- package/scripts/postinstall.js +19 -2
- package/scripts/sync-pi-models.ts +146 -0
- package/shared/config.ts +1 -1
- package/supervisor/bloby-agent.ts +2 -0
- package/supervisor/chat/OnboardWizard.tsx +327 -2
- package/supervisor/harnesses/pi/async-queue.ts +45 -0
- package/supervisor/harnesses/pi/auth-storage.ts +56 -0
- package/supervisor/harnesses/pi/index.ts +474 -0
- package/supervisor/harnesses/pi/models-catalog.generated.ts +579 -0
- package/supervisor/harnesses/pi/providers/stream-google.ts +156 -0
- package/supervisor/harnesses/pi/providers/stream.ts +21 -0
- package/supervisor/harnesses/pi/providers/types.ts +60 -0
- package/supervisor/harnesses/pi/session.ts +140 -0
- package/supervisor/harnesses/pi/sub-providers.ts +191 -0
- package/supervisor/harnesses/pi/test-completion.ts +196 -0
- package/supervisor/index.ts +6 -0
- package/worker/index.ts +86 -0
- package/dist-bloby/assets/globals-BYieEOqL.js +0 -18
- package/dist-bloby/assets/globals-BzeCWV3t.css +0 -2
- package/dist-bloby/assets/mermaid-GHXKKRXX-32SDjrR3.js +0 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi test-completion — single-shot, non-streaming completion call.
|
|
3
|
+
*
|
|
4
|
+
* Iteration 1 of the pi harness: just enough to verify the saved sub-provider
|
|
5
|
+
* + credentials actually reach an LLM and return text. Replaces the full
|
|
6
|
+
* pi-ai streaming stack until we vendor it alongside the agent loop.
|
|
7
|
+
*
|
|
8
|
+
* Supported API flavors:
|
|
9
|
+
* - openai-completions → POST {baseUrl}/chat/completions
|
|
10
|
+
* - anthropic-messages → POST {baseUrl}/messages
|
|
11
|
+
* - google-gemini → POST {baseUrl}/models/{modelId}:generateContent
|
|
12
|
+
*/
|
|
13
|
+
import { getPiSubProvider, type PiApiFlavor } from './sub-providers.js';
|
|
14
|
+
|
|
15
|
+
export interface PiTestCompletionInput {
|
|
16
|
+
subProvider: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
modelId?: string;
|
|
20
|
+
prompt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PiTestCompletionResult {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
text?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
status?: number;
|
|
28
|
+
modelId?: string;
|
|
29
|
+
subProvider?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
33
|
+
|
|
34
|
+
async function timedFetch(url: string, init: RequestInit): Promise<Response> {
|
|
35
|
+
const ctl = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => ctl.abort(), REQUEST_TIMEOUT_MS);
|
|
37
|
+
try {
|
|
38
|
+
return await fetch(url, { ...init, signal: ctl.signal });
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pickBaseUrl(input: PiTestCompletionInput): string | undefined {
|
|
45
|
+
if (input.baseUrl?.trim()) return input.baseUrl.replace(/\/+$/, '');
|
|
46
|
+
const def = getPiSubProvider(input.subProvider)?.baseUrl;
|
|
47
|
+
return def?.replace(/\/+$/, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pickModelId(input: PiTestCompletionInput): string | undefined {
|
|
51
|
+
if (input.modelId?.trim()) return input.modelId.trim();
|
|
52
|
+
return getPiSubProvider(input.subProvider)?.defaultModel;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runPiTestCompletion(input: PiTestCompletionInput): Promise<PiTestCompletionResult> {
|
|
56
|
+
const provider = getPiSubProvider(input.subProvider);
|
|
57
|
+
if (!provider) {
|
|
58
|
+
return { ok: false, error: `Unknown sub-provider: ${input.subProvider}` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const baseUrl = pickBaseUrl(input);
|
|
62
|
+
if (!baseUrl) return { ok: false, error: 'Missing base URL' };
|
|
63
|
+
|
|
64
|
+
const modelId = pickModelId(input);
|
|
65
|
+
if (!modelId) return { ok: false, error: 'Missing model ID' };
|
|
66
|
+
|
|
67
|
+
if (provider.needsApiKey && !input.apiKey?.trim()) {
|
|
68
|
+
return { ok: false, error: 'Missing API key' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const text = await callByFlavor(provider.flavor, {
|
|
73
|
+
baseUrl,
|
|
74
|
+
modelId,
|
|
75
|
+
apiKey: input.apiKey?.trim() || '',
|
|
76
|
+
prompt: input.prompt,
|
|
77
|
+
});
|
|
78
|
+
return { ok: true, text, modelId, subProvider: provider.id };
|
|
79
|
+
} catch (err: any) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: err?.message || String(err),
|
|
83
|
+
status: err?.status,
|
|
84
|
+
modelId,
|
|
85
|
+
subProvider: provider.id,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface DispatchArgs {
|
|
91
|
+
baseUrl: string;
|
|
92
|
+
modelId: string;
|
|
93
|
+
apiKey: string;
|
|
94
|
+
prompt: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function callByFlavor(flavor: PiApiFlavor, args: DispatchArgs): Promise<string> {
|
|
98
|
+
switch (flavor) {
|
|
99
|
+
case 'openai-completions':
|
|
100
|
+
return callOpenAICompletions(args);
|
|
101
|
+
case 'anthropic-messages':
|
|
102
|
+
return callAnthropicMessages(args);
|
|
103
|
+
case 'google-gemini':
|
|
104
|
+
return callGoogleGemini(args);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ── OpenAI / OpenAI-compatible ── */
|
|
109
|
+
|
|
110
|
+
async function callOpenAICompletions({ baseUrl, modelId, apiKey, prompt }: DispatchArgs): Promise<string> {
|
|
111
|
+
const headers: Record<string, string> = { 'content-type': 'application/json' };
|
|
112
|
+
if (apiKey) headers['authorization'] = `Bearer ${apiKey}`;
|
|
113
|
+
|
|
114
|
+
const res = await timedFetch(`${baseUrl}/chat/completions`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers,
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
model: modelId,
|
|
119
|
+
messages: [{ role: 'user', content: prompt }],
|
|
120
|
+
max_tokens: 256,
|
|
121
|
+
stream: false,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!res.ok) throw await httpError(res);
|
|
126
|
+
|
|
127
|
+
const body: any = await res.json();
|
|
128
|
+
const text = body?.choices?.[0]?.message?.content;
|
|
129
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
130
|
+
throw new Error(`Empty response (${JSON.stringify(body).slice(0, 200)})`);
|
|
131
|
+
}
|
|
132
|
+
return text.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* ── Anthropic Messages API ── */
|
|
136
|
+
|
|
137
|
+
async function callAnthropicMessages({ baseUrl, modelId, apiKey, prompt }: DispatchArgs): Promise<string> {
|
|
138
|
+
const res = await timedFetch(`${baseUrl}/messages`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
'content-type': 'application/json',
|
|
142
|
+
'x-api-key': apiKey,
|
|
143
|
+
'anthropic-version': '2023-06-01',
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
model: modelId,
|
|
147
|
+
max_tokens: 256,
|
|
148
|
+
messages: [{ role: 'user', content: prompt }],
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!res.ok) throw await httpError(res);
|
|
153
|
+
|
|
154
|
+
const body: any = await res.json();
|
|
155
|
+
const block = Array.isArray(body?.content)
|
|
156
|
+
? body.content.find((b: any) => b?.type === 'text')
|
|
157
|
+
: null;
|
|
158
|
+
const text = block?.text;
|
|
159
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
160
|
+
throw new Error(`Empty response (${JSON.stringify(body).slice(0, 200)})`);
|
|
161
|
+
}
|
|
162
|
+
return text.trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* ── Google Gemini ── */
|
|
166
|
+
|
|
167
|
+
async function callGoogleGemini({ baseUrl, modelId, apiKey, prompt }: DispatchArgs): Promise<string> {
|
|
168
|
+
const url = `${baseUrl}/models/${encodeURIComponent(modelId)}:generateContent?key=${encodeURIComponent(apiKey)}`;
|
|
169
|
+
const res = await timedFetch(url, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'content-type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
174
|
+
generationConfig: { maxOutputTokens: 256 },
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!res.ok) throw await httpError(res);
|
|
179
|
+
|
|
180
|
+
const body: any = await res.json();
|
|
181
|
+
const parts: any[] = body?.candidates?.[0]?.content?.parts || [];
|
|
182
|
+
const text = parts.map((p) => p?.text).filter(Boolean).join('\n').trim();
|
|
183
|
+
if (!text) throw new Error(`Empty response (${JSON.stringify(body).slice(0, 200)})`);
|
|
184
|
+
return text;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ── Helpers ── */
|
|
188
|
+
|
|
189
|
+
async function httpError(res: Response): Promise<Error> {
|
|
190
|
+
let detail = '';
|
|
191
|
+
try { detail = await res.text(); } catch {}
|
|
192
|
+
const trimmed = detail.length > 400 ? `${detail.slice(0, 400)}…` : detail;
|
|
193
|
+
const err: any = new Error(`HTTP ${res.status} ${res.statusText}${trimmed ? `: ${trimmed}` : ''}`);
|
|
194
|
+
err.status = res.status;
|
|
195
|
+
return err;
|
|
196
|
+
}
|
package/supervisor/index.ts
CHANGED
|
@@ -351,6 +351,12 @@ export async function startSupervisor() {
|
|
|
351
351
|
'POST /api/auth/codex/start',
|
|
352
352
|
'POST /api/auth/codex/cancel',
|
|
353
353
|
'GET /api/auth/codex/status',
|
|
354
|
+
'GET /api/auth/pi/providers',
|
|
355
|
+
'GET /api/auth/pi/status',
|
|
356
|
+
'POST /api/auth/pi/test',
|
|
357
|
+
'POST /api/auth/pi/save',
|
|
358
|
+
'DELETE /api/auth/pi',
|
|
359
|
+
'POST /api/auth/pi/completion',
|
|
354
360
|
'POST /api/portal/totp/setup',
|
|
355
361
|
'POST /api/portal/totp/verify-setup',
|
|
356
362
|
'POST /api/portal/totp/disable',
|
package/worker/index.ts
CHANGED
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
import { startClaudeOAuth, exchangeClaudeCode, getClaudeAuthStatus, readClaudeAccessToken } from './claude-auth.js';
|
|
17
17
|
import { checkAvailability, registerHandle, claimReservedHandle, releaseHandle, updateTunnelUrl, startHeartbeat, stopHeartbeat } from '../shared/relay.js';
|
|
18
18
|
import { ensureFileDirs } from '../supervisor/file-saver.js';
|
|
19
|
+
import { readPiAuth, writePiAuth, clearPiAuth, getPiAuthStatus } from '../supervisor/harnesses/pi/auth-storage.js';
|
|
20
|
+
import { runPiTestCompletion } from '../supervisor/harnesses/pi/test-completion.js';
|
|
21
|
+
import { PI_SUB_PROVIDERS, getPiSubProvider } from '../supervisor/harnesses/pi/sub-providers.js';
|
|
19
22
|
|
|
20
23
|
// ── Password hashing (scrypt) ──
|
|
21
24
|
|
|
@@ -228,6 +231,89 @@ app.get('/api/auth/claude/status', async (_req, res) => {
|
|
|
228
231
|
res.json(await getClaudeAuthStatus());
|
|
229
232
|
});
|
|
230
233
|
|
|
234
|
+
// ── Pi (Bloby third harness) auth routes ──
|
|
235
|
+
|
|
236
|
+
app.get('/api/auth/pi/providers', (_req, res) => {
|
|
237
|
+
res.json({
|
|
238
|
+
providers: PI_SUB_PROVIDERS.map((p) => ({
|
|
239
|
+
id: p.id,
|
|
240
|
+
name: p.name,
|
|
241
|
+
subtitle: p.subtitle,
|
|
242
|
+
flavor: p.flavor,
|
|
243
|
+
baseUrl: p.baseUrl,
|
|
244
|
+
needsBaseUrl: !!p.needsBaseUrl,
|
|
245
|
+
needsApiKey: p.needsApiKey !== false,
|
|
246
|
+
apiKeyUrl: p.apiKeyUrl,
|
|
247
|
+
models: p.models,
|
|
248
|
+
defaultModel: p.defaultModel,
|
|
249
|
+
})),
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
app.get('/api/auth/pi/status', (_req, res) => {
|
|
254
|
+
res.json(getPiAuthStatus());
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
app.post('/api/auth/pi/test', async (req, res) => {
|
|
258
|
+
const { subProvider, apiKey, baseUrl, modelId } = req.body || {};
|
|
259
|
+
if (!subProvider || typeof subProvider !== 'string') {
|
|
260
|
+
res.json({ ok: false, error: 'Missing subProvider' });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const provider = getPiSubProvider(subProvider);
|
|
264
|
+
if (!provider) {
|
|
265
|
+
res.json({ ok: false, error: `Unknown sub-provider: ${subProvider}` });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const prompt = 'Reply with the single word OK so we can confirm this LLM endpoint is reachable.';
|
|
269
|
+
const result = await runPiTestCompletion({ subProvider, apiKey, baseUrl, modelId, prompt });
|
|
270
|
+
res.json(result);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
app.post('/api/auth/pi/save', (req, res) => {
|
|
274
|
+
const { subProvider, apiKey, baseUrl, modelId } = req.body || {};
|
|
275
|
+
if (!subProvider || typeof subProvider !== 'string') {
|
|
276
|
+
res.json({ ok: false, error: 'Missing subProvider' });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const provider = getPiSubProvider(subProvider);
|
|
280
|
+
if (!provider) {
|
|
281
|
+
res.json({ ok: false, error: `Unknown sub-provider: ${subProvider}` });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const saved = writePiAuth({
|
|
285
|
+
subProvider,
|
|
286
|
+
apiKey: typeof apiKey === 'string' ? apiKey : undefined,
|
|
287
|
+
baseUrl: typeof baseUrl === 'string' && baseUrl.trim() ? baseUrl.trim() : provider.baseUrl,
|
|
288
|
+
modelId: typeof modelId === 'string' && modelId.trim() ? modelId.trim() : provider.defaultModel,
|
|
289
|
+
});
|
|
290
|
+
res.json({ ok: true, status: { configured: true, subProvider: saved.subProvider, modelId: saved.modelId, baseUrl: saved.baseUrl } });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
app.delete('/api/auth/pi', (_req, res) => {
|
|
294
|
+
clearPiAuth();
|
|
295
|
+
res.json({ ok: true });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
app.post('/api/auth/pi/completion', async (req, res) => {
|
|
299
|
+
const auth = readPiAuth();
|
|
300
|
+
if (!auth) {
|
|
301
|
+
res.json({ ok: false, error: 'Bloby provider is not configured yet' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const prompt = (req.body?.prompt && typeof req.body.prompt === 'string')
|
|
305
|
+
? req.body.prompt
|
|
306
|
+
: 'In one short sentence, introduce yourself as the model behind this endpoint and confirm the connection is working.';
|
|
307
|
+
const result = await runPiTestCompletion({
|
|
308
|
+
subProvider: auth.subProvider,
|
|
309
|
+
apiKey: auth.apiKey,
|
|
310
|
+
baseUrl: auth.baseUrl,
|
|
311
|
+
modelId: auth.modelId,
|
|
312
|
+
prompt,
|
|
313
|
+
});
|
|
314
|
+
res.json(result);
|
|
315
|
+
});
|
|
316
|
+
|
|
231
317
|
// ── Handle registration ──
|
|
232
318
|
|
|
233
319
|
app.get('/api/handle/check/:username', async (req, res) => {
|