@parall/daemon 1.32.1 → 1.34.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 (46) 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 +185 -25
  5. package/bundle/parall-codex-agent.js +185 -25
  6. package/bundle/parall-daemon.js +4647 -2856
  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 +70 -0
  14. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -0
  15. package/dist/clip-runtime/browser-profile-manager.js +608 -0
  16. package/dist/clip-runtime/clip-provider.d.ts +10 -1
  17. package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
  18. package/dist/clip-runtime/clip-provider.js +63 -21
  19. package/dist/clip-runtime/hub-client.d.ts +79 -0
  20. package/dist/clip-runtime/hub-client.d.ts.map +1 -0
  21. package/dist/clip-runtime/hub-client.js +320 -0
  22. package/dist/clip-runtime/index.d.ts +2 -0
  23. package/dist/clip-runtime/index.d.ts.map +1 -1
  24. package/dist/clip-runtime/index.js +2 -0
  25. package/dist/clip-runtime/ipc.d.ts +6 -0
  26. package/dist/clip-runtime/ipc.d.ts.map +1 -1
  27. package/dist/clip-runtime/manifest.d.ts +16 -8
  28. package/dist/clip-runtime/manifest.d.ts.map +1 -1
  29. package/dist/clip-runtime/manifest.js +13 -0
  30. package/dist/clip-runtime/process-manager.d.ts +55 -3
  31. package/dist/clip-runtime/process-manager.d.ts.map +1 -1
  32. package/dist/clip-runtime/process-manager.js +226 -48
  33. package/dist/clip-runtime/process.d.ts +15 -1
  34. package/dist/clip-runtime/process.d.ts.map +1 -1
  35. package/dist/clip-runtime/process.js +73 -10
  36. package/dist/config.d.ts +9 -0
  37. package/dist/config.d.ts.map +1 -1
  38. package/dist/config.js +12 -0
  39. package/dist/index.js +46 -5
  40. package/dist/runtime-bin-resolver.d.ts +7 -0
  41. package/dist/runtime-bin-resolver.d.ts.map +1 -0
  42. package/dist/runtime-bin-resolver.js +292 -0
  43. package/dist/supervisor.d.ts +53 -4
  44. package/dist/supervisor.d.ts.map +1 -1
  45. package/dist/supervisor.js +449 -117
  46. package/package.json +7 -6
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import * as http2 from 'node:http2';
17
17
  import { ClipCommandError } from './process.js';
18
+ import { isBrowserDependencyName } from './browser-dependency.js';
18
19
  // ---------------------------------------------------------------------------
19
20
  // Connect envelope helpers
20
21
  // ---------------------------------------------------------------------------
@@ -66,10 +67,17 @@ export class ClipProvider {
66
67
  statusUnsubscribe = null;
67
68
  manifestUnsubscribe = null;
68
69
  needsReregister = false;
69
- intentionalClose = false;
70
+ // Monotonic stream id, bumped by openStream(). Handlers from a stream that a
71
+ // newer openStream() has superseded bail in handleDisconnect(gen), so their
72
+ // late close/end/error can't reconnect against the healthy replacement.
73
+ streamGeneration = 0;
70
74
  static RECONNECT_BASE_MS = 5_000;
71
75
  static RECONNECT_MAX_MS = 60_000;
72
76
  static HEARTBEAT_INTERVAL_MS = 30_000;
77
+ // After this many consecutive reconnect failures (and every multiple
78
+ // thereafter), ask the supervisor to refetch the endpoint. See
79
+ // onPersistentFailure.
80
+ static PERSISTENT_FAILURE_ATTEMPT = 3;
73
81
  constructor(opts) {
74
82
  this.opts = opts;
75
83
  }
@@ -99,6 +107,8 @@ export class ClipProvider {
99
107
  async openStream() {
100
108
  if (this.stopped)
101
109
  return;
110
+ // Bind this stream's handlers to a fresh generation; older streams go stale.
111
+ const gen = ++this.streamGeneration;
102
112
  try {
103
113
  this.closeStream();
104
114
  this.closeSession();
@@ -107,10 +117,10 @@ export class ClipProvider {
107
117
  this.session = http2.connect(authority);
108
118
  this.session.on('error', (err) => {
109
119
  this.opts.log.warn(`[clip-provider] h2 session error: ${String(err)}`);
110
- this.handleDisconnect();
120
+ this.handleDisconnect(gen);
111
121
  });
112
122
  this.session.on('close', () => {
113
- this.handleDisconnect();
123
+ this.handleDisconnect(gen);
114
124
  });
115
125
  this.stream = this.session.request({
116
126
  ':method': 'POST',
@@ -121,7 +131,7 @@ export class ClipProvider {
121
131
  });
122
132
  this.stream.on('error', (err) => {
123
133
  this.opts.log.warn(`[clip-provider] stream error: ${String(err)}`);
124
- this.handleDisconnect();
134
+ this.handleDisconnect(gen);
125
135
  });
126
136
  // Read response envelopes
127
137
  const decoder = new EnvelopeDecoder();
@@ -144,21 +154,23 @@ export class ClipProvider {
144
154
  });
145
155
  this.stream.on('end', () => {
146
156
  this.opts.log.info('[clip-provider] stream ended by hub');
147
- this.handleDisconnect();
157
+ this.handleDisconnect(gen);
148
158
  });
149
159
  this.stream.on('close', () => {
150
- this.handleDisconnect();
160
+ this.handleDisconnect(gen);
151
161
  });
152
162
  // Send RegisterRequest as the first message.
153
163
  await this.sendRegister();
154
164
  }
155
165
  catch (err) {
156
166
  this.opts.log.warn(`[clip-provider] connect failed: ${String(err)}`);
157
- this.scheduleReconnect();
167
+ this.handleDisconnect(gen);
158
168
  }
159
169
  }
160
- handleDisconnect() {
161
- if (this.stopped || this.intentionalClose)
170
+ handleDisconnect(gen) {
171
+ if (gen !== this.streamGeneration)
172
+ return; // stale event from a superseded stream
173
+ if (this.stopped)
162
174
  return;
163
175
  if (!this.connected && this.reconnectTimer)
164
176
  return; // already scheduling
@@ -172,6 +184,19 @@ export class ClipProvider {
172
184
  const delay = Math.min(ClipProvider.RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt), ClipProvider.RECONNECT_MAX_MS);
173
185
  this.reconnectAttempt++;
174
186
  this.opts.log.info(`[clip-provider] reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
187
+ if (this.reconnectAttempt % ClipProvider.PERSISTENT_FAILURE_ATTEMPT === 0) {
188
+ // The endpoint may have changed server-side since we booted (e.g.
189
+ // clip_provider_url rolled out while our ws-gateway WS never bounced to
190
+ // re-trigger machine.hello). Ask the supervisor to refetch machine config;
191
+ // if the URL changed, applyClipProviderState rebuilds this provider against
192
+ // the new endpoint. Backoff naturally throttles these refetches.
193
+ try {
194
+ this.opts.onPersistentFailure?.();
195
+ }
196
+ catch (err) {
197
+ this.opts.log.warn(`[clip-provider] onPersistentFailure threw: ${String(err)}`);
198
+ }
199
+ }
175
200
  this.reconnectTimer = setTimeout(() => {
176
201
  this.reconnectTimer = null;
177
202
  void this.openStream();
@@ -198,13 +223,11 @@ export class ClipProvider {
198
223
  if (this.needsReregister) {
199
224
  this.needsReregister = false;
200
225
  this.opts.log.info('[clip-provider] manifest updated — reconnecting to re-register clips');
201
- this.intentionalClose = true;
202
- this.closeStream();
203
- this.closeSession();
204
- this.intentionalClose = false;
205
226
  this.connected = false;
206
227
  this.clearHeartbeatTimer();
207
228
  this.clearReconnectTimer();
229
+ // openStream() bumps the generation before closing the old stream, so its
230
+ // late close events fail the gen check (closing inline would re-arm the flap).
208
231
  void this.openStream();
209
232
  return;
210
233
  }
@@ -313,9 +336,25 @@ export class ClipProvider {
313
336
  input = decoded;
314
337
  }
315
338
  }
339
+ // Two routing shapes share this handler, distinguished by clip_name:
340
+ // - "browser" capability (cross-machine host hop): clip_token is the
341
+ // PLAINTEXT browser_profile_id (used directly as the bb-browser
342
+ // account). No local subprocess — serve it against the host
343
+ // BrowserProfileManager.
344
+ // - ordinary main invoke (this machine is the execution machine): the
345
+ // hub carries the executing clip_id in clip_token (Pinix wire
346
+ // compatibility — no dedicated field). It shards the process pool and
347
+ // fixes the caller identity so the clip's nested browser dependency
348
+ // invoke can resolve its binding via the hub. Empty clip_token (plain
349
+ // non-browser clips) falls back to the default unsharded process.
350
+ const invocation = isBrowserDependencyName(clipName)
351
+ ? this.opts.clipManager.invokeBrowserCapability(cmd.clipToken ?? '', command, input)
352
+ : this.opts.clipManager.invoke(clipName, command, input, {
353
+ clipId: cmd.clipToken || undefined,
354
+ });
316
355
  let timer;
317
356
  const output = await Promise.race([
318
- this.opts.clipManager.invoke(clipName, command, input),
357
+ invocation,
319
358
  new Promise((_, reject) => {
320
359
  timer = setTimeout(() => reject(new Error('invoke timeout')), timeoutMs);
321
360
  }),
@@ -378,7 +417,10 @@ export class ClipProvider {
378
417
  // Clip registration helpers
379
418
  // ---------------------------------------------------------------------------
380
419
  buildClipRegistrations() {
381
- const clips = this.opts.clipManager.getRegisteredClips();
420
+ // getProviderClips() appends the synthetic "browser" capability on hosts
421
+ // that can serve BrowserProfiles, so the hub can route cross-machine
422
+ // browser invokes to this provider.
423
+ const clips = this.opts.clipManager.getProviderClips();
382
424
  return clips.map((clip) => this.clipConfigToRegistration(clip));
383
425
  }
384
426
  clipConfigToRegistration(clip) {
@@ -394,7 +436,7 @@ export class ClipProvider {
394
436
  domain: manifest?.domain,
395
437
  commands,
396
438
  hasWeb: manifest?.hasWeb ?? false,
397
- dependencies: this.dependencyNames(manifest?.dependencies),
439
+ dependencies: this.dependencyNames(manifest?.dependencies, manifest?.dependencySlots),
398
440
  tokenProtected: Boolean(clip.token),
399
441
  name: pkg,
400
442
  patterns: manifest?.patterns,
@@ -410,13 +452,13 @@ export class ClipProvider {
410
452
  output: detail.output,
411
453
  };
412
454
  }
413
- dependencyNames(deps) {
414
- if (!deps)
415
- return undefined;
416
- const names = Object.entries(deps)
455
+ dependencyNames(...depMaps) {
456
+ const names = depMaps
457
+ .flatMap((deps) => (deps ? Object.entries(deps) : []))
417
458
  .map(([slot, dep]) => dep.package ?? slot)
418
459
  .filter((name) => name.length > 0);
419
- return names.length > 0 ? names : undefined;
460
+ const uniqueNames = Array.from(new Set(names));
461
+ return uniqueNames.length > 0 ? uniqueNames : undefined;
420
462
  }
421
463
  stringEntities(entities) {
422
464
  if (!entities)
@@ -0,0 +1,79 @@
1
+ /**
2
+ * HubClient — Connect-RPC unary/server-streaming client the execution-side
3
+ * daemon uses to resolve cross-machine dependency bindings and forward
4
+ * dependency invokes through the Clip Service (Hub).
5
+ *
6
+ * The execution daemon (machine A) does NOT verify the binding's clip_token —
7
+ * it reads its binding via GetBindings, then passes clip_token straight to
8
+ * Invoke(clip_name="browser", ...). The hub verifies the signed token, resolves
9
+ * the bound BrowserProfile's host machine (B), and forwards the InvokeCommand
10
+ * there. See docs/engineering-design/browser-profile-clip-integration.md §11.3.
11
+ *
12
+ * Protocol (same Connect envelope framing as clip-provider.ts):
13
+ * POST /clip.v1.ClipHubService/GetBindings (unary)
14
+ * POST /clip.v1.ClipHubService/Invoke (server-streaming)
15
+ * Content-Type: application/connect+json
16
+ * Authorization: Bearer mck_xxx (scope-gated to the caller's
17
+ * own executing Clip)
18
+ * Body / Response: envelope-framed JSON
19
+ * [flags:1][length:4 big-endian][JSON payload]
20
+ * flags=0x00 data, flags=0x02 end-of-stream (trailers)
21
+ */
22
+ /** Pinix v2 ClipBinding — a resolved dependency-slot target. */
23
+ export interface ClipBinding {
24
+ alias?: string;
25
+ hub?: string;
26
+ hubToken?: string;
27
+ clipToken?: string;
28
+ }
29
+ /**
30
+ * Error surfaced by a hub Invoke that the hub flagged as a clip
31
+ * application-level rejection (errorIsCommand=true). The execution-side IPC
32
+ * layer maps this to a non-fatal ClipCommandError so the calling clip is not
33
+ * crashed by a recoverable browser command failure.
34
+ */
35
+ export declare class HubCommandError extends Error {
36
+ readonly code?: string;
37
+ constructor(message: string, code?: string);
38
+ }
39
+ export interface HubClientOptions {
40
+ /** Clip Service URL (grey-cloud clip-rpc host for BYOC). */
41
+ serviceUrl: string;
42
+ /** mck_xxx machine key — scope-gated by the hub to this machine's Clips. */
43
+ authKey: string;
44
+ /** Optional per-request timeout. */
45
+ requestTimeoutMs?: number;
46
+ }
47
+ /**
48
+ * Stateless Connect-RPC client. Each call opens a short-lived HTTP/2 session;
49
+ * dependency invokes are infrequent relative to the agent dispatch loop, so a
50
+ * pooled session is not worth the lifecycle complexity here.
51
+ */
52
+ export declare class HubClient {
53
+ private readonly opts;
54
+ constructor(opts: HubClientOptions);
55
+ /**
56
+ * GetBindings(clip_name=clipId) → slot→binding map. The hub gates
57
+ * this to Clips the caller machine actually executes. GetBindings is a
58
+ * UNARY Connect RPC, so it uses raw JSON framing (not the streaming envelope).
59
+ */
60
+ getBindings(clipId: string): Promise<Record<string, ClipBinding>>;
61
+ /**
62
+ * Invoke a clip command through the hub, passing the binding's clip_token
63
+ * through untouched. Aggregates server-streamed output chunks into one value.
64
+ * The execution daemon does not verify clip_token — the hub does.
65
+ */
66
+ invoke(clipName: string, command: string, input: unknown, clipToken: string): Promise<unknown>;
67
+ /**
68
+ * Unary Connect call: raw JSON body (no envelope), Content-Type
69
+ * application/json. The response is raw JSON; a non-2xx status carries a
70
+ * Connect error envelope ({code,message}) in the body.
71
+ */
72
+ private unary;
73
+ /**
74
+ * Server-streaming Connect call: enveloped frames over
75
+ * application/connect+json. Collects all response envelopes.
76
+ */
77
+ private serverStream;
78
+ }
79
+ //# sourceMappingURL=hub-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub-client.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/hub-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAgDH,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAaD;;;;;GAKG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBACX,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAK3C;AAED,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,gBAAgB;IAEnD;;;;OAIG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAOvE;;;;OAIG;IACG,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC;IAgCnB;;;;OAIG;IACH,OAAO,CAAC,KAAK;IAgEb;;;OAGG;IACH,OAAO,CAAC,YAAY;CAiGrB"}
@@ -0,0 +1,320 @@
1
+ /**
2
+ * HubClient — Connect-RPC unary/server-streaming client the execution-side
3
+ * daemon uses to resolve cross-machine dependency bindings and forward
4
+ * dependency invokes through the Clip Service (Hub).
5
+ *
6
+ * The execution daemon (machine A) does NOT verify the binding's clip_token —
7
+ * it reads its binding via GetBindings, then passes clip_token straight to
8
+ * Invoke(clip_name="browser", ...). The hub verifies the signed token, resolves
9
+ * the bound BrowserProfile's host machine (B), and forwards the InvokeCommand
10
+ * there. See docs/engineering-design/browser-profile-clip-integration.md §11.3.
11
+ *
12
+ * Protocol (same Connect envelope framing as clip-provider.ts):
13
+ * POST /clip.v1.ClipHubService/GetBindings (unary)
14
+ * POST /clip.v1.ClipHubService/Invoke (server-streaming)
15
+ * Content-Type: application/connect+json
16
+ * Authorization: Bearer mck_xxx (scope-gated to the caller's
17
+ * own executing Clip)
18
+ * Body / Response: envelope-framed JSON
19
+ * [flags:1][length:4 big-endian][JSON payload]
20
+ * flags=0x00 data, flags=0x02 end-of-stream (trailers)
21
+ */
22
+ import * as http2 from 'node:http2';
23
+ // ---------------------------------------------------------------------------
24
+ // Connect envelope helpers (mirror clip-provider.ts; JSON mode)
25
+ // ---------------------------------------------------------------------------
26
+ const ENVELOPE_FLAG_DATA = 0x00;
27
+ const ENVELOPE_FLAG_TRAILER = 0x02;
28
+ const MAX_FRAME_LENGTH = 16 * 1024 * 1024; // 16 MiB
29
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
30
+ function encodeEnvelope(msg) {
31
+ const json = Buffer.from(JSON.stringify(msg), 'utf-8');
32
+ const header = Buffer.alloc(5);
33
+ header[0] = ENVELOPE_FLAG_DATA;
34
+ header.writeUInt32BE(json.length, 1);
35
+ return Buffer.concat([header, json]);
36
+ }
37
+ class EnvelopeDecoder {
38
+ buf = Buffer.alloc(0);
39
+ push(chunk) {
40
+ this.buf = this.buf.length === 0 ? Buffer.from(chunk) : Buffer.concat([this.buf, chunk]);
41
+ }
42
+ *flush() {
43
+ while (this.buf.length >= 5) {
44
+ const flags = this.buf[0];
45
+ const length = this.buf.readUInt32BE(1);
46
+ if (length > MAX_FRAME_LENGTH) {
47
+ this.buf = Buffer.alloc(0);
48
+ throw new Error(`envelope frame too large: ${length} bytes`);
49
+ }
50
+ if (this.buf.length < 5 + length)
51
+ break; // incomplete frame
52
+ const payload = this.buf.subarray(5, 5 + length);
53
+ this.buf = Buffer.from(this.buf.subarray(5 + length));
54
+ yield { flags, payload };
55
+ }
56
+ }
57
+ }
58
+ /**
59
+ * Error surfaced by a hub Invoke that the hub flagged as a clip
60
+ * application-level rejection (errorIsCommand=true). The execution-side IPC
61
+ * layer maps this to a non-fatal ClipCommandError so the calling clip is not
62
+ * crashed by a recoverable browser command failure.
63
+ */
64
+ export class HubCommandError extends Error {
65
+ code;
66
+ constructor(message, code) {
67
+ super(message);
68
+ this.name = 'HubCommandError';
69
+ this.code = code;
70
+ }
71
+ }
72
+ /**
73
+ * Stateless Connect-RPC client. Each call opens a short-lived HTTP/2 session;
74
+ * dependency invokes are infrequent relative to the agent dispatch loop, so a
75
+ * pooled session is not worth the lifecycle complexity here.
76
+ */
77
+ export class HubClient {
78
+ opts;
79
+ constructor(opts) {
80
+ this.opts = opts;
81
+ }
82
+ /**
83
+ * GetBindings(clip_name=clipId) → slot→binding map. The hub gates
84
+ * this to Clips the caller machine actually executes. GetBindings is a
85
+ * UNARY Connect RPC, so it uses raw JSON framing (not the streaming envelope).
86
+ */
87
+ async getBindings(clipId) {
88
+ const res = await this.unary('GetBindings', {
89
+ clipName: clipId,
90
+ });
91
+ return res.bindings ?? {};
92
+ }
93
+ /**
94
+ * Invoke a clip command through the hub, passing the binding's clip_token
95
+ * through untouched. Aggregates server-streamed output chunks into one value.
96
+ * The execution daemon does not verify clip_token — the hub does.
97
+ */
98
+ async invoke(clipName, command, input, clipToken) {
99
+ const inputBytes = Buffer.from(input === undefined ? '{}' : JSON.stringify(input), 'utf-8').toString('base64');
100
+ const responses = await this.serverStream('Invoke', {
101
+ clipName,
102
+ command,
103
+ input: inputBytes,
104
+ clipToken,
105
+ });
106
+ const chunks = [];
107
+ for (const resp of responses) {
108
+ if (resp.error) {
109
+ const message = resp.error.message || 'hub invoke failed';
110
+ if (resp.errorIsCommand)
111
+ throw new HubCommandError(message, resp.error.code);
112
+ throw new Error(message);
113
+ }
114
+ if (resp.output)
115
+ chunks.push(decodeOutput(resp.output));
116
+ }
117
+ if (chunks.length === 0)
118
+ return null;
119
+ if (chunks.length === 1)
120
+ return chunks[0];
121
+ return chunks;
122
+ }
123
+ // -------------------------------------------------------------------------
124
+ // Transport
125
+ // -------------------------------------------------------------------------
126
+ /**
127
+ * Unary Connect call: raw JSON body (no envelope), Content-Type
128
+ * application/json. The response is raw JSON; a non-2xx status carries a
129
+ * Connect error envelope ({code,message}) in the body.
130
+ */
131
+ unary(method, request) {
132
+ const timeoutMs = this.opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
133
+ const url = new URL(this.opts.serviceUrl);
134
+ return new Promise((resolve, reject) => {
135
+ let settled = false;
136
+ const session = http2.connect(url.origin);
137
+ const chunks = [];
138
+ let status = 0;
139
+ const cleanup = () => {
140
+ clearTimeout(timer);
141
+ try {
142
+ session.close();
143
+ }
144
+ catch {
145
+ /* already closed */
146
+ }
147
+ };
148
+ const fail = (err) => {
149
+ if (settled)
150
+ return;
151
+ settled = true;
152
+ cleanup();
153
+ reject(err);
154
+ };
155
+ const timer = setTimeout(() => fail(new Error(`hub ${method} timed out`)), timeoutMs);
156
+ timer.unref?.();
157
+ session.on('error', (err) => fail(new Error(`hub h2 session error: ${String(err)}`)));
158
+ const body = Buffer.from(JSON.stringify(request), 'utf-8');
159
+ const stream = session.request({
160
+ ':method': 'POST',
161
+ ':path': `/clip.v1.ClipHubService/${method}`,
162
+ 'content-type': 'application/json',
163
+ 'connect-protocol-version': '1',
164
+ authorization: `Bearer ${this.opts.authKey}`,
165
+ });
166
+ stream.on('response', (headers) => {
167
+ status = Number(headers[':status'] ?? 0);
168
+ });
169
+ stream.on('error', (err) => fail(new Error(`hub ${method} stream error: ${String(err)}`)));
170
+ stream.on('data', (chunk) => chunks.push(chunk));
171
+ stream.on('end', () => {
172
+ if (settled)
173
+ return;
174
+ settled = true;
175
+ cleanup();
176
+ const text = Buffer.concat(chunks).toString('utf-8');
177
+ if (status < 200 || status >= 300) {
178
+ reject(new Error(`hub ${method} failed (${status}): ${connectErrorMessage(text)}`));
179
+ return;
180
+ }
181
+ try {
182
+ resolve((text ? JSON.parse(text) : {}));
183
+ }
184
+ catch (err) {
185
+ reject(new Error(`hub ${method} bad response: ${String(err)}`));
186
+ }
187
+ });
188
+ stream.end(body);
189
+ });
190
+ }
191
+ /**
192
+ * Server-streaming Connect call: enveloped frames over
193
+ * application/connect+json. Collects all response envelopes.
194
+ */
195
+ serverStream(method, request) {
196
+ const timeoutMs = this.opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
197
+ const url = new URL(this.opts.serviceUrl);
198
+ return new Promise((resolve, reject) => {
199
+ let settled = false;
200
+ const session = http2.connect(url.origin);
201
+ const decoder = new EnvelopeDecoder();
202
+ const messages = [];
203
+ const rawChunks = [];
204
+ let status = 0;
205
+ const cleanup = () => {
206
+ clearTimeout(timer);
207
+ try {
208
+ session.close();
209
+ }
210
+ catch {
211
+ /* already closed */
212
+ }
213
+ };
214
+ const fail = (err) => {
215
+ if (settled)
216
+ return;
217
+ settled = true;
218
+ cleanup();
219
+ reject(err);
220
+ };
221
+ const succeed = () => {
222
+ if (settled)
223
+ return;
224
+ settled = true;
225
+ cleanup();
226
+ resolve(messages);
227
+ };
228
+ const timer = setTimeout(() => fail(new Error(`hub ${method} timed out`)), timeoutMs);
229
+ timer.unref?.();
230
+ session.on('error', (err) => fail(new Error(`hub h2 session error: ${String(err)}`)));
231
+ const stream = session.request({
232
+ ':method': 'POST',
233
+ ':path': `/clip.v1.ClipHubService/${method}`,
234
+ 'content-type': 'application/connect+json',
235
+ 'connect-protocol-version': '1',
236
+ authorization: `Bearer ${this.opts.authKey}`,
237
+ });
238
+ stream.on('response', (headers) => {
239
+ status = Number(headers[':status'] ?? 0);
240
+ });
241
+ stream.on('error', (err) => fail(new Error(`hub ${method} stream error: ${String(err)}`)));
242
+ stream.on('data', (chunk) => {
243
+ try {
244
+ // A non-2xx response is NOT a Connect envelope stream (auth/arg/server
245
+ // failures return a JSON error body); buffer it raw so we surface the
246
+ // hub's code/message instead of a misleading frame-decode error.
247
+ if (status < 200 || status >= 300) {
248
+ rawChunks.push(chunk);
249
+ return;
250
+ }
251
+ decoder.push(chunk);
252
+ for (const envelope of decoder.flush()) {
253
+ if (envelope.flags === ENVELOPE_FLAG_TRAILER) {
254
+ const trailer = parseTrailer(envelope.payload);
255
+ if (trailer?.error) {
256
+ fail(new Error(`hub ${method} error: ${trailer.error}`));
257
+ return;
258
+ }
259
+ continue;
260
+ }
261
+ messages.push(JSON.parse(envelope.payload.toString('utf-8')));
262
+ }
263
+ }
264
+ catch (err) {
265
+ fail(err instanceof Error ? err : new Error(String(err)));
266
+ }
267
+ });
268
+ const finish = () => {
269
+ if (status !== 0 && (status < 200 || status >= 300)) {
270
+ fail(new Error(`hub ${method} failed (${status}): ${connectErrorMessage(Buffer.concat(rawChunks).toString('utf-8'))}`));
271
+ return;
272
+ }
273
+ succeed();
274
+ };
275
+ stream.on('end', finish);
276
+ stream.on('close', finish);
277
+ stream.end(encodeEnvelope(request));
278
+ });
279
+ }
280
+ }
281
+ /**
282
+ * Connect unary errors carry a JSON body {"code","message"} alongside the HTTP
283
+ * status. Surface the message (with code) so a hub rejection is actionable.
284
+ */
285
+ function connectErrorMessage(text) {
286
+ try {
287
+ const obj = JSON.parse(text);
288
+ if (obj?.message)
289
+ return obj.code ? `${obj.code}: ${obj.message}` : obj.message;
290
+ }
291
+ catch {
292
+ /* not JSON */
293
+ }
294
+ return text || 'unknown error';
295
+ }
296
+ function decodeOutput(output) {
297
+ const decoded = Buffer.from(output, 'base64').toString('utf-8');
298
+ try {
299
+ return JSON.parse(decoded);
300
+ }
301
+ catch {
302
+ return decoded;
303
+ }
304
+ }
305
+ /**
306
+ * Connect end-of-stream trailers carry an `error` object when the RPC failed.
307
+ * Surface its message so a hub-level rejection (e.g. 403 BROWSER_BINDING_INVALID)
308
+ * does not silently resolve to an empty stream.
309
+ */
310
+ function parseTrailer(payload) {
311
+ try {
312
+ const obj = JSON.parse(payload.toString('utf-8'));
313
+ if (obj?.error)
314
+ return { error: obj.error.message || 'hub error' };
315
+ return null;
316
+ }
317
+ catch {
318
+ return null;
319
+ }
320
+ }
@@ -3,5 +3,7 @@ export { ClipProcess, ClipCommandError, type ClipProcessStatus, type InvokeEvent
3
3
  export { type IpcMessage, type IpcManifest, type ListClipInfo, type ListCommandInfo, type IpcError, MessageType, NdjsonReader, NdjsonWriter, } from './ipc.js';
4
4
  export { type ClipConfig, type ManifestCache, type CommandDetail, type ClipJson, } from './manifest.js';
5
5
  export { ClipProvider, type ClipProviderOptions } from './clip-provider.js';
6
+ export { HubClient, HubCommandError, type HubClientOptions, type ClipBinding, } from './hub-client.js';
7
+ export { BrowserProfileManager } from './browser-profile-manager.js';
6
8
  export { installClip, removeClip, parseSource, type InstallOptions, type InstallResult, } from './clip-installer.js';
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,WAAW,EACX,YAAY,EACZ,YAAY,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,QAAQ,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,WAAW,EACX,YAAY,EACZ,YAAY,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,QAAQ,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EACL,SAAS,EACT,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
@@ -2,4 +2,6 @@ export { ClipProcessManager } from './process-manager.js';
2
2
  export { ClipProcess, ClipCommandError, } from './process.js';
3
3
  export { MessageType, NdjsonReader, NdjsonWriter, } from './ipc.js';
4
4
  export { ClipProvider } from './clip-provider.js';
5
+ export { HubClient, HubCommandError, } from './hub-client.js';
6
+ export { BrowserProfileManager } from './browser-profile-manager.js';
5
7
  export { installClip, removeClip, parseSource, } from './clip-installer.js';
@@ -22,6 +22,8 @@ export interface IpcMessage {
22
22
  input?: unknown;
23
23
  output?: unknown;
24
24
  error?: string;
25
+ processExit?: boolean;
26
+ process_exit?: boolean;
25
27
  manifest?: IpcManifest;
26
28
  clips?: ListClipInfo[];
27
29
  operation?: string;
@@ -43,6 +45,10 @@ export interface IpcManifest {
43
45
  package?: string;
44
46
  version?: string;
45
47
  }>;
48
+ dependency_slots?: Record<string, {
49
+ package?: string;
50
+ version?: string;
51
+ }>;
46
52
  patterns?: string[];
47
53
  entities?: Record<string, unknown>;
48
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ipc.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/ipc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ipc.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/ipc.ts"],"names":[],"mappings":"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"}