skydive-cli 0.1.0-beta.45

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.
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env node
2
+ import { z } from "zod";
3
+ import { createParser } from "eventsource-parser";
4
+
5
+ //#region \0rolldown/runtime.js
6
+ var __defProp = Object.defineProperty;
7
+ var __exportAll = (all, no_symbols) => {
8
+ let target = {};
9
+ for (var name in all) {
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true
13
+ });
14
+ }
15
+ if (!no_symbols) {
16
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
17
+ }
18
+ return target;
19
+ };
20
+
21
+ //#endregion
22
+ //#region src/chat/api/rest.ts
23
+ var HttpError = class extends Error {
24
+ constructor(status, body) {
25
+ super(`HTTP ${status}: ${body.slice(0, 200)}`);
26
+ this.status = status;
27
+ this.body = body;
28
+ this.name = "HttpError";
29
+ }
30
+ };
31
+ const MAX_STREAM_RECONNECTS = 5;
32
+ function createRestClient({ appUrl, sessionToken }) {
33
+ const baseHeaders = {
34
+ authorization: `Bearer ${sessionToken}`,
35
+ accept: "application/json"
36
+ };
37
+ async function get(path, schema) {
38
+ const res = await fetch(`${appUrl}${path}`, { headers: baseHeaders });
39
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
40
+ return schema.parse(await res.json());
41
+ }
42
+ async function post(path, body, schema, method = "POST") {
43
+ const res = await fetch(`${appUrl}${path}`, {
44
+ method,
45
+ headers: {
46
+ ...baseHeaders,
47
+ "content-type": "application/json"
48
+ },
49
+ body: JSON.stringify(body)
50
+ });
51
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
52
+ return schema.parse(await res.json());
53
+ }
54
+ async function del(path) {
55
+ const res = await fetch(`${appUrl}${path}`, {
56
+ method: "DELETE",
57
+ headers: baseHeaders
58
+ });
59
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
60
+ }
61
+ return {
62
+ listAgents: async ({ scope, onPage }) => {
63
+ const all = [];
64
+ let cursor;
65
+ const maxAgents = 2e3;
66
+ do {
67
+ const params = new URLSearchParams({
68
+ limit: "100",
69
+ scope,
70
+ sort: "mine_first_usage",
71
+ includeStats: "false"
72
+ });
73
+ if (cursor) params.set("cursor", cursor);
74
+ const page = await get(`/api/v1/agents?${params.toString()}`, listAgentsResponseSchema);
75
+ all.push(...page.agents);
76
+ cursor = page.nextCursor ?? void 0;
77
+ onPage?.([...all]);
78
+ } while (cursor && all.length < maxAgents);
79
+ return all;
80
+ },
81
+ createAgent: async ({ name }) => {
82
+ const { agent } = await post("/api/v1/agents", { name }, createAgentResponseSchema);
83
+ return agent;
84
+ },
85
+ getConversation: async ({ conversationId }) => {
86
+ const { conversation } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}`, getConversationResponseSchema);
87
+ return conversation;
88
+ },
89
+ listModels: async () => {
90
+ const { models } = await get("/api/v1/models", listModelsResponseSchema);
91
+ return models;
92
+ },
93
+ updateAgentModel: async ({ agentId, model }) => {
94
+ const { agent } = await post(`/api/v1/agents/${encodeURIComponent(agentId)}`, { model }, updateAgentResponseSchema, "PATCH");
95
+ return { model: agent.model ?? null };
96
+ },
97
+ listConversations: async ({ agentId, limit }) => {
98
+ const params = new URLSearchParams({
99
+ agentId,
100
+ includeTotal: "false"
101
+ });
102
+ if (limit) params.set("limit", String(limit));
103
+ const { conversations } = await get(`/api/v1/conversations?${params.toString()}`, listConversationsResponseSchema);
104
+ return conversations;
105
+ },
106
+ listMessages: async ({ conversationId }) => {
107
+ const { messages } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`, listMessagesResponseSchema);
108
+ return messages;
109
+ },
110
+ uploadAttachment: async ({ agentId, fileName, mediaType, data }) => {
111
+ const size = data.byteLength;
112
+ const presign = await post("/api/v1/attachments/presign", {
113
+ agentId,
114
+ fileName,
115
+ mediaType,
116
+ size
117
+ }, presignResponseSchema);
118
+ const putRes = await fetch(presign.uploadUrl, {
119
+ method: "PUT",
120
+ headers: { "content-type": mediaType },
121
+ body: new Uint8Array(data)
122
+ });
123
+ if (!putRes.ok) throw new HttpError(putRes.status, await putRes.text().catch(() => ""));
124
+ const finalized = await post(`/api/v1/attachments/${encodeURIComponent(presign.id)}/finalize`, {
125
+ agentId,
126
+ fileName: presign.fileName,
127
+ mediaType: presign.mediaType,
128
+ size
129
+ }, finalizeResponseSchema);
130
+ return {
131
+ id: presign.id,
132
+ fileName: finalized.fileName,
133
+ mediaType: finalized.mediaType,
134
+ sizeBytes: finalized.sizeBytes ?? size
135
+ };
136
+ },
137
+ deleteConversation: async ({ conversationId }) => {
138
+ await del(`/api/v1/conversations/${encodeURIComponent(conversationId)}`);
139
+ },
140
+ sendMessage: async ({ clientSurface, ...input }) => post("/api/v1/chat/send", {
141
+ ...input,
142
+ clientSurface
143
+ }, sendResultSchema),
144
+ activeRun: async ({ conversationId }) => {
145
+ const { run } = await get(`/api/v1/chat/active-run?${new URLSearchParams({ conversationId }).toString()}`, activeRunResponseSchema);
146
+ return run;
147
+ },
148
+ cancelRun: async ({ runId }) => {
149
+ await post(`/api/v1/chat/runs/${encodeURIComponent(runId)}/cancel`, {}, z.object({ ok: z.boolean() }));
150
+ },
151
+ cancelSteer: async ({ directiveId }) => {
152
+ await post(`/api/v1/chat/steer/${encodeURIComponent(directiveId)}/cancel`, {}, z.object({ ok: z.boolean() }));
153
+ },
154
+ oauthConnect: async (input) => {
155
+ const { connectLink } = await post("/api/v1/oauth/connect", input, oauthConnectResponseSchema);
156
+ return { connectLink };
157
+ },
158
+ externalOauthConnect: async (input) => {
159
+ const { authorizationUrl } = await post("/api/v1/external-oauth/connect", input, externalOauthConnectResponseSchema);
160
+ return { authorizationUrl: authorizationUrl ?? null };
161
+ },
162
+ fulfillCredential: async ({ url, body }) => {
163
+ const target = new URL(url, appUrl).toString();
164
+ const res = await fetch(target, {
165
+ method: "POST",
166
+ headers: {
167
+ ...baseHeaders,
168
+ "content-type": "application/json"
169
+ },
170
+ body: JSON.stringify(body)
171
+ });
172
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
173
+ },
174
+ streamRun: async ({ runId, signal, onEvent }) => {
175
+ let lastEventId = null;
176
+ let finished = false;
177
+ let reconnects = 0;
178
+ for (;;) {
179
+ if (signal.aborted) return;
180
+ try {
181
+ const headers = {
182
+ authorization: `Bearer ${sessionToken}`,
183
+ accept: "text/event-stream"
184
+ };
185
+ if (lastEventId) headers["last-event-id"] = lastEventId;
186
+ const res = await fetch(`${appUrl}/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`, {
187
+ headers,
188
+ signal
189
+ });
190
+ if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
191
+ reconnects = 0;
192
+ const parser = createParser({ onEvent: (message) => {
193
+ if (message.id) lastEventId = message.id;
194
+ if (message.event === "error") {
195
+ const { error } = streamErrorSchema.parse(JSON.parse(message.data));
196
+ throw new Error(error);
197
+ }
198
+ const event = runStreamEventSchema.parse(JSON.parse(message.data));
199
+ if (event.kind === "finished") finished = true;
200
+ onEvent(event.kind === "finished" ? {
201
+ ...event,
202
+ error: event.error ?? null
203
+ } : event);
204
+ } });
205
+ const decoder = new TextDecoder();
206
+ const reader = res.body.getReader();
207
+ try {
208
+ for (;;) {
209
+ const { done, value } = await reader.read();
210
+ if (done) break;
211
+ parser.feed(decoder.decode(value, { stream: true }));
212
+ if (finished) return;
213
+ }
214
+ } finally {
215
+ try {
216
+ await reader.cancel();
217
+ } catch (_error) {}
218
+ }
219
+ } catch (err) {
220
+ if (signal.aborted) return;
221
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
222
+ reconnects += 1;
223
+ if (reconnects > MAX_STREAM_RECONNECTS) throw err;
224
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
225
+ continue;
226
+ }
227
+ if (finished) return;
228
+ reconnects += 1;
229
+ if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("run stream ended unexpectedly");
230
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
231
+ }
232
+ }
233
+ };
234
+ }
235
+ function sleep(ms) {
236
+ return new Promise((resolve) => setTimeout(resolve, ms));
237
+ }
238
+ const agentSummarySchema = z.object({
239
+ id: z.string().uuid(),
240
+ name: z.string(),
241
+ slug: z.string().nullable().optional(),
242
+ title: z.string().nullable().optional(),
243
+ description: z.string().nullable().optional(),
244
+ createdAt: z.string(),
245
+ creatorName: z.string().nullable().optional(),
246
+ model: z.string().nullable().optional(),
247
+ modelLocked: z.boolean().optional()
248
+ });
249
+ const platformModelSchema = z.object({
250
+ id: z.string(),
251
+ displayName: z.string(),
252
+ providerDisplay: z.string().optional(),
253
+ reasoning: z.boolean().optional(),
254
+ compliant: z.boolean().optional()
255
+ }).passthrough();
256
+ const listModelsResponseSchema = z.object({ models: z.array(platformModelSchema) });
257
+ const updateAgentResponseSchema = z.object({ agent: z.object({ model: z.string().nullable().optional() }).passthrough() });
258
+ const listAgentsResponseSchema = z.object({
259
+ agents: z.array(agentSummarySchema),
260
+ nextCursor: z.string().nullable().optional(),
261
+ totalCount: z.number().nullable().optional()
262
+ });
263
+ const createAgentResponseSchema = z.object({ agent: agentSummarySchema });
264
+ const conversationSummarySchema = z.object({
265
+ id: z.string().uuid(),
266
+ title: z.string().nullable(),
267
+ createdAt: z.string(),
268
+ updatedAt: z.string(),
269
+ preview: z.string().nullable(),
270
+ channel: z.string().nullable(),
271
+ channelLabel: z.string().nullable(),
272
+ agent: z.object({
273
+ id: z.string().uuid(),
274
+ name: z.string(),
275
+ slug: z.string().nullable().optional(),
276
+ title: z.string().nullable().optional()
277
+ })
278
+ });
279
+ const conversationTitleSchema = z.object({
280
+ id: z.string().uuid(),
281
+ title: z.string().nullable()
282
+ });
283
+ const getConversationResponseSchema = z.object({ conversation: conversationTitleSchema });
284
+ const listConversationsResponseSchema = z.object({
285
+ conversations: z.array(conversationSummarySchema),
286
+ nextCursor: z.string().nullable().optional(),
287
+ totalCount: z.number().optional()
288
+ });
289
+ const uiMessagePartSchema = z.union([
290
+ z.object({
291
+ type: z.literal("text"),
292
+ text: z.string()
293
+ }),
294
+ z.object({
295
+ type: z.literal("reasoning"),
296
+ text: z.string().optional()
297
+ }),
298
+ z.object({
299
+ type: z.literal("dynamic-tool"),
300
+ toolCallId: z.string(),
301
+ toolName: z.string(),
302
+ input: z.unknown().optional(),
303
+ output: z.unknown().optional(),
304
+ state: z.string().optional(),
305
+ errorText: z.string().optional()
306
+ }),
307
+ z.object({ type: z.string() }).passthrough()
308
+ ]);
309
+ const uiMessageSchema = z.object({
310
+ id: z.string(),
311
+ role: z.string(),
312
+ parts: z.array(uiMessagePartSchema)
313
+ });
314
+ const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
315
+ const sendResultSchema = z.object({
316
+ runId: z.string(),
317
+ conversationId: z.string().uuid(),
318
+ isNewConversation: z.boolean(),
319
+ steered: z.boolean().optional(),
320
+ directive: z.object({ id: z.string() }).passthrough().optional()
321
+ });
322
+ const presignResponseSchema = z.object({
323
+ id: z.string(),
324
+ uploadUrl: z.string(),
325
+ fileName: z.string(),
326
+ mediaType: z.string()
327
+ });
328
+ const finalizeResponseSchema = z.object({
329
+ fileName: z.string(),
330
+ mediaType: z.string(),
331
+ sizeBytes: z.number().nullable().optional()
332
+ });
333
+ const activeRunResponseSchema = z.object({ run: z.object({ runId: z.string() }).nullable() });
334
+ const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
335
+ const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
336
+ const runStreamEventSchema = z.union([z.object({
337
+ kind: z.literal("chunk"),
338
+ chunk: z.record(z.unknown())
339
+ }), z.object({
340
+ kind: z.literal("finished"),
341
+ status: z.string(),
342
+ error: z.string().nullish()
343
+ })]);
344
+ const streamErrorSchema = z.object({ error: z.string() });
345
+
346
+ //#endregion
347
+ //#region src/chat/print.ts
348
+ var print_exports = /* @__PURE__ */ __exportAll({
349
+ readStdin: () => readStdin,
350
+ resolveAgent: () => resolveAgent,
351
+ runPrint: () => runPrint
352
+ });
353
+ /**
354
+ * Non-interactive chat, à la `claude -p`. Sends a single prompt to an
355
+ * agent, streams the run, and prints the assistant's reply to stdout
356
+ * before exiting. No OpenTUI, no Bun requirement — this rides the same
357
+ * Node-friendly REST client the TUI uses, so it runs anywhere the
358
+ * management commands do (CI, pipes, scripts).
359
+ *
360
+ * Resolution rules kept deliberately strict because there's no human to
361
+ * disambiguate: an `--agent` selector must match exactly one agent, and
362
+ * when it's omitted we only auto-pick if the account has exactly one.
363
+ */
364
+ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversationId, json }) {
365
+ const client = createRestClient({
366
+ appUrl,
367
+ sessionToken
368
+ });
369
+ const agent = resolveAgent(await client.listAgents({
370
+ scope: "org",
371
+ onPage: null
372
+ }), agentSelector);
373
+ const send = await client.sendMessage({
374
+ agentId: agent.id,
375
+ conversationId,
376
+ content: prompt,
377
+ attachmentIds: [],
378
+ clientSurface: "cli"
379
+ });
380
+ let text = "";
381
+ const controller = new AbortController();
382
+ let streamError = null;
383
+ await client.streamRun({
384
+ runId: send.runId,
385
+ signal: controller.signal,
386
+ onEvent: (event) => {
387
+ if (event.kind === "finished") {
388
+ if (event.error) streamError = event.error;
389
+ return;
390
+ }
391
+ const chunk = event.chunk;
392
+ if (chunk["type"] === "text-delta") {
393
+ const delta = typeof chunk["delta"] === "string" ? chunk["delta"] : typeof chunk["text"] === "string" ? chunk["text"] : "";
394
+ if (delta) {
395
+ text += delta;
396
+ if (!json) process.stdout.write(delta);
397
+ }
398
+ } else if (chunk["type"] === "error") streamError = typeof chunk["errorText"] === "string" ? chunk["errorText"] : "unknown error";
399
+ }
400
+ });
401
+ if (streamError) throw new Error(streamError);
402
+ if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
403
+ return {
404
+ agentId: agent.id,
405
+ agentName: agent.name,
406
+ conversationId: send.conversationId,
407
+ isNewConversation: send.isNewConversation,
408
+ runId: send.runId,
409
+ text
410
+ };
411
+ }
412
+ /**
413
+ * Pick the target agent. With no selector, auto-pick only when the
414
+ * account has exactly one agent; otherwise the user must name one (there's
415
+ * no picker in non-interactive mode). A selector matches by id first, then
416
+ * a unique case-insensitive slug/name; ambiguous or missing matches throw
417
+ * with the candidate list so the caller knows what to pass.
418
+ */
419
+ function resolveAgent(agents, selector) {
420
+ if (!selector) {
421
+ const [only, ...rest] = agents;
422
+ if (!only) throw new Error("No agents on this account.");
423
+ if (rest.length === 0) return only;
424
+ throw new Error(`Multiple agents on this account — pass --agent <id|slug|name>. Candidates:\n${formatCandidates(agents)}`);
425
+ }
426
+ const byId = agents.find((a) => a.id === selector);
427
+ if (byId) return byId;
428
+ const needle = selector.toLowerCase();
429
+ const matches = agents.filter((a) => a.slug && a.slug.toLowerCase() === needle || a.name.toLowerCase() === needle);
430
+ const [firstMatch, ...restMatches] = matches;
431
+ if (firstMatch && restMatches.length === 0) return firstMatch;
432
+ if (restMatches.length > 0) throw new Error(`Multiple agents match "${selector}" — pass the id instead. Candidates:\n${formatCandidates(matches)}`);
433
+ throw new Error(`No agent matches "${selector}". Candidates:\n${formatCandidates(agents)}`);
434
+ }
435
+ function formatCandidates(agents) {
436
+ return agents.slice(0, 25).map((a) => ` ${a.id} ${a.slug ?? a.name}`).join("\n");
437
+ }
438
+ /** Read all of stdin as UTF-8. Used when `-p` is passed with no value. */
439
+ async function readStdin() {
440
+ const chunks = [];
441
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
442
+ return Buffer.concat(chunks).toString("utf8");
443
+ }
444
+
445
+ //#endregion
446
+ export { createRestClient as i, resolveAgent as n, HttpError as r, print_exports as t };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "skydive-cli",
3
+ "version": "0.1.0-beta.45",
4
+ "description": "Skydive CLI — manage AI agents from the command line",
5
+ "homepage": "https://skydive.com",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "skydive": "./dist/js/bin.mjs"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "type": "module",
14
+ "exports": {
15
+ "./bin": "./dist/js/bin.mjs"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "registry": "https://registry.npmjs.org"
20
+ },
21
+ "scripts": {
22
+ "build": "tsdown --no-dts --out-dir dist/js",
23
+ "test:unit": "vitest run --passWithNoTests && yarn test:tui",
24
+ "test:ci": "vitest run --coverage --coverage.reporter=lcovonly --reporter=default --reporter=github-actions --minWorkers=1 --maxWorkers=2 --passWithNoTests && yarn test:tui",
25
+ "test:tui": "bun test .tui.test",
26
+ "render:frames": "bun scripts/render-frames.tsx",
27
+ "typecheck": "tsgo --noEmit"
28
+ },
29
+ "dependencies": {
30
+ "@opentui/react": "0.4.3",
31
+ "conf": "^13.0.1",
32
+ "diff": "9.0.0",
33
+ "eventsource-parser": "^3.0.8",
34
+ "file-type": "^21.3.4",
35
+ "neverthrow": "^8.2.0",
36
+ "open": "^10.1.0",
37
+ "react": "^19.0.0",
38
+ "react-devtools-core": "^7.0.1",
39
+ "web-tree-sitter": "0.25.10",
40
+ "ws": "^8.18.0",
41
+ "yargs": "^17.7.2",
42
+ "zod": "^3.24.1",
43
+ "zustand": "^5.0.2"
44
+ },
45
+ "devDependencies": {
46
+ "@createinc/anyone-portal-protocol": "0.0.0",
47
+ "@createinc/tsconfig": "0.0.0",
48
+ "@types/bun": "^1.3.14",
49
+ "@types/node": "^24.0.0",
50
+ "@types/react": "^19.0.0",
51
+ "@types/yargs": "^17.0.33",
52
+ "@typescript/native-preview": "^7.0.0-dev.20260113.1",
53
+ "@vitest/coverage-v8": "^2.1.8",
54
+ "tsdown": "^0.20.3",
55
+ "typescript": "^5.9.3",
56
+ "vitest": "^2.1.8"
57
+ },
58
+ "engines": {
59
+ "bun": ">=1.3.14",
60
+ "node": ">=20.0.0"
61
+ }
62
+ }