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
|
@@ -46,6 +46,7 @@ const ACCESS_LABELS: Record<AccessMethod, string> = {
|
|
|
46
46
|
/* ── Provider config ── */
|
|
47
47
|
|
|
48
48
|
const PROVIDERS = [
|
|
49
|
+
{ id: 'pi', name: 'Bloby', subtitle: 'Built on top of Pi — bring your own model', icon: '/bloby-icon-192.png' },
|
|
49
50
|
{ id: 'anthropic', name: 'Claude', subtitle: 'by Anthropic', icon: '/icons/claude.png' },
|
|
50
51
|
{ id: 'openai', name: 'OpenAI Codex', subtitle: 'ChatGPT Plus / Pro', icon: '/icons/codex.png' },
|
|
51
52
|
] as const;
|
|
@@ -171,8 +172,34 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
171
172
|
const [authState, setAuthState] = useState<Record<string, 'idle' | 'authenticating' | 'connected'>>({
|
|
172
173
|
anthropic: 'idle',
|
|
173
174
|
openai: 'idle',
|
|
175
|
+
pi: 'idle',
|
|
174
176
|
});
|
|
175
177
|
|
|
178
|
+
/* ── Bloby (pi) state ── */
|
|
179
|
+
interface PiSubProviderInfo {
|
|
180
|
+
id: string;
|
|
181
|
+
name: string;
|
|
182
|
+
subtitle: string;
|
|
183
|
+
flavor: string;
|
|
184
|
+
baseUrl?: string;
|
|
185
|
+
needsBaseUrl: boolean;
|
|
186
|
+
needsApiKey: boolean;
|
|
187
|
+
apiKeyUrl?: string;
|
|
188
|
+
models: { id: string; label: string }[] | 'dynamic';
|
|
189
|
+
defaultModel?: string;
|
|
190
|
+
}
|
|
191
|
+
const [piSubProviders, setPiSubProviders] = useState<PiSubProviderInfo[]>([]);
|
|
192
|
+
const [piSubProvider, setPiSubProvider] = useState<string>('');
|
|
193
|
+
const [piApiKey, setPiApiKey] = useState('');
|
|
194
|
+
const [piBaseUrl, setPiBaseUrl] = useState('');
|
|
195
|
+
const [piModelId, setPiModelId] = useState('');
|
|
196
|
+
const [piShowKey, setPiShowKey] = useState(false);
|
|
197
|
+
const [piConnecting, setPiConnecting] = useState(false);
|
|
198
|
+
const [piError, setPiError] = useState<string | undefined>();
|
|
199
|
+
const [piTestRunning, setPiTestRunning] = useState(false);
|
|
200
|
+
const [piTestResult, setPiTestResult] = useState<{ ok: boolean; text?: string; error?: string } | null>(null);
|
|
201
|
+
const [piSavedStatus, setPiSavedStatus] = useState<{ subProvider?: string; modelId?: string; baseUrl?: string } | null>(null);
|
|
202
|
+
|
|
176
203
|
// Anthropic/Claude-specific
|
|
177
204
|
const [oauthStarted, setOauthStarted] = useState(false);
|
|
178
205
|
const [anthropicCode, setAnthropicCode] = useState('');
|
|
@@ -572,6 +599,130 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
572
599
|
setCodexVerificationUrl('');
|
|
573
600
|
setCodexFlow('device');
|
|
574
601
|
setOpenaiError(undefined);
|
|
602
|
+
// Pi flow cleanup is per-sub-provider; the load effect handles re-hydration.
|
|
603
|
+
setPiError(undefined);
|
|
604
|
+
setPiTestResult(null);
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
/* ── Bloby (pi) handlers ── */
|
|
608
|
+
|
|
609
|
+
// Load the sub-provider catalog + saved status when the user picks Bloby
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
if (provider !== 'pi') return;
|
|
612
|
+
let cancelled = false;
|
|
613
|
+
(async () => {
|
|
614
|
+
try {
|
|
615
|
+
const [provRes, statusRes] = await Promise.all([
|
|
616
|
+
fetch('/api/auth/pi/providers'),
|
|
617
|
+
fetch('/api/auth/pi/status'),
|
|
618
|
+
]);
|
|
619
|
+
const provData = await provRes.json();
|
|
620
|
+
const statusData = await statusRes.json();
|
|
621
|
+
if (cancelled) return;
|
|
622
|
+
const list: PiSubProviderInfo[] = provData?.providers || [];
|
|
623
|
+
setPiSubProviders(list);
|
|
624
|
+
if (statusData?.configured) {
|
|
625
|
+
setPiSubProvider(statusData.subProvider || '');
|
|
626
|
+
setPiModelId(statusData.modelId || '');
|
|
627
|
+
setPiBaseUrl(statusData.baseUrl || '');
|
|
628
|
+
setAuthState((s) => ({ ...s, pi: 'connected' }));
|
|
629
|
+
setPiSavedStatus({ subProvider: statusData.subProvider, modelId: statusData.modelId, baseUrl: statusData.baseUrl });
|
|
630
|
+
setModel(`${statusData.subProvider}/${statusData.modelId || ''}`);
|
|
631
|
+
} else if (!piSubProvider && list[0]) {
|
|
632
|
+
setPiSubProvider(list[0].id);
|
|
633
|
+
setPiBaseUrl(list[0].baseUrl || '');
|
|
634
|
+
setPiModelId(list[0].defaultModel || '');
|
|
635
|
+
}
|
|
636
|
+
} catch (err: any) {
|
|
637
|
+
if (!cancelled) setPiError(err?.message || 'Failed to load Bloby providers');
|
|
638
|
+
}
|
|
639
|
+
})();
|
|
640
|
+
return () => { cancelled = true; };
|
|
641
|
+
}, [provider]);
|
|
642
|
+
|
|
643
|
+
const selectedPiSub = piSubProviders.find((p) => p.id === piSubProvider);
|
|
644
|
+
|
|
645
|
+
const choosePiSubProvider = (id: string) => {
|
|
646
|
+
const next = piSubProviders.find((p) => p.id === id);
|
|
647
|
+
setPiSubProvider(id);
|
|
648
|
+
setPiBaseUrl(next?.baseUrl || '');
|
|
649
|
+
setPiModelId(next?.defaultModel || '');
|
|
650
|
+
setPiError(undefined);
|
|
651
|
+
setPiTestResult(null);
|
|
652
|
+
// Picking a new sub-provider invalidates the previously saved auth.
|
|
653
|
+
if (authState.pi === 'connected') {
|
|
654
|
+
setAuthState((s) => ({ ...s, pi: 'idle' }));
|
|
655
|
+
setPiSavedStatus(null);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const handlePiConnect = async () => {
|
|
660
|
+
if (!piSubProvider) return;
|
|
661
|
+
setPiError(undefined);
|
|
662
|
+
setPiTestResult(null);
|
|
663
|
+
setPiConnecting(true);
|
|
664
|
+
try {
|
|
665
|
+
const payload = {
|
|
666
|
+
subProvider: piSubProvider,
|
|
667
|
+
apiKey: piApiKey.trim() || undefined,
|
|
668
|
+
baseUrl: piBaseUrl.trim() || undefined,
|
|
669
|
+
modelId: piModelId.trim() || undefined,
|
|
670
|
+
};
|
|
671
|
+
const testRes = await fetch('/api/auth/pi/test', {
|
|
672
|
+
method: 'POST',
|
|
673
|
+
headers: { 'Content-Type': 'application/json' },
|
|
674
|
+
body: JSON.stringify(payload),
|
|
675
|
+
});
|
|
676
|
+
const testData = await testRes.json();
|
|
677
|
+
if (!testData?.ok) {
|
|
678
|
+
setPiError(testData?.error || 'Connection test failed');
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const saveRes = await fetch('/api/auth/pi/save', {
|
|
682
|
+
method: 'POST',
|
|
683
|
+
headers: { 'Content-Type': 'application/json' },
|
|
684
|
+
body: JSON.stringify(payload),
|
|
685
|
+
});
|
|
686
|
+
const saveData = await saveRes.json();
|
|
687
|
+
if (!saveData?.ok) {
|
|
688
|
+
setPiError(saveData?.error || 'Failed to save credentials');
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
setAuthState((s) => ({ ...s, pi: 'connected' }));
|
|
692
|
+
setPiSavedStatus(saveData.status || null);
|
|
693
|
+
setModel(`${piSubProvider}/${piModelId || selectedPiSub?.defaultModel || ''}`);
|
|
694
|
+
setPiApiKey('');
|
|
695
|
+
} catch (err: any) {
|
|
696
|
+
setPiError(err?.message || 'Connection failed');
|
|
697
|
+
} finally {
|
|
698
|
+
setPiConnecting(false);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const handlePiDisconnect = async () => {
|
|
703
|
+
try { await fetch('/api/auth/pi', { method: 'DELETE' }); } catch {}
|
|
704
|
+
setAuthState((s) => ({ ...s, pi: 'idle' }));
|
|
705
|
+
setPiSavedStatus(null);
|
|
706
|
+
setPiTestResult(null);
|
|
707
|
+
setModel('');
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const handlePiTestCompletion = async () => {
|
|
711
|
+
setPiTestRunning(true);
|
|
712
|
+
setPiTestResult(null);
|
|
713
|
+
try {
|
|
714
|
+
const res = await fetch('/api/auth/pi/completion', {
|
|
715
|
+
method: 'POST',
|
|
716
|
+
headers: { 'Content-Type': 'application/json' },
|
|
717
|
+
body: JSON.stringify({}),
|
|
718
|
+
});
|
|
719
|
+
const data = await res.json();
|
|
720
|
+
setPiTestResult({ ok: !!data?.ok, text: data?.text, error: data?.error });
|
|
721
|
+
} catch (err: any) {
|
|
722
|
+
setPiTestResult({ ok: false, error: err?.message || 'Request failed' });
|
|
723
|
+
} finally {
|
|
724
|
+
setPiTestRunning(false);
|
|
725
|
+
}
|
|
575
726
|
};
|
|
576
727
|
|
|
577
728
|
/* ── Auth handlers: Anthropic/Claude ── */
|
|
@@ -2090,6 +2241,180 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
2090
2241
|
|
|
2091
2242
|
<div className="border-t border-white/[0.06] mt-4 mb-3" />
|
|
2092
2243
|
|
|
2244
|
+
{/* ── Auth flow: Bloby (pi) ── */}
|
|
2245
|
+
{provider === 'pi' && (
|
|
2246
|
+
<div className="space-y-3">
|
|
2247
|
+
{authState.pi === 'connected' && piSavedStatus ? (
|
|
2248
|
+
<>
|
|
2249
|
+
<div className="bg-emerald-500/8 border border-emerald-500/15 rounded-lg px-3.5 py-2.5">
|
|
2250
|
+
<p className="text-emerald-400/90 text-[12px]">
|
|
2251
|
+
Connected — {piSubProviders.find((p) => p.id === piSavedStatus.subProvider)?.name || piSavedStatus.subProvider}
|
|
2252
|
+
{piSavedStatus.modelId ? <> · <span className="font-mono">{piSavedStatus.modelId}</span></> : null}
|
|
2253
|
+
</p>
|
|
2254
|
+
</div>
|
|
2255
|
+
|
|
2256
|
+
<div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-3.5 space-y-2.5">
|
|
2257
|
+
<p className="text-[12px] text-white/55">
|
|
2258
|
+
Send a quick test request to confirm the model is reachable.
|
|
2259
|
+
</p>
|
|
2260
|
+
<button
|
|
2261
|
+
onClick={handlePiTestCompletion}
|
|
2262
|
+
disabled={piTestRunning}
|
|
2263
|
+
className="w-full py-2 px-4 bg-white/[0.06] hover:bg-white/[0.1] text-white text-[12px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-60"
|
|
2264
|
+
>
|
|
2265
|
+
{piTestRunning ? (
|
|
2266
|
+
<><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Sending test request…</>
|
|
2267
|
+
) : (
|
|
2268
|
+
<>Send test request<ArrowRight className="h-3.5 w-3.5 opacity-60" /></>
|
|
2269
|
+
)}
|
|
2270
|
+
</button>
|
|
2271
|
+
{piTestResult && piTestResult.ok && (
|
|
2272
|
+
<div className="bg-emerald-500/5 border border-emerald-500/15 rounded-lg px-3 py-2">
|
|
2273
|
+
<p className="text-[11px] text-emerald-400/70 mb-1">Model reply</p>
|
|
2274
|
+
<p className="text-[12px] text-white/85 whitespace-pre-wrap">{piTestResult.text}</p>
|
|
2275
|
+
</div>
|
|
2276
|
+
)}
|
|
2277
|
+
{piTestResult && !piTestResult.ok && (
|
|
2278
|
+
<div className="bg-red-500/8 border border-red-500/15 rounded-lg px-3 py-2">
|
|
2279
|
+
<p className="text-[12px] text-red-400/90">{piTestResult.error}</p>
|
|
2280
|
+
</div>
|
|
2281
|
+
)}
|
|
2282
|
+
</div>
|
|
2283
|
+
|
|
2284
|
+
<button
|
|
2285
|
+
onClick={handlePiDisconnect}
|
|
2286
|
+
className="w-full py-1.5 text-white/25 text-[11px] hover:text-white/40 transition-colors flex items-center justify-center gap-1.5"
|
|
2287
|
+
>
|
|
2288
|
+
<RefreshCw className="h-3 w-3" />
|
|
2289
|
+
Use a different model
|
|
2290
|
+
</button>
|
|
2291
|
+
</>
|
|
2292
|
+
) : (
|
|
2293
|
+
<>
|
|
2294
|
+
<div>
|
|
2295
|
+
<p className="text-[12px] text-white/40 mb-2">Pick an LLM provider — Bloby brings your own model.</p>
|
|
2296
|
+
<div className="grid grid-cols-2 gap-2 max-h-[320px] overflow-y-auto pr-1">
|
|
2297
|
+
{piSubProviders.map((sp) => (
|
|
2298
|
+
<button
|
|
2299
|
+
key={sp.id}
|
|
2300
|
+
type="button"
|
|
2301
|
+
onClick={() => choosePiSubProvider(sp.id)}
|
|
2302
|
+
className={`relative text-left rounded-xl border px-3 py-2.5 transition-colors ${
|
|
2303
|
+
piSubProvider === sp.id
|
|
2304
|
+
? 'border-[#AF27E3]/50 bg-[#AF27E3]/10'
|
|
2305
|
+
: 'border-white/[0.08] bg-white/[0.02] hover:bg-white/[0.04]'
|
|
2306
|
+
}`}
|
|
2307
|
+
>
|
|
2308
|
+
<p className="text-[12.5px] text-white font-medium leading-tight">{sp.name}</p>
|
|
2309
|
+
<p className="text-[10.5px] text-white/40 mt-0.5 leading-tight">{sp.subtitle}</p>
|
|
2310
|
+
{piSubProvider === sp.id && (
|
|
2311
|
+
<div className="absolute top-1.5 right-1.5 w-1.5 h-1.5 rounded-full bg-gradient-brand" />
|
|
2312
|
+
)}
|
|
2313
|
+
</button>
|
|
2314
|
+
))}
|
|
2315
|
+
</div>
|
|
2316
|
+
</div>
|
|
2317
|
+
|
|
2318
|
+
{selectedPiSub && (
|
|
2319
|
+
<div className="space-y-2 pt-1">
|
|
2320
|
+
{(selectedPiSub.needsBaseUrl || selectedPiSub.flavor === 'openai-completions') && (
|
|
2321
|
+
<div>
|
|
2322
|
+
<label className="text-[11px] text-white/40 font-medium mb-1 block">
|
|
2323
|
+
Base URL{selectedPiSub.needsBaseUrl ? '' : ' (override — optional)'}
|
|
2324
|
+
</label>
|
|
2325
|
+
<input
|
|
2326
|
+
type="text"
|
|
2327
|
+
value={piBaseUrl}
|
|
2328
|
+
onChange={(e) => setPiBaseUrl(e.target.value)}
|
|
2329
|
+
placeholder={selectedPiSub.baseUrl || 'https://example.com/v1'}
|
|
2330
|
+
className={inputSmCls + ' font-mono'}
|
|
2331
|
+
/>
|
|
2332
|
+
</div>
|
|
2333
|
+
)}
|
|
2334
|
+
|
|
2335
|
+
{selectedPiSub.needsApiKey && (
|
|
2336
|
+
<div>
|
|
2337
|
+
<label className="text-[11px] text-white/40 font-medium mb-1 block flex items-center justify-between">
|
|
2338
|
+
<span>API key</span>
|
|
2339
|
+
{selectedPiSub.apiKeyUrl && (
|
|
2340
|
+
<button
|
|
2341
|
+
type="button"
|
|
2342
|
+
onClick={() => openExternal(selectedPiSub.apiKeyUrl!)}
|
|
2343
|
+
className="text-white/30 hover:text-white/60 text-[10.5px] flex items-center gap-1"
|
|
2344
|
+
>
|
|
2345
|
+
Get a key <ExternalLink className="h-3 w-3" />
|
|
2346
|
+
</button>
|
|
2347
|
+
)}
|
|
2348
|
+
</label>
|
|
2349
|
+
<div className="relative">
|
|
2350
|
+
<input
|
|
2351
|
+
type={piShowKey ? 'text' : 'password'}
|
|
2352
|
+
value={piApiKey}
|
|
2353
|
+
onChange={(e) => setPiApiKey(e.target.value)}
|
|
2354
|
+
onKeyDown={(e) => e.key === 'Enter' && handlePiConnect()}
|
|
2355
|
+
placeholder="sk-..."
|
|
2356
|
+
className={inputSmCls + ' pr-10 font-mono'}
|
|
2357
|
+
/>
|
|
2358
|
+
<button
|
|
2359
|
+
type="button"
|
|
2360
|
+
onClick={() => setPiShowKey((v) => !v)}
|
|
2361
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/20 hover:text-white/50 transition-colors"
|
|
2362
|
+
>
|
|
2363
|
+
{piShowKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
|
2364
|
+
</button>
|
|
2365
|
+
</div>
|
|
2366
|
+
</div>
|
|
2367
|
+
)}
|
|
2368
|
+
|
|
2369
|
+
<div>
|
|
2370
|
+
<label className="text-[11px] text-white/40 font-medium mb-1 block">Model</label>
|
|
2371
|
+
{Array.isArray(selectedPiSub.models) ? (
|
|
2372
|
+
<ModelDropdown
|
|
2373
|
+
models={selectedPiSub.models}
|
|
2374
|
+
value={piModelId}
|
|
2375
|
+
onChange={setPiModelId}
|
|
2376
|
+
/>
|
|
2377
|
+
) : (
|
|
2378
|
+
<input
|
|
2379
|
+
type="text"
|
|
2380
|
+
value={piModelId}
|
|
2381
|
+
onChange={(e) => setPiModelId(e.target.value)}
|
|
2382
|
+
placeholder={selectedPiSub.defaultModel || 'model-id'}
|
|
2383
|
+
className={inputSmCls + ' font-mono'}
|
|
2384
|
+
/>
|
|
2385
|
+
)}
|
|
2386
|
+
</div>
|
|
2387
|
+
|
|
2388
|
+
{piError && (
|
|
2389
|
+
<div className="bg-red-500/8 border border-red-500/15 rounded-lg px-3 py-2">
|
|
2390
|
+
<p className="text-[12px] text-red-400/90 break-words">{piError}</p>
|
|
2391
|
+
</div>
|
|
2392
|
+
)}
|
|
2393
|
+
|
|
2394
|
+
<button
|
|
2395
|
+
onClick={handlePiConnect}
|
|
2396
|
+
disabled={
|
|
2397
|
+
piConnecting ||
|
|
2398
|
+
!piSubProvider ||
|
|
2399
|
+
(selectedPiSub.needsApiKey && !piApiKey.trim()) ||
|
|
2400
|
+
(selectedPiSub.needsBaseUrl && !piBaseUrl.trim()) ||
|
|
2401
|
+
!piModelId.trim()
|
|
2402
|
+
}
|
|
2403
|
+
className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
|
|
2404
|
+
>
|
|
2405
|
+
{piConnecting ? (
|
|
2406
|
+
<><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Connecting…</>
|
|
2407
|
+
) : (
|
|
2408
|
+
<>Test & connect<ArrowRight className="h-3.5 w-3.5 opacity-60" /></>
|
|
2409
|
+
)}
|
|
2410
|
+
</button>
|
|
2411
|
+
</div>
|
|
2412
|
+
)}
|
|
2413
|
+
</>
|
|
2414
|
+
)}
|
|
2415
|
+
</div>
|
|
2416
|
+
)}
|
|
2417
|
+
|
|
2093
2418
|
{/* ── Auth flow: Anthropic ── */}
|
|
2094
2419
|
{provider === 'anthropic' && (
|
|
2095
2420
|
<div className="space-y-2.5">
|
|
@@ -2363,8 +2688,8 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
2363
2688
|
</div>
|
|
2364
2689
|
)}
|
|
2365
2690
|
|
|
2366
|
-
{/* ── Model dropdown (after auth) ── */}
|
|
2367
|
-
{isConnected && (
|
|
2691
|
+
{/* ── Model dropdown (after auth) — pi flow renders its own model picker inside its block ── */}
|
|
2692
|
+
{isConnected && provider !== 'pi' && (
|
|
2368
2693
|
<>
|
|
2369
2694
|
<div className="border-t border-white/[0.06] mt-4 mb-3" />
|
|
2370
2695
|
<label className="text-[12px] text-white/40 font-medium mb-1.5 block">
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async input queue — copy of the helper from `harnesses/claude.ts:37-68`.
|
|
3
|
+
*
|
|
4
|
+
* The Claude harness uses this exact shape as the input prompt to the Claude
|
|
5
|
+
* Agent SDK's long-lived `query()`. The pi session loop uses the same pattern
|
|
6
|
+
* so the non-blocking live-conversation behavior matches Claude byte-for-byte
|
|
7
|
+
* at the queue level: pushMessage() never awaits the model.
|
|
8
|
+
*/
|
|
9
|
+
export interface AsyncQueue<T> extends AsyncIterable<T> {
|
|
10
|
+
push(item: T): void;
|
|
11
|
+
end(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createAsyncQueue<T>(): AsyncQueue<T> {
|
|
15
|
+
const pending: T[] = [];
|
|
16
|
+
let resolve: ((value: IteratorResult<T>) => void) | null = null;
|
|
17
|
+
let done = false;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
push(item: T) {
|
|
21
|
+
if (done) return;
|
|
22
|
+
if (resolve) {
|
|
23
|
+
resolve({ value: item, done: false });
|
|
24
|
+
resolve = null;
|
|
25
|
+
} else {
|
|
26
|
+
pending.push(item);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
end() {
|
|
30
|
+
done = true;
|
|
31
|
+
if (resolve) resolve({ value: undefined as any, done: true });
|
|
32
|
+
},
|
|
33
|
+
[Symbol.asyncIterator]() {
|
|
34
|
+
return {
|
|
35
|
+
next(): Promise<IteratorResult<T>> {
|
|
36
|
+
if (pending.length > 0) {
|
|
37
|
+
return Promise.resolve({ value: pending.shift()!, done: false });
|
|
38
|
+
}
|
|
39
|
+
if (done) return Promise.resolve({ value: undefined as any, done: true });
|
|
40
|
+
return new Promise((r) => { resolve = r; });
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi auth storage — persistent credentials for the Bloby (pi) harness.
|
|
3
|
+
*
|
|
4
|
+
* Stored in ~/.bloby/pi-auth.json (separate from the main config.json so we
|
|
5
|
+
* can wipe/rotate the LLM credentials without touching the rest of the bot
|
|
6
|
+
* config). Iteration 1: a single active sub-provider at a time.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { DATA_DIR } from '../../../shared/paths.js';
|
|
11
|
+
|
|
12
|
+
export interface PiAuth {
|
|
13
|
+
subProvider: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
modelId?: string;
|
|
17
|
+
savedAt: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PI_AUTH_PATH = path.join(DATA_DIR, 'pi-auth.json');
|
|
21
|
+
|
|
22
|
+
export function readPiAuth(): PiAuth | null {
|
|
23
|
+
try {
|
|
24
|
+
if (!fs.existsSync(PI_AUTH_PATH)) return null;
|
|
25
|
+
const raw = fs.readFileSync(PI_AUTH_PATH, 'utf-8');
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (!parsed?.subProvider) return null;
|
|
28
|
+
return parsed as PiAuth;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function writePiAuth(auth: Omit<PiAuth, 'savedAt'>): PiAuth {
|
|
35
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
36
|
+
const full: PiAuth = { ...auth, savedAt: Date.now() };
|
|
37
|
+
fs.writeFileSync(PI_AUTH_PATH, JSON.stringify(full, null, 2), { mode: 0o600 });
|
|
38
|
+
return full;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clearPiAuth(): void {
|
|
42
|
+
try {
|
|
43
|
+
fs.rmSync(PI_AUTH_PATH, { force: true });
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getPiAuthStatus(): { configured: boolean; subProvider?: string; modelId?: string; baseUrl?: string } {
|
|
48
|
+
const auth = readPiAuth();
|
|
49
|
+
if (!auth) return { configured: false };
|
|
50
|
+
return {
|
|
51
|
+
configured: true,
|
|
52
|
+
subProvider: auth.subProvider,
|
|
53
|
+
modelId: auth.modelId,
|
|
54
|
+
baseUrl: auth.baseUrl,
|
|
55
|
+
};
|
|
56
|
+
}
|