skydive-cli 0.1.0 → 0.2.0-beta.421

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,19 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ import { A as getShareMachineDefault, B as saveSession, C as DEFAULT_WEB_URL, D as getPromptHistoryPath, E as getLastSeenVersion, F as resolveConfig, H as setLastSeenVersion, I as resolveManagementAuth, L as resolveSession, M as getStoredApiKeyWorkspaceName, N as getUpdateCheckDisabled, P as resolveAppUrl, T as getConfigPath, _ as setActiveWorkspace, b as API_KEY_PREFIX, d as themes, g as listWorkspaces, h as getSessionIdentity, j as getStoredApiKeyId, m as getActiveWorkspaceId, p as ensureActiveOrganization, v as API_KEYS_URL, w as deleteConfig, x as DEFAULT_API_URL, y as API_KEY_FAMILY_PREFIX, z as saveConfig } from "./theme-CuQhvqzN.mjs";
3
+ import { n as printError, r as printTable, t as output } from "./output-B4cW10Ph.mjs";
4
+ import { n as createRestClient } from "./rest-CamHVOce.mjs";
5
+ import { i as resolveAgent } from "./print-DausK_KZ.mjs";
6
+ import { a as registerPortalDevice, c as machineIdentity, n as findThisDevice, o as revokePortalAccess, r as grantPortalAccess, t as fetchPortalDevices } from "./api-CDTKq_5Q.mjs";
7
+ import { t as SandboxStream } from "./client-CpEvH2Pq.mjs";
2
8
  import { hideBin } from "yargs/helpers";
3
9
  import yargs from "yargs";
4
- import { createInterface } from "node:readline";
10
+ import { hostname } from "node:os";
5
11
  import path from "node:path";
6
- import Conf from "conf";
7
12
  import { err, ok } from "neverthrow";
8
13
  import { z } from "zod";
9
14
  import open from "open";
10
- import { spawnSync } from "node:child_process";
15
+ import { spawn, spawnSync } from "node:child_process";
11
16
  import { createHash } from "node:crypto";
12
17
  import fs from "node:fs";
13
18
  import zlib from "node:zlib";
19
+ import semver from "semver";
14
20
 
15
21
  //#region package.json
16
- var version$1 = "0.1.0";
22
+ var name = "skydive-cli";
23
+ var version = "0.2.0-beta.421";
17
24
 
18
25
  //#endregion
19
26
  //#region src/types.ts
@@ -27,130 +34,11 @@ function isNonInteractive() {
27
34
  return NON_INTERACTIVE_ENV_VARS.some((key) => process.env[key]);
28
35
  }
29
36
 
30
- //#endregion
31
- //#region src/config.ts
32
- /** Default host for the public management API (`/v1`, API-key auth). */
33
- const DEFAULT_API_URL = "https://api.skydive.com";
34
- /**
35
- * Default origin for the interactive chat client (`skydive chat`).
36
- *
37
- * The API host that serves better-auth (`/api/auth/*`) and the internal tRPC
38
- * API (`/api/v1/trpc`) that chat streams over. We target the API host
39
- * directly (not the web front door) because chat opens a WebSocket and
40
- * authenticates with a bearer token on the upgrade request. Same host as
41
- * `DEFAULT_API_URL`; override with `--api-url` / `SKYDIVE_APP_URL` for local
42
- * dev or while the DNS record is still being provisioned.
43
- */
44
- const DEFAULT_APP_URL = "https://api.skydive.com";
45
- /** Web front door, for pages opened in the user's browser. */
46
- const DEFAULT_WEB_URL = "https://skydive.com";
47
- /**
48
- * Origin for browser-facing links (e.g. opening a conversation's web page).
49
- * The app origin is the API host, which serves no web UI in production, so
50
- * map the default to the web front door. Overridden origins (local dev,
51
- * previews) serve both and pass through unchanged.
52
- */
53
- function resolveWebUrl(appUrl) {
54
- return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
55
- }
56
- /** Prefix on every Skydive API key. Kept in sync with the API's
57
- * `API_KEY_PREFIX` (`apps/anyone/api/src/lib/api-key.ts`); the CLI is a
58
- * standalone published package so it can't import the backend constant. */
59
- const API_KEY_PREFIX = "sky_live_";
60
- /** Where users mint and copy API keys. Shown in the login prompt. */
61
- const API_KEYS_URL = "skydive.com/account";
62
- const store = new Conf({
63
- projectName: process.env["SKYDIVE_CONFIG_NAME"] ?? "skydive",
64
- projectSuffix: "",
65
- configFileMode: 384
66
- });
67
- function resolveConfig(opts) {
68
- const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
69
- const apiUrl = process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL;
70
- if (!apiKey) return err({ message: "Not authenticated. Run `skydive auth login` first." });
71
- return ok({
72
- apiKey,
73
- apiUrl
74
- });
75
- }
76
- /**
77
- * Resolve the bearer credential for the management API (`agents` / `keys` /
78
- * `secrets`). Prefers an API key (`SKYDIVE_API_KEY` env, then stored), and
79
- * falls back to the `--web` chat session token: the server's `/v1` gate routes
80
- * any non-`sky_` bearer through the signed-in session, so a device login alone
81
- * is enough to run management commands — no separate API key required.
82
- */
83
- function resolveManagementAuth(opts) {
84
- const apiUrl = process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL;
85
- const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
86
- if (apiKey) return ok({
87
- token: apiKey,
88
- apiUrl,
89
- kind: "api-key"
90
- });
91
- const sessionToken = process.env["SKYDIVE_SESSION_TOKEN"] ?? store.get("sessionToken");
92
- if (sessionToken) return ok({
93
- token: sessionToken,
94
- apiUrl,
95
- kind: "session"
96
- });
97
- return err({ message: "Not authenticated. Run `skydive auth login` (API key) or `skydive auth login --web`." });
98
- }
99
- function saveConfig(config) {
100
- store.set("apiKey", config.apiKey);
101
- store.set("apiUrl", config.apiUrl);
102
- }
103
- function deleteConfig() {
104
- store.clear();
105
- }
106
- function getConfigPath() {
107
- return store.path;
108
- }
109
- /**
110
- * Where the chat TUI persists its prompt history (up-arrow recall). Kept
111
- * beside the config file so all CLI state lives in one directory.
112
- */
113
- function getPromptHistoryPath() {
114
- return path.join(path.dirname(store.path), "prompt-history.jsonl");
115
- }
116
- /**
117
- * Resolve the chat/auth origin. Precedence: `SKYDIVE_APP_URL` env > explicit
118
- * `--api-url` style override > stored value > `SKYDIVE_API_URL` env >
119
- * `DEFAULT_APP_URL`.
120
- *
121
- * The `SKYDIVE_API_URL` fallback matters for previews: the device/`--web`
122
- * flow and chat hit the same api service as the management API, so pointing
123
- * `SKYDIVE_API_URL` at a preview stack is enough — you don't also have to set
124
- * `SKYDIVE_APP_URL`. Otherwise auth would silently fall through to prod
125
- * (`DEFAULT_APP_URL`) and hand back a prod verification URL.
126
- */
127
- function resolveAppUrl(opts) {
128
- return process.env["SKYDIVE_APP_URL"] ?? opts.appUrl ?? store.get("appUrl") ?? process.env["SKYDIVE_API_URL"] ?? DEFAULT_APP_URL;
129
- }
130
- function resolveSession(opts) {
131
- const sessionToken = process.env["SKYDIVE_SESSION_TOKEN"] ?? store.get("sessionToken");
132
- const appUrl = resolveAppUrl(opts);
133
- if (!sessionToken) return err({ message: "Not signed in for chat. Run `skydive chat` to sign in." });
134
- return ok({
135
- sessionToken,
136
- appUrl
137
- });
138
- }
139
- function saveSession(session) {
140
- store.set("sessionToken", session.sessionToken);
141
- store.set("sessionObtainedAt", (/* @__PURE__ */ new Date()).toISOString());
142
- store.set("appUrl", session.appUrl);
143
- }
144
- function getSavedTheme(mode) {
145
- return store.get(mode === "dark" ? "themeDark" : "themeLight");
146
- }
147
- function saveTheme(mode, themeId) {
148
- store.set(mode === "dark" ? "themeDark" : "themeLight", themeId);
149
- }
150
-
151
37
  //#endregion
152
38
  //#region src/api-client.ts
153
- const USER_AGENT = `skydive-cli/${version$1}`;
39
+ const USER_AGENT = `skydive-cli/${version}`;
40
+ /** Largest page `/v1/agents` will return, enforced server-side. */
41
+ const MAX_AGENT_PAGE = 100;
154
42
  const AgentSchema = z.object({
155
43
  id: z.string(),
156
44
  name: z.string(),
@@ -179,20 +67,38 @@ const SetSecretResponseSchema = z.object({ key: z.string() });
179
67
  var SkydiveApiClient = class {
180
68
  baseUrl;
181
69
  token;
70
+ authKind;
182
71
  constructor(config) {
183
72
  this.baseUrl = `${config.apiUrl.replace(/\/$/, "")}/v1`;
184
73
  this.token = config.token;
185
- }
74
+ this.authKind = config.kind;
75
+ }
76
+ /**
77
+ * List agents in the caller's workspace, following the server's cursor
78
+ * until `limit` is satisfied. The endpoint caps one page at
79
+ * {@link MAX_AGENT_PAGE}, so a larger limit takes several requests —
80
+ * callers just ask for what they want. `hasMore` reports whether the
81
+ * roster continues past what was returned.
82
+ */
186
83
  async listAgents(params) {
187
- const query = new URLSearchParams();
188
- if (params.scope) query.set("scope", params.scope);
189
- if (params.limit) query.set("limit", String(params.limit));
190
- if (params.cursor) query.set("cursor", params.cursor);
191
- const qs = query.toString();
192
- return this.request({
193
- method: "GET",
194
- path: `/agents${qs ? `?${qs}` : ""}`,
195
- schema: ListAgentsResponseSchema
84
+ const agents = [];
85
+ let cursor = null;
86
+ do {
87
+ const query = new URLSearchParams({ limit: String(Math.min(MAX_AGENT_PAGE, params.limit - agents.length)) });
88
+ if (params.scope) query.set("scope", params.scope);
89
+ if (cursor) query.set("cursor", cursor);
90
+ const page = await this.request({
91
+ method: "GET",
92
+ path: `/agents?${query.toString()}`,
93
+ schema: ListAgentsResponseSchema
94
+ });
95
+ if (page.isErr()) return err(page.error);
96
+ agents.push(...page.value.agents);
97
+ cursor = page.value.nextCursor;
98
+ } while (cursor && agents.length < params.limit);
99
+ return ok({
100
+ agents,
101
+ hasMore: cursor !== null
196
102
  });
197
103
  }
198
104
  async getAgent(id) {
@@ -279,6 +185,7 @@ var SkydiveApiClient = class {
279
185
  if (body.error && typeof body.error === "string") message = body.error;
280
186
  else if (body.error?.message) message = body.error.message;
281
187
  } catch {}
188
+ if (response.status === 401 && this.authKind === "session") message = `${message} — your chat session may have expired. Run \`skydive auth login\`.`;
282
189
  return err({
283
190
  message,
284
191
  status: response.status
@@ -318,7 +225,8 @@ const tokenSuccessSchema = z.object({
318
225
  access_token: z.string(),
319
226
  token_type: z.string().optional(),
320
227
  expires_in: z.number().int().optional(),
321
- refresh_token: z.string().optional()
228
+ refresh_token: z.string().optional(),
229
+ scope: z.string().optional()
322
230
  });
323
231
  const tokenErrorSchema = z.object({
324
232
  error: z.enum([
@@ -333,6 +241,18 @@ const tokenErrorSchema = z.object({
333
241
  error_description: z.string().optional()
334
242
  });
335
243
  const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
244
+ const WORKSPACE_SCOPE_PREFIX = "workspace:";
245
+ /**
246
+ * Extract the workspace (organization) id the user chose on the /device
247
+ * approval page from the token response's `scope`. Scope is space-delimited
248
+ * per RFC 6749; anything else in it is ignored. Returns null when no
249
+ * workspace was chosen (older server, or the picker didn't load).
250
+ */
251
+ function parseWorkspaceScope(scope) {
252
+ if (!scope) return null;
253
+ const entry = scope.split(" ").find((part) => part.startsWith(WORKSPACE_SCOPE_PREFIX));
254
+ return entry ? entry.slice(10) || null : null;
255
+ }
336
256
  async function requestDeviceCode({ appUrl, signal }) {
337
257
  const res = await fetch(`${appUrl}/api/auth/device/code`, {
338
258
  method: "POST",
@@ -372,7 +292,8 @@ async function pollDeviceToken({ appUrl, deviceCode, currentIntervalMs, signal }
372
292
  return {
373
293
  kind: "success",
374
294
  accessToken: parsed.data.access_token,
375
- refreshToken: parsed.data.refresh_token ?? null
295
+ refreshToken: parsed.data.refresh_token ?? null,
296
+ workspaceId: parseWorkspaceScope(parsed.data.scope)
376
297
  };
377
298
  }
378
299
  const parsed = tokenErrorSchema.safeParse(body);
@@ -398,83 +319,6 @@ async function pollDeviceToken({ appUrl, deviceCode, currentIntervalMs, signal }
398
319
  }
399
320
  }
400
321
 
401
- //#endregion
402
- //#region src/auth/organization.ts
403
- const workspaceSchema = z.object({
404
- id: z.string(),
405
- name: z.string(),
406
- slug: z.string()
407
- });
408
- function authHeaders(sessionToken) {
409
- return { authorization: `Bearer ${sessionToken}` };
410
- }
411
- async function listWorkspaces({ appUrl, sessionToken }) {
412
- try {
413
- const res = await fetch(`${appUrl}/api/auth/organization/list`, { headers: authHeaders(sessionToken) });
414
- if (!res.ok) return err({ message: `failed to list workspaces (${res.status})` });
415
- const parsed = z.array(workspaceSchema).safeParse(await res.json());
416
- if (!parsed.success) return err({ message: "unexpected workspace list response" });
417
- return ok(parsed.data);
418
- } catch (e) {
419
- return err({ message: e instanceof Error ? e.message : String(e) });
420
- }
421
- }
422
- /** The workspace bound to the current session, or `null` if none is set. */
423
- async function getActiveWorkspaceId({ appUrl, sessionToken }) {
424
- try {
425
- const res = await fetch(`${appUrl}/api/auth/get-session?disableCookieCache=true`, { headers: authHeaders(sessionToken) });
426
- if (!res.ok) return err({ message: `failed to read session (${res.status})` });
427
- const parsed = z.object({ session: z.object({ activeOrganizationId: z.string().nullable().optional() }).nullable().optional() }).nullable().safeParse(await res.json());
428
- if (!parsed.success) return err({ message: "unexpected session response" });
429
- return ok(parsed.data?.session?.activeOrganizationId ?? null);
430
- } catch (e) {
431
- return err({ message: e instanceof Error ? e.message : String(e) });
432
- }
433
- }
434
- async function setActiveWorkspace({ appUrl, sessionToken, organizationId }) {
435
- try {
436
- const res = await fetch(`${appUrl}/api/auth/organization/set-active`, {
437
- method: "POST",
438
- headers: {
439
- ...authHeaders(sessionToken),
440
- "content-type": "application/json"
441
- },
442
- body: JSON.stringify({ organizationId })
443
- });
444
- if (!res.ok) return err({ message: `failed to set active workspace (${res.status})` });
445
- return ok(void 0);
446
- } catch (e) {
447
- return err({ message: e instanceof Error ? e.message : String(e) });
448
- }
449
- }
450
- /**
451
- * Ensures the session has an active workspace. The device flow issues a
452
- * session without one (unlike a normal web sign-in, which sets it), so the
453
- * internal API rejects every request with "no active organization" until we
454
- * set it. Picks the account's first workspace by join order — run `skydive
455
- * workspace list` + `skydive workspace switch` afterward if that's the wrong
456
- * one (e.g. a personal workspace joined before a shared team workspace).
457
- */
458
- async function ensureActiveOrganization({ appUrl, sessionToken }) {
459
- const workspaces = await listWorkspaces({
460
- appUrl,
461
- sessionToken
462
- });
463
- if (workspaces.isErr()) return err(workspaces.error);
464
- const workspace = workspaces.value[0];
465
- if (!workspace) return err({ message: "your account has no organization yet" });
466
- const setResult = await setActiveWorkspace({
467
- appUrl,
468
- sessionToken,
469
- organizationId: workspace.id
470
- });
471
- if (setResult.isErr()) return err(setResult.error);
472
- return ok({
473
- organizationId: workspace.id,
474
- name: workspace.name
475
- });
476
- }
477
-
478
322
  //#endregion
479
323
  //#region src/auth/device-login.ts
480
324
  const DEFAULT_INTERVAL_MS = 5e3;
@@ -509,11 +353,19 @@ async function loginWithDevice({ appUrl, openBrowser = true }) {
509
353
  });
510
354
  switch (result.kind) {
511
355
  case "success": {
512
- const org = await ensureActiveOrganization({
356
+ let activated = false;
357
+ if (result.workspaceId) activated = (await setActiveWorkspace({
513
358
  appUrl,
514
- sessionToken: result.accessToken
515
- });
516
- if (org.isErr()) return err({ message: org.error.message });
359
+ sessionToken: result.accessToken,
360
+ organizationId: result.workspaceId
361
+ })).isOk();
362
+ if (!activated) {
363
+ const org = await ensureActiveOrganization({
364
+ appUrl,
365
+ sessionToken: result.accessToken
366
+ });
367
+ if (org.isErr()) return err({ message: org.error.message });
368
+ }
517
369
  saveSession({
518
370
  sessionToken: result.accessToken,
519
371
  appUrl
@@ -546,131 +398,266 @@ function sleep(ms) {
546
398
  }
547
399
 
548
400
  //#endregion
549
- //#region src/output.ts
550
- function output(argv, data) {
551
- if (argv.json) {
552
- console.log(JSON.stringify(data, null, 2));
553
- return;
554
- }
555
- if (typeof data === "string") {
556
- console.log(data);
557
- return;
401
+ //#region src/auth/mint-key.ts
402
+ const mintedKeySchema = z.object({
403
+ id: z.string(),
404
+ name: z.string(),
405
+ prefix: z.string(),
406
+ key: z.string()
407
+ });
408
+ /**
409
+ * Mint an org-scoped (account-level) API key off a fresh device-flow session,
410
+ * so `auth login` ends with a durable machine credential the way
411
+ * `doppler login` does. Calls the same internal endpoint the web settings page
412
+ * uses; no `agentId` is sent, so the key is account-level, not agent-bound.
413
+ *
414
+ * Scopes are `use` + `edit` because the management commands the key powers
415
+ * (`agents create`, `keys create`, `secrets set`) are edit-tier operations.
416
+ */
417
+ async function mintCliApiKey({ appUrl, sessionToken, name }) {
418
+ try {
419
+ const res = await fetch(`${appUrl}/api/v1/api-keys`, {
420
+ method: "POST",
421
+ headers: {
422
+ authorization: `Bearer ${sessionToken}`,
423
+ "content-type": "application/json"
424
+ },
425
+ body: JSON.stringify({
426
+ name,
427
+ scopes: ["use", "edit"]
428
+ })
429
+ });
430
+ if (!res.ok) return err({ message: `failed to mint API key (${res.status})` });
431
+ const parsed = mintedKeySchema.safeParse(await res.json());
432
+ if (!parsed.success) return err({ message: "unexpected mint response" });
433
+ return ok(parsed.data);
434
+ } catch (e) {
435
+ return err({ message: e instanceof Error ? e.message : String(e) });
558
436
  }
559
- console.log(JSON.stringify(data, null, 2));
560
437
  }
561
- function printTable(headers, rows) {
562
- const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
563
- const pad = (s, w) => s.padEnd(w);
564
- const line = (cells) => cells.map((c, i) => pad(c, widths[i] ?? 0)).join(" ");
565
- console.log(line(headers));
566
- console.log(widths.map((w) => "-".repeat(w)).join(" "));
567
- for (const row of rows) console.log(line(row));
438
+ const keyListSchema = z.object({ keys: z.array(z.object({
439
+ id: z.string(),
440
+ name: z.string(),
441
+ agentId: z.string().nullish()
442
+ })) });
443
+ /**
444
+ * Revoke every active account-level key with the given name. Login calls this
445
+ * before minting so repeated logins from the same machine + workspace replace
446
+ * their key instead of piling up indistinguishable rows (`logout`'s revoke is
447
+ * best-effort, and login-without-logout is the normal case — a wiped
448
+ * container, an expired session). Agent-scoped keys are never touched even on
449
+ * a name collision.
450
+ */
451
+ async function revokeKeysByName({ appUrl, sessionToken, name }) {
452
+ try {
453
+ const res = await fetch(`${appUrl}/api/v1/api-keys`, { headers: { authorization: `Bearer ${sessionToken}` } });
454
+ if (!res.ok) return err({ message: `failed to list API keys (${res.status})` });
455
+ const parsed = keyListSchema.safeParse(await res.json());
456
+ if (!parsed.success) return err({ message: "unexpected key list response" });
457
+ const matches = parsed.data.keys.filter((key) => key.name === name && !key.agentId);
458
+ const failures = (await Promise.all(matches.map((key) => revokeApiKey({
459
+ appUrl,
460
+ token: sessionToken,
461
+ id: key.id
462
+ })))).filter((result) => result.isErr());
463
+ if (failures.length > 0) return err({ message: `failed to revoke ${failures.length} of ${matches.length} existing keys` });
464
+ return ok(matches.length);
465
+ } catch (e) {
466
+ return err({ message: e instanceof Error ? e.message : String(e) });
467
+ }
568
468
  }
569
- function printError(message) {
570
- console.error(`Error: ${message}`);
469
+ /**
470
+ * Revoke an API key by id. Used by `auth logout` to clean up the key that
471
+ * login auto-minted, so signing out on a machine doesn't leave a live
472
+ * credential behind. `token` may be a session bearer or an API key — the
473
+ * server accepts both on this route.
474
+ */
475
+ async function revokeApiKey({ appUrl, token, id }) {
476
+ try {
477
+ const res = await fetch(`${appUrl}/api/v1/api-keys/${id}`, {
478
+ method: "DELETE",
479
+ headers: { authorization: `Bearer ${token}` }
480
+ });
481
+ if (!res.ok && res.status !== 404) return err({ message: `failed to revoke API key (${res.status})` });
482
+ return ok(void 0);
483
+ } catch (e) {
484
+ return err({ message: e instanceof Error ? e.message : String(e) });
485
+ }
571
486
  }
572
487
 
573
488
  //#endregion
574
489
  //#region src/commands/auth.ts
575
490
  const loginCommand = {
576
491
  command: "login",
577
- describe: "Authenticate with a Skydive API key (or --web for chat)",
492
+ describe: "Sign in via the browser (use --api-key for CI / headless)",
578
493
  builder: (y) => y.option("api-key", {
579
494
  type: "string",
580
- describe: `API key (${API_KEY_PREFIX}...)`
495
+ describe: `Skip the browser and authenticate with an existing API key (${API_KEY_PREFIX}...)`
581
496
  }).option("web", {
582
497
  type: "boolean",
583
498
  default: false,
584
- describe: "Sign in for `skydive chat` via the browser (device flow) instead of an API key"
499
+ hidden: true,
500
+ describe: "Deprecated: browser sign-in is now the default"
585
501
  }),
586
502
  handler: async (argv) => {
587
- if (argv.web) {
588
- await runWebLogin(argv);
503
+ if (argv["api-key"]) {
504
+ await runApiKeyLogin(argv, argv["api-key"]);
589
505
  return;
590
506
  }
591
- let apiKey = argv["api-key"] ?? process.env["SKYDIVE_API_KEY"];
592
- if (!apiKey) {
593
- const rl = createInterface({
594
- input: process.stdin,
595
- output: process.stderr
596
- });
597
- apiKey = await new Promise((resolve) => {
598
- rl.question(`Enter your API key (from ${API_KEYS_URL}): `, (answer) => {
599
- rl.close();
600
- resolve(answer.trim());
601
- });
602
- });
603
- }
604
- if (!apiKey) {
605
- printError("No API key provided.");
606
- process.exit(1);
607
- }
608
- if (!apiKey.startsWith(API_KEY_PREFIX)) {
609
- printError(`API key must start with ${API_KEY_PREFIX}`);
610
- process.exit(1);
611
- }
612
- const apiUrl = argv["api-url"] ?? DEFAULT_API_URL;
613
- const result = await new SkydiveApiClient({
614
- token: apiKey,
615
- apiUrl
616
- }).listAgents({ limit: 1 });
617
- if (result.isErr()) {
618
- printError(`Invalid API key or unreachable server: ${result.error.message}`);
619
- process.exit(1);
507
+ if (isNonInteractive()) {
508
+ const envKey = process.env["SKYDIVE_API_KEY"];
509
+ if (envKey) {
510
+ await runApiKeyLogin(argv, envKey);
511
+ return;
512
+ }
620
513
  }
621
- saveConfig({
622
- apiKey,
623
- apiUrl
624
- });
625
- if (argv.json) output(argv, {
514
+ await runBrowserLogin(argv);
515
+ }
516
+ };
517
+ /**
518
+ * Browser sign-in, shaped like `doppler login`: open the approval page with a
519
+ * short code, wait for the user to approve, then automatically mint an API
520
+ * key for this machine. One flow yields both credentials — the session (chat,
521
+ * workspace-following management calls) and a durable `sky_live_…` key that
522
+ * keeps headless/management use working after the session expires.
523
+ *
524
+ * Deliberately no TTY requirement: the flow only prints the verification URL
525
+ * + code and polls, so it also works from a non-interactive shell — an agent
526
+ * driving the CLI relays the URL to its human, who approves in any browser
527
+ * (the auto-open is best-effort). The cost is that an unattended run with no
528
+ * key configured waits out the device code's expiry instead of failing fast.
529
+ */
530
+ async function runBrowserLogin(argv) {
531
+ const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
532
+ const login = await loginWithDevice({ appUrl });
533
+ if (login.isErr()) {
534
+ printError(login.error.message);
535
+ process.exit(1);
536
+ }
537
+ const { sessionToken } = login.value;
538
+ const identity = (await getSessionIdentity({
539
+ appUrl,
540
+ sessionToken
541
+ })).unwrapOr(null);
542
+ const keyName = identity?.activeWorkspaceName ? `CLI (${hostname()}) — ${identity.activeWorkspaceName}` : `CLI (${hostname()})`;
543
+ const replaced = await revokeKeysByName({
544
+ appUrl,
545
+ sessionToken,
546
+ name: keyName
547
+ });
548
+ if (replaced.isErr()) process.stderr.write(`Warning: could not revoke this machine's previous API key (${replaced.error.message}); minting a new one anyway.\n`);
549
+ const minted = await mintCliApiKey({
550
+ appUrl,
551
+ sessionToken,
552
+ name: keyName
553
+ });
554
+ if (minted.isOk()) saveConfig({
555
+ apiKey: minted.value.key,
556
+ apiUrl: appUrl,
557
+ apiKeyId: minted.value.id,
558
+ workspaceName: identity?.activeWorkspaceName ?? null
559
+ });
560
+ else process.stderr.write(`Warning: signed in, but could not mint an API key (${minted.error.message}). Management commands will use your session; run \`skydive auth login\` again to retry.
561
+ `);
562
+ if (argv.json) {
563
+ output(argv, {
626
564
  authenticated: true,
627
- prefix: apiKey.slice(0, 12),
565
+ mode: "browser",
566
+ appUrl,
567
+ prefix: minted.isOk() ? minted.value.prefix : null,
568
+ email: identity?.email ?? null,
569
+ workspaceName: identity?.activeWorkspaceName ?? null,
628
570
  configPath: getConfigPath()
629
571
  });
630
- else if (!argv.quiet) {
631
- console.log(`Authenticated successfully.`);
632
- console.log(` Key: ${apiKey.slice(0, 12)}...`);
633
- console.log(` Config: ${getConfigPath()}`);
634
- }
572
+ return;
635
573
  }
636
- };
637
- async function runWebLogin(argv) {
638
- if (isNonInteractive()) {
639
- printError("`auth login --web` needs an interactive terminal. Set SKYDIVE_SESSION_TOKEN for non-interactive use.");
574
+ if (argv.quiet) return;
575
+ const who = identity?.name ?? identity?.email;
576
+ console.log(who ? `Welcome, ${who}!` : "Signed in.");
577
+ if (identity?.activeWorkspaceName) console.log(` Workspace: ${identity.activeWorkspaceName}`);
578
+ if (minted.isOk()) console.log(` API key: ${minted.value.prefix}… (${minted.value.name})`);
579
+ console.log(` Config: ${getConfigPath()}`);
580
+ }
581
+ /** CI / headless path: validate and persist an existing key, no browser. */
582
+ async function runApiKeyLogin(argv, apiKey) {
583
+ if (!apiKey.startsWith(API_KEY_FAMILY_PREFIX)) {
584
+ printError(`API key must start with ${API_KEY_FAMILY_PREFIX}`);
640
585
  process.exit(1);
641
586
  }
642
- const result = await loginWithDevice({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
587
+ const apiUrl = argv["api-url"] ?? DEFAULT_API_URL;
588
+ const result = await new SkydiveApiClient({
589
+ token: apiKey,
590
+ apiUrl,
591
+ kind: "api-key"
592
+ }).listAgents({
593
+ limit: 1,
594
+ scope: null
595
+ });
643
596
  if (result.isErr()) {
644
- printError(result.error.message);
597
+ printError(`Invalid API key or unreachable server: ${result.error.message}`);
645
598
  process.exit(1);
646
599
  }
600
+ saveConfig({
601
+ apiKey,
602
+ apiUrl,
603
+ apiKeyId: null,
604
+ workspaceName: null
605
+ });
647
606
  if (argv.json) output(argv, {
648
607
  authenticated: true,
649
- mode: "session",
650
- appUrl: result.value.appUrl,
608
+ prefix: apiKey.slice(0, 12),
651
609
  configPath: getConfigPath()
652
610
  });
653
611
  else if (!argv.quiet) {
654
- console.log("Signed in for chat.");
655
- console.log(` App: ${result.value.appUrl}`);
612
+ console.log(`Authenticated successfully.`);
613
+ console.log(` Key: ${apiKey.slice(0, 12)}...`);
656
614
  console.log(` Config: ${getConfigPath()}`);
657
615
  }
658
616
  }
659
617
  const logoutCommand = {
660
618
  command: "logout",
661
- describe: "Clear stored credentials (API key and chat session)",
619
+ describe: "Revoke the auto-minted API key and clear stored credentials",
662
620
  handler: async (argv) => {
621
+ const keyId = getStoredApiKeyId();
622
+ let keyRevoked = false;
623
+ if (keyId) {
624
+ const session = resolveSession({ appUrl: argv["api-url"] });
625
+ const config = resolveConfig({ apiUrl: argv["api-url"] });
626
+ const credential = session.isOk() ? {
627
+ appUrl: session.value.appUrl,
628
+ token: session.value.sessionToken
629
+ } : config.isOk() ? {
630
+ appUrl: config.value.apiUrl,
631
+ token: config.value.apiKey
632
+ } : null;
633
+ if (credential) {
634
+ const revoked = await revokeApiKey({
635
+ ...credential,
636
+ id: keyId
637
+ });
638
+ keyRevoked = revoked.isOk();
639
+ if (revoked.isErr()) process.stderr.write(`Warning: could not revoke this machine's API key (${revoked.error.message}). Revoke it manually at ${API_KEYS_URL}.\n`);
640
+ }
641
+ }
663
642
  deleteConfig();
664
- if (argv.json) output(argv, { authenticated: false });
665
- else if (!argv.quiet) console.log("Logged out.");
643
+ if (argv.json) output(argv, {
644
+ authenticated: false,
645
+ keyRevoked
646
+ });
647
+ else if (!argv.quiet) console.log(keyRevoked ? "Logged out. API key revoked." : "Logged out.");
666
648
  }
667
649
  };
668
- const statusCommand = {
650
+ const statusCommand$1 = {
669
651
  command: "status",
670
652
  describe: "Show current authentication state",
671
653
  handler: async (argv) => {
672
654
  const apiKey = resolveConfig({ apiUrl: argv["api-url"] });
673
655
  const session = resolveSession({});
656
+ const management = resolveManagementAuth({ apiUrl: argv["api-url"] });
657
+ const identity = session.isOk() ? (await getSessionIdentity({
658
+ appUrl: session.value.appUrl,
659
+ sessionToken: session.value.sessionToken
660
+ })).unwrapOr(null) : null;
674
661
  if (argv.json) {
675
662
  output(argv, {
676
663
  authenticated: apiKey.isOk(),
@@ -678,48 +665,100 @@ const statusCommand = {
678
665
  apiUrl: apiKey.isOk() ? apiKey.value.apiUrl : null,
679
666
  chat: {
680
667
  authenticated: session.isOk(),
681
- appUrl: session.isOk() ? session.value.appUrl : null
668
+ appUrl: session.isOk() ? session.value.appUrl : null,
669
+ email: identity?.email ?? null,
670
+ name: identity?.name ?? null,
671
+ workspaceId: identity?.activeWorkspaceId ?? null,
672
+ workspaceName: identity?.activeWorkspaceName ?? null
682
673
  },
674
+ managementAuth: management.isOk() ? management.value.kind : null,
675
+ keyWorkspaceName: getStoredApiKeyWorkspaceName(),
683
676
  configPath: getConfigPath()
684
677
  });
685
678
  return;
686
679
  }
687
680
  if (apiKey.isErr() && session.isErr()) {
688
- console.log("Not authenticated. Run `skydive auth login` (API key) or `skydive auth login --web` (chat).");
681
+ console.log("Not authenticated. Run `skydive auth login`.");
689
682
  return;
690
683
  }
691
684
  if (apiKey.isOk()) {
692
685
  console.log("API key:");
693
686
  console.log(` Key: ${apiKey.value.apiKey.slice(0, 12)}...`);
694
687
  console.log(` API: ${apiKey.value.apiUrl}`);
688
+ const pinned = getStoredApiKeyWorkspaceName();
689
+ if (pinned) console.log(` Space: ${pinned} (the key always acts here)`);
695
690
  } else console.log("API key: not configured.");
696
691
  if (session.isOk()) {
697
692
  console.log("Chat session:");
698
693
  console.log(` App: ${session.value.appUrl}`);
694
+ if (identity?.email || identity?.name) {
695
+ const who = identity.email ?? identity.name;
696
+ const suffix = identity.email && identity.name ? ` (${identity.name})` : "";
697
+ console.log(` User: ${who}${suffix}`);
698
+ }
699
+ if (identity?.activeWorkspaceName || identity?.activeWorkspaceId) {
700
+ const ws = identity.activeWorkspaceName ?? identity.activeWorkspaceId;
701
+ console.log(` Space: ${ws}`);
702
+ }
699
703
  } else console.log("Chat session: not signed in.");
704
+ if (management.isOk()) {
705
+ const via = management.value.kind === "session" ? "chat session (follows `workspace switch`)" : management.value.pinnedWorkspaceName ? `API key (always acts in "${management.value.pinnedWorkspaceName}")` : "API key (always acts in the workspace it was created in)";
706
+ console.log(`Management commands use: ${via}`);
707
+ }
700
708
  console.log(`Config: ${getConfigPath()}`);
701
709
  }
702
710
  };
703
711
  const authCommand = {
704
712
  command: "auth",
705
713
  describe: "Manage authentication",
706
- builder: (y) => y.command(loginCommand).command(logoutCommand).command(statusCommand).demandCommand(1, "Specify a subcommand: login, logout, status"),
714
+ builder: (y) => y.command(loginCommand).command(logoutCommand).command(statusCommand$1).demandCommand(1, "Specify a subcommand: login, logout, status"),
707
715
  handler: () => {}
708
716
  };
709
717
 
710
718
  //#endregion
711
- //#region src/commands/agents.ts
712
- function requireClient$2(argv) {
713
- const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
714
- if (result.isErr()) {
715
- printError(result.error.message);
719
+ //#region src/commands/session.ts
720
+ /**
721
+ * Resolve the signed-in chat session or exit with a friendly hint. Shared by
722
+ * every command that talks to the authenticated REST API so the
723
+ * resolve-or-exit block isn't copy-pasted per command.
724
+ */
725
+ function requireSession(argv) {
726
+ const session = resolveSession({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
727
+ if (session.isErr()) {
728
+ printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
716
729
  process.exit(1);
717
730
  }
718
- return new SkydiveApiClient(result.value);
731
+ return session.value;
719
732
  }
720
- const listCommand$3 = {
733
+ /**
734
+ * Resolve the management-API credential (see
735
+ * {@link resolveManagementAuth}) or exit with a friendly hint, then build the
736
+ * `/v1` client. Shared by `agents`, `keys` and `secrets`.
737
+ */
738
+ function requireManagementClient(argv) {
739
+ const auth = resolveManagementAuth({ apiUrl: argv["api-url"] });
740
+ if (auth.isErr()) {
741
+ printError(auth.error.message);
742
+ process.exit(1);
743
+ }
744
+ if (auth.value.kind === "api-key" && auth.value.pinnedWorkspaceName) process.stderr.write(`Using this machine's API key, which always acts in the "${auth.value.pinnedWorkspaceName}" workspace (even after \`workspace switch\`). Run \`skydive auth login\` to change that.
745
+ `);
746
+ return new SkydiveApiClient(auth.value);
747
+ }
748
+ /** Resolve the session (see {@link requireSession}) and build a REST client. */
749
+ function requireRestClient(argv) {
750
+ const session = requireSession(argv);
751
+ return createRestClient({
752
+ appUrl: session.appUrl,
753
+ sessionToken: session.sessionToken
754
+ });
755
+ }
756
+
757
+ //#endregion
758
+ //#region src/commands/agents.ts
759
+ const listCommand$4 = {
721
760
  command: "list",
722
- describe: "List agents",
761
+ describe: "List agents in the active workspace",
723
762
  builder: (y) => y.option("limit", {
724
763
  type: "number",
725
764
  default: 20,
@@ -730,15 +769,15 @@ const listCommand$3 = {
730
769
  describe: "Filter scope"
731
770
  }),
732
771
  handler: async (argv) => {
733
- const result = await requireClient$2(argv).listAgents({
772
+ const result = await requireManagementClient(argv).listAgents({
734
773
  limit: argv.limit,
735
- scope: argv.scope
774
+ scope: argv.scope ?? null
736
775
  });
737
776
  if (result.isErr()) {
738
777
  printError(result.error.message);
739
778
  process.exit(1);
740
779
  }
741
- const { agents } = result.value;
780
+ const { agents, hasMore } = result.value;
742
781
  if (argv.json) {
743
782
  output(argv, agents);
744
783
  return;
@@ -747,18 +786,52 @@ const listCommand$3 = {
747
786
  console.log("No agents found.");
748
787
  return;
749
788
  }
750
- printTable([
789
+ const { headers, rows } = buildAgentTable(agents);
790
+ printTable(headers, rows);
791
+ if (hasMore && !argv.quiet) console.log(`\nShowing the ${agents.length} newest. Raise --limit to see more.`);
792
+ }
793
+ };
794
+ const DESCRIPTION_MAX = 48;
795
+ /**
796
+ * Build the `agents list` table. A Description column is added only when at
797
+ * least one agent actually has a description, so an all-empty column doesn't
798
+ * add noise for accounts that never set them. Descriptions can be long, so
799
+ * they are truncated to keep one verbose agent from blowing out the width.
800
+ */
801
+ function buildAgentTable(agents) {
802
+ if (agents.some((a) => (a.description ?? "").trim().length > 0)) return {
803
+ headers: [
751
804
  "Name",
805
+ "Description",
752
806
  "URL",
753
807
  "Model"
754
- ], agents.map((a) => [
808
+ ],
809
+ rows: agents.map((a) => [
755
810
  a.name,
811
+ truncate(a.description ?? "", DESCRIPTION_MAX),
756
812
  a.url ?? "-",
757
813
  a.model ?? "default"
758
- ]));
759
- }
760
- };
761
- const getCommand = {
814
+ ])
815
+ };
816
+ return {
817
+ headers: [
818
+ "Name",
819
+ "URL",
820
+ "Model"
821
+ ],
822
+ rows: agents.map((a) => [
823
+ a.name,
824
+ a.url ?? "-",
825
+ a.model ?? "default"
826
+ ])
827
+ };
828
+ }
829
+ function truncate(value, max) {
830
+ const trimmed = value.trim();
831
+ if (trimmed.length <= max) return trimmed || "-";
832
+ return `${trimmed.slice(0, max - 1)}\u2026`;
833
+ }
834
+ const getCommand$1 = {
762
835
  command: "get <id>",
763
836
  describe: "Get agent details",
764
837
  builder: (y) => y.positional("id", {
@@ -767,7 +840,7 @@ const getCommand = {
767
840
  describe: "Agent ID"
768
841
  }),
769
842
  handler: async (argv) => {
770
- const result = await requireClient$2(argv).getAgent(argv.id);
843
+ const result = await requireManagementClient(argv).getAgent(argv.id);
771
844
  if (result.isErr()) {
772
845
  printError(result.error.message);
773
846
  process.exit(1);
@@ -801,7 +874,7 @@ const createCommand$1 = {
801
874
  describe: "Model to use"
802
875
  }),
803
876
  handler: async (argv) => {
804
- const result = await requireClient$2(argv).createAgent({
877
+ const result = await requireManagementClient(argv).createAgent({
805
878
  name: argv.name,
806
879
  model: argv.model
807
880
  });
@@ -828,905 +901,51 @@ const createCommand$1 = {
828
901
  const agentsCommand = {
829
902
  command: "agents",
830
903
  describe: "Manage agents",
831
- builder: (y) => y.command(listCommand$3).command(getCommand).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
904
+ builder: (y) => y.command(listCommand$4).command(getCommand$1).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
832
905
  handler: () => {}
833
906
  };
834
907
 
835
908
  //#endregion
836
- //#region src/commands/keys.ts
837
- function requireClient$1(argv) {
838
- const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
839
- if (result.isErr()) {
840
- printError(result.error.message);
841
- process.exit(1);
842
- }
843
- return new SkydiveApiClient(result.value);
844
- }
845
- const listCommand$2 = {
846
- command: "list",
847
- describe: "List API keys for an agent",
848
- handler: async (argv) => {
849
- const result = await requireClient$1(argv).listKeys(argv["agent-id"]);
850
- if (result.isErr()) {
851
- printError(result.error.message);
852
- process.exit(1);
853
- }
854
- const keys = result.value;
855
- if (argv.json) {
856
- output(argv, keys);
857
- return;
858
- }
859
- if (keys.length === 0) {
860
- console.log("No API keys found.");
861
- return;
862
- }
863
- printTable([
864
- "Name",
865
- "Prefix",
866
- "Last Used",
867
- "Created"
868
- ], keys.map((k) => [
869
- k.name,
870
- k.prefix,
871
- k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : "Never",
872
- new Date(k.createdAt).toLocaleDateString()
873
- ]));
874
- }
875
- };
876
- const createCommand = {
877
- command: "create <name>",
878
- describe: "Create a new API key for an agent",
879
- builder: (y) => y.positional("name", {
880
- type: "string",
881
- demandOption: true,
882
- describe: "Key name"
883
- }),
884
- handler: async (argv) => {
885
- const result = await requireClient$1(argv).createKey(argv["agent-id"], argv.name);
886
- if (result.isErr()) {
887
- printError(result.error.message);
888
- process.exit(1);
889
- }
890
- const key = result.value;
891
- if (argv.json) {
892
- output(argv, key);
893
- return;
894
- }
895
- if (argv.quiet) {
896
- console.log(key.key);
897
- return;
898
- }
899
- console.log(`API key created.`);
900
- console.log(` Name: ${key.name}`);
901
- console.log(` Key: ${key.key}`);
902
- console.log("");
903
- console.log(" Save this key — it will not be shown again.");
904
- }
905
- };
906
- const revokeCommand = {
907
- command: "revoke <id>",
908
- describe: "Revoke an API key",
909
- builder: (y) => y.positional("id", {
910
- type: "string",
911
- demandOption: true,
912
- describe: "Key ID"
913
- }),
914
- handler: async (argv) => {
915
- const result = await requireClient$1(argv).revokeKey(argv["agent-id"], argv.id);
916
- if (result.isErr()) {
917
- printError(result.error.message);
918
- process.exit(1);
919
- }
920
- if (argv.json) output(argv, {
921
- revoked: true,
922
- id: argv.id
923
- });
924
- else if (!argv.quiet) console.log(`Key ${argv.id} revoked.`);
925
- }
926
- };
927
- const keysCommand = {
928
- command: "keys",
929
- describe: "Manage API keys for an agent",
930
- builder: (y) => y.option("agent-id", {
931
- type: "string",
932
- demandOption: true,
933
- describe: "Agent ID"
934
- }).command(listCommand$2).command(createCommand).command(revokeCommand).demandCommand(1, "Specify a subcommand: list, create, revoke"),
935
- handler: () => {}
936
- };
937
-
938
- //#endregion
939
- //#region src/commands/secrets.ts
940
- function requireClient(argv) {
941
- const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
942
- if (result.isErr()) {
943
- printError(result.error.message);
944
- process.exit(1);
945
- }
946
- return new SkydiveApiClient(result.value);
947
- }
948
- /** Read all of stdin as UTF-8, trimming a single trailing newline. */
949
- async function readStdin() {
950
- const chunks = [];
951
- for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
952
- return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
953
- }
954
- const listCommand$1 = {
955
- command: "list",
956
- describe: "List secret names for an agent (values are never shown)",
957
- handler: async (argv) => {
958
- const result = await requireClient(argv).listSecrets(argv["agent-id"]);
959
- if (result.isErr()) {
960
- printError(result.error.message);
961
- process.exit(1);
962
- }
963
- const keys = result.value;
964
- if (argv.json) {
965
- output(argv, keys);
966
- return;
967
- }
968
- if (keys.length === 0) {
969
- console.log("No secrets found.");
970
- return;
971
- }
972
- printTable(["Name"], keys.map((k) => [k]));
973
- }
974
- };
975
- const setCommand = {
976
- command: "set <key> [value]",
977
- describe: "Set (create or overwrite) a secret. Reads value from stdin if omitted.",
978
- builder: (y) => y.positional("key", {
979
- type: "string",
980
- demandOption: true,
981
- describe: "Secret name (uppercase letters, digits, underscores)"
982
- }).positional("value", {
983
- type: "string",
984
- describe: "Secret value. If omitted, read from stdin — keeps the value out of shell history."
985
- }),
986
- handler: async (argv) => {
987
- let value = argv.value;
988
- if (value === void 0) {
989
- if (process.stdin.isTTY) {
990
- printError("No value given. Pass it as an argument or pipe it on stdin, e.g. `echo -n \"$TOKEN\" | skydive secrets set MY_KEY --agent-id <id>`.");
991
- process.exit(1);
992
- }
993
- value = await readStdin();
994
- }
995
- if (!value) {
996
- printError("Empty secret value.");
997
- process.exit(1);
998
- }
999
- const result = await requireClient(argv).setSecret(argv["agent-id"], argv.key, value);
1000
- if (result.isErr()) {
1001
- printError(result.error.message);
1002
- process.exit(1);
1003
- }
1004
- if (argv.json) output(argv, {
1005
- key: result.value,
1006
- set: true
1007
- });
1008
- else if (!argv.quiet) console.log(`Secret ${result.value} set.`);
1009
- }
1010
- };
1011
- const rmCommand = {
1012
- command: "rm <key>",
1013
- aliases: ["delete"],
1014
- describe: "Remove a secret from the agent",
1015
- builder: (y) => y.positional("key", {
1016
- type: "string",
1017
- demandOption: true,
1018
- describe: "Secret name"
1019
- }),
1020
- handler: async (argv) => {
1021
- const result = await requireClient(argv).deleteSecret(argv["agent-id"], argv.key);
1022
- if (result.isErr()) {
1023
- printError(result.error.message);
1024
- process.exit(1);
1025
- }
1026
- if (argv.json) output(argv, {
1027
- key: argv.key,
1028
- removed: true
1029
- });
1030
- else if (!argv.quiet) console.log(`Secret ${argv.key} removed.`);
1031
- }
1032
- };
1033
- const secretsCommand = {
1034
- command: "secrets",
1035
- describe: "Manage an agent’s secrets",
1036
- builder: (y) => y.option("agent-id", {
1037
- type: "string",
1038
- demandOption: true,
1039
- describe: "Agent ID"
1040
- }).command(listCommand$1).command(setCommand).command(rmCommand).demandCommand(1, "Specify a subcommand: list, set, rm"),
1041
- handler: () => {}
1042
- };
1043
-
1044
- //#endregion
1045
- //#region src/chat/tui/theme.ts
1046
- const tokyonight = {
1047
- id: "tokyonight",
1048
- label: "Tokyo Night",
1049
- mode: "dark",
1050
- palette: {
1051
- fg: "#c0caf5",
1052
- muted: "#7a7a7a",
1053
- dim: "#565f89",
1054
- faint: "#3b4261",
1055
- surface: "#16161e",
1056
- accent: "#7aa2f7",
1057
- success: "#9ece6a",
1058
- warning: "#e0af68",
1059
- error: "#f7768e",
1060
- user: "#7aa2f7",
1061
- assistant: "#c0caf5",
1062
- tool: "#bb9af7",
1063
- reasoning: "#565f89",
1064
- syntaxBlue: "#7aa2f7",
1065
- syntaxCyan: "#7dcfff",
1066
- syntaxTeal: "#73daca",
1067
- syntaxGreen: "#9ece6a",
1068
- syntaxYellow: "#e0af68",
1069
- syntaxOrange: "#ff9e64",
1070
- syntaxRed: "#f7768e",
1071
- syntaxMagenta: "#bb9af7"
1072
- }
1073
- };
1074
- const tokyonightDay = {
1075
- id: "tokyonight-day",
1076
- label: "Tokyo Night Day",
1077
- mode: "light",
1078
- palette: {
1079
- fg: "#3760bf",
1080
- muted: "#848cb5",
1081
- dim: "#9da3c2",
1082
- faint: "#c4c8da",
1083
- surface: "#d0d1d8",
1084
- accent: "#2e7de9",
1085
- success: "#587539",
1086
- warning: "#8c6c3e",
1087
- error: "#f52a65",
1088
- user: "#2e7de9",
1089
- assistant: "#3760bf",
1090
- tool: "#9854f1",
1091
- reasoning: "#848cb5",
1092
- syntaxBlue: "#2e7de9",
1093
- syntaxCyan: "#007197",
1094
- syntaxTeal: "#118c74",
1095
- syntaxGreen: "#587539",
1096
- syntaxYellow: "#8c6c3e",
1097
- syntaxOrange: "#b15c00",
1098
- syntaxRed: "#f52a65",
1099
- syntaxMagenta: "#9854f1"
1100
- }
1101
- };
1102
- const catppuccinMocha = {
1103
- id: "catppuccin-mocha",
1104
- label: "Catppuccin Mocha",
1105
- mode: "dark",
1106
- palette: {
1107
- fg: "#cdd6f4",
1108
- muted: "#7f849c",
1109
- dim: "#6c7086",
1110
- faint: "#45475a",
1111
- surface: "#181825",
1112
- accent: "#89b4fa",
1113
- success: "#a6e3a1",
1114
- warning: "#f9e2af",
1115
- error: "#f38ba8",
1116
- user: "#89b4fa",
1117
- assistant: "#cdd6f4",
1118
- tool: "#cba6f7",
1119
- reasoning: "#6c7086",
1120
- syntaxBlue: "#89b4fa",
1121
- syntaxCyan: "#89dceb",
1122
- syntaxTeal: "#94e2d5",
1123
- syntaxGreen: "#a6e3a1",
1124
- syntaxYellow: "#f9e2af",
1125
- syntaxOrange: "#fab387",
1126
- syntaxRed: "#f38ba8",
1127
- syntaxMagenta: "#cba6f7"
1128
- }
1129
- };
1130
- const catppuccinLatte = {
1131
- id: "catppuccin-latte",
1132
- label: "Catppuccin Latte",
1133
- mode: "light",
1134
- palette: {
1135
- fg: "#4c4f69",
1136
- muted: "#8c8fa1",
1137
- dim: "#9ca0b0",
1138
- faint: "#bcc0cc",
1139
- surface: "#e6e9ef",
1140
- accent: "#1e66f5",
1141
- success: "#40a02b",
1142
- warning: "#df8e1d",
1143
- error: "#d20f39",
1144
- user: "#1e66f5",
1145
- assistant: "#4c4f69",
1146
- tool: "#8839ef",
1147
- reasoning: "#9ca0b0",
1148
- syntaxBlue: "#1e66f5",
1149
- syntaxCyan: "#04a5e5",
1150
- syntaxTeal: "#179299",
1151
- syntaxGreen: "#40a02b",
1152
- syntaxYellow: "#df8e1d",
1153
- syntaxOrange: "#fe640b",
1154
- syntaxRed: "#d20f39",
1155
- syntaxMagenta: "#8839ef"
1156
- }
1157
- };
1158
- const gruvboxDark = {
1159
- id: "gruvbox-dark",
1160
- label: "Gruvbox Dark",
1161
- mode: "dark",
1162
- palette: {
1163
- fg: "#ebdbb2",
1164
- muted: "#928374",
1165
- dim: "#7c6f64",
1166
- faint: "#504945",
1167
- surface: "#1d2021",
1168
- accent: "#83a598",
1169
- success: "#b8bb26",
1170
- warning: "#fabd2f",
1171
- error: "#fb4934",
1172
- user: "#83a598",
1173
- assistant: "#ebdbb2",
1174
- tool: "#d3869b",
1175
- reasoning: "#7c6f64",
1176
- syntaxBlue: "#83a598",
1177
- syntaxCyan: "#8ec07c",
1178
- syntaxTeal: "#8ec07c",
1179
- syntaxGreen: "#b8bb26",
1180
- syntaxYellow: "#fabd2f",
1181
- syntaxOrange: "#fe8019",
1182
- syntaxRed: "#fb4934",
1183
- syntaxMagenta: "#d3869b"
1184
- }
1185
- };
1186
- const gruvboxLight = {
1187
- id: "gruvbox-light",
1188
- label: "Gruvbox Light",
1189
- mode: "light",
1190
- palette: {
1191
- fg: "#3c3836",
1192
- muted: "#928374",
1193
- dim: "#a89984",
1194
- faint: "#d5c4a1",
1195
- surface: "#ebdbb2",
1196
- accent: "#076678",
1197
- success: "#79740e",
1198
- warning: "#b57614",
1199
- error: "#9d0006",
1200
- user: "#076678",
1201
- assistant: "#3c3836",
1202
- tool: "#8f3f71",
1203
- reasoning: "#a89984",
1204
- syntaxBlue: "#076678",
1205
- syntaxCyan: "#427b58",
1206
- syntaxTeal: "#427b58",
1207
- syntaxGreen: "#79740e",
1208
- syntaxYellow: "#b57614",
1209
- syntaxOrange: "#af3a03",
1210
- syntaxRed: "#9d0006",
1211
- syntaxMagenta: "#8f3f71"
1212
- }
1213
- };
1214
- const solarizedDark = {
1215
- id: "solarized-dark",
1216
- label: "Solarized Dark",
1217
- mode: "dark",
1218
- palette: {
1219
- fg: "#93a1a1",
1220
- muted: "#586e75",
1221
- dim: "#586e75",
1222
- faint: "#073642",
1223
- surface: "#00212b",
1224
- accent: "#268bd2",
1225
- success: "#859900",
1226
- warning: "#b58900",
1227
- error: "#dc322f",
1228
- user: "#268bd2",
1229
- assistant: "#93a1a1",
1230
- tool: "#6c71c4",
1231
- reasoning: "#586e75",
1232
- syntaxBlue: "#268bd2",
1233
- syntaxCyan: "#2aa198",
1234
- syntaxTeal: "#2aa198",
1235
- syntaxGreen: "#859900",
1236
- syntaxYellow: "#b58900",
1237
- syntaxOrange: "#cb4b16",
1238
- syntaxRed: "#dc322f",
1239
- syntaxMagenta: "#6c71c4"
1240
- }
1241
- };
1242
- const solarizedLight = {
1243
- id: "solarized-light",
1244
- label: "Solarized Light",
1245
- mode: "light",
1246
- palette: {
1247
- fg: "#657b83",
1248
- muted: "#839496",
1249
- dim: "#93a1a1",
1250
- faint: "#eee8d5",
1251
- surface: "#eee8d5",
1252
- accent: "#268bd2",
1253
- success: "#859900",
1254
- warning: "#b58900",
1255
- error: "#dc322f",
1256
- user: "#268bd2",
1257
- assistant: "#657b83",
1258
- tool: "#6c71c4",
1259
- reasoning: "#93a1a1",
1260
- syntaxBlue: "#268bd2",
1261
- syntaxCyan: "#2aa198",
1262
- syntaxTeal: "#2aa198",
1263
- syntaxGreen: "#859900",
1264
- syntaxYellow: "#b58900",
1265
- syntaxOrange: "#cb4b16",
1266
- syntaxRed: "#dc322f",
1267
- syntaxMagenta: "#6c71c4"
1268
- }
1269
- };
1270
- const nord = {
1271
- id: "nord",
1272
- label: "Nord",
1273
- mode: "dark",
1274
- palette: {
1275
- fg: "#d8dee9",
1276
- muted: "#616e88",
1277
- dim: "#4c566a",
1278
- faint: "#3b4252",
1279
- surface: "#272c36",
1280
- accent: "#88c0d0",
1281
- success: "#a3be8c",
1282
- warning: "#ebcb8b",
1283
- error: "#bf616a",
1284
- user: "#88c0d0",
1285
- assistant: "#d8dee9",
1286
- tool: "#b48ead",
1287
- reasoning: "#4c566a",
1288
- syntaxBlue: "#81a1c1",
1289
- syntaxCyan: "#88c0d0",
1290
- syntaxTeal: "#8fbcbb",
1291
- syntaxGreen: "#a3be8c",
1292
- syntaxYellow: "#ebcb8b",
1293
- syntaxOrange: "#d08770",
1294
- syntaxRed: "#bf616a",
1295
- syntaxMagenta: "#b48ead"
1296
- }
1297
- };
1298
- const dracula = {
1299
- id: "dracula",
1300
- label: "Dracula",
1301
- mode: "dark",
1302
- palette: {
1303
- fg: "#f8f8f2",
1304
- muted: "#6272a4",
1305
- dim: "#6272a4",
1306
- faint: "#44475a",
1307
- surface: "#21222c",
1308
- accent: "#bd93f9",
1309
- success: "#50fa7b",
1310
- warning: "#ffb86c",
1311
- error: "#ff5555",
1312
- user: "#bd93f9",
1313
- assistant: "#f8f8f2",
1314
- tool: "#ff79c6",
1315
- reasoning: "#6272a4",
1316
- syntaxBlue: "#bd93f9",
1317
- syntaxCyan: "#8be9fd",
1318
- syntaxTeal: "#8be9fd",
1319
- syntaxGreen: "#50fa7b",
1320
- syntaxYellow: "#f1fa8c",
1321
- syntaxOrange: "#ffb86c",
1322
- syntaxRed: "#ff5555",
1323
- syntaxMagenta: "#ff79c6"
1324
- }
1325
- };
1326
- const oneDark = {
1327
- id: "one-dark",
1328
- label: "One Dark",
1329
- mode: "dark",
1330
- palette: {
1331
- fg: "#abb2bf",
1332
- muted: "#5c6370",
1333
- dim: "#5c6370",
1334
- faint: "#3e4451",
1335
- surface: "#21252b",
1336
- accent: "#61afef",
1337
- success: "#98c379",
1338
- warning: "#e5c07b",
1339
- error: "#e06c75",
1340
- user: "#61afef",
1341
- assistant: "#abb2bf",
1342
- tool: "#c678dd",
1343
- reasoning: "#5c6370",
1344
- syntaxBlue: "#61afef",
1345
- syntaxCyan: "#56b6c2",
1346
- syntaxTeal: "#56b6c2",
1347
- syntaxGreen: "#98c379",
1348
- syntaxYellow: "#e5c07b",
1349
- syntaxOrange: "#d19a66",
1350
- syntaxRed: "#e06c75",
1351
- syntaxMagenta: "#c678dd"
1352
- }
1353
- };
1354
- const oneLight = {
1355
- id: "one-light",
1356
- label: "One Light",
1357
- mode: "light",
1358
- palette: {
1359
- fg: "#383a42",
1360
- muted: "#a0a1a7",
1361
- dim: "#a0a1a7",
1362
- faint: "#e5e5e6",
1363
- surface: "#f0f0f1",
1364
- accent: "#4078f2",
1365
- success: "#50a14f",
1366
- warning: "#c18401",
1367
- error: "#e45649",
1368
- user: "#4078f2",
1369
- assistant: "#383a42",
1370
- tool: "#a626a4",
1371
- reasoning: "#a0a1a7",
1372
- syntaxBlue: "#4078f2",
1373
- syntaxCyan: "#0184bc",
1374
- syntaxTeal: "#0184bc",
1375
- syntaxGreen: "#50a14f",
1376
- syntaxYellow: "#c18401",
1377
- syntaxOrange: "#986801",
1378
- syntaxRed: "#e45649",
1379
- syntaxMagenta: "#a626a4"
1380
- }
1381
- };
1382
- const rosePine = {
1383
- id: "rose-pine",
1384
- label: "Rosé Pine",
1385
- mode: "dark",
1386
- palette: {
1387
- fg: "#e0def4",
1388
- muted: "#908caa",
1389
- dim: "#6e6a86",
1390
- faint: "#403d52",
1391
- surface: "#16141f",
1392
- accent: "#c4a7e7",
1393
- success: "#9ccfd8",
1394
- warning: "#f6c177",
1395
- error: "#eb6f92",
1396
- user: "#c4a7e7",
1397
- assistant: "#e0def4",
1398
- tool: "#ebbcba",
1399
- reasoning: "#908caa",
1400
- syntaxBlue: "#9ccfd8",
1401
- syntaxCyan: "#9ccfd8",
1402
- syntaxTeal: "#31748f",
1403
- syntaxGreen: "#31748f",
1404
- syntaxYellow: "#f6c177",
1405
- syntaxOrange: "#ebbcba",
1406
- syntaxRed: "#eb6f92",
1407
- syntaxMagenta: "#c4a7e7"
1408
- }
1409
- };
1410
- const rosePineDawn = {
1411
- id: "rose-pine-dawn",
1412
- label: "Rosé Pine Dawn",
1413
- mode: "light",
1414
- palette: {
1415
- fg: "#575279",
1416
- muted: "#797593",
1417
- dim: "#9893a5",
1418
- faint: "#cecacd",
1419
- surface: "#f2e9e1",
1420
- accent: "#907aa9",
1421
- success: "#56949f",
1422
- warning: "#ea9d34",
1423
- error: "#b4637a",
1424
- user: "#907aa9",
1425
- assistant: "#575279",
1426
- tool: "#d7827e",
1427
- reasoning: "#9893a5",
1428
- syntaxBlue: "#56949f",
1429
- syntaxCyan: "#56949f",
1430
- syntaxTeal: "#286983",
1431
- syntaxGreen: "#286983",
1432
- syntaxYellow: "#ea9d34",
1433
- syntaxOrange: "#d7827e",
1434
- syntaxRed: "#b4637a",
1435
- syntaxMagenta: "#907aa9"
1436
- }
1437
- };
1438
- const everforestDark = {
1439
- id: "everforest-dark",
1440
- label: "Everforest Dark",
1441
- mode: "dark",
1442
- palette: {
1443
- fg: "#d3c6aa",
1444
- muted: "#859289",
1445
- dim: "#7a8478",
1446
- faint: "#414b50",
1447
- surface: "#232a2e",
1448
- accent: "#7fbbb3",
1449
- success: "#a7c080",
1450
- warning: "#dbbc7f",
1451
- error: "#e67e80",
1452
- user: "#7fbbb3",
1453
- assistant: "#d3c6aa",
1454
- tool: "#d699b6",
1455
- reasoning: "#7a8478",
1456
- syntaxBlue: "#7fbbb3",
1457
- syntaxCyan: "#83c092",
1458
- syntaxTeal: "#83c092",
1459
- syntaxGreen: "#a7c080",
1460
- syntaxYellow: "#dbbc7f",
1461
- syntaxOrange: "#e69875",
1462
- syntaxRed: "#e67e80",
1463
- syntaxMagenta: "#d699b6"
1464
- }
1465
- };
1466
- const everforestLight = {
1467
- id: "everforest-light",
1468
- label: "Everforest Light",
1469
- mode: "light",
1470
- palette: {
1471
- fg: "#5c6a72",
1472
- muted: "#939f91",
1473
- dim: "#a6b0a0",
1474
- faint: "#e0dcc7",
1475
- surface: "#f4f0d9",
1476
- accent: "#3a94c5",
1477
- success: "#8da101",
1478
- warning: "#dfa000",
1479
- error: "#f85552",
1480
- user: "#3a94c5",
1481
- assistant: "#5c6a72",
1482
- tool: "#df69ba",
1483
- reasoning: "#a6b0a0",
1484
- syntaxBlue: "#3a94c5",
1485
- syntaxCyan: "#35a77c",
1486
- syntaxTeal: "#35a77c",
1487
- syntaxGreen: "#8da101",
1488
- syntaxYellow: "#dfa000",
1489
- syntaxOrange: "#f57d26",
1490
- syntaxRed: "#f85552",
1491
- syntaxMagenta: "#df69ba"
1492
- }
1493
- };
1494
- const githubDark = {
1495
- id: "github-dark",
1496
- label: "GitHub Dark",
1497
- mode: "dark",
1498
- palette: {
1499
- fg: "#c9d1d9",
1500
- muted: "#8b949e",
1501
- dim: "#6e7681",
1502
- faint: "#30363d",
1503
- surface: "#161b22",
1504
- accent: "#58a6ff",
1505
- success: "#3fb950",
1506
- warning: "#d29922",
1507
- error: "#f85149",
1508
- user: "#58a6ff",
1509
- assistant: "#c9d1d9",
1510
- tool: "#bc8cff",
1511
- reasoning: "#6e7681",
1512
- syntaxBlue: "#58a6ff",
1513
- syntaxCyan: "#39c5cf",
1514
- syntaxTeal: "#39c5cf",
1515
- syntaxGreen: "#3fb950",
1516
- syntaxYellow: "#d29922",
1517
- syntaxOrange: "#db6d28",
1518
- syntaxRed: "#f85149",
1519
- syntaxMagenta: "#bc8cff"
1520
- }
1521
- };
1522
- const githubLight = {
1523
- id: "github-light",
1524
- label: "GitHub Light",
1525
- mode: "light",
1526
- palette: {
1527
- fg: "#24292f",
1528
- muted: "#57606a",
1529
- dim: "#8c959f",
1530
- faint: "#d0d7de",
1531
- surface: "#f6f8fa",
1532
- accent: "#0969da",
1533
- success: "#1a7f37",
1534
- warning: "#9a6700",
1535
- error: "#cf222e",
1536
- user: "#0969da",
1537
- assistant: "#24292f",
1538
- tool: "#8250df",
1539
- reasoning: "#8c959f",
1540
- syntaxBlue: "#0969da",
1541
- syntaxCyan: "#1b7c83",
1542
- syntaxTeal: "#1b7c83",
1543
- syntaxGreen: "#1a7f37",
1544
- syntaxYellow: "#9a6700",
1545
- syntaxOrange: "#bc4c00",
1546
- syntaxRed: "#cf222e",
1547
- syntaxMagenta: "#8250df"
1548
- }
1549
- };
1550
- const kanagawa = {
1551
- id: "kanagawa",
1552
- label: "Kanagawa",
1553
- mode: "dark",
1554
- palette: {
1555
- fg: "#dcd7ba",
1556
- muted: "#727169",
1557
- dim: "#54546d",
1558
- faint: "#363646",
1559
- surface: "#16161d",
1560
- accent: "#7e9cd8",
1561
- success: "#98bb6c",
1562
- warning: "#e6c384",
1563
- error: "#e46876",
1564
- user: "#7e9cd8",
1565
- assistant: "#dcd7ba",
1566
- tool: "#957fb8",
1567
- reasoning: "#727169",
1568
- syntaxBlue: "#7e9cd8",
1569
- syntaxCyan: "#7aa89f",
1570
- syntaxTeal: "#7aa89f",
1571
- syntaxGreen: "#98bb6c",
1572
- syntaxYellow: "#e6c384",
1573
- syntaxOrange: "#ffa066",
1574
- syntaxRed: "#e46876",
1575
- syntaxMagenta: "#957fb8"
1576
- }
1577
- };
1578
- const themes = [
1579
- tokyonight,
1580
- tokyonightDay,
1581
- catppuccinMocha,
1582
- catppuccinLatte,
1583
- gruvboxDark,
1584
- gruvboxLight,
1585
- solarizedDark,
1586
- solarizedLight,
1587
- nord,
1588
- dracula,
1589
- oneDark,
1590
- oneLight,
1591
- rosePine,
1592
- rosePineDawn,
1593
- everforestDark,
1594
- everforestLight,
1595
- githubDark,
1596
- githubLight,
1597
- kanagawa
1598
- ];
1599
- const DEFAULT_THEME_ID = {
1600
- dark: tokyonight.id,
1601
- light: tokyonightDay.id
1602
- };
1603
- /** All-undefined palette for NO_COLOR: every fg/bg falls back to the
1604
- * terminal's own defaults, so nothing emits color. Not listed in `themes` —
1605
- * it's forced, never picked. */
1606
- const monoTheme = {
1607
- id: "mono",
1608
- label: "No color",
1609
- mode: "dark",
1610
- palette: {
1611
- fg: void 0,
1612
- muted: void 0,
1613
- dim: void 0,
1614
- faint: void 0,
1615
- surface: void 0,
1616
- accent: void 0,
1617
- success: void 0,
1618
- warning: void 0,
1619
- error: void 0,
1620
- user: void 0,
1621
- assistant: void 0,
1622
- tool: void 0,
1623
- reasoning: void 0,
1624
- syntaxBlue: void 0,
1625
- syntaxCyan: void 0,
1626
- syntaxTeal: void 0,
1627
- syntaxGreen: void 0,
1628
- syntaxYellow: void 0,
1629
- syntaxOrange: void 0,
1630
- syntaxRed: void 0,
1631
- syntaxMagenta: void 0
1632
- }
1633
- };
1634
- function themesForMode(mode) {
1635
- return themes.filter((t) => t.mode === mode);
1636
- }
1637
- function findTheme(id) {
1638
- if (id === monoTheme.id) return monoTheme;
1639
- return themes.find((t) => t.id === id);
1640
- }
1641
- /**
1642
- * The saved theme to use for a mode: the persisted pick if it exists *and*
1643
- * still matches the mode (a stale/renamed id falls back), else the default.
1644
- */
1645
- function themeForMode(mode, savedId) {
1646
- if (savedId) {
1647
- const saved = findTheme(savedId);
1648
- if (saved && saved.mode === mode) return saved;
1649
- }
1650
- return findTheme(DEFAULT_THEME_ID[mode]) ?? tokyonight;
1651
- }
1652
- /** https://no-color.org — any non-empty value disables color output. */
1653
- function noColorRequested(env = process.env) {
1654
- const v = env["NO_COLOR"];
1655
- return v !== void 0 && v !== "";
1656
- }
1657
- /**
1658
- * Fallback light/dark sniff for terminals that never answer the OSC 10/11
1659
- * query: `COLORFGBG` is "<fg>;<bg>" (sometimes "<fg>;default;<bg>") with
1660
- * ANSI palette indexes. Background 7/15 (white/bright white) means a light
1661
- * terminal; anything else we call dark. Returns null when unset/unparsable.
1662
- */
1663
- function themeModeFromColorFgBg(env = process.env) {
1664
- const raw = env["COLORFGBG"];
1665
- if (!raw) return null;
1666
- const parts = raw.split(";");
1667
- const bg = parts[parts.length - 1];
1668
- if (bg === void 0 || !/^\d+$/.test(bg)) return null;
1669
- const idx = Number(bg);
1670
- return idx === 7 || idx === 15 ? "light" : "dark";
1671
- }
1672
- /** Single source of truth for colors. Mutated in place by `applyTheme` so
1673
- * existing `theme.fg`-style reads across the TUI stay valid. */
1674
- const theme = { ...tokyonight.palette };
1675
- let version = 0;
1676
- function themeVersion() {
1677
- return version;
1678
- }
1679
- let mode = tokyonight.mode;
1680
- function themeMode() {
1681
- return mode;
1682
- }
1683
- function applyTheme(def) {
1684
- Object.assign(theme, def.palette);
1685
- mode = def.mode;
1686
- version++;
1687
- }
1688
-
1689
- //#endregion
1690
- //#region src/chat/bun-runtime.ts
1691
- /**
1692
- * The chat TUI renders through OpenTUI, whose native core is Bun-first (its
1693
- * FFI/native-module loading needs the Bun runtime; the Node path requires an
1694
- * experimental `node:ffi` only present in very new Node). Rather than make the
1695
- * user install Bun by hand, the CLI provisions a private, pinned Bun the first
1696
- * time `chat` runs and transparently re-execs itself under it. Every other
1697
- * command still runs under whatever Node the CLI was launched with — Bun is
1698
- * only ever fetched for `chat`.
1699
- */
1700
- /**
1701
- * Bun release the CLI pins. Bump deliberately: the download is integrity-checked
1702
- * against this exact release's published `SHASUMS256.txt`, and the cached binary
1703
- * is keyed by version so bumping here transparently re-provisions on next `chat`.
1704
- */
1705
- const PINNED_BUN_VERSION = "1.3.14";
1706
- const BUN_RELEASE_BASE = process.env["SKYDIVE_BUN_RELEASE_BASE"] ?? "https://github.com/oven-sh/bun/releases/download";
1707
- /** Guard env var: set on the child so a re-exec can never recurse. */
1708
- const REEXEC_GUARD = "SKYDIVE_BUN_REEXEC";
1709
- function isBun() {
1710
- return typeof process !== "undefined" && "bun" in process.versions;
1711
- }
1712
- /**
1713
- * Map the current platform/arch to Bun's release asset basename (without the
1714
- * `.zip`). Returns null on platforms Bun doesn't publish a build for, so the
1715
- * caller can fall back to the manual-install message instead of a 404.
1716
- *
1717
- * We deliberately pick the portable (non-`baseline`, non-`musl`) x64 builds;
1718
- * the baseline variant only matters for pre-2013 CPUs and isn't worth
1719
- * auto-detecting here. If that ever bites someone, `SKYDIVE_BUN_PATH` lets them
1720
- * point at their own Bun.
1721
- */
1722
- function bunAssetTarget(platform = process.platform, arch = process.arch) {
1723
- const a = arch === "arm64" ? "aarch64" : arch === "x64" ? "x64" : null;
1724
- if (!a) return null;
1725
- switch (platform) {
1726
- case "linux": return `bun-linux-${a}`;
1727
- case "darwin": return `bun-darwin-${a}`;
1728
- case "win32": return a === "x64" ? "bun-windows-x64" : null;
1729
- default: return null;
909
+ //#region src/chat/bun-runtime.ts
910
+ /**
911
+ * The chat TUI renders through OpenTUI, whose native core is Bun-first (its
912
+ * FFI/native-module loading needs the Bun runtime; the Node path requires an
913
+ * experimental `node:ffi` only present in very new Node). Rather than make the
914
+ * user install Bun by hand, the CLI provisions a private, pinned Bun the first
915
+ * time `chat` runs and transparently re-execs itself under it. Every other
916
+ * command still runs under whatever Node the CLI was launched with — Bun is
917
+ * only ever fetched for `chat`.
918
+ */
919
+ /**
920
+ * Bun release the CLI pins. Bump deliberately: the download is integrity-checked
921
+ * against this exact release's published `SHASUMS256.txt`, and the cached binary
922
+ * is keyed by version so bumping here transparently re-provisions on next `chat`.
923
+ */
924
+ const PINNED_BUN_VERSION = "1.3.14";
925
+ const BUN_RELEASE_BASE = process.env["SKYDIVE_BUN_RELEASE_BASE"] ?? "https://github.com/oven-sh/bun/releases/download";
926
+ /** Guard env var: set on the child so a re-exec can never recurse. */
927
+ const REEXEC_GUARD = "SKYDIVE_BUN_REEXEC";
928
+ function isBun() {
929
+ return typeof process !== "undefined" && "bun" in process.versions;
930
+ }
931
+ /**
932
+ * Map the current platform/arch to Bun's release asset basename (without the
933
+ * `.zip`). Returns null on platforms Bun doesn't publish a build for, so the
934
+ * caller can fall back to the manual-install message instead of a 404.
935
+ *
936
+ * We deliberately pick the portable (non-`baseline`, non-`musl`) x64 builds;
937
+ * the baseline variant only matters for pre-2013 CPUs and isn't worth
938
+ * auto-detecting here. If that ever bites someone, `SKYDIVE_BUN_PATH` lets them
939
+ * point at their own Bun.
940
+ */
941
+ function bunAssetTarget(platform = process.platform, arch = process.arch) {
942
+ const a = arch === "arm64" ? "aarch64" : arch === "x64" ? "x64" : null;
943
+ if (!a) return null;
944
+ switch (platform) {
945
+ case "linux": return `bun-linux-${a}`;
946
+ case "darwin": return `bun-darwin-${a}`;
947
+ case "win32": return a === "x64" ? "bun-windows-x64" : null;
948
+ default: return null;
1730
949
  }
1731
950
  }
1732
951
  /** Directory holding CLI-managed binaries, beside the config file. */
@@ -1842,88 +1061,393 @@ function extractBunFromZip(zip, target) {
1842
1061
  }
1843
1062
  throw new Error(`bun binary not found in release zip for ${target}`);
1844
1063
  }
1845
- /**
1846
- * Download, verify, and cache the pinned Bun for this platform. Returns the
1847
- * path to the cached executable, or null if Bun can't be provisioned (no
1848
- * network, unsupported platform, checksum mismatch) — the caller then prints
1849
- * the manual-install guidance. Never throws for the expected failure modes.
1850
- */
1851
- async function downloadBun(onProgress) {
1852
- const target = bunAssetTarget();
1853
- if (!target) return null;
1854
- const assetName = `${target}.zip`;
1855
- const dest = cachedBunPath();
1856
- try {
1857
- onProgress?.(`Fetching Bun v${PINNED_BUN_VERSION} (one-time setup)…`);
1858
- const url = `${BUN_RELEASE_BASE}/bun-v${PINNED_BUN_VERSION}/${assetName}`;
1859
- const [zipRes, want] = await Promise.all([fetchOk(url), expectedSha256(PINNED_BUN_VERSION, assetName)]);
1860
- const zip = Buffer.from(await zipRes.arrayBuffer());
1861
- const got = sha256(zip);
1862
- if (got !== want) {
1863
- onProgress?.(`Bun download failed integrity check (expected ${want}, got ${got}).`);
1864
- return null;
1064
+ /**
1065
+ * Download, verify, and cache the pinned Bun for this platform. Returns the
1066
+ * path to the cached executable, or null if Bun can't be provisioned (no
1067
+ * network, unsupported platform, checksum mismatch) — the caller then prints
1068
+ * the manual-install guidance. Never throws for the expected failure modes.
1069
+ */
1070
+ async function downloadBun(onProgress) {
1071
+ const target = bunAssetTarget();
1072
+ if (!target) return null;
1073
+ const assetName = `${target}.zip`;
1074
+ const dest = cachedBunPath();
1075
+ try {
1076
+ onProgress?.(`Fetching Bun v${PINNED_BUN_VERSION} (one-time setup)…`);
1077
+ const url = `${BUN_RELEASE_BASE}/bun-v${PINNED_BUN_VERSION}/${assetName}`;
1078
+ const [zipRes, want] = await Promise.all([fetchOk(url), expectedSha256(PINNED_BUN_VERSION, assetName)]);
1079
+ const zip = Buffer.from(await zipRes.arrayBuffer());
1080
+ const got = sha256(zip);
1081
+ if (got !== want) {
1082
+ onProgress?.(`Bun download failed integrity check (expected ${want}, got ${got}).`);
1083
+ return null;
1084
+ }
1085
+ const bin = extractBunFromZip(zip, target);
1086
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
1087
+ const tmp = path.join(path.dirname(dest), `.bun.tmp-${process.pid}-${Date.now()}`);
1088
+ fs.writeFileSync(tmp, bin, { mode: 493 });
1089
+ if (process.platform !== "win32") fs.chmodSync(tmp, 493);
1090
+ fs.renameSync(tmp, dest);
1091
+ return dest;
1092
+ } catch (e) {
1093
+ onProgress?.(`Could not download Bun automatically: ${e instanceof Error ? e.message : String(e)}`);
1094
+ return null;
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Ensure a usable Bun exists: already running under Bun, found on disk/PATH, or
1099
+ * freshly downloaded and cached. Pure resolution — does not re-exec.
1100
+ */
1101
+ async function resolveBun(onProgress) {
1102
+ if (isBun()) return { kind: "already-bun" };
1103
+ const existing = findExistingBun();
1104
+ if (existing) return {
1105
+ kind: "found",
1106
+ bunPath: existing
1107
+ };
1108
+ const downloaded = await downloadBun(onProgress);
1109
+ if (downloaded) return {
1110
+ kind: "found",
1111
+ bunPath: downloaded
1112
+ };
1113
+ return { kind: "unavailable" };
1114
+ }
1115
+ /**
1116
+ * The heart of the transparent-Bun story. Called by `chat` before it touches
1117
+ * OpenTUI:
1118
+ * - Under Bun already, or if a re-exec guard is set: return 'proceed'.
1119
+ * - Otherwise resolve/provision Bun and re-exec this exact CLI invocation
1120
+ * under it (inheriting stdio + argv), then exit with the child's code.
1121
+ * - If Bun can't be provisioned: return 'unavailable' so the caller prints
1122
+ * the existing manual-install message.
1123
+ *
1124
+ * Returns 'proceed' only when it's safe to load OpenTUI in this process.
1125
+ */
1126
+ function ensureBunAndReexec(onProgress) {
1127
+ if (isBun() || process.env[REEXEC_GUARD] === "1") return Promise.resolve("proceed");
1128
+ return resolveBun(onProgress).then((resolution) => {
1129
+ if (resolution.kind === "already-bun") return "proceed";
1130
+ if (resolution.kind === "unavailable") return "unavailable";
1131
+ const argv = process.argv.slice(1);
1132
+ const result = spawnSync(resolution.bunPath, argv, {
1133
+ stdio: "inherit",
1134
+ env: {
1135
+ ...process.env,
1136
+ [REEXEC_GUARD]: "1"
1137
+ }
1138
+ });
1139
+ if (result.error) {
1140
+ onProgress?.(`Failed to launch chat under Bun (${resolution.bunPath}): ${result.error.message}`);
1141
+ return "unavailable";
1142
+ }
1143
+ process.exit(result.status ?? 0);
1144
+ });
1145
+ }
1146
+
1147
+ //#endregion
1148
+ //#region src/commands/import.ts
1149
+ /**
1150
+ * The first message of the import conversation, sent as the user. It hands
1151
+ * the real work to the agent's `import-config` skill; this text only needs
1152
+ * to establish intent, anchor the starting directory, and set the two
1153
+ * expectations the user cares about most (plan first, no credentials).
1154
+ */
1155
+ function buildImportSeedPrompt(projectDir) {
1156
+ return [
1157
+ "I'm migrating from another coding agent — import my setup from this machine.",
1158
+ "",
1159
+ `Use your import-config skill. I ran this from \`${projectDir}\`, so start there (and my home directory) when you inventory what I have. Show me the plan first — what you'll bring over, where it lands in you, and anything you're leaving out (credentials especially) — and wait for my OK before committing anything.`
1160
+ ].join("\n");
1161
+ }
1162
+ const importCommand = {
1163
+ command: "import",
1164
+ describe: "Import your Claude Code / Cursor / OpenCode setup into an agent (opens a machine-shared chat)",
1165
+ builder: (y) => y.option("agent", {
1166
+ type: "string",
1167
+ describe: "Target agent, by id, slug, or name. Optional: with one agent on the account it is auto-picked; otherwise the agent picker opens first (interactive) or resolution follows the -p rules."
1168
+ }).option("print", {
1169
+ alias: "p",
1170
+ type: "string",
1171
+ describe: "Non-interactive: send the import request as a one-shot, print the agent's plan, and exit (drivable from scripts or another coding agent). An optional value adds extra instructions to the request. The agent still waits for approval — continue the printed conversation with `skydive chat -p \"go ahead\" --resume <id> --share-machine`. Runs under Node."
1172
+ }).example("skydive import", "Import from the current directory").example("skydive import --agent kit", "Import into a specific agent, skipping the picker").example("skydive import -p --agent kit", "One-shot: print the agent's import plan and exit"),
1173
+ handler: async (argv) => {
1174
+ const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
1175
+ if (argv.print !== void 0) {
1176
+ await runImportPrintMode({
1177
+ argv,
1178
+ appUrl
1179
+ });
1180
+ return;
1181
+ }
1182
+ if (isNonInteractive()) {
1183
+ printError("import without -p is an interactive chat session and needs a terminal. For scripts (or driving this from another agent), use `skydive import -p --agent <agent>`.");
1184
+ process.exit(1);
1185
+ }
1186
+ if (await ensureBunAndReexec((msg) => process.stderr.write(`${msg}\n`)) === "unavailable") {
1187
+ printError(`import needs the Bun runtime and it couldn't be set up automatically. Install Bun (https://bun.sh) and run \`bun ${process.argv[1] ?? "skydive"} import\`, point SKYDIVE_BUN_PATH at an existing bun binary, or use the non-interactive \`import -p\` (runs under Node).`);
1188
+ process.exit(1);
1189
+ }
1190
+ let session = resolveSession({ appUrl });
1191
+ if (session.isErr()) {
1192
+ const login = await loginWithDevice({ appUrl });
1193
+ if (login.isErr()) {
1194
+ printError(login.error.message);
1195
+ process.exit(1);
1196
+ }
1197
+ session = resolveSession({ appUrl });
1198
+ if (session.isErr()) {
1199
+ printError("Signed in, but no session was stored.");
1200
+ process.exit(1);
1201
+ }
1202
+ }
1203
+ const { runChat } = await import("./boot-CQLDUeZ9.mjs");
1204
+ await runChat({
1205
+ appUrl,
1206
+ sessionToken: session.value.sessionToken,
1207
+ shareMachine: true,
1208
+ promptHistoryPath: getPromptHistoryPath(),
1209
+ notifications: true,
1210
+ agentSelector: argv.agent ?? null,
1211
+ conversationId: null,
1212
+ seedPrompt: buildImportSeedPrompt(process.cwd())
1213
+ });
1214
+ }
1215
+ };
1216
+ /**
1217
+ * `import -p`: the same seeded, machine-shared request as the TUI flow, as a
1218
+ * one-shot print run (no TTY, no Bun). The agent's reply is its import plan —
1219
+ * per the import-config skill it must NOT commit anything until approved, so
1220
+ * the caller reviews the plan and continues the conversation
1221
+ * (`chat -p "go ahead" --resume <id> --share-machine`) to apply it.
1222
+ */
1223
+ async function runImportPrintMode({ argv, appUrl }) {
1224
+ const session = resolveSession({ appUrl });
1225
+ if (session.isErr()) {
1226
+ printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
1227
+ process.exit(1);
1228
+ }
1229
+ const { connectMachineShare } = await import("./print-share-D7OSxvE2.mjs");
1230
+ const machineShare = await connectMachineShare({
1231
+ appUrl,
1232
+ sessionToken: session.value.sessionToken,
1233
+ timeoutHint: "Check `skydive portal status`, then re-run."
1234
+ });
1235
+ const extra = (argv.print ?? "").trim();
1236
+ const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
1237
+ const { runPrint } = await import("./print-B6AO13SA.mjs");
1238
+ try {
1239
+ const result = await runPrint({
1240
+ appUrl,
1241
+ sessionToken: session.value.sessionToken,
1242
+ prompt,
1243
+ agentSelector: argv.agent ?? null,
1244
+ conversationId: null,
1245
+ json: argv.json,
1246
+ machineShare,
1247
+ grantTargetAgent: true
1248
+ });
1249
+ if (argv.json) output(argv, result);
1250
+ else console.error(`\nTo approve or adjust this plan: skydive chat -p "<your answer>" --agent ${result.agentId} --resume ${result.conversationId} --share-machine`);
1251
+ } catch (error) {
1252
+ printError(error instanceof Error ? error.message : String(error));
1253
+ process.exit(1);
1254
+ } finally {
1255
+ machineShare.dispose();
1256
+ }
1257
+ }
1258
+
1259
+ //#endregion
1260
+ //#region src/commands/keys.ts
1261
+ const listCommand$3 = {
1262
+ command: "list",
1263
+ describe: "List API keys for an agent",
1264
+ handler: async (argv) => {
1265
+ const result = await requireManagementClient(argv).listKeys(argv["agent-id"]);
1266
+ if (result.isErr()) {
1267
+ printError(result.error.message);
1268
+ process.exit(1);
1269
+ }
1270
+ const keys = result.value;
1271
+ if (argv.json) {
1272
+ output(argv, keys);
1273
+ return;
1274
+ }
1275
+ if (keys.length === 0) {
1276
+ console.log("No API keys found.");
1277
+ return;
1278
+ }
1279
+ printTable([
1280
+ "Name",
1281
+ "Prefix",
1282
+ "Last Used",
1283
+ "Created"
1284
+ ], keys.map((k) => [
1285
+ k.name,
1286
+ k.prefix,
1287
+ k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : "Never",
1288
+ new Date(k.createdAt).toLocaleDateString()
1289
+ ]));
1290
+ }
1291
+ };
1292
+ const createCommand = {
1293
+ command: "create <name>",
1294
+ describe: "Create a new API key for an agent",
1295
+ builder: (y) => y.positional("name", {
1296
+ type: "string",
1297
+ demandOption: true,
1298
+ describe: "Key name"
1299
+ }),
1300
+ handler: async (argv) => {
1301
+ const result = await requireManagementClient(argv).createKey(argv["agent-id"], argv.name);
1302
+ if (result.isErr()) {
1303
+ printError(result.error.message);
1304
+ process.exit(1);
1305
+ }
1306
+ const key = result.value;
1307
+ if (argv.json) {
1308
+ output(argv, key);
1309
+ return;
1310
+ }
1311
+ if (argv.quiet) {
1312
+ console.log(key.key);
1313
+ return;
1314
+ }
1315
+ console.log(`API key created.`);
1316
+ console.log(` Name: ${key.name}`);
1317
+ console.log(` Key: ${key.key}`);
1318
+ console.log("");
1319
+ console.log(" Save this key — it will not be shown again.");
1320
+ }
1321
+ };
1322
+ const revokeCommand$1 = {
1323
+ command: "revoke <id>",
1324
+ describe: "Revoke an API key",
1325
+ builder: (y) => y.positional("id", {
1326
+ type: "string",
1327
+ demandOption: true,
1328
+ describe: "Key ID"
1329
+ }),
1330
+ handler: async (argv) => {
1331
+ const result = await requireManagementClient(argv).revokeKey(argv["agent-id"], argv.id);
1332
+ if (result.isErr()) {
1333
+ printError(result.error.message);
1334
+ process.exit(1);
1335
+ }
1336
+ if (argv.json) output(argv, {
1337
+ revoked: true,
1338
+ id: argv.id
1339
+ });
1340
+ else if (!argv.quiet) console.log(`Key ${argv.id} revoked.`);
1341
+ }
1342
+ };
1343
+ const keysCommand = {
1344
+ command: "keys",
1345
+ describe: "Manage API keys for an agent",
1346
+ builder: (y) => y.option("agent-id", {
1347
+ type: "string",
1348
+ demandOption: true,
1349
+ describe: "Agent ID"
1350
+ }).command(listCommand$3).command(createCommand).command(revokeCommand$1).demandCommand(1, "Specify a subcommand: list, create, revoke"),
1351
+ handler: () => {}
1352
+ };
1353
+
1354
+ //#endregion
1355
+ //#region src/commands/secrets.ts
1356
+ /** Read all of stdin as UTF-8, trimming a single trailing newline. */
1357
+ async function readStdin() {
1358
+ const chunks = [];
1359
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1360
+ return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
1361
+ }
1362
+ const listCommand$2 = {
1363
+ command: "list",
1364
+ describe: "List secret names for an agent (values are never shown)",
1365
+ handler: async (argv) => {
1366
+ const result = await requireManagementClient(argv).listSecrets(argv["agent-id"]);
1367
+ if (result.isErr()) {
1368
+ printError(result.error.message);
1369
+ process.exit(1);
1865
1370
  }
1866
- const bin = extractBunFromZip(zip, target);
1867
- fs.mkdirSync(path.dirname(dest), { recursive: true });
1868
- const tmp = path.join(path.dirname(dest), `.bun.tmp-${process.pid}-${Date.now()}`);
1869
- fs.writeFileSync(tmp, bin, { mode: 493 });
1870
- if (process.platform !== "win32") fs.chmodSync(tmp, 493);
1871
- fs.renameSync(tmp, dest);
1872
- return dest;
1873
- } catch (e) {
1874
- onProgress?.(`Could not download Bun automatically: ${e instanceof Error ? e.message : String(e)}`);
1875
- return null;
1371
+ const keys = result.value;
1372
+ if (argv.json) {
1373
+ output(argv, keys);
1374
+ return;
1375
+ }
1376
+ if (keys.length === 0) {
1377
+ console.log("No secrets found.");
1378
+ return;
1379
+ }
1380
+ printTable(["Name"], keys.map((k) => [k]));
1876
1381
  }
1877
- }
1878
- /**
1879
- * Ensure a usable Bun exists: already running under Bun, found on disk/PATH, or
1880
- * freshly downloaded and cached. Pure resolution does not re-exec.
1881
- */
1882
- async function resolveBun(onProgress) {
1883
- if (isBun()) return { kind: "already-bun" };
1884
- const existing = findExistingBun();
1885
- if (existing) return {
1886
- kind: "found",
1887
- bunPath: existing
1888
- };
1889
- const downloaded = await downloadBun(onProgress);
1890
- if (downloaded) return {
1891
- kind: "found",
1892
- bunPath: downloaded
1893
- };
1894
- return { kind: "unavailable" };
1895
- }
1896
- /**
1897
- * The heart of the transparent-Bun story. Called by `chat` before it touches
1898
- * OpenTUI:
1899
- * - Under Bun already, or if a re-exec guard is set: return 'proceed'.
1900
- * - Otherwise resolve/provision Bun and re-exec this exact CLI invocation
1901
- * under it (inheriting stdio + argv), then exit with the child's code.
1902
- * - If Bun can't be provisioned: return 'unavailable' so the caller prints
1903
- * the existing manual-install message.
1904
- *
1905
- * Returns 'proceed' only when it's safe to load OpenTUI in this process.
1906
- */
1907
- function ensureBunAndReexec(onProgress) {
1908
- if (isBun() || process.env[REEXEC_GUARD] === "1") return Promise.resolve("proceed");
1909
- return resolveBun(onProgress).then((resolution) => {
1910
- if (resolution.kind === "already-bun") return "proceed";
1911
- if (resolution.kind === "unavailable") return "unavailable";
1912
- const argv = process.argv.slice(1);
1913
- const result = spawnSync(resolution.bunPath, argv, {
1914
- stdio: "inherit",
1915
- env: {
1916
- ...process.env,
1917
- [REEXEC_GUARD]: "1"
1382
+ };
1383
+ const setCommand = {
1384
+ command: "set <key> [value]",
1385
+ describe: "Set (create or overwrite) a secret. Reads value from stdin if omitted.",
1386
+ builder: (y) => y.positional("key", {
1387
+ type: "string",
1388
+ demandOption: true,
1389
+ describe: "Secret name (uppercase letters, digits, underscores)"
1390
+ }).positional("value", {
1391
+ type: "string",
1392
+ describe: "Secret value. If omitted, read from stdin — keeps the value out of shell history."
1393
+ }),
1394
+ handler: async (argv) => {
1395
+ let value = argv.value;
1396
+ if (value === void 0) {
1397
+ if (process.stdin.isTTY) {
1398
+ printError("No value given. Pass it as an argument or pipe it on stdin, e.g. `echo -n \"$TOKEN\" | skydive secrets set MY_KEY --agent-id <id>`.");
1399
+ process.exit(1);
1918
1400
  }
1401
+ value = await readStdin();
1402
+ }
1403
+ if (!value) {
1404
+ printError("Empty secret value.");
1405
+ process.exit(1);
1406
+ }
1407
+ const result = await requireManagementClient(argv).setSecret(argv["agent-id"], argv.key, value);
1408
+ if (result.isErr()) {
1409
+ printError(result.error.message);
1410
+ process.exit(1);
1411
+ }
1412
+ if (argv.json) output(argv, {
1413
+ key: result.value,
1414
+ set: true
1919
1415
  });
1920
- if (result.error) {
1921
- onProgress?.(`Failed to launch chat under Bun (${resolution.bunPath}): ${result.error.message}`);
1922
- return "unavailable";
1416
+ else if (!argv.quiet) console.log(`Secret ${result.value} set.`);
1417
+ }
1418
+ };
1419
+ const rmCommand = {
1420
+ command: "rm <key>",
1421
+ aliases: ["delete"],
1422
+ describe: "Remove a secret from the agent",
1423
+ builder: (y) => y.positional("key", {
1424
+ type: "string",
1425
+ demandOption: true,
1426
+ describe: "Secret name"
1427
+ }),
1428
+ handler: async (argv) => {
1429
+ const result = await requireManagementClient(argv).deleteSecret(argv["agent-id"], argv.key);
1430
+ if (result.isErr()) {
1431
+ printError(result.error.message);
1432
+ process.exit(1);
1923
1433
  }
1924
- process.exit(result.status ?? 0);
1925
- });
1926
- }
1434
+ if (argv.json) output(argv, {
1435
+ key: argv.key,
1436
+ removed: true
1437
+ });
1438
+ else if (!argv.quiet) console.log(`Secret ${argv.key} removed.`);
1439
+ }
1440
+ };
1441
+ const secretsCommand = {
1442
+ command: "secrets",
1443
+ describe: "Manage an agent’s secrets",
1444
+ builder: (y) => y.option("agent-id", {
1445
+ type: "string",
1446
+ demandOption: true,
1447
+ describe: "Agent ID"
1448
+ }).command(listCommand$2).command(setCommand).command(rmCommand).demandCommand(1, "Specify a subcommand: list, set, rm"),
1449
+ handler: () => {}
1450
+ };
1927
1451
 
1928
1452
  //#endregion
1929
1453
  //#region src/commands/chat.ts
@@ -1938,12 +1462,12 @@ const chatCommand = {
1938
1462
  type: "string",
1939
1463
  describe: "Target agent, by id, slug, or name. With -p, the agent to send the one-shot prompt to. Without -p, pre-selects the agent and opens its conversation list, skipping the agent picker. Optional when the account has exactly one agent."
1940
1464
  }).option("conversation", {
1465
+ alias: "resume",
1941
1466
  type: "string",
1942
- describe: "For -p: continue an existing conversation by id instead of starting a new one."
1467
+ describe: "Continue an existing conversation by id. With -p, the one-shot lands in that conversation; without -p, the TUI opens directly into it (the agent is resolved from the conversation, no --agent needed)."
1943
1468
  }).option("share-machine", {
1944
1469
  type: "boolean",
1945
- default: false,
1946
- describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; you approve per agent)"
1470
+ describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; in the TUI you approve per agent, with -p the flag grants the target agent for the run). Defaults to `shareMachineDefault` in the CLI config file; --no-share-machine disables for this invocation."
1947
1471
  }).option("theme", {
1948
1472
  type: "string",
1949
1473
  describe: `Pin a colorscheme (disables automatic light/dark switching). One of: ${themes.map((t) => t.id).join(", ")}. Defaults to following the terminal background; also settable via SKYDIVE_THEME.`
@@ -1951,7 +1475,7 @@ const chatCommand = {
1951
1475
  type: "boolean",
1952
1476
  default: true,
1953
1477
  describe: "Show a desktop notification when a run finishes or needs your input while this terminal is unfocused (use --no-notify to disable)"
1954
- }).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)"),
1478
+ }).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)").example("skydive chat --resume 0197e0f3-…", "Reopen a previous conversation in the TUI (the id is printed when you quit a chat)"),
1955
1479
  handler: async (argv) => {
1956
1480
  const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
1957
1481
  if (argv.print !== void 0) {
@@ -1968,7 +1492,7 @@ const chatCommand = {
1968
1492
  let session = resolveSession({ appUrl });
1969
1493
  if (session.isErr()) {
1970
1494
  if (isNonInteractive()) {
1971
- printError("Not signed in for chat and no interactive terminal. Run `skydive auth login --web`, or set SKYDIVE_SESSION_TOKEN.");
1495
+ printError("Not signed in for chat and no interactive terminal. Run `skydive auth login`, or set SKYDIVE_SESSION_TOKEN.");
1972
1496
  process.exit(1);
1973
1497
  }
1974
1498
  const login = await loginWithDevice({ appUrl });
@@ -1982,68 +1506,250 @@ const chatCommand = {
1982
1506
  process.exit(1);
1983
1507
  }
1984
1508
  }
1985
- const themeId = argv.theme ?? process.env["SKYDIVE_THEME"] ?? void 0;
1986
- if (themeId !== void 0 && !themes.some((t) => t.id === themeId)) {
1987
- printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
1988
- process.exit(1);
1509
+ const themeId = argv.theme ?? process.env["SKYDIVE_THEME"] ?? void 0;
1510
+ if (themeId !== void 0 && !themes.some((t) => t.id === themeId)) {
1511
+ printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
1512
+ process.exit(1);
1513
+ }
1514
+ const { runChat } = await import("./boot-CQLDUeZ9.mjs");
1515
+ await runChat({
1516
+ appUrl,
1517
+ sessionToken: session.value.sessionToken,
1518
+ shareMachine: resolveShareMachine(argv),
1519
+ promptHistoryPath: getPromptHistoryPath(),
1520
+ theme: themeId,
1521
+ notifications: argv.notify,
1522
+ agentSelector: argv.agent ?? null,
1523
+ conversationId: argv.conversation ?? null,
1524
+ seedPrompt: null
1525
+ });
1526
+ }
1527
+ };
1528
+ /**
1529
+ * Whether this invocation shares the machine: an explicit
1530
+ * --share-machine/--no-share-machine wins; otherwise fall back to the
1531
+ * persisted `shareMachineDefault` config value.
1532
+ */
1533
+ function resolveShareMachine(argv) {
1534
+ return argv["share-machine"] ?? getShareMachineDefault();
1535
+ }
1536
+ async function runPrintMode({ argv, appUrl }) {
1537
+ const session = resolveSession({ appUrl });
1538
+ if (session.isErr()) {
1539
+ printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
1540
+ process.exit(1);
1541
+ }
1542
+ const { runPrint, readStdin } = await import("./print-B6AO13SA.mjs");
1543
+ let prompt = (argv.print ?? "").trim();
1544
+ if (!prompt) {
1545
+ if (process.stdin.isTTY) {
1546
+ printError("No prompt given. Pass it inline (`-p \"your prompt\"`) or pipe it on stdin.");
1547
+ process.exit(1);
1548
+ }
1549
+ prompt = (await readStdin()).trim();
1550
+ if (!prompt) {
1551
+ printError("Empty prompt on stdin.");
1552
+ process.exit(1);
1553
+ }
1554
+ }
1555
+ let machineShare = null;
1556
+ if (resolveShareMachine(argv)) {
1557
+ const { connectMachineShare } = await import("./print-share-D7OSxvE2.mjs");
1558
+ machineShare = await connectMachineShare({
1559
+ appUrl,
1560
+ sessionToken: session.value.sessionToken,
1561
+ timeoutHint: "Re-run with --no-share-machine (or unset `shareMachineDefault` in the CLI config), or check `skydive portal status`."
1562
+ });
1563
+ }
1564
+ try {
1565
+ const result = await runPrint({
1566
+ appUrl,
1567
+ sessionToken: session.value.sessionToken,
1568
+ prompt,
1569
+ agentSelector: argv.agent ?? null,
1570
+ conversationId: argv.conversation ?? null,
1571
+ json: argv.json,
1572
+ machineShare,
1573
+ grantTargetAgent: argv["share-machine"] === true
1574
+ });
1575
+ if (argv.json) output(argv, result);
1576
+ } catch (error) {
1577
+ printError(error instanceof Error ? error.message : String(error));
1578
+ process.exit(1);
1579
+ } finally {
1580
+ machineShare?.dispose();
1581
+ }
1582
+ }
1583
+
1584
+ //#endregion
1585
+ //#region src/commands/messages.ts
1586
+ /**
1587
+ * `skydive messages get <messageId>` re-attaches to an exchange by message id
1588
+ * and prints the agent's reply.
1589
+ *
1590
+ * This is the recovery path for `chat -p`: a long run's stream can drop at the
1591
+ * edge (Cloudflare 502/504) after the message was accepted, leaving the caller
1592
+ * with a messageId but no result. The agent keeps working server-side, so a
1593
+ * blind retry would double-execute an agent that may have write access.
1594
+ * Instead, fetch the finished result by message id here — the server resolves
1595
+ * the run behind the message and replays it from its persisted event log, so
1596
+ * this works whether the run is still live or already done.
1597
+ *
1598
+ * Message ids are the currency of this API: `chat -p --json` reports the
1599
+ * `messageId` and this command consumes it. Run ids stay server-side.
1600
+ */
1601
+ const getCommand = {
1602
+ command: "get <message-id>",
1603
+ describe: "Fetch a message by id and print the reply (recovers a timed-out -p)",
1604
+ builder: (y) => y.positional("message-id", {
1605
+ type: "string",
1606
+ demandOption: true,
1607
+ describe: "Message id (the `messageId` from a prior `chat -p --json`)"
1608
+ }),
1609
+ handler: async (argv) => {
1610
+ const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
1611
+ const session = resolveSession({ appUrl });
1612
+ if (session.isErr()) {
1613
+ printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
1614
+ process.exit(1);
1615
+ }
1616
+ const { messageGet } = await import("./print-B6AO13SA.mjs");
1617
+ try {
1618
+ const result = await messageGet({
1619
+ appUrl,
1620
+ sessionToken: session.value.sessionToken,
1621
+ messageId: argv["message-id"],
1622
+ json: argv.json
1623
+ });
1624
+ if (argv.json) output(argv, result);
1625
+ } catch (error) {
1626
+ printError(error instanceof Error ? error.message : String(error));
1627
+ process.exit(1);
1628
+ }
1629
+ }
1630
+ };
1631
+ const messagesCommand = {
1632
+ command: "messages",
1633
+ describe: "Inspect chat messages and re-fetch agent replies",
1634
+ builder: (y) => y.command(getCommand).demandCommand(1, "Specify a subcommand: get"),
1635
+ handler: () => {}
1636
+ };
1637
+
1638
+ //#endregion
1639
+ //#region src/commands/conversations.ts
1640
+ /**
1641
+ * Flatten a UiMessage's parts into plain text for the non-JSON view. Text and
1642
+ * reasoning parts are concatenated; tool calls are summarized to one line so a
1643
+ * transcript stays readable without the rich TUI rendering. This is a
1644
+ * lossy-but-legible view; `--json` carries the full structured parts for a
1645
+ * driving agent that wants everything.
1646
+ */
1647
+ function messageToText(message) {
1648
+ const chunks = [];
1649
+ for (const part of message.parts) if (part.type === "text" && typeof part.text === "string") chunks.push(part.text);
1650
+ else if (part.type === "reasoning" && typeof part.text === "string") chunks.push(part.text);
1651
+ else if (part.type === "dynamic-tool" && "toolName" in part) {
1652
+ const toolName = typeof part.toolName === "string" ? part.toolName : "tool";
1653
+ chunks.push(`[tool: ${toolName}]`);
1654
+ }
1655
+ return chunks.join("\n").trim();
1656
+ }
1657
+ const listCommand$1 = {
1658
+ command: "list",
1659
+ describe: "List an agent's conversations",
1660
+ builder: (y) => y.option("agent", {
1661
+ type: "string",
1662
+ describe: "Agent whose conversations to list, by id, slug, or name. Optional when the account has exactly one agent."
1663
+ }).option("limit", {
1664
+ type: "number",
1665
+ describe: "Max conversations to return"
1666
+ }),
1667
+ handler: async (argv) => {
1668
+ const client = requireRestClient(argv);
1669
+ const agent = resolveAgent(await client.listAgents({
1670
+ scope: "org",
1671
+ onPage: null
1672
+ }), argv.agent ?? null);
1673
+ const conversations = await client.listConversations({
1674
+ agentId: agent.id,
1675
+ limit: argv.limit,
1676
+ onPage: null
1677
+ });
1678
+ if (argv.json) {
1679
+ output(argv, conversations);
1680
+ return;
1681
+ }
1682
+ if (conversations.length === 0) {
1683
+ console.log("No conversations found.");
1684
+ return;
1989
1685
  }
1990
- const { runChat } = await import("./boot-ChlVx-ts.mjs");
1991
- await runChat({
1992
- appUrl,
1993
- sessionToken: session.value.sessionToken,
1994
- shareMachine: argv["share-machine"],
1995
- promptHistoryPath: getPromptHistoryPath(),
1996
- theme: themeId,
1997
- notifications: argv.notify,
1998
- agentSelector: argv.agent ?? null
1999
- });
1686
+ printTable([
1687
+ "ID",
1688
+ "Title",
1689
+ "Updated",
1690
+ "Preview"
1691
+ ], conversations.map((c) => [
1692
+ c.id,
1693
+ c.title ?? "-",
1694
+ c.updatedAt,
1695
+ (c.preview ?? "").replace(/\s+/g, " ").slice(0, 48) || "-"
1696
+ ]));
2000
1697
  }
2001
1698
  };
2002
- async function runPrintMode({ argv, appUrl }) {
2003
- const session = resolveSession({ appUrl });
2004
- if (session.isErr()) {
2005
- printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
2006
- process.exit(1);
2007
- }
2008
- const { runPrint, readStdin } = await import("./print-BHbFMxQv.mjs").then((n) => n.t);
2009
- let prompt = (argv.print ?? "").trim();
2010
- if (!prompt) {
2011
- if (process.stdin.isTTY) {
2012
- printError("No prompt given. Pass it inline (`-p \"your prompt\"`) or pipe it on stdin.");
2013
- process.exit(1);
1699
+ const showCommand = {
1700
+ command: "show <conversation-id>",
1701
+ describe: "Print a conversation transcript",
1702
+ builder: (y) => y.positional("conversation-id", {
1703
+ type: "string",
1704
+ demandOption: true,
1705
+ describe: "Conversation id"
1706
+ }).option("recap", {
1707
+ type: "boolean",
1708
+ default: false,
1709
+ describe: "Print the conversation recap instead of the full transcript"
1710
+ }),
1711
+ handler: async (argv) => {
1712
+ const client = requireRestClient(argv);
1713
+ const conversationId = argv["conversation-id"];
1714
+ if (argv.recap) {
1715
+ const recap = await client.getRecap({ conversationId });
1716
+ if (argv.json) {
1717
+ output(argv, {
1718
+ conversationId,
1719
+ recap
1720
+ });
1721
+ return;
1722
+ }
1723
+ console.log(recap ?? "(no recap available)");
1724
+ return;
2014
1725
  }
2015
- prompt = (await readStdin()).trim();
2016
- if (!prompt) {
2017
- printError("Empty prompt on stdin.");
2018
- process.exit(1);
1726
+ const messages = await client.listMessages({ conversationId });
1727
+ if (argv.json) {
1728
+ output(argv, messages);
1729
+ return;
1730
+ }
1731
+ if (messages.length === 0) {
1732
+ console.log("No messages in this conversation.");
1733
+ return;
1734
+ }
1735
+ for (const message of messages) {
1736
+ const text = messageToText(message);
1737
+ if (!text) continue;
1738
+ console.log(`\n[${message.role}]`);
1739
+ console.log(text);
2019
1740
  }
2020
1741
  }
2021
- try {
2022
- const result = await runPrint({
2023
- appUrl,
2024
- sessionToken: session.value.sessionToken,
2025
- prompt,
2026
- agentSelector: argv.agent ?? null,
2027
- conversationId: argv.conversation ?? null,
2028
- json: argv.json
2029
- });
2030
- if (argv.json) output(argv, result);
2031
- } catch (error) {
2032
- printError(error instanceof Error ? error.message : String(error));
2033
- process.exit(1);
2034
- }
2035
- }
1742
+ };
1743
+ const conversationsCommand = {
1744
+ command: "conversations",
1745
+ aliases: ["conv"],
1746
+ describe: "List and read agent conversations",
1747
+ builder: (y) => y.command(listCommand$1).command(showCommand).demandCommand(1, "Specify a subcommand: list, show"),
1748
+ handler: () => {}
1749
+ };
2036
1750
 
2037
1751
  //#endregion
2038
1752
  //#region src/commands/workspace.ts
2039
- function requireSession(argv) {
2040
- const session = resolveSession({ appUrl: argv["api-url"] });
2041
- if (session.isErr()) {
2042
- printError("Not signed in for chat. Run `skydive auth login --web` first.");
2043
- process.exit(1);
2044
- }
2045
- return session.value;
2046
- }
2047
1753
  /**
2048
1754
  * List the account's workspaces, marking the active one. Shared by the `list`
2049
1755
  * subcommand and the bare `workspace` invocation. When `hint` is true (the bare
@@ -2078,7 +1784,7 @@ async function runList(argv, { hint }) {
2078
1784
  w.name,
2079
1785
  w.slug
2080
1786
  ]));
2081
- if (hint && !argv.quiet) console.log("\nRun `skydive workspace switch <slug>` to switch which workspace `skydive chat` uses.");
1787
+ if (hint && !argv.quiet) console.log("\nRun `skydive workspace switch` to pick a workspace, or pass its slug directly.");
2082
1788
  }
2083
1789
  const listCommand = {
2084
1790
  command: "list",
@@ -2086,15 +1792,28 @@ const listCommand = {
2086
1792
  handler: (argv) => runList(argv, { hint: false })
2087
1793
  };
2088
1794
  const switchCommand = {
2089
- command: "switch <workspace>",
2090
- describe: "Switch which workspace `skydive chat` uses",
1795
+ command: "switch [workspace]",
1796
+ describe: "Switch the workspace all `skydive` commands act on",
2091
1797
  builder: (y) => y.positional("workspace", {
2092
1798
  type: "string",
2093
- demandOption: true,
1799
+ demandOption: false,
2094
1800
  describe: "Workspace slug, name, or ID"
2095
1801
  }),
2096
1802
  handler: async (argv) => {
2097
1803
  const session = requireSession(argv);
1804
+ if (!argv.workspace) {
1805
+ if (argv.json || argv.quiet || isNonInteractive()) {
1806
+ printError("Interactive workspace switching needs a terminal. Pass a workspace slug, name, or ID.");
1807
+ process.exit(1);
1808
+ }
1809
+ if (await ensureBunAndReexec((message) => process.stderr.write(`${message}\n`)) === "unavailable") {
1810
+ 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.");
1811
+ process.exit(1);
1812
+ }
1813
+ const { runWorkspacePicker } = await import("./boot-CQLDUeZ9.mjs");
1814
+ await runWorkspacePicker(session);
1815
+ return;
1816
+ }
2098
1817
  const workspaces = await listWorkspaces(session);
2099
1818
  if (workspaces.isErr()) {
2100
1819
  printError(workspaces.error.message);
@@ -2123,15 +1842,310 @@ const switchCommand = {
2123
1842
  };
2124
1843
  const workspaceCommand = {
2125
1844
  command: "workspace",
2126
- describe: "List or switch which workspace `skydive chat` uses",
1845
+ describe: "List or switch the workspace all `skydive` commands act on",
2127
1846
  builder: (y) => y.command(listCommand).command(switchCommand),
2128
1847
  handler: (argv) => runList(argv, { hint: true })
2129
1848
  };
2130
1849
 
1850
+ //#endregion
1851
+ //#region src/commands/portal.ts
1852
+ /**
1853
+ * Resolve everything a grant/revoke needs in one round trip: the devices
1854
+ * response carries both the user's machines and the org's agent roster, so
1855
+ * the `--agent` selector (id or name) resolves without a separate paginated
1856
+ * agent listing. `device` is null when this machine has no device row yet
1857
+ * (it has never shared or been registered) — grant registers on the fly,
1858
+ * revoke has nothing to remove.
1859
+ */
1860
+ async function resolveGrantTarget(argv) {
1861
+ const session = requireSession(argv);
1862
+ const { devices, agents } = await fetchPortalDevices(session);
1863
+ return {
1864
+ session,
1865
+ agent: resolveAgent(agents, argv.agent),
1866
+ device: findThisDevice(devices, machineIdentity().machineName)
1867
+ };
1868
+ }
1869
+ function agentOption(y) {
1870
+ return y.option("agent", {
1871
+ type: "string",
1872
+ demandOption: true,
1873
+ describe: "Agent, by id or name"
1874
+ });
1875
+ }
1876
+ const openCommand = {
1877
+ command: "open",
1878
+ describe: "Open the portal to this machine so granted agents can run commands here (stays in the foreground; ctrl+c to close)",
1879
+ builder: (y) => y.option("agent", {
1880
+ type: "string",
1881
+ describe: "Also grant this agent (by id or name) access to the machine once connected"
1882
+ }).option("cwd", {
1883
+ type: "string",
1884
+ describe: "Working directory for the agent's commands (default: the current directory)"
1885
+ }),
1886
+ handler: async (argv) => {
1887
+ const session = requireSession(argv);
1888
+ const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
1889
+ const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
1890
+ const { machineName } = machineIdentity();
1891
+ const { PortalClient } = await import("./client-D6NAkL9e.mjs");
1892
+ let lastLine = "";
1893
+ let signalConnected;
1894
+ const connected = new Promise((resolve) => {
1895
+ signalConnected = resolve;
1896
+ });
1897
+ const client = new PortalClient({
1898
+ appUrl: session.appUrl,
1899
+ sessionToken: session.sessionToken,
1900
+ cwd,
1901
+ onState: (state) => {
1902
+ if (state.status === "connected") signalConnected();
1903
+ const line = state.status === "error" ? `portal: connection error: ${state.error ?? "unknown"} — retrying` : `portal: ${state.status}`;
1904
+ if (line === lastLine) return;
1905
+ lastLine = line;
1906
+ if (state.status === "error") console.error(line);
1907
+ else if (!argv.quiet) console.log(line);
1908
+ }
1909
+ });
1910
+ const stop = () => {
1911
+ client.dispose();
1912
+ process.exit(0);
1913
+ };
1914
+ process.on("SIGINT", stop);
1915
+ process.on("SIGTERM", stop);
1916
+ client.enable();
1917
+ await connected;
1918
+ if (agent) {
1919
+ await client.grantAgent(agent.id);
1920
+ console.log(`portal: granted ${agent.name} access to this machine (persists until revoked)`);
1921
+ }
1922
+ console.log(`portal: open — granted agents can run commands on ${machineName} as your user, cwd ${cwd}. ctrl+c to close.`);
1923
+ await new Promise(() => {});
1924
+ }
1925
+ };
1926
+ const grantCommand = {
1927
+ command: "grant",
1928
+ describe: "Allow an agent to run commands on this machine (persists until revoked)",
1929
+ builder: agentOption,
1930
+ handler: async (argv) => {
1931
+ const { session, agent, device } = await resolveGrantTarget(argv);
1932
+ const { machineName, friendlyName } = machineIdentity();
1933
+ const deviceId = device ? device.id : (await registerPortalDevice(session, {
1934
+ machineName,
1935
+ friendlyName
1936
+ })).id;
1937
+ await grantPortalAccess(session, {
1938
+ deviceId,
1939
+ agentId: agent.id
1940
+ });
1941
+ if (argv.json) {
1942
+ output(argv, {
1943
+ deviceId,
1944
+ agentId: agent.id
1945
+ });
1946
+ return;
1947
+ }
1948
+ console.log(`Granted ${agent.name} access to ${device?.friendlyName ?? friendlyName}. The agent can run commands here whenever its portal is open — start with \`skydive portal open\`, or pass \`--share-machine\` on a \`chat -p\` run. Revoke with \`skydive portal revoke --agent ${agent.name}\`.`);
1949
+ }
1950
+ };
1951
+ const revokeCommand = {
1952
+ command: "revoke",
1953
+ describe: "Remove an agent's access to this machine",
1954
+ builder: agentOption,
1955
+ handler: async (argv) => {
1956
+ const { session, agent, device } = await resolveGrantTarget(argv);
1957
+ if (!device) throw new Error(`this machine (${machineIdentity().machineName}) isn't registered with the portal — there's nothing to revoke.`);
1958
+ await revokePortalAccess(session, {
1959
+ deviceId: device.id,
1960
+ agentId: agent.id
1961
+ });
1962
+ if (argv.json) {
1963
+ output(argv, {
1964
+ deviceId: device.id,
1965
+ agentId: agent.id
1966
+ });
1967
+ return;
1968
+ }
1969
+ console.log(`Revoked ${agent.name}'s access to ${device.friendlyName}.`);
1970
+ }
1971
+ };
1972
+ const statusCommand = {
1973
+ command: "status",
1974
+ describe: "Show your portal machines, connection state, and granted agents",
1975
+ handler: async (argv) => {
1976
+ const { devices, agents } = await fetchPortalDevices(requireSession(argv));
1977
+ if (argv.json) {
1978
+ output(argv, {
1979
+ devices,
1980
+ agents
1981
+ });
1982
+ return;
1983
+ }
1984
+ if (devices.length === 0) {
1985
+ console.log("No machines registered. Run `skydive portal open` to register this one.");
1986
+ return;
1987
+ }
1988
+ const { headers, rows } = buildDeviceTable(devices, agents, machineIdentity().machineName);
1989
+ printTable(headers, rows);
1990
+ }
1991
+ };
1992
+ /**
1993
+ * Build the `portal status` table. Grant ids are shown as agent names when the
1994
+ * org roster resolves them (deleted agents fall back to the raw id), and the
1995
+ * device this CLI would register as is marked so "which row is me" doesn't
1996
+ * depend on knowing the `-cli` naming convention.
1997
+ */
1998
+ function buildDeviceTable(devices, agents, thisMachineName) {
1999
+ const agentNames = new Map(agents.map((a) => [a.id, a.name]));
2000
+ return {
2001
+ headers: [
2002
+ "Machine",
2003
+ "Connected",
2004
+ "Last seen",
2005
+ "Granted agents"
2006
+ ],
2007
+ rows: devices.map((device) => [
2008
+ device.machineName === thisMachineName ? `${device.friendlyName} *` : device.friendlyName,
2009
+ device.connected ? "yes" : "no",
2010
+ device.lastSeen ?? "-",
2011
+ device.grantedAgentIds.length === 0 ? "-" : device.grantedAgentIds.map((id) => agentNames.get(id) ?? id).join(", ")
2012
+ ])
2013
+ };
2014
+ }
2015
+ const portalCommand = {
2016
+ command: "portal",
2017
+ describe: "Open this machine's portal to agents and manage their access",
2018
+ builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status"),
2019
+ handler: () => {}
2020
+ };
2021
+
2022
+ //#endregion
2023
+ //#region src/commands/sandbox.ts
2024
+ /** POSIX single-quote one word so the remote shell treats it as one token. */
2025
+ function shellQuote(word) {
2026
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(word)) return word;
2027
+ return `'${word.replace(/'/g, `'\\''`)}'`;
2028
+ }
2029
+ /**
2030
+ * Serialize the command words into the single shell string the relay runs.
2031
+ *
2032
+ * Words arrive already split by the caller's shell, so each must be re-quoted
2033
+ * or the remote shell re-splits any word containing spaces or metacharacters:
2034
+ * `sandbox -- sh -c 'echo hi; whoami'` would otherwise run `sh -c echo hi`
2035
+ * and then, separately, `whoami`. A pipeline still works the explicit way —
2036
+ * `sandbox -- sh -c 'ls | wc -l'` — which is what `docker`/`kubectl exec` ask
2037
+ * for too. yargs number-coerces bare numerals, so words are stringified.
2038
+ */
2039
+ function joinCommandWords(words) {
2040
+ return words.map(String).map(shellQuote).join(" ").trim();
2041
+ }
2042
+ /**
2043
+ * `skydive sandbox` — the standalone counterpart to the chat TUI's `/sandbox`
2044
+ * composer command (ANY-4928): a live terminal (or one-shot exec) in the
2045
+ * agent's own sandbox without opening the TUI. Runs under Node (no Bun/
2046
+ * OpenTUI): the PTY is a raw byte passthrough on the caller's real terminal.
2047
+ * The relay gates on EDIT access to the agent and the sandbox-terminal-enabled
2048
+ * kill switch, and boots the sandbox when it isn't running.
2049
+ */
2050
+ const sandboxCommand = {
2051
+ command: "sandbox [command..]",
2052
+ describe: "Open a live terminal in an agent's sandbox, or run a one-shot command there",
2053
+ builder: (y) => y.option("agent", {
2054
+ type: "string",
2055
+ describe: "Target agent, by id, slug, or name. Optional when the account has exactly one agent."
2056
+ }).positional("command", {
2057
+ type: "string",
2058
+ array: true,
2059
+ describe: "Command to run one-shot (streams output, exits with its exit code). Omit for a live interactive terminal. Put it after `--` if it has flags of its own."
2060
+ }).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`"),
2061
+ handler: async (argv) => {
2062
+ const session = requireSession(argv);
2063
+ const { createRestClient } = await import("./rest-COkLEZOB.mjs");
2064
+ const { resolveAgent } = await import("./print-B6AO13SA.mjs");
2065
+ const client = createRestClient({
2066
+ appUrl: session.appUrl,
2067
+ sessionToken: session.sessionToken
2068
+ });
2069
+ let agent;
2070
+ try {
2071
+ agent = resolveAgent(await client.listAgents({
2072
+ scope: "org",
2073
+ onPage: null
2074
+ }), argv.agent ?? null);
2075
+ } catch (error) {
2076
+ printError(error instanceof Error ? error.message : String(error));
2077
+ process.exit(1);
2078
+ }
2079
+ const command = joinCommandWords([...argv.command ?? [], ...argv["--"] ?? []]);
2080
+ const code = command ? await runExec({
2081
+ session,
2082
+ agentId: agent.id,
2083
+ command
2084
+ }) : await runPty({
2085
+ session,
2086
+ agentId: agent.id,
2087
+ agentName: agent.name
2088
+ });
2089
+ process.exit(code);
2090
+ }
2091
+ };
2092
+ /** One-shot exec: stream output to stdout, resolve to the command's exit code. */
2093
+ function runExec({ session, agentId, command }) {
2094
+ console.error("Connecting to the sandbox…");
2095
+ return new Promise((resolve) => {
2096
+ const finish = (code) => {
2097
+ process.stdout.write("", () => resolve(code));
2098
+ };
2099
+ SandboxStream.open({
2100
+ mode: "exec",
2101
+ appUrl: session.appUrl,
2102
+ sessionToken: session.sessionToken,
2103
+ agentId,
2104
+ command,
2105
+ onEvent: (e) => {
2106
+ switch (e.type) {
2107
+ case "data":
2108
+ process.stdout.write(e.bytes);
2109
+ break;
2110
+ case "exit":
2111
+ finish(e.code);
2112
+ break;
2113
+ case "error":
2114
+ printError(e.message);
2115
+ finish(1);
2116
+ break;
2117
+ case "close":
2118
+ printError(e.failure ? `Could not run the command in the sandbox: ${e.failure}` : "The connection to the sandbox closed before the command finished.");
2119
+ finish(1);
2120
+ break;
2121
+ }
2122
+ }
2123
+ });
2124
+ });
2125
+ }
2126
+ /** Live terminal on the caller's real TTY. */
2127
+ async function runPty({ session, agentId, agentName }) {
2128
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2129
+ printError("A live sandbox terminal needs an interactive TTY. For scripted use, pass a command: `skydive sandbox -- <cmd>`.");
2130
+ return 1;
2131
+ }
2132
+ console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
2133
+ const { runRawPtyPassthrough } = await import("./raw-pty-2_VA1kw_.mjs");
2134
+ const result = await runRawPtyPassthrough({
2135
+ stdin: process.stdin,
2136
+ stdout: process.stdout,
2137
+ appUrl: session.appUrl,
2138
+ sessionToken: session.sessionToken,
2139
+ agentId
2140
+ });
2141
+ if (result.reason === "detach") console.error("\nDetached.");
2142
+ return result.code;
2143
+ }
2144
+
2131
2145
  //#endregion
2132
2146
  //#region src/cli.ts
2133
2147
  function createCli(argv) {
2134
- return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch which workspace `skydive chat` uses").option("json", {
2148
+ return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").parserConfiguration({ "populate--": true }).example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch the workspace all `skydive` commands act on").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").option("json", {
2135
2149
  type: "boolean",
2136
2150
  default: false,
2137
2151
  global: true,
@@ -2145,7 +2159,7 @@ function createCli(argv) {
2145
2159
  type: "string",
2146
2160
  global: true,
2147
2161
  describe: "Override API base URL"
2148
- }).command(authCommand).command(chatCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
2162
+ }).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) => {
2149
2163
  printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
2150
2164
  process.exit(1);
2151
2165
  });
@@ -2164,6 +2178,7 @@ const VALUE_FLAGS = new Set([
2164
2178
  "-p",
2165
2179
  "--agent",
2166
2180
  "--conversation",
2181
+ "--resume",
2167
2182
  "--theme",
2168
2183
  "--api-url"
2169
2184
  ]);
@@ -2209,13 +2224,391 @@ function shouldDefaultToChat(args, tty, nonInteractive = isNonInteractive()) {
2209
2224
  function resolveArgv(args, tty = {
2210
2225
  stdinIsTTY: Boolean(process.stdin.isTTY),
2211
2226
  stdoutIsTTY: Boolean(process.stdout.isTTY)
2212
- }) {
2213
- return shouldDefaultToChat(args, tty) ? ["chat", ...args] : args;
2227
+ }, nonInteractive) {
2228
+ return shouldDefaultToChat(args, tty, nonInteractive) ? ["chat", ...args] : args;
2229
+ }
2230
+
2231
+ //#endregion
2232
+ //#region src/update-check/cache.ts
2233
+ /**
2234
+ * The on-disk state behind the update check: a single small JSON file the
2235
+ * fast path reads synchronously on every command and the background worker
2236
+ * rewrites after a successful fetch. Every read tolerates a missing, torn,
2237
+ * or type-mangled file (each field validates independently), and a failed
2238
+ * write is silently skipped — the cache must never break a command.
2239
+ */
2240
+ const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
2241
+ const cacheSchema = z.object({
2242
+ lastCheckedAt: z.string().optional().catch(void 0),
2243
+ channel: z.string().optional().catch(void 0),
2244
+ latestVersion: z.string().optional().catch(void 0)
2245
+ });
2246
+ /** Beside config.json so all CLI state shares one directory (and the
2247
+ * `SKYDIVE_CONFIG_NAME` profile isolation). */
2248
+ function getUpdateCachePath() {
2249
+ return path.join(path.dirname(getConfigPath()), "update-check.json");
2250
+ }
2251
+ function readUpdateCache() {
2252
+ try {
2253
+ const raw = fs.readFileSync(getUpdateCachePath(), "utf8");
2254
+ return cacheSchema.parse(JSON.parse(raw));
2255
+ } catch (_error) {
2256
+ return {};
2257
+ }
2258
+ }
2259
+ function writeUpdateCache(cache) {
2260
+ try {
2261
+ const file = getUpdateCachePath();
2262
+ fs.mkdirSync(path.dirname(file), { recursive: true });
2263
+ fs.writeFileSync(file, `${JSON.stringify(cache, null, 2)}\n`, { mode: 384 });
2264
+ } catch (_error) {}
2265
+ }
2266
+ function isCheckDue(lastCheckedAt, now = Date.now()) {
2267
+ if (!lastCheckedAt) return true;
2268
+ const then = Date.parse(lastCheckedAt);
2269
+ if (Number.isNaN(then)) return true;
2270
+ if (then > now) return true;
2271
+ return now - then >= CHECK_INTERVAL_MS;
2272
+ }
2273
+
2274
+ //#endregion
2275
+ //#region src/update-check/versions.ts
2276
+ /** Canary builds carry a prerelease suffix (`X.Y.Z-beta.N`, synthesized in
2277
+ * CI); a plain semver is a stable release. An unparseable version defaults
2278
+ * to stable. */
2279
+ function resolveChannel(version) {
2280
+ return (semver.prerelease(version, { loose: true })?.length ?? 0) > 0 ? "canary" : "stable";
2281
+ }
2282
+ /** npm dist-tag for a channel (see release-skydive-cli.yml). */
2283
+ function distTagForChannel(channel) {
2284
+ return channel === "canary" ? "beta" : "latest";
2285
+ }
2286
+ /**
2287
+ * True when `candidate` is a strictly newer release than `current`, per
2288
+ * semver precedence (the `semver` package, including prerelease ordering).
2289
+ * Unparseable input is never newer, so garbage from the registry can't
2290
+ * produce a notice.
2291
+ */
2292
+ function isNewerVersion(candidate, current) {
2293
+ if (!semver.valid(candidate, { loose: true })) return false;
2294
+ if (!semver.valid(current, { loose: true })) return false;
2295
+ return semver.gt(candidate, current, { loose: true });
2296
+ }
2297
+
2298
+ //#endregion
2299
+ //#region src/update-check/notice.ts
2300
+ /** The install-mode-specific action line. A package-manager install is never
2301
+ * self-mutated — we only tell the user what to run. */
2302
+ function renderUpdateCommand(channel, source) {
2303
+ if (source === "binary") return `curl -fsSL ${DEFAULT_WEB_URL}/api/v1/cli/install.sh | ${channel === "canary" ? "SKYDIVE_CHANNEL=canary " : ""}sh`;
2304
+ return `npm install -g ${name}@${distTagForChannel(channel)}`;
2305
+ }
2306
+ function renderUpdateNotice(opts) {
2307
+ return `\nUpdate available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun ${renderUpdateCommand(opts.channel, opts.source)}\n`;
2308
+ }
2309
+ /**
2310
+ * Whether this invocation may print the notice. Pure so it's testable; the
2311
+ * inputs are raw pre-yargs argv (parsing hasn't happened when this runs) and
2312
+ * stderr's TTY-ness. `--json`/`--quiet` go to stdout, and the notice goes to
2313
+ * stderr — but scripts commonly capture 2>&1, so machine-readable modes
2314
+ * suppress it entirely rather than risk corrupting piped output.
2315
+ */
2316
+ function shouldNotify(opts) {
2317
+ if (!opts.stderrIsTTY) return false;
2318
+ if (opts.argv.includes("--json") || opts.argv.includes("--quiet")) return false;
2319
+ return true;
2320
+ }
2321
+
2322
+ //#endregion
2323
+ //#region src/update-check/sources.ts
2324
+ function resolveInstallSource() {
2325
+ return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
2326
+ }
2327
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org";
2328
+ const npmDistTagsSchema = z.record(z.string(), z.unknown());
2329
+ const binaryManifestSchema = z.object({ version: z.unknown().optional() });
2330
+ /** npm dist-tags for the published package (`skydive-cli`). */
2331
+ const npmReleaseSource = { async fetchLatestVersion(channel, { signal }) {
2332
+ const response = await fetch(`${NPM_REGISTRY_URL}/-/package/${name}/dist-tags`, {
2333
+ signal,
2334
+ headers: { accept: "application/json" }
2335
+ });
2336
+ if (!response.ok) return null;
2337
+ const tags = npmDistTagsSchema.safeParse(await response.json());
2338
+ if (!tags.success) return null;
2339
+ const version = tags.data[distTagForChannel(channel)];
2340
+ return typeof version === "string" ? version : null;
2341
+ } };
2342
+ /**
2343
+ * Release CDN (CloudFront over the releases bucket, infra: CliReleasesCdn).
2344
+ * Serves the channel pointers the release workflow uploads
2345
+ * (`channels/{stable,canary}.json`, cached max-age=60) alongside the
2346
+ * binaries. The daily poll goes here, not to the api, so a fleet of
2347
+ * installed binaries puts no load on — and takes no dependency on — the api.
2348
+ */
2349
+ const RELEASE_CDN_URL = "https://dl.skydive.com";
2350
+ /** Channel manifest on the release CDN, for compiled binaries. The same
2351
+ * document the api's channel route serves (see
2352
+ * apps/anyone/api/src/routes/cli-releases.ts, which reads it from the
2353
+ * bucket this CDN fronts). */
2354
+ const binaryReleaseSource = { async fetchLatestVersion(channel, { signal }) {
2355
+ const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
2356
+ signal,
2357
+ headers: { accept: "application/json" }
2358
+ });
2359
+ if (!response.ok) return null;
2360
+ const manifest = binaryManifestSchema.safeParse(await response.json());
2361
+ if (!manifest.success) return null;
2362
+ return typeof manifest.data.version === "string" ? manifest.data.version : null;
2363
+ } };
2364
+ function releaseSourceForInstall(source) {
2365
+ return source === "binary" ? binaryReleaseSource : npmReleaseSource;
2366
+ }
2367
+
2368
+ //#endregion
2369
+ //#region src/update-check/index.ts
2370
+ /**
2371
+ * Non-blocking update check, update-notifier style: a command never waits on
2372
+ * the network. Each invocation reads a small on-disk cache (./cache.ts) and,
2373
+ * when it records a newer version for this build's channel, prints a notice
2374
+ * (./notice.ts) to stderr at process exit. Separately, at most once per
2375
+ * check interval, it spawns a short-lived detached child (this same
2376
+ * executable with {@link UPDATE_WORKER_FLAG}) that fetches release metadata
2377
+ * (./sources.ts) and rewrites the cache for *future* invocations. So a
2378
+ * notice is always one check behind — the price of never delaying a command.
2379
+ *
2380
+ * Two install modes share everything except the metadata source and the
2381
+ * suggested action (see ./sources.ts):
2382
+ *
2383
+ * - package-manager (npm/Yarn/pnpm/Bun): compare against npm dist-tags,
2384
+ * print the install command, never self-mutate the install.
2385
+ * - compiled binary: compare against the release channel manifest served by
2386
+ * the api, print the installer one-liner; a real `skydive update`
2387
+ * self-updater can slot in behind the same boundary later.
2388
+ *
2389
+ * Failure policy: every path here is best-effort. Network errors, timeouts,
2390
+ * a torn cache file, an unwritable config dir — all silent. The check must
2391
+ * never break or slow a command.
2392
+ */
2393
+ /** Hidden argv sentinel that turns an invocation into the background refresh
2394
+ * worker (see bin.ts). Namespaced so it can never collide with a real flag. */
2395
+ const UPDATE_WORKER_FLAG = "--skydive-internal-update-check";
2396
+ const FETCH_TIMEOUT_MS = 1e4;
2397
+ function isCheckDisabled(env) {
2398
+ return Boolean(env["SKYDIVE_NO_UPDATE_CHECK"] || env["NO_UPDATE_NOTIFIER"] || env["CI"]) || getUpdateCheckDisabled();
2399
+ }
2400
+ /** A cached answer counts only if it's for this build's channel and strictly
2401
+ * newer than what's running. */
2402
+ function updateAvailable(cache, currentVersion, channel) {
2403
+ return Boolean(cache.latestVersion && cache.channel === channel && isNewerVersion(cache.latestVersion, currentVersion));
2404
+ }
2405
+ function registerExitNotice(notice) {
2406
+ process.once("exit", () => {
2407
+ process.stderr.write(notice);
2408
+ });
2409
+ }
2410
+ /** Stamp the claim before the worker spawns, so a crashing/offline worker
2411
+ * retries next interval instead of respawning on every command. */
2412
+ function claimCheckInterval(cache) {
2413
+ writeUpdateCache({
2414
+ ...cache,
2415
+ lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
2416
+ });
2417
+ }
2418
+ function spawnUpdateCheckWorker() {
2419
+ const args = resolveInstallSource() === "binary" ? [UPDATE_WORKER_FLAG] : [...process.argv[1] ? [process.argv[1]] : [], UPDATE_WORKER_FLAG];
2420
+ spawn(process.execPath, args, {
2421
+ detached: true,
2422
+ stdio: "ignore"
2423
+ }).unref();
2424
+ }
2425
+ /**
2426
+ * The detached child's whole job: fetch the channel's current version and
2427
+ * rewrite the cache. Timeout-bounded and silent on failure by design.
2428
+ */
2429
+ async function runUpdateCheckWorker() {
2430
+ try {
2431
+ const channel = resolveChannel(version);
2432
+ const latestVersion = await releaseSourceForInstall(resolveInstallSource()).fetchLatestVersion(channel, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
2433
+ if (!latestVersion) return;
2434
+ writeUpdateCache({
2435
+ lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
2436
+ channel,
2437
+ latestVersion
2438
+ });
2439
+ } catch (_error) {}
2440
+ }
2441
+ /**
2442
+ * Called once from bin.ts before command dispatch. Synchronous — one small
2443
+ * file read; the network work happens in the detached worker.
2444
+ */
2445
+ function setupUpdateCheck(argv) {
2446
+ try {
2447
+ if (isCheckDisabled(process.env)) return;
2448
+ const currentVersion = version;
2449
+ const channel = resolveChannel(currentVersion);
2450
+ const cache = readUpdateCache();
2451
+ if (updateAvailable(cache, currentVersion, channel) && shouldNotify({
2452
+ argv,
2453
+ stderrIsTTY: Boolean(process.stderr.isTTY)
2454
+ })) registerExitNotice(renderUpdateNotice({
2455
+ currentVersion,
2456
+ latestVersion: cache.latestVersion,
2457
+ channel,
2458
+ source: resolveInstallSource()
2459
+ }));
2460
+ if (isCheckDue(cache.lastCheckedAt)) {
2461
+ claimCheckInterval(cache);
2462
+ spawnUpdateCheckWorker();
2463
+ }
2464
+ } catch (_error) {}
2465
+ }
2466
+
2467
+ //#endregion
2468
+ //#region src/changelog.generated.ts
2469
+ const CHANGELOG_MD = "# Changelog\n\nAll notable changes to the Skydive CLI are documented here.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [0.2.0] - 2026-07-29\n\n### Added\n\n**Sandbox & machine access**\n\n- Standalone `skydive sandbox` command for direct access to an agent's sandbox.\n- `/sandbox` in the chat TUI — live PTY session and one-shot command execution.\n- Headless machine sharing via `skydive portal`, with self-registering portal grants.\n- `shareMachineDefault` config key for always-on portal sharing.\n\n**Chat TUI**\n\n- File pane (`ctrl+g`) with Changes and Files tabs: a shared file tree plus source viewer and a workspace browser, mouse-resizable and responsive to terminal size.\n- Agent todo list rendered in the chat TUI, above the composer, mirroring the web chat's todo card.\n- Fuzzy finder in the conversation and agent pickers.\n- Slash-command autocomplete menu.\n- Paste or drag-and-drop any file into the composer; paste clipboard images with Cmd+V.\n- Run local shell commands with `!` in the composer.\n- Conversation recaps, streamed live title updates, and per-agent attribution on assistant turns.\n- Working timer rolls up into minutes and hours.\n- Conversation picker paginates past 50 conversations and shows only your own conversations.\n- Esc leaves a live run; typing `exit` quits the chat.\n- Cursor Dark theme.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n\n**Headless & scripting**\n\n- Resume a conversation by id.\n- `conversations list` and `conversations show` for transcript reads.\n- `messages get`, with run recovery keyed on message id.\n- Connect cards surface in headless `-p` mode so a driving agent never gets stuck.\n\n**Authentication**\n\n- `skydive auth login` via the browser now auto-mints an API key, so one login yields both a chat session and a usable management credential. Management commands announce the key's pinned workspace when it drives them, so a workspace mismatch is visible at use time.\n- Workspace picker on the device authorization page.\n- Account and workspace identity shown in `auth status`.\n\n**Platform**\n\n- Standalone binary builds compiling the CLI into a per-target executable.\n- Interactive workspace switcher; management commands follow the active workspace.\n- Terminal host integrations and agent notifications.\n\n### Fixed\n\n- Transcript errors collapse to one line, click to expand (REST and portal errors keep their full body).\n- Dragged/pasted image file paths attach the file instead of inserting path text, including macOS paths with literal parentheses.\n- Bare URLs in chat markdown are hyperlinked so they survive text wrap.\n- Composer draft is preserved across TUI overlays.\n- Relative connect links resolve before opening the browser.\n- Numbered markdown headings render colored in the TUI.\n- Chat transcript pages by 75% of a screen; picker rows stay on one line.\n- Run starts push to the TUI over the conversation stream.\n- Security: remediated high-severity dependency findings and cleared tar/shell-quote CVEs.\n\n## [0.1.0] - 2026-07-21\n\nInitial public release: `skydive chat` TUI, agent and conversation management,\ndevice authorization, and headless `-p` mode.\n";
2470
+
2471
+ //#endregion
2472
+ //#region src/whats-new.ts
2473
+ /**
2474
+ * Parse keep-a-changelog markdown into version sections. Recognizes
2475
+ * `## [x.y.z] - date` headings; `## [Unreleased]` and prerelease headings
2476
+ * are ignored. Order is preserved (newest first, as the file is written).
2477
+ */
2478
+ function parseChangelog(md) {
2479
+ const entries = [];
2480
+ let current = null;
2481
+ for (const line of md.split("\n")) {
2482
+ const heading = /^## \[(\d+\.\d+\.\d+)\]/.exec(line);
2483
+ if (heading) {
2484
+ current = {
2485
+ version: heading[1] ?? "",
2486
+ body: []
2487
+ };
2488
+ entries.push(current);
2489
+ continue;
2490
+ }
2491
+ if (line.startsWith("## ")) {
2492
+ current = null;
2493
+ continue;
2494
+ }
2495
+ if (current) current.body.push(line);
2496
+ }
2497
+ for (const entry of entries) {
2498
+ while (entry.body.length > 0 && (entry.body[0] ?? "").trim() === "") entry.body.shift();
2499
+ while (entry.body.length > 0 && (entry.body[entry.body.length - 1] ?? "").trim() === "") entry.body.pop();
2500
+ }
2501
+ return entries;
2502
+ }
2503
+ /**
2504
+ * Parse a semver into its numeric triple, ignoring any prerelease/build
2505
+ * suffix (`0.2.0-beta.363` -> [0, 2, 0]). Canary builds thereby count as
2506
+ * their base version, so a beta user stepping onto the stable line isn't
2507
+ * re-shown notes for features they already have. Returns null on anything
2508
+ * that isn't `x.y.z(-…)`.
2509
+ */
2510
+ function parseVersion(v) {
2511
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(v);
2512
+ if (!m) return null;
2513
+ return [
2514
+ Number(m[1]),
2515
+ Number(m[2]),
2516
+ Number(m[3])
2517
+ ];
2518
+ }
2519
+ function compareVersions(a, b) {
2520
+ const pa = parseVersion(a);
2521
+ const pb = parseVersion(b);
2522
+ if (!pa || !pb) return 0;
2523
+ for (let i = 0; i < 3; i++) {
2524
+ const da = pa[i] ?? 0;
2525
+ const db = pb[i] ?? 0;
2526
+ if (da !== db) return da - db;
2527
+ }
2528
+ return 0;
2529
+ }
2530
+ /**
2531
+ * The changelog sections a user upgrading from `lastSeen` to `current`
2532
+ * should see: every entry strictly newer than `lastSeen` and no newer than
2533
+ * `current`, oldest first (reading order).
2534
+ */
2535
+ function entriesBetween(lastSeen, current, entries = parseChangelog(CHANGELOG_MD)) {
2536
+ return entries.filter((e) => compareVersions(e.version, lastSeen) > 0 && compareVersions(e.version, current) <= 0).reverse();
2537
+ }
2538
+ /**
2539
+ * Render one changelog section body for the terminal: `### Group` headings
2540
+ * become `Group:` lines, `**Bold**` intro lines lose their markers, list
2541
+ * bullets become `•`, and nested bullets keep their indent.
2542
+ */
2543
+ function renderBody(body) {
2544
+ const lines = [];
2545
+ for (const raw of body) {
2546
+ const line = raw.replace(/\s+$/, "");
2547
+ if (line.trim() === "") {
2548
+ if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
2549
+ continue;
2550
+ }
2551
+ const groupHeading = /^### (.+)$/.exec(line);
2552
+ if (groupHeading) {
2553
+ lines.push(`${groupHeading[1]}:`);
2554
+ continue;
2555
+ }
2556
+ const bullet = /^(\s*)- (.*)$/.exec(line);
2557
+ if (bullet) {
2558
+ const indent = bullet[1] ?? "";
2559
+ const text = bullet[2] ?? "";
2560
+ lines.push(`${indent} • ${text.replace(/\*\*/g, "")}`);
2561
+ continue;
2562
+ }
2563
+ lines.push(line.replace(/\*\*/g, ""));
2564
+ }
2565
+ return lines;
2566
+ }
2567
+ /** Pure formatting, split out for tests. Returns [] when nothing to show. */
2568
+ function whatsNewLines(lastSeen, current, changelog = CHANGELOG_MD) {
2569
+ if (!lastSeen) return [];
2570
+ if (compareVersions(current, lastSeen) <= 0) return [];
2571
+ const entries = entriesBetween(lastSeen, current, parseChangelog(changelog));
2572
+ if (entries.length === 0) return [];
2573
+ const lines = [];
2574
+ for (const entry of entries) {
2575
+ if (lines.length > 0) lines.push("");
2576
+ lines.push(`What's new in skydive ${entry.version}:`);
2577
+ lines.push("");
2578
+ lines.push(...renderBody(entry.body));
2579
+ }
2580
+ return lines;
2581
+ }
2582
+ /**
2583
+ * Print the what's-new notice when appropriate and record the current
2584
+ * version. Called once from the bin entrypoint, before yargs dispatch.
2585
+ *
2586
+ * Prints to stderr and only when stderr is a TTY, so piped/scripted
2587
+ * invocations (including `--json` consumers) never see it mixed into their
2588
+ * output. Skipped entirely in known non-interactive environments (CI,
2589
+ * coding agents), where the version watermark is still advanced so a later
2590
+ * interactive run doesn't replay stale notes.
2591
+ */
2592
+ function maybePrintWhatsNew({ stderrIsTTY = process.stderr.isTTY ?? false, nonInteractive = isNonInteractive(), currentVersion = version } = {}) {
2593
+ try {
2594
+ const lastSeen = getLastSeenVersion();
2595
+ if (stderrIsTTY && !nonInteractive) {
2596
+ const lines = whatsNewLines(lastSeen, currentVersion);
2597
+ if (lines.length > 0) console.error(lines.join("\n") + "\n");
2598
+ }
2599
+ if (lastSeen !== currentVersion) setLastSeenVersion(currentVersion);
2600
+ } catch (_error) {}
2214
2601
  }
2215
2602
 
2216
2603
  //#endregion
2217
2604
  //#region src/bin.ts
2605
+ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
2606
+ await runUpdateCheckWorker();
2607
+ process.exit(0);
2608
+ }
2609
+ maybePrintWhatsNew();
2610
+ setupUpdateCheck(hideBin(process.argv));
2218
2611
  createCli(resolveArgv(hideBin(process.argv))).parse();
2219
2612
 
2220
2613
  //#endregion
2221
- export { noColorRequested as a, themeMode as c, themesForMode as d, DEFAULT_API_URL as f, saveTheme as g, resolveWebUrl as h, monoTheme as i, themeModeFromColorFgBg as l, getSavedTheme as m, applyTheme as n, theme as o, DEFAULT_APP_URL as p, findTheme as r, themeForMode as s, DEFAULT_THEME_ID as t, themeVersion as u };
2614
+ export { };