skydive-cli 0.4.1-beta.6 → 0.5.0-beta.10

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
@@ -261,6 +261,16 @@ internal API. On launch it walks you through:
261
261
  formatting, collapsed reasoning, and rich rendering of tool calls
262
262
  (`bash`, `edit`/`write` diffs, `read`, `grep`/`glob`).
263
263
 
264
+ The chat TUI remembers the last agent you talked to: every conversation it
265
+ opens records that agent as your default, and the next bare `skydive chat`
266
+ drops straight into a new conversation with it — no picker. An explicit
267
+ `--agent` or `--resume` still wins for that invocation, and the conversation
268
+ it opens sets the default like any other. You can also seed or override the
269
+ default by hand with `skydive config set defaultAgent <id|slug|name>`. A
270
+ stored value that no longer matches (renamed or archived agent) falls back
271
+ to the agent picker with a notice, and picking an agent there heals it
272
+ automatically.
273
+
264
274
  You can change conversation without leaving chat. `/conversation` (aliases
265
275
  `/conversations`, `/conv`) reopens the conversation list, and
266
276
  `/conversation <id>` — an id, its `/c/<id>` link, or a pasted
@@ -463,10 +473,11 @@ skydive config path # print the config.json location
463
473
 
464
474
  Editable keys:
465
475
 
466
- | Key | Type | Effect |
467
- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
468
- | `shareMachineDefault` | boolean | When `true`, `skydive chat` (TUI and `-p`) shares this machine over the portal on launch, as if `--share-machine` were passed. An explicit `--share-machine`/`--no-share-machine` overrides it per invocation. Default `false`. |
469
- | `updateCheck` | boolean | When `false`, disables the daily background update check and its "Update available" notice. The persistent equivalent of setting `SKYDIVE_NO_UPDATE_CHECK` (or `NO_UPDATE_NOTIFIER`) in the environment. Default `true`. |
476
+ | Key | Type | Effect |
477
+ | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
478
+ | `shareMachineDefault` | boolean | When `true`, `skydive chat` (TUI and `-p`) shares this machine over the portal on launch, as if `--share-machine` were passed. An explicit `--share-machine`/`--no-share-machine` overrides it per invocation. Default `false`. |
479
+ | `updateCheck` | boolean | When `false`, disables the daily background update check and its "Update available" notice. The persistent equivalent of setting `SKYDIVE_NO_UPDATE_CHECK` (or `NO_UPDATE_NOTIFIER`) in the environment. Default `true`. |
480
+ | `defaultAgent` | string | Agent (id, slug, or name) a bare `skydive chat` opens a new conversation with, skipping the agent picker. Maintained automatically: every conversation the chat TUI opens records its agent here (last used wins). `skydive config set defaultAgent` seeds or overrides it; an explicit `--agent`/`--resume` wins per invocation; a value that no longer matches falls back to the picker. |
470
481
 
471
482
  The remaining keys (`apiKey`, `apiUrl`, `sessionToken`, `appUrl`, `themeDark`,
472
483
  `themeLight`, …) are credentials or flow-managed state — leave them to the
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
3
+ import { z } from "zod";
4
+
5
+ //#region ../portal-daemon/src/api.ts
6
+ /**
7
+ * The portal's session-authed REST surface, shared by `PortalClient` (the
8
+ * TUI/`portal open` connection) and the `skydive portal` management
9
+ * commands, so the endpoint contracts and response schemas live in exactly
10
+ * one place.
11
+ */
12
+ const deviceSchema = z.object({
13
+ id: z.string(),
14
+ machineName: z.string(),
15
+ friendlyName: z.string(),
16
+ connected: z.boolean(),
17
+ lastSeen: z.string().nullable(),
18
+ grantedAgentIds: z.array(z.string())
19
+ });
20
+ const devicesResponseSchema = z.object({
21
+ devices: z.array(deviceSchema),
22
+ agents: z.array(z.object({
23
+ id: z.string(),
24
+ name: z.string()
25
+ }))
26
+ });
27
+ const deviceTokenSchema = z.object({ token: z.string().min(1) });
28
+ async function portalFetch(auth, path, init) {
29
+ const res = await fetch(`${auth.appUrl}${path}`, {
30
+ method: init.method,
31
+ headers: {
32
+ authorization: `Bearer ${auth.sessionToken}`,
33
+ accept: "application/json",
34
+ ...init.body ? { "content-type": "application/json" } : {}
35
+ },
36
+ ...init.body ? { body: init.body } : {}
37
+ });
38
+ if (!res.ok) {
39
+ const body = await res.text().catch(() => "");
40
+ throw new HttpError(res.status, body);
41
+ }
42
+ return res.json();
43
+ }
44
+ async function fetchPortalDevices(auth) {
45
+ const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
46
+ return devicesResponseSchema.parse(json);
47
+ }
48
+ const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
49
+ /**
50
+ * Register this machine's device row without connecting. Connecting registers
51
+ * as a side effect; this covers granting an agent on a machine that has never
52
+ * shared yet (the grant references the device row).
53
+ */
54
+ async function registerPortalDevice(auth, { machineName, friendlyName }) {
55
+ const json = await portalFetch(auth, "/api/v1/portal/devices", {
56
+ method: "POST",
57
+ body: JSON.stringify({
58
+ machineName,
59
+ friendlyName
60
+ })
61
+ });
62
+ return registerResponseSchema.parse(json).device;
63
+ }
64
+ /** Short-lived token the machine presents when dialing the portal WebSocket. */
65
+ async function mintPortalDeviceToken(auth) {
66
+ const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
67
+ return deviceTokenSchema.parse(json).token;
68
+ }
69
+ const forwardTargetSchema = z.object({
70
+ daemonOrigin: z.string().min(1),
71
+ token: z.string().min(1),
72
+ expiresInSeconds: z.number()
73
+ });
74
+ /**
75
+ * Everything `portal forward` needs to dial an agent's sandbox daemon through
76
+ * the agent-webserver edge Worker: the daemon's stable public origin and a
77
+ * daemon auth token (canUse-gated server-side).
78
+ */
79
+ async function fetchForwardTarget(auth, agentId) {
80
+ const json = await portalFetch(auth, "/api/v1/portal/forward-target", {
81
+ method: "POST",
82
+ body: JSON.stringify({ agentId })
83
+ });
84
+ return forwardTargetSchema.parse(json);
85
+ }
86
+ async function grantPortalAccess(auth, { deviceId, agentId, conversationId }) {
87
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
88
+ method: "POST",
89
+ body: JSON.stringify({
90
+ agentId,
91
+ conversationId
92
+ })
93
+ });
94
+ }
95
+ async function revokePortalAccess(auth, { deviceId, agentId }) {
96
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
97
+ }
98
+ /**
99
+ * The device row for a given machine identity. Matching is by `machineName`
100
+ * equality — the stable handle the machine registers under, not the display
101
+ * label.
102
+ */
103
+ function findThisDevice(devices, machineName) {
104
+ return devices.find((device) => device.machineName === machineName) ?? null;
105
+ }
106
+ /**
107
+ * One-time grant migration onto the merged device. Earlier CLI builds
108
+ * registered a separate `<machineName>-cli` device, so a user's existing
109
+ * approvals hang off that row; the merged device would start with zero grants
110
+ * and every already-authorized agent would ask again. Copy any grant the
111
+ * merged device is missing (the grant endpoint upserts, so re-runs are
112
+ * no-ops). The legacy row is left in place — an old CLI build may still
113
+ * connect under it. Returns how many grants were copied.
114
+ */
115
+ async function unifyLegacyCliGrants(auth, machineName) {
116
+ const { devices } = await fetchPortalDevices(auth);
117
+ const merged = findThisDevice(devices, machineName);
118
+ const legacy = findThisDevice(devices, `${machineName}-cli`);
119
+ if (!merged || !legacy) return 0;
120
+ const have = new Set(merged.grantedAgentIds);
121
+ const missing = legacy.grantedAgentIds.filter((id) => !have.has(id));
122
+ for (const agentId of missing) await grantPortalAccess(auth, {
123
+ deviceId: merged.id,
124
+ agentId,
125
+ conversationId: null
126
+ });
127
+ return missing.length;
128
+ }
129
+
130
+ //#endregion
131
+ export { mintPortalDeviceToken as a, unifyLegacyCliGrants as c, grantPortalAccess as i, fetchPortalDevices as n, registerPortalDevice as o, findThisDevice as r, revokePortalAccess as s, fetchForwardTarget as t };
package/dist/js/bin.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-94qrV53c.mjs";
3
- import { A as getStoredApiKeyWorkspaceName, C as getLastSeenVersion, F as resolveManagementAuth, I as resolveSession, M as resolveAppUrl, N as resolveChatAuth, O as getShareMachineDefault, P as resolveConfig, R as saveConfig, S as getConfigPath, T as getPromptHistoryPath, V as setLastSeenVersion, _ as API_KEY_PREFIX, b as PREFERENCES, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as getUpdateCheckDisabled, k as getStoredApiKeyId, v as DEFAULT_API_URL, w as getPreference, x as deleteConfig, z as saveSession } from "./print-CbayCa87.mjs";
2
+ import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-Zs3BHURc.mjs";
3
+ import { A as getStoredApiKeyId, B as saveConfig, C as getDefaultAgent, E as getPromptHistoryPath, F as resolveChatAuth, I as resolveConfig, L as resolveManagementAuth, M as getUpdateCheckDisabled, P as resolveAppUrl, R as resolveSession, S as getConfigPath, T as getPreference, U as setLastSeenVersion, V as saveSession, _ as API_KEY_PREFIX, b as PREFERENCES, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as getStoredApiKeyWorkspaceName, k as getShareMachineDefault, v as DEFAULT_API_URL, w as getLastSeenVersion, x as deleteConfig } from "./print-ClPkgR9z.mjs";
4
4
  import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
5
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
6
- import { t as createRestClient } from "./rest-BY2nADw5.mjs";
6
+ import { t as createRestClient } from "./rest-I3imNduB.mjs";
7
7
  import "./billing-blocked-2wju4gC_.mjs";
8
- import { a as registerPortalDevice, c as resolveMachineIdentity, i as grantPortalAccess, n as fetchPortalDevices, o as revokePortalAccess, r as findThisDevice } from "./client-mykp1DVb.mjs";
9
- import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-CfbpCjAw.mjs";
10
- import { t as SandboxStream } from "./client-Cn2af31H.mjs";
8
+ import { r as resolveMachineIdentity } from "./client-DabRpc_T.mjs";
9
+ import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-CYYE2BTu.mjs";
10
+ import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-DG5W6iwx.mjs";
11
+ import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
11
12
  import { hideBin } from "yargs/helpers";
12
13
  import yargs from "yargs";
13
14
  import os, { hostname, tmpdir } from "node:os";
@@ -1516,7 +1517,7 @@ const importCommand = {
1516
1517
  process.exit(1);
1517
1518
  }
1518
1519
  }
1519
- const { runChat } = await import("./boot-B48inW5D.mjs");
1520
+ const { runChat } = await import("./boot-BuxaHgFi.mjs");
1520
1521
  await runChat({
1521
1522
  appUrl,
1522
1523
  sessionToken: session.value.sessionToken,
@@ -1526,6 +1527,7 @@ const importCommand = {
1526
1527
  agentSelector: argv.agent ?? null,
1527
1528
  conversationId: null,
1528
1529
  newConversation: false,
1530
+ defaultAgentSelector: getDefaultAgent(),
1529
1531
  seedPrompt: buildImportSeedPrompt(process.cwd())
1530
1532
  });
1531
1533
  }
@@ -1543,7 +1545,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1543
1545
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
1544
1546
  process.exit(1);
1545
1547
  }
1546
- const { connectMachineShare } = await import("./print-share-ZV88PqDu.mjs");
1548
+ const { connectMachineShare } = await import("./print-share-CKLPmsg0.mjs");
1547
1549
  const machineShare = await connectMachineShare({
1548
1550
  appUrl,
1549
1551
  sessionToken: session.value.sessionToken,
@@ -1551,7 +1553,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1551
1553
  });
1552
1554
  const extra = (argv.print ?? "").trim();
1553
1555
  const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
1554
- const { runPrint } = await import("./print-ba_0hiV9.mjs");
1556
+ const { runPrint } = await import("./print-Bo06tvfV.mjs");
1555
1557
  try {
1556
1558
  const result = await runPrint({
1557
1559
  appUrl,
@@ -1928,7 +1930,7 @@ const chatCommand = {
1928
1930
  sessionToken: auth.value.token,
1929
1931
  agentSelector: argv.agent ?? null
1930
1932
  });
1931
- const { runChat } = await import("./boot-B48inW5D.mjs");
1933
+ const { runChat } = await import("./boot-BuxaHgFi.mjs");
1932
1934
  await runChat({
1933
1935
  appUrl: auth.value.appUrl,
1934
1936
  sessionToken: auth.value.token,
@@ -1940,6 +1942,7 @@ const chatCommand = {
1940
1942
  agentSelector: argv.agent ?? null,
1941
1943
  conversationId: argv.conversation ?? null,
1942
1944
  newConversation: resolveNewConversation(argv),
1945
+ defaultAgentSelector: getDefaultAgent(),
1943
1946
  seedPrompt: null
1944
1947
  });
1945
1948
  }
@@ -1995,7 +1998,7 @@ async function runPrintMode({ argv, appUrl }) {
1995
1998
  printError(`${auth.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
1996
1999
  process.exit(1);
1997
2000
  }
1998
- const { runPrint, readStdin } = await import("./print-ba_0hiV9.mjs");
2001
+ const { runPrint, readStdin } = await import("./print-Bo06tvfV.mjs");
1999
2002
  await ensureAgentWorkspace({
2000
2003
  appUrl,
2001
2004
  sessionToken: auth.value.token,
@@ -2020,7 +2023,7 @@ async function runPrintMode({ argv, appUrl }) {
2020
2023
  process.exit(1);
2021
2024
  }
2022
2025
  } else {
2023
- const { connectMachineShare } = await import("./print-share-ZV88PqDu.mjs");
2026
+ const { connectMachineShare } = await import("./print-share-CKLPmsg0.mjs");
2024
2027
  machineShare = await connectMachineShare({
2025
2028
  appUrl: auth.value.appUrl,
2026
2029
  sessionToken: auth.value.token,
@@ -2084,7 +2087,7 @@ const getCommand$1 = {
2084
2087
  printError(`${auth.error.message} Run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
2085
2088
  process.exit(1);
2086
2089
  }
2087
- const { messageGet } = await import("./print-ba_0hiV9.mjs");
2090
+ const { messageGet } = await import("./print-Bo06tvfV.mjs");
2088
2091
  try {
2089
2092
  const result = await messageGet({
2090
2093
  appUrl: auth.value.appUrl,
@@ -2287,7 +2290,7 @@ const switchCommand = {
2287
2290
  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.");
2288
2291
  process.exit(1);
2289
2292
  }
2290
- const { runWorkspacePicker } = await import("./boot-B48inW5D.mjs");
2293
+ const { runWorkspacePicker } = await import("./boot-BuxaHgFi.mjs");
2291
2294
  await runWorkspacePicker(session);
2292
2295
  return;
2293
2296
  }
@@ -2366,7 +2369,7 @@ const openCommand = {
2366
2369
  const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
2367
2370
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
2368
2371
  const { machineName } = await resolveMachineIdentity();
2369
- const { PortalDaemonClient } = await import("./daemon-client-BHehCLrO.mjs");
2372
+ const { PortalDaemonClient } = await import("./daemon-client-Dh7G9RxC.mjs");
2370
2373
  let lastLine = "";
2371
2374
  let signalConnected;
2372
2375
  const connected = new Promise((resolve) => {
@@ -2395,7 +2398,7 @@ const openCommand = {
2395
2398
  client.enable();
2396
2399
  await connected;
2397
2400
  if (agent) {
2398
- await client.grantAgent(agent.id);
2401
+ await client.grantAgent(agent.id, null);
2399
2402
  console.log(`portal: granted ${agent.name} access to this machine (persists until revoked)`);
2400
2403
  }
2401
2404
  console.log(`portal: open. granted agents can run commands on ${machineName} as your user, cwd ${cwd}. ctrl+c to close.`);
@@ -2415,7 +2418,8 @@ const grantCommand = {
2415
2418
  })).id;
2416
2419
  await grantPortalAccess(session, {
2417
2420
  deviceId,
2418
- agentId: agent.id
2421
+ agentId: agent.id,
2422
+ conversationId: null
2419
2423
  });
2420
2424
  if (argv.json) {
2421
2425
  output(argv, {
@@ -2424,7 +2428,7 @@ const grantCommand = {
2424
2428
  });
2425
2429
  return;
2426
2430
  }
2427
- console.log(`Granted ${agent.name} access to ${device?.friendlyName ?? friendlyName}. The agent can run commands here whenever its portal is open. Start with \`skydive portal open\`, or pass \`--share-machine\` on a \`chat -p\` run. Revoke with \`skydive portal revoke --agent ${agent.name}\`.`);
2431
+ console.log(`Granted ${agent.name} access to ${device?.friendlyName ?? friendlyName}. The agent can run commands here whenever its portal is open. Start with \`skydive portal open\`, or pass \`--share-machine\` on a \`chat -p\` run. Revoke with \`skydive portal revoke --agent "${agent.name}"\`.`);
2428
2432
  }
2429
2433
  };
2430
2434
  const revokeCommand = {
@@ -2592,10 +2596,45 @@ const daemonCommand = {
2592
2596
  builder: (y) => y.command(daemonStatusCommand).command(daemonStopCommand).command(daemonStartCommand).command(daemonRestartCommand).command(daemonLogsCommand).demandCommand(1, "Specify a subcommand: status, stop, start, restart, logs"),
2593
2597
  handler: () => {}
2594
2598
  };
2599
+ const forwardCommand = {
2600
+ command: "forward <port>",
2601
+ describe: "Forward localhost:<port> to the agent sandbox's port, so a dev server running in the sandbox is reachable at http://localhost:<port> (stays in the foreground; ctrl+c to stop)",
2602
+ builder: (y) => y.positional("port", {
2603
+ type: "number",
2604
+ demandOption: true,
2605
+ describe: "Sandbox port to forward to (e.g. 3000 for a dev server)"
2606
+ }).option("agent", {
2607
+ type: "string",
2608
+ describe: "Agent, by id or name (defaults when the account has one)"
2609
+ }).option("local-port", {
2610
+ type: "number",
2611
+ describe: "Local port to listen on (default: same as <port>)"
2612
+ }),
2613
+ handler: async (argv) => {
2614
+ const session = requireSession(argv);
2615
+ const agent = resolveAgent((await fetchPortalDevices(session)).agents, argv.agent ?? null);
2616
+ const targetPort = argv.port;
2617
+ const localPort = argv["local-port"] ?? targetPort;
2618
+ const { startForward } = await import("./forward-18QoL5dO.mjs");
2619
+ const listener = await startForward({
2620
+ auth: session,
2621
+ agentId: agent.id,
2622
+ localPort,
2623
+ targetPort,
2624
+ log: (msg) => console.error(msg)
2625
+ });
2626
+ console.log(`Forwarding http://localhost:${listener.port} -> ${agent.name}'s sandbox port ${targetPort}. ctrl+c to stop.`);
2627
+ await new Promise((resolve) => {
2628
+ process.once("SIGINT", () => resolve());
2629
+ process.once("SIGTERM", () => resolve());
2630
+ });
2631
+ await listener.close();
2632
+ }
2633
+ };
2595
2634
  const portalCommand = {
2596
2635
  command: "portal",
2597
2636
  describe: "Open this machine's portal to agents and manage their access",
2598
- builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, daemon"),
2637
+ builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(forwardCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, forward, daemon"),
2599
2638
  handler: () => {}
2600
2639
  };
2601
2640
 
@@ -2640,8 +2679,8 @@ const sandboxCommand = {
2640
2679
  }).example("skydive sandbox --agent grace", "Live terminal (Ctrl-] detaches)").example("skydive sandbox --agent grace -- tail -n 50 /tmp/harness.log", "One-shot command (use `--` so its flags reach the sandbox)").example("skydive sandbox --agent grace -- sh -c 'ls /tmp | wc -l'", "Shell features go through an explicit `sh -c`"),
2641
2680
  handler: async (argv) => {
2642
2681
  const session = requireSession(argv);
2643
- const { createRestClient } = await import("./rest-DADJh0bi.mjs");
2644
- const { resolveAgent } = await import("./print-ba_0hiV9.mjs");
2682
+ const { createRestClient } = await import("./rest-D29qNkto.mjs");
2683
+ const { resolveAgent } = await import("./print-Bo06tvfV.mjs");
2645
2684
  const client = createRestClient({
2646
2685
  appUrl: session.appUrl,
2647
2686
  sessionToken: session.sessionToken
@@ -2710,7 +2749,7 @@ async function runPty({ session, agentId, agentName }) {
2710
2749
  return 1;
2711
2750
  }
2712
2751
  console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
2713
- const { runRawPtyPassthrough } = await import("./raw-pty-3EkG-jjH.mjs");
2752
+ const { runRawPtyPassthrough } = await import("./raw-pty-DmdUf4_w.mjs");
2714
2753
  const result = await runRawPtyPassthrough({
2715
2754
  stdin: process.stdin,
2716
2755
  stdout: process.stdout,
@@ -2798,8 +2837,8 @@ const fsEditCommand = {
2798
2837
  handler: async (argv) => {
2799
2838
  const session = requireSession(argv);
2800
2839
  const remotePath = argv.path;
2801
- const { createRestClient } = await import("./rest-DADJh0bi.mjs");
2802
- const { resolveAgent } = await import("./print-ba_0hiV9.mjs");
2840
+ const { createRestClient } = await import("./rest-D29qNkto.mjs");
2841
+ const { resolveAgent } = await import("./print-Bo06tvfV.mjs");
2803
2842
  const client = createRestClient({
2804
2843
  appUrl: session.appUrl,
2805
2844
  sessionToken: session.sessionToken
@@ -3680,6 +3719,11 @@ function resolvePreference(key) {
3680
3719
  }
3681
3720
  return pref;
3682
3721
  }
3722
+ /** Human rendering of a preference value; unset string preferences (null)
3723
+ * print as empty rather than the string "null". */
3724
+ function formatValue(value) {
3725
+ return value === null ? "" : String(value);
3726
+ }
3683
3727
  /** One row's worth of state for a preference, used by `list` and `get`. */
3684
3728
  function snapshot(pref) {
3685
3729
  return {
@@ -3706,7 +3750,7 @@ const listCommand = {
3706
3750
  "Description"
3707
3751
  ], rows.map((r) => [
3708
3752
  r.key,
3709
- String(r.value),
3753
+ formatValue(r.value),
3710
3754
  r.source,
3711
3755
  r.describe
3712
3756
  ]));
@@ -3727,7 +3771,7 @@ const getCommand = {
3727
3771
  output(argv, snapshot(pref));
3728
3772
  return;
3729
3773
  }
3730
- console.log(String(pref.read()));
3774
+ console.log(formatValue(pref.read()));
3731
3775
  }
3732
3776
  };
3733
3777
  const setCommand = {
@@ -3745,17 +3789,16 @@ const setCommand = {
3745
3789
  }),
3746
3790
  handler: (argv) => {
3747
3791
  const pref = resolvePreference(argv.key);
3748
- const parsed = pref.parse(argv.value ?? "");
3749
- if (parsed.isErr()) {
3750
- printError(`${pref.key}: ${parsed.error}`);
3792
+ const result = pref.set(argv.value ?? "");
3793
+ if (result.isErr()) {
3794
+ printError(`${pref.key}: ${result.error}`);
3751
3795
  process.exit(1);
3752
3796
  }
3753
- pref.write(parsed.value);
3754
3797
  if (argv.json) {
3755
3798
  output(argv, snapshot(pref));
3756
3799
  return;
3757
3800
  }
3758
- if (!argv.quiet) console.log(`Set ${pref.key} = ${String(parsed.value)}`);
3801
+ if (!argv.quiet) console.log(`Set ${pref.key} = ${formatValue(pref.read())}`);
3759
3802
  }
3760
3803
  };
3761
3804
  const unsetCommand = {
@@ -3775,7 +3818,10 @@ const unsetCommand = {
3775
3818
  output(argv, snapshot(pref));
3776
3819
  return;
3777
3820
  }
3778
- if (!argv.quiet) console.log(`Unset ${pref.key} (now ${String(pref.read())} by default)`);
3821
+ if (!argv.quiet) {
3822
+ const fallback = pref.read();
3823
+ console.log(fallback === null ? `Unset ${pref.key}` : `Unset ${pref.key} (now ${formatValue(fallback)} by default)`);
3824
+ }
3779
3825
  }
3780
3826
  };
3781
3827
  const pathCommand = {
@@ -4169,7 +4215,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
4169
4215
  process.exit(0);
4170
4216
  }
4171
4217
  if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
4172
- const { runPortalDaemon } = await import("./daemon-D4ZX3wNG.mjs");
4218
+ const { runPortalDaemon } = await import("./daemon-Dk5LeS1S.mjs");
4173
4219
  runPortalDaemon(process.argv);
4174
4220
  } else runCli();
4175
4221
  function runCli() {