skydive-cli 0.1.0-beta.382 → 0.1.0-beta.389

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.
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { A as HttpError, B as getSavedTheme, C as noColorRequested, D as themeModeFromColorFgBg, E as themeMode, F as setActiveWorkspace, H as saveTheme, I as DEFAULT_API_URL, L as DEFAULT_APP_URL, M as errorDetail, N as getActiveWorkspaceId, O as themeVersion, P as listWorkspaces, R as getConfigPath, S as monoTheme, T as themeForMode, V as resolveWebUrl, _ as errorMessage, b as applyTheme, d as cardActionErrorMessage, f as parseExternalOauthConnectParams, g as parseConnectCard, h as resolveConnectUrl, j as createRestClient, k as themesForMode, l as resolveAgent, m as reconcileMaskedInput, p as parseOauthConnectParams, t as SandboxStream, u as MASK_CHAR, v as isRecord, w as theme, x as findTheme, y as DEFAULT_THEME_ID, z as getReviewStateDir } from "./bin.mjs";
3
- import { t as PortalClient } from "./client-XFsd0Wy9.mjs";
4
- import { n as runRawPtyPassthrough } from "./raw-pty-C1DXKms6.mjs";
2
+ import { D as getReviewStateDir, I as resolveWebUrl, O as getSavedTheme, S as DEFAULT_APP_URL, T as getConfigPath, _ as setActiveWorkspace, a as noColorRequested, c as themeMode, f as themesForMode, g as listWorkspaces, i as monoTheme, l as themeModeFromColorFgBg, m as getActiveWorkspaceId, n as applyTheme, o as theme, r as findTheme, s as themeForMode, t as DEFAULT_THEME_ID, u as themeVersion, x as DEFAULT_API_URL, z as saveTheme } from "./theme-DRuLtrTy.mjs";
3
+ import { n as createRestClient, r as errorDetail, t as HttpError } from "./rest-DTlkPko_.mjs";
4
+ import { n as isRecord, t as errorMessage } from "./util-z9Pne47f.mjs";
5
+ import { c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams } from "./print-BhfjRrxI.mjs";
6
+ import "./api-DRpbKHz6.mjs";
7
+ import { t as SandboxStream } from "./client-BVOAwU8M.mjs";
8
+ import { t as PortalClient } from "./client-DyRs3o5E.mjs";
9
+ import { t as runRawPtyPassthrough } from "./raw-pty-Dbi2kb9v.mjs";
5
10
  import * as os$1 from "node:os";
6
11
  import { homedir, platform, release, tmpdir } from "node:os";
7
12
  import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ import { WebSocket } from "ws";
3
+
4
+ //#region ../sandbox-stream-protocol/src/index.ts
5
+ const SANDBOX_STREAM_PATH = "/api/v1/sandbox/stream";
6
+ const FRAME = {
7
+ DATA: 1,
8
+ EXIT: 2,
9
+ ERROR: 3,
10
+ INPUT: 16,
11
+ RESIZE: 17
12
+ };
13
+ const MAX_INPUT_BYTES = 1 * 1024 * 1024;
14
+ /** Query params for the upgrade URL, from a spec. Inverse of {@link parseStreamSpec}. */
15
+ function streamSpecToQuery(spec) {
16
+ if (spec.mode === "pty") return {
17
+ agentId: spec.agentId,
18
+ mode: "pty",
19
+ cols: String(spec.cols),
20
+ rows: String(spec.rows)
21
+ };
22
+ return {
23
+ agentId: spec.agentId,
24
+ mode: "exec",
25
+ command: spec.command
26
+ };
27
+ }
28
+ function withType(type, payload) {
29
+ const frame = new Uint8Array(1 + payload.length);
30
+ frame[0] = type;
31
+ frame.set(payload, 1);
32
+ return frame;
33
+ }
34
+ /** client → server: keystroke bytes for the pty stdin. */
35
+ function encodeInput(data) {
36
+ return withType(FRAME.INPUT, data);
37
+ }
38
+ /** client → server: the client terminal was resized. */
39
+ function encodeResize(cols, rows) {
40
+ const frame = new Uint8Array(5);
41
+ frame[0] = FRAME.RESIZE;
42
+ const view = new DataView(frame.buffer);
43
+ view.setUint16(1, cols & 65535);
44
+ view.setUint16(3, rows & 65535);
45
+ return frame;
46
+ }
47
+ const view = (frame) => new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
48
+ /**
49
+ * Decode a frame the server sent. Returns null for an empty, unknown, or
50
+ * truncated frame — a peer speaking a newer protocol must not crash us.
51
+ */
52
+ function decodeServerFrame(frame) {
53
+ const payload = frame.subarray(1);
54
+ switch (frame[0]) {
55
+ case FRAME.DATA: return {
56
+ type: "data",
57
+ payload
58
+ };
59
+ case FRAME.EXIT: return {
60
+ type: "exit",
61
+ code: payload.length >= 4 ? view(frame).getInt32(1) : 0
62
+ };
63
+ case FRAME.ERROR: return {
64
+ type: "error",
65
+ message: new TextDecoder().decode(payload)
66
+ };
67
+ default: return null;
68
+ }
69
+ }
70
+
71
+ //#endregion
72
+ //#region src/chat/sandbox/client.ts
73
+ function wsBase(appUrl) {
74
+ const base = appUrl.replace(/\/+$/, "");
75
+ if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
76
+ if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
77
+ return `wss://${base}`;
78
+ }
79
+ /**
80
+ * A connected sandbox-stream session. Construct via `openSandboxStream`. Carries
81
+ * the write side (keystrokes / resize for pty mode) and teardown.
82
+ */
83
+ var SandboxStream = class SandboxStream {
84
+ ws;
85
+ closed = false;
86
+ constructor(ws, onEvent) {
87
+ this.ws = ws;
88
+ let ended = false;
89
+ const emitEnd = (event) => {
90
+ if (ended) return;
91
+ ended = true;
92
+ onEvent(event);
93
+ };
94
+ ws.on("message", (data, isBinary) => {
95
+ if (!isBinary) return;
96
+ const frame = decodeServerFrame(toBuffer(data));
97
+ if (!frame) return;
98
+ switch (frame.type) {
99
+ case "data":
100
+ onEvent({
101
+ type: "data",
102
+ bytes: new Uint8Array(frame.payload)
103
+ });
104
+ break;
105
+ case "exit":
106
+ emitEnd({
107
+ type: "exit",
108
+ code: frame.code
109
+ });
110
+ break;
111
+ case "error":
112
+ emitEnd({
113
+ type: "error",
114
+ message: frame.message
115
+ });
116
+ break;
117
+ }
118
+ });
119
+ let failure = null;
120
+ ws.on("error", (err) => {
121
+ failure = err.message;
122
+ });
123
+ ws.on("close", () => {
124
+ this.closed = true;
125
+ emitEnd({
126
+ type: "close",
127
+ failure
128
+ });
129
+ });
130
+ }
131
+ /** Feed keystroke bytes to the pty stdin. */
132
+ sendInput(data) {
133
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
134
+ this.ws.send(encodeInput(data));
135
+ }
136
+ /** Notify the pty of a terminal resize. */
137
+ resize(cols, rows) {
138
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
139
+ this.ws.send(encodeResize(cols, rows));
140
+ }
141
+ close() {
142
+ this.closed = true;
143
+ this.ws.close();
144
+ }
145
+ /** Open a stream. `mode` is 'pty' (interactive) or 'exec' (one-shot). */
146
+ static open(opts) {
147
+ const spec = opts.mode === "pty" ? {
148
+ mode: "pty",
149
+ agentId: opts.agentId,
150
+ cols: opts.cols,
151
+ rows: opts.rows
152
+ } : {
153
+ mode: "exec",
154
+ agentId: opts.agentId,
155
+ command: opts.command
156
+ };
157
+ const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
158
+ for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
159
+ return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
160
+ }
161
+ };
162
+ function toBuffer(data) {
163
+ if (Buffer.isBuffer(data)) return data;
164
+ if (Array.isArray(data)) return Buffer.concat(data);
165
+ return Buffer.from(data);
166
+ }
167
+
168
+ //#endregion
169
+ export { SandboxStream as t };
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-DTlkPko_.mjs";
3
+ import "./api-DRpbKHz6.mjs";
4
+ import { t as PortalClient } from "./client-DyRs3o5E.mjs";
5
+
6
+ export { PortalClient };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { t as __exportAll } from "./rolldown-runtime-Cz4Tg37Z.mjs";
3
- import { _ as errorMessage, a as mintPortalDeviceToken, c as portalWsUrl, i as grantPortalAccess, n as fetchPortalDevices, o as buildEnv, r as findThisDevice, s as machineIdentity } from "./bin.mjs";
2
+ import { t as errorMessage } from "./util-z9Pne47f.mjs";
3
+ import { c as machineIdentity, i as mintPortalDeviceToken, l as portalWsUrl, n as findThisDevice, r as grantPortalAccess, s as buildEnv, t as fetchPortalDevices } from "./api-DRpbKHz6.mjs";
4
4
  import { z } from "zod";
5
5
  import { spawn } from "node:child_process";
6
6
  import { WebSocket } from "ws";
@@ -196,7 +196,6 @@ var JobManager = class {
196
196
 
197
197
  //#endregion
198
198
  //#region src/chat/portal/client.ts
199
- var client_exports = /* @__PURE__ */ __exportAll({ PortalClient: () => PortalClient });
200
199
  const INITIAL_BACKOFF_MS = 500;
201
200
  const MAX_BACKOFF_MS = 1e4;
202
201
  /**
@@ -364,4 +363,4 @@ function sleep(ms) {
364
363
  }
365
364
 
366
365
  //#endregion
367
- export { client_exports as n, PortalClient as t };
366
+ export { PortalClient as t };
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env node
2
+ import { n as createRestClient, t as HttpError } from "./rest-DTlkPko_.mjs";
3
+ import { n as isRecord, t as errorMessage } from "./util-z9Pne47f.mjs";
4
+
5
+ //#region src/chat/tui/chat/card.ts
6
+ const urlActionKinds = [
7
+ "open_oauth",
8
+ "open_external_oauth",
9
+ "open_github_app",
10
+ "submit_credential"
11
+ ];
12
+ function isUrlActionKind(value) {
13
+ return typeof value === "string" && urlActionKinds.includes(value);
14
+ }
15
+ function optionalString(value) {
16
+ return typeof value === "string" && value ? value : null;
17
+ }
18
+ function bindStateKey(props) {
19
+ const value = props.value;
20
+ if (!isRecord(value)) return null;
21
+ const pointer = value.$bindState;
22
+ if (typeof pointer !== "string") return null;
23
+ return pointer.startsWith("/") ? pointer.slice(1) : pointer;
24
+ }
25
+ function parseButton(element) {
26
+ const props = isRecord(element.props) ? element.props : {};
27
+ const label = optionalString(props.label) ?? "Connect";
28
+ const on = isRecord(element.on) ? element.on : null;
29
+ const press = on && isRecord(on.press) ? on.press : null;
30
+ if (!press) return null;
31
+ const params = isRecord(press.params) ? press.params : {};
32
+ const primary = props.variant === "primary";
33
+ if (press.action === "approve_portal_access") {
34
+ const agentId = params.agentId;
35
+ if (typeof agentId !== "string" || !agentId) return null;
36
+ return {
37
+ label,
38
+ action: {
39
+ kind: "grant_portal",
40
+ agentId,
41
+ deviceId: typeof params.deviceId === "string" && params.deviceId ? params.deviceId : null
42
+ },
43
+ primary
44
+ };
45
+ }
46
+ if (!isUrlActionKind(press.action)) return null;
47
+ const url = params.url;
48
+ if (typeof url !== "string" || !url) return null;
49
+ return {
50
+ label,
51
+ action: {
52
+ kind: press.action,
53
+ url
54
+ },
55
+ primary
56
+ };
57
+ }
58
+ function parseConnectCard(spec) {
59
+ if (!isRecord(spec)) return null;
60
+ const { root, elements } = spec;
61
+ if (typeof root !== "string" || !isRecord(elements)) return null;
62
+ const rootEl = elements[root];
63
+ if (!isRecord(rootEl) || rootEl.type !== "Card") return null;
64
+ const rootProps = isRecord(rootEl.props) ? rootEl.props : {};
65
+ const title = optionalString(rootProps.title);
66
+ if (!title) return null;
67
+ const fields = [];
68
+ const buttons = [];
69
+ const children = Array.isArray(rootEl.children) ? rootEl.children : [];
70
+ for (const childId of children) {
71
+ if (typeof childId !== "string") continue;
72
+ const el = elements[childId];
73
+ if (!isRecord(el)) continue;
74
+ if (el.type === "TextInput" || el.type === "SecretInput") {
75
+ const props = isRecord(el.props) ? el.props : {};
76
+ const key = bindStateKey(props);
77
+ if (!key) continue;
78
+ fields.push({
79
+ key,
80
+ label: optionalString(props.label) ?? key,
81
+ placeholder: optionalString(props.placeholder),
82
+ secret: el.type === "SecretInput"
83
+ });
84
+ continue;
85
+ }
86
+ if (el.type === "Button") {
87
+ const button = parseButton(el);
88
+ if (button) buttons.push(button);
89
+ }
90
+ }
91
+ const preferred = buttons.find((b) => b.primary) ?? buttons[0] ?? null;
92
+ return {
93
+ title,
94
+ subtitle: optionalString(rootProps.subtitle),
95
+ description: optionalString(rootProps.description),
96
+ fields,
97
+ button: preferred ? {
98
+ label: preferred.label,
99
+ action: preferred.action
100
+ } : null,
101
+ state: isRecord(spec.state) ? spec.state : {}
102
+ };
103
+ }
104
+
105
+ //#endregion
106
+ //#region src/chat/tui/chat/card-actions.ts
107
+ function safeUrl(url) {
108
+ try {
109
+ return new URL(url, "http://localhost");
110
+ } catch (_error) {
111
+ return null;
112
+ }
113
+ }
114
+ /**
115
+ * Resolve a connect URL to an absolute one the OS can open in a browser.
116
+ *
117
+ * The server builds lazy connect links as server-relative paths (e.g.
118
+ * `/api/v1/oauth/start/slack?connect_session_token=...`). The web client
119
+ * resolves these against its own origin implicitly; the TUI runs outside a
120
+ * browser, so `open()` on a scheme-less path is a silent no-op — the browser
121
+ * never launches and the connect card sits in `launched` forever (the Slack
122
+ * channel-connect deadlock). Resolve against `appUrl` first, mirroring the
123
+ * `open_github_app` / `fulfillCredential` branches, so both the browser we
124
+ * launch and the URL shown on the card are absolute. An already-absolute URL
125
+ * passes through unchanged; if `appUrl` is missing we return the input as-is.
126
+ */
127
+ function resolveConnectUrl(url, appUrl) {
128
+ if (!appUrl) return url;
129
+ try {
130
+ return new URL(url, appUrl).toString();
131
+ } catch (_error) {
132
+ return url;
133
+ }
134
+ }
135
+ function parseOauthConnectParams(url) {
136
+ const parsed = safeUrl(url);
137
+ if (!parsed) return null;
138
+ const integrationKey = parsed.searchParams.get("integration");
139
+ const agentId = parsed.searchParams.get("agent_id");
140
+ const authConfigId = parsed.searchParams.get("auth_config_id");
141
+ const conversationId = parsed.searchParams.get("conversation_id");
142
+ if (!integrationKey || !agentId || !authConfigId || !conversationId) return null;
143
+ return {
144
+ integrationKey,
145
+ agentId,
146
+ authConfigId,
147
+ conversationId,
148
+ scopes: parsed.searchParams.get("scopes")
149
+ };
150
+ }
151
+ function parseExternalOauthConnectParams(url) {
152
+ const parsed = safeUrl(url);
153
+ if (!parsed) return null;
154
+ const agentId = parsed.searchParams.get("agent_id");
155
+ const conversationId = parsed.searchParams.get("conversation_id");
156
+ const serverUrl = parsed.searchParams.get("server_url");
157
+ if (!agentId || !conversationId || !serverUrl) return null;
158
+ return {
159
+ agentId,
160
+ conversationId,
161
+ serverUrl
162
+ };
163
+ }
164
+ /**
165
+ * A short human line for a failed card action. Fulfill/connect endpoints
166
+ * return `{ error: string }` bodies (e.g. "already fulfilled") — prefer that
167
+ * over the generic HttpError message.
168
+ */
169
+ function cardActionErrorMessage(err) {
170
+ if (err instanceof HttpError) {
171
+ try {
172
+ const parsed = JSON.parse(err.body);
173
+ if (isRecord(parsed) && typeof parsed.error === "string") return parsed.error;
174
+ } catch (_error) {}
175
+ return `request failed (HTTP ${err.status})`;
176
+ }
177
+ return errorMessage(err);
178
+ }
179
+ const MASK_CHAR = "•";
180
+ /**
181
+ * Recover the real secret from the masked input's displayed text. The input
182
+ * is controlled: after every edit we render bullets, which forces the cursor
183
+ * to the end, so the next edit is always a tail edit — the displayed text is
184
+ * some prefix of the old mask (kept characters) followed by newly typed or
185
+ * pasted plaintext. Characters beyond the retained bullets are the new tail.
186
+ */
187
+ function reconcileMaskedInput(previousValue, displayed) {
188
+ let kept = 0;
189
+ while (kept < displayed.length && kept < previousValue.length && displayed[kept] === MASK_CHAR) kept++;
190
+ return previousValue.slice(0, kept) + displayed.slice(kept);
191
+ }
192
+
193
+ //#endregion
194
+ //#region src/chat/connect-cards.ts
195
+ /**
196
+ * Turn a parsed ConnectCard into the headless summary, resolving any relative
197
+ * connect URL against the app origin so the emitted URL is directly openable.
198
+ */
199
+ function summarizeConnectCard(card, appUrl) {
200
+ const base = {
201
+ title: card.title,
202
+ subtitle: card.subtitle,
203
+ description: card.description
204
+ };
205
+ const button = card.button;
206
+ if (!button) return {
207
+ ...base,
208
+ action: { kind: "unsupported" }
209
+ };
210
+ const act = button.action;
211
+ switch (act.kind) {
212
+ case "open_oauth":
213
+ case "open_external_oauth":
214
+ case "open_github_app": return {
215
+ ...base,
216
+ action: {
217
+ kind: "open_url",
218
+ url: resolveConnectUrl(act.url, appUrl)
219
+ }
220
+ };
221
+ case "submit_credential": return {
222
+ ...base,
223
+ action: {
224
+ kind: "submit_credential",
225
+ url: resolveConnectUrl(act.url, appUrl),
226
+ fields: card.fields.map((f) => f.label)
227
+ }
228
+ };
229
+ case "grant_portal": return {
230
+ ...base,
231
+ action: {
232
+ kind: "approve_portal",
233
+ agentId: act.agentId
234
+ }
235
+ };
236
+ default: return {
237
+ ...base,
238
+ action: { kind: "unsupported" }
239
+ };
240
+ }
241
+ }
242
+ /**
243
+ * Parse a `data-anyone-render-spec` stream chunk into a connect-card summary,
244
+ * or null if the chunk isn't a connect card (other render specs — training,
245
+ * deep-learn, compute-request — parse to null, same as the TUI).
246
+ */
247
+ function connectCardFromChunk(chunk, appUrl) {
248
+ if (chunk["type"] !== "data-anyone-render-spec") return null;
249
+ const data = chunk["data"];
250
+ if (!isRecord(data)) return null;
251
+ const card = parseConnectCard(data["spec"]);
252
+ if (!card) return null;
253
+ return summarizeConnectCard(card, appUrl);
254
+ }
255
+ /** Render a connect-card summary as a human-readable action block. */
256
+ function formatConnectCard(card) {
257
+ const lines = [];
258
+ lines.push(`\n[action needed] ${card.title}`);
259
+ if (card.subtitle) lines.push(card.subtitle);
260
+ if (card.description) lines.push(card.description);
261
+ switch (card.action.kind) {
262
+ case "open_url":
263
+ lines.push(`Open to continue: ${card.action.url}`);
264
+ break;
265
+ case "submit_credential":
266
+ lines.push(`Provide credential (${card.action.fields.join(", ") || "value"}) at: ${card.action.url}`);
267
+ break;
268
+ case "approve_portal":
269
+ lines.push(`Approve local-machine access for agent ${card.action.agentId} in the TUI or web app.`);
270
+ break;
271
+ case "unsupported":
272
+ lines.push("Open this conversation in the web app to continue.");
273
+ break;
274
+ }
275
+ return lines.join("\n");
276
+ }
277
+
278
+ //#endregion
279
+ //#region src/chat/print.ts
280
+ /**
281
+ * Non-interactive chat, à la `claude -p`. Sends a single prompt to an
282
+ * agent, streams the run, and prints the assistant's reply to stdout
283
+ * before exiting. No OpenTUI, no Bun requirement — this rides the same
284
+ * Node-friendly REST client the TUI uses, so it runs anywhere the
285
+ * management commands do (CI, pipes, scripts).
286
+ *
287
+ * Resolution rules kept deliberately strict because there's no human to
288
+ * disambiguate: an `--agent` selector must match exactly one agent, and
289
+ * when it's omitted we only auto-pick if the account has exactly one.
290
+ */
291
+ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversationId, json, machineShare, grantTargetAgent }) {
292
+ const client = createRestClient({
293
+ appUrl,
294
+ sessionToken
295
+ });
296
+ const agent = resolveAgent(await client.listAgents({
297
+ scope: "org",
298
+ onPage: null
299
+ }), agentSelector);
300
+ if (machineShare) {
301
+ if (!machineShare.isGranted(agent.id) && grantTargetAgent) await machineShare.grantAgent(agent.id);
302
+ if (machineShare.isGranted(agent.id)) console.error(`portal: shared this machine with ${agent.name} for this run (grant persists until revoked)`);
303
+ else console.error(`portal: this machine is shared (shareMachineDefault), but ${agent.name} has no grant — approve its request, or run \`skydive portal grant --agent ${agent.name}\`.`);
304
+ }
305
+ let send;
306
+ try {
307
+ send = await client.sendMessage({
308
+ agentId: agent.id,
309
+ conversationId,
310
+ content: prompt,
311
+ attachmentIds: [],
312
+ clientSurface: "cli"
313
+ });
314
+ } catch (err) {
315
+ throw toPrintError(err);
316
+ }
317
+ const { text, connectCards } = await collectRunText({
318
+ client,
319
+ appUrl,
320
+ target: {
321
+ kind: "run",
322
+ id: send.runId
323
+ },
324
+ onText: json ? null : (delta) => process.stdout.write(delta),
325
+ messageIdForHint: send.messageId ?? null
326
+ });
327
+ if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
328
+ if (!json) for (const card of connectCards) process.stdout.write(`${formatConnectCard(card)}\n`);
329
+ return {
330
+ agentId: agent.id,
331
+ agentName: agent.name,
332
+ conversationId: send.conversationId,
333
+ isNewConversation: send.isNewConversation,
334
+ messageId: send.messageId ?? null,
335
+ text,
336
+ connectCards
337
+ };
338
+ }
339
+ /**
340
+ * Turn a transport failure into an actionable CLI error. A Cloudflare edge
341
+ * 5xx (502/504) on a long run otherwise leaks a raw HTML/JSON error page to
342
+ * stdout, which is impossible to act on. When a messageId is known and
343
+ * recoverable we point the user at `skydive messages get <messageId>` rather
344
+ * than a blind retry (a retry re-executes an agent that may have write access).
345
+ */
346
+ function toPrintError(err, messageId) {
347
+ if (err instanceof HttpError && err.status >= 500) {
348
+ const recovery = messageId ? ` The run may still be completing server-side. Do NOT blindly retry (it would re-run the agent). Fetch the result with: skydive messages get ${messageId}` : "";
349
+ return /* @__PURE__ */ new Error(`The request to Skydive timed out at the edge (HTTP ${err.status}).${recovery}`);
350
+ }
351
+ return err instanceof Error ? err : new Error(String(err));
352
+ }
353
+ /**
354
+ * Stream a run to completion, folding text-delta chunks into the reply and
355
+ * collecting any connect cards (OAuth / MCP / credential requests) the run
356
+ * posts. Shared by `chat -p` (streaming the run it just created) and `messages
357
+ * get` (re-attaching by message id — the server replays a finished run from its
358
+ * persisted log, so this works whether the run is live or already done).
359
+ */
360
+ async function collectRunText({ client, appUrl, target, onText, messageIdForHint }) {
361
+ let text = "";
362
+ const controller = new AbortController();
363
+ let streamError = null;
364
+ const connectCards = [];
365
+ const onEvent = (event) => {
366
+ if (event.kind === "finished") {
367
+ if (event.error) streamError = event.error;
368
+ return;
369
+ }
370
+ const chunk = event.chunk;
371
+ if (chunk["type"] === "text-delta") {
372
+ const delta = typeof chunk["delta"] === "string" ? chunk["delta"] : typeof chunk["text"] === "string" ? chunk["text"] : "";
373
+ if (delta) {
374
+ text += delta;
375
+ if (onText) onText(delta);
376
+ }
377
+ } else if (chunk["type"] === "error") streamError = typeof chunk["errorText"] === "string" ? chunk["errorText"] : "unknown error";
378
+ else {
379
+ const card = connectCardFromChunk(chunk, appUrl);
380
+ if (card) connectCards.push(card);
381
+ }
382
+ };
383
+ try {
384
+ if (target.kind === "message") await client.streamMessage({
385
+ messageId: target.id,
386
+ signal: controller.signal,
387
+ onEvent
388
+ });
389
+ else await client.streamRun({
390
+ runId: target.id,
391
+ signal: controller.signal,
392
+ onEvent
393
+ });
394
+ } catch (err) {
395
+ throw toPrintError(err, messageIdForHint);
396
+ }
397
+ if (streamError) throw new Error(streamError);
398
+ return {
399
+ text,
400
+ connectCards
401
+ };
402
+ }
403
+ /**
404
+ * Re-attach to an exchange by message id and print its reply. Backs `skydive
405
+ * messages get <messageId>` — the recovery path when a `chat -p` stream dropped
406
+ * at the edge after the message was accepted. The server resolves the run
407
+ * behind the message and replays it from its persisted event log, so this
408
+ * returns the full reply whether the run is still live or already done.
409
+ */
410
+ async function messageGet({ appUrl, sessionToken, messageId, json }) {
411
+ const { text, connectCards } = await collectRunText({
412
+ client: createRestClient({
413
+ appUrl,
414
+ sessionToken
415
+ }),
416
+ appUrl,
417
+ target: {
418
+ kind: "message",
419
+ id: messageId
420
+ },
421
+ onText: json ? null : (delta) => process.stdout.write(delta),
422
+ messageIdForHint: null
423
+ });
424
+ if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
425
+ if (!json) for (const card of connectCards) process.stdout.write(`${formatConnectCard(card)}\n`);
426
+ return {
427
+ messageId,
428
+ text,
429
+ connectCards
430
+ };
431
+ }
432
+ /**
433
+ * Pick the target agent. With no selector, auto-pick only when the
434
+ * account has exactly one agent; otherwise the user must name one (there's
435
+ * no picker in non-interactive mode). A selector matches by id first, then
436
+ * a unique case-insensitive slug/name; ambiguous or missing matches throw
437
+ * with the candidate list so the caller knows what to pass.
438
+ */
439
+ function resolveAgent(agents, selector) {
440
+ if (!selector) {
441
+ const [only, ...rest] = agents;
442
+ if (!only) throw new Error("No agents on this account.");
443
+ if (rest.length === 0) return only;
444
+ throw new Error(`Multiple agents on this account — pass --agent <id|slug|name>. Candidates:\n${formatCandidates(agents)}`);
445
+ }
446
+ const byId = agents.find((a) => a.id === selector);
447
+ if (byId) return byId;
448
+ const needle = selector.toLowerCase();
449
+ const matches = agents.filter((a) => a.slug && a.slug.toLowerCase() === needle || a.name.toLowerCase() === needle);
450
+ const [firstMatch, ...restMatches] = matches;
451
+ if (firstMatch && restMatches.length === 0) return firstMatch;
452
+ if (restMatches.length > 0) throw new Error(`Multiple agents match "${selector}" — pass the id instead. Candidates:\n${formatCandidates(matches)}`);
453
+ throw new Error(`No agent matches "${selector}". Candidates:\n${formatCandidates(agents)}`);
454
+ }
455
+ function formatCandidates(agents) {
456
+ return agents.slice(0, 25).map((a) => ` ${a.id} ${a.slug ?? a.name}`).join("\n");
457
+ }
458
+ /** Read all of stdin as UTF-8. Used when `-p` is passed with no value. */
459
+ async function readStdin() {
460
+ const chunks = [];
461
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
462
+ return Buffer.concat(chunks).toString("utf8");
463
+ }
464
+
465
+ //#endregion
466
+ export { runPrint as a, cardActionErrorMessage as c, reconcileMaskedInput as d, resolveConnectUrl as f, resolveAgent as i, parseExternalOauthConnectParams as l, messageGet as n, toPrintError as o, parseConnectCard as p, readStdin as r, MASK_CHAR as s, collectRunText as t, parseOauthConnectParams as u };
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-DTlkPko_.mjs";
3
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-BhfjRrxI.mjs";
4
+
5
+ export { messageGet, readStdin, resolveAgent, runPrint };