@pasko70/pibo 3.0.2 → 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.
- package/compute-image/Dockerfile +47 -0
- package/compute-image/Dockerfile.dockerignore +5 -0
- package/dist/agent-runtime/contract.js +78 -28
- package/dist/agent-runtime/portable-history.js +19 -9
- package/dist/agent-runtime/registry.js +8 -2
- package/dist/agent-runtime/routed-session.js +12 -6
- package/dist/agent-runtimes/pi/routed-session.js +10 -4
- package/dist/apps/chat/agent-store.js +55 -3
- package/dist/apps/chat/data/project-service.js +119 -15
- package/dist/apps/chat/data/room-service.js +21 -0
- package/dist/apps/chat/loop-api.js +10 -2
- package/dist/apps/chat/web-app.js +84 -25
- package/dist/apps/chat-ui/assets/{dist-CvaPTBTN.js → dist-CUQJqggN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BPotHecb.js → dist-ClUlQWYN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BzqQKeVO.js → dist-Djul9BmZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-paagejNj.js → dist-GD0JGdCi.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B6GZgWSj.js → dist-W5HOqHym.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
- package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/cli-ui/InkSessionApp.js +111 -12
- package/dist/compute/cli.js +35 -16
- package/dist/compute/docker.js +341 -94
- package/dist/config/config.js +3 -0
- package/dist/core/session-router.js +33 -105
- package/dist/data/pibo-store.js +8 -1
- package/dist/data/schema.js +125 -5
- package/dist/debug/agents.js +91 -89
- package/dist/debug/index.js +2 -0
- package/dist/debug/pty.js +127 -48
- package/dist/gateway/cli.js +4 -0
- package/dist/loops/channel.js +3 -1
- package/dist/loops/cli.js +2 -1
- package/dist/loops/service.js +90 -11
- package/dist/loops/store.js +100 -74
- package/dist/mcp/agent-context.js +13 -26
- package/dist/mcp/commands/info.js +3 -1
- package/dist/mcp/config-command.js +46 -2
- package/dist/mcp/config.js +18 -4
- package/dist/plugins/context-files-store.js +6 -6
- package/dist/previews/base-url.js +34 -0
- package/dist/previews/config.js +2 -32
- package/dist/ralph/store.js +5 -0
- package/dist/reliability/store.js +174 -111
- package/dist/session-ui/terminalRows.js +2 -1
- package/dist/subagents/observation-query.js +121 -0
- package/dist/tools/agent-browser-leases.js +58 -23
- package/dist/tools/agent-browser-wrapper.js +1 -1
- package/dist/tools/browser-use-leases.js +129 -16
- package/dist/tools/browser-use-wrapper.js +1 -1
- package/dist/tools/index.js +27 -11
- package/dist/tools/python-runtime.js +10 -3
- package/dist/tools/registry.js +1 -1
- package/dist/tools/runtime/node-backend.js +16 -5
- package/dist/tools/runtime/node-worker-source.js +112 -33
- package/dist/tools/runtime/python-backend.js +16 -5
- package/dist/tools/runtime/python-worker-source.js +167 -16
- package/dist/tools/runtime/registry.js +23 -10
- package/dist/user-skills/store.js +0 -1
- package/npm-shrinkwrap.json +9 -2
- package/package.json +8 -2
- package/scripts/docker-entrypoint.sh +46 -0
- package/scripts/prepare-agent-browser-wrapper.sh +70 -0
- package/scripts/prepare-browser-use-wrapper.sh +255 -0
- package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
- package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
- package/dist/apps/chat-vscode-web/assets/index-U-MSErGa.js +0 -43
|
@@ -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
|
-
|
|
119
|
+
const token = randomUUID();
|
|
114
120
|
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
115
121
|
try {
|
|
116
122
|
await mkdir(lock);
|
|
117
|
-
|
|
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 (!
|
|
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
|
-
|
|
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 });
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
158
|
+
const uvCommand = await ensureUvCommand();
|
|
152
159
|
if (!uvCommand) {
|
|
153
160
|
throw new Error(formatCliError({
|
|
154
161
|
code: ErrorCode.CLIENT_ERROR,
|
package/dist/tools/registry.js
CHANGED
|
@@ -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
|
|
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) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export const NODE_RUNTIME_WORKER_SOURCE = String.raw `
|
|
2
2
|
const vm = require("node:vm");
|
|
3
3
|
const util = require("node:util");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const childProcess = require("node:child_process");
|
|
6
|
+
const { AsyncLocalStorage } = require("node:async_hooks");
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
const outputStorage = new AsyncLocalStorage();
|
|
6
9
|
|
|
7
10
|
function bounded(value, maxBytes = 8192) {
|
|
8
11
|
const text = String(value);
|
|
@@ -48,14 +51,91 @@ function errorSummary(error) {
|
|
|
48
51
|
return out;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
function appendOutput(output, stream, chunk) {
|
|
55
|
+
if (output) output[stream] += String(chunk);
|
|
56
|
+
else if (stream === "stdout") process.stdout.write(String(chunk));
|
|
57
|
+
else process.stderr.write(String(chunk));
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
function appendStdout(chunk) {
|
|
52
|
-
|
|
61
|
+
appendOutput(outputStorage.getStore(), "stdout", chunk);
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
function appendStderr(chunk) {
|
|
56
|
-
|
|
65
|
+
appendOutput(outputStorage.getStore(), "stderr", chunk);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function routedStdio(stdio) {
|
|
69
|
+
if (stdio === "inherit") {
|
|
70
|
+
return { stdio: ["inherit", "pipe", "pipe"], stdout: true, stderr: true };
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(stdio)) return undefined;
|
|
73
|
+
const next = [...stdio];
|
|
74
|
+
const stdout = next[1] === "inherit" || next[1] === 1;
|
|
75
|
+
const stderr = next[2] === "inherit" || next[2] === 2;
|
|
76
|
+
if (!stdout && !stderr) return undefined;
|
|
77
|
+
if (stdout) next[1] = "pipe";
|
|
78
|
+
if (stderr) next[2] = "pipe";
|
|
79
|
+
return { stdio: next, stdout, stderr };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function captureSpawnedOutput(child, output, route) {
|
|
83
|
+
if (route.stdout && child.stdout) child.stdout.on("data", (chunk) => appendOutput(output, "stdout", chunk));
|
|
84
|
+
if (route.stderr && child.stderr) child.stderr.on("data", (chunk) => appendOutput(output, "stderr", chunk));
|
|
85
|
+
if (child.spawnargs && child.spawnargs.length > 0 && child.stdout && typeof child.stdout.unref === "function") child.stdout.unref();
|
|
86
|
+
if (child.spawnargs && child.spawnargs.length > 0 && child.stderr && typeof child.stderr.unref === "function") child.stderr.unref();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function routedSpawn(command, args, options) {
|
|
90
|
+
const hasArgs = Array.isArray(args);
|
|
91
|
+
const actualArgs = hasArgs ? args : [];
|
|
92
|
+
const actualOptions = (hasArgs ? options : args) || {};
|
|
93
|
+
const output = outputStorage.getStore();
|
|
94
|
+
const route = output ? routedStdio(actualOptions.stdio) : undefined;
|
|
95
|
+
if (!route) return hasArgs ? childProcess.spawn(command, actualArgs, actualOptions) : childProcess.spawn(command, actualOptions);
|
|
96
|
+
const child = childProcess.spawn(command, actualArgs, { ...actualOptions, stdio: route.stdio });
|
|
97
|
+
captureSpawnedOutput(child, output, route);
|
|
98
|
+
return child;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function routedSpawnSync(command, args, options) {
|
|
102
|
+
const hasArgs = Array.isArray(args);
|
|
103
|
+
const actualArgs = hasArgs ? args : [];
|
|
104
|
+
const actualOptions = (hasArgs ? options : args) || {};
|
|
105
|
+
const output = outputStorage.getStore();
|
|
106
|
+
const route = output ? routedStdio(actualOptions.stdio) : undefined;
|
|
107
|
+
if (!route) return hasArgs ? childProcess.spawnSync(command, actualArgs, actualOptions) : childProcess.spawnSync(command, actualOptions);
|
|
108
|
+
const result = childProcess.spawnSync(command, actualArgs, { ...actualOptions, stdio: route.stdio });
|
|
109
|
+
if (route.stdout && result.stdout != null) appendOutput(output, "stdout", result.stdout);
|
|
110
|
+
if (route.stderr && result.stderr != null) appendOutput(output, "stderr", result.stderr);
|
|
111
|
+
if (route.stdout) {
|
|
112
|
+
result.stdout = null;
|
|
113
|
+
if (result.output) result.output[1] = null;
|
|
114
|
+
}
|
|
115
|
+
if (route.stderr) {
|
|
116
|
+
result.stderr = null;
|
|
117
|
+
if (result.output) result.output[2] = null;
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
57
120
|
}
|
|
58
121
|
|
|
122
|
+
const childProcessProxy = new Proxy(childProcess, {
|
|
123
|
+
get(target, prop, receiver) {
|
|
124
|
+
if (prop === "spawn") return routedSpawn;
|
|
125
|
+
if (prop === "spawnSync") return routedSpawnSync;
|
|
126
|
+
return Reflect.get(target, prop, receiver);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function runtimeRequire(specifier) {
|
|
131
|
+
if (specifier === "child_process" || specifier === "node:child_process") return childProcessProxy;
|
|
132
|
+
return require(specifier);
|
|
133
|
+
}
|
|
134
|
+
runtimeRequire.resolve = require.resolve;
|
|
135
|
+
runtimeRequire.cache = require.cache;
|
|
136
|
+
runtimeRequire.extensions = require.extensions;
|
|
137
|
+
runtimeRequire.main = require.main;
|
|
138
|
+
|
|
59
139
|
const processProxy = new Proxy(process, {
|
|
60
140
|
get(target, prop, receiver) {
|
|
61
141
|
if (prop === "stdout") return { write: (chunk) => { appendStdout(chunk); return true; } };
|
|
@@ -75,7 +155,7 @@ const consoleProxy = {
|
|
|
75
155
|
|
|
76
156
|
const context = vm.createContext({
|
|
77
157
|
console: consoleProxy,
|
|
78
|
-
require,
|
|
158
|
+
require: runtimeRequire,
|
|
79
159
|
process: processProxy,
|
|
80
160
|
Buffer,
|
|
81
161
|
URL,
|
|
@@ -97,34 +177,33 @@ async function execute(req) {
|
|
|
97
177
|
const mode = req.mode || "exec";
|
|
98
178
|
const code = req.code || "";
|
|
99
179
|
const output = { stdout: "", stderr: "" };
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
180
|
+
return await outputStorage.run(output, async () => {
|
|
181
|
+
try {
|
|
182
|
+
const value = await (async () => {
|
|
183
|
+
if (mode === "eval") {
|
|
184
|
+
return await vm.runInContext(code, context, { filename: "<pibo-runtime>", timeout: Number(req.timeoutMs || 30000) });
|
|
185
|
+
}
|
|
186
|
+
const result = vm.runInContext(code, context, { filename: "<pibo-runtime>", timeout: Number(req.timeoutMs || 30000) });
|
|
187
|
+
if (result && typeof result.then === "function") await result;
|
|
188
|
+
return undefined;
|
|
189
|
+
})();
|
|
190
|
+
return {
|
|
191
|
+
id: req.id,
|
|
192
|
+
status: "ok",
|
|
193
|
+
stdout: output.stdout,
|
|
194
|
+
stderr: output.stderr,
|
|
195
|
+
result: value === undefined ? null : summarize(value),
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return {
|
|
199
|
+
id: req.id,
|
|
200
|
+
status: "error",
|
|
201
|
+
stdout: output.stdout,
|
|
202
|
+
stderr: output.stderr,
|
|
203
|
+
error: errorSummary(error),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
});
|
|
128
207
|
}
|
|
129
208
|
|
|
130
209
|
async function inspectValue(req) {
|
|
@@ -167,7 +246,7 @@ function listVars(req) {
|
|
|
167
246
|
}
|
|
168
247
|
|
|
169
248
|
function writeResponse(resp) {
|
|
170
|
-
|
|
249
|
+
fs.writeSync(3, JSON.stringify(resp) + "\n");
|
|
171
250
|
}
|
|
172
251
|
|
|
173
252
|
async function handle(req) {
|
|
@@ -46,13 +46,23 @@ export class PythonRuntimeBackend {
|
|
|
46
46
|
this.child = spawn(executable, [...args, "-u", "-c", PYTHON_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("Python 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 PythonRuntimeBackend {
|
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
isAlive() {
|
|
81
|
-
return this.alive
|
|
91
|
+
return this.alive;
|
|
82
92
|
}
|
|
83
93
|
getRecord() {
|
|
84
94
|
return { pid: this.child.pid, cwd: this.cwd, executable: this.executable };
|
|
@@ -145,7 +155,8 @@ export class PythonRuntimeBackend {
|
|
|
145
155
|
async interrupt() {
|
|
146
156
|
if (!this.isAlive())
|
|
147
157
|
return { status: "failed", sessionId: "", message: "Runtime worker is not alive" };
|
|
148
|
-
this.child.kill("SIGINT")
|
|
158
|
+
if (!this.child.kill("SIGINT"))
|
|
159
|
+
return { status: "failed", sessionId: "", message: "Runtime worker is not alive" };
|
|
149
160
|
return { status: "ok", sessionId: "", message: "Sent SIGINT to runtime worker" };
|
|
150
161
|
}
|
|
151
162
|
async close(force = false) {
|