@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.
Files changed (104) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +307 -31
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/models.js +48 -1
  10. package/dist/agent-runtimes/codex-native/process.js +15 -1
  11. package/dist/agent-runtimes/codex-native/thread.js +75 -31
  12. package/dist/agent-runtimes/codex-native/turn.js +30 -0
  13. package/dist/agent-runtimes/history-proof.js +8 -0
  14. package/dist/agent-runtimes/omp/adapter.js +61 -14
  15. package/dist/agent-runtimes/omp/history.js +189 -34
  16. package/dist/agent-runtimes/omp/thread.js +82 -36
  17. package/dist/agent-runtimes/pi/adapter.js +30 -3
  18. package/dist/agent-runtimes/pi/history.js +66 -5
  19. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  20. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  21. package/dist/apps/chat/agent-store.js +3 -0
  22. package/dist/apps/chat/chat-api-routes.js +1 -1
  23. package/dist/apps/chat/chat-files.js +130 -9
  24. package/dist/apps/chat/chat-request-normalizers.js +6 -0
  25. package/dist/apps/chat/chat-settings-routes.js +27 -2
  26. package/dist/apps/chat/chat-speech.js +100 -0
  27. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  28. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  29. package/dist/apps/chat/trace-v2.js +210 -8
  30. package/dist/apps/chat/trace.js +3 -0
  31. package/dist/apps/chat/web-app.js +108 -3
  32. package/dist/apps/chat-ui/assets/{dist-GS8xUdfg.js → dist-BBDMeU5-.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BpVSf6ZV.js → dist-BDIATCQt.js} +1 -1
  34. package/dist/apps/chat-ui/assets/{dist-CRv7fzi3.js → dist-JnW4v8UE.js} +1 -1
  35. package/dist/apps/chat-ui/assets/{dist-CbBXbGVi.js → dist-lRiLP9rr.js} +1 -1
  36. package/dist/apps/chat-ui/assets/{dist-HC0xQ92C.js → dist-mqCviI-c.js} +1 -1
  37. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  38. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  39. package/dist/apps/chat-ui/index.html +2 -2
  40. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  41. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  42. package/dist/apps/chat-vscode-web/index.html +2 -2
  43. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  44. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
  45. package/dist/cli.js +49 -5
  46. package/dist/config/config.js +59 -8
  47. package/dist/core/preview-server-settings.js +20 -0
  48. package/dist/core/profiles.js +1 -0
  49. package/dist/core/session-router.js +476 -44
  50. package/dist/core/user-settings.js +10 -0
  51. package/dist/data/payload-store.js +57 -1
  52. package/dist/data/schema.js +18 -1
  53. package/dist/debug/trace.js +5 -0
  54. package/dist/gateway/client.js +368 -65
  55. package/dist/gateway/protocol.js +2 -0
  56. package/dist/gateway/server.js +8 -0
  57. package/dist/gateway/web.js +2 -0
  58. package/dist/index.js +1 -1
  59. package/dist/loops/store.js +6 -4
  60. package/dist/mcp/config.js +89 -17
  61. package/dist/mcp/daemon-client.js +473 -153
  62. package/dist/mcp/daemon.js +401 -37
  63. package/dist/plugins/builtin.js +2 -2
  64. package/dist/plugins/codex-native.js +2 -0
  65. package/dist/plugins/registry.js +260 -1
  66. package/dist/previews/cli.js +345 -0
  67. package/dist/previews/config.js +61 -0
  68. package/dist/previews/manager.js +644 -0
  69. package/dist/previews/network.js +198 -0
  70. package/dist/previews/plugin.js +11 -0
  71. package/dist/previews/process-supervisor.js +38 -0
  72. package/dist/previews/proxy.js +387 -0
  73. package/dist/previews/store.js +750 -0
  74. package/dist/previews/types.js +1 -0
  75. package/dist/previews/web-app.js +446 -0
  76. package/dist/runs/registry.js +22 -0
  77. package/dist/runs/tools.js +46 -7
  78. package/dist/session-ui/terminalRows.js +187 -13
  79. package/dist/sessions/pibo-data-store.js +37 -0
  80. package/dist/sessions/runtime-binding-persistence.js +34 -0
  81. package/dist/sessions/runtime-binding.js +6 -0
  82. package/dist/sessions/sqlite-store.js +37 -0
  83. package/dist/shared/trace-engine.js +12 -64
  84. package/dist/shared/trace-event-projection.js +16 -0
  85. package/dist/shared/trace-history.js +684 -184
  86. package/dist/shared/trace-limits.js +7 -0
  87. package/dist/shared/trace-live-patch.js +61 -0
  88. package/dist/shared/trace-tool-identity.js +43 -0
  89. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  90. package/dist/speech/openai-codex.js +407 -0
  91. package/dist/speech/types.js +8 -0
  92. package/dist/subagents/context.js +5 -3
  93. package/dist/subagents/observations.js +94 -6
  94. package/dist/subagents/tool.js +103 -20
  95. package/dist/tools/codex-image-artifacts.js +24 -0
  96. package/dist/tools/codex-image-generation.js +4 -22
  97. package/dist/web/channel.js +88 -4
  98. package/dist/web/http.js +2 -1
  99. package/npm-shrinkwrap.json +2 -2
  100. package/package.json +1 -1
  101. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  102. package/dist/apps/chat-ui/assets/index-DT5jCQBP.js +0 -228
  103. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  104. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.1.vsix +0 -0
@@ -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
+ }