skydive-cli 0.5.0-beta.22 → 0.5.0-beta.24

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/dist/js/bin.mjs CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { A as setActiveWorkspace, D as getActiveWorkspaceId, E as ensureActiveOrganization, M as version, O as getSessionIdentity, S as themes, T as machineOsFromPlatform, a as installCrashHandler, j as name, k as listWorkspaces, t as maybeStartProfiling, u as brandHelpArt, w as buildImportSeedPrompt } from "./profiler-zp3MKUcM.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-OvYyj6Uk.mjs";
2
+ import { A as takenAliasNames, B as name, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, I as getSessionIdentity, L as listWorkspaces, M as defaultInstallEnv, N as detectShell, O as installAgentAlias, P as ensureActiveOrganization, R as resolveWorkspaceId, S as themes, T as machineOsFromPlatform, V as version, a as installCrashHandler, j as SUPPORTED_SHELLS, k as slugifyAliasName, t as maybeStartProfiling, u as brandHelpArt, w as buildImportSeedPrompt, z as setActiveWorkspace } from "./profiler-DxmQ_Xqv.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$1, j as getStoredApiKeyWorkspaceName, k as getShareMachineDefault, v as DEFAULT_API_URL, w as getLastSeenVersion, x as deleteConfig } from "./print-DQtIEVHh.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
6
  import { t as createRestClient } from "./rest-Dx6b-Nq_.mjs";
7
7
  import "./billing-blocked-2wju4gC_.mjs";
8
8
  import { r as resolveMachineIdentity } from "./client-DbqRBquD.mjs";
9
- import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-Dnv9p14X.mjs";
9
+ import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-BBeAB5SM.mjs";
10
10
  import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-DG5W6iwx.mjs";
11
11
  import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
12
12
  import { hideBin } from "yargs/helpers";
13
13
  import yargs from "yargs";
14
- import os, { hostname, tmpdir } from "node:os";
14
+ import { hostname, tmpdir } from "node:os";
15
15
  import path, { join } from "node:path";
16
16
  import { err, ok } from "neverthrow";
17
17
  import { z } from "zod";
@@ -70,10 +70,12 @@ var SkydiveApiClient = class {
70
70
  baseUrl;
71
71
  token;
72
72
  authKind;
73
+ workspaceId;
73
74
  constructor(config) {
74
75
  this.baseUrl = `${config.apiUrl.replace(/\/$/, "")}/v1`;
75
76
  this.token = config.token;
76
77
  this.authKind = config.kind;
78
+ this.workspaceId = config.workspaceId;
77
79
  }
78
80
  /**
79
81
  * List one page of agents in the caller's workspace. The server caps a
@@ -159,6 +161,7 @@ var SkydiveApiClient = class {
159
161
  authorization: `Bearer ${this.token}`,
160
162
  "user-agent": USER_AGENT
161
163
  };
164
+ if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
162
165
  if (params.body) headers["content-type"] = "application/json";
163
166
  let response;
164
167
  try {
@@ -718,11 +721,6 @@ const authCommand = {
718
721
  * matches on it to keep notices off the user's prompt (see isCompletionProbe).
719
722
  */
720
723
  const COMPLETION_PROBE_FLAG = "--get-yargs-completions";
721
- const SUPPORTED_SHELLS = [
722
- "bash",
723
- "zsh",
724
- "fish"
725
- ];
726
724
  /**
727
725
  * True when this invocation is a shell asking for completion candidates
728
726
  * rather than a user running a command. Such a run must print candidates and
@@ -894,24 +892,6 @@ complete -c ${name} -f -k -a '(${fn})'
894
892
  ${blockEnd(name)}
895
893
  `;
896
894
  }
897
- const defaultInstallEnv = () => ({
898
- home: os.homedir(),
899
- platform: process.platform,
900
- env: process.env,
901
- exists: fs.existsSync
902
- });
903
- /**
904
- * The shell to install for when the user didn't name one. `$SHELL` is the
905
- * only signal available: a completion install runs as a child process, so the
906
- * invoking shell's own variables (`$ZSH_VERSION`, `$FISH_VERSION`) aren't
907
- * visible here.
908
- */
909
- function detectShell(env = process.env) {
910
- const shell = env["SHELL"];
911
- if (!shell) return null;
912
- const name = path.basename(shell);
913
- return SUPPORTED_SHELLS.find((candidate) => name === candidate) ?? null;
914
- }
915
895
  function completionTarget(shell, deps = defaultInstallEnv()) {
916
896
  const { home, platform, env, exists } = deps;
917
897
  if (shell === "fish") {
@@ -1032,7 +1012,7 @@ function requireSession(argv) {
1032
1012
  * {@link resolveManagementAuth}) or exit with a friendly hint, then build the
1033
1013
  * `/v1` client. Shared by `agents`, `keys` and `secrets`.
1034
1014
  */
1035
- function requireManagementClient(argv) {
1015
+ function requireManagementClient(argv, opts = {}) {
1036
1016
  const auth = resolveManagementAuth({ apiUrl: argv["api-url"] });
1037
1017
  if (auth.isErr()) {
1038
1018
  printError(auth.error.message);
@@ -1040,7 +1020,10 @@ function requireManagementClient(argv) {
1040
1020
  }
1041
1021
  if (auth.value.kind === "api-key" && auth.value.pinnedWorkspaceName) process.stderr.write(`Using this machine's API key, which always acts in the "${auth.value.pinnedWorkspaceName}" workspace (even after \`workspace switch\`). Run \`skydive auth login\` to change that.
1042
1022
  `);
1043
- return new SkydiveApiClient(auth.value);
1023
+ return new SkydiveApiClient({
1024
+ ...auth.value,
1025
+ ...opts.workspaceId ? { workspaceId: opts.workspaceId } : {}
1026
+ });
1044
1027
  }
1045
1028
  /** Resolve the session (see {@link requireSession}) and build a REST client. */
1046
1029
  function requireRestClient(argv) {
@@ -1057,6 +1040,31 @@ function requireRestClient(argv) {
1057
1040
 
1058
1041
  //#endregion
1059
1042
  //#region src/commands/agents.ts
1043
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1044
+ /**
1045
+ * Resolve an agent from an id or a name. A uuid selector hits `GET
1046
+ * /agents/:id` directly; anything else is matched case-insensitively against
1047
+ * the roster's names. Ambiguity is an error rather than a silent pick, so an
1048
+ * alias never lands on the wrong agent.
1049
+ */
1050
+ async function resolveAgent(client, selector) {
1051
+ if (UUID_RE.test(selector)) {
1052
+ const result = await client.getAgent(selector);
1053
+ if (result.isErr()) throw new Error(result.error.message);
1054
+ return result.value;
1055
+ }
1056
+ const list = await client.listAgents({
1057
+ scope: "org",
1058
+ limit: 100
1059
+ });
1060
+ if (list.isErr()) throw new Error(list.error.message);
1061
+ const needle = selector.trim().toLowerCase();
1062
+ const matches = list.value.agents.filter((a) => a.name.toLowerCase() === needle);
1063
+ const [first] = matches;
1064
+ if (matches.length === 1 && first) return first;
1065
+ if (matches.length === 0) throw new Error(`No agent named "${selector}". Pass an id, or run \`skydive agents list\`.`);
1066
+ throw new Error(`Multiple agents named "${selector}". Pass the id instead:\n${matches.map((a) => ` ${a.name} ${a.id}`).join("\n")}`);
1067
+ }
1060
1068
  const listCommand$5 = {
1061
1069
  command: "list",
1062
1070
  describe: "List agents in the active workspace",
@@ -1206,10 +1214,82 @@ const createCommand$1 = {
1206
1214
  if (agent.url) console.log(` API: ${agent.url}`);
1207
1215
  }
1208
1216
  };
1217
+ /**
1218
+ * Resolve the `--workspace` selector (slug/name/id) to a workspace, using the
1219
+ * chat/session credential (workspace listing is a session endpoint). Exits
1220
+ * with a clear message on a bad selector or when listing isn't available
1221
+ * (e.g. a pinned API key). Returns null when the flag is absent.
1222
+ */
1223
+ async function resolveAliasWorkspace(argv) {
1224
+ if (!argv.workspace) return null;
1225
+ const auth = resolveChatAuth({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
1226
+ if (auth.isErr()) {
1227
+ printError(`${auth.error.message} Run \`skydive auth login\` to resolve a workspace by name.`);
1228
+ process.exit(1);
1229
+ }
1230
+ const resolved = await resolveWorkspaceId({
1231
+ appUrl: auth.value.appUrl,
1232
+ sessionToken: auth.value.token,
1233
+ selector: argv.workspace
1234
+ });
1235
+ if (resolved.isErr()) {
1236
+ printError(resolved.error.message);
1237
+ process.exit(1);
1238
+ }
1239
+ return resolved.value;
1240
+ }
1241
+ const aliasCommand = {
1242
+ command: "alias <agent> [name]",
1243
+ describe: "Add a shell alias that opens an agent's chat",
1244
+ builder: (y) => y.positional("agent", {
1245
+ type: "string",
1246
+ demandOption: true,
1247
+ describe: "Agent id or name to alias"
1248
+ }).positional("name", {
1249
+ type: "string",
1250
+ describe: "Alias to use (default: derived from the agent name)"
1251
+ }).option("shell", {
1252
+ type: "string",
1253
+ choices: SUPPORTED_SHELLS,
1254
+ describe: "Shell to install for (default: $SHELL)"
1255
+ }).option("workspace", {
1256
+ type: "string",
1257
+ describe: "Workspace the agent lives in (slug, name, or id). Scopes the lookup to it and pins it in the alias, so the alias opens the right agent even when it isn't your active workspace."
1258
+ }).example("skydive agents alias Ripple", "alias `ripple` -> that agent").example("skydive agents alias Ripple rip", "use a custom alias token").example("skydive agents alias Ripple --workspace acme", "pin the workspace in the alias"),
1259
+ handler: async (argv) => {
1260
+ const shell = argv.shell ? SUPPORTED_SHELLS.find((s) => s === argv.shell) : detectShell();
1261
+ if (!shell) throw new Error(argv.shell ? `Unsupported shell "${argv.shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}.` : `Could not tell which shell to use from $SHELL. Name one with --shell (${SUPPORTED_SHELLS.join(", ")}).`);
1262
+ const workspace = await resolveAliasWorkspace(argv);
1263
+ const agent = await resolveAgent(requireManagementClient(argv, { workspaceId: workspace?.id }), argv.agent);
1264
+ const preferred = argv.name ? slugifyAliasName(argv.name) : slugifyAliasName(agent.name);
1265
+ const alias = dedupeAliasName(preferred, takenAliasNames(shell));
1266
+ const result = installAgentAlias({
1267
+ alias,
1268
+ agentId: agent.id,
1269
+ shell,
1270
+ ...workspace ? { workspaceId: workspace.id } : {}
1271
+ }, void 0);
1272
+ if (argv.json) {
1273
+ output(argv, {
1274
+ ...result,
1275
+ workspace: workspace ?? null
1276
+ });
1277
+ return;
1278
+ }
1279
+ if (argv.quiet) {
1280
+ console.log(result.alias);
1281
+ return;
1282
+ }
1283
+ const where = workspace ? ` (workspace ${workspace.name})` : "";
1284
+ console.log(`${result.updated ? "Updated" : "Added"} alias \`${result.alias}\` -> ${agent.name}${where} in ${result.path}`);
1285
+ if (alias !== preferred) console.log(`(\`${preferred}\` was taken, so used \`${alias}\`.)`);
1286
+ console.log(aliasActivationHint(result));
1287
+ }
1288
+ };
1209
1289
  const agentsCommand = {
1210
1290
  command: "agents",
1211
1291
  describe: "Manage agents",
1212
- builder: (y) => y.command(listCommand$5).command(getCommand$2).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
1292
+ builder: (y) => y.command(listCommand$5).command(getCommand$2).command(createCommand$1).command(aliasCommand).demandCommand(1, "Specify a subcommand: list, get, create, alias"),
1213
1293
  handler: () => {}
1214
1294
  };
1215
1295
 
@@ -1504,10 +1584,11 @@ const importCommand = {
1504
1584
  process.exit(1);
1505
1585
  }
1506
1586
  }
1507
- const { runChat } = await import("./boot-DdYXpKMY.mjs");
1587
+ const { runChat } = await import("./boot-BPzsuEQF.mjs");
1508
1588
  await runChat({
1509
1589
  appUrl,
1510
1590
  sessionToken: session.value.sessionToken,
1591
+ workspaceId: void 0,
1511
1592
  shareMachine: true,
1512
1593
  promptHistoryPath: getPromptHistoryPath(),
1513
1594
  notifications: true,
@@ -1541,7 +1622,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1541
1622
  });
1542
1623
  const extra = (argv.print ?? "").trim();
1543
1624
  const prompt = buildImportSeedPrompt(process.cwd(), machineOsFromPlatform(process.platform)) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
1544
- const { runPrint } = await import("./print-C8DHKrJu.mjs");
1625
+ const { runPrint } = await import("./print-Pi0PQRj8.mjs");
1545
1626
  try {
1546
1627
  const result = await runPrint({
1547
1628
  appUrl,
@@ -1849,6 +1930,24 @@ async function resolveAgentWorkspace({ appUrl, sessionToken, agentSelector }) {
1849
1930
 
1850
1931
  //#endregion
1851
1932
  //#region src/commands/chat.ts
1933
+ /**
1934
+ * Resolve the `--workspace` selector to an id, or undefined when the flag is
1935
+ * absent. A bad selector is fatal — a chat scoped to the wrong (or a
1936
+ * nonexistent) workspace is worse than failing fast with how to list them.
1937
+ */
1938
+ async function resolveWorkspaceFlag({ appUrl, sessionToken, selector }) {
1939
+ if (!selector) return void 0;
1940
+ const resolved = await resolveWorkspaceId({
1941
+ appUrl,
1942
+ sessionToken,
1943
+ selector
1944
+ });
1945
+ if (resolved.isErr()) {
1946
+ printError(resolved.error.message);
1947
+ process.exit(1);
1948
+ }
1949
+ return resolved.value.id;
1950
+ }
1852
1951
  const chatCommand = {
1853
1952
  command: "chat",
1854
1953
  describe: "Open the interactive chat TUI, or -p for a one-shot",
@@ -1859,6 +1958,9 @@ const chatCommand = {
1859
1958
  }).option("agent", {
1860
1959
  type: "string",
1861
1960
  describe: "Target agent, by id, slug, or name. With -p, the agent to send the one-shot prompt to. Without -p, pre-selects the agent and opens its conversation list, skipping the agent picker. Optional when the account has exactly one agent."
1961
+ }).option("workspace", {
1962
+ type: "string",
1963
+ describe: "Scope this chat to a workspace, by slug, name, or id. Every request for this invocation is scoped to it (no persistent switch of your active workspace), so a slug/name --agent resolves inside it and the roster is that workspace. Useful in an alias for an agent that lives in a workspace other than your active one."
1862
1964
  }).option("conversation", {
1863
1965
  alias: "resume",
1864
1966
  type: "string",
@@ -1912,21 +2014,27 @@ const chatCommand = {
1912
2014
  process.exit(1);
1913
2015
  }
1914
2016
  }
2017
+ const workspaceId = await resolveWorkspaceFlag({
2018
+ appUrl,
2019
+ sessionToken: auth.value.token,
2020
+ selector: argv.workspace
2021
+ });
1915
2022
  const themeId = argv.theme ?? process.env["SKYDIVE_THEME"] ?? void 0;
1916
2023
  if (themeId !== void 0 && !themes.some((t) => t.id === themeId)) {
1917
2024
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
1918
2025
  process.exit(1);
1919
2026
  }
1920
- await ensureAgentWorkspace({
2027
+ if (!workspaceId) await ensureAgentWorkspace({
1921
2028
  appUrl,
1922
2029
  sessionToken: auth.value.token,
1923
2030
  agentSelector: argv.agent ?? null
1924
2031
  });
1925
- const { runChat } = await import("./boot-DdYXpKMY.mjs");
2032
+ const { runChat } = await import("./boot-BPzsuEQF.mjs");
1926
2033
  await runChat({
1927
2034
  appUrl: auth.value.appUrl,
1928
2035
  sessionToken: auth.value.token,
1929
2036
  authKind: auth.value.kind,
2037
+ workspaceId,
1930
2038
  shareMachine: resolveShareMachine(argv),
1931
2039
  promptHistoryPath: getPromptHistoryPath(),
1932
2040
  theme: themeId,
@@ -2005,8 +2113,13 @@ async function runPrintMode({ argv, appUrl }) {
2005
2113
  printError(`${auth.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
2006
2114
  process.exit(1);
2007
2115
  }
2008
- const { runPrint, readStdin } = await import("./print-C8DHKrJu.mjs");
2009
- await ensureAgentWorkspace({
2116
+ const { runPrint, readStdin } = await import("./print-Pi0PQRj8.mjs");
2117
+ const workspaceId = await resolveWorkspaceFlag({
2118
+ appUrl,
2119
+ sessionToken: auth.value.token,
2120
+ selector: argv.workspace
2121
+ });
2122
+ if (!workspaceId) await ensureAgentWorkspace({
2010
2123
  appUrl,
2011
2124
  sessionToken: auth.value.token,
2012
2125
  agentSelector: argv.agent ?? null
@@ -2041,6 +2154,7 @@ async function runPrintMode({ argv, appUrl }) {
2041
2154
  const result = await runPrint({
2042
2155
  appUrl: auth.value.appUrl,
2043
2156
  sessionToken: auth.value.token,
2157
+ workspaceId,
2044
2158
  prompt,
2045
2159
  agentSelector: argv.agent ?? null,
2046
2160
  conversationId: argv.conversation ?? null,
@@ -2094,7 +2208,7 @@ const getCommand$1 = {
2094
2208
  printError(`${auth.error.message} Run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
2095
2209
  process.exit(1);
2096
2210
  }
2097
- const { messageGet } = await import("./print-C8DHKrJu.mjs");
2211
+ const { messageGet } = await import("./print-Pi0PQRj8.mjs");
2098
2212
  try {
2099
2213
  const result = await messageGet({
2100
2214
  appUrl: auth.value.appUrl,
@@ -2153,7 +2267,7 @@ const listCommand$2 = {
2153
2267
  }),
2154
2268
  handler: async (argv) => {
2155
2269
  const client = requireRestClient(argv);
2156
- const agent = resolveAgent(await client.listAgents({
2270
+ const agent = resolveAgent$1(await client.listAgents({
2157
2271
  scope: "org",
2158
2272
  onPage: null
2159
2273
  }), argv.agent ?? null);
@@ -2297,7 +2411,7 @@ const switchCommand = {
2297
2411
  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.");
2298
2412
  process.exit(1);
2299
2413
  }
2300
- const { runWorkspacePicker } = await import("./boot-DdYXpKMY.mjs");
2414
+ const { runWorkspacePicker } = await import("./boot-BPzsuEQF.mjs");
2301
2415
  await runWorkspacePicker(session);
2302
2416
  return;
2303
2417
  }
@@ -2349,7 +2463,7 @@ async function resolveGrantTarget(argv) {
2349
2463
  const [{ devices, agents }, identity] = await Promise.all([fetchPortalDevices(session), resolveMachineIdentity(null)]);
2350
2464
  return {
2351
2465
  session,
2352
- agent: resolveAgent(agents, argv.agent),
2466
+ agent: resolveAgent$1(agents, argv.agent),
2353
2467
  device: findThisDevice(devices, identity.machineName),
2354
2468
  identity
2355
2469
  };
@@ -2373,10 +2487,10 @@ const openCommand = {
2373
2487
  }),
2374
2488
  handler: async (argv) => {
2375
2489
  const session = requireSession(argv);
2376
- const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
2490
+ const agent = argv.agent ? resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent) : null;
2377
2491
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
2378
2492
  const { machineName } = await resolveMachineIdentity(null);
2379
- const { PortalDaemonClient } = await import("./daemon-client-JDBIWms7.mjs");
2493
+ const { PortalDaemonClient } = await import("./daemon-client-DXXLCfAg.mjs");
2380
2494
  let lastLine = "";
2381
2495
  let signalConnected;
2382
2496
  const connected = new Promise((resolve) => {
@@ -2619,7 +2733,7 @@ const forwardCommand = {
2619
2733
  }),
2620
2734
  handler: async (argv) => {
2621
2735
  const session = requireSession(argv);
2622
- const agent = resolveAgent((await fetchPortalDevices(session)).agents, argv.agent ?? null);
2736
+ const agent = resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent ?? null);
2623
2737
  const targetPort = argv.port;
2624
2738
  const localPort = argv["local-port"] ?? targetPort;
2625
2739
  const { startForward } = await import("./forward-18QoL5dO.mjs");
@@ -2687,7 +2801,7 @@ const sandboxCommand = {
2687
2801
  handler: async (argv) => {
2688
2802
  const session = requireSession(argv);
2689
2803
  const { createRestClient } = await import("./rest-DW05dMMc.mjs");
2690
- const { resolveAgent } = await import("./print-C8DHKrJu.mjs");
2804
+ const { resolveAgent } = await import("./print-Pi0PQRj8.mjs");
2691
2805
  const client = createRestClient({
2692
2806
  appUrl: session.appUrl,
2693
2807
  sessionToken: session.sessionToken
@@ -2845,7 +2959,7 @@ const fsEditCommand = {
2845
2959
  const session = requireSession(argv);
2846
2960
  const remotePath = argv.path;
2847
2961
  const { createRestClient } = await import("./rest-DW05dMMc.mjs");
2848
- const { resolveAgent } = await import("./print-C8DHKrJu.mjs");
2962
+ const { resolveAgent } = await import("./print-Pi0PQRj8.mjs");
2849
2963
  const client = createRestClient({
2850
2964
  appUrl: session.appUrl,
2851
2965
  sessionToken: session.sessionToken
@@ -4222,7 +4336,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
4222
4336
  process.exit(0);
4223
4337
  }
4224
4338
  if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
4225
- const { runPortalDaemon } = await import("./daemon-CzhmKp_H.mjs");
4339
+ const { runPortalDaemon } = await import("./daemon-BM8EJU8-.mjs");
4226
4340
  runPortalDaemon(process.argv);
4227
4341
  } else runCli();
4228
4342
  function runCli() {
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { A as setActiveWorkspace, C as themesForMode, D as getActiveWorkspaceId, T as machineOsFromPlatform, _ as theme, a as installCrashHandler, b as themeModeFromColorFgBg, c as MARK_CELLS, d as splashFitsWidth, f as DEFAULT_THEME_ID, g as noColorRequested, h as monoTheme, i as writeArtifact, k as listWorkspaces, l as WORDMARK, m as findTheme, n as profilingEnabled, o as buildCrashReport, p as applyTheme, r as record, s as writeCrashReport, v as themeForMode, x as themeVersion, y as themeMode } from "./profiler-zp3MKUcM.mjs";
3
- import { D as getReviewStateDir, H as saveTheme, N as recordDefaultAgent, O as getSavedTheme, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL, z as resolveWebUrl } from "./print-OvYyj6Uk.mjs";
2
+ import { A as takenAliasNames, C as themesForMode, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, L as listWorkspaces, N as detectShell, O as installAgentAlias, T as machineOsFromPlatform, _ as theme, a as installCrashHandler, b as themeModeFromColorFgBg, c as MARK_CELLS, d as splashFitsWidth, f as DEFAULT_THEME_ID, g as noColorRequested, h as monoTheme, i as writeArtifact, k as slugifyAliasName, l as WORDMARK, m as findTheme, n as profilingEnabled, o as buildCrashReport, p as applyTheme, r as record, s as writeCrashReport, v as themeForMode, x as themeVersion, y as themeMode, z as setActiveWorkspace } from "./profiler-DxmQ_Xqv.mjs";
3
+ import { D as getReviewStateDir, H as saveTheme, N as recordDefaultAgent, O as getSavedTheme, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL, z as resolveWebUrl } from "./print-DQtIEVHh.mjs";
4
4
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
5
5
  import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-Dx6b-Nq_.mjs";
6
6
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
7
7
  import { t as PortalClient } from "./client-DbqRBquD.mjs";
8
- import "./daemon-Dnv9p14X.mjs";
8
+ import "./daemon-BBeAB5SM.mjs";
9
9
  import "./api-DG5W6iwx.mjs";
10
10
  import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
11
- import { t as PortalDaemonClient } from "./daemon-client-Bb-6i20B.mjs";
11
+ import { t as PortalDaemonClient } from "./daemon-client-CJrRjvat.mjs";
12
12
  import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
13
13
  import * as os$1 from "node:os";
14
14
  import { homedir, platform, release, tmpdir } from "node:os";
@@ -2141,7 +2141,54 @@ function AgentCreateScreen() {
2141
2141
  cancelled = true;
2142
2142
  };
2143
2143
  }, [rest]);
2144
+ const openChat = (agent) => {
2145
+ goTo({
2146
+ kind: "chat",
2147
+ agent,
2148
+ conversation: {
2149
+ kind: "new",
2150
+ agentId: agent.id,
2151
+ nonce: crypto.randomUUID()
2152
+ }
2153
+ });
2154
+ };
2155
+ const acceptAlias = (agent, alias) => {
2156
+ try {
2157
+ const shell = detectShell();
2158
+ if (!shell) {
2159
+ openChat(agent);
2160
+ return;
2161
+ }
2162
+ const result = installAgentAlias({
2163
+ alias,
2164
+ agentId: agent.id,
2165
+ shell
2166
+ });
2167
+ setState({
2168
+ kind: "aliased",
2169
+ agent,
2170
+ message: `Added \`${result.alias}\`. ${aliasActivationHint(result)}`
2171
+ });
2172
+ } catch {
2173
+ openChat(agent);
2174
+ }
2175
+ };
2144
2176
  useKeyboard((key) => {
2177
+ if (state.kind === "offer-alias") {
2178
+ if (key.name === "y") {
2179
+ acceptAlias(state.agent, state.alias);
2180
+ return;
2181
+ }
2182
+ if (key.name === "n" || key.name === "escape" || key.name === "return") {
2183
+ openChat(state.agent);
2184
+ return;
2185
+ }
2186
+ return;
2187
+ }
2188
+ if (state.kind === "aliased") {
2189
+ openChat(state.agent);
2190
+ return;
2191
+ }
2145
2192
  if (key.name === "escape" && state.kind !== "creating") goTo({ kind: "agent-picker" });
2146
2193
  });
2147
2194
  const createAgent = async () => {
@@ -2157,15 +2204,13 @@ function AgentCreateScreen() {
2157
2204
  setState({ kind: "creating" });
2158
2205
  try {
2159
2206
  const agent = await rest.createAgent({ name: result.name });
2160
- goTo({
2161
- kind: "chat",
2207
+ const shell = detectShell();
2208
+ if (shell) setState({
2209
+ kind: "offer-alias",
2162
2210
  agent,
2163
- conversation: {
2164
- kind: "new",
2165
- agentId: agent.id,
2166
- nonce: crypto.randomUUID()
2167
- }
2211
+ alias: dedupeAliasName(slugifyAliasName(agent.name), takenAliasNames(shell))
2168
2212
  });
2213
+ else openChat(agent);
2169
2214
  } catch (err) {
2170
2215
  if (err instanceof HttpError && err.status === 401) {
2171
2216
  fail("Session expired. Run `skydive auth login --web`, then retry.");
@@ -2205,6 +2250,11 @@ function AgentCreateScreen() {
2205
2250
  });
2206
2251
  }
2207
2252
  };
2253
+ if (state.kind === "offer-alias") return /* @__PURE__ */ jsx(AliasOffer, {
2254
+ agentName: state.agent.name,
2255
+ alias: state.alias
2256
+ });
2257
+ if (state.kind === "aliased") return /* @__PURE__ */ jsx(AliasResult, { message: state.message });
2208
2258
  return /* @__PURE__ */ jsxs("box", {
2209
2259
  style: {
2210
2260
  flexDirection: "column",
@@ -2257,6 +2307,58 @@ function AgentCreateScreen() {
2257
2307
  ]
2258
2308
  });
2259
2309
  }
2310
+ /**
2311
+ * The post-create prompt offering to alias the agent's name to a shell
2312
+ * command. Presentational so the frame gallery can render it directly.
2313
+ */
2314
+ function AliasOffer({ agentName, alias }) {
2315
+ return /* @__PURE__ */ jsxs("box", {
2316
+ style: {
2317
+ flexDirection: "column",
2318
+ flexGrow: 1,
2319
+ gap: 1
2320
+ },
2321
+ children: [
2322
+ /* @__PURE__ */ jsxs("text", {
2323
+ fg: theme.fg,
2324
+ children: [
2325
+ "Created ",
2326
+ agentName,
2327
+ "."
2328
+ ]
2329
+ }),
2330
+ /* @__PURE__ */ jsxs("text", {
2331
+ fg: theme.muted,
2332
+ children: [
2333
+ "Add a shell alias so you can run `",
2334
+ alias,
2335
+ "` to open this agent?"
2336
+ ]
2337
+ }),
2338
+ /* @__PURE__ */ jsx("text", {
2339
+ fg: theme.dim,
2340
+ children: "y add alias · n skip"
2341
+ })
2342
+ ]
2343
+ });
2344
+ }
2345
+ /** The confirmation shown after an alias is written. */
2346
+ function AliasResult({ message }) {
2347
+ return /* @__PURE__ */ jsxs("box", {
2348
+ style: {
2349
+ flexDirection: "column",
2350
+ flexGrow: 1,
2351
+ gap: 1
2352
+ },
2353
+ children: [/* @__PURE__ */ jsx("text", {
2354
+ fg: theme.fg,
2355
+ children: message
2356
+ }), /* @__PURE__ */ jsx("text", {
2357
+ fg: theme.dim,
2358
+ children: "press any key to continue"
2359
+ })]
2360
+ });
2361
+ }
2260
2362
 
2261
2363
  //#endregion
2262
2364
  //#region src/chat/tui/screens/first-run.tsx
@@ -5009,6 +5111,14 @@ function shouldPollComputer(opts) {
5009
5111
  * The command run in the sandbox to collect the snapshot. `sh`-portable, reads
5010
5112
  * only `/proc` and `df`. Each metric is one `key<TAB>value` line so the parser
5011
5113
  * never has to positionally align anything.
5114
+ *
5115
+ * The `port` lines enumerate LISTEN sockets the sandbox user's own processes
5116
+ * hold: one awk pass maps socket inodes from /proc/net/tcp{,6} to pids via
5117
+ * /proc/<pid>/fd, then a shell loop attaches each pid's cmdline. Platform
5118
+ * daemons run as root, so their /proc/<pid>/fd is unreadable here and they
5119
+ * never emit a line — the "user's servers only" filter falls out of
5120
+ * permissions rather than a curated list. Hex address:port is left for the
5121
+ * parser to decode (strtonum is gawk-only).
5012
5122
  */
5013
5123
  const SANDBOX_STATS_COMMAND = [
5014
5124
  `printf '${SANDBOX_STATS_SENTINEL}\\n'`,
@@ -5018,9 +5128,97 @@ const SANDBOX_STATS_COMMAND = [
5018
5128
  `awk '/^MemTotal:/{t=$2}/^MemAvailable:/{a=$2}END{printf "mem_total_kb\\t%d\\nmem_avail_kb\\t%d\\n",t,a}' /proc/meminfo`,
5019
5129
  `awk '/^SwapTotal:/{t=$2}/^SwapFree:/{f=$2}END{printf "swap_total_kb\\t%d\\nswap_free_kb\\t%d\\n",t,f}' /proc/meminfo`,
5020
5130
  `df -Pk / | awk 'NR==2{printf "disk_total_kb\\t%d\\ndisk_used_kb\\t%d\\n",$2,$3}'`,
5021
- `printf 'procs\\t%s\\n' "$(ls -1 /proc 2>/dev/null | grep -c '^[0-9]\\+$')"`
5131
+ `printf 'procs\\t%s\\n' "$(ls -1 /proc 2>/dev/null | grep -c '^[0-9]\\+$')"`,
5132
+ `ls -l /proc/[0-9]*/fd 2>/dev/null | awk 'BEGIN { while ((getline l < "/proc/net/tcp") > 0) { split(l,f," "); if (f[4]=="0A") want[f[10]]=f[2] } close("/proc/net/tcp"); while ((getline l < "/proc/net/tcp6") > 0) { split(l,f," "); if (f[4]=="0A") want[f[10]]=f[2] } close("/proc/net/tcp6") } $0 ~ "^/proc/[0-9]+/fd:$" { pid=substr($0,7,length($0)-10) } match($0, /socket:\\[[0-9]+\\]/) { ino=substr($0,RSTART+8,RLENGTH-9); if (ino in want && !(ino in done)) { done[ino]=1; print want[ino] "\\t" pid } }' | while read addr pid; do cl=$(head -c 200 "/proc/$pid/cmdline" 2>/dev/null | tr "\\0" " "); printf 'port\\t%s\\t%s\\t%s\\n' "$addr" "$pid" "$cl"; done`
5022
5133
  ].join(" ; ");
5023
5134
  const KB = 1024;
5135
+ const HIDDEN_PORTS = new Set([
5136
+ 38994,
5137
+ 49983,
5138
+ 38996
5139
+ ]);
5140
+ const V6_LOOPBACK_HEX = "00000000000000000000000001000000";
5141
+ /**
5142
+ * Whether a /proc/net/tcp{,6} local-address hex field is a loopback bind.
5143
+ * v4 words are little-endian, so 127.0.0.1 is `0100007F` — the first octet
5144
+ * (127) is the LAST byte pair. Covers v4-mapped v6 (`::ffff:127.x.y.z`) via
5145
+ * the same tail check.
5146
+ */
5147
+ function isLoopbackHex(addrHex) {
5148
+ const hex = addrHex.toUpperCase();
5149
+ if (hex.length === 8) return hex.slice(6) === "7F";
5150
+ if (hex.length === 32) {
5151
+ if (hex === V6_LOOPBACK_HEX) return true;
5152
+ return hex.startsWith("0000000000000000FFFF0000") && hex.slice(30) === "7F";
5153
+ }
5154
+ return false;
5155
+ }
5156
+ const RUNTIME_NAMES = new Set([
5157
+ "node",
5158
+ "bun",
5159
+ "deno",
5160
+ "python",
5161
+ "python3",
5162
+ "sh",
5163
+ "bash"
5164
+ ]);
5165
+ const FLAGS_WITH_VALUE = new Set([
5166
+ "--require",
5167
+ "-r",
5168
+ "--import",
5169
+ "--loader",
5170
+ "--experimental-loader"
5171
+ ]);
5172
+ /**
5173
+ * A short display name from a raw cmdline: basename of argv[0], plus — when
5174
+ * argv[0] is a bare runtime — the basename of the first non-flag argument with
5175
+ * its extension dropped (`node …/vite/bin/vite.js` → `node vite`).
5176
+ */
5177
+ function processDisplayName(cmdline) {
5178
+ const argv = cmdline.trim().split(/\s+/).filter(Boolean);
5179
+ const first = argv[0];
5180
+ if (first === void 0) return null;
5181
+ const base = (p) => p.split("/").pop() ?? p;
5182
+ const name = base(first);
5183
+ if (!RUNTIME_NAMES.has(name)) return name;
5184
+ let script;
5185
+ for (let i = 1; i < argv.length; i += 1) {
5186
+ const arg = argv[i];
5187
+ if (arg === void 0) break;
5188
+ if (FLAGS_WITH_VALUE.has(arg)) {
5189
+ i += 1;
5190
+ continue;
5191
+ }
5192
+ if (arg.startsWith("-")) continue;
5193
+ script = arg;
5194
+ break;
5195
+ }
5196
+ if (!script) return name;
5197
+ return `${name} ${base(script).replace(/\.(?:mjs|cjs|js|ts|tsx|py)$/, "")}`;
5198
+ }
5199
+ /**
5200
+ * Fold the collector's `port` lines (one per listen socket) into one entry
5201
+ * per port: v4/v6 binds of the same server dedupe, `loopbackOnly` only when
5202
+ * every bind is loopback, and the hidden platform ports drop out.
5203
+ */
5204
+ function parsePortLines(lines) {
5205
+ const byPort = /* @__PURE__ */ new Map();
5206
+ for (const line of lines) {
5207
+ const port = Number.parseInt(line.portHex, 16);
5208
+ if (!Number.isFinite(port) || port <= 0 || HIDDEN_PORTS.has(port)) continue;
5209
+ const entry = byPort.get(port);
5210
+ if (entry) entry.loopback.push(isLoopbackHex(line.addrHex));
5211
+ else byPort.set(port, {
5212
+ loopback: [isLoopbackHex(line.addrHex)],
5213
+ cmdline: line.cmdline
5214
+ });
5215
+ }
5216
+ return [...byPort.entries()].map(([port, entry]) => ({
5217
+ port,
5218
+ loopbackOnly: entry.loopback.every(Boolean),
5219
+ process: entry.cmdline ? processDisplayName(entry.cmdline) : null
5220
+ })).sort((a, b) => a.port - b.port);
5221
+ }
5024
5222
  /**
5025
5223
  * Parse the collector's output into `SandboxStats`, or return null when the
5026
5224
  * block is absent or malformed (so the caller can show the raw output as an
@@ -5032,10 +5230,22 @@ function parseSandboxStats(raw) {
5032
5230
  const start = raw.indexOf(SANDBOX_STATS_SENTINEL);
5033
5231
  if (start === -1) return null;
5034
5232
  const fields = /* @__PURE__ */ new Map();
5233
+ const portLines = [];
5035
5234
  for (const line of raw.slice(start).split("\n")) {
5036
5235
  const tab = line.indexOf(" ");
5037
5236
  if (tab === -1) continue;
5038
- fields.set(line.slice(0, tab).trim(), line.slice(tab + 1).trim());
5237
+ const key = line.slice(0, tab).trim();
5238
+ if (key === "port") {
5239
+ const parts = line.split(" ");
5240
+ const [addrHex = "", portHex = ""] = (parts[1] ?? "").split(":");
5241
+ if (addrHex && portHex) portLines.push({
5242
+ addrHex,
5243
+ portHex,
5244
+ cmdline: parts.slice(3).join(" ").trim()
5245
+ });
5246
+ continue;
5247
+ }
5248
+ fields.set(key, line.slice(tab + 1).trim());
5039
5249
  }
5040
5250
  const num = (key) => {
5041
5251
  const v = fields.get(key);
@@ -5067,7 +5277,8 @@ function parseSandboxStats(raw) {
5067
5277
  swapFreeBytes: (num("swap_free_kb") ?? 0) * KB,
5068
5278
  diskTotalBytes: diskTotal * KB,
5069
5279
  diskUsedBytes: diskUsed * KB,
5070
- procs: Math.round(procs)
5280
+ procs: Math.round(procs),
5281
+ ports: parsePortLines(portLines)
5071
5282
  };
5072
5283
  }
5073
5284
  function severityFor(fraction) {
@@ -5160,6 +5371,22 @@ function renderBar(fraction, width) {
5160
5371
  const filled = "█".repeat(Math.min(full, cells)) + partial;
5161
5372
  return (filled + "░".repeat(Math.max(0, cells - filled.length))).slice(0, cells);
5162
5373
  }
5374
+ /** How many port rows the card draws before folding the rest into `+N more`. */
5375
+ const MAX_PORT_ROWS = 8;
5376
+ const WELL_KNOWN_PORTS = new Map([[3e3, "agent harness"], [3001, "agent webserver"]]);
5377
+ /**
5378
+ * Reduce the snapshot's ports to drawable rows. The well-known note wins over
5379
+ * `local only` — a loopback-bound 3001 being unreachable is a distinct problem
5380
+ * the gauges' error path already surfaces, and one annotation per row keeps
5381
+ * the table scannable.
5382
+ */
5383
+ function toPortRows(ports) {
5384
+ return ports.map((p) => ({
5385
+ port: p.port,
5386
+ process: p.process ?? "?",
5387
+ note: WELL_KNOWN_PORTS.get(p.port) ?? (p.loopbackOnly ? "local only" : null)
5388
+ }));
5389
+ }
5163
5390
 
5164
5391
  //#endregion
5165
5392
  //#region src/chat/tui/chunks/computer.tsx
@@ -5224,26 +5451,34 @@ function ComputerItem({ item }) {
5224
5451
  children: ["couldn't read sandbox stats", error ? `: ${error}` : ""]
5225
5452
  }) });
5226
5453
  const gauges = toGauges(stats);
5227
- const labelWidth = Math.max(...gauges.map((g) => g.label.length));
5454
+ const portRows = toPortRows(stats.ports);
5455
+ const labelWidth = Math.max(5, ...gauges.map((g) => g.label.length));
5228
5456
  const barWidth = 24;
5229
- return /* @__PURE__ */ jsxs(Shell, { children: [gauges.map((g) => /* @__PURE__ */ jsx(GaugeRow, {
5230
- gauge: g,
5231
- labelWidth,
5232
- barWidth
5233
- }, g.label)), /* @__PURE__ */ jsxs("text", {
5234
- fg: theme.dim,
5235
- children: [
5236
- `${"uptime".padEnd(labelWidth)} `,
5237
- /* @__PURE__ */ jsx("span", {
5238
- fg: theme.muted,
5239
- children: formatUptime(stats.uptimeSeconds)
5240
- }),
5241
- /* @__PURE__ */ jsx("span", {
5242
- fg: theme.dim,
5243
- children: ` · ${stats.procs} processes`
5244
- })
5245
- ]
5246
- })] });
5457
+ return /* @__PURE__ */ jsxs(Shell, { children: [
5458
+ gauges.map((g) => /* @__PURE__ */ jsx(GaugeRow, {
5459
+ gauge: g,
5460
+ labelWidth,
5461
+ barWidth
5462
+ }, g.label)),
5463
+ /* @__PURE__ */ jsx(PortsSection, {
5464
+ rows: portRows,
5465
+ labelWidth
5466
+ }),
5467
+ /* @__PURE__ */ jsxs("text", {
5468
+ fg: theme.dim,
5469
+ children: [
5470
+ `${"uptime".padEnd(labelWidth)} `,
5471
+ /* @__PURE__ */ jsx("span", {
5472
+ fg: theme.muted,
5473
+ children: formatUptime(stats.uptimeSeconds)
5474
+ }),
5475
+ /* @__PURE__ */ jsx("span", {
5476
+ fg: theme.dim,
5477
+ children: ` · ${stats.procs} processes`
5478
+ })
5479
+ ]
5480
+ })
5481
+ ] });
5247
5482
  }
5248
5483
  /** The card frame: a bold `computer` title over the graph rows. */
5249
5484
  function Shell({ children }) {
@@ -5291,6 +5526,42 @@ function GaugeRow({ gauge, labelWidth, barWidth }) {
5291
5526
  })
5292
5527
  ] });
5293
5528
  }
5529
+ /**
5530
+ * The listening-ports table: one row per port the sandbox user's processes
5531
+ * hold, first row carrying the `ports` gutter label to line up with the
5532
+ * gauges. Folds past MAX_PORT_ROWS into a `+N more` line so a busy box can't
5533
+ * flood the transcript.
5534
+ */
5535
+ function PortsSection({ rows, labelWidth }) {
5536
+ if (rows.length === 0) return null;
5537
+ const shown = rows.slice(0, MAX_PORT_ROWS);
5538
+ const folded = rows.length - shown.length;
5539
+ const gutter = " ".repeat(labelWidth);
5540
+ return /* @__PURE__ */ jsxs(Fragment, { children: [shown.map((row, i) => /* @__PURE__ */ jsxs("text", { children: [
5541
+ /* @__PURE__ */ jsx("span", {
5542
+ fg: theme.muted,
5543
+ children: `${i === 0 ? "ports".padEnd(labelWidth) : gutter} `
5544
+ }),
5545
+ /* @__PURE__ */ jsx("span", {
5546
+ fg: theme.fg,
5547
+ children: String(row.port).padStart(5)
5548
+ }),
5549
+ /* @__PURE__ */ jsx("span", {
5550
+ fg: theme.muted,
5551
+ children: ` ${row.process}`
5552
+ }),
5553
+ row.note ? /* @__PURE__ */ jsx("span", {
5554
+ fg: theme.dim,
5555
+ children: ` · ${row.note}`
5556
+ }) : null
5557
+ ] }, row.port)), folded > 0 ? /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
5558
+ fg: theme.muted,
5559
+ children: `${gutter} `
5560
+ }), /* @__PURE__ */ jsx("span", {
5561
+ fg: theme.dim,
5562
+ children: `+${folded} more`
5563
+ })] }) : null] });
5564
+ }
5294
5565
 
5295
5566
  //#endregion
5296
5567
  //#region src/chat/tui/chunks/index.tsx
@@ -13337,7 +13608,7 @@ function ToastView() {
13337
13608
 
13338
13609
  //#endregion
13339
13610
  //#region src/chat/tui/app.tsx
13340
- function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding }) {
13611
+ function App({ appUrl, sessionToken, authKind, workspaceId, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding }) {
13341
13612
  const renderer = useRenderer();
13342
13613
  const screen = useStore((s) => s.screen);
13343
13614
  useEffect(() => {
@@ -13410,7 +13681,8 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
13410
13681
  useEffect(() => {
13411
13682
  const rest = createRestClient({
13412
13683
  appUrl,
13413
- sessionToken
13684
+ sessionToken,
13685
+ workspaceId
13414
13686
  });
13415
13687
  setClients({
13416
13688
  appUrl,
@@ -13446,6 +13718,7 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
13446
13718
  }, [
13447
13719
  appUrl,
13448
13720
  sessionToken,
13721
+ workspaceId,
13449
13722
  promptHistoryPath,
13450
13723
  agentSelector,
13451
13724
  conversationId,
@@ -14031,7 +14304,7 @@ function onRenderCommit(id, phase, actualDuration, baseDuration, startTime, comm
14031
14304
  * module — and the heavy OpenTUI dependency graph it pulls in — is
14032
14305
  * dynamically imported and never loaded on the Node management path.
14033
14306
  */
14034
- async function runChat({ appUrl, sessionToken, authKind = "session", shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding, seedPrompt }) {
14307
+ async function runChat({ appUrl, sessionToken, authKind = "session", workspaceId, shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding, seedPrompt }) {
14035
14308
  await registerGrammars();
14036
14309
  const watchdog = startMemoryWatchdog();
14037
14310
  installCrashHandler({ memoryProvider: () => watchdog.snapshot() });
@@ -14068,6 +14341,7 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
14068
14341
  appUrl,
14069
14342
  sessionToken,
14070
14343
  authKind,
14344
+ workspaceId,
14071
14345
  shareMachine,
14072
14346
  promptHistoryPath,
14073
14347
  notifications,
@@ -20,7 +20,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
20
20
  * the commit time of the built tree is one monotonic clock they share.
21
21
  */
22
22
  function portalDaemonBuild() {
23
- return "1786692209";
23
+ return "1786727732";
24
24
  }
25
25
  /**
26
26
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "./client-DbqRBquD.mjs";
3
- import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-Dnv9p14X.mjs";
3
+ import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-BBeAB5SM.mjs";
4
4
  import "./api-DG5W6iwx.mjs";
5
5
 
6
6
  export { runPortalDaemon };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-Dnv9p14X.mjs";
2
+ import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-BBeAB5SM.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import "./client-DbqRBquD.mjs";
3
+ import "./daemon-BBeAB5SM.mjs";
4
+ import "./api-DG5W6iwx.mjs";
5
+ import { t as PortalDaemonClient } from "./daemon-client-CJrRjvat.mjs";
6
+
7
+ export { PortalDaemonClient };
@@ -638,10 +638,11 @@ function formatConnectCard(card) {
638
638
  * disambiguate: an `--agent` selector must match exactly one agent, and
639
639
  * when it's omitted we only auto-pick if the account has exactly one.
640
640
  */
641
- async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversationId, json, machineShare, grantTargetAgent }) {
641
+ async function runPrint({ appUrl, sessionToken, workspaceId, prompt, agentSelector, conversationId, json, machineShare, grantTargetAgent }) {
642
642
  const client = createRestClient({
643
643
  appUrl,
644
- sessionToken
644
+ sessionToken,
645
+ workspaceId
645
646
  });
646
647
  const agent = resolveAgent(await client.listAgents({
647
648
  scope: "org",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-OvYyj6Uk.mjs";
2
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-DQtIEVHh.mjs";
3
3
  import "./rest-Dx6b-Nq_.mjs";
4
4
  import "./billing-blocked-2wju4gC_.mjs";
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { S as getConfigPath } from "./print-OvYyj6Uk.mjs";
2
+ import { S as getConfigPath } from "./print-DQtIEVHh.mjs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { err, ok } from "neverthrow";
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
 
9
9
  //#region package.json
10
10
  var name = "skydive-cli";
11
- var version$1 = "0.5.0-beta.22";
11
+ var version$1 = "0.5.0-beta.24";
12
12
 
13
13
  //#endregion
14
14
  //#region src/auth/organization.ts
@@ -32,6 +32,32 @@ async function listWorkspaces({ appUrl, sessionToken }) {
32
32
  }
33
33
  }
34
34
  /**
35
+ * Match a workspace selector against a roster the way `workspace switch` does:
36
+ * exact id first, then case-insensitive slug, then case-insensitive name.
37
+ * Returns null when nothing matches. Pure — the caller owns fetching and error
38
+ * messaging.
39
+ */
40
+ function matchWorkspace(workspaces, selector) {
41
+ const needle = selector.trim().toLowerCase();
42
+ return workspaces.find((w) => w.id === selector) ?? workspaces.find((w) => w.slug.toLowerCase() === needle) ?? workspaces.find((w) => w.name.toLowerCase() === needle) ?? null;
43
+ }
44
+ /**
45
+ * Resolve a workspace selector (slug, name, or id) to its id by listing the
46
+ * account's workspaces and matching. Used by any command that takes a
47
+ * `--workspace` flag to scope requests without persisting an active-workspace
48
+ * switch.
49
+ */
50
+ async function resolveWorkspaceId({ appUrl, sessionToken, selector }) {
51
+ const workspaces = await listWorkspaces({
52
+ appUrl,
53
+ sessionToken
54
+ });
55
+ if (workspaces.isErr()) return err(workspaces.error);
56
+ const match = matchWorkspace(workspaces.value, selector);
57
+ if (!match) return err({ message: `No workspace matches "${selector}". Run \`skydive workspace list\` to see available workspaces.` });
58
+ return ok(match);
59
+ }
60
+ /**
35
61
  * Resolve who the current chat session belongs to: the signed-in user's
36
62
  * email/name and the active workspace. Used by `auth status` so a user running
37
63
  * multiple accounts can answer "am I logged into the right one?" without
@@ -125,6 +151,199 @@ async function ensureActiveOrganization({ appUrl, sessionToken }) {
125
151
  });
126
152
  }
127
153
 
154
+ //#endregion
155
+ //#region src/shell/shell-env.ts
156
+ /**
157
+ * Shared, dependency-light shell primitives used by anything that edits the
158
+ * user's shell startup files — `completion install` and `agents alias`. Kept
159
+ * free of yargs and the CLI's output layer so the TUI can import it without
160
+ * dragging command machinery into its bundle or creating a TUI -> commands
161
+ * import cycle.
162
+ */
163
+ const SUPPORTED_SHELLS = [
164
+ "bash",
165
+ "zsh",
166
+ "fish"
167
+ ];
168
+ const defaultInstallEnv = () => ({
169
+ home: os.homedir(),
170
+ platform: process.platform,
171
+ env: process.env,
172
+ exists: fs.existsSync
173
+ });
174
+ /**
175
+ * The shell to act on when the user didn't name one. `$SHELL` is the only
176
+ * signal available: an install runs as a child process, so the invoking
177
+ * shell's own variables (`$ZSH_VERSION`, `$FISH_VERSION`) aren't visible here.
178
+ */
179
+ function detectShell(env = process.env) {
180
+ const shell = env["SHELL"];
181
+ if (!shell) return null;
182
+ const name = path.basename(shell);
183
+ return SUPPORTED_SHELLS.find((candidate) => name === candidate) ?? null;
184
+ }
185
+
186
+ //#endregion
187
+ //#region src/commands/agent-alias.ts
188
+ /**
189
+ * Shell aliases that point a friendly command name at a specific agent's chat.
190
+ *
191
+ * When someone creates an agent named "Ripple", they almost always want to
192
+ * reach it again by typing `ripple`, not `skydive chat --agent <uuid>`. This
193
+ * module writes a managed alias block into the user's shell startup file so
194
+ * `ripple` opens that agent. It reuses the same shell detection and startup-file
195
+ * machinery as `completion install` (imported from ./completion), so the two
196
+ * features agree on which file to touch and how to detect the shell.
197
+ *
198
+ * The alias always targets the agent **id**, never its name: names are not
199
+ * unique and can be renamed, so an id keeps `ripple` pointed at the same agent
200
+ * even if a second "Ripple" is created later or this one is renamed.
201
+ */
202
+ /**
203
+ * Turn a display name into a shell-safe alias token: lowercase, non-alnum runs
204
+ * collapsed to a single hyphen, leading/trailing hyphens trimmed. "Ripple" ->
205
+ * "ripple", "My Agent" -> "my-agent", "Ripple 2.0" -> "ripple-2-0".
206
+ *
207
+ * A leading digit is prefixed with `a-` because bash rejects an alias name that
208
+ * starts with a digit (`alias 2fast=...` is a syntax error). An empty result
209
+ * (a name that was all punctuation) falls back to "agent" so callers always get
210
+ * a usable token to offer or dedupe from.
211
+ */
212
+ function slugifyAliasName(name) {
213
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
214
+ if (!slug) return "agent";
215
+ return /^[0-9]/.test(slug) ? `a-${slug}` : slug;
216
+ }
217
+ /**
218
+ * The alias token to actually install, given a preferred token and the set of
219
+ * names already taken (existing aliases, and — the caller's choice — commands
220
+ * on PATH). Prefers the clean token; on a collision appends the lowest integer
221
+ * that frees it: `ripple` -> `ripple2` -> `ripple3`. Matching is
222
+ * case-insensitive because shells treat `Ripple` and `ripple` as distinct
223
+ * aliases but a user typing one means the other.
224
+ */
225
+ function dedupeAliasName(preferred, taken) {
226
+ const takenLower = new Set(Array.from(taken, (t) => t.trim().toLowerCase()).filter(Boolean));
227
+ if (!takenLower.has(preferred.toLowerCase())) return preferred;
228
+ for (let n = 2;; n += 1) {
229
+ const candidate = `${preferred}${n}`;
230
+ if (!takenLower.has(candidate.toLowerCase())) return candidate;
231
+ }
232
+ }
233
+ /** The startup file an alias block belongs in, per shell. */
234
+ function aliasTarget(shell, deps = defaultInstallEnv()) {
235
+ const { home, platform, env, exists } = deps;
236
+ if (shell === "fish") {
237
+ const configHome = env["XDG_CONFIG_HOME"] || path.join(home, ".config");
238
+ return path.join(configHome, "fish", "config.fish");
239
+ }
240
+ if (shell === "zsh") {
241
+ const zdotdir = env["ZDOTDIR"] || home;
242
+ return path.join(zdotdir, ".zshrc");
243
+ }
244
+ const rc = path.join(home, ".bashrc");
245
+ const profile = path.join(home, ".bash_profile");
246
+ const preferred = platform === "darwin" ? profile : rc;
247
+ const alternate = platform === "darwin" ? rc : profile;
248
+ if (!exists(preferred) && exists(alternate)) return alternate;
249
+ return preferred;
250
+ }
251
+ const BLOCK_START = "###-begin-skydive-agent-aliases-###";
252
+ const BLOCK_END = "###-end-skydive-agent-aliases-###";
253
+ const aliasLine = (entry) => {
254
+ const ws = entry.workspaceId ? ` --workspace ${entry.workspaceId}` : "";
255
+ return `alias ${entry.alias}='skydive chat --agent ${entry.agentId}${ws} --new' # skydive-agent:${entry.agentId}`;
256
+ };
257
+ /**
258
+ * Parse the alias entries already inside the managed block. Used to merge a new
259
+ * alias with existing ones and to answer "is this token taken?". Tolerant of
260
+ * hand-edits: only well-formed lines carrying our trailing marker are read
261
+ * back; anything else in the block is preserved verbatim on rewrite.
262
+ */
263
+ function parseAliasBlock(existing) {
264
+ const start = existing.indexOf(BLOCK_START);
265
+ const end = existing.indexOf(BLOCK_END);
266
+ if (start === -1 || end <= start) return [];
267
+ const body = existing.slice(start + 35, end);
268
+ const entries = [];
269
+ for (const line of body.split("\n")) {
270
+ const match = line.match(/^alias\s+([^=]+)=.*# skydive-agent:([0-9a-f-]+)\s*$/i);
271
+ if (match?.[1] && match[2]) {
272
+ const wsMatch = line.match(/--workspace\s+(\S+)/);
273
+ entries.push({
274
+ alias: match[1].trim(),
275
+ agentId: match[2],
276
+ workspaceId: wsMatch?.[1]
277
+ });
278
+ }
279
+ }
280
+ return entries;
281
+ }
282
+ /**
283
+ * Merge `entry` into the managed alias block of `existing`, returning the new
284
+ * file contents. Idempotent: re-installing the same alias for the same agent is
285
+ * a no-op edit; a new agent id adds a line; an existing agent id has its alias
286
+ * token refreshed. The block is created if absent.
287
+ */
288
+ function spliceAliasBlock(existing, entry) {
289
+ const current = parseAliasBlock(existing).filter((e) => e.agentId !== entry.agentId);
290
+ current.push(entry);
291
+ current.sort((a, b) => a.alias.localeCompare(b.alias));
292
+ const block = [
293
+ BLOCK_START,
294
+ "# skydive agent aliases, managed by `skydive agents alias`. Edits between",
295
+ "# these markers are overwritten when an alias is added or refreshed.",
296
+ ...current.map(aliasLine),
297
+ BLOCK_END
298
+ ].join("\n");
299
+ const start = existing.indexOf(BLOCK_START);
300
+ const endMarker = BLOCK_END;
301
+ const end = existing.indexOf(endMarker);
302
+ if (start !== -1 && end > start) return `${existing.slice(0, start)}${block}\n${existing.slice(end + 33).replace(/^\n/, "")}`;
303
+ return `${existing}${existing === "" || existing.endsWith("\n\n") ? "" : "\n"}\n${block}\n`;
304
+ }
305
+ /**
306
+ * Write (or refresh) an agent alias in the user's shell startup file. Returns
307
+ * the resolved shell, file, and the alias token actually used. Does not dedupe
308
+ * on its own — pass a token already reconciled with `takenAliasNames` so the
309
+ * caller controls the suggestion UX.
310
+ */
311
+ function installAgentAlias(input, deps = defaultInstallEnv()) {
312
+ const target = aliasTarget(input.shell, deps);
313
+ const existing = deps.exists(target) ? fs.readFileSync(target, "utf8") : "";
314
+ const contents = spliceAliasBlock(existing, {
315
+ alias: input.alias,
316
+ agentId: input.agentId,
317
+ workspaceId: input.workspaceId
318
+ });
319
+ fs.mkdirSync(path.dirname(target), { recursive: true });
320
+ fs.writeFileSync(target, contents, "utf8");
321
+ return {
322
+ shell: input.shell,
323
+ path: target,
324
+ alias: input.alias,
325
+ agentId: input.agentId,
326
+ updated: existing.includes(BLOCK_START)
327
+ };
328
+ }
329
+ /**
330
+ * Alias tokens already defined in our managed block of the given shell's
331
+ * startup file. The dedupe source: a fresh install shouldn't collide with an
332
+ * alias we wrote for another agent. (We don't try to read the user's own
333
+ * hand-written aliases or PATH — a shell child process can't see the parent's
334
+ * live alias table, and probing PATH is noisy; the dedupe covers our own
335
+ * blocks, which is where real collisions come from in practice.)
336
+ */
337
+ function takenAliasNames(shell, deps = defaultInstallEnv()) {
338
+ const target = aliasTarget(shell, deps);
339
+ if (!deps.exists(target)) return [];
340
+ return parseAliasBlock(fs.readFileSync(target, "utf8")).map((e) => e.alias);
341
+ }
342
+ /** What the user must do before the alias works in the shell they're in. */
343
+ function aliasActivationHint(result) {
344
+ return result.shell === "fish" ? "Open a new fish shell to use it." : `Open a new shell, or run: source ${result.path}`;
345
+ }
346
+
128
347
  //#endregion
129
348
  //#region src/chat/import-seed.ts
130
349
  /** Map `process.platform` to the OS family used by the seed prompts. Every
@@ -1299,4 +1518,4 @@ function maybeStartProfiling(argv, cliVersion) {
1299
1518
  }
1300
1519
 
1301
1520
  //#endregion
1302
- export { setActiveWorkspace as A, themesForMode as C, getActiveWorkspaceId as D, ensureActiveOrganization as E, version$1 as M, getSessionIdentity as O, themes as S, machineOsFromPlatform as T, theme as _, installCrashHandler as a, themeModeFromColorFgBg as b, MARK_CELLS as c, splashFitsWidth as d, DEFAULT_THEME_ID as f, noColorRequested as g, monoTheme as h, writeArtifact as i, name as j, listWorkspaces as k, WORDMARK as l, findTheme as m, profilingEnabled as n, buildCrashReport as o, applyTheme as p, record as r, writeCrashReport as s, maybeStartProfiling as t, brandHelpArt as u, themeForMode as v, buildImportSeedPrompt as w, themeVersion as x, themeMode as y };
1521
+ export { takenAliasNames as A, name as B, themesForMode as C, dedupeAliasName as D, aliasActivationHint as E, getActiveWorkspaceId as F, getSessionIdentity as I, listWorkspaces as L, defaultInstallEnv as M, detectShell as N, installAgentAlias as O, ensureActiveOrganization as P, resolveWorkspaceId as R, themes as S, machineOsFromPlatform as T, version$1 as V, theme as _, installCrashHandler as a, themeModeFromColorFgBg as b, MARK_CELLS as c, splashFitsWidth as d, DEFAULT_THEME_ID as f, noColorRequested as g, monoTheme as h, writeArtifact as i, SUPPORTED_SHELLS as j, slugifyAliasName as k, WORDMARK as l, findTheme as m, profilingEnabled as n, buildCrashReport as o, applyTheme as p, record as r, writeCrashReport as s, maybeStartProfiling as t, brandHelpArt as u, themeForMode as v, buildImportSeedPrompt as w, themeVersion as x, themeMode as y, setActiveWorkspace as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.5.0-beta.22",
3
+ "version": "0.5.0-beta.24",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-DbqRBquD.mjs";
3
- import "./daemon-Dnv9p14X.mjs";
4
- import "./api-DG5W6iwx.mjs";
5
- import { t as PortalDaemonClient } from "./daemon-client-Bb-6i20B.mjs";
6
-
7
- export { PortalDaemonClient };