skydive-cli 0.5.0-beta.13 → 0.5.0-beta.15

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.
package/dist/js/bin.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-0_q2DHpq.mjs";
2
+ import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-B97UpfHm.mjs";
3
3
  import { A as getStoredApiKeyId, B as saveConfig, C as getDefaultAgent, E as getPromptHistoryPath, F as resolveChatAuth, I as resolveConfig, L as resolveManagementAuth, M as getUpdateCheckDisabled, P as resolveAppUrl, R as resolveSession, S as getConfigPath, T as getPreference, U as setLastSeenVersion, V as saveSession, _ as API_KEY_PREFIX, b as PREFERENCES, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as getStoredApiKeyWorkspaceName, k as getShareMachineDefault, v as DEFAULT_API_URL, w as getLastSeenVersion, x as deleteConfig } from "./print-riiY5Gly.mjs";
4
4
  import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
5
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
6
6
  import { t as createRestClient } from "./rest-CLrHVkxh.mjs";
7
7
  import "./billing-blocked-2wju4gC_.mjs";
8
- import { r as resolveMachineIdentity } from "./client-DabRpc_T.mjs";
9
- import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-ShOaDbZe.mjs";
8
+ import { r as resolveMachineIdentity } from "./client-DbqRBquD.mjs";
9
+ import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-eb9dm6Ih.mjs";
10
10
  import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-DG5W6iwx.mjs";
11
11
  import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
12
12
  import { hideBin } from "yargs/helpers";
@@ -1517,7 +1517,7 @@ const importCommand = {
1517
1517
  process.exit(1);
1518
1518
  }
1519
1519
  }
1520
- const { runChat } = await import("./boot-C4OHQUCg.mjs");
1520
+ const { runChat } = await import("./boot-DZKl8JNI.mjs");
1521
1521
  await runChat({
1522
1522
  appUrl,
1523
1523
  sessionToken: session.value.sessionToken,
@@ -1545,7 +1545,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1545
1545
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
1546
1546
  process.exit(1);
1547
1547
  }
1548
- const { connectMachineShare } = await import("./print-share-CKLPmsg0.mjs");
1548
+ const { connectMachineShare } = await import("./print-share-BH9gvCls.mjs");
1549
1549
  const machineShare = await connectMachineShare({
1550
1550
  appUrl,
1551
1551
  sessionToken: session.value.sessionToken,
@@ -1930,7 +1930,7 @@ const chatCommand = {
1930
1930
  sessionToken: auth.value.token,
1931
1931
  agentSelector: argv.agent ?? null
1932
1932
  });
1933
- const { runChat } = await import("./boot-C4OHQUCg.mjs");
1933
+ const { runChat } = await import("./boot-DZKl8JNI.mjs");
1934
1934
  await runChat({
1935
1935
  appUrl: auth.value.appUrl,
1936
1936
  sessionToken: auth.value.token,
@@ -2023,7 +2023,7 @@ async function runPrintMode({ argv, appUrl }) {
2023
2023
  process.exit(1);
2024
2024
  }
2025
2025
  } else {
2026
- const { connectMachineShare } = await import("./print-share-CKLPmsg0.mjs");
2026
+ const { connectMachineShare } = await import("./print-share-BH9gvCls.mjs");
2027
2027
  machineShare = await connectMachineShare({
2028
2028
  appUrl: auth.value.appUrl,
2029
2029
  sessionToken: auth.value.token,
@@ -2290,7 +2290,7 @@ const switchCommand = {
2290
2290
  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.");
2291
2291
  process.exit(1);
2292
2292
  }
2293
- const { runWorkspacePicker } = await import("./boot-C4OHQUCg.mjs");
2293
+ const { runWorkspacePicker } = await import("./boot-DZKl8JNI.mjs");
2294
2294
  await runWorkspacePicker(session);
2295
2295
  return;
2296
2296
  }
@@ -2339,7 +2339,7 @@ const workspaceCommand = {
2339
2339
  */
2340
2340
  async function resolveGrantTarget(argv) {
2341
2341
  const session = requireSession(argv);
2342
- const [{ devices, agents }, identity] = await Promise.all([fetchPortalDevices(session), resolveMachineIdentity()]);
2342
+ const [{ devices, agents }, identity] = await Promise.all([fetchPortalDevices(session), resolveMachineIdentity(null)]);
2343
2343
  return {
2344
2344
  session,
2345
2345
  agent: resolveAgent(agents, argv.agent),
@@ -2368,8 +2368,8 @@ const openCommand = {
2368
2368
  const session = requireSession(argv);
2369
2369
  const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
2370
2370
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
2371
- const { machineName } = await resolveMachineIdentity();
2372
- const { PortalDaemonClient } = await import("./daemon-client-D_MeDMpn.mjs");
2371
+ const { machineName } = await resolveMachineIdentity(null);
2372
+ const { PortalDaemonClient } = await import("./daemon-client-Bemq-pLd.mjs");
2373
2373
  let lastLine = "";
2374
2374
  let signalConnected;
2375
2375
  const connected = new Promise((resolve) => {
@@ -2468,7 +2468,7 @@ const statusCommand = {
2468
2468
  console.log("No machines registered. Run `skydive portal open` to register this one.");
2469
2469
  return;
2470
2470
  }
2471
- const { headers, rows } = buildDeviceTable(devices, agents, (await resolveMachineIdentity()).machineName);
2471
+ const { headers, rows } = buildDeviceTable(devices, agents, (await resolveMachineIdentity(null)).machineName);
2472
2472
  printTable(headers, rows);
2473
2473
  }
2474
2474
  };
@@ -4215,7 +4215,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
4215
4215
  process.exit(0);
4216
4216
  }
4217
4217
  if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
4218
- const { runPortalDaemon } = await import("./daemon-BfqJZ_M4.mjs");
4218
+ const { runPortalDaemon } = await import("./daemon-Djei8xaW.mjs");
4219
4219
  runPortalDaemon(process.argv);
4220
4220
  } else runCli();
4221
4221
  function runCli() {
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-0_q2DHpq.mjs";
2
+ import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-B97UpfHm.mjs";
3
3
  import { D as getReviewStateDir, H as saveTheme, N as recordDefaultAgent, O as getSavedTheme, S as getConfigPath, 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, z as resolveWebUrl } from "./print-riiY5Gly.mjs";
4
4
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
5
5
  import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-CLrHVkxh.mjs";
6
6
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
7
- import { t as PortalClient } from "./client-DabRpc_T.mjs";
8
- import "./daemon-ShOaDbZe.mjs";
7
+ import { t as PortalClient } from "./client-DbqRBquD.mjs";
8
+ import "./daemon-eb9dm6Ih.mjs";
9
9
  import "./api-DG5W6iwx.mjs";
10
10
  import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
11
- import { t as PortalDaemonClient } from "./daemon-client-BVAlyN7x.mjs";
11
+ import { t as PortalDaemonClient } from "./daemon-client-N8rvGusv.mjs";
12
12
  import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
13
13
  import * as os$1 from "node:os";
14
14
  import { homedir, platform, release, tmpdir } from "node:os";
@@ -386,6 +386,8 @@ function createPortalClient(opts) {
386
386
  deviceToken: null
387
387
  }),
388
388
  resolveCwd: () => process.cwd(),
389
+ persistedMachineName: null,
390
+ onMachineName: () => {},
389
391
  onState: (state) => opts.onState({
390
392
  status: state.status,
391
393
  machineName: state.machineName,
@@ -1078,42 +1080,133 @@ function createAgentHost({ renderer, notifications }) {
1078
1080
  * take the one host we can identify unambiguously (github.com, incl. `www.`),
1079
1081
  * which covers the common product case; GHES stays the native badge's job.
1080
1082
  *
1081
- * Only the `/pull/<n>` form (not `/issues/`, not a bare `#123` — ambiguous with
1082
- * issues without local repo context we don't have). A trailing path/query/
1083
- * fragment (e.g. `/files`, `#discussion`) is allowed but not captured into the
1084
- * canonical URL. Also matches inside markdown links, backticks, and trailing
1085
- * punctuation.
1086
- */
1087
- const PR_URL_RE = /https?:\/\/(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/gi;
1088
- /**
1089
- * Extracts every distinct GitHub PR referenced in a block of text, in first-seen
1090
- * order, de-duplicated by **repo-qualified identity** (`owner/repo#number`)
1091
- * NOT by number, since `#42` can exist in many repos. Each ref carries a
1092
- * canonicalized URL (scheme + host + `/owner/repo/pull/number`, no trailing
1093
- * path/query/fragment) so the same PR linked twice with different fragments
1094
- * collapses to one pill.
1095
- */
1096
- function extractPullRequests(text, options) {
1097
- const requireCompleteNumber = options?.requireCompleteNumber ?? true;
1098
- const byKey = /* @__PURE__ */ new Map();
1099
- for (const match of text.matchAll(PR_URL_RE)) {
1083
+ * Only the `/pull/<n>` form (not `/issues/`). A trailing path/query/fragment
1084
+ * (e.g. `/files`, `#discussion`) is allowed but not captured into the canonical
1085
+ * URL. Also matches inside markdown links, backticks, and trailing punctuation.
1086
+ *
1087
+ * The `https://` (and `www.`) prefix is optional: agents and people routinely
1088
+ * write a bare `github.com/o/r/pull/7`, and that should link a PR the same way
1089
+ * the full URL does. To keep a bare host from matching inside a longer hostname
1090
+ * (`mygithub.com/...`, `notgithub.com/...`), a protocol-less match must be
1091
+ * preceded by a start-of-string or a non-host character (anything but a word
1092
+ * char or `.`) a leading boundary the `(?:^|[^\w.])` group asserts and
1093
+ * consumes. The consumed boundary char is outside the captured groups, so it
1094
+ * never leaks into owner/repo. `matchAll` won't overlap, but the boundary is a
1095
+ * single delimiter (space, `(`, `[`, etc.) between PRs in practice, so distinct
1096
+ * back-to-back URLs still each match.
1097
+ */
1098
+ const PR_URL_RE = /(?:^|[^\w.])(?:https?:\/\/)?(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/gi;
1099
+ /**
1100
+ * Matches a bare, human-style PR reference `#<n>` — the way people (and LLMs)
1101
+ * name a PR in prose ("opened #20858", "landed #42"). Requires a non-word
1102
+ * boundary before the `#` so a fragment tail like `/pull/7#discussion_r5` or a
1103
+ * hex color `#20858` embedded in a word doesn't count, and captures the digits
1104
+ * up to a word boundary. We only harvest these from **assistant prose**, never
1105
+ * from tool output, because `#N` is exactly the relevance signal we're after:
1106
+ * the agent chose to talk about this PR.
1107
+ */
1108
+ const PR_HASH_RE = /(?:^|[^\w#/])#(\d+)\b/g;
1109
+ /**
1110
+ * Every full `/pull/<n>` github.com URL in `urlText`, keyed by number to the
1111
+ * candidate refs for that number. A number can map to more than one repo, so we
1112
+ * keep all candidates; a bare `#N` label can't say which repo, but the pill's
1113
+ * own URL does. Deduped by repo-qualified identity (`owner/repo#number`) so the
1114
+ * same PR linked twice — even with a different trailing path/fragment —
1115
+ * collapses to one ref.
1116
+ *
1117
+ * `deferTrailingNumber` guards the streaming truncation case: when true, a URL
1118
+ * whose number runs to the very end of `urlText` is skipped because the next
1119
+ * delta may extend those digits (`.../pull/19` now, `.../pull/19739` later).
1120
+ * Only the still-growing PROSE buffer sets this; settled tool output is always
1121
+ * complete, so its URLs are harvested with the guard off.
1122
+ */
1123
+ function urlsByNumber(urlText, deferTrailingNumber, byNumber) {
1124
+ for (const match of urlText.matchAll(PR_URL_RE)) {
1100
1125
  const owner = match[1];
1101
1126
  const repo = match[2];
1102
1127
  const numberText = match[3];
1103
1128
  if (!owner || !repo || !numberText) continue;
1104
- if (requireCompleteNumber) {
1105
- if (match.index + match[0].length >= text.length) continue;
1129
+ if (deferTrailingNumber) {
1130
+ if (match.index + match[0].length >= urlText.length) continue;
1106
1131
  }
1107
1132
  const number = Number.parseInt(numberText, 10);
1108
1133
  if (!Number.isFinite(number) || number <= 0) continue;
1109
- const key = `${owner.toLowerCase()}/${repo.toLowerCase()}#${number}`;
1110
- if (byKey.has(key)) continue;
1111
- byKey.set(key, {
1134
+ const ref = {
1112
1135
  number,
1113
- key,
1136
+ key: `${owner.toLowerCase()}/${repo.toLowerCase()}#${number}`,
1114
1137
  url: `https://github.com/${owner}/${repo}/pull/${number}`
1115
- });
1138
+ };
1139
+ const existing = byNumber.get(number);
1140
+ if (existing) {
1141
+ if (!existing.some((r) => r.key === ref.key)) existing.push(ref);
1142
+ } else byNumber.set(number, [ref]);
1116
1143
  }
1144
+ }
1145
+ /** Every human-style `#N` named in `proseText` — the relevance signal. */
1146
+ function hashNumbers(proseText) {
1147
+ const numbers = /* @__PURE__ */ new Set();
1148
+ for (const match of proseText.matchAll(PR_HASH_RE)) {
1149
+ const numberText = match[1];
1150
+ if (!numberText) continue;
1151
+ const number = Number.parseInt(numberText, 10);
1152
+ if (Number.isFinite(number) && number > 0) numbers.add(number);
1153
+ }
1154
+ return numbers;
1155
+ }
1156
+ /**
1157
+ * The relevance contract, in one place.
1158
+ *
1159
+ * A GitHub PR URL is cheap to emit and shows up in bulk any time a tool dumps
1160
+ * PRs the conversation isn't actually about — `gh pr list`, `gh pr status`, a
1161
+ * `gh search` — printing dozens of `/pull/N` URLs the agent never discusses.
1162
+ * Pinning a sidebar pill for every one of those is noise.
1163
+ *
1164
+ * So a PR is surfaced only when BOTH hold:
1165
+ * 1. a fully-qualified `github.com/<o>/<r>/pull/<n>` URL exists — this may
1166
+ * come from the assistant's prose OR from tool output (a `gh pr create`
1167
+ * that printed the URL the agent then referenced by number), and
1168
+ * 2. the **assistant's prose** names the number as a human-style `#<n>`.
1169
+ *
1170
+ * The `#N` in prose is the intent signal — "the agent is talking about this
1171
+ * PR", as opposed to "a tool happened to list it". The full URL (from wherever)
1172
+ * proves the PR is real and pins down its repo + canonical link. A URL with no
1173
+ * `#N` in prose (the whole of a `gh pr list` dump) is dropped; a `#N` with no
1174
+ * matching URL anywhere (a bare issue-style ref) is dropped.
1175
+ *
1176
+ * `proseText` is the assistant's prose alone (the only source of the `#N`
1177
+ * relevance signal); `urlOnlyText` is settled tool output that may carry full
1178
+ * PR URLs but no `#N` intent. URLs are harvested from both; `#N` from prose
1179
+ * only.
1180
+ */
1181
+ function collectRelevant(proseText, urlOnlyText, options, byKey) {
1182
+ const byNumber = /* @__PURE__ */ new Map();
1183
+ urlsByNumber(proseText, options.requireCompleteNumber, byNumber);
1184
+ if (urlOnlyText) urlsByNumber(urlOnlyText, false, byNumber);
1185
+ if (byNumber.size === 0) return;
1186
+ const named = hashNumbers(proseText);
1187
+ if (named.size === 0) return;
1188
+ for (const [number, refs] of byNumber) {
1189
+ if (!named.has(number)) continue;
1190
+ for (const ref of refs) if (!byKey.has(ref.key)) byKey.set(ref.key, ref);
1191
+ }
1192
+ }
1193
+ /**
1194
+ * Extracts every distinct GitHub PR that is both linked (a full `/pull/<n>`
1195
+ * URL) AND named (`#<n>`), de-duplicated by **repo-qualified identity**
1196
+ * (`owner/repo#number`) — NOT by number, since `#42` can exist in many repos.
1197
+ *
1198
+ * The two-argument form takes the URL pool and the prose separately, so a URL
1199
+ * from tool output can satisfy the link requirement while the `#N` stays a
1200
+ * prose-only signal. The one-argument form (prose only) is the common case: it
1201
+ * requires the URL to be in the same prose that names it.
1202
+ *
1203
+ * See {@link collectRelevant} for the contract.
1204
+ */
1205
+ function extractPullRequests(proseText, options) {
1206
+ const requireCompleteNumber = options?.requireCompleteNumber ?? true;
1207
+ const urlOnly = options?.urlOnlyText ?? "";
1208
+ const byKey = /* @__PURE__ */ new Map();
1209
+ collectRelevant(proseText, urlOnly, { requireCompleteNumber }, byKey);
1117
1210
  return [...byKey.values()];
1118
1211
  }
1119
1212
  /**
@@ -1152,28 +1245,34 @@ function stringifyToolOutput(value) {
1152
1245
  }
1153
1246
  /**
1154
1247
  * A streaming PR scanner: fed run chunks as they stream, it reports each
1155
- * distinct PR the moment its URL is seen, exactly once, without ever rescanning
1156
- * the transcript. Two sources feed it:
1248
+ * distinct PR the moment BOTH conditions hold a full `/pull/<n>` URL has been
1249
+ * seen (in prose OR tool output) AND the assistant's prose names it `#<n>` —
1250
+ * exactly once, without ever rescanning the transcript.
1157
1251
  *
1158
- * - **Assistant text** (`text-delta`): accumulated per part id (a URL can split
1159
- * across two deltas) and only the current part's buffer is scanned, so cost
1160
- * is bounded by one text block, not the whole scrollback.
1161
- * - **Tool output** (`tool-output-available`): the settled output of a tool
1162
- * call (e.g. `gh pr create` stdout) arrives whole, not delta-streamed, so we
1163
- * flatten and scan it in one pass. This catches PRs a tool printed that the
1164
- * agent never restated in prose the common case for an opened PR.
1252
+ * Two sources feed it, with different roles:
1253
+ * - **Assistant text** (`text-delta`): the ONLY source of the `#N` relevance
1254
+ * signal, and also a source of URLs. Accumulated per part id (a URL or `#N`
1255
+ * can split across two deltas); only the current part's buffer is scanned.
1256
+ * - **Tool output** (`tool-output-available`): a URL-only source. A
1257
+ * `gh pr create` prints the URL the agent then references by number, so the
1258
+ * URL for a #N mention often lives here, not in the prose. Tool output can
1259
+ * NEVER originate a pill on its own — a `gh pr list` dump (URLs, no `#N`)
1260
+ * stays silent — but it satisfies the URL half for a PR the prose names.
1165
1261
  *
1166
1262
  * `onPullRequest` fires once per never-before-seen PR (keyed by
1167
- * `owner/repo#number`) for the life of the scanner, regardless of which source
1168
- * it came from. Construct one per run (or per conversation) and feed it the
1169
- * same chunks the transcript reducer sees.
1263
+ * `owner/repo#number`). Construct one per run (or per conversation) and feed it
1264
+ * the same chunks the transcript reducer sees. See {@link collectRelevant}.
1170
1265
  */
1171
1266
  function createPullRequestScanner(onPullRequest) {
1172
1267
  let text = "";
1173
1268
  let lastPartId;
1269
+ let toolUrlText = "";
1174
1270
  const seen = /* @__PURE__ */ new Set();
1175
- const report = (buffer, final) => {
1176
- for (const pr of extractPullRequests(buffer, { requireCompleteNumber: !final })) {
1271
+ const report = (final) => {
1272
+ for (const pr of extractPullRequests(text, {
1273
+ requireCompleteNumber: !final,
1274
+ urlOnlyText: toolUrlText
1275
+ })) {
1177
1276
  if (seen.has(pr.key)) continue;
1178
1277
  seen.add(pr.key);
1179
1278
  onPullRequest(pr);
@@ -1183,7 +1282,8 @@ function createPullRequestScanner(onPullRequest) {
1183
1282
  if (chunk["type"] === "tool-output-available") {
1184
1283
  const outputText = stringifyToolOutput(chunk["output"]);
1185
1284
  if (!outputText.includes("/pull/")) return;
1186
- report(outputText, true);
1285
+ toolUrlText += `\n${outputText}`;
1286
+ report(true);
1187
1287
  return;
1188
1288
  }
1189
1289
  if (chunk["type"] !== "text-delta") return;
@@ -1191,13 +1291,12 @@ function createPullRequestScanner(onPullRequest) {
1191
1291
  if (!delta) return;
1192
1292
  const partId = typeof chunk["id"] === "string" ? chunk["id"] : void 0;
1193
1293
  if (partId && lastPartId && partId !== lastPartId) {
1194
- if (text.includes("/pull/")) report(text, true);
1294
+ report(true);
1195
1295
  text = "";
1196
1296
  }
1197
1297
  text += delta;
1198
1298
  lastPartId = partId ?? lastPartId;
1199
- if (!text.includes("/pull/")) return;
1200
- report(text, false);
1299
+ report(false);
1201
1300
  } };
1202
1301
  }
1203
1302
 
@@ -10884,10 +10983,13 @@ function ChatScreen({ agent, conversation }) {
10884
10983
  text: recapText
10885
10984
  });
10886
10985
  setItems(loaded);
10986
+ const toolUrlText = loaded.map((item) => item.kind === "tool" && item.output?.kind === "ok" ? stringifyToolOutput(item.output.value) : "").join("\n");
10887
10987
  for (const item of loaded) {
10888
- const text = item.kind === "assistant-text" ? item.text : item.kind === "tool" && item.output?.kind === "ok" ? stringifyToolOutput(item.output.value) : null;
10889
- if (text === null) continue;
10890
- for (const pr of extractPullRequests(text, { requireCompleteNumber: false })) {
10988
+ if (item.kind !== "assistant-text") continue;
10989
+ for (const pr of extractPullRequests(item.text, {
10990
+ requireCompleteNumber: false,
10991
+ urlOnlyText: toolUrlText
10992
+ })) {
10891
10993
  if (reportedPrsRef.current.has(pr.key)) continue;
10892
10994
  reportedPrsRef.current.set(pr.key, pr);
10893
10995
  setPrLinks((prev) => [...prev, pr]);
@@ -2,11 +2,14 @@
2
2
  import { a as mintPortalDeviceToken, c as unifyLegacyCliGrants, i as grantPortalAccess, n as fetchPortalDevices, r as findThisDevice } from "./api-DG5W6iwx.mjs";
3
3
  import os from "node:os";
4
4
  import { z } from "zod";
5
- import { execFile, spawn } from "node:child_process";
5
+ import * as childProcess from "node:child_process";
6
+ import { spawn } from "node:child_process";
6
7
  import net from "node:net";
7
8
  import { WebSocket, createWebSocketStream } from "ws";
8
9
 
9
10
  //#region ../portal-daemon/src/machine.ts
11
+ const SCUTIL_TIMEOUT_MS = 2e3;
12
+ const SCUTIL_ATTEMPTS = 3;
10
13
  /**
11
14
  * Identity this machine registers under on the portal.
12
15
  *
@@ -15,38 +18,74 @@ import { WebSocket, createWebSocketStream } from "ws";
15
18
  * row, so a grant applies to the machine no matter which surface the user
16
19
  * shared from. The name must therefore match what the desktop app registers —
17
20
  * on macOS that is `scutil --get LocalHostName` (the Rust client's own
18
- * derivation, stabler than the network-assigned hostname), with the plain
19
- * hostname as the fallback for other platforms or a failed read.
21
+ * derivation, stabler than the network-assigned hostname).
22
+ *
23
+ * The machine name is a durable IDENTITY, not a fresh network reading: every
24
+ * grant hangs off the device row keyed by this name, so if it changes the
25
+ * device flips to a brand-new row with ZERO grants and every agent re-prompts
26
+ * across every open CLI. `os.hostname()` is NOT durable — macOS rewrites it
27
+ * from DHCP/DNS per network — so it must never be allowed to silently replace
28
+ * a name we already registered under. Resolution order:
29
+ *
30
+ * 1. `scutil --get LocalHostName` (retried) — the stable, correct source.
31
+ * 2. `previous` — the name we last successfully registered under, persisted
32
+ * by the daemon. Preferred over the volatile hostname so a transient
33
+ * `scutil` failure keeps our existing identity (and grants) intact.
34
+ * 3. `os.hostname()` — last resort for a machine that has NEVER resolved a
35
+ * name (first run, non-macOS, scutil permanently unavailable).
20
36
  *
21
37
  * Historically the CLI suffixed `-cli` to keep a device distinct from the
22
38
  * desktop's; `unifyLegacyCliGrants` migrates grants off those rows.
23
39
  */
24
- async function resolveMachineIdentity() {
25
- const fallback = fallbackHost();
26
- if (process.platform !== "darwin") return {
27
- machineName: fallback,
28
- friendlyName: fallback
40
+ async function resolveMachineIdentity(previous) {
41
+ const persisted = previous?.trim() || null;
42
+ if (process.platform !== "darwin") {
43
+ const machineName = persisted ?? fallbackHost();
44
+ return {
45
+ machineName,
46
+ friendlyName: machineName,
47
+ machineNameSource: persisted ? "persisted" : "hostname-fallback"
48
+ };
49
+ }
50
+ const local = await scutilRead("LocalHostName");
51
+ if (local) return {
52
+ machineName: local,
53
+ friendlyName: await scutilRead("ComputerName") ?? local,
54
+ machineNameSource: "scutil"
55
+ };
56
+ if (persisted) return {
57
+ machineName: persisted,
58
+ friendlyName: persisted,
59
+ machineNameSource: "persisted"
29
60
  };
30
- const local = await scutilRead("LocalHostName") ?? fallback;
61
+ const fallback = fallbackHost();
31
62
  return {
32
- machineName: local,
33
- friendlyName: await scutilRead("ComputerName") ?? local
63
+ machineName: fallback,
64
+ friendlyName: fallback,
65
+ machineNameSource: "hostname-fallback"
34
66
  };
35
67
  }
36
68
  function fallbackHost() {
37
69
  return (os.hostname() || "machine").trim().replace(/\.local$/i, "") || "machine";
38
70
  }
39
71
  function scutilRead(key) {
40
- return new Promise((resolve) => {
41
- execFile("/usr/sbin/scutil", ["--get", key], { timeout: 2e3 }, (error, stdout) => {
42
- if (error) {
43
- resolve(null);
44
- return;
45
- }
46
- const value = stdout.trim();
47
- resolve(value.length > 0 ? value : null);
72
+ return attempt(0);
73
+ function attempt(n) {
74
+ return new Promise((resolve) => {
75
+ childProcess.execFile("/usr/sbin/scutil", ["--get", key], { timeout: SCUTIL_TIMEOUT_MS }, (error, stdout) => {
76
+ if (error) {
77
+ if (n + 1 < SCUTIL_ATTEMPTS) {
78
+ resolve(attempt(n + 1));
79
+ return;
80
+ }
81
+ resolve(null);
82
+ return;
83
+ }
84
+ const value = stdout.trim();
85
+ resolve(value.length > 0 ? value : null);
86
+ });
48
87
  });
49
- });
88
+ }
50
89
  }
51
90
  const INHERITED_ENV = [
52
91
  "HOME",
@@ -550,9 +589,10 @@ var PortalClient = class {
550
589
  }
551
590
  async connectLoop() {
552
591
  if (!this.machineName) {
553
- const identity = await resolveMachineIdentity();
592
+ const identity = await resolveMachineIdentity(this.opts.persistedMachineName);
554
593
  this.machineName = identity.machineName;
555
594
  this.friendlyName = identity.friendlyName;
595
+ this.opts.onMachineName(identity.machineName, identity.machineNameSource);
556
596
  }
557
597
  let backoff = INITIAL_BACKOFF_MS;
558
598
  while (this.enabled && !this.disposed) {
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { t as PortalClient } from "./client-DbqRBquD.mjs";
3
+ import "./api-DG5W6iwx.mjs";
4
+
5
+ export { PortalClient };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./client-DabRpc_T.mjs";
3
- import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-ShOaDbZe.mjs";
2
+ import "./client-DbqRBquD.mjs";
3
+ import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-eb9dm6Ih.mjs";
4
4
  import "./api-DG5W6iwx.mjs";
5
5
 
6
6
  export { runPortalDaemon };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import "./client-DbqRBquD.mjs";
3
+ import "./daemon-eb9dm6Ih.mjs";
4
+ import "./api-DG5W6iwx.mjs";
5
+ import { t as PortalDaemonClient } from "./daemon-client-N8rvGusv.mjs";
6
+
7
+ export { PortalDaemonClient };
@@ -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-ShOaDbZe.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-eb9dm6Ih.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as isRecord, t as PortalClient } from "./client-DabRpc_T.mjs";
2
+ import { n as isRecord, t as PortalClient } from "./client-DbqRBquD.mjs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { z } from "zod";
@@ -20,7 +20,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
20
20
  * the commit time of the built tree is one monotonic clock they share.
21
21
  */
22
22
  function portalDaemonBuild() {
23
- return "1786601366";
23
+ return "1786605017";
24
24
  }
25
25
  /**
26
26
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -365,6 +365,7 @@ var PortalDaemon = class {
365
365
  conns = /* @__PURE__ */ new Set();
366
366
  cwds = /* @__PURE__ */ new Map();
367
367
  declined = /* @__PURE__ */ new Set();
368
+ persistedMachineName = null;
368
369
  fallbackCwd;
369
370
  idleTimer = null;
370
371
  sessionToken = null;
@@ -499,12 +500,28 @@ var PortalDaemon = class {
499
500
  deviceToken: this.deviceToken
500
501
  }),
501
502
  resolveCwd: (conversationId) => this.resolveCwd(conversationId),
503
+ persistedMachineName: this.persistedMachineName,
504
+ onMachineName: (name, source) => this.onMachineName(name, source),
502
505
  onState: (state) => {
503
506
  this.lastState = state;
504
507
  this.broadcastState();
505
508
  }
506
509
  });
507
510
  }
511
+ /**
512
+ * The identity resolved on connect. Persist the name so a later scutil
513
+ * failure reuses it instead of adopting the volatile hostname, and log the
514
+ * source — a `hostname-fallback` after we'd previously registered under
515
+ * scutil is the tell that a flap just orphaned this machine's grants.
516
+ */
517
+ onMachineName(name, source) {
518
+ if (source === "hostname-fallback" && this.persistedMachineName) this.logInfo(`portal identity WARNING: fell back to hostname "${name}" but had previously registered as "${this.persistedMachineName}" — grants may be orphaned`);
519
+ else this.logInfo(`portal identity: "${name}" (source=${source})`);
520
+ if (name && name !== this.persistedMachineName) {
521
+ this.persistedMachineName = name;
522
+ this.persistState();
523
+ }
524
+ }
508
525
  /** The cwd an exec for `conversationId` runs in. */
509
526
  resolveCwd(conversationId) {
510
527
  if (conversationId) {
@@ -538,7 +555,14 @@ var PortalDaemon = class {
538
555
  /** Append a line to the daemon log file (best-effort, for post-hoc debugging). */
539
556
  logError(context, error) {
540
557
  const message = error instanceof Error ? error.message : String(error);
541
- const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${context}: ${message}\n`;
558
+ this.logLine(`${context}: ${message}`);
559
+ }
560
+ /** Append an informational line to the daemon log file (best-effort). */
561
+ logInfo(message) {
562
+ this.logLine(message);
563
+ }
564
+ logLine(message) {
565
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${message}\n`;
542
566
  appendFile(this.paths.logPath, line).catch((_error) => {});
543
567
  }
544
568
  /** Close the listener without exiting the process (tests own the process). */
@@ -598,6 +622,7 @@ var PortalDaemon = class {
598
622
  if (Array.isArray(parsed.declined)) {
599
623
  for (const id of parsed.declined) if (typeof id === "string") this.declined.add(id);
600
624
  }
625
+ if (typeof parsed.machineName === "string" && parsed.machineName) this.persistedMachineName = parsed.machineName;
601
626
  }
602
627
  } catch (_error) {}
603
628
  }
@@ -605,7 +630,8 @@ var PortalDaemon = class {
605
630
  const state = {
606
631
  version: LOCAL_PROTOCOL_VERSION,
607
632
  cwds: Object.fromEntries(this.cwds),
608
- declined: [...this.declined]
633
+ declined: [...this.declined],
634
+ machineName: this.persistedMachineName ?? void 0
609
635
  };
610
636
  writeFile(this.paths.statePath, JSON.stringify(state)).catch((error) => {
611
637
  this.logError("persistState failed", error);
@@ -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-BuU34IVE.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`);
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
 
9
9
  //#region package.json
10
10
  var name = "skydive-cli";
11
- var version$1 = "0.5.0-beta.13";
11
+ var version$1 = "0.5.0-beta.15";
12
12
 
13
13
  //#endregion
14
14
  //#region src/auth/organization.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.5.0-beta.13",
3
+ "version": "0.5.0-beta.15",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as PortalClient } from "./client-DabRpc_T.mjs";
3
- import "./api-DG5W6iwx.mjs";
4
-
5
- export { PortalClient };
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-DabRpc_T.mjs";
3
- import "./daemon-ShOaDbZe.mjs";
4
- import "./api-DG5W6iwx.mjs";
5
- import { t as PortalDaemonClient } from "./daemon-client-BVAlyN7x.mjs";
6
-
7
- export { PortalDaemonClient };