@hasna/hooks 0.5.0 → 0.6.0

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/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
- // node_modules/commander/lib/error.js
50
+ // ../../node_modules/.bun/commander@12.1.0/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
- // node_modules/commander/lib/argument.js
74
+ // ../../node_modules/.bun/commander@12.1.0/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
- // node_modules/commander/lib/help.js
153
+ // ../../node_modules/.bun/commander@12.1.0/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
- // node_modules/commander/lib/option.js
402
+ // ../../node_modules/.bun/commander@12.1.0/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
- // node_modules/commander/lib/suggestSimilar.js
553
+ // ../../node_modules/.bun/commander@12.1.0/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
- // node_modules/commander/lib/command.js
626
+ // ../../node_modules/.bun/commander@12.1.0/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
- // node_modules/commander/index.js
1869
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/index.js
1870
1870
  var require_commander = __commonJS((exports) => {
1871
1871
  var { Argument } = require_argument();
1872
1872
  var { Command } = require_command();
@@ -2045,6 +2045,16 @@ var init_registry = __esm(() => {
2045
2045
  matcher: "Bash",
2046
2046
  tags: ["codewith", "bash", "secrets", "gitleaks", "risky-ops"]
2047
2047
  },
2048
+ {
2049
+ name: "scanoutput",
2050
+ displayName: "Scan Output",
2051
+ description: "Scans tool output for credential shapes and warns. Detection only \u2014 the output is already persisted when this runs; it cannot redact or prevent",
2052
+ version: "0.1.0",
2053
+ category: "Security",
2054
+ event: "PostToolUse",
2055
+ matcher: "Bash",
2056
+ tags: ["security", "credentials", "secrets", "detection", "audit"]
2057
+ },
2048
2058
  {
2049
2059
  name: "phonenotify",
2050
2060
  displayName: "Phone Notify",
@@ -2399,7 +2409,7 @@ var init_registry = __esm(() => {
2399
2409
  ];
2400
2410
  });
2401
2411
 
2402
- // node_modules/cli-spinners/spinners.json
2412
+ // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/spinners.json
2403
2413
  var require_spinners = __commonJS((exports, module) => {
2404
2414
  module.exports = {
2405
2415
  dots: {
@@ -4025,7 +4035,7 @@ var require_spinners = __commonJS((exports, module) => {
4025
4035
  };
4026
4036
  });
4027
4037
 
4028
- // node_modules/cli-spinners/index.js
4038
+ // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/index.js
4029
4039
  var require_cli_spinners = __commonJS((exports, module) => {
4030
4040
  var spinners = Object.assign({}, require_spinners());
4031
4041
  var spinnersList = Object.keys(spinners);
@@ -4039,10 +4049,320 @@ var require_cli_spinners = __commonJS((exports, module) => {
4039
4049
  module.exports = spinners;
4040
4050
  });
4041
4051
 
4042
- // src/lib/installer.ts
4052
+ // src/config.ts
4053
+ var exports_config = {};
4054
+ __export(exports_config, {
4055
+ writeConfig: () => writeConfig,
4056
+ resolveApiUrl: () => resolveApiUrl,
4057
+ resolveApiKey: () => resolveApiKey,
4058
+ readConfig: () => readConfig,
4059
+ getLockPath: () => getLockPath,
4060
+ getHooksDataDir: () => getHooksDataDir,
4061
+ getCustomHooksDir: () => getCustomHooksDir,
4062
+ getConfigPath: () => getConfigPath
4063
+ });
4043
4064
  import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
4044
- import { join as join2, dirname } from "path";
4065
+ import { join as join2 } from "path";
4045
4066
  import { homedir as homedir2 } from "os";
4067
+ function getHooksDataDir() {
4068
+ const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4069
+ if (explicit)
4070
+ return explicit;
4071
+ return join2(homedir2(), ".hasna", "hooks");
4072
+ }
4073
+ function getCustomHooksDir() {
4074
+ return join2(getHooksDataDir(), "hooks");
4075
+ }
4076
+ function getLockPath() {
4077
+ const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
4078
+ if (explicit)
4079
+ return explicit;
4080
+ return join2(getHooksDataDir(), "hooks.lock");
4081
+ }
4082
+ function getConfigPath() {
4083
+ const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
4084
+ if (explicit)
4085
+ return explicit;
4086
+ return join2(getHooksDataDir(), "config.json");
4087
+ }
4088
+ function readConfig() {
4089
+ try {
4090
+ const path = getConfigPath();
4091
+ if (!existsSync2(path))
4092
+ return {};
4093
+ return JSON.parse(readFileSync(path, "utf-8"));
4094
+ } catch {
4095
+ return {};
4096
+ }
4097
+ }
4098
+ function writeConfig(config) {
4099
+ const path = getConfigPath();
4100
+ mkdirSync(join2(path, ".."), { recursive: true });
4101
+ writeFileSync(path, JSON.stringify(config, null, 2) + `
4102
+ `, "utf-8");
4103
+ return path;
4104
+ }
4105
+ function resolveApiUrl() {
4106
+ const envUrl = process.env.HASNA_HOOKS_API_URL ?? process.env.HOOKS_API_URL ?? process.env.HASNA_HOOKS_REGISTRY_URL ?? process.env.HOOKS_REGISTRY_URL;
4107
+ if (envUrl)
4108
+ return envUrl.replace(/\/+$/, "");
4109
+ const cfg = readConfig();
4110
+ return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
4111
+ }
4112
+ function resolveApiKey(flagKey) {
4113
+ if (flagKey && flagKey.length > 0)
4114
+ return flagKey;
4115
+ return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
4116
+ }
4117
+ var init_config = () => {};
4118
+
4119
+ // src/lib/manifest.ts
4120
+ var exports_manifest = {};
4121
+ __export(exports_manifest, {
4122
+ writeCustomHook: () => writeCustomHook,
4123
+ shortManifestName: () => shortManifestName,
4124
+ resolveScript: () => resolveScript,
4125
+ readCustomManifest: () => readCustomManifest,
4126
+ parseManifest: () => parseManifest,
4127
+ normalizeManifestName: () => normalizeManifestName,
4128
+ manifestSchema: () => manifestSchema,
4129
+ listCustomHooks: () => listCustomHooks,
4130
+ customHookDir: () => customHookDir,
4131
+ assertContained: () => assertContained,
4132
+ ScriptContainmentError: () => ScriptContainmentError,
4133
+ HOOK_NAME_RE: () => HOOK_NAME_RE
4134
+ });
4135
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, writeFileSync as writeFileSync2 } from "fs";
4136
+ import { basename, dirname, isAbsolute, join as join3, normalize, resolve, sep } from "path";
4137
+ import { z } from "zod";
4138
+ function parseManifest(json) {
4139
+ const parsed = JSON.parse(json);
4140
+ return manifestSchema.parse(parsed);
4141
+ }
4142
+ function normalizeManifestName(name) {
4143
+ return name.startsWith("hook-") ? name : `hook-${name}`;
4144
+ }
4145
+ function shortManifestName(name) {
4146
+ return name.startsWith("hook-") ? name.slice("hook-".length) : name;
4147
+ }
4148
+ function isWithin(child, parent) {
4149
+ return child === parent || child.startsWith(parent + sep);
4150
+ }
4151
+ function assertContainedLexical(target, root, raw) {
4152
+ const lexical = resolve(target);
4153
+ const rootLexical = resolve(root);
4154
+ if (!isWithin(lexical, rootLexical)) {
4155
+ throw new ScriptContainmentError(target, root, raw);
4156
+ }
4157
+ return lexical;
4158
+ }
4159
+ function realpathNearest(path) {
4160
+ const tail = [];
4161
+ let cur = path;
4162
+ while (!existsSync3(cur)) {
4163
+ const parent = dirname(cur);
4164
+ if (parent === cur)
4165
+ break;
4166
+ tail.unshift(basename(cur));
4167
+ cur = parent;
4168
+ }
4169
+ return join3(realpathSync(cur), ...tail);
4170
+ }
4171
+ function assertContainedReal(root, target, raw) {
4172
+ const rootReal = realpathSync(root);
4173
+ const targetReal = realpathNearest(target);
4174
+ if (!isWithin(targetReal, rootReal)) {
4175
+ throw new ScriptContainmentError(target, root, raw);
4176
+ }
4177
+ }
4178
+ function assertContained(target, root, raw) {
4179
+ const lexical = assertContainedLexical(target, root, raw);
4180
+ assertContainedReal(root, lexical, raw);
4181
+ return lexical;
4182
+ }
4183
+ function resolveScript(manifest, manifestDir) {
4184
+ if (manifest.script.includes(`
4185
+ `)) {
4186
+ const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
4187
+ return { path: `script${ext}`, content: manifest.script };
4188
+ }
4189
+ const rel = normalize(manifest.script);
4190
+ if (isAbsolute(rel)) {
4191
+ throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
4192
+ }
4193
+ const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
4194
+ if (!existsSync3(resolved)) {
4195
+ throw new Error(`manifest script file not found: ${resolved}`);
4196
+ }
4197
+ return { path: rel, content: readFileSync2(resolved, "utf-8") };
4198
+ }
4199
+ function customHookDir(name) {
4200
+ return join3(getCustomHooksDir(), shortManifestName(name));
4201
+ }
4202
+ function readCustomManifest(name) {
4203
+ const dir = customHookDir(name);
4204
+ const manifestPath = join3(dir, "manifest.json");
4205
+ if (!existsSync3(manifestPath))
4206
+ return;
4207
+ try {
4208
+ const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
4209
+ const script = resolveScript(manifest, dir);
4210
+ return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline: manifest.script.includes(`
4211
+ `) };
4212
+ } catch (err) {
4213
+ if (err instanceof ScriptContainmentError)
4214
+ throw err;
4215
+ return;
4216
+ }
4217
+ }
4218
+ function listCustomHooks() {
4219
+ const dir = getCustomHooksDir();
4220
+ if (!existsSync3(dir))
4221
+ return [];
4222
+ const results = [];
4223
+ for (const entry of readdirSafe(dir)) {
4224
+ const parsed = readCustomManifest(entry);
4225
+ if (parsed)
4226
+ results.push(parsed);
4227
+ }
4228
+ return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
4229
+ }
4230
+ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
4231
+ const dir = customHookDir(name);
4232
+ if (isAbsolute(scriptRelPath)) {
4233
+ throw new ScriptContainmentError(scriptRelPath, dir);
4234
+ }
4235
+ const scriptPath = scriptRelPath.includes("/") ? join3(dir, scriptRelPath) : join3(dir, scriptRelPath);
4236
+ assertContainedLexical(scriptPath, dir, scriptRelPath);
4237
+ mkdirSync2(dir, { recursive: true });
4238
+ assertContainedReal(dir, scriptPath, scriptRelPath);
4239
+ const scriptDir = dirname(scriptPath);
4240
+ if (scriptDir !== dir)
4241
+ mkdirSync2(scriptDir, { recursive: true });
4242
+ writeFileSync2(scriptPath, scriptContent, "utf-8");
4243
+ writeFileSync2(join3(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
4244
+ `, "utf-8");
4245
+ return { dir, scriptPath };
4246
+ }
4247
+ function readdirSafe(dir) {
4248
+ try {
4249
+ return readdirSync(dir);
4250
+ } catch {
4251
+ return [];
4252
+ }
4253
+ }
4254
+ var HOOK_NAME_RE, manifestSchema, ScriptContainmentError;
4255
+ var init_manifest = __esm(() => {
4256
+ init_config();
4257
+ HOOK_NAME_RE = /^[\w-]+$/;
4258
+ manifestSchema = z.object({
4259
+ name: z.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
4260
+ version: z.string().regex(/^\d+\.\d+\.\d+/, "version must be semver (e.g. 1.2.3)"),
4261
+ description: z.string().optional(),
4262
+ events: z.array(z.string()).min(1, "at least one event is required"),
4263
+ script: z.string().min(1, "script must name a relative path or inline content"),
4264
+ args: z.array(z.string()).optional(),
4265
+ timeout_ms: z.number().int().positive().optional()
4266
+ });
4267
+ ScriptContainmentError = class ScriptContainmentError extends Error {
4268
+ constructor(path, hookDir, raw) {
4269
+ super(raw && raw !== path ? `manifest script escapes the hook directory: '${path}' (from '${raw}') resolves outside '${hookDir}'` : `manifest script escapes the hook directory: '${path}' resolves outside '${hookDir}'`);
4270
+ this.name = "ScriptContainmentError";
4271
+ }
4272
+ };
4273
+ });
4274
+
4275
+ // src/lib/resolve.ts
4276
+ var exports_resolve = {};
4277
+ __export(exports_resolve, {
4278
+ resolveScriptPath: () => resolveScriptPath,
4279
+ resolveHookMeta: () => resolveHookMeta,
4280
+ resolveHookDir: () => resolveHookDir,
4281
+ resolveHook: () => resolveHook
4282
+ });
4283
+ import { join as join4 } from "path";
4284
+ import { existsSync as existsSync4 } from "fs";
4285
+ function resolveHookDir(name) {
4286
+ const custom = readCustomManifest(name);
4287
+ if (custom)
4288
+ return customHookDir(name);
4289
+ const meta = getHook(shortManifestName(name));
4290
+ if (!meta)
4291
+ return;
4292
+ const short = shortManifestName(name);
4293
+ const bundledBase = join4(__dirname, "..", "..", "hooks");
4294
+ const direct = join4(bundledBase, short);
4295
+ if (existsSync4(direct))
4296
+ return direct;
4297
+ const prefixed = join4(bundledBase, `hook-${short}`);
4298
+ return existsSync4(prefixed) ? prefixed : undefined;
4299
+ }
4300
+ function resolveScriptPath(name) {
4301
+ const custom = readCustomManifest(name);
4302
+ if (custom)
4303
+ return custom.scriptPath;
4304
+ const dir = resolveHookDir(name);
4305
+ if (!dir)
4306
+ return;
4307
+ const script = join4(dir, "src", "hook.ts");
4308
+ return existsSync4(script) ? script : undefined;
4309
+ }
4310
+ function resolveHookMeta(name) {
4311
+ const custom = readCustomManifest(name);
4312
+ if (custom) {
4313
+ const { manifest } = custom;
4314
+ return {
4315
+ name: shortManifestName(manifest.name),
4316
+ displayName: manifest.description ? manifest.name : manifest.name,
4317
+ description: manifest.description ?? "Custom hook",
4318
+ version: manifest.version,
4319
+ category: "Workflow Automation",
4320
+ event: manifest.events[0] ?? "PostToolUse",
4321
+ events: manifest.events,
4322
+ matcher: "",
4323
+ tags: ["custom"]
4324
+ };
4325
+ }
4326
+ return getHook(shortManifestName(name));
4327
+ }
4328
+ function resolveHook(name) {
4329
+ const custom = readCustomManifest(name);
4330
+ if (custom) {
4331
+ const meta2 = resolveHookMeta(name);
4332
+ return {
4333
+ name: shortManifestName(custom.manifest.name),
4334
+ source: "custom",
4335
+ version: custom.manifest.version,
4336
+ events: custom.manifest.events,
4337
+ description: custom.manifest.description ?? "Custom hook",
4338
+ scriptPath: custom.scriptPath,
4339
+ meta: meta2
4340
+ };
4341
+ }
4342
+ const meta = getHook(shortManifestName(name));
4343
+ const scriptPath = resolveScriptPath(name);
4344
+ if (!meta || !scriptPath)
4345
+ return;
4346
+ return {
4347
+ name: shortManifestName(name),
4348
+ source: "bundled",
4349
+ version: meta.version,
4350
+ events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
4351
+ description: meta.description,
4352
+ scriptPath,
4353
+ meta
4354
+ };
4355
+ }
4356
+ var __dirname = "/home/hasna/.hasna/repos/worktrees/apps/1ec6ba31-cdbc-42ea-8b1e-705e4198abbf/apps/hooks/src/lib";
4357
+ var init_resolve = __esm(() => {
4358
+ init_registry();
4359
+ init_manifest();
4360
+ });
4361
+
4362
+ // src/lib/installer.ts
4363
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4364
+ import { join as join5, dirname as dirname2 } from "path";
4365
+ import { homedir as homedir3 } from "os";
4046
4366
  import { fileURLToPath } from "url";
4047
4367
  function normalizeHookName(name) {
4048
4368
  return name.startsWith("hook-") ? name : `hook-${name}`;
@@ -4063,33 +4383,43 @@ function getTargetSettingsDir(target) {
4063
4383
  return ".gemini";
4064
4384
  return ".claude";
4065
4385
  }
4386
+ function getGlobalSettingsPathOverride(target) {
4387
+ if (target === "claude")
4388
+ return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
4389
+ if (target === "gemini")
4390
+ return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
4391
+ return;
4392
+ }
4066
4393
  function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
4067
4394
  if (target === "codewith" && codewithConfigPath)
4068
4395
  return codewithConfigPath;
4069
4396
  if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
4070
4397
  return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
4071
4398
  }
4399
+ const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
4400
+ if (globalOverride)
4401
+ return globalOverride;
4072
4402
  const dir = getTargetSettingsDir(target);
4073
4403
  if (scope === "project") {
4074
- return target === "codewith" ? join2(process.cwd(), dir, "config.toml") : join2(process.cwd(), dir, "settings.json");
4404
+ return target === "codewith" ? join5(process.cwd(), dir, "config.toml") : join5(process.cwd(), dir, "settings.json");
4075
4405
  }
4076
- return target === "codewith" ? join2(homedir2(), dir, "config.toml") : join2(homedir2(), dir, "settings.json");
4406
+ return target === "codewith" ? join5(homedir3(), dir, "config.toml") : join5(homedir3(), dir, "settings.json");
4077
4407
  }
4078
4408
  function getHookPath(name) {
4079
4409
  const shortName = shortHookName(name);
4080
- const direct = join2(HOOKS_DIR, shortName);
4081
- if (existsSync2(direct))
4082
- return direct;
4083
- return join2(HOOKS_DIR, normalizeHookName(shortName));
4410
+ const resolved = resolveHookDir(shortName);
4411
+ if (resolved)
4412
+ return resolved;
4413
+ return join5(HOOKS_DIR, normalizeHookName(shortName));
4084
4414
  }
4085
4415
  function hookExists(name) {
4086
- return existsSync2(getHookPath(name));
4416
+ return existsSync5(getHookPath(name));
4087
4417
  }
4088
4418
  function readSettings(scope = "global", target = "claude") {
4089
4419
  const path = getSettingsPath(scope, target);
4090
4420
  try {
4091
- if (existsSync2(path)) {
4092
- return JSON.parse(readFileSync(path, "utf-8"));
4421
+ if (existsSync5(path)) {
4422
+ return JSON.parse(readFileSync3(path, "utf-8"));
4093
4423
  }
4094
4424
  } catch (error) {
4095
4425
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -4098,11 +4428,11 @@ function readSettings(scope = "global", target = "claude") {
4098
4428
  }
4099
4429
  function writeSettings(settings, scope = "global", target = "claude") {
4100
4430
  const path = getSettingsPath(scope, target);
4101
- const dir = dirname(path);
4102
- if (!existsSync2(dir)) {
4103
- mkdirSync(dir, { recursive: true });
4431
+ const dir = dirname2(path);
4432
+ if (!existsSync5(dir)) {
4433
+ mkdirSync3(dir, { recursive: true });
4104
4434
  }
4105
- writeFileSync(path, JSON.stringify(settings, null, 2) + `
4435
+ writeFileSync3(path, JSON.stringify(settings, null, 2) + `
4106
4436
  `);
4107
4437
  }
4108
4438
  function getTargetEventName(internalEvent, target) {
@@ -4160,7 +4490,7 @@ function codewithStatusMessage(name) {
4160
4490
  }
4161
4491
  function buildCodewithTomlFragment(name, profile) {
4162
4492
  const shortName = shortHookName(name);
4163
- const meta = getHook(shortName);
4493
+ const meta = resolveHookMeta(shortName);
4164
4494
  if (!meta)
4165
4495
  throw new Error(`Hook '${shortName}' not found`);
4166
4496
  const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
@@ -4188,25 +4518,25 @@ function buildCodewithTomlFragment(name, profile) {
4188
4518
  function readCodewithConfig(scope, configPath) {
4189
4519
  const path = getSettingsPath(scope, "codewith", configPath);
4190
4520
  try {
4191
- return existsSync2(path) ? readFileSync(path, "utf-8") : "";
4521
+ return existsSync5(path) ? readFileSync3(path, "utf-8") : "";
4192
4522
  } catch {
4193
4523
  return "";
4194
4524
  }
4195
4525
  }
4196
4526
  function appendCodewithFragment(fragment, scope, configPath) {
4197
4527
  const path = getSettingsPath(scope, "codewith", configPath);
4198
- const dir = dirname(path);
4199
- if (!existsSync2(dir))
4200
- mkdirSync(dir, { recursive: true });
4201
- const existing = existsSync2(path) ? readFileSync(path, "utf-8") : "";
4202
- const sep = existing.trim() ? `
4528
+ const dir = dirname2(path);
4529
+ if (!existsSync5(dir))
4530
+ mkdirSync3(dir, { recursive: true });
4531
+ const existing = existsSync5(path) ? readFileSync3(path, "utf-8") : "";
4532
+ const sep2 = existing.trim() ? `
4203
4533
 
4204
4534
  ` : "";
4205
- writeFileSync(path, `${existing.replace(/\s*$/, "")}${sep}${fragment}`);
4535
+ writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
4206
4536
  return path;
4207
4537
  }
4208
4538
  function detectConflict(name, scope, target) {
4209
- const meta = getHook(name);
4539
+ const meta = resolveHookMeta(name);
4210
4540
  if (!meta || !meta.matcher)
4211
4541
  return;
4212
4542
  const events = new Set(getHookEvents(meta));
@@ -4302,7 +4632,7 @@ function installHook(name, options = {}) {
4302
4632
  const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
4303
4633
  if (target === "all") {
4304
4634
  const shortName = shortHookName(name);
4305
- const meta = getHook(shortName);
4635
+ const meta = resolveHookMeta(shortName);
4306
4636
  if (meta) {
4307
4637
  const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
4308
4638
  if (unsupportedTargets.length > 0) {
@@ -4335,7 +4665,7 @@ function installHook(name, options = {}) {
4335
4665
  return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
4336
4666
  }
4337
4667
  function registerHook(name, scope = "global", target = "claude", profile) {
4338
- const meta = getHook(name);
4668
+ const meta = resolveHookMeta(name);
4339
4669
  if (!meta)
4340
4670
  return;
4341
4671
  const eventKeys = getHookEvents(meta).map((event) => {
@@ -4378,7 +4708,7 @@ function removeHookFromAllEvents(settings, name) {
4378
4708
  }
4379
4709
  }
4380
4710
  function unregisterHook(name, scope = "global", target = "claude") {
4381
- const meta = getHook(name);
4711
+ const meta = resolveHookMeta(name);
4382
4712
  if (!meta)
4383
4713
  return;
4384
4714
  const settings = readSettings(scope, target);
@@ -4445,8 +4775,9 @@ function removeHookForTarget(name, scope, target) {
4445
4775
  var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks;
4446
4776
  var init_installer = __esm(() => {
4447
4777
  init_registry();
4448
- __dirname2 = dirname(fileURLToPath(import.meta.url));
4449
- HOOKS_DIR = existsSync2(join2(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join2(__dirname2, "..", "..", "hooks") : join2(__dirname2, "..", "hooks");
4778
+ init_resolve();
4779
+ __dirname2 = dirname2(fileURLToPath(import.meta.url));
4780
+ HOOKS_DIR = existsSync5(join5(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join5(__dirname2, "..", "..", "hooks") : join5(__dirname2, "..", "hooks");
4450
4781
  EVENT_MAP = {
4451
4782
  claude: {
4452
4783
  PreToolUse: "PreToolUse",
@@ -4483,25 +4814,25 @@ var init_installer = __esm(() => {
4483
4814
  });
4484
4815
 
4485
4816
  // src/lib/profiles.ts
4486
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync, rmSync, cpSync } from "fs";
4487
- import { join as join3 } from "path";
4488
- import { homedir as homedir3 } from "os";
4817
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, readdirSync as readdirSync2, rmSync, cpSync } from "fs";
4818
+ import { join as join6 } from "path";
4819
+ import { homedir as homedir4 } from "os";
4489
4820
  function resolveProfilesDir() {
4490
- const newDir = join3(homedir3(), ".hasna", "hooks", "profiles");
4491
- const oldDir = join3(homedir3(), ".hooks", "profiles");
4492
- if (!existsSync3(newDir) && existsSync3(oldDir)) {
4493
- mkdirSync2(join3(homedir3(), ".hasna", "hooks"), { recursive: true });
4821
+ const newDir = join6(homedir4(), ".hasna", "hooks", "profiles");
4822
+ const oldDir = join6(homedir4(), ".hooks", "profiles");
4823
+ if (!existsSync6(newDir) && existsSync6(oldDir)) {
4824
+ mkdirSync4(join6(homedir4(), ".hasna", "hooks"), { recursive: true });
4494
4825
  cpSync(oldDir, newDir, { recursive: true });
4495
4826
  }
4496
4827
  return newDir;
4497
4828
  }
4498
4829
  function ensureProfilesDir() {
4499
- if (!existsSync3(PROFILES_DIR)) {
4500
- mkdirSync2(PROFILES_DIR, { recursive: true });
4830
+ if (!existsSync6(PROFILES_DIR)) {
4831
+ mkdirSync4(PROFILES_DIR, { recursive: true });
4501
4832
  }
4502
4833
  }
4503
4834
  function profilePath(id) {
4504
- return join3(PROFILES_DIR, `${id}.json`);
4835
+ return join6(PROFILES_DIR, `${id}.json`);
4505
4836
  }
4506
4837
  function shortUuid() {
4507
4838
  return crypto.randomUUID().slice(0, 8);
@@ -4520,29 +4851,29 @@ function createProfile(input) {
4520
4851
  if (input.name) {
4521
4852
  profile.name = input.name;
4522
4853
  }
4523
- writeFileSync2(profilePath(id), JSON.stringify(profile, null, 2) + `
4854
+ writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
4524
4855
  `);
4525
4856
  return profile;
4526
4857
  }
4527
4858
  function getProfile(id) {
4528
4859
  const path = profilePath(id);
4529
4860
  try {
4530
- if (!existsSync3(path))
4861
+ if (!existsSync6(path))
4531
4862
  return null;
4532
- return JSON.parse(readFileSync2(path, "utf-8"));
4863
+ return JSON.parse(readFileSync4(path, "utf-8"));
4533
4864
  } catch {
4534
4865
  return null;
4535
4866
  }
4536
4867
  }
4537
4868
  function listProfiles() {
4538
- if (!existsSync3(PROFILES_DIR))
4869
+ if (!existsSync6(PROFILES_DIR))
4539
4870
  return [];
4540
4871
  try {
4541
- const files = readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".json"));
4872
+ const files = readdirSync2(PROFILES_DIR).filter((f) => f.endsWith(".json"));
4542
4873
  const profiles = [];
4543
4874
  for (const file of files) {
4544
4875
  try {
4545
- const content = readFileSync2(join3(PROFILES_DIR, file), "utf-8");
4876
+ const content = readFileSync4(join6(PROFILES_DIR, file), "utf-8");
4546
4877
  profiles.push(JSON.parse(content));
4547
4878
  } catch {}
4548
4879
  }
@@ -4556,7 +4887,7 @@ function touchProfile(id) {
4556
4887
  if (!profile)
4557
4888
  return;
4558
4889
  profile.last_seen_at = new Date().toISOString();
4559
- writeFileSync2(profilePath(id), JSON.stringify(profile, null, 2) + `
4890
+ writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
4560
4891
  `);
4561
4892
  }
4562
4893
  function exportProfiles() {
@@ -4572,11 +4903,11 @@ function importProfiles(profiles) {
4572
4903
  continue;
4573
4904
  }
4574
4905
  const path = profilePath(profile.agent_id);
4575
- if (existsSync3(path)) {
4906
+ if (existsSync6(path)) {
4576
4907
  skipped++;
4577
4908
  continue;
4578
4909
  }
4579
- writeFileSync2(path, JSON.stringify(profile, null, 2) + `
4910
+ writeFileSync4(path, JSON.stringify(profile, null, 2) + `
4580
4911
  `);
4581
4912
  imported++;
4582
4913
  }
@@ -4683,6 +5014,32 @@ var init_003_user_prompt_submit_event = __esm(() => {
4683
5014
  init_schema();
4684
5015
  });
4685
5016
 
5017
+ // src/db/migrations/004_hooks_table.ts
5018
+ function up4(db) {
5019
+ db.exec(CREATE_HOOKS_TABLE);
5020
+ for (const idx of CREATE_HOOKS_INDEXES) {
5021
+ db.exec(idx);
5022
+ }
5023
+ }
5024
+ var CREATE_HOOKS_TABLE = `
5025
+ CREATE TABLE IF NOT EXISTS hooks (
5026
+ id TEXT PRIMARY KEY,
5027
+ name TEXT NOT NULL,
5028
+ version TEXT NOT NULL,
5029
+ sha256 TEXT NOT NULL,
5030
+ source_type TEXT NOT NULL,
5031
+ source_ref TEXT,
5032
+ installed_at TEXT NOT NULL,
5033
+ enabled INTEGER NOT NULL DEFAULT 1,
5034
+ last_verified_at TEXT
5035
+ )
5036
+ `, CREATE_HOOKS_INDEXES;
5037
+ var init_004_hooks_table = __esm(() => {
5038
+ CREATE_HOOKS_INDEXES = [
5039
+ `CREATE INDEX IF NOT EXISTS idx_hooks_name ON hooks (name)`
5040
+ ];
5041
+ });
5042
+
4686
5043
  // src/db/migrations/index.ts
4687
5044
  function ensureMigrationsTable(db) {
4688
5045
  db.exec(`
@@ -4714,17 +5071,19 @@ var init_migrations = __esm(() => {
4714
5071
  init_001_initial();
4715
5072
  init_002_session_events();
4716
5073
  init_003_user_prompt_submit_event();
5074
+ init_004_hooks_table();
4717
5075
  MIGRATIONS = [
4718
5076
  { version: "001_initial", up },
4719
5077
  { version: "002_session_events", up: up2 },
4720
- { version: "003_user_prompt_submit_event", up: up3 }
5078
+ { version: "003_user_prompt_submit_event", up: up3 },
5079
+ { version: "004_hooks_table", up: up4 }
4721
5080
  ];
4722
5081
  });
4723
5082
 
4724
5083
  // src/db/legacy-import.ts
4725
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
4726
- import { join as join4 } from "path";
4727
- import { homedir as homedir4 } from "os";
5084
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
5085
+ import { join as join7 } from "path";
5086
+ import { homedir as homedir5 } from "os";
4728
5087
  function ensureMetaTable(db) {
4729
5088
  db.exec(`
4730
5089
  CREATE TABLE IF NOT EXISTS _meta (
@@ -4747,7 +5106,7 @@ function nanoid() {
4747
5106
  function importJsonlFile(db, filePath) {
4748
5107
  let count = 0;
4749
5108
  try {
4750
- const lines = readFileSync3(filePath, "utf-8").split(`
5109
+ const lines = readFileSync5(filePath, "utf-8").split(`
4751
5110
  `).filter(Boolean);
4752
5111
  for (const line of lines) {
4753
5112
  try {
@@ -4773,7 +5132,7 @@ function importJsonlFile(db, filePath) {
4773
5132
  function importErrorsLog(db, filePath) {
4774
5133
  let count = 0;
4775
5134
  try {
4776
- const lines = readFileSync3(filePath, "utf-8").split(`
5135
+ const lines = readFileSync5(filePath, "utf-8").split(`
4777
5136
  `).filter(Boolean);
4778
5137
  const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
4779
5138
  for (const line of lines) {
@@ -4798,25 +5157,25 @@ function importErrorsLog(db, filePath) {
4798
5157
  } catch {}
4799
5158
  return count;
4800
5159
  }
4801
- function runLegacyImport(db) {
5160
+ function runLegacyImport(db, homeDir = homedir5()) {
4802
5161
  try {
4803
5162
  if (isAlreadyDone(db))
4804
5163
  return;
4805
5164
  let total = 0;
4806
- const claudeProjectsDir = join4(homedir4(), ".claude", "projects");
4807
- if (existsSync4(claudeProjectsDir)) {
5165
+ const claudeProjectsDir = join7(homeDir, ".claude", "projects");
5166
+ if (existsSync7(claudeProjectsDir)) {
4808
5167
  try {
4809
- const projectDirs = readdirSync2(claudeProjectsDir);
5168
+ const projectDirs = readdirSync3(claudeProjectsDir);
4810
5169
  for (const dir of projectDirs) {
4811
- const projectDir = join4(claudeProjectsDir, dir);
5170
+ const projectDir = join7(claudeProjectsDir, dir);
4812
5171
  try {
4813
- const files = readdirSync2(projectDir);
5172
+ const files = readdirSync3(projectDir);
4814
5173
  for (const file of files) {
4815
5174
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
4816
- total += importJsonlFile(db, join4(projectDir, file));
5175
+ total += importJsonlFile(db, join7(projectDir, file));
4817
5176
  }
4818
5177
  if (file === "errors.log") {
4819
- total += importErrorsLog(db, join4(projectDir, file));
5178
+ total += importErrorsLog(db, join7(projectDir, file));
4820
5179
  }
4821
5180
  }
4822
5181
  } catch {}
@@ -4859,17 +5218,17 @@ __export(exports_db, {
4859
5218
  closeDb: () => closeDb
4860
5219
  });
4861
5220
  import { Database } from "bun:sqlite";
4862
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, cpSync as cpSync2 } from "fs";
4863
- import { join as join5 } from "path";
4864
- import { homedir as homedir5 } from "os";
5221
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, cpSync as cpSync2 } from "fs";
5222
+ import { join as join8 } from "path";
5223
+ import { homedir as homedir6 } from "os";
4865
5224
  function resolveDataDir() {
4866
5225
  const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4867
5226
  if (explicit)
4868
5227
  return explicit;
4869
- const newDir = join5(homedir5(), ".hasna", "hooks");
4870
- const oldDir = join5(homedir5(), ".hooks");
4871
- if (!existsSync5(newDir) && existsSync5(oldDir)) {
4872
- mkdirSync3(join5(homedir5(), ".hasna"), { recursive: true });
5228
+ const newDir = join8(homedir6(), ".hasna", "hooks");
5229
+ const oldDir = join8(homedir6(), ".hooks");
5230
+ if (!existsSync8(newDir) && existsSync8(oldDir)) {
5231
+ mkdirSync5(join8(homedir6(), ".hasna"), { recursive: true });
4873
5232
  cpSync2(oldDir, newDir, { recursive: true });
4874
5233
  }
4875
5234
  return newDir;
@@ -4879,19 +5238,19 @@ function getDbPath() {
4879
5238
  if (explicitDb)
4880
5239
  return explicitDb;
4881
5240
  const dataDir = resolveDataDir();
4882
- return join5(dataDir, "hooks.db");
5241
+ return join8(dataDir, "hooks.db");
4883
5242
  }
4884
5243
  function ensureDir(dbPath) {
4885
5244
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
4886
- if (dir && !existsSync5(dir)) {
4887
- mkdirSync3(dir, { recursive: true });
5245
+ if (dir && !existsSync8(dir)) {
5246
+ mkdirSync5(dir, { recursive: true });
4888
5247
  }
4889
5248
  }
4890
5249
  function getDb() {
4891
5250
  if (instance)
4892
5251
  return instance;
4893
5252
  const dbPath = getDbPath();
4894
- const isNew = dbPath === ":memory:" || !existsSync5(dbPath);
5253
+ const isNew = dbPath === ":memory:" || !existsSync8(dbPath);
4895
5254
  ensureDir(dbPath);
4896
5255
  instance = new Database(dbPath);
4897
5256
  instance.exec("PRAGMA journal_mode=WAL");
@@ -4930,6 +5289,313 @@ var init_db = __esm(() => {
4930
5289
  init_legacy_import();
4931
5290
  });
4932
5291
 
5292
+ // src/lib/store.ts
5293
+ var exports_store = {};
5294
+ __export(exports_store, {
5295
+ writeLock: () => writeLock,
5296
+ verifyScriptHash: () => verifyScriptHash,
5297
+ upsertHookRecord: () => upsertHookRecord,
5298
+ sha256Of: () => sha256Of,
5299
+ sha256File: () => sha256File,
5300
+ setPinnedHook: () => setPinnedHook,
5301
+ retrustHook: () => retrustHook,
5302
+ removePinnedHook: () => removePinnedHook,
5303
+ removeHookRecord: () => removeHookRecord,
5304
+ readLock: () => readLock,
5305
+ listHookRecords: () => listHookRecords,
5306
+ getPinnedHook: () => getPinnedHook,
5307
+ getHookRecord: () => getHookRecord,
5308
+ checkScriptHash: () => checkScriptHash
5309
+ });
5310
+ import { createHash } from "crypto";
5311
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
5312
+ import { readFile as readFile2 } from "fs/promises";
5313
+ function sha256Of(input) {
5314
+ return createHash("sha256").update(input).digest("hex");
5315
+ }
5316
+ async function sha256File(path) {
5317
+ const buf = await readFile2(path);
5318
+ return sha256Of(buf);
5319
+ }
5320
+ function getHookRecord(db, name) {
5321
+ return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
5322
+ }
5323
+ function listHookRecords(db) {
5324
+ return db.query("SELECT * FROM hooks ORDER BY name").all();
5325
+ }
5326
+ function upsertHookRecord(db, record) {
5327
+ const existing = getHookRecord(db, record.name);
5328
+ const id = existing?.id ?? record.name;
5329
+ const installedAt = existing?.installed_at ?? new Date().toISOString();
5330
+ const enabled = record.enabled ?? existing?.enabled ?? 1;
5331
+ db.run(`INSERT INTO hooks (id, name, version, sha256, source_type, source_ref, installed_at, enabled, last_verified_at)
5332
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
5333
+ ON CONFLICT(id) DO UPDATE SET
5334
+ version = excluded.version,
5335
+ sha256 = excluded.sha256,
5336
+ source_type = excluded.source_type,
5337
+ source_ref = excluded.source_ref,
5338
+ enabled = excluded.enabled,
5339
+ last_verified_at = excluded.last_verified_at`, [
5340
+ id,
5341
+ record.name,
5342
+ record.version,
5343
+ record.sha256,
5344
+ record.source_type,
5345
+ record.source_ref ?? existing?.source_ref ?? null,
5346
+ installedAt,
5347
+ enabled,
5348
+ record.last_verified_at ?? null
5349
+ ]);
5350
+ }
5351
+ function removeHookRecord(db, name) {
5352
+ const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
5353
+ return res.changes > 0;
5354
+ }
5355
+ function readLock() {
5356
+ try {
5357
+ const path = getLockPath();
5358
+ if (!existsSync9(path))
5359
+ return { hooks: {} };
5360
+ const parsed = JSON.parse(readFileSync6(path, "utf-8"));
5361
+ return { hooks: parsed.hooks ?? {} };
5362
+ } catch {
5363
+ return { hooks: {} };
5364
+ }
5365
+ }
5366
+ function writeLock(lock) {
5367
+ const path = getLockPath();
5368
+ mkdirSync6(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
5369
+ const sorted = {};
5370
+ for (const name of Object.keys(lock.hooks).sort()) {
5371
+ sorted[name] = lock.hooks[name];
5372
+ }
5373
+ writeFileSync5(path, JSON.stringify({ hooks: sorted }, null, 2) + `
5374
+ `, "utf-8");
5375
+ return path;
5376
+ }
5377
+ function setPinnedHook(name, entry) {
5378
+ const lock = readLock();
5379
+ lock.hooks[name] = entry;
5380
+ return writeLock(lock);
5381
+ }
5382
+ function getPinnedHook(name) {
5383
+ return readLock().hooks[name];
5384
+ }
5385
+ function removePinnedHook(name) {
5386
+ const lock = readLock();
5387
+ if (!(name in lock.hooks))
5388
+ return false;
5389
+ delete lock.hooks[name];
5390
+ writeLock(lock);
5391
+ return true;
5392
+ }
5393
+ function checkScriptHash(name, actual) {
5394
+ const db = getDb();
5395
+ const record = getHookRecord(db, name);
5396
+ const pin = getPinnedHook(name);
5397
+ const expected = record?.sha256 ?? pin?.sha256 ?? null;
5398
+ if (expected !== null && expected !== actual) {
5399
+ return { ok: false, pinned: true, expected, actual, name };
5400
+ }
5401
+ if (expected === null) {
5402
+ const now3 = new Date().toISOString();
5403
+ const version = pin?.version ?? record?.version ?? "0.0.0";
5404
+ const source = pin?.source ?? record?.source_type ?? "local";
5405
+ upsertHookRecord(db, {
5406
+ name,
5407
+ version,
5408
+ sha256: actual,
5409
+ source_type: source,
5410
+ last_verified_at: now3
5411
+ });
5412
+ setPinnedHook(name, { version, sha256: actual, source });
5413
+ return { ok: true, pinned: false, expected: null, actual, name };
5414
+ }
5415
+ const now2 = new Date().toISOString();
5416
+ upsertHookRecord(db, {
5417
+ name,
5418
+ version: record?.version ?? pin?.version ?? "0.0.0",
5419
+ sha256: expected,
5420
+ source_type: record?.source_type ?? pin?.source ?? "local",
5421
+ last_verified_at: now2
5422
+ });
5423
+ return { ok: true, pinned: true, expected, actual, name };
5424
+ }
5425
+ async function verifyScriptHash(name, scriptPath) {
5426
+ return checkScriptHash(name, await sha256File(scriptPath));
5427
+ }
5428
+ function retrustHook(name, scriptPath, version, source) {
5429
+ const actual = sha256Of(readFileSync6(scriptPath));
5430
+ const db = getDb();
5431
+ upsertHookRecord(db, {
5432
+ name,
5433
+ version,
5434
+ sha256: actual,
5435
+ source_type: source,
5436
+ last_verified_at: new Date().toISOString()
5437
+ });
5438
+ setPinnedHook(name, { version, sha256: actual, source });
5439
+ return { ok: true, pinned: true, expected: actual, actual, name };
5440
+ }
5441
+ var init_store = __esm(() => {
5442
+ init_db();
5443
+ init_config();
5444
+ });
5445
+
5446
+ // src/lib/run.ts
5447
+ var exports_run = {};
5448
+ __export(exports_run, {
5449
+ executeVerifiedScript: () => executeVerifiedScript
5450
+ });
5451
+ import { randomBytes } from "crypto";
5452
+ import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
5453
+ import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join9 } from "path";
5454
+ function extensionOf(scriptPath) {
5455
+ const base = basename2(scriptPath);
5456
+ const dot = base.lastIndexOf(".");
5457
+ return dot > 0 ? base.slice(dot + 1) : "ts";
5458
+ }
5459
+ async function executeVerifiedScript(options) {
5460
+ const scriptDir = dirname3(options.scriptPath);
5461
+ const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${extensionOf(options.scriptPath)}`);
5462
+ try {
5463
+ writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
5464
+ const proc = Bun.spawn(["bun", "run", isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5465
+ stdin: new Response(options.stdin),
5466
+ stdout: "pipe",
5467
+ stderr: "pipe",
5468
+ env: options.env ?? process.env,
5469
+ ...options.timeout ? { timeout: options.timeout } : {}
5470
+ });
5471
+ const [stdoutText, stderrText, exitCode] = await Promise.all([
5472
+ new Response(proc.stdout).text(),
5473
+ new Response(proc.stderr).text(),
5474
+ proc.exited
5475
+ ]);
5476
+ return { stdout: stdoutText, stderr: stderrText, exitCode };
5477
+ } finally {
5478
+ try {
5479
+ rmSync2(tempPath, { force: true });
5480
+ } catch {}
5481
+ }
5482
+ }
5483
+ var init_run = () => {};
5484
+
5485
+ // src/lib/custom-install.ts
5486
+ var exports_custom_install = {};
5487
+ __export(exports_custom_install, {
5488
+ isCustomSource: () => isCustomSource,
5489
+ installCustomSource: () => installCustomSource,
5490
+ ensureCustomDir: () => ensureCustomDir
5491
+ });
5492
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
5493
+ import { join as join10 } from "path";
5494
+ import { tmpdir } from "os";
5495
+ function manifestAt(dir) {
5496
+ const path = join10(dir, "manifest.json");
5497
+ if (!existsSync10(path))
5498
+ return;
5499
+ try {
5500
+ return parseManifest(readFileSync7(path, "utf-8"));
5501
+ } catch {
5502
+ return;
5503
+ }
5504
+ }
5505
+ function copyManifestDir(srcDir, source, kind) {
5506
+ const manifest = manifestAt(srcDir);
5507
+ if (!manifest) {
5508
+ throw new Error(`No valid manifest.json found at ${srcDir}`);
5509
+ }
5510
+ return writeFromManifest(manifest, srcDir, source, kind);
5511
+ }
5512
+ function writeFromManifest(manifest, srcDir, source, kind) {
5513
+ const script = resolveScript(manifest, srcDir);
5514
+ const name = shortManifestName(manifest.name);
5515
+ const { dir, scriptPath } = writeCustomHook(name, manifest, script.content, script.path);
5516
+ return { name, version: manifest.version, kind, source, dir, scriptPath };
5517
+ }
5518
+ async function cloneToTemp(gitUrl) {
5519
+ const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
5520
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
5521
+ stdout: "pipe",
5522
+ stderr: "pipe"
5523
+ });
5524
+ const [stdout, stderr, exitCode] = await Promise.all([
5525
+ new Response(proc.stdout).text(),
5526
+ new Response(proc.stderr).text(),
5527
+ proc.exited
5528
+ ]);
5529
+ if (exitCode !== 0) {
5530
+ rmSync3(tmp, { recursive: true, force: true });
5531
+ throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
5532
+ }
5533
+ return join10(tmp, "repo");
5534
+ }
5535
+ function isGitUrl(value) {
5536
+ return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
5537
+ }
5538
+ function isCustomSource(value) {
5539
+ if (isGitUrl(value))
5540
+ return true;
5541
+ if (value.startsWith("http://") || value.startsWith("https://"))
5542
+ return true;
5543
+ return existsSync10(value);
5544
+ }
5545
+ async function installCustomSource(source) {
5546
+ if (isGitUrl(source)) {
5547
+ const repoDir = await cloneToTemp(source);
5548
+ try {
5549
+ return copyManifestDir(repoDir, source, "git");
5550
+ } finally {
5551
+ rmSync3(repoDir, { recursive: true, force: true });
5552
+ }
5553
+ }
5554
+ if (source.startsWith("http://") || source.startsWith("https://")) {
5555
+ return installManifestUrl(source);
5556
+ }
5557
+ if (!existsSync10(source)) {
5558
+ throw new Error(`No such local path: ${source}`);
5559
+ }
5560
+ if (source.endsWith("manifest.json")) {
5561
+ const manifest = parseManifest(readFileSync7(source, "utf-8"));
5562
+ return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
5563
+ }
5564
+ return copyManifestDir(source, source, "local");
5565
+ }
5566
+ async function installManifestUrl(url) {
5567
+ const res = await fetch(url, { redirect: "follow" });
5568
+ if (!res.ok) {
5569
+ throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
5570
+ }
5571
+ const manifest = parseManifest(await res.text());
5572
+ let script;
5573
+ if (manifest.script.includes(`
5574
+ `)) {
5575
+ script = { path: "script.ts", content: manifest.script };
5576
+ } else {
5577
+ const base = url.substring(0, url.lastIndexOf("/"));
5578
+ const scriptUrl = `${base}/${manifest.script}`;
5579
+ const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
5580
+ if (!scriptRes.ok) {
5581
+ throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
5582
+ }
5583
+ script = { path: manifest.script, content: await scriptRes.text() };
5584
+ }
5585
+ const name = shortManifestName(manifest.name);
5586
+ const { dir, scriptPath } = writeCustomHook(name, manifest, script.content, script.path);
5587
+ return { name, version: manifest.version, kind: "url", source: url, dir, scriptPath };
5588
+ }
5589
+ function ensureCustomDir() {
5590
+ const dir = getCustomHooksDir();
5591
+ mkdirSync7(dir, { recursive: true });
5592
+ return dir;
5593
+ }
5594
+ var init_custom_install = __esm(() => {
5595
+ init_config();
5596
+ init_manifest();
5597
+ });
5598
+
4933
5599
  // src/db/pg-migrations.ts
4934
5600
  var PG_MIGRATIONS;
4935
5601
  var init_pg_migrations = __esm(() => {
@@ -5014,11 +5680,30 @@ function readEnv(name) {
5014
5680
  const value = process.env[name]?.trim();
5015
5681
  return value || undefined;
5016
5682
  }
5017
- function normalizeStorageMode(value) {
5018
- const normalized = value?.trim().toLowerCase();
5019
- if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
5020
- return normalized;
5021
- return;
5683
+ function assertNoRetiredModeEnv() {
5684
+ for (const name of RETIRED_STORAGE_MODE_ENV) {
5685
+ const value = readEnv(name);
5686
+ if (!value)
5687
+ continue;
5688
+ const replacement = RETIRED_MODE_REPLACEMENT[value.trim().toLowerCase()];
5689
+ const mapping = replacement ? `${value} maps to ${replacement}` : `use one of ${STORAGE_BACKENDS.join(", ")}`;
5690
+ throw new Error(`${name} is a retired deployment-mode variable and is no longer read. ` + `Hooks storage is a data-backend switch, not a deployment mode: ` + `set ${HOOKS_STORAGE_BACKEND_ENV} to ${STORAGE_BACKENDS.join(" or ")} instead (${mapping}), ` + `then unset ${name}.`);
5691
+ }
5692
+ }
5693
+ function normalizeStorageBackend(value, envName) {
5694
+ if (value === undefined)
5695
+ return;
5696
+ const normalized = value.trim().toLowerCase();
5697
+ if (normalized === "")
5698
+ return;
5699
+ const backend = BACKEND_ALIASES[normalized];
5700
+ if (backend)
5701
+ return backend;
5702
+ const replacement = RETIRED_MODE_REPLACEMENT[normalized];
5703
+ if (replacement) {
5704
+ throw new Error(`${envName}=${value} names a retired deployment mode. ` + `local/hybrid/remote/self_hosted/cloud were removed: hooks storage now selects a data ` + `backend only. Set ${envName}=${replacement} instead.`);
5705
+ }
5706
+ throw new Error(`${envName}=${value} is not a known hooks storage backend. ` + `Set ${HOOKS_STORAGE_BACKEND_ENV} to one of ${STORAGE_BACKENDS.join(", ")}.`);
5022
5707
  }
5023
5708
  function getStorageDatabaseEnvName() {
5024
5709
  for (const name of STORAGE_DATABASE_ENV) {
@@ -5035,11 +5720,12 @@ function getStorageDatabaseUrl() {
5035
5720
  const env = getStorageDatabaseEnv();
5036
5721
  return env ? readEnv(env.name) ?? null : null;
5037
5722
  }
5038
- function getStorageMode() {
5039
- const mode = normalizeStorageMode(readEnv(HOOKS_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv(HOOKS_STORAGE_MODE_FALLBACK_ENV));
5040
- if (mode)
5041
- return mode;
5042
- return getStorageDatabaseUrl() ? "hybrid" : "local";
5723
+ function getStorageBackend() {
5724
+ assertNoRetiredModeEnv();
5725
+ const backend = normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_ENV), HOOKS_STORAGE_BACKEND_ENV) ?? normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_FALLBACK_ENV), HOOKS_STORAGE_BACKEND_FALLBACK_ENV);
5726
+ if (backend)
5727
+ return backend;
5728
+ return getStorageDatabaseUrl() ? "postgresql" : "sqlite";
5043
5729
  }
5044
5730
  async function getStoragePg() {
5045
5731
  const url = getStorageDatabaseUrl();
@@ -5097,7 +5783,7 @@ function getStorageStatus() {
5097
5783
  const activeEnv = getStorageDatabaseEnv();
5098
5784
  return {
5099
5785
  configured: Boolean(activeEnv),
5100
- mode: getStorageMode(),
5786
+ backend: getStorageBackend(),
5101
5787
  env: STORAGE_DATABASE_ENV,
5102
5788
  activeEnv: activeEnv?.name ?? null,
5103
5789
  service: "hooks",
@@ -5265,7 +5951,7 @@ function coerceForSqlite(value) {
5265
5951
  return JSON.stringify(value);
5266
5952
  return String(value);
5267
5953
  }
5268
- var STORAGE_TABLES, HOOKS_STORAGE_TABLES, HOOKS_STORAGE_ENV = "HASNA_HOOKS_DATABASE_URL", HOOKS_STORAGE_FALLBACK_ENV = "HOOKS_DATABASE_URL", HOOKS_STORAGE_MODE_ENV = "HASNA_HOOKS_STORAGE_MODE", HOOKS_STORAGE_MODE_FALLBACK_ENV = "HOOKS_STORAGE_MODE", STORAGE_DATABASE_ENV, STORAGE_MODE_ENV, PRIMARY_KEYS;
5954
+ var STORAGE_TABLES, HOOKS_STORAGE_TABLES, STORAGE_BACKENDS, HOOKS_STORAGE_ENV = "HASNA_HOOKS_DATABASE_URL", HOOKS_STORAGE_FALLBACK_ENV = "HOOKS_DATABASE_URL", HOOKS_STORAGE_BACKEND_ENV = "HASNA_HOOKS_STORAGE_BACKEND", HOOKS_STORAGE_BACKEND_FALLBACK_ENV = "HOOKS_STORAGE_BACKEND", STORAGE_DATABASE_ENV, STORAGE_BACKEND_ENV, RETIRED_STORAGE_MODE_ENV, PRIMARY_KEYS, RETIRED_MODE_REPLACEMENT, BACKEND_ALIASES;
5269
5955
  var init_storage_sync = __esm(() => {
5270
5956
  init_db();
5271
5957
  init_pg_migrations();
@@ -5277,14 +5963,32 @@ var init_storage_sync = __esm(() => {
5277
5963
  "feedback"
5278
5964
  ];
5279
5965
  HOOKS_STORAGE_TABLES = STORAGE_TABLES;
5966
+ STORAGE_BACKENDS = ["sqlite", "postgresql"];
5280
5967
  STORAGE_DATABASE_ENV = [HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV];
5281
- STORAGE_MODE_ENV = [HOOKS_STORAGE_MODE_ENV, HOOKS_STORAGE_MODE_FALLBACK_ENV];
5968
+ STORAGE_BACKEND_ENV = [HOOKS_STORAGE_BACKEND_ENV, HOOKS_STORAGE_BACKEND_FALLBACK_ENV];
5969
+ RETIRED_STORAGE_MODE_ENV = ["HASNA_HOOKS_STORAGE_MODE", "HOOKS_STORAGE_MODE"];
5282
5970
  PRIMARY_KEYS = {
5283
5971
  hook_events: ["id"],
5284
5972
  schema_migrations: ["version"],
5285
5973
  _meta: ["key"],
5286
5974
  feedback: ["id"]
5287
5975
  };
5976
+ RETIRED_MODE_REPLACEMENT = {
5977
+ local: "sqlite",
5978
+ hybrid: "postgresql",
5979
+ remote: "postgresql",
5980
+ self_hosted: "postgresql",
5981
+ "self-hosted": "postgresql",
5982
+ selfhosted: "postgresql",
5983
+ cloud: "postgresql"
5984
+ };
5985
+ BACKEND_ALIASES = {
5986
+ sqlite: "sqlite",
5987
+ sqlite3: "sqlite",
5988
+ postgresql: "postgresql",
5989
+ postgres: "postgresql",
5990
+ pg: "postgresql"
5991
+ };
5288
5992
  });
5289
5993
 
5290
5994
  // src/storage.ts
@@ -5299,20 +6003,22 @@ __export(exports_storage, {
5299
6003
  getSyncMetaAll: () => getSyncMetaAll,
5300
6004
  getStorageStatus: () => getStorageStatus,
5301
6005
  getStoragePg: () => getStoragePg,
5302
- getStorageMode: () => getStorageMode,
5303
6006
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
5304
6007
  getStorageDatabaseEnvName: () => getStorageDatabaseEnvName,
5305
6008
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
6009
+ getStorageBackend: () => getStorageBackend,
5306
6010
  STORAGE_TABLES: () => STORAGE_TABLES,
5307
- STORAGE_MODE_ENV: () => STORAGE_MODE_ENV,
5308
6011
  STORAGE_DATABASE_ENV: () => STORAGE_DATABASE_ENV,
6012
+ STORAGE_BACKEND_ENV: () => STORAGE_BACKEND_ENV,
6013
+ STORAGE_BACKENDS: () => STORAGE_BACKENDS,
6014
+ RETIRED_STORAGE_MODE_ENV: () => RETIRED_STORAGE_MODE_ENV,
5309
6015
  PgAdapterAsync: () => PgAdapterAsync,
5310
6016
  PG_MIGRATIONS: () => PG_MIGRATIONS,
5311
6017
  HOOKS_STORAGE_TABLES: () => HOOKS_STORAGE_TABLES,
5312
- HOOKS_STORAGE_MODE_FALLBACK_ENV: () => HOOKS_STORAGE_MODE_FALLBACK_ENV,
5313
- HOOKS_STORAGE_MODE_ENV: () => HOOKS_STORAGE_MODE_ENV,
5314
6018
  HOOKS_STORAGE_FALLBACK_ENV: () => HOOKS_STORAGE_FALLBACK_ENV,
5315
- HOOKS_STORAGE_ENV: () => HOOKS_STORAGE_ENV
6019
+ HOOKS_STORAGE_ENV: () => HOOKS_STORAGE_ENV,
6020
+ HOOKS_STORAGE_BACKEND_FALLBACK_ENV: () => HOOKS_STORAGE_BACKEND_FALLBACK_ENV,
6021
+ HOOKS_STORAGE_BACKEND_ENV: () => HOOKS_STORAGE_BACKEND_ENV
5316
6022
  });
5317
6023
  var init_storage = __esm(() => {
5318
6024
  init_storage_sync();
@@ -5320,6 +6026,374 @@ var init_storage = __esm(() => {
5320
6026
  init_pg_migrations();
5321
6027
  });
5322
6028
 
6029
+ // src/serve.ts
6030
+ var exports_serve = {};
6031
+ __export(exports_serve, {
6032
+ startServeServer: () => startServeServer,
6033
+ handleServeRequest: () => handleServeRequest,
6034
+ SERVE_SERVICE_NAME: () => SERVE_SERVICE_NAME,
6035
+ SERVE_HOST: () => SERVE_HOST,
6036
+ DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
6037
+ });
6038
+ async function buildCatalog() {
6039
+ const byName = new Map;
6040
+ for (const meta of HOOKS) {
6041
+ const scriptPath = resolveHook(meta.name)?.scriptPath;
6042
+ if (!scriptPath)
6043
+ continue;
6044
+ byName.set(meta.name, {
6045
+ name: meta.name,
6046
+ version: meta.version,
6047
+ sha256: await sha256File(scriptPath),
6048
+ events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
6049
+ description: meta.description,
6050
+ source: "bundled"
6051
+ });
6052
+ }
6053
+ for (const custom of listCustomHooks()) {
6054
+ const name = shortManifestName(custom.manifest.name);
6055
+ byName.set(name, {
6056
+ name,
6057
+ version: custom.manifest.version,
6058
+ sha256: await sha256File(custom.scriptPath),
6059
+ events: custom.manifest.events,
6060
+ description: custom.manifest.description ?? "Custom hook",
6061
+ source: byName.has(name) ? "custom-overrides-bundled" : "custom"
6062
+ });
6063
+ }
6064
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
6065
+ }
6066
+ async function artifactFor(name, version) {
6067
+ const resolved = resolveHook(name);
6068
+ if (!resolved)
6069
+ return null;
6070
+ if (version !== resolved.version)
6071
+ return null;
6072
+ const custom = listCustomHooks().find((c) => shortManifestName(c.manifest.name) === name);
6073
+ const manifest = {
6074
+ name,
6075
+ version: resolved.version,
6076
+ description: resolved.description,
6077
+ events: resolved.events,
6078
+ script: custom ? custom.manifest.script : `src/hook.ts`,
6079
+ args: custom?.manifest.args,
6080
+ timeout_ms: custom?.manifest.timeout_ms
6081
+ };
6082
+ const script = await Bun.file(resolved.scriptPath).text();
6083
+ return { manifest, script };
6084
+ }
6085
+ function authorized(req, apiKey) {
6086
+ if (!apiKey)
6087
+ return false;
6088
+ const header = req.headers.get("authorization") ?? "";
6089
+ if (header.startsWith("Bearer "))
6090
+ return header.slice("Bearer ".length) === apiKey;
6091
+ return req.headers.get("x-api-key") === apiKey;
6092
+ }
6093
+ function json(body, status = 200, headers = {}) {
6094
+ return new Response(JSON.stringify(body), {
6095
+ status,
6096
+ headers: { "content-type": "application/json", ...headers }
6097
+ });
6098
+ }
6099
+ function handleServeRequest(req, apiKey) {
6100
+ const url = new URL(req.url);
6101
+ if (url.pathname === "/health" && req.method === "GET") {
6102
+ return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
6103
+ }
6104
+ if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
6105
+ return buildCatalog().then((catalog) => json({ hooks: catalog }));
6106
+ }
6107
+ if (url.pathname === "/api/v1/lock" && req.method === "GET") {
6108
+ return Promise.resolve(json(readLock()));
6109
+ }
6110
+ const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
6111
+ if (artifactMatch && req.method === "GET") {
6112
+ const [, name, version] = artifactMatch;
6113
+ return artifactFor(name, version).then(async (artifact) => {
6114
+ if (!artifact)
6115
+ return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
6116
+ const resolved = resolveHook(name);
6117
+ const sha = resolved ? await sha256File(resolved.scriptPath) : artifact.manifest.name;
6118
+ return json(artifact, 200, { "x-hook-sha256": sha });
6119
+ });
6120
+ }
6121
+ if (url.pathname === "/api/v1/hooks" && req.method === "PUT") {
6122
+ if (!authorized(req, apiKey)) {
6123
+ return Promise.resolve(json({ error: "unauthorized: valid API key required to publish" }, 401));
6124
+ }
6125
+ return req.json().then((body) => {
6126
+ const name = body.name ?? "";
6127
+ const version = body.version ?? "";
6128
+ const resolved = resolveHook(name);
6129
+ if (!resolved)
6130
+ return json({ error: `Hook '${name}' not found in local store` }, 404);
6131
+ if (version && version !== resolved.version) {
6132
+ return json({ error: `Version mismatch: local '${name}' is ${resolved.version}, requested ${version}` }, 409);
6133
+ }
6134
+ const check = retrustHook(name, resolved.scriptPath, resolved.version, "serve");
6135
+ return json({ ok: true, hook: { name, version: resolved.version, sha256: check.actual } });
6136
+ }, () => json({ error: "invalid JSON body" }, 400));
6137
+ }
6138
+ return Promise.resolve(json({ error: "Not Found" }, 404));
6139
+ }
6140
+ function startServeServer(options) {
6141
+ const port = options.port ?? DEFAULT_SERVE_PORT;
6142
+ const host = options.host ?? SERVE_HOST;
6143
+ const apiKey = resolveApiKey(options.apiKey);
6144
+ const server = Bun.serve({
6145
+ hostname: host,
6146
+ port,
6147
+ async fetch(req) {
6148
+ return handleServeRequest(req, apiKey);
6149
+ }
6150
+ });
6151
+ console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
6152
+ return server;
6153
+ }
6154
+ var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
6155
+ var init_serve = __esm(() => {
6156
+ init_registry();
6157
+ init_manifest();
6158
+ init_store();
6159
+ init_resolve();
6160
+ init_config();
6161
+ if (false) {}
6162
+ });
6163
+
6164
+ // src/lib/sync.ts
6165
+ var exports_sync = {};
6166
+ __export(exports_sync, {
6167
+ syncHooks: () => syncHooks,
6168
+ planSync: () => planSync
6169
+ });
6170
+ import { createHash as createHash2 } from "crypto";
6171
+ import { readFileSync as readFileSync8 } from "fs";
6172
+ function sha256OfText(text) {
6173
+ return createHash2("sha256").update(text).digest("hex");
6174
+ }
6175
+ function sha256OfPath(path) {
6176
+ return createHash2("sha256").update(readFileSync8(path)).digest("hex");
6177
+ }
6178
+ function collectBundledCatalog() {
6179
+ const out = [];
6180
+ for (const meta of HOOKS) {
6181
+ const scriptPath = resolveScriptPath(meta.name);
6182
+ if (!scriptPath)
6183
+ continue;
6184
+ out.push({
6185
+ name: meta.name,
6186
+ version: meta.version,
6187
+ sha256: sha256OfPath(scriptPath)
6188
+ });
6189
+ }
6190
+ return out;
6191
+ }
6192
+ async function fetchJson(base, path) {
6193
+ const res = await fetch(`${base}${path}`, {
6194
+ headers: { accept: "application/json" },
6195
+ signal: AbortSignal.timeout(15000)
6196
+ });
6197
+ if (!res.ok) {
6198
+ throw new Error(`GET ${path} failed with status ${res.status}`);
6199
+ }
6200
+ return res.json();
6201
+ }
6202
+ async function fetchRemoteState(apiUrl) {
6203
+ const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
6204
+ const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
6205
+ if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
6206
+ throw new Error("remote catalog is malformed: missing hooks array");
6207
+ }
6208
+ if (!remoteLock.hooks || typeof remoteLock.hooks !== "object") {
6209
+ throw new Error("remote lock is malformed: missing hooks map");
6210
+ }
6211
+ return { catalog: catalog.hooks, remoteLock };
6212
+ }
6213
+ function computeDiff(catalog, remoteLock) {
6214
+ const local = readLock();
6215
+ const diff = { added: [], updated: [], unchanged: [], skipped: [] };
6216
+ for (const entry of catalog) {
6217
+ if (!entry.name || !entry.version || !entry.sha256)
6218
+ continue;
6219
+ const pin = local.hooks[entry.name];
6220
+ const remotePin = remoteLock ? remoteLock.hooks[entry.name] : entry;
6221
+ if (remoteLock && !remotePin) {
6222
+ diff.skipped.push(entry.name);
6223
+ continue;
6224
+ }
6225
+ const pinnedLocally = pin && pin.version === entry.version && pin.sha256 === entry.sha256;
6226
+ if (pinnedLocally) {
6227
+ diff.unchanged.push(entry.name);
6228
+ } else if (pin) {
6229
+ diff.updated.push(entry.name);
6230
+ } else {
6231
+ diff.added.push(entry.name);
6232
+ }
6233
+ }
6234
+ return diff;
6235
+ }
6236
+ async function planSync() {
6237
+ const apiUrl = resolveApiUrl();
6238
+ if (apiUrl) {
6239
+ const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
6240
+ return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
6241
+ }
6242
+ const diff = computeDiff(collectBundledCatalog(), null);
6243
+ return { apiUrl: null, dryRun: false, diff };
6244
+ }
6245
+ async function syncHooks(options = {}) {
6246
+ const apiUrl = resolveApiUrl();
6247
+ const plan = await planSync();
6248
+ if (options.dryRun)
6249
+ return { ...plan, dryRun: true };
6250
+ if (!apiUrl) {
6251
+ const db2 = getDb();
6252
+ for (const entry of collectBundledCatalog()) {
6253
+ setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
6254
+ upsertHookRecord(db2, {
6255
+ name: entry.name,
6256
+ version: entry.version,
6257
+ sha256: entry.sha256,
6258
+ source_type: "bundled",
6259
+ source_ref: "apps/hooks/hooks",
6260
+ last_verified_at: new Date().toISOString()
6261
+ });
6262
+ }
6263
+ return plan;
6264
+ }
6265
+ const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
6266
+ const diff = computeDiff(catalog, remoteLock);
6267
+ const toFetch = [...diff.added, ...diff.updated];
6268
+ const artifacts = new Map;
6269
+ for (const name of toFetch) {
6270
+ const entry = remoteLock.hooks[name];
6271
+ if (!entry)
6272
+ continue;
6273
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
6274
+ if (!artifact.manifest || typeof artifact.script !== "string") {
6275
+ throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
6276
+ }
6277
+ const actualSha = sha256OfText(artifact.script);
6278
+ if (actualSha !== entry.sha256) {
6279
+ throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
6280
+ }
6281
+ artifacts.set(name, artifact);
6282
+ }
6283
+ const db = getDb();
6284
+ for (const name of toFetch) {
6285
+ const artifact = artifacts.get(name);
6286
+ const entry = remoteLock.hooks[name];
6287
+ if (!artifact || !entry)
6288
+ continue;
6289
+ const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
6290
+ const scriptRel = artifact.manifest.script.includes(`
6291
+ `) ? "script.ts" : artifact.manifest.script;
6292
+ writeCustomHook(name, manifest, artifact.script, scriptRel);
6293
+ const scriptPath = resolveScriptPath(name);
6294
+ const actual = await sha256File(scriptPath);
6295
+ setPinnedHook(name, { version: entry.version, sha256: actual, source: entry.source ?? "remote" });
6296
+ upsertHookRecord(db, {
6297
+ name,
6298
+ version: entry.version,
6299
+ sha256: actual,
6300
+ source_type: entry.source ?? "remote",
6301
+ source_ref: apiUrl,
6302
+ last_verified_at: new Date().toISOString()
6303
+ });
6304
+ }
6305
+ return { apiUrl, dryRun: false, diff };
6306
+ }
6307
+ var init_sync = __esm(() => {
6308
+ init_registry();
6309
+ init_config();
6310
+ init_db();
6311
+ init_store();
6312
+ init_manifest();
6313
+ init_resolve();
6314
+ });
6315
+
6316
+ // src/cf/provision.ts
6317
+ var exports_provision = {};
6318
+ __export(exports_provision, {
6319
+ provisionCloudflareResources: () => provisionCloudflareResources
6320
+ });
6321
+ async function cfFetch(fetchFn, token, path, init) {
6322
+ const res = await fetchFn(`${CF_API}${path}`, {
6323
+ ...init,
6324
+ headers: {
6325
+ authorization: `Bearer ${token}`,
6326
+ "content-type": "application/json",
6327
+ ...init?.headers ?? {}
6328
+ }
6329
+ });
6330
+ const data = await res.json().catch(() => null);
6331
+ return { ok: res.ok, status: res.status, data };
6332
+ }
6333
+ async function provisionCloudflareResources(options) {
6334
+ const { token, accountId, databaseName, bucketName, dryRun, fetchFn = fetch } = options;
6335
+ const result = {
6336
+ d1DatabaseId: null,
6337
+ d1Created: false,
6338
+ d1Exists: false,
6339
+ r2BucketExists: false,
6340
+ r2Created: false,
6341
+ commands: []
6342
+ };
6343
+ if (dryRun) {
6344
+ result.commands = [
6345
+ `POST ${CF_API}/accounts/${accountId}/d1/database {"name": "${databaseName}"} (if missing)`,
6346
+ `PUT ${CF_API}/accounts/${accountId}/r2/buckets/${bucketName} (if missing)`,
6347
+ `wrangler d1 migrations apply ${databaseName} --remote`,
6348
+ `wrangler deploy --config wrangler.toml`
6349
+ ];
6350
+ return result;
6351
+ }
6352
+ const d1List = await cfFetch(fetchFn, token, `/accounts/${accountId}/d1/database?name=${encodeURIComponent(databaseName)}`);
6353
+ if (!d1List.ok) {
6354
+ throw new Error(`failed to list D1 databases (${d1List.status})`);
6355
+ }
6356
+ const d1Rows = d1List.data?.result ?? [];
6357
+ if (d1Rows.length > 0) {
6358
+ result.d1Exists = true;
6359
+ result.d1DatabaseId = d1Rows[0].id;
6360
+ } else {
6361
+ const created = await cfFetch(fetchFn, token, `/accounts/${accountId}/d1/database`, {
6362
+ method: "POST",
6363
+ body: JSON.stringify({ name: databaseName })
6364
+ });
6365
+ if (!created.ok) {
6366
+ throw new Error(`failed to create D1 database '${databaseName}' (${created.status})`);
6367
+ }
6368
+ result.d1Created = true;
6369
+ result.d1DatabaseId = created.data?.result?.id ?? null;
6370
+ }
6371
+ const r2List = await cfFetch(fetchFn, token, `/accounts/${accountId}/r2/buckets`);
6372
+ if (!r2List.ok) {
6373
+ throw new Error(`failed to list R2 buckets (${r2List.status})`);
6374
+ }
6375
+ const bucketNames = (r2List.data?.result?.buckets ?? []).map((b) => b.name);
6376
+ if (bucketNames.includes(bucketName)) {
6377
+ result.r2BucketExists = true;
6378
+ } else {
6379
+ const created = await cfFetch(fetchFn, token, `/accounts/${accountId}/r2/buckets/${bucketName}`, {
6380
+ method: "PUT",
6381
+ body: JSON.stringify({ name: bucketName })
6382
+ });
6383
+ if (!created.ok) {
6384
+ throw new Error(`failed to create R2 bucket '${bucketName}' (${created.status})`);
6385
+ }
6386
+ result.r2Created = true;
6387
+ }
6388
+ result.commands = [
6389
+ `wrangler d1 migrations apply ${databaseName} --remote`,
6390
+ `wrangler secret put HOOKS_API_KEY`,
6391
+ `wrangler deploy --config wrangler.toml`
6392
+ ];
6393
+ return result;
6394
+ }
6395
+ var CF_API = "https://api.cloudflare.com/client/v4";
6396
+
5323
6397
  // src/mcp/server.ts
5324
6398
  var exports_server = {};
5325
6399
  __export(exports_server, {
@@ -5331,11 +6405,20 @@ __export(exports_server, {
5331
6405
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5332
6406
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5333
6407
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5334
- import { z } from "zod";
6408
+ import { z as z2 } from "zod";
5335
6409
  import { createServer } from "http";
5336
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
5337
- import { join as join6, dirname as dirname2 } from "path";
6410
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
6411
+ import { join as join11, dirname as dirname4 } from "path";
5338
6412
  import { fileURLToPath as fileURLToPath2 } from "url";
6413
+ async function runVerifiedHook(name, scriptPath, stdin, timeoutMs) {
6414
+ const content = readFileSync9(scriptPath);
6415
+ const check = checkScriptHash(name, sha256Of(content));
6416
+ if (!check.ok) {
6417
+ throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
6418
+ }
6419
+ const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
6420
+ return executeVerifiedScript2({ name, scriptPath, content, stdin, env: process.env, timeout: timeoutMs });
6421
+ }
5339
6422
  function formatInstallResults(results, extra) {
5340
6423
  const installed = results.filter((r) => r.success).map((r) => r.hook);
5341
6424
  const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
@@ -5394,9 +6477,9 @@ function createHooksServer() {
5394
6477
  });
5395
6478
  const defineTool = (name, description, schema, handler) => server.tool(name, description, schema, handler);
5396
6479
  defineTool("hooks_list", "List available hooks. Compact by default; pass compact:false for full HookMeta objects.", {
5397
- category: z.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security')"),
5398
- compact: z.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
5399
- limit: z.number().default(25).describe("Max compact rows to return")
6480
+ category: z2.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security')"),
6481
+ compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
6482
+ limit: z2.number().default(25).describe("Max compact rows to return")
5400
6483
  }, async ({ category, compact, limit }) => {
5401
6484
  const maxRows = boundedLimit(limit, 25, 100);
5402
6485
  if (category) {
@@ -5414,15 +6497,15 @@ function createHooksServer() {
5414
6497
  return { content: [{ type: "text", text: JSON.stringify(compact ? compactHookResult(HOOKS, maxRows, "hooks_info") : result) }] };
5415
6498
  });
5416
6499
  defineTool("hooks_search", "Search hooks. Compact by default; pass compact:false for full HookMeta objects.", {
5417
- query: z.string().describe("Search query"),
5418
- compact: z.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
5419
- limit: z.number().default(10).describe("Max compact rows to return")
6500
+ query: z2.string().describe("Search query"),
6501
+ compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
6502
+ limit: z2.number().default(10).describe("Max compact rows to return")
5420
6503
  }, async ({ query, compact, limit }) => {
5421
6504
  const results = searchHooks(query);
5422
6505
  const out = compact ? compactHookResult(results, boundedLimit(limit, 10, 100), "hooks_info") : results;
5423
6506
  return { content: [{ type: "text", text: JSON.stringify(out) }] };
5424
6507
  });
5425
- defineTool("hooks_info", "Get detailed information about a specific hook including install status", { name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')") }, async ({ name }) => {
6508
+ defineTool("hooks_info", "Get detailed information about a specific hook including install status", { name: z2.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')") }, async ({ name }) => {
5426
6509
  const meta = getHook(name);
5427
6510
  if (!meta) {
5428
6511
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
@@ -5432,18 +6515,18 @@ function createHooksServer() {
5432
6515
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }) }] };
5433
6516
  });
5434
6517
  defineTool("hooks_install", "Install one or more hooks by registering them in agent settings", {
5435
- hooks: z.array(z.string()).describe("Hook names to install"),
5436
- scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
5437
- overwrite: z.boolean().default(false).describe("Overwrite if already installed"),
5438
- profile: z.string().optional().describe("Agent profile ID to scope hooks to")
6518
+ hooks: z2.array(z2.string()).describe("Hook names to install"),
6519
+ scope: z2.enum(["global", "project"]).default("global").describe("Install scope"),
6520
+ overwrite: z2.boolean().default(false).describe("Overwrite if already installed"),
6521
+ profile: z2.string().optional().describe("Agent profile ID to scope hooks to")
5439
6522
  }, async ({ hooks, scope, overwrite, profile }) => {
5440
6523
  const results = hooks.map((name) => installHook(name, { scope, overwrite, profile }));
5441
6524
  return formatInstallResults(results, { scope, profile });
5442
6525
  });
5443
6526
  defineTool("hooks_install_category", "Install all hooks in a category", {
5444
- category: z.string().describe("Category name"),
5445
- scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
5446
- overwrite: z.boolean().default(false).describe("Overwrite if already installed")
6527
+ category: z2.string().describe("Category name"),
6528
+ scope: z2.enum(["global", "project"]).default("global").describe("Install scope"),
6529
+ overwrite: z2.boolean().default(false).describe("Overwrite if already installed")
5447
6530
  }, async ({ category, scope, overwrite }) => {
5448
6531
  const cat = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
5449
6532
  if (!cat) {
@@ -5454,45 +6537,53 @@ function createHooksServer() {
5454
6537
  return formatInstallResults(results, { category: cat, scope });
5455
6538
  });
5456
6539
  defineTool("hooks_install_all", "Install all available hooks", {
5457
- scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
5458
- overwrite: z.boolean().default(false).describe("Overwrite if already installed")
6540
+ scope: z2.enum(["global", "project"]).default("global").describe("Install scope"),
6541
+ overwrite: z2.boolean().default(false).describe("Overwrite if already installed")
5459
6542
  }, async ({ scope, overwrite }) => {
5460
6543
  const results = HOOKS.map((h) => installHook(h.name, { scope, overwrite }));
5461
6544
  return formatInstallResults(results, { scope });
5462
6545
  });
5463
6546
  defineTool("hooks_remove", "Remove (unregister) a hook from agent settings", {
5464
- name: z.string().describe("Hook name to remove"),
5465
- scope: z.enum(["global", "project"]).default("global").describe("Scope to remove from")
6547
+ name: z2.string().describe("Hook name to remove"),
6548
+ scope: z2.enum(["global", "project"]).default("global").describe("Scope to remove from")
5466
6549
  }, async ({ name, scope }) => {
5467
6550
  const removed = removeHook(name, scope);
5468
6551
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, removed, scope }) }] };
5469
6552
  });
5470
6553
  defineTool("hooks_doctor", "Check health of installed hooks \u2014 verifies hook source exists, settings are correct", {
5471
- scope: z.enum(["global", "project"]).default("global").describe("Scope to check")
6554
+ scope: z2.enum(["global", "project"]).default("global").describe("Scope to check")
5472
6555
  }, async ({ scope }) => {
5473
6556
  const settingsPath = getSettingsPath(scope);
5474
6557
  const issues = [];
5475
6558
  const healthy = [];
5476
- const settingsExist = existsSync6(settingsPath);
6559
+ const settingsExist = existsSync11(settingsPath);
5477
6560
  if (!settingsExist) {
5478
6561
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
5479
6562
  }
5480
6563
  const registered = getRegisteredHooks(scope);
5481
6564
  for (const name of registered) {
5482
- const meta = getHook(name);
6565
+ const custom = readCustomManifest(name);
6566
+ const meta = custom ? resolveHookMeta(name) : getHook(name);
5483
6567
  let hookHealthy = true;
5484
- if (!hookExists(name)) {
5485
- issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
5486
- continue;
5487
- }
5488
- const hookDir = getHookPath(name);
5489
- if (!existsSync6(join6(hookDir, "src", "hook.ts"))) {
5490
- issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
5491
- hookHealthy = false;
6568
+ if (custom) {
6569
+ if (!existsSync11(custom.scriptPath)) {
6570
+ issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
6571
+ hookHealthy = false;
6572
+ }
6573
+ } else {
6574
+ if (!hookExists(name)) {
6575
+ issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
6576
+ continue;
6577
+ }
6578
+ const hookDir = getHookPath(name);
6579
+ if (!existsSync11(join11(hookDir, "src", "hook.ts"))) {
6580
+ issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
6581
+ hookHealthy = false;
6582
+ }
5492
6583
  }
5493
6584
  if (meta && settingsExist) {
5494
6585
  try {
5495
- const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
6586
+ const settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
5496
6587
  const eventHooks = settings.hooks?.[meta.event] || [];
5497
6588
  const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
5498
6589
  const match = h.command?.match(/^hooks run ([\w-]+)/);
@@ -5517,8 +6608,8 @@ function createHooksServer() {
5517
6608
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
5518
6609
  });
5519
6610
  defineTool("hooks_docs", "Get documentation. Hook README content is summarized by default; pass verbose:true for the full README.", {
5520
- name: z.string().optional().describe("Hook name for specific docs, omit for general docs"),
5521
- verbose: z.boolean().default(false).describe("Return full hook README when true")
6611
+ name: z2.string().optional().describe("Hook name for specific docs, omit for general docs"),
6612
+ verbose: z2.boolean().default(false).describe("Return full hook README when true")
5522
6613
  }, async ({ name, verbose }) => {
5523
6614
  if (name) {
5524
6615
  const meta = getHook(name);
@@ -5526,10 +6617,10 @@ function createHooksServer() {
5526
6617
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
5527
6618
  }
5528
6619
  const hookPath = getHookPath(name);
5529
- const readmePath = join6(hookPath, "README.md");
6620
+ const readmePath = join11(hookPath, "README.md");
5530
6621
  let readme = "";
5531
- if (existsSync6(readmePath)) {
5532
- readme = readFileSync4(readmePath, "utf-8");
6622
+ if (existsSync11(readmePath)) {
6623
+ readme = readFileSync9(readmePath, "utf-8");
5533
6624
  }
5534
6625
  if (verbose) {
5535
6626
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
@@ -5577,9 +6668,9 @@ function createHooksServer() {
5577
6668
  };
5578
6669
  });
5579
6670
  defineTool("hooks_registered", "Get currently registered hooks for a scope. Compact by default.", {
5580
- scope: z.enum(["global", "project"]).default("global").describe("Scope to check"),
5581
- compact: z.boolean().default(true).describe("Return compact summaries by default. Set false for descriptions."),
5582
- limit: z.number().default(25).describe("Max compact rows to return")
6671
+ scope: z2.enum(["global", "project"]).default("global").describe("Scope to check"),
6672
+ compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for descriptions."),
6673
+ limit: z2.number().default(25).describe("Max compact rows to return")
5583
6674
  }, async ({ scope, compact, limit }) => {
5584
6675
  const registered = getRegisteredHooks(scope);
5585
6676
  const result = registered.map((name) => {
@@ -5609,18 +6700,18 @@ function createHooksServer() {
5609
6700
  };
5610
6701
  });
5611
6702
  defineTool("hooks_run", "Execute a hook programmatically with the given input and return its output", {
5612
- name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')"),
5613
- input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Hook input as JSON object (HookInput)"),
5614
- profile: z.string().optional().describe("Agent profile ID to inject into hook input"),
5615
- timeout_ms: z.number().default(1e4).describe("Timeout in milliseconds (default: 10000)")
6703
+ name: z2.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')"),
6704
+ input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input as JSON object (HookInput)"),
6705
+ profile: z2.string().optional().describe("Agent profile ID to inject into hook input"),
6706
+ timeout_ms: z2.number().default(1e4).describe("Timeout in milliseconds (default: 10000)")
5616
6707
  }, async ({ name, input, profile, timeout_ms }) => {
5617
6708
  const meta = getHook(name);
5618
6709
  if (!meta) {
5619
6710
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
5620
6711
  }
5621
6712
  const hookDir = getHookPath(name);
5622
- const hookScript = join6(hookDir, "src", "hook.ts");
5623
- if (!existsSync6(hookScript)) {
6713
+ const hookScript = join11(hookDir, "src", "hook.ts");
6714
+ if (!existsSync11(hookScript)) {
5624
6715
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
5625
6716
  }
5626
6717
  let hookInput = { ...input };
@@ -5635,29 +6726,28 @@ function createHooksServer() {
5635
6726
  };
5636
6727
  }
5637
6728
  }
5638
- const proc = Bun.spawn(["bun", "run", hookScript], {
5639
- stdin: new Response(JSON.stringify(hookInput)),
5640
- stdout: "pipe",
5641
- stderr: "pipe",
5642
- env: process.env
5643
- });
5644
- const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), timeout_ms));
5645
- const result = await Promise.race([
5646
- Promise.all([
5647
- new Response(proc.stdout).text(),
5648
- new Response(proc.stderr).text(),
5649
- proc.exited
5650
- ]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode, timedOut: false })),
5651
- timeoutPromise.then(() => {
5652
- proc.kill();
5653
- return { stdout: "", stderr: "", exitCode: -1, timedOut: true };
5654
- })
5655
- ]);
6729
+ let ran = null;
6730
+ try {
6731
+ ran = await Promise.race([
6732
+ runVerifiedHook(name, hookScript, JSON.stringify(hookInput), timeout_ms),
6733
+ new Promise((resolve2) => setTimeout(() => resolve2(null), timeout_ms))
6734
+ ]);
6735
+ } catch (err) {
6736
+ return {
6737
+ content: [{
6738
+ type: "text",
6739
+ text: JSON.stringify({ hook: name, error: err instanceof Error ? err.message : String(err), trust_failed: true })
6740
+ }]
6741
+ };
6742
+ }
6743
+ const timedOut = ran === null;
6744
+ const stdoutText = ran?.stdout ?? "";
6745
+ const stderrText = ran?.stderr ?? "";
5656
6746
  let output = {};
5657
6747
  try {
5658
- output = JSON.parse(result.stdout);
6748
+ output = JSON.parse(stdoutText);
5659
6749
  } catch {
5660
- output = result.stdout ? { raw: result.stdout } : {};
6750
+ output = stdoutText ? { raw: stdoutText } : {};
5661
6751
  }
5662
6752
  return {
5663
6753
  content: [{
@@ -5665,16 +6755,16 @@ function createHooksServer() {
5665
6755
  text: JSON.stringify({
5666
6756
  hook: name,
5667
6757
  output,
5668
- stderr: result.stderr || undefined,
5669
- exitCode: result.exitCode,
5670
- ...result.timedOut ? { timedOut: true, timeout_ms } : {}
6758
+ stderr: stderrText || undefined,
6759
+ exitCode: ran?.exitCode ?? -1,
6760
+ ...timedOut ? { timedOut: true, timeout_ms } : {}
5671
6761
  })
5672
6762
  }]
5673
6763
  };
5674
6764
  });
5675
6765
  defineTool("hooks_update", "Re-register installed hooks to pick up new package version (reinstalls with overwrite)", {
5676
- hooks: z.array(z.string()).optional().describe("Hook names to update (omit to update all installed hooks)"),
5677
- scope: z.enum(["global", "project"]).default("global").describe("Scope to update")
6766
+ hooks: z2.array(z2.string()).optional().describe("Hook names to update (omit to update all installed hooks)"),
6767
+ scope: z2.enum(["global", "project"]).default("global").describe("Scope to update")
5678
6768
  }, async ({ hooks, scope }) => {
5679
6769
  const installed = getRegisteredHooks(scope);
5680
6770
  const toUpdate = hooks && hooks.length > 0 ? hooks : installed;
@@ -5692,9 +6782,9 @@ function createHooksServer() {
5692
6782
  return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
5693
6783
  });
5694
6784
  defineTool("hooks_context", "Get compact agent context in one call: installed hooks, active profile summary, settings path, and doctor status.", {
5695
- scope: z.enum(["global", "project"]).default("global").describe("Scope to inspect"),
5696
- profile: z.string().optional().describe("Agent profile ID to include in context"),
5697
- verbose: z.boolean().default(false).describe("Include full profile preferences/details when true")
6785
+ scope: z2.enum(["global", "project"]).default("global").describe("Scope to inspect"),
6786
+ profile: z2.string().optional().describe("Agent profile ID to include in context"),
6787
+ verbose: z2.boolean().default(false).describe("Include full profile preferences/details when true")
5698
6788
  }, async ({ scope, profile, verbose }) => {
5699
6789
  const settingsPath = getSettingsPath(scope);
5700
6790
  const registered = getRegisteredHooks(scope);
@@ -5712,7 +6802,7 @@ function createHooksServer() {
5712
6802
  const ctx = {
5713
6803
  scope,
5714
6804
  settings_path: settingsPath,
5715
- settings_exists: existsSync6(settingsPath),
6805
+ settings_exists: existsSync11(settingsPath),
5716
6806
  registered_hooks: hooks,
5717
6807
  hook_count: hooks.length,
5718
6808
  healthy,
@@ -5728,10 +6818,10 @@ function createHooksServer() {
5728
6818
  return { content: [{ type: "text", text: JSON.stringify(ctx) }] };
5729
6819
  });
5730
6820
  defineTool("hooks_preview", "Simulate which installed PreToolUse hooks would fire for a given tool call and what decision each returns. Use this to understand your hook environment before taking an action.", {
5731
- tool_name: z.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
5732
- tool_input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
5733
- scope: z.enum(["global", "project"]).default("global").describe("Scope to check"),
5734
- timeout_ms: z.number().default(5000).describe("Per-hook timeout in milliseconds")
6821
+ tool_name: z2.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
6822
+ tool_input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
6823
+ scope: z2.enum(["global", "project"]).default("global").describe("Scope to check"),
6824
+ timeout_ms: z2.number().default(5000).describe("Per-hook timeout in milliseconds")
5735
6825
  }, async ({ tool_name, tool_input, scope, timeout_ms }) => {
5736
6826
  const registered = getRegisteredHooks(scope);
5737
6827
  const matchingHooks = registered.filter((name) => {
@@ -5752,28 +6842,23 @@ function createHooksServer() {
5752
6842
  const input = { tool_name, tool_input };
5753
6843
  const results = await Promise.all(matchingHooks.map(async (name) => {
5754
6844
  const hookDir = getHookPath(name);
5755
- const hookScript = join6(hookDir, "src", "hook.ts");
5756
- if (!existsSync6(hookScript))
6845
+ const hookScript = join11(hookDir, "src", "hook.ts");
6846
+ if (!existsSync11(hookScript))
5757
6847
  return { name, decision: "approve", error: "script not found" };
5758
- const proc = Bun.spawn(["bun", "run", hookScript], {
5759
- stdin: new Response(JSON.stringify(input)),
5760
- stdout: "pipe",
5761
- stderr: "pipe",
5762
- env: process.env
5763
- });
5764
- const timeout = new Promise((r) => setTimeout(() => r(null), timeout_ms));
5765
- const res = await Promise.race([
5766
- Promise.all([new Response(proc.stdout).text(), proc.exited]).then(([stdout]) => ({ stdout, timedOut: false })),
5767
- timeout.then(() => {
5768
- proc.kill();
5769
- return { stdout: "", timedOut: true };
5770
- })
5771
- ]);
5772
- if (res.timedOut)
6848
+ let ran = null;
6849
+ try {
6850
+ ran = await Promise.race([
6851
+ runVerifiedHook(name, hookScript, JSON.stringify(input), timeout_ms),
6852
+ new Promise((r) => setTimeout(() => r(null), timeout_ms))
6853
+ ]);
6854
+ } catch (err) {
6855
+ return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
6856
+ }
6857
+ if (ran === null)
5773
6858
  return { name, decision: "approve", timedOut: true };
5774
6859
  let output = {};
5775
6860
  try {
5776
- output = JSON.parse(res.stdout);
6861
+ output = JSON.parse(ran.stdout);
5777
6862
  } catch {}
5778
6863
  return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
5779
6864
  }));
@@ -5793,10 +6878,10 @@ function createHooksServer() {
5793
6878
  };
5794
6879
  });
5795
6880
  defineTool("hooks_setup", "Single-shot agent onboarding: create an agent profile + install recommended hooks in one call. Ideal for agents setting up hooks at session start.", {
5796
- agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
5797
- name: z.string().optional().describe("Optional display name for the agent"),
5798
- hooks: z.array(z.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
5799
- scope: z.enum(["global", "project"]).default("global").describe("Install scope")
6881
+ agent_type: z2.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
6882
+ name: z2.string().optional().describe("Optional display name for the agent"),
6883
+ hooks: z2.array(z2.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
6884
+ scope: z2.enum(["global", "project"]).default("global").describe("Install scope")
5800
6885
  }, async ({ agent_type, name, hooks, scope }) => {
5801
6886
  const profile = createProfile({ agent_type, name });
5802
6887
  const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
@@ -5811,52 +6896,49 @@ function createHooksServer() {
5811
6896
  };
5812
6897
  });
5813
6898
  defineTool("hooks_batch_run", "Run multiple hooks in parallel in a single call. Returns all results at once \u2014 more efficient than N separate hooks_run calls.", {
5814
- hooks: z.array(z.object({
5815
- name: z.string().describe("Hook name"),
5816
- input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Hook input JSON")
6899
+ hooks: z2.array(z2.object({
6900
+ name: z2.string().describe("Hook name"),
6901
+ input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input JSON")
5817
6902
  })).describe("List of hooks to run with their inputs"),
5818
- timeout_ms: z.number().default(1e4).describe("Per-hook timeout in milliseconds")
6903
+ timeout_ms: z2.number().default(1e4).describe("Per-hook timeout in milliseconds")
5819
6904
  }, async ({ hooks, timeout_ms }) => {
5820
6905
  const results = await Promise.all(hooks.map(async ({ name, input }) => {
5821
6906
  const meta = getHook(name);
5822
6907
  if (!meta)
5823
6908
  return { name, error: `Hook '${name}' not found` };
5824
- const hookScript = join6(getHookPath(name), "src", "hook.ts");
5825
- if (!existsSync6(hookScript))
6909
+ const hookScript = join11(getHookPath(name), "src", "hook.ts");
6910
+ if (!existsSync11(hookScript))
5826
6911
  return { name, error: "script not found" };
5827
- const proc = Bun.spawn(["bun", "run", hookScript], {
5828
- stdin: new Response(JSON.stringify(input)),
5829
- stdout: "pipe",
5830
- stderr: "pipe",
5831
- env: process.env
5832
- });
5833
- const timeout = new Promise((r) => setTimeout(() => r(null), timeout_ms));
5834
- const res = await Promise.race([
5835
- Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode, timedOut: false })),
5836
- timeout.then(() => {
5837
- proc.kill();
5838
- return { stdout: "", stderr: "", exitCode: -1, timedOut: true };
5839
- })
5840
- ]);
6912
+ let ran = null;
6913
+ try {
6914
+ ran = await Promise.race([
6915
+ runVerifiedHook(name, hookScript, JSON.stringify(input), timeout_ms),
6916
+ new Promise((r) => setTimeout(() => r(null), timeout_ms))
6917
+ ]);
6918
+ } catch (err) {
6919
+ return { name, error: err instanceof Error ? err.message : String(err) };
6920
+ }
6921
+ const stdoutText = ran?.stdout ?? "";
6922
+ const timedOut = ran === null;
5841
6923
  let output = {};
5842
6924
  try {
5843
- output = JSON.parse(res.stdout);
6925
+ output = JSON.parse(stdoutText);
5844
6926
  } catch {
5845
- output = res.stdout ? { raw: res.stdout } : {};
6927
+ output = stdoutText ? { raw: stdoutText } : {};
5846
6928
  }
5847
- return { name, output, exitCode: res.exitCode, ...res.timedOut ? { timedOut: true } : {} };
6929
+ return { name, output, exitCode: ran?.exitCode ?? -1, ...timedOut ? { timedOut: true } : {} };
5848
6930
  }));
5849
6931
  return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }) }] };
5850
6932
  });
5851
6933
  defineTool("hooks_disable", "Temporarily disable a registered hook without removing it. Stores disabled list in settings under hooks.__disabled.", {
5852
- name: z.string().describe("Hook name to disable"),
5853
- scope: z.enum(["global", "project"]).default("global").describe("Scope")
6934
+ name: z2.string().describe("Hook name to disable"),
6935
+ scope: z2.enum(["global", "project"]).default("global").describe("Scope")
5854
6936
  }, async ({ name, scope }) => {
5855
6937
  const settingsPath = getSettingsPath(scope);
5856
6938
  let settings = {};
5857
6939
  try {
5858
- if (existsSync6(settingsPath))
5859
- settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
6940
+ if (existsSync11(settingsPath))
6941
+ settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
5860
6942
  } catch {}
5861
6943
  if (!settings.hooks)
5862
6944
  settings.hooks = {};
@@ -5864,43 +6946,43 @@ function createHooksServer() {
5864
6946
  if (!disabled.includes(name))
5865
6947
  disabled.push(name);
5866
6948
  settings.hooks.__disabled = disabled;
5867
- const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4 } = await import("fs");
5868
- const { dirname: dirname3 } = await import("path");
5869
- mkdirSync4(dirname3(settingsPath), { recursive: true });
5870
- writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
6949
+ const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
6950
+ const { dirname: dirname5 } = await import("path");
6951
+ mkdirSync8(dirname5(settingsPath), { recursive: true });
6952
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
5871
6953
  `);
5872
6954
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
5873
6955
  });
5874
6956
  defineTool("hooks_enable", "Re-enable a previously disabled hook.", {
5875
- name: z.string().describe("Hook name to enable"),
5876
- scope: z.enum(["global", "project"]).default("global").describe("Scope")
6957
+ name: z2.string().describe("Hook name to enable"),
6958
+ scope: z2.enum(["global", "project"]).default("global").describe("Scope")
5877
6959
  }, async ({ name, scope }) => {
5878
6960
  const settingsPath = getSettingsPath(scope);
5879
6961
  let settings = {};
5880
6962
  try {
5881
- if (existsSync6(settingsPath))
5882
- settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
6963
+ if (existsSync11(settingsPath))
6964
+ settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
5883
6965
  } catch {}
5884
6966
  if (settings.hooks?.__disabled) {
5885
6967
  settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
5886
6968
  if (settings.hooks.__disabled.length === 0)
5887
6969
  delete settings.hooks.__disabled;
5888
- const { writeFileSync: writeFileSync3 } = await import("fs");
5889
- writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
6970
+ const { writeFileSync: writeFileSync7 } = await import("fs");
6971
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
5890
6972
  `);
5891
6973
  }
5892
6974
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
5893
6975
  });
5894
6976
  defineTool("hooks_init", "Register a new agent profile \u2014 returns a unique agent_id for use with hook installation and execution", {
5895
- agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
5896
- name: z.string().optional().describe("Optional display name for the agent")
6977
+ agent_type: z2.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
6978
+ name: z2.string().optional().describe("Optional display name for the agent")
5897
6979
  }, async ({ agent_type, name }) => {
5898
6980
  const profile = createProfile({ agent_type, name });
5899
6981
  return { content: [{ type: "text", text: JSON.stringify(profile) }] };
5900
6982
  });
5901
6983
  defineTool("hooks_profiles", "List registered agent profiles. Compact by default.", {
5902
- compact: z.boolean().default(true).describe("Return compact summaries by default. Set false for full profiles."),
5903
- limit: z.number().default(25).describe("Max compact rows to return")
6984
+ compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for full profiles."),
6985
+ limit: z2.number().default(25).describe("Max compact rows to return")
5904
6986
  }, async ({ compact, limit }) => {
5905
6987
  const profiles = listProfiles();
5906
6988
  if (!compact)
@@ -5926,11 +7008,11 @@ function createHooksServer() {
5926
7008
  };
5927
7009
  });
5928
7010
  defineTool("hooks_log_list", "List hook events from SQLite. Compact summaries by default; set compact:false for full event rows.", {
5929
- hook_name: z.string().optional().describe("Filter by hook name (e.g. 'sessionlog', 'costwatch')"),
5930
- session_id: z.string().optional().describe("Filter by session ID prefix"),
5931
- limit: z.number().optional().describe("Max number of events to return. Defaults to 20 compact rows or 50 full rows."),
5932
- since: z.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from"),
5933
- compact: z.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
7011
+ hook_name: z2.string().optional().describe("Filter by hook name (e.g. 'sessionlog', 'costwatch')"),
7012
+ session_id: z2.string().optional().describe("Filter by session ID prefix"),
7013
+ limit: z2.number().optional().describe("Max number of events to return. Defaults to 20 compact rows or 50 full rows."),
7014
+ since: z2.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from"),
7015
+ compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
5934
7016
  }, async ({ hook_name, session_id, limit, since, compact }) => {
5935
7017
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
5936
7018
  const db = getDb2();
@@ -5976,8 +7058,8 @@ function createHooksServer() {
5976
7058
  };
5977
7059
  });
5978
7060
  defineTool("hooks_log_tail", "Show recent hook events from SQLite. Compact summaries by default.", {
5979
- n: z.number().default(20).describe("Number of most recent events to return"),
5980
- compact: z.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
7061
+ n: z2.number().default(20).describe("Number of most recent events to return"),
7062
+ compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
5981
7063
  }, async ({ n, compact }) => {
5982
7064
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
5983
7065
  const db = getDb2();
@@ -5996,9 +7078,9 @@ function createHooksServer() {
5996
7078
  };
5997
7079
  });
5998
7080
  defineTool("hooks_log_errors", "Show hook events that contain errors. Compact summaries by default.", {
5999
- since: z.string().default("24h").describe("Duration string (e.g. '1h', '30m', '7d') or ISO timestamp"),
6000
- limit: z.number().optional().describe("Max number of error events to return. Defaults to 20 compact rows or 50 full rows."),
6001
- compact: z.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
7081
+ since: z2.string().default("24h").describe("Duration string (e.g. '1h', '30m', '7d') or ISO timestamp"),
7082
+ limit: z2.number().optional().describe("Max number of error events to return. Defaults to 20 compact rows or 50 full rows."),
7083
+ compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
6002
7084
  }, async ({ since, limit, compact }) => {
6003
7085
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
6004
7086
  const db = getDb2();
@@ -6026,7 +7108,7 @@ function createHooksServer() {
6026
7108
  };
6027
7109
  });
6028
7110
  defineTool("hooks_log_summary", "Summarize hook execution: counts per hook, error rates, and recent activity.", {
6029
- since: z.string().default("24h").describe("Duration string (e.g. '1h', '24h', '7d') or ISO timestamp")
7111
+ since: z2.string().default("24h").describe("Duration string (e.g. '1h', '24h', '7d') or ISO timestamp")
6030
7112
  }, async ({ since }) => {
6031
7113
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
6032
7114
  const db = getDb2();
@@ -6060,13 +7142,13 @@ function createHooksServer() {
6060
7142
  };
6061
7143
  });
6062
7144
  defineTool("storage_status", "Show hooks storage sync configuration and local sync history.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(getStorageStatus()) }] }));
6063
- defineTool("storage_push", "Push local hook data to storage PostgreSQL.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePush(params.tables ? { tables: params.tables } : undefined)) }] }));
6064
- defineTool("storage_pull", "Pull hook data from storage PostgreSQL to local SQLite.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePull(params.tables ? { tables: params.tables } : undefined)) }] }));
6065
- defineTool("storage_sync", "Bidirectional hooks storage sync: pull then push.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storageSync(params.tables ? { tables: params.tables } : undefined)) }] }));
7145
+ defineTool("storage_push", "Push local hook data to storage PostgreSQL.", { tables: z2.array(z2.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePush(params.tables ? { tables: params.tables } : undefined)) }] }));
7146
+ defineTool("storage_pull", "Pull hook data from storage PostgreSQL to local SQLite.", { tables: z2.array(z2.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePull(params.tables ? { tables: params.tables } : undefined)) }] }));
7147
+ defineTool("storage_sync", "Bidirectional hooks storage sync: pull then push.", { tables: z2.array(z2.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storageSync(params.tables ? { tables: params.tables } : undefined)) }] }));
6066
7148
  defineTool("send_feedback", "Send feedback about this service", {
6067
- message: z.string(),
6068
- email: z.string().optional(),
6069
- category: z.enum(["bug", "feature", "general"]).optional()
7149
+ message: z2.string(),
7150
+ email: z2.string().optional(),
7151
+ category: z2.enum(["bug", "feature", "general"]).optional()
6070
7152
  }, async (params) => {
6071
7153
  try {
6072
7154
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
@@ -6083,8 +7165,8 @@ function createHooksServer() {
6083
7165
  }
6084
7166
  });
6085
7167
  defineTool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
6086
- name: z.string(),
6087
- session_id: z.string().optional()
7168
+ name: z2.string(),
7169
+ session_id: z2.string().optional()
6088
7170
  }, async (params) => {
6089
7171
  const existing = [..._hooksAgents.values()].find((a) => a.name === params.name);
6090
7172
  if (existing) {
@@ -6099,7 +7181,7 @@ function createHooksServer() {
6099
7181
  return { content: [{ type: "text", text: JSON.stringify(ag) }] };
6100
7182
  });
6101
7183
  defineTool("heartbeat", "Update last_seen_at to signal agent is active.", {
6102
- agent_id: z.string()
7184
+ agent_id: z2.string()
6103
7185
  }, async (params) => {
6104
7186
  const ag = _hooksAgents.get(params.agent_id);
6105
7187
  if (!ag)
@@ -6108,8 +7190,8 @@ function createHooksServer() {
6108
7190
  return { content: [{ type: "text", text: JSON.stringify({ agent_id: ag.id, last_seen_at: ag.last_seen_at }) }] };
6109
7191
  });
6110
7192
  defineTool("set_focus", "Set active project context for this agent session.", {
6111
- agent_id: z.string(),
6112
- project_id: z.string().optional()
7193
+ agent_id: z2.string(),
7194
+ project_id: z2.string().optional()
6113
7195
  }, async (params) => {
6114
7196
  const ag = _hooksAgents.get(params.agent_id);
6115
7197
  if (!ag)
@@ -6118,8 +7200,8 @@ function createHooksServer() {
6118
7200
  return { content: [{ type: "text", text: JSON.stringify({ agent_id: ag.id, project_id: ag.project_id ?? null }) }] };
6119
7201
  });
6120
7202
  defineTool("list_agents", "List registered agents. Compact by default.", {
6121
- compact: z.boolean().default(true).describe("Return compact agent summaries by default. Set false for full records."),
6122
- limit: z.number().default(25).describe("Max compact rows to return")
7203
+ compact: z2.boolean().default(true).describe("Return compact agent summaries by default. Set false for full records."),
7204
+ limit: z2.number().default(25).describe("Max compact rows to return")
6123
7205
  }, async ({ compact, limit }) => {
6124
7206
  const agents = [..._hooksAgents.values()];
6125
7207
  if (!compact)
@@ -6194,14 +7276,17 @@ var init_server = __esm(() => {
6194
7276
  init_registry();
6195
7277
  init_installer();
6196
7278
  init_profiles();
7279
+ init_manifest();
7280
+ init_resolve();
7281
+ init_store();
6197
7282
  init_storage();
6198
- __dirname3 = dirname2(fileURLToPath2(import.meta.url));
7283
+ __dirname3 = dirname4(fileURLToPath2(import.meta.url));
6199
7284
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
6200
7285
  try {
6201
7286
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
6202
- const p = join6(__dirname3, rel);
6203
- if (existsSync6(p)) {
6204
- pkg = JSON.parse(readFileSync4(p, "utf-8"));
7287
+ const p = join11(__dirname3, rel);
7288
+ if (existsSync11(p)) {
7289
+ pkg = JSON.parse(readFileSync9(p, "utf-8"));
6205
7290
  break;
6206
7291
  }
6207
7292
  }
@@ -6268,7 +7353,7 @@ function startMcpHttpServer(options) {
6268
7353
  var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
6269
7354
  var init_http = () => {};
6270
7355
 
6271
- // node_modules/@hasna/events/dist/commander.js
7356
+ // ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
6272
7357
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6273
7358
  import { Buffer as Buffer2 } from "buffer";
6274
7359
  import { existsSync } from "fs";
@@ -6685,21 +7770,27 @@ function now() {
6685
7770
  function truncate(value, max = 4096) {
6686
7771
  return value.length > max ? `${value.slice(0, max)}...` : value;
6687
7772
  }
6688
- function buildWebhookRequest(event, channel) {
7773
+ function buildWebhookRequest(event, channel, options = {}) {
6689
7774
  if (!channel.webhook)
6690
7775
  throw new Error(`Channel ${channel.id} has no webhook config`);
7776
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
7777
+ if (/^x-hasna-/i.test(name)) {
7778
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
7779
+ }
7780
+ }
6691
7781
  const body = JSON.stringify(event);
6692
- const timestamp = event.time;
7782
+ const timestamp = options.timestamp ?? new Date().toISOString();
6693
7783
  const headers = {
6694
7784
  "Content-Type": "application/json",
6695
7785
  "User-Agent": "@hasna/events",
6696
7786
  "X-Hasna-Event-Id": event.id,
6697
7787
  "X-Hasna-Event-Type": event.type,
6698
- "X-Hasna-Timestamp": timestamp,
6699
- ...channel.webhook.headers
7788
+ ...channel.webhook.headers,
7789
+ "X-Hasna-Timestamp": timestamp
6700
7790
  };
6701
- if (channel.webhook.secret) {
6702
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
7791
+ const secret = options.secret ?? channel.webhook.secret;
7792
+ if (secret) {
7793
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
6703
7794
  }
6704
7795
  return { body, headers };
6705
7796
  }
@@ -6707,7 +7798,21 @@ async function dispatchWebhook(event, channel, options = {}) {
6707
7798
  if (!channel.webhook)
6708
7799
  throw new Error(`Channel ${channel.id} has no webhook config`);
6709
7800
  const startedAt = now();
6710
- const { body, headers } = buildWebhookRequest(event, channel);
7801
+ let secret = channel.webhook.secret;
7802
+ if (channel.webhook.secretRef) {
7803
+ if (!options.secretResolver) {
7804
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
7805
+ }
7806
+ try {
7807
+ secret = await options.secretResolver(channel.webhook.secretRef);
7808
+ } catch {
7809
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
7810
+ }
7811
+ if (!secret)
7812
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
7813
+ }
7814
+ const timestamp = (options.now?.() ?? new Date).toISOString();
7815
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
6711
7816
  const controller = new AbortController;
6712
7817
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
6713
7818
  try {
@@ -6739,6 +7844,15 @@ async function dispatchWebhook(event, channel, options = {}) {
6739
7844
  clearTimeout(timeout);
6740
7845
  }
6741
7846
  }
7847
+ function failedAttempt(startedAt, error) {
7848
+ return {
7849
+ attempt: 1,
7850
+ status: "failed",
7851
+ startedAt,
7852
+ completedAt: now(),
7853
+ error
7854
+ };
7855
+ }
6742
7856
  async function dispatchCommand(event, channel) {
6743
7857
  if (!channel.command)
6744
7858
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -6872,6 +7986,45 @@ class EventTypeCatalog {
6872
7986
  }
6873
7987
  }
6874
7988
  var defaultEventTypeCatalog = new EventTypeCatalog;
7989
+ var APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
7990
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
7991
+ if (paths.length === 0)
7992
+ return event;
7993
+ const copy = structuredClone(event);
7994
+ for (const path of paths) {
7995
+ setPath(copy, path, replacement);
7996
+ }
7997
+ return copy;
7998
+ }
7999
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
8000
+ return redactValue(event, replacement);
8001
+ }
8002
+ function shouldRedactKey(key) {
8003
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
8004
+ }
8005
+ function redactValue(value, replacement) {
8006
+ if (Array.isArray(value))
8007
+ return value.map((item) => redactValue(item, replacement));
8008
+ if (!value || typeof value !== "object")
8009
+ return value;
8010
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
8011
+ key,
8012
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
8013
+ ]));
8014
+ }
8015
+ function setPath(input, path, replacement) {
8016
+ const parts = path.split(".");
8017
+ let cursor = input;
8018
+ for (const part of parts.slice(0, -1)) {
8019
+ const next = cursor[part];
8020
+ if (!next || typeof next !== "object")
8021
+ return;
8022
+ cursor = next;
8023
+ }
8024
+ const last = parts.at(-1);
8025
+ if (last && last in cursor)
8026
+ cursor[last] = replacement;
8027
+ }
6875
8028
  function createEvent(input) {
6876
8029
  return {
6877
8030
  id: input.id ?? randomUUID2(),
@@ -6897,7 +8050,11 @@ class EventsClient {
6897
8050
  constructor(options = {}) {
6898
8051
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
6899
8052
  this.redactors = options.redactors ?? [];
6900
- this.transportOptions = { fetchImpl: options.fetchImpl };
8053
+ this.transportOptions = {
8054
+ fetchImpl: options.fetchImpl,
8055
+ secretResolver: options.secretResolver,
8056
+ now: options.now
8057
+ };
6901
8058
  this.catalog = options.catalog ?? defaultEventTypeCatalog;
6902
8059
  this.validateCatalogTypes = options.validateCatalogTypes ?? false;
6903
8060
  }
@@ -7075,15 +8232,6 @@ class EventsClient {
7075
8232
  return createDeliveryResult(event, channel, attempts);
7076
8233
  }
7077
8234
  }
7078
- function redactPaths(event, paths, replacement = "[REDACTED]") {
7079
- if (paths.length === 0)
7080
- return event;
7081
- const copy = structuredClone(event);
7082
- for (const path of paths) {
7083
- setPath(copy, path, replacement);
7084
- }
7085
- return copy;
7086
- }
7087
8235
  function sanitizeChannelForOutput(channel) {
7088
8236
  const copy = structuredClone(channel);
7089
8237
  if (copy.webhook?.secret)
@@ -7096,35 +8244,6 @@ function sanitizeChannelForOutput(channel) {
7096
8244
  function sanitizeChannelsForOutput(channels) {
7097
8245
  return channels.map(sanitizeChannelForOutput);
7098
8246
  }
7099
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
7100
- return redactValue(event, replacement);
7101
- }
7102
- function shouldRedactKey(key) {
7103
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
7104
- }
7105
- function redactValue(value, replacement) {
7106
- if (Array.isArray(value))
7107
- return value.map((item) => redactValue(item, replacement));
7108
- if (!value || typeof value !== "object")
7109
- return value;
7110
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
7111
- key,
7112
- shouldRedactKey(key) ? replacement : redactValue(item, replacement)
7113
- ]));
7114
- }
7115
- function setPath(input, path, replacement) {
7116
- const parts = path.split(".");
7117
- let cursor = input;
7118
- for (const part of parts.slice(0, -1)) {
7119
- const next = cursor[part];
7120
- if (!next || typeof next !== "object")
7121
- return;
7122
- cursor = next;
7123
- }
7124
- const last = parts.at(-1);
7125
- if (last && last in cursor)
7126
- cursor[last] = replacement;
7127
- }
7128
8247
  function queryClientEvents(events, options) {
7129
8248
  let rows = events;
7130
8249
  if (options.eventId)
@@ -7419,7 +8538,7 @@ function replaySummary(events, deliveries, nextCursor) {
7419
8538
  // src/cli/index.tsx
7420
8539
  import { render } from "ink";
7421
8540
 
7422
- // node_modules/commander/esm.mjs
8541
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/esm.mjs
7423
8542
  var import__ = __toESM(require_commander(), 1);
7424
8543
  var {
7425
8544
  program,
@@ -7437,19 +8556,19 @@ var {
7437
8556
 
7438
8557
  // src/cli/index.tsx
7439
8558
  import chalk2 from "chalk";
7440
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
7441
- import { join as join7, dirname as dirname3 } from "path";
8559
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
8560
+ import { join as join12, dirname as dirname5 } from "path";
7442
8561
  import { fileURLToPath as fileURLToPath3 } from "url";
7443
8562
 
7444
8563
  // src/cli/components/App.tsx
7445
8564
  import { useState as useState7 } from "react";
7446
8565
  import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
7447
8566
 
7448
- // node_modules/ink-select-input/build/Indicator.js
8567
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
7449
8568
  import React from "react";
7450
8569
  import { Box, Text } from "ink";
7451
8570
 
7452
- // node_modules/is-unicode-supported/index.js
8571
+ // ../../node_modules/.bun/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js
7453
8572
  import process2 from "process";
7454
8573
  function isUnicodeSupported() {
7455
8574
  const { env } = process2;
@@ -7460,7 +8579,7 @@ function isUnicodeSupported() {
7460
8579
  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";
7461
8580
  }
7462
8581
 
7463
- // node_modules/figures/index.js
8582
+ // ../../node_modules/.bun/figures@6.1.0/node_modules/figures/index.js
7464
8583
  var common = {
7465
8584
  circleQuestionMark: "(?)",
7466
8585
  questionMarkPrefix: "(?)",
@@ -7736,23 +8855,23 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
7736
8855
  var figures_default = figures;
7737
8856
  var replacements = Object.entries(specialMainSymbols);
7738
8857
 
7739
- // node_modules/ink-select-input/build/Indicator.js
8858
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
7740
8859
  function Indicator({ isSelected = false }) {
7741
8860
  return React.createElement(Box, { marginRight: 1 }, isSelected ? React.createElement(Text, { color: "blue" }, figures_default.pointer) : React.createElement(Text, null, " "));
7742
8861
  }
7743
8862
  var Indicator_default = Indicator;
7744
- // node_modules/ink-select-input/build/Item.js
8863
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Item.js
7745
8864
  import * as React2 from "react";
7746
8865
  import { Text as Text2 } from "ink";
7747
8866
  function Item({ isSelected = false, label }) {
7748
8867
  return React2.createElement(Text2, { color: isSelected ? "blue" : undefined }, label);
7749
8868
  }
7750
8869
  var Item_default = Item;
7751
- // node_modules/ink-select-input/build/SelectInput.js
8870
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
7752
8871
  import { isDeepStrictEqual } from "util";
7753
8872
  import React3, { useState, useEffect, useRef, useCallback } from "react";
7754
8873
 
7755
- // node_modules/to-rotated/index.js
8874
+ // ../../node_modules/.bun/to-rotated@1.0.0/node_modules/to-rotated/index.js
7756
8875
  function toRotated(array, steps) {
7757
8876
  if (!Array.isArray(array)) {
7758
8877
  throw new TypeError(`Expected an array, got \`${typeof array}\`.`);
@@ -7774,7 +8893,7 @@ function toRotated(array, steps) {
7774
8893
  ];
7775
8894
  }
7776
8895
 
7777
- // node_modules/ink-select-input/build/SelectInput.js
8896
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
7778
8897
  import { Box as Box2, useInput } from "ink";
7779
8898
  function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent = Indicator_default, itemComponent = Item_default, limit: customLimit, onSelect, onHighlight }) {
7780
8899
  const hasLimit = typeof customLimit === "number" && items.length > customLimit;
@@ -8070,7 +9189,7 @@ function HookSelect({
8070
9189
  import { useState as useState4, useEffect as useEffect3 } from "react";
8071
9190
  import { Box as Box6, Text as Text7, useInput as useInput4 } from "ink";
8072
9191
 
8073
- // node_modules/ink-text-input/build/index.js
9192
+ // ../../node_modules/.bun/ink-text-input@6.0.0+fa875fef9f949ebd/node_modules/ink-text-input/build/index.js
8074
9193
  import React5, { useState as useState3, useEffect as useEffect2 } from "react";
8075
9194
  import { Text as Text6, useInput as useInput3 } from "ink";
8076
9195
  import chalk from "chalk";
@@ -8248,7 +9367,7 @@ function SearchView({
8248
9367
  import { useState as useState6, useEffect as useEffect5 } from "react";
8249
9368
  import { Box as Box7, Text as Text9 } from "ink";
8250
9369
 
8251
- // node_modules/ink-spinner/build/index.js
9370
+ // ../../node_modules/.bun/ink-spinner@5.0.0+fa875fef9f949ebd/node_modules/ink-spinner/build/index.js
8252
9371
  var import_cli_spinners = __toESM(require_cli_spinners(), 1);
8253
9372
  import React7, { useState as useState5, useEffect as useEffect4 } from "react";
8254
9373
  import { Text as Text8 } from "ink";
@@ -8614,10 +9733,12 @@ function App({ initialHooks, overwrite = false }) {
8614
9733
  init_registry();
8615
9734
  init_installer();
8616
9735
  init_profiles();
9736
+ init_manifest();
9737
+ init_resolve();
8617
9738
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
8618
- var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
8619
- var pkgPath = existsSync7(join7(__dirname4, "..", "package.json")) ? join7(__dirname4, "..", "package.json") : join7(__dirname4, "..", "..", "package.json");
8620
- var pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
9739
+ var __dirname4 = dirname5(fileURLToPath3(import.meta.url));
9740
+ var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
9741
+ var pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
8621
9742
  var program2 = new Command;
8622
9743
  function resolveScope(options) {
8623
9744
  if (options.project)
@@ -8645,6 +9766,16 @@ function truncateText2(value, max = 96) {
8645
9766
  return text;
8646
9767
  return `${text.slice(0, Math.max(0, max - 3))}...`;
8647
9768
  }
9769
+ function readToken(tokenFile) {
9770
+ if (tokenFile) {
9771
+ try {
9772
+ return readFileSync10(tokenFile, "utf-8").trim();
9773
+ } catch {
9774
+ return;
9775
+ }
9776
+ }
9777
+ return process.env.CF_API_TOKEN;
9778
+ }
8648
9779
  function readmePreview(readme, max = 280) {
8649
9780
  const blocks = readme.split(/\n\s*\n/).map((part) => part.trim()).filter(Boolean).filter((part) => !part.startsWith("#")).filter((part) => !part.startsWith("```")).filter((part) => !/^\[!\[/.test(part));
8650
9781
  const preview = blocks[0] ?? readme.split(`
@@ -8685,7 +9816,38 @@ program2.name("hooks").description("Install hooks for AI coding agents").version
8685
9816
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
8686
9817
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
8687
9818
  });
8688
- program2.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).action((options) => {
9819
+ program2.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).option("--cloudflare", "Configure a remote registry (Cloudflare worker)", false).option("--api-url <url>", "Remote registry API URL (with --cloudflare)").option("--api-key <ref>", "Vault key NAME for the registry API key (never the value; with --cloudflare)").action(async (options) => {
9820
+ if (options.cloudflare) {
9821
+ const { writeConfig: writeConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
9822
+ if (!options.apiUrl) {
9823
+ const message = "--cloudflare requires --api-url <url>";
9824
+ if (options.json)
9825
+ console.log(JSON.stringify({ error: message }));
9826
+ else
9827
+ console.log(chalk2.red(message));
9828
+ return;
9829
+ }
9830
+ const config = { api_url: options.apiUrl.replace(/\/+$/, ""), api_key_ref: options.apiKey };
9831
+ const path = writeConfig2(config);
9832
+ if (options.json) {
9833
+ console.log(JSON.stringify({ ok: true, api_url: config.api_url, api_key_ref: config.api_key_ref ?? null, config_path: path }));
9834
+ return;
9835
+ }
9836
+ console.log(chalk2.green(`
9837
+ \u2713 Remote registry configured
9838
+ `));
9839
+ console.log(` ${chalk2.dim("API URL:")} ${config.api_url}`);
9840
+ console.log(` ${chalk2.dim("API key ref:")} ${config.api_key_ref ?? "(none; serve will refuse publishes)"}`);
9841
+ console.log(` ${chalk2.dim("Config:")} ${path}`);
9842
+ console.log();
9843
+ console.log(chalk2.dim(" Presence of an API URL selects the remote registry; absence means local."));
9844
+ if (config.api_key_ref) {
9845
+ console.log(chalk2.dim(` Run the server with the key resolved from the vault, never the value:`));
9846
+ console.log(` secrets exec ${config.api_key_ref} --as HASNA_HOOKS_API_KEY -- hooks serve`);
9847
+ }
9848
+ console.log();
9849
+ return;
9850
+ }
8689
9851
  const agentType = options.agent;
8690
9852
  if (!["claude", "gemini", "custom"].includes(agentType)) {
8691
9853
  if (options.json) {
@@ -8716,15 +9878,27 @@ program2.command("init").description("Register a new agent profile with a unique
8716
9878
  console.log();
8717
9879
  });
8718
9880
  program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
8719
- const meta = getHook(hook);
8720
- if (!meta) {
9881
+ const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
9882
+ const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
9883
+ const resolved = resolveHook2(hook);
9884
+ if (!resolved) {
8721
9885
  console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
8722
9886
  process.exit(1);
8723
9887
  }
8724
- const hookDir = getHookPath(hook);
8725
- const hookScript = join7(hookDir, "src", "hook.ts");
8726
- if (!existsSync7(hookScript)) {
8727
- console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
9888
+ const hookScript = resolveScriptPath2(hook);
9889
+ if (!hookScript || !existsSync12(hookScript)) {
9890
+ console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
9891
+ process.exit(1);
9892
+ }
9893
+ const content = readFileSync10(hookScript);
9894
+ const check = checkScriptHash2(hook, sha256Of2(content));
9895
+ if (!check.ok) {
9896
+ console.error(JSON.stringify({
9897
+ error: `Hook '${hook}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${hook}' to trust the new content.`,
9898
+ hook,
9899
+ expected_sha256: check.expected,
9900
+ actual_sha256: check.actual
9901
+ }));
8728
9902
  process.exit(1);
8729
9903
  }
8730
9904
  const stdin = await new Response(Bun.stdin.stream()).text();
@@ -8745,22 +9919,27 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
8745
9919
  } catch {}
8746
9920
  }
8747
9921
  }
8748
- const proc = Bun.spawn(["bun", "run", hookScript], {
8749
- stdin: new Response(hookStdin),
8750
- stdout: "pipe",
8751
- stderr: "pipe",
8752
- env: process.env
9922
+ const { readCustomManifest: readCustomManifest2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
9923
+ const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
9924
+ const custom = readCustomManifest2(hook);
9925
+ const args = custom?.manifest.args ?? [];
9926
+ const timeout = custom?.manifest.timeout_ms;
9927
+ const { stdout, stderr, exitCode } = await executeVerifiedScript2({
9928
+ name: hook,
9929
+ scriptPath: hookScript,
9930
+ content,
9931
+ args,
9932
+ stdin: hookStdin,
9933
+ env: process.env,
9934
+ timeout
8753
9935
  });
8754
- const stdout = await new Response(proc.stdout).text();
8755
- const stderr = await new Response(proc.stderr).text();
8756
- const exitCode = await proc.exited;
8757
9936
  if (stdout)
8758
9937
  process.stdout.write(stdout);
8759
9938
  if (stderr)
8760
9939
  process.stderr.write(stderr);
8761
9940
  process.exit(exitCode);
8762
9941
  });
8763
- program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer open-configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
9942
+ program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer open-configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, or manifest URLs)").action(async (hooks, options) => {
8764
9943
  const scope = resolveScope(options);
8765
9944
  const target = resolveTarget(options);
8766
9945
  let toInstall = hooks;
@@ -8792,11 +9971,21 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
8792
9971
  }
8793
9972
  return;
8794
9973
  }
9974
+ const { isCustomSource: isCustomSource2, installCustomSource: installCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
9975
+ const customSources = toInstall.filter((n) => !getHook(n) && isCustomSource2(n));
8795
9976
  if (options.dryRun) {
8796
9977
  const known = toInstall.filter((n) => getHook(n));
8797
- const unknown = toInstall.filter((n) => !getHook(n));
9978
+ const unknown = toInstall.filter((n) => !getHook(n) && !isCustomSource2(n));
8798
9979
  if (options.json) {
8799
- console.log(JSON.stringify({ dryRun: true, would_install: known, unknown, scope, target, mode: target === "codewith" ? "fragment" : "write" }));
9980
+ console.log(JSON.stringify({
9981
+ dryRun: true,
9982
+ would_install: known,
9983
+ custom_sources: customSources,
9984
+ unknown,
9985
+ scope,
9986
+ target,
9987
+ mode: target === "codewith" ? "fragment" : "write"
9988
+ }));
8800
9989
  return;
8801
9990
  }
8802
9991
  console.log(chalk2.bold(`
@@ -8806,6 +9995,9 @@ Dry run \u2014 would install (${scope}, ${target}):
8806
9995
  const meta = getHook(name);
8807
9996
  console.log(chalk2.cyan(` ${name}`) + chalk2.dim(` [${meta.event}${meta.matcher ? ` ${meta.matcher}` : ""}]`));
8808
9997
  }
9998
+ for (const source of customSources) {
9999
+ console.log(chalk2.cyan(` ${source}`) + chalk2.dim(" [custom source: would fetch and install]"));
10000
+ }
8809
10001
  if (unknown.length > 0) {
8810
10002
  console.log();
8811
10003
  for (const name of unknown) {
@@ -8816,8 +10008,29 @@ Dry run \u2014 would install (${scope}, ${target}):
8816
10008
  return;
8817
10009
  }
8818
10010
  const results = [];
8819
- for (const name of toInstall) {
8820
- if (!getHook(name)) {
10011
+ const installedCustom = [];
10012
+ for (const source of customSources) {
10013
+ try {
10014
+ const custom = await installCustomSource2(source);
10015
+ installedCustom.push(custom.name);
10016
+ if (options.json) {
10017
+ console.log(JSON.stringify({ custom: { source, name: custom.name, version: custom.version } }));
10018
+ } else {
10019
+ console.log(chalk2.green(`\u2713 Installed custom hook '${custom.name}' v${custom.version} from ${source}`));
10020
+ }
10021
+ } catch (error) {
10022
+ const message = error instanceof Error ? error.message : String(error);
10023
+ results.push({ hook: source, success: false, error: message });
10024
+ if (options.json) {
10025
+ console.log(JSON.stringify({ error: `Custom hook install failed for ${source}: ${message}` }));
10026
+ } else {
10027
+ console.log(chalk2.red(`\u2717 ${message}`));
10028
+ }
10029
+ }
10030
+ }
10031
+ const namesToRegister = [...new Set([...toInstall.filter((n) => !isCustomSource2(n)), ...installedCustom])];
10032
+ for (const name of namesToRegister) {
10033
+ if (!getHook(name) && !installedCustom.includes(name)) {
8821
10034
  const suggestions = suggestHooks(name);
8822
10035
  const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
8823
10036
  results.push({ hook: name, success: false, error: `Hook '${name}' not found${hint}` });
@@ -9007,8 +10220,9 @@ Categories:
9007
10220
  console.log(` ${category} (${count})`);
9008
10221
  }
9009
10222
  });
9010
- program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action((hook, options) => {
9011
- const meta = getHook(hook);
10223
+ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action(async (hook, options) => {
10224
+ const { resolveHookMeta: resolveHookMeta2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
10225
+ const meta = resolveHookMeta2(hook) ?? getHook(hook);
9012
10226
  if (!meta) {
9013
10227
  const suggestions = suggestHooks(hook);
9014
10228
  const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
@@ -9021,8 +10235,12 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
9021
10235
  }
9022
10236
  const globalInstalled = getRegisteredHooks("global").includes(meta.name);
9023
10237
  const projectInstalled = getRegisteredHooks("project").includes(meta.name);
10238
+ const { readCustomManifest: readCustomManifest2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
10239
+ const custom = readCustomManifest2(meta.name);
10240
+ const source = custom ? "custom" : "bundled";
10241
+ const sourceNote = custom ? "custom (~/.hasna/hooks/hooks/) overrides the bundled registry" : "bundled registry";
9024
10242
  if (options.json) {
9025
- console.log(JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }));
10243
+ console.log(JSON.stringify({ ...meta, source, source_note: sourceNote, global: globalInstalled, project: projectInstalled }));
9026
10244
  return;
9027
10245
  }
9028
10246
  console.log(chalk2.bold(`
@@ -9035,6 +10253,7 @@ ${meta.displayName}
9035
10253
  console.log(` ${chalk2.dim("Event:")} ${meta.event}`);
9036
10254
  console.log(` ${chalk2.dim("Matcher:")} ${meta.matcher || "(none)"}`);
9037
10255
  console.log(` ${chalk2.dim("Tags:")} ${meta.tags.join(", ")}`);
10256
+ console.log(` ${chalk2.dim("Source:")} ${source} (${sourceNote})`);
9038
10257
  console.log(` ${chalk2.dim("Command:")} hooks run ${meta.name}`);
9039
10258
  console.log();
9040
10259
  if (globalInstalled) {
@@ -9053,28 +10272,36 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
9053
10272
  const settingsPath = getSettingsPath(scope);
9054
10273
  const issues = [];
9055
10274
  const healthy = [];
9056
- const settingsExist = existsSync7(settingsPath);
10275
+ const settingsExist = existsSync12(settingsPath);
9057
10276
  if (!settingsExist) {
9058
10277
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
9059
10278
  }
9060
10279
  const registered = getRegisteredHooks(scope);
9061
10280
  for (const name of registered) {
9062
- const meta = getHook(name);
10281
+ const custom = readCustomManifest(name);
10282
+ const meta = custom ? resolveHookMeta(name) : getHook(name);
9063
10283
  let hookHealthy = true;
9064
- if (!hookExists(name)) {
9065
- issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
9066
- hookHealthy = false;
9067
- continue;
9068
- }
9069
- const hookDir = getHookPath(name);
9070
- const hookScript = join7(hookDir, "src", "hook.ts");
9071
- if (!existsSync7(hookScript)) {
9072
- issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
9073
- hookHealthy = false;
10284
+ if (custom) {
10285
+ if (!existsSync12(custom.scriptPath)) {
10286
+ issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
10287
+ hookHealthy = false;
10288
+ }
10289
+ } else {
10290
+ if (!hookExists(name)) {
10291
+ issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
10292
+ hookHealthy = false;
10293
+ continue;
10294
+ }
10295
+ const hookDir = getHookPath(name);
10296
+ const hookScript = join12(hookDir, "src", "hook.ts");
10297
+ if (!existsSync12(hookScript)) {
10298
+ issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
10299
+ hookHealthy = false;
10300
+ }
9074
10301
  }
9075
10302
  if (meta && settingsExist) {
9076
10303
  try {
9077
- const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
10304
+ const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
9078
10305
  const eventHooks = settings.hooks?.[meta.event] || [];
9079
10306
  const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
9080
10307
  const match = h.command?.match(/^hooks run ([\w-]+)/);
@@ -9121,7 +10348,7 @@ Hook Health Check (${scope})
9121
10348
  }
9122
10349
  console.log();
9123
10350
  });
9124
- program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version)").action((hooks, options) => {
10351
+ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version) and refresh the lock pins").action(async (hooks, options) => {
9125
10352
  const scope = resolveScope(options);
9126
10353
  const installed = getInstalledHooks(scope);
9127
10354
  const toUpdate = hooks.length > 0 ? hooks : installed;
@@ -9133,6 +10360,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
9133
10360
  }
9134
10361
  return;
9135
10362
  }
10363
+ const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
10364
+ const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
10365
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
9136
10366
  const results = [];
9137
10367
  for (const name of toUpdate) {
9138
10368
  if (!installed.includes(name)) {
@@ -9140,6 +10370,20 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
9140
10370
  continue;
9141
10371
  }
9142
10372
  const result = installHook(name, { scope, overwrite: true });
10373
+ const resolved = resolveHook2(name);
10374
+ if (result.success && resolved) {
10375
+ const hash = await sha256File2(resolved.scriptPath);
10376
+ setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
10377
+ upsertHookRecord2(getDb2(), {
10378
+ name: resolved.name,
10379
+ version: resolved.version,
10380
+ sha256: hash,
10381
+ source_type: resolved.source,
10382
+ last_verified_at: new Date().toISOString()
10383
+ });
10384
+ results.push({ ...result, pinned: { version: resolved.version, sha256: hash } });
10385
+ continue;
10386
+ }
9143
10387
  results.push(result);
9144
10388
  }
9145
10389
  if (options.json) {
@@ -9154,7 +10398,7 @@ Updating hooks...
9154
10398
  `));
9155
10399
  for (const result of results) {
9156
10400
  if (result.success) {
9157
- console.log(chalk2.green(`\u2713 ${result.hook} updated`));
10401
+ console.log(chalk2.green(`\u2713 ${result.hook} updated`) + (result.pinned ? chalk2.dim(` (pinned ${result.pinned.version})`) : ""));
9158
10402
  } else {
9159
10403
  console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
9160
10404
  }
@@ -9172,10 +10416,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
9172
10416
  return;
9173
10417
  }
9174
10418
  const hookPath = getHookPath(hook);
9175
- const readmePath = join7(hookPath, "README.md");
10419
+ const readmePath = join12(hookPath, "README.md");
9176
10420
  let readme = "";
9177
- if (existsSync7(readmePath)) {
9178
- readme = readFileSync5(readmePath, "utf-8");
10421
+ if (existsSync12(readmePath)) {
10422
+ readme = readFileSync10(readmePath, "utf-8");
9179
10423
  }
9180
10424
  if (options.json) {
9181
10425
  console.log(JSON.stringify({ ...meta, readme }));
@@ -9357,14 +10601,14 @@ Upgrading @hasna/hooks (${pm})...
9357
10601
  });
9358
10602
  program2.command("profile-export").description("Export all agent profiles as JSON (for backup/cross-machine setup)").option("-o, --output <file>", "Write to file instead of stdout").option("-j, --json", "Output as JSON (default: true)", false).action(async (options) => {
9359
10603
  const profiles = exportProfiles();
9360
- const json = JSON.stringify(profiles, null, 2);
10604
+ const json2 = JSON.stringify(profiles, null, 2);
9361
10605
  if (options.output) {
9362
- const { writeFileSync: writeFileSync3 } = await import("fs");
9363
- writeFileSync3(options.output, json + `
10606
+ const { writeFileSync: writeFileSync7 } = await import("fs");
10607
+ writeFileSync7(options.output, json2 + `
9364
10608
  `);
9365
10609
  console.log(chalk2.green(`\u2713 Exported ${profiles.length} profile(s) to ${options.output}`));
9366
10610
  } else {
9367
- console.log(json);
10611
+ console.log(json2);
9368
10612
  }
9369
10613
  });
9370
10614
  program2.command("profile-import").argument("<file>", "JSON file to import profiles from (use - for stdin)").description("Import agent profiles from a JSON export file").option("-j, --json", "Output result as JSON", false).action(async (file, options) => {
@@ -9372,9 +10616,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
9372
10616
  if (file === "-") {
9373
10617
  raw = await new Response(Bun.stdin.stream()).text();
9374
10618
  } else {
9375
- const { readFileSync: readFileSync6 } = await import("fs");
10619
+ const { readFileSync: readFileSync11 } = await import("fs");
9376
10620
  try {
9377
- raw = readFileSync6(file, "utf-8");
10621
+ raw = readFileSync11(file, "utf-8");
9378
10622
  } catch {
9379
10623
  if (options.json) {
9380
10624
  console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
@@ -9569,7 +10813,7 @@ storageCmd.command("status").description("Show storage sync status").option("-j,
9569
10813
  Storage Status
9570
10814
  `));
9571
10815
  console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
9572
- console.log(` Mode: ${status.mode}`);
10816
+ console.log(` Backend: ${status.backend}`);
9573
10817
  console.log(` Tables: ${status.tables.join(", ")}`);
9574
10818
  console.log(` Sync rows: ${status.sync.length}`);
9575
10819
  });
@@ -9631,6 +10875,126 @@ storageCmd.command("sync").description("Bidirectional storage sync: pull then pu
9631
10875
  process.exitCode = 1;
9632
10876
  }
9633
10877
  });
10878
+ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json", "Output as JSON", false).description("Trust the current script content of a hook (re-pins its sha256)").action(async (hook, options) => {
10879
+ const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
10880
+ const { retrustHook: retrustHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
10881
+ const resolved = resolveHook2(hook);
10882
+ if (!resolved) {
10883
+ if (options.json)
10884
+ console.log(JSON.stringify({ error: `Hook '${hook}' not found` }));
10885
+ else
10886
+ console.log(chalk2.red(`Hook '${hook}' not found`));
10887
+ return;
10888
+ }
10889
+ const result = retrustHook2(resolved.name, resolved.scriptPath, resolved.version, resolved.source);
10890
+ if (options.json) {
10891
+ console.log(JSON.stringify({ ok: true, hook: resolved.name, version: resolved.version, sha256: result.actual, source: resolved.source }));
10892
+ return;
10893
+ }
10894
+ console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
10895
+ console.log(chalk2.dim(` sha256 ${result.actual}`));
10896
+ });
10897
+ program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").option("--api-key <key>", "API key for publish (defaults to HASNA_HOOKS_API_KEY / HOOKS_API_KEY)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock)").action(async (options) => {
10898
+ const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
10899
+ startServeServer2({
10900
+ port: options.port ? parseInt(options.port, 10) : undefined,
10901
+ host: options.host,
10902
+ apiKey: options.apiKey
10903
+ });
10904
+ });
10905
+ 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) => {
10906
+ const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
10907
+ try {
10908
+ const plan = options.dryRun ? await planSync2() : await syncHooks2();
10909
+ const diff = plan.diff;
10910
+ if (options.json) {
10911
+ console.log(JSON.stringify({ dry_run: plan.dryRun, api_url: plan.apiUrl ?? null, diff }));
10912
+ return;
10913
+ }
10914
+ const source = plan.apiUrl ? plan.apiUrl : "bundled registry";
10915
+ console.log(chalk2.bold(`
10916
+ Hook sync (${source})${plan.dryRun ? " \u2014 dry run" : ""}
10917
+ `));
10918
+ if (diff.added.length > 0) {
10919
+ console.log(chalk2.green(` + ${diff.added.length} to add:`));
10920
+ for (const name of diff.added)
10921
+ console.log(chalk2.green(` ${name}`));
10922
+ }
10923
+ if (diff.updated.length > 0) {
10924
+ console.log(chalk2.yellow(` ~ ${diff.updated.length} to update:`));
10925
+ for (const name of diff.updated)
10926
+ console.log(chalk2.yellow(` ${name}`));
10927
+ }
10928
+ if (diff.skipped.length > 0) {
10929
+ console.log(chalk2.dim(` - ${diff.skipped.length} not in remote lock (left untouched)`));
10930
+ }
10931
+ console.log(chalk2.dim(` = ${diff.unchanged.length} unchanged`));
10932
+ if (plan.dryRun)
10933
+ console.log(chalk2.dim(`
10934
+ Dry run: nothing was written.`));
10935
+ else
10936
+ console.log(chalk2.green(`
10937
+ \u2713 Synced (${diff.added.length} added, ${diff.updated.length} updated, ${diff.unchanged.length} unchanged)`));
10938
+ } catch (error) {
10939
+ const message = error instanceof Error ? error.message : String(error);
10940
+ if (options.json) {
10941
+ console.log(JSON.stringify({ error: message, changed: false }));
10942
+ } else {
10943
+ console.error(chalk2.red(`\u2717 ${message}`));
10944
+ console.error(chalk2.dim(" Nothing was changed (fail-closed)."));
10945
+ }
10946
+ process.exitCode = 1;
10947
+ }
10948
+ });
10949
+ program2.command("cf").description("Provision and deploy the hooks registry to Cloudflare").option("--deploy", "Provision D1 + R2 via the Cloudflare API", true).option("--dry-run", "Print the plan without calling the Cloudflare API", false).option("--account-id <id>", "Cloudflare account id (defaults to CF_ACCOUNT_ID env)").option("--database-name <name>", "D1 database name (default hooks-registry)").option("--bucket-name <name>", "R2 bucket name (default hooks-registry-artifacts)").option("--token-file <path>", "Read the API token from a file (defaults to CF_API_TOKEN env)").option("-j, --json", "Output as JSON", false).action(async (options) => {
10950
+ const token = readToken(options.tokenFile);
10951
+ const accountId = options.accountId ?? process.env.CF_ACCOUNT_ID;
10952
+ if (!options.dryRun) {
10953
+ if (!token) {
10954
+ console.error(chalk2.red("CF_API_TOKEN is not set (or --token-file path is unreadable)."));
10955
+ console.error(chalk2.dim(" Resolve it from the vault, never paste the value:"));
10956
+ console.error(chalk2.dim(" secrets exec <vault-key> --as CF_API_TOKEN -- hooks cf deploy"));
10957
+ process.exit(1);
10958
+ }
10959
+ if (!accountId) {
10960
+ console.error(chalk2.red("CF_ACCOUNT_ID is not set (or pass --account-id)."));
10961
+ process.exit(1);
10962
+ }
10963
+ }
10964
+ const { provisionCloudflareResources: provisionCloudflareResources2 } = await Promise.resolve().then(() => exports_provision);
10965
+ const result = await provisionCloudflareResources2({
10966
+ token: token ?? "",
10967
+ accountId: accountId ?? "",
10968
+ databaseName: options.databaseName ?? "hooks-registry",
10969
+ bucketName: options.bucketName ?? "hooks-registry-artifacts",
10970
+ dryRun: options.dryRun
10971
+ });
10972
+ if (options.json) {
10973
+ console.log(JSON.stringify({ dry_run: options.dryRun, ...result }));
10974
+ return;
10975
+ }
10976
+ if (options.dryRun) {
10977
+ console.log(chalk2.bold(`
10978
+ cf deploy dry run \u2014 would provision:
10979
+ `));
10980
+ } else {
10981
+ console.log(chalk2.bold(`
10982
+ Cloudflare provisioning complete:
10983
+ `));
10984
+ console.log(` ${chalk2.dim("D1:")} ${result.d1Created ? chalk2.green(`created '${options.databaseName ?? "hooks-registry"}'`) : chalk2.dim(`exists (${result.d1DatabaseId})`)}`);
10985
+ console.log(` ${chalk2.dim("R2:")} ${result.r2Created ? chalk2.green(`created '${options.bucketName ?? "hooks-registry-artifacts"}'`) : chalk2.dim("exists")}`);
10986
+ }
10987
+ console.log(`
10988
+ ${chalk2.bold("Worker upload (run these with wrangler):")}`);
10989
+ for (const cmd of result.commands) {
10990
+ console.log(` ${cmd}`);
10991
+ }
10992
+ console.log();
10993
+ console.log(chalk2.dim(" Worker upload is not performed by this command \u2014 the worker needs the workerd"));
10994
+ console.log(chalk2.dim(" target, which only wrangler can bundle. Copy src/cf/wrangler.toml.example to"));
10995
+ console.log(chalk2.dim(" wrangler.toml, fill in the D1 database id, then run the commands above."));
10996
+ console.log();
10997
+ });
9634
10998
  program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process per agent)", false).option("--sse", "Use legacy SSE transport (port 39427)", false).option("--http", "Use Streamable HTTP transport (explicit; this is also the default)", false).option("-p, --port <port>", "Port for HTTP/SSE transport (defaults to 8847 for HTTP, 39427 for SSE)").description("Start MCP server for AI agent integration (default: shared Streamable HTTP)").action(async (options) => {
9635
10999
  if (options.stdio) {
9636
11000
  const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));