@pasko70/pibo 3.0.1 → 3.1.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 (122) hide show
  1. package/README.md +20 -6
  2. package/compute-image/Dockerfile +47 -0
  3. package/compute-image/Dockerfile.dockerignore +5 -0
  4. package/dist/agent-runtime/context-build.js +26 -2
  5. package/dist/agent-runtime/contract.js +78 -28
  6. package/dist/agent-runtime/portable-history.js +19 -9
  7. package/dist/agent-runtime/registry.js +8 -2
  8. package/dist/agent-runtime/routed-session.js +120 -15
  9. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  10. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  11. package/dist/agent-runtimes/omp/adapter.js +3 -7
  12. package/dist/agent-runtimes/pi/adapter.js +5 -3
  13. package/dist/agent-runtimes/pi/routed-session.js +59 -12
  14. package/dist/agent-runtimes/pi/runtime.js +6 -0
  15. package/dist/apps/chat/agent-profiles.js +31 -2
  16. package/dist/apps/chat/agent-store.js +98 -6
  17. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  18. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  19. package/dist/apps/chat/data/project-service.js +119 -15
  20. package/dist/apps/chat/data/room-service.js +21 -0
  21. package/dist/apps/chat/data/session-query-service.js +33 -10
  22. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  23. package/dist/apps/chat/loop-api.js +10 -2
  24. package/dist/apps/chat/output-compactor.js +144 -21
  25. package/dist/apps/chat/output-event-policy.js +179 -2
  26. package/dist/apps/chat/stream.js +16 -7
  27. package/dist/apps/chat/trace-v2.js +1 -0
  28. package/dist/apps/chat/web-app.js +317 -86
  29. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  38. package/dist/apps/chat-vscode-web/index.html +1 -1
  39. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  40. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  41. package/dist/cli-session/localSessionSource.js +153 -161
  42. package/dist/cli.js +7 -11
  43. package/dist/compute/cli.js +35 -16
  44. package/dist/compute/docker.js +341 -94
  45. package/dist/config/config.js +3 -0
  46. package/dist/core/context-build.js +7 -16
  47. package/dist/core/output-persistence-retry.js +484 -0
  48. package/dist/core/output-render-sequence.js +331 -0
  49. package/dist/core/profiles.js +24 -0
  50. package/dist/core/provider-recovery.js +7 -1
  51. package/dist/core/session-model.js +37 -1
  52. package/dist/core/session-router.js +78 -110
  53. package/dist/data/ingest-service.js +91 -9
  54. package/dist/data/navigation-store.js +5 -5
  55. package/dist/data/pibo-store.js +8 -1
  56. package/dist/data/schema.js +243 -3
  57. package/dist/data/session-store.js +3 -3
  58. package/dist/debug/agents.js +91 -89
  59. package/dist/debug/index.js +46 -2
  60. package/dist/debug/pty.js +127 -48
  61. package/dist/gateway/cli.js +4 -0
  62. package/dist/gateway/web.js +8 -8
  63. package/dist/local/client.js +3 -2
  64. package/dist/loops/channel.js +3 -1
  65. package/dist/loops/cli.js +2 -1
  66. package/dist/loops/service.js +90 -11
  67. package/dist/loops/store.js +100 -74
  68. package/dist/mcp/agent-context.js +13 -26
  69. package/dist/mcp/commands/info.js +3 -1
  70. package/dist/mcp/config-command.js +46 -2
  71. package/dist/mcp/config.js +18 -4
  72. package/dist/plugins/context-files-store.js +36 -6
  73. package/dist/plugins/context-files.js +52 -1
  74. package/dist/plugins/registry.js +2 -0
  75. package/dist/plugins/user-profile-resources.js +22 -0
  76. package/dist/previews/base-url.js +34 -0
  77. package/dist/previews/config.js +2 -32
  78. package/dist/ralph/store.js +5 -0
  79. package/dist/reliability/store.js +378 -106
  80. package/dist/session-ui/terminalRows.js +8 -4
  81. package/dist/sessions/pibo-data-store.js +145 -0
  82. package/dist/sessions/store.js +42 -0
  83. package/dist/setup/cli.js +422 -6
  84. package/dist/setup/installation-profiles.js +464 -0
  85. package/dist/shared/deterministic-digest.js +94 -0
  86. package/dist/shared/trace-engine.js +54 -0
  87. package/dist/shared/trace-event-projection.js +124 -67
  88. package/dist/shared/trace-history.js +47 -12
  89. package/dist/shared/trace-live-reducer.js +23 -4
  90. package/dist/shared/trace-nodes.js +17 -28
  91. package/dist/shared/trace-order.js +41 -5
  92. package/dist/shared/trace-page-merge.js +44 -14
  93. package/dist/shared/trace-patch-nodes.js +2 -0
  94. package/dist/shared/trace-subagent-links.js +4 -1
  95. package/dist/shared/trace-tool-identity.js +15 -8
  96. package/dist/signals/projector.js +28 -6
  97. package/dist/subagents/observation-query.js +121 -0
  98. package/dist/subagents/tool.js +7 -3
  99. package/dist/tools/agent-browser-leases.js +58 -23
  100. package/dist/tools/agent-browser-wrapper.js +1 -1
  101. package/dist/tools/browser-use-leases.js +129 -16
  102. package/dist/tools/browser-use-wrapper.js +1 -1
  103. package/dist/tools/index.js +27 -11
  104. package/dist/tools/python-runtime.js +10 -3
  105. package/dist/tools/registry.js +1 -1
  106. package/dist/tools/runtime/node-backend.js +16 -5
  107. package/dist/tools/runtime/node-worker-source.js +112 -33
  108. package/dist/tools/runtime/python-backend.js +16 -5
  109. package/dist/tools/runtime/python-worker-source.js +167 -16
  110. package/dist/tools/runtime/registry.js +23 -10
  111. package/dist/user-skills/store.js +0 -1
  112. package/docs/ops/vscode-extension-release.md +9 -1
  113. package/npm-shrinkwrap.json +9 -2
  114. package/package.json +9 -2
  115. package/scripts/docker-entrypoint.sh +46 -0
  116. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  117. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  118. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  119. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  120. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
  121. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  122. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
@@ -68,6 +68,28 @@ async function withRegistryLock(status, action) {
68
68
  function isExpired(lease, now = new Date()) {
69
69
  return new Date(lease.expiresAt).getTime() <= now.getTime();
70
70
  }
71
+ function leaseTimestamp(lease) {
72
+ const updatedAt = Date.parse(lease.updatedAt);
73
+ if (Number.isFinite(updatedAt))
74
+ return updatedAt;
75
+ const createdAt = Date.parse(lease.createdAt);
76
+ return Number.isFinite(createdAt) ? createdAt : 0;
77
+ }
78
+ function deduplicateLeaseIdentities(registry) {
79
+ const leasesById = new Map();
80
+ for (const lease of registry.leases) {
81
+ const existing = leasesById.get(lease.id);
82
+ if (!existing) {
83
+ leasesById.set(lease.id, lease);
84
+ continue;
85
+ }
86
+ const preferLease = lease.status === 'active' && existing.status !== 'active'
87
+ || lease.status === existing.status && leaseTimestamp(lease) > leaseTimestamp(existing);
88
+ if (preferLease)
89
+ leasesById.set(lease.id, lease);
90
+ }
91
+ registry.leases = [...leasesById.values()];
92
+ }
71
93
  function copyFilter(source) {
72
94
  const base = source.split(/[\\/]/).pop() || '';
73
95
  return ![
@@ -114,6 +136,7 @@ export async function acquireAgentBrowserLease(status, options) {
114
136
  const maxSlots = options.maxSlots ?? 4;
115
137
  const result = await withRegistryLock(status, async () => {
116
138
  const registry = readRegistry(status);
139
+ deduplicateLeaseIdentities(registry);
117
140
  const now = new Date();
118
141
  for (const lease of registry.leases) {
119
142
  if (lease.status === 'active' && isExpired(lease, now)) {
@@ -131,34 +154,44 @@ export async function acquireAgentBrowserLease(status, options) {
131
154
  suggestion: 'Release a lease or rerun with a higher --max-slots value.',
132
155
  }));
133
156
  }
134
- const used = new Set(active.map((lease) => lease.slot));
135
- let slotNumber = 1;
136
- while (used.has(`slot-${String(slotNumber).padStart(3, '0')}`))
137
- slotNumber += 1;
138
- const slot = `slot-${String(slotNumber).padStart(3, '0')}`;
139
- const sessionName = `pibo-auth-${app}-${slot}`;
140
- const profileDir = leaseProfileDir(status, app, slot);
141
- const lease = {
142
- id: `${app}-${slot}`,
143
- app,
144
- holder,
145
- slot,
146
- sessionName,
147
- profileDir,
148
- status: 'active',
149
- createdAt: nowIso(),
150
- updatedAt: nowIso(),
151
- expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
152
- };
153
- await rm(profileDir, { recursive: true, force: true });
157
+ let lease = registry.leases.find((item) => item.app === app && item.status === 'released');
158
+ if (lease) {
159
+ lease.holder = holder;
160
+ lease.status = 'active';
161
+ lease.updatedAt = nowIso();
162
+ lease.expiresAt = new Date(now.getTime() + ttlMs).toISOString();
163
+ }
164
+ else {
165
+ const used = new Set(active.map((item) => item.slot));
166
+ let slotNumber = 1;
167
+ while (used.has(`slot-${String(slotNumber).padStart(3, '0')}`))
168
+ slotNumber += 1;
169
+ const slot = `slot-${String(slotNumber).padStart(3, '0')}`;
170
+ const sessionName = `pibo-auth-${app}-${slot}`;
171
+ const profileDir = leaseProfileDir(status, app, slot);
172
+ const createdAt = nowIso();
173
+ lease = {
174
+ id: `${app}-${slot}`,
175
+ app,
176
+ holder,
177
+ slot,
178
+ sessionName,
179
+ profileDir,
180
+ status: 'active',
181
+ createdAt,
182
+ updatedAt: createdAt,
183
+ expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
184
+ };
185
+ registry.leases.push(lease);
186
+ }
187
+ await rm(lease.profileDir, { recursive: true, force: true });
154
188
  const sourceTemplate = templateDir(status, app);
155
189
  if (existsSync(sourceTemplate)) {
156
- await cp(sourceTemplate, profileDir, { recursive: true, force: true, filter: copyFilter });
190
+ await cp(sourceTemplate, lease.profileDir, { recursive: true, force: true, filter: copyFilter });
157
191
  }
158
192
  else {
159
- await mkdir(profileDir, { recursive: true });
193
+ await mkdir(lease.profileDir, { recursive: true });
160
194
  }
161
- registry.leases.push(lease);
162
195
  await writeRegistry(status, registry);
163
196
  return lease;
164
197
  });
@@ -187,6 +220,7 @@ export async function listAgentBrowserLeases(status, json = false) {
187
220
  export async function releaseAgentBrowserLease(status, id, options) {
188
221
  await withRegistryLock(status, async () => {
189
222
  const registry = readRegistry(status);
223
+ deduplicateLeaseIdentities(registry);
190
224
  const lease = registry.leases.find((item) => item.id === id);
191
225
  if (!lease)
192
226
  throw new Error(`Agent Browser lease not found: ${id}`);
@@ -202,6 +236,7 @@ export async function reapStaleAgentBrowserLeases(status, json = false) {
202
236
  let released = 0;
203
237
  await withRegistryLock(status, async () => {
204
238
  const registry = readRegistry(status);
239
+ deduplicateLeaseIdentities(registry);
205
240
  const now = new Date();
206
241
  for (const lease of registry.leases) {
207
242
  if (lease.status === 'active' && isExpired(lease, now)) {
@@ -89,7 +89,7 @@ exec "$real_agent_browser" "\${args[@]}"
89
89
  `;
90
90
  }
91
91
  export function ensureAgentBrowserWrapper(status) {
92
- if (!status.homeDir || !status.executablePath)
92
+ if (!status.installed || !status.homeDir || !status.executablePath)
93
93
  return undefined;
94
94
  const wrapperPath = join(status.homeDir, 'bin', 'agent-browser');
95
95
  mkdirSync(dirname(wrapperPath), { recursive: true });
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { existsSync, readFileSync } from 'node:fs';
3
- import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { cp, mkdir, rm, stat, writeFile } from 'node:fs/promises';
4
5
  import { join, resolve } from 'node:path';
5
6
  import { ErrorCode, formatCliError } from '../cli-errors.js';
6
7
  import { BROWSER_USE_DEFAULT_PROFILE } from './browser-use-wrapper.js';
@@ -10,6 +11,8 @@ const DEFAULT_APP = 'pibo-chat';
10
11
  const DEFAULT_TTL_MINUTES = 8 * 60;
11
12
  const DEFAULT_TEMPLATE_PROFILE = 'pibo-auth-template';
12
13
  const AUTH_POOL_DIR = 'auth-pool';
14
+ const REGISTRY_LOCK_OWNER_FILE = 'owner.json';
15
+ const LEGACY_REGISTRY_LOCK_STALE_MS = 10 * 60_000;
13
16
  function shellQuote(value) {
14
17
  return `'${value.replaceAll("'", "'\\''")}'`;
15
18
  }
@@ -32,6 +35,9 @@ function registryPath(status) {
32
35
  function lockDir(status) {
33
36
  return join(status.homeDir, AUTH_POOL_DIR, '.leases.lock');
34
37
  }
38
+ function lockOwnerPath(lock) {
39
+ return join(lock, REGISTRY_LOCK_OWNER_FILE);
40
+ }
35
41
  function appPoolDir(status, app) {
36
42
  return join(status.homeDir, AUTH_POOL_DIR, sanitizeIdPart(app));
37
43
  }
@@ -110,29 +116,135 @@ async function writeRegistry(status, registry) {
110
116
  async function withRegistryLock(status, action) {
111
117
  const lock = lockDir(status);
112
118
  await mkdir(join(status.homeDir, AUTH_POOL_DIR), { recursive: true });
113
- let acquired = false;
119
+ const token = randomUUID();
114
120
  for (let attempt = 0; attempt < 50; attempt += 1) {
115
121
  try {
116
122
  await mkdir(lock);
117
- acquired = true;
123
+ const processStartId = currentProcessStartId();
124
+ try {
125
+ await writeFile(lockOwnerPath(lock), `${JSON.stringify({
126
+ pid: process.pid,
127
+ token,
128
+ acquiredAt: Date.now(),
129
+ ...(processStartId ? { processStartId } : {}),
130
+ })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
131
+ }
132
+ catch (error) {
133
+ await rm(lock, { recursive: true, force: true });
134
+ throw error;
135
+ }
118
136
  break;
119
137
  }
120
138
  catch (error) {
121
139
  if (error.code !== 'EEXIST')
122
140
  throw error;
141
+ await recoverStaleRegistryLock(lock);
142
+ if (!existsSync(lock))
143
+ continue;
123
144
  await new Promise((resolve) => setTimeout(resolve, 50));
124
145
  }
125
146
  }
126
- if (!acquired) {
147
+ if (!await registryLockOwnedBy(lock, token)) {
127
148
  throw new Error('Timed out waiting for browser-use lease registry lock');
128
149
  }
129
150
  try {
130
151
  return await action();
131
152
  }
132
153
  finally {
154
+ await releaseRegistryLock(lock, token);
155
+ }
156
+ }
157
+ function currentProcessStartId() {
158
+ if (process.platform !== 'linux')
159
+ return undefined;
160
+ const identity = linuxProcessIdentity(process.pid);
161
+ return identity.liveness === 'active' ? identity.startId : undefined;
162
+ }
163
+ function linuxProcessIdentity(pid) {
164
+ let raw;
165
+ try {
166
+ raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
167
+ }
168
+ catch (error) {
169
+ const code = error.code;
170
+ return code === 'ENOENT' || code === 'ESRCH' ? { liveness: 'dead' } : { liveness: 'ambiguous' };
171
+ }
172
+ const commandEnd = raw.lastIndexOf(')');
173
+ if (commandEnd < 2 || commandEnd + 2 >= raw.length)
174
+ return { liveness: 'ambiguous' };
175
+ const fields = raw.slice(commandEnd + 2).trim().split(/\s+/);
176
+ if (fields[0] === 'Z')
177
+ return { liveness: 'dead' };
178
+ const startTicks = fields[19];
179
+ if (!startTicks || !/^[1-9][0-9]*$/.test(startTicks))
180
+ return { liveness: 'ambiguous' };
181
+ return { liveness: 'active', startId: `${pid}:${startTicks}` };
182
+ }
183
+ function validRegistryLockOwner(value) {
184
+ if (!value || typeof value !== 'object' || Array.isArray(value))
185
+ return undefined;
186
+ const owner = value;
187
+ if (!Number.isSafeInteger(owner.pid)
188
+ || (owner.pid ?? 0) <= 0
189
+ || typeof owner.token !== 'string'
190
+ || owner.token.length === 0
191
+ || !Number.isFinite(owner.acquiredAt)
192
+ || (owner.processStartId !== undefined && typeof owner.processStartId !== 'string'))
193
+ return undefined;
194
+ return owner;
195
+ }
196
+ function readRegistryLockOwner(lock) {
197
+ try {
198
+ return validRegistryLockOwner(JSON.parse(readFileSync(lockOwnerPath(lock), 'utf8')));
199
+ }
200
+ catch {
201
+ return undefined;
202
+ }
203
+ }
204
+ function registryLockOwnerLiveness(owner) {
205
+ if (process.platform === 'linux') {
206
+ const identity = linuxProcessIdentity(owner.pid);
207
+ if (identity.liveness !== 'active')
208
+ return identity.liveness;
209
+ if (!owner.processStartId || !identity.startId)
210
+ return 'ambiguous';
211
+ return owner.processStartId === identity.startId ? 'active' : 'dead';
212
+ }
213
+ try {
214
+ process.kill(owner.pid, 0);
215
+ return 'active';
216
+ }
217
+ catch (error) {
218
+ return error.code === 'ESRCH' ? 'dead' : 'ambiguous';
219
+ }
220
+ }
221
+ async function registryLockOwnedBy(lock, token) {
222
+ return readRegistryLockOwner(lock)?.token === token;
223
+ }
224
+ async function releaseRegistryLock(lock, token) {
225
+ if (await registryLockOwnedBy(lock, token)) {
133
226
  await rm(lock, { recursive: true, force: true });
134
227
  }
135
228
  }
229
+ async function recoverStaleRegistryLock(lock) {
230
+ const owner = readRegistryLockOwner(lock);
231
+ if (owner) {
232
+ if (registryLockOwnerLiveness(owner) === 'dead' && readRegistryLockOwner(lock)?.token === owner.token) {
233
+ await rm(lock, { recursive: true, force: true });
234
+ }
235
+ return;
236
+ }
237
+ try {
238
+ const lockStat = await stat(lock);
239
+ if (Date.now() - lockStat.mtimeMs > LEGACY_REGISTRY_LOCK_STALE_MS) {
240
+ await rm(lock, { recursive: true, force: true });
241
+ }
242
+ }
243
+ catch (error) {
244
+ if (error.code !== 'ENOENT')
245
+ throw error;
246
+ }
247
+ }
136
248
  export function isExpired(lease, now = new Date()) {
137
249
  return new Date(lease.expiresAt).getTime() <= now.getTime();
138
250
  }
@@ -346,7 +458,7 @@ export async function acquireBrowserUseLease(context, options = {}) {
346
458
  const ttlMinutes = options.ttlMinutes ?? DEFAULT_TTL_MINUTES;
347
459
  const profileName = options.profileName?.trim() || BROWSER_USE_DEFAULT_PROFILE;
348
460
  const templateDir = options.templateDir?.trim() || defaultTemplateDir(context.status);
349
- await withRegistryLock(context.status, async () => {
461
+ const { lease, reapedCount } = await withRegistryLock(context.status, async () => {
350
462
  const registry = readRegistry(context.status);
351
463
  const reapedCount = await reapStaleLeasesInRegistry(context.status, registry);
352
464
  let lease = selectReusableLease(context.status, registry.leases, app);
@@ -380,18 +492,19 @@ export async function acquireBrowserUseLease(context, options = {}) {
380
492
  registry.leases.push(lease);
381
493
  }
382
494
  await writeRegistry(context.status, registry);
383
- if (reapedCount > 0 && !options.json) {
384
- console.error(`Reaped ${reapedCount} stale lease${reapedCount === 1 ? '' : 's'}`);
385
- }
386
- if (options.json)
387
- printLeaseJson(context.status, lease);
388
- else
389
- printLeaseEnv(context.status, lease);
390
- const warmup = await warmupBrowserUseLease(context, lease, 15000, options.headed);
391
- if (!warmup.success && !options.json) {
392
- console.error(`Warning: Browser warm-up failed: ${warmup.error?.trim() || 'unknown error'}`);
393
- }
495
+ return { lease, reapedCount };
394
496
  });
497
+ if (reapedCount > 0 && !options.json) {
498
+ console.error(`Reaped ${reapedCount} stale lease${reapedCount === 1 ? '' : 's'}`);
499
+ }
500
+ if (options.json)
501
+ printLeaseJson(context.status, lease);
502
+ else
503
+ printLeaseEnv(context.status, lease);
504
+ const warmup = await warmupBrowserUseLease(context, lease, 15000, options.headed);
505
+ if (!warmup.success && !options.json) {
506
+ console.error(`Warning: Browser warm-up failed: ${warmup.error?.trim() || 'unknown error'}`);
507
+ }
395
508
  }
396
509
  async function warmupBrowserUseLease(context, lease, timeoutMs = 15000, headed = false) {
397
510
  const wrapperPath = join(context.status.homeDir, 'bin', 'browser-use');
@@ -771,7 +771,7 @@ exec "$real_browser_use" "$@"
771
771
  `;
772
772
  }
773
773
  export function ensureBrowserUseWrapper(status) {
774
- if (status.entry.name !== 'browser-use')
774
+ if (!status.installed || status.entry.name !== 'browser-use')
775
775
  return undefined;
776
776
  const wrapperPath = join(status.homeDir, 'bin', 'browser-use');
777
777
  mkdirSync(dirname(wrapperPath), { recursive: true });
@@ -60,17 +60,21 @@ function printShow(name) {
60
60
  const wrapperPath = ensureBrowserUseWrapper(status);
61
61
  console.log(` wrapper: ${wrapperPath ?? 'not generated'}`);
62
62
  console.log(` executable: ${status.executablePath}`);
63
- console.log('');
64
- console.log(' IMPORTANT: Always use the wrapper path above, not the raw executable.');
65
- console.log(' The wrapper manages persistent Chrome profiles and CDP automatically.');
63
+ if (wrapperPath) {
64
+ console.log('');
65
+ console.log(' IMPORTANT: Always use the wrapper path above, not the raw executable.');
66
+ console.log(' The wrapper manages persistent Chrome profiles and CDP automatically.');
67
+ }
66
68
  }
67
69
  else if (entry.name === 'agent-browser') {
68
70
  const wrapperPath = ensureAgentBrowserWrapper(status);
69
71
  console.log(` wrapper: ${wrapperPath ?? 'not generated'}`);
70
72
  console.log(` executable: ${status.executablePath}`);
71
- console.log('');
72
- console.log(' IMPORTANT: Always use the wrapper path above, not the raw executable.');
73
- console.log(' The wrapper keeps Agent Browser state under the Pibo tool home.');
73
+ if (wrapperPath) {
74
+ console.log('');
75
+ console.log(' IMPORTANT: Always use the wrapper path above, not the raw executable.');
76
+ console.log(' The wrapper keeps Agent Browser state under the Pibo tool home.');
77
+ }
74
78
  }
75
79
  else {
76
80
  console.log(` executable: ${status.executablePath}`);
@@ -88,8 +92,9 @@ function printShow(name) {
88
92
  }
89
93
  console.log('');
90
94
  console.log('Next:');
91
- if (entry.kind !== 'internal')
92
- console.log(` pibo tools env ${entry.name}`);
95
+ if (entry.kind !== 'internal') {
96
+ console.log(status.installed ? ` pibo tools env ${entry.name}` : ` pibo tools install ${entry.name}`);
97
+ }
93
98
  console.log(` pibo tools guide ${entry.name} ${entry.guides[0]?.name ?? ''}`.trimEnd());
94
99
  if (entry.name === 'browser-use') {
95
100
  console.log(' pibo tools browser-use');
@@ -133,9 +138,21 @@ function printGuide(name, guideName) {
133
138
  }
134
139
  console.log(guide.content);
135
140
  }
136
- function printPath(name) {
141
+ function requireInstalledTool(name) {
137
142
  const entry = requireEntry(name);
138
143
  const status = getCliToolStatus(entry);
144
+ if (!status.installed) {
145
+ throw new Error(formatCliError({
146
+ code: ErrorCode.CLIENT_ERROR,
147
+ type: 'CLI_TOOL_NOT_INSTALLED',
148
+ message: `Tool "${name}" is not installed`,
149
+ suggestion: `Run pibo tools install ${name}.`,
150
+ }));
151
+ }
152
+ return { entry, status };
153
+ }
154
+ function printPath(name) {
155
+ const { entry, status } = requireInstalledTool(name);
139
156
  if (entry.name === 'browser-use') {
140
157
  const wrapperPath = ensureBrowserUseWrapper(status);
141
158
  if (wrapperPath) {
@@ -153,8 +170,7 @@ function printPath(name) {
153
170
  console.log(status.executablePath);
154
171
  }
155
172
  function printEnv(name) {
156
- const entry = requireEntry(name);
157
- const status = getCliToolStatus(entry);
173
+ const { entry, status } = requireInstalledTool(name);
158
174
  const desktop = detectDesktopEnv();
159
175
  if (entry.kind === 'internal') {
160
176
  console.log(`# ${entry.name} is built into pibo; no extra environment is required.`);
@@ -52,7 +52,7 @@ function runBuffered(command, args, env = process.env) {
52
52
  function isRootUser() {
53
53
  return typeof process.getuid === 'function' && process.getuid() === 0;
54
54
  }
55
- async function resolveUvCommand() {
55
+ async function findUvCommand() {
56
56
  const uv = await runBuffered('uv', ['--version']);
57
57
  if (uv.ok)
58
58
  return 'uv';
@@ -62,7 +62,14 @@ async function resolveUvCommand() {
62
62
  if (local.ok)
63
63
  return localUv;
64
64
  }
65
+ return undefined;
66
+ }
67
+ async function ensureUvCommand() {
68
+ const uvCommand = await findUvCommand();
69
+ if (uvCommand)
70
+ return uvCommand;
65
71
  if (process.platform === 'linux' && isRootUser()) {
72
+ const localUv = join(homedir(), '.local', 'bin', 'uv');
66
73
  console.log('uv was not found on PATH. Installing uv automatically.');
67
74
  await runInheritedCommand('sh', ['-c', 'curl -LsSf https://astral.sh/uv/install.sh | sh']);
68
75
  const installed = await runBuffered(localUv, ['--version'], {
@@ -107,7 +114,7 @@ export function runInheritedCommand(command, args, env = process.env) {
107
114
  }
108
115
  export async function printToolPythonRuntimeDoctor(name, spec) {
109
116
  const paths = getToolPythonRuntimePaths(name, spec);
110
- const uvCommand = await resolveUvCommand();
117
+ const uvCommand = await findUvCommand();
111
118
  const uv = uvCommand ? await runBuffered(uvCommand, ['--version']) : { ok: false, output: 'missing' };
112
119
  const python = uv.ok
113
120
  ? await runBuffered(uvCommand, ['python', 'find', spec.pythonVersion])
@@ -148,7 +155,7 @@ export async function printToolPythonRuntimeDoctor(name, spec) {
148
155
  }
149
156
  }
150
157
  export async function installToolPythonRuntime(name, spec) {
151
- const uvCommand = await resolveUvCommand();
158
+ const uvCommand = await ensureUvCommand();
152
159
  if (!uvCommand) {
153
160
  throw new Error(formatCliError({
154
161
  code: ErrorCode.CLIENT_ERROR,
@@ -220,7 +220,7 @@ export async function installCliTool(entry, runSetup) {
220
220
  if ((entry.name === 'browser-use' || entry.name === 'agent-browser') && process.platform === 'linux' && !hasDesktopDisplay(detectDesktopEnv())) {
221
221
  printLinuxVirtualDisplayHint(' ');
222
222
  }
223
- console.log(` env: pibo tools env ${entry.name}`);
223
+ console.log(status.installed ? ` env: pibo tools env ${entry.name}` : ` install: pibo tools install ${entry.name}`);
224
224
  }
225
225
  export async function removeCliTool(entry) {
226
226
  if (entry.kind === 'internal') {
@@ -46,13 +46,23 @@ export class NodeRuntimeBackend {
46
46
  this.child = spawn(executable, [...args, "-e", NODE_RUNTIME_WORKER_SOURCE], {
47
47
  cwd,
48
48
  env: { ...process.env, ...(env ?? {}) },
49
- stdio: "pipe",
49
+ stdio: ["pipe", "pipe", "pipe", "pipe"],
50
+ });
51
+ const protocol = this.child.stdio[3];
52
+ if (!protocol)
53
+ throw new Error("Node runtime protocol pipe was not created");
54
+ const responses = createInterface({ input: protocol });
55
+ responses.on("line", (line) => this.handleLine(line));
56
+ this.child.stdout.on("data", (chunk) => {
57
+ this.diagnostics += String(chunk);
50
58
  });
51
- const stdout = createInterface({ input: this.child.stdout });
52
- stdout.on("line", (line) => this.handleLine(line));
53
59
  this.child.stderr.on("data", (chunk) => {
54
60
  this.diagnostics += String(chunk);
55
61
  });
62
+ this.child.stdin.on("error", (error) => {
63
+ this.alive = false;
64
+ this.rejectAll(error);
65
+ });
56
66
  this.child.once("error", (error) => {
57
67
  this.alive = false;
58
68
  this.readyReject(error);
@@ -78,7 +88,7 @@ export class NodeRuntimeBackend {
78
88
  }
79
89
  }
80
90
  isAlive() {
81
- return this.alive && !this.child.killed;
91
+ return this.alive;
82
92
  }
83
93
  getRecord() {
84
94
  return { pid: this.child.pid, cwd: this.cwd, executable: this.executable };
@@ -146,7 +156,8 @@ export class NodeRuntimeBackend {
146
156
  async interrupt() {
147
157
  if (!this.isAlive())
148
158
  return { status: "failed", sessionId: "", message: "Runtime worker is not alive" };
149
- this.child.kill("SIGINT");
159
+ if (!this.child.kill("SIGINT"))
160
+ return { status: "failed", sessionId: "", message: "Runtime worker is not alive" };
150
161
  return { status: "ok", sessionId: "", message: "Sent SIGINT to runtime worker" };
151
162
  }
152
163
  async close(force = false) {