skydive-cli 0.2.0 → 0.3.0

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.
@@ -1,7 +1,191 @@
1
1
  #!/usr/bin/env node
2
- import { n as createRestClient, t as HttpError } from "./rest-CamHVOce.mjs";
3
- import { n as isRecord, t as errorMessage } from "./util-CeisaZVY.mjs";
2
+ import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, t as HttpError } from "./rest-BlN_uWmL.mjs";
3
+ import path from "node:path";
4
+ import Conf from "conf";
5
+ import { err, ok } from "neverthrow";
6
+ import stableStringify from "safe-stable-stringify";
4
7
 
8
+ //#region src/config.ts
9
+ /** Default host for the public management API (`/v1`, API-key auth). */
10
+ const DEFAULT_API_URL = "https://api.skydive.com";
11
+ /**
12
+ * Default origin for the interactive chat client (`skydive chat`).
13
+ *
14
+ * The API host that serves better-auth (`/api/auth/*`) and the internal tRPC
15
+ * API (`/api/v1/trpc`) that chat streams over. We target the API host
16
+ * directly (not the web front door) because chat opens a WebSocket and
17
+ * authenticates with a bearer token on the upgrade request. Same host as
18
+ * `DEFAULT_API_URL`; override with `--api-url` / `SKYDIVE_APP_URL` for local
19
+ * dev or while the DNS record is still being provisioned.
20
+ */
21
+ const DEFAULT_APP_URL = "https://api.skydive.com";
22
+ /** Web front door, for pages opened in the user's browser. */
23
+ const DEFAULT_WEB_URL = "https://skydive.com";
24
+ function resolveWebUrl(appUrl) {
25
+ if (appUrl == null) return appUrl;
26
+ return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
27
+ }
28
+ /** Prefix on workspace-scoped Skydive API keys. Kept in sync with the API's
29
+ * `API_KEY_PREFIX` (`apps/anyone/api/src/lib/api-key.ts`); the CLI is a
30
+ * standalone published package so it can't import the backend constant. */
31
+ const API_KEY_PREFIX = "sky_live_";
32
+ /**
33
+ * Common prefix across all Skydive API key kinds — `sky_live_…` workspace
34
+ * keys today, `sky_user_…` account keys when ANY-5105 lands. The CLI only
35
+ * sanity-checks the family on `--api-key`; the server authoritatively rejects
36
+ * a kind that can't drive a given route, with a clearer message than the
37
+ * client could produce.
38
+ */
39
+ const API_KEY_FAMILY_PREFIX = "sky_";
40
+ /** Where users mint and copy API keys. Shown in the login prompt. */
41
+ const API_KEYS_URL = "skydive.com/settings/account";
42
+ const store = new Conf({
43
+ projectName: process.env["SKYDIVE_CONFIG_NAME"] ?? "skydive",
44
+ projectSuffix: "",
45
+ configFileMode: 384
46
+ });
47
+ function resolveConfig(opts) {
48
+ const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
49
+ const apiUrl = process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL;
50
+ if (!apiKey) return err({ message: "Not authenticated. Run `skydive auth login` first." });
51
+ return ok({
52
+ apiKey,
53
+ apiUrl
54
+ });
55
+ }
56
+ /**
57
+ * Resolve the bearer credential for the management API (`agents` / `keys` /
58
+ * `secrets`). The server's `/v1` gate accepts either an API key or the
59
+ * device-flow session bearer, so both work — but only one of them tracks the
60
+ * active workspace.
61
+ *
62
+ * An API key is pinned server-side to the organization that minted it and
63
+ * ignores the workspace header by design, so it can never follow `skydive
64
+ * workspace switch`. A key is also a strictly narrower credential than its
65
+ * owner's session. So the session wins whenever there is one, and a key is
66
+ * what's left for machines that never ran an interactive login.
67
+ *
68
+ * `SKYDIVE_SESSION_TOKEN=` (empty) suppresses session auth for one
69
+ * invocation, to drive a specific organization's key while signed in.
70
+ */
71
+ function resolveManagementAuth(opts) {
72
+ const session = resolveSession({ appUrl: opts.apiUrl });
73
+ if (session.isOk()) return ok({
74
+ token: session.value.sessionToken,
75
+ apiUrl: session.value.appUrl,
76
+ kind: "session",
77
+ pinnedWorkspaceName: null
78
+ });
79
+ const envKey = process.env["SKYDIVE_API_KEY"];
80
+ const storedKey = store.get("apiKey");
81
+ const apiKey = envKey ?? storedKey;
82
+ if (apiKey) return ok({
83
+ token: apiKey,
84
+ apiUrl: process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL,
85
+ kind: "api-key",
86
+ pinnedWorkspaceName: !envKey && storedKey ? store.get("apiKeyWorkspaceName") ?? null : null
87
+ });
88
+ return err({ message: "Not authenticated. Run `skydive auth login`." });
89
+ }
90
+ function saveConfig(config) {
91
+ store.set("apiKey", config.apiKey);
92
+ store.set("apiUrl", config.apiUrl);
93
+ if (config.apiKeyId) store.set("apiKeyId", config.apiKeyId);
94
+ else store.delete("apiKeyId");
95
+ if (config.workspaceName) store.set("apiKeyWorkspaceName", config.workspaceName);
96
+ else store.delete("apiKeyWorkspaceName");
97
+ }
98
+ /** Server-side id of the auto-minted key, if login minted one. */
99
+ function getStoredApiKeyId() {
100
+ return store.get("apiKeyId") ?? null;
101
+ }
102
+ /** Workspace the auto-minted key is pinned to, if login recorded one. */
103
+ function getStoredApiKeyWorkspaceName() {
104
+ return store.get("apiKeyWorkspaceName") ?? null;
105
+ }
106
+ function deleteConfig() {
107
+ store.clear();
108
+ }
109
+ function getConfigPath() {
110
+ return store.path;
111
+ }
112
+ /**
113
+ * Where the chat TUI persists its prompt history (up-arrow recall). Kept
114
+ * beside the config file so all CLI state lives in one directory.
115
+ */
116
+ function getLastSeenVersion() {
117
+ return store.get("lastSeenVersion");
118
+ }
119
+ function setLastSeenVersion(version) {
120
+ store.set("lastSeenVersion", version);
121
+ }
122
+ function getPromptHistoryPath() {
123
+ return path.join(path.dirname(store.path), "prompt-history.jsonl");
124
+ }
125
+ /**
126
+ * Where the chat TUI persists pending review comments — one JSON file per
127
+ * conversation, so a pending comment survives conversation switches and
128
+ * process death. Kept beside the config file like prompt history.
129
+ */
130
+ function getReviewStateDir() {
131
+ return path.join(path.dirname(store.path), "review");
132
+ }
133
+ /**
134
+ * Resolve the chat/auth origin. Precedence: `SKYDIVE_APP_URL` env > explicit
135
+ * `--api-url` style override > stored value > `SKYDIVE_API_URL` env >
136
+ * `DEFAULT_APP_URL`.
137
+ *
138
+ * The `SKYDIVE_API_URL` fallback matters for previews: the device/`--web`
139
+ * flow and chat hit the same api service as the management API, so pointing
140
+ * `SKYDIVE_API_URL` at a preview stack is enough — you don't also have to set
141
+ * `SKYDIVE_APP_URL`. Otherwise auth would silently fall through to prod
142
+ * (`DEFAULT_APP_URL`) and hand back a prod verification URL.
143
+ */
144
+ function resolveAppUrl(opts) {
145
+ return process.env["SKYDIVE_APP_URL"] ?? opts.appUrl ?? store.get("appUrl") ?? process.env["SKYDIVE_API_URL"] ?? DEFAULT_APP_URL;
146
+ }
147
+ function resolveSession(opts) {
148
+ const sessionToken = process.env["SKYDIVE_SESSION_TOKEN"] ?? store.get("sessionToken");
149
+ const appUrl = resolveAppUrl(opts);
150
+ if (!sessionToken) return err({ message: "Not signed in for chat. Run `skydive chat` to sign in." });
151
+ return ok({
152
+ sessionToken,
153
+ appUrl
154
+ });
155
+ }
156
+ function saveSession(session) {
157
+ store.set("sessionToken", session.sessionToken);
158
+ store.set("sessionObtainedAt", (/* @__PURE__ */ new Date()).toISOString());
159
+ store.set("appUrl", session.appUrl);
160
+ }
161
+ function getSavedTheme(mode) {
162
+ return store.get(mode === "dark" ? "themeDark" : "themeLight");
163
+ }
164
+ function saveTheme(mode, themeId) {
165
+ store.set(mode === "dark" ? "themeDark" : "themeLight", themeId);
166
+ }
167
+ /**
168
+ * Whether `skydive chat` should enable portal machine sharing on launch
169
+ * without `--share-machine`. Set by hand-editing `shareMachineDefault` in
170
+ * config.json — deliberately no CLI command (kept off the API surface).
171
+ * Sharing only makes the machine reachable — agents still need a
172
+ * (persistent) grant to run anything, so this default skips the per-session
173
+ * enable step, not the consent step.
174
+ */
175
+ function getShareMachineDefault() {
176
+ return store.get("shareMachineDefault") ?? false;
177
+ }
178
+ /**
179
+ * Whether the update check is disabled via config.json (`"updateCheck":
180
+ * false`). The persistent counterpart to the SKYDIVE_NO_UPDATE_CHECK /
181
+ * NO_UPDATE_NOTIFIER env opt-outs; like shareMachineDefault, set by editing
182
+ * config.json — deliberately no CLI command.
183
+ */
184
+ function getUpdateCheckDisabled() {
185
+ return store.get("updateCheck") === false;
186
+ }
187
+
188
+ //#endregion
5
189
  //#region src/chat/tui/chat/card.ts
6
190
  const urlActionKinds = [
7
191
  "open_oauth",
@@ -55,6 +239,23 @@ function parseButton(element) {
55
239
  primary
56
240
  };
57
241
  }
242
+ /**
243
+ * Content identity for a card's spec, which is what separates a re-delivery of
244
+ * one card from two distinct cards: the same card reaches us twice (live on the
245
+ * run stream and again as the persisted message part when history is loaded for
246
+ * an in-flight run), while a single tool call can legitimately post SEVERAL
247
+ * different cards — `platform` subprocesses chained in one bash command all
248
+ * read the same TOOL_CALL_ID, so `toolCallId` alone identifies neither. Applies
249
+ * the same identity rule as the web client (web/src/features/chat-v2/turn-segments.ts).
250
+ *
251
+ * The serialization has to be key-order-independent, because the persisted copy
252
+ * of a spec comes back through a jsonb round-trip that can reorder keys and
253
+ * order-sensitive `JSON.stringify` would read that as a different card.
254
+ * `safe-stable-stringify` sorts keys and tolerates cycles.
255
+ */
256
+ function specKeyFor(spec) {
257
+ return stableStringify(spec) ?? crypto.randomUUID();
258
+ }
58
259
  function parseConnectCard(spec) {
59
260
  if (!isRecord(spec)) return null;
60
261
  const { root, elements } = spec;
@@ -210,14 +411,20 @@ function summarizeConnectCard(card, appUrl) {
210
411
  const act = button.action;
211
412
  switch (act.kind) {
212
413
  case "open_oauth":
213
- case "open_external_oauth":
214
- case "open_github_app": return {
414
+ case "open_external_oauth": return {
215
415
  ...base,
216
416
  action: {
217
417
  kind: "open_url",
218
418
  url: resolveConnectUrl(act.url, appUrl)
219
419
  }
220
420
  };
421
+ case "open_github_app": return {
422
+ ...base,
423
+ action: {
424
+ kind: "open_url",
425
+ url: resolveConnectUrl(act.url, resolveWebUrl(appUrl))
426
+ }
427
+ };
221
428
  case "submit_credential": return {
222
429
  ...base,
223
430
  action: {
@@ -337,17 +544,24 @@ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversat
337
544
  };
338
545
  }
339
546
  /**
340
- * Turn a transport failure into an actionable CLI error. A Cloudflare edge
341
- * 5xx (502/504) on a long run otherwise leaks a raw HTML/JSON error page to
342
- * stdout, which is impossible to act on. When a messageId is known and
343
- * recoverable we point the user at `skydive messages get <messageId>` rather
344
- * than a blind retry (a retry re-executes an agent that may have write access).
547
+ * Turn a transport failure into an actionable CLI error. Two failure classes
548
+ * reach here, both of which otherwise leak a raw HTML/JSON error page to stdout:
549
+ *
550
+ * - A Cloudflare edge 5xx (502/504) on a long run. When a messageId is known
551
+ * and recoverable we point the user at `skydive messages get <messageId>`
552
+ * rather than a blind retry (a retry re-executes an agent that may have
553
+ * write access).
554
+ * - A 4xx from the send/stream endpoints (a validation 400 whose body is a raw
555
+ * Zod issues array, a billing/permission block, an expired session). These
556
+ * go through {@link sendErrorMessage} so the `-p` path prints the same plain,
557
+ * actionable copy the interactive TUI shows instead of an obtuse blob.
345
558
  */
346
559
  function toPrintError(err, messageId) {
347
560
  if (err instanceof HttpError && err.status >= 500) {
348
561
  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}` : "";
349
562
  return /* @__PURE__ */ new Error(`The request to Skydive timed out at the edge (HTTP ${err.status}).${recovery}`);
350
563
  }
564
+ if (err instanceof HttpError) return new Error(sendErrorMessage(err));
351
565
  return err instanceof Error ? err : new Error(String(err));
352
566
  }
353
567
  /**
@@ -463,4 +677,4 @@ async function readStdin() {
463
677
  }
464
678
 
465
679
  //#endregion
466
- export { runPrint as a, cardActionErrorMessage as c, reconcileMaskedInput as d, resolveConnectUrl as f, resolveAgent as i, parseExternalOauthConnectParams as l, messageGet as n, toPrintError as o, parseConnectCard as p, readStdin as r, MASK_CHAR as s, collectRunText as t, parseOauthConnectParams as u };
680
+ export { resolveAppUrl as A, getPromptHistoryPath as C, getStoredApiKeyId as D, getShareMachineDefault as E, saveConfig as F, saveSession as I, saveTheme as L, resolveManagementAuth as M, resolveSession as N, getStoredApiKeyWorkspaceName as O, resolveWebUrl as P, setLastSeenVersion as R, getLastSeenVersion as S, getSavedTheme as T, API_KEY_PREFIX as _, runPrint as a, deleteConfig as b, cardActionErrorMessage as c, reconcileMaskedInput as d, resolveConnectUrl as f, API_KEY_FAMILY_PREFIX as g, API_KEYS_URL as h, resolveAgent as i, resolveConfig as j, getUpdateCheckDisabled as k, parseExternalOauthConnectParams as l, specKeyFor as m, messageGet as n, toPrintError as o, parseConnectCard as p, readStdin as r, MASK_CHAR as s, collectRunText as t, parseOauthConnectParams as u, DEFAULT_API_URL as v, getReviewStateDir as w, getConfigPath as x, DEFAULT_APP_URL as y };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as printError } from "./output-B4cW10Ph.mjs";
2
+ import { n as printError } from "./output-DYzzdXYV.mjs";
3
3
 
4
4
  //#region src/chat/print-share.ts
5
5
  /**
@@ -14,7 +14,7 @@ import { n as printError } from "./output-B4cW10Ph.mjs";
14
14
  * the reply (and to --json).
15
15
  */
16
16
  async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
17
- const { PortalClient } = await import("./client-D6NAkL9e.mjs");
17
+ const { PortalClient } = await import("./client-DuwxEDG4.mjs");
18
18
  let signalConnected;
19
19
  const connected = new Promise((resolve) => {
20
20
  signalConnected = resolve;
@@ -22,7 +22,7 @@ async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
22
22
  const machineShare = new PortalClient({
23
23
  appUrl,
24
24
  sessionToken,
25
- cwd: process.cwd(),
25
+ resolveCwd: () => process.cwd(),
26
26
  onState: (state) => {
27
27
  if (state.status === "connected") signalConnected();
28
28
  if (state.status === "error") console.error(`portal: connection error: ${state.error ?? "unknown"} — retrying`);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as SandboxStream } from "./client-CpEvH2Pq.mjs";
2
+ import { t as SandboxStream } from "./client-DfcJFEbh.mjs";
3
3
 
4
4
  //#region src/chat/sandbox/raw-pty.ts
5
5
  /**
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./client-DfcJFEbh.mjs";
3
+ import { t as runRawPtyPassthrough } from "./raw-pty-BcjbTjHJ.mjs";
4
+
5
+ export { runRawPtyPassthrough };
@@ -2,6 +2,19 @@
2
2
  import { z } from "zod";
3
3
  import { createParser } from "eventsource-parser";
4
4
 
5
+ //#region src/chat/util.ts
6
+ /** Narrowing helper for the many `unknown` payloads the chat stream and
7
+ * tool inputs/outputs carry. A type predicate (not an `as` cast), so call
8
+ * sites can read properties without asserting. */
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ /** Best-effort message from an unknown thrown value. */
13
+ function errorMessage(err) {
14
+ return err instanceof Error ? err.message : String(err);
15
+ }
16
+
17
+ //#endregion
5
18
  //#region src/chat/api/rest.ts
6
19
  var HttpError = class extends Error {
7
20
  constructor(status, body) {
@@ -25,12 +38,76 @@ function errorDetail(err) {
25
38
  }
26
39
  return err instanceof Error ? err.message : String(err);
27
40
  }
41
+ /**
42
+ * The server's human-readable line out of an error response body, or null when
43
+ * the body carries none. Two shapes reach the CLI on a failed send:
44
+ *
45
+ * - `{ error: string }` — the chat route's own errors (a ChatClientError like
46
+ * "agent not found" / "attachment not found: <id>", the 429 "Too many
47
+ * requests", the generic "send failed"). Returned verbatim; these are
48
+ * written to be shown.
49
+ * - `{ success: false, error: { issues: [{ message }, …] } }` — a Zod request
50
+ * validation failure from `zValidator` (e.g. the message-too-long copy).
51
+ * Without this the CLI printed the whole issues array as a raw JSON blob,
52
+ * which is the "obtuse 400 with no recourse" a user sees. We lift the first
53
+ * issue's message, which the schema authors wrote for humans.
54
+ *
55
+ * Anything else (a Cloudflare/ALB HTML error page, an empty body) yields null
56
+ * so the caller can fall back to a status-based line.
57
+ */
58
+ function serverErrorMessage(body) {
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(body);
62
+ } catch (_error) {
63
+ return null;
64
+ }
65
+ if (!isRecord(parsed)) return null;
66
+ if (typeof parsed.error === "string" && parsed.error.length > 0) return parsed.error;
67
+ if (isRecord(parsed.error) && Array.isArray(parsed.error.issues)) {
68
+ const first = parsed.error.issues.find((issue) => isRecord(issue) && typeof issue.message === "string" && !!issue.message);
69
+ if (first) return first.message;
70
+ }
71
+ return null;
72
+ }
73
+ /**
74
+ * A short, actionable line for a failed chat send, for both the interactive
75
+ * TUI error row and the headless `-p` path. The raw transport error is an
76
+ * `HTTP 400: {"success":false,"error":{"issues":[…]}}` blob that a user can do
77
+ * nothing with; this maps every send failure the server can produce to plain
78
+ * language and, where possible, a next step.
79
+ *
80
+ * Precedence: a message the SERVER wrote for humans always wins (validation
81
+ * copy, "agent not found", a billing/permission reason) — we only synthesize a
82
+ * line from the HTTP status when the body carried nothing usable. This keeps
83
+ * new server-side error copy flowing through without a CLI change, while never
84
+ * leaving the user staring at a bare status code.
85
+ */
86
+ function sendErrorMessage(err) {
87
+ if (!(err instanceof HttpError)) return `Couldn't reach Skydive (${err instanceof Error ? err.message : String(err)}). Check your connection and try again.`;
88
+ const fromServer = serverErrorMessage(err.body);
89
+ switch (err.status) {
90
+ case 401: return "Your session has expired. Run `skydive auth login --web`, then send again.";
91
+ case 403: return fromServer ?? "You don't have permission to send to this agent in the current workspace.";
92
+ case 404: return fromServer ?? "This conversation or agent no longer exists.";
93
+ case 413: return fromServer ?? "That message is too large to send. Attach it as a file instead of pasting it inline.";
94
+ case 429: return "You're sending messages too quickly. Wait a moment and try again.";
95
+ default: break;
96
+ }
97
+ if (err.status >= 500) return `Skydive had a problem sending that (HTTP ${err.status}). Wait a moment and try again.`;
98
+ if (err.status === 400) {
99
+ if (fromServer && fromServer !== "send failed") return fromServer;
100
+ return "Skydive couldn't send that message. Try again in a moment.";
101
+ }
102
+ return fromServer ?? `Send failed (HTTP ${err.status}).`;
103
+ }
28
104
  const MAX_STREAM_RECONNECTS = 5;
29
- function createRestClient({ appUrl, sessionToken }) {
105
+ function createRestClient({ appUrl, sessionToken, workspaceId }) {
30
106
  const baseHeaders = {
31
107
  authorization: `Bearer ${sessionToken}`,
32
108
  accept: "application/json"
33
109
  };
110
+ if (workspaceId) baseHeaders["x-workspace-id"] = workspaceId;
34
111
  async function get(path, schema) {
35
112
  const res = await fetch(`${appUrl}${path}`, { headers: baseHeaders });
36
113
  if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
@@ -66,6 +143,7 @@ function createRestClient({ appUrl, sessionToken }) {
66
143
  authorization: `Bearer ${sessionToken}`,
67
144
  accept: "text/event-stream"
68
145
  };
146
+ if (workspaceId) headers["x-workspace-id"] = workspaceId;
69
147
  if (lastEventId) headers["last-event-id"] = lastEventId;
70
148
  const res = await fetch(`${appUrl}${path}`, {
71
149
  headers,
@@ -138,6 +216,17 @@ function createRestClient({ appUrl, sessionToken }) {
138
216
  const { agent } = await post("/api/v1/agents", { name }, createAgentResponseSchema);
139
217
  return agent;
140
218
  },
219
+ getAgent: async ({ agentId }) => {
220
+ const { agent } = await get(`/api/v1/agents/${encodeURIComponent(agentId)}`, getAgentResponseSchema);
221
+ return {
222
+ id: agent.id,
223
+ name: agent.name
224
+ };
225
+ },
226
+ suggestAgentIdentity: async () => {
227
+ const { suggestion } = await get("/api/v1/agents/suggest", suggestAgentResponseSchema);
228
+ return suggestion;
229
+ },
141
230
  getConversation: async ({ conversationId }) => {
142
231
  const { conversation } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}`, getConversationResponseSchema);
143
232
  return conversation;
@@ -150,7 +239,8 @@ function createRestClient({ appUrl, sessionToken }) {
150
239
  const { agent } = await post(`/api/v1/agents/${encodeURIComponent(agentId)}`, { model }, updateAgentResponseSchema, "PATCH");
151
240
  return { model: agent.model ?? null };
152
241
  },
153
- listConversations: async ({ agentId, limit, channels, onPage }) => {
242
+ listConversations: async ({ agentId, limit, channels, archived, query, onPage }) => {
243
+ const trimmedQuery = query?.trim();
154
244
  const all = [];
155
245
  const maxConversations = limit ?? 5e3;
156
246
  let cursor;
@@ -162,6 +252,8 @@ function createRestClient({ appUrl, sessionToken }) {
162
252
  });
163
253
  params.set("limit", String(Math.min(remaining, 100)));
164
254
  if (cursor) params.set("cursor", cursor);
255
+ if (archived) params.set("archived", archived);
256
+ if (trimmedQuery) params.set("query", trimmedQuery);
165
257
  for (const channel of channels ?? []) params.append("channels", channel);
166
258
  const page = await get(`/api/v1/conversations?${params.toString()}`, listConversationsResponseSchema);
167
259
  all.push(...page.conversations);
@@ -222,9 +314,19 @@ function createRestClient({ appUrl, sessionToken }) {
222
314
  sizeBytes: finalized.sizeBytes ?? size
223
315
  };
224
316
  },
317
+ forkConversation: async ({ conversationId }) => {
318
+ const { conversation } = await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/fork`, {}, forkConversationResponseSchema);
319
+ return conversation;
320
+ },
321
+ compactConversation: async ({ conversationId }) => {
322
+ await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/compact`, {}, z.object({ status: z.string() }));
323
+ },
225
324
  deleteConversation: async ({ conversationId }) => {
226
325
  await del(`/api/v1/conversations/${encodeURIComponent(conversationId)}`);
227
326
  },
327
+ setConversationArchived: async ({ conversationId, archived }) => {
328
+ await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/archive`, { archived }, z.object({ archived: z.boolean() }));
329
+ },
228
330
  sendMessage: async ({ clientSurface, ...input }) => post("/api/v1/chat/send", {
229
331
  ...input,
230
332
  clientSurface
@@ -275,7 +377,8 @@ function createRestClient({ appUrl, sessionToken }) {
275
377
  const res = await fetch(`${appUrl}/api/v1/chat/conversations/${encodeURIComponent(conversationId)}/stream`, {
276
378
  headers: {
277
379
  authorization: `Bearer ${sessionToken}`,
278
- accept: "text/event-stream"
380
+ accept: "text/event-stream",
381
+ ...workspaceId ? { "x-workspace-id": workspaceId } : {}
279
382
  },
280
383
  signal
281
384
  });
@@ -343,6 +446,18 @@ const listAgentsResponseSchema = z.object({
343
446
  totalCount: z.number().nullable().optional()
344
447
  });
345
448
  const createAgentResponseSchema = z.object({ agent: agentSummarySchema });
449
+ const getAgentResponseSchema = z.object({ agent: z.object({
450
+ id: z.string(),
451
+ name: z.string()
452
+ }).passthrough() });
453
+ const agentSuggestionSchema = z.object({ name: z.string() });
454
+ const suggestAgentResponseSchema = z.object({ suggestion: agentSuggestionSchema.nullable() });
455
+ const conversationAgentSchema = z.object({
456
+ id: z.string().uuid(),
457
+ name: z.string(),
458
+ slug: z.string().nullable().optional(),
459
+ title: z.string().nullable().optional()
460
+ });
346
461
  const conversationSummarySchema = z.object({
347
462
  id: z.string().uuid(),
348
463
  title: z.string().nullable(),
@@ -351,20 +466,24 @@ const conversationSummarySchema = z.object({
351
466
  preview: z.string().nullable(),
352
467
  channel: z.string().nullable(),
353
468
  channelLabel: z.string().nullable(),
354
- agent: z.object({
355
- id: z.string().uuid(),
356
- name: z.string(),
357
- slug: z.string().nullable().optional(),
358
- title: z.string().nullable().optional()
359
- })
469
+ viewerArchivedAt: z.string().nullable().optional(),
470
+ agent: conversationAgentSchema,
471
+ agents: z.array(conversationAgentSchema).optional()
360
472
  });
361
473
  const conversationDetailSchema = z.object({
362
474
  id: z.string().uuid(),
363
475
  title: z.string().nullable(),
364
476
  agentId: z.string().uuid(),
365
477
  createdAt: z.string(),
366
- updatedAt: z.string()
478
+ updatedAt: z.string(),
479
+ channel: z.string().nullable(),
480
+ channelLabel: z.string().nullable()
481
+ });
482
+ const conversationTitleSchema = z.object({
483
+ id: z.string().uuid(),
484
+ title: z.string().nullable()
367
485
  });
486
+ const forkConversationResponseSchema = z.object({ conversation: conversationTitleSchema });
368
487
  const getConversationResponseSchema = z.object({ conversation: conversationDetailSchema });
369
488
  const listConversationsResponseSchema = z.object({
370
489
  conversations: z.array(conversationSummarySchema),
@@ -444,14 +563,26 @@ const runStreamEventSchema = z.union([z.object({
444
563
  error: z.string().nullish()
445
564
  })]);
446
565
  const streamErrorSchema = z.object({ error: z.string() });
447
- const conversationStreamEventSchema = z.discriminatedUnion("kind", [z.object({
448
- kind: z.literal("conversation"),
449
- id: z.string(),
450
- title: z.string().nullable()
451
- }), z.object({
452
- kind: z.literal("run"),
453
- runId: z.string()
454
- })]);
566
+ const conversationStreamEventSchema = z.discriminatedUnion("kind", [
567
+ z.object({
568
+ kind: z.literal("conversation"),
569
+ id: z.string(),
570
+ title: z.string().nullable()
571
+ }),
572
+ z.object({
573
+ kind: z.literal("run"),
574
+ runId: z.string(),
575
+ createdAt: z.string().nullish()
576
+ }),
577
+ z.object({
578
+ kind: z.literal("compaction"),
579
+ status: z.enum([
580
+ "started",
581
+ "completed",
582
+ "failed"
583
+ ])
584
+ })
585
+ ]);
455
586
 
456
587
  //#endregion
457
- export { createRestClient as n, errorDetail as r, HttpError as t };
588
+ export { errorMessage as a, sendErrorMessage as i, createRestClient as n, isRecord as o, errorDetail as r, HttpError as t };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { i as sendErrorMessage, n as createRestClient, r as errorDetail, t as HttpError } from "./rest-BlN_uWmL.mjs";
3
+
4
+ export { createRestClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -25,6 +25,7 @@
25
25
  "test:ci": "vitest run --coverage --coverage.reporter=lcovonly --reporter=default --reporter=github-actions --minWorkers=1 --maxWorkers=2 --passWithNoTests && yarn test:tui",
26
26
  "test:tui": "bun test .tui.test",
27
27
  "render:frames": "bun scripts/render-frames.tsx",
28
+ "render:send-errors": "bun scripts/render-send-errors.tsx",
28
29
  "typecheck": "tsgo --noEmit"
29
30
  },
30
31
  "dependencies": {
@@ -38,6 +39,7 @@
38
39
  "open": "^10.1.0",
39
40
  "react": "^19.0.0",
40
41
  "react-devtools-core": "^7.0.1",
42
+ "safe-stable-stringify": "^2.3.1",
41
43
  "semver": "^7.7.4",
42
44
  "web-tree-sitter": "0.25.10",
43
45
  "ws": "^8.21.0",