@indigoai-us/hq-cli 5.345.134 → 5.345.136

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/assets/scaffold/core/scripts/worktree-gc.sh +98 -8
  3. package/dist/command-catalog.generated.d.ts +62 -6
  4. package/dist/command-catalog.generated.js +79 -7
  5. package/dist/command-registration-plan.d.ts +6 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +3 -1
  8. package/dist/commands/core-bucket-c-handoff-knowledge-native.js +8 -4
  9. package/dist/commands/core-checkpoint-native.d.ts +10 -0
  10. package/dist/commands/core-checkpoint-native.js +60 -15
  11. package/dist/commands/core-handoff.d.ts +2 -0
  12. package/dist/commands/core-handoff.js +3 -1
  13. package/dist/commands/core-pipeline-workflow-runner-native.js +7 -5
  14. package/dist/commands/daemon.js +3 -1
  15. package/dist/commands/fingerprint.d.ts +10 -0
  16. package/dist/commands/fingerprint.js +191 -0
  17. package/dist/commands/integrations-core.d.ts +22 -0
  18. package/dist/commands/integrations-core.js +25 -0
  19. package/dist/commands/integrations-manage.d.ts +24 -1
  20. package/dist/commands/integrations-manage.js +92 -17
  21. package/dist/commands/integrations.d.ts +11 -0
  22. package/dist/commands/integrations.js +100 -19
  23. package/dist/commands/lanes-loop.d.ts +9 -0
  24. package/dist/commands/lanes-loop.js +9 -1
  25. package/dist/commands/lanes.js +78 -55
  26. package/dist/commands/meetings.d.ts +2 -0
  27. package/dist/commands/meetings.js +57 -14
  28. package/dist/commands/mesh.d.ts +19 -1
  29. package/dist/commands/mesh.js +158 -70
  30. package/dist/commands/reindex.js +80 -5
  31. package/dist/commands/storage-offload.d.ts +93 -28
  32. package/dist/commands/storage-offload.js +290 -34
  33. package/dist/commands/storage.js +60 -7
  34. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2550 -1748
  35. package/dist/hq-cloud-sync-runner/package.json +1 -1
  36. package/dist/index.js +4 -0
  37. package/dist/lib/capacity/route.js +24 -0
  38. package/dist/lib/core-utils/session-company.d.ts +11 -0
  39. package/dist/lib/core-utils/session-company.js +34 -0
  40. package/dist/lib/core-utils/worktree-resume.js +44 -4
  41. package/dist/lib/core-utils/worktree-session-owner.d.ts +27 -0
  42. package/dist/lib/core-utils/worktree-session-owner.js +157 -0
  43. package/dist/lib/daemon/catalog.d.ts +2 -0
  44. package/dist/lib/daemon/catalog.js +3 -3
  45. package/dist/lib/daemon/hq-child-bootstrap-stage.d.ts +5 -0
  46. package/dist/lib/daemon/hq-child-bootstrap-stage.js +3 -11
  47. package/dist/lib/daemon/server.js +10 -1
  48. package/dist/lib/daemon/supervisor.js +6 -0
  49. package/dist/lib/daemon/sync-runner-bundle.d.ts +5 -0
  50. package/dist/lib/daemon/sync-runner-bundle.js +4 -12
  51. package/dist/lib/doctor/checks/claude-wiring.js +2 -1
  52. package/dist/lib/doctor/fix/apply.d.ts +20 -0
  53. package/dist/lib/doctor/fix/apply.js +63 -3
  54. package/dist/lib/doctor/fix/remediation.d.ts +1 -1
  55. package/dist/lib/doctor/fix/remediation.js +16 -0
  56. package/dist/lib/doctor/shared.d.ts +11 -1
  57. package/dist/lib/doctor/shared.js +16 -2
  58. package/dist/lib/fingerprint/fixtures.d.ts +19 -0
  59. package/dist/lib/fingerprint/fixtures.js +61 -0
  60. package/dist/lib/fingerprint/format.d.ts +50 -0
  61. package/dist/lib/fingerprint/format.js +105 -0
  62. package/dist/lib/fingerprint/offline.d.ts +16 -0
  63. package/dist/lib/fingerprint/offline.js +44 -0
  64. package/dist/lib/fingerprint/registry-client.d.ts +74 -0
  65. package/dist/lib/fingerprint/registry-client.js +141 -0
  66. package/dist/lib/fingerprint/resolve-local.d.ts +14 -0
  67. package/dist/lib/fingerprint/resolve-local.js +67 -0
  68. package/dist/lib/fingerprint/self-core.d.ts +139 -0
  69. package/dist/lib/fingerprint/self-core.js +371 -0
  70. package/dist/lib/fingerprint/self.d.ts +44 -0
  71. package/dist/lib/fingerprint/self.js +191 -0
  72. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  73. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  74. package/dist/lib/lanes/monitor.js +14 -2
  75. package/dist/lib/lanes/spawn.d.ts +19 -1
  76. package/dist/lib/lanes/spawn.js +50 -6
  77. package/dist/lib/lanes/story-ledger.d.ts +1 -0
  78. package/dist/lib/lanes/story-ledger.js +3 -1
  79. package/dist/lib/lanes/types.d.ts +12 -0
  80. package/dist/lib/lanes/types.js +12 -1
  81. package/dist/lib/mesh/live/daemon/install.d.ts +12 -0
  82. package/dist/lib/mesh/live/daemon/install.js +22 -0
  83. package/dist/lib/mesh/live/flush.js +106 -3
  84. package/dist/lib/mesh/live/session-events-client.d.ts +9 -0
  85. package/dist/lib/mesh/live/session-events-client.js +15 -0
  86. package/dist/lib/work-context/state.d.ts +10 -0
  87. package/dist/lib/work-context/state.js +5 -0
  88. package/dist/utils/cli-network-lifecycle.d.ts +2 -0
  89. package/dist/utils/cli-network-lifecycle.js +5 -1
  90. package/dist/utils/cognito-session.d.ts +2 -21
  91. package/dist/utils/cognito-session.js +3 -84
  92. package/dist/utils/fsync-directory.d.ts +17 -0
  93. package/dist/utils/fsync-directory.js +27 -0
  94. package/dist/utils/person-token-cache.d.ts +32 -0
  95. package/dist/utils/person-token-cache.js +95 -0
  96. package/dist/utils/vault-api.d.ts +21 -1
  97. package/dist/utils/vault-api.js +54 -5
  98. package/dist/utils/windows-hide-default.d.ts +42 -0
  99. package/dist/utils/windows-hide-default.js +98 -0
  100. package/package.json +2 -2
@@ -25,13 +25,14 @@ import * as os from "os";
25
25
  import * as path from "path";
26
26
  import * as yaml from "js-yaml";
27
27
  import chalk from "chalk";
28
- import { loadCachedTokens as loadCachedTokensFromFile, isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
28
+ import { isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
29
29
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
30
30
  import { daemonDir } from "../lib/mesh/live/daemon/paths.js";
31
31
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
32
32
  import { acquireLockFileExclusively } from "./exclusive-lock.js";
33
33
  import { reportNoninteractivePersonRefreshRejection } from "./cognito-refresh-sentry.js";
34
- import { pickNewerSession, readKeychainTokens } from "./token-keychain.js";
34
+ import { decodeJwtPayload, loadCachedTokens, personTokenCacheFile, readPersonTokenFile, readTokenFile, } from "./person-token-cache.js";
35
+ export { loadCachedTokens, personTokenCacheFile, readPersonTokenFile };
35
36
  // Re-export the cached-session primitives for callers that must deliberately
36
37
  // avoid the refresh/login behavior in the higher-level helpers below.
37
38
  export { isExpiring, isMachineIdentity };
@@ -294,17 +295,6 @@ function withAlignedMachineCredsEnv(env, fn) {
294
295
  }
295
296
  }
296
297
  }
297
- /**
298
- * On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
299
- * Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
300
- * mint in this process, that helper returns in-memory machine tokens when the
301
- * person file is absent, which would falsely block the fresh-box fallback.
302
- */
303
- export function personTokenCacheFile(home = os.homedir(), env = process.env) {
304
- const override = env.HQ_STATE_DIR?.trim();
305
- const stateDir = override && override.length > 0 ? override : path.join(home, ".hq");
306
- return path.join(stateDir, "cognito-tokens.json");
307
- }
308
298
  /**
309
299
  * True when a usable person Cognito cache file exists under the effective
310
300
  * `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
@@ -396,23 +386,6 @@ export function wantsMachineCognitoTokens(options = {}, env = process.env) {
396
386
  export function resolveCognitoTokenSource(env = process.env) {
397
387
  return wantsMachineCognitoTokens({}, env) ? "machine" : "person";
398
388
  }
399
- function decodeJwtPayload(token) {
400
- if (!token)
401
- return null;
402
- const parts = token.split(".");
403
- if (parts.length < 2 || !parts[1])
404
- return null;
405
- try {
406
- const json = Buffer.from(parts[1], "base64url").toString("utf8");
407
- const parsed = JSON.parse(json);
408
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
409
- return null;
410
- return parsed;
411
- }
412
- catch {
413
- return null;
414
- }
415
- }
416
389
  /**
417
390
  * Refresh wrote a different Cognito subject than the active cache.
418
391
  * Callers must not fall through to browser login or machine mint — that
@@ -460,60 +433,6 @@ export function formatCognitoSubject(tokens) {
460
433
  const fromAccess = cognitoSubjectFromToken(tokens.accessToken);
461
434
  return `sub=${fromId.sub ?? fromAccess.sub ?? "-"} entityUid=${fromId.entityUid ?? fromAccess.entityUid ?? "-"}`;
462
435
  }
463
- function readTokenFile(file) {
464
- try {
465
- if (!fs.existsSync(file))
466
- return null;
467
- const raw = JSON.parse(fs.readFileSync(file, "utf8"));
468
- if (typeof raw.accessToken !== "string" || typeof raw.idToken !== "string")
469
- return null;
470
- return raw;
471
- }
472
- catch {
473
- return null;
474
- }
475
- }
476
- /**
477
- * The person session from the shared macOS Keychain item (see
478
- * ./token-keychain.ts), or null. Not read when the token file is missing:
479
- * no file means signed out. Machine-identity and invalidated tokens are
480
- * refused, the same as the file loader.
481
- */
482
- function personKeychainTokens(file, home, env, keychain = {}) {
483
- if (!fs.existsSync(file))
484
- return null;
485
- const tokens = readKeychainTokens({ env, home, ...keychain });
486
- if (!tokens)
487
- return null;
488
- const claims = decodeJwtPayload(tokens.idToken);
489
- const entityType = claims?.["custom:entityType"];
490
- if (typeof entityType === "string" && entityType.trim().length > 0)
491
- return null;
492
- if (fs.existsSync(`${file}.invalid.${accessTokenFingerprint(tokens.accessToken)}`))
493
- return null;
494
- return tokens;
495
- }
496
- /**
497
- * Person session for `env`: the Keychain item and the token file, the newer
498
- * by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
499
- * in-memory fallback, which can surface a different session when the file
500
- * is missing or unreadable.
501
- */
502
- export function readPersonTokenFile(home = os.homedir(), env = process.env, keychain = {}) {
503
- const file = personTokenCacheFile(home, env);
504
- return pickNewerSession(personKeychainTokens(file, home, env, keychain), readTokenFile(file));
505
- }
506
- /**
507
- * The person's cached session: the shared Keychain item and hq-cloud's file
508
- * loader, the newer by `expiresAt`. Writes still go only to the file in this
509
- * step.
510
- */
511
- export function loadCachedTokens(env = process.env, keychain = {}) {
512
- const home = os.homedir();
513
- const file = personTokenCacheFile(home, env);
514
- const fromFile = loadCachedTokensFromFile();
515
- return pickNewerSession(personKeychainTokens(file, home, env, keychain), fromFile);
516
- }
517
436
  function restoreTokenFile(file, before) {
518
437
  if (before) {
519
438
  const dir = path.dirname(file);
@@ -0,0 +1,17 @@
1
+ /** Filesystem seam for {@link fsyncDirectory} tests. */
2
+ export interface DirectorySyncIo {
3
+ openSync: (file: string, flags: number) => number;
4
+ fsyncSync: (fd: number) => void;
5
+ closeSync: (fd: number) => void;
6
+ }
7
+ /**
8
+ * Flush a directory entry (a rename or a new child) to disk.
9
+ *
10
+ * Windows has no directory fsync: FlushFileBuffers on a directory handle
11
+ * fails, and Node reports `EPERM: operation not permitted, fsync`. NTFS
12
+ * journals the rename itself, so there is nothing to flush and the call is
13
+ * skipped on win32 (feedback_516e4427). On every other platform a failure
14
+ * still throws, so a POSIX stage keeps its durability check.
15
+ */
16
+ export declare function fsyncDirectory(directory: string, platform?: NodeJS.Platform, io?: DirectorySyncIo): void;
17
+ //# sourceMappingURL=fsync-directory.d.ts.map
@@ -0,0 +1,27 @@
1
+ import * as fs from "node:fs";
2
+ const defaultIo = {
3
+ openSync: (file, flags) => fs.openSync(file, flags),
4
+ fsyncSync: (fd) => fs.fsyncSync(fd),
5
+ closeSync: (fd) => fs.closeSync(fd),
6
+ };
7
+ /**
8
+ * Flush a directory entry (a rename or a new child) to disk.
9
+ *
10
+ * Windows has no directory fsync: FlushFileBuffers on a directory handle
11
+ * fails, and Node reports `EPERM: operation not permitted, fsync`. NTFS
12
+ * journals the rename itself, so there is nothing to flush and the call is
13
+ * skipped on win32 (feedback_516e4427). On every other platform a failure
14
+ * still throws, so a POSIX stage keeps its durability check.
15
+ */
16
+ export function fsyncDirectory(directory, platform = process.platform, io = defaultIo) {
17
+ if (platform === "win32")
18
+ return;
19
+ const fd = io.openSync(directory, fs.constants.O_RDONLY);
20
+ try {
21
+ io.fsyncSync(fd);
22
+ }
23
+ finally {
24
+ io.closeSync(fd);
25
+ }
26
+ }
27
+ //# sourceMappingURL=fsync-directory.js.map
@@ -0,0 +1,32 @@
1
+ import { type CognitoTokens } from "@indigoai-us/hq-cloud";
2
+ import { type KeychainReadOptions } from "./token-keychain.js";
3
+ /**
4
+ * On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
5
+ * Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
6
+ * mint in this process, that helper returns in-memory machine tokens when the
7
+ * person file is absent, which would falsely block the fresh-box fallback.
8
+ */
9
+ export declare function personTokenCacheFile(home?: string, env?: NodeJS.ProcessEnv): string;
10
+ export declare function decodeJwtPayload(token: string | undefined): Record<string, unknown> | null;
11
+ export declare function readTokenFile(file: string): CognitoTokens | null;
12
+ /**
13
+ * The person session from the shared macOS Keychain item (see
14
+ * ./token-keychain.ts), or null. Not read when the token file is missing:
15
+ * no file means signed out. Machine-identity and invalidated tokens are
16
+ * refused, the same as the file loader.
17
+ */
18
+ export declare function personKeychainTokens(file: string, home: string, env: NodeJS.ProcessEnv, keychain?: KeychainReadOptions): CognitoTokens | null;
19
+ /**
20
+ * Person session for `env`: the Keychain item and the token file, the newer
21
+ * by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
22
+ * in-memory fallback, which can surface a different session when the file
23
+ * is missing or unreadable.
24
+ */
25
+ export declare function readPersonTokenFile(home?: string, env?: NodeJS.ProcessEnv, keychain?: KeychainReadOptions): CognitoTokens | null;
26
+ /**
27
+ * The person's cached session: the shared Keychain item and hq-cloud's file
28
+ * loader, the newer by `expiresAt`. Writes still go only to the file in this
29
+ * step.
30
+ */
31
+ export declare function loadCachedTokens(env?: NodeJS.ProcessEnv, keychain?: KeychainReadOptions): CognitoTokens | null;
32
+ //# sourceMappingURL=person-token-cache.d.ts.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The person's cached Cognito session, read from disk and the shared Keychain
3
+ * item without any network call. Split out of cognito-session.ts so offline
4
+ * callers (lane start and the handoff commits) can read the cached ID token
5
+ * without bundling the refresh and login clients. cognito-session.ts
6
+ * re-exports these readers.
7
+ */
8
+ import * as fs from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+ import { accessTokenFingerprint, loadCachedTokens as loadCachedTokensFromFile } from "@indigoai-us/hq-cloud";
12
+ import { pickNewerSession, readKeychainTokens } from "./token-keychain.js";
13
+ /**
14
+ * On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
15
+ * Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
16
+ * mint in this process, that helper returns in-memory machine tokens when the
17
+ * person file is absent, which would falsely block the fresh-box fallback.
18
+ */
19
+ export function personTokenCacheFile(home = os.homedir(), env = process.env) {
20
+ const override = env.HQ_STATE_DIR?.trim();
21
+ const stateDir = override && override.length > 0 ? override : path.join(home, ".hq");
22
+ return path.join(stateDir, "cognito-tokens.json");
23
+ }
24
+ export function decodeJwtPayload(token) {
25
+ if (!token)
26
+ return null;
27
+ const parts = token.split(".");
28
+ if (parts.length < 2 || !parts[1])
29
+ return null;
30
+ try {
31
+ const json = Buffer.from(parts[1], "base64url").toString("utf8");
32
+ const parsed = JSON.parse(json);
33
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
34
+ return null;
35
+ return parsed;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export function readTokenFile(file) {
42
+ try {
43
+ if (!fs.existsSync(file))
44
+ return null;
45
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
46
+ if (typeof raw.accessToken !== "string" || typeof raw.idToken !== "string")
47
+ return null;
48
+ return raw;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /**
55
+ * The person session from the shared macOS Keychain item (see
56
+ * ./token-keychain.ts), or null. Not read when the token file is missing:
57
+ * no file means signed out. Machine-identity and invalidated tokens are
58
+ * refused, the same as the file loader.
59
+ */
60
+ export function personKeychainTokens(file, home, env, keychain = {}) {
61
+ if (!fs.existsSync(file))
62
+ return null;
63
+ const tokens = readKeychainTokens({ env, home, ...keychain });
64
+ if (!tokens)
65
+ return null;
66
+ const claims = decodeJwtPayload(tokens.idToken);
67
+ const entityType = claims?.["custom:entityType"];
68
+ if (typeof entityType === "string" && entityType.trim().length > 0)
69
+ return null;
70
+ if (fs.existsSync(`${file}.invalid.${accessTokenFingerprint(tokens.accessToken)}`))
71
+ return null;
72
+ return tokens;
73
+ }
74
+ /**
75
+ * Person session for `env`: the Keychain item and the token file, the newer
76
+ * by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
77
+ * in-memory fallback, which can surface a different session when the file
78
+ * is missing or unreadable.
79
+ */
80
+ export function readPersonTokenFile(home = os.homedir(), env = process.env, keychain = {}) {
81
+ const file = personTokenCacheFile(home, env);
82
+ return pickNewerSession(personKeychainTokens(file, home, env, keychain), readTokenFile(file));
83
+ }
84
+ /**
85
+ * The person's cached session: the shared Keychain item and hq-cloud's file
86
+ * loader, the newer by `expiresAt`. Writes still go only to the file in this
87
+ * step.
88
+ */
89
+ export function loadCachedTokens(env = process.env, keychain = {}) {
90
+ const home = os.homedir();
91
+ const file = personTokenCacheFile(home, env);
92
+ const fromFile = loadCachedTokensFromFile();
93
+ return pickNewerSession(personKeychainTokens(file, home, env, keychain), fromFile);
94
+ }
95
+ //# sourceMappingURL=person-token-cache.js.map
@@ -109,7 +109,17 @@ export type CallerIdentity = {
109
109
  * {@link resolveCallerPersonUid} does. Use this where the server accepts an
110
110
  * agent principal; use `resolveCallerPersonUid` where only a person can act.
111
111
  */
112
- export declare function resolveCallerUid(token: string, baseUrl?: string): Promise<CallerIdentity>;
112
+ export declare function resolveCallerUid(token: string, baseUrl?: string, opts?: {
113
+ signal?: AbortSignal;
114
+ }): Promise<CallerIdentity>;
115
+ /**
116
+ * Thrown when the person lookup succeeds but the caller has no person entity.
117
+ * This is a definitive answer, unlike a network or HTTP failure, so callers
118
+ * that cache lookups may remember it.
119
+ */
120
+ export declare class NoPersonEntityError extends Error {
121
+ constructor();
122
+ }
113
123
  /** Thrown when a person-only command runs under an agent machine identity. */
114
124
  export declare function agentIdentityUnsupportedError(agentUid: string, command?: string): Error;
115
125
  /**
@@ -120,6 +130,16 @@ export declare function agentIdentityUnsupportedError(agentUid: string, command?
120
130
  export declare function resolveCallerPersonUid(token: string, baseUrl?: string, opts?: {
121
131
  command?: string;
122
132
  }): Promise<string>;
133
+ /**
134
+ * A signal that aborts when any input signal aborts, with that signal's
135
+ * reason. It stands in for `AbortSignal.any`, which Node only added in 20.3,
136
+ * so the CLI keeps working on Node 20.0. Call `dispose` once the guarded work
137
+ * settles to remove the listeners from the inputs.
138
+ */
139
+ export declare function anySignal(signals: AbortSignal[]): {
140
+ signal: AbortSignal;
141
+ dispose: () => void;
142
+ };
123
143
  export declare function getEntityUid(token: string, opts: {
124
144
  personal?: boolean;
125
145
  companySlug?: string;
@@ -735,11 +735,22 @@ export function agentUidFromToken(token) {
735
735
  * {@link resolveCallerPersonUid} does. Use this where the server accepts an
736
736
  * agent principal; use `resolveCallerPersonUid` where only a person can act.
737
737
  */
738
- export async function resolveCallerUid(token, baseUrl) {
738
+ export async function resolveCallerUid(token, baseUrl, opts = {}) {
739
739
  const agentUid = agentUidFromToken(token);
740
740
  if (agentUid)
741
741
  return { kind: 'agent', uid: agentUid };
742
- return { kind: 'person', uid: await lookupCallerPersonUid(token, baseUrl) };
742
+ return { kind: 'person', uid: await lookupCallerPersonUid(token, baseUrl, opts.signal) };
743
+ }
744
+ /**
745
+ * Thrown when the person lookup succeeds but the caller has no person entity.
746
+ * This is a definitive answer, unlike a network or HTTP failure, so callers
747
+ * that cache lookups may remember it.
748
+ */
749
+ export class NoPersonEntityError extends Error {
750
+ constructor() {
751
+ super('No person entity found for the caller. Sign in to HQ once to provision one.');
752
+ this.name = 'NoPersonEntityError';
753
+ }
743
754
  }
744
755
  /** Thrown when a person-only command runs under an agent machine identity. */
745
756
  export function agentIdentityUnsupportedError(agentUid, command) {
@@ -758,14 +769,52 @@ export async function resolveCallerPersonUid(token, baseUrl, opts = {}) {
758
769
  throw agentIdentityUnsupportedError(agentUid, opts.command);
759
770
  return lookupCallerPersonUid(token, baseUrl);
760
771
  }
772
+ /**
773
+ * A signal that aborts when any input signal aborts, with that signal's
774
+ * reason. It stands in for `AbortSignal.any`, which Node only added in 20.3,
775
+ * so the CLI keeps working on Node 20.0. Call `dispose` once the guarded work
776
+ * settles to remove the listeners from the inputs.
777
+ */
778
+ export function anySignal(signals) {
779
+ const controller = new AbortController();
780
+ const listeners = [];
781
+ const dispose = () => {
782
+ for (const [input, listener] of listeners.splice(0))
783
+ input.removeEventListener('abort', listener);
784
+ };
785
+ for (const input of signals) {
786
+ if (input.aborted) {
787
+ dispose();
788
+ controller.abort(input.reason);
789
+ break;
790
+ }
791
+ const listener = () => {
792
+ dispose();
793
+ controller.abort(input.reason);
794
+ };
795
+ input.addEventListener('abort', listener, { once: true });
796
+ listeners.push([input, listener]);
797
+ }
798
+ return { signal: controller.signal, dispose };
799
+ }
761
800
  // Same selection rule as the backend's `resolveCallerPersonUid`: ascending by
762
801
  // createdAt, tie-break by uid ascending. Returns the `prs_*` UID.
763
- async function lookupCallerPersonUid(token, baseUrl) {
802
+ async function lookupCallerPersonUid(token, baseUrl, signal) {
803
+ const timeout = AbortSignal.timeout(identityLookupTimeoutMs());
804
+ const combined = signal ? anySignal([timeout, signal]) : { signal: timeout, dispose: () => { } };
805
+ try {
806
+ return await lookupCallerPersonUidWith(token, baseUrl, combined.signal);
807
+ }
808
+ finally {
809
+ combined.dispose();
810
+ }
811
+ }
812
+ async function lookupCallerPersonUidWith(token, baseUrl, signal) {
764
813
  const res = await vaultApiFetch({
765
814
  token,
766
815
  path: '/entity/by-type/person',
767
816
  baseUrl,
768
- signal: AbortSignal.timeout(identityLookupTimeoutMs()),
817
+ signal,
769
818
  });
770
819
  if (!res.ok) {
771
820
  if (isUnavailableSecretLoadHttpStatus(res.status)) {
@@ -776,7 +825,7 @@ async function lookupCallerPersonUid(token, baseUrl) {
776
825
  const data = (await res.json());
777
826
  const persons = (data.entities ?? []).filter((e) => e.type === 'person');
778
827
  if (persons.length === 0) {
779
- throw new Error('No person entity found for the caller. Sign in to HQ once to provision one.');
828
+ throw new NoPersonEntityError();
780
829
  }
781
830
  persons.sort((a, b) => {
782
831
  const ac = a.createdAt ?? '';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Default `windowsHide: true` for child processes of a background hq on Windows.
3
+ *
4
+ * A console program (node, bash, git, cygpath, claude --version) started from
5
+ * a process that has no console of its own gets a new, visible console
6
+ * window. The HQ desktop app, the sync runner and hooks start hq with piped
7
+ * stdio and no console, so every child hq spawned flashed a window on screen,
8
+ * every minute or two, all day (feedback_351a40d5). Node only hides it when a
9
+ * call passes `windowsHide: true`, and hq-cli has a few hundred spawn sites.
10
+ *
11
+ * When this process is on Windows and none of its standard streams is a
12
+ * terminal, every child_process call that does not choose `windowsHide`
13
+ * itself gets `windowsHide: true`. An interactive hq (any standard stream is
14
+ * a TTY) is left alone, so a child that shares the user's console still does.
15
+ */
16
+ type ChildProcessModule = typeof import("node:child_process");
17
+ /** child_process entry points whose options object accepts `windowsHide`. */
18
+ export declare const WINDOWS_HIDE_FUNCTIONS: readonly ["spawn", "spawnSync", "execFile", "execFileSync", "exec", "execSync", "fork"];
19
+ export type WindowsHideFunction = (typeof WINDOWS_HIDE_FUNCTIONS)[number];
20
+ /**
21
+ * Return `args` with `windowsHide: true` in the options object of a
22
+ * child_process call, unless the caller set `windowsHide` either way. The
23
+ * input array and the caller's options object are not mutated.
24
+ */
25
+ export declare function applyWindowsHideDefault(fn: WindowsHideFunction, args: readonly unknown[]): unknown[];
26
+ /** True when none of stdin, stdout and stderr is a terminal. */
27
+ export declare function isBackgroundProcess(isTty?: (fd: number) => boolean): boolean;
28
+ export interface InstallWindowsHideDefaultOptions {
29
+ platform?: NodeJS.Platform;
30
+ background?: boolean;
31
+ /** child_process module to patch (tests pass a stand-in). */
32
+ childProcess?: ChildProcessModule;
33
+ /** Called after patching so ESM named imports see the wrapped functions. */
34
+ syncExports?: () => void;
35
+ }
36
+ /**
37
+ * Patch node:child_process once for this process. Returns true when the
38
+ * default is (now or already) in force.
39
+ */
40
+ export declare function installWindowsHideDefault(options?: InstallWindowsHideDefaultOptions): boolean;
41
+ export {};
42
+ //# sourceMappingURL=windows-hide-default.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Default `windowsHide: true` for child processes of a background hq on Windows.
3
+ *
4
+ * A console program (node, bash, git, cygpath, claude --version) started from
5
+ * a process that has no console of its own gets a new, visible console
6
+ * window. The HQ desktop app, the sync runner and hooks start hq with piped
7
+ * stdio and no console, so every child hq spawned flashed a window on screen,
8
+ * every minute or two, all day (feedback_351a40d5). Node only hides it when a
9
+ * call passes `windowsHide: true`, and hq-cli has a few hundred spawn sites.
10
+ *
11
+ * When this process is on Windows and none of its standard streams is a
12
+ * terminal, every child_process call that does not choose `windowsHide`
13
+ * itself gets `windowsHide: true`. An interactive hq (any standard stream is
14
+ * a TTY) is left alone, so a child that shares the user's console still does.
15
+ */
16
+ // Only node:module is imported at load time (index.ts already loads it). This
17
+ // module is imported on every hq start, including the hook-time fast routes,
18
+ // so node:tty and node:util are loaded lazily and only on Windows.
19
+ import { createRequire, syncBuiltinESMExports } from "node:module";
20
+ /** child_process entry points whose options object accepts `windowsHide`. */
21
+ export const WINDOWS_HIDE_FUNCTIONS = ["spawn", "spawnSync", "execFile", "execFileSync", "exec", "execSync", "fork"];
22
+ const INSTALLED = Symbol.for("hq.windowsHideDefault.installed");
23
+ /** Same symbol as util.promisify.custom (a registered symbol). */
24
+ const PROMISIFY_CUSTOM = Symbol.for("nodejs.util.promisify.custom");
25
+ const requireBuiltin = createRequire(import.meta.url);
26
+ function defaultIsatty(fd) {
27
+ return requireBuiltin("node:tty").isatty(fd);
28
+ }
29
+ function isOptionsObject(value) {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+ /**
33
+ * Return `args` with `windowsHide: true` in the options object of a
34
+ * child_process call, unless the caller set `windowsHide` either way. The
35
+ * input array and the caller's options object are not mutated.
36
+ */
37
+ export function applyWindowsHideDefault(fn, args) {
38
+ const out = [...args];
39
+ let index;
40
+ if (fn === "exec" || fn === "execSync") {
41
+ index = 1;
42
+ }
43
+ else {
44
+ // (file, args?, options?, callback?): the args array is optional and may
45
+ // be passed as undefined/null ahead of an options object.
46
+ index = Array.isArray(out[1]) || (out[1] == null && out.length > 2) ? 2 : 1;
47
+ }
48
+ const current = out[index];
49
+ if (isOptionsObject(current)) {
50
+ if ("windowsHide" in current)
51
+ return out;
52
+ out[index] = { ...current, windowsHide: true };
53
+ }
54
+ else if (current == null) {
55
+ out[index] = { windowsHide: true };
56
+ }
57
+ else if (typeof current === "function") {
58
+ out.splice(index, 0, { windowsHide: true });
59
+ }
60
+ return out;
61
+ }
62
+ /** True when none of stdin, stdout and stderr is a terminal. */
63
+ export function isBackgroundProcess(isTty = defaultIsatty) {
64
+ return !isTty(0) && !isTty(1) && !isTty(2);
65
+ }
66
+ /**
67
+ * Patch node:child_process once for this process. Returns true when the
68
+ * default is (now or already) in force.
69
+ */
70
+ export function installWindowsHideDefault(options = {}) {
71
+ const platform = options.platform ?? process.platform;
72
+ if (platform !== "win32")
73
+ return false;
74
+ if (!(options.background ?? isBackgroundProcess()))
75
+ return false;
76
+ const childProcess = options.childProcess ?? requireBuiltin("node:child_process");
77
+ const target = childProcess;
78
+ if (target[INSTALLED])
79
+ return true;
80
+ for (const name of WINDOWS_HIDE_FUNCTIONS) {
81
+ const original = target[name];
82
+ if (typeof original !== "function")
83
+ continue;
84
+ const originalFn = original;
85
+ const wrapped = function (...args) {
86
+ return originalFn.apply(this, applyWindowsHideDefault(name, args));
87
+ };
88
+ const custom = originalFn[PROMISIFY_CUSTOM];
89
+ if (typeof custom === "function") {
90
+ wrapped[PROMISIFY_CUSTOM] = (...args) => custom(...applyWindowsHideDefault(name, args));
91
+ }
92
+ target[name] = wrapped;
93
+ }
94
+ target[INSTALLED] = true;
95
+ (options.syncExports ?? syncBuiltinESMExports)();
96
+ return true;
97
+ }
98
+ //# sourceMappingURL=windows-hide-default.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.134",
3
+ "version": "5.345.136",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -48,7 +48,7 @@
48
48
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
49
49
  "@aws-sdk/client-s3": "^3.1049.0",
50
50
  "@aws-sdk/dsql-signer": "^3.1146.0",
51
- "@indigoai-us/hq-cloud": "~6.18.71",
51
+ "@indigoai-us/hq-cloud": "~6.18.76",
52
52
  "@indigoai-us/hq-flags-client": "^0.1.2",
53
53
  "@modelcontextprotocol/sdk": "1.29.0",
54
54
  "@sentry/node": "^10.49.0",