@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.
- package/CHANGELOG.md +77 -0
- package/assets/mesh-daemon/README.md +23 -0
- package/assets/mesh-daemon/launchd.plist.template +33 -0
- package/assets/mesh-daemon/systemd.system.service.template +27 -0
- package/assets/mesh-daemon/systemd.user.service.template +18 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
- package/dist/commands/core.js +5 -4
- package/dist/commands/mesh.d.ts +15 -0
- package/dist/commands/mesh.js +732 -4
- package/dist/commands/search.d.ts +23 -0
- package/dist/commands/search.js +42 -1
- package/dist/lib/doctor/checks/work-context.d.ts +23 -0
- package/dist/lib/doctor/checks/work-context.js +195 -0
- package/dist/lib/doctor/registry.js +4 -0
- package/dist/lib/mesh/api.d.ts +18 -0
- package/dist/lib/mesh/api.js +55 -0
- package/dist/lib/mesh/cache.js +35 -4
- package/dist/lib/mesh/client.d.ts +118 -0
- package/dist/lib/mesh/client.js +268 -0
- package/dist/lib/mesh/live/backoff.d.ts +16 -0
- package/dist/lib/mesh/live/backoff.js +20 -0
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
- package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
- package/dist/lib/mesh/live/daemon/credentials.js +227 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/doctor.js +139 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/index.js +14 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
- package/dist/lib/mesh/live/daemon/install.js +330 -0
- package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
- package/dist/lib/mesh/live/daemon/log.js +79 -0
- package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/paths.js +31 -0
- package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
- package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
- package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
- package/dist/lib/mesh/live/daemon/presence.js +250 -0
- package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/presign.js +65 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
- package/dist/lib/mesh/live/daemon/run.js +400 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/state.js +57 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
- package/dist/lib/mesh/live/enqueue.d.ts +48 -0
- package/dist/lib/mesh/live/enqueue.js +100 -0
- package/dist/lib/mesh/live/flush.d.ts +56 -0
- package/dist/lib/mesh/live/flush.js +360 -0
- package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
- package/dist/lib/mesh/live/format-spool-line.js +106 -0
- package/dist/lib/mesh/live/index.d.ts +22 -0
- package/dist/lib/mesh/live/index.js +16 -0
- package/dist/lib/mesh/live/paths.d.ts +13 -0
- package/dist/lib/mesh/live/paths.js +30 -0
- package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
- package/dist/lib/mesh/live/session-events-client.js +120 -0
- package/dist/lib/mesh/live/session-identity.d.ts +39 -0
- package/dist/lib/mesh/live/session-identity.js +91 -0
- package/dist/lib/mesh/live/spool.d.ts +52 -0
- package/dist/lib/mesh/live/spool.js +193 -0
- package/dist/lib/mesh/live/ulid.d.ts +12 -0
- package/dist/lib/mesh/live/ulid.js +39 -0
- package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
- package/dist/lib/mesh/live/validate-session-event.js +128 -0
- package/dist/lib/search-index/index.d.ts +45 -0
- package/dist/lib/search-index/index.js +76 -0
- package/dist/lib/work-context/atomic.d.ts +18 -0
- package/dist/lib/work-context/atomic.js +132 -0
- package/dist/lib/work-context/company.d.ts +103 -0
- package/dist/lib/work-context/company.js +274 -0
- package/dist/lib/work-context/config.d.ts +59 -0
- package/dist/lib/work-context/config.js +172 -0
- package/dist/lib/work-context/contract.d.ts +81 -0
- package/dist/lib/work-context/contract.js +65 -0
- package/dist/lib/work-context/errors.d.ts +27 -0
- package/dist/lib/work-context/errors.js +46 -0
- package/dist/lib/work-context/index.d.ts +23 -0
- package/dist/lib/work-context/index.js +20 -0
- package/dist/lib/work-context/migrate.d.ts +84 -0
- package/dist/lib/work-context/migrate.js +287 -0
- package/dist/lib/work-context/organize.d.ts +130 -0
- package/dist/lib/work-context/organize.js +471 -0
- package/dist/lib/work-context/outbox.d.ts +97 -0
- package/dist/lib/work-context/outbox.js +357 -0
- package/dist/lib/work-context/paths.d.ts +14 -0
- package/dist/lib/work-context/paths.js +39 -0
- package/dist/lib/work-context/project.d.ts +78 -0
- package/dist/lib/work-context/project.js +247 -0
- package/dist/lib/work-context/reconcile.d.ts +85 -0
- package/dist/lib/work-context/reconcile.js +651 -0
- package/dist/lib/work-context/repo-remote.d.ts +43 -0
- package/dist/lib/work-context/repo-remote.js +200 -0
- package/dist/lib/work-context/state.d.ts +112 -0
- package/dist/lib/work-context/state.js +162 -0
- package/dist/main.js +40 -3
- package/dist/utils/cognito-session.d.ts +85 -9
- package/dist/utils/cognito-session.js +216 -9
- package/dist/utils/qmd-model-download-error.d.ts +27 -0
- package/dist/utils/qmd-model-download-error.js +114 -0
- package/dist/utils/qmd-query-document-error.d.ts +23 -0
- package/dist/utils/qmd-query-document-error.js +117 -0
- package/dist/utils/sentry-fingerprint.js +2 -0
- 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.
|
|
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.
|
|
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
|
-
|
|
245
|
-
|
|
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 (
|
|
287
|
-
const machine = await
|
|
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
|
-
|
|
554
|
+
// Tokens land under the daemon state dir when HQ_STATE_DIR is unset.
|
|
555
|
+
if (wantsMachineCognitoTokens(options)) {
|
|
349
556
|
try {
|
|
350
|
-
await
|
|
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
|