@indigoai-us/hq-cli 5.345.148 → 5.345.149
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 +35 -0
- package/README.md +2 -0
- package/dist/command-catalog.generated.d.ts +44 -8
- package/dist/command-catalog.generated.js +53 -8
- package/dist/command-help.js +2 -1
- 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/install-global.d.ts +1 -8
- package/dist/commands/install-global.js +11 -27
- 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/reindex.js +19 -0
- package/dist/hook-engine-path.js +10 -3
- 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/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/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 +8 -8
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,
|
|
@@ -4,7 +4,8 @@ export declare const CLI_PATH_FAMILY_ID = "cli-path";
|
|
|
4
4
|
export declare const CLI_PATH_CHECK_ID = "cli-path.npm-shim-on-path";
|
|
5
5
|
/**
|
|
6
6
|
* Windows: is the folder holding the npm `hq.cmd` shim on the PATH a new
|
|
7
|
-
* terminal gets?
|
|
7
|
+
* terminal gets? This check only reads. The CLI adds the folder to HKCU Path
|
|
8
|
+
* on startup; the warning is the fallback when that add did not stick.
|
|
8
9
|
*/
|
|
9
10
|
export declare const cliPathFamily: CheckFamily;
|
|
10
11
|
export declare function checkCliPath(_context: CheckContext, check?: () => ShimPathCheck | null): CheckResult[];
|
|
@@ -3,7 +3,8 @@ export const CLI_PATH_FAMILY_ID = "cli-path";
|
|
|
3
3
|
export const CLI_PATH_CHECK_ID = "cli-path.npm-shim-on-path";
|
|
4
4
|
/**
|
|
5
5
|
* Windows: is the folder holding the npm `hq.cmd` shim on the PATH a new
|
|
6
|
-
* terminal gets?
|
|
6
|
+
* terminal gets? This check only reads. The CLI adds the folder to HKCU Path
|
|
7
|
+
* on startup; the warning is the fallback when that add did not stick.
|
|
7
8
|
*/
|
|
8
9
|
export const cliPathFamily = {
|
|
9
10
|
id: CLI_PATH_FAMILY_ID,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { launcherEnabled } from '../../hook-engine/registration.js';
|
|
1
2
|
import { findHookEngine, hookEngineRequired, pinnedHookEngineMatches, HOOK_ENGINE_REPAIR, HOOK_ENGINE_REPAIR_PIN } from '../../hook-engine/resolve.js';
|
|
2
3
|
export function checkHookEngine() {
|
|
3
4
|
const required = hookEngineRequired();
|
|
@@ -5,7 +6,7 @@ export function checkHookEngine() {
|
|
|
5
6
|
try {
|
|
6
7
|
const engine = findHookEngine();
|
|
7
8
|
if (engine && pinnedHookEngineMatches(engine))
|
|
8
|
-
return { checkId: 'hooks.native-engine', status: 'PASS', target: engine.pinned, message: `Native hook engine ${engine.version} installed at its version-pinned path.` };
|
|
9
|
+
return { checkId: 'hooks.native-engine', status: 'PASS', target: engine.pinned, message: `Native hook engine ${engine.version} installed at its version-pinned path. Launcher flag: ${launcherEnabled() ? 'on (run hq reindex to reconcile settings)' : 'off (bash registration)'}.` };
|
|
9
10
|
return { checkId: 'hooks.native-engine', status: required ? 'FAIL' : 'WARN', message: `Native hook engine is missing or damaged. ${fallback}`, remediation: engine ? `Close HQ sessions, then run ${HOOK_ENGINE_REPAIR_PIN}` : HOOK_ENGINE_REPAIR };
|
|
10
11
|
}
|
|
11
12
|
catch (error) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ReconcileResult {
|
|
2
|
+
/** Codex user-scope trust keys for the HQ entries in the global hooks.json. */
|
|
3
|
+
codexUserKeys: string[];
|
|
4
|
+
/** One `<file>: <reason>` line per registration file left unchanged by an error. */
|
|
5
|
+
failures: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Reconcile existing registrations only; reindex must not install a runtime. */
|
|
8
|
+
export declare function reconcileHookRegistrations(root: string, home?: string, env?: NodeJS.ProcessEnv, daemonInstalled?: boolean): ReconcileResult;
|
|
9
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { writeConfigAtomic, jsonFormat } from '../../commands/mcp-registration.js';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { addHqHooks, removeHqHooks } from '../install/claude-targets.js';
|
|
6
|
+
import { hookTargetFor } from '../install/hook-target.js';
|
|
7
|
+
import { addCodexHooks, removeCodexHooks, codexHookEntry, codexHookTrustKeys } from '../install/codex-targets.js';
|
|
8
|
+
import { engineArgs, isNativeEntry, registrationEngine } from './registration.js';
|
|
9
|
+
/** The project master-hook command shipped in HQ's .claude/settings.json. */
|
|
10
|
+
function projectClaudeCommand(event) {
|
|
11
|
+
return 'env BASH_ENV=/dev/null bash "$CLAUDE_PROJECT_DIR/.claude/hooks/master-hook.sh" ' + event;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Rewrite matching entries where they sit. Groups, order, foreign entries and
|
|
15
|
+
* extra fields such as timeout are kept, so a flag-on then flag-off pass
|
|
16
|
+
* restores the original file.
|
|
17
|
+
*/
|
|
18
|
+
function rewriteEntries(doc, convert) {
|
|
19
|
+
let changed = false;
|
|
20
|
+
const hooks = {};
|
|
21
|
+
for (const [event, groups] of Object.entries(doc.hooks)) {
|
|
22
|
+
hooks[event] = !Array.isArray(groups) ? groups : groups.map((group) => {
|
|
23
|
+
if (!Array.isArray(group?.hooks))
|
|
24
|
+
return group;
|
|
25
|
+
return { ...group, hooks: group.hooks.map((entry) => {
|
|
26
|
+
const next = entry && typeof entry === 'object' ? convert(entry, event) : undefined;
|
|
27
|
+
if (!next)
|
|
28
|
+
return entry;
|
|
29
|
+
changed = true;
|
|
30
|
+
return next;
|
|
31
|
+
}) };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return changed ? { ...doc, hooks } : doc;
|
|
35
|
+
}
|
|
36
|
+
/** Reconcile existing registrations only; reindex must not install a runtime. */
|
|
37
|
+
export function reconcileHookRegistrations(root, home = homedir(), env = process.env, daemonInstalled) {
|
|
38
|
+
const result = { codexUserKeys: [], failures: [] };
|
|
39
|
+
const engine = registrationEngine(home, env);
|
|
40
|
+
// Flag-off rollback restores the same bash target install would choose (the
|
|
41
|
+
// hqd shim when the daemon serves this root). Resolved only when a
|
|
42
|
+
// registration is rewritten, so the default-off path reads no daemon state.
|
|
43
|
+
let target;
|
|
44
|
+
const globalTarget = () => (target ??= engine ? { engine } : hookTargetFor(root, home, daemonInstalled, env));
|
|
45
|
+
const codexBash = (event) => [codexHookEntry(root, event, 'master').command, codexHookEntry(root, event, 'shim').command];
|
|
46
|
+
for (const [file, runtime, global] of [
|
|
47
|
+
[join(root, '.claude/settings.json'), 'claude', false],
|
|
48
|
+
[join(env.CLAUDE_CONFIG_DIR || join(home, '.claude'), 'settings.json'), 'claude', true],
|
|
49
|
+
[join(env.CODEX_HOME || join(home, '.codex'), 'hooks.json'), 'codex', true],
|
|
50
|
+
[join(root, '.codex/hooks.json'), 'codex', false],
|
|
51
|
+
]) {
|
|
52
|
+
// Each file stands alone: an unreadable or locked project file must not
|
|
53
|
+
// undo or hide the trust keys of a global file already rewritten.
|
|
54
|
+
try {
|
|
55
|
+
if (!existsSync(file))
|
|
56
|
+
continue;
|
|
57
|
+
// Default-off must not acquire a write lock or create backups for a legacy
|
|
58
|
+
// registration. The merge below repeats this check under the lock when a
|
|
59
|
+
// native entry was found, so a concurrent writer still wins safely.
|
|
60
|
+
if (!engine) {
|
|
61
|
+
let current;
|
|
62
|
+
// A file we cannot parse holds no native entry of ours; leave it untouched.
|
|
63
|
+
try {
|
|
64
|
+
current = jsonFormat.parse(readFileSync(file, 'utf8'));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const entries = current?.hooks && typeof current.hooks === 'object'
|
|
70
|
+
? Object.values(current.hooks).flatMap(groups => Array.isArray(groups) ? groups.flatMap(g => Array.isArray(g?.hooks) ? g.hooks : []) : [])
|
|
71
|
+
: [];
|
|
72
|
+
if (!entries.some(e => e && isNativeEntry(e, root)))
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let expected;
|
|
76
|
+
let codexKeys;
|
|
77
|
+
writeConfigAtomic({ target: file, pack: 'hq-hook-engine', format: {
|
|
78
|
+
...jsonFormat,
|
|
79
|
+
assertEntry: (doc) => {
|
|
80
|
+
if (expected !== undefined && JSON.stringify(doc) !== expected)
|
|
81
|
+
throw new Error('Hook registration verification failed');
|
|
82
|
+
},
|
|
83
|
+
}, env: { home }, assertName: 'hooks', merge: (doc) => {
|
|
84
|
+
if (!doc || typeof doc !== 'object' || !doc.hooks || typeof doc.hooks !== 'object')
|
|
85
|
+
return doc;
|
|
86
|
+
const entries = Object.values(doc.hooks).flatMap(groups => Array.isArray(groups) ? groups.flatMap(g => Array.isArray(g?.hooks) ? g.hooks : []) : []);
|
|
87
|
+
if (!engine && !entries.some(e => e && isNativeEntry(e, root)))
|
|
88
|
+
return doc;
|
|
89
|
+
if (global && (runtime === 'codex' ? removeCodexHooks(doc, root) : removeHqHooks(doc, root)) === doc)
|
|
90
|
+
return doc;
|
|
91
|
+
let next;
|
|
92
|
+
if (global)
|
|
93
|
+
next = runtime === 'codex' ? addCodexHooks(doc, root, globalTarget()) : addHqHooks(doc, root, globalTarget());
|
|
94
|
+
else if (runtime === 'codex') {
|
|
95
|
+
// Only an entry install itself writes is converted. HQ's own project
|
|
96
|
+
// file routes through hook-gate.sh and has none, so it stays as is.
|
|
97
|
+
next = rewriteEntries(doc, (entry, event) => {
|
|
98
|
+
if (!isNativeEntry(entry, root) && !codexBash(event).includes(entry.command))
|
|
99
|
+
return undefined;
|
|
100
|
+
const { command: _command, commandWindows: _windows, ...rest } = entry;
|
|
101
|
+
const { command, commandWindows } = codexHookEntry(root, event, globalTarget());
|
|
102
|
+
return { ...rest, command, ...(commandWindows ? { commandWindows } : {}) };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Only the exact shipped master-hook command is converted; a wrapped
|
|
107
|
+
// or extended invocation is someone's edit and is left alone.
|
|
108
|
+
next = rewriteEntries(doc, (entry, event) => {
|
|
109
|
+
if (!isNativeEntry(entry, root) && entry.command !== projectClaudeCommand(event))
|
|
110
|
+
return undefined;
|
|
111
|
+
const { args, commandWindows: _windows, ...rest } = entry;
|
|
112
|
+
if (engine)
|
|
113
|
+
return { ...rest, command: engine, args: engineArgs(root, event, 'claude') };
|
|
114
|
+
return { ...rest, command: projectClaudeCommand(Array.isArray(args) && typeof args[0] === 'string' ? args[0] : event) };
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
expected = JSON.stringify(next);
|
|
118
|
+
if (runtime === 'codex' && global)
|
|
119
|
+
codexKeys = codexHookTrustKeys(next, file, root);
|
|
120
|
+
return next;
|
|
121
|
+
} });
|
|
122
|
+
// Keys are positions in the file. Only once the write and its verify have
|
|
123
|
+
// succeeded do they point at HQ's entries; a failed write leaves the old
|
|
124
|
+
// file, where the same position can hold a foreign hook.
|
|
125
|
+
if (codexKeys)
|
|
126
|
+
result.codexUserKeys = codexKeys;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
result.failures.push(`${file}: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function launcherEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
2
|
+
export declare function registrationEngine(home: string, env?: NodeJS.ProcessEnv): string | null;
|
|
3
|
+
export interface NativeTarget {
|
|
4
|
+
engine: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Root spelling passed to the launcher. A trailing separator is dropped because
|
|
8
|
+
* Windows PowerShell 5.1 re-quotes a native argument containing a space as
|
|
9
|
+
* "...", and a final backslash would then escape the closing quote.
|
|
10
|
+
*/
|
|
11
|
+
export declare function launcherRoot(root: string): string;
|
|
12
|
+
export declare function engineArgs(root: string, event: string, runtime: 'claude' | 'codex', global?: boolean): string[];
|
|
13
|
+
/** Recognize only our pinned executable with the exact root argument. */
|
|
14
|
+
export declare function isNativeEntry(entry: Record<string, unknown>, root: string): boolean;
|
|
15
|
+
export declare function quotePosix(value: string): string;
|
|
16
|
+
export declare function quoteWindows(value: string): string;
|
|
17
|
+
//# sourceMappingURL=registration.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Offline, default-off registration gate. No network work on the hook path. */
|
|
2
|
+
import { readFlagSnapshotFile, readPersonFlagSnapshotFile } from '../flag-snapshot-file.js';
|
|
3
|
+
import { resolveProcessFlagCompanyContext } from '../process-flag-company-context.js';
|
|
4
|
+
import { installHookEngine } from './resolve.js';
|
|
5
|
+
export function launcherEnabled(env = process.env) {
|
|
6
|
+
const { companyUid: uid, companySlug } = resolveProcessFlagCompanyContext(env);
|
|
7
|
+
if (!uid && companySlug)
|
|
8
|
+
return false;
|
|
9
|
+
const snapshot = uid ? readFlagSnapshotFile(uid, { env }) : readPersonFlagSnapshotFile({ env });
|
|
10
|
+
return snapshot?.flags['hook-engine-launcher'] === true;
|
|
11
|
+
}
|
|
12
|
+
export function registrationEngine(home, env = process.env) {
|
|
13
|
+
if (!launcherEnabled({ ...env, HOME: home, USERPROFILE: home }))
|
|
14
|
+
return null;
|
|
15
|
+
return installHookEngine({ home });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Root spelling passed to the launcher. A trailing separator is dropped because
|
|
19
|
+
* Windows PowerShell 5.1 re-quotes a native argument containing a space as
|
|
20
|
+
* "...", and a final backslash would then escape the closing quote.
|
|
21
|
+
*/
|
|
22
|
+
export function launcherRoot(root) {
|
|
23
|
+
const trimmed = root.replace(/[\\/]+$/, '');
|
|
24
|
+
return trimmed === '' || /^[A-Za-z]:$/.test(trimmed) ? root : trimmed;
|
|
25
|
+
}
|
|
26
|
+
export function engineArgs(root, event, runtime, global = false) {
|
|
27
|
+
return [event, '--hq-root', launcherRoot(root), '--runtime', runtime, ...(global && runtime === 'claude' ? ['--global'] : [])];
|
|
28
|
+
}
|
|
29
|
+
/** Recognize only our pinned executable with the exact root argument. */
|
|
30
|
+
export function isNativeEntry(entry, root) {
|
|
31
|
+
if (typeof entry.command !== 'string')
|
|
32
|
+
return false;
|
|
33
|
+
const pinned = /[\\/]\.hq[\\/]bin[\\/]hq-hook[\\/]\d+\.\d+\.\d+[^\\/]*[\\/]/;
|
|
34
|
+
if (!pinned.test(entry.command))
|
|
35
|
+
return false;
|
|
36
|
+
const spelling = launcherRoot(root);
|
|
37
|
+
if (Array.isArray(entry.args)) {
|
|
38
|
+
const i = entry.args.indexOf('--hq-root');
|
|
39
|
+
return i >= 0 && entry.args[i + 1] === spelling;
|
|
40
|
+
}
|
|
41
|
+
return entry.command.includes(quotePosix('--hq-root') + ' ' + quotePosix(spelling));
|
|
42
|
+
}
|
|
43
|
+
export function quotePosix(value) { return "'" + value.replace(/'/g, "'\\''") + "'"; }
|
|
44
|
+
export function quoteWindows(value) {
|
|
45
|
+
// Codex 0.162.1 uses its Windows PowerShell runtime shell for hooks.
|
|
46
|
+
// Single-quoted literals prevent variable, subexpression and wildcard expansion.
|
|
47
|
+
// PowerShell also closes them on the typographic quotes U+2018-U+201B.
|
|
48
|
+
return "'" + value.replace(/['‘’‚‛]/g, '$&$&') + "'";
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=registration.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NativeTarget } from '../hook-engine/registration.js';
|
|
1
2
|
/** Name of the HQ MCP server entry in ~/.claude.json. */
|
|
2
3
|
export declare const HQ_MCP_SERVER_NAME = "hq";
|
|
3
4
|
/** `_hqPack` provenance stamped on the HQ MCP server entry. */
|
|
@@ -21,7 +22,7 @@ export declare function masterHookPath(hqRoot: string): string;
|
|
|
21
22
|
/** hqd hook shim (US-010): registered instead of master-hook.sh once hqd is installed. */
|
|
22
23
|
export declare function shimPath(hqRoot: string): string;
|
|
23
24
|
/** Which HQ hook script the user-scope install registers. */
|
|
24
|
-
export type HookTarget = 'master' | 'shim';
|
|
25
|
+
export type HookTarget = 'master' | 'shim' | NativeTarget;
|
|
25
26
|
export declare function charterPath(hqRoot: string): string;
|
|
26
27
|
export declare function hqSkillsSource(hqRoot: string): string;
|
|
27
28
|
/** The exact hook command registered for one event (absolute path, quoted). */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* safe-write substrate.
|
|
6
6
|
*/
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import { engineArgs, isNativeEntry } from '../hook-engine/registration.js';
|
|
8
9
|
/** Events master-hook.sh is registered for at user scope. */
|
|
9
10
|
const GLOBAL_HOOK_EVENTS = [
|
|
10
11
|
'SessionStart',
|
|
@@ -98,6 +99,8 @@ export function removeClaudeMdBlock(current) {
|
|
|
98
99
|
function isHqHookEntry(entry, hqRoot) {
|
|
99
100
|
if (!entry || typeof entry !== 'object')
|
|
100
101
|
return false;
|
|
102
|
+
if (isNativeEntry(entry, hqRoot))
|
|
103
|
+
return true;
|
|
101
104
|
const cmd = entry.command;
|
|
102
105
|
if (typeof cmd !== 'string')
|
|
103
106
|
return false;
|
|
@@ -111,13 +114,15 @@ export function addHqHooks(doc, hqRoot, target = 'master') {
|
|
|
111
114
|
const hooks = { ...(doc.hooks ?? {}) };
|
|
112
115
|
let changed = false;
|
|
113
116
|
for (const event of GLOBAL_HOOK_EVENTS) {
|
|
114
|
-
const want =
|
|
117
|
+
const want = typeof target === 'object'
|
|
118
|
+
? { type: 'command', command: target.engine, args: engineArgs(hqRoot, event, 'claude', true), timeout: 300 }
|
|
119
|
+
: { type: 'command', command: hookCommand(hqRoot, event, target), timeout: 300 };
|
|
115
120
|
let groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
116
|
-
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => h.command === want));
|
|
121
|
+
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => h.command === want.command && JSON.stringify(h.args) === JSON.stringify('args' in want ? want.args : undefined)));
|
|
117
122
|
if (present)
|
|
118
123
|
continue;
|
|
119
124
|
groups = removeHqHooks({ hooks: { [event]: groups } }, hqRoot).hooks?.[event] ?? [];
|
|
120
|
-
groups.push({ hooks: [
|
|
125
|
+
groups.push({ hooks: [want] });
|
|
121
126
|
hooks[event] = groups;
|
|
122
127
|
changed = true;
|
|
123
128
|
}
|