vana-cli 0.15.0 → 0.23.3

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.
Files changed (100) hide show
  1. package/README.md +73 -15
  2. package/dist/cli/app/ask.d.ts +38 -0
  3. package/dist/cli/app/ask.d.ts.map +1 -0
  4. package/dist/cli/app/ask.js +110 -0
  5. package/dist/cli/app/ask.js.map +1 -0
  6. package/dist/cli/app/derivatives.d.ts +41 -0
  7. package/dist/cli/app/derivatives.d.ts.map +1 -0
  8. package/dist/cli/app/derivatives.js +227 -0
  9. package/dist/cli/app/derivatives.js.map +1 -0
  10. package/dist/cli/app/escrow.d.ts +44 -0
  11. package/dist/cli/app/escrow.d.ts.map +1 -0
  12. package/dist/cli/app/escrow.js +275 -0
  13. package/dist/cli/app/escrow.js.map +1 -0
  14. package/dist/cli/app/index.d.ts +11 -0
  15. package/dist/cli/app/index.d.ts.map +1 -0
  16. package/dist/cli/app/index.js +151 -0
  17. package/dist/cli/app/index.js.map +1 -0
  18. package/dist/cli/app/onchain.d.ts +14 -0
  19. package/dist/cli/app/onchain.d.ts.map +1 -0
  20. package/dist/cli/app/onchain.js +84 -0
  21. package/dist/cli/app/onchain.js.map +1 -0
  22. package/dist/cli/app/outcome.d.ts +45 -0
  23. package/dist/cli/app/outcome.d.ts.map +1 -0
  24. package/dist/cli/app/outcome.js +55 -0
  25. package/dist/cli/app/outcome.js.map +1 -0
  26. package/dist/cli/app/read.d.ts +43 -0
  27. package/dist/cli/app/read.d.ts.map +1 -0
  28. package/dist/cli/app/read.js +429 -0
  29. package/dist/cli/app/read.js.map +1 -0
  30. package/dist/cli/app/register.d.ts +34 -0
  31. package/dist/cli/app/register.d.ts.map +1 -0
  32. package/dist/cli/app/register.js +113 -0
  33. package/dist/cli/app/register.js.map +1 -0
  34. package/dist/cli/app/request.d.ts +50 -0
  35. package/dist/cli/app/request.d.ts.map +1 -0
  36. package/dist/cli/app/request.js +252 -0
  37. package/dist/cli/app/request.js.map +1 -0
  38. package/dist/cli/app/requests.d.ts +21 -0
  39. package/dist/cli/app/requests.d.ts.map +1 -0
  40. package/dist/cli/app/requests.js +163 -0
  41. package/dist/cli/app/requests.js.map +1 -0
  42. package/dist/cli/app/whoami.d.ts +13 -0
  43. package/dist/cli/app/whoami.d.ts.map +1 -0
  44. package/dist/cli/app/whoami.js +71 -0
  45. package/dist/cli/app/whoami.js.map +1 -0
  46. package/dist/cli/auth.d.ts +18 -0
  47. package/dist/cli/auth.d.ts.map +1 -1
  48. package/dist/cli/auth.js +109 -7
  49. package/dist/cli/auth.js.map +1 -1
  50. package/dist/cli/index.d.ts +2 -2
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +307 -49
  53. package/dist/cli/index.js.map +1 -1
  54. package/dist/cli/mcp-server.d.ts.map +1 -1
  55. package/dist/cli/mcp-server.js +32 -3
  56. package/dist/cli/mcp-server.js.map +1 -1
  57. package/dist/core/app-key.d.ts +50 -0
  58. package/dist/core/app-key.d.ts.map +1 -0
  59. package/dist/core/app-key.js +171 -0
  60. package/dist/core/app-key.js.map +1 -0
  61. package/dist/core/assets.d.ts +33 -0
  62. package/dist/core/assets.d.ts.map +1 -0
  63. package/dist/core/assets.js +87 -0
  64. package/dist/core/assets.js.map +1 -0
  65. package/dist/core/cli-types.d.ts +30 -0
  66. package/dist/core/cli-types.d.ts.map +1 -1
  67. package/dist/core/cli-types.js +16 -0
  68. package/dist/core/cli-types.js.map +1 -1
  69. package/dist/core/exit-codes.d.ts +48 -0
  70. package/dist/core/exit-codes.d.ts.map +1 -0
  71. package/dist/core/exit-codes.js +96 -0
  72. package/dist/core/exit-codes.js.map +1 -0
  73. package/dist/core/network.d.ts +52 -0
  74. package/dist/core/network.d.ts.map +1 -0
  75. package/dist/core/network.js +83 -0
  76. package/dist/core/network.js.map +1 -0
  77. package/dist/core/paths.d.ts +4 -2
  78. package/dist/core/paths.d.ts.map +1 -1
  79. package/dist/core/paths.js +8 -6
  80. package/dist/core/paths.js.map +1 -1
  81. package/dist/core/receipts.d.ts +37 -0
  82. package/dist/core/receipts.d.ts.map +1 -0
  83. package/dist/core/receipts.js +72 -0
  84. package/dist/core/receipts.js.map +1 -0
  85. package/dist/core/requests-store.d.ts +54 -0
  86. package/dist/core/requests-store.d.ts.map +1 -0
  87. package/dist/core/requests-store.js +83 -0
  88. package/dist/core/requests-store.js.map +1 -0
  89. package/dist/personal-server/registered.d.ts +25 -0
  90. package/dist/personal-server/registered.d.ts.map +1 -0
  91. package/dist/personal-server/registered.js +49 -0
  92. package/dist/personal-server/registered.js.map +1 -0
  93. package/dist/server/request-signer.d.ts.map +1 -1
  94. package/dist/server/request-signer.js +7 -24
  95. package/dist/server/request-signer.js.map +1 -1
  96. package/dist/server/session-relay.d.ts +6 -5
  97. package/dist/server/session-relay.d.ts.map +1 -1
  98. package/dist/server/session-relay.js +48 -51
  99. package/dist/server/session-relay.js.map +1 -1
  100. package/package.json +2 -1
package/dist/cli/index.js CHANGED
@@ -2,11 +2,12 @@ import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { createRequire } from "node:module";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { spawn, execSync } from "node:child_process";
6
7
  import os from "node:os";
7
8
  import { confirm, input, password } from "@inquirer/prompts";
8
9
  import { searchSelect } from "./search-select.js";
9
- import { Command, CommanderError } from "commander";
10
+ import { Command, CommanderError, Option } from "commander";
10
11
  // Vana-branded theme for inquirer prompts — matches brand palette
11
12
  const VANA_BLUE = "\x1b[38;2;65;65;252m";
12
13
  const VANA_GREEN = "\x1b[38;2;0;213;11m";
@@ -28,14 +29,91 @@ const vanaPromptTheme = {
28
29
  };
29
30
  import { createConnectRenderer, createHumanRenderer, createLoginRenderer, formatDisplayPath, formatRelativeTime, } from "./render/index.js";
30
31
  import { CliOutcomeStatus, migrateLegacyDataHome, getBrowserProfilesDir, getConnectorCacheDir, getLogsDir, getSessionsDir, getSourceResultPath, readCliState, readCliConfig, updateCliConfig, updateSourceState, } from "../core/index.js";
32
+ import { UnknownNetworkError, VANA_NETWORKS, isVanaNetworkName, resolveNetwork, } from "../core/network.js";
33
+ import { CliExitCode } from "../core/exit-codes.js";
34
+ import { registerAppCommands } from "./app/index.js";
35
+ import { lookupRegisteredServers } from "../personal-server/registered.js";
31
36
  import { fetchConnectorToCache, listAvailableSources, readCachedConnectorMetadata, } from "../connectors/registry.js";
32
37
  import { detectPersonalServerTarget, ingestResult, resolvePersonalServerAuthConfig, } from "../personal-server/index.js";
33
38
  import { findDataConnectorsDir, ManagedPlaywrightRuntime, } from "../runtime/index.js";
34
39
  import { listAvailableSkills, installSkill, readInstalledSkills, } from "../skills/index.js";
35
40
  import { queryStatus, querySources, queryDataList, queryDataShow, queryDoctor, } from "./queries.js";
36
41
  import { checkForUpdate, readUpdateCheck, isNewerVersion, } from "./update-check.js";
37
- import { loadCredentials, saveCredentials, clearCredentials, isExpired, formatAddress, formatExpiresIn, getAuthTarget, resolvePersonalServerUrl, runDeviceCodeFlow, runSelfHostedLoginFlow, } from "./auth.js";
42
+ import { loadCredentials, readStoredAuthFile, saveCredentials, clearCredentials, isExpired, formatAddress, formatExpiresIn, getAuthTarget, resolvePersonalServerUrl, runDeviceCodeFlow, runSelfHostedLoginFlow, } from "./auth.js";
38
43
  import { createCliTelemetrySession, flushTelemetryOutbox, getActiveTelemetrySession, getTelemetryStatus, setActiveTelemetrySession, setTelemetryEnabled, trackActiveTelemetryEvent, } from "./telemetry.js";
44
+ /** Long flags shared by every command; declared once, applied to all leaves. */
45
+ const GLOBAL_FLAG_DEFS = [
46
+ ["--json", "Output machine-readable JSON"],
47
+ ["--no-input", "Never prompt; fail or print a URL instead"],
48
+ ["--yes", "Assume yes for confirmation prompts"],
49
+ ["--quiet", "Suppress non-essential output"],
50
+ ["--ipc", "Emit structured IPC events (internal)"],
51
+ ["--detach", "Run in the background where supported"],
52
+ ];
53
+ /**
54
+ * Declare the shared global flags on every leaf command that does not
55
+ * already define them, so commander accepts them anywhere on the line and
56
+ * `--help` documents them. Idempotent per flag.
57
+ */
58
+ function applyGlobalOptions(command) {
59
+ if (command.commands.length === 0) {
60
+ for (const [flags, description] of GLOBAL_FLAG_DEFS) {
61
+ const long = `--${flags.replace(/^--(no-)?/, "")}`;
62
+ const exists = command.options.some((option) => option.long === flags || option.long === long);
63
+ if (!exists) {
64
+ command.option(flags, description);
65
+ }
66
+ }
67
+ if (!command.options.some((option) => option.long === "--network")) {
68
+ command.addOption(new Option("--network <network>", "Vana network").choices([
69
+ ...VANA_NETWORKS,
70
+ ]));
71
+ }
72
+ return;
73
+ }
74
+ for (const child of command.commands) {
75
+ applyGlobalOptions(child);
76
+ }
77
+ }
78
+ /**
79
+ * Merge the options commander actually parsed (root -> action command, child
80
+ * wins) into the pre-parse seed, so action handlers see the authoritative
81
+ * values while pre-parse consumers (update-notifier suppression, telemetry
82
+ * context) keep working from the argv scan.
83
+ */
84
+ function mergeParsedGlobalOptions(target, actionCommand) {
85
+ const chain = [];
86
+ for (let current = actionCommand; current; current = current.parent) {
87
+ chain.unshift(current);
88
+ }
89
+ // Only values commander actually parsed count; a leaf's implicit
90
+ // default (e.g. --no-input declaring input: true) must never overwrite a
91
+ // value the user set at another level of the command line.
92
+ const merged = {};
93
+ for (const command of chain) {
94
+ const opts = command.opts();
95
+ for (const key of Object.keys(opts)) {
96
+ if (command.getOptionValueSource(key) !== "default") {
97
+ merged[key] = opts[key];
98
+ }
99
+ }
100
+ }
101
+ if (merged.json === true)
102
+ target.json = true;
103
+ if (merged.input === false)
104
+ target.noInput = true;
105
+ if (merged.yes === true)
106
+ target.yes = true;
107
+ if (merged.quiet === true)
108
+ target.quiet = true;
109
+ if (merged.ipc === true)
110
+ target.ipc = true;
111
+ if (merged.detach === true)
112
+ target.detach = true;
113
+ if (typeof merged.network === "string" && isVanaNetworkName(merged.network)) {
114
+ target.network = merged.network;
115
+ }
116
+ }
39
117
  function cleanDescription(desc) {
40
118
  return desc
41
119
  .replace(/ using Playwright browser automation\.?/i, ".")
@@ -43,8 +121,10 @@ function cleanDescription(desc) {
43
121
  }
44
122
  const require = createRequire(import.meta.url);
45
123
  export async function runCli(argv = process.argv) {
46
- // Migrate ~/.dataconnect → ~/.vana, symlink old path for DataConnect compat
47
- if (migrateLegacyDataHome()) {
124
+ // Migrate ~/.dataconnect → ~/.vana, symlink old path for DataConnect compat.
125
+ // Only an actual data migration announces itself; the compat symlink that
126
+ // every fresh install gets on its second run stays silent.
127
+ if (migrateLegacyDataHome() === "migrated") {
48
128
  process.stderr.write("Moved your data to ~/.vana.\n\n");
49
129
  }
50
130
  const normalizedArgv = normalizeArgv(argv);
@@ -110,6 +190,16 @@ More:
110
190
  vana setup Install or repair runtime
111
191
  `);
112
192
  program.exitOverride();
193
+ // Global flags: declared here for `vana --help`, mirrored onto every leaf
194
+ // command by applyGlobalOptions() so they are accepted anywhere on the
195
+ // line. Values are merged back into parsedOptions in the preAction hook.
196
+ for (const [flags, description] of GLOBAL_FLAG_DEFS) {
197
+ program.option(flags, description);
198
+ }
199
+ program.addOption(new Option("--network <network>", "Vana network for protocol operations (default: moksha)").choices([...VANA_NETWORKS]));
200
+ program.hook("preAction", (_thisCommand, actionCommand) => {
201
+ mergeParsedGlobalOptions(parsedOptions, actionCommand);
202
+ });
113
203
  program
114
204
  .command("version")
115
205
  .description("Print CLI version")
@@ -383,12 +473,14 @@ Examples:
383
473
  telemetry
384
474
  .command("enable")
385
475
  .description("Enable telemetry")
476
+ .option("--json", "Output machine-readable JSON")
386
477
  .action(async () => {
387
478
  process.exitCode = await runTelemetryEnable(parsedOptions);
388
479
  });
389
480
  telemetry
390
481
  .command("disable")
391
482
  .description("Disable telemetry")
483
+ .option("--json", "Output machine-readable JSON")
392
484
  .action(async () => {
393
485
  process.exitCode = await runTelemetryDisable(parsedOptions);
394
486
  });
@@ -468,6 +560,8 @@ Examples:
468
560
  .action(async () => {
469
561
  process.exitCode = await runCommandWithTelemetry({ ...telemetryBaseContext, command: "schedule", subcommand: "remove" }, async () => runScheduleRemove(parsedOptions));
470
562
  });
563
+ registerAppCommands(program, () => parsedOptions);
564
+ applyGlobalOptions(program);
471
565
  try {
472
566
  await program.parseAsync(normalizedArgv);
473
567
  }
@@ -479,8 +573,13 @@ Examples:
479
573
  process.exitCode = error.exitCode;
480
574
  return Number(process.exitCode ?? 0);
481
575
  }
482
- // Commander already printed to stderr; just set exit code.
483
- process.exitCode = error.exitCode;
576
+ // Commander already printed to stderr. Usage errors (unknown command
577
+ // or option, missing or invalid argument) exit 2 per the exit-code
578
+ // table in docs/CLI-EXIT-CODE-MATRIX.md; anything else keeps
579
+ // commander's own code.
580
+ process.exitCode = error.code.startsWith("commander.")
581
+ ? CliExitCode.USAGE
582
+ : error.exitCode;
484
583
  return Number(process.exitCode ?? 1);
485
584
  }
486
585
  throw error;
@@ -767,9 +866,7 @@ async function runConnect(rawSource, options) {
767
866
  connectorInstalled: false,
768
867
  lastRunAt: new Date().toISOString(),
769
868
  lastRunOutcome: CliOutcomeStatus.CONNECTOR_UNAVAILABLE,
770
- dataState: "none",
771
869
  lastError: message,
772
- lastResultPath: null,
773
870
  lastLogPath: getErrorLogPath(retryError),
774
871
  });
775
872
  renderer?.fail(`${displayName} connector could not be verified.`);
@@ -790,9 +887,7 @@ async function runConnect(rawSource, options) {
790
887
  connectorInstalled: false,
791
888
  lastRunAt: new Date().toISOString(),
792
889
  lastRunOutcome: CliOutcomeStatus.CONNECTOR_UNAVAILABLE,
793
- dataState: "none",
794
890
  lastError: message,
795
- lastResultPath: null,
796
891
  lastLogPath: getErrorLogPath(firstError),
797
892
  });
798
893
  renderer?.fail(`${displayName} is not available.`);
@@ -845,9 +940,7 @@ async function runConnect(rawSource, options) {
845
940
  sessionPresent: fs.existsSync(profilePath),
846
941
  lastRunAt: new Date().toISOString(),
847
942
  lastRunOutcome: CliOutcomeStatus.LEGACY_AUTH,
848
- dataState: "none",
849
943
  lastError: message,
850
- lastResultPath: null,
851
944
  lastLogPath: fetchLogPath ?? null,
852
945
  });
853
946
  renderer?.fail(`${displayName} requires a browser window, but no display is available.`);
@@ -1002,8 +1095,6 @@ async function runConnect(rawSource, options) {
1002
1095
  lastRunAt: new Date().toISOString(),
1003
1096
  lastRunOutcome: CliOutcomeStatus.LEGACY_AUTH,
1004
1097
  lastError: event.message ?? "Legacy authentication is required.",
1005
- dataState: "none",
1006
- lastResultPath: null,
1007
1098
  lastLogPath: event.logPath,
1008
1099
  connectionHealth: "needs_reauth",
1009
1100
  connectionHealthChangedAt: new Date().toISOString(),
@@ -1118,9 +1209,7 @@ async function runConnect(rawSource, options) {
1118
1209
  sessionPresent: fs.existsSync(profilePath),
1119
1210
  lastRunAt: new Date().toISOString(),
1120
1211
  lastRunOutcome: CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR,
1121
- dataState: "none",
1122
1212
  lastError: "Connector run ended without a result.",
1123
- lastResultPath: null,
1124
1213
  lastLogPath: runLogPath ?? fetchLogPath ?? null,
1125
1214
  });
1126
1215
  renderer?.fail(`Problem connecting ${displayName}.`);
@@ -1171,6 +1260,9 @@ async function runConnect(rawSource, options) {
1171
1260
  else if (finalDataState === "ingest_unavailable") {
1172
1261
  successSummary = `Collected your ${displayName} data. Personal Server sync is pending.`;
1173
1262
  }
1263
+ else if (finalDataState === "ingest_failed") {
1264
+ successSummary = `Collected your ${displayName} data, but Personal Server sync failed.`;
1265
+ }
1174
1266
  else {
1175
1267
  successSummary = `Collected your ${displayName} data and saved it locally.`;
1176
1268
  }
@@ -1187,6 +1279,14 @@ async function runConnect(rawSource, options) {
1187
1279
  renderer?.detail(`Pending sync will retry during scheduled collection.`);
1188
1280
  renderer?.detail(`Retry now: vana server sync`);
1189
1281
  }
1282
+ else if (finalDataState === "ingest_failed") {
1283
+ if (ingestFailureMessage?.includes("401")) {
1284
+ renderer?.detail("Your Personal Server requires authentication. Run `vana login` to authenticate, then `vana server sync`.");
1285
+ }
1286
+ else {
1287
+ renderer?.detail(`Retry: vana server sync`);
1288
+ }
1289
+ }
1190
1290
  // Journey-aware next step
1191
1291
  const state = await readCliState();
1192
1292
  const connectedSourceCount = Object.values(state.sources ?? {}).filter((s) => hasCollectedData(s?.dataState)).length;
@@ -1288,15 +1388,15 @@ async function runConnectEntry(options) {
1288
1388
  }
1289
1389
  : null,
1290
1390
  })}\n`);
1291
- return 1;
1391
+ return CliExitCode.USAGE;
1292
1392
  }
1293
1393
  if (options.noInput) {
1294
1394
  emit.info(missingSourceMessage);
1295
- return 1;
1395
+ return CliExitCode.USAGE;
1296
1396
  }
1297
1397
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
1298
1398
  emit.info(missingSourceMessage);
1299
- return 1;
1399
+ return CliExitCode.USAGE;
1300
1400
  }
1301
1401
  if (enrichedSources.length === 0) {
1302
1402
  emit.info("No sources are available right now.");
@@ -1587,7 +1687,41 @@ function formatSyncFailureSummary(source) {
1587
1687
  return `${failedScopes.length} scopes failed to sync: ${summaryParts.join("; ")}. Run \`vana connect ${source.source}\`.`;
1588
1688
  }
1589
1689
  async function runDoctor(options) {
1590
- const payload = await queryDoctor();
1690
+ const basePayload = await queryDoctor();
1691
+ // Builder-side protocol network (owner-side server config is separate and
1692
+ // reported by its own checks). Additive field; the rest of the payload
1693
+ // still matches CliDoctor exactly. A bad VANA_NETWORK/VANA_ENV combination
1694
+ // is a usage problem, not a crash: exit 2 with a structured message.
1695
+ let network;
1696
+ try {
1697
+ network = resolveNetwork(options.network);
1698
+ }
1699
+ catch (error) {
1700
+ if (error instanceof UnknownNetworkError) {
1701
+ if (options.json) {
1702
+ process.stdout.write(`${JSON.stringify({ error: "bad_usage", message: error.message })}\n`);
1703
+ }
1704
+ else {
1705
+ process.stderr.write(`${error.message}\n`);
1706
+ }
1707
+ return CliExitCode.USAGE;
1708
+ }
1709
+ throw error;
1710
+ }
1711
+ const payload = {
1712
+ ...basePayload,
1713
+ network: {
1714
+ name: network.name,
1715
+ env: network.env,
1716
+ chainId: network.chainId,
1717
+ gatewayUrl: network.gatewayUrl,
1718
+ source: options.network
1719
+ ? "flag"
1720
+ : process.env.VANA_NETWORK
1721
+ ? "env"
1722
+ : "default",
1723
+ },
1724
+ };
1591
1725
  if (options.json) {
1592
1726
  process.stdout.write(`${JSON.stringify(payload)}\n`);
1593
1727
  return 0;
@@ -1603,6 +1737,7 @@ async function runDoctor(options) {
1603
1737
  emit.keyValue("Install", formatInstallMethodLabel(payload.installMethod), "muted");
1604
1738
  emit.keyValue("Runtime", payload.runtime, toneForRuntime(payload.runtime));
1605
1739
  emit.keyValue("Personal Server", payload.personalServer, payload.personalServer === "available" ? "success" : "warning");
1740
+ emit.keyValue("Network", `${payload.network.name} (chain ${payload.network.chainId}, ${payload.network.source})`, "muted");
1606
1741
  emit.keyValue("Tracked sources", String(payload.summary.trackedSourceCount), "muted");
1607
1742
  emit.keyValue("Attention", String(payload.summary.attentionCount), payload.summary.attentionCount > 0 ? "warning" : "muted");
1608
1743
  emit.keyValue("Connected", String(payload.summary.connectedCount), payload.summary.connectedCount > 0 ? "success" : "muted");
@@ -1669,6 +1804,11 @@ async function runDoctor(options) {
1669
1804
  async function runServerStatus(options) {
1670
1805
  const emit = createEmitter(options);
1671
1806
  const target = await detectPersonalServerTarget();
1807
+ // The local transport URL and the gateway-registered public URL are two
1808
+ // names for the same server; show both when the owner is known.
1809
+ const registeredServers = target.health?.owner
1810
+ ? await lookupRegisteredServers(target.health.owner)
1811
+ : [];
1672
1812
  const state = await readCliState();
1673
1813
  // Count scopes from state
1674
1814
  let totalScopeCount = 0;
@@ -1682,6 +1822,7 @@ async function runServerStatus(options) {
1682
1822
  state: target.state,
1683
1823
  url: target.url,
1684
1824
  source: target.source,
1825
+ registeredServers,
1685
1826
  health: target.health,
1686
1827
  scopeCount: totalScopeCount,
1687
1828
  })}\n`);
@@ -1701,6 +1842,9 @@ async function runServerStatus(options) {
1701
1842
  : `(${target.source ?? "unknown"})`;
1702
1843
  emit.keyValue("URL", `${target.url} ${urlSuffix}`, "muted");
1703
1844
  }
1845
+ for (const server of registeredServers) {
1846
+ emit.keyValue("Registered", `${server.url} (${server.network})`, "muted");
1847
+ }
1704
1848
  const stateLabel = target.state === "available" ? "healthy" : "Not connected";
1705
1849
  emit.keyValue("Status", stateLabel, target.state === "available" ? "success" : "warning");
1706
1850
  if (target.health) {
@@ -3293,8 +3437,25 @@ export function getCliChannel(version = getCliVersion()) {
3293
3437
  ? "canary"
3294
3438
  : "stable";
3295
3439
  }
3296
- export function getCliInstallMethod(execPath = process.execPath) {
3297
- const candidates = [process.env.VANA_APP_ROOT ?? "", execPath].map((value) => value.replace(/\\/g, "/").toLowerCase());
3440
+ function normalizePathForMatch(value) {
3441
+ return value.replace(/\\/g, "/").toLowerCase();
3442
+ }
3443
+ /** Where this file itself sits, which is what separates npm from a checkout. */
3444
+ function getOwnModulePath() {
3445
+ try {
3446
+ return fileURLToPath(import.meta.url);
3447
+ }
3448
+ catch {
3449
+ return "";
3450
+ }
3451
+ }
3452
+ /** True when the CLI is running out of an npm install or an npx cache. */
3453
+ function isPackagedUnderNodeModules(normalizedPath) {
3454
+ return (normalizedPath.includes("/node_modules/vana-cli/") ||
3455
+ normalizedPath.includes("/_npx/"));
3456
+ }
3457
+ export function getCliInstallMethod(execPath = process.execPath, modulePath = getOwnModulePath()) {
3458
+ const candidates = [process.env.VANA_APP_ROOT ?? "", execPath].map(normalizePathForMatch);
3298
3459
  for (const normalizedPath of candidates) {
3299
3460
  if (!normalizedPath) {
3300
3461
  continue;
@@ -3308,6 +3469,18 @@ export function getCliInstallMethod(execPath = process.execPath) {
3308
3469
  /\/releases\/[^/]+\/app$/.test(normalizedPath)) {
3309
3470
  return "installer";
3310
3471
  }
3472
+ }
3473
+ // npm and npx both run under the user's own node, so execPath looks exactly
3474
+ // like a development checkout. Only the package's own location tells them
3475
+ // apart, and getting it wrong costs an npm user every update notification.
3476
+ if (modulePath &&
3477
+ isPackagedUnderNodeModules(normalizePathForMatch(modulePath))) {
3478
+ return "npm";
3479
+ }
3480
+ for (const normalizedPath of candidates) {
3481
+ if (!normalizedPath) {
3482
+ continue;
3483
+ }
3311
3484
  if (normalizedPath.endsWith("/node") ||
3312
3485
  normalizedPath.endsWith("/node.exe") ||
3313
3486
  normalizedPath.includes("/.nvm/") ||
@@ -3336,14 +3509,28 @@ export function formatInstallMethodLabel(method) {
3336
3509
  return "Homebrew";
3337
3510
  case "installer":
3338
3511
  return "Hosted installer";
3512
+ case "npm":
3513
+ return "npm";
3339
3514
  case "development":
3340
3515
  return "Development checkout";
3341
3516
  default:
3342
3517
  return "Unknown";
3343
3518
  }
3344
3519
  }
3345
- export function getLifecycleCommands(installMethod, channel) {
3520
+ export function getLifecycleCommands(installMethod, channel, modulePath = getOwnModulePath()) {
3346
3521
  switch (installMethod) {
3522
+ case "npm":
3523
+ // npx installs nothing, so there is nothing to upgrade or remove:
3524
+ // asking for @latest is the whole update story there.
3525
+ return normalizePathForMatch(modulePath).includes("/_npx/")
3526
+ ? {
3527
+ upgrade: "npx vana-cli@latest",
3528
+ uninstall: "npx leaves nothing installed. Remove ~/.vana for any state you no longer need.",
3529
+ }
3530
+ : {
3531
+ upgrade: "npm install -g vana-cli@latest",
3532
+ uninstall: "npm uninstall -g vana-cli",
3533
+ };
3347
3534
  case "homebrew":
3348
3535
  return {
3349
3536
  upgrade: "brew update && brew upgrade vana",
@@ -3374,7 +3561,30 @@ export function getLifecycleCommands(installMethod, channel) {
3374
3561
  };
3375
3562
  }
3376
3563
  }
3377
- function extractGlobalOptions(argv) {
3564
+ /**
3565
+ * Pre-parse seed for the global options. Needed before commander runs (the
3566
+ * update-notifier suppression and the telemetry context read it); the
3567
+ * preAction hook overlays the authoritative parsed values afterwards.
3568
+ */
3569
+ function extractGlobalOptions(rawArgv) {
3570
+ // Everything after a bare "--" is positional; commander will not parse it
3571
+ // as options, so the seed must not either.
3572
+ const boundary = rawArgv.indexOf("--");
3573
+ const argv = boundary === -1 ? rawArgv : rawArgv.slice(0, boundary);
3574
+ let network;
3575
+ for (let index = 0; index < argv.length; index += 1) {
3576
+ const argument = argv[index];
3577
+ let candidate;
3578
+ if (argument === "--network") {
3579
+ candidate = argv[index + 1];
3580
+ }
3581
+ else if (argument.startsWith("--network=")) {
3582
+ candidate = argument.slice("--network=".length);
3583
+ }
3584
+ if (candidate && isVanaNetworkName(candidate)) {
3585
+ network = candidate;
3586
+ }
3587
+ }
3378
3588
  return {
3379
3589
  json: argv.includes("--json"),
3380
3590
  noInput: argv.includes("--no-input"),
@@ -3382,6 +3592,7 @@ function extractGlobalOptions(argv) {
3382
3592
  yes: argv.includes("--yes"),
3383
3593
  quiet: argv.includes("--quiet"),
3384
3594
  detach: argv.includes("--detach"),
3595
+ network,
3385
3596
  };
3386
3597
  }
3387
3598
  export function createSourceLabelMap(sources) {
@@ -4337,7 +4548,7 @@ async function runSkillsGuidedPicker(options) {
4337
4548
  // In JSON mode, fall back to list behavior
4338
4549
  return runSkillList(options);
4339
4550
  }
4340
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
4551
+ if (options.noInput || !process.stdin.isTTY || !process.stdout.isTTY) {
4341
4552
  return runSkillList(options);
4342
4553
  }
4343
4554
  try {
@@ -4506,23 +4717,32 @@ async function runLogin(options, serverUrl, clientId) {
4506
4717
  try {
4507
4718
  const result = await runSelfHostedLoginFlow(psUrl, (url) => {
4508
4719
  if (!options.json && !options.quiet) {
4509
- process.stderr.write(` ${humanRenderer.theme.muted("Open this URL in your browser:")}\n`);
4510
- process.stderr.write(` ${humanRenderer.theme.muted(url)}\n`);
4720
+ // Same emphasis rule as the cloud flow: the actionable URL
4721
+ // carries the Vana accent, the label stays muted.
4722
+ process.stderr.write(` ${humanRenderer.theme.muted("Open")} ${humanRenderer.theme.accent(url)}\n`);
4511
4723
  }
4512
4724
  renderer?.scopeActive("Waiting for authorization");
4513
4725
  // Try to open browser — use spawn with args array to prevent shell injection
4514
- // (a malicious self-hosted PS could return a URL with shell metacharacters)
4515
- try {
4516
- const { spawn } = require("node:child_process");
4517
- const opener = process.platform === "darwin"
4518
- ? "open"
4519
- : process.platform === "win32"
4520
- ? "start"
4521
- : "xdg-open";
4522
- spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
4523
- }
4524
- catch {
4525
- // Browser open failed — user will open manually
4726
+ // (a malicious self-hosted PS could return a URL with shell metacharacters).
4727
+ // Interactive terminals only: agents, CI and the test suite must never
4728
+ // pop a browser (a vitest run used to open the fixture URL for real).
4729
+ const browserAllowed = Boolean(process.stdout.isTTY) &&
4730
+ !options.json &&
4731
+ !options.noInput &&
4732
+ !process.env.VANA_NO_BROWSER;
4733
+ if (browserAllowed) {
4734
+ try {
4735
+ const { spawn } = require("node:child_process");
4736
+ const opener = process.platform === "darwin"
4737
+ ? "open"
4738
+ : process.platform === "win32"
4739
+ ? "start"
4740
+ : "xdg-open";
4741
+ spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
4742
+ }
4743
+ catch {
4744
+ // Browser open failed — user will open manually
4745
+ }
4526
4746
  }
4527
4747
  });
4528
4748
  await saveCredentials({
@@ -4570,6 +4790,10 @@ async function runLogin(options, serverUrl, clientId) {
4570
4790
  }
4571
4791
  // Check if already logged in
4572
4792
  const existing = loadCredentials();
4793
+ // For the account-switch check below: the previous auth file content even
4794
+ // when the stored credentials have expired (loadCredentials hides those).
4795
+ const previousAuth = readStoredAuthFile();
4796
+ const previousAddress = previousAuth?.address ?? null;
4573
4797
  if (existing && !isExpired(existing)) {
4574
4798
  if (options.json) {
4575
4799
  process.stdout.write(`${JSON.stringify({
@@ -4610,9 +4834,26 @@ async function runLogin(options, serverUrl, clientId) {
4610
4834
  },
4611
4835
  }, { clientId });
4612
4836
  if (creds) {
4837
+ if (!creds.personal_server &&
4838
+ previousAuth?.personalServer &&
4839
+ (!previousAddress || previousAddress === creds.account.address)) {
4840
+ // The cloud flow cannot mint PS sessions; keep the one we had.
4841
+ creds.personal_server = previousAuth.personalServer;
4842
+ }
4613
4843
  await saveCredentials(creds);
4844
+ // Always sync the pinned PS config to this login's result — including
4845
+ // clearing it when this account has no PS, so a stale PS URL from a
4846
+ // previously logged-in account can't linger and be used by mistake.
4614
4847
  if (creds.personal_server?.url) {
4615
- await updateCliConfig({ personalServerUrl: creds.personal_server.url });
4848
+ await updateCliConfig({
4849
+ personalServerUrl: creds.personal_server.url,
4850
+ });
4851
+ }
4852
+ else if (previousAddress && previousAddress !== creds.account.address) {
4853
+ // A different account logged in: its pinned Personal Server URL is
4854
+ // stale. Same-account logins keep the pin - the prod token flow
4855
+ // carries no PS info, so its absence proves nothing.
4856
+ await updateCliConfig({ personalServerUrl: undefined });
4616
4857
  }
4617
4858
  process.stdout.write(`${JSON.stringify({
4618
4859
  status: "authenticated",
@@ -4627,37 +4868,54 @@ async function runLogin(options, serverUrl, clientId) {
4627
4868
  // Interactive mode
4628
4869
  const renderer = createLoginRenderer();
4629
4870
  const humanRenderer = createHumanRenderer();
4630
- const writeStaticLoginNotes = (lines) => {
4871
+ // Branding lives in emphasis, not chrome (CLI-BEAUTY doctrine: no
4872
+ // banners). The two things the user must act on - the URL and the code -
4873
+ // carry the Vana accent; everything around them stays muted.
4874
+ const writeDeviceCodePrompt = (code, uri) => {
4631
4875
  if (options.json || options.quiet) {
4632
4876
  return;
4633
4877
  }
4634
- for (const line of lines) {
4635
- process.stderr.write(` ${humanRenderer.theme.muted(line)}\n`);
4636
- }
4878
+ const theme = humanRenderer.theme;
4879
+ process.stderr.write(` ${theme.muted("Open")} ${theme.accent(uri)}\n`);
4880
+ process.stderr.write(` ${theme.muted("Enter")} ${theme.heading(code)}\n`);
4637
4881
  };
4638
4882
  renderer.title("Vana");
4639
4883
  const creds = await runDeviceCodeFlow({
4640
4884
  onCode: (code, uri) => {
4641
- writeStaticLoginNotes([
4642
- "Open this URL in your browser:",
4643
- uri,
4644
- `Enter this code: ${code}`,
4645
- ]);
4885
+ writeDeviceCodePrompt(code, uri);
4646
4886
  },
4647
4887
  onWaiting: () => {
4648
4888
  renderer.scopeActive("Waiting for authorization");
4649
4889
  },
4650
4890
  onAuthorized: async (authedCreds) => {
4891
+ if (!authedCreds.personal_server &&
4892
+ previousAuth?.personalServer &&
4893
+ (!previousAddress || previousAddress === authedCreds.account.address)) {
4894
+ // The cloud flow cannot mint PS sessions; keep the one we had.
4895
+ authedCreds.personal_server = previousAuth.personalServer;
4896
+ }
4651
4897
  await saveCredentials(authedCreds);
4898
+ // Always sync the pinned PS config to this login's result — including
4899
+ // clearing it when this account has no PS, so a stale PS URL from a
4900
+ // previously logged-in account can't linger and be used by mistake.
4652
4901
  if (authedCreds.personal_server?.url) {
4653
4902
  await updateCliConfig({
4654
4903
  personalServerUrl: authedCreds.personal_server.url,
4655
4904
  });
4656
4905
  }
4906
+ else if (previousAddress &&
4907
+ previousAddress !== authedCreds.account.address) {
4908
+ // See the JSON branch: clear only on an account switch.
4909
+ await updateCliConfig({ personalServerUrl: undefined });
4910
+ }
4657
4911
  renderer.success(`Logged in as ${formatAddress(authedCreds.account.address)}`);
4658
4912
  if (authedCreds.personal_server) {
4659
4913
  renderer.detail(`Personal Server: ${authedCreds.personal_server.url}`);
4660
4914
  }
4915
+ else {
4916
+ renderer.detail("No Personal Server found for this account yet.");
4917
+ renderer.next("vana server set-url <url>");
4918
+ }
4661
4919
  renderer.detail("Credentials saved to ~/.vana/auth.json");
4662
4920
  },
4663
4921
  onExpired: () => {