@hasna/hooks 0.6.5 → 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 +589 -168
- 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 +412 -115
- 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,7 +6059,7 @@ 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(() => {
|
|
@@ -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;
|
|
@@ -5934,6 +6130,7 @@ function killGroup(proc, options) {
|
|
|
5934
6130
|
}
|
|
5935
6131
|
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
5936
6132
|
var init_run = __esm(() => {
|
|
6133
|
+
init_hook_env();
|
|
5937
6134
|
HookTimeoutError = class HookTimeoutError extends Error {
|
|
5938
6135
|
timeoutMs;
|
|
5939
6136
|
constructor(timeoutMs) {
|
|
@@ -6028,19 +6225,20 @@ async function installCustomSource(source) {
|
|
|
6028
6225
|
return copyManifestDir(source, source, "local");
|
|
6029
6226
|
}
|
|
6030
6227
|
async function installManifestUrl(url) {
|
|
6031
|
-
const res = await fetch(url, { redirect: "
|
|
6228
|
+
const res = await fetch(url, { redirect: "error" });
|
|
6032
6229
|
if (!res.ok) {
|
|
6033
6230
|
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
6034
6231
|
}
|
|
6035
6232
|
const manifest = parseManifest(await res.text());
|
|
6036
6233
|
let script;
|
|
6037
|
-
|
|
6038
|
-
`)
|
|
6234
|
+
const inline = manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
6235
|
+
`);
|
|
6236
|
+
if (inline) {
|
|
6039
6237
|
script = { path: "script.ts", content: manifest.script };
|
|
6040
6238
|
} else {
|
|
6041
6239
|
const base = url.substring(0, url.lastIndexOf("/"));
|
|
6042
6240
|
const scriptUrl = `${base}/${manifest.script}`;
|
|
6043
|
-
const scriptRes = await fetch(scriptUrl, { redirect: "
|
|
6241
|
+
const scriptRes = await fetch(scriptUrl, { redirect: "error" });
|
|
6044
6242
|
if (!scriptRes.ok) {
|
|
6045
6243
|
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
6046
6244
|
}
|
|
@@ -6064,8 +6262,10 @@ var init_custom_install = __esm(() => {
|
|
|
6064
6262
|
var exports_sync = {};
|
|
6065
6263
|
__export(exports_sync, {
|
|
6066
6264
|
syncHooks: () => syncHooks,
|
|
6265
|
+
stageSyncArtifacts: () => stageSyncArtifacts,
|
|
6067
6266
|
planSync: () => planSync,
|
|
6068
|
-
fetchPinnedHook: () => fetchPinnedHook
|
|
6267
|
+
fetchPinnedHook: () => fetchPinnedHook,
|
|
6268
|
+
commitSyncArtifacts: () => commitSyncArtifacts
|
|
6069
6269
|
});
|
|
6070
6270
|
import { createHash as createHash2 } from "crypto";
|
|
6071
6271
|
import { readFileSync as readFileSync8 } from "fs";
|
|
@@ -6089,7 +6289,7 @@ function collectBundledCatalog() {
|
|
|
6089
6289
|
}
|
|
6090
6290
|
return out;
|
|
6091
6291
|
}
|
|
6092
|
-
async function fetchJson(base, path) {
|
|
6292
|
+
async function fetchJson(base, path, opts) {
|
|
6093
6293
|
const headers = { accept: "application/json" };
|
|
6094
6294
|
const apiKey = resolveApiKey();
|
|
6095
6295
|
if (apiKey)
|
|
@@ -6105,7 +6305,11 @@ async function fetchJson(base, path) {
|
|
|
6105
6305
|
if (!res.ok) {
|
|
6106
6306
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6107
6307
|
}
|
|
6108
|
-
|
|
6308
|
+
const body = await res.json();
|
|
6309
|
+
if (opts?.withHeader) {
|
|
6310
|
+
body[opts.withHeader] = res.headers.get("x-hook-sha256");
|
|
6311
|
+
}
|
|
6312
|
+
return body;
|
|
6109
6313
|
}
|
|
6110
6314
|
async function fetchRemoteState(apiUrl) {
|
|
6111
6315
|
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
@@ -6118,6 +6322,16 @@ async function fetchRemoteState(apiUrl) {
|
|
|
6118
6322
|
}
|
|
6119
6323
|
return { catalog: catalog.hooks, remoteLock };
|
|
6120
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
|
+
}
|
|
6121
6335
|
function computeDiff(catalog, remoteLock) {
|
|
6122
6336
|
const local = readLock();
|
|
6123
6337
|
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
@@ -6125,30 +6339,30 @@ function computeDiff(catalog, remoteLock) {
|
|
|
6125
6339
|
if (!entry.name || !entry.version || !entry.sha256)
|
|
6126
6340
|
continue;
|
|
6127
6341
|
const pin = local.hooks[entry.name];
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
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);
|
|
6138
6352
|
} else {
|
|
6139
|
-
diff.
|
|
6353
|
+
diff[classifyRemotePin(pin, entry.version, entry.sha256)].push(entry.name);
|
|
6140
6354
|
}
|
|
6141
6355
|
}
|
|
6142
6356
|
return diff;
|
|
6143
6357
|
}
|
|
6144
|
-
async function planSync() {
|
|
6358
|
+
async function planSync(options = {}) {
|
|
6145
6359
|
const apiUrl = resolveApiUrl();
|
|
6146
6360
|
if (apiUrl) {
|
|
6147
6361
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6148
|
-
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
6362
|
+
return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
6149
6363
|
}
|
|
6150
6364
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6151
|
-
return { apiUrl: null, dryRun: false, diff };
|
|
6365
|
+
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
6152
6366
|
}
|
|
6153
6367
|
async function syncHooks(options = {}) {
|
|
6154
6368
|
const apiUrl = resolveApiUrl();
|
|
@@ -6156,10 +6370,10 @@ async function syncHooks(options = {}) {
|
|
|
6156
6370
|
if (options.dryRun)
|
|
6157
6371
|
return { ...plan, dryRun: true };
|
|
6158
6372
|
if (!apiUrl) {
|
|
6159
|
-
const
|
|
6373
|
+
const db = getDb();
|
|
6160
6374
|
for (const entry of collectBundledCatalog()) {
|
|
6161
6375
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6162
|
-
upsertHookRecord(
|
|
6376
|
+
upsertHookRecord(db, {
|
|
6163
6377
|
name: entry.name,
|
|
6164
6378
|
version: entry.version,
|
|
6165
6379
|
sha256: entry.sha256,
|
|
@@ -6172,13 +6386,17 @@ async function syncHooks(options = {}) {
|
|
|
6172
6386
|
}
|
|
6173
6387
|
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6174
6388
|
const diff = computeDiff(catalog, remoteLock);
|
|
6175
|
-
const
|
|
6176
|
-
|
|
6177
|
-
|
|
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) {
|
|
6178
6396
|
const entry = remoteLock.hooks[name];
|
|
6179
6397
|
if (!entry)
|
|
6180
6398
|
continue;
|
|
6181
|
-
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)}`);
|
|
6182
6400
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6183
6401
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6184
6402
|
}
|
|
@@ -6186,34 +6404,64 @@ async function syncHooks(options = {}) {
|
|
|
6186
6404
|
if (actualSha !== entry.sha256) {
|
|
6187
6405
|
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6188
6406
|
}
|
|
6189
|
-
artifacts.set(name, artifact);
|
|
6190
|
-
}
|
|
6191
|
-
const db = getDb();
|
|
6192
|
-
for (const name of toFetch) {
|
|
6193
|
-
const artifact = artifacts.get(name);
|
|
6194
|
-
const entry = remoteLock.hooks[name];
|
|
6195
|
-
if (!artifact || !entry)
|
|
6196
|
-
continue;
|
|
6197
6407
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6198
|
-
|
|
6199
|
-
`
|
|
6200
|
-
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6201
|
-
const scriptPath = resolveScriptPath(name);
|
|
6202
|
-
const actual = await sha256File(scriptPath);
|
|
6203
|
-
if (actual !== entry.sha256) {
|
|
6204
|
-
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}')`);
|
|
6205
6410
|
}
|
|
6206
|
-
|
|
6207
|
-
|
|
6411
|
+
const scriptRel = scriptRelFor(manifest);
|
|
6412
|
+
staged.push({
|
|
6208
6413
|
name,
|
|
6209
6414
|
version: entry.version,
|
|
6210
6415
|
sha256: entry.sha256,
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6416
|
+
source: entry.source ?? "remote",
|
|
6417
|
+
manifest,
|
|
6418
|
+
scriptContent: artifact.script,
|
|
6419
|
+
scriptRel
|
|
6214
6420
|
});
|
|
6215
6421
|
}
|
|
6216
|
-
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
|
+
}
|
|
6217
6465
|
}
|
|
6218
6466
|
async function fetchPinnedHook(name, version, apiUrl) {
|
|
6219
6467
|
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
@@ -6221,10 +6469,11 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6221
6469
|
if (!entry) {
|
|
6222
6470
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
6223
6471
|
}
|
|
6224
|
-
|
|
6225
|
-
|
|
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(", ")})`);
|
|
6226
6475
|
}
|
|
6227
|
-
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" });
|
|
6228
6477
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6229
6478
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
6230
6479
|
}
|
|
@@ -6236,24 +6485,31 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6236
6485
|
throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
|
|
6237
6486
|
}
|
|
6238
6487
|
const actualSha = sha256OfText(artifact.script);
|
|
6239
|
-
|
|
6240
|
-
|
|
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}`);
|
|
6241
6498
|
}
|
|
6242
6499
|
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6243
|
-
const scriptRel =
|
|
6244
|
-
`) ? "script.ts" : artifact.manifest.script;
|
|
6500
|
+
const scriptRel = scriptRelFor(manifest);
|
|
6245
6501
|
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6246
6502
|
const scriptPath = resolveScriptPath(name);
|
|
6247
6503
|
const actual = await sha256File(scriptPath);
|
|
6248
|
-
if (actual !==
|
|
6249
|
-
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}`);
|
|
6250
6506
|
}
|
|
6251
6507
|
const db = getDb();
|
|
6252
|
-
setPinnedHook(name, { version, sha256:
|
|
6508
|
+
setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
|
|
6253
6509
|
upsertHookRecord(db, {
|
|
6254
6510
|
name,
|
|
6255
6511
|
version,
|
|
6256
|
-
sha256:
|
|
6512
|
+
sha256: expectedSha,
|
|
6257
6513
|
source_type: entry.source ?? "remote",
|
|
6258
6514
|
source_ref: apiUrl,
|
|
6259
6515
|
last_verified_at: new Date().toISOString()
|
|
@@ -6261,7 +6517,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
6261
6517
|
return {
|
|
6262
6518
|
name,
|
|
6263
6519
|
version,
|
|
6264
|
-
sha256:
|
|
6520
|
+
sha256: expectedSha,
|
|
6265
6521
|
source: entry.source ?? "remote",
|
|
6266
6522
|
source_ref: apiUrl,
|
|
6267
6523
|
artifact,
|
|
@@ -6286,7 +6542,7 @@ var init_pg_migrations = __esm(() => {
|
|
|
6286
6542
|
timestamp TEXT NOT NULL,
|
|
6287
6543
|
session_id TEXT NOT NULL,
|
|
6288
6544
|
hook_name TEXT NOT NULL,
|
|
6289
|
-
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')),
|
|
6290
6546
|
tool_name TEXT,
|
|
6291
6547
|
tool_input TEXT,
|
|
6292
6548
|
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
@@ -6319,7 +6575,10 @@ var init_pg_migrations = __esm(() => {
|
|
|
6319
6575
|
)`,
|
|
6320
6576
|
`ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
|
|
6321
6577
|
`ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
|
|
6322
|
-
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'))`
|
|
6323
6582
|
];
|
|
6324
6583
|
});
|
|
6325
6584
|
|
|
@@ -6334,7 +6593,34 @@ function normalizeParams(params) {
|
|
|
6334
6593
|
return flat.map((value) => value === undefined ? null : value);
|
|
6335
6594
|
}
|
|
6336
6595
|
function sslConfigFor(connectionString) {
|
|
6337
|
-
|
|
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;
|
|
6338
6624
|
}
|
|
6339
6625
|
|
|
6340
6626
|
class PgAdapterAsync {
|
|
@@ -6496,9 +6782,15 @@ async function pushTable(db, remote, table) {
|
|
|
6496
6782
|
result.rowsRead = rows.length;
|
|
6497
6783
|
if (rows.length === 0)
|
|
6498
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;
|
|
6499
6791
|
const remoteColumns = await getRemoteColumns(remote, table);
|
|
6500
|
-
const columns = filterRemoteColumns(remoteColumns, Object.keys(
|
|
6501
|
-
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);
|
|
6502
6794
|
} catch (error) {
|
|
6503
6795
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6504
6796
|
}
|
|
@@ -6513,8 +6805,14 @@ async function pullTable(remote, db, table) {
|
|
|
6513
6805
|
result.rowsRead = rows.length;
|
|
6514
6806
|
if (rows.length === 0)
|
|
6515
6807
|
return result;
|
|
6516
|
-
const
|
|
6517
|
-
|
|
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);
|
|
6518
6816
|
} catch (error) {
|
|
6519
6817
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6520
6818
|
}
|
|
@@ -6637,6 +6935,7 @@ var init_storage_sync = __esm(() => {
|
|
|
6637
6935
|
init_db();
|
|
6638
6936
|
init_pg_migrations();
|
|
6639
6937
|
init_remote_storage();
|
|
6938
|
+
init_redact();
|
|
6640
6939
|
STORAGE_TABLES = [
|
|
6641
6940
|
"hook_events",
|
|
6642
6941
|
"schema_migrations",
|
|
@@ -6735,24 +7034,29 @@ async function buildCatalog() {
|
|
|
6735
7034
|
const scriptPath = resolveHook(meta.name)?.scriptPath;
|
|
6736
7035
|
if (!scriptPath)
|
|
6737
7036
|
continue;
|
|
7037
|
+
const sha = await sha256File(scriptPath);
|
|
6738
7038
|
byName.set(meta.name, {
|
|
6739
7039
|
name: meta.name,
|
|
6740
7040
|
version: meta.version,
|
|
6741
|
-
sha256:
|
|
7041
|
+
sha256: sha,
|
|
6742
7042
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
6743
7043
|
description: meta.description,
|
|
6744
|
-
source: "bundled"
|
|
7044
|
+
source: "bundled",
|
|
7045
|
+
versions: [meta.version]
|
|
6745
7046
|
});
|
|
6746
7047
|
}
|
|
6747
7048
|
for (const custom of listCustomHooks()) {
|
|
6748
7049
|
const name = shortManifestName(custom.manifest.name);
|
|
7050
|
+
const sha = await sha256File(custom.scriptPath);
|
|
7051
|
+
const existing = byName.get(name);
|
|
6749
7052
|
byName.set(name, {
|
|
6750
7053
|
name,
|
|
6751
7054
|
version: custom.manifest.version,
|
|
6752
|
-
sha256:
|
|
7055
|
+
sha256: sha,
|
|
6753
7056
|
events: custom.manifest.events,
|
|
6754
7057
|
description: custom.manifest.description ?? "Custom hook",
|
|
6755
|
-
source:
|
|
7058
|
+
source: existing ? "custom-overrides-bundled" : "custom",
|
|
7059
|
+
versions: existing ? [...new Set([...existing.versions, custom.manifest.version])] : [custom.manifest.version]
|
|
6756
7060
|
});
|
|
6757
7061
|
}
|
|
6758
7062
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -6770,6 +7074,7 @@ async function artifactFor(name, version) {
|
|
|
6770
7074
|
description: resolved.description,
|
|
6771
7075
|
events: resolved.events,
|
|
6772
7076
|
script: custom ? custom.manifest.script : `src/hook.ts`,
|
|
7077
|
+
script_kind: custom?.manifest.script_kind,
|
|
6773
7078
|
args: custom?.manifest.args,
|
|
6774
7079
|
timeout_ms: custom?.manifest.timeout_ms
|
|
6775
7080
|
};
|
|
@@ -6799,11 +7104,32 @@ function handleServeRequest(req, apiKey) {
|
|
|
6799
7104
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
6800
7105
|
}
|
|
6801
7106
|
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
6802
|
-
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
|
+
});
|
|
6803
7118
|
}
|
|
6804
|
-
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(
|
|
7119
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
|
|
6805
7120
|
if (artifactMatch && req.method === "GET") {
|
|
6806
|
-
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
|
+
}
|
|
6807
7133
|
return artifactFor(name, version).then(async (artifact) => {
|
|
6808
7134
|
if (!artifact)
|
|
6809
7135
|
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
@@ -6834,7 +7160,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
6834
7160
|
function startServeServer(options) {
|
|
6835
7161
|
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
6836
7162
|
const host = options.host ?? SERVE_HOST;
|
|
6837
|
-
const apiKey = resolveApiKey(
|
|
7163
|
+
const apiKey = resolveApiKey();
|
|
6838
7164
|
const server = Bun.serve({
|
|
6839
7165
|
hostname: host,
|
|
6840
7166
|
port,
|
|
@@ -6852,6 +7178,7 @@ var init_serve = __esm(() => {
|
|
|
6852
7178
|
init_store();
|
|
6853
7179
|
init_resolve();
|
|
6854
7180
|
init_config();
|
|
7181
|
+
init_semver();
|
|
6855
7182
|
if (false) {}
|
|
6856
7183
|
});
|
|
6857
7184
|
|
|
@@ -6941,7 +7268,9 @@ var exports_server = {};
|
|
|
6941
7268
|
__export(exports_server, {
|
|
6942
7269
|
startStdioServer: () => startStdioServer,
|
|
6943
7270
|
startSSEServer: () => startSSEServer,
|
|
7271
|
+
sseAuthToken: () => sseAuthToken,
|
|
6944
7272
|
createHooksServer: () => createHooksServer,
|
|
7273
|
+
MCP_SSE_HOST: () => MCP_SSE_HOST,
|
|
6945
7274
|
MCP_PORT: () => MCP_PORT
|
|
6946
7275
|
});
|
|
6947
7276
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -6969,7 +7298,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
|
6969
7298
|
const started = Date.now();
|
|
6970
7299
|
let ran;
|
|
6971
7300
|
try {
|
|
6972
|
-
ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin,
|
|
7301
|
+
ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, timeout: timeoutMs });
|
|
6973
7302
|
} catch (err) {
|
|
6974
7303
|
try {
|
|
6975
7304
|
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
@@ -7075,6 +7404,9 @@ function compactEvent(row) {
|
|
|
7075
7404
|
tool_input_preview: row.tool_input ? truncateText(row.tool_input, 180) : undefined
|
|
7076
7405
|
};
|
|
7077
7406
|
}
|
|
7407
|
+
function projectLogRows(rows) {
|
|
7408
|
+
return rows.map((row) => projectEventRowForRead({ ...row }));
|
|
7409
|
+
}
|
|
7078
7410
|
function createHooksServer() {
|
|
7079
7411
|
const server = new McpServer({
|
|
7080
7412
|
name: "@hasna/hooks",
|
|
@@ -7456,7 +7788,13 @@ function createHooksServer() {
|
|
|
7456
7788
|
} catch (err) {
|
|
7457
7789
|
const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7458
7790
|
if (err instanceof HookTimeoutError2) {
|
|
7459
|
-
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
|
+
};
|
|
7460
7798
|
}
|
|
7461
7799
|
return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
|
|
7462
7800
|
}
|
|
@@ -7482,15 +7820,25 @@ function createHooksServer() {
|
|
|
7482
7820
|
hooks: z2.array(z2.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
|
|
7483
7821
|
scope: z2.enum(["global", "project"]).default("global").describe("Install scope")
|
|
7484
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
|
+
}
|
|
7485
7832
|
const profile = createProfile({ agent_type, name });
|
|
7833
|
+
const target = agent_type === "gemini" ? "gemini" : "claude";
|
|
7486
7834
|
const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
|
|
7487
|
-
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 }));
|
|
7488
7836
|
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
7489
7837
|
const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
|
|
7490
7838
|
return {
|
|
7491
7839
|
content: [{
|
|
7492
7840
|
type: "text",
|
|
7493
|
-
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}` })
|
|
7494
7842
|
}]
|
|
7495
7843
|
};
|
|
7496
7844
|
});
|
|
@@ -7642,12 +7990,13 @@ function createHooksServer() {
|
|
|
7642
7990
|
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
7643
7991
|
params.push(maxRows);
|
|
7644
7992
|
const rows = db.query(sql).all(...params);
|
|
7993
|
+
const projected = projectLogRows(rows);
|
|
7645
7994
|
return {
|
|
7646
7995
|
content: [{
|
|
7647
7996
|
type: "text",
|
|
7648
7997
|
text: JSON.stringify({
|
|
7649
|
-
events: compact ?
|
|
7650
|
-
count:
|
|
7998
|
+
events: compact ? projected.map(compactEvent) : projected,
|
|
7999
|
+
count: projected.length,
|
|
7651
8000
|
compact,
|
|
7652
8001
|
hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
|
|
7653
8002
|
})
|
|
@@ -7662,12 +8011,13 @@ function createHooksServer() {
|
|
|
7662
8011
|
const db = getDb2();
|
|
7663
8012
|
const maxRows = boundedLimit(n, 20, compact ? 100 : 500);
|
|
7664
8013
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(maxRows);
|
|
8014
|
+
const projected = projectLogRows(rows);
|
|
7665
8015
|
return {
|
|
7666
8016
|
content: [{
|
|
7667
8017
|
type: "text",
|
|
7668
8018
|
text: JSON.stringify({
|
|
7669
|
-
events: compact ?
|
|
7670
|
-
count:
|
|
8019
|
+
events: compact ? projected.map(compactEvent) : projected,
|
|
8020
|
+
count: projected.length,
|
|
7671
8021
|
compact,
|
|
7672
8022
|
hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
|
|
7673
8023
|
})
|
|
@@ -7692,12 +8042,13 @@ function createHooksServer() {
|
|
|
7692
8042
|
}
|
|
7693
8043
|
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
7694
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);
|
|
7695
8046
|
return {
|
|
7696
8047
|
content: [{
|
|
7697
8048
|
type: "text",
|
|
7698
8049
|
text: JSON.stringify({
|
|
7699
|
-
events: compact ?
|
|
7700
|
-
count:
|
|
8050
|
+
events: compact ? projected.map(compactEvent) : projected,
|
|
8051
|
+
count: projected.length,
|
|
7701
8052
|
compact,
|
|
7702
8053
|
hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
|
|
7703
8054
|
})
|
|
@@ -7825,11 +8176,48 @@ function createHooksServer() {
|
|
|
7825
8176
|
});
|
|
7826
8177
|
return server;
|
|
7827
8178
|
}
|
|
7828
|
-
|
|
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
|
+
}
|
|
7829
8200
|
const server = createHooksServer();
|
|
7830
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
|
+
}
|
|
7831
8212
|
const httpServer = createServer(async (req, res) => {
|
|
7832
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
|
+
}
|
|
7833
8221
|
if (url.pathname === "/sse") {
|
|
7834
8222
|
const transport = new SSEServerTransport("/messages", res);
|
|
7835
8223
|
transports.set(transport.sessionId, transport);
|
|
@@ -7852,9 +8240,9 @@ async function startSSEServer(port = MCP_PORT) {
|
|
|
7852
8240
|
res.end(JSON.stringify({ name: "@hasna/hooks", version: pkg.version, transport: "sse", port }));
|
|
7853
8241
|
}
|
|
7854
8242
|
});
|
|
7855
|
-
httpServer.listen(port, () => {
|
|
7856
|
-
console.error(`@hasna/hooks MCP server running on http
|
|
7857
|
-
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`);
|
|
7858
8246
|
});
|
|
7859
8247
|
}
|
|
7860
8248
|
async function startStdioServer() {
|
|
@@ -7868,7 +8256,7 @@ async function startStdioServer() {
|
|
|
7868
8256
|
process.exit(1);
|
|
7869
8257
|
}
|
|
7870
8258
|
}
|
|
7871
|
-
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;
|
|
7872
8260
|
var init_server = __esm(() => {
|
|
7873
8261
|
init_registry();
|
|
7874
8262
|
init_installer();
|
|
@@ -7876,6 +8264,7 @@ var init_server = __esm(() => {
|
|
|
7876
8264
|
init_manifest();
|
|
7877
8265
|
init_resolve();
|
|
7878
8266
|
init_store();
|
|
8267
|
+
init_redact();
|
|
7879
8268
|
init_storage();
|
|
7880
8269
|
__dirname3 = dirname5(fileURLToPath3(import.meta.url));
|
|
7881
8270
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
@@ -7889,6 +8278,7 @@ var init_server = __esm(() => {
|
|
|
7889
8278
|
}
|
|
7890
8279
|
} catch {}
|
|
7891
8280
|
_hooksAgents = new Map;
|
|
8281
|
+
MCP_SSE_AUTH_ENV = ["HASNA_HOOKS_MCP_TOKEN", "HOOKS_MCP_TOKEN"];
|
|
7892
8282
|
});
|
|
7893
8283
|
|
|
7894
8284
|
// src/mcp/http.ts
|
|
@@ -7950,7 +8340,7 @@ function startMcpHttpServer(options) {
|
|
|
7950
8340
|
var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
|
|
7951
8341
|
var init_http = () => {};
|
|
7952
8342
|
|
|
7953
|
-
//
|
|
8343
|
+
// node_modules/@hasna/events/dist/commander.js
|
|
7954
8344
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
7955
8345
|
import { Buffer as Buffer2 } from "buffer";
|
|
7956
8346
|
import { existsSync } from "fs";
|
|
@@ -9135,7 +9525,7 @@ function replaySummary(events, deliveries, nextCursor) {
|
|
|
9135
9525
|
// src/cli/index.tsx
|
|
9136
9526
|
import { render } from "ink";
|
|
9137
9527
|
|
|
9138
|
-
//
|
|
9528
|
+
// node_modules/commander/esm.mjs
|
|
9139
9529
|
var import__ = __toESM(require_commander(), 1);
|
|
9140
9530
|
var {
|
|
9141
9531
|
program,
|
|
@@ -9161,11 +9551,11 @@ import { fileURLToPath as fileURLToPath4 } from "url";
|
|
|
9161
9551
|
import { useState as useState7 } from "react";
|
|
9162
9552
|
import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
|
|
9163
9553
|
|
|
9164
|
-
//
|
|
9554
|
+
// node_modules/ink-select-input/build/Indicator.js
|
|
9165
9555
|
import React from "react";
|
|
9166
9556
|
import { Box, Text } from "ink";
|
|
9167
9557
|
|
|
9168
|
-
//
|
|
9558
|
+
// node_modules/is-unicode-supported/index.js
|
|
9169
9559
|
import process2 from "process";
|
|
9170
9560
|
function isUnicodeSupported() {
|
|
9171
9561
|
const { env } = process2;
|
|
@@ -9176,7 +9566,7 @@ function isUnicodeSupported() {
|
|
|
9176
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";
|
|
9177
9567
|
}
|
|
9178
9568
|
|
|
9179
|
-
//
|
|
9569
|
+
// node_modules/figures/index.js
|
|
9180
9570
|
var common = {
|
|
9181
9571
|
circleQuestionMark: "(?)",
|
|
9182
9572
|
questionMarkPrefix: "(?)",
|
|
@@ -9452,23 +9842,23 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
|
|
|
9452
9842
|
var figures_default = figures;
|
|
9453
9843
|
var replacements = Object.entries(specialMainSymbols);
|
|
9454
9844
|
|
|
9455
|
-
//
|
|
9845
|
+
// node_modules/ink-select-input/build/Indicator.js
|
|
9456
9846
|
function Indicator({ isSelected = false }) {
|
|
9457
9847
|
return React.createElement(Box, { marginRight: 1 }, isSelected ? React.createElement(Text, { color: "blue" }, figures_default.pointer) : React.createElement(Text, null, " "));
|
|
9458
9848
|
}
|
|
9459
9849
|
var Indicator_default = Indicator;
|
|
9460
|
-
//
|
|
9850
|
+
// node_modules/ink-select-input/build/Item.js
|
|
9461
9851
|
import * as React2 from "react";
|
|
9462
9852
|
import { Text as Text2 } from "ink";
|
|
9463
9853
|
function Item({ isSelected = false, label }) {
|
|
9464
9854
|
return React2.createElement(Text2, { color: isSelected ? "blue" : undefined }, label);
|
|
9465
9855
|
}
|
|
9466
9856
|
var Item_default = Item;
|
|
9467
|
-
//
|
|
9857
|
+
// node_modules/ink-select-input/build/SelectInput.js
|
|
9468
9858
|
import { isDeepStrictEqual } from "util";
|
|
9469
9859
|
import React3, { useState, useEffect, useRef, useCallback } from "react";
|
|
9470
9860
|
|
|
9471
|
-
//
|
|
9861
|
+
// node_modules/to-rotated/index.js
|
|
9472
9862
|
function toRotated(array, steps) {
|
|
9473
9863
|
if (!Array.isArray(array)) {
|
|
9474
9864
|
throw new TypeError(`Expected an array, got \`${typeof array}\`.`);
|
|
@@ -9490,7 +9880,7 @@ function toRotated(array, steps) {
|
|
|
9490
9880
|
];
|
|
9491
9881
|
}
|
|
9492
9882
|
|
|
9493
|
-
//
|
|
9883
|
+
// node_modules/ink-select-input/build/SelectInput.js
|
|
9494
9884
|
import { Box as Box2, useInput } from "ink";
|
|
9495
9885
|
function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent = Indicator_default, itemComponent = Item_default, limit: customLimit, onSelect, onHighlight }) {
|
|
9496
9886
|
const hasLimit = typeof customLimit === "number" && items.length > customLimit;
|
|
@@ -9786,7 +10176,7 @@ function HookSelect({
|
|
|
9786
10176
|
import { useState as useState4, useEffect as useEffect3 } from "react";
|
|
9787
10177
|
import { Box as Box6, Text as Text7, useInput as useInput4 } from "ink";
|
|
9788
10178
|
|
|
9789
|
-
//
|
|
10179
|
+
// node_modules/ink-text-input/build/index.js
|
|
9790
10180
|
import React5, { useState as useState3, useEffect as useEffect2 } from "react";
|
|
9791
10181
|
import { Text as Text6, useInput as useInput3 } from "ink";
|
|
9792
10182
|
import chalk from "chalk";
|
|
@@ -9964,7 +10354,7 @@ function SearchView({
|
|
|
9964
10354
|
import { useState as useState6, useEffect as useEffect5 } from "react";
|
|
9965
10355
|
import { Box as Box7, Text as Text9 } from "ink";
|
|
9966
10356
|
|
|
9967
|
-
//
|
|
10357
|
+
// node_modules/ink-spinner/build/index.js
|
|
9968
10358
|
var import_cli_spinners = __toESM(require_cli_spinners(), 1);
|
|
9969
10359
|
import React7, { useState as useState5, useEffect as useEffect4 } from "react";
|
|
9970
10360
|
import { Text as Text8 } from "ink";
|
|
@@ -10329,10 +10719,12 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
10329
10719
|
// src/cli/index.tsx
|
|
10330
10720
|
init_registry();
|
|
10331
10721
|
init_installer();
|
|
10722
|
+
init_redact();
|
|
10332
10723
|
init_profiles();
|
|
10333
10724
|
init_manifest();
|
|
10334
10725
|
init_resolve();
|
|
10335
10726
|
init_store();
|
|
10727
|
+
init_semver();
|
|
10336
10728
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
10337
10729
|
var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
|
|
10338
10730
|
var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
|
|
@@ -10364,6 +10756,9 @@ function truncateText2(value, max = 96) {
|
|
|
10364
10756
|
return text;
|
|
10365
10757
|
return `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
10366
10758
|
}
|
|
10759
|
+
function projectLogRows2(rows) {
|
|
10760
|
+
return rows.map((row) => projectEventRowForRead({ ...row }));
|
|
10761
|
+
}
|
|
10367
10762
|
function readToken(tokenFile) {
|
|
10368
10763
|
if (tokenFile) {
|
|
10369
10764
|
try {
|
|
@@ -10625,6 +11020,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
10625
11020
|
console.log(chalk2.red(`Unknown category: ${options.category}`));
|
|
10626
11021
|
console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
|
|
10627
11022
|
}
|
|
11023
|
+
process.exitCode = 1;
|
|
10628
11024
|
return;
|
|
10629
11025
|
}
|
|
10630
11026
|
toInstall = getHooksByCategory(category).map((h) => h.name);
|
|
@@ -10657,7 +11053,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
10657
11053
|
const version = arg.slice(at + 1);
|
|
10658
11054
|
if (!HOOK_NAME_RE2.test(name))
|
|
10659
11055
|
return null;
|
|
10660
|
-
if (
|
|
11056
|
+
if (!SEMVER_PATTERN.test(version))
|
|
10661
11057
|
return null;
|
|
10662
11058
|
return { name, version };
|
|
10663
11059
|
}
|
|
@@ -10791,7 +11187,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10791
11187
|
target,
|
|
10792
11188
|
applied: results.some((r) => r.applied)
|
|
10793
11189
|
}));
|
|
10794
|
-
if (results.
|
|
11190
|
+
if (results.some((r) => !r.success)) {
|
|
10795
11191
|
process.exitCode = 1;
|
|
10796
11192
|
}
|
|
10797
11193
|
return;
|
|
@@ -10829,6 +11225,11 @@ Installing hooks (${scope}, ${target})...
|
|
|
10829
11225
|
process.exitCode = 1;
|
|
10830
11226
|
return;
|
|
10831
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
|
+
}
|
|
10832
11233
|
console.log(chalk2.dim(`
|
|
10833
11234
|
Registered in ${settingsFile}`));
|
|
10834
11235
|
});
|
|
@@ -11024,6 +11425,7 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
|
|
|
11024
11425
|
} else {
|
|
11025
11426
|
console.log(chalk2.red(`Hook '${hook}' not found${hint}`));
|
|
11026
11427
|
}
|
|
11428
|
+
process.exitCode = 1;
|
|
11027
11429
|
return;
|
|
11028
11430
|
}
|
|
11029
11431
|
const globalInstalled = getRegisteredHooks("global").includes(meta.name);
|
|
@@ -11070,6 +11472,13 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11070
11472
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
11071
11473
|
}
|
|
11072
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
|
+
}
|
|
11073
11482
|
for (const name of registered) {
|
|
11074
11483
|
const custom = readCustomManifest(name);
|
|
11075
11484
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
@@ -11107,7 +11516,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11107
11516
|
}
|
|
11108
11517
|
}
|
|
11109
11518
|
if (options.json) {
|
|
11110
|
-
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 }));
|
|
11111
11520
|
return;
|
|
11112
11521
|
}
|
|
11113
11522
|
console.log(chalk2.bold(`
|
|
@@ -11116,6 +11525,9 @@ Hook Health Check (${scope})
|
|
|
11116
11525
|
if (registered.length === 0) {
|
|
11117
11526
|
console.log(chalk2.dim(" No hooks registered."));
|
|
11118
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;
|
|
11119
11531
|
return;
|
|
11120
11532
|
}
|
|
11121
11533
|
if (healthy.length > 0) {
|
|
@@ -11124,6 +11536,9 @@ Hook Health Check (${scope})
|
|
|
11124
11536
|
console.log(chalk2.green(` ${name}`));
|
|
11125
11537
|
}
|
|
11126
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
|
+
}
|
|
11127
11542
|
if (issues.length > 0) {
|
|
11128
11543
|
console.log();
|
|
11129
11544
|
for (const issue of issues) {
|
|
@@ -11131,9 +11546,11 @@ Hook Health Check (${scope})
|
|
|
11131
11546
|
console.log(` ${icon} ${chalk2.cyan(issue.hook)}: ${issue.issue}`);
|
|
11132
11547
|
}
|
|
11133
11548
|
}
|
|
11134
|
-
if (issues.length === 0) {
|
|
11549
|
+
if (issues.length === 0 && registered.length > 0) {
|
|
11135
11550
|
console.log(chalk2.green(`
|
|
11136
|
-
All
|
|
11551
|
+
All ${healthy.length} registered hook(s) healthy`));
|
|
11552
|
+
} else if (issues.some((i) => i.severity === "error")) {
|
|
11553
|
+
process.exitCode = 1;
|
|
11137
11554
|
}
|
|
11138
11555
|
console.log();
|
|
11139
11556
|
});
|
|
@@ -11166,7 +11583,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
11166
11583
|
const version = arg.slice(at + 1);
|
|
11167
11584
|
if (!HOOK_NAME_RE2.test(name))
|
|
11168
11585
|
return null;
|
|
11169
|
-
if (
|
|
11586
|
+
if (!SEMVER_PATTERN.test(version))
|
|
11170
11587
|
return null;
|
|
11171
11588
|
return { name, version };
|
|
11172
11589
|
}
|
|
@@ -11249,6 +11666,7 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
11249
11666
|
} else {
|
|
11250
11667
|
console.log(chalk2.red(`Hook '${hook}' not found`));
|
|
11251
11668
|
}
|
|
11669
|
+
process.exitCode = 1;
|
|
11252
11670
|
return;
|
|
11253
11671
|
}
|
|
11254
11672
|
const hookPath = getHookPath(hook);
|
|
@@ -11503,18 +11921,19 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
|
|
|
11503
11921
|
sql += " ORDER BY timestamp DESC LIMIT ?";
|
|
11504
11922
|
params.push(String(limit));
|
|
11505
11923
|
const rows = db.query(sql).all(...params);
|
|
11924
|
+
const projected = projectLogRows2(rows);
|
|
11506
11925
|
if (options.json) {
|
|
11507
|
-
console.log(JSON.stringify(
|
|
11926
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11508
11927
|
return;
|
|
11509
11928
|
}
|
|
11510
|
-
if (
|
|
11929
|
+
if (projected.length === 0) {
|
|
11511
11930
|
console.log(chalk2.dim("No events found."));
|
|
11512
11931
|
return;
|
|
11513
11932
|
}
|
|
11514
11933
|
console.log(chalk2.bold(`
|
|
11515
|
-
Hook Events (${
|
|
11934
|
+
Hook Events (${projected.length})
|
|
11516
11935
|
`));
|
|
11517
|
-
for (const row of
|
|
11936
|
+
for (const row of projected) {
|
|
11518
11937
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11519
11938
|
const err = row.error ? chalk2.red(` ERR: ${truncateText2(row.error, 60)}`) : "";
|
|
11520
11939
|
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
@@ -11529,18 +11948,19 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
|
|
|
11529
11948
|
const limit = parseInt(options.limit) || 50;
|
|
11530
11949
|
const q = `%${text}%`;
|
|
11531
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);
|
|
11532
11952
|
if (options.json) {
|
|
11533
|
-
console.log(JSON.stringify(
|
|
11953
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11534
11954
|
return;
|
|
11535
11955
|
}
|
|
11536
|
-
if (
|
|
11956
|
+
if (projected.length === 0) {
|
|
11537
11957
|
console.log(chalk2.dim(`No events matching "${text}".`));
|
|
11538
11958
|
return;
|
|
11539
11959
|
}
|
|
11540
11960
|
console.log(chalk2.bold(`
|
|
11541
|
-
Search results for "${text}" (${
|
|
11961
|
+
Search results for "${text}" (${projected.length})
|
|
11542
11962
|
`));
|
|
11543
|
-
for (const row of
|
|
11963
|
+
for (const row of projected) {
|
|
11544
11964
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11545
11965
|
const snippet = truncateText2(row.tool_input || row.error || "", 80);
|
|
11546
11966
|
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
|
|
@@ -11553,18 +11973,19 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
|
|
|
11553
11973
|
const db = getDb2();
|
|
11554
11974
|
const limit = parseInt(options.n) || 20;
|
|
11555
11975
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
11976
|
+
const projected = projectLogRows2(rows);
|
|
11556
11977
|
if (options.json) {
|
|
11557
|
-
console.log(JSON.stringify(
|
|
11978
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11558
11979
|
return;
|
|
11559
11980
|
}
|
|
11560
|
-
if (
|
|
11981
|
+
if (projected.length === 0) {
|
|
11561
11982
|
console.log(chalk2.dim("No events yet."));
|
|
11562
11983
|
return;
|
|
11563
11984
|
}
|
|
11564
11985
|
console.log(chalk2.bold(`
|
|
11565
|
-
Last ${
|
|
11986
|
+
Last ${projected.length} events
|
|
11566
11987
|
`));
|
|
11567
|
-
for (const row of
|
|
11988
|
+
for (const row of projected) {
|
|
11568
11989
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11569
11990
|
const err = row.error ? chalk2.red(` \u2717 ${truncateText2(row.error, 60)}`) : "";
|
|
11570
11991
|
const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
|
|
@@ -11597,18 +12018,19 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
|
|
|
11597
12018
|
}
|
|
11598
12019
|
const since = new Date(Date.now() - parseDuration(options.since)).toISOString();
|
|
11599
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);
|
|
11600
12022
|
if (options.json) {
|
|
11601
|
-
console.log(JSON.stringify(
|
|
12023
|
+
console.log(JSON.stringify(projected, null, 2));
|
|
11602
12024
|
return;
|
|
11603
12025
|
}
|
|
11604
|
-
if (
|
|
12026
|
+
if (projected.length === 0) {
|
|
11605
12027
|
console.log(chalk2.dim(`No errors in the last ${options.since}.`));
|
|
11606
12028
|
return;
|
|
11607
12029
|
}
|
|
11608
12030
|
console.log(chalk2.bold(`
|
|
11609
|
-
Errors (last ${options.since}, ${
|
|
12031
|
+
Errors (last ${options.since}, ${projected.length} found)
|
|
11610
12032
|
`));
|
|
11611
|
-
for (const row of
|
|
12033
|
+
for (const row of projected) {
|
|
11612
12034
|
const ts = row.timestamp.slice(0, 19).replace("T", " ");
|
|
11613
12035
|
console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(truncateText2(row.error, 100))}`);
|
|
11614
12036
|
}
|
|
@@ -11730,18 +12152,17 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
|
|
|
11730
12152
|
console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
|
|
11731
12153
|
console.log(chalk2.dim(` sha256 ${result.actual}`));
|
|
11732
12154
|
});
|
|
11733
|
-
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) => {
|
|
11734
12156
|
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
11735
12157
|
startServeServer2({
|
|
11736
12158
|
port: options.port ? parseInt(options.port, 10) : undefined,
|
|
11737
|
-
host: options.host
|
|
11738
|
-
apiKey: options.apiKey
|
|
12159
|
+
host: options.host
|
|
11739
12160
|
});
|
|
11740
12161
|
});
|
|
11741
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) => {
|
|
11742
12163
|
const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
11743
12164
|
try {
|
|
11744
|
-
const plan = options.dryRun ? await
|
|
12165
|
+
const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
|
|
11745
12166
|
const diff = plan.diff;
|
|
11746
12167
|
if (options.json) {
|
|
11747
12168
|
console.log(JSON.stringify({ dry_run: plan.dryRun, api_url: plan.apiUrl ?? null, diff }));
|
|
@@ -11837,7 +12258,7 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
11837
12258
|
await startStdioServer2();
|
|
11838
12259
|
} else if (options.sse) {
|
|
11839
12260
|
const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
11840
|
-
await startSSEServer2(options.port ? parseInt(options.port) : 39427);
|
|
12261
|
+
await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
|
|
11841
12262
|
} else {
|
|
11842
12263
|
const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
11843
12264
|
const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|