@indigoai-us/hq-cli 5.108.1 → 5.108.2

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 (106) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/assets/mesh-daemon/README.md +23 -0
  3. package/assets/mesh-daemon/launchd.plist.template +33 -0
  4. package/assets/mesh-daemon/systemd.system.service.template +27 -0
  5. package/assets/mesh-daemon/systemd.user.service.template +18 -0
  6. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
  7. package/dist/commands/core.js +5 -4
  8. package/dist/commands/mesh.d.ts +15 -0
  9. package/dist/commands/mesh.js +732 -4
  10. package/dist/commands/search.d.ts +23 -0
  11. package/dist/commands/search.js +42 -1
  12. package/dist/lib/doctor/checks/work-context.d.ts +23 -0
  13. package/dist/lib/doctor/checks/work-context.js +195 -0
  14. package/dist/lib/doctor/registry.js +4 -0
  15. package/dist/lib/mesh/api.d.ts +18 -0
  16. package/dist/lib/mesh/api.js +55 -0
  17. package/dist/lib/mesh/cache.js +35 -4
  18. package/dist/lib/mesh/client.d.ts +118 -0
  19. package/dist/lib/mesh/client.js +268 -0
  20. package/dist/lib/mesh/live/backoff.d.ts +16 -0
  21. package/dist/lib/mesh/live/backoff.js +20 -0
  22. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
  23. package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
  24. package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
  25. package/dist/lib/mesh/live/daemon/credentials.js +227 -0
  26. package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
  27. package/dist/lib/mesh/live/daemon/doctor.js +139 -0
  28. package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
  29. package/dist/lib/mesh/live/daemon/index.js +14 -0
  30. package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
  31. package/dist/lib/mesh/live/daemon/install.js +330 -0
  32. package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
  33. package/dist/lib/mesh/live/daemon/log.js +79 -0
  34. package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
  35. package/dist/lib/mesh/live/daemon/paths.js +31 -0
  36. package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
  37. package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
  38. package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
  39. package/dist/lib/mesh/live/daemon/presence.js +250 -0
  40. package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
  41. package/dist/lib/mesh/live/daemon/presign.js +65 -0
  42. package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
  43. package/dist/lib/mesh/live/daemon/run.js +400 -0
  44. package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
  45. package/dist/lib/mesh/live/daemon/state.js +57 -0
  46. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
  47. package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
  48. package/dist/lib/mesh/live/enqueue.d.ts +48 -0
  49. package/dist/lib/mesh/live/enqueue.js +100 -0
  50. package/dist/lib/mesh/live/flush.d.ts +56 -0
  51. package/dist/lib/mesh/live/flush.js +360 -0
  52. package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
  53. package/dist/lib/mesh/live/format-spool-line.js +106 -0
  54. package/dist/lib/mesh/live/index.d.ts +22 -0
  55. package/dist/lib/mesh/live/index.js +16 -0
  56. package/dist/lib/mesh/live/paths.d.ts +13 -0
  57. package/dist/lib/mesh/live/paths.js +30 -0
  58. package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
  59. package/dist/lib/mesh/live/session-events-client.js +120 -0
  60. package/dist/lib/mesh/live/session-identity.d.ts +39 -0
  61. package/dist/lib/mesh/live/session-identity.js +91 -0
  62. package/dist/lib/mesh/live/spool.d.ts +52 -0
  63. package/dist/lib/mesh/live/spool.js +193 -0
  64. package/dist/lib/mesh/live/ulid.d.ts +12 -0
  65. package/dist/lib/mesh/live/ulid.js +39 -0
  66. package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
  67. package/dist/lib/mesh/live/validate-session-event.js +128 -0
  68. package/dist/lib/search-index/index.d.ts +45 -0
  69. package/dist/lib/search-index/index.js +76 -0
  70. package/dist/lib/work-context/atomic.d.ts +18 -0
  71. package/dist/lib/work-context/atomic.js +132 -0
  72. package/dist/lib/work-context/company.d.ts +103 -0
  73. package/dist/lib/work-context/company.js +274 -0
  74. package/dist/lib/work-context/config.d.ts +59 -0
  75. package/dist/lib/work-context/config.js +172 -0
  76. package/dist/lib/work-context/contract.d.ts +81 -0
  77. package/dist/lib/work-context/contract.js +65 -0
  78. package/dist/lib/work-context/errors.d.ts +27 -0
  79. package/dist/lib/work-context/errors.js +46 -0
  80. package/dist/lib/work-context/index.d.ts +23 -0
  81. package/dist/lib/work-context/index.js +20 -0
  82. package/dist/lib/work-context/migrate.d.ts +84 -0
  83. package/dist/lib/work-context/migrate.js +287 -0
  84. package/dist/lib/work-context/organize.d.ts +130 -0
  85. package/dist/lib/work-context/organize.js +471 -0
  86. package/dist/lib/work-context/outbox.d.ts +97 -0
  87. package/dist/lib/work-context/outbox.js +357 -0
  88. package/dist/lib/work-context/paths.d.ts +14 -0
  89. package/dist/lib/work-context/paths.js +39 -0
  90. package/dist/lib/work-context/project.d.ts +78 -0
  91. package/dist/lib/work-context/project.js +247 -0
  92. package/dist/lib/work-context/reconcile.d.ts +85 -0
  93. package/dist/lib/work-context/reconcile.js +651 -0
  94. package/dist/lib/work-context/repo-remote.d.ts +43 -0
  95. package/dist/lib/work-context/repo-remote.js +200 -0
  96. package/dist/lib/work-context/state.d.ts +112 -0
  97. package/dist/lib/work-context/state.js +162 -0
  98. package/dist/main.js +40 -3
  99. package/dist/utils/cognito-session.d.ts +85 -9
  100. package/dist/utils/cognito-session.js +216 -9
  101. package/dist/utils/qmd-model-download-error.d.ts +27 -0
  102. package/dist/utils/qmd-model-download-error.js +114 -0
  103. package/dist/utils/qmd-query-document-error.d.ts +23 -0
  104. package/dist/utils/qmd-query-document-error.js +117 -0
  105. package/dist/utils/sentry-fingerprint.js +2 -0
  106. package/package.json +5 -1
@@ -18,8 +18,18 @@
18
18
  * HQ_COGNITO_CALLBACK_PORT — Loopback OAuth callback port
19
19
  * HQ_VAULT_API_URL — vault-service API Gateway URL
20
20
  */
21
- import { loadCachedTokens, isExpiring, isMachineIdentity, type CognitoAuthConfig, type ClientInfo, type VaultServiceConfig } from "@indigoai-us/hq-cloud";
21
+ import { loadCachedTokens, isExpiring, isMachineIdentity, type CognitoAuthConfig, type CognitoTokens, type ClientInfo, type VaultServiceConfig } from "@indigoai-us/hq-cloud";
22
22
  export { isExpiring, isMachineIdentity, loadCachedTokens };
23
+ /** Person browser/login cache vs machine-creds USER_PASSWORD_AUTH mint. */
24
+ export type CognitoTokenSource = "person" | "machine";
25
+ /** Actor kind derived from the active token / machine-creds signal. */
26
+ export type CognitoActorKind = "human" | "agent" | "unknown";
27
+ /**
28
+ * Override for where machine-minted Cognito tokens are cached.
29
+ * Default: the work-mesh daemon state dir (`~/.hq/work-mesh/daemon`), so a
30
+ * machine mint never overwrites a person's `~/.hq/cognito-tokens.json`.
31
+ */
32
+ export declare const HQ_MACHINE_TOKEN_STATE_DIR_ENV = "HQ_MACHINE_TOKEN_STATE_DIR";
23
33
  export declare const DEFAULT_COGNITO: CognitoAuthConfig;
24
34
  export declare const DEFAULT_VAULT_API_URL: string;
25
35
  /**
@@ -73,17 +83,85 @@ export declare function resolveDefaultHqRoot(opts?: {
73
83
  onMissing?: "throw" | "fallback";
74
84
  }): string;
75
85
  export declare const DEFAULT_HQ_ROOT: string;
86
+ /**
87
+ * Directory used as `HQ_STATE_DIR` while minting/caching machine Cognito
88
+ * tokens. Never the person's `~/.hq` — agent boxes write under the daemon
89
+ * state dir so a mint cannot clobber a human login cache.
90
+ */
91
+ export declare function machineTokenStateDir(home?: string, env?: NodeJS.ProcessEnv): string;
92
+ /** Absolute path of the machine-token cache file (daemon state dir). */
93
+ export declare function machineTokenCacheFile(home?: string, env?: NodeJS.ProcessEnv): string;
94
+ /**
95
+ * True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
96
+ * The mere presence of `~/.hq-agent/machine-creds.json` is NOT enough — minting
97
+ * and doctor "machine" reporting are opt-in via this env (systemd unit) or an
98
+ * explicit `{ tokenSource: "machine" }` caller option.
99
+ */
100
+ export declare function isMachineCredsFileEnvSet(env?: NodeJS.ProcessEnv): boolean;
101
+ export type EnsureCognitoTokenOptions = {
102
+ interactive?: boolean;
103
+ /**
104
+ * Force machine-credential minting (`"machine"`) or the person login cache
105
+ * (`"person"`). When omitted, machine minting is used only if
106
+ * `HQ_MACHINE_CREDS_FILE` is explicitly set and readable.
107
+ */
108
+ tokenSource?: CognitoTokenSource;
109
+ };
110
+ /**
111
+ * Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
112
+ * Opt-in only: explicit `{ tokenSource: "machine" }`, or `HQ_MACHINE_CREDS_FILE`
113
+ * set in the environment. Default-path `~/.hq-agent/machine-creds.json` alone
114
+ * never flips other CLI commands onto the machine path.
115
+ */
116
+ export declare function wantsMachineCognitoTokens(options?: Pick<EnsureCognitoTokenOptions, "tokenSource">, env?: NodeJS.ProcessEnv): boolean;
117
+ /**
118
+ * Which Cognito token source this process will use for doctor / reporting:
119
+ * machine only when `HQ_MACHINE_CREDS_FILE` is explicitly set and readable;
120
+ * otherwise the person login cache. Default-path machine-creds alone → person.
121
+ */
122
+ export declare function resolveCognitoTokenSource(env?: NodeJS.ProcessEnv): CognitoTokenSource;
123
+ /** Derive actor kind from an ID token's custom:entity* claims (no verify). */
124
+ export declare function actorKindFromIdToken(idToken: string | undefined): CognitoActorKind;
125
+ /**
126
+ * Peek the machine-token cache under the daemon state dir without minting.
127
+ * Used by doctor; never logs token material.
128
+ */
129
+ export declare function loadMachineCachedTokens(home?: string, env?: NodeJS.ProcessEnv): CognitoTokens | null;
130
+ /**
131
+ * Report token source + actor kind for `hq mesh daemon doctor`. Does not mint
132
+ * or hit the network. Machine identity without a readable cache still reports
133
+ * actorKind `agent` (the box identity signal).
134
+ */
135
+ export declare function describeCognitoTokenSource(opts?: {
136
+ home?: string;
137
+ env?: NodeJS.ProcessEnv;
138
+ }): {
139
+ tokenSource: CognitoTokenSource;
140
+ actorKind: CognitoActorKind;
141
+ };
142
+ /**
143
+ * Run `fn` with `HQ_STATE_DIR` pointed at the machine/daemon token cache so
144
+ * hq-cloud's mint writes never touch the person login file.
145
+ *
146
+ * When `HQ_STATE_DIR` is already set (tests / explicit callers), leave it alone
147
+ * so a pre-seeded machine session is still found — only redirect when unset.
148
+ */
149
+ export declare function withMachineTokenStateDir<T>(fn: () => Promise<T>, opts?: {
150
+ home?: string;
151
+ env?: NodeJS.ProcessEnv;
152
+ }): Promise<T>;
76
153
  /**
77
154
  * Return a non-expired Cognito access token, refreshing or browser-logging-in
78
- * as needed. Cache lives at ~/.hq/cognito-tokens.json.
155
+ * as needed. Person cache lives at ~/.hq/cognito-tokens.json. Machine minting
156
+ * is opt-in: `HQ_MACHINE_CREDS_FILE` explicitly set, or
157
+ * `{ tokenSource: "machine" }` — then USER_PASSWORD_AUTH and (when
158
+ * `HQ_STATE_DIR` is unset) a cache under the daemon state dir.
79
159
  *
80
160
  * Pass `interactive: false` from automated contexts (e.g. the `hq-auth-refresh`
81
161
  * bin invoked by the deploy skill) where failing fast is better than opening
82
162
  * a browser.
83
163
  */
84
- export declare function ensureCognitoToken(options?: {
85
- interactive?: boolean;
86
- }): Promise<string>;
164
+ export declare function ensureCognitoToken(options?: EnsureCognitoTokenOptions): Promise<string>;
87
165
  /**
88
166
  * Return a non-expired Cognito **ID** token (mirrors `ensureCognitoToken`'s
89
167
  * refresh/login flow but yields the id_token, not the access token).
@@ -95,9 +173,7 @@ export declare function ensureCognitoToken(options?: {
95
173
  * identities the vault API already authenticates with the ID token, so this is
96
174
  * identical to `ensureCognitoToken` there.
97
175
  */
98
- export declare function ensureCognitoIdToken(options?: {
99
- interactive?: boolean;
100
- }): Promise<string>;
176
+ export declare function ensureCognitoIdToken(options?: EnsureCognitoTokenOptions): Promise<string>;
101
177
  /**
102
178
  * The CLI's own ClientInfo. Resolved once at module load so every VaultClient
103
179
  * built in this process stamps the same name/version onto its requests.
@@ -117,7 +193,7 @@ export declare function buildVaultConfig(authToken: string): VaultServiceConfig;
117
193
  * means no valid session could be ensured non-interactively; the caller gets a
118
194
  * reason string and can decide what to do next. Never opens a browser.
119
195
  */
120
- export declare function refreshCachedSession(): Promise<{
196
+ export declare function refreshCachedSession(options?: Pick<EnsureCognitoTokenOptions, "tokenSource">): Promise<{
121
197
  refreshed: boolean;
122
198
  reason?: string;
123
199
  }>;
@@ -25,9 +25,17 @@ import * as yaml from "js-yaml";
25
25
  import chalk from "chalk";
26
26
  import { loadCachedTokens, isExpiring, refreshTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, } from "@indigoai-us/hq-cloud";
27
27
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
28
+ import { daemonDir } from "../lib/mesh/live/daemon/paths.js";
29
+ import { workMeshRoot } from "../lib/mesh/live/paths.js";
28
30
  // Re-export the cached-session primitives for callers that must deliberately
29
31
  // avoid the refresh/login behavior in the higher-level helpers below.
30
32
  export { isExpiring, isMachineIdentity, loadCachedTokens };
33
+ /**
34
+ * Override for where machine-minted Cognito tokens are cached.
35
+ * Default: the work-mesh daemon state dir (`~/.hq/work-mesh/daemon`), so a
36
+ * machine mint never overwrites a person's `~/.hq/cognito-tokens.json`.
37
+ */
38
+ export const HQ_MACHINE_TOKEN_STATE_DIR_ENV = "HQ_MACHINE_TOKEN_STATE_DIR";
31
39
  export const DEFAULT_COGNITO = {
32
40
  region: process.env.AWS_REGION ?? "us-east-1",
33
41
  userPoolDomain: process.env.HQ_COGNITO_DOMAIN ?? "vault-indigo-hq-prod",
@@ -223,9 +231,206 @@ function isHqRoot(dir) {
223
231
  }
224
232
  }
225
233
  export const DEFAULT_HQ_ROOT = resolveDefaultHqRoot();
234
+ /**
235
+ * Directory used as `HQ_STATE_DIR` while minting/caching machine Cognito
236
+ * tokens. Never the person's `~/.hq` — agent boxes write under the daemon
237
+ * state dir so a mint cannot clobber a human login cache.
238
+ */
239
+ export function machineTokenStateDir(home = os.homedir(), env = process.env) {
240
+ const override = env[HQ_MACHINE_TOKEN_STATE_DIR_ENV]?.trim();
241
+ if (override)
242
+ return override;
243
+ return daemonDir(workMeshRoot(home, env), home, env);
244
+ }
245
+ /** Absolute path of the machine-token cache file (daemon state dir). */
246
+ export function machineTokenCacheFile(home = os.homedir(), env = process.env) {
247
+ return path.join(machineTokenStateDir(home, env), "cognito-tokens.json");
248
+ }
249
+ /**
250
+ * True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
251
+ * The mere presence of `~/.hq-agent/machine-creds.json` is NOT enough — minting
252
+ * and doctor "machine" reporting are opt-in via this env (systemd unit) or an
253
+ * explicit `{ tokenSource: "machine" }` caller option.
254
+ */
255
+ export function isMachineCredsFileEnvSet(env = process.env) {
256
+ const raw = env.HQ_MACHINE_CREDS_FILE;
257
+ return typeof raw === "string" && raw.trim().length > 0;
258
+ }
259
+ /**
260
+ * Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
261
+ * Opt-in only: explicit `{ tokenSource: "machine" }`, or `HQ_MACHINE_CREDS_FILE`
262
+ * set in the environment. Default-path `~/.hq-agent/machine-creds.json` alone
263
+ * never flips other CLI commands onto the machine path.
264
+ */
265
+ export function wantsMachineCognitoTokens(options = {}, env = process.env) {
266
+ if (options.tokenSource === "person")
267
+ return false;
268
+ if (options.tokenSource === "machine") {
269
+ return isMachineIdentity();
270
+ }
271
+ if (!isMachineCredsFileEnvSet(env))
272
+ return false;
273
+ // Align process.env for hq-cloud's isMachineIdentity() when a synthetic env
274
+ // is supplied (doctor / tests).
275
+ const prevCreds = process.env.HQ_MACHINE_CREDS_FILE;
276
+ const nextCreds = env.HQ_MACHINE_CREDS_FILE;
277
+ if (nextCreds !== prevCreds) {
278
+ if (nextCreds === undefined)
279
+ delete process.env.HQ_MACHINE_CREDS_FILE;
280
+ else
281
+ process.env.HQ_MACHINE_CREDS_FILE = nextCreds;
282
+ }
283
+ try {
284
+ return isMachineIdentity();
285
+ }
286
+ finally {
287
+ if (nextCreds !== prevCreds) {
288
+ if (prevCreds === undefined)
289
+ delete process.env.HQ_MACHINE_CREDS_FILE;
290
+ else
291
+ process.env.HQ_MACHINE_CREDS_FILE = prevCreds;
292
+ }
293
+ }
294
+ }
295
+ /**
296
+ * Which Cognito token source this process will use for doctor / reporting:
297
+ * machine only when `HQ_MACHINE_CREDS_FILE` is explicitly set and readable;
298
+ * otherwise the person login cache. Default-path machine-creds alone → person.
299
+ */
300
+ export function resolveCognitoTokenSource(env = process.env) {
301
+ return wantsMachineCognitoTokens({}, env) ? "machine" : "person";
302
+ }
303
+ function decodeJwtPayload(token) {
304
+ if (!token)
305
+ return null;
306
+ const parts = token.split(".");
307
+ if (parts.length < 2 || !parts[1])
308
+ return null;
309
+ try {
310
+ const json = Buffer.from(parts[1], "base64url").toString("utf8");
311
+ const parsed = JSON.parse(json);
312
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
313
+ return null;
314
+ return parsed;
315
+ }
316
+ catch {
317
+ return null;
318
+ }
319
+ }
320
+ /** Derive actor kind from an ID token's custom:entity* claims (no verify). */
321
+ export function actorKindFromIdToken(idToken) {
322
+ const claims = decodeJwtPayload(idToken);
323
+ if (!claims)
324
+ return "unknown";
325
+ const entityType = claims["custom:entityType"];
326
+ const entityUid = claims["custom:entityUid"];
327
+ if (entityType === "agent" || (typeof entityUid === "string" && entityUid.startsWith("agt_"))) {
328
+ return "agent";
329
+ }
330
+ if (entityType === "outpost" || (typeof entityUid === "string" && entityUid.startsWith("out_"))) {
331
+ return "unknown";
332
+ }
333
+ return "human";
334
+ }
335
+ /**
336
+ * Peek the machine-token cache under the daemon state dir without minting.
337
+ * Used by doctor; never logs token material.
338
+ */
339
+ export function loadMachineCachedTokens(home = os.homedir(), env = process.env) {
340
+ const file = machineTokenCacheFile(home, env);
341
+ try {
342
+ if (!fs.existsSync(file))
343
+ return null;
344
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
345
+ if (typeof raw.accessToken !== "string" ||
346
+ typeof raw.idToken !== "string" ||
347
+ raw.accessToken.length === 0 ||
348
+ raw.idToken.length === 0) {
349
+ return null;
350
+ }
351
+ return raw;
352
+ }
353
+ catch {
354
+ return null;
355
+ }
356
+ }
357
+ /**
358
+ * Report token source + actor kind for `hq mesh daemon doctor`. Does not mint
359
+ * or hit the network. Machine identity without a readable cache still reports
360
+ * actorKind `agent` (the box identity signal).
361
+ */
362
+ export function describeCognitoTokenSource(opts = {}) {
363
+ const home = opts.home ?? os.homedir();
364
+ const env = opts.env ?? process.env;
365
+ const tokenSource = resolveCognitoTokenSource(env);
366
+ if (tokenSource === "machine") {
367
+ const cached = loadMachineCachedTokens(home, env);
368
+ const fromToken = actorKindFromIdToken(cached?.idToken);
369
+ return {
370
+ tokenSource,
371
+ actorKind: fromToken === "unknown" ? "agent" : fromToken,
372
+ };
373
+ }
374
+ // Honor HQ_STATE_DIR from the supplied env so doctor stays hermetic in tests.
375
+ const prevState = process.env.HQ_STATE_DIR;
376
+ const nextState = env.HQ_STATE_DIR;
377
+ if (nextState !== prevState) {
378
+ if (nextState === undefined)
379
+ delete process.env.HQ_STATE_DIR;
380
+ else
381
+ process.env.HQ_STATE_DIR = nextState;
382
+ }
383
+ try {
384
+ const person = loadCachedTokens();
385
+ return {
386
+ tokenSource,
387
+ actorKind: actorKindFromIdToken(person?.idToken),
388
+ };
389
+ }
390
+ finally {
391
+ if (nextState !== prevState) {
392
+ if (prevState === undefined)
393
+ delete process.env.HQ_STATE_DIR;
394
+ else
395
+ process.env.HQ_STATE_DIR = prevState;
396
+ }
397
+ }
398
+ }
399
+ /**
400
+ * Run `fn` with `HQ_STATE_DIR` pointed at the machine/daemon token cache so
401
+ * hq-cloud's mint writes never touch the person login file.
402
+ *
403
+ * When `HQ_STATE_DIR` is already set (tests / explicit callers), leave it alone
404
+ * so a pre-seeded machine session is still found — only redirect when unset.
405
+ */
406
+ export async function withMachineTokenStateDir(fn, opts = {}) {
407
+ const home = opts.home ?? os.homedir();
408
+ const env = opts.env ?? process.env;
409
+ const existing = env.HQ_STATE_DIR?.trim() || process.env.HQ_STATE_DIR?.trim() || "";
410
+ if (existing) {
411
+ return fn();
412
+ }
413
+ const prev = process.env.HQ_STATE_DIR;
414
+ process.env.HQ_STATE_DIR = machineTokenStateDir(home, env);
415
+ try {
416
+ return await fn();
417
+ }
418
+ finally {
419
+ if (prev === undefined)
420
+ delete process.env.HQ_STATE_DIR;
421
+ else
422
+ process.env.HQ_STATE_DIR = prev;
423
+ }
424
+ }
425
+ async function ensureMachineTokens() {
426
+ return withMachineTokenStateDir(() => getValidMachineTokens(DEFAULT_COGNITO));
427
+ }
226
428
  /**
227
429
  * Return a non-expired Cognito access token, refreshing or browser-logging-in
228
- * as needed. Cache lives at ~/.hq/cognito-tokens.json.
430
+ * as needed. Person cache lives at ~/.hq/cognito-tokens.json. Machine minting
431
+ * is opt-in: `HQ_MACHINE_CREDS_FILE` explicitly set, or
432
+ * `{ tokenSource: "machine" }` — then USER_PASSWORD_AUTH and (when
433
+ * `HQ_STATE_DIR` is unset) a cache under the daemon state dir.
229
434
  *
230
435
  * Pass `interactive: false` from automated contexts (e.g. the `hq-auth-refresh`
231
436
  * bin invoked by the deploy skill) where failing fast is better than opening
@@ -240,9 +445,10 @@ export async function ensureCognitoToken(options = {}) {
240
445
  // so vault-API calls from a machine identity send the ID token. The cached
241
446
  // token file keeps correct field semantics (real access token in
242
447
  // accessToken) for consumers that need token_use=access, e.g. the deploy
243
- // API via the deploy skill.
244
- if (isMachineIdentity()) {
245
- const machine = await getValidMachineTokens(DEFAULT_COGNITO);
448
+ // API via the deploy skill. Cache path: daemon state dir when HQ_STATE_DIR
449
+ // is unset, never a silent overwrite of a person's ~/.hq.
450
+ if (wantsMachineCognitoTokens(options)) {
451
+ const machine = await ensureMachineTokens();
246
452
  return machine.idToken;
247
453
  }
248
454
  const cached = loadCachedTokens();
@@ -283,8 +489,8 @@ export async function ensureCognitoToken(options = {}) {
283
489
  */
284
490
  export async function ensureCognitoIdToken(options = {}) {
285
491
  const interactive = options.interactive ?? true;
286
- if (isMachineIdentity()) {
287
- const machine = await getValidMachineTokens(DEFAULT_COGNITO);
492
+ if (wantsMachineCognitoTokens(options)) {
493
+ const machine = await ensureMachineTokens();
288
494
  return machine.idToken;
289
495
  }
290
496
  const cached = loadCachedTokens();
@@ -342,12 +548,13 @@ export function buildVaultConfig(authToken) {
342
548
  * means no valid session could be ensured non-interactively; the caller gets a
343
549
  * reason string and can decide what to do next. Never opens a browser.
344
550
  */
345
- export async function refreshCachedSession() {
551
+ export async function refreshCachedSession(options = {}) {
346
552
  // Machine identities have no refresh token; ensure a valid cached machine
347
553
  // session without forcing a re-mint when the cache is already healthy.
348
- if (isMachineIdentity()) {
554
+ // Tokens land under the daemon state dir when HQ_STATE_DIR is unset.
555
+ if (wantsMachineCognitoTokens(options)) {
349
556
  try {
350
- await getValidMachineTokens(DEFAULT_COGNITO);
557
+ await ensureMachineTokens();
351
558
  return { refreshed: true };
352
559
  }
353
560
  catch (err) {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The actionable remedy shown to the caller. Input-free — nothing is
3
+ * interpolated from the error, the argv, or qmd's output — so there is no
4
+ * injection surface. The disposition holds for every reading of a vanished
5
+ * `.ipull` partial (a concurrent finisher, a cache eviction, a partially-written
6
+ * file): it is transient and retryable, and re-running resumes the download.
7
+ */
8
+ export declare const QMD_MODEL_DOWNLOAD_REMEDY: string;
9
+ /**
10
+ * True when `err` is a qmd interrupted-model-download failure: qmd's OWN captured
11
+ * streams carry BOTH an ENOENT-rename signature AND a `.ipull` partial-download
12
+ * marker. Requiring both keeps the gate narrow — a durable ENOSPC/EACCES/network
13
+ * failure matches neither the ENOENT code nor (for ENOSPC/EACCES) is it a
14
+ * transient race, so it stays a reportable QmdExitError. finishRunQmd calls this
15
+ * on the raw streams to type the condition {@link
16
+ * import('../lib/search-index/index.js').QmdModelDownloadError}.
17
+ */
18
+ export declare function isQmdModelDownloadError(err: unknown): boolean;
19
+ /**
20
+ * If `err` is a caller-/build-driven qmd model-download failure (a
21
+ * QmdModelDownloadError from `query`/`vsearch`/`embed`), return the actionable,
22
+ * input-free retry remedy; otherwise return `null`. Mirrors qmdLlmDisabledMessage
23
+ * so the top-level handler branches the same way: a non-null result means
24
+ * print-and-skip-Sentry, null means "handle as usual (capture to Sentry)".
25
+ */
26
+ export declare function qmdModelDownloadMessage(err: unknown): string | null;
27
+ //# sourceMappingURL=qmd-model-download-error.d.ts.map
@@ -0,0 +1,114 @@
1
+ // src/utils/qmd-model-download-error.ts
2
+ //
3
+ // Classify a qmd failure caused by an INTERRUPTED MODEL DOWNLOAD — qmd crashed
4
+ // with ENOENT while renaming its own partial `.ipull` download file into place —
5
+ // rather than an hq-cli code defect. The partial file vanished before the rename
6
+ // ran, the signature of ANOTHER qmd process on the same box finishing (or
7
+ // clearing) the same download first. That is a transient, retryable LOCAL-CACHE
8
+ // race hq cannot fix in code — hq cannot coordinate qmd children other users
9
+ // start on the same host — so the CLI surfaces an actionable "re-run, it
10
+ // resumes" remedy and SKIPS Sentry capture. Sibling of qmd-llm-disabled-error.ts
11
+ // (HQ-CLI 7688850003) and qmd-store-missing-error.ts (HQ-CLI-16): a failure that
12
+ // is NOT an hq-cli defect is printed with an actionable message and never filed
13
+ // as a crash.
14
+ //
15
+ // HQ-CLI-1B (Sentry indigo-d0/hq-cli 7705711476, the newest event): `qmd query`
16
+ // reached its reranking stage, its reranker-model downloader crashed uncaught
17
+ // with `[Error: ENOENT: no such file or directory, rename '<cache>/…-reranker-…
18
+ // .gguf.ipull' -> '<cache>/…-reranker-….gguf'] { errno: -2, code: 'ENOENT',
19
+ // syscall: 'rename' }`, and qmd exited 1. hq-cli had no classifier for that
20
+ // wording, so it took the same fall-through to capture as the query-document
21
+ // crash — and because both share the (QmdExitError, qmd:query, exit:1)
22
+ // fingerprint, an M1-only fix would leave this issue alive.
23
+ //
24
+ // The gate is deliberately narrow so it can neither be tripped by user input nor
25
+ // silence a real bug:
26
+ // 1. CLASS: only a QmdModelDownloadError (the typed subclass finishRunQmd
27
+ // raises) qualifies — never a plain QmdExitError carrying the same wording.
28
+ // 2. INVOCATION: only the reads that fetch a model on demand — the caller
29
+ // reads query/vsearch (query expansion + reranking) and the index build
30
+ // embed. Any other subcommand returns null and stays captured.
31
+ // 3. SIGNATURE: BOTH the ENOENT-rename shape AND the `.ipull` partial-download
32
+ // marker are required. A durable download failure (a full disk / ENOSPC, a
33
+ // permission error / EACCES, a network fault) has a DIFFERENT signature and
34
+ // stays a reportable QmdExitError, so this never silences a real fault.
35
+ // The predicate reads qmd's OWN captured streams, never the synthesized message.
36
+ /** ipull's suffix for an in-flight model download's partial file. */
37
+ const IPULL_PARTIAL = /\.ipull\b/i;
38
+ /** better-sqlite3-free: the ENOENT rename shape as qmd prints its MESSAGE form. */
39
+ const ENOENT_RENAME_MESSAGE = /ENOENT:\s*no such file or directory,\s*rename/i;
40
+ /** The ENOENT code/errno as the PROPERTIES form prints it (`code: 'ENOENT'` / `errno: -2`). */
41
+ const ENOENT_TOKEN = /\bENOENT\b|errno:\s*-2\b/i;
42
+ /** The rename syscall as the properties form prints it (`syscall: 'rename'`). */
43
+ const RENAME_SYSCALL = /syscall:\s*['"]rename['"]/i;
44
+ /** Reads that fetch a model on demand: query/vsearch expand + rerank, embed builds vectors. */
45
+ const MODEL_FETCH_READS = new Set(["query", "vsearch", "embed"]);
46
+ /**
47
+ * The actionable remedy shown to the caller. Input-free — nothing is
48
+ * interpolated from the error, the argv, or qmd's output — so there is no
49
+ * injection surface. The disposition holds for every reading of a vanished
50
+ * `.ipull` partial (a concurrent finisher, a cache eviction, a partially-written
51
+ * file): it is transient and retryable, and re-running resumes the download.
52
+ */
53
+ export const QMD_MODEL_DOWNLOAD_REMEDY = "hq's local search couldn't finish downloading a model it needs: the partial " +
54
+ "download file disappeared before it could be saved, which usually means " +
55
+ "another hq or qmd process on this machine finished or cleared the same " +
56
+ "download. Re-run the command — the download resumes.";
57
+ /**
58
+ * qmd's OWN captured streams (stderr then stdout), joined. The synthesized
59
+ * `message` is deliberately NOT consulted: it embeds the caller's qmd arguments,
60
+ * so reading it would let a user query for this phrase trip the classifier. A
61
+ * bare string is treated as captured text directly (test convenience). A value
62
+ * with no captured streams yields "".
63
+ */
64
+ function capturedStreams(err) {
65
+ if (typeof err === "string")
66
+ return err;
67
+ if (err === null || typeof err !== "object")
68
+ return "";
69
+ const record = err;
70
+ const stderr = typeof record.stderr === "string" ? record.stderr : "";
71
+ const stdout = typeof record.stdout === "string" ? record.stdout : "";
72
+ return stderr || stdout ? `${stderr}\n${stdout}` : "";
73
+ }
74
+ /** True when `text` carries an ENOENT rename, in either qmd's message or props form. */
75
+ function hasEnoentRename(text) {
76
+ if (ENOENT_RENAME_MESSAGE.test(text))
77
+ return true;
78
+ return ENOENT_TOKEN.test(text) && RENAME_SYSCALL.test(text);
79
+ }
80
+ /**
81
+ * True when `err` is a qmd interrupted-model-download failure: qmd's OWN captured
82
+ * streams carry BOTH an ENOENT-rename signature AND a `.ipull` partial-download
83
+ * marker. Requiring both keeps the gate narrow — a durable ENOSPC/EACCES/network
84
+ * failure matches neither the ENOENT code nor (for ENOSPC/EACCES) is it a
85
+ * transient race, so it stays a reportable QmdExitError. finishRunQmd calls this
86
+ * on the raw streams to type the condition {@link
87
+ * import('../lib/search-index/index.js').QmdModelDownloadError}.
88
+ */
89
+ export function isQmdModelDownloadError(err) {
90
+ const text = capturedStreams(err);
91
+ if (!text)
92
+ return false;
93
+ return hasEnoentRename(text) && IPULL_PARTIAL.test(text);
94
+ }
95
+ /**
96
+ * If `err` is a caller-/build-driven qmd model-download failure (a
97
+ * QmdModelDownloadError from `query`/`vsearch`/`embed`), return the actionable,
98
+ * input-free retry remedy; otherwise return `null`. Mirrors qmdLlmDisabledMessage
99
+ * so the top-level handler branches the same way: a non-null result means
100
+ * print-and-skip-Sentry, null means "handle as usual (capture to Sentry)".
101
+ */
102
+ export function qmdModelDownloadMessage(err) {
103
+ if (err === null || typeof err !== "object")
104
+ return null;
105
+ const record = err;
106
+ if (record.name !== "QmdModelDownloadError")
107
+ return null;
108
+ const args = Array.isArray(record.args) ? record.args : [];
109
+ const subcommand = args[0];
110
+ if (typeof subcommand !== "string" || !MODEL_FETCH_READS.has(subcommand))
111
+ return null;
112
+ return QMD_MODEL_DOWNLOAD_REMEDY;
113
+ }
114
+ //# sourceMappingURL=qmd-model-download-error.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The actionable remedy shown to the caller. Input-free: nothing is interpolated
3
+ * from the error, the argv, or qmd's output, so there is no injection surface —
4
+ * matching the bounded-fingerprint discipline of its siblings.
5
+ */
6
+ export declare const QMD_QUERY_DOCUMENT_REMEDY: string;
7
+ /**
8
+ * True when `err` carries, in qmd's OWN captured streams, a query-document
9
+ * syntax diagnostic — i.e. qmd's `query` parser rejected the positional as a
10
+ * malformed document. finishRunQmd calls this on the raw streams to type the
11
+ * condition {@link import('../lib/search-index/index.js').QmdQueryDocumentError}.
12
+ */
13
+ export declare function isQmdQueryDocumentError(err: unknown): boolean;
14
+ /**
15
+ * If `err` is a caller-driven qmd query-document failure (a QmdQueryDocumentError
16
+ * from `hq search --mode hybrid`, i.e. the `query` read), return the actionable,
17
+ * input-free remedy; otherwise return `null`. Mirrors qmdLlmDisabledMessage /
18
+ * qmdModuleMissingMessage so the top-level handler branches the same way: a
19
+ * non-null result means print-and-skip-Sentry, null means "handle as usual
20
+ * (capture to Sentry)".
21
+ */
22
+ export declare function qmdQueryDocumentMessage(err: unknown): string | null;
23
+ //# sourceMappingURL=qmd-query-document-error.d.ts.map