skydive-cli 0.5.0-beta.3 → 0.5.0-beta.31

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-Bq93vOIk.mjs";
2
+ import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-BTyg329O.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -12,7 +12,7 @@ var PortalDaemonClient = class {
12
12
  lastBind = null;
13
13
  fallbackCwd = null;
14
14
  wantEnabled = false;
15
- pendingGrants = /* @__PURE__ */ new Set();
15
+ pendingGrants = /* @__PURE__ */ new Map();
16
16
  pendingDeclines = /* @__PURE__ */ new Map();
17
17
  grantedAgentIds = /* @__PURE__ */ new Set();
18
18
  constructor(opts) {
@@ -50,9 +50,10 @@ var PortalDaemonClient = class {
50
50
  conversationId: this.lastBind.conversationId,
51
51
  cwd: this.lastBind.cwd
52
52
  });
53
- for (const agentId of this.pendingGrants) this.send({
53
+ for (const [agentId, conversationId] of this.pendingGrants) this.send({
54
54
  t: "grant",
55
- agentId
55
+ agentId,
56
+ conversationId
56
57
  });
57
58
  for (const [agentId, declined] of this.pendingDeclines) this.send({
58
59
  t: "decline",
@@ -157,13 +158,16 @@ var PortalDaemonClient = class {
157
158
  * Authorize one agent to run commands on this machine. Resolves once the
158
159
  * request is sent to the daemon (the daemon performs the grant and pushes the
159
160
  * updated state); kept async so it's a drop-in for the old in-process client's
160
- * awaited `grantAgent`.
161
+ * awaited `grantAgent`. `conversationId` names the conversation whose run
162
+ * asked, so the grant can wake the waiting agent; null when the grant is not
163
+ * answering an in-chat request.
161
164
  */
162
- grantAgent(agentId) {
163
- this.pendingGrants.add(agentId);
165
+ grantAgent(agentId, conversationId) {
166
+ this.pendingGrants.set(agentId, conversationId);
164
167
  this.send({
165
168
  t: "grant",
166
- agentId
169
+ agentId,
170
+ conversationId
167
171
  });
168
172
  return Promise.resolve();
169
173
  }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import "./client-DbqRBquD.mjs";
3
+ import "./daemon-BTyg329O.mjs";
4
+ import "./api-DG5W6iwx.mjs";
5
+ import { t as PortalDaemonClient } from "./daemon-client--A_yMKq6.mjs";
6
+
7
+ export { PortalDaemonClient };
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ import { t as fetchForwardTarget } from "./api-DG5W6iwx.mjs";
3
+ import net from "node:net";
4
+ import { WebSocket, createWebSocketStream } from "ws";
5
+
6
+ //#region src/chat/portal/tunnel-pool.ts
7
+ const MAX_AGE_MS = 4e4;
8
+ const SWEEP_INTERVAL_MS = 1e4;
9
+ const REFILL_BACKOFF_MS = 15e3;
10
+ /**
11
+ * Pre-warmed tunnel pool for the forward portal's accept path.
12
+ *
13
+ * Dialing a tunnel costs a full edge round-trip (~300ms measured). The pool
14
+ * pays that in the background so accept can adopt a warm tunnel and move the
15
+ * first byte immediately. Target starts at `initialSize` and grows by one on
16
+ * each miss, never above `maxSize` — a miss still cold-dials (today's path);
17
+ * growth only helps the next wave. No decay: a runaway client can hold
18
+ * `maxSize` idle edge sockets until `forward` exits.
19
+ *
20
+ * `dial` must resolve with a stream that is already paused, so any bytes a
21
+ * server-first protocol sends before adoption buffer instead of dropping.
22
+ */
23
+ function createTunnelPool({ initialSize, maxSize, dial, log }) {
24
+ let entries = [];
25
+ let dialsInFlight = 0;
26
+ let backoffUntil = 0;
27
+ let closed = false;
28
+ let targetSize = initialSize;
29
+ function refill() {
30
+ if (closed) return;
31
+ if (Date.now() < backoffUntil) return;
32
+ while (entries.length + dialsInFlight < targetSize) {
33
+ dialsInFlight += 1;
34
+ dial().then((tunnel) => {
35
+ dialsInFlight -= 1;
36
+ if (closed) {
37
+ tunnel.close();
38
+ return;
39
+ }
40
+ const entry = {
41
+ tunnel,
42
+ dialedAt: Date.now(),
43
+ dead: false
44
+ };
45
+ tunnel.stream.once("close", () => {
46
+ entry.dead = true;
47
+ });
48
+ tunnel.stream.once("error", () => {
49
+ entry.dead = true;
50
+ });
51
+ entries.push(entry);
52
+ }).catch((err) => {
53
+ dialsInFlight -= 1;
54
+ backoffUntil = Date.now() + REFILL_BACKOFF_MS;
55
+ log(`forward: pool dial failed (backing off): ${err instanceof Error ? err.message : String(err)}`);
56
+ });
57
+ }
58
+ }
59
+ function sweep() {
60
+ const now = Date.now();
61
+ const keep = [];
62
+ for (const entry of entries) if (entry.dead || now - entry.dialedAt > MAX_AGE_MS) entry.tunnel.close();
63
+ else keep.push(entry);
64
+ entries = keep;
65
+ refill();
66
+ }
67
+ const sweeper = setInterval(sweep, SWEEP_INTERVAL_MS);
68
+ sweeper.unref();
69
+ refill();
70
+ return {
71
+ take: () => {
72
+ while (entries.length > 0) {
73
+ const entry = entries.pop();
74
+ if (!entry) break;
75
+ if (entry.dead || entry.tunnel.stream.destroyed) continue;
76
+ refill();
77
+ return entry.tunnel;
78
+ }
79
+ if (targetSize < maxSize) targetSize += 1;
80
+ refill();
81
+ return null;
82
+ },
83
+ close: () => {
84
+ closed = true;
85
+ clearInterval(sweeper);
86
+ for (const entry of entries) entry.tunnel.close();
87
+ entries = [];
88
+ }
89
+ };
90
+ }
91
+
92
+ //#endregion
93
+ //#region src/chat/portal/forward.ts
94
+ const TARGET_REFRESH_SAFETY_MS = 12e4;
95
+ const INITIAL_POOL_SIZE = 8;
96
+ const MAX_POOL_SIZE = 16;
97
+ /**
98
+ * The reverse portal's client half: listen on the local machine's loopback and
99
+ * pipe each TCP connection to the agent sandbox's daemon (`/portal/tcp`),
100
+ * which pipes to the sandbox's own loopback. The daemon is reached through the
101
+ * agent-webserver edge Worker (sandboxes have public ingress disabled; the
102
+ * Worker owns boot-resolution and injects the sandbox edge-auth token), so a
103
+ * sandbox recycle just costs the next connection a cold-start wait rather
104
+ * than invalidating the forward.
105
+ */
106
+ async function startForward({ auth, agentId, localPort, targetPort, log }) {
107
+ let target = await fetchForwardTarget(auth, agentId);
108
+ let mintedAt = Date.now();
109
+ async function freshTarget() {
110
+ const ttlMs = target.expiresInSeconds * 1e3;
111
+ if (Date.now() - mintedAt > ttlMs - TARGET_REFRESH_SAFETY_MS) {
112
+ target = await fetchForwardTarget(auth, agentId);
113
+ mintedAt = Date.now();
114
+ }
115
+ return target;
116
+ }
117
+ /**
118
+ * Dial one tunnel through the edge to the daemon's /portal/tcp. Resolves
119
+ * once the upgrade completes, with the duplex PAUSED — the pool holds it
120
+ * warm without dropping any early bytes, and adoption resumes it by piping.
121
+ */
122
+ async function dialTunnel() {
123
+ const resolved = await freshTarget();
124
+ const ws = new WebSocket(`${resolved.daemonOrigin.replace(/^http/, "ws")}/portal/tcp?port=${targetPort}`, { headers: { authorization: `Bearer ${resolved.token}` } });
125
+ return new Promise((resolve, reject) => {
126
+ ws.once("error", reject);
127
+ ws.once("open", () => {
128
+ ws.removeListener("error", reject);
129
+ const stream = createWebSocketStream(ws);
130
+ stream.pause();
131
+ resolve({
132
+ stream,
133
+ close: () => stream.destroy()
134
+ });
135
+ });
136
+ });
137
+ }
138
+ const pool = createTunnelPool({
139
+ initialSize: INITIAL_POOL_SIZE,
140
+ maxSize: MAX_POOL_SIZE,
141
+ dial: dialTunnel,
142
+ log
143
+ });
144
+ const server = net.createServer((sock) => {
145
+ sock.pause();
146
+ const warm = pool.take();
147
+ if (warm) {
148
+ warm.stream.on("error", () => sock.destroy());
149
+ sock.on("error", () => warm.stream.destroy());
150
+ sock.pipe(warm.stream).pipe(sock);
151
+ sock.resume();
152
+ return;
153
+ }
154
+ (async () => {
155
+ let tunnel;
156
+ try {
157
+ tunnel = await dialTunnel();
158
+ } catch (err) {
159
+ log(`forward: tunnel connect failed: ${err instanceof Error ? err.message : String(err)}`);
160
+ sock.destroy();
161
+ return;
162
+ }
163
+ tunnel.stream.on("error", () => sock.destroy());
164
+ sock.on("error", () => tunnel.stream.destroy());
165
+ sock.pipe(tunnel.stream).pipe(sock);
166
+ sock.resume();
167
+ })();
168
+ });
169
+ await new Promise((resolve, reject) => {
170
+ server.once("error", reject);
171
+ server.listen(localPort, "127.0.0.1", () => {
172
+ server.removeListener("error", reject);
173
+ resolve();
174
+ });
175
+ });
176
+ const addr = server.address();
177
+ return {
178
+ port: addr && typeof addr === "object" ? addr.port : localPort,
179
+ close: () => new Promise((resolve) => {
180
+ pool.close();
181
+ server.close(() => resolve());
182
+ })
183
+ };
184
+ }
185
+
186
+ //#endregion
187
+ export { startForward };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
3
- import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-BY2nADw5.mjs";
3
+ import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-CgfbKXst.mjs";
4
4
  import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-2wju4gC_.mjs";
5
5
  import path from "node:path";
6
6
  import Conf from "conf";
@@ -9,7 +9,7 @@ import stableStringify from "safe-stable-stringify";
9
9
 
10
10
  //#region src/config.ts
11
11
  /** Default host for the public management API (`/v1`, API-key auth). */
12
- const DEFAULT_API_URL = "https://api.skydive.com";
12
+ const DEFAULT_API_URL = typeof SKYDIVE_BUILD_API_URL === "string" ? SKYDIVE_BUILD_API_URL : "https://api.skydive.com";
13
13
  /**
14
14
  * Default origin for the interactive chat client (`skydive chat`).
15
15
  *
@@ -20,9 +20,9 @@ const DEFAULT_API_URL = "https://api.skydive.com";
20
20
  * `DEFAULT_API_URL`; override with `--api-url` / `SKYDIVE_APP_URL` for local
21
21
  * dev or while the DNS record is still being provisioned.
22
22
  */
23
- const DEFAULT_APP_URL = "https://api.skydive.com";
23
+ const DEFAULT_APP_URL = DEFAULT_API_URL;
24
24
  /** Web front door, for pages opened in the user's browser. */
25
- const DEFAULT_WEB_URL = "https://skydive.com";
25
+ const DEFAULT_WEB_URL = typeof SKYDIVE_BUILD_WEB_URL === "string" ? SKYDIVE_BUILD_WEB_URL : "https://skydive.com";
26
26
  function resolveWebUrl(appUrl) {
27
27
  if (appUrl == null) return appUrl;
28
28
  return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
@@ -213,6 +213,32 @@ function saveUpdateCheck(value) {
213
213
  }
214
214
  store.set("updateCheck", value);
215
215
  }
216
+ /**
217
+ * Agent a bare `skydive chat` (no --agent/--resume) opens a new conversation
218
+ * with. Any selector `--agent` accepts works: an id, or a unique
219
+ * case-insensitive slug/name, resolved against the active workspace's roster
220
+ * at launch. Null when unset (the launch falls back to the agent picker).
221
+ * A hand-edited blank value counts as unset rather than as a selector no
222
+ * agent could ever match.
223
+ */
224
+ function getDefaultAgent() {
225
+ const value = store.get("defaultAgent")?.trim();
226
+ return value ? value : null;
227
+ }
228
+ function saveDefaultAgent(value) {
229
+ store.set("defaultAgent", value);
230
+ }
231
+ /**
232
+ * Remember the agent the chat TUI just opened a conversation with, so the
233
+ * next bare `skydive chat` returns to it (last used wins). Records the id —
234
+ * stable across renames, unlike a slug/name selector. Skips the write when
235
+ * the value already matches: this runs on every chat-screen entry, and an
236
+ * unchanged default shouldn't rewrite config.json.
237
+ */
238
+ function recordDefaultAgent(agentId) {
239
+ if (getDefaultAgent() === agentId) return;
240
+ store.set("defaultAgent", agentId);
241
+ }
216
242
  function parseBoolean(raw) {
217
243
  const v = raw.trim().toLowerCase();
218
244
  if ([
@@ -229,25 +255,40 @@ function parseBoolean(raw) {
229
255
  ].includes(v)) return ok(false);
230
256
  return err(`Expected a boolean (true/false), got "${raw}".`);
231
257
  }
232
- const PREFERENCES = [{
233
- key: "shareMachineDefault",
234
- type: "boolean",
235
- describe: "Share this machine over the portal on `skydive chat` launch, as if --share-machine were passed. Default false.",
236
- read: () => getShareMachineDefault(),
237
- isSet: () => store.has("shareMachineDefault"),
238
- parse: parseBoolean,
239
- write: (value) => saveShareMachineDefault(value),
240
- clear: () => store.delete("shareMachineDefault")
241
- }, {
242
- key: "updateCheck",
243
- type: "boolean",
244
- describe: "Run the daily background update check and its \"Update available\" notice. Set false to disable. Default true.",
245
- read: () => !getUpdateCheckDisabled(),
246
- isSet: () => store.has("updateCheck"),
247
- parse: parseBoolean,
248
- write: (value) => saveUpdateCheck(value),
249
- clear: () => store.delete("updateCheck")
250
- }];
258
+ const PREFERENCES = [
259
+ {
260
+ key: "shareMachineDefault",
261
+ type: "boolean",
262
+ describe: "Share this machine over the portal on `skydive chat` launch, as if --share-machine were passed. Default false.",
263
+ read: () => getShareMachineDefault(),
264
+ isSet: () => store.has("shareMachineDefault"),
265
+ set: (raw) => parseBoolean(raw).map(saveShareMachineDefault),
266
+ clear: () => store.delete("shareMachineDefault")
267
+ },
268
+ {
269
+ key: "updateCheck",
270
+ type: "boolean",
271
+ describe: "Run the daily background update check and its \"Update available\" notice. Set false to disable. Default true.",
272
+ read: () => !getUpdateCheckDisabled(),
273
+ isSet: () => store.has("updateCheck"),
274
+ set: (raw) => parseBoolean(raw).map(saveUpdateCheck),
275
+ clear: () => store.delete("updateCheck")
276
+ },
277
+ {
278
+ key: "defaultAgent",
279
+ type: "string",
280
+ describe: "Agent (id, slug, or name) a bare `skydive chat` opens a new conversation with, skipping the agent picker. Unset by default.",
281
+ read: () => getDefaultAgent(),
282
+ isSet: () => getDefaultAgent() !== null,
283
+ set: (raw) => {
284
+ const value = raw.trim();
285
+ if (!value) return err("Expected an agent id, slug, or name (use `config unset defaultAgent` to clear it).");
286
+ saveDefaultAgent(value);
287
+ return ok(void 0);
288
+ },
289
+ clear: () => store.delete("defaultAgent")
290
+ }
291
+ ];
251
292
  function getPreference(key) {
252
293
  return PREFERENCES.find((p) => p.key === key);
253
294
  }
@@ -282,14 +323,15 @@ function parseButton(element) {
282
323
  const params = isRecord(press.params) ? press.params : {};
283
324
  const primary = props.variant === "primary";
284
325
  if (press.action === "approve_portal_access") {
285
- const agentId = params.agentId;
286
- if (typeof agentId !== "string" || !agentId) return null;
326
+ const agentId = optionalString(params.agentId);
327
+ if (!agentId) return null;
287
328
  return {
288
329
  label,
289
330
  action: {
290
331
  kind: "grant_portal",
291
332
  agentId,
292
- deviceId: typeof params.deviceId === "string" && params.deviceId ? params.deviceId : null
333
+ deviceId: optionalString(params.deviceId),
334
+ conversationId: optionalString(params.conversationId)
293
335
  },
294
336
  primary
295
337
  };
@@ -323,12 +365,131 @@ function parseButton(element) {
323
365
  function specKeyFor(spec) {
324
366
  return stableStringify(spec) ?? crypto.randomUUID();
325
367
  }
368
+ const COMPUTE_TIER_NAMES = {
369
+ small: "Lite",
370
+ large: "Standard",
371
+ xlarge: "Pro",
372
+ xxlarge: "Max",
373
+ xxxlarge: "Ultra"
374
+ };
375
+ function computeTierLabel(tier, memoryGb) {
376
+ const name = typeof tier === "string" ? COMPUTE_TIER_NAMES[tier] ?? null : null;
377
+ const memory = Number(memoryGb);
378
+ const memoryLabel = Number.isFinite(memory) && memory > 0 ? `${Math.round(memory)} GB` : null;
379
+ if (name && memoryLabel) return `${name} (${memoryLabel})`;
380
+ if (name) return name;
381
+ if (memoryLabel) return memoryLabel;
382
+ return typeof tier === "string" && tier ? tier : "unknown tier";
383
+ }
384
+ /**
385
+ * The card line for a settled decision, shared by the spec parser (a settled
386
+ * re-emission) and the chat screen (the decision POST's response, which on
387
+ * `alreadyDecided` reports whatever decision actually stuck). Wording follows
388
+ * the web card's chips (Upgraded / Kept current / Withdrawn).
389
+ */
390
+ /**
391
+ * The full status domain the api emits on a compute request. Anything else —
392
+ * missing, or a value this CLI predates — returns null so the caller can
393
+ * refuse it instead of guessing at unknown semantics.
394
+ */
395
+ function parseComputeRequestStatus(value) {
396
+ switch (value) {
397
+ case "pending":
398
+ case "approved":
399
+ case "denied":
400
+ case "cancelled": return value;
401
+ default: return null;
402
+ }
403
+ }
404
+ function computeSettledLabel(status) {
405
+ switch (status) {
406
+ case "approved": return "upgraded";
407
+ case "denied": return "kept the current tier";
408
+ case "cancelled": return "withdrawn";
409
+ default: return null;
410
+ }
411
+ }
412
+ /**
413
+ * The `platform portal request` consent card. The server emits one
414
+ * DesktopHandoffCard spec for every surface (api routes/portal.ts): the web
415
+ * renders it as the "continue in the Skydive desktop app" handoff, but this
416
+ * terminal's own portal daemon can provide the machine, so here it maps onto
417
+ * the existing grant_portal approval instead of a pointer to the app. Without
418
+ * this the spec is not a Card, parses to null, and the request is silently
419
+ * dropped — `platform portal request` looks desktop-only from the CLI
420
+ * (ANY-6521).
421
+ */
422
+ function parseDesktopHandoffCard(rootEl) {
423
+ const props = isRecord(rootEl.props) ? rootEl.props : {};
424
+ const agentId = optionalString(props.agentId);
425
+ if (!agentId) return null;
426
+ const agentName = optionalString(props.agentName) ?? "This agent";
427
+ return {
428
+ title: `Let ${agentName} use your computer?`,
429
+ subtitle: null,
430
+ description: `Approving shares this machine with ${agentName} while you're signed in. Revoke anytime: skydive portal revoke --agent "${agentName}"`,
431
+ fields: [],
432
+ button: {
433
+ label: "Approve",
434
+ action: {
435
+ kind: "grant_portal",
436
+ agentId,
437
+ deviceId: optionalString(props.deviceId),
438
+ conversationId: optionalString(props.conversationId)
439
+ }
440
+ },
441
+ state: {},
442
+ computeRequestId: null,
443
+ settled: null
444
+ };
445
+ }
446
+ /**
447
+ * The `platform compute request` approval card. The web renders it as the
448
+ * Approve/Deny "More compute requested" card; here it maps onto a
449
+ * decide_compute button (ctrl+r opens the y/n decision prompt). Without this
450
+ * the spec is not a Card, parses to null, and the request is silently
451
+ * dropped — the user never sees it and the agent waits on a decision the
452
+ * terminal can't give (ANY-6653). A settled spec (the decision route re-emits
453
+ * the card with its final status) parses to a button-less resolved card. A
454
+ * status this CLI doesn't know also parses to null — rendering it as pending
455
+ * would offer a decision on a request in a state we can't represent.
456
+ */
457
+ function parseComputeRequestCard(rootEl) {
458
+ const props = isRecord(rootEl.props) ? rootEl.props : {};
459
+ const requestId = optionalString(props.requestId);
460
+ if (!requestId) return null;
461
+ const status = parseComputeRequestStatus(props.status);
462
+ if (status === null) return null;
463
+ const agentName = optionalString(props.agentName) ?? "This agent";
464
+ const from = computeTierLabel(props.fromTier, props.fromMemoryGb);
465
+ const to = computeTierLabel(props.requestedTier, props.requestedMemoryGb);
466
+ const settled = computeSettledLabel(status);
467
+ return {
468
+ title: "More compute requested",
469
+ subtitle: `${agentName}: ${from} → ${to}`,
470
+ description: optionalString(props.reason),
471
+ fields: [],
472
+ button: settled ? null : {
473
+ label: "Decide",
474
+ action: {
475
+ kind: "decide_compute",
476
+ requestId
477
+ }
478
+ },
479
+ state: {},
480
+ computeRequestId: requestId,
481
+ settled
482
+ };
483
+ }
326
484
  function parseConnectCard(spec) {
327
485
  if (!isRecord(spec)) return null;
328
486
  const { root, elements } = spec;
329
487
  if (typeof root !== "string" || !isRecord(elements)) return null;
330
488
  const rootEl = elements[root];
331
- if (!isRecord(rootEl) || rootEl.type !== "Card") return null;
489
+ if (!isRecord(rootEl)) return null;
490
+ if (rootEl.type === "DesktopHandoffCard") return parseDesktopHandoffCard(rootEl);
491
+ if (rootEl.type === "ComputeRequestCard") return parseComputeRequestCard(rootEl);
492
+ if (rootEl.type !== "Card") return null;
332
493
  const rootProps = isRecord(rootEl.props) ? rootEl.props : {};
333
494
  const title = optionalString(rootProps.title);
334
495
  if (!title) return null;
@@ -366,7 +527,9 @@ function parseConnectCard(spec) {
366
527
  label: preferred.label,
367
528
  action: preferred.action
368
529
  } : null,
369
- state: isRecord(spec.state) ? spec.state : {}
530
+ state: isRecord(spec.state) ? spec.state : {},
531
+ computeRequestId: null,
532
+ settled: null
370
533
  };
371
534
  }
372
535
 
@@ -507,6 +670,13 @@ function summarizeConnectCard(card, appUrl) {
507
670
  agentId: act.agentId
508
671
  }
509
672
  };
673
+ case "decide_compute": return {
674
+ ...base,
675
+ action: {
676
+ kind: "decide_compute",
677
+ requestId: act.requestId
678
+ }
679
+ };
510
680
  default: return {
511
681
  ...base,
512
682
  action: { kind: "unsupported" }
@@ -516,14 +686,16 @@ function summarizeConnectCard(card, appUrl) {
516
686
  /**
517
687
  * Parse a `data-anyone-render-spec` stream chunk into a connect-card summary,
518
688
  * or null if the chunk isn't a connect card (other render specs — training,
519
- * deep-learn, compute-request — parse to null, same as the TUI).
689
+ * deep-learn — parse to null, same as the TUI). A card that arrives already
690
+ * settled (a compute request's decided re-emission) is an FYI, not an action,
691
+ * so it stays out of the "[action needed]" stream too.
520
692
  */
521
693
  function connectCardFromChunk(chunk, appUrl) {
522
694
  if (chunk["type"] !== "data-anyone-render-spec") return null;
523
695
  const data = chunk["data"];
524
696
  if (!isRecord(data)) return null;
525
697
  const card = parseConnectCard(data["spec"]);
526
- if (!card) return null;
698
+ if (!card || card.settled !== null) return null;
527
699
  return summarizeConnectCard(card, appUrl);
528
700
  }
529
701
  /** Render a connect-card summary as a human-readable action block. */
@@ -542,6 +714,9 @@ function formatConnectCard(card) {
542
714
  case "approve_portal":
543
715
  lines.push(`Approve local-machine access for agent ${card.action.agentId} in the TUI or web app.`);
544
716
  break;
717
+ case "decide_compute":
718
+ lines.push("Approve or deny this compute increase in the interactive TUI or web app.");
719
+ break;
545
720
  case "unsupported":
546
721
  lines.push("Open this conversation in the web app to continue.");
547
722
  break;
@@ -562,17 +737,18 @@ function formatConnectCard(card) {
562
737
  * disambiguate: an `--agent` selector must match exactly one agent, and
563
738
  * when it's omitted we only auto-pick if the account has exactly one.
564
739
  */
565
- async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversationId, json, machineShare, grantTargetAgent }) {
740
+ async function runPrint({ appUrl, sessionToken, workspaceId, prompt, agentSelector, conversationId, json, machineShare, grantTargetAgent }) {
566
741
  const client = createRestClient({
567
742
  appUrl,
568
- sessionToken
743
+ sessionToken,
744
+ workspaceId
569
745
  });
570
746
  const agent = resolveAgent(await client.listAgents({
571
747
  scope: "org",
572
748
  onPage: null
573
749
  }), agentSelector);
574
750
  if (machineShare) {
575
- if (!machineShare.isGranted(agent.id) && grantTargetAgent) await machineShare.grantAgent(agent.id);
751
+ if (!machineShare.isGranted(agent.id) && grantTargetAgent) await machineShare.grantAgent(agent.id, null);
576
752
  if (machineShare.isGranted(agent.id)) console.error(`portal: shared this machine with ${agent.name} for this run (grant persists until revoked)`);
577
753
  else console.error(`portal: this machine is shared (shareMachineDefault), but ${agent.name} has no grant. Approve its request, or run \`skydive portal grant --agent ${agent.name}\`.`);
578
754
  }
@@ -657,7 +833,7 @@ async function collectRunText({ client, appUrl, target, onText, messageIdForHint
657
833
  const onEvent = (event) => {
658
834
  if (event.kind === "finished") {
659
835
  if (event.outcome) billingBlocked = event.outcome;
660
- if (event.error) streamError = event.error;
836
+ if (event.error && streamError === null) streamError = event.error;
661
837
  return;
662
838
  }
663
839
  const chunk = event.chunk;
@@ -760,4 +936,4 @@ async function readStdin() {
760
936
  }
761
937
 
762
938
  //#endregion
763
- export { getStoredApiKeyWorkspaceName as A, saveTheme as B, getLastSeenVersion as C, getSavedTheme as D, getReviewStateDir as E, resolveManagementAuth as F, resolveSession as I, resolveWebUrl as L, resolveAppUrl as M, resolveChatAuth as N, getShareMachineDefault as O, resolveConfig as P, saveConfig as R, getConfigPath as S, getPromptHistoryPath as T, setLastSeenVersion as V, API_KEY_PREFIX as _, runPrint as a, PREFERENCES as b, cardActionErrorMessage as c, reconcileMaskedInput as d, resolveConnectUrl as f, API_KEY_FAMILY_PREFIX as g, API_KEYS_URL as h, resolveAgent as i, getUpdateCheckDisabled as j, getStoredApiKeyId as k, parseExternalOauthConnectParams as l, specKeyFor as m, messageGet as n, toPrintError as o, parseConnectCard as p, readStdin as r, MASK_CHAR as s, collectRunText as t, parseOauthConnectParams as u, DEFAULT_API_URL as v, getPreference as w, deleteConfig as x, DEFAULT_APP_URL as y, saveSession as z };
939
+ export { getShareMachineDefault as A, resolveWebUrl as B, getConfigPath as C, getPromptHistoryPath as D, getPreference as E, resolveAppUrl as F, saveSession as H, resolveChatAuth as I, resolveConfig as L, getStoredApiKeyWorkspaceName as M, getUpdateCheckDisabled as N, getReviewStateDir as O, recordDefaultAgent as P, resolveManagementAuth as R, deleteConfig as S, getLastSeenVersion as T, saveTheme as U, saveConfig as V, setLastSeenVersion as W, API_KEY_FAMILY_PREFIX as _, runPrint as a, DEFAULT_APP_URL as b, cardActionErrorMessage as c, reconcileMaskedInput as d, resolveConnectUrl as f, API_KEYS_URL as g, specKeyFor as h, resolveAgent as i, getStoredApiKeyId as j, getSavedTheme as k, parseExternalOauthConnectParams as l, parseConnectCard as m, messageGet as n, toPrintError as o, computeSettledLabel as p, readStdin as r, MASK_CHAR as s, collectRunText as t, parseOauthConnectParams as u, API_KEY_PREFIX as v, getDefaultAgent as w, PREFERENCES as x, DEFAULT_API_URL as y, resolveSession as z };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-CbayCa87.mjs";
3
- import "./rest-BY2nADw5.mjs";
2
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-C_TnQSPg.mjs";
3
+ import "./rest-CgfbKXst.mjs";
4
4
  import "./billing-blocked-2wju4gC_.mjs";
5
5
 
6
6
  export { messageGet, readStdin, resolveAgent, runPrint };
@@ -14,7 +14,7 @@ import { n as printError } from "./output-DYzzdXYV.mjs";
14
14
  * the reply (and to --json).
15
15
  */
16
16
  async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
17
- const { PortalClient } = await import("./client-CKBQ8pft.mjs");
17
+ const { PortalClient } = await import("./client-hH2PJL8y.mjs");
18
18
  let signalConnected;
19
19
  const connected = new Promise((resolve) => {
20
20
  signalConnected = resolve;
@@ -26,6 +26,8 @@ async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
26
26
  deviceToken: null
27
27
  }),
28
28
  resolveCwd: () => process.cwd(),
29
+ persistedMachineName: null,
30
+ onMachineName: () => {},
29
31
  onState: (state) => {
30
32
  if (state.status === "connected") signalConnected();
31
33
  if (state.status === "error") console.error(`portal: connection error: ${state.error ?? "unknown"}; retrying`);