@indigoai-us/hq-cli 5.345.4 → 5.345.6

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,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.6] — 2026-10-05
6
+ - `hq mesh context default set <slug> --count-usage` now works for people signed in with a normal HQ account. It previously refused with "the signed-in person could not be determined". An opt-in saved by 5.344.0 is ignored; run the command again to opt in.
7
+ - `hq daemon sync status` now identifies the executing runner and derives its hq-cloud version from the owner status or runner package when needed.
8
+
5
9
  ## [5.345.4] — 2026-10-05
6
10
  - Run a pinned or newer managed hq-cloud sync runner ahead of the staged sync bundle, so daemon sync no longer stays on the hq-cloud version bundled with the CLI after an update or pin (hq-cli#1547).
7
11
  - Avoid dispatching the global hook when the working directory is inside an HQ root.
@@ -16,7 +16,7 @@
16
16
  import * as fs from "node:fs";
17
17
  import * as os from "node:os";
18
18
  import * as path from "node:path";
19
- import { major as semverMajor, valid as validSemver } from "semver";
19
+ import { compare as semverCompare, major as semverMajor, valid as validSemver } from "semver";
20
20
  import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
21
21
  import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
22
22
  import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
@@ -24,13 +24,14 @@ import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/d
24
24
  import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
25
25
  import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
26
26
  import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
27
- import { bundledHqCloud } from "../lib/daemon/hq-cloud.js";
27
+ import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy } from "../lib/daemon/hq-cloud.js";
28
28
  import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
29
29
  import { daemonPaths } from "../lib/daemon/paths.js";
30
30
  import { processCommandLine } from "../lib/daemon/lock.js";
31
31
  import { writeControlRequest } from "../lib/daemon/requests.js";
32
32
  import { runDaemonCall } from "../lib/daemon/command-call.js";
33
- import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
33
+ import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, syncRunnerPathFromCommandLine, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
34
+ import { stagedSyncRunnerVersion, syncRunnerBundleStageRoot } from "../lib/daemon/sync-runner-bundle.js";
34
35
  import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
35
36
  import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
36
37
  import { hqdRequest, hqdSocketPath, HqdClientError } from "../lib/daemon/protocol.js";
@@ -440,6 +441,38 @@ function daemonSyncModeClient(d) {
440
441
  return new VaultClient(buildVaultConfig(token));
441
442
  })();
442
443
  }
444
+ function pathIsWithin(candidate, root, platform) {
445
+ const pathApi = platform === "win32" ? path.win32 : path;
446
+ const relative = pathApi.relative(pathApi.resolve(root), pathApi.resolve(candidate));
447
+ return relative === "" || (relative !== ".." && !relative.startsWith(".." + pathApi.sep) && !pathApi.isAbsolute(relative));
448
+ }
449
+ function syncRunnerSource(runnerPath, daemonRoot, platform) {
450
+ if (!runnerPath)
451
+ return "unknown";
452
+ if (pathIsWithin(runnerPath, syncRunnerBundleStageRoot(daemonRoot), platform))
453
+ return "staged-sync";
454
+ if (pathIsWithin(runnerPath, managedHqCloudRoot(daemonRoot), platform))
455
+ return "managed-full-package";
456
+ const bundled = bundledHqCloud();
457
+ if (bundled && pathIsWithin(runnerPath, bundled.root, platform))
458
+ return "bundled-full-package";
459
+ return "unknown";
460
+ }
461
+ function syncVersionMismatch(status) {
462
+ const running = status.hqCloudVersion;
463
+ if (!running)
464
+ return null;
465
+ if (status.pinnedHqCloudVersion && running !== status.pinnedHqCloudVersion) {
466
+ return "Sync runs hq-cloud " + running + ", while the configured pin expects " + status.pinnedHqCloudVersion + ". Run hq daemon restart sync after updating hq-cli to apply it.";
467
+ }
468
+ if (status.managedHqCloudVersion
469
+ && validSemver(status.managedHqCloudVersion)
470
+ && validSemver(running)
471
+ && semverCompare(status.managedHqCloudVersion, running) > 0) {
472
+ return "Sync runs hq-cloud " + running + ", while managed hq-cloud " + status.managedHqCloudVersion + " is newer. Run hq daemon restart sync after updating hq-cli to apply it.";
473
+ }
474
+ return null;
475
+ }
443
476
  function daemonSyncStatus(d) {
444
477
  const host = currentHost(d);
445
478
  const { paths } = host;
@@ -461,22 +494,64 @@ function daemonSyncStatus(d) {
461
494
  const ownerCommandLine = ownerStatusCandidate
462
495
  ? processCommandLine(ownerStatusCandidate.pid, { platform: d.platform, run: d.run })
463
496
  : null;
464
- const ownerStatus = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
465
- ? ownerStatusCandidate
497
+ const ownerRunnerPath = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
498
+ ? syncRunnerPathFromCommandLine(ownerCommandLine)
466
499
  : null;
467
- return {
500
+ const ownerStatus = ownerRunnerPath ? ownerStatusCandidate : null;
501
+ const unitCommandLine = ownerRunnerPath || !syncUnit?.pid
502
+ ? null
503
+ : processCommandLine(syncUnit.pid, { platform: d.platform, run: d.run });
504
+ const unitRunnerPath = unitCommandLine && isSyncRunnerCommandLine(unitCommandLine)
505
+ ? syncRunnerPathFromCommandLine(unitCommandLine)
506
+ : null;
507
+ const runnerPath = ownerRunnerPath ?? unitRunnerPath;
508
+ const runnerSource = syncRunnerSource(runnerPath, paths.root, d.platform);
509
+ const managedCopy = managedHqCloud(paths.root);
510
+ const managedVersion = managedCopy?.version ?? null;
511
+ let hqCloudVersion = ownerStatus?.version ?? null;
512
+ let hqCloudVersionSource = hqCloudVersion ? "owner-status" : "unknown";
513
+ if (!hqCloudVersion && runnerPath) {
514
+ try {
515
+ const copy = runnerSource === "staged-sync"
516
+ ? { root: "", version: stagedSyncRunnerVersion(runnerPath) }
517
+ : runnerSource === "managed-full-package"
518
+ ? readHqCloudCopy(managedHqCloudRoot(paths.root))
519
+ : runnerSource === "bundled-full-package"
520
+ ? bundledHqCloud()
521
+ : null;
522
+ if (copy) {
523
+ hqCloudVersion = copy.version;
524
+ hqCloudVersionSource = "runner-path";
525
+ }
526
+ }
527
+ catch {
528
+ d.err("Could not read sync runner version metadata; treating it as unavailable.");
529
+ }
530
+ }
531
+ // Compare the executing version, not the installed managed copy: the copy can be updated on disk while an older runner still runs.
532
+ const runnerMatchesPin = runnerSource === "managed-full-package" && config.hqCloudVersion && hqCloudVersion
533
+ ? hqCloudVersion === config.hqCloudVersion
534
+ : null;
535
+ const status = {
468
536
  running: syncUnit?.status === "running",
469
537
  paused: config.syncPaused === true,
470
538
  syncOwner: effectiveSyncOwner(config, host.detected.type),
471
539
  owner: ownerStatus?.owner ?? (syncUnit?.status === "running" ? "hq-daemon" : null),
472
540
  runnerPid: ownerStatus?.pid ?? syncUnit?.pid ?? null,
473
- hqCloudVersion: ownerStatus?.version ?? null,
541
+ hqCloudVersion,
542
+ hqCloudVersionSource,
543
+ runnerMatchesPin,
544
+ runnerPath,
545
+ runnerSource,
546
+ managedHqCloudVersion: managedVersion,
547
+ pinnedHqCloudVersion: config.hqCloudVersion ?? null,
474
548
  lastHeartbeat: syncUnit?.lastHeartbeatAt ?? ownerStatus?.heartbeatAt ?? null,
475
549
  lastPassResult: syncUnit?.lastPassResult ?? null,
476
550
  unitStatus: syncUnit?.status ?? "not-running",
477
551
  reason: syncUnit?.reason ?? null,
478
552
  logPath: paths.unitLogPath("sync"),
479
553
  };
554
+ return status;
480
555
  }
481
556
  async function runDaemonSyncStatus(d, opts) {
482
557
  const status = daemonSyncStatus(d);
@@ -487,6 +562,15 @@ async function runDaemonSyncStatus(d, opts) {
487
562
  d.out(`Sync ${status.running ? "running" : status.unitStatus}${status.paused ? " (paused)" : ""}.`);
488
563
  d.out(`Configured sync owner: ${status.syncOwner}.`);
489
564
  d.out(`Owner: ${status.owner ?? "none"}; runner pid: ${status.runnerPid ?? "none"}; hq-cloud: ${status.hqCloudVersion ?? "unknown"}.`);
565
+ d.out(`Sync runner: ${status.runnerPath ?? "unknown"} (${status.runnerSource}); hq-cloud ${status.hqCloudVersion ?? "unknown"}.`);
566
+ d.out("Managed hq-cloud: " + (status.managedHqCloudVersion ?? "unknown") + "; pinned hq-cloud: " + (status.pinnedHqCloudVersion ?? "none") + ".");
567
+ if (status.runnerSource === "managed-full-package") {
568
+ const pinMatch = status.runnerMatchesPin === null ? "unknown" : status.runnerMatchesPin ? "yes" : "no";
569
+ d.out("Managed runner matches pin: " + pinMatch + ".");
570
+ }
571
+ const versionMismatch = syncVersionMismatch(status);
572
+ if (versionMismatch)
573
+ d.out(versionMismatch);
490
574
  if (status.reason)
491
575
  d.out(`Reason: ${status.reason}`);
492
576
  d.out(`Log: ${status.logPath}`);
@@ -9,7 +9,7 @@ import { cognitoConfigForLoginProvider } from '../utils/login-provider.js';
9
9
  import * as readline from "node:readline/promises";
10
10
  import { listActiveMembershipCompanies } from "../lib/mesh/api.js";
11
11
  import { retainUsageAttributionForPerson, setDefaultCompany } from "../lib/work-context/config.js";
12
- import { personUidFromTokens } from "../lib/work-context/person-identity.js";
12
+ import { cognitoSubFromTokens } from "../lib/work-context/person-identity.js";
13
13
  import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
14
14
  import { workContextRoot } from "../lib/work-context/paths.js";
15
15
  function peekIdToken(idToken) {
@@ -66,7 +66,7 @@ export function registerLoginCommand(program) {
66
66
  try {
67
67
  // The usage-attribution opt-in belongs to the person who gave it. A
68
68
  // different (or unknown) account signing in on this OS user drops it.
69
- retainUsageAttributionForPerson(personUidFromTokens(loadCachedTokens()), {
69
+ retainUsageAttributionForPerson(cognitoSubFromTokens(loadCachedTokens()), {
70
70
  root: workContextRoot(),
71
71
  });
72
72
  }
@@ -20,7 +20,7 @@ import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.j
20
20
  import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
21
21
  import { clearDefaultCompany, effectiveUsageAttribution, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, setUsageAttribution, } from "../lib/work-context/config.js";
22
22
  import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
23
- import { personUidFromTokens } from "../lib/work-context/person-identity.js";
23
+ import { cognitoSubFromTokens } from "../lib/work-context/person-identity.js";
24
24
  import { DefaultCompanyUnavailableError, WorkContextError, } from "../lib/work-context/errors.js";
25
25
  import { isValidSessionId } from "../lib/mesh/live/session-identity.js";
26
26
  import { flushSessionEvents } from "../lib/mesh/live/flush.js";
@@ -1212,17 +1212,17 @@ export async function askYesNo(question, io = {}) {
1212
1212
  rl.close();
1213
1213
  }
1214
1214
  }
1215
- /** The signed-in person's prs_ uid from the cached Cognito session, or null. */
1216
- function currentPersonUid() {
1215
+ /** The signed-in person's Cognito sub from the cached session, or null. */
1216
+ function currentCognitoSub() {
1217
1217
  try {
1218
- return personUidFromTokens(loadCachedTokens());
1218
+ return cognitoSubFromTokens(loadCachedTokens());
1219
1219
  }
1220
1220
  catch {
1221
1221
  return null;
1222
1222
  }
1223
1223
  }
1224
- function usageAttributionJson(cfg, personUid) {
1225
- const effective = effectiveUsageAttribution(cfg, personUid);
1224
+ function usageAttributionJson(cfg, cognitoSub) {
1225
+ const effective = effectiveUsageAttribution(cfg, cognitoSub);
1226
1226
  return { countUsage: effective !== null, usageAttribution: effective };
1227
1227
  }
1228
1228
  async function runContextDefaultGet(opts) {
@@ -1244,7 +1244,7 @@ async function runContextDefaultGet(opts) {
1244
1244
  needsChoice: !current && !disabled && memberships.length > 1,
1245
1245
  memberships,
1246
1246
  repairHeldWithDefault: cfg.repairHeldWithDefault === true,
1247
- ...usageAttributionJson(cfg, currentPersonUid()),
1247
+ ...usageAttributionJson(cfg, currentCognitoSub()),
1248
1248
  }, null, 2));
1249
1249
  return;
1250
1250
  }
@@ -1257,7 +1257,7 @@ async function runContextDefaultGet(opts) {
1257
1257
  return;
1258
1258
  }
1259
1259
  console.log(`Default company: ${current.slug}${current.uid ? ` (${current.uid})` : ""} enabled=${current.enabled} source=${source}`);
1260
- console.log(effectiveUsageAttribution(cfg, currentPersonUid())
1260
+ console.log(effectiveUsageAttribution(cfg, currentCognitoSub())
1261
1261
  ? usageCountedLine(current.slug)
1262
1262
  : usageNotCountedLine(current.slug));
1263
1263
  }
@@ -1269,8 +1269,8 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
1269
1269
  : opts.countUsage === false
1270
1270
  ? "opt-out"
1271
1271
  : "keep-if-same";
1272
- const personUid = currentPersonUid();
1273
- const priorOptIn = effectiveUsageAttribution(readDeviceConfig({ root }), personUid);
1272
+ const cognitoSub = currentCognitoSub();
1273
+ const priorOptIn = effectiveUsageAttribution(readDeviceConfig({ root }), cognitoSub);
1274
1274
  let token;
1275
1275
  try {
1276
1276
  token = await requireToken();
@@ -1311,7 +1311,7 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
1311
1311
  root,
1312
1312
  migrationCapability,
1313
1313
  usageAttribution: usageChoice,
1314
- personUid,
1314
+ cognitoSub,
1315
1315
  validateMembership: async (candidate) => {
1316
1316
  if (!token) {
1317
1317
  throw new DefaultCompanyUnavailableError(`Cannot verify membership for company "${candidate}" (no Cognito session; offline or not logged in)`);
@@ -1340,7 +1340,7 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
1340
1340
  ok: true,
1341
1341
  config: configured,
1342
1342
  migrationWarning,
1343
- ...usageAttributionJson(configured, personUid),
1343
+ ...usageAttributionJson(configured, cognitoSub),
1344
1344
  }, null, 2));
1345
1345
  return;
1346
1346
  }
@@ -1352,8 +1352,8 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
1352
1352
  (() => isInteractiveTerminal()))();
1353
1353
  if (usageChoice === "keep-if-same" &&
1354
1354
  interactive &&
1355
- personUid &&
1356
- !effectiveUsageAttribution(configured, personUid) &&
1355
+ cognitoSub &&
1356
+ !effectiveUsageAttribution(configured, cognitoSub) &&
1357
1357
  stored?.uid?.startsWith("cmp_")) {
1358
1358
  console.error(usageOptInExplainer(company));
1359
1359
  let yes = false;
@@ -1364,9 +1364,9 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
1364
1364
  yes = false;
1365
1365
  }
1366
1366
  if (yes)
1367
- configured = setUsageAttribution({ companyUid: stored.uid, personUid }, { root });
1367
+ configured = setUsageAttribution({ companyUid: stored.uid, cognitoSub }, { root });
1368
1368
  }
1369
- console.log(effectiveUsageAttribution(configured, personUid)
1369
+ console.log(effectiveUsageAttribution(configured, cognitoSub)
1370
1370
  ? usageCountedLine(company)
1371
1371
  : usageNotCountedLine(company));
1372
1372
  if (migrationWarning)
@@ -1503,7 +1503,7 @@ async function runContextDefaultClear(opts) {
1503
1503
  const hadOptIn = readDeviceConfig({ root }).usageAttribution !== undefined;
1504
1504
  const cfg = clearDefaultCompany({ root });
1505
1505
  if (opts.json) {
1506
- console.log(JSON.stringify({ ok: true, config: cfg, ...usageAttributionJson(cfg, currentPersonUid()) }, null, 2));
1506
+ console.log(JSON.stringify({ ok: true, config: cfg, ...usageAttributionJson(cfg, currentCognitoSub()) }, null, 2));
1507
1507
  return;
1508
1508
  }
1509
1509
  console.log("Default company cleared.");
@@ -60,6 +60,9 @@ export declare function hqCloudInstallCommand(prefix: string, version: string, p
60
60
  args: string[];
61
61
  shell: boolean;
62
62
  };
63
+ export declare function readHqCloudCopy(root: string): HqCloudCopy | null;
64
+ /** The hq-cloud copy installed in the daemon-owned managed package root. */
65
+ export declare function managedHqCloud(daemonRoot: string): HqCloudCopy | null;
63
66
  /** The copy bundled with this CLI. */
64
67
  export declare function bundledHqCloud(): HqCloudCopy | null;
65
68
  /** The hq-cloud the daemon should run right now. */
@@ -118,7 +118,7 @@ export function hqCloudInstallCommand(prefix, version, platform = process.platfo
118
118
  const plan = buildSpawnPlan("npm", ["install", "--prefix", prefix, "--no-audit", "--no-fund", "--omit=dev", `${HQ_CLOUD_PACKAGE}@${version}`], platform);
119
119
  return { file: plan.cmd, args: plan.args, shell: plan.shell };
120
120
  }
121
- function readHqCloudCopy(root) {
121
+ export function readHqCloudCopy(root) {
122
122
  try {
123
123
  const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
124
124
  return typeof pkg.version === "string" ? { root, version: pkg.version } : null;
@@ -127,6 +127,10 @@ function readHqCloudCopy(root) {
127
127
  return null;
128
128
  }
129
129
  }
130
+ /** The hq-cloud copy installed in the daemon-owned managed package root. */
131
+ export function managedHqCloud(daemonRoot) {
132
+ return readHqCloudCopy(managedHqCloudRoot(daemonRoot));
133
+ }
130
134
  /** The copy bundled with this CLI. */
131
135
  export function bundledHqCloud() {
132
136
  try {
@@ -140,12 +144,12 @@ export function bundledHqCloud() {
140
144
  /** The hq-cloud the daemon should run right now. */
141
145
  export function currentHqCloud(daemonRoot) {
142
146
  const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
143
- return pickHqCloud(bundledHqCloud(), readHqCloudCopy(managedHqCloudRoot(daemonRoot)), (copy) => hqCloudRunners(copy.root).syncRunner !== null, pinnedVersion);
147
+ return pickHqCloud(bundledHqCloud(), managedHqCloud(daemonRoot), (copy) => hqCloudRunners(copy.root).syncRunner !== null, pinnedVersion);
144
148
  }
145
149
  /** After the CLI package lease is released, only daemon-owned package files are eligible. */
146
150
  export function currentHqCloudAfterLeaseRelease(daemonRoot, expectedVersion) {
147
151
  const config = readDaemonConfig(path.join(daemonRoot, "config.json"));
148
- const managed = readHqCloudCopy(managedHqCloudRoot(daemonRoot));
152
+ const managed = managedHqCloud(daemonRoot);
149
153
  if (!managed || !hqCloudRunners(managed.root).syncRunner)
150
154
  return null;
151
155
  if (config.hqCloudVersion && config.hqCloudVersion !== managed.version)
@@ -4,6 +4,8 @@ export declare const SYNC_RUNNER_BUNDLE_RUNNER_FILE = "bin/sync-runner.js";
4
4
  export declare const SYNC_RUNNER_BUNDLE_METADATA_FILE = "package.json";
5
5
  export declare const SYNC_RUNNER_BUNDLE_DIRECTORY = "hq-cloud-sync-runner";
6
6
  export declare const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
7
+ /** Root directory where validated sync-runner bundles are selected. */
8
+ export declare function syncRunnerBundleStageRoot(daemonRoot: string): string;
7
9
  /** package.json contract that the hq-cli build step emits beside the bundled bin directory. */
8
10
  export interface SyncRunnerBundleMetadata {
9
11
  name: "hq-cli-daemon-sync-runner";
@@ -32,6 +34,8 @@ export interface StageSyncRunnerBundleOptions {
32
34
  copyFile?: typeof fs.copyFileSync;
33
35
  renameDirectory?: (from: string, to: string) => void;
34
36
  }
37
+ /** Read the validated metadata version for a runner inside its staged bundle. */
38
+ export declare function stagedSyncRunnerVersion(runnerPath: string): string;
35
39
  /**
36
40
  * Stage one validated runner bundle into an immutable daemon-owned directory.
37
41
  * Only a completed, fsynced directory is returned; temp siblings are ignored.
@@ -9,6 +9,10 @@ export const SYNC_RUNNER_BUNDLE_RUNNER_FILE = "bin/sync-runner.js";
9
9
  export const SYNC_RUNNER_BUNDLE_METADATA_FILE = "package.json";
10
10
  export const SYNC_RUNNER_BUNDLE_DIRECTORY = "hq-cloud-sync-runner";
11
11
  export const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
12
+ /** Root directory where validated sync-runner bundles are selected. */
13
+ export function syncRunnerBundleStageRoot(daemonRoot) {
14
+ return path.join(daemonRoot, "bundles", "sync-runner");
15
+ }
12
16
  export class SyncRunnerBundleStageError extends Error {
13
17
  code;
14
18
  constructor(code, message, options) {
@@ -55,6 +59,11 @@ function readMetadata(file) {
55
59
  }
56
60
  return metadata;
57
61
  }
62
+ /** Read the validated metadata version for a runner inside its staged bundle. */
63
+ export function stagedSyncRunnerVersion(runnerPath) {
64
+ const bundleDirectory = path.dirname(path.dirname(runnerPath));
65
+ return readMetadata(path.join(bundleDirectory, SYNC_RUNNER_BUNDLE_METADATA_FILE)).version;
66
+ }
58
67
  function hqCloudRange() {
59
68
  const cliPackagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../package.json");
60
69
  try {
@@ -138,7 +147,7 @@ export function stageSyncRunnerBundle(options) {
138
147
  if (sha256(sourceRunner) !== metadata.bundleSha256) {
139
148
  fail("HASH_MISMATCH", "The sync-runner bundle sha256 does not match its sidecar");
140
149
  }
141
- const stageRoot = path.join(options.daemonRoot, "bundles", "sync-runner");
150
+ const stageRoot = syncRunnerBundleStageRoot(options.daemonRoot);
142
151
  const selectedDirectory = path.join(stageRoot, stagedLeaf(metadata));
143
152
  const existing = readStaged(selectedDirectory, metadata, range);
144
153
  if (existing)
@@ -44,6 +44,8 @@ export declare function watchRunnerOwnerStatusPath(hqRoot: string, home?: string
44
44
  export declare function readWatchRunnerOwnerStatus(statusPath: string): WatchRunnerOwnerStatus | null;
45
45
  /** Match a live process command line to the hq-cloud watch runner executable. */
46
46
  export declare function isSyncRunnerCommandLine(commandLine: string): boolean;
47
+ /** Return the script path from a command line already verified as a sync runner. */
48
+ export declare function syncRunnerPathFromCommandLine(commandLine: string): string | null;
47
49
  /** Return a clear wait reason when an unleased runner already targets this HQ root. */
48
50
  export declare function syncRunnerUnleasedBlockReason(hqRoot: string, processes: readonly SyncRunnerProcess[], ownerPid: number | null, platform?: NodeJS.Platform, daemonRunnerPids?: ReadonlySet<number>): string | undefined;
49
51
  /** Read complete process command lines so root matching can include trailing arguments. */
@@ -59,6 +59,14 @@ export function readWatchRunnerOwnerStatus(statusPath) {
59
59
  export function isSyncRunnerCommandLine(commandLine) {
60
60
  return SYNC_RUNNER_COMMAND.test(commandLine);
61
61
  }
62
+ /** Return the script path from a command line already verified as a sync runner. */
63
+ export function syncRunnerPathFromCommandLine(commandLine) {
64
+ if (!isSyncRunnerCommandLine(commandLine))
65
+ return null;
66
+ const args = commandLineArgs(commandLine);
67
+ return args.find((arg) => (path.isAbsolute(arg) || path.win32.isAbsolute(arg))
68
+ && /(?:^|[\\/])(?:sync-runner\.js|hq-sync-runner(?:\.(?:cmd|exe))?)$/i.test(arg)) ?? null;
69
+ }
62
70
  function commandLineArgs(commandLine) {
63
71
  const args = [];
64
72
  let current = "";
@@ -56,15 +56,16 @@ export interface UsageAttributionOptIn {
56
56
  mode: "default-company";
57
57
  companyUid: string;
58
58
  /**
59
- * The signed-in person who consented: the `custom:entityUid` claim (prs_...)
60
- * of the cached Cognito person session. Consent never carries to another
61
- * account on the same OS user.
59
+ * The signed-in person who consented: the Cognito `sub` claim of the cached
60
+ * person session. Consent never carries to another account on the same OS
61
+ * user. Older records that carry only `personUid` are treated as not opted
62
+ * in (fail closed).
62
63
  */
63
- personUid: string;
64
+ cognitoSub: string;
64
65
  optedInAt: string;
65
66
  }
66
- /** A person principal uid, as carried in the `custom:entityUid` token claim. */
67
- export declare function isPersonUid(value: unknown): value is string;
67
+ /** A Cognito `sub` claim: any non-empty string (after trimming). */
68
+ export declare function isCognitoSub(value: unknown): value is string;
68
69
  /** How `setDefaultCompany` treats the usage-attribution opt-in. */
69
70
  export type UsageAttributionChoice =
70
71
  /** Write an opt-in for the newly set company (requires its uid). */
@@ -113,10 +114,10 @@ export interface DeviceConfigDeps {
113
114
  */
114
115
  usageAttribution?: UsageAttributionChoice;
115
116
  /**
116
- * The signed-in person (prs_...). Required to write an opt-in; an existing
117
+ * The signed-in person (Cognito `sub`). Required to write an opt-in; an existing
117
118
  * opt-in is kept only when it was given by this person.
118
119
  */
119
- personUid?: string | null;
120
+ cognitoSub?: string | null;
120
121
  }
121
122
  /** Acquire an interprocess config lock; exported for direct lock regression tests. */
122
123
  export declare function acquireDeviceConfigLock(root: string, options?: {
@@ -130,7 +131,7 @@ export declare function sanitizeUsageAttribution(raw: unknown): UsageAttribution
130
131
  * default company, and given by the signed-in person. Mirrors the hq-cloud
131
132
  * reader. No signed-in person means no opt-in.
132
133
  */
133
- export declare function effectiveUsageAttribution(cfg: Pick<WorkContextDeviceConfig, "defaultCompany" | "usageAttribution">, personUid: string | null | undefined): UsageAttributionOptIn | null;
134
+ export declare function effectiveUsageAttribution(cfg: Pick<WorkContextDeviceConfig, "defaultCompany" | "usageAttribution">, cognitoSub: string | null | undefined): UsageAttributionOptIn | null;
134
135
  /** Injection points for assertSafeConfigPath (tests only). */
135
136
  export interface SafeConfigPathIo {
136
137
  platform?: NodeJS.Platform;
@@ -162,14 +163,14 @@ export declare function setDefaultCompany(slug: string, deps: DeviceConfigDeps):
162
163
  */
163
164
  export declare function setUsageAttribution(optIn: {
164
165
  companyUid: string;
165
- personUid: string;
166
+ cognitoSub: string;
166
167
  } | null, deps: Pick<DeviceConfigDeps, "root" | "now">): WorkContextDeviceConfig;
167
168
  /**
168
- * Drop the usage-attribution opt-in unless it was given by `personUid`. Called
169
+ * Drop the usage-attribution opt-in unless it was given by `cognitoSub`. Called
169
170
  * on login (identity may have changed) and logout (null: always drop). A
170
171
  * config with no opt-in, or one already matching, is left untouched.
171
172
  */
172
- export declare function retainUsageAttributionForPerson(personUid: string | null | undefined, deps: Pick<DeviceConfigDeps, "root" | "now">): boolean;
173
+ export declare function retainUsageAttributionForPerson(cognitoSub: string | null | undefined, deps: Pick<DeviceConfigDeps, "root" | "now">): boolean;
173
174
  export declare function clearDefaultCompany(deps: DeviceConfigDeps): WorkContextDeviceConfig;
174
175
  /**
175
176
  * Read the persisted company mapping for a repo identity key (gap 4).
@@ -9,9 +9,9 @@ import { DefaultCompanyUnavailableError, UnsafeConfigPathError, WorkContextError
9
9
  import { workContextConfigPath } from "./paths.js";
10
10
  import { assertWindowsOwnerOnlyAcl } from "./windows-acl.js";
11
11
  const DEVICE_CONFIG_SCHEMA_VERSION = 1;
12
- /** A person principal uid, as carried in the `custom:entityUid` token claim. */
13
- export function isPersonUid(value) {
14
- return typeof value === "string" && /^prs_[A-Za-z0-9]+$/.test(value);
12
+ /** A Cognito `sub` claim: any non-empty string (after trimming). */
13
+ export function isCognitoSub(value) {
14
+ return typeof value === "string" && value.trim().length > 0;
15
15
  }
16
16
  const CONFIG_LOCK_TIMEOUT_MS = 5_000;
17
17
  const CONFIG_LOCK_RETRY_MS = 10;
@@ -158,24 +158,24 @@ export function sanitizeUsageAttribution(raw) {
158
158
  if (v.mode !== "default-company")
159
159
  return undefined;
160
160
  const companyUid = typeof v.companyUid === "string" ? v.companyUid.trim() : "";
161
- const personUid = typeof v.personUid === "string" ? v.personUid.trim() : "";
161
+ const cognitoSub = typeof v.cognitoSub === "string" ? v.cognitoSub.trim() : "";
162
162
  const optedInAt = typeof v.optedInAt === "string" ? v.optedInAt.trim() : "";
163
163
  if (!companyUid.startsWith("cmp_") ||
164
- !isPersonUid(personUid) ||
164
+ !isCognitoSub(cognitoSub) ||
165
165
  !optedInAt ||
166
166
  !Number.isFinite(Date.parse(optedInAt))) {
167
167
  return undefined;
168
168
  }
169
- return { mode: "default-company", companyUid, personUid, optedInAt };
169
+ return { mode: "default-company", companyUid, cognitoSub, optedInAt };
170
170
  }
171
171
  /**
172
172
  * The opt-in that is in effect: present, for the uid of the current enabled
173
173
  * default company, and given by the signed-in person. Mirrors the hq-cloud
174
174
  * reader. No signed-in person means no opt-in.
175
175
  */
176
- export function effectiveUsageAttribution(cfg, personUid) {
176
+ export function effectiveUsageAttribution(cfg, cognitoSub) {
177
177
  const opt = cfg.usageAttribution;
178
- if (!opt || !isPersonUid(personUid) || opt.personUid !== personUid)
178
+ if (!opt || !isCognitoSub(cognitoSub) || opt.cognitoSub !== cognitoSub)
179
179
  return null;
180
180
  const def = cfg.defaultCompany;
181
181
  if (!opt || !def || def.enabled !== true || def.source === "disabled")
@@ -332,27 +332,27 @@ export async function setDefaultCompany(slug, deps) {
332
332
  defaultCompany.uid = resolvedUid;
333
333
  const choice = deps.usageAttribution ?? "keep-if-same";
334
334
  let usageAttribution;
335
- const personUid = isPersonUid(deps.personUid) ? deps.personUid : undefined;
335
+ const cognitoSub = isCognitoSub(deps.cognitoSub) ? deps.cognitoSub : undefined;
336
336
  if (choice === "opt-in") {
337
337
  if (!resolvedUid?.startsWith("cmp_")) {
338
338
  throw new WorkContextError("UsageAttributionUnavailable", `Cannot count usage toward "${trimmed}": its company uid is unknown`);
339
339
  }
340
- if (!personUid) {
340
+ if (!cognitoSub) {
341
341
  throw new WorkContextError("UsageAttributionUnavailable", `Cannot count usage toward "${trimmed}": the signed-in person could not be determined`);
342
342
  }
343
343
  usageAttribution = {
344
344
  mode: "default-company",
345
345
  companyUid: resolvedUid,
346
- personUid,
346
+ cognitoSub,
347
347
  optedInAt: now().toISOString(),
348
348
  };
349
349
  }
350
350
  else if (choice === "keep-if-same") {
351
351
  usageAttribution =
352
352
  resolvedUid &&
353
- personUid &&
353
+ cognitoSub &&
354
354
  prior.usageAttribution?.companyUid === resolvedUid &&
355
- prior.usageAttribution.personUid === personUid
355
+ prior.usageAttribution.cognitoSub === cognitoSub
356
356
  ? prior.usageAttribution
357
357
  : undefined;
358
358
  }
@@ -388,13 +388,13 @@ export function setUsageAttribution(optIn, deps) {
388
388
  def.source !== "chosen" ||
389
389
  !def.uid?.startsWith("cmp_") ||
390
390
  def.uid !== optIn.companyUid ||
391
- !isPersonUid(optIn.personUid)) {
391
+ !isCognitoSub(optIn.cognitoSub)) {
392
392
  throw new WorkContextError("UsageAttributionUnavailable", "Usage can only be counted toward a default company you chose on this device");
393
393
  }
394
394
  usageAttribution = {
395
395
  mode: "default-company",
396
396
  companyUid: def.uid,
397
- personUid: optIn.personUid,
397
+ cognitoSub: optIn.cognitoSub,
398
398
  optedInAt: now().toISOString(),
399
399
  };
400
400
  }
@@ -413,11 +413,11 @@ export function setUsageAttribution(optIn, deps) {
413
413
  });
414
414
  }
415
415
  /**
416
- * Drop the usage-attribution opt-in unless it was given by `personUid`. Called
416
+ * Drop the usage-attribution opt-in unless it was given by `cognitoSub`. Called
417
417
  * on login (identity may have changed) and logout (null: always drop). A
418
418
  * config with no opt-in, or one already matching, is left untouched.
419
419
  */
420
- export function retainUsageAttributionForPerson(personUid, deps) {
420
+ export function retainUsageAttributionForPerson(cognitoSub, deps) {
421
421
  if (!fs.existsSync(workContextConfigPath(deps.root)))
422
422
  return false;
423
423
  return withDeviceConfigLock(deps.root, () => {
@@ -425,7 +425,7 @@ export function retainUsageAttributionForPerson(personUid, deps) {
425
425
  const opt = prior.usageAttribution;
426
426
  if (!opt)
427
427
  return false;
428
- if (isPersonUid(personUid) && opt.personUid === personUid)
428
+ if (isCognitoSub(cognitoSub) && opt.cognitoSub === cognitoSub)
429
429
  return false;
430
430
  const now = deps.now ?? (() => new Date());
431
431
  const { usageAttribution: _dropped, ...rest } = prior;
@@ -2,14 +2,18 @@
2
2
  * Signed-in person identity for the usage-attribution opt-in.
3
3
  *
4
4
  * Source: the cached Cognito person session (`~/.hq/cognito-tokens.json`,
5
- * read through `loadCachedTokens`). The person uid is the `custom:entityUid`
6
- * claim of the ID token (falling back to the access token), and only a
7
- * `prs_` value counts. Agent (`agt_`) or unknown identities return null, so
8
- * no opt-in is written or kept for them. Claims are read without
9
- * verification; this decides only whether local consent belongs to the
10
- * account that is signed in, never authorization.
5
+ * read through `loadCachedTokens`). The identity key is the Cognito `sub`
6
+ * claim of the ID token (falling back to the access token); it must be a
7
+ * non-empty string. Human sessions carry no `custom:entityUid` claim (hq-pro
8
+ * sets it only on agent and outpost machine identities), so it is not used.
9
+ * An agent or outpost session has its own Cognito user and therefore a
10
+ * different `sub`, so it could never match a person's opt-in; a token that
11
+ * names a machine identity (`custom:entityType` agent/outpost, or a
12
+ * non-`prs_` `custom:entityUid`) is refused outright. Claims are read
13
+ * without verification; this decides only whether local consent belongs to
14
+ * the account that is signed in, never authorization.
11
15
  */
12
- export declare function personUidFromTokens(tokens: {
16
+ export declare function cognitoSubFromTokens(tokens: {
13
17
  idToken?: string;
14
18
  accessToken?: string;
15
19
  } | null | undefined): string | null;
@@ -2,14 +2,18 @@
2
2
  * Signed-in person identity for the usage-attribution opt-in.
3
3
  *
4
4
  * Source: the cached Cognito person session (`~/.hq/cognito-tokens.json`,
5
- * read through `loadCachedTokens`). The person uid is the `custom:entityUid`
6
- * claim of the ID token (falling back to the access token), and only a
7
- * `prs_` value counts. Agent (`agt_`) or unknown identities return null, so
8
- * no opt-in is written or kept for them. Claims are read without
9
- * verification; this decides only whether local consent belongs to the
10
- * account that is signed in, never authorization.
5
+ * read through `loadCachedTokens`). The identity key is the Cognito `sub`
6
+ * claim of the ID token (falling back to the access token); it must be a
7
+ * non-empty string. Human sessions carry no `custom:entityUid` claim (hq-pro
8
+ * sets it only on agent and outpost machine identities), so it is not used.
9
+ * An agent or outpost session has its own Cognito user and therefore a
10
+ * different `sub`, so it could never match a person's opt-in; a token that
11
+ * names a machine identity (`custom:entityType` agent/outpost, or a
12
+ * non-`prs_` `custom:entityUid`) is refused outright. Claims are read
13
+ * without verification; this decides only whether local consent belongs to
14
+ * the account that is signed in, never authorization.
11
15
  */
12
- import { isPersonUid } from "./config.js";
16
+ import { isCognitoSub } from "./config.js";
13
17
  function claimsOf(token) {
14
18
  if (typeof token !== "string")
15
19
  return null;
@@ -26,13 +30,22 @@ function claimsOf(token) {
26
30
  return null;
27
31
  }
28
32
  }
29
- export function personUidFromTokens(tokens) {
33
+ export function cognitoSubFromTokens(tokens) {
30
34
  if (!tokens)
31
35
  return null;
32
36
  for (const token of [tokens.idToken, tokens.accessToken]) {
33
- const uid = claimsOf(token)?.["custom:entityUid"];
34
- if (isPersonUid(uid))
35
- return uid;
37
+ const claims = claimsOf(token);
38
+ if (!claims)
39
+ continue;
40
+ const entityType = claims["custom:entityType"];
41
+ const entityUid = claims["custom:entityUid"];
42
+ if (entityType === "agent" || entityType === "outpost")
43
+ return null;
44
+ if (typeof entityUid === "string" && !entityUid.startsWith("prs_"))
45
+ return null;
46
+ const sub = claims.sub;
47
+ if (isCognitoSub(sub))
48
+ return sub.trim();
36
49
  }
37
50
  return null;
38
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.4",
3
+ "version": "5.345.6",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {