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 +1070 -1042
- package/dist/js/{boot-FPpnUcyM.mjs → boot-DtAYHuRB.mjs} +173 -79
- package/package.json +1 -1
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.
|
|
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`).
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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/
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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 (
|
|
922
|
-
|
|
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
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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
|
-
*
|
|
2030
|
-
*
|
|
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
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
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
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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
|
-
* `
|
|
2217
|
-
*
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
*
|
|
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
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
if (
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
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
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
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
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
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
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
if (
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
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
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
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
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
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
|
-
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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
|
-
|
|
2552
|
-
|
|
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
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
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
|
-
|
|
2624
|
-
]);
|
|
2625
|
-
const
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
}
|
|
2630
|
-
const
|
|
2631
|
-
|
|
2632
|
-
const
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
})
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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 {
|
|
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 };
|