skydive-cli 0.1.0-beta.276 → 0.1.0-beta.287

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
@@ -8,15 +8,15 @@ import Conf from "conf";
8
8
  import { err, ok } from "neverthrow";
9
9
  import { z } from "zod";
10
10
  import open from "open";
11
+ import { createParser } from "eventsource-parser";
11
12
  import { spawnSync } from "node:child_process";
12
13
  import { createHash } from "node:crypto";
13
14
  import fs from "node:fs";
14
15
  import zlib from "node:zlib";
15
- import { createParser } from "eventsource-parser";
16
16
  import os from "node:os";
17
17
 
18
18
  //#region package.json
19
- var version$1 = "0.1.0-beta.276";
19
+ var version$1 = "0.1.0-beta.287";
20
20
 
21
21
  //#endregion
22
22
  //#region src/types.ts
@@ -78,25 +78,32 @@ function resolveConfig(opts) {
78
78
  }
79
79
  /**
80
80
  * Resolve the bearer credential for the management API (`agents` / `keys` /
81
- * `secrets`). Prefers an API key (`SKYDIVE_API_KEY` env, then stored), and
82
- * falls back to the `--web` chat session token: the server's `/v1` gate routes
83
- * any non-`sky_` bearer through the signed-in session, so a device login alone
84
- * is enough to run management commands — no separate API key required.
81
+ * `secrets`). The server's `/v1` gate accepts either an API key or the `--web`
82
+ * session bearer, so both work but only one of them tracks the active
83
+ * workspace.
84
+ *
85
+ * An API key is pinned server-side to the organization that minted it and
86
+ * ignores the workspace header by design, so it can never follow `skydive
87
+ * workspace switch`. A key is also a strictly narrower credential than its
88
+ * owner's session. So the session wins whenever there is one, and a key is
89
+ * what's left for machines that never ran an interactive login.
90
+ *
91
+ * `SKYDIVE_SESSION_TOKEN=` (empty) suppresses session auth for one
92
+ * invocation, to drive a specific organization's key while signed in.
85
93
  */
86
94
  function resolveManagementAuth(opts) {
87
- const apiUrl = process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL;
95
+ const session = resolveSession({ appUrl: opts.apiUrl });
96
+ if (session.isOk()) return ok({
97
+ token: session.value.sessionToken,
98
+ apiUrl: session.value.appUrl,
99
+ kind: "session"
100
+ });
88
101
  const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
89
102
  if (apiKey) return ok({
90
103
  token: apiKey,
91
- apiUrl,
104
+ apiUrl: process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL,
92
105
  kind: "api-key"
93
106
  });
94
- const sessionToken = process.env["SKYDIVE_SESSION_TOKEN"] ?? store.get("sessionToken");
95
- if (sessionToken) return ok({
96
- token: sessionToken,
97
- apiUrl,
98
- kind: "session"
99
- });
100
107
  return err({ message: "Not authenticated. Run `skydive auth login` (API key) or `skydive auth login --web`." });
101
108
  }
102
109
  function saveConfig(config) {
@@ -154,6 +161,8 @@ function saveTheme(mode, themeId) {
154
161
  //#endregion
155
162
  //#region src/api-client.ts
156
163
  const USER_AGENT = `skydive-cli/${version$1}`;
164
+ /** Largest page `/v1/agents` will return, enforced server-side. */
165
+ const MAX_AGENT_PAGE = 100;
157
166
  const AgentSchema = z.object({
158
167
  id: z.string(),
159
168
  name: z.string(),
@@ -182,20 +191,38 @@ const SetSecretResponseSchema = z.object({ key: z.string() });
182
191
  var SkydiveApiClient = class {
183
192
  baseUrl;
184
193
  token;
194
+ authKind;
185
195
  constructor(config) {
186
196
  this.baseUrl = `${config.apiUrl.replace(/\/$/, "")}/v1`;
187
197
  this.token = config.token;
188
- }
198
+ this.authKind = config.kind;
199
+ }
200
+ /**
201
+ * List agents in the caller's workspace, following the server's cursor
202
+ * until `limit` is satisfied. The endpoint caps one page at
203
+ * {@link MAX_AGENT_PAGE}, so a larger limit takes several requests —
204
+ * callers just ask for what they want. `hasMore` reports whether the
205
+ * roster continues past what was returned.
206
+ */
189
207
  async listAgents(params) {
190
- const query = new URLSearchParams();
191
- if (params.scope) query.set("scope", params.scope);
192
- if (params.limit) query.set("limit", String(params.limit));
193
- if (params.cursor) query.set("cursor", params.cursor);
194
- const qs = query.toString();
195
- return this.request({
196
- method: "GET",
197
- path: `/agents${qs ? `?${qs}` : ""}`,
198
- schema: ListAgentsResponseSchema
208
+ const agents = [];
209
+ let cursor = null;
210
+ do {
211
+ const query = new URLSearchParams({ limit: String(Math.min(MAX_AGENT_PAGE, params.limit - agents.length)) });
212
+ if (params.scope) query.set("scope", params.scope);
213
+ if (cursor) query.set("cursor", cursor);
214
+ const page = await this.request({
215
+ method: "GET",
216
+ path: `/agents?${query.toString()}`,
217
+ schema: ListAgentsResponseSchema
218
+ });
219
+ if (page.isErr()) return err(page.error);
220
+ agents.push(...page.value.agents);
221
+ cursor = page.value.nextCursor;
222
+ } while (cursor && agents.length < params.limit);
223
+ return ok({
224
+ agents,
225
+ hasMore: cursor !== null
199
226
  });
200
227
  }
201
228
  async getAgent(id) {
@@ -282,6 +309,7 @@ var SkydiveApiClient = class {
282
309
  if (body.error && typeof body.error === "string") message = body.error;
283
310
  else if (body.error?.message) message = body.error.message;
284
311
  } catch {}
312
+ if (response.status === 401 && this.authKind === "session") message = `${message} — your chat session may have expired. Run \`skydive auth login --web\`.`;
285
313
  return err({
286
314
  message,
287
315
  status: response.status
@@ -675,8 +703,12 @@ const loginCommand = {
675
703
  const apiUrl = argv["api-url"] ?? DEFAULT_API_URL;
676
704
  const result = await new SkydiveApiClient({
677
705
  token: apiKey,
678
- apiUrl
679
- }).listAgents({ limit: 1 });
706
+ apiUrl,
707
+ kind: "api-key"
708
+ }).listAgents({
709
+ limit: 1,
710
+ scope: null
711
+ });
680
712
  if (result.isErr()) {
681
713
  printError(`Invalid API key or unreachable server: ${result.error.message}`);
682
714
  process.exit(1);
@@ -734,6 +766,7 @@ const statusCommand$1 = {
734
766
  handler: async (argv) => {
735
767
  const apiKey = resolveConfig({ apiUrl: argv["api-url"] });
736
768
  const session = resolveSession({});
769
+ const management = resolveManagementAuth({ apiUrl: argv["api-url"] });
737
770
  const identity = session.isOk() ? (await getSessionIdentity({
738
771
  appUrl: session.value.appUrl,
739
772
  sessionToken: session.value.sessionToken
@@ -751,6 +784,7 @@ const statusCommand$1 = {
751
784
  workspaceId: identity?.activeWorkspaceId ?? null,
752
785
  workspaceName: identity?.activeWorkspaceName ?? null
753
786
  },
787
+ managementAuth: management.isOk() ? management.value.kind : null,
754
788
  configPath: getConfigPath()
755
789
  });
756
790
  return;
@@ -777,6 +811,10 @@ const statusCommand$1 = {
777
811
  console.log(` Space: ${ws}`);
778
812
  }
779
813
  } else console.log("Chat session: not signed in.");
814
+ if (management.isOk()) {
815
+ const via = management.value.kind === "session" ? "chat session (follows `workspace switch`)" : "API key (pinned to the workspace that minted it)";
816
+ console.log(`Management commands use: ${via}`);
817
+ }
780
818
  console.log(`Config: ${getConfigPath()}`);
781
819
  }
782
820
  };
@@ -788,232 +826,656 @@ const authCommand = {
788
826
  };
789
827
 
790
828
  //#endregion
791
- //#region src/commands/agents.ts
792
- function requireClient$2(argv) {
793
- const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
794
- if (result.isErr()) {
795
- printError(result.error.message);
796
- process.exit(1);
797
- }
798
- return new SkydiveApiClient(result.value);
799
- }
800
- const listCommand$4 = {
801
- command: "list",
802
- describe: "List agents",
803
- builder: (y) => y.option("limit", {
804
- type: "number",
805
- default: 20,
806
- describe: "Max results"
807
- }).option("scope", {
808
- type: "string",
809
- choices: ["mine", "org"],
810
- describe: "Filter scope"
811
- }),
812
- handler: async (argv) => {
813
- const result = await requireClient$2(argv).listAgents({
814
- limit: argv.limit,
815
- scope: argv.scope
816
- });
817
- if (result.isErr()) {
818
- printError(result.error.message);
819
- process.exit(1);
820
- }
821
- const { agents } = result.value;
822
- if (argv.json) {
823
- output(argv, agents);
824
- return;
825
- }
826
- if (agents.length === 0) {
827
- console.log("No agents found.");
828
- return;
829
- }
830
- const { headers, rows } = buildAgentTable(agents);
831
- printTable(headers, rows);
829
+ //#region src/chat/api/rest.ts
830
+ var HttpError = class extends Error {
831
+ constructor(status, body) {
832
+ super(`HTTP ${status}: ${body.slice(0, 200)}`);
833
+ this.status = status;
834
+ this.body = body;
835
+ this.name = "HttpError";
832
836
  }
833
837
  };
834
- const DESCRIPTION_MAX = 48;
835
- /**
836
- * Build the `agents list` table. A Description column is added only when at
837
- * least one agent actually has a description, so an all-empty column doesn't
838
- * add noise for accounts that never set them. Descriptions can be long, so
839
- * they are truncated to keep one verbose agent from blowing out the width.
840
- */
841
- function buildAgentTable(agents) {
842
- if (agents.some((a) => (a.description ?? "").trim().length > 0)) return {
843
- headers: [
844
- "Name",
845
- "Description",
846
- "URL",
847
- "Model"
848
- ],
849
- rows: agents.map((a) => [
850
- a.name,
851
- truncate(a.description ?? "", DESCRIPTION_MAX),
852
- a.url ?? "-",
853
- a.model ?? "default"
854
- ])
855
- };
856
- return {
857
- headers: [
858
- "Name",
859
- "URL",
860
- "Model"
861
- ],
862
- rows: agents.map((a) => [
863
- a.name,
864
- a.url ?? "-",
865
- a.model ?? "default"
866
- ])
838
+ const MAX_STREAM_RECONNECTS = 5;
839
+ function createRestClient({ appUrl, sessionToken }) {
840
+ const baseHeaders = {
841
+ authorization: `Bearer ${sessionToken}`,
842
+ accept: "application/json"
867
843
  };
868
- }
869
- function truncate(value, max) {
870
- const trimmed = value.trim();
871
- if (trimmed.length <= max) return trimmed || "-";
872
- return `${trimmed.slice(0, max - 1)}\u2026`;
873
- }
874
- const getCommand$1 = {
875
- command: "get <id>",
876
- describe: "Get agent details",
877
- builder: (y) => y.positional("id", {
878
- type: "string",
879
- demandOption: true,
880
- describe: "Agent ID"
881
- }),
882
- handler: async (argv) => {
883
- const result = await requireClient$2(argv).getAgent(argv.id);
884
- if (result.isErr()) {
885
- printError(result.error.message);
886
- process.exit(1);
887
- }
888
- const agent = result.value;
889
- if (argv.json) {
890
- output(argv, agent);
891
- return;
892
- }
893
- if (argv.quiet) {
894
- console.log(agent.id);
895
- return;
896
- }
897
- console.log(`Name: ${agent.name}`);
898
- console.log(`ID: ${agent.id}`);
899
- console.log(`Model: ${agent.model ?? "default"}`);
900
- if (agent.description) console.log(`Description: ${agent.description}`);
901
- if (agent.url) console.log(`Endpoint: ${agent.url}`);
902
- console.log(`Git: ${agent.gitUrl}`);
844
+ async function get(path, schema) {
845
+ const res = await fetch(`${appUrl}${path}`, { headers: baseHeaders });
846
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
847
+ return schema.parse(await res.json());
903
848
  }
904
- };
905
- const createCommand$1 = {
906
- command: "create",
907
- describe: "Create a new agent",
908
- builder: (y) => y.option("name", {
909
- type: "string",
910
- demandOption: true,
911
- describe: "Agent name"
912
- }).option("model", {
913
- type: "string",
914
- describe: "Model to use"
915
- }),
916
- handler: async (argv) => {
917
- const result = await requireClient$2(argv).createAgent({
918
- name: argv.name,
919
- model: argv.model
849
+ async function post(path, body, schema, method = "POST") {
850
+ const res = await fetch(`${appUrl}${path}`, {
851
+ method,
852
+ headers: {
853
+ ...baseHeaders,
854
+ "content-type": "application/json"
855
+ },
856
+ body: JSON.stringify(body)
920
857
  });
921
- if (result.isErr()) {
922
- printError(result.error.message);
923
- process.exit(1);
924
- }
925
- const agent = result.value;
926
- if (argv.json) {
927
- output(argv, agent);
928
- return;
929
- }
930
- if (argv.quiet) {
931
- console.log(agent.id);
932
- return;
933
- }
934
- console.log(`Agent created.`);
935
- console.log(` Name: ${agent.name}`);
936
- console.log(` ID: ${agent.id}`);
937
- console.log(` Git: ${agent.gitUrl}`);
938
- if (agent.url) console.log(` API: ${agent.url}`);
939
- }
940
- };
941
- const agentsCommand = {
942
- command: "agents",
943
- describe: "Manage agents",
944
- builder: (y) => y.command(listCommand$4).command(getCommand$1).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
945
- handler: () => {}
946
- };
947
-
948
- //#endregion
949
- //#region src/commands/keys.ts
950
- function requireClient$1(argv) {
951
- const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
952
- if (result.isErr()) {
953
- printError(result.error.message);
954
- process.exit(1);
955
- }
956
- return new SkydiveApiClient(result.value);
957
- }
958
- const listCommand$3 = {
959
- command: "list",
960
- describe: "List API keys for an agent",
961
- handler: async (argv) => {
962
- const result = await requireClient$1(argv).listKeys(argv["agent-id"]);
963
- if (result.isErr()) {
964
- printError(result.error.message);
965
- process.exit(1);
966
- }
967
- const keys = result.value;
968
- if (argv.json) {
969
- output(argv, keys);
970
- return;
971
- }
972
- if (keys.length === 0) {
973
- console.log("No API keys found.");
974
- return;
975
- }
976
- printTable([
977
- "Name",
978
- "Prefix",
979
- "Last Used",
980
- "Created"
981
- ], keys.map((k) => [
982
- k.name,
983
- k.prefix,
984
- k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : "Never",
985
- new Date(k.createdAt).toLocaleDateString()
986
- ]));
858
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
859
+ return schema.parse(await res.json());
987
860
  }
988
- };
989
- const createCommand = {
990
- command: "create <name>",
991
- describe: "Create a new API key for an agent",
992
- builder: (y) => y.positional("name", {
993
- type: "string",
994
- demandOption: true,
995
- describe: "Key name"
996
- }),
997
- handler: async (argv) => {
998
- const result = await requireClient$1(argv).createKey(argv["agent-id"], argv.name);
999
- if (result.isErr()) {
1000
- printError(result.error.message);
1001
- process.exit(1);
1002
- }
1003
- const key = result.value;
1004
- if (argv.json) {
1005
- output(argv, key);
1006
- return;
1007
- }
1008
- if (argv.quiet) {
1009
- console.log(key.key);
1010
- return;
1011
- }
1012
- console.log(`API key created.`);
1013
- console.log(` Name: ${key.name}`);
1014
- console.log(` Key: ${key.key}`);
1015
- console.log("");
1016
- console.log(" Save this key it will not be shown again.");
861
+ async function del(path) {
862
+ const res = await fetch(`${appUrl}${path}`, {
863
+ method: "DELETE",
864
+ headers: baseHeaders
865
+ });
866
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
867
+ }
868
+ const streamEvents = async ({ path, label, signal, onEvent }) => {
869
+ let lastEventId = null;
870
+ let finished = false;
871
+ let reconnects = 0;
872
+ for (;;) {
873
+ if (signal.aborted) return;
874
+ try {
875
+ const headers = {
876
+ authorization: `Bearer ${sessionToken}`,
877
+ accept: "text/event-stream"
878
+ };
879
+ if (lastEventId) headers["last-event-id"] = lastEventId;
880
+ const res = await fetch(`${appUrl}${path}`, {
881
+ headers,
882
+ signal
883
+ });
884
+ if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
885
+ reconnects = 0;
886
+ const parser = createParser({ onEvent: (message) => {
887
+ if (message.id) lastEventId = message.id;
888
+ if (message.event === "error") {
889
+ const { error } = streamErrorSchema.parse(JSON.parse(message.data));
890
+ throw new Error(error);
891
+ }
892
+ const event = runStreamEventSchema.parse(JSON.parse(message.data));
893
+ if (event.kind === "finished") finished = true;
894
+ onEvent(event.kind === "finished" ? {
895
+ ...event,
896
+ error: event.error ?? null
897
+ } : event);
898
+ } });
899
+ const decoder = new TextDecoder();
900
+ const reader = res.body.getReader();
901
+ try {
902
+ for (;;) {
903
+ const { done, value } = await reader.read();
904
+ if (done) break;
905
+ parser.feed(decoder.decode(value, { stream: true }));
906
+ if (finished) return;
907
+ }
908
+ } finally {
909
+ try {
910
+ await reader.cancel();
911
+ } catch (_error) {}
912
+ }
913
+ } catch (err) {
914
+ if (signal.aborted) return;
915
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
916
+ reconnects += 1;
917
+ if (reconnects > MAX_STREAM_RECONNECTS) throw err;
918
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
919
+ continue;
920
+ }
921
+ if (finished) return;
922
+ reconnects += 1;
923
+ if (reconnects > MAX_STREAM_RECONNECTS) throw new Error(`${label} stream ended unexpectedly`);
924
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
925
+ }
926
+ };
927
+ return {
928
+ listAgents: async ({ scope, onPage }) => {
929
+ const all = [];
930
+ let cursor;
931
+ const maxAgents = 2e3;
932
+ do {
933
+ const params = new URLSearchParams({
934
+ limit: "100",
935
+ scope,
936
+ sort: "mine_first_usage",
937
+ includeStats: "false"
938
+ });
939
+ if (cursor) params.set("cursor", cursor);
940
+ const page = await get(`/api/v1/agents?${params.toString()}`, listAgentsResponseSchema);
941
+ all.push(...page.agents);
942
+ cursor = page.nextCursor ?? void 0;
943
+ onPage?.([...all]);
944
+ } while (cursor && all.length < maxAgents);
945
+ return all;
946
+ },
947
+ createAgent: async ({ name }) => {
948
+ const { agent } = await post("/api/v1/agents", { name }, createAgentResponseSchema);
949
+ return agent;
950
+ },
951
+ getConversation: async ({ conversationId }) => {
952
+ const { conversation } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}`, getConversationResponseSchema);
953
+ return conversation;
954
+ },
955
+ listModels: async () => {
956
+ const { models } = await get("/api/v1/models", listModelsResponseSchema);
957
+ return models;
958
+ },
959
+ updateAgentModel: async ({ agentId, model }) => {
960
+ const { agent } = await post(`/api/v1/agents/${encodeURIComponent(agentId)}`, { model }, updateAgentResponseSchema, "PATCH");
961
+ return { model: agent.model ?? null };
962
+ },
963
+ listConversations: async ({ agentId, limit }) => {
964
+ const all = [];
965
+ const maxConversations = limit ?? 5e3;
966
+ let cursor;
967
+ do {
968
+ const remaining = maxConversations - all.length;
969
+ const params = new URLSearchParams({
970
+ agentId,
971
+ includeTotal: "false"
972
+ });
973
+ params.set("limit", String(Math.min(remaining, 100)));
974
+ if (cursor) params.set("cursor", cursor);
975
+ const page = await get(`/api/v1/conversations?${params.toString()}`, listConversationsResponseSchema);
976
+ all.push(...page.conversations);
977
+ cursor = page.nextCursor ?? void 0;
978
+ } while (cursor && all.length < maxConversations);
979
+ return limit ? all.slice(0, limit) : all;
980
+ },
981
+ listMessages: async ({ conversationId }) => {
982
+ const { messages } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`, listMessagesResponseSchema);
983
+ return messages;
984
+ },
985
+ getRecap: async ({ conversationId }) => {
986
+ const { recap } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/recap`, recapResponseSchema);
987
+ return recap?.text ?? null;
988
+ },
989
+ uploadAttachment: async ({ agentId, fileName, mediaType, data }) => {
990
+ const size = data.byteLength;
991
+ const presign = await post("/api/v1/attachments/presign", {
992
+ agentId,
993
+ fileName,
994
+ mediaType,
995
+ size
996
+ }, presignResponseSchema);
997
+ const putRes = await fetch(presign.uploadUrl, {
998
+ method: "PUT",
999
+ headers: { "content-type": mediaType },
1000
+ body: new Uint8Array(data)
1001
+ });
1002
+ if (!putRes.ok) throw new HttpError(putRes.status, await putRes.text().catch(() => ""));
1003
+ const finalized = await post(`/api/v1/attachments/${encodeURIComponent(presign.id)}/finalize`, {
1004
+ agentId,
1005
+ fileName: presign.fileName,
1006
+ mediaType: presign.mediaType,
1007
+ size
1008
+ }, finalizeResponseSchema);
1009
+ return {
1010
+ id: presign.id,
1011
+ fileName: finalized.fileName,
1012
+ mediaType: finalized.mediaType,
1013
+ sizeBytes: finalized.sizeBytes ?? size
1014
+ };
1015
+ },
1016
+ deleteConversation: async ({ conversationId }) => {
1017
+ await del(`/api/v1/conversations/${encodeURIComponent(conversationId)}`);
1018
+ },
1019
+ sendMessage: async ({ clientSurface, ...input }) => post("/api/v1/chat/send", {
1020
+ ...input,
1021
+ clientSurface
1022
+ }, sendResultSchema),
1023
+ activeRun: async ({ conversationId }) => {
1024
+ const { run } = await get(`/api/v1/chat/active-run?${new URLSearchParams({ conversationId }).toString()}`, activeRunResponseSchema);
1025
+ return run;
1026
+ },
1027
+ cancelRun: async ({ runId }) => {
1028
+ await post(`/api/v1/chat/runs/${encodeURIComponent(runId)}/cancel`, {}, z.object({ ok: z.boolean() }));
1029
+ },
1030
+ cancelSteer: async ({ directiveId }) => {
1031
+ await post(`/api/v1/chat/steer/${encodeURIComponent(directiveId)}/cancel`, {}, z.object({ ok: z.boolean() }));
1032
+ },
1033
+ oauthConnect: async (input) => {
1034
+ const { connectLink } = await post("/api/v1/oauth/connect", input, oauthConnectResponseSchema);
1035
+ return { connectLink };
1036
+ },
1037
+ externalOauthConnect: async (input) => {
1038
+ const { authorizationUrl } = await post("/api/v1/external-oauth/connect", input, externalOauthConnectResponseSchema);
1039
+ return { authorizationUrl: authorizationUrl ?? null };
1040
+ },
1041
+ fulfillCredential: async ({ url, body }) => {
1042
+ const target = new URL(url, appUrl).toString();
1043
+ const res = await fetch(target, {
1044
+ method: "POST",
1045
+ headers: {
1046
+ ...baseHeaders,
1047
+ "content-type": "application/json"
1048
+ },
1049
+ body: JSON.stringify(body)
1050
+ });
1051
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
1052
+ },
1053
+ streamRun: async ({ runId, signal, onEvent }) => streamEvents({
1054
+ path: `/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`,
1055
+ label: "run",
1056
+ signal,
1057
+ onEvent
1058
+ }),
1059
+ streamMessage: async ({ messageId, signal, onEvent }) => streamEvents({
1060
+ path: `/api/v1/chat/messages/${encodeURIComponent(messageId)}/stream`,
1061
+ label: "message",
1062
+ signal,
1063
+ onEvent
1064
+ }),
1065
+ streamConversation: async ({ conversationId, signal, onEvent }) => {
1066
+ let reconnects = 0;
1067
+ for (;;) {
1068
+ if (signal.aborted) return;
1069
+ try {
1070
+ const res = await fetch(`${appUrl}/api/v1/chat/conversations/${encodeURIComponent(conversationId)}/stream`, {
1071
+ headers: {
1072
+ authorization: `Bearer ${sessionToken}`,
1073
+ accept: "text/event-stream"
1074
+ },
1075
+ signal
1076
+ });
1077
+ if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
1078
+ reconnects = 0;
1079
+ const parser = createParser({ onEvent: (message) => {
1080
+ const parsed = conversationStreamEventSchema.safeParse(JSON.parse(message.data));
1081
+ if (parsed.success) onEvent(parsed.data);
1082
+ } });
1083
+ const decoder = new TextDecoder();
1084
+ const reader = res.body.getReader();
1085
+ try {
1086
+ for (;;) {
1087
+ const { done, value } = await reader.read();
1088
+ if (done) break;
1089
+ parser.feed(decoder.decode(value, { stream: true }));
1090
+ }
1091
+ } finally {
1092
+ try {
1093
+ await reader.cancel();
1094
+ } catch (_error) {}
1095
+ }
1096
+ } catch (err) {
1097
+ if (signal.aborted) return;
1098
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
1099
+ reconnects += 1;
1100
+ if (reconnects > MAX_STREAM_RECONNECTS) throw err;
1101
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
1102
+ continue;
1103
+ }
1104
+ if (signal.aborted) return;
1105
+ reconnects += 1;
1106
+ if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("conversation stream ended unexpectedly");
1107
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
1108
+ }
1109
+ }
1110
+ };
1111
+ }
1112
+ function sleep(ms) {
1113
+ return new Promise((resolve) => setTimeout(resolve, ms));
1114
+ }
1115
+ const agentSummarySchema = z.object({
1116
+ id: z.string().uuid(),
1117
+ name: z.string(),
1118
+ slug: z.string().nullable().optional(),
1119
+ title: z.string().nullable().optional(),
1120
+ description: z.string().nullable().optional(),
1121
+ createdAt: z.string(),
1122
+ creatorName: z.string().nullable().optional(),
1123
+ model: z.string().nullable().optional(),
1124
+ modelLocked: z.boolean().optional()
1125
+ });
1126
+ const platformModelSchema = z.object({
1127
+ id: z.string(),
1128
+ displayName: z.string(),
1129
+ providerDisplay: z.string().optional(),
1130
+ reasoning: z.boolean().optional(),
1131
+ compliant: z.boolean().optional()
1132
+ }).passthrough();
1133
+ const listModelsResponseSchema = z.object({ models: z.array(platformModelSchema) });
1134
+ const updateAgentResponseSchema = z.object({ agent: z.object({ model: z.string().nullable().optional() }).passthrough() });
1135
+ const listAgentsResponseSchema = z.object({
1136
+ agents: z.array(agentSummarySchema),
1137
+ nextCursor: z.string().nullable().optional(),
1138
+ totalCount: z.number().nullable().optional()
1139
+ });
1140
+ const createAgentResponseSchema = z.object({ agent: agentSummarySchema });
1141
+ const conversationSummarySchema = z.object({
1142
+ id: z.string().uuid(),
1143
+ title: z.string().nullable(),
1144
+ createdAt: z.string(),
1145
+ updatedAt: z.string(),
1146
+ preview: z.string().nullable(),
1147
+ channel: z.string().nullable(),
1148
+ channelLabel: z.string().nullable(),
1149
+ agent: z.object({
1150
+ id: z.string().uuid(),
1151
+ name: z.string(),
1152
+ slug: z.string().nullable().optional(),
1153
+ title: z.string().nullable().optional()
1154
+ })
1155
+ });
1156
+ const conversationTitleSchema = z.object({
1157
+ id: z.string().uuid(),
1158
+ title: z.string().nullable()
1159
+ });
1160
+ const getConversationResponseSchema = z.object({ conversation: conversationTitleSchema });
1161
+ const listConversationsResponseSchema = z.object({
1162
+ conversations: z.array(conversationSummarySchema),
1163
+ nextCursor: z.string().nullable().optional(),
1164
+ totalCount: z.number().optional()
1165
+ });
1166
+ const uiMessagePartSchema = z.union([
1167
+ z.object({
1168
+ type: z.literal("text"),
1169
+ text: z.string()
1170
+ }),
1171
+ z.object({
1172
+ type: z.literal("reasoning"),
1173
+ text: z.string().optional()
1174
+ }),
1175
+ z.object({
1176
+ type: z.literal("dynamic-tool"),
1177
+ toolCallId: z.string(),
1178
+ toolName: z.string(),
1179
+ input: z.unknown().optional(),
1180
+ output: z.unknown().optional(),
1181
+ state: z.string().optional(),
1182
+ errorText: z.string().optional()
1183
+ }),
1184
+ z.object({ type: z.string() }).passthrough()
1185
+ ]);
1186
+ const uiMessageSchema = z.object({
1187
+ id: z.string(),
1188
+ role: z.string(),
1189
+ parts: z.array(uiMessagePartSchema)
1190
+ });
1191
+ const recapResponseSchema = z.object({ recap: z.object({ text: z.string() }).nullable() });
1192
+ const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
1193
+ const sendResultSchema = z.object({
1194
+ runId: z.string(),
1195
+ messageId: z.string().uuid().nullish(),
1196
+ conversationId: z.string().uuid(),
1197
+ isNewConversation: z.boolean(),
1198
+ steered: z.boolean().optional(),
1199
+ directive: z.object({ id: z.string() }).passthrough().optional()
1200
+ });
1201
+ const presignResponseSchema = z.object({
1202
+ id: z.string(),
1203
+ uploadUrl: z.string(),
1204
+ fileName: z.string(),
1205
+ mediaType: z.string()
1206
+ });
1207
+ const finalizeResponseSchema = z.object({
1208
+ fileName: z.string(),
1209
+ mediaType: z.string(),
1210
+ sizeBytes: z.number().nullable().optional()
1211
+ });
1212
+ const activeRunResponseSchema = z.object({ run: z.object({ runId: z.string() }).nullable() });
1213
+ const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
1214
+ const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
1215
+ const runStreamEventSchema = z.union([z.object({
1216
+ kind: z.literal("chunk"),
1217
+ chunk: z.record(z.unknown())
1218
+ }), z.object({
1219
+ kind: z.literal("finished"),
1220
+ status: z.string(),
1221
+ error: z.string().nullish()
1222
+ })]);
1223
+ const streamErrorSchema = z.object({ error: z.string() });
1224
+ const conversationStreamEventSchema = z.object({
1225
+ kind: z.literal("conversation"),
1226
+ id: z.string(),
1227
+ title: z.string().nullable()
1228
+ });
1229
+
1230
+ //#endregion
1231
+ //#region src/commands/session.ts
1232
+ /**
1233
+ * Resolve the signed-in chat session or exit with a friendly hint. Shared by
1234
+ * every command that talks to the authenticated REST API so the
1235
+ * resolve-or-exit block isn't copy-pasted per command.
1236
+ */
1237
+ function requireSession(argv) {
1238
+ const session = resolveSession({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
1239
+ if (session.isErr()) {
1240
+ printError(`${session.error.message} Run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
1241
+ process.exit(1);
1242
+ }
1243
+ return session.value;
1244
+ }
1245
+ /**
1246
+ * Resolve the management-API credential (see
1247
+ * {@link resolveManagementAuth}) or exit with a friendly hint, then build the
1248
+ * `/v1` client. Shared by `agents`, `keys` and `secrets`.
1249
+ */
1250
+ function requireManagementClient(argv) {
1251
+ const auth = resolveManagementAuth({ apiUrl: argv["api-url"] });
1252
+ if (auth.isErr()) {
1253
+ printError(auth.error.message);
1254
+ process.exit(1);
1255
+ }
1256
+ return new SkydiveApiClient(auth.value);
1257
+ }
1258
+ /** Resolve the session (see {@link requireSession}) and build a REST client. */
1259
+ function requireRestClient(argv) {
1260
+ const session = requireSession(argv);
1261
+ return createRestClient({
1262
+ appUrl: session.appUrl,
1263
+ sessionToken: session.sessionToken
1264
+ });
1265
+ }
1266
+
1267
+ //#endregion
1268
+ //#region src/commands/agents.ts
1269
+ const listCommand$4 = {
1270
+ command: "list",
1271
+ describe: "List agents in the active workspace",
1272
+ builder: (y) => y.option("limit", {
1273
+ type: "number",
1274
+ default: 20,
1275
+ describe: "Max results"
1276
+ }).option("scope", {
1277
+ type: "string",
1278
+ choices: ["mine", "org"],
1279
+ describe: "Filter scope"
1280
+ }),
1281
+ handler: async (argv) => {
1282
+ const result = await requireManagementClient(argv).listAgents({
1283
+ limit: argv.limit,
1284
+ scope: argv.scope ?? null
1285
+ });
1286
+ if (result.isErr()) {
1287
+ printError(result.error.message);
1288
+ process.exit(1);
1289
+ }
1290
+ const { agents, hasMore } = result.value;
1291
+ if (argv.json) {
1292
+ output(argv, agents);
1293
+ return;
1294
+ }
1295
+ if (agents.length === 0) {
1296
+ console.log("No agents found.");
1297
+ return;
1298
+ }
1299
+ const { headers, rows } = buildAgentTable(agents);
1300
+ printTable(headers, rows);
1301
+ if (hasMore && !argv.quiet) console.log(`\nShowing the ${agents.length} newest. Raise --limit to see more.`);
1302
+ }
1303
+ };
1304
+ const DESCRIPTION_MAX = 48;
1305
+ /**
1306
+ * Build the `agents list` table. A Description column is added only when at
1307
+ * least one agent actually has a description, so an all-empty column doesn't
1308
+ * add noise for accounts that never set them. Descriptions can be long, so
1309
+ * they are truncated to keep one verbose agent from blowing out the width.
1310
+ */
1311
+ function buildAgentTable(agents) {
1312
+ if (agents.some((a) => (a.description ?? "").trim().length > 0)) return {
1313
+ headers: [
1314
+ "Name",
1315
+ "Description",
1316
+ "URL",
1317
+ "Model"
1318
+ ],
1319
+ rows: agents.map((a) => [
1320
+ a.name,
1321
+ truncate(a.description ?? "", DESCRIPTION_MAX),
1322
+ a.url ?? "-",
1323
+ a.model ?? "default"
1324
+ ])
1325
+ };
1326
+ return {
1327
+ headers: [
1328
+ "Name",
1329
+ "URL",
1330
+ "Model"
1331
+ ],
1332
+ rows: agents.map((a) => [
1333
+ a.name,
1334
+ a.url ?? "-",
1335
+ a.model ?? "default"
1336
+ ])
1337
+ };
1338
+ }
1339
+ function truncate(value, max) {
1340
+ const trimmed = value.trim();
1341
+ if (trimmed.length <= max) return trimmed || "-";
1342
+ return `${trimmed.slice(0, max - 1)}\u2026`;
1343
+ }
1344
+ const getCommand$1 = {
1345
+ command: "get <id>",
1346
+ describe: "Get agent details",
1347
+ builder: (y) => y.positional("id", {
1348
+ type: "string",
1349
+ demandOption: true,
1350
+ describe: "Agent ID"
1351
+ }),
1352
+ handler: async (argv) => {
1353
+ const result = await requireManagementClient(argv).getAgent(argv.id);
1354
+ if (result.isErr()) {
1355
+ printError(result.error.message);
1356
+ process.exit(1);
1357
+ }
1358
+ const agent = result.value;
1359
+ if (argv.json) {
1360
+ output(argv, agent);
1361
+ return;
1362
+ }
1363
+ if (argv.quiet) {
1364
+ console.log(agent.id);
1365
+ return;
1366
+ }
1367
+ console.log(`Name: ${agent.name}`);
1368
+ console.log(`ID: ${agent.id}`);
1369
+ console.log(`Model: ${agent.model ?? "default"}`);
1370
+ if (agent.description) console.log(`Description: ${agent.description}`);
1371
+ if (agent.url) console.log(`Endpoint: ${agent.url}`);
1372
+ console.log(`Git: ${agent.gitUrl}`);
1373
+ }
1374
+ };
1375
+ const createCommand$1 = {
1376
+ command: "create",
1377
+ describe: "Create a new agent",
1378
+ builder: (y) => y.option("name", {
1379
+ type: "string",
1380
+ demandOption: true,
1381
+ describe: "Agent name"
1382
+ }).option("model", {
1383
+ type: "string",
1384
+ describe: "Model to use"
1385
+ }),
1386
+ handler: async (argv) => {
1387
+ const result = await requireManagementClient(argv).createAgent({
1388
+ name: argv.name,
1389
+ model: argv.model
1390
+ });
1391
+ if (result.isErr()) {
1392
+ printError(result.error.message);
1393
+ process.exit(1);
1394
+ }
1395
+ const agent = result.value;
1396
+ if (argv.json) {
1397
+ output(argv, agent);
1398
+ return;
1399
+ }
1400
+ if (argv.quiet) {
1401
+ console.log(agent.id);
1402
+ return;
1403
+ }
1404
+ console.log(`Agent created.`);
1405
+ console.log(` Name: ${agent.name}`);
1406
+ console.log(` ID: ${agent.id}`);
1407
+ console.log(` Git: ${agent.gitUrl}`);
1408
+ if (agent.url) console.log(` API: ${agent.url}`);
1409
+ }
1410
+ };
1411
+ const agentsCommand = {
1412
+ command: "agents",
1413
+ describe: "Manage agents",
1414
+ builder: (y) => y.command(listCommand$4).command(getCommand$1).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
1415
+ handler: () => {}
1416
+ };
1417
+
1418
+ //#endregion
1419
+ //#region src/commands/keys.ts
1420
+ const listCommand$3 = {
1421
+ command: "list",
1422
+ describe: "List API keys for an agent",
1423
+ handler: async (argv) => {
1424
+ const result = await requireManagementClient(argv).listKeys(argv["agent-id"]);
1425
+ if (result.isErr()) {
1426
+ printError(result.error.message);
1427
+ process.exit(1);
1428
+ }
1429
+ const keys = result.value;
1430
+ if (argv.json) {
1431
+ output(argv, keys);
1432
+ return;
1433
+ }
1434
+ if (keys.length === 0) {
1435
+ console.log("No API keys found.");
1436
+ return;
1437
+ }
1438
+ printTable([
1439
+ "Name",
1440
+ "Prefix",
1441
+ "Last Used",
1442
+ "Created"
1443
+ ], keys.map((k) => [
1444
+ k.name,
1445
+ k.prefix,
1446
+ k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : "Never",
1447
+ new Date(k.createdAt).toLocaleDateString()
1448
+ ]));
1449
+ }
1450
+ };
1451
+ const createCommand = {
1452
+ command: "create <name>",
1453
+ describe: "Create a new API key for an agent",
1454
+ builder: (y) => y.positional("name", {
1455
+ type: "string",
1456
+ demandOption: true,
1457
+ describe: "Key name"
1458
+ }),
1459
+ handler: async (argv) => {
1460
+ const result = await requireManagementClient(argv).createKey(argv["agent-id"], argv.name);
1461
+ if (result.isErr()) {
1462
+ printError(result.error.message);
1463
+ process.exit(1);
1464
+ }
1465
+ const key = result.value;
1466
+ if (argv.json) {
1467
+ output(argv, key);
1468
+ return;
1469
+ }
1470
+ if (argv.quiet) {
1471
+ console.log(key.key);
1472
+ return;
1473
+ }
1474
+ console.log(`API key created.`);
1475
+ console.log(` Name: ${key.name}`);
1476
+ console.log(` Key: ${key.key}`);
1477
+ console.log("");
1478
+ console.log(" Save this key — it will not be shown again.");
1017
1479
  }
1018
1480
  };
1019
1481
  const revokeCommand$1 = {
@@ -1025,7 +1487,7 @@ const revokeCommand$1 = {
1025
1487
  describe: "Key ID"
1026
1488
  }),
1027
1489
  handler: async (argv) => {
1028
- const result = await requireClient$1(argv).revokeKey(argv["agent-id"], argv.id);
1490
+ const result = await requireManagementClient(argv).revokeKey(argv["agent-id"], argv.id);
1029
1491
  if (result.isErr()) {
1030
1492
  printError(result.error.message);
1031
1493
  process.exit(1);
@@ -1050,14 +1512,6 @@ const keysCommand = {
1050
1512
 
1051
1513
  //#endregion
1052
1514
  //#region src/commands/secrets.ts
1053
- function requireClient(argv) {
1054
- const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
1055
- if (result.isErr()) {
1056
- printError(result.error.message);
1057
- process.exit(1);
1058
- }
1059
- return new SkydiveApiClient(result.value);
1060
- }
1061
1515
  /** Read all of stdin as UTF-8, trimming a single trailing newline. */
1062
1516
  async function readStdin$1() {
1063
1517
  const chunks = [];
@@ -1068,7 +1522,7 @@ const listCommand$2 = {
1068
1522
  command: "list",
1069
1523
  describe: "List secret names for an agent (values are never shown)",
1070
1524
  handler: async (argv) => {
1071
- const result = await requireClient(argv).listSecrets(argv["agent-id"]);
1525
+ const result = await requireManagementClient(argv).listSecrets(argv["agent-id"]);
1072
1526
  if (result.isErr()) {
1073
1527
  printError(result.error.message);
1074
1528
  process.exit(1);
@@ -1109,7 +1563,7 @@ const setCommand = {
1109
1563
  printError("Empty secret value.");
1110
1564
  process.exit(1);
1111
1565
  }
1112
- const result = await requireClient(argv).setSecret(argv["agent-id"], argv.key, value);
1566
+ const result = await requireManagementClient(argv).setSecret(argv["agent-id"], argv.key, value);
1113
1567
  if (result.isErr()) {
1114
1568
  printError(result.error.message);
1115
1569
  process.exit(1);
@@ -1131,7 +1585,7 @@ const rmCommand = {
1131
1585
  describe: "Secret name"
1132
1586
  }),
1133
1587
  handler: async (argv) => {
1134
- const result = await requireClient(argv).deleteSecret(argv["agent-id"], argv.key);
1588
+ const result = await requireManagementClient(argv).deleteSecret(argv["agent-id"], argv.key);
1135
1589
  if (result.isErr()) {
1136
1590
  printError(result.error.message);
1137
1591
  process.exit(1);
@@ -1885,786 +2339,384 @@ function binDir() {
1885
2339
  }
1886
2340
  /** Absolute path to the version-pinned cached Bun (may not exist yet). */
1887
2341
  function cachedBunPath(version = PINNED_BUN_VERSION) {
1888
- const exe = process.platform === "win32" ? "bun.exe" : "bun";
1889
- return path.join(binDir(), `bun-${version}`, exe);
1890
- }
1891
- function isExecutableFile(p) {
1892
- try {
1893
- if (!fs.statSync(p).isFile()) return false;
1894
- if (process.platform !== "win32") fs.accessSync(p, fs.constants.X_OK);
1895
- return true;
1896
- } catch {
1897
- return false;
1898
- }
1899
- }
1900
- /**
1901
- * Locate a usable Bun without downloading. Order:
1902
- * 1. `SKYDIVE_BUN_PATH` (explicit override / air-gapped installs)
1903
- * 2. the version-pinned cache this CLI manages
1904
- * 3. `bun` already on PATH
1905
- * Returns null if none is usable.
1906
- */
1907
- function findExistingBun() {
1908
- const override = process.env["SKYDIVE_BUN_PATH"];
1909
- if (override && isExecutableFile(override)) return override;
1910
- const cached = cachedBunPath();
1911
- if (isExecutableFile(cached)) return cached;
1912
- const onPath = resolveBunOnPath();
1913
- if (onPath) return onPath;
1914
- return null;
1915
- }
1916
- /** `which`/`where` for bun, without spawning a shell. */
1917
- function resolveBunOnPath() {
1918
- const pathVar = process.env["PATH"] ?? "";
1919
- if (!pathVar) return null;
1920
- const exts = process.platform === "win32" ? (process.env["PATHEXT"] ?? ".EXE").split(";") : [""];
1921
- for (const dir of pathVar.split(path.delimiter)) {
1922
- if (!dir) continue;
1923
- for (const ext of exts) {
1924
- const candidate = path.join(dir, `bun${ext}`);
1925
- if (isExecutableFile(candidate)) return candidate;
1926
- }
1927
- }
1928
- return null;
1929
- }
1930
- async function fetchOk(url) {
1931
- const res = await fetch(url, {
1932
- headers: { "user-agent": "skydive-cli" },
1933
- redirect: "follow"
1934
- });
1935
- if (!res.ok) throw new Error(`GET ${url} -> ${res.status} ${res.statusText}`);
1936
- return res;
1937
- }
1938
- /**
1939
- * Look up the published SHA-256 for `assetName` in the pinned release's
1940
- * `SHASUMS256.txt`. Verifying against the release's own manifest keeps
1941
- * integrity checking correct across version bumps without hardcoding a hash
1942
- * per platform.
1943
- */
1944
- async function expectedSha256(version, assetName) {
1945
- const url = `${BUN_RELEASE_BASE}/bun-v${version}/SHASUMS256.txt`;
1946
- const text = await (await fetchOk(url)).text();
1947
- for (const line of text.split("\n")) {
1948
- const [sum, name] = line.trim().split(/\s+/);
1949
- if (name === assetName && sum) return sum.toLowerCase();
1950
- }
1951
- throw new Error(`${assetName} not found in ${url}`);
1952
- }
1953
- function sha256(buf) {
1954
- return createHash("sha256").update(buf).digest("hex");
1955
- }
1956
- /**
1957
- * Extract the single `bun` executable from a Bun release zip buffer. The zip
1958
- * lays the binary out as `<target>/bun` (or `bun.exe` on Windows). Implemented
1959
- * without a zip dependency by parsing the central directory — Bun's release zips
1960
- * use a streaming data descriptor, so the *local* file header carries zeroed
1961
- * sizes; the central directory is the only place with correct sizes/offsets.
1962
- */
1963
- function extractBunFromZip(zip, target) {
1964
- const exeName = process.platform === "win32" ? "bun.exe" : "bun";
1965
- let eocd = -1;
1966
- for (let i = zip.length - 22; i >= 0; i--) if (zip.readUInt32LE(i) === 101010256) {
1967
- eocd = i;
1968
- break;
1969
- }
1970
- if (eocd < 0) throw new Error("release zip has no end-of-central-directory");
1971
- const entryCount = zip.readUInt16LE(eocd + 10);
1972
- let off = zip.readUInt32LE(eocd + 16);
1973
- for (let n = 0; n < entryCount; n++) {
1974
- if (zip.readUInt32LE(off) !== 33639248) throw new Error("malformed central directory in release zip");
1975
- const method = zip.readUInt16LE(off + 10);
1976
- const compSize = zip.readUInt32LE(off + 20);
1977
- const nameLen = zip.readUInt16LE(off + 28);
1978
- const extraLen = zip.readUInt16LE(off + 30);
1979
- const commentLen = zip.readUInt16LE(off + 32);
1980
- const localHeaderOffset = zip.readUInt32LE(off + 42);
1981
- const name = zip.toString("utf8", off + 46, off + 46 + nameLen);
1982
- if (name === `${target}/${exeName}` || name === exeName) {
1983
- const lNameLen = zip.readUInt16LE(localHeaderOffset + 26);
1984
- const lExtraLen = zip.readUInt16LE(localHeaderOffset + 28);
1985
- const dataStart = localHeaderOffset + 30 + lNameLen + lExtraLen;
1986
- const data = zip.subarray(dataStart, dataStart + compSize);
1987
- if (method === 0) return Buffer.from(data);
1988
- if (method === 8) return zlib.inflateRawSync(data);
1989
- throw new Error(`unsupported zip compression method ${method}`);
1990
- }
1991
- off += 46 + nameLen + extraLen + commentLen;
1992
- }
1993
- throw new Error(`bun binary not found in release zip for ${target}`);
2342
+ const exe = process.platform === "win32" ? "bun.exe" : "bun";
2343
+ return path.join(binDir(), `bun-${version}`, exe);
1994
2344
  }
1995
- /**
1996
- * Download, verify, and cache the pinned Bun for this platform. Returns the
1997
- * path to the cached executable, or null if Bun can't be provisioned (no
1998
- * network, unsupported platform, checksum mismatch) — the caller then prints
1999
- * the manual-install guidance. Never throws for the expected failure modes.
2000
- */
2001
- async function downloadBun(onProgress) {
2002
- const target = bunAssetTarget();
2003
- if (!target) return null;
2004
- const assetName = `${target}.zip`;
2005
- const dest = cachedBunPath();
2345
+ function isExecutableFile(p) {
2006
2346
  try {
2007
- onProgress?.(`Fetching Bun v${PINNED_BUN_VERSION} (one-time setup)…`);
2008
- const url = `${BUN_RELEASE_BASE}/bun-v${PINNED_BUN_VERSION}/${assetName}`;
2009
- const [zipRes, want] = await Promise.all([fetchOk(url), expectedSha256(PINNED_BUN_VERSION, assetName)]);
2010
- const zip = Buffer.from(await zipRes.arrayBuffer());
2011
- const got = sha256(zip);
2012
- if (got !== want) {
2013
- onProgress?.(`Bun download failed integrity check (expected ${want}, got ${got}).`);
2014
- return null;
2015
- }
2016
- const bin = extractBunFromZip(zip, target);
2017
- fs.mkdirSync(path.dirname(dest), { recursive: true });
2018
- const tmp = path.join(path.dirname(dest), `.bun.tmp-${process.pid}-${Date.now()}`);
2019
- fs.writeFileSync(tmp, bin, { mode: 493 });
2020
- if (process.platform !== "win32") fs.chmodSync(tmp, 493);
2021
- fs.renameSync(tmp, dest);
2022
- return dest;
2023
- } catch (e) {
2024
- onProgress?.(`Could not download Bun automatically: ${e instanceof Error ? e.message : String(e)}`);
2025
- return null;
2347
+ if (!fs.statSync(p).isFile()) return false;
2348
+ if (process.platform !== "win32") fs.accessSync(p, fs.constants.X_OK);
2349
+ return true;
2350
+ } catch {
2351
+ return false;
2026
2352
  }
2027
2353
  }
2028
2354
  /**
2029
- * Ensure a usable Bun exists: already running under Bun, found on disk/PATH, or
2030
- * freshly downloaded and cached. Pure resolution does not re-exec.
2355
+ * Locate a usable Bun without downloading. Order:
2356
+ * 1. `SKYDIVE_BUN_PATH` (explicit override / air-gapped installs)
2357
+ * 2. the version-pinned cache this CLI manages
2358
+ * 3. `bun` already on PATH
2359
+ * Returns null if none is usable.
2031
2360
  */
2032
- async function resolveBun(onProgress) {
2033
- if (isBun()) return { kind: "already-bun" };
2034
- const existing = findExistingBun();
2035
- if (existing) return {
2036
- kind: "found",
2037
- bunPath: existing
2038
- };
2039
- const downloaded = await downloadBun(onProgress);
2040
- if (downloaded) return {
2041
- kind: "found",
2042
- bunPath: downloaded
2043
- };
2044
- return { kind: "unavailable" };
2361
+ function findExistingBun() {
2362
+ const override = process.env["SKYDIVE_BUN_PATH"];
2363
+ if (override && isExecutableFile(override)) return override;
2364
+ const cached = cachedBunPath();
2365
+ if (isExecutableFile(cached)) return cached;
2366
+ const onPath = resolveBunOnPath();
2367
+ if (onPath) return onPath;
2368
+ return null;
2045
2369
  }
2046
- /**
2047
- * The heart of the transparent-Bun story. Called by `chat` before it touches
2048
- * OpenTUI:
2049
- * - Under Bun already, or if a re-exec guard is set: return 'proceed'.
2050
- * - Otherwise resolve/provision Bun and re-exec this exact CLI invocation
2051
- * under it (inheriting stdio + argv), then exit with the child's code.
2052
- * - If Bun can't be provisioned: return 'unavailable' so the caller prints
2053
- * the existing manual-install message.
2054
- *
2055
- * Returns 'proceed' only when it's safe to load OpenTUI in this process.
2056
- */
2057
- function ensureBunAndReexec(onProgress) {
2058
- if (isBun() || process.env[REEXEC_GUARD] === "1") return Promise.resolve("proceed");
2059
- return resolveBun(onProgress).then((resolution) => {
2060
- if (resolution.kind === "already-bun") return "proceed";
2061
- if (resolution.kind === "unavailable") return "unavailable";
2062
- const argv = process.argv.slice(1);
2063
- const result = spawnSync(resolution.bunPath, argv, {
2064
- stdio: "inherit",
2065
- env: {
2066
- ...process.env,
2067
- [REEXEC_GUARD]: "1"
2068
- }
2069
- });
2070
- if (result.error) {
2071
- onProgress?.(`Failed to launch chat under Bun (${resolution.bunPath}): ${result.error.message}`);
2072
- return "unavailable";
2370
+ /** `which`/`where` for bun, without spawning a shell. */
2371
+ function resolveBunOnPath() {
2372
+ const pathVar = process.env["PATH"] ?? "";
2373
+ if (!pathVar) return null;
2374
+ const exts = process.platform === "win32" ? (process.env["PATHEXT"] ?? ".EXE").split(";") : [""];
2375
+ for (const dir of pathVar.split(path.delimiter)) {
2376
+ if (!dir) continue;
2377
+ for (const ext of exts) {
2378
+ const candidate = path.join(dir, `bun${ext}`);
2379
+ if (isExecutableFile(candidate)) return candidate;
2073
2380
  }
2074
- process.exit(result.status ?? 0);
2381
+ }
2382
+ return null;
2383
+ }
2384
+ async function fetchOk(url) {
2385
+ const res = await fetch(url, {
2386
+ headers: { "user-agent": "skydive-cli" },
2387
+ redirect: "follow"
2075
2388
  });
2389
+ if (!res.ok) throw new Error(`GET ${url} -> ${res.status} ${res.statusText}`);
2390
+ return res;
2076
2391
  }
2077
-
2078
- //#endregion
2079
- //#region src/commands/chat.ts
2080
- const chatCommand = {
2081
- command: "chat",
2082
- describe: "Open the interactive chat TUI, or -p for a one-shot",
2083
- builder: (y) => y.option("print", {
2084
- alias: "p",
2085
- type: "string",
2086
- describe: "Non-interactive: send one prompt, print the reply, and exit (like `claude -p`). Reads the prompt from stdin if given no value. Runs under Node — no Bun required."
2087
- }).option("agent", {
2088
- type: "string",
2089
- describe: "Target agent, by id, slug, or name. With -p, the agent to send the one-shot prompt to. Without -p, pre-selects the agent and opens its conversation list, skipping the agent picker. Optional when the account has exactly one agent."
2090
- }).option("conversation", {
2091
- type: "string",
2092
- describe: "For -p: continue an existing conversation by id instead of starting a new one."
2093
- }).option("share-machine", {
2094
- type: "boolean",
2095
- default: false,
2096
- describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; in the TUI you approve per agent, with -p the flag grants the target agent for the run)"
2097
- }).option("theme", {
2098
- type: "string",
2099
- describe: `Pin a colorscheme (disables automatic light/dark switching). One of: ${themes.map((t) => t.id).join(", ")}. Defaults to following the terminal background; also settable via SKYDIVE_THEME.`
2100
- }).option("notify", {
2101
- type: "boolean",
2102
- default: true,
2103
- describe: "Show a desktop notification when a run finishes or needs your input while this terminal is unfocused (use --no-notify to disable)"
2104
- }).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)"),
2105
- handler: async (argv) => {
2106
- const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
2107
- if (argv.print !== void 0) {
2108
- await runPrintMode({
2109
- argv,
2110
- appUrl
2111
- });
2112
- return;
2113
- }
2114
- if (await ensureBunAndReexec((msg) => process.stderr.write(`${msg}\n`)) === "unavailable") {
2115
- printError(`chat needs the Bun runtime and it couldn't be set up automatically (no network, an unsupported platform, or a failed download). Install Bun (https://bun.sh) and run chat under it, e.g. \`bun ${process.argv[1] ?? "skydive"} chat\`, or point SKYDIVE_BUN_PATH at an existing bun binary. For a non-interactive one-shot that runs under Node, use \`chat -p "<prompt>"\`.`);
2116
- process.exit(1);
2117
- }
2118
- let session = resolveSession({ appUrl });
2119
- if (session.isErr()) {
2120
- if (isNonInteractive()) {
2121
- printError("Not signed in for chat and no interactive terminal. Run `skydive auth login --web`, or set SKYDIVE_SESSION_TOKEN.");
2122
- process.exit(1);
2123
- }
2124
- const login = await loginWithDevice({ appUrl });
2125
- if (login.isErr()) {
2126
- printError(login.error.message);
2127
- process.exit(1);
2128
- }
2129
- session = resolveSession({ appUrl });
2130
- if (session.isErr()) {
2131
- printError("Signed in, but no session was stored.");
2132
- process.exit(1);
2133
- }
2134
- }
2135
- const themeId = argv.theme ?? process.env["SKYDIVE_THEME"] ?? void 0;
2136
- if (themeId !== void 0 && !themes.some((t) => t.id === themeId)) {
2137
- printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2138
- process.exit(1);
2139
- }
2140
- const { runChat } = await import("./boot-FPpnUcyM.mjs");
2141
- await runChat({
2142
- appUrl,
2143
- sessionToken: session.value.sessionToken,
2144
- shareMachine: argv["share-machine"],
2145
- promptHistoryPath: getPromptHistoryPath(),
2146
- theme: themeId,
2147
- notifications: argv.notify,
2148
- agentSelector: argv.agent ?? null
2149
- });
2150
- }
2151
- };
2152
- async function runPrintMode({ argv, appUrl }) {
2153
- const session = resolveSession({ appUrl });
2154
- if (session.isErr()) {
2155
- printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
2156
- process.exit(1);
2157
- }
2158
- const { runPrint, readStdin } = await Promise.resolve().then(() => print_exports);
2159
- let prompt = (argv.print ?? "").trim();
2160
- if (!prompt) {
2161
- if (process.stdin.isTTY) {
2162
- printError("No prompt given. Pass it inline (`-p \"your prompt\"`) or pipe it on stdin.");
2163
- process.exit(1);
2164
- }
2165
- prompt = (await readStdin()).trim();
2166
- if (!prompt) {
2167
- printError("Empty prompt on stdin.");
2168
- process.exit(1);
2169
- }
2170
- }
2171
- let machineShare = null;
2172
- if (argv["share-machine"]) {
2173
- const { PortalClient } = await import("./client-_OL8-XGH.mjs").then((n) => n.n);
2174
- let signalConnected;
2175
- const connected = new Promise((resolve) => {
2176
- signalConnected = resolve;
2177
- });
2178
- machineShare = new PortalClient({
2179
- appUrl,
2180
- sessionToken: session.value.sessionToken,
2181
- cwd: process.cwd(),
2182
- onState: (state) => {
2183
- if (state.status === "connected") signalConnected();
2184
- if (state.status === "error") console.error(`portal: connection error: ${state.error ?? "unknown"} — retrying`);
2185
- }
2186
- });
2187
- machineShare.enable();
2188
- if (await Promise.race([connected.then(() => false), new Promise((resolve) => setTimeout(() => resolve(true), 3e4).unref())])) {
2189
- machineShare.dispose();
2190
- printError("Could not connect the portal within 30s — machine sharing is unavailable (network, or the portal kill switch is off). Re-run without --share-machine, or check `skydive portal status`.");
2191
- process.exit(1);
2192
- }
2193
- }
2194
- try {
2195
- const result = await runPrint({
2196
- appUrl,
2197
- sessionToken: session.value.sessionToken,
2198
- prompt,
2199
- agentSelector: argv.agent ?? null,
2200
- conversationId: argv.conversation ?? null,
2201
- json: argv.json,
2202
- machineShare
2203
- });
2204
- if (argv.json) output(argv, result);
2205
- } catch (error) {
2206
- printError(error instanceof Error ? error.message : String(error));
2207
- process.exit(1);
2208
- } finally {
2209
- machineShare?.dispose();
2392
+ /**
2393
+ * Look up the published SHA-256 for `assetName` in the pinned release's
2394
+ * `SHASUMS256.txt`. Verifying against the release's own manifest keeps
2395
+ * integrity checking correct across version bumps without hardcoding a hash
2396
+ * per platform.
2397
+ */
2398
+ async function expectedSha256(version, assetName) {
2399
+ const url = `${BUN_RELEASE_BASE}/bun-v${version}/SHASUMS256.txt`;
2400
+ const text = await (await fetchOk(url)).text();
2401
+ for (const line of text.split("\n")) {
2402
+ const [sum, name] = line.trim().split(/\s+/);
2403
+ if (name === assetName && sum) return sum.toLowerCase();
2210
2404
  }
2405
+ throw new Error(`${assetName} not found in ${url}`);
2406
+ }
2407
+ function sha256(buf) {
2408
+ return createHash("sha256").update(buf).digest("hex");
2211
2409
  }
2212
-
2213
- //#endregion
2214
- //#region src/commands/messages.ts
2215
2410
  /**
2216
- * `skydive messages get <messageId>` re-attaches to an exchange by message id
2217
- * and prints the agent's reply.
2218
- *
2219
- * This is the recovery path for `chat -p`: a long run's stream can drop at the
2220
- * edge (Cloudflare 502/504) after the message was accepted, leaving the caller
2221
- * with a messageId but no result. The agent keeps working server-side, so a
2222
- * blind retry would double-execute an agent that may have write access.
2223
- * Instead, fetch the finished result by message id here — the server resolves
2224
- * the run behind the message and replays it from its persisted event log, so
2225
- * this works whether the run is still live or already done.
2226
- *
2227
- * Message ids are the currency of this API: `chat -p --json` reports the
2228
- * `messageId` and this command consumes it. Run ids stay server-side.
2411
+ * Extract the single `bun` executable from a Bun release zip buffer. The zip
2412
+ * lays the binary out as `<target>/bun` (or `bun.exe` on Windows). Implemented
2413
+ * without a zip dependency by parsing the central directory — Bun's release zips
2414
+ * use a streaming data descriptor, so the *local* file header carries zeroed
2415
+ * sizes; the central directory is the only place with correct sizes/offsets.
2229
2416
  */
2230
- const getCommand = {
2231
- command: "get <message-id>",
2232
- describe: "Fetch a message by id and print the reply (recovers a timed-out -p)",
2233
- builder: (y) => y.positional("message-id", {
2234
- type: "string",
2235
- demandOption: true,
2236
- describe: "Message id (the `messageId` from a prior `chat -p --json`)"
2237
- }),
2238
- handler: async (argv) => {
2239
- const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
2240
- const session = resolveSession({ appUrl });
2241
- if (session.isErr()) {
2242
- printError(`${session.error.message} Run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
2243
- process.exit(1);
2244
- }
2245
- const { messageGet } = await Promise.resolve().then(() => print_exports);
2246
- try {
2247
- const result = await messageGet({
2248
- appUrl,
2249
- sessionToken: session.value.sessionToken,
2250
- messageId: argv["message-id"],
2251
- json: argv.json
2252
- });
2253
- if (argv.json) output(argv, result);
2254
- } catch (error) {
2255
- printError(error instanceof Error ? error.message : String(error));
2256
- process.exit(1);
2417
+ function extractBunFromZip(zip, target) {
2418
+ const exeName = process.platform === "win32" ? "bun.exe" : "bun";
2419
+ let eocd = -1;
2420
+ for (let i = zip.length - 22; i >= 0; i--) if (zip.readUInt32LE(i) === 101010256) {
2421
+ eocd = i;
2422
+ break;
2423
+ }
2424
+ if (eocd < 0) throw new Error("release zip has no end-of-central-directory");
2425
+ const entryCount = zip.readUInt16LE(eocd + 10);
2426
+ let off = zip.readUInt32LE(eocd + 16);
2427
+ for (let n = 0; n < entryCount; n++) {
2428
+ if (zip.readUInt32LE(off) !== 33639248) throw new Error("malformed central directory in release zip");
2429
+ const method = zip.readUInt16LE(off + 10);
2430
+ const compSize = zip.readUInt32LE(off + 20);
2431
+ const nameLen = zip.readUInt16LE(off + 28);
2432
+ const extraLen = zip.readUInt16LE(off + 30);
2433
+ const commentLen = zip.readUInt16LE(off + 32);
2434
+ const localHeaderOffset = zip.readUInt32LE(off + 42);
2435
+ const name = zip.toString("utf8", off + 46, off + 46 + nameLen);
2436
+ if (name === `${target}/${exeName}` || name === exeName) {
2437
+ const lNameLen = zip.readUInt16LE(localHeaderOffset + 26);
2438
+ const lExtraLen = zip.readUInt16LE(localHeaderOffset + 28);
2439
+ const dataStart = localHeaderOffset + 30 + lNameLen + lExtraLen;
2440
+ const data = zip.subarray(dataStart, dataStart + compSize);
2441
+ if (method === 0) return Buffer.from(data);
2442
+ if (method === 8) return zlib.inflateRawSync(data);
2443
+ throw new Error(`unsupported zip compression method ${method}`);
2257
2444
  }
2445
+ off += 46 + nameLen + extraLen + commentLen;
2258
2446
  }
2259
- };
2260
- const messagesCommand = {
2261
- command: "messages",
2262
- describe: "Inspect chat messages and re-fetch agent replies",
2263
- builder: (y) => y.command(getCommand).demandCommand(1, "Specify a subcommand: get"),
2264
- handler: () => {}
2265
- };
2266
-
2267
- //#endregion
2268
- //#region src/chat/api/rest.ts
2269
- var HttpError = class extends Error {
2270
- constructor(status, body) {
2271
- super(`HTTP ${status}: ${body.slice(0, 200)}`);
2272
- this.status = status;
2273
- this.body = body;
2274
- this.name = "HttpError";
2447
+ throw new Error(`bun binary not found in release zip for ${target}`);
2448
+ }
2449
+ /**
2450
+ * Download, verify, and cache the pinned Bun for this platform. Returns the
2451
+ * path to the cached executable, or null if Bun can't be provisioned (no
2452
+ * network, unsupported platform, checksum mismatch) the caller then prints
2453
+ * the manual-install guidance. Never throws for the expected failure modes.
2454
+ */
2455
+ async function downloadBun(onProgress) {
2456
+ const target = bunAssetTarget();
2457
+ if (!target) return null;
2458
+ const assetName = `${target}.zip`;
2459
+ const dest = cachedBunPath();
2460
+ try {
2461
+ onProgress?.(`Fetching Bun v${PINNED_BUN_VERSION} (one-time setup)…`);
2462
+ const url = `${BUN_RELEASE_BASE}/bun-v${PINNED_BUN_VERSION}/${assetName}`;
2463
+ const [zipRes, want] = await Promise.all([fetchOk(url), expectedSha256(PINNED_BUN_VERSION, assetName)]);
2464
+ const zip = Buffer.from(await zipRes.arrayBuffer());
2465
+ const got = sha256(zip);
2466
+ if (got !== want) {
2467
+ onProgress?.(`Bun download failed integrity check (expected ${want}, got ${got}).`);
2468
+ return null;
2469
+ }
2470
+ const bin = extractBunFromZip(zip, target);
2471
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
2472
+ const tmp = path.join(path.dirname(dest), `.bun.tmp-${process.pid}-${Date.now()}`);
2473
+ fs.writeFileSync(tmp, bin, { mode: 493 });
2474
+ if (process.platform !== "win32") fs.chmodSync(tmp, 493);
2475
+ fs.renameSync(tmp, dest);
2476
+ return dest;
2477
+ } catch (e) {
2478
+ onProgress?.(`Could not download Bun automatically: ${e instanceof Error ? e.message : String(e)}`);
2479
+ return null;
2275
2480
  }
2276
- };
2277
- const MAX_STREAM_RECONNECTS = 5;
2278
- function createRestClient({ appUrl, sessionToken }) {
2279
- const baseHeaders = {
2280
- authorization: `Bearer ${sessionToken}`,
2281
- accept: "application/json"
2481
+ }
2482
+ /**
2483
+ * Ensure a usable Bun exists: already running under Bun, found on disk/PATH, or
2484
+ * freshly downloaded and cached. Pure resolution — does not re-exec.
2485
+ */
2486
+ async function resolveBun(onProgress) {
2487
+ if (isBun()) return { kind: "already-bun" };
2488
+ const existing = findExistingBun();
2489
+ if (existing) return {
2490
+ kind: "found",
2491
+ bunPath: existing
2282
2492
  };
2283
- async function get(path, schema) {
2284
- const res = await fetch(`${appUrl}${path}`, { headers: baseHeaders });
2285
- if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
2286
- return schema.parse(await res.json());
2287
- }
2288
- async function post(path, body, schema, method = "POST") {
2289
- const res = await fetch(`${appUrl}${path}`, {
2290
- method,
2291
- headers: {
2292
- ...baseHeaders,
2293
- "content-type": "application/json"
2294
- },
2295
- body: JSON.stringify(body)
2296
- });
2297
- if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
2298
- return schema.parse(await res.json());
2299
- }
2300
- async function del(path) {
2301
- const res = await fetch(`${appUrl}${path}`, {
2302
- method: "DELETE",
2303
- headers: baseHeaders
2304
- });
2305
- if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
2306
- }
2307
- const streamEvents = async ({ path, label, signal, onEvent }) => {
2308
- let lastEventId = null;
2309
- let finished = false;
2310
- let reconnects = 0;
2311
- for (;;) {
2312
- if (signal.aborted) return;
2313
- try {
2314
- const headers = {
2315
- authorization: `Bearer ${sessionToken}`,
2316
- accept: "text/event-stream"
2317
- };
2318
- if (lastEventId) headers["last-event-id"] = lastEventId;
2319
- const res = await fetch(`${appUrl}${path}`, {
2320
- headers,
2321
- signal
2322
- });
2323
- if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
2324
- reconnects = 0;
2325
- const parser = createParser({ onEvent: (message) => {
2326
- if (message.id) lastEventId = message.id;
2327
- if (message.event === "error") {
2328
- const { error } = streamErrorSchema.parse(JSON.parse(message.data));
2329
- throw new Error(error);
2330
- }
2331
- const event = runStreamEventSchema.parse(JSON.parse(message.data));
2332
- if (event.kind === "finished") finished = true;
2333
- onEvent(event.kind === "finished" ? {
2334
- ...event,
2335
- error: event.error ?? null
2336
- } : event);
2337
- } });
2338
- const decoder = new TextDecoder();
2339
- const reader = res.body.getReader();
2340
- try {
2341
- for (;;) {
2342
- const { done, value } = await reader.read();
2343
- if (done) break;
2344
- parser.feed(decoder.decode(value, { stream: true }));
2345
- if (finished) return;
2346
- }
2347
- } finally {
2348
- try {
2349
- await reader.cancel();
2350
- } catch (_error) {}
2351
- }
2352
- } catch (err) {
2353
- if (signal.aborted) return;
2354
- if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
2355
- reconnects += 1;
2356
- if (reconnects > MAX_STREAM_RECONNECTS) throw err;
2357
- await sleep(Math.min(500 * 2 ** reconnects, 5e3));
2358
- continue;
2493
+ const downloaded = await downloadBun(onProgress);
2494
+ if (downloaded) return {
2495
+ kind: "found",
2496
+ bunPath: downloaded
2497
+ };
2498
+ return { kind: "unavailable" };
2499
+ }
2500
+ /**
2501
+ * The heart of the transparent-Bun story. Called by `chat` before it touches
2502
+ * OpenTUI:
2503
+ * - Under Bun already, or if a re-exec guard is set: return 'proceed'.
2504
+ * - Otherwise resolve/provision Bun and re-exec this exact CLI invocation
2505
+ * under it (inheriting stdio + argv), then exit with the child's code.
2506
+ * - If Bun can't be provisioned: return 'unavailable' so the caller prints
2507
+ * the existing manual-install message.
2508
+ *
2509
+ * Returns 'proceed' only when it's safe to load OpenTUI in this process.
2510
+ */
2511
+ function ensureBunAndReexec(onProgress) {
2512
+ if (isBun() || process.env[REEXEC_GUARD] === "1") return Promise.resolve("proceed");
2513
+ return resolveBun(onProgress).then((resolution) => {
2514
+ if (resolution.kind === "already-bun") return "proceed";
2515
+ if (resolution.kind === "unavailable") return "unavailable";
2516
+ const argv = process.argv.slice(1);
2517
+ const result = spawnSync(resolution.bunPath, argv, {
2518
+ stdio: "inherit",
2519
+ env: {
2520
+ ...process.env,
2521
+ [REEXEC_GUARD]: "1"
2359
2522
  }
2360
- if (finished) return;
2361
- reconnects += 1;
2362
- if (reconnects > MAX_STREAM_RECONNECTS) throw new Error(`${label} stream ended unexpectedly`);
2363
- await sleep(Math.min(500 * 2 ** reconnects, 5e3));
2523
+ });
2524
+ if (result.error) {
2525
+ onProgress?.(`Failed to launch chat under Bun (${resolution.bunPath}): ${result.error.message}`);
2526
+ return "unavailable";
2364
2527
  }
2365
- };
2366
- return {
2367
- listAgents: async ({ scope, onPage }) => {
2368
- const all = [];
2369
- let cursor;
2370
- const maxAgents = 2e3;
2371
- do {
2372
- const params = new URLSearchParams({
2373
- limit: "100",
2374
- scope,
2375
- sort: "mine_first_usage",
2376
- includeStats: "false"
2377
- });
2378
- if (cursor) params.set("cursor", cursor);
2379
- const page = await get(`/api/v1/agents?${params.toString()}`, listAgentsResponseSchema);
2380
- all.push(...page.agents);
2381
- cursor = page.nextCursor ?? void 0;
2382
- onPage?.([...all]);
2383
- } while (cursor && all.length < maxAgents);
2384
- return all;
2385
- },
2386
- createAgent: async ({ name }) => {
2387
- const { agent } = await post("/api/v1/agents", { name }, createAgentResponseSchema);
2388
- return agent;
2389
- },
2390
- getConversation: async ({ conversationId }) => {
2391
- const { conversation } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}`, getConversationResponseSchema);
2392
- return conversation;
2393
- },
2394
- listModels: async () => {
2395
- const { models } = await get("/api/v1/models", listModelsResponseSchema);
2396
- return models;
2397
- },
2398
- updateAgentModel: async ({ agentId, model }) => {
2399
- const { agent } = await post(`/api/v1/agents/${encodeURIComponent(agentId)}`, { model }, updateAgentResponseSchema, "PATCH");
2400
- return { model: agent.model ?? null };
2401
- },
2402
- listConversations: async ({ agentId, limit }) => {
2403
- const all = [];
2404
- const maxConversations = limit ?? 5e3;
2405
- let cursor;
2406
- do {
2407
- const remaining = maxConversations - all.length;
2408
- const params = new URLSearchParams({
2409
- agentId,
2410
- includeTotal: "false"
2411
- });
2412
- params.set("limit", String(Math.min(remaining, 100)));
2413
- if (cursor) params.set("cursor", cursor);
2414
- const page = await get(`/api/v1/conversations?${params.toString()}`, listConversationsResponseSchema);
2415
- all.push(...page.conversations);
2416
- cursor = page.nextCursor ?? void 0;
2417
- } while (cursor && all.length < maxConversations);
2418
- return limit ? all.slice(0, limit) : all;
2419
- },
2420
- listMessages: async ({ conversationId }) => {
2421
- const { messages } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`, listMessagesResponseSchema);
2422
- return messages;
2423
- },
2424
- getRecap: async ({ conversationId }) => {
2425
- const { recap } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/recap`, recapResponseSchema);
2426
- return recap?.text ?? null;
2427
- },
2428
- uploadAttachment: async ({ agentId, fileName, mediaType, data }) => {
2429
- const size = data.byteLength;
2430
- const presign = await post("/api/v1/attachments/presign", {
2431
- agentId,
2432
- fileName,
2433
- mediaType,
2434
- size
2435
- }, presignResponseSchema);
2436
- const putRes = await fetch(presign.uploadUrl, {
2437
- method: "PUT",
2438
- headers: { "content-type": mediaType },
2439
- body: new Uint8Array(data)
2440
- });
2441
- if (!putRes.ok) throw new HttpError(putRes.status, await putRes.text().catch(() => ""));
2442
- const finalized = await post(`/api/v1/attachments/${encodeURIComponent(presign.id)}/finalize`, {
2443
- agentId,
2444
- fileName: presign.fileName,
2445
- mediaType: presign.mediaType,
2446
- size
2447
- }, finalizeResponseSchema);
2448
- return {
2449
- id: presign.id,
2450
- fileName: finalized.fileName,
2451
- mediaType: finalized.mediaType,
2452
- sizeBytes: finalized.sizeBytes ?? size
2453
- };
2454
- },
2455
- deleteConversation: async ({ conversationId }) => {
2456
- await del(`/api/v1/conversations/${encodeURIComponent(conversationId)}`);
2457
- },
2458
- sendMessage: async ({ clientSurface, ...input }) => post("/api/v1/chat/send", {
2459
- ...input,
2460
- clientSurface
2461
- }, sendResultSchema),
2462
- activeRun: async ({ conversationId }) => {
2463
- const { run } = await get(`/api/v1/chat/active-run?${new URLSearchParams({ conversationId }).toString()}`, activeRunResponseSchema);
2464
- return run;
2465
- },
2466
- cancelRun: async ({ runId }) => {
2467
- await post(`/api/v1/chat/runs/${encodeURIComponent(runId)}/cancel`, {}, z.object({ ok: z.boolean() }));
2468
- },
2469
- cancelSteer: async ({ directiveId }) => {
2470
- await post(`/api/v1/chat/steer/${encodeURIComponent(directiveId)}/cancel`, {}, z.object({ ok: z.boolean() }));
2471
- },
2472
- oauthConnect: async (input) => {
2473
- const { connectLink } = await post("/api/v1/oauth/connect", input, oauthConnectResponseSchema);
2474
- return { connectLink };
2475
- },
2476
- externalOauthConnect: async (input) => {
2477
- const { authorizationUrl } = await post("/api/v1/external-oauth/connect", input, externalOauthConnectResponseSchema);
2478
- return { authorizationUrl: authorizationUrl ?? null };
2479
- },
2480
- fulfillCredential: async ({ url, body }) => {
2481
- const target = new URL(url, appUrl).toString();
2482
- const res = await fetch(target, {
2483
- method: "POST",
2484
- headers: {
2485
- ...baseHeaders,
2486
- "content-type": "application/json"
2487
- },
2488
- body: JSON.stringify(body)
2528
+ process.exit(result.status ?? 0);
2529
+ });
2530
+ }
2531
+
2532
+ //#endregion
2533
+ //#region src/commands/chat.ts
2534
+ const chatCommand = {
2535
+ command: "chat",
2536
+ describe: "Open the interactive chat TUI, or -p for a one-shot",
2537
+ builder: (y) => y.option("print", {
2538
+ alias: "p",
2539
+ type: "string",
2540
+ describe: "Non-interactive: send one prompt, print the reply, and exit (like `claude -p`). Reads the prompt from stdin if given no value. Runs under Node — no Bun required."
2541
+ }).option("agent", {
2542
+ type: "string",
2543
+ describe: "Target agent, by id, slug, or name. With -p, the agent to send the one-shot prompt to. Without -p, pre-selects the agent and opens its conversation list, skipping the agent picker. Optional when the account has exactly one agent."
2544
+ }).option("conversation", {
2545
+ type: "string",
2546
+ describe: "For -p: continue an existing conversation by id instead of starting a new one."
2547
+ }).option("share-machine", {
2548
+ type: "boolean",
2549
+ default: false,
2550
+ describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; in the TUI you approve per agent, with -p the flag grants the target agent for the run)"
2551
+ }).option("theme", {
2552
+ type: "string",
2553
+ describe: `Pin a colorscheme (disables automatic light/dark switching). One of: ${themes.map((t) => t.id).join(", ")}. Defaults to following the terminal background; also settable via SKYDIVE_THEME.`
2554
+ }).option("notify", {
2555
+ type: "boolean",
2556
+ default: true,
2557
+ describe: "Show a desktop notification when a run finishes or needs your input while this terminal is unfocused (use --no-notify to disable)"
2558
+ }).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)"),
2559
+ handler: async (argv) => {
2560
+ const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
2561
+ if (argv.print !== void 0) {
2562
+ await runPrintMode({
2563
+ argv,
2564
+ appUrl
2489
2565
  });
2490
- if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
2491
- },
2492
- streamRun: async ({ runId, signal, onEvent }) => streamEvents({
2493
- path: `/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`,
2494
- label: "run",
2495
- signal,
2496
- onEvent
2497
- }),
2498
- streamMessage: async ({ messageId, signal, onEvent }) => streamEvents({
2499
- path: `/api/v1/chat/messages/${encodeURIComponent(messageId)}/stream`,
2500
- label: "message",
2501
- signal,
2502
- onEvent
2503
- }),
2504
- streamConversation: async ({ conversationId, signal, onEvent }) => {
2505
- let reconnects = 0;
2506
- for (;;) {
2507
- if (signal.aborted) return;
2508
- try {
2509
- const res = await fetch(`${appUrl}/api/v1/chat/conversations/${encodeURIComponent(conversationId)}/stream`, {
2510
- headers: {
2511
- authorization: `Bearer ${sessionToken}`,
2512
- accept: "text/event-stream"
2513
- },
2514
- signal
2515
- });
2516
- if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
2517
- reconnects = 0;
2518
- const parser = createParser({ onEvent: (message) => {
2519
- const parsed = conversationStreamEventSchema.safeParse(JSON.parse(message.data));
2520
- if (parsed.success) onEvent(parsed.data);
2521
- } });
2522
- const decoder = new TextDecoder();
2523
- const reader = res.body.getReader();
2524
- try {
2525
- for (;;) {
2526
- const { done, value } = await reader.read();
2527
- if (done) break;
2528
- parser.feed(decoder.decode(value, { stream: true }));
2529
- }
2530
- } finally {
2531
- try {
2532
- await reader.cancel();
2533
- } catch (_error) {}
2534
- }
2535
- } catch (err) {
2536
- if (signal.aborted) return;
2537
- if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
2538
- reconnects += 1;
2539
- if (reconnects > MAX_STREAM_RECONNECTS) throw err;
2540
- await sleep(Math.min(500 * 2 ** reconnects, 5e3));
2541
- continue;
2542
- }
2543
- if (signal.aborted) return;
2544
- reconnects += 1;
2545
- if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("conversation stream ended unexpectedly");
2546
- await sleep(Math.min(500 * 2 ** reconnects, 5e3));
2566
+ return;
2567
+ }
2568
+ if (await ensureBunAndReexec((msg) => process.stderr.write(`${msg}\n`)) === "unavailable") {
2569
+ printError(`chat needs the Bun runtime and it couldn't be set up automatically (no network, an unsupported platform, or a failed download). Install Bun (https://bun.sh) and run chat under it, e.g. \`bun ${process.argv[1] ?? "skydive"} chat\`, or point SKYDIVE_BUN_PATH at an existing bun binary. For a non-interactive one-shot that runs under Node, use \`chat -p "<prompt>"\`.`);
2570
+ process.exit(1);
2571
+ }
2572
+ let session = resolveSession({ appUrl });
2573
+ if (session.isErr()) {
2574
+ if (isNonInteractive()) {
2575
+ printError("Not signed in for chat and no interactive terminal. Run `skydive auth login --web`, or set SKYDIVE_SESSION_TOKEN.");
2576
+ process.exit(1);
2577
+ }
2578
+ const login = await loginWithDevice({ appUrl });
2579
+ if (login.isErr()) {
2580
+ printError(login.error.message);
2581
+ process.exit(1);
2582
+ }
2583
+ session = resolveSession({ appUrl });
2584
+ if (session.isErr()) {
2585
+ printError("Signed in, but no session was stored.");
2586
+ process.exit(1);
2587
+ }
2588
+ }
2589
+ const themeId = argv.theme ?? process.env["SKYDIVE_THEME"] ?? void 0;
2590
+ if (themeId !== void 0 && !themes.some((t) => t.id === themeId)) {
2591
+ printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2592
+ process.exit(1);
2593
+ }
2594
+ const { runChat } = await import("./boot-DtAYHuRB.mjs");
2595
+ await runChat({
2596
+ appUrl,
2597
+ sessionToken: session.value.sessionToken,
2598
+ shareMachine: argv["share-machine"],
2599
+ promptHistoryPath: getPromptHistoryPath(),
2600
+ theme: themeId,
2601
+ notifications: argv.notify,
2602
+ agentSelector: argv.agent ?? null
2603
+ });
2604
+ }
2605
+ };
2606
+ async function runPrintMode({ argv, appUrl }) {
2607
+ const session = resolveSession({ appUrl });
2608
+ if (session.isErr()) {
2609
+ printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
2610
+ process.exit(1);
2611
+ }
2612
+ const { runPrint, readStdin } = await Promise.resolve().then(() => print_exports);
2613
+ let prompt = (argv.print ?? "").trim();
2614
+ if (!prompt) {
2615
+ if (process.stdin.isTTY) {
2616
+ printError("No prompt given. Pass it inline (`-p \"your prompt\"`) or pipe it on stdin.");
2617
+ process.exit(1);
2618
+ }
2619
+ prompt = (await readStdin()).trim();
2620
+ if (!prompt) {
2621
+ printError("Empty prompt on stdin.");
2622
+ process.exit(1);
2623
+ }
2624
+ }
2625
+ let machineShare = null;
2626
+ if (argv["share-machine"]) {
2627
+ const { PortalClient } = await import("./client-_OL8-XGH.mjs").then((n) => n.n);
2628
+ let signalConnected;
2629
+ const connected = new Promise((resolve) => {
2630
+ signalConnected = resolve;
2631
+ });
2632
+ machineShare = new PortalClient({
2633
+ appUrl,
2634
+ sessionToken: session.value.sessionToken,
2635
+ cwd: process.cwd(),
2636
+ onState: (state) => {
2637
+ if (state.status === "connected") signalConnected();
2638
+ if (state.status === "error") console.error(`portal: connection error: ${state.error ?? "unknown"} — retrying`);
2547
2639
  }
2640
+ });
2641
+ machineShare.enable();
2642
+ if (await Promise.race([connected.then(() => false), new Promise((resolve) => setTimeout(() => resolve(true), 3e4).unref())])) {
2643
+ machineShare.dispose();
2644
+ printError("Could not connect the portal within 30s — machine sharing is unavailable (network, or the portal kill switch is off). Re-run without --share-machine, or check `skydive portal status`.");
2645
+ process.exit(1);
2548
2646
  }
2549
- };
2550
- }
2551
- function sleep(ms) {
2552
- return new Promise((resolve) => setTimeout(resolve, ms));
2647
+ }
2648
+ try {
2649
+ const result = await runPrint({
2650
+ appUrl,
2651
+ sessionToken: session.value.sessionToken,
2652
+ prompt,
2653
+ agentSelector: argv.agent ?? null,
2654
+ conversationId: argv.conversation ?? null,
2655
+ json: argv.json,
2656
+ machineShare
2657
+ });
2658
+ if (argv.json) output(argv, result);
2659
+ } catch (error) {
2660
+ printError(error instanceof Error ? error.message : String(error));
2661
+ process.exit(1);
2662
+ } finally {
2663
+ machineShare?.dispose();
2664
+ }
2553
2665
  }
2554
- const agentSummarySchema = z.object({
2555
- id: z.string().uuid(),
2556
- name: z.string(),
2557
- slug: z.string().nullable().optional(),
2558
- title: z.string().nullable().optional(),
2559
- description: z.string().nullable().optional(),
2560
- createdAt: z.string(),
2561
- creatorName: z.string().nullable().optional(),
2562
- model: z.string().nullable().optional(),
2563
- modelLocked: z.boolean().optional()
2564
- });
2565
- const platformModelSchema = z.object({
2566
- id: z.string(),
2567
- displayName: z.string(),
2568
- providerDisplay: z.string().optional(),
2569
- reasoning: z.boolean().optional(),
2570
- compliant: z.boolean().optional()
2571
- }).passthrough();
2572
- const listModelsResponseSchema = z.object({ models: z.array(platformModelSchema) });
2573
- const updateAgentResponseSchema = z.object({ agent: z.object({ model: z.string().nullable().optional() }).passthrough() });
2574
- const listAgentsResponseSchema = z.object({
2575
- agents: z.array(agentSummarySchema),
2576
- nextCursor: z.string().nullable().optional(),
2577
- totalCount: z.number().nullable().optional()
2578
- });
2579
- const createAgentResponseSchema = z.object({ agent: agentSummarySchema });
2580
- const conversationSummarySchema = z.object({
2581
- id: z.string().uuid(),
2582
- title: z.string().nullable(),
2583
- createdAt: z.string(),
2584
- updatedAt: z.string(),
2585
- preview: z.string().nullable(),
2586
- channel: z.string().nullable(),
2587
- channelLabel: z.string().nullable(),
2588
- agent: z.object({
2589
- id: z.string().uuid(),
2590
- name: z.string(),
2591
- slug: z.string().nullable().optional(),
2592
- title: z.string().nullable().optional()
2593
- })
2594
- });
2595
- const conversationTitleSchema = z.object({
2596
- id: z.string().uuid(),
2597
- title: z.string().nullable()
2598
- });
2599
- const getConversationResponseSchema = z.object({ conversation: conversationTitleSchema });
2600
- const listConversationsResponseSchema = z.object({
2601
- conversations: z.array(conversationSummarySchema),
2602
- nextCursor: z.string().nullable().optional(),
2603
- totalCount: z.number().optional()
2604
- });
2605
- const uiMessagePartSchema = z.union([
2606
- z.object({
2607
- type: z.literal("text"),
2608
- text: z.string()
2609
- }),
2610
- z.object({
2611
- type: z.literal("reasoning"),
2612
- text: z.string().optional()
2613
- }),
2614
- z.object({
2615
- type: z.literal("dynamic-tool"),
2616
- toolCallId: z.string(),
2617
- toolName: z.string(),
2618
- input: z.unknown().optional(),
2619
- output: z.unknown().optional(),
2620
- state: z.string().optional(),
2621
- errorText: z.string().optional()
2666
+
2667
+ //#endregion
2668
+ //#region src/commands/messages.ts
2669
+ /**
2670
+ * `skydive messages get <messageId>` re-attaches to an exchange by message id
2671
+ * and prints the agent's reply.
2672
+ *
2673
+ * This is the recovery path for `chat -p`: a long run's stream can drop at the
2674
+ * edge (Cloudflare 502/504) after the message was accepted, leaving the caller
2675
+ * with a messageId but no result. The agent keeps working server-side, so a
2676
+ * blind retry would double-execute an agent that may have write access.
2677
+ * Instead, fetch the finished result by message id here — the server resolves
2678
+ * the run behind the message and replays it from its persisted event log, so
2679
+ * this works whether the run is still live or already done.
2680
+ *
2681
+ * Message ids are the currency of this API: `chat -p --json` reports the
2682
+ * `messageId` and this command consumes it. Run ids stay server-side.
2683
+ */
2684
+ const getCommand = {
2685
+ command: "get <message-id>",
2686
+ describe: "Fetch a message by id and print the reply (recovers a timed-out -p)",
2687
+ builder: (y) => y.positional("message-id", {
2688
+ type: "string",
2689
+ demandOption: true,
2690
+ describe: "Message id (the `messageId` from a prior `chat -p --json`)"
2622
2691
  }),
2623
- z.object({ type: z.string() }).passthrough()
2624
- ]);
2625
- const uiMessageSchema = z.object({
2626
- id: z.string(),
2627
- role: z.string(),
2628
- parts: z.array(uiMessagePartSchema)
2629
- });
2630
- const recapResponseSchema = z.object({ recap: z.object({ text: z.string() }).nullable() });
2631
- const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
2632
- const sendResultSchema = z.object({
2633
- runId: z.string(),
2634
- messageId: z.string().uuid().nullish(),
2635
- conversationId: z.string().uuid(),
2636
- isNewConversation: z.boolean(),
2637
- steered: z.boolean().optional(),
2638
- directive: z.object({ id: z.string() }).passthrough().optional()
2639
- });
2640
- const presignResponseSchema = z.object({
2641
- id: z.string(),
2642
- uploadUrl: z.string(),
2643
- fileName: z.string(),
2644
- mediaType: z.string()
2645
- });
2646
- const finalizeResponseSchema = z.object({
2647
- fileName: z.string(),
2648
- mediaType: z.string(),
2649
- sizeBytes: z.number().nullable().optional()
2650
- });
2651
- const activeRunResponseSchema = z.object({ run: z.object({ runId: z.string() }).nullable() });
2652
- const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
2653
- const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
2654
- const runStreamEventSchema = z.union([z.object({
2655
- kind: z.literal("chunk"),
2656
- chunk: z.record(z.unknown())
2657
- }), z.object({
2658
- kind: z.literal("finished"),
2659
- status: z.string(),
2660
- error: z.string().nullish()
2661
- })]);
2662
- const streamErrorSchema = z.object({ error: z.string() });
2663
- const conversationStreamEventSchema = z.object({
2664
- kind: z.literal("conversation"),
2665
- id: z.string(),
2666
- title: z.string().nullable()
2667
- });
2692
+ handler: async (argv) => {
2693
+ const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
2694
+ const session = resolveSession({ appUrl });
2695
+ if (session.isErr()) {
2696
+ printError(`${session.error.message} Run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
2697
+ process.exit(1);
2698
+ }
2699
+ const { messageGet } = await Promise.resolve().then(() => print_exports);
2700
+ try {
2701
+ const result = await messageGet({
2702
+ appUrl,
2703
+ sessionToken: session.value.sessionToken,
2704
+ messageId: argv["message-id"],
2705
+ json: argv.json
2706
+ });
2707
+ if (argv.json) output(argv, result);
2708
+ } catch (error) {
2709
+ printError(error instanceof Error ? error.message : String(error));
2710
+ process.exit(1);
2711
+ }
2712
+ }
2713
+ };
2714
+ const messagesCommand = {
2715
+ command: "messages",
2716
+ describe: "Inspect chat messages and re-fetch agent replies",
2717
+ builder: (y) => y.command(getCommand).demandCommand(1, "Specify a subcommand: get"),
2718
+ handler: () => {}
2719
+ };
2668
2720
 
2669
2721
  //#endregion
2670
2722
  //#region src/chat/util.ts
@@ -3147,30 +3199,6 @@ async function readStdin() {
3147
3199
  return Buffer.concat(chunks).toString("utf8");
3148
3200
  }
3149
3201
 
3150
- //#endregion
3151
- //#region src/commands/session.ts
3152
- /**
3153
- * Resolve the signed-in chat session or exit with a friendly hint. Shared by
3154
- * every command that talks to the authenticated REST API so the
3155
- * resolve-or-exit block isn't copy-pasted per command.
3156
- */
3157
- function requireSession(argv) {
3158
- const session = resolveSession({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
3159
- if (session.isErr()) {
3160
- printError(`${session.error.message} Run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
3161
- process.exit(1);
3162
- }
3163
- return session.value;
3164
- }
3165
- /** Resolve the session (see {@link requireSession}) and build a REST client. */
3166
- function requireRestClient(argv) {
3167
- const session = requireSession(argv);
3168
- return createRestClient({
3169
- appUrl: session.appUrl,
3170
- sessionToken: session.sessionToken
3171
- });
3172
- }
3173
-
3174
3202
  //#endregion
3175
3203
  //#region src/commands/conversations.ts
3176
3204
  /**
@@ -3328,7 +3356,7 @@ const listCommand = {
3328
3356
  };
3329
3357
  const switchCommand = {
3330
3358
  command: "switch [workspace]",
3331
- describe: "Switch which workspace `skydive chat` uses",
3359
+ describe: "Switch the workspace all `skydive` commands act on",
3332
3360
  builder: (y) => y.positional("workspace", {
3333
3361
  type: "string",
3334
3362
  demandOption: false,
@@ -3345,7 +3373,7 @@ const switchCommand = {
3345
3373
  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.");
3346
3374
  process.exit(1);
3347
3375
  }
3348
- const { runWorkspacePicker } = await import("./boot-FPpnUcyM.mjs");
3376
+ const { runWorkspacePicker } = await import("./boot-DtAYHuRB.mjs");
3349
3377
  await runWorkspacePicker(session);
3350
3378
  return;
3351
3379
  }
@@ -3377,7 +3405,7 @@ const switchCommand = {
3377
3405
  };
3378
3406
  const workspaceCommand = {
3379
3407
  command: "workspace",
3380
- describe: "List or switch which workspace `skydive chat` uses",
3408
+ describe: "List or switch the workspace all `skydive` commands act on",
3381
3409
  builder: (y) => y.command(listCommand).command(switchCommand),
3382
3410
  handler: (argv) => runList(argv, { hint: true })
3383
3411
  };
@@ -3676,7 +3704,7 @@ const portalCommand = {
3676
3704
  //#endregion
3677
3705
  //#region src/cli.ts
3678
3706
  function createCli(argv) {
3679
- return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch which workspace `skydive chat` uses").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").option("json", {
3707
+ return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch the workspace all `skydive` commands act on").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").option("json", {
3680
3708
  type: "boolean",
3681
3709
  default: false,
3682
3710
  global: true,
@@ -3763,4 +3791,4 @@ function resolveArgv(args, tty = {
3763
3791
  createCli(resolveArgv(hideBin(process.argv))).parse();
3764
3792
 
3765
3793
  //#endregion
3766
- export { themesForMode as A, monoTheme as C, themeMode as D, themeForMode as E, DEFAULT_APP_URL as F, getConfigPath as I, getSavedTheme as L, listWorkspaces as M, setActiveWorkspace as N, themeModeFromColorFgBg as O, DEFAULT_API_URL as P, resolveWebUrl as R, findTheme as S, theme as T, isRecord as _, buildEnv as a, DEFAULT_THEME_ID as b, resolveAgent as c, parseExternalOauthConnectParams as d, parseOauthConnectParams as f, errorMessage as g, parseConnectCard as h, mintPortalDeviceToken as i, getActiveWorkspaceId as j, themeVersion as k, MASK_CHAR as l, resolveConnectUrl as m, findThisDevice as n, machineIdentity as o, reconcileMaskedInput as p, grantPortalAccess as r, portalWsUrl as s, fetchPortalDevices as t, cardActionErrorMessage as u, HttpError as v, noColorRequested as w, applyTheme as x, createRestClient as y, saveTheme as z };
3794
+ export { createRestClient as A, theme as C, themeVersion as D, themeModeFromColorFgBg as E, DEFAULT_APP_URL as F, getConfigPath as I, getSavedTheme as L, listWorkspaces as M, setActiveWorkspace as N, themesForMode as O, DEFAULT_API_URL as P, resolveWebUrl as R, noColorRequested as S, themeMode as T, isRecord as _, buildEnv as a, findTheme as b, resolveAgent as c, parseExternalOauthConnectParams as d, parseOauthConnectParams as f, errorMessage as g, parseConnectCard as h, mintPortalDeviceToken as i, getActiveWorkspaceId as j, HttpError as k, MASK_CHAR as l, resolveConnectUrl as m, findThisDevice as n, machineIdentity as o, reconcileMaskedInput as p, grantPortalAccess as r, portalWsUrl as s, fetchPortalDevices as t, cardActionErrorMessage as u, DEFAULT_THEME_ID as v, themeForMode as w, monoTheme as x, applyTheme as y, saveTheme as z };