skydive-cli 0.1.0-beta.89 → 0.1.0-beta.96

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 CHANGED
@@ -8,10 +8,20 @@ npx skydive-cli --help
8
8
 
9
9
  ## Authentication
10
10
 
11
- The CLI has two credentials, stored together in `~/.config/skydive/config.json`:
11
+ The CLI has two credentials, stored together in a single `config.json`. The
12
+ location is platform-conventional (resolved via `env-paths`), so it is **not**
13
+ `~/.config/skydive/` everywhere:
12
14
 
13
- - **API key** (`sky_live_…`) for the management commands. Mint one at
14
- `skydive.com/account`, then:
15
+ | Platform | Path |
16
+ | -------- | ---------------------------------------------------------------- |
17
+ | macOS | `~/Library/Preferences/skydive/config.json` |
18
+ | Linux | `~/.config/skydive/config.json` (or `$XDG_CONFIG_HOME/skydive/`) |
19
+ | Windows | `%APPDATA%\skydive\config.json` |
20
+
21
+ `skydive auth status` prints the real resolved path (`Config: …`) — trust that
22
+ over this table if they ever disagree.
23
+
24
+ - **API key** (`sky_live_…`). Mint one at `skydive.com/account`, then:
15
25
 
16
26
  ```sh
17
27
  skydive auth login # paste the key
@@ -26,6 +36,12 @@ The CLI has two credentials, stored together in `~/.config/skydive/config.json`:
26
36
  skydive auth login --web # opens the browser, prompts for approval
27
37
  ```
28
38
 
39
+ Either credential authenticates the management commands (`agents`, `keys`,
40
+ `secrets`): the CLI prefers the API key and falls back to the `--web` chat
41
+ session, so a `--web` login alone is enough to run them — you do **not** need a
42
+ separate API key. The API key is only required when you have no chat session
43
+ (e.g. CI). `skydive chat` requires the chat session specifically.
44
+
29
45
  `skydive auth status` shows both; `skydive auth logout` clears them.
30
46
 
31
47
  ## Commands
@@ -58,7 +74,7 @@ API key with edit access to that agent.
58
74
  ```sh
59
75
  skydive secrets list --agent-id <id> # names only — values are never returned
60
76
  skydive secrets set OPENAI_API_KEY --agent-id <id> # reads the value from stdin
61
- echo -n "$TOKEN" | skydive secrets set MY_TOKEN --agent-id <id>
77
+ printf '%s' "$TOKEN" | skydive secrets set MY_TOKEN --agent-id <id>
62
78
  skydive secrets set MY_TOKEN "literal-value" --agent-id <id>
63
79
  skydive secrets rm MY_TOKEN --agent-id <id>
64
80
  ```
@@ -67,6 +83,11 @@ skydive secrets rm MY_TOKEN --agent-id <id>
67
83
  - `set` reads the value from **stdin** when no value argument is given, so the
68
84
  secret stays out of shell history. Passing it inline is supported for
69
85
  scripting but avoid it in an interactive shell.
86
+ - Use `printf '%s'` rather than `echo -n` when piping a value: `echo -n` is not
87
+ portable (some `sh` builtins emit `-n` literally instead of suppressing the
88
+ newline), which would silently store a corrupted value. Since values are
89
+ write-only and cannot be read back to verify, that failure stays invisible
90
+ until something downstream breaks.
70
91
  - **Values are write-only over the API.** `list` returns key names, never
71
92
  values — the same contract as the in-sandbox `platform secrets` surface.
72
93
  - Setting a secret updates the agent's vault. If an outbound-proxy rule
@@ -122,6 +143,17 @@ skydive chat -p "status?" --agent grace --json # structured envelo
122
143
  `SKYDIVE_SESSION_TOKEN`.
123
144
  - `--json` prints `{ agentId, agentName, conversationId, isNewConversation,
124
145
  runId, text }` instead of streaming the raw text.
146
+ - **Recovering a dropped run.** A long run's stream can be cut off at the edge
147
+ (a gateway 502/504) after the run was created. The run keeps going
148
+ server-side, so a blind retry would re-execute the agent. Instead, note the
149
+ `runId` (always in `--json` output) and fetch the result once it's done:
150
+
151
+ ```sh
152
+ skydive runs get <runId> # prints the reply; --json for the envelope
153
+ ```
154
+
155
+ `runs get` re-attaches to the run and replays its full reply whether it's
156
+ still streaming or already finished.
125
157
 
126
158
  It defaults to the production API (`https://api.skydive.com`). For local
127
159
  dev, point it at your stack:
@@ -136,22 +168,62 @@ management API used by `agents`/`keys`.
136
168
 
137
169
  ### Keybindings
138
170
 
139
- | Context | Key | Action |
140
- | ------------ | ------------- | ------------------------------------ |
141
- | Pickers | _type_ | filter the list |
142
- | Pickers | `↑` / `↓` | move selection |
143
- | Pickers | `↵` | open / select |
144
- | Pickers | `esc` | back |
145
- | Agent picker | `ctrl+n` | create an agent |
146
- | Chat | `↵` | send (or queue, while streaming) |
147
- | Chat | `pgup`/`pgdn` | scroll the transcript |
148
- | Chat | `ctrl+c` | cancel the active run; again to quit |
149
- | Chat | `esc` | back to the conversation picker |
171
+ | Context | Key | Action |
172
+ | ------------------- | ------------- | ------------------------------------- |
173
+ | Pickers | _type_ | filter the list |
174
+ | Pickers | `↑` / `↓` | move selection |
175
+ | Pickers | `↵` | open / select |
176
+ | Pickers | `esc` | back |
177
+ | Agent picker | `ctrl+n` | create an agent |
178
+ | Agent picker | `ctrl+w` | switch workspace |
179
+ | Agent picker | `tab` | toggle mine / whole-org agents |
180
+ | Conversation picker | `ctrl+d` | delete the highlighted conversation |
181
+ | Chat | `↵` | send (or queue, while streaming) |
182
+ | Chat | `pgup`/`pgdn` | scroll the transcript |
183
+ | Chat | `ctrl+c` | cancel the active run; again to quit |
184
+ | Chat | `esc` | back to the conversation picker |
185
+ | Chat | `ctrl+p` | switch model |
186
+ | Chat | `ctrl+o` | switch theme |
187
+ | Chat | `ctrl+t` | grant / revoke local machine access |
188
+ | Chat | `ctrl+l` | open this conversation in the browser |
189
+ | Chat | `ctrl+r` | run the newest pending connect card |
190
+ | Chat | `ctrl+v` | paste an image from the clipboard |
191
+ | Chat | `ctrl+x` | drop the most recently staged image |
192
+ | Chat | `?` | show the keybinding reference |
150
193
 
151
194
  Messages typed while a response is streaming are **queued** and sent in
152
195
  order as each run finishes; `ctrl+c` cancels the current run and clears the
153
196
  queue.
154
197
 
198
+ ### Local machine access (`--share-machine` / `ctrl+t`)
199
+
200
+ The chat TUI can grant the agent the ability to run commands **on your local
201
+ machine** so it can read the repo you launched from, run local tooling, etc.
202
+ This is off by default (default-deny) and you opt in explicitly:
203
+
204
+ - `skydive chat --share-machine` enables it at launch, or
205
+ - `ctrl+t` toggles it from the chat screen (the status bar shows `local access`
206
+ when on, `revoke access` when off).
207
+
208
+ What to know before enabling it:
209
+
210
+ - **Per-agent.** The grant is scoped to the agent you're chatting with; other
211
+ agents don't inherit it.
212
+ - **The working directory is a starting point, not a jail.** Commands run with
213
+ their working directory set to wherever you launched `skydive chat`, but this
214
+ is **not** a hard filesystem boundary. This is genuine remote command
215
+ execution (a WebSocket streaming stdin/stdout/stderr), so the agent runs
216
+ commands as your user and can reach anything your user can, including paths
217
+ outside the launch directory. Launch from a scoped directory, but treat the
218
+ grant as "this agent can act as me on this machine," not "this agent can only
219
+ see this folder."
220
+ - **Teardown is automatic.** Access is revoked when the TUI unmounts, so
221
+ quitting the chat drops the grant and reaps any processes the agent spawned.
222
+ - **Interactive only.** Machine sharing is **not** available in `-p`
223
+ (non-interactive) mode \u2014 `--share-machine` has no effect there and the
224
+ print path carries no portal code. You can have scripted (`-p`) or local
225
+ access, not both, in a single invocation.
226
+
155
227
  ## Environment variables
156
228
 
157
229
  | Variable | Effect |
@@ -160,6 +232,8 @@ queue.
160
232
  | `SKYDIVE_API_URL` | Management API base URL |
161
233
  | `SKYDIVE_SESSION_TOKEN` | Chat session token (for non-interactive use) |
162
234
  | `SKYDIVE_APP_URL` | Chat app/session origin (overrides stored value) |
235
+ | `SKYDIVE_THEME` | Chat colorscheme id (flag `--theme` overrides) |
236
+ | `SKYDIVE_CONFIG_NAME` | On-disk config profile name (default `skydive`) |
163
237
 
164
238
  ## Testing the TUI
165
239
 
package/dist/js/bin.mjs CHANGED
@@ -13,7 +13,7 @@ import fs from "node:fs";
13
13
  import zlib from "node:zlib";
14
14
 
15
15
  //#region package.json
16
- var version$1 = "0.1.0-beta.89";
16
+ var version$1 = "0.1.0-beta.96";
17
17
 
18
18
  //#endregion
19
19
  //#region src/types.ts
@@ -2101,7 +2101,7 @@ const chatCommand = {
2101
2101
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2102
2102
  process.exit(1);
2103
2103
  }
2104
- const { runChat } = await import("./boot-DTUbLzPt.mjs");
2104
+ const { runChat } = await import("./boot-DRWra2vY.mjs");
2105
2105
  await runChat({
2106
2106
  appUrl,
2107
2107
  sessionToken: session.value.sessionToken,
@@ -2119,7 +2119,7 @@ async function runPrintMode({ argv, appUrl }) {
2119
2119
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
2120
2120
  process.exit(1);
2121
2121
  }
2122
- const { runPrint, readStdin } = await import("./print-BtMdNPJR.mjs").then((n) => n.t);
2122
+ const { runPrint, readStdin } = await import("./print-CQ8b7JUC.mjs").then((n) => n.t);
2123
2123
  let prompt = (argv.print ?? "").trim();
2124
2124
  if (!prompt) {
2125
2125
  if (process.stdin.isTTY) {
@@ -2218,7 +2218,7 @@ const switchCommand = {
2218
2218
  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.");
2219
2219
  process.exit(1);
2220
2220
  }
2221
- const { runWorkspacePicker } = await import("./boot-DTUbLzPt.mjs");
2221
+ const { runWorkspacePicker } = await import("./boot-DRWra2vY.mjs");
2222
2222
  await runWorkspacePicker(session);
2223
2223
  return;
2224
2224
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as createRestClient, n as resolveAgent, r as HttpError } from "./print-BtMdNPJR.mjs";
2
+ import { a as parseExternalOauthConnectParams, c as resolveConnectUrl, d as isRecord, f as HttpError, i as cardActionErrorMessage, l as parseConnectCard, n as resolveAgent, o as parseOauthConnectParams, p as createRestClient, r as MASK_CHAR, s as reconcileMaskedInput, u as errorMessage } from "./print-CQ8b7JUC.mjs";
3
3
  import { _ as getSavedTheme, a as noColorRequested, c as themeMode, d as themesForMode, f as getActiveWorkspaceId, g as DEFAULT_APP_URL, h as DEFAULT_API_URL, i as monoTheme, l as themeModeFromColorFgBg, m as setActiveWorkspace, n as applyTheme, o as theme, p as listWorkspaces, r as findTheme, s as themeForMode, t as DEFAULT_THEME_ID, u as themeVersion, v as resolveWebUrl, y as saveTheme } from "./bin.mjs";
4
4
  import path, { basename, isAbsolute, join, win32 } from "node:path";
5
5
  import { z } from "zod";
@@ -217,19 +217,6 @@ function decodeFrame(frame) {
217
217
  };
218
218
  }
219
219
 
220
- //#endregion
221
- //#region src/chat/util.ts
222
- /** Narrowing helper for the many `unknown` payloads the chat stream and
223
- * tool inputs/outputs carry. A type predicate (not an `as` cast), so call
224
- * sites can read properties without asserting. */
225
- function isRecord(value) {
226
- return typeof value === "object" && value !== null && !Array.isArray(value);
227
- }
228
- /** Best-effort message from an unknown thrown value. */
229
- function errorMessage(err) {
230
- return err instanceof Error ? err.message : String(err);
231
- }
232
-
233
220
  //#endregion
234
221
  //#region src/chat/portal/machine.ts
235
222
  /**
@@ -3253,107 +3240,6 @@ function formatElapsed(totalSeconds) {
3253
3240
  return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
3254
3241
  }
3255
3242
 
3256
- //#endregion
3257
- //#region src/chat/tui/chat/card.ts
3258
- const urlActionKinds = [
3259
- "open_oauth",
3260
- "open_external_oauth",
3261
- "open_github_app",
3262
- "submit_credential"
3263
- ];
3264
- function isUrlActionKind(value) {
3265
- return typeof value === "string" && urlActionKinds.includes(value);
3266
- }
3267
- function optionalString(value) {
3268
- return typeof value === "string" && value ? value : null;
3269
- }
3270
- function bindStateKey(props) {
3271
- const value = props.value;
3272
- if (!isRecord(value)) return null;
3273
- const pointer = value.$bindState;
3274
- if (typeof pointer !== "string") return null;
3275
- return pointer.startsWith("/") ? pointer.slice(1) : pointer;
3276
- }
3277
- function parseButton(element) {
3278
- const props = isRecord(element.props) ? element.props : {};
3279
- const label = optionalString(props.label) ?? "Connect";
3280
- const on = isRecord(element.on) ? element.on : null;
3281
- const press = on && isRecord(on.press) ? on.press : null;
3282
- if (!press) return null;
3283
- const params = isRecord(press.params) ? press.params : {};
3284
- const primary = props.variant === "primary";
3285
- if (press.action === "approve_portal_access") {
3286
- const agentId = params.agentId;
3287
- if (typeof agentId !== "string" || !agentId) return null;
3288
- return {
3289
- label,
3290
- action: {
3291
- kind: "grant_portal",
3292
- agentId,
3293
- deviceId: typeof params.deviceId === "string" && params.deviceId ? params.deviceId : null
3294
- },
3295
- primary
3296
- };
3297
- }
3298
- if (!isUrlActionKind(press.action)) return null;
3299
- const url = params.url;
3300
- if (typeof url !== "string" || !url) return null;
3301
- return {
3302
- label,
3303
- action: {
3304
- kind: press.action,
3305
- url
3306
- },
3307
- primary
3308
- };
3309
- }
3310
- function parseConnectCard(spec) {
3311
- if (!isRecord(spec)) return null;
3312
- const { root, elements } = spec;
3313
- if (typeof root !== "string" || !isRecord(elements)) return null;
3314
- const rootEl = elements[root];
3315
- if (!isRecord(rootEl) || rootEl.type !== "Card") return null;
3316
- const rootProps = isRecord(rootEl.props) ? rootEl.props : {};
3317
- const title = optionalString(rootProps.title);
3318
- if (!title) return null;
3319
- const fields = [];
3320
- const buttons = [];
3321
- const children = Array.isArray(rootEl.children) ? rootEl.children : [];
3322
- for (const childId of children) {
3323
- if (typeof childId !== "string") continue;
3324
- const el = elements[childId];
3325
- if (!isRecord(el)) continue;
3326
- if (el.type === "TextInput" || el.type === "SecretInput") {
3327
- const props = isRecord(el.props) ? el.props : {};
3328
- const key = bindStateKey(props);
3329
- if (!key) continue;
3330
- fields.push({
3331
- key,
3332
- label: optionalString(props.label) ?? key,
3333
- placeholder: optionalString(props.placeholder),
3334
- secret: el.type === "SecretInput"
3335
- });
3336
- continue;
3337
- }
3338
- if (el.type === "Button") {
3339
- const button = parseButton(el);
3340
- if (button) buttons.push(button);
3341
- }
3342
- }
3343
- const preferred = buttons.find((b) => b.primary) ?? buttons[0] ?? null;
3344
- return {
3345
- title,
3346
- subtitle: optionalString(rootProps.subtitle),
3347
- description: optionalString(rootProps.description),
3348
- fields,
3349
- button: preferred ? {
3350
- label: preferred.label,
3351
- action: preferred.action
3352
- } : null,
3353
- state: isRecord(spec.state) ? spec.state : {}
3354
- };
3355
- }
3356
-
3357
3243
  //#endregion
3358
3244
  //#region src/chat/tui/chat/reducer.ts
3359
3245
  function str(chunk, key) {
@@ -3719,94 +3605,6 @@ function formatModelHint(model) {
3719
3605
  return `ctrl+p · ${model ?? "default"}`;
3720
3606
  }
3721
3607
 
3722
- //#endregion
3723
- //#region src/chat/tui/chat/card-actions.ts
3724
- function safeUrl(url) {
3725
- try {
3726
- return new URL(url, "http://localhost");
3727
- } catch (_error) {
3728
- return null;
3729
- }
3730
- }
3731
- /**
3732
- * Resolve a connect URL to an absolute one the OS can open in a browser.
3733
- *
3734
- * The server builds lazy connect links as server-relative paths (e.g.
3735
- * `/api/v1/oauth/start/slack?connect_session_token=...`). The web client
3736
- * resolves these against its own origin implicitly; the TUI runs outside a
3737
- * browser, so `open()` on a scheme-less path is a silent no-op — the browser
3738
- * never launches and the connect card sits in `launched` forever (the Slack
3739
- * channel-connect deadlock). Resolve against `appUrl` first, mirroring the
3740
- * `open_github_app` / `fulfillCredential` branches, so both the browser we
3741
- * launch and the URL shown on the card are absolute. An already-absolute URL
3742
- * passes through unchanged; if `appUrl` is missing we return the input as-is.
3743
- */
3744
- function resolveConnectUrl(url, appUrl) {
3745
- if (!appUrl) return url;
3746
- try {
3747
- return new URL(url, appUrl).toString();
3748
- } catch (_error) {
3749
- return url;
3750
- }
3751
- }
3752
- function parseOauthConnectParams(url) {
3753
- const parsed = safeUrl(url);
3754
- if (!parsed) return null;
3755
- const integrationKey = parsed.searchParams.get("integration");
3756
- const agentId = parsed.searchParams.get("agent_id");
3757
- const authConfigId = parsed.searchParams.get("auth_config_id");
3758
- const conversationId = parsed.searchParams.get("conversation_id");
3759
- if (!integrationKey || !agentId || !authConfigId || !conversationId) return null;
3760
- return {
3761
- integrationKey,
3762
- agentId,
3763
- authConfigId,
3764
- conversationId,
3765
- scopes: parsed.searchParams.get("scopes")
3766
- };
3767
- }
3768
- function parseExternalOauthConnectParams(url) {
3769
- const parsed = safeUrl(url);
3770
- if (!parsed) return null;
3771
- const agentId = parsed.searchParams.get("agent_id");
3772
- const conversationId = parsed.searchParams.get("conversation_id");
3773
- const serverUrl = parsed.searchParams.get("server_url");
3774
- if (!agentId || !conversationId || !serverUrl) return null;
3775
- return {
3776
- agentId,
3777
- conversationId,
3778
- serverUrl
3779
- };
3780
- }
3781
- /**
3782
- * A short human line for a failed card action. Fulfill/connect endpoints
3783
- * return `{ error: string }` bodies (e.g. "already fulfilled") — prefer that
3784
- * over the generic HttpError message.
3785
- */
3786
- function cardActionErrorMessage(err) {
3787
- if (err instanceof HttpError) {
3788
- try {
3789
- const parsed = JSON.parse(err.body);
3790
- if (isRecord(parsed) && typeof parsed.error === "string") return parsed.error;
3791
- } catch (_error) {}
3792
- return `request failed (HTTP ${err.status})`;
3793
- }
3794
- return errorMessage(err);
3795
- }
3796
- const MASK_CHAR = "•";
3797
- /**
3798
- * Recover the real secret from the masked input's displayed text. The input
3799
- * is controlled: after every edit we render bullets, which forces the cursor
3800
- * to the end, so the next edit is always a tail edit — the displayed text is
3801
- * some prefix of the old mask (kept characters) followed by newly typed or
3802
- * pasted plaintext. Characters beyond the retained bullets are the new tail.
3803
- */
3804
- function reconcileMaskedInput(previousValue, displayed) {
3805
- let kept = 0;
3806
- while (kept < displayed.length && kept < previousValue.length && displayed[kept] === MASK_CHAR) kept++;
3807
- return previousValue.slice(0, kept) + displayed.slice(kept);
3808
- }
3809
-
3810
3608
  //#endregion
3811
3609
  //#region src/chat/notification-preview.ts
3812
3610
  const MAX_NOTIFICATION_PREVIEW_CHARS = 240;
@@ -348,6 +348,293 @@ const runStreamEventSchema = z.union([z.object({
348
348
  })]);
349
349
  const streamErrorSchema = z.object({ error: z.string() });
350
350
 
351
+ //#endregion
352
+ //#region src/chat/util.ts
353
+ /** Narrowing helper for the many `unknown` payloads the chat stream and
354
+ * tool inputs/outputs carry. A type predicate (not an `as` cast), so call
355
+ * sites can read properties without asserting. */
356
+ function isRecord(value) {
357
+ return typeof value === "object" && value !== null && !Array.isArray(value);
358
+ }
359
+ /** Best-effort message from an unknown thrown value. */
360
+ function errorMessage(err) {
361
+ return err instanceof Error ? err.message : String(err);
362
+ }
363
+
364
+ //#endregion
365
+ //#region src/chat/tui/chat/card.ts
366
+ const urlActionKinds = [
367
+ "open_oauth",
368
+ "open_external_oauth",
369
+ "open_github_app",
370
+ "submit_credential"
371
+ ];
372
+ function isUrlActionKind(value) {
373
+ return typeof value === "string" && urlActionKinds.includes(value);
374
+ }
375
+ function optionalString(value) {
376
+ return typeof value === "string" && value ? value : null;
377
+ }
378
+ function bindStateKey(props) {
379
+ const value = props.value;
380
+ if (!isRecord(value)) return null;
381
+ const pointer = value.$bindState;
382
+ if (typeof pointer !== "string") return null;
383
+ return pointer.startsWith("/") ? pointer.slice(1) : pointer;
384
+ }
385
+ function parseButton(element) {
386
+ const props = isRecord(element.props) ? element.props : {};
387
+ const label = optionalString(props.label) ?? "Connect";
388
+ const on = isRecord(element.on) ? element.on : null;
389
+ const press = on && isRecord(on.press) ? on.press : null;
390
+ if (!press) return null;
391
+ const params = isRecord(press.params) ? press.params : {};
392
+ const primary = props.variant === "primary";
393
+ if (press.action === "approve_portal_access") {
394
+ const agentId = params.agentId;
395
+ if (typeof agentId !== "string" || !agentId) return null;
396
+ return {
397
+ label,
398
+ action: {
399
+ kind: "grant_portal",
400
+ agentId,
401
+ deviceId: typeof params.deviceId === "string" && params.deviceId ? params.deviceId : null
402
+ },
403
+ primary
404
+ };
405
+ }
406
+ if (!isUrlActionKind(press.action)) return null;
407
+ const url = params.url;
408
+ if (typeof url !== "string" || !url) return null;
409
+ return {
410
+ label,
411
+ action: {
412
+ kind: press.action,
413
+ url
414
+ },
415
+ primary
416
+ };
417
+ }
418
+ function parseConnectCard(spec) {
419
+ if (!isRecord(spec)) return null;
420
+ const { root, elements } = spec;
421
+ if (typeof root !== "string" || !isRecord(elements)) return null;
422
+ const rootEl = elements[root];
423
+ if (!isRecord(rootEl) || rootEl.type !== "Card") return null;
424
+ const rootProps = isRecord(rootEl.props) ? rootEl.props : {};
425
+ const title = optionalString(rootProps.title);
426
+ if (!title) return null;
427
+ const fields = [];
428
+ const buttons = [];
429
+ const children = Array.isArray(rootEl.children) ? rootEl.children : [];
430
+ for (const childId of children) {
431
+ if (typeof childId !== "string") continue;
432
+ const el = elements[childId];
433
+ if (!isRecord(el)) continue;
434
+ if (el.type === "TextInput" || el.type === "SecretInput") {
435
+ const props = isRecord(el.props) ? el.props : {};
436
+ const key = bindStateKey(props);
437
+ if (!key) continue;
438
+ fields.push({
439
+ key,
440
+ label: optionalString(props.label) ?? key,
441
+ placeholder: optionalString(props.placeholder),
442
+ secret: el.type === "SecretInput"
443
+ });
444
+ continue;
445
+ }
446
+ if (el.type === "Button") {
447
+ const button = parseButton(el);
448
+ if (button) buttons.push(button);
449
+ }
450
+ }
451
+ const preferred = buttons.find((b) => b.primary) ?? buttons[0] ?? null;
452
+ return {
453
+ title,
454
+ subtitle: optionalString(rootProps.subtitle),
455
+ description: optionalString(rootProps.description),
456
+ fields,
457
+ button: preferred ? {
458
+ label: preferred.label,
459
+ action: preferred.action
460
+ } : null,
461
+ state: isRecord(spec.state) ? spec.state : {}
462
+ };
463
+ }
464
+
465
+ //#endregion
466
+ //#region src/chat/tui/chat/card-actions.ts
467
+ function safeUrl(url) {
468
+ try {
469
+ return new URL(url, "http://localhost");
470
+ } catch (_error) {
471
+ return null;
472
+ }
473
+ }
474
+ /**
475
+ * Resolve a connect URL to an absolute one the OS can open in a browser.
476
+ *
477
+ * The server builds lazy connect links as server-relative paths (e.g.
478
+ * `/api/v1/oauth/start/slack?connect_session_token=...`). The web client
479
+ * resolves these against its own origin implicitly; the TUI runs outside a
480
+ * browser, so `open()` on a scheme-less path is a silent no-op — the browser
481
+ * never launches and the connect card sits in `launched` forever (the Slack
482
+ * channel-connect deadlock). Resolve against `appUrl` first, mirroring the
483
+ * `open_github_app` / `fulfillCredential` branches, so both the browser we
484
+ * launch and the URL shown on the card are absolute. An already-absolute URL
485
+ * passes through unchanged; if `appUrl` is missing we return the input as-is.
486
+ */
487
+ function resolveConnectUrl(url, appUrl) {
488
+ if (!appUrl) return url;
489
+ try {
490
+ return new URL(url, appUrl).toString();
491
+ } catch (_error) {
492
+ return url;
493
+ }
494
+ }
495
+ function parseOauthConnectParams(url) {
496
+ const parsed = safeUrl(url);
497
+ if (!parsed) return null;
498
+ const integrationKey = parsed.searchParams.get("integration");
499
+ const agentId = parsed.searchParams.get("agent_id");
500
+ const authConfigId = parsed.searchParams.get("auth_config_id");
501
+ const conversationId = parsed.searchParams.get("conversation_id");
502
+ if (!integrationKey || !agentId || !authConfigId || !conversationId) return null;
503
+ return {
504
+ integrationKey,
505
+ agentId,
506
+ authConfigId,
507
+ conversationId,
508
+ scopes: parsed.searchParams.get("scopes")
509
+ };
510
+ }
511
+ function parseExternalOauthConnectParams(url) {
512
+ const parsed = safeUrl(url);
513
+ if (!parsed) return null;
514
+ const agentId = parsed.searchParams.get("agent_id");
515
+ const conversationId = parsed.searchParams.get("conversation_id");
516
+ const serverUrl = parsed.searchParams.get("server_url");
517
+ if (!agentId || !conversationId || !serverUrl) return null;
518
+ return {
519
+ agentId,
520
+ conversationId,
521
+ serverUrl
522
+ };
523
+ }
524
+ /**
525
+ * A short human line for a failed card action. Fulfill/connect endpoints
526
+ * return `{ error: string }` bodies (e.g. "already fulfilled") — prefer that
527
+ * over the generic HttpError message.
528
+ */
529
+ function cardActionErrorMessage(err) {
530
+ if (err instanceof HttpError) {
531
+ try {
532
+ const parsed = JSON.parse(err.body);
533
+ if (isRecord(parsed) && typeof parsed.error === "string") return parsed.error;
534
+ } catch (_error) {}
535
+ return `request failed (HTTP ${err.status})`;
536
+ }
537
+ return errorMessage(err);
538
+ }
539
+ const MASK_CHAR = "•";
540
+ /**
541
+ * Recover the real secret from the masked input's displayed text. The input
542
+ * is controlled: after every edit we render bullets, which forces the cursor
543
+ * to the end, so the next edit is always a tail edit — the displayed text is
544
+ * some prefix of the old mask (kept characters) followed by newly typed or
545
+ * pasted plaintext. Characters beyond the retained bullets are the new tail.
546
+ */
547
+ function reconcileMaskedInput(previousValue, displayed) {
548
+ let kept = 0;
549
+ while (kept < displayed.length && kept < previousValue.length && displayed[kept] === MASK_CHAR) kept++;
550
+ return previousValue.slice(0, kept) + displayed.slice(kept);
551
+ }
552
+
553
+ //#endregion
554
+ //#region src/chat/connect-cards.ts
555
+ /**
556
+ * Turn a parsed ConnectCard into the headless summary, resolving any relative
557
+ * connect URL against the app origin so the emitted URL is directly openable.
558
+ */
559
+ function summarizeConnectCard(card, appUrl) {
560
+ const base = {
561
+ title: card.title,
562
+ subtitle: card.subtitle,
563
+ description: card.description
564
+ };
565
+ const button = card.button;
566
+ if (!button) return {
567
+ ...base,
568
+ action: { kind: "unsupported" }
569
+ };
570
+ const act = button.action;
571
+ switch (act.kind) {
572
+ case "open_oauth":
573
+ case "open_external_oauth":
574
+ case "open_github_app": return {
575
+ ...base,
576
+ action: {
577
+ kind: "open_url",
578
+ url: resolveConnectUrl(act.url, appUrl)
579
+ }
580
+ };
581
+ case "submit_credential": return {
582
+ ...base,
583
+ action: {
584
+ kind: "submit_credential",
585
+ url: resolveConnectUrl(act.url, appUrl),
586
+ fields: card.fields.map((f) => f.label)
587
+ }
588
+ };
589
+ case "grant_portal": return {
590
+ ...base,
591
+ action: {
592
+ kind: "approve_portal",
593
+ agentId: act.agentId
594
+ }
595
+ };
596
+ default: return {
597
+ ...base,
598
+ action: { kind: "unsupported" }
599
+ };
600
+ }
601
+ }
602
+ /**
603
+ * Parse a `data-anyone-render-spec` stream chunk into a connect-card summary,
604
+ * or null if the chunk isn't a connect card (other render specs — training,
605
+ * deep-learn, compute-request — parse to null, same as the TUI).
606
+ */
607
+ function connectCardFromChunk(chunk, appUrl) {
608
+ if (chunk["type"] !== "data-anyone-render-spec") return null;
609
+ const data = chunk["data"];
610
+ if (!isRecord(data)) return null;
611
+ const card = parseConnectCard(data["spec"]);
612
+ if (!card) return null;
613
+ return summarizeConnectCard(card, appUrl);
614
+ }
615
+ /** Render a connect-card summary as a human-readable action block. */
616
+ function formatConnectCard(card) {
617
+ const lines = [];
618
+ lines.push(`\n[action needed] ${card.title}`);
619
+ if (card.subtitle) lines.push(card.subtitle);
620
+ if (card.description) lines.push(card.description);
621
+ switch (card.action.kind) {
622
+ case "open_url":
623
+ lines.push(`Open to continue: ${card.action.url}`);
624
+ break;
625
+ case "submit_credential":
626
+ lines.push(`Provide credential (${card.action.fields.join(", ") || "value"}) at: ${card.action.url}`);
627
+ break;
628
+ case "approve_portal":
629
+ lines.push(`Approve local-machine access for agent ${card.action.agentId} in the TUI or web app.`);
630
+ break;
631
+ case "unsupported":
632
+ lines.push("Open this conversation in the web app to continue.");
633
+ break;
634
+ }
635
+ return lines.join("\n");
636
+ }
637
+
351
638
  //#endregion
352
639
  //#region src/chat/print.ts
353
640
  var print_exports = /* @__PURE__ */ __exportAll({
@@ -385,6 +672,7 @@ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversat
385
672
  let text = "";
386
673
  const controller = new AbortController();
387
674
  let streamError = null;
675
+ const connectCards = [];
388
676
  await client.streamRun({
389
677
  runId: send.runId,
390
678
  signal: controller.signal,
@@ -401,17 +689,23 @@ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversat
401
689
  if (!json) process.stdout.write(delta);
402
690
  }
403
691
  } else if (chunk["type"] === "error") streamError = typeof chunk["errorText"] === "string" ? chunk["errorText"] : "unknown error";
692
+ else {
693
+ const card = connectCardFromChunk(chunk, appUrl);
694
+ if (card) connectCards.push(card);
695
+ }
404
696
  }
405
697
  });
406
698
  if (streamError) throw new Error(streamError);
407
699
  if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
700
+ if (!json) for (const card of connectCards) process.stdout.write(`${formatConnectCard(card)}\n`);
408
701
  return {
409
702
  agentId: agent.id,
410
703
  agentName: agent.name,
411
704
  conversationId: send.conversationId,
412
705
  isNewConversation: send.isNewConversation,
413
706
  runId: send.runId,
414
- text
707
+ text,
708
+ connectCards
415
709
  };
416
710
  }
417
711
  /**
@@ -448,4 +742,4 @@ async function readStdin() {
448
742
  }
449
743
 
450
744
  //#endregion
451
- export { createRestClient as i, resolveAgent as n, HttpError as r, print_exports as t };
745
+ export { parseExternalOauthConnectParams as a, resolveConnectUrl as c, isRecord as d, HttpError as f, cardActionErrorMessage as i, parseConnectCard as l, resolveAgent as n, parseOauthConnectParams as o, createRestClient as p, MASK_CHAR as r, reconcileMaskedInput as s, print_exports as t, errorMessage as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.89",
3
+ "version": "0.1.0-beta.96",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",