skydive-cli 0.2.0-beta.610 → 0.2.0-beta.615

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 { t as HttpError } from "./rest-DMpigwmN.mjs";
2
+ import { t as HttpError } from "./rest-Q5yl_b2-.mjs";
3
3
  import os from "node:os";
4
4
  import { z } from "zod";
5
5
 
package/dist/js/bin.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { A as deleteConfig, B as resolveAppUrl, C as setActiveWorkspace, D as DEFAULT_API_URL, E as API_KEY_PREFIX, G as saveConfig, H as resolveManagementAuth, I as getShareMachineDefault, J as setLastSeenVersion, K as saveSession, L as getStoredApiKeyId, M as getLastSeenVersion, N as getPromptHistoryPath, R as getStoredApiKeyWorkspaceName, S as listWorkspaces, T as API_KEY_FAMILY_PREFIX, U as resolveSession, V as resolveConfig, X as version, Y as name, _ as themes, b as getActiveWorkspaceId, j as getConfigPath, k as DEFAULT_WEB_URL, o as brandHelpArt, t as installCrashHandler, w as API_KEYS_URL, x as getSessionIdentity, y as ensureActiveOrganization, z as getUpdateCheckDisabled } from "./install-BDaFCeB_.mjs";
3
- import { n as printError, r as printTable, t as output } from "./output-B4cW10Ph.mjs";
4
- import { n as createRestClient } from "./rest-DMpigwmN.mjs";
5
- import { i as resolveAgent } from "./print-6Bm51XhR.mjs";
6
- import { a as registerPortalDevice, c as machineIdentity, n as findThisDevice, o as revokePortalAccess, r as grantPortalAccess, t as fetchPortalDevices } from "./api-BGWcPuik.mjs";
2
+ import { C as setActiveWorkspace, S as listWorkspaces, T as version, _ as themes, b as getActiveWorkspaceId, o as brandHelpArt, t as installCrashHandler, w as name, x as getSessionIdentity, y as ensureActiveOrganization } from "./install-B5sKLfRc.mjs";
3
+ import { A as getUpdateCheckDisabled, C as getLastSeenVersion, D as getShareMachineDefault, I as saveConfig, L as saveSession, M as resolveConfig, N as resolveManagementAuth, O as getStoredApiKeyId, P as resolveSession, S as getConfigPath, _ as API_KEY_PREFIX, b as DEFAULT_WEB_URL, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as resolveAppUrl, k as getStoredApiKeyWorkspaceName, v as DEFAULT_API_URL, w as getPromptHistoryPath, x as deleteConfig, z as setLastSeenVersion } from "./print-CNIvmpqW.mjs";
4
+ import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
+ import { n as createRestClient } from "./rest-Q5yl_b2-.mjs";
6
+ import { a as registerPortalDevice, c as machineIdentity, n as findThisDevice, o as revokePortalAccess, r as grantPortalAccess, t as fetchPortalDevices } from "./api-CLTp-mKM.mjs";
7
7
  import { t as SandboxStream } from "./client-BVOAwU8M.mjs";
8
8
  import { hideBin } from "yargs/helpers";
9
9
  import yargs from "yargs";
@@ -1195,7 +1195,7 @@ const importCommand = {
1195
1195
  process.exit(1);
1196
1196
  }
1197
1197
  }
1198
- const { runChat } = await import("./boot-DFERQrw5.mjs");
1198
+ const { runChat } = await import("./boot-BJZrdTk0.mjs");
1199
1199
  await runChat({
1200
1200
  appUrl,
1201
1201
  sessionToken: session.value.sessionToken,
@@ -1222,7 +1222,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1222
1222
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
1223
1223
  process.exit(1);
1224
1224
  }
1225
- const { connectMachineShare } = await import("./print-share-DE_dkZX7.mjs");
1225
+ const { connectMachineShare } = await import("./print-share-rM8kNeEw.mjs");
1226
1226
  const machineShare = await connectMachineShare({
1227
1227
  appUrl,
1228
1228
  sessionToken: session.value.sessionToken,
@@ -1230,7 +1230,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1230
1230
  });
1231
1231
  const extra = (argv.print ?? "").trim();
1232
1232
  const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
1233
- const { runPrint } = await import("./print-qXJymvuF.mjs");
1233
+ const { runPrint } = await import("./print-nefugWMx.mjs");
1234
1234
  try {
1235
1235
  const result = await runPrint({
1236
1236
  appUrl,
@@ -1511,7 +1511,7 @@ const chatCommand = {
1511
1511
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
1512
1512
  process.exit(1);
1513
1513
  }
1514
- const { runChat } = await import("./boot-DFERQrw5.mjs");
1514
+ const { runChat } = await import("./boot-BJZrdTk0.mjs");
1515
1515
  await runChat({
1516
1516
  appUrl,
1517
1517
  sessionToken: session.value.sessionToken,
@@ -1548,7 +1548,7 @@ async function runPrintMode({ argv, appUrl }) {
1548
1548
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
1549
1549
  process.exit(1);
1550
1550
  }
1551
- const { runPrint, readStdin } = await import("./print-qXJymvuF.mjs");
1551
+ const { runPrint, readStdin } = await import("./print-nefugWMx.mjs");
1552
1552
  let prompt = (argv.print ?? "").trim();
1553
1553
  if (!prompt) {
1554
1554
  if (process.stdin.isTTY) {
@@ -1563,7 +1563,7 @@ async function runPrintMode({ argv, appUrl }) {
1563
1563
  }
1564
1564
  let machineShare = null;
1565
1565
  if (resolveShareMachine(argv)) {
1566
- const { connectMachineShare } = await import("./print-share-DE_dkZX7.mjs");
1566
+ const { connectMachineShare } = await import("./print-share-rM8kNeEw.mjs");
1567
1567
  machineShare = await connectMachineShare({
1568
1568
  appUrl,
1569
1569
  sessionToken: session.value.sessionToken,
@@ -1622,7 +1622,7 @@ const getCommand = {
1622
1622
  printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
1623
1623
  process.exit(1);
1624
1624
  }
1625
- const { messageGet } = await import("./print-qXJymvuF.mjs");
1625
+ const { messageGet } = await import("./print-nefugWMx.mjs");
1626
1626
  try {
1627
1627
  const result = await messageGet({
1628
1628
  appUrl,
@@ -1819,7 +1819,7 @@ const switchCommand = {
1819
1819
  printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
1820
1820
  process.exit(1);
1821
1821
  }
1822
- const { runWorkspacePicker } = await import("./boot-DFERQrw5.mjs");
1822
+ const { runWorkspacePicker } = await import("./boot-BJZrdTk0.mjs");
1823
1823
  await runWorkspacePicker(session);
1824
1824
  return;
1825
1825
  }
@@ -1897,7 +1897,7 @@ const openCommand = {
1897
1897
  const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
1898
1898
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
1899
1899
  const { machineName } = machineIdentity();
1900
- const { PortalClient } = await import("./client-BRfmlbgI.mjs");
1900
+ const { PortalClient } = await import("./client-D6UUwESi.mjs");
1901
1901
  let lastLine = "";
1902
1902
  let signalConnected;
1903
1903
  const connected = new Promise((resolve) => {
@@ -2069,8 +2069,8 @@ const sandboxCommand = {
2069
2069
  }).example("skydive sandbox --agent grace", "Live terminal (Ctrl-] detaches)").example("skydive sandbox --agent grace -- tail -n 50 /tmp/harness.log", "One-shot command (use `--` so its flags reach the sandbox)").example("skydive sandbox --agent grace -- sh -c 'ls /tmp | wc -l'", "Shell features go through an explicit `sh -c`"),
2070
2070
  handler: async (argv) => {
2071
2071
  const session = requireSession(argv);
2072
- const { createRestClient } = await import("./rest-C3RySrPa.mjs");
2073
- const { resolveAgent } = await import("./print-qXJymvuF.mjs");
2072
+ const { createRestClient } = await import("./rest-CBROy0L-.mjs");
2073
+ const { resolveAgent } = await import("./print-nefugWMx.mjs");
2074
2074
  const client = createRestClient({
2075
2075
  appUrl: session.appUrl,
2076
2076
  sessionToken: session.sessionToken
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { C as setActiveWorkspace, D as DEFAULT_API_URL, F as getSavedTheme, O as DEFAULT_APP_URL, P as getReviewStateDir, S as listWorkspaces, W as resolveWebUrl, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, j as getConfigPath, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, q as saveTheme, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-BDaFCeB_.mjs";
3
- import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, r as errorDetail, t as HttpError } from "./rest-DMpigwmN.mjs";
4
- import { c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams } from "./print-6Bm51XhR.mjs";
5
- import "./api-BGWcPuik.mjs";
2
+ import { C as setActiveWorkspace, S as listWorkspaces, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-B5sKLfRc.mjs";
3
+ import { E as getSavedTheme, F as resolveWebUrl, R as saveTheme, S as getConfigPath, T as getReviewStateDir, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-CNIvmpqW.mjs";
4
+ import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, r as errorDetail, t as HttpError } from "./rest-Q5yl_b2-.mjs";
5
+ import "./api-CLTp-mKM.mjs";
6
6
  import { t as SandboxStream } from "./client-BVOAwU8M.mjs";
7
- import { t as PortalClient } from "./client-l3OrGKaC.mjs";
7
+ import { t as PortalClient } from "./client-BigvBsPk.mjs";
8
8
  import { t as runRawPtyPassthrough } from "./raw-pty-Dbi2kb9v.mjs";
9
9
  import * as os$1 from "node:os";
10
10
  import { homedir, platform, release, tmpdir } from "node:os";
@@ -14,7 +14,7 @@ import open from "open";
14
14
  import { execFile, spawn } from "node:child_process";
15
15
  import { createHash } from "node:crypto";
16
16
  import { constants } from "node:fs";
17
- import { MarkdownRenderable, RenderableEvents, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks } from "@opentui/core";
17
+ import { MarkdownRenderable, RenderableEvents, StyledText, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks, fg, link } from "@opentui/core";
18
18
  import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
19
19
  import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
20
20
  import { create } from "zustand";
@@ -273,6 +273,8 @@ const noopAgentHost = {
273
273
  transition() {},
274
274
  setSession() {},
275
275
  setTitle() {},
276
+ addPullRequest() {},
277
+ removePullRequest() {},
276
278
  dispose() {}
277
279
  };
278
280
  const AgentHostContext = createContext(noopAgentHost);
@@ -298,6 +300,14 @@ function composeAgentHost(adapters) {
298
300
  if (disposed) return;
299
301
  for (const adapter of adapters) adapter.setTitle?.(title);
300
302
  },
303
+ addPullRequest(pr) {
304
+ if (disposed) return;
305
+ for (const adapter of adapters) adapter.addPullRequest?.(pr);
306
+ },
307
+ removePullRequest(pr) {
308
+ if (disposed) return;
309
+ for (const adapter of adapters) adapter.removePullRequest?.(pr);
310
+ },
301
311
  dispose() {
302
312
  if (disposed) return;
303
313
  disposed = true;
@@ -463,6 +473,61 @@ function createCmuxWorkspaceTitleAdapter({ socket, workspaceId }) {
463
473
  } };
464
474
  }
465
475
 
476
+ //#endregion
477
+ //#region src/chat/agent-host/adapters/cmux/sidebar-pr.ts
478
+ /** Neutral gray — deliberately not green/purple; we don't render PR state. */
479
+ const PILL_COLOR = "#8b949e";
480
+ const PILL_ICON = "git-pull-request";
481
+ const PILL_PRIORITY = 90;
482
+ /**
483
+ * The cmux sidebar status key for a PR, derived from its repo-qualified
484
+ * identity (`owner/repo#number`) so PRs from different repos never collide on a
485
+ * shared `pr-<number>` slot. cmux keys are space-split tokens, so we sanitize
486
+ * to `pr-<owner>-<repo>-<number>` (any non-alphanumeric run becomes `-`).
487
+ */
488
+ function pillKey(prKey) {
489
+ return `pr-${prKey.replace(/[^a-z0-9]+/gi, "-")}`;
490
+ }
491
+ /**
492
+ * A V1 socket arg value. cmux's line protocol splits on spaces, so a value
493
+ * with spaces (our `PR #123` label) must be double-quoted; the flag values we
494
+ * send (hex color, url, integers, the workspace id) never contain spaces or
495
+ * quotes, so simple quoting of the label is sufficient and there is nothing to
496
+ * escape.
497
+ */
498
+ function quote(value) {
499
+ return `"${value}"`;
500
+ }
501
+ function createCmuxSidebarPrAdapter({ socket, workspaceId }) {
502
+ const applied = /* @__PURE__ */ new Set();
503
+ let chain = Promise.resolve();
504
+ const enqueue = (step) => {
505
+ chain = chain.then(step).catch((_error) => {});
506
+ };
507
+ const runSet = async (pr) => {
508
+ if (applied.has(pr.key)) return;
509
+ applied.add(pr.key);
510
+ const line = `set_status ${pillKey(pr.key)} ${quote(`PR #${pr.number}`)} --icon=${PILL_ICON} --color=${PILL_COLOR} --priority=${PILL_PRIORITY} --url=${pr.url} --tab=${workspaceId}`;
511
+ if (!await socket.command(line)) applied.delete(pr.key);
512
+ };
513
+ const runClear = async (prKey) => {
514
+ if (!applied.has(prKey)) return;
515
+ if (await socket.command(`clear_status ${pillKey(prKey)} --tab=${workspaceId}`)) applied.delete(prKey);
516
+ };
517
+ return {
518
+ addPullRequest(pr) {
519
+ enqueue(() => runSet(pr));
520
+ },
521
+ removePullRequest(pr) {
522
+ enqueue(() => runClear(pr.key));
523
+ },
524
+ dispose() {
525
+ const keys = [...applied];
526
+ for (const key of keys) enqueue(() => runClear(key));
527
+ }
528
+ };
529
+ }
530
+
466
531
  //#endregion
467
532
  //#region src/chat/agent-host/adapters/cmux/socket.ts
468
533
  const cmuxRpcResponseSchema = z.object({
@@ -474,50 +539,92 @@ const cmuxRpcResponseSchema = z.object({
474
539
  const RPC_TIMEOUT_MS = 1500;
475
540
  function createCmuxSocket({ socketPath, timeoutMs = RPC_TIMEOUT_MS }) {
476
541
  let seq = 0;
477
- return { request(method, params) {
478
- return new Promise((resolve) => {
479
- let settled = false;
480
- const finish = (value) => {
481
- if (settled) return;
482
- settled = true;
483
- clearTimeout(timer);
484
- try {
485
- socket.destroy();
486
- } catch (_error) {}
487
- resolve(value);
488
- };
489
- const timer = setTimeout(() => finish(null), timeoutMs);
490
- const id = `skydive-${++seq}`;
491
- let buffer = "";
492
- const socket = connect(socketPath);
493
- socket.setEncoding("utf8");
494
- socket.on("connect", () => {
495
- try {
496
- socket.write(`${JSON.stringify({
497
- id,
498
- method,
499
- params
500
- })}\n`);
501
- } catch (_error) {
502
- finish(null);
503
- }
504
- });
505
- socket.on("data", (chunk) => {
506
- buffer += chunk;
507
- const newline = buffer.indexOf("\n");
508
- if (newline === -1) return;
509
- const line = buffer.slice(0, newline);
510
- try {
511
- const parsed = cmuxRpcResponseSchema.safeParse(JSON.parse(line));
512
- finish(parsed.success ? parsed.data : null);
513
- } catch (_error) {
514
- finish(null);
515
- }
516
- });
517
- socket.on("error", () => finish(null));
518
- socket.on("close", () => finish(null));
542
+ const roundTrip = (requestLine, parseReply, onFailure) => new Promise((resolve) => {
543
+ let settled = false;
544
+ const finish = (value) => {
545
+ if (settled) return;
546
+ settled = true;
547
+ clearTimeout(timer);
548
+ try {
549
+ socket.destroy();
550
+ } catch (_error) {}
551
+ resolve(value);
552
+ };
553
+ const timer = setTimeout(() => finish(onFailure), timeoutMs);
554
+ let buffer = "";
555
+ const socket = connect(socketPath);
556
+ socket.setEncoding("utf8");
557
+ socket.on("connect", () => {
558
+ try {
559
+ socket.write(requestLine);
560
+ } catch (_error) {
561
+ finish(onFailure);
562
+ }
519
563
  });
520
- } };
564
+ socket.on("data", (chunk) => {
565
+ buffer += chunk;
566
+ const newline = buffer.indexOf("\n");
567
+ if (newline === -1) return;
568
+ const line = buffer.slice(0, newline);
569
+ try {
570
+ finish(parseReply(line));
571
+ } catch (_error) {
572
+ finish(onFailure);
573
+ }
574
+ });
575
+ socket.on("error", () => finish(onFailure));
576
+ socket.on("close", () => finish(onFailure));
577
+ });
578
+ return {
579
+ command(line) {
580
+ `${++seq}`;
581
+ return roundTrip(`${line}\n`, (reply) => reply.startsWith("OK") || reply === "OK", false);
582
+ },
583
+ request(method, params) {
584
+ return new Promise((resolve) => {
585
+ let settled = false;
586
+ const finish = (value) => {
587
+ if (settled) return;
588
+ settled = true;
589
+ clearTimeout(timer);
590
+ try {
591
+ socket.destroy();
592
+ } catch (_error) {}
593
+ resolve(value);
594
+ };
595
+ const timer = setTimeout(() => finish(null), timeoutMs);
596
+ const id = `skydive-${++seq}`;
597
+ let buffer = "";
598
+ const socket = connect(socketPath);
599
+ socket.setEncoding("utf8");
600
+ socket.on("connect", () => {
601
+ try {
602
+ socket.write(`${JSON.stringify({
603
+ id,
604
+ method,
605
+ params
606
+ })}\n`);
607
+ } catch (_error) {
608
+ finish(null);
609
+ }
610
+ });
611
+ socket.on("data", (chunk) => {
612
+ buffer += chunk;
613
+ const newline = buffer.indexOf("\n");
614
+ if (newline === -1) return;
615
+ const line = buffer.slice(0, newline);
616
+ try {
617
+ const parsed = cmuxRpcResponseSchema.safeParse(JSON.parse(line));
618
+ finish(parsed.success ? parsed.data : null);
619
+ } catch (_error) {
620
+ finish(null);
621
+ }
622
+ });
623
+ socket.on("error", () => finish(null));
624
+ socket.on("close", () => finish(null));
625
+ });
626
+ }
627
+ };
521
628
  }
522
629
 
523
630
  //#endregion
@@ -691,15 +798,102 @@ function createAgentHost({ renderer, notifications }) {
691
798
  enabled: notifications
692
799
  }), createTerminalTitleAdapter({ renderer })];
693
800
  const cmux = cmuxWorkspaceEnv();
694
- if (cmux) adapters.push(createCmuxWorkspaceTitleAdapter({
695
- socket: createCmuxSocket({ socketPath: cmux.socketPath }),
696
- workspaceId: cmux.workspaceId
697
- }));
801
+ if (cmux) {
802
+ const socket = createCmuxSocket({ socketPath: cmux.socketPath });
803
+ adapters.push(createCmuxWorkspaceTitleAdapter({
804
+ socket,
805
+ workspaceId: cmux.workspaceId
806
+ }), createCmuxSidebarPrAdapter({
807
+ socket,
808
+ workspaceId: cmux.workspaceId
809
+ }));
810
+ }
698
811
  const herdr = createHerdrReporter();
699
812
  if (herdr) adapters.push(createHerdrAdapter(herdr));
700
813
  return composeAgentHost(adapters);
701
814
  }
702
815
 
816
+ //#endregion
817
+ //#region src/chat/agent-host/pull-requests.ts
818
+ /**
819
+ * Matches a GitHub pull-request URL and captures its number. Scoped to
820
+ * **github.com** on purpose, and this is a scope decision, not an oversight:
821
+ *
822
+ * cmux's native sidebar badge also resolves GitHub Enterprise (GHES) PRs, but
823
+ * it does so from the workspace's **local git remote** (host-qualified
824
+ * `(host, owner, repo)` + a per-host `gh auth token`). We parse a transcript
825
+ * in a remote sandbox with no local `.git`, so from a bare URL we cannot tell
826
+ * `git.acme.com/o/r/pull/5` is a GHES host from any other tool's `/pull/`
827
+ * route — and a pill linking a misidentified URL is worse than no pill. So we
828
+ * take the one host we can identify unambiguously (github.com, incl. `www.`),
829
+ * which covers the common product case; GHES stays the native badge's job.
830
+ *
831
+ * Only the `/pull/<n>` form (not `/issues/`, not a bare `#123` — ambiguous with
832
+ * issues without local repo context we don't have). A trailing path/query/
833
+ * fragment (e.g. `/files`, `#discussion`) is allowed but not captured into the
834
+ * canonical URL. Also matches inside markdown links, backticks, and trailing
835
+ * punctuation.
836
+ */
837
+ const PR_URL_RE = /https?:\/\/(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/gi;
838
+ /**
839
+ * Extracts every distinct GitHub PR referenced in a block of text, in first-seen
840
+ * order, de-duplicated by **repo-qualified identity** (`owner/repo#number`) —
841
+ * NOT by number, since `#42` can exist in many repos. Each ref carries a
842
+ * canonicalized URL (scheme + host + `/owner/repo/pull/number`, no trailing
843
+ * path/query/fragment) so the same PR linked twice with different fragments
844
+ * collapses to one pill.
845
+ */
846
+ function extractPullRequests(text) {
847
+ const byKey = /* @__PURE__ */ new Map();
848
+ for (const match of text.matchAll(PR_URL_RE)) {
849
+ const owner = match[1];
850
+ const repo = match[2];
851
+ const numberText = match[3];
852
+ if (!owner || !repo || !numberText) continue;
853
+ const number = Number.parseInt(numberText, 10);
854
+ if (!Number.isFinite(number) || number <= 0) continue;
855
+ const key = `${owner.toLowerCase()}/${repo.toLowerCase()}#${number}`;
856
+ if (byKey.has(key)) continue;
857
+ byKey.set(key, {
858
+ number,
859
+ key,
860
+ url: `https://github.com/${owner}/${repo}/pull/${number}`
861
+ });
862
+ }
863
+ return [...byKey.values()];
864
+ }
865
+ /**
866
+ * A streaming PR scanner: fed `text-delta` chunks as a run streams, it reports
867
+ * each distinct PR the moment its URL completes, exactly once, without ever
868
+ * rescanning the transcript. It accumulates text per part id (a URL can split
869
+ * across two deltas) and only scans the current part's buffer, so cost is
870
+ * bounded by one text block, not the whole scrollback.
871
+ *
872
+ * `onPullRequest` fires once per never-before-seen PR number for the life of
873
+ * the scanner. Construct one per run (or per conversation) and feed it the
874
+ * same `text-delta` chunks the transcript reducer sees.
875
+ */
876
+ function createPullRequestScanner(onPullRequest) {
877
+ let text = "";
878
+ let lastPartId;
879
+ const seen = /* @__PURE__ */ new Set();
880
+ return { addChunk(chunk) {
881
+ if (chunk["type"] !== "text-delta") return;
882
+ const delta = typeof chunk["delta"] === "string" ? chunk["delta"] : typeof chunk["text"] === "string" ? chunk["text"] : "";
883
+ if (!delta) return;
884
+ const partId = typeof chunk["id"] === "string" ? chunk["id"] : void 0;
885
+ if (partId && lastPartId && partId !== lastPartId) text = "";
886
+ text += delta;
887
+ lastPartId = partId ?? lastPartId;
888
+ if (!delta.includes("/pull/") && !/\/\d/.test(delta)) return;
889
+ for (const pr of extractPullRequests(text)) {
890
+ if (seen.has(pr.key)) continue;
891
+ seen.add(pr.key);
892
+ onPullRequest(pr);
893
+ }
894
+ } };
895
+ }
896
+
703
897
  //#endregion
704
898
  //#region src/chat/clipboard.ts
705
899
  /** Spawns a command with no stdin and collects its stdout. Resolves (never
@@ -4636,6 +4830,50 @@ function TodoCardView({ todos, isActive }) {
4636
4830
  });
4637
4831
  }
4638
4832
 
4833
+ //#endregion
4834
+ //#region src/chat/tui/chat/pr-links.tsx
4835
+ /**
4836
+ * Most PRs to show inline before collapsing the rest into a `+N` count. The
4837
+ * chip shares one row with the send/steer hint, so more than a couple would
4838
+ * crowd or wrap that row; the newest few are the useful ones.
4839
+ */
4840
+ const MAX_PR_CHIPS = 3;
4841
+ /**
4842
+ * Builds the styled `· PR #123 #124` chip as OpenTUI chunks. The number chunks
4843
+ * carry a first-class `link` (OpenTUI emits it as a zero-width OSC 8 hyperlink,
4844
+ * clickable in terminals that support it) — the same mechanism the transcript's
4845
+ * linkified markdown uses. We do NOT hand-roll OSC 8 escape strings into text
4846
+ * children: OpenTUI measures width by string length and emits its own SGR, so a
4847
+ * raw escape corrupts layout and leaks bytes on screen.
4848
+ *
4849
+ * Caps at {@link MAX_PR_CHIPS} shown; any beyond that collapse into a trailing
4850
+ * `+N` so the footer row can't be flooded by a run that links many PRs. Shows
4851
+ * the most recent (last-seen) PRs, since those are what's in play.
4852
+ */
4853
+ function prLinksStyledText(prs) {
4854
+ const dot = fg(theme.dim ?? "#565f89");
4855
+ const label = fg(theme.muted ?? "#7a7a7a");
4856
+ const accent = fg(theme.accent ?? "#7aa2f7");
4857
+ const shown = prs.slice(-MAX_PR_CHIPS);
4858
+ const overflow = prs.length - shown.length;
4859
+ const chunks = [dot("· "), label("PR ")];
4860
+ shown.forEach((pr, i) => {
4861
+ if (i > 0) chunks.push(label(" "));
4862
+ chunks.push(accent(link(pr.url)(`#${pr.number}`)));
4863
+ });
4864
+ if (overflow > 0) chunks.push(label(` +${overflow}`));
4865
+ return new StyledText(chunks);
4866
+ }
4867
+ /**
4868
+ * An inline chip for the conversation's linked PR(s), rendered on the
4869
+ * composer's status/hint row just after the send/steer hint — mirrors the
4870
+ * Claude Code affordance. Renders nothing when there are no PRs.
4871
+ */
4872
+ function PrLinksView({ prs }) {
4873
+ if (prs.length === 0) return null;
4874
+ return /* @__PURE__ */ jsx("text", { content: prLinksStyledText(prs) });
4875
+ }
4876
+
4639
4877
  //#endregion
4640
4878
  //#region src/chat/tui/chat/use-compaction.ts
4641
4879
  const COMPACTING_INDICATOR_TIMEOUT_MS = 12e4;
@@ -8943,6 +9181,12 @@ function ChatScreen({ agent, conversation }) {
8943
9181
  agentHost.transition({ state: "idle" });
8944
9182
  agentHost.setSession(null);
8945
9183
  }, [agentHost]);
9184
+ const [prLinks, setPrLinks] = useState([]);
9185
+ const reportedPrsRef = useRef(/* @__PURE__ */ new Map());
9186
+ useEffect(() => () => {
9187
+ for (const pr of reportedPrsRef.current.values()) agentHost.removePullRequest(pr);
9188
+ reportedPrsRef.current = /* @__PURE__ */ new Map();
9189
+ }, [agentHost]);
8946
9190
  useEffect(() => {
8947
9191
  agentHost.setSession(conversationId);
8948
9192
  setResumeConversation(conversationId);
@@ -9045,6 +9289,15 @@ function ChatScreen({ agent, conversation }) {
9045
9289
  text: recapText
9046
9290
  });
9047
9291
  setItems(loaded);
9292
+ for (const item of loaded) {
9293
+ if (item.kind !== "assistant-text") continue;
9294
+ for (const pr of extractPullRequests(item.text)) {
9295
+ if (reportedPrsRef.current.has(pr.key)) continue;
9296
+ reportedPrsRef.current.set(pr.key, pr);
9297
+ setPrLinks((prev) => [...prev, pr]);
9298
+ agentHost.addPullRequest(pr);
9299
+ }
9300
+ }
9048
9301
  setTodos(latestTodosFromHistory(messages));
9049
9302
  } catch (err) {
9050
9303
  if (cancelled) return;
@@ -9060,12 +9313,22 @@ function ChatScreen({ agent, conversation }) {
9060
9313
  return () => {
9061
9314
  cancelled = true;
9062
9315
  };
9063
- }, [rest, initialConversationId]);
9316
+ }, [
9317
+ rest,
9318
+ initialConversationId,
9319
+ agentHost
9320
+ ]);
9064
9321
  const attachToRun = useCallback((runId, runAgentName, runStartedAtMs) => {
9065
9322
  if (!rest) return;
9066
9323
  seenRunsRef.current.add(runId);
9067
9324
  const abort = new AbortController();
9068
9325
  const responsePreview = createResponsePreview();
9326
+ const prScanner = createPullRequestScanner((pr) => {
9327
+ if (reportedPrsRef.current.has(pr.key)) return;
9328
+ reportedPrsRef.current.set(pr.key, pr);
9329
+ setPrLinks((prev) => [...prev, pr]);
9330
+ agentHost.addPullRequest(pr);
9331
+ });
9069
9332
  const streamAgentName = runAgentName ?? agent.name;
9070
9333
  setRun((prev) => ({
9071
9334
  kind: "streaming",
@@ -9079,6 +9342,7 @@ function ChatScreen({ agent, conversation }) {
9079
9342
  onEvent: (event) => {
9080
9343
  if (event.kind === "chunk") {
9081
9344
  responsePreview.addChunk(event.chunk);
9345
+ prScanner.addChunk(event.chunk);
9082
9346
  if (event.chunk.type === "data-anyone-todos-updated") {
9083
9347
  const next = todosFromPayload(event.chunk.data);
9084
9348
  if (next) setTodos(next.length > 0 ? next : null);
@@ -9359,7 +9623,7 @@ function ChatScreen({ agent, conversation }) {
9359
9623
  detail: "already connected"
9360
9624
  });
9361
9625
  } else if (action.kind === "open_github_app") {
9362
- const url = resolveConnectUrl(action.url, appUrl);
9626
+ const url = resolveConnectUrl(action.url, resolveWebUrl(appUrl));
9363
9627
  await openUrlInBrowser(url);
9364
9628
  updateCard(item.id, {
9365
9629
  status: "launched",
@@ -10634,6 +10898,7 @@ function ChatScreen({ agent, conversation }) {
10634
10898
  fg: theme.muted,
10635
10899
  children: statusText
10636
10900
  }),
10901
+ prLinks.length > 0 ? /* @__PURE__ */ jsx(PrLinksView, { prs: prLinks }) : null,
10637
10902
  /* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }),
10638
10903
  /* @__PURE__ */ jsx("text", {
10639
10904
  fg: theme.dim,
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as errorMessage } from "./rest-DMpigwmN.mjs";
3
- import { c as machineIdentity, i as mintPortalDeviceToken, l as portalWsUrl, n as findThisDevice, r as grantPortalAccess, s as buildEnv, t as fetchPortalDevices } from "./api-BGWcPuik.mjs";
2
+ import { a as errorMessage } from "./rest-Q5yl_b2-.mjs";
3
+ import { c as machineIdentity, i as mintPortalDeviceToken, l as portalWsUrl, n as findThisDevice, r as grantPortalAccess, s as buildEnv, t as fetchPortalDevices } from "./api-CLTp-mKM.mjs";
4
4
  import { z } from "zod";
5
5
  import { spawn } from "node:child_process";
6
6
  import { WebSocket } from "ws";
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-Q5yl_b2-.mjs";
3
+ import "./api-CLTp-mKM.mjs";
4
+ import { t as PortalClient } from "./client-BigvBsPk.mjs";
5
+
6
+ export { PortalClient };
@@ -1,200 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { S as getConfigPath } from "./print-CNIvmpqW.mjs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
- import Conf from "conf";
5
5
  import { err, ok } from "neverthrow";
6
6
  import { z } from "zod";
7
7
  import fs from "node:fs";
8
8
 
9
9
  //#region package.json
10
10
  var name = "skydive-cli";
11
- var version$1 = "0.2.0-beta.610";
12
-
13
- //#endregion
14
- //#region src/config.ts
15
- /** Default host for the public management API (`/v1`, API-key auth). */
16
- const DEFAULT_API_URL = "https://api.skydive.com";
17
- /**
18
- * Default origin for the interactive chat client (`skydive chat`).
19
- *
20
- * The API host that serves better-auth (`/api/auth/*`) and the internal tRPC
21
- * API (`/api/v1/trpc`) that chat streams over. We target the API host
22
- * directly (not the web front door) because chat opens a WebSocket and
23
- * authenticates with a bearer token on the upgrade request. Same host as
24
- * `DEFAULT_API_URL`; override with `--api-url` / `SKYDIVE_APP_URL` for local
25
- * dev or while the DNS record is still being provisioned.
26
- */
27
- const DEFAULT_APP_URL = "https://api.skydive.com";
28
- /** Web front door, for pages opened in the user's browser. */
29
- const DEFAULT_WEB_URL = "https://skydive.com";
30
- /**
31
- * Origin for browser-facing links (e.g. opening a conversation's web page).
32
- * The app origin is the API host, which serves no web UI in production, so
33
- * map the default to the web front door. Overridden origins (local dev,
34
- * previews) serve both and pass through unchanged.
35
- */
36
- function resolveWebUrl(appUrl) {
37
- return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
38
- }
39
- /** Prefix on workspace-scoped Skydive API keys. Kept in sync with the API's
40
- * `API_KEY_PREFIX` (`apps/anyone/api/src/lib/api-key.ts`); the CLI is a
41
- * standalone published package so it can't import the backend constant. */
42
- const API_KEY_PREFIX = "sky_live_";
43
- /**
44
- * Common prefix across all Skydive API key kinds — `sky_live_…` workspace
45
- * keys today, `sky_user_…` account keys when ANY-5105 lands. The CLI only
46
- * sanity-checks the family on `--api-key`; the server authoritatively rejects
47
- * a kind that can't drive a given route, with a clearer message than the
48
- * client could produce.
49
- */
50
- const API_KEY_FAMILY_PREFIX = "sky_";
51
- /** Where users mint and copy API keys. Shown in the login prompt. */
52
- const API_KEYS_URL = "skydive.com/settings/account";
53
- const store = new Conf({
54
- projectName: process.env["SKYDIVE_CONFIG_NAME"] ?? "skydive",
55
- projectSuffix: "",
56
- configFileMode: 384
57
- });
58
- function resolveConfig(opts) {
59
- const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
60
- const apiUrl = process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL;
61
- if (!apiKey) return err({ message: "Not authenticated. Run `skydive auth login` first." });
62
- return ok({
63
- apiKey,
64
- apiUrl
65
- });
66
- }
67
- /**
68
- * Resolve the bearer credential for the management API (`agents` / `keys` /
69
- * `secrets`). The server's `/v1` gate accepts either an API key or the
70
- * device-flow session bearer, so both work — but only one of them tracks the
71
- * active workspace.
72
- *
73
- * An API key is pinned server-side to the organization that minted it and
74
- * ignores the workspace header by design, so it can never follow `skydive
75
- * workspace switch`. A key is also a strictly narrower credential than its
76
- * owner's session. So the session wins whenever there is one, and a key is
77
- * what's left for machines that never ran an interactive login.
78
- *
79
- * `SKYDIVE_SESSION_TOKEN=` (empty) suppresses session auth for one
80
- * invocation, to drive a specific organization's key while signed in.
81
- */
82
- function resolveManagementAuth(opts) {
83
- const session = resolveSession({ appUrl: opts.apiUrl });
84
- if (session.isOk()) return ok({
85
- token: session.value.sessionToken,
86
- apiUrl: session.value.appUrl,
87
- kind: "session",
88
- pinnedWorkspaceName: null
89
- });
90
- const envKey = process.env["SKYDIVE_API_KEY"];
91
- const storedKey = store.get("apiKey");
92
- const apiKey = envKey ?? storedKey;
93
- if (apiKey) return ok({
94
- token: apiKey,
95
- apiUrl: process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL,
96
- kind: "api-key",
97
- pinnedWorkspaceName: !envKey && storedKey ? store.get("apiKeyWorkspaceName") ?? null : null
98
- });
99
- return err({ message: "Not authenticated. Run `skydive auth login`." });
100
- }
101
- function saveConfig(config) {
102
- store.set("apiKey", config.apiKey);
103
- store.set("apiUrl", config.apiUrl);
104
- if (config.apiKeyId) store.set("apiKeyId", config.apiKeyId);
105
- else store.delete("apiKeyId");
106
- if (config.workspaceName) store.set("apiKeyWorkspaceName", config.workspaceName);
107
- else store.delete("apiKeyWorkspaceName");
108
- }
109
- /** Server-side id of the auto-minted key, if login minted one. */
110
- function getStoredApiKeyId() {
111
- return store.get("apiKeyId") ?? null;
112
- }
113
- /** Workspace the auto-minted key is pinned to, if login recorded one. */
114
- function getStoredApiKeyWorkspaceName() {
115
- return store.get("apiKeyWorkspaceName") ?? null;
116
- }
117
- function deleteConfig() {
118
- store.clear();
119
- }
120
- function getConfigPath() {
121
- return store.path;
122
- }
123
- /**
124
- * Where the chat TUI persists its prompt history (up-arrow recall). Kept
125
- * beside the config file so all CLI state lives in one directory.
126
- */
127
- function getLastSeenVersion() {
128
- return store.get("lastSeenVersion");
129
- }
130
- function setLastSeenVersion(version) {
131
- store.set("lastSeenVersion", version);
132
- }
133
- function getPromptHistoryPath() {
134
- return path.join(path.dirname(store.path), "prompt-history.jsonl");
135
- }
136
- /**
137
- * Where the chat TUI persists pending review comments — one JSON file per
138
- * conversation, so a pending comment survives conversation switches and
139
- * process death. Kept beside the config file like prompt history.
140
- */
141
- function getReviewStateDir() {
142
- return path.join(path.dirname(store.path), "review");
143
- }
144
- /**
145
- * Resolve the chat/auth origin. Precedence: `SKYDIVE_APP_URL` env > explicit
146
- * `--api-url` style override > stored value > `SKYDIVE_API_URL` env >
147
- * `DEFAULT_APP_URL`.
148
- *
149
- * The `SKYDIVE_API_URL` fallback matters for previews: the device/`--web`
150
- * flow and chat hit the same api service as the management API, so pointing
151
- * `SKYDIVE_API_URL` at a preview stack is enough — you don't also have to set
152
- * `SKYDIVE_APP_URL`. Otherwise auth would silently fall through to prod
153
- * (`DEFAULT_APP_URL`) and hand back a prod verification URL.
154
- */
155
- function resolveAppUrl(opts) {
156
- return process.env["SKYDIVE_APP_URL"] ?? opts.appUrl ?? store.get("appUrl") ?? process.env["SKYDIVE_API_URL"] ?? DEFAULT_APP_URL;
157
- }
158
- function resolveSession(opts) {
159
- const sessionToken = process.env["SKYDIVE_SESSION_TOKEN"] ?? store.get("sessionToken");
160
- const appUrl = resolveAppUrl(opts);
161
- if (!sessionToken) return err({ message: "Not signed in for chat. Run `skydive chat` to sign in." });
162
- return ok({
163
- sessionToken,
164
- appUrl
165
- });
166
- }
167
- function saveSession(session) {
168
- store.set("sessionToken", session.sessionToken);
169
- store.set("sessionObtainedAt", (/* @__PURE__ */ new Date()).toISOString());
170
- store.set("appUrl", session.appUrl);
171
- }
172
- function getSavedTheme(mode) {
173
- return store.get(mode === "dark" ? "themeDark" : "themeLight");
174
- }
175
- function saveTheme(mode, themeId) {
176
- store.set(mode === "dark" ? "themeDark" : "themeLight", themeId);
177
- }
178
- /**
179
- * Whether `skydive chat` should enable portal machine sharing on launch
180
- * without `--share-machine`. Set by hand-editing `shareMachineDefault` in
181
- * config.json — deliberately no CLI command (kept off the API surface).
182
- * Sharing only makes the machine reachable — agents still need a
183
- * (persistent) grant to run anything, so this default skips the per-session
184
- * enable step, not the consent step.
185
- */
186
- function getShareMachineDefault() {
187
- return store.get("shareMachineDefault") ?? false;
188
- }
189
- /**
190
- * Whether the update check is disabled via config.json (`"updateCheck":
191
- * false`). The persistent counterpart to the SKYDIVE_NO_UPDATE_CHECK /
192
- * NO_UPDATE_NOTIFIER env opt-outs; like shareMachineDefault, set by editing
193
- * config.json — deliberately no CLI command.
194
- */
195
- function getUpdateCheckDisabled() {
196
- return store.get("updateCheck") === false;
197
- }
11
+ var version$1 = "0.2.0-beta.615";
198
12
 
199
13
  //#endregion
200
14
  //#region src/auth/organization.ts
@@ -1243,4 +1057,4 @@ function printFatalNotice(file, memory) {
1243
1057
  }
1244
1058
 
1245
1059
  //#endregion
1246
- export { deleteConfig as A, resolveAppUrl as B, setActiveWorkspace as C, DEFAULT_API_URL as D, API_KEY_PREFIX as E, getSavedTheme as F, saveConfig as G, resolveManagementAuth as H, getShareMachineDefault as I, setLastSeenVersion as J, saveSession as K, getStoredApiKeyId as L, getLastSeenVersion as M, getPromptHistoryPath as N, DEFAULT_APP_URL as O, getReviewStateDir as P, getStoredApiKeyWorkspaceName as R, listWorkspaces as S, API_KEY_FAMILY_PREFIX as T, resolveSession as U, resolveConfig as V, resolveWebUrl as W, version$1 as X, name as Y, themes as _, WORDMARK as a, getActiveWorkspaceId as b, applyTheme as c, noColorRequested as d, theme as f, themeVersion as g, themeModeFromColorFgBg as h, MARK_CELLS as i, getConfigPath as j, DEFAULT_WEB_URL as k, findTheme as l, themeMode as m, buildCrashReport as n, brandHelpArt as o, themeForMode as p, saveTheme as q, writeCrashReport as r, DEFAULT_THEME_ID as s, installCrashHandler as t, monoTheme as u, themesForMode as v, API_KEYS_URL as w, getSessionIdentity as x, ensureActiveOrganization as y, getUpdateCheckDisabled as z };
1060
+ export { setActiveWorkspace as C, listWorkspaces as S, version$1 as T, themes as _, WORDMARK as a, getActiveWorkspaceId as b, applyTheme as c, noColorRequested as d, theme as f, themeVersion as g, themeModeFromColorFgBg as h, MARK_CELLS as i, findTheme as l, themeMode as m, buildCrashReport as n, brandHelpArt as o, themeForMode as p, writeCrashReport as r, DEFAULT_THEME_ID as s, installCrashHandler as t, monoTheme as u, themesForMode as v, name as w, getSessionIdentity as x, ensureActiveOrganization as y };
@@ -1,7 +1,191 @@
1
1
  #!/usr/bin/env node
2
- import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, t as HttpError } from "./rest-DMpigwmN.mjs";
2
+ import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, t as HttpError } from "./rest-Q5yl_b2-.mjs";
3
+ import path from "node:path";
4
+ import Conf from "conf";
5
+ import { err, ok } from "neverthrow";
3
6
  import stableStringify from "safe-stable-stringify";
4
7
 
8
+ //#region src/config.ts
9
+ /** Default host for the public management API (`/v1`, API-key auth). */
10
+ const DEFAULT_API_URL = "https://api.skydive.com";
11
+ /**
12
+ * Default origin for the interactive chat client (`skydive chat`).
13
+ *
14
+ * The API host that serves better-auth (`/api/auth/*`) and the internal tRPC
15
+ * API (`/api/v1/trpc`) that chat streams over. We target the API host
16
+ * directly (not the web front door) because chat opens a WebSocket and
17
+ * authenticates with a bearer token on the upgrade request. Same host as
18
+ * `DEFAULT_API_URL`; override with `--api-url` / `SKYDIVE_APP_URL` for local
19
+ * dev or while the DNS record is still being provisioned.
20
+ */
21
+ const DEFAULT_APP_URL = "https://api.skydive.com";
22
+ /** Web front door, for pages opened in the user's browser. */
23
+ const DEFAULT_WEB_URL = "https://skydive.com";
24
+ function resolveWebUrl(appUrl) {
25
+ if (appUrl == null) return appUrl;
26
+ return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
27
+ }
28
+ /** Prefix on workspace-scoped Skydive API keys. Kept in sync with the API's
29
+ * `API_KEY_PREFIX` (`apps/anyone/api/src/lib/api-key.ts`); the CLI is a
30
+ * standalone published package so it can't import the backend constant. */
31
+ const API_KEY_PREFIX = "sky_live_";
32
+ /**
33
+ * Common prefix across all Skydive API key kinds — `sky_live_…` workspace
34
+ * keys today, `sky_user_…` account keys when ANY-5105 lands. The CLI only
35
+ * sanity-checks the family on `--api-key`; the server authoritatively rejects
36
+ * a kind that can't drive a given route, with a clearer message than the
37
+ * client could produce.
38
+ */
39
+ const API_KEY_FAMILY_PREFIX = "sky_";
40
+ /** Where users mint and copy API keys. Shown in the login prompt. */
41
+ const API_KEYS_URL = "skydive.com/settings/account";
42
+ const store = new Conf({
43
+ projectName: process.env["SKYDIVE_CONFIG_NAME"] ?? "skydive",
44
+ projectSuffix: "",
45
+ configFileMode: 384
46
+ });
47
+ function resolveConfig(opts) {
48
+ const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
49
+ const apiUrl = process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL;
50
+ if (!apiKey) return err({ message: "Not authenticated. Run `skydive auth login` first." });
51
+ return ok({
52
+ apiKey,
53
+ apiUrl
54
+ });
55
+ }
56
+ /**
57
+ * Resolve the bearer credential for the management API (`agents` / `keys` /
58
+ * `secrets`). The server's `/v1` gate accepts either an API key or the
59
+ * device-flow session bearer, so both work — but only one of them tracks the
60
+ * active workspace.
61
+ *
62
+ * An API key is pinned server-side to the organization that minted it and
63
+ * ignores the workspace header by design, so it can never follow `skydive
64
+ * workspace switch`. A key is also a strictly narrower credential than its
65
+ * owner's session. So the session wins whenever there is one, and a key is
66
+ * what's left for machines that never ran an interactive login.
67
+ *
68
+ * `SKYDIVE_SESSION_TOKEN=` (empty) suppresses session auth for one
69
+ * invocation, to drive a specific organization's key while signed in.
70
+ */
71
+ function resolveManagementAuth(opts) {
72
+ const session = resolveSession({ appUrl: opts.apiUrl });
73
+ if (session.isOk()) return ok({
74
+ token: session.value.sessionToken,
75
+ apiUrl: session.value.appUrl,
76
+ kind: "session",
77
+ pinnedWorkspaceName: null
78
+ });
79
+ const envKey = process.env["SKYDIVE_API_KEY"];
80
+ const storedKey = store.get("apiKey");
81
+ const apiKey = envKey ?? storedKey;
82
+ if (apiKey) return ok({
83
+ token: apiKey,
84
+ apiUrl: process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL,
85
+ kind: "api-key",
86
+ pinnedWorkspaceName: !envKey && storedKey ? store.get("apiKeyWorkspaceName") ?? null : null
87
+ });
88
+ return err({ message: "Not authenticated. Run `skydive auth login`." });
89
+ }
90
+ function saveConfig(config) {
91
+ store.set("apiKey", config.apiKey);
92
+ store.set("apiUrl", config.apiUrl);
93
+ if (config.apiKeyId) store.set("apiKeyId", config.apiKeyId);
94
+ else store.delete("apiKeyId");
95
+ if (config.workspaceName) store.set("apiKeyWorkspaceName", config.workspaceName);
96
+ else store.delete("apiKeyWorkspaceName");
97
+ }
98
+ /** Server-side id of the auto-minted key, if login minted one. */
99
+ function getStoredApiKeyId() {
100
+ return store.get("apiKeyId") ?? null;
101
+ }
102
+ /** Workspace the auto-minted key is pinned to, if login recorded one. */
103
+ function getStoredApiKeyWorkspaceName() {
104
+ return store.get("apiKeyWorkspaceName") ?? null;
105
+ }
106
+ function deleteConfig() {
107
+ store.clear();
108
+ }
109
+ function getConfigPath() {
110
+ return store.path;
111
+ }
112
+ /**
113
+ * Where the chat TUI persists its prompt history (up-arrow recall). Kept
114
+ * beside the config file so all CLI state lives in one directory.
115
+ */
116
+ function getLastSeenVersion() {
117
+ return store.get("lastSeenVersion");
118
+ }
119
+ function setLastSeenVersion(version) {
120
+ store.set("lastSeenVersion", version);
121
+ }
122
+ function getPromptHistoryPath() {
123
+ return path.join(path.dirname(store.path), "prompt-history.jsonl");
124
+ }
125
+ /**
126
+ * Where the chat TUI persists pending review comments — one JSON file per
127
+ * conversation, so a pending comment survives conversation switches and
128
+ * process death. Kept beside the config file like prompt history.
129
+ */
130
+ function getReviewStateDir() {
131
+ return path.join(path.dirname(store.path), "review");
132
+ }
133
+ /**
134
+ * Resolve the chat/auth origin. Precedence: `SKYDIVE_APP_URL` env > explicit
135
+ * `--api-url` style override > stored value > `SKYDIVE_API_URL` env >
136
+ * `DEFAULT_APP_URL`.
137
+ *
138
+ * The `SKYDIVE_API_URL` fallback matters for previews: the device/`--web`
139
+ * flow and chat hit the same api service as the management API, so pointing
140
+ * `SKYDIVE_API_URL` at a preview stack is enough — you don't also have to set
141
+ * `SKYDIVE_APP_URL`. Otherwise auth would silently fall through to prod
142
+ * (`DEFAULT_APP_URL`) and hand back a prod verification URL.
143
+ */
144
+ function resolveAppUrl(opts) {
145
+ return process.env["SKYDIVE_APP_URL"] ?? opts.appUrl ?? store.get("appUrl") ?? process.env["SKYDIVE_API_URL"] ?? DEFAULT_APP_URL;
146
+ }
147
+ function resolveSession(opts) {
148
+ const sessionToken = process.env["SKYDIVE_SESSION_TOKEN"] ?? store.get("sessionToken");
149
+ const appUrl = resolveAppUrl(opts);
150
+ if (!sessionToken) return err({ message: "Not signed in for chat. Run `skydive chat` to sign in." });
151
+ return ok({
152
+ sessionToken,
153
+ appUrl
154
+ });
155
+ }
156
+ function saveSession(session) {
157
+ store.set("sessionToken", session.sessionToken);
158
+ store.set("sessionObtainedAt", (/* @__PURE__ */ new Date()).toISOString());
159
+ store.set("appUrl", session.appUrl);
160
+ }
161
+ function getSavedTheme(mode) {
162
+ return store.get(mode === "dark" ? "themeDark" : "themeLight");
163
+ }
164
+ function saveTheme(mode, themeId) {
165
+ store.set(mode === "dark" ? "themeDark" : "themeLight", themeId);
166
+ }
167
+ /**
168
+ * Whether `skydive chat` should enable portal machine sharing on launch
169
+ * without `--share-machine`. Set by hand-editing `shareMachineDefault` in
170
+ * config.json — deliberately no CLI command (kept off the API surface).
171
+ * Sharing only makes the machine reachable — agents still need a
172
+ * (persistent) grant to run anything, so this default skips the per-session
173
+ * enable step, not the consent step.
174
+ */
175
+ function getShareMachineDefault() {
176
+ return store.get("shareMachineDefault") ?? false;
177
+ }
178
+ /**
179
+ * Whether the update check is disabled via config.json (`"updateCheck":
180
+ * false`). The persistent counterpart to the SKYDIVE_NO_UPDATE_CHECK /
181
+ * NO_UPDATE_NOTIFIER env opt-outs; like shareMachineDefault, set by editing
182
+ * config.json — deliberately no CLI command.
183
+ */
184
+ function getUpdateCheckDisabled() {
185
+ return store.get("updateCheck") === false;
186
+ }
187
+
188
+ //#endregion
5
189
  //#region src/chat/tui/chat/card.ts
6
190
  const urlActionKinds = [
7
191
  "open_oauth",
@@ -227,14 +411,20 @@ function summarizeConnectCard(card, appUrl) {
227
411
  const act = button.action;
228
412
  switch (act.kind) {
229
413
  case "open_oauth":
230
- case "open_external_oauth":
231
- case "open_github_app": return {
414
+ case "open_external_oauth": return {
232
415
  ...base,
233
416
  action: {
234
417
  kind: "open_url",
235
418
  url: resolveConnectUrl(act.url, appUrl)
236
419
  }
237
420
  };
421
+ case "open_github_app": return {
422
+ ...base,
423
+ action: {
424
+ kind: "open_url",
425
+ url: resolveConnectUrl(act.url, resolveWebUrl(appUrl))
426
+ }
427
+ };
238
428
  case "submit_credential": return {
239
429
  ...base,
240
430
  action: {
@@ -487,4 +677,4 @@ async function readStdin() {
487
677
  }
488
678
 
489
679
  //#endregion
490
- export { runPrint as a, cardActionErrorMessage as c, reconcileMaskedInput as d, resolveConnectUrl as f, resolveAgent as i, 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 };
680
+ export { getUpdateCheckDisabled as A, getLastSeenVersion as C, getShareMachineDefault as D, getSavedTheme as E, resolveWebUrl as F, saveConfig as I, saveSession as L, resolveConfig as M, resolveManagementAuth as N, getStoredApiKeyId as O, resolveSession as P, saveTheme as R, getConfigPath as S, getReviewStateDir as T, API_KEY_PREFIX as _, runPrint as a, DEFAULT_WEB_URL 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, resolveAppUrl as j, getStoredApiKeyWorkspaceName 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, getPromptHistoryPath as w, deleteConfig as x, DEFAULT_APP_URL as y, setLastSeenVersion as z };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import "./rest-DMpigwmN.mjs";
3
- import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-6Bm51XhR.mjs";
2
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-CNIvmpqW.mjs";
3
+ import "./rest-Q5yl_b2-.mjs";
4
4
 
5
5
  export { messageGet, readStdin, resolveAgent, runPrint };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as printError } from "./output-B4cW10Ph.mjs";
2
+ import { n as printError } from "./output-DYzzdXYV.mjs";
3
3
 
4
4
  //#region src/chat/print-share.ts
5
5
  /**
@@ -14,7 +14,7 @@ import { n as printError } from "./output-B4cW10Ph.mjs";
14
14
  * the reply (and to --json).
15
15
  */
16
16
  async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
17
- const { PortalClient } = await import("./client-BRfmlbgI.mjs");
17
+ const { PortalClient } = await import("./client-D6UUwESi.mjs");
18
18
  let signalConnected;
19
19
  const connected = new Promise((resolve) => {
20
20
  signalConnected = resolve;
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { i as sendErrorMessage, n as createRestClient, r as errorDetail, t as HttpError } from "./rest-DMpigwmN.mjs";
2
+ import { i as sendErrorMessage, n as createRestClient, r as errorDetail, t as HttpError } from "./rest-Q5yl_b2-.mjs";
3
3
 
4
4
  export { createRestClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.2.0-beta.610",
3
+ "version": "0.2.0-beta.615",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./rest-DMpigwmN.mjs";
3
- import "./api-BGWcPuik.mjs";
4
- import { t as PortalClient } from "./client-l3OrGKaC.mjs";
5
-
6
- export { PortalClient };