@hasna/hooks 0.6.4 → 0.6.6
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/README.md +1 -1
- package/bin/index.js +619 -174
- package/bin/serve.js +115 -35
- package/dist/config.d.ts +8 -1
- package/dist/db/remote-storage.d.ts +22 -0
- package/dist/index.js +442 -121
- package/dist/lib/hook-env.d.ts +42 -0
- package/dist/lib/manifest.d.ts +24 -2
- package/dist/lib/redact.d.ts +40 -0
- package/dist/lib/semver.d.ts +19 -0
- package/dist/lib/store.d.ts +16 -0
- package/dist/lib/sync.d.ts +55 -4
- package/dist/serve.d.ts +2 -1
- package/dist/storage.js +104 -7
- package/package.json +2 -2
package/bin/index.js
CHANGED
|
@@ -47,7 +47,7 @@ var __export = (target, all) => {
|
|
|
47
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
48
48
|
var __require = import.meta.require;
|
|
49
49
|
|
|
50
|
-
//
|
|
50
|
+
// node_modules/commander/lib/error.js
|
|
51
51
|
var require_error = __commonJS((exports) => {
|
|
52
52
|
class CommanderError extends Error {
|
|
53
53
|
constructor(exitCode, code, message) {
|
|
@@ -71,7 +71,7 @@ var require_error = __commonJS((exports) => {
|
|
|
71
71
|
exports.InvalidArgumentError = InvalidArgumentError;
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// node_modules/commander/lib/argument.js
|
|
75
75
|
var require_argument = __commonJS((exports) => {
|
|
76
76
|
var { InvalidArgumentError } = require_error();
|
|
77
77
|
|
|
@@ -150,7 +150,7 @@ var require_argument = __commonJS((exports) => {
|
|
|
150
150
|
exports.humanReadableArgName = humanReadableArgName;
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
//
|
|
153
|
+
// node_modules/commander/lib/help.js
|
|
154
154
|
var require_help = __commonJS((exports) => {
|
|
155
155
|
var { humanReadableArgName } = require_argument();
|
|
156
156
|
|
|
@@ -399,7 +399,7 @@ var require_help = __commonJS((exports) => {
|
|
|
399
399
|
exports.Help = Help;
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
-
//
|
|
402
|
+
// node_modules/commander/lib/option.js
|
|
403
403
|
var require_option = __commonJS((exports) => {
|
|
404
404
|
var { InvalidArgumentError } = require_error();
|
|
405
405
|
|
|
@@ -550,7 +550,7 @@ var require_option = __commonJS((exports) => {
|
|
|
550
550
|
exports.DualOptions = DualOptions;
|
|
551
551
|
});
|
|
552
552
|
|
|
553
|
-
//
|
|
553
|
+
// node_modules/commander/lib/suggestSimilar.js
|
|
554
554
|
var require_suggestSimilar = __commonJS((exports) => {
|
|
555
555
|
var maxDistance = 3;
|
|
556
556
|
function editDistance(a, b) {
|
|
@@ -623,7 +623,7 @@ var require_suggestSimilar = __commonJS((exports) => {
|
|
|
623
623
|
exports.suggestSimilar = suggestSimilar;
|
|
624
624
|
});
|
|
625
625
|
|
|
626
|
-
//
|
|
626
|
+
// node_modules/commander/lib/command.js
|
|
627
627
|
var require_command = __commonJS((exports) => {
|
|
628
628
|
var EventEmitter = __require("events").EventEmitter;
|
|
629
629
|
var childProcess = __require("child_process");
|
|
@@ -1866,7 +1866,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1866
1866
|
exports.Command = Command;
|
|
1867
1867
|
});
|
|
1868
1868
|
|
|
1869
|
-
//
|
|
1869
|
+
// node_modules/commander/index.js
|
|
1870
1870
|
var require_commander = __commonJS((exports) => {
|
|
1871
1871
|
var { Argument } = require_argument();
|
|
1872
1872
|
var { Command } = require_command();
|
|
@@ -2419,7 +2419,7 @@ var init_registry = __esm(() => {
|
|
|
2419
2419
|
];
|
|
2420
2420
|
});
|
|
2421
2421
|
|
|
2422
|
-
//
|
|
2422
|
+
// node_modules/cli-spinners/spinners.json
|
|
2423
2423
|
var require_spinners = __commonJS((exports, module) => {
|
|
2424
2424
|
module.exports = {
|
|
2425
2425
|
dots: {
|
|
@@ -4045,7 +4045,7 @@ var require_spinners = __commonJS((exports, module) => {
|
|
|
4045
4045
|
};
|
|
4046
4046
|
});
|
|
4047
4047
|
|
|
4048
|
-
//
|
|
4048
|
+
// node_modules/cli-spinners/index.js
|
|
4049
4049
|
var require_cli_spinners = __commonJS((exports, module) => {
|
|
4050
4050
|
var spinners = Object.assign({}, require_spinners());
|
|
4051
4051
|
var spinnersList = Object.keys(spinners);
|
|
@@ -4119,24 +4119,30 @@ function resolveApiUrl() {
|
|
|
4119
4119
|
const cfg = readConfig();
|
|
4120
4120
|
return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
|
|
4121
4121
|
}
|
|
4122
|
-
function resolveApiKey(
|
|
4123
|
-
if (flagKey && flagKey.length > 0)
|
|
4124
|
-
return flagKey;
|
|
4122
|
+
function resolveApiKey() {
|
|
4125
4123
|
return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
|
|
4126
4124
|
}
|
|
4127
4125
|
var init_config = () => {};
|
|
4128
4126
|
|
|
4127
|
+
// src/lib/semver.ts
|
|
4128
|
+
var SEMVER_PATTERN;
|
|
4129
|
+
var init_semver = __esm(() => {
|
|
4130
|
+
SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
|
|
4131
|
+
});
|
|
4132
|
+
|
|
4129
4133
|
// src/lib/manifest.ts
|
|
4130
4134
|
var exports_manifest = {};
|
|
4131
4135
|
__export(exports_manifest, {
|
|
4132
4136
|
writeCustomHook: () => writeCustomHook,
|
|
4133
4137
|
shortManifestName: () => shortManifestName,
|
|
4138
|
+
scriptRelFor: () => scriptRelFor,
|
|
4134
4139
|
resolveScript: () => resolveScript,
|
|
4135
4140
|
readCustomManifest: () => readCustomManifest,
|
|
4136
4141
|
parseManifest: () => parseManifest,
|
|
4137
4142
|
normalizeManifestName: () => normalizeManifestName,
|
|
4138
4143
|
manifestSchema: () => manifestSchema,
|
|
4139
4144
|
listCustomHooks: () => listCustomHooks,
|
|
4145
|
+
isInlineScript: () => isInlineScript,
|
|
4140
4146
|
customHookDir: () => customHookDir,
|
|
4141
4147
|
assertContained: () => assertContained,
|
|
4142
4148
|
ScriptContainmentError: () => ScriptContainmentError,
|
|
@@ -4190,16 +4196,25 @@ function assertContained(target, root, raw) {
|
|
|
4190
4196
|
assertContainedReal(root, lexical, raw);
|
|
4191
4197
|
return lexical;
|
|
4192
4198
|
}
|
|
4193
|
-
function
|
|
4194
|
-
|
|
4195
|
-
`)
|
|
4199
|
+
function isInlineScript(manifest) {
|
|
4200
|
+
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
4201
|
+
`);
|
|
4202
|
+
}
|
|
4203
|
+
function scriptRelFor(manifest) {
|
|
4204
|
+
if (isInlineScript(manifest)) {
|
|
4196
4205
|
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4197
|
-
return
|
|
4206
|
+
return `script${ext}`;
|
|
4198
4207
|
}
|
|
4199
4208
|
const rel = normalize(manifest.script);
|
|
4200
4209
|
if (isAbsolute(rel)) {
|
|
4201
4210
|
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4202
4211
|
}
|
|
4212
|
+
return rel;
|
|
4213
|
+
}
|
|
4214
|
+
function resolveScript(manifest, manifestDir) {
|
|
4215
|
+
if (isInlineScript(manifest))
|
|
4216
|
+
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4217
|
+
const rel = scriptRelFor(manifest);
|
|
4203
4218
|
const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
|
|
4204
4219
|
if (!existsSync3(resolved)) {
|
|
4205
4220
|
throw new Error(`manifest script file not found: ${resolved}`);
|
|
@@ -4217,8 +4232,8 @@ function readCustomManifest(name) {
|
|
|
4217
4232
|
try {
|
|
4218
4233
|
const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
|
|
4219
4234
|
const script = resolveScript(manifest, dir);
|
|
4220
|
-
|
|
4221
|
-
|
|
4235
|
+
const scriptIsInline = isInlineScript(manifest);
|
|
4236
|
+
return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4222
4237
|
} catch (err) {
|
|
4223
4238
|
if (err instanceof ScriptContainmentError)
|
|
4224
4239
|
throw err;
|
|
@@ -4264,13 +4279,15 @@ function readdirSafe(dir) {
|
|
|
4264
4279
|
var HOOK_NAME_RE, manifestSchema, ScriptContainmentError;
|
|
4265
4280
|
var init_manifest = __esm(() => {
|
|
4266
4281
|
init_config();
|
|
4282
|
+
init_semver();
|
|
4267
4283
|
HOOK_NAME_RE = /^[\w-]+$/;
|
|
4268
4284
|
manifestSchema = z.object({
|
|
4269
4285
|
name: z.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4270
|
-
version: z.string().regex(
|
|
4286
|
+
version: z.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
|
|
4271
4287
|
description: z.string().optional(),
|
|
4272
4288
|
events: z.array(z.string()).min(1, "at least one event is required"),
|
|
4273
4289
|
script: z.string().min(1, "script must name a relative path or inline content"),
|
|
4290
|
+
script_kind: z.enum(["inline", "file"]).optional(),
|
|
4274
4291
|
args: z.array(z.string()).optional(),
|
|
4275
4292
|
timeout_ms: z.number().int().positive().optional()
|
|
4276
4293
|
});
|
|
@@ -4799,11 +4816,13 @@ __export(exports_store, {
|
|
|
4799
4816
|
listHookRecords: () => listHookRecords,
|
|
4800
4817
|
getPinnedHook: () => getPinnedHook,
|
|
4801
4818
|
getHookRecord: () => getHookRecord,
|
|
4802
|
-
checkScriptHash: () => checkScriptHash
|
|
4819
|
+
checkScriptHash: () => checkScriptHash,
|
|
4820
|
+
LockFileError: () => LockFileError
|
|
4803
4821
|
});
|
|
4804
4822
|
import { createHash } from "crypto";
|
|
4805
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4823
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync } from "fs";
|
|
4806
4824
|
import { readFile as readFile2 } from "fs/promises";
|
|
4825
|
+
import { randomBytes } from "crypto";
|
|
4807
4826
|
function sha256Of(input) {
|
|
4808
4827
|
return createHash("sha256").update(input).digest("hex");
|
|
4809
4828
|
}
|
|
@@ -4846,16 +4865,39 @@ function removeHookRecord(db, name) {
|
|
|
4846
4865
|
const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
|
|
4847
4866
|
return res.changes > 0;
|
|
4848
4867
|
}
|
|
4868
|
+
function assertLockShape(parsed, path) {
|
|
4869
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4870
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: expected a JSON object with a "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
4871
|
+
}
|
|
4872
|
+
const hooks = parsed.hooks;
|
|
4873
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
4874
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: missing or invalid "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
4875
|
+
}
|
|
4876
|
+
for (const [name, entry] of Object.entries(hooks)) {
|
|
4877
|
+
const e = entry;
|
|
4878
|
+
if (e === null || typeof e !== "object" || typeof e.version !== "string" || typeof e.sha256 !== "string") {
|
|
4879
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: entry for '${name}' is not a valid pin (version and sha256 strings required). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it.`);
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4849
4883
|
function readLock() {
|
|
4884
|
+
const path = getLockPath();
|
|
4885
|
+
if (!existsSync7(path))
|
|
4886
|
+
return { hooks: {} };
|
|
4887
|
+
let raw;
|
|
4850
4888
|
try {
|
|
4851
|
-
|
|
4852
|
-
if (!existsSync7(path))
|
|
4853
|
-
return { hooks: {} };
|
|
4854
|
-
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
4855
|
-
return { hooks: parsed.hooks ?? {} };
|
|
4889
|
+
raw = readFileSync4(path, "utf-8");
|
|
4856
4890
|
} catch {
|
|
4857
|
-
|
|
4891
|
+
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
4858
4892
|
}
|
|
4893
|
+
let parsed;
|
|
4894
|
+
try {
|
|
4895
|
+
parsed = JSON.parse(raw);
|
|
4896
|
+
} catch {
|
|
4897
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: not valid JSON (${String((raw ?? "").slice(0, 80))}). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
4898
|
+
}
|
|
4899
|
+
assertLockShape(parsed, path);
|
|
4900
|
+
return parsed;
|
|
4859
4901
|
}
|
|
4860
4902
|
function writeLock(lock) {
|
|
4861
4903
|
const path = getLockPath();
|
|
@@ -4864,8 +4906,10 @@ function writeLock(lock) {
|
|
|
4864
4906
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
4865
4907
|
sorted[name] = lock.hooks[name];
|
|
4866
4908
|
}
|
|
4867
|
-
|
|
4909
|
+
const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
|
|
4910
|
+
writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
4868
4911
|
`, "utf-8");
|
|
4912
|
+
renameSync(tmp, path);
|
|
4869
4913
|
return path;
|
|
4870
4914
|
}
|
|
4871
4915
|
function setPinnedHook(name, entry) {
|
|
@@ -4951,9 +4995,16 @@ function retrustHook(name, scriptPath, version, source) {
|
|
|
4951
4995
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
4952
4996
|
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
4953
4997
|
}
|
|
4998
|
+
var LockFileError;
|
|
4954
4999
|
var init_store = __esm(() => {
|
|
4955
5000
|
init_db();
|
|
4956
5001
|
init_config();
|
|
5002
|
+
LockFileError = class LockFileError extends Error {
|
|
5003
|
+
constructor(message) {
|
|
5004
|
+
super(message);
|
|
5005
|
+
this.name = "LockFileError";
|
|
5006
|
+
}
|
|
5007
|
+
};
|
|
4957
5008
|
});
|
|
4958
5009
|
|
|
4959
5010
|
// src/lib/installer.ts
|
|
@@ -5006,7 +5057,7 @@ function getGlobalSettingsPathOverride(target) {
|
|
|
5006
5057
|
function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
|
|
5007
5058
|
if (target === "codewith" && codewithConfigPath)
|
|
5008
5059
|
return codewithConfigPath;
|
|
5009
|
-
if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
5060
|
+
if (target === "codewith" && scope === "global" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
5010
5061
|
return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
|
|
5011
5062
|
}
|
|
5012
5063
|
const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
|
|
@@ -5439,8 +5490,8 @@ function escapeRegExp(value) {
|
|
|
5439
5490
|
function codewithHasHookEntryInText(configText, name) {
|
|
5440
5491
|
return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
|
|
5441
5492
|
}
|
|
5442
|
-
function codewithHasHookEntry(name) {
|
|
5443
|
-
const path = getSettingsPath(
|
|
5493
|
+
function codewithHasHookEntry(name, scope = "global") {
|
|
5494
|
+
const path = getSettingsPath(scope, "codewith");
|
|
5444
5495
|
if (!existsSync8(path))
|
|
5445
5496
|
return false;
|
|
5446
5497
|
try {
|
|
@@ -5457,7 +5508,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5457
5508
|
const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
|
|
5458
5509
|
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
5459
5510
|
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
5460
|
-
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
|
|
5511
|
+
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
|
|
5461
5512
|
if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
5462
5513
|
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
5463
5514
|
}
|
|
@@ -5474,7 +5525,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5474
5525
|
}
|
|
5475
5526
|
const registrationsRemaining = [];
|
|
5476
5527
|
if (target === "codewith" || target === "all") {
|
|
5477
|
-
const codewithPath = getSettingsPath(
|
|
5528
|
+
const codewithPath = getSettingsPath(scope, "codewith");
|
|
5478
5529
|
if (existsSync8(codewithPath)) {
|
|
5479
5530
|
const before = readFileSync5(codewithPath, "utf-8");
|
|
5480
5531
|
const after = removeCodewithHookEntry(before, shortName);
|
|
@@ -5566,6 +5617,22 @@ function safeMatcherTest(pattern, value) {
|
|
|
5566
5617
|
return false;
|
|
5567
5618
|
}
|
|
5568
5619
|
}
|
|
5620
|
+
function countSettingsWiring(settings) {
|
|
5621
|
+
const hooks = settings.hooks;
|
|
5622
|
+
if (!hooks || typeof hooks !== "object")
|
|
5623
|
+
return 0;
|
|
5624
|
+
let count = 0;
|
|
5625
|
+
for (const eventKey of Object.keys(hooks)) {
|
|
5626
|
+
const entries = hooks[eventKey];
|
|
5627
|
+
if (!Array.isArray(entries))
|
|
5628
|
+
continue;
|
|
5629
|
+
for (const entry of entries) {
|
|
5630
|
+
if (Array.isArray(entry?.hooks))
|
|
5631
|
+
count += entry.hooks.length;
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
return count;
|
|
5635
|
+
}
|
|
5569
5636
|
function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
5570
5637
|
const separator = eventSpec.indexOf(":");
|
|
5571
5638
|
const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
|
|
@@ -5587,6 +5654,71 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
5587
5654
|
}));
|
|
5588
5655
|
}
|
|
5589
5656
|
|
|
5657
|
+
// src/lib/redact.ts
|
|
5658
|
+
function redactText(value) {
|
|
5659
|
+
let out = value;
|
|
5660
|
+
for (const pattern of SECRET_SHAPES) {
|
|
5661
|
+
out = out.replace(pattern, REDACTED);
|
|
5662
|
+
}
|
|
5663
|
+
return out;
|
|
5664
|
+
}
|
|
5665
|
+
function redactValue2(value) {
|
|
5666
|
+
if (typeof value === "string")
|
|
5667
|
+
return redactText(value);
|
|
5668
|
+
if (Array.isArray(value))
|
|
5669
|
+
return value.map(redactValue2);
|
|
5670
|
+
if (value !== null && typeof value === "object") {
|
|
5671
|
+
const out = {};
|
|
5672
|
+
for (const [key, child] of Object.entries(value)) {
|
|
5673
|
+
out[key] = SECRET_KEY_NAMES.test(key) ? REDACTED : redactValue2(child);
|
|
5674
|
+
}
|
|
5675
|
+
return out;
|
|
5676
|
+
}
|
|
5677
|
+
return value;
|
|
5678
|
+
}
|
|
5679
|
+
function redactEventPayload(text) {
|
|
5680
|
+
if (text === null || text === undefined)
|
|
5681
|
+
return null;
|
|
5682
|
+
try {
|
|
5683
|
+
const parsed = JSON.parse(text);
|
|
5684
|
+
return redactText(JSON.stringify(redactValue2(parsed)));
|
|
5685
|
+
} catch {
|
|
5686
|
+
return redactText(text);
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
function projectEventRowForRead(row) {
|
|
5690
|
+
const out = { ...row };
|
|
5691
|
+
for (const field of ["tool_input", "error", "metadata"]) {
|
|
5692
|
+
const value = out[field];
|
|
5693
|
+
if (typeof value === "string")
|
|
5694
|
+
out[field] = redactEventPayload(value);
|
|
5695
|
+
else if (value === null)
|
|
5696
|
+
out[field] = null;
|
|
5697
|
+
}
|
|
5698
|
+
return out;
|
|
5699
|
+
}
|
|
5700
|
+
var REDACTED = "[REDACTED]", SECRET_KEY_NAMES, SECRET_SHAPES;
|
|
5701
|
+
var init_redact = __esm(() => {
|
|
5702
|
+
SECRET_KEY_NAMES = /^(key|keys|token|tokens|secret|secrets|password|passwd|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret|refresh[_-]?token|authorization|auth|cookie|credential|credentials|session[_-]?id|otp|mfa[_-]?code|two[_-]?fa[_-]?code|verification[_-]?code|database_url|db_url|connection_string|dsn)$/i;
|
|
5703
|
+
SECRET_SHAPES = [
|
|
5704
|
+
/\bsk-(?:proj|svc|ant)-[A-Za-z0-9_-]{10,}/g,
|
|
5705
|
+
/\bsk-[A-Za-z0-9]{16,}/g,
|
|
5706
|
+
/\btpe_[A-Za-z0-9]{8,}/g,
|
|
5707
|
+
/\b(?:rk|sk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
|
|
5708
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
5709
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
5710
|
+
/xox[baprs]-[A-Za-z0-9-]{20,}/g,
|
|
5711
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
5712
|
+
/ASIA[0-9A-Z]{16}/g,
|
|
5713
|
+
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
5714
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
5715
|
+
/-----BEGIN OPENSSH PRIVATE KEY-----/g,
|
|
5716
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
|
|
5717
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
|
|
5718
|
+
/\b(?:password|passwd|pwd|token|api[_-]?key|secret|access[_-]?key|client[_-]?secret|authorization|auth|credential|database_url|db_url|connection_string|dsn)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']{6,})/gi
|
|
5719
|
+
];
|
|
5720
|
+
});
|
|
5721
|
+
|
|
5590
5722
|
// src/lib/profiles.ts
|
|
5591
5723
|
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
|
|
5592
5724
|
import { join as join8 } from "path";
|
|
@@ -5708,6 +5840,9 @@ function writeHookEvent(event) {
|
|
|
5708
5840
|
const db = getDb();
|
|
5709
5841
|
const id = nanoid2();
|
|
5710
5842
|
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
5843
|
+
const toolInput = redactEventPayload(event.tool_input);
|
|
5844
|
+
const error = redactEventPayload(event.error);
|
|
5845
|
+
const metadata = redactEventPayload(event.metadata);
|
|
5711
5846
|
db.run(`INSERT INTO hook_events
|
|
5712
5847
|
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
5713
5848
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
@@ -5717,12 +5852,12 @@ function writeHookEvent(event) {
|
|
|
5717
5852
|
event.hook_name,
|
|
5718
5853
|
event.event_type,
|
|
5719
5854
|
event.tool_name ?? null,
|
|
5720
|
-
|
|
5855
|
+
toolInput ? toolInput.slice(0, 500) : null,
|
|
5721
5856
|
event.result ?? null,
|
|
5722
|
-
|
|
5857
|
+
error ?? null,
|
|
5723
5858
|
event.duration_ms ?? null,
|
|
5724
5859
|
event.project_dir ?? null,
|
|
5725
|
-
|
|
5860
|
+
metadata ?? null
|
|
5726
5861
|
]);
|
|
5727
5862
|
} catch (err) {
|
|
5728
5863
|
process.stderr.write(`[hooks db-writer] failed to write event: ${err}
|
|
@@ -5772,6 +5907,7 @@ function recordHookRun(record) {
|
|
|
5772
5907
|
var ALLOWED_EVENT_TYPES;
|
|
5773
5908
|
var init_db_writer = __esm(() => {
|
|
5774
5909
|
init_db();
|
|
5910
|
+
init_redact();
|
|
5775
5911
|
ALLOWED_EVENT_TYPES = new Set([
|
|
5776
5912
|
"PreToolUse",
|
|
5777
5913
|
"PostToolUse",
|
|
@@ -5784,13 +5920,73 @@ var init_db_writer = __esm(() => {
|
|
|
5784
5920
|
]);
|
|
5785
5921
|
});
|
|
5786
5922
|
|
|
5923
|
+
// src/lib/hook-env.ts
|
|
5924
|
+
function isDeniedEnvName(name) {
|
|
5925
|
+
const upper = name.toUpperCase();
|
|
5926
|
+
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
5927
|
+
return true;
|
|
5928
|
+
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
5929
|
+
return true;
|
|
5930
|
+
return DENY_CONTAINS.some((token) => upper.includes(token));
|
|
5931
|
+
}
|
|
5932
|
+
function buildHookEnv(source, extra) {
|
|
5933
|
+
const out = {};
|
|
5934
|
+
for (const name of ALLOWLIST) {
|
|
5935
|
+
const value = source[name] ?? extra?.[name];
|
|
5936
|
+
if (value !== undefined && value !== null)
|
|
5937
|
+
out[name] = value;
|
|
5938
|
+
}
|
|
5939
|
+
for (const [from, to] of CONFIG_PROJECTIONS) {
|
|
5940
|
+
const value = source[from] ?? extra?.[from];
|
|
5941
|
+
if (value !== undefined && value !== null)
|
|
5942
|
+
out[to] = value;
|
|
5943
|
+
}
|
|
5944
|
+
const merged = { ...source, ...extra ?? {} };
|
|
5945
|
+
for (const [name, value] of Object.entries(merged)) {
|
|
5946
|
+
if (out[name] !== undefined)
|
|
5947
|
+
continue;
|
|
5948
|
+
if (isDeniedEnvName(name))
|
|
5949
|
+
continue;
|
|
5950
|
+
if (value !== undefined && value !== null)
|
|
5951
|
+
out[name] = value;
|
|
5952
|
+
}
|
|
5953
|
+
return out;
|
|
5954
|
+
}
|
|
5955
|
+
var ALLOWLIST, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, CONFIG_PROJECTIONS;
|
|
5956
|
+
var init_hook_env = __esm(() => {
|
|
5957
|
+
ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
|
|
5958
|
+
DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
|
|
5959
|
+
DENY_PREFIXES = [
|
|
5960
|
+
"HASNA_",
|
|
5961
|
+
"AWS_",
|
|
5962
|
+
"AZURE_",
|
|
5963
|
+
"GCP_",
|
|
5964
|
+
"VAULT_",
|
|
5965
|
+
"GOOGLE_",
|
|
5966
|
+
"OPENAI_",
|
|
5967
|
+
"ANTHROPIC_",
|
|
5968
|
+
"POSTGRES_",
|
|
5969
|
+
"MYSQL_",
|
|
5970
|
+
"REDIS_",
|
|
5971
|
+
"MONGO_",
|
|
5972
|
+
"MEMENTOS_"
|
|
5973
|
+
];
|
|
5974
|
+
DENY_CONTAINS = ["DATABASE_URL"];
|
|
5975
|
+
CONFIG_PROJECTIONS = [
|
|
5976
|
+
["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
|
|
5977
|
+
["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
|
|
5978
|
+
["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
|
|
5979
|
+
["HASNA_HOOKS_CONFIG_PATH", "HOOKS_CONFIG_PATH"]
|
|
5980
|
+
];
|
|
5981
|
+
});
|
|
5982
|
+
|
|
5787
5983
|
// src/lib/run.ts
|
|
5788
5984
|
var exports_run = {};
|
|
5789
5985
|
__export(exports_run, {
|
|
5790
5986
|
executeVerifiedScript: () => executeVerifiedScript,
|
|
5791
5987
|
HookTimeoutError: () => HookTimeoutError
|
|
5792
5988
|
});
|
|
5793
|
-
import { randomBytes } from "crypto";
|
|
5989
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
5794
5990
|
import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
5795
5991
|
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
5796
5992
|
function extensionOf(scriptPath) {
|
|
@@ -5863,12 +6059,12 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
5863
6059
|
async function executeVerifiedScript(options) {
|
|
5864
6060
|
const scriptDir = dirname4(options.scriptPath);
|
|
5865
6061
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
5866
|
-
const tempPath = join9(scriptDir, `.hooks-verified-${
|
|
6062
|
+
const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
5867
6063
|
let proc = null;
|
|
5868
6064
|
let timedOut = false;
|
|
5869
6065
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
5870
6066
|
timedOut = true;
|
|
5871
|
-
killGroup(proc);
|
|
6067
|
+
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
5872
6068
|
}, options.timeout) : null;
|
|
5873
6069
|
try {
|
|
5874
6070
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
@@ -5876,7 +6072,7 @@ async function executeVerifiedScript(options) {
|
|
|
5876
6072
|
stdin: new Response(options.stdin),
|
|
5877
6073
|
stdout: "pipe",
|
|
5878
6074
|
stderr: "pipe",
|
|
5879
|
-
env:
|
|
6075
|
+
env: buildHookEnv(process.env, options.env),
|
|
5880
6076
|
detached: true
|
|
5881
6077
|
});
|
|
5882
6078
|
const exitCode = await proc.exited;
|
|
@@ -5887,7 +6083,7 @@ async function executeVerifiedScript(options) {
|
|
|
5887
6083
|
readPipeWithDeadline(proc.stdout, 750),
|
|
5888
6084
|
readPipeWithDeadline(proc.stderr, 750)
|
|
5889
6085
|
]);
|
|
5890
|
-
killGroup(proc);
|
|
6086
|
+
killGroup(proc, { leaderAlive: false });
|
|
5891
6087
|
return { stdout: stdoutText, stderr: stderrText, exitCode };
|
|
5892
6088
|
} finally {
|
|
5893
6089
|
if (timeoutTimer)
|
|
@@ -5897,12 +6093,36 @@ async function executeVerifiedScript(options) {
|
|
|
5897
6093
|
} catch {}
|
|
5898
6094
|
}
|
|
5899
6095
|
}
|
|
5900
|
-
function killGroup(proc) {
|
|
6096
|
+
function killGroup(proc, options) {
|
|
5901
6097
|
if (!proc)
|
|
5902
6098
|
return;
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
6099
|
+
const pgid = proc.pid;
|
|
6100
|
+
for (let pass = 0;pass < 5; pass++) {
|
|
6101
|
+
let members = [];
|
|
6102
|
+
try {
|
|
6103
|
+
const res = Bun.spawnSync(["ps", "-eo", "pid=,pgid="], { stdout: "pipe", stderr: "pipe" });
|
|
6104
|
+
if (res.exitCode === 0) {
|
|
6105
|
+
members = res.stdout.toString().split(`
|
|
6106
|
+
`).map((line) => line.trim().split(/\s+/)).filter((cols) => cols.length === 2 && cols[1] === String(pgid)).map((cols) => Number(cols[0])).filter((pid) => Number.isInteger(pid) && pid > 1).filter((pid) => options.leaderAlive || pid !== proc.pid);
|
|
6107
|
+
}
|
|
6108
|
+
} catch {}
|
|
6109
|
+
if (members.length === 0)
|
|
6110
|
+
break;
|
|
6111
|
+
let killed = 0;
|
|
6112
|
+
for (const pid of members) {
|
|
6113
|
+
try {
|
|
6114
|
+
const probe = Bun.spawnSync(["ps", "-o", "pid=,pgid=", "-p", String(pid)], { stdout: "pipe", stderr: "pipe" });
|
|
6115
|
+
const cols = probe.stdout.toString().trim().split(/\s+/);
|
|
6116
|
+
if (cols.length === 2 && cols[1] === String(pgid)) {
|
|
6117
|
+
process.kill(pid, "SIGKILL");
|
|
6118
|
+
killed++;
|
|
6119
|
+
}
|
|
6120
|
+
} catch {}
|
|
6121
|
+
}
|
|
6122
|
+
if (killed === 0)
|
|
6123
|
+
break;
|
|
6124
|
+
}
|
|
6125
|
+
if (options.leaderAlive) {
|
|
5906
6126
|
try {
|
|
5907
6127
|
process.kill(proc.pid, "SIGKILL");
|
|
5908
6128
|
} catch {}
|
|
@@ -5910,6 +6130,7 @@ function killGroup(proc) {
|
|
|
5910
6130
|
}
|
|
5911
6131
|
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
5912
6132
|
var init_run = __esm(() => {
|
|
6133
|
+
init_hook_env();
|
|
5913
6134
|
HookTimeoutError = class HookTimeoutError extends Error {
|
|
5914
6135
|
timeoutMs;
|
|
5915
6136
|
constructor(timeoutMs) {
|
|
@@ -6004,19 +6225,20 @@ async function installCustomSource(source) {
|
|
|
6004
6225
|
return copyManifestDir(source, source, "local");
|
|
6005
6226
|
}
|
|
6006
6227
|
async function installManifestUrl(url) {
|
|
6007
|
-
const res = await fetch(url, { redirect: "
|
|
6228
|
+
const res = await fetch(url, { redirect: "error" });
|
|
6008
6229
|
if (!res.ok) {
|
|
6009
6230
|
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
6010
6231
|
}
|
|
6011
6232
|
const manifest = parseManifest(await res.text());
|
|
6012
6233
|
let script;
|
|
6013
|
-
|
|
6014
|
-
`)
|
|
6234
|
+
const inline = manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
6235
|
+
`);
|
|
6236
|
+
if (inline) {
|
|
6015
6237
|
script = { path: "script.ts", content: manifest.script };
|
|
6016
6238
|
} else {
|
|
6017
6239
|
const base = url.substring(0, url.lastIndexOf("/"));
|
|
6018
6240
|
const scriptUrl = `${base}/${manifest.script}`;
|
|
6019
|
-
const scriptRes = await fetch(scriptUrl, { redirect: "
|
|
6241
|
+
const scriptRes = await fetch(scriptUrl, { redirect: "error" });
|
|
6020
6242
|
if (!scriptRes.ok) {
|
|
6021
6243
|
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
6022
6244
|
}
|
|
@@ -6040,8 +6262,10 @@ var init_custom_install = __esm(() => {
|
|
|
6040
6262
|
var exports_sync = {};
|
|
6041
6263
|
__export(exports_sync, {
|
|
6042
6264
|
syncHooks: () => syncHooks,
|
|
6265
|
+
stageSyncArtifacts: () => stageSyncArtifacts,
|
|
6043
6266
|
planSync: () => planSync,
|
|
6044
|
-
fetchPinnedHook: () => fetchPinnedHook
|
|
6267
|
+
fetchPinnedHook: () => fetchPinnedHook,
|
|
6268
|
+
commitSyncArtifacts: () => commitSyncArtifacts
|
|
6045
6269
|
});
|
|
6046
6270
|
import { createHash as createHash2 } from "crypto";
|
|
6047
6271
|
import { readFileSync as readFileSync8 } from "fs";
|
|
@@ -6065,7 +6289,7 @@ function collectBundledCatalog() {
|
|
|
6065
6289
|
}
|
|
6066
6290
|
return out;
|
|
6067
6291
|
}
|
|
6068
|
-
async function fetchJson(base, path) {
|
|
6292
|
+
async function fetchJson(base, path, opts) {
|
|
6069
6293
|
const headers = { accept: "application/json" };
|
|
6070
6294
|
const apiKey = resolveApiKey();
|
|
6071
6295
|
if (apiKey)
|
|
@@ -6081,7 +6305,11 @@ async function fetchJson(base, path) {
|
|
|
6081
6305
|
if (!res.ok) {
|
|
6082
6306
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6083
6307
|
}
|
|
6084
|
-
|
|
6308
|
+
const body = await res.json();
|
|
6309
|
+
if (opts?.withHeader) {
|
|
6310
|
+
body[opts.withHeader] = res.headers.get("x-hook-sha256");
|
|
6311
|
+
}
|
|
6312
|
+
return body;
|
|
6085
6313
|
}
|
|
6086
6314
|
async function fetchRemoteState(apiUrl) {
|
|
6087
6315
|
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
@@ -6094,6 +6322,16 @@ async function fetchRemoteState(apiUrl) {
|
|
|
6094
6322
|
}
|
|
6095
6323
|
return { catalog: catalog.hooks, remoteLock };
|
|
6096
6324
|
}
|
|
6325
|
+
function classifyRemotePin(pin, remoteVersion, remoteSha) {
|
|
6326
|
+
const pinnedLocally = pin !== undefined && pin.version === remoteVersion && pin.sha256 === remoteSha;
|
|
6327
|
+
if (pinnedLocally)
|
|
6328
|
+
return "unchanged";
|
|
6329
|
+
if (pin !== undefined && pin.pinned === true && pin.version !== remoteVersion)
|
|
6330
|
+
return "unchanged";
|
|
6331
|
+
if (pin !== undefined)
|
|
6332
|
+
return "updated";
|
|
6333
|
+
return "added";
|
|
6334
|
+
}
|
|
6097
6335
|
function computeDiff(catalog, remoteLock) {
|
|
6098
6336
|
const local = readLock();
|
|
6099
6337
|
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
@@ -6101,30 +6339,30 @@ function computeDiff(catalog, remoteLock) {
|
|
|
6101
6339
|
if (!entry.name || !entry.version || !entry.sha256)
|
|
6102
6340
|
continue;
|
|
6103
6341
|
const pin = local.hooks[entry.name];
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
diff.
|
|
6342
|
+
if (remoteLock) {
|
|
6343
|
+
const remotePin = remoteLock.hooks[entry.name];
|
|
6344
|
+
if (!remotePin) {
|
|
6345
|
+
diff.skipped.push(entry.name);
|
|
6346
|
+
continue;
|
|
6347
|
+
}
|
|
6348
|
+
if (remotePin.version !== entry.version) {
|
|
6349
|
+
throw new Error(`remote registry is ambiguous for '${entry.name}': lock pins ${remotePin.version} but catalog latest is ${entry.version}. ` + `Fix the registry (or its lock) before syncing \u2014 nothing was changed.`);
|
|
6350
|
+
}
|
|
6351
|
+
diff[classifyRemotePin(pin, remotePin.version, remotePin.sha256)].push(entry.name);
|
|
6114
6352
|
} else {
|
|
6115
|
-
diff.
|
|
6353
|
+
diff[classifyRemotePin(pin, entry.version, entry.sha256)].push(entry.name);
|
|
6116
6354
|
}
|
|
6117
6355
|
}
|
|
6118
6356
|
return diff;
|
|
6119
6357
|
}
|
|
6120
|
-
async function planSync() {
|
|
6358
|
+
async function planSync(options = {}) {
|
|
6121
6359
|
const apiUrl = resolveApiUrl();
|
|
6122
6360
|
if (apiUrl) {
|
|
6123
6361
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6124
|
-
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
6362
|
+
return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
6125
6363
|
}
|
|
6126
6364
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6127
|
-
return { apiUrl: null, dryRun: false, diff };
|
|
6365
|
+
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
6128
6366
|
}
|
|
6129
6367
|
async function syncHooks(options = {}) {
|
|
6130
6368
|
const apiUrl = resolveApiUrl();
|
|
@@ -6132,10 +6370,10 @@ async function syncHooks(options = {}) {
|
|
|
6132
6370
|
if (options.dryRun)
|
|
6133
6371
|
return { ...plan, dryRun: true };
|
|
6134
6372
|
if (!apiUrl) {
|
|
6135
|
-
const
|
|
6373
|
+
const db = getDb();
|
|
6136
6374
|
for (const entry of collectBundledCatalog()) {
|
|
6137
6375
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6138
|
-
upsertHookRecord(
|
|
6376
|
+
upsertHookRecord(db, {
|
|
6139
6377
|
name: entry.name,
|
|
6140
6378
|
version: entry.version,
|
|
6141
6379
|
sha256: entry.sha256,
|
|
@@ -6148,13 +6386,17 @@ async function syncHooks(options = {}) {
|
|
|
6148
6386
|
}
|
|
6149
6387
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6150
6388
|
const diff = computeDiff(catalog, remoteLock);
|
|
6151
|
-
const
|
|
6152
|
-
|
|
6153
|
-
|
|
6389
|
+
const staged = await stageSyncArtifacts(apiUrl, [...diff.added, ...diff.updated], remoteLock);
|
|
6390
|
+
await commitSyncArtifacts(staged, apiUrl, remoteLock);
|
|
6391
|
+
return { apiUrl, dryRun: false, diff };
|
|
6392
|
+
}
|
|
6393
|
+
async function stageSyncArtifacts(apiUrl, names, remoteLock) {
|
|
6394
|
+
const staged = [];
|
|
6395
|
+
for (const name of names) {
|
|
6154
6396
|
const entry = remoteLock.hooks[name];
|
|
6155
6397
|
if (!entry)
|
|
6156
6398
|
continue;
|
|
6157
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
6399
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
|
|
6158
6400
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6159
6401
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6160
6402
|
}
|
|
@@ -6162,34 +6404,64 @@ async function syncHooks(options = {}) {
|
|
|
6162
6404
|
if (actualSha !== entry.sha256) {
|
|
6163
6405
|
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6164
6406
|
}
|
|
6165
|
-
artifacts.set(name, artifact);
|
|
6166
|
-
}
|
|
6167
|
-
const db = getDb();
|
|
6168
|
-
for (const name of toFetch) {
|
|
6169
|
-
const artifact = artifacts.get(name);
|
|
6170
|
-
const entry = remoteLock.hooks[name];
|
|
6171
|
-
if (!artifact || !entry)
|
|
6172
|
-
continue;
|
|
6173
6407
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6174
|
-
|
|
6175
|
-
`
|
|
6176
|
-
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6177
|
-
const scriptPath = resolveScriptPath(name);
|
|
6178
|
-
const actual = await sha256File(scriptPath);
|
|
6179
|
-
if (actual !== entry.sha256) {
|
|
6180
|
-
throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6408
|
+
if (manifest.version !== entry.version) {
|
|
6409
|
+
throw new Error(`artifact for '${name}@${entry.version}' declares a different version ('${manifest.version}')`);
|
|
6181
6410
|
}
|
|
6182
|
-
|
|
6183
|
-
|
|
6411
|
+
const scriptRel = scriptRelFor(manifest);
|
|
6412
|
+
staged.push({
|
|
6184
6413
|
name,
|
|
6185
6414
|
version: entry.version,
|
|
6186
6415
|
sha256: entry.sha256,
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6416
|
+
source: entry.source ?? "remote",
|
|
6417
|
+
manifest,
|
|
6418
|
+
scriptContent: artifact.script,
|
|
6419
|
+
scriptRel
|
|
6190
6420
|
});
|
|
6191
6421
|
}
|
|
6192
|
-
return
|
|
6422
|
+
return staged;
|
|
6423
|
+
}
|
|
6424
|
+
async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
6425
|
+
const db = getDb();
|
|
6426
|
+
for (const item of staged) {
|
|
6427
|
+
const entry = remoteLock.hooks[item.name];
|
|
6428
|
+
if (!entry)
|
|
6429
|
+
continue;
|
|
6430
|
+
writeCustomHook(item.name, item.manifest, item.scriptContent, item.scriptRel);
|
|
6431
|
+
const scriptPath = resolveScriptPath(item.name);
|
|
6432
|
+
const actual = await sha256File(scriptPath);
|
|
6433
|
+
if (actual !== entry.sha256) {
|
|
6434
|
+
throw new Error(`sha256 mismatch after write for '${item.name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
const lock = readLock();
|
|
6438
|
+
for (const item of staged) {
|
|
6439
|
+
const entry = remoteLock.hooks[item.name];
|
|
6440
|
+
if (!entry)
|
|
6441
|
+
continue;
|
|
6442
|
+
lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
|
|
6443
|
+
}
|
|
6444
|
+
writeLock(lock);
|
|
6445
|
+
db.exec("BEGIN");
|
|
6446
|
+
try {
|
|
6447
|
+
for (const item of staged) {
|
|
6448
|
+
const entry = remoteLock.hooks[item.name];
|
|
6449
|
+
if (!entry)
|
|
6450
|
+
continue;
|
|
6451
|
+
upsertHookRecord(db, {
|
|
6452
|
+
name: item.name,
|
|
6453
|
+
version: entry.version,
|
|
6454
|
+
sha256: entry.sha256,
|
|
6455
|
+
source_type: entry.source ?? "remote",
|
|
6456
|
+
source_ref: apiUrl,
|
|
6457
|
+
last_verified_at: new Date().toISOString()
|
|
6458
|
+
});
|
|
6459
|
+
}
|
|
6460
|
+
db.exec("COMMIT");
|
|
6461
|
+
} catch (err) {
|
|
6462
|
+
db.exec("ROLLBACK");
|
|
6463
|
+
throw err;
|
|
6464
|
+
}
|
|
6193
6465
|
}
|
|
6194
6466
|
async function fetchPinnedHook(name, version, apiUrl) {
|
|
6195
6467
|
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
@@ -6197,10 +6469,11 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6197
6469
|
if (!entry) {
|
|
6198
6470
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
6199
6471
|
}
|
|
6200
|
-
|
|
6201
|
-
|
|
6472
|
+
const isLatest = entry.version === version;
|
|
6473
|
+
if (!isLatest && !(entry.versions ?? []).includes(version)) {
|
|
6474
|
+
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
|
|
6202
6475
|
}
|
|
6203
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}
|
|
6476
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
|
|
6204
6477
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6205
6478
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
6206
6479
|
}
|
|
@@ -6212,24 +6485,31 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6212
6485
|
throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
|
|
6213
6486
|
}
|
|
6214
6487
|
const actualSha = sha256OfText(artifact.script);
|
|
6215
|
-
|
|
6216
|
-
|
|
6488
|
+
let expectedSha = entry.sha256;
|
|
6489
|
+
if (!isLatest) {
|
|
6490
|
+
const headerSha = artifact._headerSha;
|
|
6491
|
+
if (!headerSha) {
|
|
6492
|
+
throw new Error(`registry did not return a sha256 header for '${name}@${version}' (older pin); refusing to trust an unverified artifact`);
|
|
6493
|
+
}
|
|
6494
|
+
expectedSha = headerSha;
|
|
6495
|
+
}
|
|
6496
|
+
if (actualSha !== expectedSha) {
|
|
6497
|
+
throw new Error(`sha256 mismatch for '${name}@${version}': expected ${expectedSha}, artifact has ${actualSha}`);
|
|
6217
6498
|
}
|
|
6218
6499
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6219
|
-
const scriptRel =
|
|
6220
|
-
`) ? "script.ts" : artifact.manifest.script;
|
|
6500
|
+
const scriptRel = scriptRelFor(manifest);
|
|
6221
6501
|
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6222
6502
|
const scriptPath = resolveScriptPath(name);
|
|
6223
6503
|
const actual = await sha256File(scriptPath);
|
|
6224
|
-
if (actual !==
|
|
6225
|
-
throw new Error(`sha256 mismatch after write for '${name}@${version}':
|
|
6504
|
+
if (actual !== expectedSha) {
|
|
6505
|
+
throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
|
|
6226
6506
|
}
|
|
6227
6507
|
const db = getDb();
|
|
6228
|
-
setPinnedHook(name, { version, sha256:
|
|
6508
|
+
setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
|
|
6229
6509
|
upsertHookRecord(db, {
|
|
6230
6510
|
name,
|
|
6231
6511
|
version,
|
|
6232
|
-
sha256:
|
|
6512
|
+
sha256: expectedSha,
|
|
6233
6513
|
source_type: entry.source ?? "remote",
|
|
6234
6514
|
source_ref: apiUrl,
|
|
6235
6515
|
last_verified_at: new Date().toISOString()
|
|
@@ -6237,7 +6517,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6237
6517
|
return {
|
|
6238
6518
|
name,
|
|
6239
6519
|
version,
|
|
6240
|
-
sha256:
|
|
6520
|
+
sha256: expectedSha,
|
|
6241
6521
|
source: entry.source ?? "remote",
|
|
6242
6522
|
source_ref: apiUrl,
|
|
6243
6523
|
artifact,
|
|
@@ -6262,7 +6542,7 @@ var init_pg_migrations = __esm(() => {
|
|
|
6262
6542
|
timestamp TEXT NOT NULL,
|
|
6263
6543
|
session_id TEXT NOT NULL,
|
|
6264
6544
|
hook_name TEXT NOT NULL,
|
|
6265
|
-
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
|
|
6545
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
|
|
6266
6546
|
tool_name TEXT,
|
|
6267
6547
|
tool_input TEXT,
|
|
6268
6548
|
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
@@ -6295,7 +6575,10 @@ var init_pg_migrations = __esm(() => {
|
|
|
6295
6575
|
)`,
|
|
6296
6576
|
`ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
|
|
6297
6577
|
`ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
|
|
6298
|
-
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))
|
|
6578
|
+
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`,
|
|
6579
|
+
`ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
|
|
6580
|
+
`ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
|
|
6581
|
+
CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart'))`
|
|
6299
6582
|
];
|
|
6300
6583
|
});
|
|
6301
6584
|
|
|
@@ -6310,7 +6593,34 @@ function normalizeParams(params) {
|
|
|
6310
6593
|
return flat.map((value) => value === undefined ? null : value);
|
|
6311
6594
|
}
|
|
6312
6595
|
function sslConfigFor(connectionString) {
|
|
6313
|
-
|
|
6596
|
+
const wantsSsl = wantsSslFor(connectionString);
|
|
6597
|
+
if (!wantsSsl)
|
|
6598
|
+
return;
|
|
6599
|
+
const insecure = process.env.HASNA_HOOKS_PG_INSECURE_TLS === "1" || process.env.HOOKS_PG_INSECURE_TLS === "1";
|
|
6600
|
+
if (insecure && false) {}
|
|
6601
|
+
return insecure ? { rejectUnauthorized: false } : { rejectUnauthorized: true };
|
|
6602
|
+
}
|
|
6603
|
+
function wantsSslFor(connectionString) {
|
|
6604
|
+
let raw;
|
|
6605
|
+
try {
|
|
6606
|
+
raw = new URL(connectionString).search.replace(/^\?/, "");
|
|
6607
|
+
} catch {
|
|
6608
|
+
raw = connectionString;
|
|
6609
|
+
}
|
|
6610
|
+
try {
|
|
6611
|
+
raw = decodeURIComponent(raw);
|
|
6612
|
+
} catch {}
|
|
6613
|
+
const paramValue = (name) => {
|
|
6614
|
+
const match = raw.match(new RegExp(`(?:^|[&\\s])${name}\\s*=\\s*([^&\\s]+)`, "i"));
|
|
6615
|
+
return match ? match[1].toLowerCase() : undefined;
|
|
6616
|
+
};
|
|
6617
|
+
const sslmode = paramValue("sslmode");
|
|
6618
|
+
if (sslmode !== undefined)
|
|
6619
|
+
return sslmode !== "disable" && sslmode !== "allow";
|
|
6620
|
+
const ssl = paramValue("ssl");
|
|
6621
|
+
if (ssl !== undefined)
|
|
6622
|
+
return ssl !== "false" && ssl !== "0" && ssl !== "no";
|
|
6623
|
+
return false;
|
|
6314
6624
|
}
|
|
6315
6625
|
|
|
6316
6626
|
class PgAdapterAsync {
|
|
@@ -6472,9 +6782,15 @@ async function pushTable(db, remote, table) {
|
|
|
6472
6782
|
result.rowsRead = rows.length;
|
|
6473
6783
|
if (rows.length === 0)
|
|
6474
6784
|
return result;
|
|
6785
|
+
const projected = table === "hook_events" ? rows.map((row) => ({
|
|
6786
|
+
...row,
|
|
6787
|
+
tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
|
|
6788
|
+
error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
|
|
6789
|
+
metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
|
|
6790
|
+
})) : rows;
|
|
6475
6791
|
const remoteColumns = await getRemoteColumns(remote, table);
|
|
6476
|
-
const columns = filterRemoteColumns(remoteColumns, Object.keys(
|
|
6477
|
-
result.rowsWritten = await upsertPg(remote, table, columns,
|
|
6792
|
+
const columns = filterRemoteColumns(remoteColumns, Object.keys(projected[0]));
|
|
6793
|
+
result.rowsWritten = await upsertPg(remote, table, columns, projected, remoteColumns);
|
|
6478
6794
|
} catch (error) {
|
|
6479
6795
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6480
6796
|
}
|
|
@@ -6489,8 +6805,14 @@ async function pullTable(remote, db, table) {
|
|
|
6489
6805
|
result.rowsRead = rows.length;
|
|
6490
6806
|
if (rows.length === 0)
|
|
6491
6807
|
return result;
|
|
6492
|
-
const
|
|
6493
|
-
|
|
6808
|
+
const projected = table === "hook_events" ? rows.map((row) => ({
|
|
6809
|
+
...row,
|
|
6810
|
+
tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
|
|
6811
|
+
error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
|
|
6812
|
+
metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
|
|
6813
|
+
})) : rows;
|
|
6814
|
+
const columns = filterLocalColumns(db, table, Object.keys(projected[0]));
|
|
6815
|
+
result.rowsWritten = upsertSqlite(db, table, columns, projected);
|
|
6494
6816
|
} catch (error) {
|
|
6495
6817
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6496
6818
|
}
|
|
@@ -6613,6 +6935,7 @@ var init_storage_sync = __esm(() => {
|
|
|
6613
6935
|
init_db();
|
|
6614
6936
|
init_pg_migrations();
|
|
6615
6937
|
init_remote_storage();
|
|
6938
|
+
init_redact();
|
|
6616
6939
|
STORAGE_TABLES = [
|
|
6617
6940
|
"hook_events",
|
|
6618
6941
|
"schema_migrations",
|
|
@@ -6711,24 +7034,29 @@ async function buildCatalog() {
|
|
|
6711
7034
|
const scriptPath = resolveHook(meta.name)?.scriptPath;
|
|
6712
7035
|
if (!scriptPath)
|
|
6713
7036
|
continue;
|
|
7037
|
+
const sha = await sha256File(scriptPath);
|
|
6714
7038
|
byName.set(meta.name, {
|
|
6715
7039
|
name: meta.name,
|
|
6716
7040
|
version: meta.version,
|
|
6717
|
-
sha256:
|
|
7041
|
+
sha256: sha,
|
|
6718
7042
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
6719
7043
|
description: meta.description,
|
|
6720
|
-
source: "bundled"
|
|
7044
|
+
source: "bundled",
|
|
7045
|
+
versions: [meta.version]
|
|
6721
7046
|
});
|
|
6722
7047
|
}
|
|
6723
7048
|
for (const custom of listCustomHooks()) {
|
|
6724
7049
|
const name = shortManifestName(custom.manifest.name);
|
|
7050
|
+
const sha = await sha256File(custom.scriptPath);
|
|
7051
|
+
const existing = byName.get(name);
|
|
6725
7052
|
byName.set(name, {
|
|
6726
7053
|
name,
|
|
6727
7054
|
version: custom.manifest.version,
|
|
6728
|
-
sha256:
|
|
7055
|
+
sha256: sha,
|
|
6729
7056
|
events: custom.manifest.events,
|
|
6730
7057
|
description: custom.manifest.description ?? "Custom hook",
|
|
6731
|
-
source:
|
|
7058
|
+
source: existing ? "custom-overrides-bundled" : "custom",
|
|
7059
|
+
versions: existing ? [...new Set([...existing.versions, custom.manifest.version])] : [custom.manifest.version]
|
|
6732
7060
|
});
|
|
6733
7061
|
}
|
|
6734
7062
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -6746,6 +7074,7 @@ async function artifactFor(name, version) {
|
|
|
6746
7074
|
description: resolved.description,
|
|
6747
7075
|
events: resolved.events,
|
|
6748
7076
|
script: custom ? custom.manifest.script : `src/hook.ts`,
|
|
7077
|
+
script_kind: custom?.manifest.script_kind,
|
|
6749
7078
|
args: custom?.manifest.args,
|
|
6750
7079
|
timeout_ms: custom?.manifest.timeout_ms
|
|
6751
7080
|
};
|
|
@@ -6775,11 +7104,32 @@ function handleServeRequest(req, apiKey) {
|
|
|
6775
7104
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
6776
7105
|
}
|
|
6777
7106
|
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
6778
|
-
return
|
|
7107
|
+
return buildCatalog().then((catalog) => {
|
|
7108
|
+
const lock = readLock();
|
|
7109
|
+
const byVersion = {};
|
|
7110
|
+
for (const entry of catalog)
|
|
7111
|
+
byVersion[entry.name] = entry.versions;
|
|
7112
|
+
const hooks = {};
|
|
7113
|
+
for (const [name, pin] of Object.entries(lock.hooks)) {
|
|
7114
|
+
hooks[name] = { ...pin, versions: byVersion[name] ?? [pin.version] };
|
|
7115
|
+
}
|
|
7116
|
+
return json({ hooks });
|
|
7117
|
+
});
|
|
6779
7118
|
}
|
|
6780
|
-
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(
|
|
7119
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
|
|
6781
7120
|
if (artifactMatch && req.method === "GET") {
|
|
6782
|
-
const [,
|
|
7121
|
+
const [, rawName, rawVersion] = artifactMatch;
|
|
7122
|
+
let name;
|
|
7123
|
+
let version;
|
|
7124
|
+
try {
|
|
7125
|
+
name = decodeURIComponent(rawName);
|
|
7126
|
+
version = decodeURIComponent(rawVersion);
|
|
7127
|
+
} catch {
|
|
7128
|
+
return Promise.resolve(json({ error: "invalid URL encoding" }, 400));
|
|
7129
|
+
}
|
|
7130
|
+
if (!SEMVER_PATTERN.test(version)) {
|
|
7131
|
+
return Promise.resolve(json({ error: `invalid semver version '${version}'` }, 400));
|
|
7132
|
+
}
|
|
6783
7133
|
return artifactFor(name, version).then(async (artifact) => {
|
|
6784
7134
|
if (!artifact)
|
|
6785
7135
|
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
@@ -6810,7 +7160,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
6810
7160
|
function startServeServer(options) {
|
|
6811
7161
|
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
6812
7162
|
const host = options.host ?? SERVE_HOST;
|
|
6813
|
-
const apiKey = resolveApiKey(
|
|
7163
|
+
const apiKey = resolveApiKey();
|
|
6814
7164
|
const server = Bun.serve({
|
|
6815
7165
|
hostname: host,
|
|
6816
7166
|
port,
|
|
@@ -6828,6 +7178,7 @@ var init_serve = __esm(() => {
|
|
|
6828
7178
|
init_store();
|
|
6829
7179
|
init_resolve();
|
|
6830
7180
|
init_config();
|
|
7181
|
+
init_semver();
|
|
6831
7182
|
if (false) {}
|
|
6832
7183
|
});
|
|
6833
7184
|
|
|
@@ -6917,7 +7268,9 @@ var exports_server = {};
|
|
|
6917
7268
|
__export(exports_server, {
|
|
6918
7269
|
startStdioServer: () => startStdioServer,
|
|
6919
7270
|
startSSEServer: () => startSSEServer,
|
|
7271
|
+
sseAuthToken: () => sseAuthToken,
|
|
6920
7272
|
createHooksServer: () => createHooksServer,
|
|
7273
|
+
MCP_SSE_HOST: () => MCP_SSE_HOST,
|
|
6921
7274
|
MCP_PORT: () => MCP_PORT
|
|
6922
7275
|
});
|
|
6923
7276
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -6945,7 +7298,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
|
6945
7298
|
const started = Date.now();
|
|
6946
7299
|
let ran;
|
|
6947
7300
|
try {
|
|
6948
|
-
ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin,
|
|
7301
|
+
ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, timeout: timeoutMs });
|
|
6949
7302
|
} catch (err) {
|
|
6950
7303
|
try {
|
|
6951
7304
|
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
@@ -7051,6 +7404,9 @@ function compactEvent(row) {
|
|
|
7051
7404
|
tool_input_preview: row.tool_input ? truncateText(row.tool_input, 180) : undefined
|
|
7052
7405
|
};
|
|
7053
7406
|
}
|
|
7407
|
+
function projectLogRows(rows) {
|
|
7408
|
+
return rows.map((row) => projectEventRowForRead({ ...row }));
|
|
7409
|
+
}
|
|
7054
7410
|
function createHooksServer() {
|
|
7055
7411
|
const server = new McpServer({
|
|
7056
7412
|
name: "@hasna/hooks",
|
|
@@ -7432,7 +7788,13 @@ function createHooksServer() {
|
|
|
7432
7788
|
} catch (err) {
|
|
7433
7789
|
const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7434
7790
|
if (err instanceof HookTimeoutError2) {
|
|
7435
|
-
return {
|
|
7791
|
+
return {
|
|
7792
|
+
name,
|
|
7793
|
+
decision: "block",
|
|
7794
|
+
timedOut: true,
|
|
7795
|
+
timeout_ms: effectiveTimeout,
|
|
7796
|
+
error: `hook timed out after ${effectiveTimeout}ms \u2014 preview blocked (never auto-approved)`
|
|
7797
|
+
};
|
|
7436
7798
|
}
|
|
7437
7799
|
return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
|
|
7438
7800
|
}
|
|
@@ -7458,15 +7820,25 @@ function createHooksServer() {
|
|
|
7458
7820
|
hooks: z2.array(z2.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
|
|
7459
7821
|
scope: z2.enum(["global", "project"]).default("global").describe("Install scope")
|
|
7460
7822
|
}, async ({ agent_type, name, hooks, scope }) => {
|
|
7823
|
+
if (agent_type !== "claude" && agent_type !== "gemini" && agent_type !== "custom") {
|
|
7824
|
+
return {
|
|
7825
|
+
content: [{
|
|
7826
|
+
type: "text",
|
|
7827
|
+
text: JSON.stringify({ error: `unsupported agent_type '${String(agent_type)}' (supported: claude, gemini, custom)` })
|
|
7828
|
+
}],
|
|
7829
|
+
isError: true
|
|
7830
|
+
};
|
|
7831
|
+
}
|
|
7461
7832
|
const profile = createProfile({ agent_type, name });
|
|
7833
|
+
const target = agent_type === "gemini" ? "gemini" : "claude";
|
|
7462
7834
|
const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
|
|
7463
|
-
const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id }));
|
|
7835
|
+
const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id, target }));
|
|
7464
7836
|
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
7465
7837
|
const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
|
|
7466
7838
|
return {
|
|
7467
7839
|
content: [{
|
|
7468
7840
|
type: "text",
|
|
7469
|
-
text: JSON.stringify({ profile, installed, failed, scope, run_with: `hooks run <name> --profile ${profile.agent_id}` })
|
|
7841
|
+
text: JSON.stringify({ profile, installed, failed, scope, target, run_with: `hooks run <name> --profile ${profile.agent_id}` })
|
|
7470
7842
|
}]
|
|
7471
7843
|
};
|
|
7472
7844
|
});
|
|
@@ -7618,12 +7990,13 @@ function createHooksServer() {
|
|
|
7618
7990
|
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
7619
7991
|
params.push(maxRows);
|
|
7620
7992
|
const rows = db.query(sql).all(...params);
|
|
7993
|
+
const projected = projectLogRows(rows);
|
|
7621
7994
|
return {
|
|
7622
7995
|
content: [{
|
|
7623
7996
|
type: "text",
|
|
7624
7997
|
text: JSON.stringify({
|
|
7625
|
-
events: compact ?
|
|
7626
|
-
count:
|
|
7998
|
+
events: compact ? projected.map(compactEvent) : projected,
|
|
7999
|
+
count: projected.length,
|
|
7627
8000
|
compact,
|
|
7628
8001
|
hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
|
|
7629
8002
|
})
|
|
@@ -7638,12 +8011,13 @@ function createHooksServer() {
|
|
|
7638
8011
|
const db = getDb2();
|
|
7639
8012
|
const maxRows = boundedLimit(n, 20, compact ? 100 : 500);
|
|
7640
8013
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(maxRows);
|
|
8014
|
+
const projected = projectLogRows(rows);
|
|
7641
8015
|
return {
|
|
7642
8016
|
content: [{
|
|
7643
8017
|
type: "text",
|
|
7644
8018
|
text: JSON.stringify({
|
|
7645
|
-
events: compact ?
|
|
7646
|
-
count:
|
|
8019
|
+
events: compact ? projected.map(compactEvent) : projected,
|
|
8020
|
+
count: projected.length,
|
|
7647
8021
|
compact,
|
|
7648
8022
|
hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
|
|
7649
8023
|
})
|
|
@@ -7668,12 +8042,13 @@ function createHooksServer() {
|
|
|
7668
8042
|
}
|
|
7669
8043
|
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
7670
8044
|
const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, maxRows);
|
|
8045
|
+
const projected = projectLogRows(rows);
|
|
7671
8046
|
return {
|
|
7672
8047
|
content: [{
|
|
7673
8048
|
type: "text",
|
|
7674
8049
|
text: JSON.stringify({
|
|
7675
|
-
events: compact ?
|
|
7676
|
-
count:
|
|
8050
|
+
events: compact ? projected.map(compactEvent) : projected,
|
|
8051
|
+
count: projected.length,
|
|
7677
8052
|
compact,
|
|
7678
8053
|
hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
|
|
7679
8054
|
})
|
|
@@ -7801,11 +8176,48 @@ function createHooksServer() {
|
|
|
7801
8176
|
});
|
|
7802
8177
|
return server;
|
|
7803
8178
|
}
|
|
7804
|
-
|
|
8179
|
+
function isLoopbackHost(host) {
|
|
8180
|
+
return host === "" || host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
8181
|
+
}
|
|
8182
|
+
function sseAuthToken(explicit) {
|
|
8183
|
+
if (explicit && explicit.length > 0)
|
|
8184
|
+
return explicit;
|
|
8185
|
+
for (const name of MCP_SSE_AUTH_ENV) {
|
|
8186
|
+
const value = process.env[name];
|
|
8187
|
+
if (value && value.length > 0)
|
|
8188
|
+
return value;
|
|
8189
|
+
}
|
|
8190
|
+
return;
|
|
8191
|
+
}
|
|
8192
|
+
async function startSSEServer(options = {}) {
|
|
8193
|
+
const port = options.port ?? MCP_PORT;
|
|
8194
|
+
const host = options.host ?? MCP_SSE_HOST;
|
|
8195
|
+
const authToken = sseAuthToken(options.authToken);
|
|
8196
|
+
const loopback = isLoopbackHost(host);
|
|
8197
|
+
if (!loopback && !authToken) {
|
|
8198
|
+
throw new Error(`Refusing to serve MCP SSE on non-loopback host '${host}' without an auth token. Set HASNA_HOOKS_MCP_TOKEN (or HOOKS_MCP_TOKEN) to authorize a non-loopback bind.`);
|
|
8199
|
+
}
|
|
7805
8200
|
const server = createHooksServer();
|
|
7806
8201
|
const transports = new Map;
|
|
8202
|
+
function authorized2(headers) {
|
|
8203
|
+
if (!authToken)
|
|
8204
|
+
return false;
|
|
8205
|
+
const rawAuth = headers["authorization"];
|
|
8206
|
+
const header = (Array.isArray(rawAuth) ? rawAuth[0] : rawAuth) ?? "";
|
|
8207
|
+
if (header.startsWith("Bearer "))
|
|
8208
|
+
return secureEqual(header.slice("Bearer ".length), authToken);
|
|
8209
|
+
const rawKey = headers["x-api-key"];
|
|
8210
|
+
return secureEqual((Array.isArray(rawKey) ? rawKey[0] : rawKey) ?? "", authToken);
|
|
8211
|
+
}
|
|
7807
8212
|
const httpServer = createServer(async (req, res) => {
|
|
7808
8213
|
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
8214
|
+
if (url.pathname === "/sse" || url.pathname === "/messages") {
|
|
8215
|
+
if (authToken && !authorized2(req.headers)) {
|
|
8216
|
+
res.writeHead(401);
|
|
8217
|
+
res.end("unauthorized: valid MCP token required");
|
|
8218
|
+
return;
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
7809
8221
|
if (url.pathname === "/sse") {
|
|
7810
8222
|
const transport = new SSEServerTransport("/messages", res);
|
|
7811
8223
|
transports.set(transport.sessionId, transport);
|
|
@@ -7828,9 +8240,9 @@ async function startSSEServer(port = MCP_PORT) {
|
|
|
7828
8240
|
res.end(JSON.stringify({ name: "@hasna/hooks", version: pkg.version, transport: "sse", port }));
|
|
7829
8241
|
}
|
|
7830
8242
|
});
|
|
7831
|
-
httpServer.listen(port, () => {
|
|
7832
|
-
console.error(`@hasna/hooks MCP server running on http
|
|
7833
|
-
console.error(`SSE endpoint: http
|
|
8243
|
+
httpServer.listen(port, host, () => {
|
|
8244
|
+
console.error(`@hasna/hooks MCP server running on http://${host}:${port}`);
|
|
8245
|
+
console.error(`SSE endpoint: http://${host}:${port}/sse`);
|
|
7834
8246
|
});
|
|
7835
8247
|
}
|
|
7836
8248
|
async function startStdioServer() {
|
|
@@ -7844,7 +8256,7 @@ async function startStdioServer() {
|
|
|
7844
8256
|
process.exit(1);
|
|
7845
8257
|
}
|
|
7846
8258
|
}
|
|
7847
|
-
var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents;
|
|
8259
|
+
var __dirname3, pkg, MCP_PORT = 39427, _hooksAgents, MCP_SSE_HOST = "127.0.0.1", MCP_SSE_AUTH_ENV;
|
|
7848
8260
|
var init_server = __esm(() => {
|
|
7849
8261
|
init_registry();
|
|
7850
8262
|
init_installer();
|
|
@@ -7852,6 +8264,7 @@ var init_server = __esm(() => {
|
|
|
7852
8264
|
init_manifest();
|
|
7853
8265
|
init_resolve();
|
|
7854
8266
|
init_store();
|
|
8267
|
+
init_redact();
|
|
7855
8268
|
init_storage();
|
|
7856
8269
|
__dirname3 = dirname5(fileURLToPath3(import.meta.url));
|
|
7857
8270
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
@@ -7865,6 +8278,7 @@ var init_server = __esm(() => {
|
|
|
7865
8278
|
}
|
|
7866
8279
|
} catch {}
|
|
7867
8280
|
_hooksAgents = new Map;
|
|
8281
|
+
MCP_SSE_AUTH_ENV = ["HASNA_HOOKS_MCP_TOKEN", "HOOKS_MCP_TOKEN"];
|
|
7868
8282
|
});
|
|
7869
8283
|
|
|
7870
8284
|
// src/mcp/http.ts
|
|
@@ -7926,7 +8340,7 @@ function startMcpHttpServer(options) {
|
|
|
7926
8340
|
var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
|
|
7927
8341
|
var init_http = () => {};
|
|
7928
8342
|
|
|
7929
|
-
//
|
|
8343
|
+
// node_modules/@hasna/events/dist/commander.js
|
|
7930
8344
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
7931
8345
|
import { Buffer as Buffer2 } from "buffer";
|
|
7932
8346
|
import { existsSync } from "fs";
|
|
@@ -9111,7 +9525,7 @@ function replaySummary(events, deliveries, nextCursor) {
|
|
|
9111
9525
|
// src/cli/index.tsx
|
|
9112
9526
|
import { render } from "ink";
|
|
9113
9527
|
|
|
9114
|
-
//
|
|
9528
|
+
// node_modules/commander/esm.mjs
|
|
9115
9529
|
var import__ = __toESM(require_commander(), 1);
|
|
9116
9530
|
var {
|
|
9117
9531
|
program,
|
|
@@ -9137,11 +9551,11 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
9137
9551
|
import { useState as useState7 } from "react";
|
|
9138
9552
|
import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
|
|
9139
9553
|
|
|
9140
|
-
//
|
|
9554
|
+
// node_modules/ink-select-input/build/Indicator.js
|
|
9141
9555
|
import React from "react";
|
|
9142
9556
|
import { Box, Text } from "ink";
|
|
9143
9557
|
|
|
9144
|
-
//
|
|
9558
|
+
// node_modules/is-unicode-supported/index.js
|
|
9145
9559
|
import process2 from "process";
|
|
9146
9560
|
function isUnicodeSupported() {
|
|
9147
9561
|
const { env } = process2;
|
|
@@ -9152,7 +9566,7 @@ function isUnicodeSupported() {
|
|
|
9152
9566
|
return Boolean(env.WT_SESSION) || Boolean(env.TERMINUS_SUBLIME) || env.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
|
|
9153
9567
|
}
|
|
9154
9568
|
|
|
9155
|
-
//
|
|
9569
|
+
// node_modules/figures/index.js
|
|
9156
9570
|
var common = {
|
|
9157
9571
|
circleQuestionMark: "(?)",
|
|
9158
9572
|
questionMarkPrefix: "(?)",
|
|
@@ -9428,23 +9842,23 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
|
|
|
9428
9842
|
var figures_default = figures;
|
|
9429
9843
|
var replacements = Object.entries(specialMainSymbols);
|
|
9430
9844
|
|
|
9431
|
-
//
|
|
9845
|
+
// node_modules/ink-select-input/build/Indicator.js
|
|
9432
9846
|
function Indicator({ isSelected = false }) {
|
|
9433
9847
|
return React.createElement(Box, { marginRight: 1 }, isSelected ? React.createElement(Text, { color: "blue" }, figures_default.pointer) : React.createElement(Text, null, " "));
|
|
9434
9848
|
}
|
|
9435
9849
|
var Indicator_default = Indicator;
|
|
9436
|
-
//
|
|
9850
|
+
// node_modules/ink-select-input/build/Item.js
|
|
9437
9851
|
import * as React2 from "react";
|
|
9438
9852
|
import { Text as Text2 } from "ink";
|
|
9439
9853
|
function Item({ isSelected = false, label }) {
|
|
9440
9854
|
return React2.createElement(Text2, { color: isSelected ? "blue" : undefined }, label);
|
|
9441
9855
|
}
|
|
9442
9856
|
var Item_default = Item;
|
|
9443
|
-
//
|
|
9857
|
+
// node_modules/ink-select-input/build/SelectInput.js
|
|
9444
9858
|
import { isDeepStrictEqual } from "util";
|
|
9445
9859
|
import React3, { useState, useEffect, useRef, useCallback } from "react";
|
|
9446
9860
|
|
|
9447
|
-
//
|
|
9861
|
+
// node_modules/to-rotated/index.js
|
|
9448
9862
|
function toRotated(array, steps) {
|
|
9449
9863
|
if (!Array.isArray(array)) {
|
|
9450
9864
|
throw new TypeError(`Expected an array, got \`${typeof array}\`.`);
|
|
@@ -9466,7 +9880,7 @@ function toRotated(array, steps) {
|
|
|
9466
9880
|
];
|
|
9467
9881
|
}
|
|
9468
9882
|
|
|
9469
|
-
//
|
|
9883
|
+
// node_modules/ink-select-input/build/SelectInput.js
|
|
9470
9884
|
import { Box as Box2, useInput } from "ink";
|
|
9471
9885
|
function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent = Indicator_default, itemComponent = Item_default, limit: customLimit, onSelect, onHighlight }) {
|
|
9472
9886
|
const hasLimit = typeof customLimit === "number" && items.length > customLimit;
|
|
@@ -9762,7 +10176,7 @@ function HookSelect({
|
|
|
9762
10176
|
import { useState as useState4, useEffect as useEffect3 } from "react";
|
|
9763
10177
|
import { Box as Box6, Text as Text7, useInput as useInput4 } from "ink";
|
|
9764
10178
|
|
|
9765
|
-
//
|
|
10179
|
+
// node_modules/ink-text-input/build/index.js
|
|
9766
10180
|
import React5, { useState as useState3, useEffect as useEffect2 } from "react";
|
|
9767
10181
|
import { Text as Text6, useInput as useInput3 } from "ink";
|
|
9768
10182
|
import chalk from "chalk";
|
|
@@ -9940,7 +10354,7 @@ function SearchView({
|
|
|
9940
10354
|
import { useState as useState6, useEffect as useEffect5 } from "react";
|
|
9941
10355
|
import { Box as Box7, Text as Text9 } from "ink";
|
|
9942
10356
|
|
|
9943
|
-
//
|
|
10357
|
+
// node_modules/ink-spinner/build/index.js
|
|
9944
10358
|
var import_cli_spinners = __toESM(require_cli_spinners(), 1);
|
|
9945
10359
|
import React7, { useState as useState5, useEffect as useEffect4 } from "react";
|
|
9946
10360
|
import { Text as Text8 } from "ink";
|
|
@@ -10305,10 +10719,12 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
10305
10719
|
// src/cli/index.tsx
|
|
10306
10720
|
init_registry();
|
|
10307
10721
|
init_installer();
|
|
10722
|
+
init_redact();
|
|
10308
10723
|
init_profiles();
|
|
10309
10724
|
init_manifest();
|
|
10310
10725
|
init_resolve();
|
|
10311
10726
|
init_store();
|
|
10727
|
+
init_semver();
|
|
10312
10728
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
10313
10729
|
var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
|
|
10314
10730
|
var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
|
|
@@ -10340,6 +10756,9 @@ function truncateText2(value, max = 96) {
|
|
|
10340
10756
|
return text;
|
|
10341
10757
|
return `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
10342
10758
|
}
|
|
10759
|
+
function projectLogRows2(rows) {
|
|
10760
|
+
return rows.map((row) => projectEventRowForRead({ ...row }));
|
|
10761
|
+
}
|
|
10343
10762
|
function readToken(tokenFile) {
|
|
10344
10763
|
if (tokenFile) {
|
|
10345
10764
|
try {
|
|
@@ -10601,6 +11020,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
10601
11020
|
console.log(chalk2.red(`Unknown category: ${options.category}`));
|
|
10602
11021
|
console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
|
|
10603
11022
|
}
|
|
11023
|
+
process.exitCode = 1;
|
|
10604
11024
|
return;
|
|
10605
11025
|
}
|
|
10606
11026
|
toInstall = getHooksByCategory(category).map((h) => h.name);
|
|
@@ -10633,7 +11053,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
10633
11053
|
const version = arg.slice(at + 1);
|
|
10634
11054
|
if (!HOOK_NAME_RE2.test(name))
|
|
10635
11055
|
return null;
|
|
10636
|
-
if (
|
|
11056
|
+
if (!SEMVER_PATTERN.test(version))
|
|
10637
11057
|
return null;
|
|
10638
11058
|
return { name, version };
|
|
10639
11059
|
}
|
|
@@ -10767,7 +11187,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10767
11187
|
target,
|
|
10768
11188
|
applied: results.some((r) => r.applied)
|
|
10769
11189
|
}));
|
|
10770
|
-
if (results.
|
|
11190
|
+
if (results.some((r) => !r.success)) {
|
|
10771
11191
|
process.exitCode = 1;
|
|
10772
11192
|
}
|
|
10773
11193
|
return;
|
|
@@ -10805,6 +11225,11 @@ Installing hooks (${scope}, ${target})...
|
|
|
10805
11225
|
process.exitCode = 1;
|
|
10806
11226
|
return;
|
|
10807
11227
|
}
|
|
11228
|
+
if (successCount < results.length) {
|
|
11229
|
+
console.log(chalk2.red(`
|
|
11230
|
+
\u2717 ${results.length - successCount} of ${results.length} hook(s) failed to install.`));
|
|
11231
|
+
process.exitCode = 1;
|
|
11232
|
+
}
|
|
10808
11233
|
console.log(chalk2.dim(`
|
|
10809
11234
|
Registered in ${settingsFile}`));
|
|
10810
11235
|
});
|
|
@@ -11000,6 +11425,7 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
|
|
|
11000
11425
|
} else {
|
|
11001
11426
|
console.log(chalk2.red(`Hook '${hook}' not found${hint}`));
|
|
11002
11427
|
}
|
|
11428
|
+
process.exitCode = 1;
|
|
11003
11429
|
return;
|
|
11004
11430
|
}
|
|
11005
11431
|
const globalInstalled = getRegisteredHooks("global").includes(meta.name);
|
|
@@ -11046,6 +11472,13 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11046
11472
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
11047
11473
|
}
|
|
11048
11474
|
const registered = getRegisteredHooks(scope);
|
|
11475
|
+
let wiringCount = 0;
|
|
11476
|
+
if (settingsExist) {
|
|
11477
|
+
try {
|
|
11478
|
+
const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
11479
|
+
wiringCount = countSettingsWiring(settings);
|
|
11480
|
+
} catch {}
|
|
11481
|
+
}
|
|
11049
11482
|
for (const name of registered) {
|
|
11050
11483
|
const custom = readCustomManifest(name);
|
|
11051
11484
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
@@ -11083,7 +11516,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11083
11516
|
}
|
|
11084
11517
|
}
|
|
11085
11518
|
if (options.json) {
|
|
11086
|
-
console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }));
|
|
11519
|
+
console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, wiring_count: wiringCount, scope }));
|
|
11087
11520
|
return;
|
|
11088
11521
|
}
|
|
11089
11522
|
console.log(chalk2.bold(`
|
|
@@ -11092,6 +11525,9 @@ Hook Health Check (${scope})
|
|
|
11092
11525
|
if (registered.length === 0) {
|
|
11093
11526
|
console.log(chalk2.dim(" No hooks registered."));
|
|
11094
11527
|
console.log(chalk2.dim(" Run: hooks install gitguard"));
|
|
11528
|
+
console.log(chalk2.dim(` (checked 0 registered \`hooks run\` entries of ${wiringCount} settings wiring entries; direct-path wiring outside the registered surface is not covered by this check)`));
|
|
11529
|
+
if (issues.length > 0)
|
|
11530
|
+
process.exitCode = 1;
|
|
11095
11531
|
return;
|
|
11096
11532
|
}
|
|
11097
11533
|
if (healthy.length > 0) {
|
|
@@ -11100,6 +11536,9 @@ Hook Health Check (${scope})
|
|
|
11100
11536
|
console.log(chalk2.green(` ${name}`));
|
|
11101
11537
|
}
|
|
11102
11538
|
}
|
|
11539
|
+
if (registered.length > 0) {
|
|
11540
|
+
console.log(chalk2.dim(` (checked ${registered.length} registered \`hooks run\` entries of ${wiringCount} settings wiring entries; direct-path wiring outside the registered surface is not covered by this check)`));
|
|
11541
|
+
}
|
|
11103
11542
|
if (issues.length > 0) {
|
|
11104
11543
|
console.log();
|
|
11105
11544
|
for (const issue of issues) {
|
|
@@ -11107,9 +11546,11 @@ Hook Health Check (${scope})
|
|
|
11107
11546
|
console.log(` ${icon} ${chalk2.cyan(issue.hook)}: ${issue.issue}`);
|
|
11108
11547
|
}
|
|
11109
11548
|
}
|
|
11110
|
-
if (issues.length === 0) {
|
|
11549
|
+
if (issues.length === 0 && registered.length > 0) {
|
|
11111
11550
|
console.log(chalk2.green(`
|
|
11112
|
-
All
|
|
11551
|
+
All ${healthy.length} registered hook(s) healthy`));
|
|
11552
|
+
} else if (issues.some((i) => i.severity === "error")) {
|
|
11553
|
+
process.exitCode = 1;
|
|
11113
11554
|
}
|
|
11114
11555
|
console.log();
|
|
11115
11556
|
});
|
|
@@ -11142,7 +11583,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
11142
11583
|
const version = arg.slice(at + 1);
|
|
11143
11584
|
if (!HOOK_NAME_RE2.test(name))
|
|
11144
11585
|
return null;
|
|
11145
|
-
if (
|
|
11586
|
+
if (!SEMVER_PATTERN.test(version))
|
|
11146
11587
|
return null;
|
|
11147
11588
|
return { name, version };
|
|
11148
11589
|
}
|
|
@@ -11225,6 +11666,7 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
11225
11666
|
} else {
|
|
11226
11667
|
console.log(chalk2.red(`Hook '${hook}' not found`));
|
|
11227
11668
|
}
|
|
11669
|
+
process.exitCode = 1;
|
|
11228
11670
|
return;
|
|
11229
11671
|
}
|
|
11230
11672
|
const hookPath = getHookPath(hook);
|
|
@@ -11479,18 +11921,19 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
|
|
|
11479
11921
|
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
11480
11922
|
params.push(String(limit));
|
|
11481
11923
|
const rows = db.query(sql).all(...params);
|
|
11924
|
+
const projected = projectLogRows2(rows);
|
|
11482
11925
|
if (options.json) {
|
|
11483
|
-
console.log(JSON.stringify(
|
|
11926
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11484
11927
|
return;
|
|
11485
11928
|
}
|
|
11486
|
-
if (
|
|
11929
|
+
if (projected.length === 0) {
|
|
11487
11930
|
console.log(chalk2.dim("No events found."));
|
|
11488
11931
|
return;
|
|
11489
11932
|
}
|
|
11490
11933
|
console.log(chalk2.bold(`
|
|
11491
|
-
Hook Events (${
|
|
11934
|
+
Hook Events (${projected.length})
|
|
11492
11935
|
`));
|
|
11493
|
-
for (const row of
|
|
11936
|
+
for (const row of projected) {
|
|
11494
11937
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11495
11938
|
const err = row.error ? chalk2.red(` ERR: ${truncateText2(row.error, 60)}`) : "";
|
|
11496
11939
|
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
@@ -11505,18 +11948,19 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
|
|
|
11505
11948
|
const limit = parseInt(options.limit) || 50;
|
|
11506
11949
|
const q = `%${text}%`;
|
|
11507
11950
|
const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
|
|
11951
|
+
const projected = projectLogRows2(rows);
|
|
11508
11952
|
if (options.json) {
|
|
11509
|
-
console.log(JSON.stringify(
|
|
11953
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11510
11954
|
return;
|
|
11511
11955
|
}
|
|
11512
|
-
if (
|
|
11956
|
+
if (projected.length === 0) {
|
|
11513
11957
|
console.log(chalk2.dim(`No events matching "${text}".`));
|
|
11514
11958
|
return;
|
|
11515
11959
|
}
|
|
11516
11960
|
console.log(chalk2.bold(`
|
|
11517
|
-
Search results for "${text}" (${
|
|
11961
|
+
Search results for "${text}" (${projected.length})
|
|
11518
11962
|
`));
|
|
11519
|
-
for (const row of
|
|
11963
|
+
for (const row of projected) {
|
|
11520
11964
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11521
11965
|
const snippet = truncateText2(row.tool_input || row.error || "", 80);
|
|
11522
11966
|
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
|
|
@@ -11529,18 +11973,19 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
|
|
|
11529
11973
|
const db = getDb2();
|
|
11530
11974
|
const limit = parseInt(options.n) || 20;
|
|
11531
11975
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
11976
|
+
const projected = projectLogRows2(rows);
|
|
11532
11977
|
if (options.json) {
|
|
11533
|
-
console.log(JSON.stringify(
|
|
11978
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11534
11979
|
return;
|
|
11535
11980
|
}
|
|
11536
|
-
if (
|
|
11981
|
+
if (projected.length === 0) {
|
|
11537
11982
|
console.log(chalk2.dim("No events yet."));
|
|
11538
11983
|
return;
|
|
11539
11984
|
}
|
|
11540
11985
|
console.log(chalk2.bold(`
|
|
11541
|
-
Last ${
|
|
11986
|
+
Last ${projected.length} events
|
|
11542
11987
|
`));
|
|
11543
|
-
for (const row of
|
|
11988
|
+
for (const row of projected) {
|
|
11544
11989
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11545
11990
|
const err = row.error ? chalk2.red(` \u2717 ${truncateText2(row.error, 60)}`) : "";
|
|
11546
11991
|
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
@@ -11573,18 +12018,19 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
|
|
|
11573
12018
|
}
|
|
11574
12019
|
const since = new Date(Date.now() - parseDuration(options.since)).toISOString();
|
|
11575
12020
|
const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(since, limit);
|
|
12021
|
+
const projected = projectLogRows2(rows);
|
|
11576
12022
|
if (options.json) {
|
|
11577
|
-
console.log(JSON.stringify(
|
|
12023
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11578
12024
|
return;
|
|
11579
12025
|
}
|
|
11580
|
-
if (
|
|
12026
|
+
if (projected.length === 0) {
|
|
11581
12027
|
console.log(chalk2.dim(`No errors in the last ${options.since}.`));
|
|
11582
12028
|
return;
|
|
11583
12029
|
}
|
|
11584
12030
|
console.log(chalk2.bold(`
|
|
11585
|
-
Errors (last ${options.since}, ${
|
|
12031
|
+
Errors (last ${options.since}, ${projected.length} found)
|
|
11586
12032
|
`));
|
|
11587
|
-
for (const row of
|
|
12033
|
+
for (const row of projected) {
|
|
11588
12034
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11589
12035
|
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(truncateText2(row.error, 100))}`);
|
|
11590
12036
|
}
|
|
@@ -11706,18 +12152,17 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
|
|
|
11706
12152
|
console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
|
|
11707
12153
|
console.log(chalk2.dim(` sha256 ${result.actual}`));
|
|
11708
12154
|
});
|
|
11709
|
-
program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").
|
|
12155
|
+
program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock). The publish API key resolves from HASNA_HOOKS_API_KEY / HOOKS_API_KEY only \u2014 there is deliberately no --api-key flag (P1-8).").action(async (options) => {
|
|
11710
12156
|
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
11711
12157
|
startServeServer2({
|
|
11712
12158
|
port: options.port ? parseInt(options.port, 10) : undefined,
|
|
11713
|
-
host: options.host
|
|
11714
|
-
apiKey: options.apiKey
|
|
12159
|
+
host: options.host
|
|
11715
12160
|
});
|
|
11716
12161
|
});
|
|
11717
12162
|
program2.command("sync").option("--dry-run", "Print the plan without changing anything", false).option("-j, --json", "Output as JSON", false).description("Sync local hooks with the remote registry (or bundled registry when no API URL is configured)").action(async (options) => {
|
|
11718
12163
|
const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
11719
12164
|
try {
|
|
11720
|
-
const plan = options.dryRun ? await
|
|
12165
|
+
const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
|
|
11721
12166
|
const diff = plan.diff;
|
|
11722
12167
|
if (options.json) {
|
|
11723
12168
|
console.log(JSON.stringify({ dry_run: plan.dryRun, api_url: plan.apiUrl ?? null, diff }));
|
|
@@ -11813,7 +12258,7 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
11813
12258
|
await startStdioServer2();
|
|
11814
12259
|
} else if (options.sse) {
|
|
11815
12260
|
const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
11816
|
-
await startSSEServer2(options.port ? parseInt(options.port) : 39427);
|
|
12261
|
+
await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
|
|
11817
12262
|
} else {
|
|
11818
12263
|
const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
11819
12264
|
const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|