skydive-cli 0.1.0-beta.163 → 0.1.0-beta.186
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/README.md +8 -7
- package/dist/js/bin.mjs +248 -97
- package/dist/js/{boot-B-eA8M6t.mjs → boot-BEMmVR8C.mjs} +51 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,18 +142,19 @@ skydive chat -p "status?" --agent grace --json # structured envelo
|
|
|
142
142
|
browser login — sign in first with `skydive auth login --web` or set
|
|
143
143
|
`SKYDIVE_SESSION_TOKEN`.
|
|
144
144
|
- `--json` prints `{ agentId, agentName, conversationId, isNewConversation,
|
|
145
|
-
|
|
145
|
+
messageId, text }` instead of streaming the raw text.
|
|
146
146
|
- **Recovering a dropped run.** A long run's stream can be cut off at the edge
|
|
147
|
-
(a gateway 502/504) after the
|
|
148
|
-
server-side, so a blind retry would re-execute
|
|
149
|
-
`
|
|
147
|
+
(a gateway 502/504) after the message was accepted. The agent keeps working
|
|
148
|
+
server-side, so a blind retry would re-execute it. Instead, note the
|
|
149
|
+
`messageId` (always in `--json` output) and fetch the result once it's done:
|
|
150
150
|
|
|
151
151
|
```sh
|
|
152
|
-
skydive
|
|
152
|
+
skydive messages get <messageId> # prints the reply; --json for the envelope
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
`
|
|
156
|
-
still streaming or already finished.
|
|
155
|
+
`messages get` re-attaches to the exchange behind that message id and replays
|
|
156
|
+
the full reply whether it's still streaming or already finished. Message ids
|
|
157
|
+
are the handle this API works in; run ids stay server-side.
|
|
157
158
|
|
|
158
159
|
It defaults to the production API (`https://api.skydive.com`). For local
|
|
159
160
|
dev, point it at your stack:
|
package/dist/js/bin.mjs
CHANGED
|
@@ -31,7 +31,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
31
31
|
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region package.json
|
|
34
|
-
var version$1 = "0.1.0-beta.
|
|
34
|
+
var version$1 = "0.1.0-beta.186";
|
|
35
35
|
|
|
36
36
|
//#endregion
|
|
37
37
|
//#region src/types.ts
|
|
@@ -886,7 +886,7 @@ function truncate(value, max) {
|
|
|
886
886
|
if (trimmed.length <= max) return trimmed || "-";
|
|
887
887
|
return `${trimmed.slice(0, max - 1)}\u2026`;
|
|
888
888
|
}
|
|
889
|
-
const getCommand = {
|
|
889
|
+
const getCommand$1 = {
|
|
890
890
|
command: "get <id>",
|
|
891
891
|
describe: "Get agent details",
|
|
892
892
|
builder: (y) => y.positional("id", {
|
|
@@ -956,7 +956,7 @@ const createCommand$1 = {
|
|
|
956
956
|
const agentsCommand = {
|
|
957
957
|
command: "agents",
|
|
958
958
|
describe: "Manage agents",
|
|
959
|
-
builder: (y) => y.command(listCommand$4).command(getCommand).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
|
|
959
|
+
builder: (y) => y.command(listCommand$4).command(getCommand$1).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
|
|
960
960
|
handler: () => {}
|
|
961
961
|
};
|
|
962
962
|
|
|
@@ -2152,7 +2152,7 @@ const chatCommand = {
|
|
|
2152
2152
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
2153
2153
|
process.exit(1);
|
|
2154
2154
|
}
|
|
2155
|
-
const { runChat } = await import("./boot-
|
|
2155
|
+
const { runChat } = await import("./boot-BEMmVR8C.mjs");
|
|
2156
2156
|
await runChat({
|
|
2157
2157
|
appUrl,
|
|
2158
2158
|
sessionToken: session.value.sessionToken,
|
|
@@ -2199,6 +2199,60 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2199
2199
|
}
|
|
2200
2200
|
}
|
|
2201
2201
|
|
|
2202
|
+
//#endregion
|
|
2203
|
+
//#region src/commands/messages.ts
|
|
2204
|
+
/**
|
|
2205
|
+
* `skydive messages get <messageId>` re-attaches to an exchange by message id
|
|
2206
|
+
* and prints the agent's reply.
|
|
2207
|
+
*
|
|
2208
|
+
* This is the recovery path for `chat -p`: a long run's stream can drop at the
|
|
2209
|
+
* edge (Cloudflare 502/504) after the message was accepted, leaving the caller
|
|
2210
|
+
* with a messageId but no result. The agent keeps working server-side, so a
|
|
2211
|
+
* blind retry would double-execute an agent that may have write access.
|
|
2212
|
+
* Instead, fetch the finished result by message id here — the server resolves
|
|
2213
|
+
* the run behind the message and replays it from its persisted event log, so
|
|
2214
|
+
* this works whether the run is still live or already done.
|
|
2215
|
+
*
|
|
2216
|
+
* Message ids are the currency of this API: `chat -p --json` reports the
|
|
2217
|
+
* `messageId` and this command consumes it. Run ids stay server-side.
|
|
2218
|
+
*/
|
|
2219
|
+
const getCommand = {
|
|
2220
|
+
command: "get <message-id>",
|
|
2221
|
+
describe: "Fetch a message by id and print the reply (recovers a timed-out -p)",
|
|
2222
|
+
builder: (y) => y.positional("message-id", {
|
|
2223
|
+
type: "string",
|
|
2224
|
+
demandOption: true,
|
|
2225
|
+
describe: "Message id (the `messageId` from a prior `chat -p --json`)"
|
|
2226
|
+
}),
|
|
2227
|
+
handler: async (argv) => {
|
|
2228
|
+
const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
|
|
2229
|
+
const session = resolveSession({ appUrl });
|
|
2230
|
+
if (session.isErr()) {
|
|
2231
|
+
printError(`${session.error.message} Run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2232
|
+
process.exit(1);
|
|
2233
|
+
}
|
|
2234
|
+
const { messageGet } = await Promise.resolve().then(() => print_exports);
|
|
2235
|
+
try {
|
|
2236
|
+
const result = await messageGet({
|
|
2237
|
+
appUrl,
|
|
2238
|
+
sessionToken: session.value.sessionToken,
|
|
2239
|
+
messageId: argv["message-id"],
|
|
2240
|
+
json: argv.json
|
|
2241
|
+
});
|
|
2242
|
+
if (argv.json) output(argv, result);
|
|
2243
|
+
} catch (error) {
|
|
2244
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
2245
|
+
process.exit(1);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
const messagesCommand = {
|
|
2250
|
+
command: "messages",
|
|
2251
|
+
describe: "Inspect chat messages and re-fetch agent replies",
|
|
2252
|
+
builder: (y) => y.command(getCommand).demandCommand(1, "Specify a subcommand: get"),
|
|
2253
|
+
handler: () => {}
|
|
2254
|
+
};
|
|
2255
|
+
|
|
2202
2256
|
//#endregion
|
|
2203
2257
|
//#region src/chat/api/rest.ts
|
|
2204
2258
|
var HttpError = class extends Error {
|
|
@@ -2239,6 +2293,65 @@ function createRestClient({ appUrl, sessionToken }) {
|
|
|
2239
2293
|
});
|
|
2240
2294
|
if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
2241
2295
|
}
|
|
2296
|
+
const streamEvents = async ({ path, label, signal, onEvent }) => {
|
|
2297
|
+
let lastEventId = null;
|
|
2298
|
+
let finished = false;
|
|
2299
|
+
let reconnects = 0;
|
|
2300
|
+
for (;;) {
|
|
2301
|
+
if (signal.aborted) return;
|
|
2302
|
+
try {
|
|
2303
|
+
const headers = {
|
|
2304
|
+
authorization: `Bearer ${sessionToken}`,
|
|
2305
|
+
accept: "text/event-stream"
|
|
2306
|
+
};
|
|
2307
|
+
if (lastEventId) headers["last-event-id"] = lastEventId;
|
|
2308
|
+
const res = await fetch(`${appUrl}${path}`, {
|
|
2309
|
+
headers,
|
|
2310
|
+
signal
|
|
2311
|
+
});
|
|
2312
|
+
if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
2313
|
+
reconnects = 0;
|
|
2314
|
+
const parser = createParser({ onEvent: (message) => {
|
|
2315
|
+
if (message.id) lastEventId = message.id;
|
|
2316
|
+
if (message.event === "error") {
|
|
2317
|
+
const { error } = streamErrorSchema.parse(JSON.parse(message.data));
|
|
2318
|
+
throw new Error(error);
|
|
2319
|
+
}
|
|
2320
|
+
const event = runStreamEventSchema.parse(JSON.parse(message.data));
|
|
2321
|
+
if (event.kind === "finished") finished = true;
|
|
2322
|
+
onEvent(event.kind === "finished" ? {
|
|
2323
|
+
...event,
|
|
2324
|
+
error: event.error ?? null
|
|
2325
|
+
} : event);
|
|
2326
|
+
} });
|
|
2327
|
+
const decoder = new TextDecoder();
|
|
2328
|
+
const reader = res.body.getReader();
|
|
2329
|
+
try {
|
|
2330
|
+
for (;;) {
|
|
2331
|
+
const { done, value } = await reader.read();
|
|
2332
|
+
if (done) break;
|
|
2333
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
2334
|
+
if (finished) return;
|
|
2335
|
+
}
|
|
2336
|
+
} finally {
|
|
2337
|
+
try {
|
|
2338
|
+
await reader.cancel();
|
|
2339
|
+
} catch (_error) {}
|
|
2340
|
+
}
|
|
2341
|
+
} catch (err) {
|
|
2342
|
+
if (signal.aborted) return;
|
|
2343
|
+
if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
|
|
2344
|
+
reconnects += 1;
|
|
2345
|
+
if (reconnects > MAX_STREAM_RECONNECTS) throw err;
|
|
2346
|
+
await sleep(Math.min(500 * 2 ** reconnects, 5e3));
|
|
2347
|
+
continue;
|
|
2348
|
+
}
|
|
2349
|
+
if (finished) return;
|
|
2350
|
+
reconnects += 1;
|
|
2351
|
+
if (reconnects > MAX_STREAM_RECONNECTS) throw new Error(`${label} stream ended unexpectedly`);
|
|
2352
|
+
await sleep(Math.min(500 * 2 ** reconnects, 5e3));
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2242
2355
|
return {
|
|
2243
2356
|
listAgents: async ({ scope, onPage }) => {
|
|
2244
2357
|
const all = [];
|
|
@@ -2365,65 +2478,18 @@ function createRestClient({ appUrl, sessionToken }) {
|
|
|
2365
2478
|
});
|
|
2366
2479
|
if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
2367
2480
|
},
|
|
2368
|
-
streamRun: async ({ runId, signal, onEvent }) => {
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
const res = await fetch(`${appUrl}/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`, {
|
|
2381
|
-
headers,
|
|
2382
|
-
signal
|
|
2383
|
-
});
|
|
2384
|
-
if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
|
|
2385
|
-
reconnects = 0;
|
|
2386
|
-
const parser = createParser({ onEvent: (message) => {
|
|
2387
|
-
if (message.id) lastEventId = message.id;
|
|
2388
|
-
if (message.event === "error") {
|
|
2389
|
-
const { error } = streamErrorSchema.parse(JSON.parse(message.data));
|
|
2390
|
-
throw new Error(error);
|
|
2391
|
-
}
|
|
2392
|
-
const event = runStreamEventSchema.parse(JSON.parse(message.data));
|
|
2393
|
-
if (event.kind === "finished") finished = true;
|
|
2394
|
-
onEvent(event.kind === "finished" ? {
|
|
2395
|
-
...event,
|
|
2396
|
-
error: event.error ?? null
|
|
2397
|
-
} : event);
|
|
2398
|
-
} });
|
|
2399
|
-
const decoder = new TextDecoder();
|
|
2400
|
-
const reader = res.body.getReader();
|
|
2401
|
-
try {
|
|
2402
|
-
for (;;) {
|
|
2403
|
-
const { done, value } = await reader.read();
|
|
2404
|
-
if (done) break;
|
|
2405
|
-
parser.feed(decoder.decode(value, { stream: true }));
|
|
2406
|
-
if (finished) return;
|
|
2407
|
-
}
|
|
2408
|
-
} finally {
|
|
2409
|
-
try {
|
|
2410
|
-
await reader.cancel();
|
|
2411
|
-
} catch (_error) {}
|
|
2412
|
-
}
|
|
2413
|
-
} catch (err) {
|
|
2414
|
-
if (signal.aborted) return;
|
|
2415
|
-
if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
|
|
2416
|
-
reconnects += 1;
|
|
2417
|
-
if (reconnects > MAX_STREAM_RECONNECTS) throw err;
|
|
2418
|
-
await sleep(Math.min(500 * 2 ** reconnects, 5e3));
|
|
2419
|
-
continue;
|
|
2420
|
-
}
|
|
2421
|
-
if (finished) return;
|
|
2422
|
-
reconnects += 1;
|
|
2423
|
-
if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("run stream ended unexpectedly");
|
|
2424
|
-
await sleep(Math.min(500 * 2 ** reconnects, 5e3));
|
|
2425
|
-
}
|
|
2426
|
-
},
|
|
2481
|
+
streamRun: async ({ runId, signal, onEvent }) => streamEvents({
|
|
2482
|
+
path: `/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`,
|
|
2483
|
+
label: "run",
|
|
2484
|
+
signal,
|
|
2485
|
+
onEvent
|
|
2486
|
+
}),
|
|
2487
|
+
streamMessage: async ({ messageId, signal, onEvent }) => streamEvents({
|
|
2488
|
+
path: `/api/v1/chat/messages/${encodeURIComponent(messageId)}/stream`,
|
|
2489
|
+
label: "message",
|
|
2490
|
+
signal,
|
|
2491
|
+
onEvent
|
|
2492
|
+
}),
|
|
2427
2493
|
streamConversation: async ({ conversationId, signal, onEvent }) => {
|
|
2428
2494
|
let reconnects = 0;
|
|
2429
2495
|
for (;;) {
|
|
@@ -2554,6 +2620,7 @@ const recapResponseSchema = z.object({ recap: z.object({ text: z.string() }).nul
|
|
|
2554
2620
|
const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
|
|
2555
2621
|
const sendResultSchema = z.object({
|
|
2556
2622
|
runId: z.string(),
|
|
2623
|
+
messageId: z.string().uuid().nullish(),
|
|
2557
2624
|
conversationId: z.string().uuid(),
|
|
2558
2625
|
isNewConversation: z.boolean(),
|
|
2559
2626
|
steered: z.boolean().optional(),
|
|
@@ -2878,9 +2945,12 @@ function formatConnectCard(card) {
|
|
|
2878
2945
|
//#endregion
|
|
2879
2946
|
//#region src/chat/print.ts
|
|
2880
2947
|
var print_exports = /* @__PURE__ */ __exportAll({
|
|
2948
|
+
collectRunText: () => collectRunText,
|
|
2949
|
+
messageGet: () => messageGet,
|
|
2881
2950
|
readStdin: () => readStdin,
|
|
2882
2951
|
resolveAgent: () => resolveAgent,
|
|
2883
|
-
runPrint: () => runPrint
|
|
2952
|
+
runPrint: () => runPrint,
|
|
2953
|
+
toPrintError: () => toPrintError
|
|
2884
2954
|
});
|
|
2885
2955
|
/**
|
|
2886
2956
|
* Non-interactive chat, à la `claude -p`. Sends a single prompt to an
|
|
@@ -2902,48 +2972,129 @@ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversat
|
|
|
2902
2972
|
scope: "org",
|
|
2903
2973
|
onPage: null
|
|
2904
2974
|
}), agentSelector);
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2975
|
+
let send;
|
|
2976
|
+
try {
|
|
2977
|
+
send = await client.sendMessage({
|
|
2978
|
+
agentId: agent.id,
|
|
2979
|
+
conversationId,
|
|
2980
|
+
content: prompt,
|
|
2981
|
+
attachmentIds: [],
|
|
2982
|
+
clientSurface: "cli"
|
|
2983
|
+
});
|
|
2984
|
+
} catch (err) {
|
|
2985
|
+
throw toPrintError(err);
|
|
2986
|
+
}
|
|
2987
|
+
const { text, connectCards } = await collectRunText({
|
|
2988
|
+
client,
|
|
2989
|
+
appUrl,
|
|
2990
|
+
target: {
|
|
2991
|
+
kind: "run",
|
|
2992
|
+
id: send.runId
|
|
2993
|
+
},
|
|
2994
|
+
onText: json ? null : (delta) => process.stdout.write(delta),
|
|
2995
|
+
messageIdForHint: send.messageId ?? null
|
|
2911
2996
|
});
|
|
2997
|
+
if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
|
|
2998
|
+
if (!json) for (const card of connectCards) process.stdout.write(`${formatConnectCard(card)}\n`);
|
|
2999
|
+
return {
|
|
3000
|
+
agentId: agent.id,
|
|
3001
|
+
agentName: agent.name,
|
|
3002
|
+
conversationId: send.conversationId,
|
|
3003
|
+
isNewConversation: send.isNewConversation,
|
|
3004
|
+
messageId: send.messageId ?? null,
|
|
3005
|
+
text,
|
|
3006
|
+
connectCards
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Turn a transport failure into an actionable CLI error. A Cloudflare edge
|
|
3011
|
+
* 5xx (502/504) on a long run otherwise leaks a raw HTML/JSON error page to
|
|
3012
|
+
* stdout, which is impossible to act on. When a messageId is known and
|
|
3013
|
+
* recoverable we point the user at `skydive messages get <messageId>` rather
|
|
3014
|
+
* than a blind retry (a retry re-executes an agent that may have write access).
|
|
3015
|
+
*/
|
|
3016
|
+
function toPrintError(err, messageId) {
|
|
3017
|
+
if (err instanceof HttpError && err.status >= 500) {
|
|
3018
|
+
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}` : "";
|
|
3019
|
+
return /* @__PURE__ */ new Error(`The request to Skydive timed out at the edge (HTTP ${err.status}).${recovery}`);
|
|
3020
|
+
}
|
|
3021
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Stream a run to completion, folding text-delta chunks into the reply and
|
|
3025
|
+
* collecting any connect cards (OAuth / MCP / credential requests) the run
|
|
3026
|
+
* posts. Shared by `chat -p` (streaming the run it just created) and `messages
|
|
3027
|
+
* get` (re-attaching by message id — the server replays a finished run from its
|
|
3028
|
+
* persisted log, so this works whether the run is live or already done).
|
|
3029
|
+
*/
|
|
3030
|
+
async function collectRunText({ client, appUrl, target, onText, messageIdForHint }) {
|
|
2912
3031
|
let text = "";
|
|
2913
3032
|
const controller = new AbortController();
|
|
2914
3033
|
let streamError = null;
|
|
2915
3034
|
const connectCards = [];
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
if (delta) {
|
|
2928
|
-
text += delta;
|
|
2929
|
-
if (!json) process.stdout.write(delta);
|
|
2930
|
-
}
|
|
2931
|
-
} else if (chunk["type"] === "error") streamError = typeof chunk["errorText"] === "string" ? chunk["errorText"] : "unknown error";
|
|
2932
|
-
else {
|
|
2933
|
-
const card = connectCardFromChunk(chunk, appUrl);
|
|
2934
|
-
if (card) connectCards.push(card);
|
|
3035
|
+
const onEvent = (event) => {
|
|
3036
|
+
if (event.kind === "finished") {
|
|
3037
|
+
if (event.error) streamError = event.error;
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
const chunk = event.chunk;
|
|
3041
|
+
if (chunk["type"] === "text-delta") {
|
|
3042
|
+
const delta = typeof chunk["delta"] === "string" ? chunk["delta"] : typeof chunk["text"] === "string" ? chunk["text"] : "";
|
|
3043
|
+
if (delta) {
|
|
3044
|
+
text += delta;
|
|
3045
|
+
if (onText) onText(delta);
|
|
2935
3046
|
}
|
|
3047
|
+
} else if (chunk["type"] === "error") streamError = typeof chunk["errorText"] === "string" ? chunk["errorText"] : "unknown error";
|
|
3048
|
+
else {
|
|
3049
|
+
const card = connectCardFromChunk(chunk, appUrl);
|
|
3050
|
+
if (card) connectCards.push(card);
|
|
2936
3051
|
}
|
|
2937
|
-
}
|
|
3052
|
+
};
|
|
3053
|
+
try {
|
|
3054
|
+
if (target.kind === "message") await client.streamMessage({
|
|
3055
|
+
messageId: target.id,
|
|
3056
|
+
signal: controller.signal,
|
|
3057
|
+
onEvent
|
|
3058
|
+
});
|
|
3059
|
+
else await client.streamRun({
|
|
3060
|
+
runId: target.id,
|
|
3061
|
+
signal: controller.signal,
|
|
3062
|
+
onEvent
|
|
3063
|
+
});
|
|
3064
|
+
} catch (err) {
|
|
3065
|
+
throw toPrintError(err, messageIdForHint);
|
|
3066
|
+
}
|
|
2938
3067
|
if (streamError) throw new Error(streamError);
|
|
3068
|
+
return {
|
|
3069
|
+
text,
|
|
3070
|
+
connectCards
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Re-attach to an exchange by message id and print its reply. Backs `skydive
|
|
3075
|
+
* messages get <messageId>` — the recovery path when a `chat -p` stream dropped
|
|
3076
|
+
* at the edge after the message was accepted. The server resolves the run
|
|
3077
|
+
* behind the message and replays it from its persisted event log, so this
|
|
3078
|
+
* returns the full reply whether the run is still live or already done.
|
|
3079
|
+
*/
|
|
3080
|
+
async function messageGet({ appUrl, sessionToken, messageId, json }) {
|
|
3081
|
+
const { text, connectCards } = await collectRunText({
|
|
3082
|
+
client: createRestClient({
|
|
3083
|
+
appUrl,
|
|
3084
|
+
sessionToken
|
|
3085
|
+
}),
|
|
3086
|
+
appUrl,
|
|
3087
|
+
target: {
|
|
3088
|
+
kind: "message",
|
|
3089
|
+
id: messageId
|
|
3090
|
+
},
|
|
3091
|
+
onText: json ? null : (delta) => process.stdout.write(delta),
|
|
3092
|
+
messageIdForHint: null
|
|
3093
|
+
});
|
|
2939
3094
|
if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
|
|
2940
3095
|
if (!json) for (const card of connectCards) process.stdout.write(`${formatConnectCard(card)}\n`);
|
|
2941
3096
|
return {
|
|
2942
|
-
|
|
2943
|
-
agentName: agent.name,
|
|
2944
|
-
conversationId: send.conversationId,
|
|
2945
|
-
isNewConversation: send.isNewConversation,
|
|
2946
|
-
runId: send.runId,
|
|
3097
|
+
messageId,
|
|
2947
3098
|
text,
|
|
2948
3099
|
connectCards
|
|
2949
3100
|
};
|
|
@@ -3179,7 +3330,7 @@ const switchCommand = {
|
|
|
3179
3330
|
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.");
|
|
3180
3331
|
process.exit(1);
|
|
3181
3332
|
}
|
|
3182
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
3333
|
+
const { runWorkspacePicker } = await import("./boot-BEMmVR8C.mjs");
|
|
3183
3334
|
await runWorkspacePicker(session);
|
|
3184
3335
|
return;
|
|
3185
3336
|
}
|
|
@@ -3233,7 +3384,7 @@ function createCli(argv) {
|
|
|
3233
3384
|
type: "string",
|
|
3234
3385
|
global: true,
|
|
3235
3386
|
describe: "Override API base URL"
|
|
3236
|
-
}).command(authCommand).command(chatCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
3387
|
+
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
3237
3388
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
3238
3389
|
process.exit(1);
|
|
3239
3390
|
});
|
|
@@ -1953,6 +1953,30 @@ function parseDroppedPaths(text) {
|
|
|
1953
1953
|
}
|
|
1954
1954
|
return paths;
|
|
1955
1955
|
}
|
|
1956
|
+
/** Decide how to handle a paste. `kind: 'binary'` events carry the mime of
|
|
1957
|
+
* bytes the terminal forwarded; text events carry the decoded paste text.
|
|
1958
|
+
*
|
|
1959
|
+
* The empty-text case is the fix for "can't paste an image": on macOS the
|
|
1960
|
+
* reflex is Cmd+V, which the terminal turns into a bracketed *text* paste, so
|
|
1961
|
+
* a screenshot on the clipboard arrives as empty bytes. We route that to an OS
|
|
1962
|
+
* clipboard read instead of dropping it, matching the explicit ctrl+v path. */
|
|
1963
|
+
function routePaste(input) {
|
|
1964
|
+
if (input.kind === "binary") {
|
|
1965
|
+
if (input.mimeType?.startsWith("image/")) return {
|
|
1966
|
+
kind: "binary-image",
|
|
1967
|
+
mediaType: input.mimeType
|
|
1968
|
+
};
|
|
1969
|
+
return { kind: "text" };
|
|
1970
|
+
}
|
|
1971
|
+
const paths = parseDroppedPaths(input.text);
|
|
1972
|
+
if (paths) return {
|
|
1973
|
+
kind: "dropped-paths",
|
|
1974
|
+
paths,
|
|
1975
|
+
text: input.text
|
|
1976
|
+
};
|
|
1977
|
+
if (input.text.trim() === "") return { kind: "clipboard-image" };
|
|
1978
|
+
return { kind: "text" };
|
|
1979
|
+
}
|
|
1956
1980
|
/**
|
|
1957
1981
|
* Resolve path candidates into images using the filesystem and file magic,
|
|
1958
1982
|
* not extensions. The whole batch must be regular image files; otherwise the
|
|
@@ -5171,31 +5195,40 @@ function ChatScreen({ agent, conversation }) {
|
|
|
5171
5195
|
}, [stageImages]);
|
|
5172
5196
|
usePaste((event) => {
|
|
5173
5197
|
if (modelPickerOpen || themePickerOpen || helpOpen || credPromptOpen || grantPrompt) return;
|
|
5174
|
-
const
|
|
5175
|
-
|
|
5176
|
-
|
|
5198
|
+
const text = event.metadata?.kind === "binary" ? "" : decodePasteBytes(event.bytes);
|
|
5199
|
+
const route = routePaste({
|
|
5200
|
+
kind: event.metadata?.kind,
|
|
5201
|
+
mimeType: event.metadata?.mimeType,
|
|
5202
|
+
text
|
|
5203
|
+
});
|
|
5204
|
+
switch (route.kind) {
|
|
5205
|
+
case "binary-image": {
|
|
5177
5206
|
event.preventDefault();
|
|
5178
|
-
const ext =
|
|
5207
|
+
const ext = route.mediaType.split("/")[1]?.split("+")[0] ?? "png";
|
|
5179
5208
|
stageImages([{
|
|
5180
5209
|
fileName: `pasted-image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.${ext}`,
|
|
5181
|
-
mediaType:
|
|
5210
|
+
mediaType: route.mediaType,
|
|
5182
5211
|
data: event.bytes
|
|
5183
5212
|
}]);
|
|
5184
|
-
}
|
|
5185
|
-
return;
|
|
5186
|
-
}
|
|
5187
|
-
const text = decodePasteBytes(event.bytes);
|
|
5188
|
-
const paths = parseDroppedPaths(text);
|
|
5189
|
-
if (!paths) return;
|
|
5190
|
-
event.preventDefault();
|
|
5191
|
-
resolveDroppedImages(paths).then((images) => {
|
|
5192
|
-
if (images) {
|
|
5193
|
-
stageImages(images);
|
|
5194
5213
|
return;
|
|
5195
5214
|
}
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5215
|
+
case "clipboard-image":
|
|
5216
|
+
event.preventDefault();
|
|
5217
|
+
pasteImage();
|
|
5218
|
+
return;
|
|
5219
|
+
case "dropped-paths":
|
|
5220
|
+
event.preventDefault();
|
|
5221
|
+
resolveDroppedImages(route.paths).then((images) => {
|
|
5222
|
+
if (images) {
|
|
5223
|
+
stageImages(images);
|
|
5224
|
+
return;
|
|
5225
|
+
}
|
|
5226
|
+
composerRef.current?.insertText(route.text);
|
|
5227
|
+
handleComposerChange();
|
|
5228
|
+
});
|
|
5229
|
+
return;
|
|
5230
|
+
case "text": return;
|
|
5231
|
+
}
|
|
5199
5232
|
});
|
|
5200
5233
|
const applyComposerText = useCallback((text, caret) => {
|
|
5201
5234
|
const composer = composerRef.current;
|