skydive-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,442 @@
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 (input) => post("/api/v1/chat/send", input, sendResultSchema),
141
+ activeRun: async ({ conversationId }) => {
142
+ const { run } = await get(`/api/v1/chat/active-run?${new URLSearchParams({ conversationId }).toString()}`, activeRunResponseSchema);
143
+ return run;
144
+ },
145
+ cancelRun: async ({ runId }) => {
146
+ await post(`/api/v1/chat/runs/${encodeURIComponent(runId)}/cancel`, {}, z.object({ ok: z.boolean() }));
147
+ },
148
+ cancelSteer: async ({ directiveId }) => {
149
+ await post(`/api/v1/chat/steer/${encodeURIComponent(directiveId)}/cancel`, {}, z.object({ ok: z.boolean() }));
150
+ },
151
+ oauthConnect: async (input) => {
152
+ const { connectLink } = await post("/api/v1/oauth/connect", input, oauthConnectResponseSchema);
153
+ return { connectLink };
154
+ },
155
+ externalOauthConnect: async (input) => {
156
+ const { authorizationUrl } = await post("/api/v1/external-oauth/connect", input, externalOauthConnectResponseSchema);
157
+ return { authorizationUrl: authorizationUrl ?? null };
158
+ },
159
+ fulfillCredential: async ({ url, body }) => {
160
+ const target = new URL(url, appUrl).toString();
161
+ const res = await fetch(target, {
162
+ method: "POST",
163
+ headers: {
164
+ ...baseHeaders,
165
+ "content-type": "application/json"
166
+ },
167
+ body: JSON.stringify(body)
168
+ });
169
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
170
+ },
171
+ streamRun: async ({ runId, signal, onEvent }) => {
172
+ let lastEventId = null;
173
+ let finished = false;
174
+ let reconnects = 0;
175
+ for (;;) {
176
+ if (signal.aborted) return;
177
+ try {
178
+ const headers = {
179
+ authorization: `Bearer ${sessionToken}`,
180
+ accept: "text/event-stream"
181
+ };
182
+ if (lastEventId) headers["last-event-id"] = lastEventId;
183
+ const res = await fetch(`${appUrl}/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`, {
184
+ headers,
185
+ signal
186
+ });
187
+ if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
188
+ reconnects = 0;
189
+ const parser = createParser({ onEvent: (message) => {
190
+ if (message.id) lastEventId = message.id;
191
+ if (message.event === "error") {
192
+ const { error } = streamErrorSchema.parse(JSON.parse(message.data));
193
+ throw new Error(error);
194
+ }
195
+ const event = runStreamEventSchema.parse(JSON.parse(message.data));
196
+ if (event.kind === "finished") finished = true;
197
+ onEvent(event.kind === "finished" ? {
198
+ ...event,
199
+ error: event.error ?? null
200
+ } : event);
201
+ } });
202
+ const decoder = new TextDecoder();
203
+ const reader = res.body.getReader();
204
+ try {
205
+ for (;;) {
206
+ const { done, value } = await reader.read();
207
+ if (done) break;
208
+ parser.feed(decoder.decode(value, { stream: true }));
209
+ if (finished) return;
210
+ }
211
+ } finally {
212
+ try {
213
+ await reader.cancel();
214
+ } catch (_error) {}
215
+ }
216
+ } catch (err) {
217
+ if (signal.aborted) return;
218
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
219
+ reconnects += 1;
220
+ if (reconnects > MAX_STREAM_RECONNECTS) throw err;
221
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
222
+ continue;
223
+ }
224
+ if (finished) return;
225
+ reconnects += 1;
226
+ if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("run stream ended unexpectedly");
227
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
228
+ }
229
+ }
230
+ };
231
+ }
232
+ function sleep(ms) {
233
+ return new Promise((resolve) => setTimeout(resolve, ms));
234
+ }
235
+ const agentSummarySchema = z.object({
236
+ id: z.string().uuid(),
237
+ name: z.string(),
238
+ slug: z.string().nullable().optional(),
239
+ title: z.string().nullable().optional(),
240
+ description: z.string().nullable().optional(),
241
+ createdAt: z.string(),
242
+ creatorName: z.string().nullable().optional(),
243
+ model: z.string().nullable().optional(),
244
+ modelLocked: z.boolean().optional()
245
+ });
246
+ const platformModelSchema = z.object({
247
+ id: z.string(),
248
+ displayName: z.string(),
249
+ providerDisplay: z.string().optional(),
250
+ reasoning: z.boolean().optional(),
251
+ compliant: z.boolean().optional()
252
+ }).passthrough();
253
+ const listModelsResponseSchema = z.object({ models: z.array(platformModelSchema) });
254
+ const updateAgentResponseSchema = z.object({ agent: z.object({ model: z.string().nullable().optional() }).passthrough() });
255
+ const listAgentsResponseSchema = z.object({
256
+ agents: z.array(agentSummarySchema),
257
+ nextCursor: z.string().nullable().optional(),
258
+ totalCount: z.number().nullable().optional()
259
+ });
260
+ const createAgentResponseSchema = z.object({ agent: agentSummarySchema });
261
+ const conversationSummarySchema = z.object({
262
+ id: z.string().uuid(),
263
+ title: z.string().nullable(),
264
+ createdAt: z.string(),
265
+ updatedAt: z.string(),
266
+ preview: z.string().nullable(),
267
+ channel: z.string().nullable(),
268
+ channelLabel: z.string().nullable(),
269
+ agent: z.object({
270
+ id: z.string().uuid(),
271
+ name: z.string(),
272
+ slug: z.string().nullable().optional(),
273
+ title: z.string().nullable().optional()
274
+ })
275
+ });
276
+ const conversationTitleSchema = z.object({
277
+ id: z.string().uuid(),
278
+ title: z.string().nullable()
279
+ });
280
+ const getConversationResponseSchema = z.object({ conversation: conversationTitleSchema });
281
+ const listConversationsResponseSchema = z.object({
282
+ conversations: z.array(conversationSummarySchema),
283
+ nextCursor: z.string().nullable().optional(),
284
+ totalCount: z.number().optional()
285
+ });
286
+ const uiMessagePartSchema = z.union([
287
+ z.object({
288
+ type: z.literal("text"),
289
+ text: z.string()
290
+ }),
291
+ z.object({
292
+ type: z.literal("reasoning"),
293
+ text: z.string().optional()
294
+ }),
295
+ z.object({
296
+ type: z.literal("dynamic-tool"),
297
+ toolCallId: z.string(),
298
+ toolName: z.string(),
299
+ input: z.unknown().optional(),
300
+ output: z.unknown().optional(),
301
+ state: z.string().optional(),
302
+ errorText: z.string().optional()
303
+ }),
304
+ z.object({ type: z.string() }).passthrough()
305
+ ]);
306
+ const uiMessageSchema = z.object({
307
+ id: z.string(),
308
+ role: z.string(),
309
+ parts: z.array(uiMessagePartSchema)
310
+ });
311
+ const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
312
+ const sendResultSchema = z.object({
313
+ runId: z.string(),
314
+ conversationId: z.string().uuid(),
315
+ isNewConversation: z.boolean(),
316
+ steered: z.boolean().optional(),
317
+ directive: z.object({ id: z.string() }).passthrough().optional()
318
+ });
319
+ const presignResponseSchema = z.object({
320
+ id: z.string(),
321
+ uploadUrl: z.string(),
322
+ fileName: z.string(),
323
+ mediaType: z.string()
324
+ });
325
+ const finalizeResponseSchema = z.object({
326
+ fileName: z.string(),
327
+ mediaType: z.string(),
328
+ sizeBytes: z.number().nullable().optional()
329
+ });
330
+ const activeRunResponseSchema = z.object({ run: z.object({ runId: z.string() }).nullable() });
331
+ const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
332
+ const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
333
+ const runStreamEventSchema = z.union([z.object({
334
+ kind: z.literal("chunk"),
335
+ chunk: z.record(z.unknown())
336
+ }), z.object({
337
+ kind: z.literal("finished"),
338
+ status: z.string(),
339
+ error: z.string().nullish()
340
+ })]);
341
+ const streamErrorSchema = z.object({ error: z.string() });
342
+
343
+ //#endregion
344
+ //#region src/chat/print.ts
345
+ var print_exports = /* @__PURE__ */ __exportAll({
346
+ readStdin: () => readStdin,
347
+ resolveAgent: () => resolveAgent,
348
+ runPrint: () => runPrint
349
+ });
350
+ /**
351
+ * Non-interactive chat, à la `claude -p`. Sends a single prompt to an
352
+ * agent, streams the run, and prints the assistant's reply to stdout
353
+ * before exiting. No OpenTUI, no Bun requirement — this rides the same
354
+ * Node-friendly REST client the TUI uses, so it runs anywhere the
355
+ * management commands do (CI, pipes, scripts).
356
+ *
357
+ * Resolution rules kept deliberately strict because there's no human to
358
+ * disambiguate: an `--agent` selector must match exactly one agent, and
359
+ * when it's omitted we only auto-pick if the account has exactly one.
360
+ */
361
+ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversationId, json }) {
362
+ const client = createRestClient({
363
+ appUrl,
364
+ sessionToken
365
+ });
366
+ const agent = resolveAgent(await client.listAgents({
367
+ scope: "org",
368
+ onPage: null
369
+ }), agentSelector);
370
+ const send = await client.sendMessage({
371
+ agentId: agent.id,
372
+ conversationId,
373
+ content: prompt,
374
+ attachmentIds: []
375
+ });
376
+ let text = "";
377
+ const controller = new AbortController();
378
+ let streamError = null;
379
+ await client.streamRun({
380
+ runId: send.runId,
381
+ signal: controller.signal,
382
+ onEvent: (event) => {
383
+ if (event.kind === "finished") {
384
+ if (event.error) streamError = event.error;
385
+ return;
386
+ }
387
+ const chunk = event.chunk;
388
+ if (chunk["type"] === "text-delta") {
389
+ const delta = typeof chunk["delta"] === "string" ? chunk["delta"] : typeof chunk["text"] === "string" ? chunk["text"] : "";
390
+ if (delta) {
391
+ text += delta;
392
+ if (!json) process.stdout.write(delta);
393
+ }
394
+ } else if (chunk["type"] === "error") streamError = typeof chunk["errorText"] === "string" ? chunk["errorText"] : "unknown error";
395
+ }
396
+ });
397
+ if (streamError) throw new Error(streamError);
398
+ if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
399
+ return {
400
+ agentId: agent.id,
401
+ agentName: agent.name,
402
+ conversationId: send.conversationId,
403
+ isNewConversation: send.isNewConversation,
404
+ runId: send.runId,
405
+ text
406
+ };
407
+ }
408
+ /**
409
+ * Pick the target agent. With no selector, auto-pick only when the
410
+ * account has exactly one agent; otherwise the user must name one (there's
411
+ * no picker in non-interactive mode). A selector matches by id first, then
412
+ * a unique case-insensitive slug/name; ambiguous or missing matches throw
413
+ * with the candidate list so the caller knows what to pass.
414
+ */
415
+ function resolveAgent(agents, selector) {
416
+ if (!selector) {
417
+ const [only, ...rest] = agents;
418
+ if (!only) throw new Error("No agents on this account.");
419
+ if (rest.length === 0) return only;
420
+ throw new Error(`Multiple agents on this account — pass --agent <id|slug|name>. Candidates:\n${formatCandidates(agents)}`);
421
+ }
422
+ const byId = agents.find((a) => a.id === selector);
423
+ if (byId) return byId;
424
+ const needle = selector.toLowerCase();
425
+ const matches = agents.filter((a) => a.slug && a.slug.toLowerCase() === needle || a.name.toLowerCase() === needle);
426
+ const [firstMatch, ...restMatches] = matches;
427
+ if (firstMatch && restMatches.length === 0) return firstMatch;
428
+ if (restMatches.length > 0) throw new Error(`Multiple agents match "${selector}" — pass the id instead. Candidates:\n${formatCandidates(matches)}`);
429
+ throw new Error(`No agent matches "${selector}". Candidates:\n${formatCandidates(agents)}`);
430
+ }
431
+ function formatCandidates(agents) {
432
+ return agents.slice(0, 25).map((a) => ` ${a.id} ${a.slug ?? a.name}`).join("\n");
433
+ }
434
+ /** Read all of stdin as UTF-8. Used when `-p` is passed with no value. */
435
+ async function readStdin() {
436
+ const chunks = [];
437
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
438
+ return Buffer.concat(chunks).toString("utf8");
439
+ }
440
+
441
+ //#endregion
442
+ export { createRestClient as i, resolveAgent as n, HttpError as r, print_exports as t };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "skydive-cli",
3
+ "version": "0.1.0",
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",
24
+ "test:ci": "vitest run --coverage --coverage.reporter=lcovonly --reporter=default --reporter=github-actions --minWorkers=1 --maxWorkers=2 --passWithNoTests",
25
+ "typecheck": "tsgo --noEmit"
26
+ },
27
+ "dependencies": {
28
+ "@opentui/react": "0.4.3",
29
+ "conf": "^13.0.1",
30
+ "diff": "9.0.0",
31
+ "eventsource-parser": "^3.0.8",
32
+ "neverthrow": "^8.2.0",
33
+ "open": "^10.1.0",
34
+ "react": "^19.0.0",
35
+ "react-devtools-core": "^7.0.1",
36
+ "web-tree-sitter": "0.25.10",
37
+ "ws": "^8.18.0",
38
+ "yargs": "^17.7.2",
39
+ "zod": "^3.24.1",
40
+ "zustand": "^5.0.2"
41
+ },
42
+ "devDependencies": {
43
+ "@createinc/anyone-portal-protocol": "workspace:*",
44
+ "@createinc/tsconfig": "workspace:*",
45
+ "@types/node": "^24.0.0",
46
+ "@types/react": "^19.0.0",
47
+ "@types/yargs": "^17.0.33",
48
+ "@typescript/native-preview": "^7.0.0-dev.20260113.1",
49
+ "@vitest/coverage-v8": "^2.1.8",
50
+ "tsdown": "^0.20.3",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^2.1.8"
53
+ },
54
+ "engines": {
55
+ "node": ">=20.0.0"
56
+ }
57
+ }