skydive-cli 0.1.0-beta.96 → 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/CHANGELOG.md +68 -0
- package/README.md +187 -41
- package/dist/js/api-CDTKq_5Q.mjs +145 -0
- package/dist/js/bin.mjs +1558 -1292
- package/dist/js/{boot-DRWra2vY.mjs → boot-CQLDUeZ9.mjs} +4629 -1024
- package/dist/js/client-B3ZhhF7e.mjs +366 -0
- package/dist/js/client-CpEvH2Pq.mjs +169 -0
- package/dist/js/client-D6NAkL9e.mjs +6 -0
- package/dist/js/output-B4cW10Ph.mjs +27 -0
- package/dist/js/print-B6AO13SA.mjs +5 -0
- package/dist/js/print-DausK_KZ.mjs +484 -0
- package/dist/js/print-share-D7OSxvE2.mjs +41 -0
- package/dist/js/raw-pty-2_VA1kw_.mjs +5 -0
- package/dist/js/raw-pty-GAvxm2ol.mjs +99 -0
- package/dist/js/rest-COkLEZOB.mjs +4 -0
- package/dist/js/rest-CamHVOce.mjs +457 -0
- package/dist/js/theme-CuQhvqzN.mjs +990 -0
- package/dist/js/util-CeisaZVY.mjs +15 -0
- package/package.json +9 -3
- package/dist/js/print-CQ8b7JUC.mjs +0 -745
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 {
|
|
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
|
|
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
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
@@ -412,121 +319,6 @@ async function pollDeviceToken({ appUrl, deviceCode, currentIntervalMs, signal }
|
|
|
412
319
|
}
|
|
413
320
|
}
|
|
414
321
|
|
|
415
|
-
//#endregion
|
|
416
|
-
//#region src/auth/organization.ts
|
|
417
|
-
const workspaceSchema = z.object({
|
|
418
|
-
id: z.string(),
|
|
419
|
-
name: z.string(),
|
|
420
|
-
slug: z.string()
|
|
421
|
-
});
|
|
422
|
-
function authHeaders(sessionToken) {
|
|
423
|
-
return { authorization: `Bearer ${sessionToken}` };
|
|
424
|
-
}
|
|
425
|
-
async function listWorkspaces({ appUrl, sessionToken }) {
|
|
426
|
-
try {
|
|
427
|
-
const res = await fetch(`${appUrl}/api/auth/organization/list`, { headers: authHeaders(sessionToken) });
|
|
428
|
-
if (!res.ok) return err({ message: `failed to list workspaces (${res.status})` });
|
|
429
|
-
const parsed = z.array(workspaceSchema).safeParse(await res.json());
|
|
430
|
-
if (!parsed.success) return err({ message: "unexpected workspace list response" });
|
|
431
|
-
return ok(parsed.data);
|
|
432
|
-
} catch (e) {
|
|
433
|
-
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Resolve who the current chat session belongs to: the signed-in user's
|
|
438
|
-
* email/name and the active workspace. Used by `auth status` so a user running
|
|
439
|
-
* multiple accounts can answer "am I logged into the right one?" without
|
|
440
|
-
* another command. Best-effort: any failure yields nulls rather than throwing,
|
|
441
|
-
* since `auth status` should still report the rest of the state.
|
|
442
|
-
*/
|
|
443
|
-
async function getSessionIdentity({ appUrl, sessionToken }) {
|
|
444
|
-
try {
|
|
445
|
-
const res = await fetch(`${appUrl}/api/auth/get-session?disableCookieCache=true`, { headers: authHeaders(sessionToken) });
|
|
446
|
-
if (!res.ok) return err({ message: `failed to read session (${res.status})` });
|
|
447
|
-
const parsed = z.object({
|
|
448
|
-
user: z.object({
|
|
449
|
-
email: z.string().nullable().optional(),
|
|
450
|
-
name: z.string().nullable().optional()
|
|
451
|
-
}).nullable().optional(),
|
|
452
|
-
session: z.object({ activeOrganizationId: z.string().nullable().optional() }).nullable().optional()
|
|
453
|
-
}).nullable().safeParse(await res.json());
|
|
454
|
-
if (!parsed.success) return err({ message: "unexpected session response" });
|
|
455
|
-
const activeWorkspaceId = parsed.data?.session?.activeOrganizationId ?? null;
|
|
456
|
-
let activeWorkspaceName = null;
|
|
457
|
-
if (activeWorkspaceId) {
|
|
458
|
-
const workspaces = await listWorkspaces({
|
|
459
|
-
appUrl,
|
|
460
|
-
sessionToken
|
|
461
|
-
});
|
|
462
|
-
if (workspaces.isOk()) activeWorkspaceName = workspaces.value.find((w) => w.id === activeWorkspaceId)?.name ?? null;
|
|
463
|
-
}
|
|
464
|
-
return ok({
|
|
465
|
-
email: parsed.data?.user?.email ?? null,
|
|
466
|
-
name: parsed.data?.user?.name ?? null,
|
|
467
|
-
activeWorkspaceId,
|
|
468
|
-
activeWorkspaceName
|
|
469
|
-
});
|
|
470
|
-
} catch (e) {
|
|
471
|
-
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
/** The workspace bound to the current session, or `null` if none is set. */
|
|
475
|
-
async function getActiveWorkspaceId({ appUrl, sessionToken }) {
|
|
476
|
-
try {
|
|
477
|
-
const res = await fetch(`${appUrl}/api/auth/get-session?disableCookieCache=true`, { headers: authHeaders(sessionToken) });
|
|
478
|
-
if (!res.ok) return err({ message: `failed to read session (${res.status})` });
|
|
479
|
-
const parsed = z.object({ session: z.object({ activeOrganizationId: z.string().nullable().optional() }).nullable().optional() }).nullable().safeParse(await res.json());
|
|
480
|
-
if (!parsed.success) return err({ message: "unexpected session response" });
|
|
481
|
-
return ok(parsed.data?.session?.activeOrganizationId ?? null);
|
|
482
|
-
} catch (e) {
|
|
483
|
-
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
async function setActiveWorkspace({ appUrl, sessionToken, organizationId }) {
|
|
487
|
-
try {
|
|
488
|
-
const res = await fetch(`${appUrl}/api/v1/workspaces/switch`, {
|
|
489
|
-
method: "POST",
|
|
490
|
-
headers: {
|
|
491
|
-
...authHeaders(sessionToken),
|
|
492
|
-
"content-type": "application/json"
|
|
493
|
-
},
|
|
494
|
-
body: JSON.stringify({ organizationId })
|
|
495
|
-
});
|
|
496
|
-
if (!res.ok) return err({ message: `failed to switch workspace (${res.status})` });
|
|
497
|
-
return ok(void 0);
|
|
498
|
-
} catch (e) {
|
|
499
|
-
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Ensures the session has an active workspace. The device flow issues a
|
|
504
|
-
* session without one (unlike a normal web sign-in, which sets it), so the
|
|
505
|
-
* internal API rejects every request with "no active organization" until we
|
|
506
|
-
* set it. Picks the account's first workspace by join order — run `skydive
|
|
507
|
-
* workspace list` + `skydive workspace switch` afterward if that's the wrong
|
|
508
|
-
* one (e.g. a personal workspace joined before a shared team workspace).
|
|
509
|
-
*/
|
|
510
|
-
async function ensureActiveOrganization({ appUrl, sessionToken }) {
|
|
511
|
-
const workspaces = await listWorkspaces({
|
|
512
|
-
appUrl,
|
|
513
|
-
sessionToken
|
|
514
|
-
});
|
|
515
|
-
if (workspaces.isErr()) return err(workspaces.error);
|
|
516
|
-
const workspace = workspaces.value[0];
|
|
517
|
-
if (!workspace) return err({ message: "your account has no organization yet" });
|
|
518
|
-
const setResult = await setActiveWorkspace({
|
|
519
|
-
appUrl,
|
|
520
|
-
sessionToken,
|
|
521
|
-
organizationId: workspace.id
|
|
522
|
-
});
|
|
523
|
-
if (setResult.isErr()) return err(setResult.error);
|
|
524
|
-
return ok({
|
|
525
|
-
organizationId: workspace.id,
|
|
526
|
-
name: workspace.name
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
|
|
530
322
|
//#endregion
|
|
531
323
|
//#region src/auth/device-login.ts
|
|
532
324
|
const DEFAULT_INTERVAL_MS = 5e3;
|
|
@@ -606,131 +398,262 @@ function sleep(ms) {
|
|
|
606
398
|
}
|
|
607
399
|
|
|
608
400
|
//#endregion
|
|
609
|
-
//#region src/
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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) });
|
|
618
436
|
}
|
|
619
|
-
console.log(JSON.stringify(data, null, 2));
|
|
620
437
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
+
}
|
|
628
468
|
}
|
|
629
|
-
|
|
630
|
-
|
|
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
|
+
}
|
|
631
486
|
}
|
|
632
487
|
|
|
633
488
|
//#endregion
|
|
634
489
|
//#region src/commands/auth.ts
|
|
635
490
|
const loginCommand = {
|
|
636
491
|
command: "login",
|
|
637
|
-
describe: "
|
|
492
|
+
describe: "Sign in via the browser (use --api-key for CI / headless)",
|
|
638
493
|
builder: (y) => y.option("api-key", {
|
|
639
494
|
type: "string",
|
|
640
|
-
describe: `API key (${API_KEY_PREFIX}...)`
|
|
495
|
+
describe: `Skip the browser and authenticate with an existing API key (${API_KEY_PREFIX}...)`
|
|
641
496
|
}).option("web", {
|
|
642
497
|
type: "boolean",
|
|
643
498
|
default: false,
|
|
644
|
-
|
|
499
|
+
hidden: true,
|
|
500
|
+
describe: "Deprecated: browser sign-in is now the default"
|
|
645
501
|
}),
|
|
646
502
|
handler: async (argv) => {
|
|
647
|
-
if (argv
|
|
648
|
-
await
|
|
503
|
+
if (argv["api-key"]) {
|
|
504
|
+
await runApiKeyLogin(argv, argv["api-key"]);
|
|
649
505
|
return;
|
|
650
506
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
apiKey = await new Promise((resolve) => {
|
|
658
|
-
rl.question(`Enter your API key (from ${API_KEYS_URL}): `, (answer) => {
|
|
659
|
-
rl.close();
|
|
660
|
-
resolve(answer.trim());
|
|
661
|
-
});
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
if (!apiKey) {
|
|
665
|
-
printError("No API key provided.");
|
|
666
|
-
process.exit(1);
|
|
667
|
-
}
|
|
668
|
-
if (!apiKey.startsWith(API_KEY_PREFIX)) {
|
|
669
|
-
printError(`API key must start with ${API_KEY_PREFIX}`);
|
|
670
|
-
process.exit(1);
|
|
671
|
-
}
|
|
672
|
-
const apiUrl = argv["api-url"] ?? DEFAULT_API_URL;
|
|
673
|
-
const result = await new SkydiveApiClient({
|
|
674
|
-
token: apiKey,
|
|
675
|
-
apiUrl
|
|
676
|
-
}).listAgents({ limit: 1 });
|
|
677
|
-
if (result.isErr()) {
|
|
678
|
-
printError(`Invalid API key or unreachable server: ${result.error.message}`);
|
|
679
|
-
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
|
+
}
|
|
680
513
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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, {
|
|
686
564
|
authenticated: true,
|
|
687
|
-
|
|
565
|
+
mode: "browser",
|
|
566
|
+
appUrl,
|
|
567
|
+
prefix: minted.isOk() ? minted.value.prefix : null,
|
|
568
|
+
email: identity?.email ?? null,
|
|
569
|
+
workspaceName: identity?.activeWorkspaceName ?? null,
|
|
688
570
|
configPath: getConfigPath()
|
|
689
571
|
});
|
|
690
|
-
|
|
691
|
-
console.log(`Authenticated successfully.`);
|
|
692
|
-
console.log(` Key: ${apiKey.slice(0, 12)}...`);
|
|
693
|
-
console.log(` Config: ${getConfigPath()}`);
|
|
694
|
-
}
|
|
572
|
+
return;
|
|
695
573
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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}`);
|
|
700
585
|
process.exit(1);
|
|
701
586
|
}
|
|
702
|
-
const
|
|
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
|
+
});
|
|
703
596
|
if (result.isErr()) {
|
|
704
|
-
printError(result.error.message);
|
|
597
|
+
printError(`Invalid API key or unreachable server: ${result.error.message}`);
|
|
705
598
|
process.exit(1);
|
|
706
599
|
}
|
|
600
|
+
saveConfig({
|
|
601
|
+
apiKey,
|
|
602
|
+
apiUrl,
|
|
603
|
+
apiKeyId: null,
|
|
604
|
+
workspaceName: null
|
|
605
|
+
});
|
|
707
606
|
if (argv.json) output(argv, {
|
|
708
607
|
authenticated: true,
|
|
709
|
-
|
|
710
|
-
appUrl: result.value.appUrl,
|
|
608
|
+
prefix: apiKey.slice(0, 12),
|
|
711
609
|
configPath: getConfigPath()
|
|
712
610
|
});
|
|
713
611
|
else if (!argv.quiet) {
|
|
714
|
-
console.log(
|
|
715
|
-
console.log(`
|
|
612
|
+
console.log(`Authenticated successfully.`);
|
|
613
|
+
console.log(` Key: ${apiKey.slice(0, 12)}...`);
|
|
716
614
|
console.log(` Config: ${getConfigPath()}`);
|
|
717
615
|
}
|
|
718
616
|
}
|
|
719
617
|
const logoutCommand = {
|
|
720
618
|
command: "logout",
|
|
721
|
-
describe: "
|
|
619
|
+
describe: "Revoke the auto-minted API key and clear stored credentials",
|
|
722
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
|
+
}
|
|
723
642
|
deleteConfig();
|
|
724
|
-
if (argv.json) output(argv, {
|
|
725
|
-
|
|
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.");
|
|
726
648
|
}
|
|
727
649
|
};
|
|
728
|
-
const statusCommand = {
|
|
650
|
+
const statusCommand$1 = {
|
|
729
651
|
command: "status",
|
|
730
652
|
describe: "Show current authentication state",
|
|
731
653
|
handler: async (argv) => {
|
|
732
654
|
const apiKey = resolveConfig({ apiUrl: argv["api-url"] });
|
|
733
655
|
const session = resolveSession({});
|
|
656
|
+
const management = resolveManagementAuth({ apiUrl: argv["api-url"] });
|
|
734
657
|
const identity = session.isOk() ? (await getSessionIdentity({
|
|
735
658
|
appUrl: session.value.appUrl,
|
|
736
659
|
sessionToken: session.value.sessionToken
|
|
@@ -748,18 +671,22 @@ const statusCommand = {
|
|
|
748
671
|
workspaceId: identity?.activeWorkspaceId ?? null,
|
|
749
672
|
workspaceName: identity?.activeWorkspaceName ?? null
|
|
750
673
|
},
|
|
674
|
+
managementAuth: management.isOk() ? management.value.kind : null,
|
|
675
|
+
keyWorkspaceName: getStoredApiKeyWorkspaceName(),
|
|
751
676
|
configPath: getConfigPath()
|
|
752
677
|
});
|
|
753
678
|
return;
|
|
754
679
|
}
|
|
755
680
|
if (apiKey.isErr() && session.isErr()) {
|
|
756
|
-
console.log("Not authenticated. Run `skydive auth login
|
|
681
|
+
console.log("Not authenticated. Run `skydive auth login`.");
|
|
757
682
|
return;
|
|
758
683
|
}
|
|
759
684
|
if (apiKey.isOk()) {
|
|
760
685
|
console.log("API key:");
|
|
761
686
|
console.log(` Key: ${apiKey.value.apiKey.slice(0, 12)}...`);
|
|
762
687
|
console.log(` API: ${apiKey.value.apiUrl}`);
|
|
688
|
+
const pinned = getStoredApiKeyWorkspaceName();
|
|
689
|
+
if (pinned) console.log(` Space: ${pinned} (the key always acts here)`);
|
|
763
690
|
} else console.log("API key: not configured.");
|
|
764
691
|
if (session.isOk()) {
|
|
765
692
|
console.log("Chat session:");
|
|
@@ -774,29 +701,64 @@ const statusCommand = {
|
|
|
774
701
|
console.log(` Space: ${ws}`);
|
|
775
702
|
}
|
|
776
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
|
+
}
|
|
777
708
|
console.log(`Config: ${getConfigPath()}`);
|
|
778
709
|
}
|
|
779
710
|
};
|
|
780
711
|
const authCommand = {
|
|
781
712
|
command: "auth",
|
|
782
713
|
describe: "Manage authentication",
|
|
783
|
-
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"),
|
|
784
715
|
handler: () => {}
|
|
785
716
|
};
|
|
786
717
|
|
|
787
718
|
//#endregion
|
|
788
|
-
//#region src/commands/
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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.`);
|
|
793
729
|
process.exit(1);
|
|
794
730
|
}
|
|
795
|
-
return
|
|
731
|
+
return session.value;
|
|
796
732
|
}
|
|
797
|
-
|
|
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 = {
|
|
798
760
|
command: "list",
|
|
799
|
-
describe: "List agents",
|
|
761
|
+
describe: "List agents in the active workspace",
|
|
800
762
|
builder: (y) => y.option("limit", {
|
|
801
763
|
type: "number",
|
|
802
764
|
default: 20,
|
|
@@ -807,15 +769,15 @@ const listCommand$3 = {
|
|
|
807
769
|
describe: "Filter scope"
|
|
808
770
|
}),
|
|
809
771
|
handler: async (argv) => {
|
|
810
|
-
const result = await
|
|
772
|
+
const result = await requireManagementClient(argv).listAgents({
|
|
811
773
|
limit: argv.limit,
|
|
812
|
-
scope: argv.scope
|
|
774
|
+
scope: argv.scope ?? null
|
|
813
775
|
});
|
|
814
776
|
if (result.isErr()) {
|
|
815
777
|
printError(result.error.message);
|
|
816
778
|
process.exit(1);
|
|
817
779
|
}
|
|
818
|
-
const { agents } = result.value;
|
|
780
|
+
const { agents, hasMore } = result.value;
|
|
819
781
|
if (argv.json) {
|
|
820
782
|
output(argv, agents);
|
|
821
783
|
return;
|
|
@@ -824,18 +786,52 @@ const listCommand$3 = {
|
|
|
824
786
|
console.log("No agents found.");
|
|
825
787
|
return;
|
|
826
788
|
}
|
|
827
|
-
|
|
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: [
|
|
828
804
|
"Name",
|
|
805
|
+
"Description",
|
|
829
806
|
"URL",
|
|
830
807
|
"Model"
|
|
831
|
-
],
|
|
808
|
+
],
|
|
809
|
+
rows: agents.map((a) => [
|
|
832
810
|
a.name,
|
|
811
|
+
truncate(a.description ?? "", DESCRIPTION_MAX),
|
|
833
812
|
a.url ?? "-",
|
|
834
813
|
a.model ?? "default"
|
|
835
|
-
])
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
|
|
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 = {
|
|
839
835
|
command: "get <id>",
|
|
840
836
|
describe: "Get agent details",
|
|
841
837
|
builder: (y) => y.positional("id", {
|
|
@@ -844,7 +840,7 @@ const getCommand = {
|
|
|
844
840
|
describe: "Agent ID"
|
|
845
841
|
}),
|
|
846
842
|
handler: async (argv) => {
|
|
847
|
-
const result = await
|
|
843
|
+
const result = await requireManagementClient(argv).getAgent(argv.id);
|
|
848
844
|
if (result.isErr()) {
|
|
849
845
|
printError(result.error.message);
|
|
850
846
|
process.exit(1);
|
|
@@ -878,7 +874,7 @@ const createCommand$1 = {
|
|
|
878
874
|
describe: "Model to use"
|
|
879
875
|
}),
|
|
880
876
|
handler: async (argv) => {
|
|
881
|
-
const result = await
|
|
877
|
+
const result = await requireManagementClient(argv).createAgent({
|
|
882
878
|
name: argv.name,
|
|
883
879
|
model: argv.model
|
|
884
880
|
});
|
|
@@ -905,923 +901,32 @@ const createCommand$1 = {
|
|
|
905
901
|
const agentsCommand = {
|
|
906
902
|
command: "agents",
|
|
907
903
|
describe: "Manage agents",
|
|
908
|
-
builder: (y) => y.command(listCommand$
|
|
904
|
+
builder: (y) => y.command(listCommand$4).command(getCommand$1).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
|
|
909
905
|
handler: () => {}
|
|
910
906
|
};
|
|
911
907
|
|
|
912
908
|
//#endregion
|
|
913
|
-
//#region src/
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
if (keys.length === 0) {
|
|
937
|
-
console.log("No API keys found.");
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
printTable([
|
|
941
|
-
"Name",
|
|
942
|
-
"Prefix",
|
|
943
|
-
"Last Used",
|
|
944
|
-
"Created"
|
|
945
|
-
], keys.map((k) => [
|
|
946
|
-
k.name,
|
|
947
|
-
k.prefix,
|
|
948
|
-
k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : "Never",
|
|
949
|
-
new Date(k.createdAt).toLocaleDateString()
|
|
950
|
-
]));
|
|
951
|
-
}
|
|
952
|
-
};
|
|
953
|
-
const createCommand = {
|
|
954
|
-
command: "create <name>",
|
|
955
|
-
describe: "Create a new API key for an agent",
|
|
956
|
-
builder: (y) => y.positional("name", {
|
|
957
|
-
type: "string",
|
|
958
|
-
demandOption: true,
|
|
959
|
-
describe: "Key name"
|
|
960
|
-
}),
|
|
961
|
-
handler: async (argv) => {
|
|
962
|
-
const result = await requireClient$1(argv).createKey(argv["agent-id"], argv.name);
|
|
963
|
-
if (result.isErr()) {
|
|
964
|
-
printError(result.error.message);
|
|
965
|
-
process.exit(1);
|
|
966
|
-
}
|
|
967
|
-
const key = result.value;
|
|
968
|
-
if (argv.json) {
|
|
969
|
-
output(argv, key);
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
|
-
if (argv.quiet) {
|
|
973
|
-
console.log(key.key);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
console.log(`API key created.`);
|
|
977
|
-
console.log(` Name: ${key.name}`);
|
|
978
|
-
console.log(` Key: ${key.key}`);
|
|
979
|
-
console.log("");
|
|
980
|
-
console.log(" Save this key — it will not be shown again.");
|
|
981
|
-
}
|
|
982
|
-
};
|
|
983
|
-
const revokeCommand = {
|
|
984
|
-
command: "revoke <id>",
|
|
985
|
-
describe: "Revoke an API key",
|
|
986
|
-
builder: (y) => y.positional("id", {
|
|
987
|
-
type: "string",
|
|
988
|
-
demandOption: true,
|
|
989
|
-
describe: "Key ID"
|
|
990
|
-
}),
|
|
991
|
-
handler: async (argv) => {
|
|
992
|
-
const result = await requireClient$1(argv).revokeKey(argv["agent-id"], argv.id);
|
|
993
|
-
if (result.isErr()) {
|
|
994
|
-
printError(result.error.message);
|
|
995
|
-
process.exit(1);
|
|
996
|
-
}
|
|
997
|
-
if (argv.json) output(argv, {
|
|
998
|
-
revoked: true,
|
|
999
|
-
id: argv.id
|
|
1000
|
-
});
|
|
1001
|
-
else if (!argv.quiet) console.log(`Key ${argv.id} revoked.`);
|
|
1002
|
-
}
|
|
1003
|
-
};
|
|
1004
|
-
const keysCommand = {
|
|
1005
|
-
command: "keys",
|
|
1006
|
-
describe: "Manage API keys for an agent",
|
|
1007
|
-
builder: (y) => y.option("agent-id", {
|
|
1008
|
-
type: "string",
|
|
1009
|
-
demandOption: true,
|
|
1010
|
-
describe: "Agent ID"
|
|
1011
|
-
}).command(listCommand$2).command(createCommand).command(revokeCommand).demandCommand(1, "Specify a subcommand: list, create, revoke"),
|
|
1012
|
-
handler: () => {}
|
|
1013
|
-
};
|
|
1014
|
-
|
|
1015
|
-
//#endregion
|
|
1016
|
-
//#region src/commands/secrets.ts
|
|
1017
|
-
function requireClient(argv) {
|
|
1018
|
-
const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
|
|
1019
|
-
if (result.isErr()) {
|
|
1020
|
-
printError(result.error.message);
|
|
1021
|
-
process.exit(1);
|
|
1022
|
-
}
|
|
1023
|
-
return new SkydiveApiClient(result.value);
|
|
1024
|
-
}
|
|
1025
|
-
/** Read all of stdin as UTF-8, trimming a single trailing newline. */
|
|
1026
|
-
async function readStdin() {
|
|
1027
|
-
const chunks = [];
|
|
1028
|
-
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1029
|
-
return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
1030
|
-
}
|
|
1031
|
-
const listCommand$1 = {
|
|
1032
|
-
command: "list",
|
|
1033
|
-
describe: "List secret names for an agent (values are never shown)",
|
|
1034
|
-
handler: async (argv) => {
|
|
1035
|
-
const result = await requireClient(argv).listSecrets(argv["agent-id"]);
|
|
1036
|
-
if (result.isErr()) {
|
|
1037
|
-
printError(result.error.message);
|
|
1038
|
-
process.exit(1);
|
|
1039
|
-
}
|
|
1040
|
-
const keys = result.value;
|
|
1041
|
-
if (argv.json) {
|
|
1042
|
-
output(argv, keys);
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
if (keys.length === 0) {
|
|
1046
|
-
console.log("No secrets found.");
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
printTable(["Name"], keys.map((k) => [k]));
|
|
1050
|
-
}
|
|
1051
|
-
};
|
|
1052
|
-
const setCommand = {
|
|
1053
|
-
command: "set <key> [value]",
|
|
1054
|
-
describe: "Set (create or overwrite) a secret. Reads value from stdin if omitted.",
|
|
1055
|
-
builder: (y) => y.positional("key", {
|
|
1056
|
-
type: "string",
|
|
1057
|
-
demandOption: true,
|
|
1058
|
-
describe: "Secret name (uppercase letters, digits, underscores)"
|
|
1059
|
-
}).positional("value", {
|
|
1060
|
-
type: "string",
|
|
1061
|
-
describe: "Secret value. If omitted, read from stdin — keeps the value out of shell history."
|
|
1062
|
-
}),
|
|
1063
|
-
handler: async (argv) => {
|
|
1064
|
-
let value = argv.value;
|
|
1065
|
-
if (value === void 0) {
|
|
1066
|
-
if (process.stdin.isTTY) {
|
|
1067
|
-
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>`.");
|
|
1068
|
-
process.exit(1);
|
|
1069
|
-
}
|
|
1070
|
-
value = await readStdin();
|
|
1071
|
-
}
|
|
1072
|
-
if (!value) {
|
|
1073
|
-
printError("Empty secret value.");
|
|
1074
|
-
process.exit(1);
|
|
1075
|
-
}
|
|
1076
|
-
const result = await requireClient(argv).setSecret(argv["agent-id"], argv.key, value);
|
|
1077
|
-
if (result.isErr()) {
|
|
1078
|
-
printError(result.error.message);
|
|
1079
|
-
process.exit(1);
|
|
1080
|
-
}
|
|
1081
|
-
if (argv.json) output(argv, {
|
|
1082
|
-
key: result.value,
|
|
1083
|
-
set: true
|
|
1084
|
-
});
|
|
1085
|
-
else if (!argv.quiet) console.log(`Secret ${result.value} set.`);
|
|
1086
|
-
}
|
|
1087
|
-
};
|
|
1088
|
-
const rmCommand = {
|
|
1089
|
-
command: "rm <key>",
|
|
1090
|
-
aliases: ["delete"],
|
|
1091
|
-
describe: "Remove a secret from the agent",
|
|
1092
|
-
builder: (y) => y.positional("key", {
|
|
1093
|
-
type: "string",
|
|
1094
|
-
demandOption: true,
|
|
1095
|
-
describe: "Secret name"
|
|
1096
|
-
}),
|
|
1097
|
-
handler: async (argv) => {
|
|
1098
|
-
const result = await requireClient(argv).deleteSecret(argv["agent-id"], argv.key);
|
|
1099
|
-
if (result.isErr()) {
|
|
1100
|
-
printError(result.error.message);
|
|
1101
|
-
process.exit(1);
|
|
1102
|
-
}
|
|
1103
|
-
if (argv.json) output(argv, {
|
|
1104
|
-
key: argv.key,
|
|
1105
|
-
removed: true
|
|
1106
|
-
});
|
|
1107
|
-
else if (!argv.quiet) console.log(`Secret ${argv.key} removed.`);
|
|
1108
|
-
}
|
|
1109
|
-
};
|
|
1110
|
-
const secretsCommand = {
|
|
1111
|
-
command: "secrets",
|
|
1112
|
-
describe: "Manage an agent’s secrets",
|
|
1113
|
-
builder: (y) => y.option("agent-id", {
|
|
1114
|
-
type: "string",
|
|
1115
|
-
demandOption: true,
|
|
1116
|
-
describe: "Agent ID"
|
|
1117
|
-
}).command(listCommand$1).command(setCommand).command(rmCommand).demandCommand(1, "Specify a subcommand: list, set, rm"),
|
|
1118
|
-
handler: () => {}
|
|
1119
|
-
};
|
|
1120
|
-
|
|
1121
|
-
//#endregion
|
|
1122
|
-
//#region src/chat/tui/theme.ts
|
|
1123
|
-
const tokyonight = {
|
|
1124
|
-
id: "tokyonight",
|
|
1125
|
-
label: "Tokyo Night",
|
|
1126
|
-
mode: "dark",
|
|
1127
|
-
palette: {
|
|
1128
|
-
fg: "#c0caf5",
|
|
1129
|
-
muted: "#7a7a7a",
|
|
1130
|
-
dim: "#565f89",
|
|
1131
|
-
faint: "#3b4261",
|
|
1132
|
-
surface: "#16161e",
|
|
1133
|
-
accent: "#7aa2f7",
|
|
1134
|
-
success: "#9ece6a",
|
|
1135
|
-
warning: "#e0af68",
|
|
1136
|
-
error: "#f7768e",
|
|
1137
|
-
user: "#7aa2f7",
|
|
1138
|
-
assistant: "#c0caf5",
|
|
1139
|
-
tool: "#bb9af7",
|
|
1140
|
-
reasoning: "#565f89",
|
|
1141
|
-
syntaxBlue: "#7aa2f7",
|
|
1142
|
-
syntaxCyan: "#7dcfff",
|
|
1143
|
-
syntaxTeal: "#73daca",
|
|
1144
|
-
syntaxGreen: "#9ece6a",
|
|
1145
|
-
syntaxYellow: "#e0af68",
|
|
1146
|
-
syntaxOrange: "#ff9e64",
|
|
1147
|
-
syntaxRed: "#f7768e",
|
|
1148
|
-
syntaxMagenta: "#bb9af7"
|
|
1149
|
-
}
|
|
1150
|
-
};
|
|
1151
|
-
const tokyonightDay = {
|
|
1152
|
-
id: "tokyonight-day",
|
|
1153
|
-
label: "Tokyo Night Day",
|
|
1154
|
-
mode: "light",
|
|
1155
|
-
palette: {
|
|
1156
|
-
fg: "#3760bf",
|
|
1157
|
-
muted: "#848cb5",
|
|
1158
|
-
dim: "#9da3c2",
|
|
1159
|
-
faint: "#c4c8da",
|
|
1160
|
-
surface: "#d0d1d8",
|
|
1161
|
-
accent: "#2e7de9",
|
|
1162
|
-
success: "#587539",
|
|
1163
|
-
warning: "#8c6c3e",
|
|
1164
|
-
error: "#f52a65",
|
|
1165
|
-
user: "#2e7de9",
|
|
1166
|
-
assistant: "#3760bf",
|
|
1167
|
-
tool: "#9854f1",
|
|
1168
|
-
reasoning: "#848cb5",
|
|
1169
|
-
syntaxBlue: "#2e7de9",
|
|
1170
|
-
syntaxCyan: "#007197",
|
|
1171
|
-
syntaxTeal: "#118c74",
|
|
1172
|
-
syntaxGreen: "#587539",
|
|
1173
|
-
syntaxYellow: "#8c6c3e",
|
|
1174
|
-
syntaxOrange: "#b15c00",
|
|
1175
|
-
syntaxRed: "#f52a65",
|
|
1176
|
-
syntaxMagenta: "#9854f1"
|
|
1177
|
-
}
|
|
1178
|
-
};
|
|
1179
|
-
const catppuccinMocha = {
|
|
1180
|
-
id: "catppuccin-mocha",
|
|
1181
|
-
label: "Catppuccin Mocha",
|
|
1182
|
-
mode: "dark",
|
|
1183
|
-
palette: {
|
|
1184
|
-
fg: "#cdd6f4",
|
|
1185
|
-
muted: "#7f849c",
|
|
1186
|
-
dim: "#6c7086",
|
|
1187
|
-
faint: "#45475a",
|
|
1188
|
-
surface: "#181825",
|
|
1189
|
-
accent: "#89b4fa",
|
|
1190
|
-
success: "#a6e3a1",
|
|
1191
|
-
warning: "#f9e2af",
|
|
1192
|
-
error: "#f38ba8",
|
|
1193
|
-
user: "#89b4fa",
|
|
1194
|
-
assistant: "#cdd6f4",
|
|
1195
|
-
tool: "#cba6f7",
|
|
1196
|
-
reasoning: "#6c7086",
|
|
1197
|
-
syntaxBlue: "#89b4fa",
|
|
1198
|
-
syntaxCyan: "#89dceb",
|
|
1199
|
-
syntaxTeal: "#94e2d5",
|
|
1200
|
-
syntaxGreen: "#a6e3a1",
|
|
1201
|
-
syntaxYellow: "#f9e2af",
|
|
1202
|
-
syntaxOrange: "#fab387",
|
|
1203
|
-
syntaxRed: "#f38ba8",
|
|
1204
|
-
syntaxMagenta: "#cba6f7"
|
|
1205
|
-
}
|
|
1206
|
-
};
|
|
1207
|
-
const catppuccinLatte = {
|
|
1208
|
-
id: "catppuccin-latte",
|
|
1209
|
-
label: "Catppuccin Latte",
|
|
1210
|
-
mode: "light",
|
|
1211
|
-
palette: {
|
|
1212
|
-
fg: "#4c4f69",
|
|
1213
|
-
muted: "#8c8fa1",
|
|
1214
|
-
dim: "#9ca0b0",
|
|
1215
|
-
faint: "#bcc0cc",
|
|
1216
|
-
surface: "#e6e9ef",
|
|
1217
|
-
accent: "#1e66f5",
|
|
1218
|
-
success: "#40a02b",
|
|
1219
|
-
warning: "#df8e1d",
|
|
1220
|
-
error: "#d20f39",
|
|
1221
|
-
user: "#1e66f5",
|
|
1222
|
-
assistant: "#4c4f69",
|
|
1223
|
-
tool: "#8839ef",
|
|
1224
|
-
reasoning: "#9ca0b0",
|
|
1225
|
-
syntaxBlue: "#1e66f5",
|
|
1226
|
-
syntaxCyan: "#04a5e5",
|
|
1227
|
-
syntaxTeal: "#179299",
|
|
1228
|
-
syntaxGreen: "#40a02b",
|
|
1229
|
-
syntaxYellow: "#df8e1d",
|
|
1230
|
-
syntaxOrange: "#fe640b",
|
|
1231
|
-
syntaxRed: "#d20f39",
|
|
1232
|
-
syntaxMagenta: "#8839ef"
|
|
1233
|
-
}
|
|
1234
|
-
};
|
|
1235
|
-
const gruvboxDark = {
|
|
1236
|
-
id: "gruvbox-dark",
|
|
1237
|
-
label: "Gruvbox Dark",
|
|
1238
|
-
mode: "dark",
|
|
1239
|
-
palette: {
|
|
1240
|
-
fg: "#ebdbb2",
|
|
1241
|
-
muted: "#928374",
|
|
1242
|
-
dim: "#7c6f64",
|
|
1243
|
-
faint: "#504945",
|
|
1244
|
-
surface: "#1d2021",
|
|
1245
|
-
accent: "#83a598",
|
|
1246
|
-
success: "#b8bb26",
|
|
1247
|
-
warning: "#fabd2f",
|
|
1248
|
-
error: "#fb4934",
|
|
1249
|
-
user: "#83a598",
|
|
1250
|
-
assistant: "#ebdbb2",
|
|
1251
|
-
tool: "#d3869b",
|
|
1252
|
-
reasoning: "#7c6f64",
|
|
1253
|
-
syntaxBlue: "#83a598",
|
|
1254
|
-
syntaxCyan: "#8ec07c",
|
|
1255
|
-
syntaxTeal: "#8ec07c",
|
|
1256
|
-
syntaxGreen: "#b8bb26",
|
|
1257
|
-
syntaxYellow: "#fabd2f",
|
|
1258
|
-
syntaxOrange: "#fe8019",
|
|
1259
|
-
syntaxRed: "#fb4934",
|
|
1260
|
-
syntaxMagenta: "#d3869b"
|
|
1261
|
-
}
|
|
1262
|
-
};
|
|
1263
|
-
const gruvboxLight = {
|
|
1264
|
-
id: "gruvbox-light",
|
|
1265
|
-
label: "Gruvbox Light",
|
|
1266
|
-
mode: "light",
|
|
1267
|
-
palette: {
|
|
1268
|
-
fg: "#3c3836",
|
|
1269
|
-
muted: "#928374",
|
|
1270
|
-
dim: "#a89984",
|
|
1271
|
-
faint: "#d5c4a1",
|
|
1272
|
-
surface: "#ebdbb2",
|
|
1273
|
-
accent: "#076678",
|
|
1274
|
-
success: "#79740e",
|
|
1275
|
-
warning: "#b57614",
|
|
1276
|
-
error: "#9d0006",
|
|
1277
|
-
user: "#076678",
|
|
1278
|
-
assistant: "#3c3836",
|
|
1279
|
-
tool: "#8f3f71",
|
|
1280
|
-
reasoning: "#a89984",
|
|
1281
|
-
syntaxBlue: "#076678",
|
|
1282
|
-
syntaxCyan: "#427b58",
|
|
1283
|
-
syntaxTeal: "#427b58",
|
|
1284
|
-
syntaxGreen: "#79740e",
|
|
1285
|
-
syntaxYellow: "#b57614",
|
|
1286
|
-
syntaxOrange: "#af3a03",
|
|
1287
|
-
syntaxRed: "#9d0006",
|
|
1288
|
-
syntaxMagenta: "#8f3f71"
|
|
1289
|
-
}
|
|
1290
|
-
};
|
|
1291
|
-
const solarizedDark = {
|
|
1292
|
-
id: "solarized-dark",
|
|
1293
|
-
label: "Solarized Dark",
|
|
1294
|
-
mode: "dark",
|
|
1295
|
-
palette: {
|
|
1296
|
-
fg: "#93a1a1",
|
|
1297
|
-
muted: "#586e75",
|
|
1298
|
-
dim: "#586e75",
|
|
1299
|
-
faint: "#073642",
|
|
1300
|
-
surface: "#00212b",
|
|
1301
|
-
accent: "#268bd2",
|
|
1302
|
-
success: "#859900",
|
|
1303
|
-
warning: "#b58900",
|
|
1304
|
-
error: "#dc322f",
|
|
1305
|
-
user: "#268bd2",
|
|
1306
|
-
assistant: "#93a1a1",
|
|
1307
|
-
tool: "#6c71c4",
|
|
1308
|
-
reasoning: "#586e75",
|
|
1309
|
-
syntaxBlue: "#268bd2",
|
|
1310
|
-
syntaxCyan: "#2aa198",
|
|
1311
|
-
syntaxTeal: "#2aa198",
|
|
1312
|
-
syntaxGreen: "#859900",
|
|
1313
|
-
syntaxYellow: "#b58900",
|
|
1314
|
-
syntaxOrange: "#cb4b16",
|
|
1315
|
-
syntaxRed: "#dc322f",
|
|
1316
|
-
syntaxMagenta: "#6c71c4"
|
|
1317
|
-
}
|
|
1318
|
-
};
|
|
1319
|
-
const solarizedLight = {
|
|
1320
|
-
id: "solarized-light",
|
|
1321
|
-
label: "Solarized Light",
|
|
1322
|
-
mode: "light",
|
|
1323
|
-
palette: {
|
|
1324
|
-
fg: "#657b83",
|
|
1325
|
-
muted: "#839496",
|
|
1326
|
-
dim: "#93a1a1",
|
|
1327
|
-
faint: "#eee8d5",
|
|
1328
|
-
surface: "#eee8d5",
|
|
1329
|
-
accent: "#268bd2",
|
|
1330
|
-
success: "#859900",
|
|
1331
|
-
warning: "#b58900",
|
|
1332
|
-
error: "#dc322f",
|
|
1333
|
-
user: "#268bd2",
|
|
1334
|
-
assistant: "#657b83",
|
|
1335
|
-
tool: "#6c71c4",
|
|
1336
|
-
reasoning: "#93a1a1",
|
|
1337
|
-
syntaxBlue: "#268bd2",
|
|
1338
|
-
syntaxCyan: "#2aa198",
|
|
1339
|
-
syntaxTeal: "#2aa198",
|
|
1340
|
-
syntaxGreen: "#859900",
|
|
1341
|
-
syntaxYellow: "#b58900",
|
|
1342
|
-
syntaxOrange: "#cb4b16",
|
|
1343
|
-
syntaxRed: "#dc322f",
|
|
1344
|
-
syntaxMagenta: "#6c71c4"
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1347
|
-
const nord = {
|
|
1348
|
-
id: "nord",
|
|
1349
|
-
label: "Nord",
|
|
1350
|
-
mode: "dark",
|
|
1351
|
-
palette: {
|
|
1352
|
-
fg: "#d8dee9",
|
|
1353
|
-
muted: "#616e88",
|
|
1354
|
-
dim: "#4c566a",
|
|
1355
|
-
faint: "#3b4252",
|
|
1356
|
-
surface: "#272c36",
|
|
1357
|
-
accent: "#88c0d0",
|
|
1358
|
-
success: "#a3be8c",
|
|
1359
|
-
warning: "#ebcb8b",
|
|
1360
|
-
error: "#bf616a",
|
|
1361
|
-
user: "#88c0d0",
|
|
1362
|
-
assistant: "#d8dee9",
|
|
1363
|
-
tool: "#b48ead",
|
|
1364
|
-
reasoning: "#4c566a",
|
|
1365
|
-
syntaxBlue: "#81a1c1",
|
|
1366
|
-
syntaxCyan: "#88c0d0",
|
|
1367
|
-
syntaxTeal: "#8fbcbb",
|
|
1368
|
-
syntaxGreen: "#a3be8c",
|
|
1369
|
-
syntaxYellow: "#ebcb8b",
|
|
1370
|
-
syntaxOrange: "#d08770",
|
|
1371
|
-
syntaxRed: "#bf616a",
|
|
1372
|
-
syntaxMagenta: "#b48ead"
|
|
1373
|
-
}
|
|
1374
|
-
};
|
|
1375
|
-
const dracula = {
|
|
1376
|
-
id: "dracula",
|
|
1377
|
-
label: "Dracula",
|
|
1378
|
-
mode: "dark",
|
|
1379
|
-
palette: {
|
|
1380
|
-
fg: "#f8f8f2",
|
|
1381
|
-
muted: "#6272a4",
|
|
1382
|
-
dim: "#6272a4",
|
|
1383
|
-
faint: "#44475a",
|
|
1384
|
-
surface: "#21222c",
|
|
1385
|
-
accent: "#bd93f9",
|
|
1386
|
-
success: "#50fa7b",
|
|
1387
|
-
warning: "#ffb86c",
|
|
1388
|
-
error: "#ff5555",
|
|
1389
|
-
user: "#bd93f9",
|
|
1390
|
-
assistant: "#f8f8f2",
|
|
1391
|
-
tool: "#ff79c6",
|
|
1392
|
-
reasoning: "#6272a4",
|
|
1393
|
-
syntaxBlue: "#bd93f9",
|
|
1394
|
-
syntaxCyan: "#8be9fd",
|
|
1395
|
-
syntaxTeal: "#8be9fd",
|
|
1396
|
-
syntaxGreen: "#50fa7b",
|
|
1397
|
-
syntaxYellow: "#f1fa8c",
|
|
1398
|
-
syntaxOrange: "#ffb86c",
|
|
1399
|
-
syntaxRed: "#ff5555",
|
|
1400
|
-
syntaxMagenta: "#ff79c6"
|
|
1401
|
-
}
|
|
1402
|
-
};
|
|
1403
|
-
const oneDark = {
|
|
1404
|
-
id: "one-dark",
|
|
1405
|
-
label: "One Dark",
|
|
1406
|
-
mode: "dark",
|
|
1407
|
-
palette: {
|
|
1408
|
-
fg: "#abb2bf",
|
|
1409
|
-
muted: "#5c6370",
|
|
1410
|
-
dim: "#5c6370",
|
|
1411
|
-
faint: "#3e4451",
|
|
1412
|
-
surface: "#21252b",
|
|
1413
|
-
accent: "#61afef",
|
|
1414
|
-
success: "#98c379",
|
|
1415
|
-
warning: "#e5c07b",
|
|
1416
|
-
error: "#e06c75",
|
|
1417
|
-
user: "#61afef",
|
|
1418
|
-
assistant: "#abb2bf",
|
|
1419
|
-
tool: "#c678dd",
|
|
1420
|
-
reasoning: "#5c6370",
|
|
1421
|
-
syntaxBlue: "#61afef",
|
|
1422
|
-
syntaxCyan: "#56b6c2",
|
|
1423
|
-
syntaxTeal: "#56b6c2",
|
|
1424
|
-
syntaxGreen: "#98c379",
|
|
1425
|
-
syntaxYellow: "#e5c07b",
|
|
1426
|
-
syntaxOrange: "#d19a66",
|
|
1427
|
-
syntaxRed: "#e06c75",
|
|
1428
|
-
syntaxMagenta: "#c678dd"
|
|
1429
|
-
}
|
|
1430
|
-
};
|
|
1431
|
-
const oneLight = {
|
|
1432
|
-
id: "one-light",
|
|
1433
|
-
label: "One Light",
|
|
1434
|
-
mode: "light",
|
|
1435
|
-
palette: {
|
|
1436
|
-
fg: "#383a42",
|
|
1437
|
-
muted: "#a0a1a7",
|
|
1438
|
-
dim: "#a0a1a7",
|
|
1439
|
-
faint: "#e5e5e6",
|
|
1440
|
-
surface: "#f0f0f1",
|
|
1441
|
-
accent: "#4078f2",
|
|
1442
|
-
success: "#50a14f",
|
|
1443
|
-
warning: "#c18401",
|
|
1444
|
-
error: "#e45649",
|
|
1445
|
-
user: "#4078f2",
|
|
1446
|
-
assistant: "#383a42",
|
|
1447
|
-
tool: "#a626a4",
|
|
1448
|
-
reasoning: "#a0a1a7",
|
|
1449
|
-
syntaxBlue: "#4078f2",
|
|
1450
|
-
syntaxCyan: "#0184bc",
|
|
1451
|
-
syntaxTeal: "#0184bc",
|
|
1452
|
-
syntaxGreen: "#50a14f",
|
|
1453
|
-
syntaxYellow: "#c18401",
|
|
1454
|
-
syntaxOrange: "#986801",
|
|
1455
|
-
syntaxRed: "#e45649",
|
|
1456
|
-
syntaxMagenta: "#a626a4"
|
|
1457
|
-
}
|
|
1458
|
-
};
|
|
1459
|
-
const rosePine = {
|
|
1460
|
-
id: "rose-pine",
|
|
1461
|
-
label: "Rosé Pine",
|
|
1462
|
-
mode: "dark",
|
|
1463
|
-
palette: {
|
|
1464
|
-
fg: "#e0def4",
|
|
1465
|
-
muted: "#908caa",
|
|
1466
|
-
dim: "#6e6a86",
|
|
1467
|
-
faint: "#403d52",
|
|
1468
|
-
surface: "#16141f",
|
|
1469
|
-
accent: "#c4a7e7",
|
|
1470
|
-
success: "#9ccfd8",
|
|
1471
|
-
warning: "#f6c177",
|
|
1472
|
-
error: "#eb6f92",
|
|
1473
|
-
user: "#c4a7e7",
|
|
1474
|
-
assistant: "#e0def4",
|
|
1475
|
-
tool: "#ebbcba",
|
|
1476
|
-
reasoning: "#908caa",
|
|
1477
|
-
syntaxBlue: "#9ccfd8",
|
|
1478
|
-
syntaxCyan: "#9ccfd8",
|
|
1479
|
-
syntaxTeal: "#31748f",
|
|
1480
|
-
syntaxGreen: "#31748f",
|
|
1481
|
-
syntaxYellow: "#f6c177",
|
|
1482
|
-
syntaxOrange: "#ebbcba",
|
|
1483
|
-
syntaxRed: "#eb6f92",
|
|
1484
|
-
syntaxMagenta: "#c4a7e7"
|
|
1485
|
-
}
|
|
1486
|
-
};
|
|
1487
|
-
const rosePineDawn = {
|
|
1488
|
-
id: "rose-pine-dawn",
|
|
1489
|
-
label: "Rosé Pine Dawn",
|
|
1490
|
-
mode: "light",
|
|
1491
|
-
palette: {
|
|
1492
|
-
fg: "#575279",
|
|
1493
|
-
muted: "#797593",
|
|
1494
|
-
dim: "#9893a5",
|
|
1495
|
-
faint: "#cecacd",
|
|
1496
|
-
surface: "#f2e9e1",
|
|
1497
|
-
accent: "#907aa9",
|
|
1498
|
-
success: "#56949f",
|
|
1499
|
-
warning: "#ea9d34",
|
|
1500
|
-
error: "#b4637a",
|
|
1501
|
-
user: "#907aa9",
|
|
1502
|
-
assistant: "#575279",
|
|
1503
|
-
tool: "#d7827e",
|
|
1504
|
-
reasoning: "#9893a5",
|
|
1505
|
-
syntaxBlue: "#56949f",
|
|
1506
|
-
syntaxCyan: "#56949f",
|
|
1507
|
-
syntaxTeal: "#286983",
|
|
1508
|
-
syntaxGreen: "#286983",
|
|
1509
|
-
syntaxYellow: "#ea9d34",
|
|
1510
|
-
syntaxOrange: "#d7827e",
|
|
1511
|
-
syntaxRed: "#b4637a",
|
|
1512
|
-
syntaxMagenta: "#907aa9"
|
|
1513
|
-
}
|
|
1514
|
-
};
|
|
1515
|
-
const everforestDark = {
|
|
1516
|
-
id: "everforest-dark",
|
|
1517
|
-
label: "Everforest Dark",
|
|
1518
|
-
mode: "dark",
|
|
1519
|
-
palette: {
|
|
1520
|
-
fg: "#d3c6aa",
|
|
1521
|
-
muted: "#859289",
|
|
1522
|
-
dim: "#7a8478",
|
|
1523
|
-
faint: "#414b50",
|
|
1524
|
-
surface: "#232a2e",
|
|
1525
|
-
accent: "#7fbbb3",
|
|
1526
|
-
success: "#a7c080",
|
|
1527
|
-
warning: "#dbbc7f",
|
|
1528
|
-
error: "#e67e80",
|
|
1529
|
-
user: "#7fbbb3",
|
|
1530
|
-
assistant: "#d3c6aa",
|
|
1531
|
-
tool: "#d699b6",
|
|
1532
|
-
reasoning: "#7a8478",
|
|
1533
|
-
syntaxBlue: "#7fbbb3",
|
|
1534
|
-
syntaxCyan: "#83c092",
|
|
1535
|
-
syntaxTeal: "#83c092",
|
|
1536
|
-
syntaxGreen: "#a7c080",
|
|
1537
|
-
syntaxYellow: "#dbbc7f",
|
|
1538
|
-
syntaxOrange: "#e69875",
|
|
1539
|
-
syntaxRed: "#e67e80",
|
|
1540
|
-
syntaxMagenta: "#d699b6"
|
|
1541
|
-
}
|
|
1542
|
-
};
|
|
1543
|
-
const everforestLight = {
|
|
1544
|
-
id: "everforest-light",
|
|
1545
|
-
label: "Everforest Light",
|
|
1546
|
-
mode: "light",
|
|
1547
|
-
palette: {
|
|
1548
|
-
fg: "#5c6a72",
|
|
1549
|
-
muted: "#939f91",
|
|
1550
|
-
dim: "#a6b0a0",
|
|
1551
|
-
faint: "#e0dcc7",
|
|
1552
|
-
surface: "#f4f0d9",
|
|
1553
|
-
accent: "#3a94c5",
|
|
1554
|
-
success: "#8da101",
|
|
1555
|
-
warning: "#dfa000",
|
|
1556
|
-
error: "#f85552",
|
|
1557
|
-
user: "#3a94c5",
|
|
1558
|
-
assistant: "#5c6a72",
|
|
1559
|
-
tool: "#df69ba",
|
|
1560
|
-
reasoning: "#a6b0a0",
|
|
1561
|
-
syntaxBlue: "#3a94c5",
|
|
1562
|
-
syntaxCyan: "#35a77c",
|
|
1563
|
-
syntaxTeal: "#35a77c",
|
|
1564
|
-
syntaxGreen: "#8da101",
|
|
1565
|
-
syntaxYellow: "#dfa000",
|
|
1566
|
-
syntaxOrange: "#f57d26",
|
|
1567
|
-
syntaxRed: "#f85552",
|
|
1568
|
-
syntaxMagenta: "#df69ba"
|
|
1569
|
-
}
|
|
1570
|
-
};
|
|
1571
|
-
const githubDark = {
|
|
1572
|
-
id: "github-dark",
|
|
1573
|
-
label: "GitHub Dark",
|
|
1574
|
-
mode: "dark",
|
|
1575
|
-
palette: {
|
|
1576
|
-
fg: "#c9d1d9",
|
|
1577
|
-
muted: "#8b949e",
|
|
1578
|
-
dim: "#6e7681",
|
|
1579
|
-
faint: "#30363d",
|
|
1580
|
-
surface: "#161b22",
|
|
1581
|
-
accent: "#58a6ff",
|
|
1582
|
-
success: "#3fb950",
|
|
1583
|
-
warning: "#d29922",
|
|
1584
|
-
error: "#f85149",
|
|
1585
|
-
user: "#58a6ff",
|
|
1586
|
-
assistant: "#c9d1d9",
|
|
1587
|
-
tool: "#bc8cff",
|
|
1588
|
-
reasoning: "#6e7681",
|
|
1589
|
-
syntaxBlue: "#58a6ff",
|
|
1590
|
-
syntaxCyan: "#39c5cf",
|
|
1591
|
-
syntaxTeal: "#39c5cf",
|
|
1592
|
-
syntaxGreen: "#3fb950",
|
|
1593
|
-
syntaxYellow: "#d29922",
|
|
1594
|
-
syntaxOrange: "#db6d28",
|
|
1595
|
-
syntaxRed: "#f85149",
|
|
1596
|
-
syntaxMagenta: "#bc8cff"
|
|
1597
|
-
}
|
|
1598
|
-
};
|
|
1599
|
-
const githubLight = {
|
|
1600
|
-
id: "github-light",
|
|
1601
|
-
label: "GitHub Light",
|
|
1602
|
-
mode: "light",
|
|
1603
|
-
palette: {
|
|
1604
|
-
fg: "#24292f",
|
|
1605
|
-
muted: "#57606a",
|
|
1606
|
-
dim: "#8c959f",
|
|
1607
|
-
faint: "#d0d7de",
|
|
1608
|
-
surface: "#f6f8fa",
|
|
1609
|
-
accent: "#0969da",
|
|
1610
|
-
success: "#1a7f37",
|
|
1611
|
-
warning: "#9a6700",
|
|
1612
|
-
error: "#cf222e",
|
|
1613
|
-
user: "#0969da",
|
|
1614
|
-
assistant: "#24292f",
|
|
1615
|
-
tool: "#8250df",
|
|
1616
|
-
reasoning: "#8c959f",
|
|
1617
|
-
syntaxBlue: "#0969da",
|
|
1618
|
-
syntaxCyan: "#1b7c83",
|
|
1619
|
-
syntaxTeal: "#1b7c83",
|
|
1620
|
-
syntaxGreen: "#1a7f37",
|
|
1621
|
-
syntaxYellow: "#9a6700",
|
|
1622
|
-
syntaxOrange: "#bc4c00",
|
|
1623
|
-
syntaxRed: "#cf222e",
|
|
1624
|
-
syntaxMagenta: "#8250df"
|
|
1625
|
-
}
|
|
1626
|
-
};
|
|
1627
|
-
const kanagawa = {
|
|
1628
|
-
id: "kanagawa",
|
|
1629
|
-
label: "Kanagawa",
|
|
1630
|
-
mode: "dark",
|
|
1631
|
-
palette: {
|
|
1632
|
-
fg: "#dcd7ba",
|
|
1633
|
-
muted: "#727169",
|
|
1634
|
-
dim: "#54546d",
|
|
1635
|
-
faint: "#363646",
|
|
1636
|
-
surface: "#16161d",
|
|
1637
|
-
accent: "#7e9cd8",
|
|
1638
|
-
success: "#98bb6c",
|
|
1639
|
-
warning: "#e6c384",
|
|
1640
|
-
error: "#e46876",
|
|
1641
|
-
user: "#7e9cd8",
|
|
1642
|
-
assistant: "#dcd7ba",
|
|
1643
|
-
tool: "#957fb8",
|
|
1644
|
-
reasoning: "#727169",
|
|
1645
|
-
syntaxBlue: "#7e9cd8",
|
|
1646
|
-
syntaxCyan: "#7aa89f",
|
|
1647
|
-
syntaxTeal: "#7aa89f",
|
|
1648
|
-
syntaxGreen: "#98bb6c",
|
|
1649
|
-
syntaxYellow: "#e6c384",
|
|
1650
|
-
syntaxOrange: "#ffa066",
|
|
1651
|
-
syntaxRed: "#e46876",
|
|
1652
|
-
syntaxMagenta: "#957fb8"
|
|
1653
|
-
}
|
|
1654
|
-
};
|
|
1655
|
-
const cursorDark = {
|
|
1656
|
-
id: "cursor-dark",
|
|
1657
|
-
label: "Cursor Dark",
|
|
1658
|
-
mode: "dark",
|
|
1659
|
-
palette: {
|
|
1660
|
-
background: "#181818",
|
|
1661
|
-
fg: "#d4d4d4",
|
|
1662
|
-
muted: "#898989",
|
|
1663
|
-
dim: "#636363",
|
|
1664
|
-
faint: "#272727",
|
|
1665
|
-
surface: "#141414",
|
|
1666
|
-
accent: "#88c0d0",
|
|
1667
|
-
success: "#70b489",
|
|
1668
|
-
warning: "#f1b467",
|
|
1669
|
-
error: "#fc6b83",
|
|
1670
|
-
diffAdded: "#3fa266",
|
|
1671
|
-
diffRemoved: "#b80049",
|
|
1672
|
-
diffWeight: .2,
|
|
1673
|
-
user: "#88c0d0",
|
|
1674
|
-
assistant: "#d4d4d4",
|
|
1675
|
-
tool: "#aaa0fa",
|
|
1676
|
-
reasoning: "#636363",
|
|
1677
|
-
syntaxBlue: "#ebc88d",
|
|
1678
|
-
syntaxCyan: "#87c3ff",
|
|
1679
|
-
syntaxTeal: "#aaa0fa",
|
|
1680
|
-
syntaxGreen: "#e394dc",
|
|
1681
|
-
syntaxYellow: "#d4d4d4",
|
|
1682
|
-
syntaxOrange: "#f8c762",
|
|
1683
|
-
syntaxRed: "#cc7c8a",
|
|
1684
|
-
syntaxMagenta: "#82d2ce"
|
|
1685
|
-
}
|
|
1686
|
-
};
|
|
1687
|
-
const themes = [
|
|
1688
|
-
tokyonight,
|
|
1689
|
-
tokyonightDay,
|
|
1690
|
-
catppuccinMocha,
|
|
1691
|
-
catppuccinLatte,
|
|
1692
|
-
gruvboxDark,
|
|
1693
|
-
gruvboxLight,
|
|
1694
|
-
solarizedDark,
|
|
1695
|
-
solarizedLight,
|
|
1696
|
-
nord,
|
|
1697
|
-
dracula,
|
|
1698
|
-
oneDark,
|
|
1699
|
-
oneLight,
|
|
1700
|
-
rosePine,
|
|
1701
|
-
rosePineDawn,
|
|
1702
|
-
everforestDark,
|
|
1703
|
-
everforestLight,
|
|
1704
|
-
githubDark,
|
|
1705
|
-
githubLight,
|
|
1706
|
-
kanagawa,
|
|
1707
|
-
cursorDark
|
|
1708
|
-
];
|
|
1709
|
-
const DEFAULT_THEME_ID = {
|
|
1710
|
-
dark: tokyonight.id,
|
|
1711
|
-
light: tokyonightDay.id
|
|
1712
|
-
};
|
|
1713
|
-
/** All-undefined palette for NO_COLOR: every fg/bg falls back to the
|
|
1714
|
-
* terminal's own defaults, so nothing emits color. Not listed in `themes` —
|
|
1715
|
-
* it's forced, never picked. */
|
|
1716
|
-
const monoTheme = {
|
|
1717
|
-
id: "mono",
|
|
1718
|
-
label: "No color",
|
|
1719
|
-
mode: "dark",
|
|
1720
|
-
palette: {
|
|
1721
|
-
fg: void 0,
|
|
1722
|
-
muted: void 0,
|
|
1723
|
-
dim: void 0,
|
|
1724
|
-
faint: void 0,
|
|
1725
|
-
surface: void 0,
|
|
1726
|
-
accent: void 0,
|
|
1727
|
-
success: void 0,
|
|
1728
|
-
warning: void 0,
|
|
1729
|
-
error: void 0,
|
|
1730
|
-
user: void 0,
|
|
1731
|
-
assistant: void 0,
|
|
1732
|
-
tool: void 0,
|
|
1733
|
-
reasoning: void 0,
|
|
1734
|
-
syntaxBlue: void 0,
|
|
1735
|
-
syntaxCyan: void 0,
|
|
1736
|
-
syntaxTeal: void 0,
|
|
1737
|
-
syntaxGreen: void 0,
|
|
1738
|
-
syntaxYellow: void 0,
|
|
1739
|
-
syntaxOrange: void 0,
|
|
1740
|
-
syntaxRed: void 0,
|
|
1741
|
-
syntaxMagenta: void 0
|
|
1742
|
-
}
|
|
1743
|
-
};
|
|
1744
|
-
function themesForMode(mode) {
|
|
1745
|
-
return themes.filter((t) => t.mode === mode);
|
|
1746
|
-
}
|
|
1747
|
-
function findTheme(id) {
|
|
1748
|
-
if (id === monoTheme.id) return monoTheme;
|
|
1749
|
-
return themes.find((t) => t.id === id);
|
|
1750
|
-
}
|
|
1751
|
-
/**
|
|
1752
|
-
* The saved theme to use for a mode: the persisted pick if it exists *and*
|
|
1753
|
-
* still matches the mode (a stale/renamed id falls back), else the default.
|
|
1754
|
-
*/
|
|
1755
|
-
function themeForMode(mode, savedId) {
|
|
1756
|
-
if (savedId) {
|
|
1757
|
-
const saved = findTheme(savedId);
|
|
1758
|
-
if (saved && saved.mode === mode) return saved;
|
|
1759
|
-
}
|
|
1760
|
-
return findTheme(DEFAULT_THEME_ID[mode]) ?? tokyonight;
|
|
1761
|
-
}
|
|
1762
|
-
/** https://no-color.org — any non-empty value disables color output. */
|
|
1763
|
-
function noColorRequested(env = process.env) {
|
|
1764
|
-
const v = env["NO_COLOR"];
|
|
1765
|
-
return v !== void 0 && v !== "";
|
|
1766
|
-
}
|
|
1767
|
-
/**
|
|
1768
|
-
* Fallback light/dark sniff for terminals that never answer the OSC 10/11
|
|
1769
|
-
* query: `COLORFGBG` is "<fg>;<bg>" (sometimes "<fg>;default;<bg>") with
|
|
1770
|
-
* ANSI palette indexes. Background 7/15 (white/bright white) means a light
|
|
1771
|
-
* terminal; anything else we call dark. Returns null when unset/unparsable.
|
|
1772
|
-
*/
|
|
1773
|
-
function themeModeFromColorFgBg(env = process.env) {
|
|
1774
|
-
const raw = env["COLORFGBG"];
|
|
1775
|
-
if (!raw) return null;
|
|
1776
|
-
const parts = raw.split(";");
|
|
1777
|
-
const bg = parts[parts.length - 1];
|
|
1778
|
-
if (bg === void 0 || !/^\d+$/.test(bg)) return null;
|
|
1779
|
-
const idx = Number(bg);
|
|
1780
|
-
return idx === 7 || idx === 15 ? "light" : "dark";
|
|
1781
|
-
}
|
|
1782
|
-
/** Single source of truth for colors. Mutated in place by `applyTheme` so
|
|
1783
|
-
* existing `theme.fg`-style reads across the TUI stay valid. */
|
|
1784
|
-
const theme = { ...tokyonight.palette };
|
|
1785
|
-
let version = 0;
|
|
1786
|
-
function themeVersion() {
|
|
1787
|
-
return version;
|
|
1788
|
-
}
|
|
1789
|
-
let mode = tokyonight.mode;
|
|
1790
|
-
function themeMode() {
|
|
1791
|
-
return mode;
|
|
1792
|
-
}
|
|
1793
|
-
function applyTheme(def) {
|
|
1794
|
-
Object.assign(theme, def.palette);
|
|
1795
|
-
theme.background = def.palette.background;
|
|
1796
|
-
theme.diffAdded = def.palette.diffAdded;
|
|
1797
|
-
theme.diffRemoved = def.palette.diffRemoved;
|
|
1798
|
-
theme.diffWeight = def.palette.diffWeight;
|
|
1799
|
-
mode = def.mode;
|
|
1800
|
-
version++;
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
//#endregion
|
|
1804
|
-
//#region src/chat/bun-runtime.ts
|
|
1805
|
-
/**
|
|
1806
|
-
* The chat TUI renders through OpenTUI, whose native core is Bun-first (its
|
|
1807
|
-
* FFI/native-module loading needs the Bun runtime; the Node path requires an
|
|
1808
|
-
* experimental `node:ffi` only present in very new Node). Rather than make the
|
|
1809
|
-
* user install Bun by hand, the CLI provisions a private, pinned Bun the first
|
|
1810
|
-
* time `chat` runs and transparently re-execs itself under it. Every other
|
|
1811
|
-
* command still runs under whatever Node the CLI was launched with — Bun is
|
|
1812
|
-
* only ever fetched for `chat`.
|
|
1813
|
-
*/
|
|
1814
|
-
/**
|
|
1815
|
-
* Bun release the CLI pins. Bump deliberately: the download is integrity-checked
|
|
1816
|
-
* against this exact release's published `SHASUMS256.txt`, and the cached binary
|
|
1817
|
-
* is keyed by version so bumping here transparently re-provisions on next `chat`.
|
|
1818
|
-
*/
|
|
1819
|
-
const PINNED_BUN_VERSION = "1.3.14";
|
|
1820
|
-
const BUN_RELEASE_BASE = process.env["SKYDIVE_BUN_RELEASE_BASE"] ?? "https://github.com/oven-sh/bun/releases/download";
|
|
1821
|
-
/** Guard env var: set on the child so a re-exec can never recurse. */
|
|
1822
|
-
const REEXEC_GUARD = "SKYDIVE_BUN_REEXEC";
|
|
1823
|
-
function isBun() {
|
|
1824
|
-
return typeof process !== "undefined" && "bun" in process.versions;
|
|
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;
|
|
1825
930
|
}
|
|
1826
931
|
/**
|
|
1827
932
|
* Map the current platform/arch to Bun's release asset basename (without the
|
|
@@ -2040,11 +1145,316 @@ function ensureBunAndReexec(onProgress) {
|
|
|
2040
1145
|
}
|
|
2041
1146
|
|
|
2042
1147
|
//#endregion
|
|
2043
|
-
//#region src/commands/
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
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);
|
|
1370
|
+
}
|
|
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]));
|
|
1381
|
+
}
|
|
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);
|
|
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
|
|
1415
|
+
});
|
|
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);
|
|
1433
|
+
}
|
|
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
|
+
};
|
|
1451
|
+
|
|
1452
|
+
//#endregion
|
|
1453
|
+
//#region src/commands/chat.ts
|
|
1454
|
+
const chatCommand = {
|
|
1455
|
+
command: "chat",
|
|
1456
|
+
describe: "Open the interactive chat TUI, or -p for a one-shot",
|
|
1457
|
+
builder: (y) => y.option("print", {
|
|
2048
1458
|
alias: "p",
|
|
2049
1459
|
type: "string",
|
|
2050
1460
|
describe: "Non-interactive: send one prompt, print the reply, and exit (like `claude -p`). Reads the prompt from stdin if given no value. Runs under Node — no Bun required."
|
|
@@ -2052,12 +1462,12 @@ const chatCommand = {
|
|
|
2052
1462
|
type: "string",
|
|
2053
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."
|
|
2054
1464
|
}).option("conversation", {
|
|
1465
|
+
alias: "resume",
|
|
2055
1466
|
type: "string",
|
|
2056
|
-
describe: "
|
|
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)."
|
|
2057
1468
|
}).option("share-machine", {
|
|
2058
1469
|
type: "boolean",
|
|
2059
|
-
|
|
2060
|
-
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."
|
|
2061
1471
|
}).option("theme", {
|
|
2062
1472
|
type: "string",
|
|
2063
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.`
|
|
@@ -2065,7 +1475,7 @@ const chatCommand = {
|
|
|
2065
1475
|
type: "boolean",
|
|
2066
1476
|
default: true,
|
|
2067
1477
|
describe: "Show a desktop notification when a run finishes or needs your input while this terminal is unfocused (use --no-notify to disable)"
|
|
2068
|
-
}).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)"),
|
|
2069
1479
|
handler: async (argv) => {
|
|
2070
1480
|
const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
|
|
2071
1481
|
if (argv.print !== void 0) {
|
|
@@ -2082,7 +1492,7 @@ const chatCommand = {
|
|
|
2082
1492
|
let session = resolveSession({ appUrl });
|
|
2083
1493
|
if (session.isErr()) {
|
|
2084
1494
|
if (isNonInteractive()) {
|
|
2085
|
-
printError("Not signed in for chat and no interactive terminal. Run `skydive auth login
|
|
1495
|
+
printError("Not signed in for chat and no interactive terminal. Run `skydive auth login`, or set SKYDIVE_SESSION_TOKEN.");
|
|
2086
1496
|
process.exit(1);
|
|
2087
1497
|
}
|
|
2088
1498
|
const login = await loginWithDevice({ appUrl });
|
|
@@ -2101,25 +1511,35 @@ const chatCommand = {
|
|
|
2101
1511
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
2102
1512
|
process.exit(1);
|
|
2103
1513
|
}
|
|
2104
|
-
const { runChat } = await import("./boot-
|
|
1514
|
+
const { runChat } = await import("./boot-CQLDUeZ9.mjs");
|
|
2105
1515
|
await runChat({
|
|
2106
1516
|
appUrl,
|
|
2107
1517
|
sessionToken: session.value.sessionToken,
|
|
2108
|
-
shareMachine: argv
|
|
1518
|
+
shareMachine: resolveShareMachine(argv),
|
|
2109
1519
|
promptHistoryPath: getPromptHistoryPath(),
|
|
2110
1520
|
theme: themeId,
|
|
2111
1521
|
notifications: argv.notify,
|
|
2112
|
-
agentSelector: argv.agent ?? null
|
|
1522
|
+
agentSelector: argv.agent ?? null,
|
|
1523
|
+
conversationId: argv.conversation ?? null,
|
|
1524
|
+
seedPrompt: null
|
|
2113
1525
|
});
|
|
2114
1526
|
}
|
|
2115
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
|
+
}
|
|
2116
1536
|
async function runPrintMode({ argv, appUrl }) {
|
|
2117
1537
|
const session = resolveSession({ appUrl });
|
|
2118
1538
|
if (session.isErr()) {
|
|
2119
|
-
printError(`${session.error.message} For non-interactive use, run \`skydive auth login
|
|
1539
|
+
printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2120
1540
|
process.exit(1);
|
|
2121
1541
|
}
|
|
2122
|
-
const { runPrint, readStdin } = await import("./print-
|
|
1542
|
+
const { runPrint, readStdin } = await import("./print-B6AO13SA.mjs");
|
|
2123
1543
|
let prompt = (argv.print ?? "").trim();
|
|
2124
1544
|
if (!prompt) {
|
|
2125
1545
|
if (process.stdin.isTTY) {
|
|
@@ -2132,6 +1552,15 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2132
1552
|
process.exit(1);
|
|
2133
1553
|
}
|
|
2134
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
|
+
}
|
|
2135
1564
|
try {
|
|
2136
1565
|
const result = await runPrint({
|
|
2137
1566
|
appUrl,
|
|
@@ -2139,25 +1568,188 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2139
1568
|
prompt,
|
|
2140
1569
|
agentSelector: argv.agent ?? null,
|
|
2141
1570
|
conversationId: argv.conversation ?? null,
|
|
2142
|
-
json: argv.json
|
|
1571
|
+
json: argv.json,
|
|
1572
|
+
machineShare,
|
|
1573
|
+
grantTargetAgent: argv["share-machine"] === true
|
|
2143
1574
|
});
|
|
2144
1575
|
if (argv.json) output(argv, result);
|
|
2145
1576
|
} catch (error) {
|
|
2146
1577
|
printError(error instanceof Error ? error.message : String(error));
|
|
2147
1578
|
process.exit(1);
|
|
1579
|
+
} finally {
|
|
1580
|
+
machineShare?.dispose();
|
|
2148
1581
|
}
|
|
2149
1582
|
}
|
|
2150
1583
|
|
|
2151
1584
|
//#endregion
|
|
2152
|
-
//#region src/commands/
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
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
|
+
}
|
|
2158
1629
|
}
|
|
2159
|
-
|
|
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();
|
|
2160
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;
|
|
1685
|
+
}
|
|
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
|
+
]));
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
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;
|
|
1725
|
+
}
|
|
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);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
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
|
+
};
|
|
1750
|
+
|
|
1751
|
+
//#endregion
|
|
1752
|
+
//#region src/commands/workspace.ts
|
|
2161
1753
|
/**
|
|
2162
1754
|
* List the account's workspaces, marking the active one. Shared by the `list`
|
|
2163
1755
|
* subcommand and the bare `workspace` invocation. When `hint` is true (the bare
|
|
@@ -2201,7 +1793,7 @@ const listCommand = {
|
|
|
2201
1793
|
};
|
|
2202
1794
|
const switchCommand = {
|
|
2203
1795
|
command: "switch [workspace]",
|
|
2204
|
-
describe: "Switch
|
|
1796
|
+
describe: "Switch the workspace all `skydive` commands act on",
|
|
2205
1797
|
builder: (y) => y.positional("workspace", {
|
|
2206
1798
|
type: "string",
|
|
2207
1799
|
demandOption: false,
|
|
@@ -2218,7 +1810,7 @@ const switchCommand = {
|
|
|
2218
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.");
|
|
2219
1811
|
process.exit(1);
|
|
2220
1812
|
}
|
|
2221
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
1813
|
+
const { runWorkspacePicker } = await import("./boot-CQLDUeZ9.mjs");
|
|
2222
1814
|
await runWorkspacePicker(session);
|
|
2223
1815
|
return;
|
|
2224
1816
|
}
|
|
@@ -2250,15 +1842,310 @@ const switchCommand = {
|
|
|
2250
1842
|
};
|
|
2251
1843
|
const workspaceCommand = {
|
|
2252
1844
|
command: "workspace",
|
|
2253
|
-
describe: "List or switch
|
|
1845
|
+
describe: "List or switch the workspace all `skydive` commands act on",
|
|
2254
1846
|
builder: (y) => y.command(listCommand).command(switchCommand),
|
|
2255
1847
|
handler: (argv) => runList(argv, { hint: true })
|
|
2256
1848
|
};
|
|
2257
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
|
+
|
|
2258
2145
|
//#endregion
|
|
2259
2146
|
//#region src/cli.ts
|
|
2260
2147
|
function createCli(argv) {
|
|
2261
|
-
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
|
|
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", {
|
|
2262
2149
|
type: "boolean",
|
|
2263
2150
|
default: false,
|
|
2264
2151
|
global: true,
|
|
@@ -2272,7 +2159,7 @@ function createCli(argv) {
|
|
|
2272
2159
|
type: "string",
|
|
2273
2160
|
global: true,
|
|
2274
2161
|
describe: "Override API base URL"
|
|
2275
|
-
}).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
|
|
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) => {
|
|
2276
2163
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
2277
2164
|
process.exit(1);
|
|
2278
2165
|
});
|
|
@@ -2291,6 +2178,7 @@ const VALUE_FLAGS = new Set([
|
|
|
2291
2178
|
"-p",
|
|
2292
2179
|
"--agent",
|
|
2293
2180
|
"--conversation",
|
|
2181
|
+
"--resume",
|
|
2294
2182
|
"--theme",
|
|
2295
2183
|
"--api-url"
|
|
2296
2184
|
]);
|
|
@@ -2340,9 +2228,387 @@ function resolveArgv(args, tty = {
|
|
|
2340
2228
|
return shouldDefaultToChat(args, tty, nonInteractive) ? ["chat", ...args] : args;
|
|
2341
2229
|
}
|
|
2342
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) {}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2343
2603
|
//#endregion
|
|
2344
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));
|
|
2345
2611
|
createCli(resolveArgv(hideBin(process.argv))).parse();
|
|
2346
2612
|
|
|
2347
2613
|
//#endregion
|
|
2348
|
-
export {
|
|
2614
|
+
export { };
|