@pasko70/pibo 2.5.1 → 3.0.0
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/context/pibo-native-tooling.md +1 -0
- package/dist/agent-runtime/contract.js +2 -1
- package/dist/agent-runtime/history.js +53 -1
- package/dist/agent-runtime/routed-session.js +100 -26
- package/dist/agent-runtimes/codex-native/adapter.js +307 -31
- package/dist/agent-runtimes/codex-native/client.js +22 -3
- package/dist/agent-runtimes/codex-native/first-use.js +294 -0
- package/dist/agent-runtimes/codex-native/history.js +26 -7
- package/dist/agent-runtimes/codex-native/models.js +48 -1
- package/dist/agent-runtimes/codex-native/process.js +15 -1
- package/dist/agent-runtimes/codex-native/thread.js +75 -31
- package/dist/agent-runtimes/codex-native/turn.js +30 -0
- package/dist/agent-runtimes/history-proof.js +8 -0
- package/dist/agent-runtimes/omp/adapter.js +61 -14
- package/dist/agent-runtimes/omp/history.js +189 -34
- package/dist/agent-runtimes/omp/thread.js +82 -36
- package/dist/agent-runtimes/pi/adapter.js +30 -3
- package/dist/agent-runtimes/pi/history.js +66 -5
- package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
- package/dist/agent-runtimes/pi/routed-session.js +344 -37
- package/dist/apps/chat/agent-store.js +3 -0
- package/dist/apps/chat/chat-api-routes.js +1 -1
- package/dist/apps/chat/chat-files.js +130 -9
- package/dist/apps/chat/chat-request-normalizers.js +6 -0
- package/dist/apps/chat/chat-settings-routes.js +27 -2
- package/dist/apps/chat/chat-speech.js +100 -0
- package/dist/apps/chat/data/chat-data-mappers.js +25 -5
- package/dist/apps/chat/data/timeline-query-service.js +89 -1
- package/dist/apps/chat/trace-v2.js +210 -8
- package/dist/apps/chat/trace.js +3 -0
- package/dist/apps/chat/web-app.js +108 -3
- package/dist/apps/chat-ui/assets/{dist-GS8xUdfg.js → dist-BBDMeU5-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BpVSf6ZV.js → dist-BDIATCQt.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CRv7fzi3.js → dist-JnW4v8UE.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CbBXbGVi.js → dist-lRiLP9rr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-HC0xQ92C.js → dist-mqCviI-c.js} +1 -1
- package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
- package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
- package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
- package/dist/apps/chat-vscode-web/index.html +2 -2
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
- package/dist/cli.js +49 -5
- package/dist/config/config.js +59 -8
- package/dist/core/preview-server-settings.js +20 -0
- package/dist/core/profiles.js +1 -0
- package/dist/core/session-router.js +476 -44
- package/dist/core/user-settings.js +10 -0
- package/dist/data/payload-store.js +57 -1
- package/dist/data/schema.js +18 -1
- package/dist/debug/trace.js +5 -0
- package/dist/gateway/client.js +368 -65
- package/dist/gateway/protocol.js +2 -0
- package/dist/gateway/server.js +8 -0
- package/dist/gateway/web.js +2 -0
- package/dist/index.js +1 -1
- package/dist/loops/store.js +6 -4
- package/dist/mcp/config.js +89 -17
- package/dist/mcp/daemon-client.js +473 -153
- package/dist/mcp/daemon.js +401 -37
- package/dist/plugins/builtin.js +2 -2
- package/dist/plugins/codex-native.js +2 -0
- package/dist/plugins/registry.js +260 -1
- package/dist/previews/cli.js +345 -0
- package/dist/previews/config.js +61 -0
- package/dist/previews/manager.js +644 -0
- package/dist/previews/network.js +198 -0
- package/dist/previews/plugin.js +11 -0
- package/dist/previews/process-supervisor.js +38 -0
- package/dist/previews/proxy.js +387 -0
- package/dist/previews/store.js +750 -0
- package/dist/previews/types.js +1 -0
- package/dist/previews/web-app.js +446 -0
- package/dist/runs/registry.js +22 -0
- package/dist/runs/tools.js +46 -7
- package/dist/session-ui/terminalRows.js +187 -13
- package/dist/sessions/pibo-data-store.js +37 -0
- package/dist/sessions/runtime-binding-persistence.js +34 -0
- package/dist/sessions/runtime-binding.js +6 -0
- package/dist/sessions/sqlite-store.js +37 -0
- package/dist/shared/trace-engine.js +12 -64
- package/dist/shared/trace-event-projection.js +16 -0
- package/dist/shared/trace-history.js +684 -184
- package/dist/shared/trace-limits.js +7 -0
- package/dist/shared/trace-live-patch.js +61 -0
- package/dist/shared/trace-tool-identity.js +43 -0
- package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
- package/dist/speech/openai-codex.js +407 -0
- package/dist/speech/types.js +8 -0
- package/dist/subagents/context.js +5 -3
- package/dist/subagents/observations.js +94 -6
- package/dist/subagents/tool.js +103 -20
- package/dist/tools/codex-image-artifacts.js +24 -0
- package/dist/tools/codex-image-generation.js +4 -22
- package/dist/web/channel.js +88 -4
- package/dist/web/http.js +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
- package/dist/apps/chat-ui/assets/index-DT5jCQBP.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.1.vsix +0 -0
package/dist/plugins/registry.js
CHANGED
|
@@ -1,8 +1,77 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { normalizeToolProfile } from "../core/profiles.js";
|
|
3
|
+
import { PiboSpeechError, } from "../speech/types.js";
|
|
3
4
|
import { listInstalledCliToolAgentContexts } from "../tools/registry.js";
|
|
4
5
|
import { listPiPackages } from "../pi-packages/store.js";
|
|
5
6
|
import { AgentRuntimeAdapterRegistry } from "../agent-runtime/registry.js";
|
|
7
|
+
const MAX_ACTIVE_SPEECH_SESSIONS = 8;
|
|
8
|
+
const SPEECH_SESSION_IDLE_TIMEOUT_MS = 2 * 60 * 1_000;
|
|
9
|
+
const SPEECH_SESSION_START_TIMEOUT_MS = 30_000;
|
|
10
|
+
function deferred() {
|
|
11
|
+
let resolve;
|
|
12
|
+
const promise = new Promise((resolvePromise) => {
|
|
13
|
+
resolve = resolvePromise;
|
|
14
|
+
});
|
|
15
|
+
return { promise, resolve };
|
|
16
|
+
}
|
|
17
|
+
function ownSpeechProviderSession(session) {
|
|
18
|
+
let stopPromise;
|
|
19
|
+
return {
|
|
20
|
+
session,
|
|
21
|
+
stop() {
|
|
22
|
+
stopPromise ??= Promise.resolve().then(() => session.stop());
|
|
23
|
+
return stopPromise;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function speechAbortError(signal) {
|
|
28
|
+
if (signal.reason instanceof PiboSpeechError)
|
|
29
|
+
return signal.reason;
|
|
30
|
+
const message = signal.reason instanceof Error && signal.reason.message
|
|
31
|
+
? signal.reason.message
|
|
32
|
+
: "Speech session startup was aborted.";
|
|
33
|
+
return new PiboSpeechError(message, "aborted", { cause: signal.reason });
|
|
34
|
+
}
|
|
35
|
+
async function waitForSpeechProviderStart(start, signal) {
|
|
36
|
+
if (signal.aborted)
|
|
37
|
+
throw speechAbortError(signal);
|
|
38
|
+
let onAbort;
|
|
39
|
+
try {
|
|
40
|
+
return await Promise.race([
|
|
41
|
+
start,
|
|
42
|
+
new Promise((_resolve, reject) => {
|
|
43
|
+
onAbort = () => reject(speechAbortError(signal));
|
|
44
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
45
|
+
}),
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
if (onAbort)
|
|
50
|
+
signal.removeEventListener("abort", onAbort);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function positiveTimeout(value, fallback, name) {
|
|
54
|
+
const selected = value ?? fallback;
|
|
55
|
+
if (!Number.isSafeInteger(selected) || selected <= 0)
|
|
56
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
57
|
+
return selected;
|
|
58
|
+
}
|
|
59
|
+
async function waitBounded(promise, timeoutMs) {
|
|
60
|
+
let timer;
|
|
61
|
+
try {
|
|
62
|
+
await Promise.race([
|
|
63
|
+
promise,
|
|
64
|
+
new Promise((resolve) => {
|
|
65
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
66
|
+
timer.unref?.();
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (timer)
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
6
75
|
function getWebAppRoutes(app) {
|
|
7
76
|
return [
|
|
8
77
|
{ label: "mountPath", prefix: app.mountPath },
|
|
@@ -33,6 +102,9 @@ function toolIsPortable(tool) {
|
|
|
33
102
|
}
|
|
34
103
|
}
|
|
35
104
|
export class PiboPluginRegistry {
|
|
105
|
+
maxActiveSpeechSessions;
|
|
106
|
+
speechSessionIdleTimeoutMs;
|
|
107
|
+
speechSessionStartTimeoutMs;
|
|
36
108
|
agentRuntimes = new AgentRuntimeAdapterRegistry();
|
|
37
109
|
tools = new Map();
|
|
38
110
|
subagents = new Map();
|
|
@@ -45,6 +117,11 @@ export class PiboPluginRegistry {
|
|
|
45
117
|
channels = new Map();
|
|
46
118
|
authService;
|
|
47
119
|
transcriptionProviders = new Map();
|
|
120
|
+
speechProviders = new Map();
|
|
121
|
+
pendingSpeechStarts = new Map();
|
|
122
|
+
speechSessions = new Map();
|
|
123
|
+
speechProvidersDisposed = false;
|
|
124
|
+
speechDisposePromise;
|
|
48
125
|
webApps = new Map();
|
|
49
126
|
capabilityPackages = new Map();
|
|
50
127
|
eventListeners = new Set();
|
|
@@ -53,8 +130,13 @@ export class PiboPluginRegistry {
|
|
|
53
130
|
pluginIds = new Set();
|
|
54
131
|
pluginNames = new Map();
|
|
55
132
|
eventErrors = [];
|
|
133
|
+
constructor(options = {}) {
|
|
134
|
+
this.maxActiveSpeechSessions = positiveTimeout(options.maxActiveSpeechSessions, MAX_ACTIVE_SPEECH_SESSIONS, "maxActiveSpeechSessions");
|
|
135
|
+
this.speechSessionIdleTimeoutMs = positiveTimeout(options.speechSessionIdleTimeoutMs, SPEECH_SESSION_IDLE_TIMEOUT_MS, "speechSessionIdleTimeoutMs");
|
|
136
|
+
this.speechSessionStartTimeoutMs = positiveTimeout(options.speechSessionStartTimeoutMs, SPEECH_SESSION_START_TIMEOUT_MS, "speechSessionStartTimeoutMs");
|
|
137
|
+
}
|
|
56
138
|
static create(options = {}) {
|
|
57
|
-
const registry = new PiboPluginRegistry();
|
|
139
|
+
const registry = new PiboPluginRegistry(options);
|
|
58
140
|
for (const plugin of options.plugins ?? []) {
|
|
59
141
|
registry.registerPlugin(plugin);
|
|
60
142
|
}
|
|
@@ -199,6 +281,177 @@ export class PiboPluginRegistry {
|
|
|
199
281
|
const provider = this.getRequired(this.transcriptionProviders, providerId, "transcription provider");
|
|
200
282
|
return { providerId, ...await provider.transcribe(input) };
|
|
201
283
|
}
|
|
284
|
+
registerSpeechProvider(provider) {
|
|
285
|
+
this.addUnique(this.speechProviders, provider.id, provider, "speech provider");
|
|
286
|
+
}
|
|
287
|
+
getSpeechProviderIds() {
|
|
288
|
+
return [...this.speechProviders.keys()];
|
|
289
|
+
}
|
|
290
|
+
async getSpeechProviderInfos() {
|
|
291
|
+
return await Promise.all([...this.speechProviders.values()].map(async (provider) => ({
|
|
292
|
+
id: provider.id,
|
|
293
|
+
name: provider.name,
|
|
294
|
+
description: provider.description,
|
|
295
|
+
configured: provider.isConfigured ? await Promise.resolve(provider.isConfigured()).catch(() => false) : true,
|
|
296
|
+
pluginId: provider.pluginId,
|
|
297
|
+
pluginName: provider.pluginId ? this.pluginNames.get(provider.pluginId) : undefined,
|
|
298
|
+
})));
|
|
299
|
+
}
|
|
300
|
+
async startSpeechSession(providerId, input, options = {}) {
|
|
301
|
+
const provider = this.getRequired(this.speechProviders, providerId, "speech provider");
|
|
302
|
+
if (this.speechProvidersDisposed)
|
|
303
|
+
throw new PiboSpeechError("Speech providers are shutting down.", "aborted");
|
|
304
|
+
if (this.speechSessions.size + this.pendingSpeechStarts.size >= this.maxActiveSpeechSessions) {
|
|
305
|
+
throw new PiboSpeechError("Too many speech sessions are active. Try again shortly.", "capacity_exceeded");
|
|
306
|
+
}
|
|
307
|
+
const reservationId = randomUUID();
|
|
308
|
+
const controller = new AbortController();
|
|
309
|
+
const settled = deferred();
|
|
310
|
+
const abortFromCaller = () => controller.abort(options.signal?.reason);
|
|
311
|
+
if (options.signal?.aborted)
|
|
312
|
+
abortFromCaller();
|
|
313
|
+
else
|
|
314
|
+
options.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
315
|
+
const timeout = setTimeout(() => {
|
|
316
|
+
controller.abort(new PiboSpeechError("Speech session startup timed out.", "provider_error"));
|
|
317
|
+
}, this.speechSessionStartTimeoutMs);
|
|
318
|
+
timeout.unref?.();
|
|
319
|
+
let owned;
|
|
320
|
+
let published = false;
|
|
321
|
+
let releaseInFinally = true;
|
|
322
|
+
let reservationReleased = false;
|
|
323
|
+
const releaseReservation = () => {
|
|
324
|
+
if (reservationReleased)
|
|
325
|
+
return;
|
|
326
|
+
reservationReleased = true;
|
|
327
|
+
clearTimeout(timeout);
|
|
328
|
+
this.pendingSpeechStarts.delete(reservationId);
|
|
329
|
+
settled.resolve();
|
|
330
|
+
if (!published)
|
|
331
|
+
options.signal?.removeEventListener("abort", abortFromCaller);
|
|
332
|
+
};
|
|
333
|
+
this.pendingSpeechStarts.set(reservationId, { controller, timeout, settled, release: releaseReservation });
|
|
334
|
+
let providerSettled = false;
|
|
335
|
+
let providerResult;
|
|
336
|
+
const providerStart = Promise.resolve()
|
|
337
|
+
.then(() => provider.startSession(input, { signal: controller.signal }))
|
|
338
|
+
.then((session) => {
|
|
339
|
+
providerSettled = true;
|
|
340
|
+
providerResult = session;
|
|
341
|
+
return session;
|
|
342
|
+
}, (error) => {
|
|
343
|
+
providerSettled = true;
|
|
344
|
+
throw error;
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
if (controller.signal.aborted)
|
|
348
|
+
throw speechAbortError(controller.signal);
|
|
349
|
+
owned = ownSpeechProviderSession(await waitForSpeechProviderStart(providerStart, controller.signal));
|
|
350
|
+
if (controller.signal.aborted || this.speechProvidersDisposed) {
|
|
351
|
+
await owned.stop().catch(() => { });
|
|
352
|
+
throw speechAbortError(controller.signal);
|
|
353
|
+
}
|
|
354
|
+
const { sessionId, answerSdp } = owned.session;
|
|
355
|
+
if (this.speechSessions.has(sessionId)) {
|
|
356
|
+
await owned.stop().catch(() => { });
|
|
357
|
+
throw new PiboSpeechError("Speech provider returned a duplicate session.", "provider_error");
|
|
358
|
+
}
|
|
359
|
+
const unlinkAbort = () => options.signal?.removeEventListener("abort", abortFromCaller);
|
|
360
|
+
const entry = {
|
|
361
|
+
provider,
|
|
362
|
+
owned,
|
|
363
|
+
timer: setTimeout(() => {
|
|
364
|
+
void this.closeSpeechSession(sessionId, entry).catch(() => { });
|
|
365
|
+
}, this.speechSessionIdleTimeoutMs),
|
|
366
|
+
unlinkAbort,
|
|
367
|
+
};
|
|
368
|
+
entry.timer.unref?.();
|
|
369
|
+
this.speechSessions.set(sessionId, entry);
|
|
370
|
+
published = true;
|
|
371
|
+
controller.signal.addEventListener("abort", () => {
|
|
372
|
+
void this.closeSpeechSession(sessionId, entry).catch(() => { });
|
|
373
|
+
}, { once: true });
|
|
374
|
+
if (controller.signal.aborted) {
|
|
375
|
+
await this.closeSpeechSession(sessionId, entry).catch(() => { });
|
|
376
|
+
throw speechAbortError(controller.signal);
|
|
377
|
+
}
|
|
378
|
+
return { providerId, sessionId, answerSdp };
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (controller.signal.aborted && !owned && providerResult) {
|
|
382
|
+
await ownSpeechProviderSession(providerResult).stop().catch(() => { });
|
|
383
|
+
}
|
|
384
|
+
else if (controller.signal.aborted && !providerSettled) {
|
|
385
|
+
releaseInFinally = false;
|
|
386
|
+
options.signal?.removeEventListener("abort", abortFromCaller);
|
|
387
|
+
void providerStart
|
|
388
|
+
.then(async (lateSession) => await ownSpeechProviderSession(lateSession).stop())
|
|
389
|
+
.catch(() => { })
|
|
390
|
+
.finally(releaseReservation);
|
|
391
|
+
}
|
|
392
|
+
if (controller.signal.aborted)
|
|
393
|
+
throw speechAbortError(controller.signal);
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
if (releaseInFinally)
|
|
398
|
+
releaseReservation();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async speakSpeechSession(sessionId, input) {
|
|
402
|
+
const entry = this.speechSessions.get(sessionId);
|
|
403
|
+
if (!entry)
|
|
404
|
+
throw new PiboSpeechError("Speech session was not found.", "session_not_found");
|
|
405
|
+
let speechError;
|
|
406
|
+
try {
|
|
407
|
+
await entry.owned.session.speak(input);
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
speechError = error;
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
try {
|
|
414
|
+
await this.closeSpeechSession(sessionId, entry);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
if (speechError === undefined)
|
|
418
|
+
speechError = error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (speechError !== undefined)
|
|
422
|
+
throw speechError;
|
|
423
|
+
}
|
|
424
|
+
async stopSpeechSession(sessionId) {
|
|
425
|
+
const entry = this.speechSessions.get(sessionId);
|
|
426
|
+
if (!entry)
|
|
427
|
+
return;
|
|
428
|
+
await this.closeSpeechSession(sessionId, entry);
|
|
429
|
+
}
|
|
430
|
+
async disposeSpeechProviders() {
|
|
431
|
+
this.speechDisposePromise ??= (async () => {
|
|
432
|
+
this.speechProvidersDisposed = true;
|
|
433
|
+
const shutdownError = new PiboSpeechError("Speech providers are shutting down.", "aborted");
|
|
434
|
+
const pending = [...this.pendingSpeechStarts.values()];
|
|
435
|
+
for (const reservation of pending)
|
|
436
|
+
reservation.controller.abort(shutdownError);
|
|
437
|
+
const cleanup = Promise.allSettled([
|
|
438
|
+
...([...this.speechSessions.entries()].map(async ([sessionId, entry]) => await this.closeSpeechSession(sessionId, entry))),
|
|
439
|
+
...pending.map((reservation) => reservation.settled.promise),
|
|
440
|
+
...([...this.speechProviders.values()].map(async (provider) => await provider.dispose?.())),
|
|
441
|
+
]);
|
|
442
|
+
await waitBounded(cleanup, this.speechSessionStartTimeoutMs);
|
|
443
|
+
for (const reservation of this.pendingSpeechStarts.values())
|
|
444
|
+
reservation.release();
|
|
445
|
+
})();
|
|
446
|
+
await this.speechDisposePromise;
|
|
447
|
+
}
|
|
448
|
+
closeSpeechSession(sessionId, entry) {
|
|
449
|
+
clearTimeout(entry.timer);
|
|
450
|
+
entry.unlinkAbort();
|
|
451
|
+
if (this.speechSessions.get(sessionId) === entry)
|
|
452
|
+
this.speechSessions.delete(sessionId);
|
|
453
|
+
return entry.owned.stop();
|
|
454
|
+
}
|
|
202
455
|
registerWebApp(app) {
|
|
203
456
|
if (this.webApps.has(app.name)) {
|
|
204
457
|
throw new Error(`Duplicate web app "${app.name}"`);
|
|
@@ -316,6 +569,7 @@ export class PiboPluginRegistry {
|
|
|
316
569
|
timeoutMs: subagent.timeoutMs,
|
|
317
570
|
model: subagent.model ? { ...subagent.model } : undefined,
|
|
318
571
|
thinkingLevel: subagent.thinkingLevel,
|
|
572
|
+
...(subagent.runtimeOptions ? { runtimeOptions: structuredClone(subagent.runtimeOptions) } : {}),
|
|
319
573
|
maxDepth: subagent.maxDepth,
|
|
320
574
|
})),
|
|
321
575
|
contextFiles: [...this.contextFiles.entries()].map(([key, contextFile]) => ({
|
|
@@ -436,6 +690,10 @@ export class PiboPluginRegistry {
|
|
|
436
690
|
...provider,
|
|
437
691
|
pluginId: provider.pluginId ?? pluginId,
|
|
438
692
|
});
|
|
693
|
+
const withPluginSpeechProviderContext = (provider) => ({
|
|
694
|
+
...provider,
|
|
695
|
+
pluginId: provider.pluginId ?? pluginId,
|
|
696
|
+
});
|
|
439
697
|
return {
|
|
440
698
|
registerAgentRuntimeDriver: (driver) => this.registerAgentRuntimeDriver(driver),
|
|
441
699
|
registerAgentRuntimeInstance: (instance) => this.registerAgentRuntimeInstance(instance),
|
|
@@ -453,6 +711,7 @@ export class PiboPluginRegistry {
|
|
|
453
711
|
registerChannel: (channel) => this.registerChannel(channel),
|
|
454
712
|
registerAuthService: (service) => this.registerAuthService(service),
|
|
455
713
|
registerTranscriptionProvider: (provider) => this.registerTranscriptionProvider(withPluginTranscriptionProviderContext(provider)),
|
|
714
|
+
registerSpeechProvider: (provider) => this.registerSpeechProvider(withPluginSpeechProviderContext(provider)),
|
|
456
715
|
registerWebApp: (app) => this.registerWebApp(app),
|
|
457
716
|
registerCapabilityPackage: (pkg) => this.registerCapabilityPackage(withPluginPackageContext(pkg)),
|
|
458
717
|
registerLoopStopCondition: (condition) => this.registerLoopStopCondition(condition, pluginId),
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { sanitizePreviewServerSettings } from "../core/preview-server-settings.js";
|
|
5
|
+
import { createDefaultPiboDataSessionStore } from "../sessions/pibo-data-store.js";
|
|
6
|
+
import { DEFAULT_PREVIEW_TTL_MINUTES, loadPreviewConfig, previewPublicURL, requirePreviewBaseURL } from "./config.js";
|
|
7
|
+
import { reconcileManagedPreviews, startManagedPreview, stopManagedPreview, validatePreviewStartCommand, } from "./manager.js";
|
|
8
|
+
import { findPreviewTargetProcess, isPiboControlProcess, isPiboYieldedProcess, isPreviewTargetProcessCurrent, probePreviewTarget, validatePreviewPort, } from "./network.js";
|
|
9
|
+
import { createDefaultPreviewStore, previewExposureState } from "./store.js";
|
|
10
|
+
import { DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS, DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS_PER_PREVIEW, } from "./proxy.js";
|
|
11
|
+
function printJson(value) {
|
|
12
|
+
console.log(JSON.stringify(value, null, 2));
|
|
13
|
+
}
|
|
14
|
+
function parsePort(value) {
|
|
15
|
+
return validatePreviewPort(Number(value));
|
|
16
|
+
}
|
|
17
|
+
function parsePositiveInteger(value) {
|
|
18
|
+
const parsed = Number(value);
|
|
19
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
20
|
+
throw new Error("Value must be a positive integer");
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
function parsePreviewTtlMinutes(value) {
|
|
24
|
+
const parsed = parsePositiveInteger(value);
|
|
25
|
+
if (parsed > 7 * 24 * 60)
|
|
26
|
+
throw new Error("Preview lifetime cannot exceed 7 days");
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
function createPreviewId() {
|
|
30
|
+
return `pv-${randomBytes(9).toString("hex")}`;
|
|
31
|
+
}
|
|
32
|
+
function inferredProjectId(metadata) {
|
|
33
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
|
|
34
|
+
return undefined;
|
|
35
|
+
const projectId = metadata.projectId;
|
|
36
|
+
return typeof projectId === "string" && projectId.trim() ? projectId.trim() : undefined;
|
|
37
|
+
}
|
|
38
|
+
async function previewHealth(exposure) {
|
|
39
|
+
const state = previewExposureState(exposure);
|
|
40
|
+
if (state !== "active")
|
|
41
|
+
return state;
|
|
42
|
+
if (exposure.managementMode === "managed") {
|
|
43
|
+
if (exposure.serverState === "starting")
|
|
44
|
+
return "starting";
|
|
45
|
+
if (exposure.serverState === "stopping")
|
|
46
|
+
return "stopping";
|
|
47
|
+
if (exposure.serverState === "stopped")
|
|
48
|
+
return "stopped";
|
|
49
|
+
if (exposure.serverState === "error")
|
|
50
|
+
return "error";
|
|
51
|
+
}
|
|
52
|
+
const processCurrent = isPreviewTargetProcessCurrent(exposure, { cacheMs: 0 });
|
|
53
|
+
const target = processCurrent ? await probePreviewTarget(exposure.targetPort) : undefined;
|
|
54
|
+
return target ? "online" : "offline";
|
|
55
|
+
}
|
|
56
|
+
async function exposureView(exposure) {
|
|
57
|
+
const health = await previewHealth(exposure);
|
|
58
|
+
const target = health === "online" ? await probePreviewTarget(exposure.targetPort) : undefined;
|
|
59
|
+
return {
|
|
60
|
+
...exposure,
|
|
61
|
+
managed: exposure.managementMode === "managed",
|
|
62
|
+
state: previewExposureState(exposure),
|
|
63
|
+
health,
|
|
64
|
+
publicUrl: previewPublicURL(exposure.id).toString(),
|
|
65
|
+
...(target ? { latencyMs: target.latencyMs } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function printPreviewDiscovery() {
|
|
69
|
+
console.log(`pibo preview - Session-linked live development previews
|
|
70
|
+
|
|
71
|
+
Commands:
|
|
72
|
+
expose <port> Register an external port or start a managed server with --command
|
|
73
|
+
list List preview registrations
|
|
74
|
+
show <preview-id> Inspect one preview
|
|
75
|
+
start <preview-id> Start a saved managed Preview server
|
|
76
|
+
stop <preview-id> Stop a managed Preview server without removing it
|
|
77
|
+
doctor [preview-id] Check preview configuration and reachability
|
|
78
|
+
remove <preview-id> Stop and remove a preview definition
|
|
79
|
+
close <preview-id> Alias for remove
|
|
80
|
+
|
|
81
|
+
Next:
|
|
82
|
+
pibo preview expose --help
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
export async function runPreviewCli(argv = process.argv) {
|
|
86
|
+
if (argv.length <= 2 || argv[2] === "--help" || argv[2] === "-h") {
|
|
87
|
+
printPreviewDiscovery();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const program = new Command();
|
|
91
|
+
program.name("pibo preview").description("Manage session-linked live development previews");
|
|
92
|
+
program
|
|
93
|
+
.command("expose")
|
|
94
|
+
.argument("<port>", "Reachable loopback development port", parsePort)
|
|
95
|
+
.requiredOption("--session <pibo-session-id>", "Pibo Session that owns the preview")
|
|
96
|
+
.option("--project <project-id>", "Optional Project association")
|
|
97
|
+
.option("--name <label>", "Preview label")
|
|
98
|
+
.option("--workspace <path>", "Workspace used by the managed command and recorded for diagnostics")
|
|
99
|
+
.option("--command <shell-command>", "Save and start this command as a Preview-managed server")
|
|
100
|
+
.option("--ttl-minutes <minutes>", "Automatic preview-definition lifetime", parsePreviewTtlMinutes, DEFAULT_PREVIEW_TTL_MINUTES)
|
|
101
|
+
.option("--json", "Print JSON")
|
|
102
|
+
.action(async (port, options) => {
|
|
103
|
+
const baseURL = requirePreviewBaseURL();
|
|
104
|
+
const piboSessionId = options.session.trim();
|
|
105
|
+
if (!piboSessionId)
|
|
106
|
+
throw new Error("--session must contain a Pibo Session ID");
|
|
107
|
+
const sessionStore = createDefaultPiboDataSessionStore();
|
|
108
|
+
let session;
|
|
109
|
+
try {
|
|
110
|
+
session = sessionStore.get(piboSessionId);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
sessionStore.close();
|
|
114
|
+
}
|
|
115
|
+
if (!session)
|
|
116
|
+
throw new Error(`Pibo Session "${piboSessionId}" was not found`);
|
|
117
|
+
const command = options.command === undefined ? undefined : validatePreviewStartCommand(options.command);
|
|
118
|
+
let target = command ? undefined : await probePreviewTarget(port);
|
|
119
|
+
if (!command && !target)
|
|
120
|
+
throw new Error(`No development server is reachable on loopback port ${port}`);
|
|
121
|
+
if (command && await probePreviewTarget(port, { timeoutMs: 150 }))
|
|
122
|
+
throw new Error(`Preview port ${port} is already occupied`);
|
|
123
|
+
const now = new Date();
|
|
124
|
+
const id = createPreviewId();
|
|
125
|
+
const targetProcess = target ? findPreviewTargetProcess(target.host, port) : undefined;
|
|
126
|
+
if (!command && process.platform === "linux" && !targetProcess) {
|
|
127
|
+
throw new Error(`Preview port ${port} is not bound exclusively to the selected loopback address`);
|
|
128
|
+
}
|
|
129
|
+
if (!command && targetProcess && isPiboControlProcess(targetProcess.pid)) {
|
|
130
|
+
throw new Error(`Preview port ${port} belongs to a Pibo control process and cannot be exposed`);
|
|
131
|
+
}
|
|
132
|
+
if (!command && process.env.NODE_ENV !== "test" && targetProcess && isPiboYieldedProcess(targetProcess.pid)) {
|
|
133
|
+
throw new Error("Preview servers cannot run as yielded agent resources; stop that process and use --command so Preview owns its lifecycle");
|
|
134
|
+
}
|
|
135
|
+
const store = createDefaultPreviewStore();
|
|
136
|
+
let exposure;
|
|
137
|
+
try {
|
|
138
|
+
exposure = store.createExposure({
|
|
139
|
+
id,
|
|
140
|
+
piboSessionId,
|
|
141
|
+
projectId: options.project?.trim() || inferredProjectId(session.metadata),
|
|
142
|
+
label: options.name?.trim() || `Preview ${port}`,
|
|
143
|
+
targetHost: target?.host ?? "127.0.0.1",
|
|
144
|
+
targetPort: port,
|
|
145
|
+
targetProcessId: targetProcess?.pid,
|
|
146
|
+
targetProcessStartTicks: targetProcess?.startTicks,
|
|
147
|
+
workspace: resolve(options.workspace ?? session.workspace ?? process.cwd()),
|
|
148
|
+
managementMode: command ? "managed" : "external",
|
|
149
|
+
startCommand: command,
|
|
150
|
+
serverState: command ? "stopped" : undefined,
|
|
151
|
+
createdAt: now.toISOString(),
|
|
152
|
+
expiresAt: new Date(now.getTime() + options.ttlMinutes * 60_000).toISOString(),
|
|
153
|
+
});
|
|
154
|
+
if (command)
|
|
155
|
+
exposure = await startManagedPreview(store, id);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
store.close();
|
|
159
|
+
}
|
|
160
|
+
const result = { ...await exposureView(exposure), publicUrl: previewPublicURL(id, baseURL).toString() };
|
|
161
|
+
if (options.json)
|
|
162
|
+
printJson(result);
|
|
163
|
+
else {
|
|
164
|
+
console.log(`${result.id}\t${result.health}\t${result.label}`);
|
|
165
|
+
console.log(`url\t${result.publicUrl}`);
|
|
166
|
+
console.log(`session\t${result.piboSessionId}`);
|
|
167
|
+
console.log(`managed\t${result.managed}`);
|
|
168
|
+
if (result.serverStopAt)
|
|
169
|
+
console.log(`autoStop\t${result.serverStopAt}`);
|
|
170
|
+
console.log(`expires\t${result.expiresAt}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
program
|
|
174
|
+
.command("list")
|
|
175
|
+
.option("--session <pibo-session-id>", "Filter by Pibo Session")
|
|
176
|
+
.option("--all", "Include expired and closed previews")
|
|
177
|
+
.option("--json", "Print JSON")
|
|
178
|
+
.action(async (options) => {
|
|
179
|
+
requirePreviewBaseURL();
|
|
180
|
+
const store = createDefaultPreviewStore();
|
|
181
|
+
let exposures;
|
|
182
|
+
try {
|
|
183
|
+
await reconcileManagedPreviews(store);
|
|
184
|
+
exposures = store.listExposures({ piboSessionId: options.session, includeInactive: options.all === true });
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
store.close();
|
|
188
|
+
}
|
|
189
|
+
const rows = await Promise.all(exposures.map(exposureView));
|
|
190
|
+
if (options.json)
|
|
191
|
+
printJson(rows);
|
|
192
|
+
else if (rows.length === 0)
|
|
193
|
+
console.log("No previews.");
|
|
194
|
+
else
|
|
195
|
+
for (const row of rows)
|
|
196
|
+
console.log(`${row.id}\t${row.health}\t${row.targetPort}\t${row.piboSessionId}\t${row.label}`);
|
|
197
|
+
});
|
|
198
|
+
program
|
|
199
|
+
.command("show")
|
|
200
|
+
.argument("<preview-id>")
|
|
201
|
+
.option("--json", "Print JSON")
|
|
202
|
+
.action(async (id, options) => {
|
|
203
|
+
requirePreviewBaseURL();
|
|
204
|
+
const store = createDefaultPreviewStore();
|
|
205
|
+
let exposure;
|
|
206
|
+
try {
|
|
207
|
+
await reconcileManagedPreviews(store);
|
|
208
|
+
exposure = store.requireExposure(id);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
store.close();
|
|
212
|
+
}
|
|
213
|
+
const row = await exposureView(exposure);
|
|
214
|
+
if (options.json)
|
|
215
|
+
printJson(row);
|
|
216
|
+
else
|
|
217
|
+
for (const [key, value] of Object.entries(row))
|
|
218
|
+
console.log(`${key}\t${String(value)}`);
|
|
219
|
+
});
|
|
220
|
+
program
|
|
221
|
+
.command("start")
|
|
222
|
+
.argument("<preview-id>")
|
|
223
|
+
.option("--json", "Print JSON")
|
|
224
|
+
.action(async (id, options) => {
|
|
225
|
+
requirePreviewBaseURL();
|
|
226
|
+
const store = createDefaultPreviewStore();
|
|
227
|
+
let exposure;
|
|
228
|
+
try {
|
|
229
|
+
exposure = await startManagedPreview(store, id);
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
store.close();
|
|
233
|
+
}
|
|
234
|
+
const row = await exposureView(exposure);
|
|
235
|
+
if (options.json)
|
|
236
|
+
printJson(row);
|
|
237
|
+
else
|
|
238
|
+
console.log(`${row.id}\t${row.health}\tautoStop=${row.serverStopAt ?? "-"}`);
|
|
239
|
+
});
|
|
240
|
+
program
|
|
241
|
+
.command("stop")
|
|
242
|
+
.argument("<preview-id>")
|
|
243
|
+
.option("--json", "Print JSON")
|
|
244
|
+
.action(async (id, options) => {
|
|
245
|
+
const store = createDefaultPreviewStore();
|
|
246
|
+
let exposure;
|
|
247
|
+
try {
|
|
248
|
+
exposure = await stopManagedPreview(store, id);
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
store.close();
|
|
252
|
+
}
|
|
253
|
+
const row = await exposureView(exposure);
|
|
254
|
+
if (options.json)
|
|
255
|
+
printJson(row);
|
|
256
|
+
else
|
|
257
|
+
console.log(`${row.id}\t${row.health}`);
|
|
258
|
+
});
|
|
259
|
+
program
|
|
260
|
+
.command("doctor")
|
|
261
|
+
.argument("[preview-id]")
|
|
262
|
+
.option("--json", "Print JSON")
|
|
263
|
+
.action(async (id, options) => {
|
|
264
|
+
const baseURL = requirePreviewBaseURL();
|
|
265
|
+
if (!id) {
|
|
266
|
+
const config = loadPreviewConfig();
|
|
267
|
+
const serverLimits = sanitizePreviewServerSettings({
|
|
268
|
+
maxRunningServers: config.maxRunningServers,
|
|
269
|
+
autoStopMinutes: config.autoStopMinutes,
|
|
270
|
+
});
|
|
271
|
+
const store = createDefaultPreviewStore();
|
|
272
|
+
let diagnostics;
|
|
273
|
+
try {
|
|
274
|
+
await reconcileManagedPreviews(store);
|
|
275
|
+
store.prune();
|
|
276
|
+
diagnostics = store.diagnostics();
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
store.close();
|
|
280
|
+
}
|
|
281
|
+
const result = {
|
|
282
|
+
configured: true,
|
|
283
|
+
baseURL: baseURL.toString(),
|
|
284
|
+
limits: {
|
|
285
|
+
...serverLimits,
|
|
286
|
+
maxProxyConnections: config.maxProxyConnections ?? DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS,
|
|
287
|
+
maxProxyConnectionsPerPreview: config.maxProxyConnectionsPerPreview ?? DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS_PER_PREVIEW,
|
|
288
|
+
},
|
|
289
|
+
diagnostics,
|
|
290
|
+
next: "pibo preview list",
|
|
291
|
+
};
|
|
292
|
+
if (options.json)
|
|
293
|
+
printJson(result);
|
|
294
|
+
else {
|
|
295
|
+
console.log("preview configuration\tok");
|
|
296
|
+
console.log(`baseURL\t${result.baseURL}`);
|
|
297
|
+
console.log(`active\t${diagnostics.activeExposures}`);
|
|
298
|
+
console.log(`managed\t${diagnostics.managedStartingOrRunning}`);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const store = createDefaultPreviewStore();
|
|
303
|
+
let exposure;
|
|
304
|
+
try {
|
|
305
|
+
await reconcileManagedPreviews(store);
|
|
306
|
+
exposure = store.requireExposure(id);
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
store.close();
|
|
310
|
+
}
|
|
311
|
+
const result = await exposureView(exposure);
|
|
312
|
+
if (options.json)
|
|
313
|
+
printJson(result);
|
|
314
|
+
else {
|
|
315
|
+
console.log(`${result.id}\t${result.health}`);
|
|
316
|
+
console.log(`target\t${result.targetHost}:${result.targetPort}`);
|
|
317
|
+
console.log(`url\t${result.publicUrl}`);
|
|
318
|
+
}
|
|
319
|
+
if (result.health !== "online")
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
});
|
|
322
|
+
const removeAction = async (id, options) => {
|
|
323
|
+
const store = createDefaultPreviewStore();
|
|
324
|
+
let exposure;
|
|
325
|
+
try {
|
|
326
|
+
const current = store.requireExposure(id);
|
|
327
|
+
if (current.managementMode === "managed" && ["running", "starting", "stopping", "error"].includes(current.serverState ?? "")) {
|
|
328
|
+
await stopManagedPreview(store, id);
|
|
329
|
+
}
|
|
330
|
+
exposure = store.closeExposure(id);
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
store.close();
|
|
334
|
+
}
|
|
335
|
+
if (!exposure)
|
|
336
|
+
throw new Error(`Preview "${id}" was not found`);
|
|
337
|
+
if (options.json)
|
|
338
|
+
printJson({ removed: true, preview: exposure });
|
|
339
|
+
else
|
|
340
|
+
console.log(`${id}\tremoved`);
|
|
341
|
+
};
|
|
342
|
+
program.command("remove").argument("<preview-id>").option("--json", "Print JSON").action(removeAction);
|
|
343
|
+
program.command("close").argument("<preview-id>").option("--json", "Print JSON").action(removeAction);
|
|
344
|
+
await program.parseAsync(argv);
|
|
345
|
+
}
|