skydive-cli 0.2.0-beta.618 → 0.2.0-beta.626

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/README.md CHANGED
@@ -6,6 +6,53 @@ The Skydive CLI — manage agents and chat with them from the terminal.
6
6
  npx skydive-cli --help
7
7
  ```
8
8
 
9
+ ## Installing
10
+
11
+ Two ways to install, depending on whether you have a Node toolchain:
12
+
13
+ **npm (needs Node ≥ 20):**
14
+
15
+ ```sh
16
+ npm install -g skydive-cli # then: skydive --help
17
+ # or run without installing:
18
+ npx skydive-cli --help
19
+ ```
20
+
21
+ **Standalone binary (no Node required):**
22
+
23
+ ```sh
24
+ curl -fsSL https://skydive.com/api/v1/cli/install.sh | sh
25
+ ```
26
+
27
+ Installs a self-contained `skydive` to `~/.local/bin`. The installer detects
28
+ your OS/arch, downloads the matching binary from the release CDN, verifies its
29
+ sha256, and installs it. Options via env vars: `SKYDIVE_CHANNEL=canary` for the
30
+ canary line, `SKYDIVE_VERSION=X.Y.Z` to pin a version, `SKYDIVE_INSTALL_DIR` to
31
+ choose the directory.
32
+
33
+ ## Updating
34
+
35
+ ```sh
36
+ skydive update # update to the latest release
37
+ skydive update --check # just report whether an update is available
38
+ ```
39
+
40
+ A **binary** install self-updates in place (download → verify sha256 → atomic
41
+ replace). An **npm/yarn/pnpm/bun** install is owned by your package manager, so
42
+ `skydive update` offers to run the right upgrade command for it and relaunches
43
+ the CLI on the new version.
44
+
45
+ The CLI also checks for updates in the background (at most once a day, never
46
+ blocking a command) and prints a one-line notice when a newer version exists.
47
+ Control this in `config.json` or via env vars:
48
+
49
+ - `autoupdate`: `"patch"` (default — silently self-apply a stable **patch**
50
+ release of a binary install; notify for minor/major, canary, and
51
+ package-manager installs), `"notify"` (only ever print the notice), or
52
+ `false` (no auto-update and no notice). Env override: `SKYDIVE_AUTOUPDATE`.
53
+ - `updateCheck: false` (or `SKYDIVE_NO_UPDATE_CHECK` / `NO_UPDATE_NOTIFIER`)
54
+ turns the background check off entirely.
55
+
9
56
  ## Authentication
10
57
 
11
58
  Sign in once via the browser — the same shape as `doppler login`: the CLI
@@ -244,28 +291,31 @@ management API used by `agents`/`keys`.
244
291
 
245
292
  ### Keybindings
246
293
 
247
- | Context | Key | Action |
248
- | ------------------- | ------------- | ------------------------------------- |
249
- | Pickers | _type_ | filter the list |
250
- | Pickers | `↑` / `↓` | move selection |
251
- | Pickers | `↵` | open / select |
252
- | Pickers | `esc` | back |
253
- | Agent picker | `ctrl+n` | create an agent |
254
- | Agent picker | `ctrl+w` | switch workspace |
255
- | Agent picker | `tab` | toggle mine / whole-org agents |
256
- | Conversation picker | `ctrl+d` | delete the highlighted conversation |
257
- | Chat | `↵` | send (or queue, while streaming) |
258
- | Chat | `pgup`/`pgdn` | scroll the transcript |
259
- | Chat | `ctrl+c` | cancel the active run; again to quit |
260
- | Chat | `esc` | back to the conversation picker |
261
- | Chat | `ctrl+p` | switch model |
262
- | Chat | `ctrl+o` | switch theme |
263
- | Chat | `ctrl+t` | grant / revoke local machine access |
264
- | Chat | `ctrl+l` | open this conversation in the browser |
265
- | Chat | `ctrl+r` | run the newest pending connect card |
266
- | Chat | `ctrl+v` | paste an image from the clipboard |
267
- | Chat | `ctrl+x` | drop the most recently staged image |
268
- | Chat | `?` | show the keybinding reference |
294
+ | Context | Key | Action |
295
+ | ------------------- | ------------- | ------------------------------------------ |
296
+ | Pickers | _type_ | filter the list |
297
+ | Pickers | `↑` / `↓` | move selection |
298
+ | Pickers | `↵` | open / select |
299
+ | Pickers | `esc` | back |
300
+ | Agent picker | `ctrl+n` | create an agent |
301
+ | Agent picker | `ctrl+w` | switch workspace |
302
+ | Agent picker | `tab` | toggle mine / whole-org agents |
303
+ | Conversation picker | `ctrl+a` | archive / restore it for you only |
304
+ | Conversation picker | `ctrl+d` | delete the highlighted conversation |
305
+ | Conversation picker | `shift+tab` | show / hide the ones you archived |
306
+ | Chat | `↵` | send (or queue, while streaming) |
307
+ | Chat | `pgup`/`pgdn` | scroll the transcript |
308
+ | Chat | `ctrl+c` | cancel the active run; again to quit |
309
+ | Chat | `esc` | back to the conversation picker |
310
+ | Chat | `ctrl+a` | archive this conversation (empty composer) |
311
+ | Chat | `ctrl+p` | switch model |
312
+ | Chat | `ctrl+o` | switch theme |
313
+ | Chat | `ctrl+t` | grant / revoke local machine access |
314
+ | Chat | `ctrl+l` | open this conversation in the browser |
315
+ | Chat | `ctrl+r` | run the newest pending connect card |
316
+ | Chat | `ctrl+v` | paste an image from the clipboard |
317
+ | Chat | `ctrl+x` | drop the most recently staged image |
318
+ | Chat | `?` | show the keybinding reference |
269
319
 
270
320
  Messages typed while a response is streaming are **queued** and sent in
271
321
  order as each run finishes; `ctrl+c` cancels the current run and clears the
package/dist/js/bin.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
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-BhPuzBxW.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";
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-CQNnRJ0V.mjs";
3
+ import { A as resolveAppUrl, C as getPromptHistoryPath, D as getStoredApiKeyId, E as getShareMachineDefault, F as saveConfig, I as saveSession, M as resolveManagementAuth, N as resolveSession, O as getStoredApiKeyWorkspaceName, R as setLastSeenVersion, S as getLastSeenVersion, _ as API_KEY_PREFIX, b as deleteConfig, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as resolveConfig, k as getUpdateCheckDisabled, v as DEFAULT_API_URL, x as getConfigPath } from "./print-DaPpTqxu.mjs";
4
4
  import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
5
  import { n as createRestClient } from "./rest-Q5yl_b2-.mjs";
6
6
  import { a as registerPortalDevice, i as grantPortalAccess, n as fetchPortalDevices, o as revokePortalAccess, r as findThisDevice, s as machineIdentity } from "./client-DT9bWi1D.mjs";
@@ -17,7 +17,9 @@ import { spawn, spawnSync } from "node:child_process";
17
17
  import { createHash } from "node:crypto";
18
18
  import fs from "node:fs";
19
19
  import zlib from "node:zlib";
20
+ import fsp from "node:fs/promises";
20
21
  import semver from "semver";
22
+ import { createInterface } from "node:readline";
21
23
 
22
24
  //#region src/types.ts
23
25
  const NON_INTERACTIVE_ENV_VARS = [
@@ -1196,7 +1198,7 @@ const importCommand = {
1196
1198
  process.exit(1);
1197
1199
  }
1198
1200
  }
1199
- const { runChat } = await import("./boot-CaWHIM4R.mjs");
1201
+ const { runChat } = await import("./boot-Bk_R-SnG.mjs");
1200
1202
  await runChat({
1201
1203
  appUrl,
1202
1204
  sessionToken: session.value.sessionToken,
@@ -1231,7 +1233,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1231
1233
  });
1232
1234
  const extra = (argv.print ?? "").trim();
1233
1235
  const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
1234
- const { runPrint } = await import("./print-nefugWMx.mjs");
1236
+ const { runPrint } = await import("./print-CB6L_1YB.mjs");
1235
1237
  try {
1236
1238
  const result = await runPrint({
1237
1239
  appUrl,
@@ -1512,7 +1514,7 @@ const chatCommand = {
1512
1514
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
1513
1515
  process.exit(1);
1514
1516
  }
1515
- const { runChat } = await import("./boot-CaWHIM4R.mjs");
1517
+ const { runChat } = await import("./boot-Bk_R-SnG.mjs");
1516
1518
  await runChat({
1517
1519
  appUrl,
1518
1520
  sessionToken: session.value.sessionToken,
@@ -1549,7 +1551,7 @@ async function runPrintMode({ argv, appUrl }) {
1549
1551
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
1550
1552
  process.exit(1);
1551
1553
  }
1552
- const { runPrint, readStdin } = await import("./print-nefugWMx.mjs");
1554
+ const { runPrint, readStdin } = await import("./print-CB6L_1YB.mjs");
1553
1555
  let prompt = (argv.print ?? "").trim();
1554
1556
  if (!prompt) {
1555
1557
  if (process.stdin.isTTY) {
@@ -1623,7 +1625,7 @@ const getCommand = {
1623
1625
  printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
1624
1626
  process.exit(1);
1625
1627
  }
1626
- const { messageGet } = await import("./print-nefugWMx.mjs");
1628
+ const { messageGet } = await import("./print-CB6L_1YB.mjs");
1627
1629
  try {
1628
1630
  const result = await messageGet({
1629
1631
  appUrl,
@@ -1820,7 +1822,7 @@ const switchCommand = {
1820
1822
  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.");
1821
1823
  process.exit(1);
1822
1824
  }
1823
- const { runWorkspacePicker } = await import("./boot-CaWHIM4R.mjs");
1825
+ const { runWorkspacePicker } = await import("./boot-Bk_R-SnG.mjs");
1824
1826
  await runWorkspacePicker(session);
1825
1827
  return;
1826
1828
  }
@@ -2172,7 +2174,7 @@ const sandboxCommand = {
2172
2174
  handler: async (argv) => {
2173
2175
  const session = requireSession(argv);
2174
2176
  const { createRestClient } = await import("./rest-CBROy0L-.mjs");
2175
- const { resolveAgent } = await import("./print-nefugWMx.mjs");
2177
+ const { resolveAgent } = await import("./print-CB6L_1YB.mjs");
2176
2178
  const client = createRestClient({
2177
2179
  appUrl: session.appUrl,
2178
2180
  sessionToken: session.sessionToken
@@ -2253,6 +2255,630 @@ async function runPty({ session, agentId, agentName }) {
2253
2255
  return result.code;
2254
2256
  }
2255
2257
 
2258
+ //#endregion
2259
+ //#region src/update-check/manifest.ts
2260
+ /**
2261
+ * The release manifest the binary pipeline publishes
2262
+ * (`.github/workflows/release-skydive-cli-binaries.yml`) and the api serves
2263
+ * (`apps/anyone/api/src/routes/cli-releases.ts`). The update *check* only
2264
+ * needs `version` (see ./sources.ts), but the self-updater
2265
+ * (`../commands/update.ts`) needs the per-target byte pointer + integrity
2266
+ * digest too, so the full shape lives here and both consume it.
2267
+ *
2268
+ * Kept deliberately in sync with the api's `manifestSchema`: a field the api
2269
+ * validates but we drop here is fine, but a field we *require* that the api
2270
+ * doesn't emit would make every install fail — so required fields mirror the
2271
+ * api exactly (name, version, channel, per-target key/sha256/size).
2272
+ */
2273
+ const releaseManifestSchema = z.object({
2274
+ name: z.string().min(1),
2275
+ version: z.string().min(1),
2276
+ channel: z.string().min(1),
2277
+ pubDate: z.string().optional(),
2278
+ targets: z.record(z.object({
2279
+ key: z.string().min(1),
2280
+ sha256: z.string().regex(/^[0-9a-f]{64}$/),
2281
+ size: z.number().int().positive()
2282
+ }))
2283
+ });
2284
+ /**
2285
+ * The four release targets the pipeline builds. Named `<os>-<arch>` to match
2286
+ * the manifest's `targets` keys and the install script's `$OS-$ARCH`.
2287
+ */
2288
+ const RELEASE_TARGETS = [
2289
+ "darwin-arm64",
2290
+ "darwin-x64",
2291
+ "linux-arm64",
2292
+ "linux-x64"
2293
+ ];
2294
+ function isReleaseTarget(value) {
2295
+ return RELEASE_TARGETS.some((target) => target === value);
2296
+ }
2297
+ /**
2298
+ * Map this host's `process.platform` / `process.arch` to a release target,
2299
+ * or null when we don't publish a binary for it. Mirrors the install
2300
+ * script's `uname -s` / `uname -m` normalization (arm64/aarch64 -> arm64,
2301
+ * x86_64/amd64 -> x64) so a CLI self-update resolves the same object a fresh
2302
+ * `curl | sh` install would.
2303
+ */
2304
+ function hostReleaseTarget(platform, arch) {
2305
+ const os = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : null;
2306
+ if (!os) return null;
2307
+ const cpu = arch === "arm64" || arch === "aarch64" ? "arm64" : arch === "x64" || arch === "x86_64" || arch === "amd64" ? "x64" : null;
2308
+ if (!cpu) return null;
2309
+ const target = `${os}-${cpu}`;
2310
+ return isReleaseTarget(target) ? target : null;
2311
+ }
2312
+ function lookupTarget(manifest, platform, arch) {
2313
+ const target = hostReleaseTarget(platform, arch);
2314
+ if (!target) return {
2315
+ ok: false,
2316
+ reason: "unsupported-host",
2317
+ target: null
2318
+ };
2319
+ const entry = manifest.targets[target];
2320
+ if (!entry) return {
2321
+ ok: false,
2322
+ reason: "missing-from-manifest",
2323
+ target
2324
+ };
2325
+ return {
2326
+ ok: true,
2327
+ target,
2328
+ entry
2329
+ };
2330
+ }
2331
+
2332
+ //#endregion
2333
+ //#region src/update-check/versions.ts
2334
+ /** Canary builds carry a prerelease suffix (`X.Y.Z-beta.N`, synthesized in
2335
+ * CI); a plain semver is a stable release. An unparseable version defaults
2336
+ * to stable. */
2337
+ function resolveChannel(version) {
2338
+ return (semver.prerelease(version, { loose: true })?.length ?? 0) > 0 ? "canary" : "stable";
2339
+ }
2340
+ /** npm dist-tag for a channel (see release-skydive-cli.yml). */
2341
+ function distTagForChannel(channel) {
2342
+ return channel === "canary" ? "beta" : "latest";
2343
+ }
2344
+ /**
2345
+ * True when `candidate` is a strictly newer release than `current`, per
2346
+ * semver precedence (the `semver` package, including prerelease ordering).
2347
+ * Unparseable input is never newer, so garbage from the registry can't
2348
+ * produce a notice.
2349
+ */
2350
+ function isNewerVersion(candidate, current) {
2351
+ if (!semver.valid(candidate, { loose: true })) return false;
2352
+ if (!semver.valid(current, { loose: true })) return false;
2353
+ return semver.gt(candidate, current, { loose: true });
2354
+ }
2355
+
2356
+ //#endregion
2357
+ //#region src/update-check/install.ts
2358
+ /**
2359
+ * The self-updater for compiled-binary installs. The update *check*
2360
+ * (./index.ts) is notify-only; this is what a user-invoked `skydive update`
2361
+ * (../commands/update.ts) runs to actually replace the binary.
2362
+ *
2363
+ * package-manager installs are never self-mutated here — that's the install
2364
+ * mode's own tool's job, and silently reaching into a global npm/yarn/pnpm
2365
+ * prefix is exactly the footgun the existing notice policy avoids. The
2366
+ * command handles that mode by printing the upgrade command; this module is
2367
+ * binary-only.
2368
+ *
2369
+ * The dangerous part is replacing a running executable in place, so the flow
2370
+ * is split: a pure {@link planUpdate} decides *whether* and *what* (testable
2371
+ * with no IO), and {@link downloadAndVerify} / {@link applyBinary} do the IO
2372
+ * with integrity checks and an atomic rename.
2373
+ */
2374
+ /** Cap so a wrong URL or a hung CDN can't wedge `skydive update` forever. */
2375
+ const DOWNLOAD_TIMEOUT_MS = 6e4;
2376
+ /**
2377
+ * Decide what `skydive update` should do, given the running version, the
2378
+ * install source, the fetched manifest (or null), and this host. Pure — no
2379
+ * network, no filesystem — so every branch is unit-testable.
2380
+ *
2381
+ * `allowSameVersion` forces a reinstall of the current version (the `--force`
2382
+ * escape hatch for a corrupt binary); `allowDowngrade` lets an explicit
2383
+ * pinned/older manifest install over a newer running binary (rollback).
2384
+ */
2385
+ function planUpdate(opts) {
2386
+ const { currentVersion, source, manifest, platform, arch } = opts;
2387
+ if (source !== "binary") return {
2388
+ kind: "unsupported-source",
2389
+ source,
2390
+ latestVersion: manifest?.version ?? currentVersion
2391
+ };
2392
+ if (!manifest) return {
2393
+ kind: "up-to-date",
2394
+ currentVersion
2395
+ };
2396
+ const newer = isNewerVersion(manifest.version, currentVersion);
2397
+ const same = manifest.version === currentVersion;
2398
+ if (!(newer || same && opts.allowSameVersion === true || !newer && !same && opts.allowDowngrade === true)) return {
2399
+ kind: "up-to-date",
2400
+ currentVersion
2401
+ };
2402
+ const lookup = lookupTarget(manifest, platform, arch);
2403
+ if (!lookup.ok) return lookup.reason === "unsupported-host" ? {
2404
+ kind: "unsupported-host",
2405
+ platform,
2406
+ arch
2407
+ } : {
2408
+ kind: "missing-from-manifest",
2409
+ target: lookup.target,
2410
+ version: manifest.version
2411
+ };
2412
+ return {
2413
+ kind: "update",
2414
+ target: lookup.target,
2415
+ version: manifest.version,
2416
+ key: lookup.entry.key,
2417
+ sha256: lookup.entry.sha256,
2418
+ size: lookup.entry.size
2419
+ };
2420
+ }
2421
+ var UpdateError = class extends Error {};
2422
+ /** Read a Node errno string (`EACCES`, `EROFS`, ...) off an unknown thrown
2423
+ * value without a type assertion — the property is validated before it's read. */
2424
+ function errnoCode(error) {
2425
+ if (error && typeof error === "object" && "code" in error) {
2426
+ const { code } = error;
2427
+ if (typeof code === "string") return code;
2428
+ }
2429
+ }
2430
+ /** Lowercase hex sha256 of a buffer, for comparing against the manifest. */
2431
+ function sha256Hex(bytes) {
2432
+ return createHash("sha256").update(bytes).digest("hex");
2433
+ }
2434
+ /**
2435
+ * Download the release object and verify its digest before it ever touches
2436
+ * the install path. Returns the raw bytes; the caller writes them atomically.
2437
+ * Throws {@link UpdateError} (with an actionable message) on any HTTP,
2438
+ * timeout, or integrity failure — never a bare fetch error.
2439
+ */
2440
+ async function downloadAndVerify(opts) {
2441
+ const url = `${opts.downloadBaseUrl}/${opts.key}`;
2442
+ const doFetch = opts.fetchImpl ?? fetch;
2443
+ const signal = opts.signal ?? AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS);
2444
+ let response;
2445
+ try {
2446
+ response = await doFetch(url, {
2447
+ signal,
2448
+ redirect: "follow"
2449
+ });
2450
+ } catch (error) {
2451
+ throw new UpdateError(`download failed from ${url}: ${error instanceof Error ? error.message : String(error)}`);
2452
+ }
2453
+ if (!response.ok) throw new UpdateError(`download failed from ${url}: HTTP ${response.status}`);
2454
+ const bytes = new Uint8Array(await response.arrayBuffer());
2455
+ if (bytes.byteLength === 0) throw new UpdateError(`download from ${url} was empty`);
2456
+ const actual = sha256Hex(bytes);
2457
+ if (actual !== opts.expectedSha256) throw new UpdateError(`sha256 mismatch (expected ${opts.expectedSha256}, got ${actual}) — the download may be corrupt; re-run \`skydive update\``);
2458
+ return bytes;
2459
+ }
2460
+ /**
2461
+ * Atomically replace the executable at `destPath` with `bytes`. The temp file
2462
+ * is written in the *same directory* as the target so the final `rename` is
2463
+ * atomic (rename across filesystems is not) — a crash mid-update leaves either
2464
+ * the old binary or the new one, never a truncated file.
2465
+ *
2466
+ * On a locked destination (Windows, or a read-only install dir) the rename
2467
+ * fails; we surface an actionable UpdateError instead of a raw errno. macOS
2468
+ * and Linux happily rename over a running executable (the old inode stays
2469
+ * live for the current process), which is what makes in-place self-update
2470
+ * safe here.
2471
+ */
2472
+ async function applyBinary(destPath, bytes) {
2473
+ const dir = path.dirname(destPath);
2474
+ const tmp = path.join(dir, `.skydive-update-${process.pid}-${Date.now()}.tmp`);
2475
+ try {
2476
+ await fsp.writeFile(tmp, bytes, { mode: 493 });
2477
+ await fsp.chmod(tmp, 493);
2478
+ await fsp.rename(tmp, destPath);
2479
+ } catch (error) {
2480
+ await fsp.rm(tmp, { force: true }).catch(() => {});
2481
+ const code = errnoCode(error);
2482
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new UpdateError(`cannot write ${destPath} (permission denied). Re-run with the right permissions, or reinstall with the install script.`);
2483
+ throw new UpdateError(`failed to install the new binary at ${destPath}: ${error instanceof Error ? error.message : String(error)}`);
2484
+ }
2485
+ }
2486
+ /**
2487
+ * Resolve the path of the currently running executable to overwrite. For a
2488
+ * `bun --compile` binary that's `process.execPath`; we resolve symlinks so a
2489
+ * `~/.local/bin/skydive` symlink into a versioned store updates the real
2490
+ * file, not the link. Returns null when the path can't be determined (e.g. a
2491
+ * non-binary context), which the command treats as "can't self-update".
2492
+ */
2493
+ function resolveSelfPath(execPath = process.execPath) {
2494
+ if (!execPath) return null;
2495
+ try {
2496
+ return fs.realpathSync(execPath);
2497
+ } catch (_error) {
2498
+ return execPath;
2499
+ }
2500
+ }
2501
+
2502
+ //#endregion
2503
+ //#region src/update-check/sources.ts
2504
+ function resolveInstallSource() {
2505
+ return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
2506
+ }
2507
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org";
2508
+ const npmDistTagsSchema = z.record(z.string(), z.unknown());
2509
+ const binaryManifestSchema = z.object({ version: z.unknown().optional() });
2510
+ /** npm dist-tags for the published package (`skydive-cli`). */
2511
+ const npmReleaseSource = {
2512
+ async fetchLatestVersion(channel, { signal }) {
2513
+ const response = await fetch(`${NPM_REGISTRY_URL}/-/package/${name}/dist-tags`, {
2514
+ signal,
2515
+ headers: { accept: "application/json" }
2516
+ });
2517
+ if (!response.ok) return null;
2518
+ const tags = npmDistTagsSchema.safeParse(await response.json());
2519
+ if (!tags.success) return null;
2520
+ const version = tags.data[distTagForChannel(channel)];
2521
+ return typeof version === "string" ? version : null;
2522
+ },
2523
+ async fetchManifest() {
2524
+ return null;
2525
+ }
2526
+ };
2527
+ /**
2528
+ * Release CDN (CloudFront over the releases bucket, infra: CliReleasesCdn).
2529
+ * Serves the channel pointers the release workflow uploads
2530
+ * (`channels/{stable,canary}.json`, cached max-age=60) alongside the
2531
+ * binaries. The daily poll goes here, not to the api, so a fleet of
2532
+ * installed binaries puts no load on — and takes no dependency on — the api.
2533
+ */
2534
+ const RELEASE_CDN_URL = "https://dl.skydive.com";
2535
+ /** Channel manifest on the release CDN, for compiled binaries. The same
2536
+ * document the api's channel route serves (see
2537
+ * apps/anyone/api/src/routes/cli-releases.ts, which reads it from the
2538
+ * bucket this CDN fronts). */
2539
+ const binaryReleaseSource = {
2540
+ async fetchLatestVersion(channel, { signal }) {
2541
+ const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
2542
+ signal,
2543
+ headers: { accept: "application/json" }
2544
+ });
2545
+ if (!response.ok) return null;
2546
+ const manifest = binaryManifestSchema.safeParse(await response.json());
2547
+ if (!manifest.success) return null;
2548
+ return typeof manifest.data.version === "string" ? manifest.data.version : null;
2549
+ },
2550
+ async fetchManifest(channel, { signal }) {
2551
+ const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
2552
+ signal,
2553
+ headers: { accept: "application/json" }
2554
+ });
2555
+ if (!response.ok) return null;
2556
+ const manifest = releaseManifestSchema.safeParse(await response.json());
2557
+ return manifest.success ? manifest.data : null;
2558
+ }
2559
+ };
2560
+ function releaseSourceForInstall(source) {
2561
+ return source === "binary" ? binaryReleaseSource : npmReleaseSource;
2562
+ }
2563
+
2564
+ //#endregion
2565
+ //#region src/update-check/notice.ts
2566
+ /** The install-mode-specific action line. A package-manager install is never
2567
+ * self-mutated — we only tell the user what to run. A binary install has a
2568
+ * built-in self-updater (`skydive update`, ../commands/update.ts), so that's
2569
+ * the action; the curl re-install stays the documented fallback. */
2570
+ function renderUpdateCommand(channel, source) {
2571
+ if (source === "binary") return channel === "canary" ? "skydive update --channel canary" : "skydive update";
2572
+ return `npm install -g ${name}@${distTagForChannel(channel)}`;
2573
+ }
2574
+ function renderUpdateNotice(opts) {
2575
+ return `\nUpdate available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun ${renderUpdateCommand(opts.channel, opts.source)}\n`;
2576
+ }
2577
+ /**
2578
+ * Whether this invocation may print the notice. Pure so it's testable; the
2579
+ * inputs are raw pre-yargs argv (parsing hasn't happened when this runs) and
2580
+ * stderr's TTY-ness. `--json`/`--quiet` go to stdout, and the notice goes to
2581
+ * stderr — but scripts commonly capture 2>&1, so machine-readable modes
2582
+ * suppress it entirely rather than risk corrupting piped output.
2583
+ */
2584
+ function shouldNotify(opts) {
2585
+ if (!opts.stderrIsTTY) return false;
2586
+ if (opts.argv.includes("--json") || opts.argv.includes("--quiet")) return false;
2587
+ return true;
2588
+ }
2589
+
2590
+ //#endregion
2591
+ //#region src/update-check/package-manager.ts
2592
+ const KNOWN = [
2593
+ "npm",
2594
+ "yarn",
2595
+ "pnpm",
2596
+ "bun"
2597
+ ];
2598
+ function isKnown(value) {
2599
+ return KNOWN.some((m) => m === value);
2600
+ }
2601
+ /**
2602
+ * Parse the manager name out of an `npm_config_user_agent` string. npm, yarn,
2603
+ * pnpm and bun all set it to `"<manager>/<version> ..."` when they spawn a
2604
+ * script, which is the most reliable signal for how *this* process was
2605
+ * launched. Returns null when the var is absent or unrecognized.
2606
+ */
2607
+ function parseUserAgent(userAgent) {
2608
+ if (!userAgent) return null;
2609
+ const name = userAgent.trim().split("/")[0]?.toLowerCase();
2610
+ return name && isKnown(name) ? name : null;
2611
+ }
2612
+ /**
2613
+ * Best-effort detection of the manager that owns this install. Order of
2614
+ * signals, most to least reliable:
2615
+ * 1. `npm_config_user_agent` — set by the manager that spawned us.
2616
+ * 2. the executable path — a global bin under a `pnpm`/`yarn`/`bun` store
2617
+ * names the manager even when we weren't spawned by it (e.g. the user
2618
+ * typed `skydive` directly).
2619
+ * Falls back to `npm`, the documented default install path, so the command we
2620
+ * show is always runnable — the prompt still lets the user decline and copy a
2621
+ * different one.
2622
+ */
2623
+ function detectPackageManager(opts) {
2624
+ const fromUa = parseUserAgent(opts.env["npm_config_user_agent"]);
2625
+ if (fromUa) return fromUa;
2626
+ const exec = opts.execPath.toLowerCase();
2627
+ if (exec.includes("pnpm")) return "pnpm";
2628
+ if (exec.includes("yarn")) return "yarn";
2629
+ if (exec.includes("bun")) return "bun";
2630
+ return "npm";
2631
+ }
2632
+ /**
2633
+ * The global-install command (as argv) for a manager + channel. Each manager's
2634
+ * global-add spelling differs; the version target is the channel's dist-tag
2635
+ * (`latest`/`beta`), matching what the release workflow publishes.
2636
+ */
2637
+ function installCommand(manager, channel) {
2638
+ const spec = `${name}@${distTagForChannel(channel)}`;
2639
+ switch (manager) {
2640
+ case "npm": return [
2641
+ "npm",
2642
+ "install",
2643
+ "-g",
2644
+ spec
2645
+ ];
2646
+ case "pnpm": return [
2647
+ "pnpm",
2648
+ "add",
2649
+ "-g",
2650
+ spec
2651
+ ];
2652
+ case "bun": return [
2653
+ "bun",
2654
+ "add",
2655
+ "-g",
2656
+ spec
2657
+ ];
2658
+ case "yarn": return [
2659
+ "yarn",
2660
+ "global",
2661
+ "add",
2662
+ spec
2663
+ ];
2664
+ }
2665
+ }
2666
+ /** The command as a copy-pasteable string for prompts and notices. */
2667
+ function installCommandString(manager, channel) {
2668
+ return installCommand(manager, channel).join(" ");
2669
+ }
2670
+
2671
+ //#endregion
2672
+ //#region src/update-check/pm-update.ts
2673
+ /**
2674
+ * The interactive upgrade path for a package-manager install: instead of only
2675
+ * printing "run npm install -g …", offer to run it now, and on confirm run
2676
+ * that exact command and relaunch the CLI on the new version.
2677
+ *
2678
+ * We never silently mutate a package-manager install (the manager owns it),
2679
+ * but an explicit prompt the user answers with a keypress is honest consent —
2680
+ * and it runs the *same* command we detected + display, so what you approve is
2681
+ * what executes. Non-interactive contexts (no TTY, --json/--quiet, CI) skip
2682
+ * the prompt entirely and fall back to the printed notice.
2683
+ */
2684
+ /** Whether it's safe to show an interactive prompt at all. */
2685
+ function canPromptInteractively(opts) {
2686
+ if (opts.json || opts.quiet) return false;
2687
+ if (!opts.stdinIsTTY || !opts.stdoutIsTTY) return false;
2688
+ if (opts.env["CI"] || opts.env["SKYDIVE_NO_UPDATE_CHECK"]) return false;
2689
+ return true;
2690
+ }
2691
+ /**
2692
+ * Interpret a raw prompt answer. Default (empty / Enter) is **yes** — the
2693
+ * prompt renders `[Y/n]`, so a bare Enter accepts. Anything starting with `n`
2694
+ * (case-insensitive) is no; any other non-empty input is treated as no too,
2695
+ * so a fat-fingered answer never runs an install the user didn't mean.
2696
+ */
2697
+ function interpretYesNo(answer) {
2698
+ const a = answer.trim().toLowerCase();
2699
+ if (a === "") return true;
2700
+ return a === "y" || a === "yes";
2701
+ }
2702
+ /** Ask a `[Y/n]` question on the TTY and resolve to the boolean answer. */
2703
+ async function promptYesNo(question) {
2704
+ const rl = createInterface({
2705
+ input: process.stdin,
2706
+ output: process.stdout
2707
+ });
2708
+ try {
2709
+ return interpretYesNo(await new Promise((resolve) => {
2710
+ rl.question(question, resolve);
2711
+ }));
2712
+ } finally {
2713
+ rl.close();
2714
+ }
2715
+ }
2716
+ /**
2717
+ * Offer + run the package-manager upgrade. Returns a result the caller turns
2718
+ * into output and an exit/relaunch decision — kept separate from the actual
2719
+ * relaunch so it's testable without re-execing the test runner.
2720
+ */
2721
+ async function offerPackageManagerUpdate(opts) {
2722
+ const manager = detectPackageManager({
2723
+ env: opts.env,
2724
+ execPath: opts.execPath
2725
+ });
2726
+ const command = installCommand(manager, opts.channel);
2727
+ const commandStr = installCommandString(manager, opts.channel);
2728
+ if (!opts.interactive) return { status: "skipped-noninteractive" };
2729
+ if (!await (opts.ask ?? promptYesNo)(`Update available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun \`${commandStr}\` now? [Y/n] `)) return { status: "declined" };
2730
+ const { status } = (opts.run ?? ((cmd) => {
2731
+ const [bin, ...args] = cmd;
2732
+ return { status: spawnSync(bin ?? "", args, { stdio: "inherit" }).status };
2733
+ }))(command);
2734
+ if (status !== 0) return {
2735
+ status: "failed",
2736
+ code: status,
2737
+ command: commandStr
2738
+ };
2739
+ return {
2740
+ status: "installed",
2741
+ command: commandStr
2742
+ };
2743
+ }
2744
+ /**
2745
+ * Relaunch the CLI after a successful in-place upgrade so the user lands back
2746
+ * on their command running the new version. Uses the same interpreter + argv
2747
+ * the current process was launched with; detaches so the parent can exit
2748
+ * cleanly. Never throws — a failed relaunch just means the user re-runs
2749
+ * manually, which the caller's message covers.
2750
+ */
2751
+ function relaunchCli(argv = process.argv) {
2752
+ try {
2753
+ const [, script, ...rest] = argv;
2754
+ spawn(process.execPath, script ? [script, ...rest] : rest, { stdio: "inherit" }).on("exit", (code) => process.exit(code ?? 0));
2755
+ } catch (_error) {
2756
+ process.exit(0);
2757
+ }
2758
+ }
2759
+
2760
+ //#endregion
2761
+ //#region src/commands/update.ts
2762
+ /**
2763
+ * `skydive update` — the user-invoked self-updater the update *check*
2764
+ * (../update-check/index.ts) has always pointed at. Notify-only remains the
2765
+ * automatic behavior; this is the explicit "do it now" action.
2766
+ *
2767
+ * Binary installs self-update in place (download -> verify sha256 -> atomic
2768
+ * replace); package-manager installs are never self-mutated (the notice
2769
+ * policy) — we print the exact upgrade command instead. `--check` is a dry
2770
+ * run; `--force` reinstalls the current version (repair a corrupt binary).
2771
+ */
2772
+ const FETCH_TIMEOUT_MS$1 = 15e3;
2773
+ function normalizeChannel(raw, currentVersion) {
2774
+ if (raw === "stable" || raw === "canary") return raw;
2775
+ return resolveChannel(currentVersion);
2776
+ }
2777
+ /** Human-readable line for a plan outcome. Pure so the handler stays thin. */
2778
+ function describePlan(plan, channel, isCheck) {
2779
+ switch (plan.kind) {
2780
+ case "up-to-date": return `Already on the latest ${channel} release (${plan.currentVersion}).`;
2781
+ case "unsupported-source": return `This is a ${plan.source} install, so update through your package manager:\n ${renderUpdateCommand(channel, plan.source)}`;
2782
+ case "unsupported-host": return `No Skydive binary is published for ${plan.platform}/${plan.arch}.`;
2783
+ case "missing-from-manifest": return `The ${channel} ${plan.version} release has no binary for your platform (${plan.target}).`;
2784
+ case "update": return isCheck ? `Update available: ${version} \u2192 ${plan.version} (${channel}, ${plan.target}).\nRun \`skydive update\` to install it.` : `Updating ${version} \u2192 ${plan.version} (${channel}, ${plan.target})...`;
2785
+ }
2786
+ }
2787
+ const updateCommand = {
2788
+ command: "update",
2789
+ describe: "Update the Skydive CLI to the latest release",
2790
+ builder: (y) => y.option("channel", {
2791
+ type: "string",
2792
+ choices: ["stable", "canary"],
2793
+ describe: "Release channel to update to (default: this build's channel)"
2794
+ }).option("check", {
2795
+ type: "boolean",
2796
+ default: false,
2797
+ describe: "Only report whether an update is available; don't install"
2798
+ }).option("force", {
2799
+ type: "boolean",
2800
+ default: false,
2801
+ describe: "Reinstall the current version even if already up to date (repairs a corrupt binary)"
2802
+ }).example("skydive update", "Install the latest release").example("skydive update --check", "See if an update is available").example("skydive update --channel canary", "Switch to the canary line"),
2803
+ handler: async (argv) => {
2804
+ const currentVersion = version;
2805
+ const source = resolveInstallSource();
2806
+ const channel = normalizeChannel(argv.channel, currentVersion);
2807
+ const manifest = await releaseSourceForInstall(source).fetchManifest(channel, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS$1) }).catch(() => null);
2808
+ if (source === "binary" && manifest === null) {
2809
+ if (argv.json) {
2810
+ output(argv, {
2811
+ status: "unavailable",
2812
+ channel,
2813
+ currentVersion
2814
+ });
2815
+ return;
2816
+ }
2817
+ throw new UpdateError(`couldn't reach the ${channel} release channel — check your connection and try again.`);
2818
+ }
2819
+ const plan = planUpdate({
2820
+ currentVersion,
2821
+ source,
2822
+ manifest,
2823
+ platform: process.platform,
2824
+ arch: process.arch,
2825
+ allowSameVersion: argv.force
2826
+ });
2827
+ if (argv.json) {
2828
+ output(argv, {
2829
+ plan,
2830
+ channel,
2831
+ currentVersion,
2832
+ dryRun: argv.check
2833
+ });
2834
+ return;
2835
+ }
2836
+ if (plan.kind === "unsupported-source" && !argv.check) {
2837
+ const interactive = canPromptInteractively({
2838
+ stdinIsTTY: Boolean(process.stdin.isTTY),
2839
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
2840
+ json: argv.json,
2841
+ quiet: argv.quiet,
2842
+ env: process.env
2843
+ });
2844
+ const result = await offerPackageManagerUpdate({
2845
+ channel,
2846
+ currentVersion,
2847
+ latestVersion: plan.latestVersion,
2848
+ env: process.env,
2849
+ execPath: process.execPath,
2850
+ interactive
2851
+ });
2852
+ switch (result.status) {
2853
+ case "installed":
2854
+ console.log(`Updated via \`${result.command}\`. Relaunching \`skydive\`...`);
2855
+ relaunchCli();
2856
+ return;
2857
+ case "failed": throw new UpdateError(`\`${result.command}\` exited with code ${result.code ?? "unknown"}. Try running it yourself.`);
2858
+ case "declined":
2859
+ console.log(describePlan(plan, channel, false));
2860
+ return;
2861
+ case "skipped-noninteractive":
2862
+ console.log(describePlan(plan, channel, false));
2863
+ return;
2864
+ }
2865
+ }
2866
+ if (plan.kind !== "update" || argv.check) {
2867
+ console.log(describePlan(plan, channel, argv.check));
2868
+ return;
2869
+ }
2870
+ const selfPath = resolveSelfPath();
2871
+ if (!selfPath) throw new UpdateError("couldn't determine this binary's path to replace it — reinstall with the install script instead.");
2872
+ console.log(describePlan(plan, channel, false));
2873
+ await applyBinary(selfPath, await downloadAndVerify({
2874
+ downloadBaseUrl: RELEASE_CDN_URL,
2875
+ key: plan.key,
2876
+ expectedSha256: plan.sha256
2877
+ }));
2878
+ console.log(`Updated to ${plan.version}. Run \`skydive --version\` to confirm.`);
2879
+ }
2880
+ };
2881
+
2256
2882
  //#endregion
2257
2883
  //#region src/cli.ts
2258
2884
  function createCli(argv) {
@@ -2270,7 +2896,7 @@ function createCli(argv) {
2270
2896
  type: "string",
2271
2897
  global: true,
2272
2898
  describe: "Override API base URL"
2273
- }).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(importCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
2899
+ }).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(importCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).command(updateCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
2274
2900
  printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
2275
2901
  process.exit(1);
2276
2902
  });
@@ -2382,100 +3008,6 @@ function isCheckDue(lastCheckedAt, now = Date.now()) {
2382
3008
  return now - then >= CHECK_INTERVAL_MS;
2383
3009
  }
2384
3010
 
2385
- //#endregion
2386
- //#region src/update-check/versions.ts
2387
- /** Canary builds carry a prerelease suffix (`X.Y.Z-beta.N`, synthesized in
2388
- * CI); a plain semver is a stable release. An unparseable version defaults
2389
- * to stable. */
2390
- function resolveChannel(version) {
2391
- return (semver.prerelease(version, { loose: true })?.length ?? 0) > 0 ? "canary" : "stable";
2392
- }
2393
- /** npm dist-tag for a channel (see release-skydive-cli.yml). */
2394
- function distTagForChannel(channel) {
2395
- return channel === "canary" ? "beta" : "latest";
2396
- }
2397
- /**
2398
- * True when `candidate` is a strictly newer release than `current`, per
2399
- * semver precedence (the `semver` package, including prerelease ordering).
2400
- * Unparseable input is never newer, so garbage from the registry can't
2401
- * produce a notice.
2402
- */
2403
- function isNewerVersion(candidate, current) {
2404
- if (!semver.valid(candidate, { loose: true })) return false;
2405
- if (!semver.valid(current, { loose: true })) return false;
2406
- return semver.gt(candidate, current, { loose: true });
2407
- }
2408
-
2409
- //#endregion
2410
- //#region src/update-check/notice.ts
2411
- /** The install-mode-specific action line. A package-manager install is never
2412
- * self-mutated — we only tell the user what to run. */
2413
- function renderUpdateCommand(channel, source) {
2414
- if (source === "binary") return `curl -fsSL ${DEFAULT_WEB_URL}/api/v1/cli/install.sh | ${channel === "canary" ? "SKYDIVE_CHANNEL=canary " : ""}sh`;
2415
- return `npm install -g ${name}@${distTagForChannel(channel)}`;
2416
- }
2417
- function renderUpdateNotice(opts) {
2418
- return `\nUpdate available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun ${renderUpdateCommand(opts.channel, opts.source)}\n`;
2419
- }
2420
- /**
2421
- * Whether this invocation may print the notice. Pure so it's testable; the
2422
- * inputs are raw pre-yargs argv (parsing hasn't happened when this runs) and
2423
- * stderr's TTY-ness. `--json`/`--quiet` go to stdout, and the notice goes to
2424
- * stderr — but scripts commonly capture 2>&1, so machine-readable modes
2425
- * suppress it entirely rather than risk corrupting piped output.
2426
- */
2427
- function shouldNotify(opts) {
2428
- if (!opts.stderrIsTTY) return false;
2429
- if (opts.argv.includes("--json") || opts.argv.includes("--quiet")) return false;
2430
- return true;
2431
- }
2432
-
2433
- //#endregion
2434
- //#region src/update-check/sources.ts
2435
- function resolveInstallSource() {
2436
- return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
2437
- }
2438
- const NPM_REGISTRY_URL = "https://registry.npmjs.org";
2439
- const npmDistTagsSchema = z.record(z.string(), z.unknown());
2440
- const binaryManifestSchema = z.object({ version: z.unknown().optional() });
2441
- /** npm dist-tags for the published package (`skydive-cli`). */
2442
- const npmReleaseSource = { async fetchLatestVersion(channel, { signal }) {
2443
- const response = await fetch(`${NPM_REGISTRY_URL}/-/package/${name}/dist-tags`, {
2444
- signal,
2445
- headers: { accept: "application/json" }
2446
- });
2447
- if (!response.ok) return null;
2448
- const tags = npmDistTagsSchema.safeParse(await response.json());
2449
- if (!tags.success) return null;
2450
- const version = tags.data[distTagForChannel(channel)];
2451
- return typeof version === "string" ? version : null;
2452
- } };
2453
- /**
2454
- * Release CDN (CloudFront over the releases bucket, infra: CliReleasesCdn).
2455
- * Serves the channel pointers the release workflow uploads
2456
- * (`channels/{stable,canary}.json`, cached max-age=60) alongside the
2457
- * binaries. The daily poll goes here, not to the api, so a fleet of
2458
- * installed binaries puts no load on — and takes no dependency on — the api.
2459
- */
2460
- const RELEASE_CDN_URL = "https://dl.skydive.com";
2461
- /** Channel manifest on the release CDN, for compiled binaries. The same
2462
- * document the api's channel route serves (see
2463
- * apps/anyone/api/src/routes/cli-releases.ts, which reads it from the
2464
- * bucket this CDN fronts). */
2465
- const binaryReleaseSource = { async fetchLatestVersion(channel, { signal }) {
2466
- const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
2467
- signal,
2468
- headers: { accept: "application/json" }
2469
- });
2470
- if (!response.ok) return null;
2471
- const manifest = binaryManifestSchema.safeParse(await response.json());
2472
- if (!manifest.success) return null;
2473
- return typeof manifest.data.version === "string" ? manifest.data.version : null;
2474
- } };
2475
- function releaseSourceForInstall(source) {
2476
- return source === "binary" ? binaryReleaseSource : npmReleaseSource;
2477
- }
2478
-
2479
3011
  //#endregion
2480
3012
  //#region src/update-check/index.ts
2481
3013
  /**
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
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-BhPuzBxW.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";
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-CQNnRJ0V.mjs";
3
+ import { L as saveTheme, P as resolveWebUrl, T as getSavedTheme, 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, w as getReviewStateDir, x as getConfigPath, y as DEFAULT_APP_URL } from "./print-DaPpTqxu.mjs";
4
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
5
  import { t as PortalClient } from "./client-DT9bWi1D.mjs";
6
6
  import { d as makeLineParser, f as parseDaemonMessage, l as daemonPaths, n as ensureDaemonRunning, s as LOCAL_PROTOCOL_VERSION, u as encodeLine } from "./daemon-DfMgVlYE.mjs";
@@ -5480,6 +5480,10 @@ const keybindGroups = [
5480
5480
  keys: "exit",
5481
5481
  action: "quit (type it as a message)"
5482
5482
  },
5483
+ {
5484
+ keys: "ctrl+a",
5485
+ action: "archive this conversation (composer empty; hides it for you only)"
5486
+ },
5483
5487
  {
5484
5488
  keys: "ctrl+p",
5485
5489
  action: "switch model"
@@ -5966,6 +5970,13 @@ const slashCommands = [
5966
5970
  description: "start a new conversation with this agent",
5967
5971
  action: { kind: "new-conversation" }
5968
5972
  },
5973
+ {
5974
+ name: "archive",
5975
+ aliases: [],
5976
+ argsHint: null,
5977
+ description: "archive this conversation and hide it from your list",
5978
+ action: { kind: "archive-conversation" }
5979
+ },
5969
5980
  {
5970
5981
  name: "conversation",
5971
5982
  aliases: ["conversations", "conv"],
@@ -9107,6 +9118,7 @@ function routeInput(raw) {
9107
9118
  };
9108
9119
  }
9109
9120
  case "new-conversation":
9121
+ case "archive-conversation":
9110
9122
  case "model-picker":
9111
9123
  case "theme-picker":
9112
9124
  case "status":
@@ -10248,6 +10260,41 @@ function ChatScreen({ agent, conversation }) {
10248
10260
  agent,
10249
10261
  goTo
10250
10262
  ]);
10263
+ const archiveConversation = useCallback(() => {
10264
+ if (grantPrompt || credPrompt) return;
10265
+ if (runRef.current.kind !== "idle") {
10266
+ useStore.getState().showToast({ message: "finish or cancel the run first (ctrl+c)" });
10267
+ return;
10268
+ }
10269
+ if (!conversationId) {
10270
+ useStore.getState().showToast({ message: "nothing to archive yet — send a message to start this one" });
10271
+ return;
10272
+ }
10273
+ if (!rest) return;
10274
+ rest.setConversationArchived({
10275
+ conversationId,
10276
+ archived: true
10277
+ }).then(() => {
10278
+ useStore.getState().showToast({
10279
+ variant: "success",
10280
+ message: "archived — shift+tab in the picker shows it, ctrl+a restores"
10281
+ });
10282
+ goTo({
10283
+ kind: "conversation-picker",
10284
+ agent
10285
+ });
10286
+ }).catch((err) => useStore.getState().showToast({
10287
+ variant: "error",
10288
+ message: err instanceof HttpError && err.status === 404 ? "can't archive this one — it's already gone from your list" : `couldn't archive: ${errorMessage(err)}`
10289
+ }));
10290
+ }, [
10291
+ grantPrompt,
10292
+ credPrompt,
10293
+ conversationId,
10294
+ rest,
10295
+ goTo,
10296
+ agent
10297
+ ]);
10251
10298
  /**
10252
10299
  * Leave this conversation for another screen without ending its run: drop
10253
10300
  * our own SSE subscription and nothing else — never `rest.cancelRun` — so
@@ -10406,6 +10453,9 @@ function ChatScreen({ agent, conversation }) {
10406
10453
  case "sandbox-pty":
10407
10454
  runSandboxPty();
10408
10455
  break;
10456
+ case "archive-conversation":
10457
+ archiveConversation();
10458
+ break;
10409
10459
  case "new-conversation":
10410
10460
  if (runRef.current.kind !== "idle") {
10411
10461
  useStore.getState().showToast({ message: "finish or cancel the run first (ctrl+c)" });
@@ -10507,6 +10557,7 @@ function ChatScreen({ agent, conversation }) {
10507
10557
  runLocalShell,
10508
10558
  runSandboxExec,
10509
10559
  runSandboxPty,
10560
+ archiveConversation,
10510
10561
  leaveForScreen,
10511
10562
  openConversationRef,
10512
10563
  toggleShare,
@@ -10703,6 +10754,11 @@ function ChatScreen({ agent, conversation }) {
10703
10754
  setThemePickerOpen(true);
10704
10755
  return;
10705
10756
  }
10757
+ if (key.name === "a" && key.ctrl && input.trim() === "") {
10758
+ key.preventDefault();
10759
+ archiveConversation();
10760
+ return;
10761
+ }
10706
10762
  if (key.name === "?" && !key.ctrl && !key.meta && input.trim() === "") {
10707
10763
  composerRef.current?.clear();
10708
10764
  setInput("");
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { S as getConfigPath } from "./print-CNIvmpqW.mjs";
2
+ import { x as getConfigPath } from "./print-DaPpTqxu.mjs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { err, ok } from "neverthrow";
@@ -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.2.0-beta.618";
11
+ var version$1 = "0.2.0-beta.626";
12
12
 
13
13
  //#endregion
14
14
  //#region src/auth/organization.ts
@@ -1,5 +1,5 @@
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-CNIvmpqW.mjs";
2
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-DaPpTqxu.mjs";
3
3
  import "./rest-Q5yl_b2-.mjs";
4
4
 
5
5
  export { messageGet, readStdin, resolveAgent, runPrint };
@@ -677,4 +677,4 @@ async function readStdin() {
677
677
  }
678
678
 
679
679
  //#endregion
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 };
680
+ export { resolveAppUrl as A, getPromptHistoryPath as C, getStoredApiKeyId as D, getShareMachineDefault as E, saveConfig as F, saveSession as I, saveTheme as L, resolveManagementAuth as M, resolveSession as N, getStoredApiKeyWorkspaceName as O, resolveWebUrl as P, setLastSeenVersion as R, getLastSeenVersion as S, getSavedTheme as T, API_KEY_PREFIX as _, runPrint as a, deleteConfig 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, resolveConfig as j, getUpdateCheckDisabled 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, getReviewStateDir as w, getConfigPath as x, DEFAULT_APP_URL as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.2.0-beta.618",
3
+ "version": "0.2.0-beta.626",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",