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