skydive-cli 0.4.0-beta.1033 → 0.4.0-beta.1068

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/js/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as setActiveWorkspace, S as listWorkspaces, T as version, _ as themes, b as getActiveWorkspaceId, o as brandHelpArt, t as installCrashHandler, w as name, x as getSessionIdentity, y as ensureActiveOrganization } from "./install-DCcGbpsS.mjs";
2
+ import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-UzSfz08d.mjs";
3
3
  import { A as getStoredApiKeyWorkspaceName, C as getLastSeenVersion, F as resolveManagementAuth, I as resolveSession, M as resolveAppUrl, N as resolveChatAuth, O as getShareMachineDefault, P as resolveConfig, R as saveConfig, S as getConfigPath, T as getPromptHistoryPath, V as setLastSeenVersion, _ as API_KEY_PREFIX, b as PREFERENCES, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as getUpdateCheckDisabled, k as getStoredApiKeyId, v as DEFAULT_API_URL, w as getPreference, x as deleteConfig, z as saveSession } from "./print-CiaU2ygh.mjs";
4
4
  import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
5
  import { n as createRestClient, t as HttpError } from "./rest-CG7VpQ56.mjs";
@@ -1505,7 +1505,7 @@ const importCommand = {
1505
1505
  process.exit(1);
1506
1506
  }
1507
1507
  }
1508
- const { runChat } = await import("./boot-C6THTSAl.mjs");
1508
+ const { runChat } = await import("./boot-BLwtCROn.mjs");
1509
1509
  await runChat({
1510
1510
  appUrl,
1511
1511
  sessionToken: session.value.sessionToken,
@@ -1917,7 +1917,7 @@ const chatCommand = {
1917
1917
  sessionToken: auth.value.token,
1918
1918
  agentSelector: argv.agent ?? null
1919
1919
  });
1920
- const { runChat } = await import("./boot-C6THTSAl.mjs");
1920
+ const { runChat } = await import("./boot-BLwtCROn.mjs");
1921
1921
  await runChat({
1922
1922
  appUrl: auth.value.appUrl,
1923
1923
  sessionToken: auth.value.token,
@@ -2276,7 +2276,7 @@ const switchCommand = {
2276
2276
  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.");
2277
2277
  process.exit(1);
2278
2278
  }
2279
- const { runWorkspacePicker } = await import("./boot-C6THTSAl.mjs");
2279
+ const { runWorkspacePicker } = await import("./boot-BLwtCROn.mjs");
2280
2280
  await runWorkspacePicker(session);
2281
2281
  return;
2282
2282
  }
@@ -3881,6 +3881,7 @@ function maybePrintWhatsNew({ stderrIsTTY = process.stderr.isTTY ?? false, nonIn
3881
3881
  //#endregion
3882
3882
  //#region src/bin.ts
3883
3883
  installCrashHandler();
3884
+ maybeStartProfiling(hideBin(process.argv), version);
3884
3885
  if (process.argv.includes(UPDATE_WORKER_FLAG)) {
3885
3886
  await runUpdateCheckWorker();
3886
3887
  process.exit(0);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as setActiveWorkspace, S as listWorkspaces, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-DCcGbpsS.mjs";
2
+ import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-UzSfz08d.mjs";
3
3
  import { B as saveTheme, D as getSavedTheme, E as getReviewStateDir, L as resolveWebUrl, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-CiaU2ygh.mjs";
4
4
  import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, r as errorDetail, t as HttpError } from "./rest-CG7VpQ56.mjs";
5
5
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-CwfG1BTR.mjs";
@@ -19,7 +19,7 @@ import { connect, createConnection } from "node:net";
19
19
  import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
20
20
  import { MarkdownRenderable, RenderableEvents, StyledText, SyntaxStyle, addDefaultParsers, createCliRenderer, decodePasteBytes, detectLinks, fg, link } from "@opentui/core";
21
21
  import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
22
- import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
22
+ import { Profiler, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
23
23
  import { create } from "zustand";
24
24
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
25
25
  import fuzzysort from "fuzzysort";
@@ -12317,6 +12317,97 @@ async function settleBootThemeMode({ source, applyThemeMode, graceMs = THEME_QUE
12317
12317
  if (answered) applyThemeMode(answered);
12318
12318
  }
12319
12319
 
12320
+ //#endregion
12321
+ //#region src/profiling/tui.ts
12322
+ /**
12323
+ * TUI-side profiling instrumentation. Everything here rides on the shared
12324
+ * profile directory created in bin.ts (see profiler.ts) and is inert unless
12325
+ * SKYDIVE_PROFILE is set.
12326
+ */
12327
+ /**
12328
+ * Renderer options to merge into createCliRenderer when profiling:
12329
+ * gatherStats makes OpenTUI retain per-frame timings (frame time, cells
12330
+ * updated, native render + stdout write time) that getStats() exposes.
12331
+ */
12332
+ function profilingRendererOptions() {
12333
+ return profilingEnabled() ? { gatherStats: true } : {};
12334
+ }
12335
+ /**
12336
+ * Samples renderer stats periodically and dumps the full series at exit.
12337
+ * The periodic samples give the timeline shape (fps over the session); the
12338
+ * final dump carries the complete frame-time distribution.
12339
+ */
12340
+ function startRendererStatsCapture(renderer) {
12341
+ if (!profilingEnabled()) return;
12342
+ const sample = () => {
12343
+ const s = renderer.getStats();
12344
+ record("render", {
12345
+ fps: s.fps,
12346
+ frameCount: s.frameCount,
12347
+ averageFrameTime: s.averageFrameTime,
12348
+ maxFrameTime: s.maxFrameTime,
12349
+ cellsUpdated: s.cellsUpdated,
12350
+ nativeRenderTime: s.nativeRenderTime ?? null,
12351
+ nativeStdoutWriteTime: s.nativeStdoutWriteTime ?? null
12352
+ });
12353
+ };
12354
+ const interval = setInterval(sample, 1e3);
12355
+ interval.unref?.();
12356
+ process.on("exit", () => {
12357
+ clearInterval(interval);
12358
+ const s = renderer.getStats();
12359
+ writeArtifact("render.json", {
12360
+ fps: s.fps,
12361
+ frameCount: s.frameCount,
12362
+ frameTimes: s.frameTimes,
12363
+ averageFrameTime: s.averageFrameTime,
12364
+ minFrameTime: s.minFrameTime,
12365
+ maxFrameTime: s.maxFrameTime,
12366
+ cellsUpdated: s.cellsUpdated,
12367
+ averageCellsUpdated: s.averageCellsUpdated
12368
+ });
12369
+ });
12370
+ }
12371
+ /**
12372
+ * Records zustand store transitions. Subscribes to the whole store and
12373
+ * logs a compact diff-flavored event per change: the screen kind always,
12374
+ * plus which top-level keys changed. Gives an agent the "what was the UI
12375
+ * doing at t=X" axis without serializing message payloads.
12376
+ */
12377
+ function startStateCapture(store) {
12378
+ if (!profilingEnabled()) return;
12379
+ store.subscribe((state, prev) => {
12380
+ const prevEntries = new Map(Object.entries(prev));
12381
+ const changed = [];
12382
+ let screenKind = null;
12383
+ for (const [key, value] of Object.entries(state)) {
12384
+ if (typeof value === "function") continue;
12385
+ if (value !== prevEntries.get(key)) changed.push(key);
12386
+ if (key === "screen" && typeof value === "object" && value !== null && "kind" in value && typeof value.kind === "string") screenKind = value.kind;
12387
+ }
12388
+ if (changed.length === 0) return;
12389
+ record("state", {
12390
+ screen: screenKind,
12391
+ changed
12392
+ });
12393
+ });
12394
+ }
12395
+ /**
12396
+ * React Profiler onRender callback for the <Profiler> wrapping <App>.
12397
+ * One event per commit: which subtree, mount/update, and actual vs base
12398
+ * duration (actualDuration >> baseDuration means memoization is losing).
12399
+ */
12400
+ function onRenderCommit(id, phase, actualDuration, baseDuration, startTime, commitTime) {
12401
+ record("commits", {
12402
+ id,
12403
+ phase,
12404
+ actualDuration,
12405
+ baseDuration,
12406
+ startTime,
12407
+ commitTime
12408
+ });
12409
+ }
12410
+
12320
12411
  //#endregion
12321
12412
  //#region src/chat/boot.tsx
12322
12413
  /**
@@ -12329,7 +12420,12 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
12329
12420
  await registerGrammars();
12330
12421
  const watchdog = startMemoryWatchdog();
12331
12422
  installCrashHandler({ memoryProvider: () => watchdog.snapshot() });
12332
- const renderer = await createCliRenderer({ exitOnCtrlC: false });
12423
+ const renderer = await createCliRenderer({
12424
+ exitOnCtrlC: false,
12425
+ ...profilingRendererOptions()
12426
+ });
12427
+ startRendererStatsCapture(renderer);
12428
+ startStateCapture(useStore);
12333
12429
  extend({ markdown: LinkifiedMarkdownRenderable });
12334
12430
  useStore.setState({
12335
12431
  authKind,
@@ -12349,16 +12445,20 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
12349
12445
  source: renderer,
12350
12446
  applyThemeMode: (mode) => useStore.getState().setThemeMode(mode)
12351
12447
  });
12352
- createRoot(renderer).render(/* @__PURE__ */ jsx(App, {
12353
- appUrl,
12354
- sessionToken,
12355
- authKind,
12356
- shareMachine,
12357
- promptHistoryPath,
12358
- notifications,
12359
- agentSelector: agentSelector ?? null,
12360
- conversationId: conversationId ?? null,
12361
- newConversation
12448
+ createRoot(renderer).render(/* @__PURE__ */ jsx(Profiler, {
12449
+ id: "app",
12450
+ onRender: onRenderCommit,
12451
+ children: /* @__PURE__ */ jsx(App, {
12452
+ appUrl,
12453
+ sessionToken,
12454
+ authKind,
12455
+ shareMachine,
12456
+ promptHistoryPath,
12457
+ notifications,
12458
+ agentSelector: agentSelector ?? null,
12459
+ conversationId: conversationId ?? null,
12460
+ newConversation
12461
+ })
12362
12462
  }));
12363
12463
  }
12364
12464
  /** Open the same workspace picker used by the initial agent screen. */
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
 
9
9
  //#region package.json
10
10
  var name = "skydive-cli";
11
- var version$1 = "0.4.0-beta.1033";
11
+ var version$1 = "0.4.0-beta.1068";
12
12
 
13
13
  //#endregion
14
14
  //#region src/auth/organization.ts
@@ -1057,4 +1057,211 @@ function printFatalNotice(file, memory) {
1057
1057
  }
1058
1058
 
1059
1059
  //#endregion
1060
- export { setActiveWorkspace as C, listWorkspaces as S, version$1 as T, themes as _, WORDMARK as a, getActiveWorkspaceId as b, applyTheme as c, noColorRequested as d, theme as f, themeVersion as g, themeModeFromColorFgBg as h, MARK_CELLS as i, findTheme as l, themeMode as m, buildCrashReport as n, brandHelpArt as o, themeForMode as p, writeCrashReport as r, DEFAULT_THEME_ID as s, installCrashHandler as t, monoTheme as u, themesForMode as v, name as w, getSessionIdentity as x, ensureActiveOrganization as y };
1060
+ //#region src/profiling/profiler.ts
1061
+ /**
1062
+ * Opt-in session profiling. When SKYDIVE_PROFILE is set, an invocation
1063
+ * records everything needed to reconstruct what happened performance-wise —
1064
+ * designed so the whole directory can be handed to an agent (or a human)
1065
+ * and read without special tooling:
1066
+ *
1067
+ * network.ndjson every fetch: method, url, status, timing, content type
1068
+ * state.ndjson TUI state transitions (screen changes, store updates)
1069
+ * commits.ndjson React commits: which subtree rendered, when, how long
1070
+ * render.ndjson OpenTUI renderer samples: fps, frame times, cells drawn
1071
+ * render.json final renderer stats dump (full frame-time series)
1072
+ * cpu-<pid>.cpuprofile V8 CPU profile (Node-run commands only)
1073
+ * meta-<pid>.json argv, versions, runtime, exit code, wall time
1074
+ *
1075
+ * Every ndjson event carries a wall-clock `t` (epoch ms) and `pid`, so
1076
+ * records from the Node parent and the Bun-re-exec'd chat TUI land in the
1077
+ * same files and stay correlatable on one clock. The profile directory is
1078
+ * created by the first process and shared with children through
1079
+ * SKYDIVE_PROFILE_DIR (the chat re-exec inherits the environment).
1080
+ *
1081
+ * Inert unless SKYDIVE_PROFILE is set: no patched fetch, no subscriptions,
1082
+ * no inspector session.
1083
+ */
1084
+ const ENV_FLAG = "SKYDIVE_PROFILE";
1085
+ const ENV_DIR = "SKYDIVE_PROFILE_DIR";
1086
+ let activeDir = null;
1087
+ let startedAtMs = 0;
1088
+ /**
1089
+ * Buffered event lines per stream, flushed asynchronously. Events are
1090
+ * appended to an in-memory buffer and written with fs.promises off the
1091
+ * hot path, so recording never blocks the TUI's event loop — high-rate
1092
+ * streams (React commits, renderer samples) stay cheap. Whatever is
1093
+ * still buffered when the process exits is drained synchronously in the
1094
+ * exit handler, where async I/O would never flush.
1095
+ */
1096
+ const pendingLines = /* @__PURE__ */ new Map();
1097
+ let flushScheduled = false;
1098
+ let flushing = Promise.resolve();
1099
+ function drainBuffersSync() {
1100
+ if (activeDir === null) return;
1101
+ for (const [stream, lines] of pendingLines) {
1102
+ if (lines.length === 0) continue;
1103
+ pendingLines.set(stream, []);
1104
+ try {
1105
+ fs.appendFileSync(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
1106
+ } catch (_error) {}
1107
+ }
1108
+ }
1109
+ function scheduleFlush() {
1110
+ if (flushScheduled || activeDir === null) return;
1111
+ flushScheduled = true;
1112
+ setTimeout(() => {
1113
+ flushScheduled = false;
1114
+ flushing = flushing.then(async () => {
1115
+ if (activeDir === null) return;
1116
+ for (const [stream, lines] of pendingLines) {
1117
+ if (lines.length === 0) continue;
1118
+ pendingLines.set(stream, []);
1119
+ try {
1120
+ await fs.promises.appendFile(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
1121
+ } catch (_error) {}
1122
+ }
1123
+ });
1124
+ }, 100).unref?.();
1125
+ }
1126
+ function profilingEnabled() {
1127
+ const v = process.env[ENV_FLAG];
1128
+ return v !== void 0 && v !== "" && v !== "0";
1129
+ }
1130
+ function sanitizeToken(token) {
1131
+ return token.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
1132
+ }
1133
+ /**
1134
+ * Appends one event to a stream file. Buffered and written asynchronously
1135
+ * so recording never blocks the event loop; the exit handler drains any
1136
+ * remainder synchronously so abrupt exits still keep their events.
1137
+ */
1138
+ function record(stream, event) {
1139
+ if (activeDir === null) return;
1140
+ const line = JSON.stringify({
1141
+ t: Date.now(),
1142
+ pid: process.pid,
1143
+ ...event
1144
+ });
1145
+ const lines = pendingLines.get(stream);
1146
+ if (lines === void 0) pendingLines.set(stream, [line]);
1147
+ else lines.push(line);
1148
+ scheduleFlush();
1149
+ }
1150
+ /** Writes a JSON artifact (non-append) into the profile directory. */
1151
+ function writeArtifact(name, data) {
1152
+ if (activeDir === null) return;
1153
+ try {
1154
+ fs.writeFileSync(path.join(activeDir, name), JSON.stringify(data, null, 2));
1155
+ } catch (_error) {}
1156
+ }
1157
+ /**
1158
+ * Patches globalThis.fetch to record request timing. A wrapper (not
1159
+ * undici's diagnostics_channel) because chat re-execs under Bun, where
1160
+ * fetch is Bun-native and undici events never fire; the wrapper behaves
1161
+ * identically in both runtimes.
1162
+ */
1163
+ function installFetchRecorder() {
1164
+ const original = globalThis.fetch;
1165
+ const wrapped = async (...args) => {
1166
+ const [input, init] = args;
1167
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1168
+ const method = init?.method ?? (input instanceof Request ? input.method : "GET");
1169
+ const started = Date.now();
1170
+ try {
1171
+ const res = await original(...args);
1172
+ const contentType = res.headers.get("content-type") ?? "";
1173
+ record("network", {
1174
+ method,
1175
+ url,
1176
+ status: res.status,
1177
+ durationMs: Date.now() - started,
1178
+ contentType,
1179
+ streamed: contentType.includes("text/event-stream")
1180
+ });
1181
+ return res;
1182
+ } catch (err) {
1183
+ record("network", {
1184
+ method,
1185
+ url,
1186
+ status: 0,
1187
+ durationMs: Date.now() - started,
1188
+ error: err instanceof Error ? err.message : String(err)
1189
+ });
1190
+ throw err;
1191
+ }
1192
+ };
1193
+ globalThis.fetch = Object.assign(wrapped, original);
1194
+ }
1195
+ function isBunRuntime() {
1196
+ return typeof process !== "undefined" && "bun" in process.versions;
1197
+ }
1198
+ /**
1199
+ * Starts the V8 CPU profiler via node:inspector. Node-run commands only —
1200
+ * Bun does not implement the inspector Profiler domain, so the chat TUI
1201
+ * skips this artifact.
1202
+ */
1203
+ function startCpuProfile() {
1204
+ if (isBunRuntime()) return;
1205
+ (async () => {
1206
+ try {
1207
+ const { Session } = await import("node:inspector/promises");
1208
+ const session = new Session();
1209
+ session.connect();
1210
+ await session.post("Profiler.enable");
1211
+ await session.post("Profiler.start");
1212
+ process.once("beforeExit", () => {
1213
+ (async () => {
1214
+ try {
1215
+ const { profile } = await session.post("Profiler.stop");
1216
+ writeArtifact(`cpu-${process.pid}.cpuprofile`, profile);
1217
+ session.disconnect();
1218
+ } catch (_error) {}
1219
+ })();
1220
+ });
1221
+ } catch (_error) {}
1222
+ })();
1223
+ }
1224
+ /**
1225
+ * Activates profiling for this process if SKYDIVE_PROFILE is set. Creates
1226
+ * the profile directory (or joins the one a parent process created),
1227
+ * patches fetch, starts the CPU profiler, and registers the exit-time
1228
+ * meta dump. Call once, as early as possible.
1229
+ */
1230
+ function maybeStartProfiling(argv, cliVersion) {
1231
+ if (!profilingEnabled() || activeDir !== null) return;
1232
+ const inherited = process.env[ENV_DIR];
1233
+ if (inherited !== void 0 && inherited !== "") activeDir = inherited;
1234
+ else {
1235
+ const flagValue = process.env[ENV_FLAG] ?? "1";
1236
+ const command = argv.find((a) => !a.startsWith("-")) ?? "chat";
1237
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
1238
+ activeDir = flagValue === "1" || flagValue.toLowerCase() === "true" ? path.resolve(process.cwd(), `skydive-profile-${sanitizeToken(command)}-${stamp}`) : path.resolve(process.cwd(), flagValue);
1239
+ process.env[ENV_DIR] = activeDir;
1240
+ }
1241
+ try {
1242
+ fs.mkdirSync(activeDir, { recursive: true });
1243
+ } catch (_error) {
1244
+ activeDir = null;
1245
+ return;
1246
+ }
1247
+ startedAtMs = Date.now();
1248
+ installFetchRecorder();
1249
+ startCpuProfile();
1250
+ process.on("exit", (code) => {
1251
+ drainBuffersSync();
1252
+ writeArtifact(`meta-${process.pid}.json`, {
1253
+ argv,
1254
+ cliVersion,
1255
+ runtime: isBunRuntime() ? `bun ${process.versions["bun"]}` : `node ${process.version}`,
1256
+ platform: process.platform,
1257
+ pid: process.pid,
1258
+ exitCode: code,
1259
+ startedAt: new Date(startedAtMs).toISOString(),
1260
+ wallTimeMs: Date.now() - startedAtMs
1261
+ });
1262
+ if (process.env[ENV_DIR] === activeDir && inherited === void 0) process.stderr.write(`\nprofile written to ${activeDir}\n`);
1263
+ });
1264
+ }
1265
+
1266
+ //#endregion
1267
+ export { ensureActiveOrganization as C, setActiveWorkspace as D, listWorkspaces as E, name as O, themesForMode as S, getSessionIdentity as T, themeForMode as _, installCrashHandler as a, themeVersion as b, MARK_CELLS as c, DEFAULT_THEME_ID as d, applyTheme as f, theme as g, noColorRequested as h, writeArtifact as i, version$1 as k, WORDMARK as l, monoTheme as m, profilingEnabled as n, buildCrashReport as o, findTheme as p, record as r, writeCrashReport as s, maybeStartProfiling as t, brandHelpArt as u, themeMode as v, getActiveWorkspaceId as w, themes as x, themeModeFromColorFgBg as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.4.0-beta.1033",
3
+ "version": "0.4.0-beta.1068",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",