@indigoai-us/hq-cli 5.345.148 → 5.345.150
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 +75 -0
- package/README.md +2 -0
- package/dist/command-catalog.generated.d.ts +62 -14
- package/dist/command-catalog.generated.js +74 -14
- package/dist/command-help.js +2 -1
- package/dist/commands/_patterns.d.ts +6 -0
- package/dist/commands/_patterns.js +6 -0
- package/dist/commands/access.d.ts +45 -2
- package/dist/commands/access.js +537 -223
- package/dist/commands/billing-usage.d.ts +33 -2
- package/dist/commands/billing-usage.js +75 -4
- package/dist/commands/bot.js +2 -2
- package/dist/commands/channels.d.ts +27 -0
- package/dist/commands/channels.js +108 -4
- package/dist/commands/core-refresh-vault-access-native.d.ts +16 -0
- package/dist/commands/core-refresh-vault-access-native.js +142 -9
- package/dist/commands/core.js +4 -1
- package/dist/commands/dm.js +9 -1
- package/dist/commands/groups.js +9 -6
- package/dist/commands/install-global.d.ts +1 -8
- package/dist/commands/install-global.js +11 -27
- package/dist/commands/lanes-questions.js +7 -4
- package/dist/commands/lanes.js +13 -6
- package/dist/commands/library-add.d.ts +31 -1
- package/dist/commands/library-add.js +69 -2
- package/dist/commands/library.js +37 -3
- package/dist/commands/members.d.ts +35 -0
- package/dist/commands/members.js +65 -14
- package/dist/commands/reindex.js +19 -0
- package/dist/commands/secrets.d.ts +5 -0
- package/dist/commands/secrets.js +11 -6
- package/dist/hook-engine-path.js +10 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +63282 -62480
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +70 -5
- package/dist/lazy-commands.js +2 -0
- package/dist/lib/billing/usage-cost.d.ts +31 -1
- package/dist/lib/billing/usage-cost.js +46 -0
- package/dist/lib/db/local.d.ts +2 -2
- package/dist/lib/db/local.js +2 -2
- package/dist/lib/db/sql.js +2 -2
- package/dist/lib/db/sqlite-binding.d.ts +22 -0
- package/dist/lib/db/sqlite-binding.js +168 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +12 -0
- package/dist/lib/doctor/checks/anywhere.js +63 -12
- package/dist/lib/doctor/checks/cli-path.d.ts +2 -1
- package/dist/lib/doctor/checks/cli-path.js +2 -1
- package/dist/lib/doctor/checks/hook-engine.js +2 -1
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/reconcile.d.ts +9 -0
- package/dist/lib/hook-engine/reconcile.js +134 -0
- package/dist/lib/hook-engine/registration.d.ts +17 -0
- package/dist/lib/hook-engine/registration.js +50 -0
- package/dist/lib/install/claude-targets.d.ts +2 -1
- package/dist/lib/install/claude-targets.js +8 -3
- package/dist/lib/install/codex-targets.d.ts +3 -0
- package/dist/lib/install/codex-targets.js +15 -4
- package/dist/lib/install/hook-target.d.ts +9 -0
- package/dist/lib/install/hook-target.js +39 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/store.d.ts +11 -0
- package/dist/lib/lanes/store.js +20 -0
- package/dist/lib/lanes/watch.js +5 -3
- package/dist/lib/windows/console-encoding.d.ts +24 -0
- package/dist/lib/windows/console-encoding.js +83 -0
- package/dist/lib/windows/install-followup.d.ts +35 -0
- package/dist/lib/windows/install-followup.js +142 -0
- package/dist/lib/windows/user-path.d.ts +80 -3
- package/dist/lib/windows/user-path.js +187 -26
- package/dist/main.js +2 -1
- package/dist/unknown-command.d.ts +18 -0
- package/dist/unknown-command.js +88 -0
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +14 -2
- package/dist/utils/hook-trust.d.ts +9 -0
- package/dist/utils/hook-trust.js +30 -2
- package/dist/utils/qmd-native-binding-error.js +3 -1
- package/dist/utils/resolve-executable.d.ts +2 -0
- package/dist/utils/resolve-executable.js +6 -2
- package/package.json +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
1
|
+
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.90","bundleSha256":"cb49486573d2d1141ee6e18d73dde825f2b7c02074560f19504e3b1daf7eeba8","formatVersion":1}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,15 @@ import "./node-preflight.js";
|
|
|
3
3
|
import "./node-network-compat-fast.js";
|
|
4
4
|
import { isVersionRequest } from "./version-request.js";
|
|
5
5
|
import { isFastCoreRequest } from "./fast-scaffold.js";
|
|
6
|
+
/**
|
|
7
|
+
* Whether the install follow-up would read the registry or spawn the mesh
|
|
8
|
+
* daemon. False for fast routes' callers, and for commands that are not run
|
|
9
|
+
* from an npm install: the PATH repair needs the npm shim folder above
|
|
10
|
+
* node_modules, and the daemon step needs the installed package. Inlined so
|
|
11
|
+
* those routes do not load user-path.ts. Keep the skip rules aligned with
|
|
12
|
+
* install-followup.ts.
|
|
13
|
+
*/
|
|
14
|
+
export declare function installFollowupWouldDoWork(argv: readonly string[], env: NodeJS.ProcessEnv, platform: NodeJS.Platform): boolean;
|
|
6
15
|
type PackageUseLeaseLoader = () => Promise<void>;
|
|
7
16
|
type PackageUseUpdateProbe = () => Promise<boolean>;
|
|
8
17
|
/** Resolves true once the command was re-run (exit code already set). */
|
|
@@ -18,6 +27,7 @@ declare function runCliEntry(entryUrl: string, dispatch: () => void, loadLease?:
|
|
|
18
27
|
export declare const __test__: {
|
|
19
28
|
isVersionRequest: typeof isVersionRequest;
|
|
20
29
|
isFastCoreRequest: typeof isFastCoreRequest;
|
|
30
|
+
installFollowupWouldDoWork: typeof installFollowupWouldDoWork;
|
|
21
31
|
dispatchWithPackageUseLease: typeof dispatchWithPackageUseLease;
|
|
22
32
|
waitForUpdateThenReexec: typeof waitForUpdateThenReexec;
|
|
23
33
|
runCliEntry: typeof runCliEntry;
|
package/dist/index.js
CHANGED
|
@@ -156,6 +156,26 @@ async function runMonitorCheckFastWithRecovery(request) {
|
|
|
156
156
|
return;
|
|
157
157
|
await runMainWithInstallTreeRecovery();
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Whether the install follow-up would read the registry or spawn the mesh
|
|
161
|
+
* daemon. False for fast routes' callers, and for commands that are not run
|
|
162
|
+
* from an npm install: the PATH repair needs the npm shim folder above
|
|
163
|
+
* node_modules, and the daemon step needs the installed package. Inlined so
|
|
164
|
+
* those routes do not load user-path.ts. Keep the skip rules aligned with
|
|
165
|
+
* install-followup.ts.
|
|
166
|
+
*/
|
|
167
|
+
export function installFollowupWouldDoWork(argv, env, platform) {
|
|
168
|
+
if (env.HQ_INSTALL_FOLLOWUP === "1" || env.HQ_STAGED_INSTALL === "1")
|
|
169
|
+
return false;
|
|
170
|
+
const args = argv.slice(2).filter((arg) => arg !== "--verbose");
|
|
171
|
+
if (args[0] === "mesh" && args[1] === "daemon")
|
|
172
|
+
return false;
|
|
173
|
+
if (args[0] === "daemon" && (args[1] === "run" || args[1] === "install"))
|
|
174
|
+
return false;
|
|
175
|
+
if (platform === "win32")
|
|
176
|
+
return /[/\\]node_modules[/\\]/i.test(argv[1] ?? "");
|
|
177
|
+
return /node_modules[/\\]@indigoai-us[/\\]hq-cli[/\\]/i.test(argv[1] ?? "");
|
|
178
|
+
}
|
|
159
179
|
function dispatchCliEntry() {
|
|
160
180
|
// Before anything spawns: a background hq on Windows (no terminal) hides the
|
|
161
181
|
// console window of every child it starts (feedback_351a40d5).
|
|
@@ -165,6 +185,19 @@ function dispatchCliEntry() {
|
|
|
165
185
|
void runHookEnginePath(engineArgs);
|
|
166
186
|
return;
|
|
167
187
|
}
|
|
188
|
+
// cmd.exe code page 437 prints em dashes as mojibake. Pipes stay UTF-8.
|
|
189
|
+
// Loaded only on Windows so Linux fast routes do not pay for the module.
|
|
190
|
+
if (process.platform === "win32") {
|
|
191
|
+
void import("./lib/windows/console-encoding.js").then(({ installWindowsConsoleEncoding }) => {
|
|
192
|
+
installWindowsConsoleEncoding();
|
|
193
|
+
}).catch(() => undefined).then(() => {
|
|
194
|
+
dispatchCliRoutes();
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
dispatchCliRoutes();
|
|
199
|
+
}
|
|
200
|
+
function dispatchCliRoutes() {
|
|
168
201
|
if (process.platform === "win32" && coreCommandHasPathOperands(process.argv)) {
|
|
169
202
|
// Keep cygpath off non-Windows and non-path command routes.
|
|
170
203
|
process.argv = normalizeCorePathOperands(process.argv, "win32", cygpathMixedPath);
|
|
@@ -172,6 +205,37 @@ function dispatchCliEntry() {
|
|
|
172
205
|
const versionRequest = isVersionRequest(process.argv);
|
|
173
206
|
const helpRequest = !versionRequest && isCommandHelpRequest(process.argv);
|
|
174
207
|
const parsedFastCore = versionRequest || helpRequest ? null : parseFastCore(process.argv);
|
|
208
|
+
const monitorRequest = versionRequest || helpRequest ? null : parseMonitorCheckFastRequest(process.argv);
|
|
209
|
+
const secretsRequest = versionRequest || helpRequest || monitorRequest || parsedFastCore
|
|
210
|
+
? null
|
|
211
|
+
: parseSecretsEnvFastRequest(process.argv);
|
|
212
|
+
const mcpRequest = !versionRequest &&
|
|
213
|
+
!helpRequest &&
|
|
214
|
+
!monitorRequest &&
|
|
215
|
+
!parsedFastCore &&
|
|
216
|
+
!secretsRequest &&
|
|
217
|
+
isMcpServeRequest(process.argv);
|
|
218
|
+
// `--version`, hook, MCP, and secrets fast routes must not pay for PATH or
|
|
219
|
+
// daemon work, so they never start PowerShell or reg.exe. The module stays
|
|
220
|
+
// unloaded when there is nothing to do, so a Linux fast route does not
|
|
221
|
+
// import it.
|
|
222
|
+
if (!versionRequest && !monitorRequest && !parsedFastCore && !secretsRequest && !mcpRequest
|
|
223
|
+
&& installFollowupWouldDoWork(process.argv, process.env, process.platform)) {
|
|
224
|
+
void import("./lib/windows/install-followup.js").then(({ runWindowsInstallFollowup }) => {
|
|
225
|
+
try {
|
|
226
|
+
runWindowsInstallFollowup({ argv: process.argv });
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Install repair must not turn into a failed command.
|
|
230
|
+
}
|
|
231
|
+
}).catch(() => undefined).then(() => {
|
|
232
|
+
dispatchCliCommand(versionRequest, helpRequest, parsedFastCore, monitorRequest, secretsRequest, mcpRequest);
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
dispatchCliCommand(versionRequest, helpRequest, parsedFastCore, monitorRequest, secretsRequest, mcpRequest);
|
|
237
|
+
}
|
|
238
|
+
function dispatchCliCommand(versionRequest, helpRequest, parsedFastCore, monitorRequest, secretsRequest, mcpRequest) {
|
|
175
239
|
if (versionRequest) {
|
|
176
240
|
// Bare `--version` is the CLI semver only (the contract field updaters
|
|
177
241
|
// compare). `hq version` and `--json` print the labelled HQ/CLI report.
|
|
@@ -202,11 +266,11 @@ function dispatchCliEntry() {
|
|
|
202
266
|
await runMainWithInstallTreeRecovery();
|
|
203
267
|
});
|
|
204
268
|
}
|
|
205
|
-
else if (
|
|
269
|
+
else if (monitorRequest) {
|
|
206
270
|
// The Stop hook runs this every turn in every session. Answer the common
|
|
207
271
|
// no-active-lane case from the lane-record directory alone; anything the
|
|
208
272
|
// fast route cannot prove falls back to the full command graph.
|
|
209
|
-
void runMonitorCheckFastWithRecovery(
|
|
273
|
+
void runMonitorCheckFastWithRecovery(monitorRequest);
|
|
210
274
|
}
|
|
211
275
|
else if (parsedFastCore?.entry.kind === "native" &&
|
|
212
276
|
(process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
|
|
@@ -222,8 +286,8 @@ function dispatchCliEntry() {
|
|
|
222
286
|
// existing full-graph route.
|
|
223
287
|
void runNativeFastRouteWithRecovery(parsedFastCore);
|
|
224
288
|
}
|
|
225
|
-
else if (
|
|
226
|
-
const request =
|
|
289
|
+
else if (secretsRequest) {
|
|
290
|
+
const request = secretsRequest;
|
|
227
291
|
void import("./secrets-env-fast-route.js").then(async (route) => {
|
|
228
292
|
await route.dispatchSecretsEnvFastRequest(request, {
|
|
229
293
|
isEnabled: () => route.secretsEnvFastRouteEnabled(request),
|
|
@@ -237,7 +301,7 @@ function dispatchCliEntry() {
|
|
|
237
301
|
});
|
|
238
302
|
});
|
|
239
303
|
}
|
|
240
|
-
else if (
|
|
304
|
+
else if (mcpRequest) {
|
|
241
305
|
// Keep the minimal startup route; runMcpServe applies the hq-flags gate.
|
|
242
306
|
void import("./commands/mcp-serve.js").then(({ runMcpServe }) => runMcpServe());
|
|
243
307
|
}
|
|
@@ -373,6 +437,7 @@ void runCliEntry(import.meta.url, dispatchCliEntry);
|
|
|
373
437
|
export const __test__ = {
|
|
374
438
|
isVersionRequest,
|
|
375
439
|
isFastCoreRequest,
|
|
440
|
+
installFollowupWouldDoWork,
|
|
376
441
|
dispatchWithPackageUseLease,
|
|
377
442
|
waitForUpdateThenReexec,
|
|
378
443
|
runCliEntry,
|
package/dist/lazy-commands.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { COMMAND_CATALOG } from "./command-catalog.generated.js";
|
|
6
|
+
import { installUnknownCommandSuggestions } from "./unknown-command.js";
|
|
6
7
|
const commandCatalog = COMMAND_CATALOG;
|
|
7
8
|
/**
|
|
8
9
|
* One entry for every root, including groups that are assembled by several
|
|
@@ -105,6 +106,7 @@ export function findRuntimeHqRoot(start, parentOf = dirname, isRoot = (directory
|
|
|
105
106
|
export function registerCommandCatalog(program) {
|
|
106
107
|
for (const command of commandCatalog)
|
|
107
108
|
program.addCommand(commandDefinition(command));
|
|
109
|
+
installUnknownCommandSuggestions(program);
|
|
108
110
|
}
|
|
109
111
|
function commandForToken(token) {
|
|
110
112
|
return commandCatalog.find((candidate) => candidate.name === token || candidate.aliases.includes(token));
|
|
@@ -201,14 +201,44 @@ export interface UsageReport {
|
|
|
201
201
|
freeItems: string;
|
|
202
202
|
drivers: Driver[];
|
|
203
203
|
noSyncGuideUrl: string;
|
|
204
|
+
/** Set by `hq billing usage`: the account is on usage pricing, so meters are priced. */
|
|
205
|
+
onUsagePricing?: true;
|
|
204
206
|
readOnly: true;
|
|
205
207
|
}
|
|
208
|
+
/** One meter's reading with no price attached. */
|
|
209
|
+
export interface MeterReadingRow {
|
|
210
|
+
key: UsageMeterKey;
|
|
211
|
+
displayName: string;
|
|
212
|
+
/** In billed units (GB for storage). Null unless measured. */
|
|
213
|
+
value: number | null;
|
|
214
|
+
state: MeterState;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The report for an account that is not on usage pricing (a flat plan, or the
|
|
218
|
+
* server did not say). Readings only: no rates, no cost, no projected bill.
|
|
219
|
+
*/
|
|
220
|
+
export interface UsageReadingsReport {
|
|
221
|
+
scope: UsageScope;
|
|
222
|
+
source: MonthUsage["source"];
|
|
223
|
+
/** False: flat plan, meters are not billed. Null: HQ did not say. */
|
|
224
|
+
onUsagePricing: false | null;
|
|
225
|
+
period: MonthUsage["period"];
|
|
226
|
+
meters: MeterReadingRow[];
|
|
227
|
+
storage?: StorageSplit;
|
|
228
|
+
/** The one line that says why no cost is shown. */
|
|
229
|
+
notice: string;
|
|
230
|
+
readOnly: true;
|
|
231
|
+
}
|
|
232
|
+
export type BillingUsageReport = UsageReport | UsageReadingsReport;
|
|
233
|
+
export declare function isReadingsReport(r: BillingUsageReport): r is UsageReadingsReport;
|
|
234
|
+
/** Readings for an account not on usage pricing. Never prices a meter. */
|
|
235
|
+
export declare function buildReadingsReport(usage: MonthUsage, onUsagePricing: false | null): UsageReadingsReport;
|
|
206
236
|
export declare function buildReport(usage: MonthUsage, rates: {
|
|
207
237
|
rates: readonly UsageMeterRate[];
|
|
208
238
|
allowanceUsd: number;
|
|
209
239
|
source: "published" | "bundled";
|
|
210
240
|
card?: UsageCard;
|
|
211
241
|
}): UsageReport;
|
|
212
|
-
export declare function formatReport(r:
|
|
242
|
+
export declare function formatReport(r: BillingUsageReport): string;
|
|
213
243
|
export {};
|
|
214
244
|
//# sourceMappingURL=usage-cost.d.ts.map
|
|
@@ -362,6 +362,38 @@ export function findDrivers(usage, bill) {
|
|
|
362
362
|
// ── Report ──────────────────────────────────────────────────────────────────
|
|
363
363
|
/** The self-hosted / no-sync guide the usage card links (feed `signup.selfHosted.url`). */
|
|
364
364
|
export const NO_SYNC_GUIDE_URL = SELF_HOSTED_GUIDE_URL;
|
|
365
|
+
export function isReadingsReport(r) {
|
|
366
|
+
return !("bill" in r);
|
|
367
|
+
}
|
|
368
|
+
function readingsNotice(scope, onUsagePricing) {
|
|
369
|
+
if (onUsagePricing === false) {
|
|
370
|
+
return scope.kind === "company"
|
|
371
|
+
? `${scope.companySlug} is on a flat plan, so usage meters are not billed.`
|
|
372
|
+
: "Your personal account is on a flat plan, so usage meters are not billed.";
|
|
373
|
+
}
|
|
374
|
+
const who = scope.kind === "company" ? scope.companySlug : "your personal account";
|
|
375
|
+
return `HQ did not say whether ${who} is on usage pricing, so no costs are shown.`;
|
|
376
|
+
}
|
|
377
|
+
/** Readings for an account not on usage pricing. Never prices a meter. */
|
|
378
|
+
export function buildReadingsReport(usage, onUsagePricing) {
|
|
379
|
+
const byKey = new Map(usage.meters.map((m) => [m.key, m]));
|
|
380
|
+
const meters = BUNDLED_USAGE_METER_RATES.map((rate) => {
|
|
381
|
+
const reading = byKey.get(rate.key);
|
|
382
|
+
return reading && reading.state === "measured" && reading.value !== null
|
|
383
|
+
? { key: rate.key, displayName: rate.displayName, value: reading.value, state: "measured" }
|
|
384
|
+
: { key: rate.key, displayName: rate.displayName, value: null, state: reading?.state ?? "not-available" };
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
scope: usage.scope,
|
|
388
|
+
source: usage.source,
|
|
389
|
+
onUsagePricing,
|
|
390
|
+
period: usage.period,
|
|
391
|
+
meters,
|
|
392
|
+
...(usage.storage ? { storage: usage.storage } : {}),
|
|
393
|
+
notice: readingsNotice(usage.scope, onUsagePricing),
|
|
394
|
+
readOnly: true,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
365
397
|
export function buildReport(usage, rates) {
|
|
366
398
|
const bill = computeBill(usage, rates.rates, rates.allowanceUsd);
|
|
367
399
|
return {
|
|
@@ -394,7 +426,21 @@ function valueText(m) {
|
|
|
394
426
|
return `${Math.round(m.value)} vault${Math.round(m.value) === 1 ? "" : "s"}`;
|
|
395
427
|
return `${Math.round(m.value)}`;
|
|
396
428
|
}
|
|
429
|
+
function formatReadingsReport(r) {
|
|
430
|
+
const who = r.scope.kind === "company" ? r.scope.companySlug : "your personal HQ";
|
|
431
|
+
const lines = [];
|
|
432
|
+
lines.push(`Usage for ${who}, ${r.period.start.slice(0, 7)} (as of ${r.period.asOf.slice(0, 10)})`);
|
|
433
|
+
lines.push("");
|
|
434
|
+
const nameW = Math.max(...r.meters.map((m) => m.displayName.length));
|
|
435
|
+
for (const m of r.meters)
|
|
436
|
+
lines.push(` ${m.displayName.padEnd(nameW)} ${valueText(m)}`);
|
|
437
|
+
lines.push("");
|
|
438
|
+
lines.push(r.notice);
|
|
439
|
+
return lines.join("\n");
|
|
440
|
+
}
|
|
397
441
|
export function formatReport(r) {
|
|
442
|
+
if (isReadingsReport(r))
|
|
443
|
+
return formatReadingsReport(r);
|
|
398
444
|
const who = r.scope.kind === "company" ? r.scope.companySlug : "your personal HQ";
|
|
399
445
|
const month = r.period.start.slice(0, 7);
|
|
400
446
|
const lines = [];
|
package/dist/lib/db/local.d.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* File lives at ~/.hq/db/{company}/vault.db with WAL journal mode.
|
|
5
5
|
* Never returns or logs remote connection strings.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import type BetterSqlite3 from "better-sqlite3";
|
|
8
8
|
import { type LocalDbPathEnv } from "./paths.js";
|
|
9
|
-
export type LocalDb =
|
|
9
|
+
export type LocalDb = BetterSqlite3.Database;
|
|
10
10
|
export interface OpenLocalDbOptions extends LocalDbPathEnv {
|
|
11
11
|
/** When true, do not create the file if missing (throws). Default false. */
|
|
12
12
|
readonly?: boolean;
|
package/dist/lib/db/local.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Never returns or logs remote connection strings.
|
|
6
6
|
*/
|
|
7
7
|
import fs from "node:fs";
|
|
8
|
-
import
|
|
8
|
+
import { loadBetterSqlite3 } from "./sqlite-binding.js";
|
|
9
9
|
import { ensureLocalDbDir, resolveLocalDbPath, } from "./paths.js";
|
|
10
10
|
const MIGRATIONS_TABLE = "hq_schema_migrations";
|
|
11
11
|
/**
|
|
@@ -33,7 +33,7 @@ export function openLocalDb(companySlug, opts) {
|
|
|
33
33
|
}
|
|
34
34
|
ensureLocalDbDir(companySlug, opts);
|
|
35
35
|
}
|
|
36
|
-
const db = new
|
|
36
|
+
const db = new (loadBetterSqlite3())(dbPath);
|
|
37
37
|
// WAL for concurrent readers; safe for agent/CLI workloads.
|
|
38
38
|
db.pragma("journal_mode = WAL");
|
|
39
39
|
db.pragma("foreign_keys = ON");
|
package/dist/lib/db/sql.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* resolved company slug from HQ session/flags. Path overrides that open
|
|
6
6
|
* another company's DB are denied unless an explicit dangerous flag is set.
|
|
7
7
|
*/
|
|
8
|
-
import
|
|
8
|
+
import { loadBetterSqlite3 } from "./sqlite-binding.js";
|
|
9
9
|
import { openLocalDb } from "./local.js";
|
|
10
10
|
import { normalizeCompanySlugForLocalDb, resolveLocalDbPath, } from "./paths.js";
|
|
11
11
|
const WRITE_PATTERN = /^\s*(INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|ALTER|TRUNCATE|REINDEX|VACUUM|BEGIN|COMMIT|ROLLBACK)\b/i;
|
|
@@ -49,7 +49,7 @@ export function resolveSqlDbPath(opts) {
|
|
|
49
49
|
return opts.dbPathOverride;
|
|
50
50
|
}
|
|
51
51
|
function openByPath(dbPath) {
|
|
52
|
-
const db = new
|
|
52
|
+
const db = new (loadBetterSqlite3())(dbPath);
|
|
53
53
|
db.pragma("journal_mode = WAL");
|
|
54
54
|
db.pragma("foreign_keys = ON");
|
|
55
55
|
return db;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type BetterSqlite3 from "better-sqlite3";
|
|
2
|
+
import { type SpawnPlan } from "../../utils/windows-spawn.js";
|
|
3
|
+
export type BetterSqlite3Constructor = typeof BetterSqlite3;
|
|
4
|
+
export declare class SqliteBindingError extends Error {
|
|
5
|
+
constructor(cause?: unknown);
|
|
6
|
+
}
|
|
7
|
+
export interface LoadBetterSqlite3Options {
|
|
8
|
+
importDatabase?: () => BetterSqlite3Constructor;
|
|
9
|
+
/** Return true when the native binary is ready to load again. */
|
|
10
|
+
repair?: () => boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function resetSqliteBindingForTests(): void;
|
|
13
|
+
export declare function clearBetterSqlite3RequireCache(): void;
|
|
14
|
+
/**
|
|
15
|
+
* A `.js` bin runs under this Node. A `.cmd` shim goes through the escaped
|
|
16
|
+
* cmd.exe plan, so a profile path with spaces or `&` stays one argument.
|
|
17
|
+
*/
|
|
18
|
+
export declare function planRepairToolSpawn(cmd: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): SpawnPlan;
|
|
19
|
+
/** Download or build better-sqlite3's native binary inside its own package directory. */
|
|
20
|
+
export declare function repairBetterSqlite3(): boolean;
|
|
21
|
+
export declare function loadBetterSqlite3(options?: LoadBetterSqlite3Options): BetterSqlite3Constructor;
|
|
22
|
+
//# sourceMappingURL=sqlite-binding.d.ts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load better-sqlite3 without requiring the user to approve npm scripts.
|
|
3
|
+
* npm 11 skips dependency install scripts, so the prebuilt binary is often
|
|
4
|
+
* missing after `npm install -g`. Try a one-shot prebuild download from the
|
|
5
|
+
* installed package, then surface the exact allow-scripts command.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { planResolvedCommandSpawn } from "../../utils/windows-spawn.js";
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
export class SqliteBindingError extends Error {
|
|
14
|
+
constructor(cause) {
|
|
15
|
+
super("better-sqlite3 did not load. npm 11.16 and later skip its install script unless scripts are allowed, so the prebuilt binary was not downloaded. " +
|
|
16
|
+
"HQ tried to download the prebuild and it still failed. " +
|
|
17
|
+
"Fix: npm install -g @indigoai-us/hq-cli --allow-scripts=better-sqlite3");
|
|
18
|
+
this.name = "SqliteBindingError";
|
|
19
|
+
if (cause !== undefined)
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
let cached;
|
|
24
|
+
let failed;
|
|
25
|
+
let defaultRepairAttempted = false;
|
|
26
|
+
export function resetSqliteBindingForTests() {
|
|
27
|
+
cached = undefined;
|
|
28
|
+
failed = undefined;
|
|
29
|
+
defaultRepairAttempted = false;
|
|
30
|
+
}
|
|
31
|
+
function isNativeBindingError(error) {
|
|
32
|
+
const record = error;
|
|
33
|
+
const text = error instanceof Error ? `${record?.code ?? ""} ${error.message}` : String(error);
|
|
34
|
+
return /better_sqlite3\.node|could not locate the bindings file|NODE_MODULE_VERSION|was compiled against a different node|not a valid win32 application|dlopen|ERR_DLOPEN_FAILED|MODULE_NOT_FOUND/i.test(text);
|
|
35
|
+
}
|
|
36
|
+
export function clearBetterSqlite3RequireCache() {
|
|
37
|
+
for (const id of Object.keys(require.cache)) {
|
|
38
|
+
if (id.includes(`${path.sep}better-sqlite3${path.sep}`))
|
|
39
|
+
delete require.cache[id];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function defaultImport() {
|
|
43
|
+
return require("better-sqlite3");
|
|
44
|
+
}
|
|
45
|
+
function resolveTool(dir, tool) {
|
|
46
|
+
const names = process.platform === "win32" ? [`${tool}.cmd`, `${tool}.exe`, tool] : [tool];
|
|
47
|
+
const bins = [path.join(path.dirname(dir), ".bin"), path.join(dir, "node_modules", ".bin")];
|
|
48
|
+
for (const bin of bins) {
|
|
49
|
+
for (const name of names) {
|
|
50
|
+
const candidate = path.join(bin, name);
|
|
51
|
+
if (fs.existsSync(candidate))
|
|
52
|
+
return candidate;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const localRequire = createRequire(path.join(dir, "package.json"));
|
|
57
|
+
const pkgJson = localRequire.resolve(`${tool}/package.json`);
|
|
58
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf8"));
|
|
59
|
+
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.[tool];
|
|
60
|
+
if (typeof rel === "string")
|
|
61
|
+
return path.join(path.dirname(pkgJson), rel);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// The tool is not installed next to better-sqlite3.
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A `.js` bin runs under this Node. A `.cmd` shim goes through the escaped
|
|
70
|
+
* cmd.exe plan, so a profile path with spaces or `&` stays one argument.
|
|
71
|
+
*/
|
|
72
|
+
export function planRepairToolSpawn(cmd, args, env = process.env, platform = process.platform) {
|
|
73
|
+
if (cmd.endsWith(".js"))
|
|
74
|
+
return { cmd: process.execPath, args: [cmd, ...args], shell: false };
|
|
75
|
+
return planResolvedCommandSpawn(cmd, args, env, platform);
|
|
76
|
+
}
|
|
77
|
+
function runTool(cmd, args, cwd) {
|
|
78
|
+
const plan = planRepairToolSpawn(cmd, args);
|
|
79
|
+
spawnSync(plan.cmd, plan.args, {
|
|
80
|
+
cwd,
|
|
81
|
+
timeout: 60_000,
|
|
82
|
+
windowsHide: true,
|
|
83
|
+
stdio: "ignore",
|
|
84
|
+
shell: plan.shell,
|
|
85
|
+
...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Download or build better-sqlite3's native binary inside its own package directory. */
|
|
89
|
+
export function repairBetterSqlite3() {
|
|
90
|
+
let dir;
|
|
91
|
+
try {
|
|
92
|
+
dir = path.dirname(require.resolve("better-sqlite3/package.json"));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const artifact = path.join(dir, "build", "Release", "better_sqlite3.node");
|
|
98
|
+
if (fs.existsSync(artifact))
|
|
99
|
+
return true;
|
|
100
|
+
const prebuild = resolveTool(dir, "prebuild-install");
|
|
101
|
+
if (prebuild)
|
|
102
|
+
runTool(prebuild, [], dir);
|
|
103
|
+
if (fs.existsSync(artifact))
|
|
104
|
+
return true;
|
|
105
|
+
const nodeGyp = resolveTool(dir, "node-gyp");
|
|
106
|
+
if (nodeGyp)
|
|
107
|
+
runTool(nodeGyp, ["rebuild", "--release"], dir);
|
|
108
|
+
return fs.existsSync(artifact);
|
|
109
|
+
}
|
|
110
|
+
function attemptRepair(options) {
|
|
111
|
+
if (options.repair) {
|
|
112
|
+
try {
|
|
113
|
+
return options.repair();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (defaultRepairAttempted)
|
|
120
|
+
return false;
|
|
121
|
+
defaultRepairAttempted = true;
|
|
122
|
+
try {
|
|
123
|
+
return repairBetterSqlite3();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function loadBetterSqlite3(options = {}) {
|
|
130
|
+
const injected = options.importDatabase !== undefined;
|
|
131
|
+
if (!injected && cached)
|
|
132
|
+
return cached;
|
|
133
|
+
if (!injected && failed)
|
|
134
|
+
throw failed;
|
|
135
|
+
const importDatabase = options.importDatabase ?? defaultImport;
|
|
136
|
+
const load = () => {
|
|
137
|
+
const ctor = importDatabase();
|
|
138
|
+
if (!injected)
|
|
139
|
+
cached = ctor;
|
|
140
|
+
return ctor;
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
return load();
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (!isNativeBindingError(error))
|
|
147
|
+
throw error;
|
|
148
|
+
const repaired = attemptRepair(options);
|
|
149
|
+
if (!repaired) {
|
|
150
|
+
const wrapped = new SqliteBindingError(error);
|
|
151
|
+
if (!injected)
|
|
152
|
+
failed = wrapped;
|
|
153
|
+
throw wrapped;
|
|
154
|
+
}
|
|
155
|
+
if (!injected)
|
|
156
|
+
clearBetterSqlite3RequireCache();
|
|
157
|
+
try {
|
|
158
|
+
return load();
|
|
159
|
+
}
|
|
160
|
+
catch (retryError) {
|
|
161
|
+
const wrapped = new SqliteBindingError(retryError);
|
|
162
|
+
if (!injected)
|
|
163
|
+
failed = wrapped;
|
|
164
|
+
throw wrapped;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=sqlite-binding.js.map
|
|
@@ -48,10 +48,22 @@ export interface AnywhereDeps {
|
|
|
48
48
|
serviceState?: () => DaemonServiceState;
|
|
49
49
|
/** Is an hqd process alive? Default: the daemon lock names a live hqd pid. */
|
|
50
50
|
daemonRunning?: () => boolean;
|
|
51
|
+
/** Transport to report when health throws. Production uses the platform default. */
|
|
52
|
+
unreachableTransport?: "windows_named_pipe" | "unix_socket";
|
|
51
53
|
now?: () => Date;
|
|
52
54
|
}
|
|
55
|
+
export interface PipeConnectFailure {
|
|
56
|
+
code?: string;
|
|
57
|
+
detail?: string;
|
|
58
|
+
}
|
|
53
59
|
export declare function createAnywhereFamily(deps?: AnywhereDeps): CheckFamily;
|
|
54
60
|
export declare const anywhereFamily: CheckFamily;
|
|
61
|
+
/**
|
|
62
|
+
* What to say when the Windows named pipe cannot be used. The old wording
|
|
63
|
+
* guessed an ACL refusal whenever any process looked like a daemon. This
|
|
64
|
+
* reports the connect error that was actually observed.
|
|
65
|
+
*/
|
|
66
|
+
export declare function windowsNamedPipeUnreachableMessage(socketPath: string, daemonRunning: boolean, failure?: PipeConnectFailure): string;
|
|
55
67
|
export declare function defaultServiceState(home: string, options?: DaemonServiceProbeOptions): DaemonServiceState;
|
|
56
68
|
export {};
|
|
57
69
|
//# sourceMappingURL=anywhere.d.ts.map
|
|
@@ -63,6 +63,7 @@ async function runAnywhereChecks(context, deps = {}) {
|
|
|
63
63
|
: result(personSetting ? "PASS" : "WARN", "person-setting", `HQ Anywhere person setting is ${personSetting ? "on" : "off"}`);
|
|
64
64
|
let health;
|
|
65
65
|
let healthUnsupported = false;
|
|
66
|
+
let connectFailure;
|
|
66
67
|
try {
|
|
67
68
|
if (deps.health) {
|
|
68
69
|
health = await deps.health(cwd);
|
|
@@ -107,12 +108,14 @@ async function runAnywhereChecks(context, deps = {}) {
|
|
|
107
108
|
};
|
|
108
109
|
healthUnsupported = true;
|
|
109
110
|
}
|
|
110
|
-
catch {
|
|
111
|
-
health = unreachableHealth(socketPath);
|
|
111
|
+
catch (inner) {
|
|
112
|
+
health = unreachableHealth(socketPath, deps.unreachableTransport);
|
|
113
|
+
connectFailure = connectFailureFrom(inner);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
else {
|
|
115
|
-
health = unreachableHealth(socketPath);
|
|
117
|
+
health = unreachableHealth(socketPath, deps.unreachableTransport);
|
|
118
|
+
connectFailure = connectFailureFrom(error);
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
const claude = claudeTargets(home);
|
|
@@ -126,7 +129,7 @@ async function runAnywhereChecks(context, deps = {}) {
|
|
|
126
129
|
const daemonRunning = health.reachable
|
|
127
130
|
? true
|
|
128
131
|
: (deps.daemonRunning ?? (() => runningDaemonPid(daemonPaths(home, env).lockPath) !== null))();
|
|
129
|
-
const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState, daemonRunning);
|
|
132
|
+
const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState, daemonRunning, connectFailure);
|
|
130
133
|
if (!runtimeEnabled)
|
|
131
134
|
return [healthCheck, settingCheck];
|
|
132
135
|
const remoteModes = [
|
|
@@ -177,17 +180,67 @@ async function runAnywhereChecks(context, deps = {}) {
|
|
|
177
180
|
out.push(checkSkillLinks("codex", codex.skillsDir, REMEDIATION.installCodex, REMEDIATION.repairCodexSkills));
|
|
178
181
|
return out;
|
|
179
182
|
}
|
|
180
|
-
function unreachableHealth(socketPath) {
|
|
183
|
+
function unreachableHealth(socketPath, transport) {
|
|
181
184
|
return {
|
|
182
185
|
reachable: false,
|
|
183
186
|
version: null,
|
|
184
187
|
uptimeMs: null,
|
|
185
188
|
flag: { enabled: null, lastSuccessfulReadAgeMs: null, heldThroughReadError: false },
|
|
186
189
|
resolution: { scope: "unknown", company: null, fallbackReason: null },
|
|
187
|
-
socket: {
|
|
190
|
+
socket: {
|
|
191
|
+
path: socketPath,
|
|
192
|
+
transport: transport ?? (process.platform === "win32" ? "windows_named_pipe" : "unix_socket"),
|
|
193
|
+
mode: null,
|
|
194
|
+
ownerMatchesUser: null,
|
|
195
|
+
userOnly: null,
|
|
196
|
+
},
|
|
188
197
|
};
|
|
189
198
|
}
|
|
190
|
-
function
|
|
199
|
+
function connectFailureFrom(error) {
|
|
200
|
+
if (error instanceof HqdClientError)
|
|
201
|
+
return { code: error.code, detail: error.message };
|
|
202
|
+
if (error instanceof Error) {
|
|
203
|
+
const code = error.code;
|
|
204
|
+
return { code: typeof code === "string" ? code : undefined, detail: error.message };
|
|
205
|
+
}
|
|
206
|
+
return { detail: String(error) };
|
|
207
|
+
}
|
|
208
|
+
function failureSuffix(failure) {
|
|
209
|
+
if (!failure)
|
|
210
|
+
return "";
|
|
211
|
+
const parts = [failure.code, failure.detail].filter((part) => Boolean(part && part.trim()));
|
|
212
|
+
return parts.length === 0 ? "" : ` (${parts.join(": ")})`;
|
|
213
|
+
}
|
|
214
|
+
function accessDenied(failure) {
|
|
215
|
+
const code = (failure?.code ?? "").toUpperCase();
|
|
216
|
+
if (code === "EACCES" || code === "EPERM")
|
|
217
|
+
return true;
|
|
218
|
+
return /access is denied|unsafe acl/i.test(failure?.detail ?? "");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* What to say when the Windows named pipe cannot be used. The old wording
|
|
222
|
+
* guessed an ACL refusal whenever any process looked like a daemon. This
|
|
223
|
+
* reports the connect error that was actually observed.
|
|
224
|
+
*/
|
|
225
|
+
export function windowsNamedPipeUnreachableMessage(socketPath, daemonRunning, failure) {
|
|
226
|
+
const suffix = failureSuffix(failure);
|
|
227
|
+
if (!daemonRunning) {
|
|
228
|
+
return `hqd is not running, so its named pipe ${socketPath} does not exist${suffix}. Run ${REMEDIATION.daemonStatus} to see why it has not started.`;
|
|
229
|
+
}
|
|
230
|
+
if (accessDenied(failure)) {
|
|
231
|
+
return `hqd is running but access to named pipe ${socketPath} was denied${suffix}. The pipe ACL does not allow this user. Run ${REMEDIATION.daemonRestart} to recreate the pipe.`;
|
|
232
|
+
}
|
|
233
|
+
const code = (failure?.code ?? "").toUpperCase();
|
|
234
|
+
if (code === "ENOENT") {
|
|
235
|
+
return `hqd is running but named pipe ${socketPath} does not exist${suffix}. Run ${REMEDIATION.daemonRestart} to recreate the pipe.`;
|
|
236
|
+
}
|
|
237
|
+
if (code === "ETIMEDOUT") {
|
|
238
|
+
return `hqd is running but did not answer on named pipe ${socketPath}${suffix}. Run ${REMEDIATION.daemonStatus}, then ${REMEDIATION.daemonRestart}.`;
|
|
239
|
+
}
|
|
240
|
+
const observed = suffix || " (observed)";
|
|
241
|
+
return `hqd is not reachable on ${socketPath}${observed}. HQ did not see an access-denied error, so this is not a named-pipe ACL refusal. Run ${REMEDIATION.daemonStatus} to see why, then ${REMEDIATION.daemonRestart}.`;
|
|
242
|
+
}
|
|
243
|
+
function runtimeHealthCheck(health, healthUnsupported = false, serviceState, daemonRunning = true, failure) {
|
|
191
244
|
const { flag, resolution, socket } = health;
|
|
192
245
|
const age = flag.lastSuccessfulReadAgeMs === null
|
|
193
246
|
? "unknown"
|
|
@@ -204,11 +257,9 @@ function runtimeHealthCheck(health, healthUnsupported = false, serviceState, dae
|
|
|
204
257
|
? "Windows named pipe (mode and owner are not reported)"
|
|
205
258
|
: `mode ${socket.mode ?? "unknown"}; owner matches current user: ${socket.ownerMatchesUser === null ? "unknown" : socket.ownerMatchesUser ? "yes" : "no"}; user-only: ${socket.userOnly === null ? "unknown" : socket.userOnly ? "yes" : "no"}`;
|
|
206
259
|
const pipeWithoutDaemon = socket.transport === "windows_named_pipe" && !daemonRunning;
|
|
207
|
-
const unreachableMessage =
|
|
208
|
-
?
|
|
209
|
-
: socket.
|
|
210
|
-
? `hqd is not reachable on ${socket.path}; secure Windows named-pipe setup may have refused an unsafe ACL`
|
|
211
|
-
: `hqd is not reachable on ${socket.path}; version, uptime, flag state, and cwd binding are unavailable`;
|
|
260
|
+
const unreachableMessage = socket.transport === "windows_named_pipe"
|
|
261
|
+
? windowsNamedPipeUnreachableMessage(socket.path, daemonRunning, failure)
|
|
262
|
+
: `hqd is not reachable on ${socket.path}; version, uptime, flag state, and cwd binding are unavailable`;
|
|
212
263
|
const socketNotUserOnly = health.reachable && socket.transport === "unix_socket" && (socket.userOnly !== true || socket.ownerMatchesUser !== true);
|
|
213
264
|
const warningNotes = [
|
|
214
265
|
healthUnsupported ? "resident hqd does not support detailed health; restart hqd for full diagnostics" : null,
|