skydive-cli 0.5.0-beta.9 → 0.5.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 (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +61 -13
  3. package/dist/js/api-BFQ4PQDA.mjs +315 -0
  4. package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
  5. package/dist/js/bin.mjs +674 -307
  6. package/dist/js/{boot-Q-Kh3nn5.mjs → boot-DD4T-61U.mjs} +4558 -930
  7. package/dist/js/chunk-BbwQpWto.mjs +33 -0
  8. package/dist/js/{client-DabRpc_T.mjs → client--k9cjfkX.mjs} +437 -39
  9. package/dist/js/{client-c4c5MmgN.mjs → client-Btq6bMzX.mjs} +108 -2
  10. package/dist/js/client-Ct0-JZSS.mjs +5 -0
  11. package/dist/js/daemon-CCgNLD0H.mjs +7 -0
  12. package/dist/js/{daemon-D21wQ7DI.mjs → daemon-Do1jU2UF.mjs} +123 -43
  13. package/dist/js/daemon-client-C7nE-lLK.mjs +8 -0
  14. package/dist/js/{daemon-client-DPUNjhBB.mjs → daemon-client-Dvad009G.mjs} +1 -1
  15. package/dist/js/dist-CRtjM7ba.mjs +1750 -0
  16. package/dist/js/forward-C-f04uyE.mjs +208 -0
  17. package/dist/js/{profiler-BkCV__ao.mjs → install-CtAVvERm.mjs} +545 -215
  18. package/dist/js/launcher.mjs +49 -0
  19. package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
  20. package/dist/js/{print-Wakr3GJd.mjs → print-Bx8qUC9U.mjs} +3 -3
  21. package/dist/js/{print-BpuyEfWX.mjs → print-D_UEjdSw.mjs} +257 -35
  22. package/dist/js/{print-share-CKLPmsg0.mjs → print-share-Cz0EO2RK.mjs} +9 -3
  23. package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
  24. package/dist/js/{raw-pty-DY4KelZW.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
  25. package/dist/js/{rest-I3imNduB.mjs → rest-B9__Zsuk.mjs} +144 -19
  26. package/dist/js/rest-Dc0EEok3.mjs +6 -0
  27. package/dist/js/tls-cert-BpCaD5AT.mjs +4 -0
  28. package/dist/js/tls-cert-Rua2oV7n.mjs +67 -0
  29. package/package.json +12 -4
  30. package/dist/js/api-DG5W6iwx.mjs +0 -131
  31. package/dist/js/client-BuU34IVE.mjs +0 -5
  32. package/dist/js/daemon-LSDSvMaC.mjs +0 -6
  33. package/dist/js/daemon-client-CUSq-Wuh.mjs +0 -7
  34. package/dist/js/forward-18QoL5dO.mjs +0 -68
  35. package/dist/js/raw-pty-DmdUf4_w.mjs +0 -5
  36. package/dist/js/rest-D29qNkto.mjs +0 -6
  37. /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
  38. /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-BF2NZZE3.mjs} +0 -0
  39. /package/dist/js/{output-DYzzdXYV.mjs → output-C9mb3sUB.mjs} +0 -0
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ //#region src/launcher.ts
8
+ const PLATFORM_PACKAGES = {
9
+ "darwin-arm64": "skydive-cli-darwin-arm64",
10
+ "darwin-x64": "skydive-cli-darwin-x64",
11
+ "linux-x64": "skydive-cli-linux-x64",
12
+ "linux-arm64": "skydive-cli-linux-arm64"
13
+ };
14
+ const require = createRequire(import.meta.url);
15
+ /** The compiled per-platform binary, or null if none is installed/available. */
16
+ function resolveBinaryPath() {
17
+ const pkg = PLATFORM_PACKAGES[`${process.platform}-${process.arch}`];
18
+ if (!pkg) return null;
19
+ try {
20
+ return require.resolve(`${pkg}/bin`);
21
+ } catch (_error) {
22
+ return null;
23
+ }
24
+ }
25
+ /** The JS bundle sibling of this launcher (`dist/js/bin.mjs`). */
26
+ function jsBundlePath() {
27
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), "bin.mjs");
28
+ }
29
+ const args = process.argv.slice(2);
30
+ const binaryPath = resolveBinaryPath();
31
+ let child;
32
+ if (binaryPath) {
33
+ const env = { ...process.env };
34
+ if (env["SKYDIVE_CLI_INSTALL_SOURCE"] === void 0) env["SKYDIVE_CLI_INSTALL_SOURCE"] = "package-manager";
35
+ child = spawnSync(binaryPath, args, {
36
+ stdio: "inherit",
37
+ env
38
+ });
39
+ } else child = spawnSync(process.execPath, [jsBundlePath(), ...args], { stdio: "inherit" });
40
+ if (child.error) {
41
+ const what = binaryPath ? `the platform binary (${binaryPath})` : `the CLI under Node (${process.execPath})`;
42
+ process.stderr.write(`skydive: failed to launch ${what}: ${child.error.message}\n`);
43
+ process.exit(1);
44
+ }
45
+ if (child.signal) process.kill(process.pid, child.signal);
46
+ else process.exit(child.status ?? 0);
47
+
48
+ //#endregion
49
+ export { };
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { r as __toESM } from "./chunk-BbwQpWto.mjs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
6
+
7
+ //#region ../portal-daemon/src/localhost-cert.ts
8
+ /**
9
+ * Durable home for the one piece of state devcert doesn't keep for us: the
10
+ * user's decision to skip automatic cert setup. The daemon's own state dir is
11
+ * tmpdir-based (wiped on reboot), and re-raising a system trust prompt on
12
+ * every `portal forward` after a decline is exactly the nagging that trains
13
+ * people to fear the feature.
14
+ */
15
+ function declineMarkerPath() {
16
+ return path.join(os.homedir(), ".config", "skydive-portal", "tls-declined");
17
+ }
18
+ async function hasDeclined() {
19
+ try {
20
+ await readFile(declineMarkerPath());
21
+ return true;
22
+ } catch (_error) {
23
+ return false;
24
+ }
25
+ }
26
+ async function recordDecline(reason) {
27
+ const marker = declineMarkerPath();
28
+ try {
29
+ await mkdir(path.dirname(marker), { recursive: true });
30
+ await writeFile(marker, `Automatic localhost TLS setup failed or was declined:\n${reason}\n\nDelete this file to let it try again.\n`);
31
+ } catch (_error) {}
32
+ }
33
+ /**
34
+ * Fully automatic localhost cert: devcert generates a per-machine local CA on
35
+ * first use, installs it into the system/browser trust stores (one OS-native
36
+ * consent prompt, the mkcert pattern), signs a `localhost` leaf, and reuses
37
+ * both silently on every later run. The private key never leaves the machine
38
+ * and the CA is unique to it, so trusting it vouches only for this user's own
39
+ * loopback.
40
+ *
41
+ * Every failure — the user declining the trust prompt included — degrades to
42
+ * the plain-http origin (the product default) with one log line, and writes a
43
+ * marker so subsequent runs don't nag. `resetLocalhostCertDecline` (or
44
+ * deleting the marker file) re-arms it.
45
+ */
46
+ function localhostCertSource(log) {
47
+ return async () => {
48
+ if (await hasDeclined()) return null;
49
+ try {
50
+ const { certificateFor } = await import("./dist-CRtjM7ba.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
51
+ const { key, cert } = await certificateFor("localhost", { skipHostsFile: true });
52
+ return {
53
+ key: key.toString(),
54
+ cert: cert.toString(),
55
+ hostname: "localhost"
56
+ };
57
+ } catch (error) {
58
+ const reason = error instanceof Error ? error.message : String(error);
59
+ await recordDecline(reason);
60
+ log(`portal: automatic localhost TLS setup didn't complete (${reason}). Continuing with http only; delete ~/.config/skydive-portal/tls-declined to try again.`);
61
+ return null;
62
+ }
63
+ };
64
+ }
65
+
66
+ //#endregion
67
+ export { localhostCertSource };
@@ -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-BpuyEfWX.mjs";
3
- import "./rest-I3imNduB.mjs";
4
- import "./billing-blocked-2wju4gC_.mjs";
2
+ import "./rest-B9__Zsuk.mjs";
3
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-D_UEjdSw.mjs";
4
+ import "./billing-blocked-D3l5kJlX.mjs";
5
5
 
6
6
  export { messageGet, readStdin, resolveAgent, runPrint };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
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-I3imNduB.mjs";
4
- import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-2wju4gC_.mjs";
2
+ import { a as errorMessage, i as sendErrorMessage, o as isRecord, t as createRestClient } from "./rest-B9__Zsuk.mjs";
3
+ import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
4
+ import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-D3l5kJlX.mjs";
5
5
  import path from "node:path";
6
6
  import Conf from "conf";
7
7
  import { err, ok } from "neverthrow";
@@ -21,8 +21,13 @@ const DEFAULT_API_URL = typeof SKYDIVE_BUILD_API_URL === "string" ? SKYDIVE_BUIL
21
21
  * dev or while the DNS record is still being provisioned.
22
22
  */
23
23
  const DEFAULT_APP_URL = DEFAULT_API_URL;
24
+ /** The production web front door. Also the marker for "this is a production
25
+ * build": anything else means a preview or local stack baked its own origin
26
+ * in, and callers that reference production-only hosts (the `skydive.sh`
27
+ * installer in update-check/notice.ts) fall back to origin-relative URLs. */
28
+ const PRODUCTION_WEB_URL = "https://skydive.com";
24
29
  /** Web front door, for pages opened in the user's browser. */
25
- const DEFAULT_WEB_URL = typeof SKYDIVE_BUILD_WEB_URL === "string" ? SKYDIVE_BUILD_WEB_URL : "https://skydive.com";
30
+ const DEFAULT_WEB_URL = typeof SKYDIVE_BUILD_WEB_URL === "string" ? SKYDIVE_BUILD_WEB_URL : PRODUCTION_WEB_URL;
26
31
  function resolveWebUrl(appUrl) {
27
32
  if (appUrl == null) return appUrl;
28
33
  return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
@@ -40,7 +45,7 @@ const API_KEY_PREFIX = "sky_live_";
40
45
  */
41
46
  const API_KEY_FAMILY_PREFIX = "sky_";
42
47
  /** Where users mint and copy API keys. Shown in the login prompt. */
43
- const API_KEYS_URL = "skydive.com/settings/account";
48
+ const API_KEYS_URL = "skydive.com/settings/workspace";
44
49
  const store = new Conf({
45
50
  projectName: process.env["SKYDIVE_CONFIG_NAME"] ?? "skydive",
46
51
  projectSuffix: "",
@@ -213,6 +218,32 @@ function saveUpdateCheck(value) {
213
218
  }
214
219
  store.set("updateCheck", value);
215
220
  }
221
+ /**
222
+ * Agent a bare `skydive chat` (no --agent/--resume) opens a new conversation
223
+ * with. Any selector `--agent` accepts works: an id, or a unique
224
+ * case-insensitive slug/name, resolved against the active workspace's roster
225
+ * at launch. Null when unset (the launch falls back to the agent picker).
226
+ * A hand-edited blank value counts as unset rather than as a selector no
227
+ * agent could ever match.
228
+ */
229
+ function getDefaultAgent() {
230
+ const value = store.get("defaultAgent")?.trim();
231
+ return value ? value : null;
232
+ }
233
+ function saveDefaultAgent(value) {
234
+ store.set("defaultAgent", value);
235
+ }
236
+ /**
237
+ * Remember the agent the chat TUI just opened a conversation with, so the
238
+ * next bare `skydive chat` returns to it (last used wins). Records the id —
239
+ * stable across renames, unlike a slug/name selector. Skips the write when
240
+ * the value already matches: this runs on every chat-screen entry, and an
241
+ * unchanged default shouldn't rewrite config.json.
242
+ */
243
+ function recordDefaultAgent(agentId) {
244
+ if (getDefaultAgent() === agentId) return;
245
+ store.set("defaultAgent", agentId);
246
+ }
216
247
  function parseBoolean(raw) {
217
248
  const v = raw.trim().toLowerCase();
218
249
  if ([
@@ -229,25 +260,40 @@ function parseBoolean(raw) {
229
260
  ].includes(v)) return ok(false);
230
261
  return err(`Expected a boolean (true/false), got "${raw}".`);
231
262
  }
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
- }];
263
+ const PREFERENCES = [
264
+ {
265
+ key: "shareMachineDefault",
266
+ type: "boolean",
267
+ describe: "Share this machine over the portal on `skydive chat` launch, as if --share-machine were passed. Default false.",
268
+ read: () => getShareMachineDefault(),
269
+ isSet: () => store.has("shareMachineDefault"),
270
+ set: (raw) => parseBoolean(raw).map(saveShareMachineDefault),
271
+ clear: () => store.delete("shareMachineDefault")
272
+ },
273
+ {
274
+ key: "updateCheck",
275
+ type: "boolean",
276
+ describe: "Run the daily background update check and its \"Update available\" notice. Set false to disable. Default true.",
277
+ read: () => !getUpdateCheckDisabled(),
278
+ isSet: () => store.has("updateCheck"),
279
+ set: (raw) => parseBoolean(raw).map(saveUpdateCheck),
280
+ clear: () => store.delete("updateCheck")
281
+ },
282
+ {
283
+ key: "defaultAgent",
284
+ type: "string",
285
+ describe: "Agent (id, slug, or name) a bare `skydive chat` opens a new conversation with, skipping the agent picker. Unset by default.",
286
+ read: () => getDefaultAgent(),
287
+ isSet: () => getDefaultAgent() !== null,
288
+ set: (raw) => {
289
+ const value = raw.trim();
290
+ if (!value) return err("Expected an agent id, slug, or name (use `config unset defaultAgent` to clear it).");
291
+ saveDefaultAgent(value);
292
+ return ok(void 0);
293
+ },
294
+ clear: () => store.delete("defaultAgent")
295
+ }
296
+ ];
251
297
  function getPreference(key) {
252
298
  return PREFERENCES.find((p) => p.key === key);
253
299
  }
@@ -324,6 +370,50 @@ function parseButton(element) {
324
370
  function specKeyFor(spec) {
325
371
  return stableStringify(spec) ?? crypto.randomUUID();
326
372
  }
373
+ const COMPUTE_TIER_NAMES = {
374
+ small: "Lite",
375
+ large: "Standard",
376
+ xlarge: "Pro",
377
+ xxlarge: "Max",
378
+ xxxlarge: "Ultra"
379
+ };
380
+ function computeTierLabel(tier, memoryGb) {
381
+ const name = typeof tier === "string" ? COMPUTE_TIER_NAMES[tier] ?? null : null;
382
+ const memory = Number(memoryGb);
383
+ const memoryLabel = Number.isFinite(memory) && memory > 0 ? `${Math.round(memory)} GB` : null;
384
+ if (name && memoryLabel) return `${name} (${memoryLabel})`;
385
+ if (name) return name;
386
+ if (memoryLabel) return memoryLabel;
387
+ return typeof tier === "string" && tier ? tier : "unknown tier";
388
+ }
389
+ /**
390
+ * The card line for a settled decision, shared by the spec parser (a settled
391
+ * re-emission) and the chat screen (the decision POST's response, which on
392
+ * `alreadyDecided` reports whatever decision actually stuck). Wording follows
393
+ * the web card's chips (Upgraded / Kept current / Withdrawn).
394
+ */
395
+ /**
396
+ * The full status domain the api emits on a compute request. Anything else —
397
+ * missing, or a value this CLI predates — returns null so the caller can
398
+ * refuse it instead of guessing at unknown semantics.
399
+ */
400
+ function parseComputeRequestStatus(value) {
401
+ switch (value) {
402
+ case "pending":
403
+ case "approved":
404
+ case "denied":
405
+ case "cancelled": return value;
406
+ default: return null;
407
+ }
408
+ }
409
+ function computeSettledLabel(status) {
410
+ switch (status) {
411
+ case "approved": return "upgraded";
412
+ case "denied": return "kept the current tier";
413
+ case "cancelled": return "withdrawn";
414
+ default: return null;
415
+ }
416
+ }
327
417
  /**
328
418
  * The `platform portal request` consent card. The server emits one
329
419
  * DesktopHandoffCard spec for every surface (api routes/portal.ts): the web
@@ -340,9 +430,9 @@ function parseDesktopHandoffCard(rootEl) {
340
430
  if (!agentId) return null;
341
431
  const agentName = optionalString(props.agentName) ?? "This agent";
342
432
  return {
343
- title: `Let ${agentName} use your computer?`,
433
+ title: `Let ${agentName} run commands on your machine?`,
344
434
  subtitle: null,
345
- description: `Approving shares this machine with ${agentName} while you're signed in. Revoke anytime: skydive portal revoke --agent "${agentName}"`,
435
+ description: `Approving opens the portal to ${agentName} while you're signed in. Revoke anytime: skydive portal revoke --agent "${agentName}"`,
346
436
  fields: [],
347
437
  button: {
348
438
  label: "Approve",
@@ -353,7 +443,111 @@ function parseDesktopHandoffCard(rootEl) {
353
443
  conversationId: optionalString(props.conversationId)
354
444
  }
355
445
  },
356
- state: {}
446
+ state: {},
447
+ computeRequestId: null,
448
+ settled: null,
449
+ questions: []
450
+ };
451
+ }
452
+ /**
453
+ * The `platform compute request` approval card. The web renders it as the
454
+ * Approve/Deny "More compute requested" card; here it maps onto a
455
+ * decide_compute button (ctrl+r opens the y/n decision prompt). Without this
456
+ * the spec is not a Card, parses to null, and the request is silently
457
+ * dropped — the user never sees it and the agent waits on a decision the
458
+ * terminal can't give (ANY-6653). A settled spec (the decision route re-emits
459
+ * the card with its final status) parses to a button-less resolved card. A
460
+ * status this CLI doesn't know also parses to null — rendering it as pending
461
+ * would offer a decision on a request in a state we can't represent.
462
+ */
463
+ function parseComputeRequestCard(rootEl) {
464
+ const props = isRecord(rootEl.props) ? rootEl.props : {};
465
+ const requestId = optionalString(props.requestId);
466
+ if (!requestId) return null;
467
+ const status = parseComputeRequestStatus(props.status);
468
+ if (status === null) return null;
469
+ const agentName = optionalString(props.agentName) ?? "This agent";
470
+ const from = computeTierLabel(props.fromTier, props.fromMemoryGb);
471
+ const to = computeTierLabel(props.requestedTier, props.requestedMemoryGb);
472
+ const settled = computeSettledLabel(status);
473
+ return {
474
+ title: "More compute requested",
475
+ subtitle: `${agentName}: ${from} → ${to}`,
476
+ description: optionalString(props.reason),
477
+ fields: [],
478
+ button: settled ? null : {
479
+ label: "Decide",
480
+ action: {
481
+ kind: "decide_compute",
482
+ requestId
483
+ }
484
+ },
485
+ state: {},
486
+ computeRequestId: requestId,
487
+ settled,
488
+ questions: []
489
+ };
490
+ }
491
+ function readQuestionOptions(raw) {
492
+ if (!Array.isArray(raw)) return [];
493
+ return raw.flatMap((o) => {
494
+ if (!isRecord(o)) return [];
495
+ const label = optionalString(o.label);
496
+ if (!label) return [];
497
+ return [{ label }];
498
+ });
499
+ }
500
+ function readQuestionEntries(raw) {
501
+ if (!Array.isArray(raw)) return [];
502
+ return raw.flatMap((q) => {
503
+ if (!isRecord(q)) return [];
504
+ const question = optionalString(q.question);
505
+ if (!question) return [];
506
+ const options = readQuestionOptions(q.options);
507
+ if (options.length < 2) return [];
508
+ return [{
509
+ question,
510
+ options,
511
+ multiSelect: Boolean(q.multiSelect)
512
+ }];
513
+ });
514
+ }
515
+ /**
516
+ * The `platform ask` form. The web renders it as an interactive picker; here
517
+ * it maps onto an answer_question button so ctrl+r (and letter shortcuts)
518
+ * open an in-TUI picker. The composed answer is sent as a normal user
519
+ * message — same non-blocking model as the web card. Without this the spec
520
+ * is not a Card, parses to null, and the question is silently dropped.
521
+ */
522
+ /** Compose the user-message body the web card also sends. */
523
+ function composeQuestionAnswer(questions, answers) {
524
+ if (questions.length === 1) return (answers[0] ?? []).join(", ");
525
+ return questions.map((q, i) => `${q.question} → ${(answers[i] ?? []).join(", ")}`).join("\n");
526
+ }
527
+ function parseQuestionCard(rootEl) {
528
+ const props = isRecord(rootEl.props) ? rootEl.props : {};
529
+ const cardId = optionalString(props.cardId);
530
+ if (!cardId) return null;
531
+ const questions = readQuestionEntries(props.questions);
532
+ if (questions.length === 0) return null;
533
+ const settled = (props.status === "answered" ? "answered" : "pending") === "answered" ? optionalString(props.answerSummary) ?? "answered" : null;
534
+ const only = questions.length === 1 ? questions[0] : void 0;
535
+ return {
536
+ title: only ? only.question : "Questions",
537
+ subtitle: null,
538
+ description: null,
539
+ fields: [],
540
+ button: settled ? null : {
541
+ label: "Answer",
542
+ action: {
543
+ kind: "answer_question",
544
+ cardId
545
+ }
546
+ },
547
+ state: {},
548
+ computeRequestId: null,
549
+ settled,
550
+ questions
357
551
  };
358
552
  }
359
553
  function parseConnectCard(spec) {
@@ -363,6 +557,8 @@ function parseConnectCard(spec) {
363
557
  const rootEl = elements[root];
364
558
  if (!isRecord(rootEl)) return null;
365
559
  if (rootEl.type === "DesktopHandoffCard") return parseDesktopHandoffCard(rootEl);
560
+ if (rootEl.type === "ComputeRequestCard") return parseComputeRequestCard(rootEl);
561
+ if (rootEl.type === "QuestionCard") return parseQuestionCard(rootEl);
366
562
  if (rootEl.type !== "Card") return null;
367
563
  const rootProps = isRecord(rootEl.props) ? rootEl.props : {};
368
564
  const title = optionalString(rootProps.title);
@@ -401,7 +597,10 @@ function parseConnectCard(spec) {
401
597
  label: preferred.label,
402
598
  action: preferred.action
403
599
  } : null,
404
- state: isRecord(spec.state) ? spec.state : {}
600
+ state: isRecord(spec.state) ? spec.state : {},
601
+ computeRequestId: null,
602
+ settled: null,
603
+ questions: []
405
604
  };
406
605
  }
407
606
 
@@ -542,6 +741,20 @@ function summarizeConnectCard(card, appUrl) {
542
741
  agentId: act.agentId
543
742
  }
544
743
  };
744
+ case "decide_compute": return {
745
+ ...base,
746
+ action: {
747
+ kind: "decide_compute",
748
+ requestId: act.requestId
749
+ }
750
+ };
751
+ case "answer_question": return {
752
+ ...base,
753
+ action: {
754
+ kind: "answer_question",
755
+ questions: card.questions.map((q) => q.question)
756
+ }
757
+ };
545
758
  default: return {
546
759
  ...base,
547
760
  action: { kind: "unsupported" }
@@ -551,14 +764,16 @@ function summarizeConnectCard(card, appUrl) {
551
764
  /**
552
765
  * Parse a `data-anyone-render-spec` stream chunk into a connect-card summary,
553
766
  * or null if the chunk isn't a connect card (other render specs — training,
554
- * deep-learn, compute-request — parse to null, same as the TUI).
767
+ * deep-learn — parse to null, same as the TUI). A card that arrives already
768
+ * settled (a compute request's decided re-emission) is an FYI, not an action,
769
+ * so it stays out of the "[action needed]" stream too.
555
770
  */
556
771
  function connectCardFromChunk(chunk, appUrl) {
557
772
  if (chunk["type"] !== "data-anyone-render-spec") return null;
558
773
  const data = chunk["data"];
559
774
  if (!isRecord(data)) return null;
560
775
  const card = parseConnectCard(data["spec"]);
561
- if (!card) return null;
776
+ if (!card || card.settled !== null) return null;
562
777
  return summarizeConnectCard(card, appUrl);
563
778
  }
564
779
  /** Render a connect-card summary as a human-readable action block. */
@@ -575,7 +790,13 @@ function formatConnectCard(card) {
575
790
  lines.push(`Provide credential (${card.action.fields.join(", ") || "value"}) at: ${card.action.url}`);
576
791
  break;
577
792
  case "approve_portal":
578
- lines.push(`Approve local-machine access for agent ${card.action.agentId} in the TUI or web app.`);
793
+ lines.push(`Approve portal access to this machine for agent ${card.action.agentId} in the TUI or web app.`);
794
+ break;
795
+ case "decide_compute":
796
+ lines.push("Approve or deny this compute increase in the interactive TUI or web app.");
797
+ break;
798
+ case "answer_question":
799
+ lines.push(`Answer in the interactive TUI or web app: ${card.action.questions.join("; ")}`);
579
800
  break;
580
801
  case "unsupported":
581
802
  lines.push("Open this conversation in the web app to continue.");
@@ -597,10 +818,11 @@ function formatConnectCard(card) {
597
818
  * disambiguate: an `--agent` selector must match exactly one agent, and
598
819
  * when it's omitted we only auto-pick if the account has exactly one.
599
820
  */
600
- async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversationId, json, machineShare, grantTargetAgent }) {
821
+ async function runPrint({ appUrl, sessionToken, workspaceId, prompt, agentSelector, conversationId, json, machineShare, grantTargetAgent }) {
601
822
  const client = createRestClient({
602
823
  appUrl,
603
- sessionToken
824
+ sessionToken,
825
+ workspaceId
604
826
  });
605
827
  const agent = resolveAgent(await client.listAgents({
606
828
  scope: "org",
@@ -692,7 +914,7 @@ async function collectRunText({ client, appUrl, target, onText, messageIdForHint
692
914
  const onEvent = (event) => {
693
915
  if (event.kind === "finished") {
694
916
  if (event.outcome) billingBlocked = event.outcome;
695
- if (event.error) streamError = event.error;
917
+ if (event.error && streamError === null) streamError = event.error;
696
918
  return;
697
919
  }
698
920
  const chunk = event.chunk;
@@ -795,4 +1017,4 @@ async function readStdin() {
795
1017
  }
796
1018
 
797
1019
  //#endregion
798
- 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 };
1020
+ export { getSavedTheme as A, resolveSession as B, deleteConfig as C, getPreference as D, getLastSeenVersion as E, recordDefaultAgent as F, setLastSeenVersion as G, saveConfig as H, resolveAppUrl as I, resolveChatAuth as L, getStoredApiKeyId as M, getStoredApiKeyWorkspaceName as N, getPromptHistoryPath as O, getUpdateCheckDisabled as P, resolveConfig as R, PREFERENCES as S, getDefaultAgent as T, saveSession as U, resolveWebUrl as V, saveTheme as W, API_KEYS_URL as _, runPrint as a, DEFAULT_API_URL as b, cardActionErrorMessage as c, reconcileMaskedInput as d, resolveConnectUrl as f, specKeyFor as g, parseConnectCard as h, resolveAgent as i, getShareMachineDefault as j, getReviewStateDir as k, parseExternalOauthConnectParams as l, computeSettledLabel as m, messageGet as n, toPrintError as o, composeQuestionAnswer as p, readStdin as r, MASK_CHAR as s, collectRunText as t, parseOauthConnectParams as u, API_KEY_FAMILY_PREFIX as v, getConfigPath as w, DEFAULT_APP_URL as x, API_KEY_PREFIX as y, resolveManagementAuth as z };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as printError } from "./output-DYzzdXYV.mjs";
2
+ import { n as printError } from "./output-C9mb3sUB.mjs";
3
3
 
4
4
  //#region src/chat/print-share.ts
5
5
  /**
@@ -14,7 +14,8 @@ 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-BuU34IVE.mjs");
17
+ const { PortalClient } = await import("./client-Ct0-JZSS.mjs");
18
+ const { defaultTlsCertSource } = await import("./tls-cert-BpCaD5AT.mjs");
18
19
  let signalConnected;
19
20
  const connected = new Promise((resolve) => {
20
21
  signalConnected = resolve;
@@ -26,6 +27,11 @@ async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
26
27
  deviceToken: null
27
28
  }),
28
29
  resolveCwd: () => process.cwd(),
30
+ tlsCertSource: defaultTlsCertSource(process.env, (msg) => {
31
+ console.error(msg);
32
+ }),
33
+ persistedMachineName: null,
34
+ onMachineName: () => {},
29
35
  onState: (state) => {
30
36
  if (state.status === "connected") signalConnected();
31
37
  if (state.status === "error") console.error(`portal: connection error: ${state.error ?? "unknown"}; retrying`);
@@ -34,7 +40,7 @@ async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
34
40
  machineShare.enable();
35
41
  if (await Promise.race([connected.then(() => false), new Promise((resolve) => setTimeout(() => resolve(true), 3e4).unref())])) {
36
42
  machineShare.dispose();
37
- printError(`Could not connect the portal within 30s. Machine sharing is unavailable (network, or the portal kill switch is off). ${timeoutHint ?? "Check `skydive portal status`."}`);
43
+ printError(`Could not connect the portal within 30s. The portal is unavailable (network, or the portal kill switch is off). ${timeoutHint ?? "Check `skydive portal status`."}`);
38
44
  process.exit(1);
39
45
  }
40
46
  return machineShare;
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./client-Btq6bMzX.mjs";
3
+ import { t as runRawPtyPassthrough } from "./raw-pty-D5PhKZSl.mjs";
4
+
5
+ export { runRawPtyPassthrough };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
2
+ import { t as SandboxStream } from "./client-Btq6bMzX.mjs";
3
3
 
4
4
  //#region src/chat/sandbox/raw-pty.ts
5
5
  /**