@indigoai-us/hq-cli 5.345.198 → 5.345.200

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 (129) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +6 -4
  3. package/dist/command-catalog.generated.d.ts +150 -6
  4. package/dist/command-catalog.generated.js +182 -6
  5. package/dist/commands/bot-events.d.ts +19 -0
  6. package/dist/commands/bot-events.js +46 -0
  7. package/dist/commands/bot-jobs.d.ts +2 -0
  8. package/dist/commands/bot-jobs.js +43 -2
  9. package/dist/commands/bot-runtime.d.ts +35 -0
  10. package/dist/commands/bot-runtime.js +115 -0
  11. package/dist/commands/bot.d.ts +11 -0
  12. package/dist/commands/bot.js +72 -11
  13. package/dist/commands/creators.js +2 -1
  14. package/dist/commands/lanes-report.d.ts +2 -1
  15. package/dist/commands/lanes-report.js +44 -1
  16. package/dist/commands/lanes.d.ts +9 -0
  17. package/dist/commands/lanes.js +95 -9
  18. package/dist/commands/library-remove.js +2 -1
  19. package/dist/commands/library.js +5 -4
  20. package/dist/commands/meetings.js +1 -21
  21. package/dist/commands/mesh.d.ts +0 -7
  22. package/dist/commands/mesh.js +5 -1
  23. package/dist/commands/signals-run.d.ts +2 -0
  24. package/dist/commands/signals-run.js +6 -1
  25. package/dist/commands/sync-held.js +2 -2
  26. package/dist/lanes-drain-hook.js +9 -0
  27. package/dist/lib/bot/api.d.ts +5 -0
  28. package/dist/lib/bot/claude-login.d.ts +31 -0
  29. package/dist/lib/bot/claude-login.js +160 -0
  30. package/dist/lib/bot/events.d.ts +18 -0
  31. package/dist/lib/bot/events.js +160 -0
  32. package/dist/lib/bot/executive-settings.d.ts +10 -0
  33. package/dist/lib/bot/executive-settings.js +43 -0
  34. package/dist/lib/bot/fleet-cron.js +4 -0
  35. package/dist/lib/bot/fleet-download.d.ts +27 -0
  36. package/dist/lib/bot/fleet-download.js +162 -0
  37. package/dist/lib/bot/fleet-local.d.ts +51 -2
  38. package/dist/lib/bot/fleet-local.js +179 -76
  39. package/dist/lib/bot/fleet-python.d.ts +2 -0
  40. package/dist/lib/bot/fleet-python.js +29 -0
  41. package/dist/lib/bot/job-run.js +1 -1
  42. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +8 -0
  43. package/dist/lib/bot/jobs/hot-signal-sync.js +2 -0
  44. package/dist/lib/bot/jobs/meeting-detect.d.ts +28 -0
  45. package/dist/lib/bot/jobs/meeting-detect.js +176 -0
  46. package/dist/lib/bot/jobs/meeting-rider.d.ts +53 -0
  47. package/dist/lib/bot/jobs/meeting-rider.js +284 -0
  48. package/dist/lib/bot/jobs/signal-to-lane.d.ts +32 -0
  49. package/dist/lib/bot/jobs/signal-to-lane.js +169 -0
  50. package/dist/lib/bot/local-jobs.d.ts +15 -2
  51. package/dist/lib/bot/local-jobs.js +24 -7
  52. package/dist/lib/bot/run.d.ts +7 -49
  53. package/dist/lib/bot/run.js +100 -4
  54. package/dist/lib/bot/status.d.ts +21 -0
  55. package/dist/lib/doctor/checks/bots.d.ts +6 -0
  56. package/dist/lib/doctor/checks/bots.js +47 -1
  57. package/dist/lib/hot-channels/adapters/meetings.d.ts +1 -0
  58. package/dist/lib/hot-channels/adapters/meetings.js +3 -1
  59. package/dist/lib/hot-channels/meeting-source-ref.d.ts +3 -0
  60. package/dist/lib/hot-channels/meeting-source-ref.js +7 -0
  61. package/dist/lib/hot-channels/schedule.js +2 -2
  62. package/dist/lib/hot-channels/signals-pass.d.ts +9 -32
  63. package/dist/lib/hot-channels/signals-pass.js +51 -17
  64. package/dist/lib/lanes/admission.d.ts +2 -0
  65. package/dist/lib/lanes/admission.js +4 -0
  66. package/dist/lib/lanes/approval-gate.js +4 -1
  67. package/dist/lib/lanes/links.d.ts +8 -1
  68. package/dist/lib/lanes/links.js +84 -3
  69. package/dist/lib/lanes/mesh-events.js +3 -1
  70. package/dist/lib/lanes/outcome.d.ts +154 -0
  71. package/dist/lib/lanes/outcome.js +151 -0
  72. package/dist/lib/lanes/reconcile.js +1 -0
  73. package/dist/lib/lanes/signal-brief-template.d.ts +7 -0
  74. package/dist/lib/lanes/signal-brief-template.js +40 -0
  75. package/dist/lib/lanes/signal-lane.d.ts +24 -0
  76. package/dist/lib/lanes/signal-lane.js +99 -0
  77. package/dist/lib/lanes/source-report.d.ts +37 -0
  78. package/dist/lib/lanes/source-report.js +66 -0
  79. package/dist/lib/lanes/spawn.d.ts +6 -0
  80. package/dist/lib/lanes/spawn.js +21 -0
  81. package/dist/lib/lanes/store.d.ts +3 -0
  82. package/dist/lib/lanes/store.js +64 -0
  83. package/dist/lib/lanes/story-ledger.d.ts +3 -0
  84. package/dist/lib/lanes/story-ledger.js +1 -0
  85. package/dist/lib/lanes/types.d.ts +137 -0
  86. package/dist/lib/lanes/types.js +17 -2
  87. package/dist/lib/lanes/watcher.js +4 -0
  88. package/dist/lib/live-meetings.d.ts +19 -0
  89. package/dist/lib/live-meetings.js +23 -0
  90. package/dist/lib/mesh/api.js +2 -1
  91. package/dist/lib/mesh/live/daemon/doctor.js +6 -7
  92. package/dist/lib/mesh/live/daemon/run.d.ts +0 -10
  93. package/dist/lib/mesh/live/daemon/run.js +3 -0
  94. package/dist/lib/mesh/live/emit.d.ts +2 -11
  95. package/dist/lib/mesh/live/emit.js +20 -2
  96. package/dist/lib/mesh/live/flush.d.ts +2 -0
  97. package/dist/lib/mesh/live/flush.js +34 -3
  98. package/dist/lib/mesh/live/reporting-privacy.d.ts +7 -0
  99. package/dist/lib/mesh/live/reporting-privacy.js +47 -0
  100. package/dist/lib/mesh/live/spool.d.ts +8 -0
  101. package/dist/lib/mesh/live/spool.js +59 -40
  102. package/dist/lib/plan-limit-nag.d.ts +30 -4
  103. package/dist/lib/plan-limit-nag.js +68 -17
  104. package/dist/lib/signals/engine.d.ts +4 -0
  105. package/dist/lib/signals/engine.js +25 -5
  106. package/dist/lib/signals/incremental.d.ts +42 -0
  107. package/dist/lib/signals/incremental.js +191 -0
  108. package/dist/lib/signals/prior-signals.d.ts +11 -0
  109. package/dist/lib/signals/prior-signals.js +44 -0
  110. package/dist/lib/signals/reply.js +6 -1
  111. package/dist/lib/signals/settings.d.ts +5 -0
  112. package/dist/lib/signals/settings.js +17 -0
  113. package/dist/lib/signals/steps/extract.d.ts +23 -2
  114. package/dist/lib/signals/steps/extract.js +142 -9
  115. package/dist/lib/signals/steps/types.d.ts +1 -0
  116. package/dist/lib/signals/write-guard.js +7 -0
  117. package/dist/lib/work-context/company.d.ts +2 -3
  118. package/dist/lib/work-context/company.js +4 -22
  119. package/dist/secrets-env-action.js +5 -2
  120. package/dist/utils/cognito-callback-port.d.ts +17 -0
  121. package/dist/utils/cognito-callback-port.js +56 -0
  122. package/dist/utils/cognito-config.js +7 -3
  123. package/dist/utils/cognito-session.d.ts +7 -6
  124. package/dist/utils/cognito-session.js +19 -14
  125. package/dist/utils/interactive-caller.d.ts +23 -0
  126. package/dist/utils/interactive-caller.js +80 -0
  127. package/dist/utils/login-browser.js +5 -2
  128. package/dist/utils/resident-package-preload.js +4 -0
  129. package/package.json +7 -7
@@ -12,11 +12,10 @@
12
12
  * 4b. persisted per-repo company map (gap 4 — person "always ask per repo",
13
13
  * remembered by repoIdentityKey; above the git-remote heuristic + device
14
14
  * default, below the identity file so an agent box always wins)
15
- * 5. enabled device default
16
- * 6. exactly one deterministic mapping (cwd under companies/{slug}/ or repo remote)
15
+ * 5. exactly one deterministic mapping (cwd under companies/{slug}/ or repo remote)
17
16
  *
18
17
  * Prompt-text matching is never used.
19
- * Device default + conflicting deterministic evidence → company_conflict.
18
+ * Device defaults never attribute a session; explicit evidence is required.
20
19
  */
21
20
  import type { TrustedExplicitContext } from "./contract.js";
22
21
  import { type SessionStateFile } from "./state.js";
@@ -12,15 +12,14 @@
12
12
  * 4b. persisted per-repo company map (gap 4 — person "always ask per repo",
13
13
  * remembered by repoIdentityKey; above the git-remote heuristic + device
14
14
  * default, below the identity file so an agent box always wins)
15
- * 5. enabled device default
16
- * 6. exactly one deterministic mapping (cwd under companies/{slug}/ or repo remote)
15
+ * 5. exactly one deterministic mapping (cwd under companies/{slug}/ or repo remote)
17
16
  *
18
17
  * Prompt-text matching is never used.
19
- * Device default + conflicting deterministic evidence → company_conflict.
18
+ * Device defaults never attribute a session; explicit evidence is required.
20
19
  */
21
20
  import * as fs from "node:fs";
22
21
  import * as path from "node:path";
23
- import { getDefaultCompany, getRepoCompany } from "./config.js";
22
+ import { getRepoCompany } from "./config.js";
24
23
  import { authoritativeCompanyFromState, readSessionState, } from "./state.js";
25
24
  /** Pointer to the US-017B correction client (never auto-switches). */
26
25
  const COMPANY_CORRECTION_PATH_PREFIX = "hq mesh context correct --session";
@@ -333,8 +332,7 @@ export function resolveCompany(input) {
333
332
  };
334
333
  }
335
334
  }
336
- // 5 + 6. Device default vs deterministic evidence.
337
- const deviceDefault = input.allowDeviceDefault === false ? null : getDefaultCompany({ root: input.root });
335
+ // Membership and device preferences are not session attribution evidence.
338
336
  const deterministic = resolveDeterministicCompany({
339
337
  cwd: input.cwd,
340
338
  hqRoot: input.hqRoot,
@@ -354,22 +352,6 @@ export function resolveCompany(input) {
354
352
  evidenceCompany: deterministicConflict.remote,
355
353
  };
356
354
  }
357
- if (deviceDefault?.enabled && deviceDefault.slug) {
358
- const def = {
359
- slug: deviceDefault.slug,
360
- uid: deviceDefault.uid,
361
- source: "device_default",
362
- };
363
- if (deterministic &&
364
- !sameCompany(def, deterministic)) {
365
- return {
366
- status: "company_conflict",
367
- defaultCompany: def,
368
- evidenceCompany: deterministic,
369
- };
370
- }
371
- return { status: "resolved", company: def };
372
- }
373
355
  if (deterministic) {
374
356
  return { status: "resolved", company: deterministic };
375
357
  }
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import { writeSync } from "node:fs";
3
3
  import * as nodePath from "node:path";
4
4
  import { cognitoCachePrincipalUid } from "./utils/cognito-session.js";
5
+ import { gatedInteractiveLogin } from "./utils/interactive-caller.js";
5
6
  import { DEFAULT_SECRETS_CACHE_TTL_MS, hasCacheEntry, readCache, removeCacheEntry, writeCache, } from "./utils/secrets-cache.js";
6
7
  import { computeSha256 } from "./utils/integrity.js";
7
8
  import { SECRET_NAME_PATTERN } from "./commands/_patterns.js";
@@ -21,10 +22,12 @@ const SECRET_LOAD_TIMEOUT_MS = 30_000;
21
22
  * stdin and stderr must be TTYs, and CI is always non-interactive.
22
23
  */
23
24
  export function secretsCredentialOptions() {
25
+ // The shared gate also refuses lanes, daemons, bot runtimes, launchd
26
+ // tasks, and HQ_NO_BROWSER_LOGIN children that happen to hold a TTY.
24
27
  return {
25
- interactive: process.stdin.isTTY === true &&
28
+ interactive: gatedInteractiveLogin(process.stdin.isTTY === true &&
26
29
  process.stderr.isTTY === true &&
27
- !process.env.CI,
30
+ !process.env.CI),
28
31
  };
29
32
  }
30
33
  export function scopeOpts(opts) {
@@ -0,0 +1,17 @@
1
+ import type { CognitoAuthConfig } from "@indigoai-us/hq-cloud";
2
+ /** Fallback and default loopback callback port. */
3
+ export declare const DEFAULT_COGNITO_CALLBACK_PORT = 8765;
4
+ /** Ports registered as callback URLs on the HQ CLI Cognito app client. */
5
+ export declare const REGISTERED_COGNITO_CALLBACK_PORTS: readonly number[];
6
+ export type PortWarn = (message: string) => void;
7
+ /** Test hook: forget that the stray-port warning was printed. */
8
+ export declare function resetStrayPortWarningForTests(): void;
9
+ /**
10
+ * Resolve a callback port value, a string from the environment or a number,
11
+ * to a registered port. Unset resolves to 8765 silently; an unregistered value
12
+ * resolves to 8765 and logs a warning.
13
+ */
14
+ export declare function resolveCognitoCallbackPort(raw: string | number | undefined, warn?: PortWarn): number;
15
+ /** Return `config` with its callback port forced into the registered set. */
16
+ export declare function withRegisteredCallbackPort<T extends CognitoAuthConfig>(config: T, warn?: PortWarn): T;
17
+ //# sourceMappingURL=cognito-callback-port.d.ts.map
@@ -0,0 +1,56 @@
1
+ // src/utils/cognito-callback-port.ts
2
+ //
3
+ // The HQ CLI Cognito app client accepts exactly three loopback redirect URIs:
4
+ // http://localhost:{8765,3000,53682}/callback (hq-pro infra/cognito.ts,
5
+ // callbackUrls). browserLogin builds its redirect_uri from config.port, so any
6
+ // other port opens a sign-in tab that Cognito rejects with redirect_mismatch.
7
+ // A stray HQ_COGNITO_CALLBACK_PORT (seen with random ports such as 57726 and
8
+ // 61604) produced exactly that tab. Every browser sign-in now goes through
9
+ // this guard: an unregistered port falls back to 8765 with a stderr warning.
10
+ /** Fallback and default loopback callback port. */
11
+ export const DEFAULT_COGNITO_CALLBACK_PORT = 8765;
12
+ /** Ports registered as callback URLs on the HQ CLI Cognito app client. */
13
+ export const REGISTERED_COGNITO_CALLBACK_PORTS = [8765, 3000, 53682];
14
+ /** One stray-port warning per process: config and session both resolve the env value. */
15
+ let warnedStrayPort = false;
16
+ function defaultWarn(message) {
17
+ console.error(message);
18
+ }
19
+ function warnOnce(message, warn) {
20
+ if (warn === defaultWarn) {
21
+ if (warnedStrayPort)
22
+ return;
23
+ warnedStrayPort = true;
24
+ }
25
+ warn(message);
26
+ }
27
+ /** Test hook: forget that the stray-port warning was printed. */
28
+ export function resetStrayPortWarningForTests() {
29
+ warnedStrayPort = false;
30
+ }
31
+ /**
32
+ * Resolve a callback port value, a string from the environment or a number,
33
+ * to a registered port. Unset resolves to 8765 silently; an unregistered value
34
+ * resolves to 8765 and logs a warning.
35
+ */
36
+ export function resolveCognitoCallbackPort(raw, warn = defaultWarn) {
37
+ if (raw === undefined || (typeof raw === "string" && !raw.trim())) {
38
+ return DEFAULT_COGNITO_CALLBACK_PORT;
39
+ }
40
+ const port = typeof raw === "number" ? raw : Number(raw.trim());
41
+ if (REGISTERED_COGNITO_CALLBACK_PORTS.includes(port))
42
+ return port;
43
+ warnOnce(`hq: ignoring callback port ${String(raw)}: it is not a registered Cognito callback ` +
44
+ `(${REGISTERED_COGNITO_CALLBACK_PORTS.join(", ")}). Using ${DEFAULT_COGNITO_CALLBACK_PORT}. ` +
45
+ "Unset HQ_COGNITO_CALLBACK_PORT or set it to a registered port.", warn);
46
+ return DEFAULT_COGNITO_CALLBACK_PORT;
47
+ }
48
+ /** Return `config` with its callback port forced into the registered set. */
49
+ export function withRegisteredCallbackPort(config, warn = defaultWarn) {
50
+ // Unset keeps hq-cloud's own default (3000), which is registered.
51
+ if (config.port === undefined)
52
+ return config;
53
+ const port = resolveCognitoCallbackPort(config.port, warn);
54
+ return port === config.port ? config : { ...config, port };
55
+ }
56
+ //# sourceMappingURL=cognito-callback-port.js.map
@@ -1,11 +1,15 @@
1
+ import { resolveCognitoCallbackPort } from "./cognito-callback-port.js";
1
2
  /** Lightweight Cognito and Vault defaults shared by startup-safe modules. */
2
3
  export const DEFAULT_COGNITO = {
3
4
  region: process.env.AWS_REGION ?? "us-east-1",
4
5
  userPoolDomain: process.env.HQ_COGNITO_DOMAIN ?? "vault-indigo-hq-prod",
5
6
  clientId: process.env.HQ_COGNITO_CLIENT_ID ?? "7acei2c8v870enheptb1j5foln",
6
- port: process.env.HQ_COGNITO_CALLBACK_PORT
7
- ? Number(process.env.HQ_COGNITO_CALLBACK_PORT)
8
- : 8765,
7
+ // Resolved on read, not at module load: main.ts imports this for every
8
+ // command, and a stray HQ_COGNITO_CALLBACK_PORT should warn only when a
9
+ // sign-in actually reads the port.
10
+ get port() {
11
+ return resolveCognitoCallbackPort(process.env.HQ_COGNITO_CALLBACK_PORT);
12
+ },
9
13
  identityProvider: process.env.HQ_COGNITO_IDENTITY_PROVIDER !== undefined
10
14
  ? process.env.HQ_COGNITO_IDENTITY_PROVIDER || undefined
11
15
  : "Google",
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { isExpiring, isMachineIdentity, type CognitoAuthConfig, type CognitoTokens, type ClientInfo, type VaultServiceConfig } from "@indigoai-us/hq-cloud";
22
22
  import { hasPersonCachedSession, isMachineCredsFileEnvSet, wantsMachineCognitoTokens, type CognitoTokenSource } from "./machine-principal-state.js";
23
+ import { HQ_NO_BROWSER_LOGIN_ENV, type InteractiveCallerInput } from "./interactive-caller.js";
23
24
  import { loadCachedTokens, personTokenCacheFile, readPersonTokenFile } from "./person-token-cache.js";
24
25
  export { loadCachedTokens, personTokenCacheFile, readPersonTokenFile };
25
26
  export { isExpiring, isMachineIdentity };
@@ -115,14 +116,14 @@ export type EnsureCognitoTokenOptions = {
115
116
  /** Cancels bounded, non-interactive person-session refreshes. */
116
117
  signal?: AbortSignal;
117
118
  };
119
+ export { HQ_NO_BROWSER_LOGIN_ENV };
118
120
  /**
119
- * Env switch a parent process sets so a child `hq` never opens a browser
120
- * sign-in. A hook-driven helper (SessionStart's refresh-vault-access) cannot
121
- * complete a browser login, and a child waiting on one outlives its caller.
121
+ * An explicit `interactive` option wins. Otherwise the implicit browser sign-in
122
+ * is allowed only for an interactive caller (see isInteractiveCaller): a TTY on
123
+ * stdin and stdout, and not a daemon, lane, monitor, bot, `mcp serve`, sync
124
+ * runner, launchd task, or HQ_NO_BROWSER_LOGIN=1 child.
122
125
  */
123
- export declare const HQ_NO_BROWSER_LOGIN_ENV = "HQ_NO_BROWSER_LOGIN";
124
- /** An explicit `interactive` option wins; otherwise interactive unless the env switch is "1". */
125
- export declare function resolveInteractiveLogin(options: Pick<EnsureCognitoTokenOptions, "interactive">, env?: NodeJS.ProcessEnv): boolean;
126
+ export declare function resolveInteractiveLogin(options: Pick<EnsureCognitoTokenOptions, "interactive">, env?: NodeJS.ProcessEnv, caller?: Omit<InteractiveCallerInput, "env">): boolean;
126
127
  /**
127
128
  * Fresh-box / Outpost bootstrap: env unset, not forced to person, no person
128
129
  * cache, and the default machine-creds file is readable. Distinct from
@@ -32,6 +32,8 @@ import { daemonDir } from "../lib/mesh/live/daemon/paths.js";
32
32
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
33
33
  import { defaultHqRootFallback, hqDiscoveryCandidates, pathForPlatform, } from "./hq-root-resolver.js";
34
34
  import { acquireLockFileExclusively } from "./exclusive-lock.js";
35
+ import { resolveCognitoCallbackPort, withRegisteredCallbackPort } from "./cognito-callback-port.js";
36
+ import { HQ_NO_BROWSER_LOGIN_ENV, NON_INTERACTIVE_LOGIN_MESSAGE, isInteractiveCaller, } from "./interactive-caller.js";
35
37
  import { reportNoninteractivePersonRefreshRejection } from "./cognito-refresh-sentry.js";
36
38
  import { decodeJwtPayload, loadCachedTokens, personTokenCacheFile, readPersonTokenFile, readTokenFile, } from "./person-token-cache.js";
37
39
  export { loadCachedTokens, personTokenCacheFile, readPersonTokenFile };
@@ -49,9 +51,12 @@ export const DEFAULT_COGNITO = {
49
51
  region: process.env.AWS_REGION ?? "us-east-1",
50
52
  userPoolDomain: process.env.HQ_COGNITO_DOMAIN ?? "vault-indigo-hq-prod",
51
53
  clientId: process.env.HQ_COGNITO_CLIENT_ID ?? "7acei2c8v870enheptb1j5foln",
52
- port: process.env.HQ_COGNITO_CALLBACK_PORT
53
- ? Number(process.env.HQ_COGNITO_CALLBACK_PORT)
54
- : 8765,
54
+ // Resolved on read, not at module load: main.ts imports this for every
55
+ // command, and a stray HQ_COGNITO_CALLBACK_PORT should warn only when a
56
+ // sign-in actually reads the port.
57
+ get port() {
58
+ return resolveCognitoCallbackPort(process.env.HQ_COGNITO_CALLBACK_PORT);
59
+ },
55
60
  // Skip Cognito's Hosted UI — go straight to Google OAuth. The Cognito
56
61
  // /oauth2/authorize endpoint honors `identity_provider` and performs an
57
62
  // internal redirect, so the user never sees the (un-styleable) Hosted UI.
@@ -244,17 +249,17 @@ export function machineTokenCacheFile(home = os.homedir(), env = process.env) {
244
249
  export function defaultMachineCredsFile(home = os.homedir()) {
245
250
  return path.join(home, ".hq-agent", "machine-creds.json");
246
251
  }
252
+ export { HQ_NO_BROWSER_LOGIN_ENV };
247
253
  /**
248
- * Env switch a parent process sets so a child `hq` never opens a browser
249
- * sign-in. A hook-driven helper (SessionStart's refresh-vault-access) cannot
250
- * complete a browser login, and a child waiting on one outlives its caller.
254
+ * An explicit `interactive` option wins. Otherwise the implicit browser sign-in
255
+ * is allowed only for an interactive caller (see isInteractiveCaller): a TTY on
256
+ * stdin and stdout, and not a daemon, lane, monitor, bot, `mcp serve`, sync
257
+ * runner, launchd task, or HQ_NO_BROWSER_LOGIN=1 child.
251
258
  */
252
- export const HQ_NO_BROWSER_LOGIN_ENV = "HQ_NO_BROWSER_LOGIN";
253
- /** An explicit `interactive` option wins; otherwise interactive unless the env switch is "1". */
254
- export function resolveInteractiveLogin(options, env = process.env) {
259
+ export function resolveInteractiveLogin(options, env = process.env, caller = {}) {
255
260
  if (options.interactive !== undefined)
256
261
  return options.interactive;
257
- return env[HQ_NO_BROWSER_LOGIN_ENV]?.trim() !== "1";
262
+ return isInteractiveCaller({ ...caller, env });
258
263
  }
259
264
  /**
260
265
  * Fresh-box / Outpost bootstrap: env unset, not forced to person, no person
@@ -939,7 +944,7 @@ async function ensureCognitoTokenUnserialized(options) {
939
944
  return fallback.idToken;
940
945
  }
941
946
  if (!interactive) {
942
- throw new Error("No valid HQ session and interactive login is disabled. Run `hq login` first.");
947
+ throw new Error(NON_INTERACTIVE_LOGIN_MESSAGE);
943
948
  }
944
949
  if (isHeadlessHost() && hostRunsAnAgent()) {
945
950
  // An agent host with no display has no one to complete a browser sign-in,
@@ -951,7 +956,7 @@ async function ensureCognitoTokenUnserialized(options) {
951
956
  "(or HQ_MACHINE_CREDS_FILE to its machine-creds.json). Otherwise run `hq login` on a computer with a browser.");
952
957
  }
953
958
  console.error(chalk.cyan(" No cached HQ session — launching browser sign-in..."));
954
- const tokens = await browserLogin(DEFAULT_COGNITO);
959
+ const tokens = await browserLogin(withRegisteredCallbackPort(DEFAULT_COGNITO));
955
960
  return tokens.accessToken;
956
961
  }
957
962
  /**
@@ -1022,7 +1027,7 @@ async function ensureCognitoIdTokenUnserialized(options) {
1022
1027
  return fallback.idToken;
1023
1028
  }
1024
1029
  if (!interactive) {
1025
- throw new Error("No valid HQ session and interactive login is disabled. Run `hq login` first.");
1030
+ throw new Error(NON_INTERACTIVE_LOGIN_MESSAGE);
1026
1031
  }
1027
1032
  if (isHeadlessHost() && hostRunsAnAgent()) {
1028
1033
  // A browser sign-in on a server has no one to complete it and blocks
@@ -1032,7 +1037,7 @@ async function ensureCognitoIdTokenUnserialized(options) {
1032
1037
  "(or HQ_MACHINE_CREDS_FILE to its machine-creds.json). Otherwise run `hq login` on a computer with a browser.");
1033
1038
  }
1034
1039
  console.error(chalk.cyan(" No cached HQ session — launching browser sign-in..."));
1035
- const tokens = await browserLogin(DEFAULT_COGNITO);
1040
+ const tokens = await browserLogin(withRegisteredCallbackPort(DEFAULT_COGNITO));
1036
1041
  return tokens.idToken;
1037
1042
  }
1038
1043
  /**
@@ -0,0 +1,23 @@
1
+ /** Parent-set switch: a child `hq` must never open a browser sign-in. */
2
+ export declare const HQ_NO_BROWSER_LOGIN_ENV = "HQ_NO_BROWSER_LOGIN";
3
+ /** Remedy printed when an implicit sign-in is refused. */
4
+ export declare const NON_INTERACTIVE_LOGIN_MESSAGE: string;
5
+ export type InteractiveCallerInput = {
6
+ env?: NodeJS.ProcessEnv;
7
+ argv?: readonly string[];
8
+ stdinIsTTY?: boolean;
9
+ stdoutIsTTY?: boolean;
10
+ };
11
+ /** The reason a caller is not interactive, or null when it is. */
12
+ export declare function nonInteractiveReason(input?: InteractiveCallerInput): string | null;
13
+ /** True only when a person is at a terminal and may complete a browser sign-in. */
14
+ export declare function isInteractiveCaller(input?: InteractiveCallerInput): boolean;
15
+ /**
16
+ * Gate a call site's request for an implicit browser sign-in. A command that
17
+ * asks for `interactive: true` gets it only when a person is at a terminal, so
18
+ * the same command run from a lane, daemon, bot, launchd task, or piped child
19
+ * fails with the `hq login` remedy instead of opening a sign-in tab. Explicit
20
+ * `false` stays false. `hq login` calls browserLogin directly and is not gated.
21
+ */
22
+ export declare function gatedInteractiveLogin(requested?: boolean, input?: InteractiveCallerInput): boolean;
23
+ //# sourceMappingURL=interactive-caller.d.ts.map
@@ -0,0 +1,80 @@
1
+ // src/utils/interactive-caller.ts
2
+ //
3
+ // One predicate for "may this process open an implicit browser sign-in?".
4
+ //
5
+ // HQ-CLI-15 follow-up: ~100 ensureCognitoToken/ensureCognitoIdToken call sites
6
+ // fall back to browserLogin when a token refresh fails. Before this predicate
7
+ // the fallback ran for every caller unless HQ_NO_BROWSER_LOGIN=1 was set, so a
8
+ // daemon, lane, monitor, bot, `hq mcp serve`, sync runner, or launchd task
9
+ // opened a sign-in tab on its own that nobody had asked for. The implicit
10
+ // sign-in is now allowed only when a person is at a terminal: stdin AND stdout
11
+ // are TTYs and none of the background markers below is present. Explicit
12
+ // `hq login` calls browserLogin directly and is not gated here.
13
+ /** Parent-set switch: a child `hq` must never open a browser sign-in. */
14
+ export const HQ_NO_BROWSER_LOGIN_ENV = "HQ_NO_BROWSER_LOGIN";
15
+ /** Remedy printed when an implicit sign-in is refused. */
16
+ export const NON_INTERACTIVE_LOGIN_MESSAGE = "No valid HQ session and interactive login is disabled. Run `hq login` first. " +
17
+ "This process is not interactive (no terminal, or a background HQ runtime), so it did not open a browser sign-in.";
18
+ /** launchd job labels the HQ service manager installs (see main.ts runCli). */
19
+ const HQ_LAUNCHD_LABEL_PREFIXES = ["ai.indigo.", "ai.getindigo."];
20
+ /** Env markers set for background HQ runtimes. Any non-empty value counts. */
21
+ const BACKGROUND_ENV_MARKERS = [
22
+ "HQ_LANE_ID", // hq lanes worker sessions
23
+ "HQ_BOT_AGENT_UID", // hq bot runtimes
24
+ "HQ_DAEMON_UNIT", // daemon service units
25
+ ];
26
+ /**
27
+ * argv prefixes (after `hq`) for long-running background entry points. The
28
+ * daemon's sync runner child has no TTY on stdio, so the TTY check covers it.
29
+ */
30
+ const BACKGROUND_COMMANDS = [
31
+ ["daemon"],
32
+ ["mesh", "daemon"],
33
+ ["bot", "run"],
34
+ ["mcp", "serve"],
35
+ ["monitor"],
36
+ ];
37
+ function nonEmpty(value) {
38
+ return Boolean(value && value.trim());
39
+ }
40
+ /** The reason a caller is not interactive, or null when it is. */
41
+ export function nonInteractiveReason(input = {}) {
42
+ const env = input.env ?? process.env;
43
+ const argv = input.argv ?? process.argv.slice(2);
44
+ const stdinIsTTY = input.stdinIsTTY ?? process.stdin.isTTY === true;
45
+ const stdoutIsTTY = input.stdoutIsTTY ?? process.stdout.isTTY === true;
46
+ if (env[HQ_NO_BROWSER_LOGIN_ENV]?.trim() === "1")
47
+ return `${HQ_NO_BROWSER_LOGIN_ENV}=1`;
48
+ if (!stdinIsTTY)
49
+ return "stdin is not a terminal";
50
+ if (!stdoutIsTTY)
51
+ return "stdout is not a terminal";
52
+ for (const marker of BACKGROUND_ENV_MARKERS) {
53
+ if (nonEmpty(env[marker]))
54
+ return `${marker} is set`;
55
+ }
56
+ const xpc = env.XPC_SERVICE_NAME ?? "";
57
+ if (HQ_LAUNCHD_LABEL_PREFIXES.some((p) => xpc.startsWith(p)))
58
+ return "launchd task";
59
+ const args = argv.filter((a) => !a.startsWith("-"));
60
+ for (const cmd of BACKGROUND_COMMANDS) {
61
+ if (cmd.every((part, i) => args[i] === part))
62
+ return `\`hq ${cmd.join(" ")}\``;
63
+ }
64
+ return null;
65
+ }
66
+ /** True only when a person is at a terminal and may complete a browser sign-in. */
67
+ export function isInteractiveCaller(input = {}) {
68
+ return nonInteractiveReason(input) === null;
69
+ }
70
+ /**
71
+ * Gate a call site's request for an implicit browser sign-in. A command that
72
+ * asks for `interactive: true` gets it only when a person is at a terminal, so
73
+ * the same command run from a lane, daemon, bot, launchd task, or piped child
74
+ * fails with the `hq login` remedy instead of opening a sign-in tab. Explicit
75
+ * `false` stays false. `hq login` calls browserLogin directly and is not gated.
76
+ */
77
+ export function gatedInteractiveLogin(requested = true, input = {}) {
78
+ return requested && isInteractiveCaller(input);
79
+ }
80
+ //# sourceMappingURL=interactive-caller.js.map
@@ -2,6 +2,7 @@ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { browserLogin, saveCachedTokens, CognitoAuthError } from "@indigoai-us/hq-cloud";
4
4
  import { isHeadless } from "./headless.js";
5
+ import { withRegisteredCallbackPort } from "./cognito-callback-port.js";
5
6
  function assertHumanLoginAllowed() {
6
7
  // Preserve hq-cloud's company-machine boundary on the manual URL path.
7
8
  if (["1", "true", "yes"].includes(process.env.HQ_MACHINE_AUTH_REQUIRED?.trim().toLowerCase() ?? "")) {
@@ -10,10 +11,12 @@ function assertHumanLoginAllowed() {
10
11
  }
11
12
  /** Keep desktop login unchanged; manual login uses the same PKCE loopback flow. */
12
13
  export async function loginWithBrowserPreference(config, browser = true) {
14
+ // A callback port outside the registered set opens a redirect_mismatch tab.
13
15
  if (browser && !isHeadless())
14
- return browserLogin(config);
16
+ return browserLogin(withRegisteredCallbackPort(config));
15
17
  assertHumanLoginAllowed();
16
- const port = config.port ?? 3000;
18
+ // The manual URL path builds the same redirect_uri, so it needs the same guard.
19
+ const port = withRegisteredCallbackPort(config).port ?? 3000;
17
20
  const redirectUri = `http://localhost:${port}/callback`;
18
21
  const base = `https://${config.userPoolDomain}.auth.${config.region}.amazoncognito.com`;
19
22
  const verifier = randomBytes(32).toString("base64url");
@@ -28,6 +28,8 @@ export const RESIDENT_PACKAGE_MODULES = {
28
28
  ],
29
29
  "agents-terminal": ["../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "open", "../utils/cognito-session.js", "../lib/first-sync-next-step.js", "@indigoai-us/hq-cloud"],
30
30
  bot: [
31
+ "../commands/lanes.js",
32
+ "../lib/lanes/reconcile.js",
31
33
  "../lib/flag-registry.js",
32
34
  "../commands/access.js",
33
35
  "../commands/sources.js",
@@ -176,6 +178,8 @@ const LOADERS = {
176
178
  () => import("@indigoai-us/hq-cloud"),
177
179
  ],
178
180
  bot: [
181
+ () => import("../commands/lanes.js"),
182
+ () => import("../lib/lanes/reconcile.js"),
179
183
  () => import("../lib/flag-registry.js"),
180
184
  () => import("../commands/access.js"),
181
185
  // The signals engine's adapters step (lib/signals/steps/adapters.ts)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.198",
3
+ "version": "5.345.200",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -116,11 +116,11 @@
116
116
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
117
117
  },
118
118
  "optionalDependencies": {
119
- "@indigoai-us/hq-hook-win32-x64": "5.345.198",
120
- "@indigoai-us/hq-hook-win32-arm64": "5.345.198",
121
- "@indigoai-us/hq-hook-darwin-x64": "5.345.198",
122
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.198",
123
- "@indigoai-us/hq-hook-linux-x64": "5.345.198",
124
- "@indigoai-us/hq-hook-linux-arm64": "5.345.198"
119
+ "@indigoai-us/hq-hook-win32-x64": "5.345.200",
120
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.200",
121
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.200",
122
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.200",
123
+ "@indigoai-us/hq-hook-linux-x64": "5.345.200",
124
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.200"
125
125
  }
126
126
  }