@indigoai-us/hq-cli 5.345.203 → 5.345.204

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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.204] — 2026-11-05
6
+ - `hq bot create`, `hq doctor` and the Claude bot sign-in check now read the macOS Keychain item that Claude Code keeps under your login account name first. A Mac can also hold an older item for the same service under another account (often "unknown", with no Claude login in it). The check used to read that one and said Claude was not signed in right after you signed in from HQ Desktop, so Create bot failed.
7
+ - `hq bot create --json` now includes `brainSignIn` (the same shape `hq bot list`, `status` and `start` report) when it refuses a Claude bot because Claude Code is not signed in, so HQ Desktop can offer its own sign-in instead of "Could not create".
8
+ - `hq capacity` renews expired Claude tokens on a busy Mac again. The refresh check reads the lane list once per collect instead of once per account, runs its two reads together without blocking the mesh daemon, skips the Outpost mirror refresh it does not need, and allows each read 30 seconds instead of 15. On macOS it now reads each running `claude` process's environment, so an open session on the default home no longer blocks the refresh of every other account home. A process whose environment cannot be read still blocks every home, and if the lane read fails no token is refreshed. A session started with `HOME` set to another directory and no `CLAUDE_CONFIG_DIR` now blocks the refresh of that directory's `.claude` home.
9
+ - `hq signals run` now runs the extract step on the Grok CLI when the main local bot runs on Grok, instead of failing every source item with "grok has no restricted tool mode". Each turn runs grok 1.0.70 or newer in `dontAsk` mode with only the file reader allowed, grok's own read-only shell list denied, the MCP bridge, web tools and subagents off, a turn cap, and a per-turn `strict` sandbox profile that can read only the item's channel folder (plus the scripts the candidate command needs) and grants no extra writes. Reads of the Grok login are denied for the literal `~/.grok` spelling, the isolated home and the real home, because Grok matches a `~` path literally before expanding it. An older grok, or a main bot on grok with no grok installed, is never run unrestricted: the step uses Claude Code, then Codex, when that tool is installed and signed in, records the fallback in the run's step detail, and otherwise fails the run naming the grok version it needs. On macOS and Windows the step does not select Grok, because Grok only blocks child-process network on Linux, and falls through the same way. Grok has no terminal tool: HQ validates its returned candidate commands, checks the pinned writer still matches the script it generated, and then invokes that writer with fixed arguments, rejecting shell chaining, substitutions and redirections. The host writes candidates after the turn. The resolved Grok executable is pinned for the probe and turn, and probe caching distinguishes changes to PATH. Its isolated temporary home carries only a private login-file copy, excludes user configuration and hooks, and is removed when the turn ends, and its environment is an allowlist.
10
+ - `hq lanes` admission on macOS no longer holds every new lane while `kern.memorystatus_vm_pressure_level` is warn. A warn level now holds only when the free percentage (`kern.memorystatus_level`) is at or below `throttle.memory_warn_min_free_percent` (default 20, a number from 0 to 100); critical still always holds, and `throttle.min_available_mem_mb` still applies. The hold message states this rule.
11
+ - The daemon's new-sync-engine units now plan on Windows. A Windows machine downloads the `windows-x64` or `windows-arm64` zip, installs `hq-syncd.exe` (moving a running copy aside to `hq-syncd.exe.old`), and writes an engine home with no symlinks: the config names the token file and the engine reads `menubar.json` and `work-mesh/` from the daemon's state folder. Windows vaults stay on the current sync, and no engine is downloaded, until `SYNC_CORE_WINDOWS_FULL_RELEASE` names an engine release whose Windows build runs full sync; it is unset in this release. One machine can test a build first by setting `HQ_SYNC_CORE_WINDOWS_FULL_RELEASE` to that release.
12
+ - Show the upload-hold explanation and recovery command in sync status.
13
+
5
14
  ## [5.345.203] — 2026-11-05
6
15
  - Fix Codex capacity routing after plan exhaustion: available credits keep accounts usable at lowest priority, and superseded snapshots or passed resets no longer retain exhausted usage.
7
16
  - Keep company Outpost integration commands on their delegated machine session and allow sharing connections with an `out_*` principal.
@@ -935,7 +935,11 @@ function requireClaudeCodeLoginForCreate(name) {
935
935
  const check = botCreateSeams.claudeLogin();
936
936
  if (check.signedIn)
937
937
  return;
938
- throw Object.assign(new Error(`Claude Code is not signed in on this computer, so ${name} could not answer. ${describeClaudeLoginSources(check)} ${claudeLoginFix()}`.replace(/\s+/g, " ").trim()), { expected: true });
938
+ const reason = `Claude Code is not signed in on this computer, so ${name} could not answer.`;
939
+ // The same `brainSignIn` shape list, status and start report, so the
940
+ // desktop app offers its own sign-in instead of showing a terminal command.
941
+ const brainSignIn = { provider: "claude", needed: true, reason, command: "claude /login" };
942
+ throw Object.assign(new Error(`${reason} ${describeClaudeLoginSources(check)} ${claudeLoginFix()}`.replace(/\s+/g, " ").trim()), { expected: true, brainSignIn });
939
943
  }
940
944
  export const botStopSeams = {
941
945
  /** True when this process runs inside the bot process `botPid` (one of its turns). */
@@ -1099,7 +1103,8 @@ export async function runBotCreate(nameArg, opts) {
1099
1103
  // there too, not only a line on stderr.
1100
1104
  if (opts.json && !err?.jsonReported) {
1101
1105
  const message = err instanceof Error ? err.message : String(err);
1102
- console.log(JSON.stringify({ ok: false, name: nameArg, reason: "create-failed", message }, null, 2));
1106
+ const brainSignIn = err?.brainSignIn ?? null;
1107
+ console.log(JSON.stringify({ ok: false, name: nameArg, reason: "create-failed", message, ...(brainSignIn ? { brainSignIn } : {}) }, null, 2));
1103
1108
  }
1104
1109
  throw err;
1105
1110
  }
@@ -210,15 +210,27 @@ export declare function scopeExcludedWarning(count: number): string | null;
210
210
  * largest count it sees instead of a sum: a repeated emit for the same hold
211
211
  * cannot inflate the total. A missing, non-numeric or negative count reads as 0.
212
212
  */
213
+ export interface IdentityHeldNotice {
214
+ count: number;
215
+ /** Plain-language hold from hq-cloud, when that build sends one. */
216
+ message?: string;
217
+ }
213
218
  export declare function createIdentityHeldTracker(): {
214
219
  observe: (event: {
215
220
  type: string;
216
221
  count?: unknown;
222
+ message?: unknown;
223
+ reason?: unknown;
217
224
  }) => void;
218
225
  count: () => number;
226
+ notice: () => IdentityHeldNotice;
219
227
  };
220
- /** Recovery lines for files held by the account-switch push fence. */
221
- export declare function identityHeldLines(count: number): string[];
228
+ /**
229
+ * Recovery lines for files held by the account-switch push fence.
230
+ * A newer hq-cloud sends `message` (file names and `hq sync release-held`).
231
+ * Older builds send a count only, and this keeps the previous sentence.
232
+ */
233
+ export declare function identityHeldLines(count: number, notice?: Pick<IdentityHeldNotice, "message">): string[];
222
234
  /** Actionable recovery for files refused because the vault baseline is stale. */
223
235
  export declare function staleUploadRefusalLines(count: number, paths?: readonly string[]): string[];
224
236
  /**
@@ -500,19 +500,9 @@ export function scopeExcludedWarning(count) {
500
500
  `They were skipped, not synced. Re-run with --json to list them, or ` +
501
501
  `ask an admin to grant you write on those paths.`);
502
502
  }
503
- /**
504
- * Counts the files share() held back because this sync scope is fenced after an account
505
- * switch on this computer (feedback_a69e12ba). share() reports the hold only as
506
- * an `identity-switch-push-held` event and leaves the files out of every
507
- * result counter, so without this the push printed "Pushed 0 file(s) (0 B,
508
- * 0 skipped)" as a success while the vault kept the old copy.
509
- *
510
- * Each event carries the size of one fenced hold, so the tracker keeps the
511
- * largest count it sees instead of a sum: a repeated emit for the same hold
512
- * cannot inflate the total. A missing, non-numeric or negative count reads as 0.
513
- */
514
503
  export function createIdentityHeldTracker() {
515
504
  let held = 0;
505
+ let message;
516
506
  return {
517
507
  observe(event) {
518
508
  if (event.type !== "identity-switch-push-held")
@@ -521,12 +511,24 @@ export function createIdentityHeldTracker() {
521
511
  ? Math.max(0, Math.trunc(event.count))
522
512
  : 0;
523
513
  held = Math.max(held, n);
514
+ // A company rebind uses the same event type and keeps its own warning.
515
+ if (event.reason === "company-rebind")
516
+ return;
517
+ if (typeof event.message === "string" && event.message.trim())
518
+ message = event.message.trim();
524
519
  },
525
520
  count: () => held,
521
+ notice: () => ({ count: held, ...(message ? { message } : {}) }),
526
522
  };
527
523
  }
528
- /** Recovery lines for files held by the account-switch push fence. */
529
- export function identityHeldLines(count) {
524
+ /**
525
+ * Recovery lines for files held by the account-switch push fence.
526
+ * A newer hq-cloud sends `message` (file names and `hq sync release-held`).
527
+ * Older builds send a count only, and this keeps the previous sentence.
528
+ */
529
+ export function identityHeldLines(count, notice) {
530
+ if (notice?.message)
531
+ return [`⚠ ${notice.message}`];
530
532
  if (count <= 0)
531
533
  return [];
532
534
  return [
@@ -1566,7 +1568,7 @@ export function registerCloudCommands(program) {
1566
1568
  for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
1567
1569
  log(chalk.yellow(line));
1568
1570
  }
1569
- for (const line of identityHeldLines(identityHeldCount)) {
1571
+ for (const line of identityHeldLines(identityHeldCount, identityHeld.notice())) {
1570
1572
  log(chalk.yellow(line));
1571
1573
  }
1572
1574
  await syncHealth.succeeded();
@@ -1599,7 +1601,12 @@ export function registerCloudCommands(program) {
1599
1601
  for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
1600
1602
  log(chalk.yellow(line));
1601
1603
  }
1602
- for (const line of identityHeldLines(identityHeldCount)) {
1604
+ for (const line of identityHeldLines(identityHeldCount, identityHeld.notice())) {
1605
+ log(chalk.yellow(line));
1606
+ }
1607
+ }
1608
+ else if (identityHeld.notice().message) {
1609
+ for (const line of identityHeldLines(identityHeldCount, identityHeld.notice())) {
1603
1610
  log(chalk.yellow(line));
1604
1611
  }
1605
1612
  }
@@ -2469,8 +2476,10 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2469
2476
  const pushRefused = planLimitTracker.summary();
2470
2477
  const pushUploadFailed = readUploadFailures(pushResult);
2471
2478
  const pushHeldCount = pushIdentityHeld.count();
2479
+ const pushHeldNotice = pushIdentityHeld.notice();
2472
2480
  const pushStatus = pushResult.aborted ||
2473
2481
  pushHeldCount > 0 ||
2482
+ Boolean(pushHeldNotice.message) ||
2474
2483
  pushResult.filesExcludedByScope > 0 ||
2475
2484
  (pushResult.filesRefusedStale ?? 0) > 0 ||
2476
2485
  pushUploadFailed.count > 0 ||
@@ -2498,7 +2507,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2498
2507
  for (const line of uploadFailedLines(pushUploadFailed.count, pushUploadFailed.paths)) {
2499
2508
  console.log(chalk.yellow(line));
2500
2509
  }
2501
- for (const line of identityHeldLines(pushHeldCount)) {
2510
+ for (const line of identityHeldLines(pushHeldCount, pushHeldNotice)) {
2502
2511
  console.log(chalk.yellow(line));
2503
2512
  }
2504
2513
  if (pushResult.aborted) {
@@ -59,6 +59,8 @@ export async function runSyncCoreReconcile(opts, d = defaultDeps()) {
59
59
  }),
60
60
  readRolloutFlag: (companyUid) => readCompanyRolloutFlag(companyUid, token, { env: d.env }),
61
61
  ensureBinary: (release) => ensureSyncCoreBinary({ home: d.home, platform: d.platform, arch: d.arch, release, env: d.env }),
62
+ platform: d.platform,
63
+ env: d.env,
62
64
  readPin: () => readSyncCorePin({ env: d.env }),
63
65
  log: opts.json ? () => { } : d.out,
64
66
  });
@@ -21,7 +21,13 @@ export interface ClaudeLoginDeps {
21
21
  env?: NodeJS.ProcessEnv;
22
22
  home?: string;
23
23
  platform?: NodeJS.Platform;
24
- readKeychain?: () => KeychainRead;
24
+ /**
25
+ * Reads the Keychain item; `account` scopes the read to one account name.
26
+ * Omitted account means "any item for the service" (the first one found).
27
+ */
28
+ readKeychain?: (account?: string) => KeychainRead;
29
+ /** The login user's account name; null when unknown. */
30
+ username?: string | null;
25
31
  now?: () => number;
26
32
  }
27
33
  export declare function checkClaudeCodeLogin(deps?: ClaudeLoginDeps): ClaudeLoginCheck;
@@ -17,9 +17,20 @@ import * as path from "node:path";
17
17
  import { fleetHome } from "./fleet-local.js";
18
18
  export const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
19
19
  export const CLAUDE_LOGIN_ENV_VARS = ["ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"];
20
- function readKeychainWithSecurity() {
20
+ function safeUsername() {
21
21
  try {
22
- const stdout = execFileSync("security", ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"], {
22
+ return os.userInfo().username || null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function readKeychainWithSecurity(account) {
29
+ const args = account
30
+ ? ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-a", account, "-w"]
31
+ : ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"];
32
+ try {
33
+ const stdout = execFileSync("security", args, {
23
34
  encoding: "utf8",
24
35
  stdio: ["ignore", "pipe", "pipe"],
25
36
  timeout: 5000,
@@ -113,30 +124,59 @@ export function checkClaudeCodeLogin(deps = {}) {
113
124
  sources.push({ source: "macos_keychain", status: "skipped", reason: "not macOS" });
114
125
  }
115
126
  else {
116
- const read = (deps.readKeychain ?? readKeychainWithSecurity)();
117
- if (read.code !== 0) {
118
- const detail = read.stderr.replace(/\s+/g, " ").trim().slice(0, 200) || `exit ${read.code}`;
119
- sources.push({ source: "macos_keychain", status: "missing", reason: `no readable '${CLAUDE_KEYCHAIN_SERVICE}' item (${detail})` });
120
- }
121
- else {
127
+ // Claude Code (and the HQ desktop sign-in, which runs `claude auth login`)
128
+ // writes its login under the login user's account name. Another item with
129
+ // the same service can exist under a different account (an old one written
130
+ // as "unknown", or one holding only MCP OAuth), and an unscoped read
131
+ // returns whichever the Keychain finds first. Ask for the user's item
132
+ // first, then fall back to any item for the service.
133
+ const readKeychain = deps.readKeychain ?? readKeychainWithSecurity;
134
+ const username = deps.username === undefined ? safeUsername() : deps.username;
135
+ const recordOf = (result) => {
136
+ if (result.code !== 0)
137
+ return null;
122
138
  let payload;
123
139
  try {
124
- payload = JSON.parse(read.stdout.trim());
140
+ payload = JSON.parse(result.stdout.trim());
125
141
  }
126
142
  catch {
127
143
  payload = null;
128
144
  }
129
- const record = payload && typeof payload === "object" ? oauthRecord(payload.claudeAiOauth) : null;
130
- if (record) {
131
- const described = describeRecord(record, now);
132
- sources.push({ source: "macos_keychain", status: described.status, reason: described.reason });
133
- if (described.usable)
134
- found("macos_keychain");
145
+ return payload && typeof payload === "object" ? oauthRecord(payload.claudeAiOauth) : null;
146
+ };
147
+ const scoped = username ? readKeychain(username) : null;
148
+ const scopedRecord = scoped ? recordOf(scoped) : null;
149
+ let read;
150
+ let record;
151
+ if (scoped && scopedRecord) {
152
+ read = scoped;
153
+ record = scopedRecord;
154
+ }
155
+ else {
156
+ const any = readKeychain();
157
+ const anyRecord = recordOf(any);
158
+ if (anyRecord || !scoped || scoped.code !== 0) {
159
+ read = any;
160
+ record = anyRecord;
135
161
  }
136
162
  else {
137
- sources.push({ source: "macos_keychain", status: "missing", reason: "keychain item has no claudeAiOauth access token" });
163
+ read = scoped;
164
+ record = null;
138
165
  }
139
166
  }
167
+ if (read.code !== 0) {
168
+ const detail = read.stderr.replace(/\s+/g, " ").trim().slice(0, 200) || `exit ${read.code}`;
169
+ sources.push({ source: "macos_keychain", status: "missing", reason: `no readable '${CLAUDE_KEYCHAIN_SERVICE}' item (${detail})` });
170
+ }
171
+ else if (record) {
172
+ const described = describeRecord(record, now);
173
+ sources.push({ source: "macos_keychain", status: described.status, reason: described.reason });
174
+ if (described.usable)
175
+ found("macos_keychain");
176
+ }
177
+ else {
178
+ sources.push({ source: "macos_keychain", status: "missing", reason: "keychain item has no claudeAiOauth access token" });
179
+ }
140
180
  }
141
181
  return { signedIn: via !== null, via, sources };
142
182
  }
@@ -140,8 +140,10 @@ export type WriteClaudeCredentials = (request: WriteClaudeCredentialsRequest) =>
140
140
  * way). The gate treats this as proof the account is already signed out and
141
141
  * records revoked without hitting the token endpoint.
142
142
  * - `home` is the resolved per-host provider home for the account; the gate
143
- * compares it to the home being refreshed with `path.resolve` so symlinks
144
- * do not slip through. `null` is accepted only when the row is not live.
143
+ * compares it to the home being refreshed with `canonicalHomePath` (realpath,
144
+ * lower case on darwin) so symlinks and case differences do not slip through;
145
+ * a home that cannot be resolved blocks. `null` is accepted only when the row
146
+ * is not live.
145
147
  */
146
148
  export interface LiveLane {
147
149
  home: string | null;
@@ -164,8 +166,13 @@ export type ListLiveLanes = () => Promise<readonly LiveLane[]>;
164
166
  */
165
167
  export interface ClaudeProcess {
166
168
  pid: number;
169
+ /**
170
+ * `HOME` is set when the probe read it. Claude Code resolves its default config dir as
171
+ * HOME/.claude, so a process with HOME set and no CLAUDE_CONFIG_DIR holds that dir.
172
+ */
167
173
  env: {
168
174
  CLAUDE_CONFIG_DIR?: string;
175
+ HOME?: string;
169
176
  };
170
177
  probe: "env" | "command-only";
171
178
  }
@@ -245,7 +252,7 @@ export declare function mapClaudeOAuthUsage(body: unknown, options: MapOptions):
245
252
  readings: ReadingInput[];
246
253
  failures: ParserFailureEvent[];
247
254
  };
248
- export declare function collectClaudeOAuth(options?: CollectClaudeOAuthOptions): Promise<ClaudeOAuthCollectResult>;
255
+ export declare function collectClaudeOAuth(inputOptions?: CollectClaudeOAuthOptions): Promise<ClaudeOAuthCollectResult>;
249
256
  /**
250
257
  * Ledger account id → hashed profile account id. Rows with no captured
251
258
  * identity are omitted. The raw uuid is never returned.
@@ -349,11 +356,16 @@ export declare function resolveHqInvocation(argv?: readonly string[], execPath?:
349
356
  command: string;
350
357
  args: string[];
351
358
  };
352
- /** Exported for tests; `invocation` defaults to {@link resolveHqInvocation}. */
359
+ /**
360
+ * Run `hq ... --json` and parse stdout. Async on purpose: the collector runs inside the mesh
361
+ * daemon, and a synchronous read that takes 15-30 s on a loaded machine froze the daemon's
362
+ * event loop (MQTT keepalive, owner heartbeat) for the whole read.
363
+ * Exported for tests; `invocation` defaults to {@link resolveHqInvocation}.
364
+ */
353
365
  export declare function execHqJson(args: readonly string[], invocation?: {
354
366
  command: string;
355
367
  args: string[];
356
- }, env?: NodeJS.ProcessEnv): unknown;
368
+ }, env?: NodeJS.ProcessEnv): Promise<unknown>;
357
369
  /**
358
370
  * Parse `hq lanes list --json` payload into typed rows.
359
371
  * Exported for tests; fail-closed semantics live in defaultListLiveLanes.
@@ -369,12 +381,48 @@ export declare function parseAccountsListPayload(payload: unknown): readonly Raw
369
381
  * Exported for tests.
370
382
  */
371
383
  export declare function buildLiveLanesFromPayloads(lanes: readonly RawLaneRow[], accounts: readonly RawAccountRow[]): readonly LiveLane[];
384
+ /**
385
+ * Share one lane read across every home in a collect. Without this each expired home ran its
386
+ * own pair of `hq lanes ... --json` reads, so one slow machine turned into N timeouts per
387
+ * collect. A failure is shared too: the next home fails closed without waiting again.
388
+ * Exported for tests.
389
+ */
390
+ export declare function memoizeListLiveLanes(list: ListLiveLanes): ListLiveLanes;
372
391
  /**
373
392
  * Parse `ps -axo pid=,command=` into rows that mention the `claude` executable.
374
393
  * Exported for tests. Returned rows carry `probe: "command-only"` so the gate
375
394
  * knows it cannot read per-process CLAUDE_CONFIG_DIR on darwin.
376
395
  */
377
396
  export declare function parsePsClaudeOutput(stdout: string): readonly ClaudeProcess[];
397
+ /** Pid to full command line, from `ps -ww -o pid=,command= -p <pids>`. Exported for tests. */
398
+ export declare function parsePsCommandLines(stdout: string): ReadonlyMap<number, string>;
399
+ export interface PsEnvProbeOptions {
400
+ isDirectory?: (dir: string) => boolean;
401
+ /** False only when the pid is known to be gone (ESRCH). Unknown counts as alive. */
402
+ isAlive?: (pid: number) => boolean;
403
+ }
404
+ /**
405
+ * Upgrade darwin command-only rows with each process's environment. `ps -wwE` prints a
406
+ * same-user process's command line followed by its environment, unquoted and space separated.
407
+ * Only the text after the exact command line (read separately with `ps -ww`) is treated as the
408
+ * environment, so a prompt argument that mentions CLAUDE_CONFIG_DIR= or HOME= is never read as
409
+ * env. A row becomes `probe: "env"` only when:
410
+ * - the `-E` line starts with the command line plus a space, and the rest starts with NAME=;
411
+ * - CLAUDE_CONFIG_DIR= appears at most once in that rest, and its value is absolute and
412
+ * names an existing directory (the value runs to the next ` NAME=` or the end of line).
413
+ * Anything else stays command-only, which blocks every home. A pid missing from the output
414
+ * is dropped only when it is known to have exited; otherwise it stays command-only.
415
+ * Exported for tests.
416
+ */
417
+ export declare function parsePsEnvOutput(envStdout: string, commandLines: ReadonlyMap<number, string>, rows: readonly ClaudeProcess[], options?: PsEnvProbeOptions): readonly ClaudeProcess[];
418
+ /** Default config dir of an env-probed process with CLAUDE_CONFIG_DIR unset. Exported for tests. */
419
+ export declare function processDefaultConfigDir(proc: ClaudeProcess): string;
420
+ /**
421
+ * Canonical form of a home directory for comparison: symlinks resolved by the OS, and lower
422
+ * case on darwin, where APFS is case-insensitive by default. Null when the path cannot be
423
+ * resolved. Exported for tests.
424
+ */
425
+ export declare function canonicalHomePath(dir: string, platform?: NodeJS.Platform): string | null;
378
426
  export interface ClaudeTokenRefreshResult {
379
427
  outcome: "refreshed" | "revoked" | "error" | "skipped-race" | "skipped-live" | "skipped-live-check-failed" | "skipped-process" | "skipped-process-check-failed" | "skipped-backoff" | "skipped-revoked";
380
428
  newAccessToken?: string;