skydive-cli 0.1.0 → 0.2.0-beta.421

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.
@@ -2,23 +2,6 @@
2
2
  import { z } from "zod";
3
3
  import { createParser } from "eventsource-parser";
4
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
5
  //#region src/chat/api/rest.ts
23
6
  var HttpError = class extends Error {
24
7
  constructor(status, body) {
@@ -28,6 +11,20 @@ var HttpError = class extends Error {
28
11
  this.name = "HttpError";
29
12
  }
30
13
  };
14
+ const ERROR_DETAIL_MAX_BODY = 2e3;
15
+ /**
16
+ * Fullest renderable text for a thrown value. `HttpError.message` clips the
17
+ * response body to 200 chars (it flows into logs and one-line UIs); the
18
+ * transcript renders errors collapsed to a single line, so it can afford the
19
+ * whole body — capped with an explicit marker, never cut silently.
20
+ */
21
+ function errorDetail(err) {
22
+ if (err instanceof HttpError) {
23
+ const body = err.body.length > ERROR_DETAIL_MAX_BODY ? `${err.body.slice(0, ERROR_DETAIL_MAX_BODY)}… (+${err.body.length - ERROR_DETAIL_MAX_BODY} chars)` : err.body;
24
+ return body ? `HTTP ${err.status}: ${body}` : `HTTP ${err.status}`;
25
+ }
26
+ return err instanceof Error ? err.message : String(err);
27
+ }
31
28
  const MAX_STREAM_RECONNECTS = 5;
32
29
  function createRestClient({ appUrl, sessionToken }) {
33
30
  const baseHeaders = {
@@ -58,6 +55,65 @@ function createRestClient({ appUrl, sessionToken }) {
58
55
  });
59
56
  if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
60
57
  }
58
+ const streamEvents = async ({ path, label, signal, onEvent }) => {
59
+ let lastEventId = null;
60
+ let finished = false;
61
+ let reconnects = 0;
62
+ for (;;) {
63
+ if (signal.aborted) return;
64
+ try {
65
+ const headers = {
66
+ authorization: `Bearer ${sessionToken}`,
67
+ accept: "text/event-stream"
68
+ };
69
+ if (lastEventId) headers["last-event-id"] = lastEventId;
70
+ const res = await fetch(`${appUrl}${path}`, {
71
+ headers,
72
+ signal
73
+ });
74
+ if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
75
+ reconnects = 0;
76
+ const parser = createParser({ onEvent: (message) => {
77
+ if (message.id) lastEventId = message.id;
78
+ if (message.event === "error") {
79
+ const { error } = streamErrorSchema.parse(JSON.parse(message.data));
80
+ throw new Error(error);
81
+ }
82
+ const event = runStreamEventSchema.parse(JSON.parse(message.data));
83
+ if (event.kind === "finished") finished = true;
84
+ onEvent(event.kind === "finished" ? {
85
+ ...event,
86
+ error: event.error ?? null
87
+ } : event);
88
+ } });
89
+ const decoder = new TextDecoder();
90
+ const reader = res.body.getReader();
91
+ try {
92
+ for (;;) {
93
+ const { done, value } = await reader.read();
94
+ if (done) break;
95
+ parser.feed(decoder.decode(value, { stream: true }));
96
+ if (finished) return;
97
+ }
98
+ } finally {
99
+ try {
100
+ await reader.cancel();
101
+ } catch (_error) {}
102
+ }
103
+ } catch (err) {
104
+ if (signal.aborted) return;
105
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
106
+ reconnects += 1;
107
+ if (reconnects > MAX_STREAM_RECONNECTS) throw err;
108
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
109
+ continue;
110
+ }
111
+ if (finished) return;
112
+ reconnects += 1;
113
+ if (reconnects > MAX_STREAM_RECONNECTS) throw new Error(`${label} stream ended unexpectedly`);
114
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
115
+ }
116
+ };
61
117
  return {
62
118
  listAgents: async ({ scope, onPage }) => {
63
119
  const all = [];
@@ -94,19 +150,51 @@ function createRestClient({ appUrl, sessionToken }) {
94
150
  const { agent } = await post(`/api/v1/agents/${encodeURIComponent(agentId)}`, { model }, updateAgentResponseSchema, "PATCH");
95
151
  return { model: agent.model ?? null };
96
152
  },
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;
153
+ listConversations: async ({ agentId, limit, channels, onPage }) => {
154
+ const all = [];
155
+ const maxConversations = limit ?? 5e3;
156
+ let cursor;
157
+ do {
158
+ const remaining = maxConversations - all.length;
159
+ const params = new URLSearchParams({
160
+ agentId,
161
+ includeTotal: "false"
162
+ });
163
+ params.set("limit", String(Math.min(remaining, 100)));
164
+ if (cursor) params.set("cursor", cursor);
165
+ for (const channel of channels ?? []) params.append("channels", channel);
166
+ const page = await get(`/api/v1/conversations?${params.toString()}`, listConversationsResponseSchema);
167
+ all.push(...page.conversations);
168
+ cursor = page.nextCursor ?? void 0;
169
+ onPage?.(limit ? all.slice(0, limit) : [...all]);
170
+ } while (cursor && all.length < maxConversations);
171
+ return limit ? all.slice(0, limit) : all;
105
172
  },
106
173
  listMessages: async ({ conversationId }) => {
107
174
  const { messages } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`, listMessagesResponseSchema);
108
175
  return messages;
109
176
  },
177
+ listWorkspaceFiles: async ({ agentId }) => {
178
+ const { files } = await get(`/api/v1/workspace-files?${new URLSearchParams({ agentId }).toString()}`, listWorkspaceFilesResponseSchema);
179
+ return files;
180
+ },
181
+ readWorkspaceFile: async ({ fileId, maxBytes = 512 * 1024 }) => {
182
+ const res = await fetch(`${appUrl}/api/v1/workspace-files/${encodeURIComponent(fileId)}/download?disposition=inline`, { headers: {
183
+ ...baseHeaders,
184
+ Range: `bytes=0-${maxBytes}`
185
+ } });
186
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
187
+ const bytes = new Uint8Array(await res.arrayBuffer());
188
+ const truncated = bytes.byteLength > maxBytes;
189
+ return {
190
+ text: new TextDecoder().decode(bytes.subarray(0, maxBytes)),
191
+ truncated
192
+ };
193
+ },
194
+ getRecap: async ({ conversationId }) => {
195
+ const { recap } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/recap`, recapResponseSchema);
196
+ return recap?.text ?? null;
197
+ },
110
198
  uploadAttachment: async ({ agentId, fileName, mediaType, data }) => {
111
199
  const size = data.byteLength;
112
200
  const presign = await post("/api/v1/attachments/presign", {
@@ -137,11 +225,10 @@ function createRestClient({ appUrl, sessionToken }) {
137
225
  deleteConversation: async ({ conversationId }) => {
138
226
  await del(`/api/v1/conversations/${encodeURIComponent(conversationId)}`);
139
227
  },
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
- },
228
+ sendMessage: async ({ clientSurface, ...input }) => post("/api/v1/chat/send", {
229
+ ...input,
230
+ clientSurface
231
+ }, sendResultSchema),
145
232
  cancelRun: async ({ runId }) => {
146
233
  await post(`/api/v1/chat/runs/${encodeURIComponent(runId)}/cancel`, {}, z.object({ ok: z.boolean() }));
147
234
  },
@@ -168,36 +255,35 @@ function createRestClient({ appUrl, sessionToken }) {
168
255
  });
169
256
  if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
170
257
  },
171
- streamRun: async ({ runId, signal, onEvent }) => {
172
- let lastEventId = null;
173
- let finished = false;
258
+ streamRun: async ({ runId, signal, onEvent }) => streamEvents({
259
+ path: `/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`,
260
+ label: "run",
261
+ signal,
262
+ onEvent
263
+ }),
264
+ streamMessage: async ({ messageId, signal, onEvent }) => streamEvents({
265
+ path: `/api/v1/chat/messages/${encodeURIComponent(messageId)}/stream`,
266
+ label: "message",
267
+ signal,
268
+ onEvent
269
+ }),
270
+ streamConversation: async ({ conversationId, signal, onEvent }) => {
174
271
  let reconnects = 0;
175
272
  for (;;) {
176
273
  if (signal.aborted) return;
177
274
  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,
275
+ const res = await fetch(`${appUrl}/api/v1/chat/conversations/${encodeURIComponent(conversationId)}/stream`, {
276
+ headers: {
277
+ authorization: `Bearer ${sessionToken}`,
278
+ accept: "text/event-stream"
279
+ },
185
280
  signal
186
281
  });
187
282
  if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
188
283
  reconnects = 0;
189
284
  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);
285
+ const parsed = conversationStreamEventSchema.safeParse(JSON.parse(message.data));
286
+ if (parsed.success) onEvent(parsed.data);
201
287
  } });
202
288
  const decoder = new TextDecoder();
203
289
  const reader = res.body.getReader();
@@ -206,7 +292,6 @@ function createRestClient({ appUrl, sessionToken }) {
206
292
  const { done, value } = await reader.read();
207
293
  if (done) break;
208
294
  parser.feed(decoder.decode(value, { stream: true }));
209
- if (finished) return;
210
295
  }
211
296
  } finally {
212
297
  try {
@@ -221,9 +306,9 @@ function createRestClient({ appUrl, sessionToken }) {
221
306
  await sleep(Math.min(500 * 2 ** reconnects, 5e3));
222
307
  continue;
223
308
  }
224
- if (finished) return;
309
+ if (signal.aborted) return;
225
310
  reconnects += 1;
226
- if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("run stream ended unexpectedly");
311
+ if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("conversation stream ended unexpectedly");
227
312
  await sleep(Math.min(500 * 2 ** reconnects, 5e3));
228
313
  }
229
314
  }
@@ -273,11 +358,14 @@ const conversationSummarySchema = z.object({
273
358
  title: z.string().nullable().optional()
274
359
  })
275
360
  });
276
- const conversationTitleSchema = z.object({
361
+ const conversationDetailSchema = z.object({
277
362
  id: z.string().uuid(),
278
- title: z.string().nullable()
363
+ title: z.string().nullable(),
364
+ agentId: z.string().uuid(),
365
+ createdAt: z.string(),
366
+ updatedAt: z.string()
279
367
  });
280
- const getConversationResponseSchema = z.object({ conversation: conversationTitleSchema });
368
+ const getConversationResponseSchema = z.object({ conversation: conversationDetailSchema });
281
369
  const listConversationsResponseSchema = z.object({
282
370
  conversations: z.array(conversationSummarySchema),
283
371
  nextCursor: z.string().nullable().optional(),
@@ -306,13 +394,31 @@ const uiMessagePartSchema = z.union([
306
394
  const uiMessageSchema = z.object({
307
395
  id: z.string(),
308
396
  role: z.string(),
309
- parts: z.array(uiMessagePartSchema)
397
+ parts: z.array(uiMessagePartSchema),
398
+ metadata: z.object({ custom: z.object({
399
+ agentId: z.string().nullish(),
400
+ agentName: z.string().nullish()
401
+ }).passthrough().optional() }).passthrough().optional()
310
402
  });
403
+ const recapResponseSchema = z.object({ recap: z.object({ text: z.string() }).nullable() });
311
404
  const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
405
+ const workspaceFileSchema = z.object({
406
+ id: z.string(),
407
+ path: z.string(),
408
+ mediaType: z.string(),
409
+ sizeBytes: z.number(),
410
+ contentHash: z.string(),
411
+ updatedAt: z.string(),
412
+ shareUrl: z.string()
413
+ });
414
+ const listWorkspaceFilesResponseSchema = z.object({ files: z.array(workspaceFileSchema) });
312
415
  const sendResultSchema = z.object({
313
416
  runId: z.string(),
417
+ messageId: z.string().uuid().nullish(),
314
418
  conversationId: z.string().uuid(),
315
419
  isNewConversation: z.boolean(),
420
+ agentId: z.string().nullish(),
421
+ agentName: z.string().nullish(),
316
422
  steered: z.boolean().optional(),
317
423
  directive: z.object({ id: z.string() }).passthrough().optional()
318
424
  });
@@ -327,7 +433,6 @@ const finalizeResponseSchema = z.object({
327
433
  mediaType: z.string(),
328
434
  sizeBytes: z.number().nullable().optional()
329
435
  });
330
- const activeRunResponseSchema = z.object({ run: z.object({ runId: z.string() }).nullable() });
331
436
  const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
332
437
  const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
333
438
  const runStreamEventSchema = z.union([z.object({
@@ -339,104 +444,14 @@ const runStreamEventSchema = z.union([z.object({
339
444
  error: z.string().nullish()
340
445
  })]);
341
446
  const streamErrorSchema = z.object({ error: z.string() });
447
+ const conversationStreamEventSchema = z.discriminatedUnion("kind", [z.object({
448
+ kind: z.literal("conversation"),
449
+ id: z.string(),
450
+ title: z.string().nullable()
451
+ }), z.object({
452
+ kind: z.literal("run"),
453
+ runId: z.string()
454
+ })]);
342
455
 
343
456
  //#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 };
457
+ export { createRestClient as n, errorDetail as r, HttpError as t };