skydive-cli 0.1.0-beta.382 → 0.1.0-beta.389
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/js/api-DRpbKHz6.mjs +145 -0
- package/dist/js/bin.mjs +267 -2224
- package/dist/js/{boot-B48ty_OV.mjs → boot-CMhIv1BG.mjs} +8 -3
- package/dist/js/client-BVOAwU8M.mjs +169 -0
- package/dist/js/client-C-s6b9Yu.mjs +6 -0
- package/dist/js/{client-XFsd0Wy9.mjs → client-DyRs3o5E.mjs} +3 -4
- package/dist/js/print-BhfjRrxI.mjs +466 -0
- package/dist/js/print-DFPzQQk8.mjs +5 -0
- package/dist/js/{raw-pty-C1DXKms6.mjs → raw-pty-Dbi2kb9v.mjs} +2 -4
- package/dist/js/raw-pty-pspO57gT.mjs +5 -0
- package/dist/js/rest-DQruM5kj.mjs +4 -0
- package/dist/js/rest-DTlkPko_.mjs +457 -0
- package/dist/js/theme-DRuLtrTy.mjs +974 -0
- package/dist/js/util-z9Pne47f.mjs +15 -0
- package/package.json +4 -2
- package/dist/js/rolldown-runtime-Cz4Tg37Z.mjs +0 -19
package/dist/js/bin.mjs
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { A as getStoredApiKeyId, C as DEFAULT_WEB_URL, E as getPromptHistoryPath, F as resolveSession, L as saveConfig, M as resolveAppUrl, N as resolveConfig, P as resolveManagementAuth, R as saveSession, T as getConfigPath, _ as setActiveWorkspace, b as API_KEY_PREFIX, d as themes, g as listWorkspaces, h as getSessionIdentity, j as getUpdateCheckDisabled, k as getShareMachineDefault, 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 } from "./theme-DRuLtrTy.mjs";
|
|
3
|
+
import { n as createRestClient } from "./rest-DTlkPko_.mjs";
|
|
4
|
+
import { i as resolveAgent } from "./print-BhfjRrxI.mjs";
|
|
5
|
+
import { a as registerPortalDevice, c as machineIdentity, n as findThisDevice, o as revokePortalAccess, r as grantPortalAccess, t as fetchPortalDevices } from "./api-DRpbKHz6.mjs";
|
|
6
|
+
import { t as SandboxStream } from "./client-BVOAwU8M.mjs";
|
|
3
7
|
import { hideBin } from "yargs/helpers";
|
|
4
8
|
import yargs from "yargs";
|
|
5
|
-
import
|
|
9
|
+
import { hostname } from "node:os";
|
|
6
10
|
import path from "node:path";
|
|
7
|
-
import Conf from "conf";
|
|
8
11
|
import { err, ok } from "neverthrow";
|
|
9
12
|
import { z } from "zod";
|
|
10
13
|
import open from "open";
|
|
11
|
-
import {
|
|
12
|
-
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
13
15
|
import { createHash } from "node:crypto";
|
|
14
16
|
import fs from "node:fs";
|
|
15
17
|
import zlib from "node:zlib";
|
|
16
|
-
import
|
|
18
|
+
import semver from "semver";
|
|
17
19
|
|
|
18
20
|
//#region package.json
|
|
19
|
-
var
|
|
21
|
+
var name = "skydive-cli";
|
|
22
|
+
var version = "0.1.0-beta.389";
|
|
20
23
|
|
|
21
24
|
//#endregion
|
|
22
25
|
//#region src/types.ts
|
|
@@ -30,170 +33,9 @@ function isNonInteractive() {
|
|
|
30
33
|
return NON_INTERACTIVE_ENV_VARS.some((key) => process.env[key]);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
//#endregion
|
|
34
|
-
//#region src/config.ts
|
|
35
|
-
/** Default host for the public management API (`/v1`, API-key auth). */
|
|
36
|
-
const DEFAULT_API_URL = "https://api.skydive.com";
|
|
37
|
-
/**
|
|
38
|
-
* Default origin for the interactive chat client (`skydive chat`).
|
|
39
|
-
*
|
|
40
|
-
* The API host that serves better-auth (`/api/auth/*`) and the internal tRPC
|
|
41
|
-
* API (`/api/v1/trpc`) that chat streams over. We target the API host
|
|
42
|
-
* directly (not the web front door) because chat opens a WebSocket and
|
|
43
|
-
* authenticates with a bearer token on the upgrade request. Same host as
|
|
44
|
-
* `DEFAULT_API_URL`; override with `--api-url` / `SKYDIVE_APP_URL` for local
|
|
45
|
-
* dev or while the DNS record is still being provisioned.
|
|
46
|
-
*/
|
|
47
|
-
const DEFAULT_APP_URL = "https://api.skydive.com";
|
|
48
|
-
/** Web front door, for pages opened in the user's browser. */
|
|
49
|
-
const DEFAULT_WEB_URL = "https://skydive.com";
|
|
50
|
-
/**
|
|
51
|
-
* Origin for browser-facing links (e.g. opening a conversation's web page).
|
|
52
|
-
* The app origin is the API host, which serves no web UI in production, so
|
|
53
|
-
* map the default to the web front door. Overridden origins (local dev,
|
|
54
|
-
* previews) serve both and pass through unchanged.
|
|
55
|
-
*/
|
|
56
|
-
function resolveWebUrl(appUrl) {
|
|
57
|
-
return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
|
|
58
|
-
}
|
|
59
|
-
/** Prefix on workspace-scoped Skydive API keys. Kept in sync with the API's
|
|
60
|
-
* `API_KEY_PREFIX` (`apps/anyone/api/src/lib/api-key.ts`); the CLI is a
|
|
61
|
-
* standalone published package so it can't import the backend constant. */
|
|
62
|
-
const API_KEY_PREFIX = "sky_live_";
|
|
63
|
-
/**
|
|
64
|
-
* Common prefix across all Skydive API key kinds — `sky_live_…` workspace
|
|
65
|
-
* keys today, `sky_user_…` account keys when ANY-5105 lands. The CLI only
|
|
66
|
-
* sanity-checks the family on `--api-key`; the server authoritatively rejects
|
|
67
|
-
* a kind that can't drive a given route, with a clearer message than the
|
|
68
|
-
* client could produce.
|
|
69
|
-
*/
|
|
70
|
-
const API_KEY_FAMILY_PREFIX = "sky_";
|
|
71
|
-
/** Where users mint and copy API keys. Shown in the login prompt. */
|
|
72
|
-
const API_KEYS_URL = "skydive.com/settings/account";
|
|
73
|
-
const store = new Conf({
|
|
74
|
-
projectName: process.env["SKYDIVE_CONFIG_NAME"] ?? "skydive",
|
|
75
|
-
projectSuffix: "",
|
|
76
|
-
configFileMode: 384
|
|
77
|
-
});
|
|
78
|
-
function resolveConfig(opts) {
|
|
79
|
-
const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
|
|
80
|
-
const apiUrl = process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL;
|
|
81
|
-
if (!apiKey) return err({ message: "Not authenticated. Run `skydive auth login` first." });
|
|
82
|
-
return ok({
|
|
83
|
-
apiKey,
|
|
84
|
-
apiUrl
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Resolve the bearer credential for the management API (`agents` / `keys` /
|
|
89
|
-
* `secrets`). The server's `/v1` gate accepts either an API key or the
|
|
90
|
-
* device-flow session bearer, so both work — but only one of them tracks the
|
|
91
|
-
* active workspace.
|
|
92
|
-
*
|
|
93
|
-
* An API key is pinned server-side to the organization that minted it and
|
|
94
|
-
* ignores the workspace header by design, so it can never follow `skydive
|
|
95
|
-
* workspace switch`. A key is also a strictly narrower credential than its
|
|
96
|
-
* owner's session. So the session wins whenever there is one, and a key is
|
|
97
|
-
* what's left for machines that never ran an interactive login.
|
|
98
|
-
*
|
|
99
|
-
* `SKYDIVE_SESSION_TOKEN=` (empty) suppresses session auth for one
|
|
100
|
-
* invocation, to drive a specific organization's key while signed in.
|
|
101
|
-
*/
|
|
102
|
-
function resolveManagementAuth(opts) {
|
|
103
|
-
const session = resolveSession({ appUrl: opts.apiUrl });
|
|
104
|
-
if (session.isOk()) return ok({
|
|
105
|
-
token: session.value.sessionToken,
|
|
106
|
-
apiUrl: session.value.appUrl,
|
|
107
|
-
kind: "session"
|
|
108
|
-
});
|
|
109
|
-
const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
|
|
110
|
-
if (apiKey) return ok({
|
|
111
|
-
token: apiKey,
|
|
112
|
-
apiUrl: process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL,
|
|
113
|
-
kind: "api-key"
|
|
114
|
-
});
|
|
115
|
-
return err({ message: "Not authenticated. Run `skydive auth login`." });
|
|
116
|
-
}
|
|
117
|
-
function saveConfig(config) {
|
|
118
|
-
store.set("apiKey", config.apiKey);
|
|
119
|
-
store.set("apiUrl", config.apiUrl);
|
|
120
|
-
if (config.apiKeyId) store.set("apiKeyId", config.apiKeyId);
|
|
121
|
-
else store.delete("apiKeyId");
|
|
122
|
-
}
|
|
123
|
-
/** Server-side id of the auto-minted key, if login minted one. */
|
|
124
|
-
function getStoredApiKeyId() {
|
|
125
|
-
return store.get("apiKeyId") ?? null;
|
|
126
|
-
}
|
|
127
|
-
function deleteConfig() {
|
|
128
|
-
store.clear();
|
|
129
|
-
}
|
|
130
|
-
function getConfigPath() {
|
|
131
|
-
return store.path;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Where the chat TUI persists its prompt history (up-arrow recall). Kept
|
|
135
|
-
* beside the config file so all CLI state lives in one directory.
|
|
136
|
-
*/
|
|
137
|
-
function getPromptHistoryPath() {
|
|
138
|
-
return path.join(path.dirname(store.path), "prompt-history.jsonl");
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Where the chat TUI persists pending review comments — one JSON file per
|
|
142
|
-
* conversation, so a pending comment survives conversation switches and
|
|
143
|
-
* process death. Kept beside the config file like prompt history.
|
|
144
|
-
*/
|
|
145
|
-
function getReviewStateDir() {
|
|
146
|
-
return path.join(path.dirname(store.path), "review");
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Resolve the chat/auth origin. Precedence: `SKYDIVE_APP_URL` env > explicit
|
|
150
|
-
* `--api-url` style override > stored value > `SKYDIVE_API_URL` env >
|
|
151
|
-
* `DEFAULT_APP_URL`.
|
|
152
|
-
*
|
|
153
|
-
* The `SKYDIVE_API_URL` fallback matters for previews: the device/`--web`
|
|
154
|
-
* flow and chat hit the same api service as the management API, so pointing
|
|
155
|
-
* `SKYDIVE_API_URL` at a preview stack is enough — you don't also have to set
|
|
156
|
-
* `SKYDIVE_APP_URL`. Otherwise auth would silently fall through to prod
|
|
157
|
-
* (`DEFAULT_APP_URL`) and hand back a prod verification URL.
|
|
158
|
-
*/
|
|
159
|
-
function resolveAppUrl(opts) {
|
|
160
|
-
return process.env["SKYDIVE_APP_URL"] ?? opts.appUrl ?? store.get("appUrl") ?? process.env["SKYDIVE_API_URL"] ?? DEFAULT_APP_URL;
|
|
161
|
-
}
|
|
162
|
-
function resolveSession(opts) {
|
|
163
|
-
const sessionToken = process.env["SKYDIVE_SESSION_TOKEN"] ?? store.get("sessionToken");
|
|
164
|
-
const appUrl = resolveAppUrl(opts);
|
|
165
|
-
if (!sessionToken) return err({ message: "Not signed in for chat. Run `skydive chat` to sign in." });
|
|
166
|
-
return ok({
|
|
167
|
-
sessionToken,
|
|
168
|
-
appUrl
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
function saveSession(session) {
|
|
172
|
-
store.set("sessionToken", session.sessionToken);
|
|
173
|
-
store.set("sessionObtainedAt", (/* @__PURE__ */ new Date()).toISOString());
|
|
174
|
-
store.set("appUrl", session.appUrl);
|
|
175
|
-
}
|
|
176
|
-
function getSavedTheme(mode) {
|
|
177
|
-
return store.get(mode === "dark" ? "themeDark" : "themeLight");
|
|
178
|
-
}
|
|
179
|
-
function saveTheme(mode, themeId) {
|
|
180
|
-
store.set(mode === "dark" ? "themeDark" : "themeLight", themeId);
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Whether `skydive chat` should enable portal machine sharing on launch
|
|
184
|
-
* without `--share-machine`. Set by hand-editing `shareMachineDefault` in
|
|
185
|
-
* config.json — deliberately no CLI command (kept off the API surface).
|
|
186
|
-
* Sharing only makes the machine reachable — agents still need a
|
|
187
|
-
* (persistent) grant to run anything, so this default skips the per-session
|
|
188
|
-
* enable step, not the consent step.
|
|
189
|
-
*/
|
|
190
|
-
function getShareMachineDefault() {
|
|
191
|
-
return store.get("shareMachineDefault") ?? false;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
36
|
//#endregion
|
|
195
37
|
//#region src/api-client.ts
|
|
196
|
-
const USER_AGENT = `skydive-cli/${version
|
|
38
|
+
const USER_AGENT = `skydive-cli/${version}`;
|
|
197
39
|
/** Largest page `/v1/agents` will return, enforced server-side. */
|
|
198
40
|
const MAX_AGENT_PAGE = 100;
|
|
199
41
|
const AgentSchema = z.object({
|
|
@@ -476,121 +318,6 @@ async function pollDeviceToken({ appUrl, deviceCode, currentIntervalMs, signal }
|
|
|
476
318
|
}
|
|
477
319
|
}
|
|
478
320
|
|
|
479
|
-
//#endregion
|
|
480
|
-
//#region src/auth/organization.ts
|
|
481
|
-
const workspaceSchema = z.object({
|
|
482
|
-
id: z.string(),
|
|
483
|
-
name: z.string(),
|
|
484
|
-
slug: z.string()
|
|
485
|
-
});
|
|
486
|
-
function authHeaders(sessionToken) {
|
|
487
|
-
return { authorization: `Bearer ${sessionToken}` };
|
|
488
|
-
}
|
|
489
|
-
async function listWorkspaces({ appUrl, sessionToken }) {
|
|
490
|
-
try {
|
|
491
|
-
const res = await fetch(`${appUrl}/api/auth/organization/list`, { headers: authHeaders(sessionToken) });
|
|
492
|
-
if (!res.ok) return err({ message: `failed to list workspaces (${res.status})` });
|
|
493
|
-
const parsed = z.array(workspaceSchema).safeParse(await res.json());
|
|
494
|
-
if (!parsed.success) return err({ message: "unexpected workspace list response" });
|
|
495
|
-
return ok(parsed.data);
|
|
496
|
-
} catch (e) {
|
|
497
|
-
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Resolve who the current chat session belongs to: the signed-in user's
|
|
502
|
-
* email/name and the active workspace. Used by `auth status` so a user running
|
|
503
|
-
* multiple accounts can answer "am I logged into the right one?" without
|
|
504
|
-
* another command. Best-effort: any failure yields nulls rather than throwing,
|
|
505
|
-
* since `auth status` should still report the rest of the state.
|
|
506
|
-
*/
|
|
507
|
-
async function getSessionIdentity({ appUrl, sessionToken }) {
|
|
508
|
-
try {
|
|
509
|
-
const res = await fetch(`${appUrl}/api/auth/get-session?disableCookieCache=true`, { headers: authHeaders(sessionToken) });
|
|
510
|
-
if (!res.ok) return err({ message: `failed to read session (${res.status})` });
|
|
511
|
-
const parsed = z.object({
|
|
512
|
-
user: z.object({
|
|
513
|
-
email: z.string().nullable().optional(),
|
|
514
|
-
name: z.string().nullable().optional()
|
|
515
|
-
}).nullable().optional(),
|
|
516
|
-
session: z.object({ activeOrganizationId: z.string().nullable().optional() }).nullable().optional()
|
|
517
|
-
}).nullable().safeParse(await res.json());
|
|
518
|
-
if (!parsed.success) return err({ message: "unexpected session response" });
|
|
519
|
-
const activeWorkspaceId = parsed.data?.session?.activeOrganizationId ?? null;
|
|
520
|
-
let activeWorkspaceName = null;
|
|
521
|
-
if (activeWorkspaceId) {
|
|
522
|
-
const workspaces = await listWorkspaces({
|
|
523
|
-
appUrl,
|
|
524
|
-
sessionToken
|
|
525
|
-
});
|
|
526
|
-
if (workspaces.isOk()) activeWorkspaceName = workspaces.value.find((w) => w.id === activeWorkspaceId)?.name ?? null;
|
|
527
|
-
}
|
|
528
|
-
return ok({
|
|
529
|
-
email: parsed.data?.user?.email ?? null,
|
|
530
|
-
name: parsed.data?.user?.name ?? null,
|
|
531
|
-
activeWorkspaceId,
|
|
532
|
-
activeWorkspaceName
|
|
533
|
-
});
|
|
534
|
-
} catch (e) {
|
|
535
|
-
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
/** The workspace bound to the current session, or `null` if none is set. */
|
|
539
|
-
async function getActiveWorkspaceId({ appUrl, sessionToken }) {
|
|
540
|
-
try {
|
|
541
|
-
const res = await fetch(`${appUrl}/api/auth/get-session?disableCookieCache=true`, { headers: authHeaders(sessionToken) });
|
|
542
|
-
if (!res.ok) return err({ message: `failed to read session (${res.status})` });
|
|
543
|
-
const parsed = z.object({ session: z.object({ activeOrganizationId: z.string().nullable().optional() }).nullable().optional() }).nullable().safeParse(await res.json());
|
|
544
|
-
if (!parsed.success) return err({ message: "unexpected session response" });
|
|
545
|
-
return ok(parsed.data?.session?.activeOrganizationId ?? null);
|
|
546
|
-
} catch (e) {
|
|
547
|
-
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
async function setActiveWorkspace({ appUrl, sessionToken, organizationId }) {
|
|
551
|
-
try {
|
|
552
|
-
const res = await fetch(`${appUrl}/api/v1/workspaces/switch`, {
|
|
553
|
-
method: "POST",
|
|
554
|
-
headers: {
|
|
555
|
-
...authHeaders(sessionToken),
|
|
556
|
-
"content-type": "application/json"
|
|
557
|
-
},
|
|
558
|
-
body: JSON.stringify({ organizationId })
|
|
559
|
-
});
|
|
560
|
-
if (!res.ok) return err({ message: `failed to switch workspace (${res.status})` });
|
|
561
|
-
return ok(void 0);
|
|
562
|
-
} catch (e) {
|
|
563
|
-
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* Ensures the session has an active workspace. The device flow issues a
|
|
568
|
-
* session without one (unlike a normal web sign-in, which sets it), so the
|
|
569
|
-
* internal API rejects every request with "no active organization" until we
|
|
570
|
-
* set it. Picks the account's first workspace by join order — run `skydive
|
|
571
|
-
* workspace list` + `skydive workspace switch` afterward if that's the wrong
|
|
572
|
-
* one (e.g. a personal workspace joined before a shared team workspace).
|
|
573
|
-
*/
|
|
574
|
-
async function ensureActiveOrganization({ appUrl, sessionToken }) {
|
|
575
|
-
const workspaces = await listWorkspaces({
|
|
576
|
-
appUrl,
|
|
577
|
-
sessionToken
|
|
578
|
-
});
|
|
579
|
-
if (workspaces.isErr()) return err(workspaces.error);
|
|
580
|
-
const workspace = workspaces.value[0];
|
|
581
|
-
if (!workspace) return err({ message: "your account has no organization yet" });
|
|
582
|
-
const setResult = await setActiveWorkspace({
|
|
583
|
-
appUrl,
|
|
584
|
-
sessionToken,
|
|
585
|
-
organizationId: workspace.id
|
|
586
|
-
});
|
|
587
|
-
if (setResult.isErr()) return err(setResult.error);
|
|
588
|
-
return ok({
|
|
589
|
-
organizationId: workspace.id,
|
|
590
|
-
name: workspace.name
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
|
|
594
321
|
//#endregion
|
|
595
322
|
//#region src/auth/device-login.ts
|
|
596
323
|
const DEFAULT_INTERVAL_MS = 5e3;
|
|
@@ -617,7 +344,7 @@ async function loginWithDevice({ appUrl, openBrowser = true }) {
|
|
|
617
344
|
let intervalMs = (code.interval ?? 5) * 1e3 || DEFAULT_INTERVAL_MS;
|
|
618
345
|
const expiresAt = Date.now() + code.expires_in * 1e3;
|
|
619
346
|
while (Date.now() < expiresAt) {
|
|
620
|
-
await sleep
|
|
347
|
+
await sleep(intervalMs);
|
|
621
348
|
const result = await pollDeviceToken({
|
|
622
349
|
appUrl,
|
|
623
350
|
deviceCode: code.device_code,
|
|
@@ -665,7 +392,7 @@ function formatUserCode(code) {
|
|
|
665
392
|
if (code.length !== 8) return code;
|
|
666
393
|
return `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
667
394
|
}
|
|
668
|
-
function sleep
|
|
395
|
+
function sleep(ms) {
|
|
669
396
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
670
397
|
}
|
|
671
398
|
|
|
@@ -1007,463 +734,6 @@ const authCommand = {
|
|
|
1007
734
|
handler: () => {}
|
|
1008
735
|
};
|
|
1009
736
|
|
|
1010
|
-
//#endregion
|
|
1011
|
-
//#region src/chat/api/rest.ts
|
|
1012
|
-
var rest_exports = /* @__PURE__ */ __exportAll({
|
|
1013
|
-
HttpError: () => HttpError,
|
|
1014
|
-
createRestClient: () => createRestClient,
|
|
1015
|
-
errorDetail: () => errorDetail
|
|
1016
|
-
});
|
|
1017
|
-
var HttpError = class extends Error {
|
|
1018
|
-
constructor(status, body) {
|
|
1019
|
-
super(`HTTP ${status}: ${body.slice(0, 200)}`);
|
|
1020
|
-
this.status = status;
|
|
1021
|
-
this.body = body;
|
|
1022
|
-
this.name = "HttpError";
|
|
1023
|
-
}
|
|
1024
|
-
};
|
|
1025
|
-
const ERROR_DETAIL_MAX_BODY = 2e3;
|
|
1026
|
-
/**
|
|
1027
|
-
* Fullest renderable text for a thrown value. `HttpError.message` clips the
|
|
1028
|
-
* response body to 200 chars (it flows into logs and one-line UIs); the
|
|
1029
|
-
* transcript renders errors collapsed to a single line, so it can afford the
|
|
1030
|
-
* whole body — capped with an explicit marker, never cut silently.
|
|
1031
|
-
*/
|
|
1032
|
-
function errorDetail(err) {
|
|
1033
|
-
if (err instanceof HttpError) {
|
|
1034
|
-
const body = err.body.length > ERROR_DETAIL_MAX_BODY ? `${err.body.slice(0, ERROR_DETAIL_MAX_BODY)}… (+${err.body.length - ERROR_DETAIL_MAX_BODY} chars)` : err.body;
|
|
1035
|
-
return body ? `HTTP ${err.status}: ${body}` : `HTTP ${err.status}`;
|
|
1036
|
-
}
|
|
1037
|
-
return err instanceof Error ? err.message : String(err);
|
|
1038
|
-
}
|
|
1039
|
-
const MAX_STREAM_RECONNECTS = 5;
|
|
1040
|
-
function createRestClient({ appUrl, sessionToken }) {
|
|
1041
|
-
const baseHeaders = {
|
|
1042
|
-
authorization: `Bearer ${sessionToken}`,
|
|
1043
|
-
accept: "application/json"
|
|
1044
|
-
};
|
|
1045
|
-
async function get(path, schema) {
|
|
1046
|
-
const res = await fetch(`${appUrl}${path}`, { headers: baseHeaders });
|
|
1047
|
-
if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
1048
|
-
return schema.parse(await res.json());
|
|
1049
|
-
}
|
|
1050
|
-
async function post(path, body, schema, method = "POST") {
|
|
1051
|
-
const res = await fetch(`${appUrl}${path}`, {
|
|
1052
|
-
method,
|
|
1053
|
-
headers: {
|
|
1054
|
-
...baseHeaders,
|
|
1055
|
-
"content-type": "application/json"
|
|
1056
|
-
},
|
|
1057
|
-
body: JSON.stringify(body)
|
|
1058
|
-
});
|
|
1059
|
-
if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
1060
|
-
return schema.parse(await res.json());
|
|
1061
|
-
}
|
|
1062
|
-
async function del(path) {
|
|
1063
|
-
const res = await fetch(`${appUrl}${path}`, {
|
|
1064
|
-
method: "DELETE",
|
|
1065
|
-
headers: baseHeaders
|
|
1066
|
-
});
|
|
1067
|
-
if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
1068
|
-
}
|
|
1069
|
-
const streamEvents = async ({ path, label, signal, onEvent }) => {
|
|
1070
|
-
let lastEventId = null;
|
|
1071
|
-
let finished = false;
|
|
1072
|
-
let reconnects = 0;
|
|
1073
|
-
for (;;) {
|
|
1074
|
-
if (signal.aborted) return;
|
|
1075
|
-
try {
|
|
1076
|
-
const headers = {
|
|
1077
|
-
authorization: `Bearer ${sessionToken}`,
|
|
1078
|
-
accept: "text/event-stream"
|
|
1079
|
-
};
|
|
1080
|
-
if (lastEventId) headers["last-event-id"] = lastEventId;
|
|
1081
|
-
const res = await fetch(`${appUrl}${path}`, {
|
|
1082
|
-
headers,
|
|
1083
|
-
signal
|
|
1084
|
-
});
|
|
1085
|
-
if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
1086
|
-
reconnects = 0;
|
|
1087
|
-
const parser = createParser({ onEvent: (message) => {
|
|
1088
|
-
if (message.id) lastEventId = message.id;
|
|
1089
|
-
if (message.event === "error") {
|
|
1090
|
-
const { error } = streamErrorSchema.parse(JSON.parse(message.data));
|
|
1091
|
-
throw new Error(error);
|
|
1092
|
-
}
|
|
1093
|
-
const event = runStreamEventSchema.parse(JSON.parse(message.data));
|
|
1094
|
-
if (event.kind === "finished") finished = true;
|
|
1095
|
-
onEvent(event.kind === "finished" ? {
|
|
1096
|
-
...event,
|
|
1097
|
-
error: event.error ?? null
|
|
1098
|
-
} : event);
|
|
1099
|
-
} });
|
|
1100
|
-
const decoder = new TextDecoder();
|
|
1101
|
-
const reader = res.body.getReader();
|
|
1102
|
-
try {
|
|
1103
|
-
for (;;) {
|
|
1104
|
-
const { done, value } = await reader.read();
|
|
1105
|
-
if (done) break;
|
|
1106
|
-
parser.feed(decoder.decode(value, { stream: true }));
|
|
1107
|
-
if (finished) return;
|
|
1108
|
-
}
|
|
1109
|
-
} finally {
|
|
1110
|
-
try {
|
|
1111
|
-
await reader.cancel();
|
|
1112
|
-
} catch (_error) {}
|
|
1113
|
-
}
|
|
1114
|
-
} catch (err) {
|
|
1115
|
-
if (signal.aborted) return;
|
|
1116
|
-
if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
|
|
1117
|
-
reconnects += 1;
|
|
1118
|
-
if (reconnects > MAX_STREAM_RECONNECTS) throw err;
|
|
1119
|
-
await sleep(Math.min(500 * 2 ** reconnects, 5e3));
|
|
1120
|
-
continue;
|
|
1121
|
-
}
|
|
1122
|
-
if (finished) return;
|
|
1123
|
-
reconnects += 1;
|
|
1124
|
-
if (reconnects > MAX_STREAM_RECONNECTS) throw new Error(`${label} stream ended unexpectedly`);
|
|
1125
|
-
await sleep(Math.min(500 * 2 ** reconnects, 5e3));
|
|
1126
|
-
}
|
|
1127
|
-
};
|
|
1128
|
-
return {
|
|
1129
|
-
listAgents: async ({ scope, onPage }) => {
|
|
1130
|
-
const all = [];
|
|
1131
|
-
let cursor;
|
|
1132
|
-
const maxAgents = 2e3;
|
|
1133
|
-
do {
|
|
1134
|
-
const params = new URLSearchParams({
|
|
1135
|
-
limit: "100",
|
|
1136
|
-
scope,
|
|
1137
|
-
sort: "mine_first_usage",
|
|
1138
|
-
includeStats: "false"
|
|
1139
|
-
});
|
|
1140
|
-
if (cursor) params.set("cursor", cursor);
|
|
1141
|
-
const page = await get(`/api/v1/agents?${params.toString()}`, listAgentsResponseSchema);
|
|
1142
|
-
all.push(...page.agents);
|
|
1143
|
-
cursor = page.nextCursor ?? void 0;
|
|
1144
|
-
onPage?.([...all]);
|
|
1145
|
-
} while (cursor && all.length < maxAgents);
|
|
1146
|
-
return all;
|
|
1147
|
-
},
|
|
1148
|
-
createAgent: async ({ name }) => {
|
|
1149
|
-
const { agent } = await post("/api/v1/agents", { name }, createAgentResponseSchema);
|
|
1150
|
-
return agent;
|
|
1151
|
-
},
|
|
1152
|
-
getConversation: async ({ conversationId }) => {
|
|
1153
|
-
const { conversation } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}`, getConversationResponseSchema);
|
|
1154
|
-
return conversation;
|
|
1155
|
-
},
|
|
1156
|
-
listModels: async () => {
|
|
1157
|
-
const { models } = await get("/api/v1/models", listModelsResponseSchema);
|
|
1158
|
-
return models;
|
|
1159
|
-
},
|
|
1160
|
-
updateAgentModel: async ({ agentId, model }) => {
|
|
1161
|
-
const { agent } = await post(`/api/v1/agents/${encodeURIComponent(agentId)}`, { model }, updateAgentResponseSchema, "PATCH");
|
|
1162
|
-
return { model: agent.model ?? null };
|
|
1163
|
-
},
|
|
1164
|
-
listConversations: async ({ agentId, limit, channels, onPage }) => {
|
|
1165
|
-
const all = [];
|
|
1166
|
-
const maxConversations = limit ?? 5e3;
|
|
1167
|
-
let cursor;
|
|
1168
|
-
do {
|
|
1169
|
-
const remaining = maxConversations - all.length;
|
|
1170
|
-
const params = new URLSearchParams({
|
|
1171
|
-
agentId,
|
|
1172
|
-
includeTotal: "false"
|
|
1173
|
-
});
|
|
1174
|
-
params.set("limit", String(Math.min(remaining, 100)));
|
|
1175
|
-
if (cursor) params.set("cursor", cursor);
|
|
1176
|
-
for (const channel of channels ?? []) params.append("channels", channel);
|
|
1177
|
-
const page = await get(`/api/v1/conversations?${params.toString()}`, listConversationsResponseSchema);
|
|
1178
|
-
all.push(...page.conversations);
|
|
1179
|
-
cursor = page.nextCursor ?? void 0;
|
|
1180
|
-
onPage?.(limit ? all.slice(0, limit) : [...all]);
|
|
1181
|
-
} while (cursor && all.length < maxConversations);
|
|
1182
|
-
return limit ? all.slice(0, limit) : all;
|
|
1183
|
-
},
|
|
1184
|
-
listMessages: async ({ conversationId }) => {
|
|
1185
|
-
const { messages } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`, listMessagesResponseSchema);
|
|
1186
|
-
return messages;
|
|
1187
|
-
},
|
|
1188
|
-
listWorkspaceFiles: async ({ agentId }) => {
|
|
1189
|
-
const { files } = await get(`/api/v1/workspace-files?${new URLSearchParams({ agentId }).toString()}`, listWorkspaceFilesResponseSchema);
|
|
1190
|
-
return files;
|
|
1191
|
-
},
|
|
1192
|
-
readWorkspaceFile: async ({ fileId, maxBytes = 512 * 1024 }) => {
|
|
1193
|
-
const res = await fetch(`${appUrl}/api/v1/workspace-files/${encodeURIComponent(fileId)}/download?disposition=inline`, { headers: {
|
|
1194
|
-
...baseHeaders,
|
|
1195
|
-
Range: `bytes=0-${maxBytes}`
|
|
1196
|
-
} });
|
|
1197
|
-
if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
1198
|
-
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
1199
|
-
const truncated = bytes.byteLength > maxBytes;
|
|
1200
|
-
return {
|
|
1201
|
-
text: new TextDecoder().decode(bytes.subarray(0, maxBytes)),
|
|
1202
|
-
truncated
|
|
1203
|
-
};
|
|
1204
|
-
},
|
|
1205
|
-
getRecap: async ({ conversationId }) => {
|
|
1206
|
-
const { recap } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/recap`, recapResponseSchema);
|
|
1207
|
-
return recap?.text ?? null;
|
|
1208
|
-
},
|
|
1209
|
-
uploadAttachment: async ({ agentId, fileName, mediaType, data }) => {
|
|
1210
|
-
const size = data.byteLength;
|
|
1211
|
-
const presign = await post("/api/v1/attachments/presign", {
|
|
1212
|
-
agentId,
|
|
1213
|
-
fileName,
|
|
1214
|
-
mediaType,
|
|
1215
|
-
size
|
|
1216
|
-
}, presignResponseSchema);
|
|
1217
|
-
const putRes = await fetch(presign.uploadUrl, {
|
|
1218
|
-
method: "PUT",
|
|
1219
|
-
headers: { "content-type": mediaType },
|
|
1220
|
-
body: new Uint8Array(data)
|
|
1221
|
-
});
|
|
1222
|
-
if (!putRes.ok) throw new HttpError(putRes.status, await putRes.text().catch(() => ""));
|
|
1223
|
-
const finalized = await post(`/api/v1/attachments/${encodeURIComponent(presign.id)}/finalize`, {
|
|
1224
|
-
agentId,
|
|
1225
|
-
fileName: presign.fileName,
|
|
1226
|
-
mediaType: presign.mediaType,
|
|
1227
|
-
size
|
|
1228
|
-
}, finalizeResponseSchema);
|
|
1229
|
-
return {
|
|
1230
|
-
id: presign.id,
|
|
1231
|
-
fileName: finalized.fileName,
|
|
1232
|
-
mediaType: finalized.mediaType,
|
|
1233
|
-
sizeBytes: finalized.sizeBytes ?? size
|
|
1234
|
-
};
|
|
1235
|
-
},
|
|
1236
|
-
deleteConversation: async ({ conversationId }) => {
|
|
1237
|
-
await del(`/api/v1/conversations/${encodeURIComponent(conversationId)}`);
|
|
1238
|
-
},
|
|
1239
|
-
sendMessage: async ({ clientSurface, ...input }) => post("/api/v1/chat/send", {
|
|
1240
|
-
...input,
|
|
1241
|
-
clientSurface
|
|
1242
|
-
}, sendResultSchema),
|
|
1243
|
-
cancelRun: async ({ runId }) => {
|
|
1244
|
-
await post(`/api/v1/chat/runs/${encodeURIComponent(runId)}/cancel`, {}, z.object({ ok: z.boolean() }));
|
|
1245
|
-
},
|
|
1246
|
-
cancelSteer: async ({ directiveId }) => {
|
|
1247
|
-
await post(`/api/v1/chat/steer/${encodeURIComponent(directiveId)}/cancel`, {}, z.object({ ok: z.boolean() }));
|
|
1248
|
-
},
|
|
1249
|
-
oauthConnect: async (input) => {
|
|
1250
|
-
const { connectLink } = await post("/api/v1/oauth/connect", input, oauthConnectResponseSchema);
|
|
1251
|
-
return { connectLink };
|
|
1252
|
-
},
|
|
1253
|
-
externalOauthConnect: async (input) => {
|
|
1254
|
-
const { authorizationUrl } = await post("/api/v1/external-oauth/connect", input, externalOauthConnectResponseSchema);
|
|
1255
|
-
return { authorizationUrl: authorizationUrl ?? null };
|
|
1256
|
-
},
|
|
1257
|
-
fulfillCredential: async ({ url, body }) => {
|
|
1258
|
-
const target = new URL(url, appUrl).toString();
|
|
1259
|
-
const res = await fetch(target, {
|
|
1260
|
-
method: "POST",
|
|
1261
|
-
headers: {
|
|
1262
|
-
...baseHeaders,
|
|
1263
|
-
"content-type": "application/json"
|
|
1264
|
-
},
|
|
1265
|
-
body: JSON.stringify(body)
|
|
1266
|
-
});
|
|
1267
|
-
if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
1268
|
-
},
|
|
1269
|
-
streamRun: async ({ runId, signal, onEvent }) => streamEvents({
|
|
1270
|
-
path: `/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`,
|
|
1271
|
-
label: "run",
|
|
1272
|
-
signal,
|
|
1273
|
-
onEvent
|
|
1274
|
-
}),
|
|
1275
|
-
streamMessage: async ({ messageId, signal, onEvent }) => streamEvents({
|
|
1276
|
-
path: `/api/v1/chat/messages/${encodeURIComponent(messageId)}/stream`,
|
|
1277
|
-
label: "message",
|
|
1278
|
-
signal,
|
|
1279
|
-
onEvent
|
|
1280
|
-
}),
|
|
1281
|
-
streamConversation: async ({ conversationId, signal, onEvent }) => {
|
|
1282
|
-
let reconnects = 0;
|
|
1283
|
-
for (;;) {
|
|
1284
|
-
if (signal.aborted) return;
|
|
1285
|
-
try {
|
|
1286
|
-
const res = await fetch(`${appUrl}/api/v1/chat/conversations/${encodeURIComponent(conversationId)}/stream`, {
|
|
1287
|
-
headers: {
|
|
1288
|
-
authorization: `Bearer ${sessionToken}`,
|
|
1289
|
-
accept: "text/event-stream"
|
|
1290
|
-
},
|
|
1291
|
-
signal
|
|
1292
|
-
});
|
|
1293
|
-
if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
1294
|
-
reconnects = 0;
|
|
1295
|
-
const parser = createParser({ onEvent: (message) => {
|
|
1296
|
-
const parsed = conversationStreamEventSchema.safeParse(JSON.parse(message.data));
|
|
1297
|
-
if (parsed.success) onEvent(parsed.data);
|
|
1298
|
-
} });
|
|
1299
|
-
const decoder = new TextDecoder();
|
|
1300
|
-
const reader = res.body.getReader();
|
|
1301
|
-
try {
|
|
1302
|
-
for (;;) {
|
|
1303
|
-
const { done, value } = await reader.read();
|
|
1304
|
-
if (done) break;
|
|
1305
|
-
parser.feed(decoder.decode(value, { stream: true }));
|
|
1306
|
-
}
|
|
1307
|
-
} finally {
|
|
1308
|
-
try {
|
|
1309
|
-
await reader.cancel();
|
|
1310
|
-
} catch (_error) {}
|
|
1311
|
-
}
|
|
1312
|
-
} catch (err) {
|
|
1313
|
-
if (signal.aborted) return;
|
|
1314
|
-
if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
|
|
1315
|
-
reconnects += 1;
|
|
1316
|
-
if (reconnects > MAX_STREAM_RECONNECTS) throw err;
|
|
1317
|
-
await sleep(Math.min(500 * 2 ** reconnects, 5e3));
|
|
1318
|
-
continue;
|
|
1319
|
-
}
|
|
1320
|
-
if (signal.aborted) return;
|
|
1321
|
-
reconnects += 1;
|
|
1322
|
-
if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("conversation stream ended unexpectedly");
|
|
1323
|
-
await sleep(Math.min(500 * 2 ** reconnects, 5e3));
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
function sleep(ms) {
|
|
1329
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1330
|
-
}
|
|
1331
|
-
const agentSummarySchema = z.object({
|
|
1332
|
-
id: z.string().uuid(),
|
|
1333
|
-
name: z.string(),
|
|
1334
|
-
slug: z.string().nullable().optional(),
|
|
1335
|
-
title: z.string().nullable().optional(),
|
|
1336
|
-
description: z.string().nullable().optional(),
|
|
1337
|
-
createdAt: z.string(),
|
|
1338
|
-
creatorName: z.string().nullable().optional(),
|
|
1339
|
-
model: z.string().nullable().optional(),
|
|
1340
|
-
modelLocked: z.boolean().optional()
|
|
1341
|
-
});
|
|
1342
|
-
const platformModelSchema = z.object({
|
|
1343
|
-
id: z.string(),
|
|
1344
|
-
displayName: z.string(),
|
|
1345
|
-
providerDisplay: z.string().optional(),
|
|
1346
|
-
reasoning: z.boolean().optional(),
|
|
1347
|
-
compliant: z.boolean().optional()
|
|
1348
|
-
}).passthrough();
|
|
1349
|
-
const listModelsResponseSchema = z.object({ models: z.array(platformModelSchema) });
|
|
1350
|
-
const updateAgentResponseSchema = z.object({ agent: z.object({ model: z.string().nullable().optional() }).passthrough() });
|
|
1351
|
-
const listAgentsResponseSchema = z.object({
|
|
1352
|
-
agents: z.array(agentSummarySchema),
|
|
1353
|
-
nextCursor: z.string().nullable().optional(),
|
|
1354
|
-
totalCount: z.number().nullable().optional()
|
|
1355
|
-
});
|
|
1356
|
-
const createAgentResponseSchema = z.object({ agent: agentSummarySchema });
|
|
1357
|
-
const conversationSummarySchema = z.object({
|
|
1358
|
-
id: z.string().uuid(),
|
|
1359
|
-
title: z.string().nullable(),
|
|
1360
|
-
createdAt: z.string(),
|
|
1361
|
-
updatedAt: z.string(),
|
|
1362
|
-
preview: z.string().nullable(),
|
|
1363
|
-
channel: z.string().nullable(),
|
|
1364
|
-
channelLabel: z.string().nullable(),
|
|
1365
|
-
agent: z.object({
|
|
1366
|
-
id: z.string().uuid(),
|
|
1367
|
-
name: z.string(),
|
|
1368
|
-
slug: z.string().nullable().optional(),
|
|
1369
|
-
title: z.string().nullable().optional()
|
|
1370
|
-
})
|
|
1371
|
-
});
|
|
1372
|
-
const conversationDetailSchema = z.object({
|
|
1373
|
-
id: z.string().uuid(),
|
|
1374
|
-
title: z.string().nullable(),
|
|
1375
|
-
agentId: z.string().uuid(),
|
|
1376
|
-
createdAt: z.string(),
|
|
1377
|
-
updatedAt: z.string()
|
|
1378
|
-
});
|
|
1379
|
-
const getConversationResponseSchema = z.object({ conversation: conversationDetailSchema });
|
|
1380
|
-
const listConversationsResponseSchema = z.object({
|
|
1381
|
-
conversations: z.array(conversationSummarySchema),
|
|
1382
|
-
nextCursor: z.string().nullable().optional(),
|
|
1383
|
-
totalCount: z.number().optional()
|
|
1384
|
-
});
|
|
1385
|
-
const uiMessagePartSchema = z.union([
|
|
1386
|
-
z.object({
|
|
1387
|
-
type: z.literal("text"),
|
|
1388
|
-
text: z.string()
|
|
1389
|
-
}),
|
|
1390
|
-
z.object({
|
|
1391
|
-
type: z.literal("reasoning"),
|
|
1392
|
-
text: z.string().optional()
|
|
1393
|
-
}),
|
|
1394
|
-
z.object({
|
|
1395
|
-
type: z.literal("dynamic-tool"),
|
|
1396
|
-
toolCallId: z.string(),
|
|
1397
|
-
toolName: z.string(),
|
|
1398
|
-
input: z.unknown().optional(),
|
|
1399
|
-
output: z.unknown().optional(),
|
|
1400
|
-
state: z.string().optional(),
|
|
1401
|
-
errorText: z.string().optional()
|
|
1402
|
-
}),
|
|
1403
|
-
z.object({ type: z.string() }).passthrough()
|
|
1404
|
-
]);
|
|
1405
|
-
const uiMessageSchema = z.object({
|
|
1406
|
-
id: z.string(),
|
|
1407
|
-
role: z.string(),
|
|
1408
|
-
parts: z.array(uiMessagePartSchema),
|
|
1409
|
-
metadata: z.object({ custom: z.object({
|
|
1410
|
-
agentId: z.string().nullish(),
|
|
1411
|
-
agentName: z.string().nullish()
|
|
1412
|
-
}).passthrough().optional() }).passthrough().optional()
|
|
1413
|
-
});
|
|
1414
|
-
const recapResponseSchema = z.object({ recap: z.object({ text: z.string() }).nullable() });
|
|
1415
|
-
const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
|
|
1416
|
-
const workspaceFileSchema = z.object({
|
|
1417
|
-
id: z.string(),
|
|
1418
|
-
path: z.string(),
|
|
1419
|
-
mediaType: z.string(),
|
|
1420
|
-
sizeBytes: z.number(),
|
|
1421
|
-
contentHash: z.string(),
|
|
1422
|
-
updatedAt: z.string(),
|
|
1423
|
-
shareUrl: z.string()
|
|
1424
|
-
});
|
|
1425
|
-
const listWorkspaceFilesResponseSchema = z.object({ files: z.array(workspaceFileSchema) });
|
|
1426
|
-
const sendResultSchema = z.object({
|
|
1427
|
-
runId: z.string(),
|
|
1428
|
-
messageId: z.string().uuid().nullish(),
|
|
1429
|
-
conversationId: z.string().uuid(),
|
|
1430
|
-
isNewConversation: z.boolean(),
|
|
1431
|
-
agentId: z.string().nullish(),
|
|
1432
|
-
agentName: z.string().nullish(),
|
|
1433
|
-
steered: z.boolean().optional(),
|
|
1434
|
-
directive: z.object({ id: z.string() }).passthrough().optional()
|
|
1435
|
-
});
|
|
1436
|
-
const presignResponseSchema = z.object({
|
|
1437
|
-
id: z.string(),
|
|
1438
|
-
uploadUrl: z.string(),
|
|
1439
|
-
fileName: z.string(),
|
|
1440
|
-
mediaType: z.string()
|
|
1441
|
-
});
|
|
1442
|
-
const finalizeResponseSchema = z.object({
|
|
1443
|
-
fileName: z.string(),
|
|
1444
|
-
mediaType: z.string(),
|
|
1445
|
-
sizeBytes: z.number().nullable().optional()
|
|
1446
|
-
});
|
|
1447
|
-
const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
|
|
1448
|
-
const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
|
|
1449
|
-
const runStreamEventSchema = z.union([z.object({
|
|
1450
|
-
kind: z.literal("chunk"),
|
|
1451
|
-
chunk: z.record(z.unknown())
|
|
1452
|
-
}), z.object({
|
|
1453
|
-
kind: z.literal("finished"),
|
|
1454
|
-
status: z.string(),
|
|
1455
|
-
error: z.string().nullish()
|
|
1456
|
-
})]);
|
|
1457
|
-
const streamErrorSchema = z.object({ error: z.string() });
|
|
1458
|
-
const conversationStreamEventSchema = z.discriminatedUnion("kind", [z.object({
|
|
1459
|
-
kind: z.literal("conversation"),
|
|
1460
|
-
id: z.string(),
|
|
1461
|
-
title: z.string().nullable()
|
|
1462
|
-
}), z.object({
|
|
1463
|
-
kind: z.literal("run"),
|
|
1464
|
-
runId: z.string()
|
|
1465
|
-
})]);
|
|
1466
|
-
|
|
1467
737
|
//#endregion
|
|
1468
738
|
//#region src/commands/session.ts
|
|
1469
739
|
/**
|
|
@@ -1750,7 +1020,7 @@ const keysCommand = {
|
|
|
1750
1020
|
//#endregion
|
|
1751
1021
|
//#region src/commands/secrets.ts
|
|
1752
1022
|
/** Read all of stdin as UTF-8, trimming a single trailing newline. */
|
|
1753
|
-
async function readStdin
|
|
1023
|
+
async function readStdin() {
|
|
1754
1024
|
const chunks = [];
|
|
1755
1025
|
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1756
1026
|
return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
@@ -1794,7 +1064,7 @@ const setCommand = {
|
|
|
1794
1064
|
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>`.");
|
|
1795
1065
|
process.exit(1);
|
|
1796
1066
|
}
|
|
1797
|
-
value = await readStdin
|
|
1067
|
+
value = await readStdin();
|
|
1798
1068
|
}
|
|
1799
1069
|
if (!value) {
|
|
1800
1070
|
printError("Empty secret value.");
|
|
@@ -1845,688 +1115,6 @@ const secretsCommand = {
|
|
|
1845
1115
|
handler: () => {}
|
|
1846
1116
|
};
|
|
1847
1117
|
|
|
1848
|
-
//#endregion
|
|
1849
|
-
//#region src/chat/tui/theme.ts
|
|
1850
|
-
const tokyonight = {
|
|
1851
|
-
id: "tokyonight",
|
|
1852
|
-
label: "Tokyo Night",
|
|
1853
|
-
mode: "dark",
|
|
1854
|
-
palette: {
|
|
1855
|
-
fg: "#c0caf5",
|
|
1856
|
-
muted: "#7a7a7a",
|
|
1857
|
-
dim: "#565f89",
|
|
1858
|
-
faint: "#3b4261",
|
|
1859
|
-
surface: "#16161e",
|
|
1860
|
-
accent: "#7aa2f7",
|
|
1861
|
-
success: "#9ece6a",
|
|
1862
|
-
warning: "#e0af68",
|
|
1863
|
-
error: "#f7768e",
|
|
1864
|
-
user: "#7aa2f7",
|
|
1865
|
-
assistant: "#c0caf5",
|
|
1866
|
-
tool: "#bb9af7",
|
|
1867
|
-
reasoning: "#565f89",
|
|
1868
|
-
syntaxBlue: "#7aa2f7",
|
|
1869
|
-
syntaxCyan: "#7dcfff",
|
|
1870
|
-
syntaxTeal: "#73daca",
|
|
1871
|
-
syntaxGreen: "#9ece6a",
|
|
1872
|
-
syntaxYellow: "#e0af68",
|
|
1873
|
-
syntaxOrange: "#ff9e64",
|
|
1874
|
-
syntaxRed: "#f7768e",
|
|
1875
|
-
syntaxMagenta: "#bb9af7"
|
|
1876
|
-
}
|
|
1877
|
-
};
|
|
1878
|
-
const tokyonightDay = {
|
|
1879
|
-
id: "tokyonight-day",
|
|
1880
|
-
label: "Tokyo Night Day",
|
|
1881
|
-
mode: "light",
|
|
1882
|
-
palette: {
|
|
1883
|
-
fg: "#3760bf",
|
|
1884
|
-
muted: "#848cb5",
|
|
1885
|
-
dim: "#9da3c2",
|
|
1886
|
-
faint: "#c4c8da",
|
|
1887
|
-
surface: "#d0d1d8",
|
|
1888
|
-
accent: "#2e7de9",
|
|
1889
|
-
success: "#587539",
|
|
1890
|
-
warning: "#8c6c3e",
|
|
1891
|
-
error: "#f52a65",
|
|
1892
|
-
user: "#2e7de9",
|
|
1893
|
-
assistant: "#3760bf",
|
|
1894
|
-
tool: "#9854f1",
|
|
1895
|
-
reasoning: "#848cb5",
|
|
1896
|
-
syntaxBlue: "#2e7de9",
|
|
1897
|
-
syntaxCyan: "#007197",
|
|
1898
|
-
syntaxTeal: "#118c74",
|
|
1899
|
-
syntaxGreen: "#587539",
|
|
1900
|
-
syntaxYellow: "#8c6c3e",
|
|
1901
|
-
syntaxOrange: "#b15c00",
|
|
1902
|
-
syntaxRed: "#f52a65",
|
|
1903
|
-
syntaxMagenta: "#9854f1"
|
|
1904
|
-
}
|
|
1905
|
-
};
|
|
1906
|
-
const catppuccinMocha = {
|
|
1907
|
-
id: "catppuccin-mocha",
|
|
1908
|
-
label: "Catppuccin Mocha",
|
|
1909
|
-
mode: "dark",
|
|
1910
|
-
palette: {
|
|
1911
|
-
fg: "#cdd6f4",
|
|
1912
|
-
muted: "#7f849c",
|
|
1913
|
-
dim: "#6c7086",
|
|
1914
|
-
faint: "#45475a",
|
|
1915
|
-
surface: "#181825",
|
|
1916
|
-
accent: "#89b4fa",
|
|
1917
|
-
success: "#a6e3a1",
|
|
1918
|
-
warning: "#f9e2af",
|
|
1919
|
-
error: "#f38ba8",
|
|
1920
|
-
user: "#89b4fa",
|
|
1921
|
-
assistant: "#cdd6f4",
|
|
1922
|
-
tool: "#cba6f7",
|
|
1923
|
-
reasoning: "#6c7086",
|
|
1924
|
-
syntaxBlue: "#89b4fa",
|
|
1925
|
-
syntaxCyan: "#89dceb",
|
|
1926
|
-
syntaxTeal: "#94e2d5",
|
|
1927
|
-
syntaxGreen: "#a6e3a1",
|
|
1928
|
-
syntaxYellow: "#f9e2af",
|
|
1929
|
-
syntaxOrange: "#fab387",
|
|
1930
|
-
syntaxRed: "#f38ba8",
|
|
1931
|
-
syntaxMagenta: "#cba6f7"
|
|
1932
|
-
}
|
|
1933
|
-
};
|
|
1934
|
-
const catppuccinLatte = {
|
|
1935
|
-
id: "catppuccin-latte",
|
|
1936
|
-
label: "Catppuccin Latte",
|
|
1937
|
-
mode: "light",
|
|
1938
|
-
palette: {
|
|
1939
|
-
fg: "#4c4f69",
|
|
1940
|
-
muted: "#8c8fa1",
|
|
1941
|
-
dim: "#9ca0b0",
|
|
1942
|
-
faint: "#bcc0cc",
|
|
1943
|
-
surface: "#e6e9ef",
|
|
1944
|
-
accent: "#1e66f5",
|
|
1945
|
-
success: "#40a02b",
|
|
1946
|
-
warning: "#df8e1d",
|
|
1947
|
-
error: "#d20f39",
|
|
1948
|
-
user: "#1e66f5",
|
|
1949
|
-
assistant: "#4c4f69",
|
|
1950
|
-
tool: "#8839ef",
|
|
1951
|
-
reasoning: "#9ca0b0",
|
|
1952
|
-
syntaxBlue: "#1e66f5",
|
|
1953
|
-
syntaxCyan: "#04a5e5",
|
|
1954
|
-
syntaxTeal: "#179299",
|
|
1955
|
-
syntaxGreen: "#40a02b",
|
|
1956
|
-
syntaxYellow: "#df8e1d",
|
|
1957
|
-
syntaxOrange: "#fe640b",
|
|
1958
|
-
syntaxRed: "#d20f39",
|
|
1959
|
-
syntaxMagenta: "#8839ef"
|
|
1960
|
-
}
|
|
1961
|
-
};
|
|
1962
|
-
const gruvboxDark = {
|
|
1963
|
-
id: "gruvbox-dark",
|
|
1964
|
-
label: "Gruvbox Dark",
|
|
1965
|
-
mode: "dark",
|
|
1966
|
-
palette: {
|
|
1967
|
-
fg: "#ebdbb2",
|
|
1968
|
-
muted: "#928374",
|
|
1969
|
-
dim: "#7c6f64",
|
|
1970
|
-
faint: "#504945",
|
|
1971
|
-
surface: "#1d2021",
|
|
1972
|
-
accent: "#83a598",
|
|
1973
|
-
success: "#b8bb26",
|
|
1974
|
-
warning: "#fabd2f",
|
|
1975
|
-
error: "#fb4934",
|
|
1976
|
-
user: "#83a598",
|
|
1977
|
-
assistant: "#ebdbb2",
|
|
1978
|
-
tool: "#d3869b",
|
|
1979
|
-
reasoning: "#7c6f64",
|
|
1980
|
-
syntaxBlue: "#83a598",
|
|
1981
|
-
syntaxCyan: "#8ec07c",
|
|
1982
|
-
syntaxTeal: "#8ec07c",
|
|
1983
|
-
syntaxGreen: "#b8bb26",
|
|
1984
|
-
syntaxYellow: "#fabd2f",
|
|
1985
|
-
syntaxOrange: "#fe8019",
|
|
1986
|
-
syntaxRed: "#fb4934",
|
|
1987
|
-
syntaxMagenta: "#d3869b"
|
|
1988
|
-
}
|
|
1989
|
-
};
|
|
1990
|
-
const gruvboxLight = {
|
|
1991
|
-
id: "gruvbox-light",
|
|
1992
|
-
label: "Gruvbox Light",
|
|
1993
|
-
mode: "light",
|
|
1994
|
-
palette: {
|
|
1995
|
-
fg: "#3c3836",
|
|
1996
|
-
muted: "#928374",
|
|
1997
|
-
dim: "#a89984",
|
|
1998
|
-
faint: "#d5c4a1",
|
|
1999
|
-
surface: "#ebdbb2",
|
|
2000
|
-
accent: "#076678",
|
|
2001
|
-
success: "#79740e",
|
|
2002
|
-
warning: "#b57614",
|
|
2003
|
-
error: "#9d0006",
|
|
2004
|
-
user: "#076678",
|
|
2005
|
-
assistant: "#3c3836",
|
|
2006
|
-
tool: "#8f3f71",
|
|
2007
|
-
reasoning: "#a89984",
|
|
2008
|
-
syntaxBlue: "#076678",
|
|
2009
|
-
syntaxCyan: "#427b58",
|
|
2010
|
-
syntaxTeal: "#427b58",
|
|
2011
|
-
syntaxGreen: "#79740e",
|
|
2012
|
-
syntaxYellow: "#b57614",
|
|
2013
|
-
syntaxOrange: "#af3a03",
|
|
2014
|
-
syntaxRed: "#9d0006",
|
|
2015
|
-
syntaxMagenta: "#8f3f71"
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
const solarizedDark = {
|
|
2019
|
-
id: "solarized-dark",
|
|
2020
|
-
label: "Solarized Dark",
|
|
2021
|
-
mode: "dark",
|
|
2022
|
-
palette: {
|
|
2023
|
-
fg: "#93a1a1",
|
|
2024
|
-
muted: "#586e75",
|
|
2025
|
-
dim: "#586e75",
|
|
2026
|
-
faint: "#073642",
|
|
2027
|
-
surface: "#00212b",
|
|
2028
|
-
accent: "#268bd2",
|
|
2029
|
-
success: "#859900",
|
|
2030
|
-
warning: "#b58900",
|
|
2031
|
-
error: "#dc322f",
|
|
2032
|
-
user: "#268bd2",
|
|
2033
|
-
assistant: "#93a1a1",
|
|
2034
|
-
tool: "#6c71c4",
|
|
2035
|
-
reasoning: "#586e75",
|
|
2036
|
-
syntaxBlue: "#268bd2",
|
|
2037
|
-
syntaxCyan: "#2aa198",
|
|
2038
|
-
syntaxTeal: "#2aa198",
|
|
2039
|
-
syntaxGreen: "#859900",
|
|
2040
|
-
syntaxYellow: "#b58900",
|
|
2041
|
-
syntaxOrange: "#cb4b16",
|
|
2042
|
-
syntaxRed: "#dc322f",
|
|
2043
|
-
syntaxMagenta: "#6c71c4"
|
|
2044
|
-
}
|
|
2045
|
-
};
|
|
2046
|
-
const solarizedLight = {
|
|
2047
|
-
id: "solarized-light",
|
|
2048
|
-
label: "Solarized Light",
|
|
2049
|
-
mode: "light",
|
|
2050
|
-
palette: {
|
|
2051
|
-
fg: "#657b83",
|
|
2052
|
-
muted: "#839496",
|
|
2053
|
-
dim: "#93a1a1",
|
|
2054
|
-
faint: "#eee8d5",
|
|
2055
|
-
surface: "#eee8d5",
|
|
2056
|
-
accent: "#268bd2",
|
|
2057
|
-
success: "#859900",
|
|
2058
|
-
warning: "#b58900",
|
|
2059
|
-
error: "#dc322f",
|
|
2060
|
-
user: "#268bd2",
|
|
2061
|
-
assistant: "#657b83",
|
|
2062
|
-
tool: "#6c71c4",
|
|
2063
|
-
reasoning: "#93a1a1",
|
|
2064
|
-
syntaxBlue: "#268bd2",
|
|
2065
|
-
syntaxCyan: "#2aa198",
|
|
2066
|
-
syntaxTeal: "#2aa198",
|
|
2067
|
-
syntaxGreen: "#859900",
|
|
2068
|
-
syntaxYellow: "#b58900",
|
|
2069
|
-
syntaxOrange: "#cb4b16",
|
|
2070
|
-
syntaxRed: "#dc322f",
|
|
2071
|
-
syntaxMagenta: "#6c71c4"
|
|
2072
|
-
}
|
|
2073
|
-
};
|
|
2074
|
-
const nord = {
|
|
2075
|
-
id: "nord",
|
|
2076
|
-
label: "Nord",
|
|
2077
|
-
mode: "dark",
|
|
2078
|
-
palette: {
|
|
2079
|
-
fg: "#d8dee9",
|
|
2080
|
-
muted: "#616e88",
|
|
2081
|
-
dim: "#4c566a",
|
|
2082
|
-
faint: "#3b4252",
|
|
2083
|
-
surface: "#272c36",
|
|
2084
|
-
accent: "#88c0d0",
|
|
2085
|
-
success: "#a3be8c",
|
|
2086
|
-
warning: "#ebcb8b",
|
|
2087
|
-
error: "#bf616a",
|
|
2088
|
-
user: "#88c0d0",
|
|
2089
|
-
assistant: "#d8dee9",
|
|
2090
|
-
tool: "#b48ead",
|
|
2091
|
-
reasoning: "#4c566a",
|
|
2092
|
-
syntaxBlue: "#81a1c1",
|
|
2093
|
-
syntaxCyan: "#88c0d0",
|
|
2094
|
-
syntaxTeal: "#8fbcbb",
|
|
2095
|
-
syntaxGreen: "#a3be8c",
|
|
2096
|
-
syntaxYellow: "#ebcb8b",
|
|
2097
|
-
syntaxOrange: "#d08770",
|
|
2098
|
-
syntaxRed: "#bf616a",
|
|
2099
|
-
syntaxMagenta: "#b48ead"
|
|
2100
|
-
}
|
|
2101
|
-
};
|
|
2102
|
-
const dracula = {
|
|
2103
|
-
id: "dracula",
|
|
2104
|
-
label: "Dracula",
|
|
2105
|
-
mode: "dark",
|
|
2106
|
-
palette: {
|
|
2107
|
-
fg: "#f8f8f2",
|
|
2108
|
-
muted: "#6272a4",
|
|
2109
|
-
dim: "#6272a4",
|
|
2110
|
-
faint: "#44475a",
|
|
2111
|
-
surface: "#21222c",
|
|
2112
|
-
accent: "#bd93f9",
|
|
2113
|
-
success: "#50fa7b",
|
|
2114
|
-
warning: "#ffb86c",
|
|
2115
|
-
error: "#ff5555",
|
|
2116
|
-
user: "#bd93f9",
|
|
2117
|
-
assistant: "#f8f8f2",
|
|
2118
|
-
tool: "#ff79c6",
|
|
2119
|
-
reasoning: "#6272a4",
|
|
2120
|
-
syntaxBlue: "#bd93f9",
|
|
2121
|
-
syntaxCyan: "#8be9fd",
|
|
2122
|
-
syntaxTeal: "#8be9fd",
|
|
2123
|
-
syntaxGreen: "#50fa7b",
|
|
2124
|
-
syntaxYellow: "#f1fa8c",
|
|
2125
|
-
syntaxOrange: "#ffb86c",
|
|
2126
|
-
syntaxRed: "#ff5555",
|
|
2127
|
-
syntaxMagenta: "#ff79c6"
|
|
2128
|
-
}
|
|
2129
|
-
};
|
|
2130
|
-
const oneDark = {
|
|
2131
|
-
id: "one-dark",
|
|
2132
|
-
label: "One Dark",
|
|
2133
|
-
mode: "dark",
|
|
2134
|
-
palette: {
|
|
2135
|
-
fg: "#abb2bf",
|
|
2136
|
-
muted: "#5c6370",
|
|
2137
|
-
dim: "#5c6370",
|
|
2138
|
-
faint: "#3e4451",
|
|
2139
|
-
surface: "#21252b",
|
|
2140
|
-
accent: "#61afef",
|
|
2141
|
-
success: "#98c379",
|
|
2142
|
-
warning: "#e5c07b",
|
|
2143
|
-
error: "#e06c75",
|
|
2144
|
-
user: "#61afef",
|
|
2145
|
-
assistant: "#abb2bf",
|
|
2146
|
-
tool: "#c678dd",
|
|
2147
|
-
reasoning: "#5c6370",
|
|
2148
|
-
syntaxBlue: "#61afef",
|
|
2149
|
-
syntaxCyan: "#56b6c2",
|
|
2150
|
-
syntaxTeal: "#56b6c2",
|
|
2151
|
-
syntaxGreen: "#98c379",
|
|
2152
|
-
syntaxYellow: "#e5c07b",
|
|
2153
|
-
syntaxOrange: "#d19a66",
|
|
2154
|
-
syntaxRed: "#e06c75",
|
|
2155
|
-
syntaxMagenta: "#c678dd"
|
|
2156
|
-
}
|
|
2157
|
-
};
|
|
2158
|
-
const oneLight = {
|
|
2159
|
-
id: "one-light",
|
|
2160
|
-
label: "One Light",
|
|
2161
|
-
mode: "light",
|
|
2162
|
-
palette: {
|
|
2163
|
-
fg: "#383a42",
|
|
2164
|
-
muted: "#a0a1a7",
|
|
2165
|
-
dim: "#a0a1a7",
|
|
2166
|
-
faint: "#e5e5e6",
|
|
2167
|
-
surface: "#f0f0f1",
|
|
2168
|
-
accent: "#4078f2",
|
|
2169
|
-
success: "#50a14f",
|
|
2170
|
-
warning: "#c18401",
|
|
2171
|
-
error: "#e45649",
|
|
2172
|
-
user: "#4078f2",
|
|
2173
|
-
assistant: "#383a42",
|
|
2174
|
-
tool: "#a626a4",
|
|
2175
|
-
reasoning: "#a0a1a7",
|
|
2176
|
-
syntaxBlue: "#4078f2",
|
|
2177
|
-
syntaxCyan: "#0184bc",
|
|
2178
|
-
syntaxTeal: "#0184bc",
|
|
2179
|
-
syntaxGreen: "#50a14f",
|
|
2180
|
-
syntaxYellow: "#c18401",
|
|
2181
|
-
syntaxOrange: "#986801",
|
|
2182
|
-
syntaxRed: "#e45649",
|
|
2183
|
-
syntaxMagenta: "#a626a4"
|
|
2184
|
-
}
|
|
2185
|
-
};
|
|
2186
|
-
const rosePine = {
|
|
2187
|
-
id: "rose-pine",
|
|
2188
|
-
label: "Rosé Pine",
|
|
2189
|
-
mode: "dark",
|
|
2190
|
-
palette: {
|
|
2191
|
-
fg: "#e0def4",
|
|
2192
|
-
muted: "#908caa",
|
|
2193
|
-
dim: "#6e6a86",
|
|
2194
|
-
faint: "#403d52",
|
|
2195
|
-
surface: "#16141f",
|
|
2196
|
-
accent: "#c4a7e7",
|
|
2197
|
-
success: "#9ccfd8",
|
|
2198
|
-
warning: "#f6c177",
|
|
2199
|
-
error: "#eb6f92",
|
|
2200
|
-
user: "#c4a7e7",
|
|
2201
|
-
assistant: "#e0def4",
|
|
2202
|
-
tool: "#ebbcba",
|
|
2203
|
-
reasoning: "#908caa",
|
|
2204
|
-
syntaxBlue: "#9ccfd8",
|
|
2205
|
-
syntaxCyan: "#9ccfd8",
|
|
2206
|
-
syntaxTeal: "#31748f",
|
|
2207
|
-
syntaxGreen: "#31748f",
|
|
2208
|
-
syntaxYellow: "#f6c177",
|
|
2209
|
-
syntaxOrange: "#ebbcba",
|
|
2210
|
-
syntaxRed: "#eb6f92",
|
|
2211
|
-
syntaxMagenta: "#c4a7e7"
|
|
2212
|
-
}
|
|
2213
|
-
};
|
|
2214
|
-
const rosePineDawn = {
|
|
2215
|
-
id: "rose-pine-dawn",
|
|
2216
|
-
label: "Rosé Pine Dawn",
|
|
2217
|
-
mode: "light",
|
|
2218
|
-
palette: {
|
|
2219
|
-
fg: "#575279",
|
|
2220
|
-
muted: "#797593",
|
|
2221
|
-
dim: "#9893a5",
|
|
2222
|
-
faint: "#cecacd",
|
|
2223
|
-
surface: "#f2e9e1",
|
|
2224
|
-
accent: "#907aa9",
|
|
2225
|
-
success: "#56949f",
|
|
2226
|
-
warning: "#ea9d34",
|
|
2227
|
-
error: "#b4637a",
|
|
2228
|
-
user: "#907aa9",
|
|
2229
|
-
assistant: "#575279",
|
|
2230
|
-
tool: "#d7827e",
|
|
2231
|
-
reasoning: "#9893a5",
|
|
2232
|
-
syntaxBlue: "#56949f",
|
|
2233
|
-
syntaxCyan: "#56949f",
|
|
2234
|
-
syntaxTeal: "#286983",
|
|
2235
|
-
syntaxGreen: "#286983",
|
|
2236
|
-
syntaxYellow: "#ea9d34",
|
|
2237
|
-
syntaxOrange: "#d7827e",
|
|
2238
|
-
syntaxRed: "#b4637a",
|
|
2239
|
-
syntaxMagenta: "#907aa9"
|
|
2240
|
-
}
|
|
2241
|
-
};
|
|
2242
|
-
const everforestDark = {
|
|
2243
|
-
id: "everforest-dark",
|
|
2244
|
-
label: "Everforest Dark",
|
|
2245
|
-
mode: "dark",
|
|
2246
|
-
palette: {
|
|
2247
|
-
fg: "#d3c6aa",
|
|
2248
|
-
muted: "#859289",
|
|
2249
|
-
dim: "#7a8478",
|
|
2250
|
-
faint: "#414b50",
|
|
2251
|
-
surface: "#232a2e",
|
|
2252
|
-
accent: "#7fbbb3",
|
|
2253
|
-
success: "#a7c080",
|
|
2254
|
-
warning: "#dbbc7f",
|
|
2255
|
-
error: "#e67e80",
|
|
2256
|
-
user: "#7fbbb3",
|
|
2257
|
-
assistant: "#d3c6aa",
|
|
2258
|
-
tool: "#d699b6",
|
|
2259
|
-
reasoning: "#7a8478",
|
|
2260
|
-
syntaxBlue: "#7fbbb3",
|
|
2261
|
-
syntaxCyan: "#83c092",
|
|
2262
|
-
syntaxTeal: "#83c092",
|
|
2263
|
-
syntaxGreen: "#a7c080",
|
|
2264
|
-
syntaxYellow: "#dbbc7f",
|
|
2265
|
-
syntaxOrange: "#e69875",
|
|
2266
|
-
syntaxRed: "#e67e80",
|
|
2267
|
-
syntaxMagenta: "#d699b6"
|
|
2268
|
-
}
|
|
2269
|
-
};
|
|
2270
|
-
const everforestLight = {
|
|
2271
|
-
id: "everforest-light",
|
|
2272
|
-
label: "Everforest Light",
|
|
2273
|
-
mode: "light",
|
|
2274
|
-
palette: {
|
|
2275
|
-
fg: "#5c6a72",
|
|
2276
|
-
muted: "#939f91",
|
|
2277
|
-
dim: "#a6b0a0",
|
|
2278
|
-
faint: "#e0dcc7",
|
|
2279
|
-
surface: "#f4f0d9",
|
|
2280
|
-
accent: "#3a94c5",
|
|
2281
|
-
success: "#8da101",
|
|
2282
|
-
warning: "#dfa000",
|
|
2283
|
-
error: "#f85552",
|
|
2284
|
-
user: "#3a94c5",
|
|
2285
|
-
assistant: "#5c6a72",
|
|
2286
|
-
tool: "#df69ba",
|
|
2287
|
-
reasoning: "#a6b0a0",
|
|
2288
|
-
syntaxBlue: "#3a94c5",
|
|
2289
|
-
syntaxCyan: "#35a77c",
|
|
2290
|
-
syntaxTeal: "#35a77c",
|
|
2291
|
-
syntaxGreen: "#8da101",
|
|
2292
|
-
syntaxYellow: "#dfa000",
|
|
2293
|
-
syntaxOrange: "#f57d26",
|
|
2294
|
-
syntaxRed: "#f85552",
|
|
2295
|
-
syntaxMagenta: "#df69ba"
|
|
2296
|
-
}
|
|
2297
|
-
};
|
|
2298
|
-
const githubDark = {
|
|
2299
|
-
id: "github-dark",
|
|
2300
|
-
label: "GitHub Dark",
|
|
2301
|
-
mode: "dark",
|
|
2302
|
-
palette: {
|
|
2303
|
-
fg: "#c9d1d9",
|
|
2304
|
-
muted: "#8b949e",
|
|
2305
|
-
dim: "#6e7681",
|
|
2306
|
-
faint: "#30363d",
|
|
2307
|
-
surface: "#161b22",
|
|
2308
|
-
accent: "#58a6ff",
|
|
2309
|
-
success: "#3fb950",
|
|
2310
|
-
warning: "#d29922",
|
|
2311
|
-
error: "#f85149",
|
|
2312
|
-
user: "#58a6ff",
|
|
2313
|
-
assistant: "#c9d1d9",
|
|
2314
|
-
tool: "#bc8cff",
|
|
2315
|
-
reasoning: "#6e7681",
|
|
2316
|
-
syntaxBlue: "#58a6ff",
|
|
2317
|
-
syntaxCyan: "#39c5cf",
|
|
2318
|
-
syntaxTeal: "#39c5cf",
|
|
2319
|
-
syntaxGreen: "#3fb950",
|
|
2320
|
-
syntaxYellow: "#d29922",
|
|
2321
|
-
syntaxOrange: "#db6d28",
|
|
2322
|
-
syntaxRed: "#f85149",
|
|
2323
|
-
syntaxMagenta: "#bc8cff"
|
|
2324
|
-
}
|
|
2325
|
-
};
|
|
2326
|
-
const githubLight = {
|
|
2327
|
-
id: "github-light",
|
|
2328
|
-
label: "GitHub Light",
|
|
2329
|
-
mode: "light",
|
|
2330
|
-
palette: {
|
|
2331
|
-
fg: "#24292f",
|
|
2332
|
-
muted: "#57606a",
|
|
2333
|
-
dim: "#8c959f",
|
|
2334
|
-
faint: "#d0d7de",
|
|
2335
|
-
surface: "#f6f8fa",
|
|
2336
|
-
accent: "#0969da",
|
|
2337
|
-
success: "#1a7f37",
|
|
2338
|
-
warning: "#9a6700",
|
|
2339
|
-
error: "#cf222e",
|
|
2340
|
-
user: "#0969da",
|
|
2341
|
-
assistant: "#24292f",
|
|
2342
|
-
tool: "#8250df",
|
|
2343
|
-
reasoning: "#8c959f",
|
|
2344
|
-
syntaxBlue: "#0969da",
|
|
2345
|
-
syntaxCyan: "#1b7c83",
|
|
2346
|
-
syntaxTeal: "#1b7c83",
|
|
2347
|
-
syntaxGreen: "#1a7f37",
|
|
2348
|
-
syntaxYellow: "#9a6700",
|
|
2349
|
-
syntaxOrange: "#bc4c00",
|
|
2350
|
-
syntaxRed: "#cf222e",
|
|
2351
|
-
syntaxMagenta: "#8250df"
|
|
2352
|
-
}
|
|
2353
|
-
};
|
|
2354
|
-
const kanagawa = {
|
|
2355
|
-
id: "kanagawa",
|
|
2356
|
-
label: "Kanagawa",
|
|
2357
|
-
mode: "dark",
|
|
2358
|
-
palette: {
|
|
2359
|
-
fg: "#dcd7ba",
|
|
2360
|
-
muted: "#727169",
|
|
2361
|
-
dim: "#54546d",
|
|
2362
|
-
faint: "#363646",
|
|
2363
|
-
surface: "#16161d",
|
|
2364
|
-
accent: "#7e9cd8",
|
|
2365
|
-
success: "#98bb6c",
|
|
2366
|
-
warning: "#e6c384",
|
|
2367
|
-
error: "#e46876",
|
|
2368
|
-
user: "#7e9cd8",
|
|
2369
|
-
assistant: "#dcd7ba",
|
|
2370
|
-
tool: "#957fb8",
|
|
2371
|
-
reasoning: "#727169",
|
|
2372
|
-
syntaxBlue: "#7e9cd8",
|
|
2373
|
-
syntaxCyan: "#7aa89f",
|
|
2374
|
-
syntaxTeal: "#7aa89f",
|
|
2375
|
-
syntaxGreen: "#98bb6c",
|
|
2376
|
-
syntaxYellow: "#e6c384",
|
|
2377
|
-
syntaxOrange: "#ffa066",
|
|
2378
|
-
syntaxRed: "#e46876",
|
|
2379
|
-
syntaxMagenta: "#957fb8"
|
|
2380
|
-
}
|
|
2381
|
-
};
|
|
2382
|
-
const cursorDark = {
|
|
2383
|
-
id: "cursor-dark",
|
|
2384
|
-
label: "Cursor Dark",
|
|
2385
|
-
mode: "dark",
|
|
2386
|
-
palette: {
|
|
2387
|
-
background: "#181818",
|
|
2388
|
-
fg: "#d4d4d4",
|
|
2389
|
-
muted: "#898989",
|
|
2390
|
-
dim: "#636363",
|
|
2391
|
-
faint: "#272727",
|
|
2392
|
-
surface: "#141414",
|
|
2393
|
-
accent: "#88c0d0",
|
|
2394
|
-
success: "#70b489",
|
|
2395
|
-
warning: "#f1b467",
|
|
2396
|
-
error: "#fc6b83",
|
|
2397
|
-
diffAdded: "#3fa266",
|
|
2398
|
-
diffRemoved: "#b80049",
|
|
2399
|
-
diffWeight: .2,
|
|
2400
|
-
user: "#88c0d0",
|
|
2401
|
-
assistant: "#d4d4d4",
|
|
2402
|
-
tool: "#aaa0fa",
|
|
2403
|
-
reasoning: "#636363",
|
|
2404
|
-
syntaxBlue: "#ebc88d",
|
|
2405
|
-
syntaxCyan: "#87c3ff",
|
|
2406
|
-
syntaxTeal: "#aaa0fa",
|
|
2407
|
-
syntaxGreen: "#e394dc",
|
|
2408
|
-
syntaxYellow: "#d4d4d4",
|
|
2409
|
-
syntaxOrange: "#f8c762",
|
|
2410
|
-
syntaxRed: "#cc7c8a",
|
|
2411
|
-
syntaxMagenta: "#82d2ce"
|
|
2412
|
-
}
|
|
2413
|
-
};
|
|
2414
|
-
const themes = [
|
|
2415
|
-
tokyonight,
|
|
2416
|
-
tokyonightDay,
|
|
2417
|
-
catppuccinMocha,
|
|
2418
|
-
catppuccinLatte,
|
|
2419
|
-
gruvboxDark,
|
|
2420
|
-
gruvboxLight,
|
|
2421
|
-
solarizedDark,
|
|
2422
|
-
solarizedLight,
|
|
2423
|
-
nord,
|
|
2424
|
-
dracula,
|
|
2425
|
-
oneDark,
|
|
2426
|
-
oneLight,
|
|
2427
|
-
rosePine,
|
|
2428
|
-
rosePineDawn,
|
|
2429
|
-
everforestDark,
|
|
2430
|
-
everforestLight,
|
|
2431
|
-
githubDark,
|
|
2432
|
-
githubLight,
|
|
2433
|
-
kanagawa,
|
|
2434
|
-
cursorDark
|
|
2435
|
-
];
|
|
2436
|
-
const DEFAULT_THEME_ID = {
|
|
2437
|
-
dark: tokyonight.id,
|
|
2438
|
-
light: tokyonightDay.id
|
|
2439
|
-
};
|
|
2440
|
-
/** All-undefined palette for NO_COLOR: every fg/bg falls back to the
|
|
2441
|
-
* terminal's own defaults, so nothing emits color. Not listed in `themes` —
|
|
2442
|
-
* it's forced, never picked. */
|
|
2443
|
-
const monoTheme = {
|
|
2444
|
-
id: "mono",
|
|
2445
|
-
label: "No color",
|
|
2446
|
-
mode: "dark",
|
|
2447
|
-
palette: {
|
|
2448
|
-
fg: void 0,
|
|
2449
|
-
muted: void 0,
|
|
2450
|
-
dim: void 0,
|
|
2451
|
-
faint: void 0,
|
|
2452
|
-
surface: void 0,
|
|
2453
|
-
accent: void 0,
|
|
2454
|
-
success: void 0,
|
|
2455
|
-
warning: void 0,
|
|
2456
|
-
error: void 0,
|
|
2457
|
-
user: void 0,
|
|
2458
|
-
assistant: void 0,
|
|
2459
|
-
tool: void 0,
|
|
2460
|
-
reasoning: void 0,
|
|
2461
|
-
syntaxBlue: void 0,
|
|
2462
|
-
syntaxCyan: void 0,
|
|
2463
|
-
syntaxTeal: void 0,
|
|
2464
|
-
syntaxGreen: void 0,
|
|
2465
|
-
syntaxYellow: void 0,
|
|
2466
|
-
syntaxOrange: void 0,
|
|
2467
|
-
syntaxRed: void 0,
|
|
2468
|
-
syntaxMagenta: void 0
|
|
2469
|
-
}
|
|
2470
|
-
};
|
|
2471
|
-
function themesForMode(mode) {
|
|
2472
|
-
return themes.filter((t) => t.mode === mode);
|
|
2473
|
-
}
|
|
2474
|
-
function findTheme(id) {
|
|
2475
|
-
if (id === monoTheme.id) return monoTheme;
|
|
2476
|
-
return themes.find((t) => t.id === id);
|
|
2477
|
-
}
|
|
2478
|
-
/**
|
|
2479
|
-
* The saved theme to use for a mode: the persisted pick if it exists *and*
|
|
2480
|
-
* still matches the mode (a stale/renamed id falls back), else the default.
|
|
2481
|
-
*/
|
|
2482
|
-
function themeForMode(mode, savedId) {
|
|
2483
|
-
if (savedId) {
|
|
2484
|
-
const saved = findTheme(savedId);
|
|
2485
|
-
if (saved && saved.mode === mode) return saved;
|
|
2486
|
-
}
|
|
2487
|
-
return findTheme(DEFAULT_THEME_ID[mode]) ?? tokyonight;
|
|
2488
|
-
}
|
|
2489
|
-
/** https://no-color.org — any non-empty value disables color output. */
|
|
2490
|
-
function noColorRequested(env = process.env) {
|
|
2491
|
-
const v = env["NO_COLOR"];
|
|
2492
|
-
return v !== void 0 && v !== "";
|
|
2493
|
-
}
|
|
2494
|
-
/**
|
|
2495
|
-
* Fallback light/dark sniff for terminals that never answer the OSC 10/11
|
|
2496
|
-
* query: `COLORFGBG` is "<fg>;<bg>" (sometimes "<fg>;default;<bg>") with
|
|
2497
|
-
* ANSI palette indexes. Background 7/15 (white/bright white) means a light
|
|
2498
|
-
* terminal; anything else we call dark. Returns null when unset/unparsable.
|
|
2499
|
-
*/
|
|
2500
|
-
function themeModeFromColorFgBg(env = process.env) {
|
|
2501
|
-
const raw = env["COLORFGBG"];
|
|
2502
|
-
if (!raw) return null;
|
|
2503
|
-
const parts = raw.split(";");
|
|
2504
|
-
const bg = parts[parts.length - 1];
|
|
2505
|
-
if (bg === void 0 || !/^\d+$/.test(bg)) return null;
|
|
2506
|
-
const idx = Number(bg);
|
|
2507
|
-
return idx === 7 || idx === 15 ? "light" : "dark";
|
|
2508
|
-
}
|
|
2509
|
-
/** Single source of truth for colors. Mutated in place by `applyTheme` so
|
|
2510
|
-
* existing `theme.fg`-style reads across the TUI stay valid. */
|
|
2511
|
-
const theme = { ...tokyonight.palette };
|
|
2512
|
-
let version = 0;
|
|
2513
|
-
function themeVersion() {
|
|
2514
|
-
return version;
|
|
2515
|
-
}
|
|
2516
|
-
let mode = tokyonight.mode;
|
|
2517
|
-
function themeMode() {
|
|
2518
|
-
return mode;
|
|
2519
|
-
}
|
|
2520
|
-
function applyTheme(def) {
|
|
2521
|
-
Object.assign(theme, def.palette);
|
|
2522
|
-
theme.background = def.palette.background;
|
|
2523
|
-
theme.diffAdded = def.palette.diffAdded;
|
|
2524
|
-
theme.diffRemoved = def.palette.diffRemoved;
|
|
2525
|
-
theme.diffWeight = def.palette.diffWeight;
|
|
2526
|
-
mode = def.mode;
|
|
2527
|
-
version++;
|
|
2528
|
-
}
|
|
2529
|
-
|
|
2530
1118
|
//#endregion
|
|
2531
1119
|
//#region src/chat/bun-runtime.ts
|
|
2532
1120
|
/**
|
|
@@ -2828,7 +1416,7 @@ const chatCommand = {
|
|
|
2828
1416
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
2829
1417
|
process.exit(1);
|
|
2830
1418
|
}
|
|
2831
|
-
const { runChat } = await import("./boot-
|
|
1419
|
+
const { runChat } = await import("./boot-CMhIv1BG.mjs");
|
|
2832
1420
|
await runChat({
|
|
2833
1421
|
appUrl,
|
|
2834
1422
|
sessionToken: session.value.sessionToken,
|
|
@@ -2855,7 +1443,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2855
1443
|
printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2856
1444
|
process.exit(1);
|
|
2857
1445
|
}
|
|
2858
|
-
const { runPrint, readStdin } = await
|
|
1446
|
+
const { runPrint, readStdin } = await import("./print-DFPzQQk8.mjs");
|
|
2859
1447
|
let prompt = (argv.print ?? "").trim();
|
|
2860
1448
|
if (!prompt) {
|
|
2861
1449
|
if (process.stdin.isTTY) {
|
|
@@ -2870,7 +1458,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2870
1458
|
}
|
|
2871
1459
|
let machineShare = null;
|
|
2872
1460
|
if (resolveShareMachine(argv)) {
|
|
2873
|
-
const { PortalClient } = await import("./client-
|
|
1461
|
+
const { PortalClient } = await import("./client-C-s6b9Yu.mjs");
|
|
2874
1462
|
let signalConnected;
|
|
2875
1463
|
const connected = new Promise((resolve) => {
|
|
2876
1464
|
signalConnected = resolve;
|
|
@@ -2943,7 +1531,7 @@ const getCommand = {
|
|
|
2943
1531
|
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2944
1532
|
process.exit(1);
|
|
2945
1533
|
}
|
|
2946
|
-
const { messageGet } = await
|
|
1534
|
+
const { messageGet } = await import("./print-DFPzQQk8.mjs");
|
|
2947
1535
|
try {
|
|
2948
1536
|
const result = await messageGet({
|
|
2949
1537
|
appUrl,
|
|
@@ -2965,488 +1553,6 @@ const messagesCommand = {
|
|
|
2965
1553
|
handler: () => {}
|
|
2966
1554
|
};
|
|
2967
1555
|
|
|
2968
|
-
//#endregion
|
|
2969
|
-
//#region src/chat/util.ts
|
|
2970
|
-
/** Narrowing helper for the many `unknown` payloads the chat stream and
|
|
2971
|
-
* tool inputs/outputs carry. A type predicate (not an `as` cast), so call
|
|
2972
|
-
* sites can read properties without asserting. */
|
|
2973
|
-
function isRecord(value) {
|
|
2974
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2975
|
-
}
|
|
2976
|
-
/** Best-effort message from an unknown thrown value. */
|
|
2977
|
-
function errorMessage(err) {
|
|
2978
|
-
return err instanceof Error ? err.message : String(err);
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
//#endregion
|
|
2982
|
-
//#region src/chat/tui/chat/card.ts
|
|
2983
|
-
const urlActionKinds = [
|
|
2984
|
-
"open_oauth",
|
|
2985
|
-
"open_external_oauth",
|
|
2986
|
-
"open_github_app",
|
|
2987
|
-
"submit_credential"
|
|
2988
|
-
];
|
|
2989
|
-
function isUrlActionKind(value) {
|
|
2990
|
-
return typeof value === "string" && urlActionKinds.includes(value);
|
|
2991
|
-
}
|
|
2992
|
-
function optionalString(value) {
|
|
2993
|
-
return typeof value === "string" && value ? value : null;
|
|
2994
|
-
}
|
|
2995
|
-
function bindStateKey(props) {
|
|
2996
|
-
const value = props.value;
|
|
2997
|
-
if (!isRecord(value)) return null;
|
|
2998
|
-
const pointer = value.$bindState;
|
|
2999
|
-
if (typeof pointer !== "string") return null;
|
|
3000
|
-
return pointer.startsWith("/") ? pointer.slice(1) : pointer;
|
|
3001
|
-
}
|
|
3002
|
-
function parseButton(element) {
|
|
3003
|
-
const props = isRecord(element.props) ? element.props : {};
|
|
3004
|
-
const label = optionalString(props.label) ?? "Connect";
|
|
3005
|
-
const on = isRecord(element.on) ? element.on : null;
|
|
3006
|
-
const press = on && isRecord(on.press) ? on.press : null;
|
|
3007
|
-
if (!press) return null;
|
|
3008
|
-
const params = isRecord(press.params) ? press.params : {};
|
|
3009
|
-
const primary = props.variant === "primary";
|
|
3010
|
-
if (press.action === "approve_portal_access") {
|
|
3011
|
-
const agentId = params.agentId;
|
|
3012
|
-
if (typeof agentId !== "string" || !agentId) return null;
|
|
3013
|
-
return {
|
|
3014
|
-
label,
|
|
3015
|
-
action: {
|
|
3016
|
-
kind: "grant_portal",
|
|
3017
|
-
agentId,
|
|
3018
|
-
deviceId: typeof params.deviceId === "string" && params.deviceId ? params.deviceId : null
|
|
3019
|
-
},
|
|
3020
|
-
primary
|
|
3021
|
-
};
|
|
3022
|
-
}
|
|
3023
|
-
if (!isUrlActionKind(press.action)) return null;
|
|
3024
|
-
const url = params.url;
|
|
3025
|
-
if (typeof url !== "string" || !url) return null;
|
|
3026
|
-
return {
|
|
3027
|
-
label,
|
|
3028
|
-
action: {
|
|
3029
|
-
kind: press.action,
|
|
3030
|
-
url
|
|
3031
|
-
},
|
|
3032
|
-
primary
|
|
3033
|
-
};
|
|
3034
|
-
}
|
|
3035
|
-
function parseConnectCard(spec) {
|
|
3036
|
-
if (!isRecord(spec)) return null;
|
|
3037
|
-
const { root, elements } = spec;
|
|
3038
|
-
if (typeof root !== "string" || !isRecord(elements)) return null;
|
|
3039
|
-
const rootEl = elements[root];
|
|
3040
|
-
if (!isRecord(rootEl) || rootEl.type !== "Card") return null;
|
|
3041
|
-
const rootProps = isRecord(rootEl.props) ? rootEl.props : {};
|
|
3042
|
-
const title = optionalString(rootProps.title);
|
|
3043
|
-
if (!title) return null;
|
|
3044
|
-
const fields = [];
|
|
3045
|
-
const buttons = [];
|
|
3046
|
-
const children = Array.isArray(rootEl.children) ? rootEl.children : [];
|
|
3047
|
-
for (const childId of children) {
|
|
3048
|
-
if (typeof childId !== "string") continue;
|
|
3049
|
-
const el = elements[childId];
|
|
3050
|
-
if (!isRecord(el)) continue;
|
|
3051
|
-
if (el.type === "TextInput" || el.type === "SecretInput") {
|
|
3052
|
-
const props = isRecord(el.props) ? el.props : {};
|
|
3053
|
-
const key = bindStateKey(props);
|
|
3054
|
-
if (!key) continue;
|
|
3055
|
-
fields.push({
|
|
3056
|
-
key,
|
|
3057
|
-
label: optionalString(props.label) ?? key,
|
|
3058
|
-
placeholder: optionalString(props.placeholder),
|
|
3059
|
-
secret: el.type === "SecretInput"
|
|
3060
|
-
});
|
|
3061
|
-
continue;
|
|
3062
|
-
}
|
|
3063
|
-
if (el.type === "Button") {
|
|
3064
|
-
const button = parseButton(el);
|
|
3065
|
-
if (button) buttons.push(button);
|
|
3066
|
-
}
|
|
3067
|
-
}
|
|
3068
|
-
const preferred = buttons.find((b) => b.primary) ?? buttons[0] ?? null;
|
|
3069
|
-
return {
|
|
3070
|
-
title,
|
|
3071
|
-
subtitle: optionalString(rootProps.subtitle),
|
|
3072
|
-
description: optionalString(rootProps.description),
|
|
3073
|
-
fields,
|
|
3074
|
-
button: preferred ? {
|
|
3075
|
-
label: preferred.label,
|
|
3076
|
-
action: preferred.action
|
|
3077
|
-
} : null,
|
|
3078
|
-
state: isRecord(spec.state) ? spec.state : {}
|
|
3079
|
-
};
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
//#endregion
|
|
3083
|
-
//#region src/chat/tui/chat/card-actions.ts
|
|
3084
|
-
function safeUrl(url) {
|
|
3085
|
-
try {
|
|
3086
|
-
return new URL(url, "http://localhost");
|
|
3087
|
-
} catch (_error) {
|
|
3088
|
-
return null;
|
|
3089
|
-
}
|
|
3090
|
-
}
|
|
3091
|
-
/**
|
|
3092
|
-
* Resolve a connect URL to an absolute one the OS can open in a browser.
|
|
3093
|
-
*
|
|
3094
|
-
* The server builds lazy connect links as server-relative paths (e.g.
|
|
3095
|
-
* `/api/v1/oauth/start/slack?connect_session_token=...`). The web client
|
|
3096
|
-
* resolves these against its own origin implicitly; the TUI runs outside a
|
|
3097
|
-
* browser, so `open()` on a scheme-less path is a silent no-op — the browser
|
|
3098
|
-
* never launches and the connect card sits in `launched` forever (the Slack
|
|
3099
|
-
* channel-connect deadlock). Resolve against `appUrl` first, mirroring the
|
|
3100
|
-
* `open_github_app` / `fulfillCredential` branches, so both the browser we
|
|
3101
|
-
* launch and the URL shown on the card are absolute. An already-absolute URL
|
|
3102
|
-
* passes through unchanged; if `appUrl` is missing we return the input as-is.
|
|
3103
|
-
*/
|
|
3104
|
-
function resolveConnectUrl(url, appUrl) {
|
|
3105
|
-
if (!appUrl) return url;
|
|
3106
|
-
try {
|
|
3107
|
-
return new URL(url, appUrl).toString();
|
|
3108
|
-
} catch (_error) {
|
|
3109
|
-
return url;
|
|
3110
|
-
}
|
|
3111
|
-
}
|
|
3112
|
-
function parseOauthConnectParams(url) {
|
|
3113
|
-
const parsed = safeUrl(url);
|
|
3114
|
-
if (!parsed) return null;
|
|
3115
|
-
const integrationKey = parsed.searchParams.get("integration");
|
|
3116
|
-
const agentId = parsed.searchParams.get("agent_id");
|
|
3117
|
-
const authConfigId = parsed.searchParams.get("auth_config_id");
|
|
3118
|
-
const conversationId = parsed.searchParams.get("conversation_id");
|
|
3119
|
-
if (!integrationKey || !agentId || !authConfigId || !conversationId) return null;
|
|
3120
|
-
return {
|
|
3121
|
-
integrationKey,
|
|
3122
|
-
agentId,
|
|
3123
|
-
authConfigId,
|
|
3124
|
-
conversationId,
|
|
3125
|
-
scopes: parsed.searchParams.get("scopes")
|
|
3126
|
-
};
|
|
3127
|
-
}
|
|
3128
|
-
function parseExternalOauthConnectParams(url) {
|
|
3129
|
-
const parsed = safeUrl(url);
|
|
3130
|
-
if (!parsed) return null;
|
|
3131
|
-
const agentId = parsed.searchParams.get("agent_id");
|
|
3132
|
-
const conversationId = parsed.searchParams.get("conversation_id");
|
|
3133
|
-
const serverUrl = parsed.searchParams.get("server_url");
|
|
3134
|
-
if (!agentId || !conversationId || !serverUrl) return null;
|
|
3135
|
-
return {
|
|
3136
|
-
agentId,
|
|
3137
|
-
conversationId,
|
|
3138
|
-
serverUrl
|
|
3139
|
-
};
|
|
3140
|
-
}
|
|
3141
|
-
/**
|
|
3142
|
-
* A short human line for a failed card action. Fulfill/connect endpoints
|
|
3143
|
-
* return `{ error: string }` bodies (e.g. "already fulfilled") — prefer that
|
|
3144
|
-
* over the generic HttpError message.
|
|
3145
|
-
*/
|
|
3146
|
-
function cardActionErrorMessage(err) {
|
|
3147
|
-
if (err instanceof HttpError) {
|
|
3148
|
-
try {
|
|
3149
|
-
const parsed = JSON.parse(err.body);
|
|
3150
|
-
if (isRecord(parsed) && typeof parsed.error === "string") return parsed.error;
|
|
3151
|
-
} catch (_error) {}
|
|
3152
|
-
return `request failed (HTTP ${err.status})`;
|
|
3153
|
-
}
|
|
3154
|
-
return errorMessage(err);
|
|
3155
|
-
}
|
|
3156
|
-
const MASK_CHAR = "•";
|
|
3157
|
-
/**
|
|
3158
|
-
* Recover the real secret from the masked input's displayed text. The input
|
|
3159
|
-
* is controlled: after every edit we render bullets, which forces the cursor
|
|
3160
|
-
* to the end, so the next edit is always a tail edit — the displayed text is
|
|
3161
|
-
* some prefix of the old mask (kept characters) followed by newly typed or
|
|
3162
|
-
* pasted plaintext. Characters beyond the retained bullets are the new tail.
|
|
3163
|
-
*/
|
|
3164
|
-
function reconcileMaskedInput(previousValue, displayed) {
|
|
3165
|
-
let kept = 0;
|
|
3166
|
-
while (kept < displayed.length && kept < previousValue.length && displayed[kept] === MASK_CHAR) kept++;
|
|
3167
|
-
return previousValue.slice(0, kept) + displayed.slice(kept);
|
|
3168
|
-
}
|
|
3169
|
-
|
|
3170
|
-
//#endregion
|
|
3171
|
-
//#region src/chat/connect-cards.ts
|
|
3172
|
-
/**
|
|
3173
|
-
* Turn a parsed ConnectCard into the headless summary, resolving any relative
|
|
3174
|
-
* connect URL against the app origin so the emitted URL is directly openable.
|
|
3175
|
-
*/
|
|
3176
|
-
function summarizeConnectCard(card, appUrl) {
|
|
3177
|
-
const base = {
|
|
3178
|
-
title: card.title,
|
|
3179
|
-
subtitle: card.subtitle,
|
|
3180
|
-
description: card.description
|
|
3181
|
-
};
|
|
3182
|
-
const button = card.button;
|
|
3183
|
-
if (!button) return {
|
|
3184
|
-
...base,
|
|
3185
|
-
action: { kind: "unsupported" }
|
|
3186
|
-
};
|
|
3187
|
-
const act = button.action;
|
|
3188
|
-
switch (act.kind) {
|
|
3189
|
-
case "open_oauth":
|
|
3190
|
-
case "open_external_oauth":
|
|
3191
|
-
case "open_github_app": return {
|
|
3192
|
-
...base,
|
|
3193
|
-
action: {
|
|
3194
|
-
kind: "open_url",
|
|
3195
|
-
url: resolveConnectUrl(act.url, appUrl)
|
|
3196
|
-
}
|
|
3197
|
-
};
|
|
3198
|
-
case "submit_credential": return {
|
|
3199
|
-
...base,
|
|
3200
|
-
action: {
|
|
3201
|
-
kind: "submit_credential",
|
|
3202
|
-
url: resolveConnectUrl(act.url, appUrl),
|
|
3203
|
-
fields: card.fields.map((f) => f.label)
|
|
3204
|
-
}
|
|
3205
|
-
};
|
|
3206
|
-
case "grant_portal": return {
|
|
3207
|
-
...base,
|
|
3208
|
-
action: {
|
|
3209
|
-
kind: "approve_portal",
|
|
3210
|
-
agentId: act.agentId
|
|
3211
|
-
}
|
|
3212
|
-
};
|
|
3213
|
-
default: return {
|
|
3214
|
-
...base,
|
|
3215
|
-
action: { kind: "unsupported" }
|
|
3216
|
-
};
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
/**
|
|
3220
|
-
* Parse a `data-anyone-render-spec` stream chunk into a connect-card summary,
|
|
3221
|
-
* or null if the chunk isn't a connect card (other render specs — training,
|
|
3222
|
-
* deep-learn, compute-request — parse to null, same as the TUI).
|
|
3223
|
-
*/
|
|
3224
|
-
function connectCardFromChunk(chunk, appUrl) {
|
|
3225
|
-
if (chunk["type"] !== "data-anyone-render-spec") return null;
|
|
3226
|
-
const data = chunk["data"];
|
|
3227
|
-
if (!isRecord(data)) return null;
|
|
3228
|
-
const card = parseConnectCard(data["spec"]);
|
|
3229
|
-
if (!card) return null;
|
|
3230
|
-
return summarizeConnectCard(card, appUrl);
|
|
3231
|
-
}
|
|
3232
|
-
/** Render a connect-card summary as a human-readable action block. */
|
|
3233
|
-
function formatConnectCard(card) {
|
|
3234
|
-
const lines = [];
|
|
3235
|
-
lines.push(`\n[action needed] ${card.title}`);
|
|
3236
|
-
if (card.subtitle) lines.push(card.subtitle);
|
|
3237
|
-
if (card.description) lines.push(card.description);
|
|
3238
|
-
switch (card.action.kind) {
|
|
3239
|
-
case "open_url":
|
|
3240
|
-
lines.push(`Open to continue: ${card.action.url}`);
|
|
3241
|
-
break;
|
|
3242
|
-
case "submit_credential":
|
|
3243
|
-
lines.push(`Provide credential (${card.action.fields.join(", ") || "value"}) at: ${card.action.url}`);
|
|
3244
|
-
break;
|
|
3245
|
-
case "approve_portal":
|
|
3246
|
-
lines.push(`Approve local-machine access for agent ${card.action.agentId} in the TUI or web app.`);
|
|
3247
|
-
break;
|
|
3248
|
-
case "unsupported":
|
|
3249
|
-
lines.push("Open this conversation in the web app to continue.");
|
|
3250
|
-
break;
|
|
3251
|
-
}
|
|
3252
|
-
return lines.join("\n");
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
//#endregion
|
|
3256
|
-
//#region src/chat/print.ts
|
|
3257
|
-
var print_exports = /* @__PURE__ */ __exportAll({
|
|
3258
|
-
collectRunText: () => collectRunText,
|
|
3259
|
-
messageGet: () => messageGet,
|
|
3260
|
-
readStdin: () => readStdin,
|
|
3261
|
-
resolveAgent: () => resolveAgent,
|
|
3262
|
-
runPrint: () => runPrint,
|
|
3263
|
-
toPrintError: () => toPrintError
|
|
3264
|
-
});
|
|
3265
|
-
/**
|
|
3266
|
-
* Non-interactive chat, à la `claude -p`. Sends a single prompt to an
|
|
3267
|
-
* agent, streams the run, and prints the assistant's reply to stdout
|
|
3268
|
-
* before exiting. No OpenTUI, no Bun requirement — this rides the same
|
|
3269
|
-
* Node-friendly REST client the TUI uses, so it runs anywhere the
|
|
3270
|
-
* management commands do (CI, pipes, scripts).
|
|
3271
|
-
*
|
|
3272
|
-
* Resolution rules kept deliberately strict because there's no human to
|
|
3273
|
-
* disambiguate: an `--agent` selector must match exactly one agent, and
|
|
3274
|
-
* when it's omitted we only auto-pick if the account has exactly one.
|
|
3275
|
-
*/
|
|
3276
|
-
async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversationId, json, machineShare, grantTargetAgent }) {
|
|
3277
|
-
const client = createRestClient({
|
|
3278
|
-
appUrl,
|
|
3279
|
-
sessionToken
|
|
3280
|
-
});
|
|
3281
|
-
const agent = resolveAgent(await client.listAgents({
|
|
3282
|
-
scope: "org",
|
|
3283
|
-
onPage: null
|
|
3284
|
-
}), agentSelector);
|
|
3285
|
-
if (machineShare) {
|
|
3286
|
-
if (!machineShare.isGranted(agent.id) && grantTargetAgent) await machineShare.grantAgent(agent.id);
|
|
3287
|
-
if (machineShare.isGranted(agent.id)) console.error(`portal: shared this machine with ${agent.name} for this run (grant persists until revoked)`);
|
|
3288
|
-
else console.error(`portal: this machine is shared (shareMachineDefault), but ${agent.name} has no grant — approve its request, or run \`skydive portal grant --agent ${agent.name}\`.`);
|
|
3289
|
-
}
|
|
3290
|
-
let send;
|
|
3291
|
-
try {
|
|
3292
|
-
send = await client.sendMessage({
|
|
3293
|
-
agentId: agent.id,
|
|
3294
|
-
conversationId,
|
|
3295
|
-
content: prompt,
|
|
3296
|
-
attachmentIds: [],
|
|
3297
|
-
clientSurface: "cli"
|
|
3298
|
-
});
|
|
3299
|
-
} catch (err) {
|
|
3300
|
-
throw toPrintError(err);
|
|
3301
|
-
}
|
|
3302
|
-
const { text, connectCards } = await collectRunText({
|
|
3303
|
-
client,
|
|
3304
|
-
appUrl,
|
|
3305
|
-
target: {
|
|
3306
|
-
kind: "run",
|
|
3307
|
-
id: send.runId
|
|
3308
|
-
},
|
|
3309
|
-
onText: json ? null : (delta) => process.stdout.write(delta),
|
|
3310
|
-
messageIdForHint: send.messageId ?? null
|
|
3311
|
-
});
|
|
3312
|
-
if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
|
|
3313
|
-
if (!json) for (const card of connectCards) process.stdout.write(`${formatConnectCard(card)}\n`);
|
|
3314
|
-
return {
|
|
3315
|
-
agentId: agent.id,
|
|
3316
|
-
agentName: agent.name,
|
|
3317
|
-
conversationId: send.conversationId,
|
|
3318
|
-
isNewConversation: send.isNewConversation,
|
|
3319
|
-
messageId: send.messageId ?? null,
|
|
3320
|
-
text,
|
|
3321
|
-
connectCards
|
|
3322
|
-
};
|
|
3323
|
-
}
|
|
3324
|
-
/**
|
|
3325
|
-
* Turn a transport failure into an actionable CLI error. A Cloudflare edge
|
|
3326
|
-
* 5xx (502/504) on a long run otherwise leaks a raw HTML/JSON error page to
|
|
3327
|
-
* stdout, which is impossible to act on. When a messageId is known and
|
|
3328
|
-
* recoverable we point the user at `skydive messages get <messageId>` rather
|
|
3329
|
-
* than a blind retry (a retry re-executes an agent that may have write access).
|
|
3330
|
-
*/
|
|
3331
|
-
function toPrintError(err, messageId) {
|
|
3332
|
-
if (err instanceof HttpError && err.status >= 500) {
|
|
3333
|
-
const recovery = messageId ? ` The run may still be completing server-side. Do NOT blindly retry (it would re-run the agent). Fetch the result with: skydive messages get ${messageId}` : "";
|
|
3334
|
-
return /* @__PURE__ */ new Error(`The request to Skydive timed out at the edge (HTTP ${err.status}).${recovery}`);
|
|
3335
|
-
}
|
|
3336
|
-
return err instanceof Error ? err : new Error(String(err));
|
|
3337
|
-
}
|
|
3338
|
-
/**
|
|
3339
|
-
* Stream a run to completion, folding text-delta chunks into the reply and
|
|
3340
|
-
* collecting any connect cards (OAuth / MCP / credential requests) the run
|
|
3341
|
-
* posts. Shared by `chat -p` (streaming the run it just created) and `messages
|
|
3342
|
-
* get` (re-attaching by message id — the server replays a finished run from its
|
|
3343
|
-
* persisted log, so this works whether the run is live or already done).
|
|
3344
|
-
*/
|
|
3345
|
-
async function collectRunText({ client, appUrl, target, onText, messageIdForHint }) {
|
|
3346
|
-
let text = "";
|
|
3347
|
-
const controller = new AbortController();
|
|
3348
|
-
let streamError = null;
|
|
3349
|
-
const connectCards = [];
|
|
3350
|
-
const onEvent = (event) => {
|
|
3351
|
-
if (event.kind === "finished") {
|
|
3352
|
-
if (event.error) streamError = event.error;
|
|
3353
|
-
return;
|
|
3354
|
-
}
|
|
3355
|
-
const chunk = event.chunk;
|
|
3356
|
-
if (chunk["type"] === "text-delta") {
|
|
3357
|
-
const delta = typeof chunk["delta"] === "string" ? chunk["delta"] : typeof chunk["text"] === "string" ? chunk["text"] : "";
|
|
3358
|
-
if (delta) {
|
|
3359
|
-
text += delta;
|
|
3360
|
-
if (onText) onText(delta);
|
|
3361
|
-
}
|
|
3362
|
-
} else if (chunk["type"] === "error") streamError = typeof chunk["errorText"] === "string" ? chunk["errorText"] : "unknown error";
|
|
3363
|
-
else {
|
|
3364
|
-
const card = connectCardFromChunk(chunk, appUrl);
|
|
3365
|
-
if (card) connectCards.push(card);
|
|
3366
|
-
}
|
|
3367
|
-
};
|
|
3368
|
-
try {
|
|
3369
|
-
if (target.kind === "message") await client.streamMessage({
|
|
3370
|
-
messageId: target.id,
|
|
3371
|
-
signal: controller.signal,
|
|
3372
|
-
onEvent
|
|
3373
|
-
});
|
|
3374
|
-
else await client.streamRun({
|
|
3375
|
-
runId: target.id,
|
|
3376
|
-
signal: controller.signal,
|
|
3377
|
-
onEvent
|
|
3378
|
-
});
|
|
3379
|
-
} catch (err) {
|
|
3380
|
-
throw toPrintError(err, messageIdForHint);
|
|
3381
|
-
}
|
|
3382
|
-
if (streamError) throw new Error(streamError);
|
|
3383
|
-
return {
|
|
3384
|
-
text,
|
|
3385
|
-
connectCards
|
|
3386
|
-
};
|
|
3387
|
-
}
|
|
3388
|
-
/**
|
|
3389
|
-
* Re-attach to an exchange by message id and print its reply. Backs `skydive
|
|
3390
|
-
* messages get <messageId>` — the recovery path when a `chat -p` stream dropped
|
|
3391
|
-
* at the edge after the message was accepted. The server resolves the run
|
|
3392
|
-
* behind the message and replays it from its persisted event log, so this
|
|
3393
|
-
* returns the full reply whether the run is still live or already done.
|
|
3394
|
-
*/
|
|
3395
|
-
async function messageGet({ appUrl, sessionToken, messageId, json }) {
|
|
3396
|
-
const { text, connectCards } = await collectRunText({
|
|
3397
|
-
client: createRestClient({
|
|
3398
|
-
appUrl,
|
|
3399
|
-
sessionToken
|
|
3400
|
-
}),
|
|
3401
|
-
appUrl,
|
|
3402
|
-
target: {
|
|
3403
|
-
kind: "message",
|
|
3404
|
-
id: messageId
|
|
3405
|
-
},
|
|
3406
|
-
onText: json ? null : (delta) => process.stdout.write(delta),
|
|
3407
|
-
messageIdForHint: null
|
|
3408
|
-
});
|
|
3409
|
-
if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
|
|
3410
|
-
if (!json) for (const card of connectCards) process.stdout.write(`${formatConnectCard(card)}\n`);
|
|
3411
|
-
return {
|
|
3412
|
-
messageId,
|
|
3413
|
-
text,
|
|
3414
|
-
connectCards
|
|
3415
|
-
};
|
|
3416
|
-
}
|
|
3417
|
-
/**
|
|
3418
|
-
* Pick the target agent. With no selector, auto-pick only when the
|
|
3419
|
-
* account has exactly one agent; otherwise the user must name one (there's
|
|
3420
|
-
* no picker in non-interactive mode). A selector matches by id first, then
|
|
3421
|
-
* a unique case-insensitive slug/name; ambiguous or missing matches throw
|
|
3422
|
-
* with the candidate list so the caller knows what to pass.
|
|
3423
|
-
*/
|
|
3424
|
-
function resolveAgent(agents, selector) {
|
|
3425
|
-
if (!selector) {
|
|
3426
|
-
const [only, ...rest] = agents;
|
|
3427
|
-
if (!only) throw new Error("No agents on this account.");
|
|
3428
|
-
if (rest.length === 0) return only;
|
|
3429
|
-
throw new Error(`Multiple agents on this account — pass --agent <id|slug|name>. Candidates:\n${formatCandidates(agents)}`);
|
|
3430
|
-
}
|
|
3431
|
-
const byId = agents.find((a) => a.id === selector);
|
|
3432
|
-
if (byId) return byId;
|
|
3433
|
-
const needle = selector.toLowerCase();
|
|
3434
|
-
const matches = agents.filter((a) => a.slug && a.slug.toLowerCase() === needle || a.name.toLowerCase() === needle);
|
|
3435
|
-
const [firstMatch, ...restMatches] = matches;
|
|
3436
|
-
if (firstMatch && restMatches.length === 0) return firstMatch;
|
|
3437
|
-
if (restMatches.length > 0) throw new Error(`Multiple agents match "${selector}" — pass the id instead. Candidates:\n${formatCandidates(matches)}`);
|
|
3438
|
-
throw new Error(`No agent matches "${selector}". Candidates:\n${formatCandidates(agents)}`);
|
|
3439
|
-
}
|
|
3440
|
-
function formatCandidates(agents) {
|
|
3441
|
-
return agents.slice(0, 25).map((a) => ` ${a.id} ${a.slug ?? a.name}`).join("\n");
|
|
3442
|
-
}
|
|
3443
|
-
/** Read all of stdin as UTF-8. Used when `-p` is passed with no value. */
|
|
3444
|
-
async function readStdin() {
|
|
3445
|
-
const chunks = [];
|
|
3446
|
-
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3447
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
3448
|
-
}
|
|
3449
|
-
|
|
3450
1556
|
//#endregion
|
|
3451
1557
|
//#region src/commands/conversations.ts
|
|
3452
1558
|
/**
|
|
@@ -3622,7 +1728,7 @@ const switchCommand = {
|
|
|
3622
1728
|
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.");
|
|
3623
1729
|
process.exit(1);
|
|
3624
1730
|
}
|
|
3625
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
1731
|
+
const { runWorkspacePicker } = await import("./boot-CMhIv1BG.mjs");
|
|
3626
1732
|
await runWorkspacePicker(session);
|
|
3627
1733
|
return;
|
|
3628
1734
|
}
|
|
@@ -3659,145 +1765,6 @@ const workspaceCommand = {
|
|
|
3659
1765
|
handler: (argv) => runList(argv, { hint: true })
|
|
3660
1766
|
};
|
|
3661
1767
|
|
|
3662
|
-
//#endregion
|
|
3663
|
-
//#region src/chat/portal/machine.ts
|
|
3664
|
-
/**
|
|
3665
|
-
* Identity this machine registers under when the CLI shares it via the portal.
|
|
3666
|
-
*
|
|
3667
|
-
* The `-cli` suffix / `(CLI)` label keep a CLI-shared machine a DISTINCT portal
|
|
3668
|
-
* device from the same host's Skydive Desktop app. `portal_device` is unique on
|
|
3669
|
-
* (org, user, machineName), and directives route to whichever socket holds the
|
|
3670
|
-
* device — if the CLI and desktop registered the same name they'd share a
|
|
3671
|
-
* device row and both execute every directive. Distinct names also make the
|
|
3672
|
-
* grant UI unambiguous about which surface is being authorized.
|
|
3673
|
-
*/
|
|
3674
|
-
function machineIdentity() {
|
|
3675
|
-
const host = (os.hostname() || "machine").trim().replace(/\.local$/i, "") || "machine";
|
|
3676
|
-
return {
|
|
3677
|
-
machineName: `${host}-cli`,
|
|
3678
|
-
friendlyName: `${host} (CLI)`
|
|
3679
|
-
};
|
|
3680
|
-
}
|
|
3681
|
-
const INHERITED_ENV = [
|
|
3682
|
-
"HOME",
|
|
3683
|
-
"USER",
|
|
3684
|
-
"LOGNAME",
|
|
3685
|
-
"SHELL",
|
|
3686
|
-
"LANG",
|
|
3687
|
-
"LC_ALL",
|
|
3688
|
-
"TMPDIR",
|
|
3689
|
-
"TERM",
|
|
3690
|
-
"PATH"
|
|
3691
|
-
];
|
|
3692
|
-
function buildEnv(extra) {
|
|
3693
|
-
const env = {};
|
|
3694
|
-
for (const key of INHERITED_ENV) {
|
|
3695
|
-
const value = process.env[key];
|
|
3696
|
-
if (value !== void 0) env[key] = value;
|
|
3697
|
-
}
|
|
3698
|
-
if (extra) for (const [key, value] of Object.entries(extra)) env[key] = value;
|
|
3699
|
-
return env;
|
|
3700
|
-
}
|
|
3701
|
-
/**
|
|
3702
|
-
* Build the desktop-portal WebSocket URL from the chat origin. Mirrors the Rust
|
|
3703
|
-
* desktop client: http→ws, https→wss, scheme-less defaults to wss, and the
|
|
3704
|
-
* machine/label ride as query pairs (percent-encoded by URL).
|
|
3705
|
-
*/
|
|
3706
|
-
function portalWsUrl(appUrl, machine, label) {
|
|
3707
|
-
const base = appUrl.replace(/\/+$/, "");
|
|
3708
|
-
let wsBase;
|
|
3709
|
-
if (base.startsWith("https://")) wsBase = `wss://${base.slice(8)}`;
|
|
3710
|
-
else if (base.startsWith("http://")) wsBase = `ws://${base.slice(7)}`;
|
|
3711
|
-
else wsBase = `wss://${base}`;
|
|
3712
|
-
const url = new URL(`${wsBase}/api/v1/portal/desktop`);
|
|
3713
|
-
url.searchParams.set("machine", machine);
|
|
3714
|
-
url.searchParams.set("label", label);
|
|
3715
|
-
return url.toString();
|
|
3716
|
-
}
|
|
3717
|
-
|
|
3718
|
-
//#endregion
|
|
3719
|
-
//#region src/chat/portal/api.ts
|
|
3720
|
-
/**
|
|
3721
|
-
* The portal's session-authed REST surface, shared by `PortalClient` (the
|
|
3722
|
-
* TUI/`portal open` connection) and the `skydive portal` management
|
|
3723
|
-
* commands, so the endpoint contracts and response schemas live in exactly
|
|
3724
|
-
* one place.
|
|
3725
|
-
*/
|
|
3726
|
-
const deviceSchema = z.object({
|
|
3727
|
-
id: z.string(),
|
|
3728
|
-
machineName: z.string(),
|
|
3729
|
-
friendlyName: z.string(),
|
|
3730
|
-
connected: z.boolean(),
|
|
3731
|
-
lastSeen: z.string().nullable(),
|
|
3732
|
-
grantedAgentIds: z.array(z.string())
|
|
3733
|
-
});
|
|
3734
|
-
const devicesResponseSchema = z.object({
|
|
3735
|
-
devices: z.array(deviceSchema),
|
|
3736
|
-
agents: z.array(z.object({
|
|
3737
|
-
id: z.string(),
|
|
3738
|
-
name: z.string()
|
|
3739
|
-
}))
|
|
3740
|
-
});
|
|
3741
|
-
const deviceTokenSchema = z.object({ token: z.string().min(1) });
|
|
3742
|
-
async function portalFetch(auth, path, init) {
|
|
3743
|
-
const res = await fetch(`${auth.appUrl}${path}`, {
|
|
3744
|
-
method: init.method,
|
|
3745
|
-
headers: {
|
|
3746
|
-
authorization: `Bearer ${auth.sessionToken}`,
|
|
3747
|
-
accept: "application/json",
|
|
3748
|
-
...init.body ? { "content-type": "application/json" } : {}
|
|
3749
|
-
},
|
|
3750
|
-
...init.body ? { body: init.body } : {}
|
|
3751
|
-
});
|
|
3752
|
-
if (!res.ok) {
|
|
3753
|
-
const body = await res.text().catch(() => "");
|
|
3754
|
-
throw new HttpError(res.status, body);
|
|
3755
|
-
}
|
|
3756
|
-
return res.json();
|
|
3757
|
-
}
|
|
3758
|
-
async function fetchPortalDevices(auth) {
|
|
3759
|
-
const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
|
|
3760
|
-
return devicesResponseSchema.parse(json);
|
|
3761
|
-
}
|
|
3762
|
-
const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
|
|
3763
|
-
/**
|
|
3764
|
-
* Register this machine's device row without connecting. Connecting registers
|
|
3765
|
-
* as a side effect; this covers granting an agent on a machine that has never
|
|
3766
|
-
* shared yet (the grant references the device row).
|
|
3767
|
-
*/
|
|
3768
|
-
async function registerPortalDevice(auth, { machineName, friendlyName }) {
|
|
3769
|
-
const json = await portalFetch(auth, "/api/v1/portal/devices", {
|
|
3770
|
-
method: "POST",
|
|
3771
|
-
body: JSON.stringify({
|
|
3772
|
-
machineName,
|
|
3773
|
-
friendlyName
|
|
3774
|
-
})
|
|
3775
|
-
});
|
|
3776
|
-
return registerResponseSchema.parse(json).device;
|
|
3777
|
-
}
|
|
3778
|
-
/** Short-lived token the machine presents when dialing the portal WebSocket. */
|
|
3779
|
-
async function mintPortalDeviceToken(auth) {
|
|
3780
|
-
const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
|
|
3781
|
-
return deviceTokenSchema.parse(json).token;
|
|
3782
|
-
}
|
|
3783
|
-
async function grantPortalAccess(auth, { deviceId, agentId }) {
|
|
3784
|
-
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
|
|
3785
|
-
method: "POST",
|
|
3786
|
-
body: JSON.stringify({ agentId })
|
|
3787
|
-
});
|
|
3788
|
-
}
|
|
3789
|
-
async function revokePortalAccess(auth, { deviceId, agentId }) {
|
|
3790
|
-
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
|
|
3791
|
-
}
|
|
3792
|
-
/**
|
|
3793
|
-
* The device row for a given machine identity. Matching is by `machineName`
|
|
3794
|
-
* equality — the stable per-surface handle (`<host>-cli` vs the desktop's
|
|
3795
|
-
* `<host>`), not the display label.
|
|
3796
|
-
*/
|
|
3797
|
-
function findThisDevice(devices, machineName) {
|
|
3798
|
-
return devices.find((device) => device.machineName === machineName) ?? null;
|
|
3799
|
-
}
|
|
3800
|
-
|
|
3801
1768
|
//#endregion
|
|
3802
1769
|
//#region src/commands/portal.ts
|
|
3803
1770
|
/**
|
|
@@ -3839,7 +1806,7 @@ const openCommand = {
|
|
|
3839
1806
|
const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
3840
1807
|
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
3841
1808
|
const { machineName } = machineIdentity();
|
|
3842
|
-
const { PortalClient } = await import("./client-
|
|
1809
|
+
const { PortalClient } = await import("./client-C-s6b9Yu.mjs");
|
|
3843
1810
|
let lastLine = "";
|
|
3844
1811
|
let signalConnected;
|
|
3845
1812
|
const connected = new Promise((resolve) => {
|
|
@@ -3970,171 +1937,6 @@ const portalCommand = {
|
|
|
3970
1937
|
handler: () => {}
|
|
3971
1938
|
};
|
|
3972
1939
|
|
|
3973
|
-
//#endregion
|
|
3974
|
-
//#region ../sandbox-stream-protocol/src/index.ts
|
|
3975
|
-
const SANDBOX_STREAM_PATH = "/api/v1/sandbox/stream";
|
|
3976
|
-
const FRAME = {
|
|
3977
|
-
DATA: 1,
|
|
3978
|
-
EXIT: 2,
|
|
3979
|
-
ERROR: 3,
|
|
3980
|
-
INPUT: 16,
|
|
3981
|
-
RESIZE: 17
|
|
3982
|
-
};
|
|
3983
|
-
const MAX_INPUT_BYTES = 1 * 1024 * 1024;
|
|
3984
|
-
/** Query params for the upgrade URL, from a spec. Inverse of {@link parseStreamSpec}. */
|
|
3985
|
-
function streamSpecToQuery(spec) {
|
|
3986
|
-
if (spec.mode === "pty") return {
|
|
3987
|
-
agentId: spec.agentId,
|
|
3988
|
-
mode: "pty",
|
|
3989
|
-
cols: String(spec.cols),
|
|
3990
|
-
rows: String(spec.rows)
|
|
3991
|
-
};
|
|
3992
|
-
return {
|
|
3993
|
-
agentId: spec.agentId,
|
|
3994
|
-
mode: "exec",
|
|
3995
|
-
command: spec.command
|
|
3996
|
-
};
|
|
3997
|
-
}
|
|
3998
|
-
function withType(type, payload) {
|
|
3999
|
-
const frame = new Uint8Array(1 + payload.length);
|
|
4000
|
-
frame[0] = type;
|
|
4001
|
-
frame.set(payload, 1);
|
|
4002
|
-
return frame;
|
|
4003
|
-
}
|
|
4004
|
-
/** client → server: keystroke bytes for the pty stdin. */
|
|
4005
|
-
function encodeInput(data) {
|
|
4006
|
-
return withType(FRAME.INPUT, data);
|
|
4007
|
-
}
|
|
4008
|
-
/** client → server: the client terminal was resized. */
|
|
4009
|
-
function encodeResize(cols, rows) {
|
|
4010
|
-
const frame = new Uint8Array(5);
|
|
4011
|
-
frame[0] = FRAME.RESIZE;
|
|
4012
|
-
const view = new DataView(frame.buffer);
|
|
4013
|
-
view.setUint16(1, cols & 65535);
|
|
4014
|
-
view.setUint16(3, rows & 65535);
|
|
4015
|
-
return frame;
|
|
4016
|
-
}
|
|
4017
|
-
const view = (frame) => new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
4018
|
-
/**
|
|
4019
|
-
* Decode a frame the server sent. Returns null for an empty, unknown, or
|
|
4020
|
-
* truncated frame — a peer speaking a newer protocol must not crash us.
|
|
4021
|
-
*/
|
|
4022
|
-
function decodeServerFrame(frame) {
|
|
4023
|
-
const payload = frame.subarray(1);
|
|
4024
|
-
switch (frame[0]) {
|
|
4025
|
-
case FRAME.DATA: return {
|
|
4026
|
-
type: "data",
|
|
4027
|
-
payload
|
|
4028
|
-
};
|
|
4029
|
-
case FRAME.EXIT: return {
|
|
4030
|
-
type: "exit",
|
|
4031
|
-
code: payload.length >= 4 ? view(frame).getInt32(1) : 0
|
|
4032
|
-
};
|
|
4033
|
-
case FRAME.ERROR: return {
|
|
4034
|
-
type: "error",
|
|
4035
|
-
message: new TextDecoder().decode(payload)
|
|
4036
|
-
};
|
|
4037
|
-
default: return null;
|
|
4038
|
-
}
|
|
4039
|
-
}
|
|
4040
|
-
|
|
4041
|
-
//#endregion
|
|
4042
|
-
//#region src/chat/sandbox/client.ts
|
|
4043
|
-
function wsBase(appUrl) {
|
|
4044
|
-
const base = appUrl.replace(/\/+$/, "");
|
|
4045
|
-
if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
|
|
4046
|
-
if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
|
|
4047
|
-
return `wss://${base}`;
|
|
4048
|
-
}
|
|
4049
|
-
/**
|
|
4050
|
-
* A connected sandbox-stream session. Construct via `openSandboxStream`. Carries
|
|
4051
|
-
* the write side (keystrokes / resize for pty mode) and teardown.
|
|
4052
|
-
*/
|
|
4053
|
-
var SandboxStream = class SandboxStream {
|
|
4054
|
-
ws;
|
|
4055
|
-
closed = false;
|
|
4056
|
-
constructor(ws, onEvent) {
|
|
4057
|
-
this.ws = ws;
|
|
4058
|
-
let ended = false;
|
|
4059
|
-
const emitEnd = (event) => {
|
|
4060
|
-
if (ended) return;
|
|
4061
|
-
ended = true;
|
|
4062
|
-
onEvent(event);
|
|
4063
|
-
};
|
|
4064
|
-
ws.on("message", (data, isBinary) => {
|
|
4065
|
-
if (!isBinary) return;
|
|
4066
|
-
const frame = decodeServerFrame(toBuffer(data));
|
|
4067
|
-
if (!frame) return;
|
|
4068
|
-
switch (frame.type) {
|
|
4069
|
-
case "data":
|
|
4070
|
-
onEvent({
|
|
4071
|
-
type: "data",
|
|
4072
|
-
bytes: new Uint8Array(frame.payload)
|
|
4073
|
-
});
|
|
4074
|
-
break;
|
|
4075
|
-
case "exit":
|
|
4076
|
-
emitEnd({
|
|
4077
|
-
type: "exit",
|
|
4078
|
-
code: frame.code
|
|
4079
|
-
});
|
|
4080
|
-
break;
|
|
4081
|
-
case "error":
|
|
4082
|
-
emitEnd({
|
|
4083
|
-
type: "error",
|
|
4084
|
-
message: frame.message
|
|
4085
|
-
});
|
|
4086
|
-
break;
|
|
4087
|
-
}
|
|
4088
|
-
});
|
|
4089
|
-
let failure = null;
|
|
4090
|
-
ws.on("error", (err) => {
|
|
4091
|
-
failure = err.message;
|
|
4092
|
-
});
|
|
4093
|
-
ws.on("close", () => {
|
|
4094
|
-
this.closed = true;
|
|
4095
|
-
emitEnd({
|
|
4096
|
-
type: "close",
|
|
4097
|
-
failure
|
|
4098
|
-
});
|
|
4099
|
-
});
|
|
4100
|
-
}
|
|
4101
|
-
/** Feed keystroke bytes to the pty stdin. */
|
|
4102
|
-
sendInput(data) {
|
|
4103
|
-
if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
|
|
4104
|
-
this.ws.send(encodeInput(data));
|
|
4105
|
-
}
|
|
4106
|
-
/** Notify the pty of a terminal resize. */
|
|
4107
|
-
resize(cols, rows) {
|
|
4108
|
-
if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
|
|
4109
|
-
this.ws.send(encodeResize(cols, rows));
|
|
4110
|
-
}
|
|
4111
|
-
close() {
|
|
4112
|
-
this.closed = true;
|
|
4113
|
-
this.ws.close();
|
|
4114
|
-
}
|
|
4115
|
-
/** Open a stream. `mode` is 'pty' (interactive) or 'exec' (one-shot). */
|
|
4116
|
-
static open(opts) {
|
|
4117
|
-
const spec = opts.mode === "pty" ? {
|
|
4118
|
-
mode: "pty",
|
|
4119
|
-
agentId: opts.agentId,
|
|
4120
|
-
cols: opts.cols,
|
|
4121
|
-
rows: opts.rows
|
|
4122
|
-
} : {
|
|
4123
|
-
mode: "exec",
|
|
4124
|
-
agentId: opts.agentId,
|
|
4125
|
-
command: opts.command
|
|
4126
|
-
};
|
|
4127
|
-
const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
|
|
4128
|
-
for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
|
|
4129
|
-
return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
|
|
4130
|
-
}
|
|
4131
|
-
};
|
|
4132
|
-
function toBuffer(data) {
|
|
4133
|
-
if (Buffer.isBuffer(data)) return data;
|
|
4134
|
-
if (Array.isArray(data)) return Buffer.concat(data);
|
|
4135
|
-
return Buffer.from(data);
|
|
4136
|
-
}
|
|
4137
|
-
|
|
4138
1940
|
//#endregion
|
|
4139
1941
|
//#region src/commands/sandbox.ts
|
|
4140
1942
|
/** POSIX single-quote one word so the remote shell treats it as one token. */
|
|
@@ -4176,8 +1978,8 @@ const sandboxCommand = {
|
|
|
4176
1978
|
}).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`"),
|
|
4177
1979
|
handler: async (argv) => {
|
|
4178
1980
|
const session = requireSession(argv);
|
|
4179
|
-
const { createRestClient } = await
|
|
4180
|
-
const { resolveAgent } = await
|
|
1981
|
+
const { createRestClient } = await import("./rest-DQruM5kj.mjs");
|
|
1982
|
+
const { resolveAgent } = await import("./print-DFPzQQk8.mjs");
|
|
4181
1983
|
const client = createRestClient({
|
|
4182
1984
|
appUrl: session.appUrl,
|
|
4183
1985
|
sessionToken: session.sessionToken
|
|
@@ -4246,7 +2048,7 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
4246
2048
|
return 1;
|
|
4247
2049
|
}
|
|
4248
2050
|
console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
|
|
4249
|
-
const { runRawPtyPassthrough } = await import("./raw-pty-
|
|
2051
|
+
const { runRawPtyPassthrough } = await import("./raw-pty-pspO57gT.mjs");
|
|
4250
2052
|
const result = await runRawPtyPassthrough({
|
|
4251
2053
|
stdin: process.stdin,
|
|
4252
2054
|
stdout: process.stdout,
|
|
@@ -4275,7 +2077,7 @@ function createCli(argv) {
|
|
|
4275
2077
|
type: "string",
|
|
4276
2078
|
global: true,
|
|
4277
2079
|
describe: "Override API base URL"
|
|
4278
|
-
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version
|
|
2080
|
+
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).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) => {
|
|
4279
2081
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
4280
2082
|
process.exit(1);
|
|
4281
2083
|
});
|
|
@@ -4344,9 +2146,250 @@ function resolveArgv(args, tty = {
|
|
|
4344
2146
|
return shouldDefaultToChat(args, tty, nonInteractive) ? ["chat", ...args] : args;
|
|
4345
2147
|
}
|
|
4346
2148
|
|
|
2149
|
+
//#endregion
|
|
2150
|
+
//#region src/update-check/cache.ts
|
|
2151
|
+
/**
|
|
2152
|
+
* The on-disk state behind the update check: a single small JSON file the
|
|
2153
|
+
* fast path reads synchronously on every command and the background worker
|
|
2154
|
+
* rewrites after a successful fetch. Every read tolerates a missing, torn,
|
|
2155
|
+
* or type-mangled file (each field validates independently), and a failed
|
|
2156
|
+
* write is silently skipped — the cache must never break a command.
|
|
2157
|
+
*/
|
|
2158
|
+
const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
2159
|
+
const cacheSchema = z.object({
|
|
2160
|
+
lastCheckedAt: z.string().optional().catch(void 0),
|
|
2161
|
+
channel: z.string().optional().catch(void 0),
|
|
2162
|
+
latestVersion: z.string().optional().catch(void 0)
|
|
2163
|
+
});
|
|
2164
|
+
/** Beside config.json so all CLI state shares one directory (and the
|
|
2165
|
+
* `SKYDIVE_CONFIG_NAME` profile isolation). */
|
|
2166
|
+
function getUpdateCachePath() {
|
|
2167
|
+
return path.join(path.dirname(getConfigPath()), "update-check.json");
|
|
2168
|
+
}
|
|
2169
|
+
function readUpdateCache() {
|
|
2170
|
+
try {
|
|
2171
|
+
const raw = fs.readFileSync(getUpdateCachePath(), "utf8");
|
|
2172
|
+
return cacheSchema.parse(JSON.parse(raw));
|
|
2173
|
+
} catch (_error) {
|
|
2174
|
+
return {};
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
function writeUpdateCache(cache) {
|
|
2178
|
+
try {
|
|
2179
|
+
const file = getUpdateCachePath();
|
|
2180
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
2181
|
+
fs.writeFileSync(file, `${JSON.stringify(cache, null, 2)}\n`, { mode: 384 });
|
|
2182
|
+
} catch (_error) {}
|
|
2183
|
+
}
|
|
2184
|
+
function isCheckDue(lastCheckedAt, now = Date.now()) {
|
|
2185
|
+
if (!lastCheckedAt) return true;
|
|
2186
|
+
const then = Date.parse(lastCheckedAt);
|
|
2187
|
+
if (Number.isNaN(then)) return true;
|
|
2188
|
+
if (then > now) return true;
|
|
2189
|
+
return now - then >= CHECK_INTERVAL_MS;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
//#endregion
|
|
2193
|
+
//#region src/update-check/versions.ts
|
|
2194
|
+
/** Canary builds carry a prerelease suffix (`X.Y.Z-beta.N`, synthesized in
|
|
2195
|
+
* CI); a plain semver is a stable release. An unparseable version defaults
|
|
2196
|
+
* to stable. */
|
|
2197
|
+
function resolveChannel(version) {
|
|
2198
|
+
return (semver.prerelease(version, { loose: true })?.length ?? 0) > 0 ? "canary" : "stable";
|
|
2199
|
+
}
|
|
2200
|
+
/** npm dist-tag for a channel (see release-skydive-cli.yml). */
|
|
2201
|
+
function distTagForChannel(channel) {
|
|
2202
|
+
return channel === "canary" ? "beta" : "latest";
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* True when `candidate` is a strictly newer release than `current`, per
|
|
2206
|
+
* semver precedence (the `semver` package, including prerelease ordering).
|
|
2207
|
+
* Unparseable input is never newer, so garbage from the registry can't
|
|
2208
|
+
* produce a notice.
|
|
2209
|
+
*/
|
|
2210
|
+
function isNewerVersion(candidate, current) {
|
|
2211
|
+
if (!semver.valid(candidate, { loose: true })) return false;
|
|
2212
|
+
if (!semver.valid(current, { loose: true })) return false;
|
|
2213
|
+
return semver.gt(candidate, current, { loose: true });
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
//#endregion
|
|
2217
|
+
//#region src/update-check/notice.ts
|
|
2218
|
+
/** The install-mode-specific action line. A package-manager install is never
|
|
2219
|
+
* self-mutated — we only tell the user what to run. */
|
|
2220
|
+
function renderUpdateCommand(channel, source) {
|
|
2221
|
+
if (source === "binary") return `curl -fsSL ${DEFAULT_WEB_URL}/api/v1/cli/install.sh | ${channel === "canary" ? "SKYDIVE_CHANNEL=canary " : ""}sh`;
|
|
2222
|
+
return `npm install -g ${name}@${distTagForChannel(channel)}`;
|
|
2223
|
+
}
|
|
2224
|
+
function renderUpdateNotice(opts) {
|
|
2225
|
+
return `\nUpdate available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun ${renderUpdateCommand(opts.channel, opts.source)}\n`;
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Whether this invocation may print the notice. Pure so it's testable; the
|
|
2229
|
+
* inputs are raw pre-yargs argv (parsing hasn't happened when this runs) and
|
|
2230
|
+
* stderr's TTY-ness. `--json`/`--quiet` go to stdout, and the notice goes to
|
|
2231
|
+
* stderr — but scripts commonly capture 2>&1, so machine-readable modes
|
|
2232
|
+
* suppress it entirely rather than risk corrupting piped output.
|
|
2233
|
+
*/
|
|
2234
|
+
function shouldNotify(opts) {
|
|
2235
|
+
if (!opts.stderrIsTTY) return false;
|
|
2236
|
+
if (opts.argv.includes("--json") || opts.argv.includes("--quiet")) return false;
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
//#endregion
|
|
2241
|
+
//#region src/update-check/sources.ts
|
|
2242
|
+
function resolveInstallSource() {
|
|
2243
|
+
return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
|
|
2244
|
+
}
|
|
2245
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2246
|
+
const npmDistTagsSchema = z.record(z.string(), z.unknown());
|
|
2247
|
+
const binaryManifestSchema = z.object({ version: z.unknown().optional() });
|
|
2248
|
+
/** npm dist-tags for the published package (`skydive-cli`). */
|
|
2249
|
+
const npmReleaseSource = { async fetchLatestVersion(channel, { signal }) {
|
|
2250
|
+
const response = await fetch(`${NPM_REGISTRY_URL}/-/package/${name}/dist-tags`, {
|
|
2251
|
+
signal,
|
|
2252
|
+
headers: { accept: "application/json" }
|
|
2253
|
+
});
|
|
2254
|
+
if (!response.ok) return null;
|
|
2255
|
+
const tags = npmDistTagsSchema.safeParse(await response.json());
|
|
2256
|
+
if (!tags.success) return null;
|
|
2257
|
+
const version = tags.data[distTagForChannel(channel)];
|
|
2258
|
+
return typeof version === "string" ? version : null;
|
|
2259
|
+
} };
|
|
2260
|
+
/**
|
|
2261
|
+
* Release CDN (CloudFront over the releases bucket, infra: CliReleasesCdn).
|
|
2262
|
+
* Serves the channel pointers the release workflow uploads
|
|
2263
|
+
* (`channels/{stable,canary}.json`, cached max-age=60) alongside the
|
|
2264
|
+
* binaries. The daily poll goes here, not to the api, so a fleet of
|
|
2265
|
+
* installed binaries puts no load on — and takes no dependency on — the api.
|
|
2266
|
+
*/
|
|
2267
|
+
const RELEASE_CDN_URL = "https://dl.skydive.com";
|
|
2268
|
+
/** Channel manifest on the release CDN, for compiled binaries. The same
|
|
2269
|
+
* document the api's channel route serves (see
|
|
2270
|
+
* apps/anyone/api/src/routes/cli-releases.ts, which reads it from the
|
|
2271
|
+
* bucket this CDN fronts). */
|
|
2272
|
+
const binaryReleaseSource = { async fetchLatestVersion(channel, { signal }) {
|
|
2273
|
+
const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
|
|
2274
|
+
signal,
|
|
2275
|
+
headers: { accept: "application/json" }
|
|
2276
|
+
});
|
|
2277
|
+
if (!response.ok) return null;
|
|
2278
|
+
const manifest = binaryManifestSchema.safeParse(await response.json());
|
|
2279
|
+
if (!manifest.success) return null;
|
|
2280
|
+
return typeof manifest.data.version === "string" ? manifest.data.version : null;
|
|
2281
|
+
} };
|
|
2282
|
+
function releaseSourceForInstall(source) {
|
|
2283
|
+
return source === "binary" ? binaryReleaseSource : npmReleaseSource;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
//#endregion
|
|
2287
|
+
//#region src/update-check/index.ts
|
|
2288
|
+
/**
|
|
2289
|
+
* Non-blocking update check, update-notifier style: a command never waits on
|
|
2290
|
+
* the network. Each invocation reads a small on-disk cache (./cache.ts) and,
|
|
2291
|
+
* when it records a newer version for this build's channel, prints a notice
|
|
2292
|
+
* (./notice.ts) to stderr at process exit. Separately, at most once per
|
|
2293
|
+
* check interval, it spawns a short-lived detached child (this same
|
|
2294
|
+
* executable with {@link UPDATE_WORKER_FLAG}) that fetches release metadata
|
|
2295
|
+
* (./sources.ts) and rewrites the cache for *future* invocations. So a
|
|
2296
|
+
* notice is always one check behind — the price of never delaying a command.
|
|
2297
|
+
*
|
|
2298
|
+
* Two install modes share everything except the metadata source and the
|
|
2299
|
+
* suggested action (see ./sources.ts):
|
|
2300
|
+
*
|
|
2301
|
+
* - package-manager (npm/Yarn/pnpm/Bun): compare against npm dist-tags,
|
|
2302
|
+
* print the install command, never self-mutate the install.
|
|
2303
|
+
* - compiled binary: compare against the release channel manifest served by
|
|
2304
|
+
* the api, print the installer one-liner; a real `skydive update`
|
|
2305
|
+
* self-updater can slot in behind the same boundary later.
|
|
2306
|
+
*
|
|
2307
|
+
* Failure policy: every path here is best-effort. Network errors, timeouts,
|
|
2308
|
+
* a torn cache file, an unwritable config dir — all silent. The check must
|
|
2309
|
+
* never break or slow a command.
|
|
2310
|
+
*/
|
|
2311
|
+
/** Hidden argv sentinel that turns an invocation into the background refresh
|
|
2312
|
+
* worker (see bin.ts). Namespaced so it can never collide with a real flag. */
|
|
2313
|
+
const UPDATE_WORKER_FLAG = "--skydive-internal-update-check";
|
|
2314
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
2315
|
+
function isCheckDisabled(env) {
|
|
2316
|
+
return Boolean(env["SKYDIVE_NO_UPDATE_CHECK"] || env["NO_UPDATE_NOTIFIER"] || env["CI"]) || getUpdateCheckDisabled();
|
|
2317
|
+
}
|
|
2318
|
+
/** A cached answer counts only if it's for this build's channel and strictly
|
|
2319
|
+
* newer than what's running. */
|
|
2320
|
+
function updateAvailable(cache, currentVersion, channel) {
|
|
2321
|
+
return Boolean(cache.latestVersion && cache.channel === channel && isNewerVersion(cache.latestVersion, currentVersion));
|
|
2322
|
+
}
|
|
2323
|
+
function registerExitNotice(notice) {
|
|
2324
|
+
process.once("exit", () => {
|
|
2325
|
+
process.stderr.write(notice);
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
/** Stamp the claim before the worker spawns, so a crashing/offline worker
|
|
2329
|
+
* retries next interval instead of respawning on every command. */
|
|
2330
|
+
function claimCheckInterval(cache) {
|
|
2331
|
+
writeUpdateCache({
|
|
2332
|
+
...cache,
|
|
2333
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
function spawnUpdateCheckWorker() {
|
|
2337
|
+
const args = resolveInstallSource() === "binary" ? [UPDATE_WORKER_FLAG] : [...process.argv[1] ? [process.argv[1]] : [], UPDATE_WORKER_FLAG];
|
|
2338
|
+
spawn(process.execPath, args, {
|
|
2339
|
+
detached: true,
|
|
2340
|
+
stdio: "ignore"
|
|
2341
|
+
}).unref();
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* The detached child's whole job: fetch the channel's current version and
|
|
2345
|
+
* rewrite the cache. Timeout-bounded and silent on failure by design.
|
|
2346
|
+
*/
|
|
2347
|
+
async function runUpdateCheckWorker() {
|
|
2348
|
+
try {
|
|
2349
|
+
const channel = resolveChannel(version);
|
|
2350
|
+
const latestVersion = await releaseSourceForInstall(resolveInstallSource()).fetchLatestVersion(channel, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
2351
|
+
if (!latestVersion) return;
|
|
2352
|
+
writeUpdateCache({
|
|
2353
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2354
|
+
channel,
|
|
2355
|
+
latestVersion
|
|
2356
|
+
});
|
|
2357
|
+
} catch (_error) {}
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Called once from bin.ts before command dispatch. Synchronous — one small
|
|
2361
|
+
* file read; the network work happens in the detached worker.
|
|
2362
|
+
*/
|
|
2363
|
+
function setupUpdateCheck(argv) {
|
|
2364
|
+
try {
|
|
2365
|
+
if (isCheckDisabled(process.env)) return;
|
|
2366
|
+
const currentVersion = version;
|
|
2367
|
+
const channel = resolveChannel(currentVersion);
|
|
2368
|
+
const cache = readUpdateCache();
|
|
2369
|
+
if (updateAvailable(cache, currentVersion, channel) && shouldNotify({
|
|
2370
|
+
argv,
|
|
2371
|
+
stderrIsTTY: Boolean(process.stderr.isTTY)
|
|
2372
|
+
})) registerExitNotice(renderUpdateNotice({
|
|
2373
|
+
currentVersion,
|
|
2374
|
+
latestVersion: cache.latestVersion,
|
|
2375
|
+
channel,
|
|
2376
|
+
source: resolveInstallSource()
|
|
2377
|
+
}));
|
|
2378
|
+
if (isCheckDue(cache.lastCheckedAt)) {
|
|
2379
|
+
claimCheckInterval(cache);
|
|
2380
|
+
spawnUpdateCheckWorker();
|
|
2381
|
+
}
|
|
2382
|
+
} catch (_error) {}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
4347
2385
|
//#endregion
|
|
4348
2386
|
//#region src/bin.ts
|
|
2387
|
+
if (process.argv.includes(UPDATE_WORKER_FLAG)) {
|
|
2388
|
+
await runUpdateCheckWorker();
|
|
2389
|
+
process.exit(0);
|
|
2390
|
+
}
|
|
2391
|
+
setupUpdateCheck(hideBin(process.argv));
|
|
4349
2392
|
createCli(resolveArgv(hideBin(process.argv))).parse();
|
|
4350
2393
|
|
|
4351
2394
|
//#endregion
|
|
4352
|
-
export {
|
|
2395
|
+
export { };
|