@parall/daemon 1.32.0 → 1.33.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 (51) hide show
  1. package/bundle/bb-browser-daemon.js +15628 -0
  2. package/bundle/buildDomTree.js +1501 -0
  3. package/bundle/manifest.json +19 -11
  4. package/bundle/parall-claude-agent.js +224 -58
  5. package/bundle/parall-codex-agent.js +224 -58
  6. package/bundle/parall-daemon.js +4507 -2668
  7. package/bundle/parall-openclaw-agent.js +4 -3
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +8 -2
  10. package/dist/clip-runtime/browser-dependency.d.ts +20 -0
  11. package/dist/clip-runtime/browser-dependency.d.ts.map +1 -0
  12. package/dist/clip-runtime/browser-dependency.js +52 -0
  13. package/dist/clip-runtime/browser-profile-manager.d.ts +67 -0
  14. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -0
  15. package/dist/clip-runtime/browser-profile-manager.js +595 -0
  16. package/dist/clip-runtime/bun-resolver.d.ts +24 -0
  17. package/dist/clip-runtime/bun-resolver.d.ts.map +1 -0
  18. package/dist/clip-runtime/bun-resolver.js +58 -0
  19. package/dist/clip-runtime/clip-installer.d.ts.map +1 -1
  20. package/dist/clip-runtime/clip-installer.js +59 -18
  21. package/dist/clip-runtime/clip-provider.d.ts +13 -2
  22. package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
  23. package/dist/clip-runtime/clip-provider.js +106 -36
  24. package/dist/clip-runtime/hub-client.d.ts +79 -0
  25. package/dist/clip-runtime/hub-client.d.ts.map +1 -0
  26. package/dist/clip-runtime/hub-client.js +320 -0
  27. package/dist/clip-runtime/index.d.ts +2 -0
  28. package/dist/clip-runtime/index.d.ts.map +1 -1
  29. package/dist/clip-runtime/index.js +2 -0
  30. package/dist/clip-runtime/ipc.d.ts +6 -0
  31. package/dist/clip-runtime/ipc.d.ts.map +1 -1
  32. package/dist/clip-runtime/manifest.d.ts +16 -8
  33. package/dist/clip-runtime/manifest.d.ts.map +1 -1
  34. package/dist/clip-runtime/manifest.js +13 -0
  35. package/dist/clip-runtime/process-manager.d.ts +55 -3
  36. package/dist/clip-runtime/process-manager.d.ts.map +1 -1
  37. package/dist/clip-runtime/process-manager.js +233 -76
  38. package/dist/clip-runtime/process.d.ts +15 -1
  39. package/dist/clip-runtime/process.d.ts.map +1 -1
  40. package/dist/clip-runtime/process.js +73 -10
  41. package/dist/config.d.ts +9 -0
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +12 -0
  44. package/dist/index.js +46 -5
  45. package/dist/runtime-bin-resolver.d.ts +7 -0
  46. package/dist/runtime-bin-resolver.d.ts.map +1 -0
  47. package/dist/runtime-bin-resolver.js +292 -0
  48. package/dist/supervisor.d.ts +53 -4
  49. package/dist/supervisor.d.ts.map +1 -1
  50. package/dist/supervisor.js +449 -117
  51. package/package.json +7 -6
@@ -1,11 +1,16 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import { execFileSync } from 'node:child_process';
4
3
  import { ClipProcess, ClipCommandError, } from './process.js';
4
+ import { findBunBinary } from './bun-resolver.js';
5
+ import { BROWSER_CAPABILITY_NAME, BROWSER_CAPABILITY_PACKAGE, browserCapabilityCommands, isBrowserDependencyName, } from './browser-dependency.js';
6
+ import { HubCommandError } from './hub-client.js';
5
7
  export class ClipProcessManager {
6
8
  bunPath;
7
9
  clipsDir;
8
10
  dataDir;
11
+ browserProfileManager;
12
+ hubClient;
13
+ ensureInstalled;
9
14
  processes = new Map();
10
15
  startingUp = new Map();
11
16
  statuses = new Map();
@@ -17,6 +22,13 @@ export class ClipProcessManager {
17
22
  this.bunPath = opts.bunPath ?? '';
18
23
  this.clipsDir = opts.clipsDir;
19
24
  this.dataDir = opts.dataDir;
25
+ this.browserProfileManager = opts.browserProfileManager;
26
+ this.hubClient = opts.hubClient;
27
+ this.ensureInstalled = opts.ensureInstalled;
28
+ }
29
+ /** True when this daemon can host BrowserProfiles (serve inbound hub browser invokes). */
30
+ canHostBrowser() {
31
+ return this.browserProfileManager != null;
20
32
  }
21
33
  /** Backward-compat: set a single status listener (wraps addStatusListener). */
22
34
  set onStatusChange(fn) {
@@ -68,17 +80,26 @@ export class ClipProcessManager {
68
80
  this.setStatus(config.name, 'sleeping', 'registered');
69
81
  }
70
82
  }
83
+ async replaceClipConfig(config) {
84
+ this.registerClip(config);
85
+ await this.stopClip(config.name);
86
+ // An in-flight cold start may have resolved ensureInstalled() for the old
87
+ // config while stopClip was waiting. Re-apply the desired config after the
88
+ // stop window so the next lazy spawn uses the server-declared state.
89
+ this.registerClip(config);
90
+ }
71
91
  async unregisterClip(name) {
72
92
  this.shuttingDown.add(name);
73
93
  try {
74
- const startup = this.startingUp.get(name);
75
- if (startup) {
76
- this.startingUp.delete(name);
77
- await startup.then((proc) => proc.stop()).catch(() => { });
78
- }
79
- if (this.processes.has(name)) {
80
- await this.stopClip(name);
94
+ const startupStops = [];
95
+ for (const [key, startup] of this.startingUp) {
96
+ if (!this.processKeyBelongsToName(key, name))
97
+ continue;
98
+ this.startingUp.delete(key);
99
+ startupStops.push(startup.then((proc) => proc.stop()).catch(() => { }));
81
100
  }
101
+ await Promise.allSettled(startupStops);
102
+ await this.stopClip(name);
82
103
  this.clipConfigs.delete(name);
83
104
  this.statuses.delete(name);
84
105
  }
@@ -92,11 +113,27 @@ export class ClipProcessManager {
92
113
  async stopClip(name) {
93
114
  this.shuttingDown.add(name);
94
115
  try {
95
- const proc = this.processes.get(name);
96
- if (!proc)
116
+ // Also tear down any same-name shards still in their cold-start window.
117
+ // Without this an in-flight startup would complete and re-register into
118
+ // this.processes after stopClip returns, defeating the explicit stop.
119
+ const startupStops = [];
120
+ for (const [key, startup] of this.startingUp) {
121
+ if (!this.processKeyBelongsToName(key, name))
122
+ continue;
123
+ this.startingUp.delete(key);
124
+ startupStops.push(startup.then((proc) => proc.stop()).catch(() => { }));
125
+ }
126
+ const stops = [];
127
+ for (const [key, proc] of this.processes) {
128
+ if (!this.processKeyBelongsToName(key, name))
129
+ continue;
130
+ stops.push(proc.stop().finally(() => {
131
+ this.processes.delete(key);
132
+ }));
133
+ }
134
+ if (stops.length === 0 && startupStops.length === 0)
97
135
  return;
98
- await proc.stop();
99
- this.processes.delete(name);
136
+ await Promise.allSettled([...startupStops, ...stops]);
100
137
  this.setStatus(name, 'sleeping', 'stopped');
101
138
  }
102
139
  finally {
@@ -105,11 +142,19 @@ export class ClipProcessManager {
105
142
  }
106
143
  async stopAll() {
107
144
  // Mark all clips as shutting down before any async work
108
- for (const name of this.processes.keys())
145
+ const names = new Set();
146
+ for (const key of this.processes.keys())
147
+ names.add(this.processNameFromKey(key));
148
+ for (const key of this.startingUp.keys())
149
+ names.add(this.processNameFromKey(key));
150
+ for (const name of names)
109
151
  this.shuttingDown.add(name);
110
- for (const name of this.startingUp.keys())
111
- this.shuttingDown.add(name);
112
- const stops = Array.from(this.processes.keys()).map((name) => this.stopClip(name));
152
+ const stops = [];
153
+ for (const [key, proc] of this.processes) {
154
+ stops.push(proc.stop().finally(() => {
155
+ this.processes.delete(key);
156
+ }));
157
+ }
113
158
  // Also abort any in-flight startups (register window)
114
159
  for (const [, startup] of this.startingUp) {
115
160
  stops.push(startup.then((proc) => proc.stop()).catch(() => { }));
@@ -125,38 +170,40 @@ export class ClipProcessManager {
125
170
  * Application-level errors (ClipCommandError) are NOT retried.
126
171
  * Corresponds to Pinix ProcessManager.Invoke().
127
172
  */
128
- async invoke(name, command, input) {
173
+ async invoke(name, command, input, context = {}) {
174
+ const key = this.processKey(name, context);
129
175
  try {
130
- const proc = await this.ensureProcess(name);
176
+ const proc = await this.ensureProcess(name, context);
131
177
  const result = await proc.invoke(command, input);
132
178
  return result.output;
133
179
  }
134
180
  catch (err) {
135
181
  if (err instanceof ClipCommandError)
136
182
  throw err;
137
- const old = this.processes.get(name);
183
+ const old = this.processes.get(key);
138
184
  if (old?.alive())
139
185
  await old.stop().catch(() => { });
140
- this.processes.delete(name);
141
- const proc = await this.ensureProcess(name);
186
+ this.processes.delete(key);
187
+ const proc = await this.ensureProcess(name, context);
142
188
  const result = await proc.invoke(command, input);
143
189
  return result.output;
144
190
  }
145
191
  }
146
- async invokeStream(name, command, input, onChunk) {
192
+ async invokeStream(name, command, input, onChunk, context = {}) {
193
+ const key = this.processKey(name, context);
147
194
  try {
148
- const proc = await this.ensureProcess(name);
195
+ const proc = await this.ensureProcess(name, context);
149
196
  const result = await proc.invokeStream(command, input, onChunk);
150
197
  return result.output;
151
198
  }
152
199
  catch (err) {
153
200
  if (err instanceof ClipCommandError)
154
201
  throw err;
155
- const old = this.processes.get(name);
202
+ const old = this.processes.get(key);
156
203
  if (old?.alive())
157
204
  await old.stop().catch(() => { });
158
- this.processes.delete(name);
159
- const proc = await this.ensureProcess(name);
205
+ this.processes.delete(key);
206
+ const proc = await this.ensureProcess(name, context);
160
207
  const result = await proc.invokeStream(command, input, onChunk);
161
208
  return result.output;
162
209
  }
@@ -175,12 +222,29 @@ export class ClipProcessManager {
175
222
  return { status: entry.status, message: entry.message };
176
223
  }
177
224
  isRunning(name) {
178
- const proc = this.processes.get(name);
179
- return proc !== undefined && proc.alive();
225
+ for (const [key, proc] of this.processes) {
226
+ if (this.processKeyBelongsToName(key, name) && proc.alive())
227
+ return true;
228
+ }
229
+ return false;
180
230
  }
181
231
  getRegisteredClips() {
182
232
  return Array.from(this.clipConfigs.values());
183
233
  }
234
+ /**
235
+ * Clips this daemon advertises to the hub as a provider: the locally
236
+ * installed clips plus, when this machine hosts BrowserProfiles, a synthetic
237
+ * "browser" capability so the hub can route cross-machine browser invokes
238
+ * here. The browser capability has no local subprocess — it is served by
239
+ * invokeBrowserCapability() against bb-browser.
240
+ */
241
+ getProviderClips() {
242
+ const clips = this.getRegisteredClips();
243
+ if (this.canHostBrowser()) {
244
+ clips.push(browserCapabilityConfig());
245
+ }
246
+ return clips;
247
+ }
184
248
  /**
185
249
  * Load clip configs from the clips directory.
186
250
  * Reads clip-config.json if present, or scans subdirectories for clip.json files.
@@ -210,11 +274,20 @@ export class ClipProcessManager {
210
274
  // Try clip.json first, fall back to package.json
211
275
  const clipJsonPath = path.join(clipDir, 'clip.json');
212
276
  const pkgJsonPath = path.join(clipDir, 'package.json');
277
+ // Key by the install-directory alias, NOT the manifest name. The
278
+ // installer writes to clipsDir/{org alias}/ and the install-time
279
+ // registration (supervisor handleClipInstall) keys by that alias — the
280
+ // hub routes org invokes by it (sess.clips[alias]). Preferring
281
+ // clipJson.name here re-keyed clips by their package name
282
+ // ("@pinix/reddit") after a daemon restart, so org-alias invokes
283
+ // ("reddit") failed with "no online provider" until reinstall (caught
284
+ // on the staging closed-loop E2E, 2026-06-04). The manifest/package
285
+ // identity stays on `package`.
213
286
  if (fs.existsSync(clipJsonPath)) {
214
287
  try {
215
288
  const clipJson = JSON.parse(fs.readFileSync(clipJsonPath, 'utf-8'));
216
289
  this.registerClip({
217
- name: clipJson.name || entry.name,
290
+ name: entry.name,
218
291
  package: clipJson.name,
219
292
  version: clipJson.version,
220
293
  source: clipDir,
@@ -229,7 +302,7 @@ export class ClipProcessManager {
229
302
  try {
230
303
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
231
304
  this.registerClip({
232
- name: pkgJson.name || entry.name,
305
+ name: entry.name,
233
306
  package: pkgJson.name,
234
307
  version: pkgJson.version,
235
308
  source: clipDir,
@@ -243,40 +316,54 @@ export class ClipProcessManager {
243
316
  }
244
317
  }
245
318
  // --- internal ---
246
- async ensureProcess(name) {
319
+ async ensureProcess(name, context = {}) {
247
320
  if (this.shuttingDown.has(name)) {
248
321
  throw new Error(`clip "${name}" is shutting down`);
249
322
  }
250
- const existing = this.processes.get(name);
323
+ const key = this.processKey(name, context);
324
+ const existing = this.processes.get(key);
251
325
  if (existing && existing.alive())
252
326
  return existing;
253
327
  // Deduplicate concurrent cold-start requests
254
- const inflight = this.startingUp.get(name);
328
+ const inflight = this.startingUp.get(key);
255
329
  if (inflight)
256
330
  return inflight;
257
- const config = this.clipConfigs.get(name);
258
- if (!config)
259
- throw new Error(`clip "${name}" not registered`);
260
- const startup = this.spawnProcess(config);
261
- this.startingUp.set(name, startup);
331
+ const startup = (async () => {
332
+ let config = this.clipConfigs.get(name);
333
+ if (!config)
334
+ throw new Error(`clip "${name}" not registered`);
335
+ if (this.ensureInstalled) {
336
+ config = await this.ensureInstalled(config);
337
+ this.clipConfigs.set(name, config);
338
+ }
339
+ return this.spawnProcess(config, context);
340
+ })();
341
+ this.startingUp.set(key, startup);
262
342
  try {
263
343
  return await startup;
264
344
  }
265
345
  finally {
266
- this.startingUp.delete(name);
346
+ this.startingUp.delete(key);
267
347
  }
268
348
  }
269
- async spawnProcess(config) {
270
- const proc = new ClipProcess(config);
271
- const clipDataDir = this.ensureClipDataDir(config);
272
- proc.spawn(this.resolveBun(), {
349
+ async spawnProcess(config, context) {
350
+ const proc = new ClipProcess(config, context, (clipName, command, input, invokeContext) => this.invokeDependency(clipName, command, input, invokeContext));
351
+ const clipDataDir = this.ensureClipDataDir(config, context);
352
+ const bunPath = this.resolveBun();
353
+ proc.spawn(bunPath, {
273
354
  PINIX_DATA_DIR: clipDataDir,
355
+ PRLL_CLIP_ID: context.clipId ?? '',
356
+ // Expose the embedded bun's directory on the clip's PATH so a clip that
357
+ // shells out to `bun` by name resolves it — the daemon's launchd PATH
358
+ // does not include Frameworks/. Symmetric with installDeps(); relies on
359
+ // the embedded binary being named `bun` (Frameworks/bun).
360
+ PATH: `${path.dirname(bunPath)}${path.delimiter}${process.env.PATH ?? ''}`,
274
361
  });
275
362
  this.setStatus(config.name, 'running', 'starting');
276
363
  try {
277
364
  await proc.waitReady();
278
365
  // Only expose to concurrent callers after registration completes
279
- this.processes.set(config.name, proc);
366
+ this.processes.set(this.processKey(config.name, context), proc);
280
367
  this.setStatus(config.name, 'running', 'ready');
281
368
  // Always cache manifest so Provider registrations have commands
282
369
  const manifest = proc.getManifest();
@@ -292,7 +379,7 @@ export class ClipProcessManager {
292
379
  }
293
380
  // Monitor process exit in background
294
381
  proc.waitDone().then(() => {
295
- this.removeIfSame(config.name, proc);
382
+ this.removeIfSame(config.name, proc, context);
296
383
  const exitErr = proc.getError();
297
384
  if (exitErr) {
298
385
  this.setStatus(config.name, 'error', exitErr.message);
@@ -303,16 +390,91 @@ export class ClipProcessManager {
303
390
  });
304
391
  return proc;
305
392
  }
306
- ensureClipDataDir(config) {
307
- const dir = path.join(this.dataDir, config.name);
393
+ ensureClipDataDir(config, context = {}) {
394
+ const dir = path.join(this.dataDir, config.name, context.clipId ?? 'default');
308
395
  fs.mkdirSync(dir, { recursive: true });
309
396
  return dir;
310
397
  }
311
- removeIfSame(name, proc) {
312
- if (this.processes.get(name) === proc) {
313
- this.processes.delete(name);
398
+ removeIfSame(name, proc, context = {}) {
399
+ const key = this.processKey(name, context);
400
+ if (this.processes.get(key) === proc) {
401
+ this.processes.delete(key);
314
402
  }
315
403
  }
404
+ processKey(name, context = {}) {
405
+ return context.clipId ? `${name}::${context.clipId}` : name;
406
+ }
407
+ processKeyBelongsToName(key, name) {
408
+ return key === name || key.startsWith(`${name}::`);
409
+ }
410
+ processNameFromKey(key) {
411
+ const marker = key.indexOf('::');
412
+ return marker === -1 ? key : key.slice(0, marker);
413
+ }
414
+ /**
415
+ * Execution-side nested dependency invoke (from a child clip's IPC). The
416
+ * browser slot is resolved UNIQUELY via the hub: read the Clip's binding
417
+ * (GetBindings), then forward the invoke through the hub (Invoke) to the bound
418
+ * BrowserProfile's host. No local shortcut, no fallback — unbound is rejected.
419
+ * The execution daemon never verifies clip_token; the hub does.
420
+ * See docs/engineering-design/browser-profile-clip-integration.md §11.3.
421
+ */
422
+ async invokeDependency(clipName, command, input, context) {
423
+ if (!isBrowserDependencyName(clipName)) {
424
+ throw new Error(`clip dependency "${clipName}" is not supported`);
425
+ }
426
+ const clipId = context.clipId;
427
+ if (!clipId) {
428
+ // Without the executing Clip identity we cannot resolve the binding. The
429
+ // process key shards by clipId, so this should not happen for a
430
+ // browser-bound Clip.
431
+ throw new ClipCommandError('browser dependency requires a clip binding');
432
+ }
433
+ const hub = this.hubClient?.();
434
+ if (!hub) {
435
+ throw new Error('browser dependency runtime is not configured (no hub client)');
436
+ }
437
+ const bindings = await hub.getBindings(clipId);
438
+ const binding = bindings[BROWSER_CAPABILITY_NAME];
439
+ if (!binding || !binding.clipToken) {
440
+ // Unbound browser slot: surface as a clip-level error so the calling clip
441
+ // gets a recoverable rejection instead of a process crash.
442
+ throw new ClipCommandError(`clip "${clipId}" has no browser binding`, 'CLIP_PROFILE_UNBOUND');
443
+ }
444
+ try {
445
+ return await hub.invoke(BROWSER_CAPABILITY_NAME, command, input, binding.clipToken);
446
+ }
447
+ catch (err) {
448
+ // A hub-flagged application error (e.g. BROWSER_COMMAND_ERROR) is a
449
+ // recoverable clip-level rejection; translate it so the calling clip is
450
+ // not torn down. Transport/internal errors propagate as fatal.
451
+ if (err instanceof HubCommandError) {
452
+ throw new ClipCommandError(err.message, err.code);
453
+ }
454
+ throw err;
455
+ }
456
+ }
457
+ /**
458
+ * Host-side browser capability handler (inbound hub InvokeCommand for the
459
+ * synthetic "browser" clip). The hub forwards clip_token as the PLAINTEXT
460
+ * browser_profile_id, which the host daemon uses directly as the bb-browser
461
+ * account. Only daemons that host BrowserProfiles register this capability.
462
+ * See docs/engineering-design/browser-profile-clip-integration.md §11.2.
463
+ */
464
+ async invokeBrowserCapability(clipToken, command, input) {
465
+ if (!this.browserProfileManager) {
466
+ throw new Error('browser capability is not hosted on this machine');
467
+ }
468
+ if (!clipToken) {
469
+ throw new ClipCommandError('browser invoke missing profile identity (clip_token)');
470
+ }
471
+ return this.browserProfileManager.invoke({
472
+ profileId: clipToken,
473
+ account: clipToken,
474
+ command,
475
+ input,
476
+ });
477
+ }
316
478
  setStatus(name, status, message) {
317
479
  this.statuses.set(name, { status, message, since: new Date() });
318
480
  for (const fn of this.statusListeners) {
@@ -325,30 +487,25 @@ export class ClipProcessManager {
325
487
  }
326
488
  }
327
489
  }
328
- function findBunBinary() {
329
- const isWindows = process.platform === 'win32';
330
- if (!isWindows) {
331
- const candidates = [
332
- path.join(process.env.HOME || '', '.bun', 'bin', 'bun'),
333
- '/usr/local/bin/bun',
334
- '/opt/homebrew/bin/bun',
335
- ];
336
- for (const candidate of candidates) {
337
- if (fs.existsSync(candidate))
338
- return candidate;
339
- }
340
- }
341
- // PATH lookup — platform-aware
342
- const lookupCmd = isWindows ? 'where.exe' : 'which';
343
- const lookupArg = isWindows ? 'bun.exe' : 'bun';
344
- try {
345
- const result = execFileSync(lookupCmd, [lookupArg], { encoding: 'utf-8' }).trim();
346
- const firstLine = result.split('\n')[0]?.trim();
347
- if (firstLine)
348
- return firstLine;
349
- }
350
- catch {
351
- // not found in PATH
352
- }
353
- throw new Error('bun binary not found — install Bun (https://bun.sh) or set bunPath option');
490
+ /**
491
+ * Synthetic ClipConfig for the host-side "browser" capability. It has no local
492
+ * subprocess (no `path` to spawn); the hub routes browser invokes here and they
493
+ * are served by ClipProcessManager.invokeBrowserCapability() against bb-browser.
494
+ */
495
+ function browserCapabilityConfig() {
496
+ return {
497
+ name: BROWSER_CAPABILITY_NAME,
498
+ package: BROWSER_CAPABILITY_PACKAGE,
499
+ version: '0.15.0',
500
+ source: BROWSER_CAPABILITY_PACKAGE,
501
+ path: '',
502
+ manifest: {
503
+ name: BROWSER_CAPABILITY_NAME,
504
+ package: BROWSER_CAPABILITY_PACKAGE,
505
+ version: '0.15.0',
506
+ description: 'Profile-bound browser capability (bb-browser-pro)',
507
+ commands: browserCapabilityCommands().map((c) => c.name),
508
+ commandDetails: browserCapabilityCommands(),
509
+ },
510
+ };
354
511
  }
@@ -12,11 +12,24 @@ export interface InvokeEvent {
12
12
  export interface InvokeResult {
13
13
  output: unknown;
14
14
  }
15
+ export interface ClipInvokeContext {
16
+ /**
17
+ * The executing Clip id. It shards the process pool and fixes the caller
18
+ * identity for nested dependency invokes, so the execution side can resolve
19
+ * the Clip's browser binding via the hub. Browser
20
+ * profile / account identity is NOT carried here — it is resolved from the
21
+ * hub binding (GetBindings) and rides on clip_token end-to-end.
22
+ */
23
+ clipId?: string;
24
+ }
25
+ export type DependencyInvokeResolver = (clipName: string, command: string, input: unknown, context: ClipInvokeContext) => Promise<unknown>;
15
26
  export declare class ClipCommandError extends Error {
16
27
  readonly code?: string;
17
28
  constructor(message: string, code?: string);
18
29
  }
19
30
  export declare class ClipProcess {
31
+ private readonly context;
32
+ private readonly dependencyResolver?;
20
33
  readonly clip: ClipConfig;
21
34
  private child;
22
35
  private reader;
@@ -32,7 +45,7 @@ export declare class ClipProcess {
32
45
  private doneDeferred;
33
46
  private doneResolved;
34
47
  private exitError;
35
- constructor(clip: ClipConfig);
48
+ constructor(clip: ClipConfig, context?: ClipInvokeContext, dependencyResolver?: DependencyInvokeResolver | undefined);
36
49
  /**
37
50
  * Spawn the Bun subprocess and start the IPC read loop.
38
51
  * Corresponds to Pinix startLocked().
@@ -51,6 +64,7 @@ export declare class ClipProcess {
51
64
  private handleRegister;
52
65
  private dispatchInvokeEvent;
53
66
  private handleData;
67
+ private handleDependencyInvoke;
54
68
  private send;
55
69
  private signalReady;
56
70
  private finish;
@@ -1 +1 @@
1
- {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/process.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,aAAa,EAInB,MAAM,eAAe,CAAC;AAevB,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAEjE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBACX,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAK3C;AAgBD,qBAAa,WAAW;IACtB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAE1B,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,MAAM,CAA6B;IAE3C,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,OAAO,CAAmD;IAClE,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,aAAa,CAAwB;IAC7C,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAoB;gBAEzB,IAAI,EAAE,UAAU;IAI5B;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAwCnD,SAAS,CAAC,SAAS,GAAE,MAAiC,GAAG,OAAO,CAAC,IAAI,CAAC;IAe5E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAInB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC;IAI/D,YAAY,CAChB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,OAAO,CAAC,YAAY,CAAC;IA2DlB,IAAI,CAAC,SAAS,GAAE,MAA6B,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBnE,KAAK,IAAI,OAAO;IAShB,WAAW,IAAI,aAAa,GAAG,IAAI;IAInC,QAAQ,IAAI,KAAK,GAAG,SAAS;YAMf,QAAQ;IAWtB,OAAO,CAAC,aAAa;IA6CrB,OAAO,CAAC,cAAc;IA4BtB,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,UAAU;YASJ,IAAI;IAKlB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,MAAM;IAiCd,OAAO,CAAC,KAAK;CAOd"}
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/process.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,8 @@ import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
3
  import { clearAllProviderCreds } from '../runtimes.js';
4
4
  import { NdjsonReader, NdjsonWriter, MessageType } from './ipc.js';
5
- import { manifestFromIpc, enrichManifest, resolveEntrypoint, } from './manifest.js';
5
+ import { enrichManifest, manifestFromIpc, resolveEntrypoint, } from './manifest.js';
6
+ import { manifestDeclaresBrowserDependency, manifestMapsDependencyToBrowser, } from './browser-dependency.js';
6
7
  const CLIP_REGISTER_TIMEOUT_MS = 10_000;
7
8
  const CLIP_STOP_TIMEOUT_MS = 5_000;
8
9
  function sanitizeEnvForClip(extra) {
@@ -32,6 +33,8 @@ function makeDeferred() {
32
33
  return { promise, resolve, reject };
33
34
  }
34
35
  export class ClipProcess {
36
+ context;
37
+ dependencyResolver;
35
38
  clip;
36
39
  child = null;
37
40
  reader = null;
@@ -47,7 +50,9 @@ export class ClipProcess {
47
50
  doneDeferred = makeDeferred();
48
51
  doneResolved = false;
49
52
  exitError;
50
- constructor(clip) {
53
+ constructor(clip, context = {}, dependencyResolver) {
54
+ this.context = context;
55
+ this.dependencyResolver = dependencyResolver;
51
56
  this.clip = clip;
52
57
  }
53
58
  /**
@@ -231,20 +236,27 @@ export class ClipProcess {
231
236
  this.handleData(msg);
232
237
  break;
233
238
  case MessageType.ListClips:
234
- // Clip-to-clip list request respond with empty for now (Phase 2)
239
+ // Advertise the "browser" dependency to the child whenever its manifest
240
+ // declares it. Binding resolution + cross-machine routing happen at
241
+ // invoke time via the hub (GetBindings → Invoke); an unbound or
242
+ // unavailable profile surfaces as an invoke-time error, not here.
235
243
  this.send({
236
244
  id: msg.id,
237
245
  type: MessageType.ListClipsResult,
238
- clips: [],
246
+ clips: this.context.clipId && manifestDeclaresBrowserDependency(this.manifest)
247
+ ? [
248
+ {
249
+ name: 'browser',
250
+ package: '@pinixai/bb-browser-pro',
251
+ version: '0.15.0',
252
+ description: 'Profile-bound browser capability',
253
+ },
254
+ ]
255
+ : [],
239
256
  }).catch(() => { });
240
257
  break;
241
258
  case MessageType.Invoke:
242
- // Clip-to-clip invoke — not routed in Phase 1
243
- this.send({
244
- id: msg.id,
245
- type: MessageType.Error,
246
- error: 'clip-to-clip invoke not supported yet',
247
- }).catch(() => { });
259
+ void this.handleDependencyInvoke(msg);
248
260
  break;
249
261
  case MessageType.Heartbeat:
250
262
  break;
@@ -291,6 +303,8 @@ export class ClipProcess {
291
303
  event.output = msg.output;
292
304
  if (msg.error)
293
305
  event.error = { message: msg.error };
306
+ if (msg.processExit || msg.process_exit)
307
+ event.processExit = true;
294
308
  callback(event);
295
309
  }
296
310
  handleData(msg) {
@@ -301,6 +315,55 @@ export class ClipProcess {
301
315
  error: 'data operations not implemented yet',
302
316
  }).catch(() => { });
303
317
  }
318
+ async handleDependencyInvoke(msg) {
319
+ const dependencyName = msg.clip ?? msg.alias ?? '';
320
+ if (!msg.id) {
321
+ console.warn(`[clip:${this.clip.name}] dependency invoke missing id`);
322
+ return;
323
+ }
324
+ if (!dependencyName || !msg.command) {
325
+ await this.send({
326
+ id: msg.id,
327
+ type: MessageType.Error,
328
+ error: 'dependency clip and command are required',
329
+ }).catch(() => { });
330
+ return;
331
+ }
332
+ if (!manifestMapsDependencyToBrowser(this.manifest, dependencyName)) {
333
+ await this.send({
334
+ id: msg.id,
335
+ type: MessageType.Error,
336
+ error: 'browser dependency is not declared by this clip',
337
+ }).catch(() => { });
338
+ return;
339
+ }
340
+ if (!this.dependencyResolver) {
341
+ await this.send({
342
+ id: msg.id,
343
+ type: MessageType.Error,
344
+ error: 'clip dependency invoke is not configured',
345
+ }).catch(() => { });
346
+ return;
347
+ }
348
+ try {
349
+ const output = await this.dependencyResolver('browser', msg.command, msg.input ?? {}, this.context);
350
+ await this.send({
351
+ id: msg.id,
352
+ type: MessageType.Result,
353
+ output,
354
+ });
355
+ }
356
+ catch (err) {
357
+ const isCommandError = err instanceof ClipCommandError;
358
+ await this.send({
359
+ id: msg.id,
360
+ type: MessageType.Error,
361
+ error: err instanceof Error ? err.message : String(err),
362
+ processExit: !isCommandError,
363
+ process_exit: !isCommandError,
364
+ }).catch(() => { });
365
+ }
366
+ }
304
367
  async send(msg) {
305
368
  if (!this.writer)
306
369
  throw new Error('writer not initialized');
package/dist/config.d.ts CHANGED
@@ -54,6 +54,15 @@ export type ClaudeDaemonConfig = {
54
54
  /** Disable self-update entirely (K8s env auto-disables). */
55
55
  updateDisabled: boolean;
56
56
  };
57
+ /**
58
+ * Root dir for all on-disk daemon state: `config.json`, the self-update
59
+ * `bundle/` overlay (versions + current symlink + update-state.json), and
60
+ * bridge-bin resolution all derive from this single path. `PRLL_DAEMON_CONFIG_DIR`
61
+ * overrides it so multiple daemons can coexist on one host without colliding —
62
+ * e.g. a staging desktop install points its launchd agent at
63
+ * `~/.parall-daemon-staging` while prod keeps the default `~/.parall-daemon`.
64
+ * Unset → default (no behavior change for existing single-env installs).
65
+ */
57
66
  export declare function daemonConfigDir(env?: NodeJS.ProcessEnv): string;
58
67
  export declare function daemonConfigPath(env?: NodeJS.ProcessEnv): string;
59
68
  export declare function resolveClaudeDaemonConfig(env?: NodeJS.ProcessEnv): ClaudeDaemonConfig;