@hasna/hooks 0.6.8 → 0.7.2

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.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/bin/index.js +579 -113
  3. package/bin/serve.js +503 -38
  4. package/dist/config.d.ts +7 -5
  5. package/dist/db/pg-migrations.d.ts +1 -1
  6. package/dist/index.js +533 -71
  7. package/dist/openapi.d.ts +216 -0
  8. package/hooks/codewith-native-common.test.ts +25 -25
  9. package/hooks/codewith-native-common.ts +1 -1
  10. package/hooks/hook-autoformat/package.json +1 -1
  11. package/hooks/hook-desktopnotify/package.json +1 -1
  12. package/hooks/hook-envsetup/package.json +1 -1
  13. package/hooks/hook-knowledge-context/README.md +1 -1
  14. package/hooks/hook-knowledge-context/src/hook.test.ts +1 -1
  15. package/hooks/hook-permissionguard/package.json +1 -1
  16. package/hooks/hook-protectfiles/package.json +1 -1
  17. package/hooks/hook-spiral-detector/LICENSE +191 -0
  18. package/hooks/hook-spiral-detector/README.md +23 -0
  19. package/hooks/hook-spiral-detector/package.json +12 -0
  20. package/hooks/hook-spiral-detector/src/hook.test.ts +72 -0
  21. package/hooks/hook-spiral-detector/src/hook.ts +93 -0
  22. package/hooks/hook-workspace-repos-guard/README.md +4 -3
  23. package/hooks/hook-workspace-repos-guard/src/hook.test.ts +7 -7
  24. package/hooks/hook-workspace-repos-guard/src/hook.ts +5 -3
  25. package/hooks/mention-context/src/hasna-mention-context.py +2 -2
  26. package/hooks/mention-context/src/hasna-mention-warm.py +1 -1
  27. package/hooks/pre-bash/README.md +1 -1
  28. package/hooks/prompt-guard/README.md +1 -1
  29. package/hooks/session-start/README.md +1 -1
  30. package/hooks/stop-sync/README.md +1 -1
  31. package/hooks/worktree-guard/README.md +1 -1
  32. package/package.json +10 -6
package/bin/index.js CHANGED
@@ -2266,6 +2266,16 @@ var init_registry = __esm(() => {
2266
2266
  matcher: "",
2267
2267
  tags: ["errors", "failures", "logging", "debugging"]
2268
2268
  },
2269
+ {
2270
+ name: "spiral-detector",
2271
+ displayName: "Spiral Detector",
2272
+ description: "Interrupts sessions after five consecutive identical command failures",
2273
+ version: "0.1.0",
2274
+ category: "Observability",
2275
+ event: "PostToolUse",
2276
+ matcher: "",
2277
+ tags: ["spiral", "failures", "retries", "safety", "interrupt"]
2278
+ },
2269
2279
  {
2270
2280
  name: "stylescheck",
2271
2281
  displayName: "Styles Check",
@@ -4059,6 +4069,280 @@ var require_cli_spinners = __commonJS((exports, module) => {
4059
4069
  module.exports = spinners;
4060
4070
  });
4061
4071
 
4072
+ // ../contracts/dist/client/transport.js
4073
+ import { readFileSync, statSync } from "fs";
4074
+ import { join as join2 } from "path";
4075
+ function envToken(name) {
4076
+ return name.toUpperCase().replace(/-/g, "_");
4077
+ }
4078
+ function clientTransportEnvKeys(name) {
4079
+ const envSegment = envToken(name);
4080
+ return {
4081
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
4082
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
4083
+ };
4084
+ }
4085
+ function credentialOverrideEnvKey(name) {
4086
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
4087
+ }
4088
+ function homeDir(env) {
4089
+ const home = env.HOME?.trim();
4090
+ return home ? home : null;
4091
+ }
4092
+ function credentialDiskSources(name, env) {
4093
+ return profileDiskSources(name, env, null);
4094
+ }
4095
+ function profileDiskSources(name, env, profile) {
4096
+ const home = homeDir(env);
4097
+ if (!home || !SAFE_APP_SLUG.test(name))
4098
+ return [];
4099
+ const stem = profile ? `${name}.${profile}` : name;
4100
+ const configStem = profile ? `${name}-${profile}` : name;
4101
+ return [
4102
+ join2(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
4103
+ join2(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
4104
+ ];
4105
+ }
4106
+ function parseEnvFile(text) {
4107
+ const values = new Map;
4108
+ for (const rawLine of text.split(/\r?\n/)) {
4109
+ const line = rawLine.trim();
4110
+ if (line.length === 0 || line.startsWith("#"))
4111
+ continue;
4112
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
4113
+ const equals = withoutExport.indexOf("=");
4114
+ if (equals <= 0)
4115
+ continue;
4116
+ const key = withoutExport.slice(0, equals).trim();
4117
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
4118
+ continue;
4119
+ let value = withoutExport.slice(equals + 1).trim();
4120
+ const quote = value[0];
4121
+ if (quote === '"' || quote === "'") {
4122
+ if (value.length < 2 || !value.endsWith(quote))
4123
+ continue;
4124
+ value = value.slice(1, -1);
4125
+ }
4126
+ if (value.length === 0)
4127
+ continue;
4128
+ values.set(key, value);
4129
+ }
4130
+ return values;
4131
+ }
4132
+ function readAppConfigFile(path) {
4133
+ let text;
4134
+ try {
4135
+ const stats = statSync(path);
4136
+ if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
4137
+ return null;
4138
+ text = readFileSync(path, "utf8");
4139
+ } catch {
4140
+ return null;
4141
+ }
4142
+ return parseEnvFile(text);
4143
+ }
4144
+ function readCredentialFile(path, apiKeyKeys) {
4145
+ const values = readAppConfigFile(path);
4146
+ if (!values)
4147
+ return null;
4148
+ for (const key of apiKeyKeys) {
4149
+ const value = values.get(key)?.trim();
4150
+ if (value)
4151
+ return value;
4152
+ }
4153
+ return null;
4154
+ }
4155
+ function assertUsableCredential(appName, source, value) {
4156
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
4157
+ return;
4158
+ throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
4159
+ }
4160
+ function sealCredential(fields) {
4161
+ const { apiKey } = fields;
4162
+ const visible = {
4163
+ tier: fields.tier,
4164
+ source: fields.source,
4165
+ deliberate: fields.deliberate,
4166
+ deprecated: fields.deprecated,
4167
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
4168
+ warning: fields.warning
4169
+ };
4170
+ const sealed = { ...visible };
4171
+ Object.defineProperty(sealed, "apiKey", {
4172
+ value: apiKey,
4173
+ enumerable: false,
4174
+ writable: false,
4175
+ configurable: false
4176
+ });
4177
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
4178
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
4179
+ enumerable: false,
4180
+ writable: false,
4181
+ configurable: false
4182
+ });
4183
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
4184
+ value: true,
4185
+ enumerable: false,
4186
+ writable: false,
4187
+ configurable: false
4188
+ });
4189
+ return Object.freeze(sealed);
4190
+ }
4191
+ function firstEnvValue(env, keys) {
4192
+ for (const key of keys) {
4193
+ const value = env[key]?.trim();
4194
+ if (value)
4195
+ return { key, value };
4196
+ }
4197
+ return null;
4198
+ }
4199
+ function deprecationNotified() {
4200
+ const host = globalThis;
4201
+ const existing = host[DEPRECATION_REGISTRY];
4202
+ if (existing instanceof Set)
4203
+ return existing;
4204
+ const created = new Set;
4205
+ host[DEPRECATION_REGISTRY] = created;
4206
+ return created;
4207
+ }
4208
+ function defaultDeprecationSink(message) {
4209
+ if (typeof process !== "undefined" && process.stderr) {
4210
+ process.stderr.write(`${message}
4211
+ `);
4212
+ }
4213
+ }
4214
+ function resolveCredential(name, env, options = {}) {
4215
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
4216
+ const diskPaths = credentialDiskSources(name, env);
4217
+ const explicitKey = options.apiKey?.trim();
4218
+ if (explicitKey) {
4219
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
4220
+ return sealCredential({
4221
+ apiKey: explicitKey,
4222
+ tier: "argument",
4223
+ source: "explicit apiKey argument",
4224
+ deliberate: true,
4225
+ deprecated: false,
4226
+ diskCandidates: diskPaths,
4227
+ warning: null
4228
+ });
4229
+ }
4230
+ const overrideKeyName = credentialOverrideEnvKey(name);
4231
+ const overrideRaw = env[overrideKeyName];
4232
+ if (overrideRaw !== undefined) {
4233
+ const override = overrideRaw.trim();
4234
+ if (!override) {
4235
+ throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
4236
+ }
4237
+ assertUsableCredential(name, overrideKeyName, override);
4238
+ return sealCredential({
4239
+ apiKey: override,
4240
+ tier: "override",
4241
+ source: overrideKeyName,
4242
+ deliberate: true,
4243
+ deprecated: false,
4244
+ diskCandidates: diskPaths,
4245
+ warning: null
4246
+ });
4247
+ }
4248
+ const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
4249
+ if (profile) {
4250
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
4251
+ if (!SAFE_PROFILE.test(profile)) {
4252
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
4253
+ }
4254
+ const paths = profileDiskSources(name, env, profile);
4255
+ for (const path of paths) {
4256
+ const value = readCredentialFile(path, apiKeyKeys);
4257
+ if (value) {
4258
+ assertUsableCredential(name, path, value);
4259
+ return sealCredential({
4260
+ apiKey: value,
4261
+ tier: "profile",
4262
+ source: path,
4263
+ deliberate: true,
4264
+ deprecated: false,
4265
+ diskCandidates: paths,
4266
+ warning: null
4267
+ });
4268
+ }
4269
+ }
4270
+ throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
4271
+ }
4272
+ const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
4273
+ if (diskHits.length > 0) {
4274
+ const winner = diskHits[0];
4275
+ assertUsableCredential(name, winner.path, winner.value);
4276
+ const divergentSources = [
4277
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
4278
+ ...(() => {
4279
+ const legacyHit = firstEnvValue(env, apiKeyKeys);
4280
+ return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
4281
+ })()
4282
+ ];
4283
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
4284
+ return sealCredential({
4285
+ apiKey: winner.value,
4286
+ tier: "disk",
4287
+ source: winner.path,
4288
+ deliberate: false,
4289
+ deprecated: false,
4290
+ diskCandidates: diskPaths,
4291
+ warning
4292
+ });
4293
+ }
4294
+ const legacy = firstEnvValue(env, apiKeyKeys);
4295
+ if (legacy) {
4296
+ assertUsableCredential(name, legacy.key, legacy.value);
4297
+ const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
4298
+ const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
4299
+ const sink = options.onDeprecation ?? defaultDeprecationSink;
4300
+ const notified = deprecationNotified();
4301
+ if (!notified.has(name)) {
4302
+ notified.add(name);
4303
+ sink(message);
4304
+ }
4305
+ return sealCredential({
4306
+ apiKey: legacy.value,
4307
+ tier: "legacy-env",
4308
+ source: legacy.key,
4309
+ deliberate: false,
4310
+ deprecated: true,
4311
+ diskCandidates: diskPaths,
4312
+ warning: message
4313
+ });
4314
+ }
4315
+ return null;
4316
+ }
4317
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, HASNA_STATE_DIR = ".hasna", FLEET_CREDENTIAL_DIR = "cloud", CONFIG_DIR = ".config", CONFIG_NAMESPACE = "hasna", MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, INSPECT_CUSTOM, CREDENTIAL_SEAL, DEPRECATION_REGISTRY, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
4318
+ var init_transport = __esm(() => {
4319
+ CredentialResolutionError = class CredentialResolutionError extends Error {
4320
+ appName;
4321
+ attempted;
4322
+ constructor(appName, message, attempted) {
4323
+ super(message);
4324
+ this.name = "CredentialResolutionError";
4325
+ this.appName = appName;
4326
+ this.attempted = attempted;
4327
+ }
4328
+ };
4329
+ MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
4330
+ SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
4331
+ SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
4332
+ ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
4333
+ INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
4334
+ CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
4335
+ DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
4336
+ IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
4337
+ AUTHORITY_OVERRIDE_HEADERS = new Set([
4338
+ "host",
4339
+ ":authority",
4340
+ "forwarded",
4341
+ "x-forwarded-host",
4342
+ "x-original-host"
4343
+ ]);
4344
+ });
4345
+
4062
4346
  // src/config.ts
4063
4347
  var exports_config = {};
4064
4348
  __export(exports_config, {
@@ -4071,43 +4355,43 @@ __export(exports_config, {
4071
4355
  getCustomHooksDir: () => getCustomHooksDir,
4072
4356
  getConfigPath: () => getConfigPath
4073
4357
  });
4074
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
4075
- import { join as join2 } from "path";
4358
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
4359
+ import { join as join3 } from "path";
4076
4360
  import { homedir as homedir2 } from "os";
4077
4361
  function getHooksDataDir() {
4078
4362
  const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4079
4363
  if (explicit)
4080
4364
  return explicit;
4081
- return join2(homedir2(), ".hasna", "hooks");
4365
+ return join3(homedir2(), ".hasna", "hooks");
4082
4366
  }
4083
4367
  function getCustomHooksDir() {
4084
- return join2(getHooksDataDir(), "hooks");
4368
+ return join3(getHooksDataDir(), "hooks");
4085
4369
  }
4086
4370
  function getLockPath() {
4087
4371
  const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
4088
4372
  if (explicit)
4089
4373
  return explicit;
4090
- return join2(getHooksDataDir(), "hooks.lock");
4374
+ return join3(getHooksDataDir(), "hooks.lock");
4091
4375
  }
4092
4376
  function getConfigPath() {
4093
4377
  const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
4094
4378
  if (explicit)
4095
4379
  return explicit;
4096
- return join2(getHooksDataDir(), "config.json");
4380
+ return join3(getHooksDataDir(), "config.json");
4097
4381
  }
4098
4382
  function readConfig() {
4099
4383
  try {
4100
4384
  const path = getConfigPath();
4101
4385
  if (!existsSync2(path))
4102
4386
  return {};
4103
- return JSON.parse(readFileSync(path, "utf-8"));
4387
+ return JSON.parse(readFileSync2(path, "utf-8"));
4104
4388
  } catch {
4105
4389
  return {};
4106
4390
  }
4107
4391
  }
4108
4392
  function writeConfig(config) {
4109
4393
  const path = getConfigPath();
4110
- mkdirSync(join2(path, ".."), { recursive: true });
4394
+ mkdirSync(join3(path, ".."), { recursive: true });
4111
4395
  writeFileSync(path, JSON.stringify(config, null, 2) + `
4112
4396
  `, "utf-8");
4113
4397
  return path;
@@ -4120,9 +4404,11 @@ function resolveApiUrl() {
4120
4404
  return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
4121
4405
  }
4122
4406
  function resolveApiKey() {
4123
- return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
4407
+ return resolveCredential("hooks", process.env)?.apiKey;
4124
4408
  }
4125
- var init_config = () => {};
4409
+ var init_config = __esm(() => {
4410
+ init_transport();
4411
+ });
4126
4412
 
4127
4413
  // src/lib/semver.ts
4128
4414
  var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
@@ -4148,8 +4434,8 @@ __export(exports_manifest, {
4148
4434
  ScriptContainmentError: () => ScriptContainmentError,
4149
4435
  HOOK_NAME_RE: () => HOOK_NAME_RE
4150
4436
  });
4151
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, writeFileSync as writeFileSync2 } from "fs";
4152
- import { basename, dirname, isAbsolute, join as join3, normalize, resolve, sep } from "path";
4437
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync, realpathSync, writeFileSync as writeFileSync2 } from "fs";
4438
+ import { basename, dirname, isAbsolute, join as join4, normalize, resolve, sep } from "path";
4153
4439
  import { z } from "zod";
4154
4440
  function parseManifest(json) {
4155
4441
  const parsed = JSON.parse(json);
@@ -4182,7 +4468,7 @@ function realpathNearest(path) {
4182
4468
  tail.unshift(basename(cur));
4183
4469
  cur = parent;
4184
4470
  }
4185
- return join3(realpathSync(cur), ...tail);
4471
+ return join4(realpathSync(cur), ...tail);
4186
4472
  }
4187
4473
  function assertContainedReal(root, target, raw) {
4188
4474
  const rootReal = realpathSync(root);
@@ -4215,25 +4501,25 @@ function resolveScript(manifest, manifestDir) {
4215
4501
  if (isInlineScript(manifest))
4216
4502
  return { path: scriptRelFor(manifest), content: manifest.script };
4217
4503
  const rel = scriptRelFor(manifest);
4218
- const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
4504
+ const resolved = assertContained(join4(manifestDir, rel), manifestDir, manifest.script);
4219
4505
  if (!existsSync3(resolved)) {
4220
4506
  throw new Error(`manifest script file not found: ${resolved}`);
4221
4507
  }
4222
- return { path: rel, content: readFileSync2(resolved, "utf-8") };
4508
+ return { path: rel, content: readFileSync3(resolved, "utf-8") };
4223
4509
  }
4224
4510
  function customHookDir(name) {
4225
- return join3(getCustomHooksDir(), shortManifestName(name));
4511
+ return join4(getCustomHooksDir(), shortManifestName(name));
4226
4512
  }
4227
4513
  function readCustomManifest(name) {
4228
4514
  const dir = customHookDir(name);
4229
- const manifestPath = join3(dir, "manifest.json");
4515
+ const manifestPath = join4(dir, "manifest.json");
4230
4516
  if (!existsSync3(manifestPath))
4231
4517
  return;
4232
4518
  try {
4233
- const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
4519
+ const manifest = parseManifest(readFileSync3(manifestPath, "utf-8"));
4234
4520
  const script = resolveScript(manifest, dir);
4235
4521
  const scriptIsInline = isInlineScript(manifest);
4236
- return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline };
4522
+ return { manifest, scriptPath: join4(dir, script.path), scriptContent: script.content, scriptIsInline };
4237
4523
  } catch (err) {
4238
4524
  if (err instanceof ScriptContainmentError)
4239
4525
  throw err;
@@ -4257,7 +4543,7 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
4257
4543
  if (isAbsolute(scriptRelPath)) {
4258
4544
  throw new ScriptContainmentError(scriptRelPath, dir);
4259
4545
  }
4260
- const scriptPath = scriptRelPath.includes("/") ? join3(dir, scriptRelPath) : join3(dir, scriptRelPath);
4546
+ const scriptPath = scriptRelPath.includes("/") ? join4(dir, scriptRelPath) : join4(dir, scriptRelPath);
4261
4547
  assertContainedLexical(scriptPath, dir, scriptRelPath);
4262
4548
  mkdirSync2(dir, { recursive: true });
4263
4549
  assertContainedReal(dir, scriptPath, scriptRelPath);
@@ -4265,7 +4551,7 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
4265
4551
  if (scriptDir !== dir)
4266
4552
  mkdirSync2(scriptDir, { recursive: true });
4267
4553
  writeFileSync2(scriptPath, scriptContent, "utf-8");
4268
- writeFileSync2(join3(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
4554
+ writeFileSync2(join4(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
4269
4555
  `, "utf-8");
4270
4556
  return { dir, scriptPath };
4271
4557
  }
@@ -4309,14 +4595,14 @@ __export(exports_resolve, {
4309
4595
  resolveHook: () => resolveHook,
4310
4596
  resolveBundledHooksDir: () => resolveBundledHooksDir
4311
4597
  });
4312
- import { dirname as dirname2, join as join4 } from "path";
4598
+ import { dirname as dirname2, join as join5 } from "path";
4313
4599
  import { existsSync as existsSync4 } from "fs";
4314
4600
  import { fileURLToPath } from "url";
4315
4601
  function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
4316
- const sourceLayout = join4(moduleDir, "..", "..", "hooks");
4317
- if (existsSync4(join4(sourceLayout, "hook-gitguard")))
4602
+ const sourceLayout = join5(moduleDir, "..", "..", "hooks");
4603
+ if (existsSync4(join5(sourceLayout, "hook-gitguard")))
4318
4604
  return sourceLayout;
4319
- return join4(moduleDir, "..", "hooks");
4605
+ return join5(moduleDir, "..", "hooks");
4320
4606
  }
4321
4607
  function resolveHookDir(name) {
4322
4608
  const custom = readCustomManifest(name);
@@ -4327,10 +4613,10 @@ function resolveHookDir(name) {
4327
4613
  return;
4328
4614
  const short = shortManifestName(name);
4329
4615
  const bundledBase = resolveBundledHooksDir();
4330
- const direct = join4(bundledBase, short);
4616
+ const direct = join5(bundledBase, short);
4331
4617
  if (existsSync4(direct))
4332
4618
  return direct;
4333
- const prefixed = join4(bundledBase, `hook-${short}`);
4619
+ const prefixed = join5(bundledBase, `hook-${short}`);
4334
4620
  return existsSync4(prefixed) ? prefixed : undefined;
4335
4621
  }
4336
4622
  function resolveScriptPath(name) {
@@ -4340,7 +4626,7 @@ function resolveScriptPath(name) {
4340
4626
  const dir = resolveHookDir(name);
4341
4627
  if (!dir)
4342
4628
  return;
4343
- const script = join4(dir, "src", "hook.ts");
4629
+ const script = join5(dir, "src", "hook.ts");
4344
4630
  return existsSync4(script) ? script : undefined;
4345
4631
  }
4346
4632
  function resolveHookMeta(name) {
@@ -4590,8 +4876,8 @@ var init_migrations = __esm(() => {
4590
4876
  });
4591
4877
 
4592
4878
  // src/db/legacy-import.ts
4593
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
4594
- import { join as join5 } from "path";
4879
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
4880
+ import { join as join6 } from "path";
4595
4881
  import { homedir as homedir3 } from "os";
4596
4882
  function ensureMetaTable(db) {
4597
4883
  db.exec(`
@@ -4615,7 +4901,7 @@ function nanoid() {
4615
4901
  function importJsonlFile(db, filePath) {
4616
4902
  let count = 0;
4617
4903
  try {
4618
- const lines = readFileSync3(filePath, "utf-8").split(`
4904
+ const lines = readFileSync4(filePath, "utf-8").split(`
4619
4905
  `).filter(Boolean);
4620
4906
  for (const line of lines) {
4621
4907
  try {
@@ -4641,7 +4927,7 @@ function importJsonlFile(db, filePath) {
4641
4927
  function importErrorsLog(db, filePath) {
4642
4928
  let count = 0;
4643
4929
  try {
4644
- const lines = readFileSync3(filePath, "utf-8").split(`
4930
+ const lines = readFileSync4(filePath, "utf-8").split(`
4645
4931
  `).filter(Boolean);
4646
4932
  const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
4647
4933
  for (const line of lines) {
@@ -4666,25 +4952,25 @@ function importErrorsLog(db, filePath) {
4666
4952
  } catch {}
4667
4953
  return count;
4668
4954
  }
4669
- function runLegacyImport(db, homeDir = homedir3()) {
4955
+ function runLegacyImport(db, homeDir2 = homedir3()) {
4670
4956
  try {
4671
4957
  if (isAlreadyDone(db))
4672
4958
  return;
4673
4959
  let total = 0;
4674
- const claudeProjectsDir = join5(homeDir, ".claude", "projects");
4960
+ const claudeProjectsDir = join6(homeDir2, ".claude", "projects");
4675
4961
  if (existsSync5(claudeProjectsDir)) {
4676
4962
  try {
4677
4963
  const projectDirs = readdirSync2(claudeProjectsDir);
4678
4964
  for (const dir of projectDirs) {
4679
- const projectDir = join5(claudeProjectsDir, dir);
4965
+ const projectDir = join6(claudeProjectsDir, dir);
4680
4966
  try {
4681
4967
  const files = readdirSync2(projectDir);
4682
4968
  for (const file of files) {
4683
4969
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
4684
- total += importJsonlFile(db, join5(projectDir, file));
4970
+ total += importJsonlFile(db, join6(projectDir, file));
4685
4971
  }
4686
4972
  if (file === "errors.log") {
4687
- total += importErrorsLog(db, join5(projectDir, file));
4973
+ total += importErrorsLog(db, join6(projectDir, file));
4688
4974
  }
4689
4975
  }
4690
4976
  } catch {}
@@ -4728,16 +5014,16 @@ __export(exports_db, {
4728
5014
  });
4729
5015
  import { Database } from "bun:sqlite";
4730
5016
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, cpSync } from "fs";
4731
- import { join as join6 } from "path";
5017
+ import { join as join7 } from "path";
4732
5018
  import { homedir as homedir4 } from "os";
4733
5019
  function resolveDataDir() {
4734
5020
  const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4735
5021
  if (explicit)
4736
5022
  return explicit;
4737
- const newDir = join6(homedir4(), ".hasna", "hooks");
4738
- const oldDir = join6(homedir4(), ".hooks");
5023
+ const newDir = join7(homedir4(), ".hasna", "hooks");
5024
+ const oldDir = join7(homedir4(), ".hooks");
4739
5025
  if (!existsSync6(newDir) && existsSync6(oldDir)) {
4740
- mkdirSync3(join6(homedir4(), ".hasna"), { recursive: true });
5026
+ mkdirSync3(join7(homedir4(), ".hasna"), { recursive: true });
4741
5027
  cpSync(oldDir, newDir, { recursive: true });
4742
5028
  }
4743
5029
  return newDir;
@@ -4747,7 +5033,7 @@ function getDbPath() {
4747
5033
  if (explicitDb)
4748
5034
  return explicitDb;
4749
5035
  const dataDir = resolveDataDir();
4750
- return join6(dataDir, "hooks.db");
5036
+ return join7(dataDir, "hooks.db");
4751
5037
  }
4752
5038
  function ensureDir(dbPath) {
4753
5039
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
@@ -4821,7 +5107,7 @@ __export(exports_store, {
4821
5107
  LockFileError: () => LockFileError
4822
5108
  });
4823
5109
  import { createHash } from "crypto";
4824
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync } from "fs";
5110
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync } from "fs";
4825
5111
  import { readFile as readFile2 } from "fs/promises";
4826
5112
  import { randomBytes } from "crypto";
4827
5113
  function sha256Of(input) {
@@ -4887,7 +5173,7 @@ function readLock() {
4887
5173
  return { hooks: {} };
4888
5174
  let raw;
4889
5175
  try {
4890
- raw = readFileSync4(path, "utf-8");
5176
+ raw = readFileSync5(path, "utf-8");
4891
5177
  } catch {
4892
5178
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
4893
5179
  }
@@ -4984,7 +5270,7 @@ async function verifyScriptHash(name, scriptPath) {
4984
5270
  return checkScriptHash(name, await sha256File(scriptPath));
4985
5271
  }
4986
5272
  function retrustHook(name, scriptPath, version, source) {
4987
- const actual = sha256Of(readFileSync4(scriptPath));
5273
+ const actual = sha256Of(readFileSync5(scriptPath));
4988
5274
  const db = getDb();
4989
5275
  upsertHookRecord(db, {
4990
5276
  name,
@@ -5025,8 +5311,8 @@ __export(exports_installer, {
5025
5311
  getHookPath: () => getHookPath,
5026
5312
  buildCodewithTomlFragment: () => buildCodewithTomlFragment
5027
5313
  });
5028
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
5029
- import { join as join7, dirname as dirname3 } from "path";
5314
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
5315
+ import { join as join8, dirname as dirname3 } from "path";
5030
5316
  import { homedir as homedir5 } from "os";
5031
5317
  import { fileURLToPath as fileURLToPath2 } from "url";
5032
5318
  function normalizeHookName(name) {
@@ -5066,16 +5352,16 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
5066
5352
  return globalOverride;
5067
5353
  const dir = getTargetSettingsDir(target);
5068
5354
  if (scope === "project") {
5069
- return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
5355
+ return target === "codewith" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
5070
5356
  }
5071
- return target === "codewith" ? join7(homedir5(), dir, "config.toml") : join7(homedir5(), dir, "settings.json");
5357
+ return target === "codewith" ? join8(homedir5(), dir, "config.toml") : join8(homedir5(), dir, "settings.json");
5072
5358
  }
5073
5359
  function getHookPath(name) {
5074
5360
  const shortName = shortHookName(name);
5075
5361
  const resolved = resolveHookDir(shortName);
5076
5362
  if (resolved)
5077
5363
  return resolved;
5078
- return join7(HOOKS_DIR, normalizeHookName(shortName));
5364
+ return join8(HOOKS_DIR, normalizeHookName(shortName));
5079
5365
  }
5080
5366
  function hookExists(name) {
5081
5367
  return existsSync8(getHookPath(name));
@@ -5084,7 +5370,7 @@ function readSettings(scope = "global", target = "claude") {
5084
5370
  const path = getSettingsPath(scope, target);
5085
5371
  try {
5086
5372
  if (existsSync8(path)) {
5087
- return JSON.parse(readFileSync5(path, "utf-8"));
5373
+ return JSON.parse(readFileSync6(path, "utf-8"));
5088
5374
  }
5089
5375
  } catch (error) {
5090
5376
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -5183,7 +5469,7 @@ function buildCodewithTomlFragment(name, profile) {
5183
5469
  function readCodewithConfig(scope, configPath) {
5184
5470
  const path = getSettingsPath(scope, "codewith", configPath);
5185
5471
  try {
5186
- return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
5472
+ return existsSync8(path) ? readFileSync6(path, "utf-8") : "";
5187
5473
  } catch {
5188
5474
  return "";
5189
5475
  }
@@ -5193,7 +5479,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
5193
5479
  const dir = dirname3(path);
5194
5480
  if (!existsSync8(dir))
5195
5481
  mkdirSync5(dir, { recursive: true });
5196
- const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
5482
+ const existing = existsSync8(path) ? readFileSync6(path, "utf-8") : "";
5197
5483
  const sep2 = existing.trim() ? `
5198
5484
 
5199
5485
  ` : "";
@@ -5238,7 +5524,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
5238
5524
  target,
5239
5525
  fragment,
5240
5526
  applied: false,
5241
- note: "Codewith install is fragment-only by default; open-configs should own applying this TOML."
5527
+ note: "Codewith install is fragment-only by default; configs should own applying this TOML."
5242
5528
  };
5243
5529
  }
5244
5530
  if (!codewithConfigPath) {
@@ -5265,7 +5551,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
5265
5551
  fragment,
5266
5552
  applied: true,
5267
5553
  configPath: path,
5268
- note: "Direct Codewith config write was explicitly requested; prefer open-configs for managed machines."
5554
+ note: "Direct Codewith config write was explicitly requested; prefer configs for managed machines."
5269
5555
  };
5270
5556
  } catch (error) {
5271
5557
  return {
@@ -5496,7 +5782,7 @@ function codewithHasHookEntry(name, scope = "global") {
5496
5782
  if (!existsSync8(path))
5497
5783
  return false;
5498
5784
  try {
5499
- return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
5785
+ return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
5500
5786
  } catch {
5501
5787
  return false;
5502
5788
  }
@@ -5528,7 +5814,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
5528
5814
  if (target === "codewith" || target === "all") {
5529
5815
  const codewithPath = getSettingsPath(scope, "codewith");
5530
5816
  if (existsSync8(codewithPath)) {
5531
- const before = readFileSync5(codewithPath, "utf-8");
5817
+ const before = readFileSync6(codewithPath, "utf-8");
5532
5818
  const after = removeCodewithHookEntry(before, shortName);
5533
5819
  if (after.removed) {
5534
5820
  writeFileSync4(codewithPath, after.text, "utf-8");
@@ -5574,7 +5860,7 @@ var init_installer = __esm(() => {
5574
5860
  init_manifest();
5575
5861
  init_store();
5576
5862
  __dirname2 = dirname3(fileURLToPath2(import.meta.url));
5577
- HOOKS_DIR = existsSync8(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
5863
+ HOOKS_DIR = existsSync8(join8(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join8(__dirname2, "..", "..", "hooks") : join8(__dirname2, "..", "hooks");
5578
5864
  EVENT_MAP = {
5579
5865
  claude: {
5580
5866
  PreToolUse: "PreToolUse",
@@ -5721,14 +6007,14 @@ var init_redact = __esm(() => {
5721
6007
  });
5722
6008
 
5723
6009
  // src/lib/profiles.ts
5724
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
5725
- import { join as join8 } from "path";
6010
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
6011
+ import { join as join9 } from "path";
5726
6012
  import { homedir as homedir6 } from "os";
5727
6013
  function resolveProfilesDir() {
5728
- const newDir = join8(homedir6(), ".hasna", "hooks", "profiles");
5729
- const oldDir = join8(homedir6(), ".hooks", "profiles");
6014
+ const newDir = join9(homedir6(), ".hasna", "hooks", "profiles");
6015
+ const oldDir = join9(homedir6(), ".hooks", "profiles");
5730
6016
  if (!existsSync9(newDir) && existsSync9(oldDir)) {
5731
- mkdirSync6(join8(homedir6(), ".hasna", "hooks"), { recursive: true });
6017
+ mkdirSync6(join9(homedir6(), ".hasna", "hooks"), { recursive: true });
5732
6018
  cpSync2(oldDir, newDir, { recursive: true });
5733
6019
  }
5734
6020
  return newDir;
@@ -5739,7 +6025,7 @@ function ensureProfilesDir() {
5739
6025
  }
5740
6026
  }
5741
6027
  function profilePath(id) {
5742
- return join8(PROFILES_DIR, `${id}.json`);
6028
+ return join9(PROFILES_DIR, `${id}.json`);
5743
6029
  }
5744
6030
  function shortUuid() {
5745
6031
  return crypto.randomUUID().slice(0, 8);
@@ -5767,7 +6053,7 @@ function getProfile(id) {
5767
6053
  try {
5768
6054
  if (!existsSync9(path))
5769
6055
  return null;
5770
- return JSON.parse(readFileSync6(path, "utf-8"));
6056
+ return JSON.parse(readFileSync7(path, "utf-8"));
5771
6057
  } catch {
5772
6058
  return null;
5773
6059
  }
@@ -5780,7 +6066,7 @@ function listProfiles() {
5780
6066
  const profiles = [];
5781
6067
  for (const file of files) {
5782
6068
  try {
5783
- const content = readFileSync6(join8(PROFILES_DIR, file), "utf-8");
6069
+ const content = readFileSync7(join9(PROFILES_DIR, file), "utf-8");
5784
6070
  profiles.push(JSON.parse(content));
5785
6071
  } catch {}
5786
6072
  }
@@ -5923,7 +6209,7 @@ var init_db_writer = __esm(() => {
5923
6209
 
5924
6210
  // src/lib/hook-env.ts
5925
6211
  import { dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
5926
- import { existsSync as existsSync10, statSync } from "fs";
6212
+ import { existsSync as existsSync10, statSync as statSync2 } from "fs";
5927
6213
  function isDeniedEnvName(name) {
5928
6214
  const upper = name.toUpperCase();
5929
6215
  if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
@@ -5950,7 +6236,7 @@ function isUnsafePathEntry(entry, home) {
5950
6236
  if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
5951
6237
  return true;
5952
6238
  try {
5953
- if ((statSync(entry).mode & 2) !== 0)
6239
+ if ((statSync2(entry).mode & 2) !== 0)
5954
6240
  return true;
5955
6241
  } catch {}
5956
6242
  return false;
@@ -6083,7 +6369,7 @@ __export(exports_run, {
6083
6369
  });
6084
6370
  import { randomBytes as randomBytes2 } from "crypto";
6085
6371
  import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
6086
- import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join9 } from "path";
6372
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join10 } from "path";
6087
6373
  function extensionOf(scriptPath) {
6088
6374
  const base = basename2(scriptPath);
6089
6375
  const dot = base.lastIndexOf(".");
@@ -6154,7 +6440,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
6154
6440
  async function executeVerifiedScript(options) {
6155
6441
  const scriptDir = dirname5(options.scriptPath);
6156
6442
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
6157
- const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6443
+ const tempPath = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6158
6444
  let proc = null;
6159
6445
  let timedOut = false;
6160
6446
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -6245,15 +6531,15 @@ __export(exports_custom_install, {
6245
6531
  installCustomSource: () => installCustomSource,
6246
6532
  ensureCustomDir: () => ensureCustomDir
6247
6533
  });
6248
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
6249
- import { join as join10 } from "path";
6534
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync4 } from "fs";
6535
+ import { join as join11 } from "path";
6250
6536
  import { tmpdir } from "os";
6251
6537
  function manifestAt(dir) {
6252
- const path = join10(dir, "manifest.json");
6538
+ const path = join11(dir, "manifest.json");
6253
6539
  if (!existsSync11(path))
6254
6540
  return;
6255
6541
  try {
6256
- return parseManifest(readFileSync7(path, "utf-8"));
6542
+ return parseManifest(readFileSync8(path, "utf-8"));
6257
6543
  } catch {
6258
6544
  return;
6259
6545
  }
@@ -6272,8 +6558,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
6272
6558
  return { name, version: manifest.version, kind, source, dir, scriptPath };
6273
6559
  }
6274
6560
  async function cloneToTemp(gitUrl) {
6275
- const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
6276
- const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
6561
+ const tmp = mkdtempSync(join11(tmpdir(), "hooks-install-"));
6562
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join11(tmp, "repo")], {
6277
6563
  stdout: "pipe",
6278
6564
  stderr: "pipe"
6279
6565
  });
@@ -6286,7 +6572,7 @@ async function cloneToTemp(gitUrl) {
6286
6572
  rmSync4(tmp, { recursive: true, force: true });
6287
6573
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
6288
6574
  }
6289
- return join10(tmp, "repo");
6575
+ return join11(tmp, "repo");
6290
6576
  }
6291
6577
  function isGitUrl(value) {
6292
6578
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -6314,7 +6600,7 @@ async function installCustomSource(source) {
6314
6600
  throw new Error(`No such local path: ${source}`);
6315
6601
  }
6316
6602
  if (source.endsWith("manifest.json")) {
6317
- const manifest = parseManifest(readFileSync7(source, "utf-8"));
6603
+ const manifest = parseManifest(readFileSync8(source, "utf-8"));
6318
6604
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
6319
6605
  }
6320
6606
  return copyManifestDir(source, source, "local");
@@ -6363,12 +6649,12 @@ __export(exports_sync, {
6363
6649
  commitSyncArtifacts: () => commitSyncArtifacts
6364
6650
  });
6365
6651
  import { createHash as createHash2 } from "crypto";
6366
- import { readFileSync as readFileSync8 } from "fs";
6652
+ import { readFileSync as readFileSync9 } from "fs";
6367
6653
  function sha256OfText(text) {
6368
6654
  return createHash2("sha256").update(text).digest("hex");
6369
6655
  }
6370
6656
  function sha256OfPath(path) {
6371
- return createHash2("sha256").update(readFileSync8(path)).digest("hex");
6657
+ return createHash2("sha256").update(readFileSync9(path)).digest("hex");
6372
6658
  }
6373
6659
  function collectBundledCatalog() {
6374
6660
  const out = [];
@@ -7114,6 +7400,160 @@ function secureEqual(a, b) {
7114
7400
  return diff === 0;
7115
7401
  }
7116
7402
 
7403
+ // src/openapi.ts
7404
+ var openApiDocument;
7405
+ var init_openapi = __esm(() => {
7406
+ openApiDocument = {
7407
+ openapi: "3.1.0",
7408
+ info: {
7409
+ title: "Hooks Registry API",
7410
+ version: "0.1.0",
7411
+ description: "Local registry HTTP API for @hasna/hooks: catalog, lock, and artifact reads are open; publishing (PUT /api/v1/hooks) requires the API key (authorization: Bearer or x-api-key)."
7412
+ },
7413
+ servers: [{ url: "/" }],
7414
+ paths: {
7415
+ "/health": {
7416
+ get: {
7417
+ summary: "Liveness probe",
7418
+ operationId: "getHealth",
7419
+ responses: {
7420
+ "200": {
7421
+ description: "Service is alive",
7422
+ content: {
7423
+ "application/json": {
7424
+ schema: { type: "object", additionalProperties: true }
7425
+ }
7426
+ }
7427
+ }
7428
+ }
7429
+ }
7430
+ },
7431
+ "/ready": {
7432
+ get: {
7433
+ summary: "Readiness probe \u2014 the hooks.lock store is readable",
7434
+ operationId: "getReady",
7435
+ responses: {
7436
+ "200": { description: "Lock store is readable" },
7437
+ "503": { description: "Lock store is not readable" }
7438
+ }
7439
+ }
7440
+ },
7441
+ "/version": {
7442
+ get: {
7443
+ summary: "Package version",
7444
+ operationId: "getVersion",
7445
+ responses: {
7446
+ "200": {
7447
+ description: "The installed @hasna/hooks version",
7448
+ content: {
7449
+ "application/json": {
7450
+ schema: { type: "object", properties: { version: { type: "string" } } }
7451
+ }
7452
+ }
7453
+ }
7454
+ }
7455
+ }
7456
+ },
7457
+ "/openapi.json": {
7458
+ get: {
7459
+ summary: "This OpenAPI document",
7460
+ operationId: "getOpenApi",
7461
+ responses: { "200": { description: "OpenAPI 3.1 document" } }
7462
+ }
7463
+ },
7464
+ "/api/v1/catalog": {
7465
+ get: {
7466
+ summary: "List the full hook catalog with versions and sha256",
7467
+ operationId: "getCatalog",
7468
+ responses: {
7469
+ "200": {
7470
+ description: "Catalog entries",
7471
+ content: {
7472
+ "application/json": {
7473
+ schema: {
7474
+ type: "object",
7475
+ properties: {
7476
+ hooks: {
7477
+ type: "array",
7478
+ items: {
7479
+ type: "object",
7480
+ properties: {
7481
+ name: { type: "string" },
7482
+ version: { type: "string" },
7483
+ sha256: { type: "string" },
7484
+ events: { type: "array", items: { type: "string" } }
7485
+ }
7486
+ }
7487
+ }
7488
+ }
7489
+ }
7490
+ }
7491
+ }
7492
+ }
7493
+ }
7494
+ }
7495
+ },
7496
+ "/api/v1/lock": {
7497
+ get: {
7498
+ summary: "Read the published hooks.lock with per-hook versions",
7499
+ operationId: "getLock",
7500
+ responses: { "200": { description: "The lock file" } }
7501
+ }
7502
+ },
7503
+ "/api/v1/hooks/{name}/{version}": {
7504
+ get: {
7505
+ summary: "Fetch one hook's manifest and script",
7506
+ operationId: "getHookArtifact",
7507
+ parameters: [
7508
+ { name: "name", in: "path", required: true, schema: { type: "string" } },
7509
+ { name: "version", in: "path", required: true, schema: { type: "string" } }
7510
+ ],
7511
+ responses: {
7512
+ "200": {
7513
+ description: "Hook artifact; x-hook-sha256 header carries the script hash"
7514
+ },
7515
+ "400": { description: "Invalid URL encoding or semver version" },
7516
+ "404": { description: "Hook not found locally" }
7517
+ }
7518
+ }
7519
+ },
7520
+ "/api/v1/hooks": {
7521
+ put: {
7522
+ summary: "Publish/retrust a hook (requires the API key)",
7523
+ operationId: "putHook",
7524
+ requestBody: {
7525
+ required: true,
7526
+ content: {
7527
+ "application/json": {
7528
+ schema: {
7529
+ type: "object",
7530
+ properties: { name: { type: "string" }, version: { type: "string" } }
7531
+ }
7532
+ }
7533
+ }
7534
+ },
7535
+ responses: {
7536
+ "200": { description: "Hook retrusted and pin updated" },
7537
+ "401": { description: "Missing or invalid API key" },
7538
+ "404": { description: "Hook not found in local store" },
7539
+ "409": { description: "Version mismatch with the local store" }
7540
+ },
7541
+ security: [{ apiKey: [] }]
7542
+ }
7543
+ }
7544
+ },
7545
+ components: {
7546
+ securitySchemes: {
7547
+ apiKey: {
7548
+ type: "http",
7549
+ scheme: "bearer",
7550
+ description: "The registry API key (HASNA_HOOKS_API_KEY / HOOKS_API_KEY)."
7551
+ }
7552
+ }
7553
+ }
7554
+ };
7555
+ });
7556
+
7117
7557
  // src/serve.ts
7118
7558
  var exports_serve = {};
7119
7559
  __export(exports_serve, {
@@ -7123,6 +7563,17 @@ __export(exports_serve, {
7123
7563
  SERVE_HOST: () => SERVE_HOST,
7124
7564
  DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
7125
7565
  });
7566
+ import { readFileSync as readFileSync10 } from "fs";
7567
+ import { dirname as dirname6, join as join12 } from "path";
7568
+ import { fileURLToPath as fileURLToPath3 } from "url";
7569
+ function packageVersion() {
7570
+ try {
7571
+ const pkgPath = join12(dirname6(fileURLToPath3(import.meta.url)), "..", "package.json");
7572
+ return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
7573
+ } catch {
7574
+ return "0.0.0";
7575
+ }
7576
+ }
7126
7577
  async function buildCatalog() {
7127
7578
  const byName = new Map;
7128
7579
  for (const meta of HOOKS) {
@@ -7195,6 +7646,20 @@ function handleServeRequest(req, apiKey) {
7195
7646
  if (url.pathname === "/health" && req.method === "GET") {
7196
7647
  return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
7197
7648
  }
7649
+ if (url.pathname === "/ready" && req.method === "GET") {
7650
+ try {
7651
+ readLock();
7652
+ return Promise.resolve(json({ status: "ready", name: SERVE_SERVICE_NAME }));
7653
+ } catch {
7654
+ return Promise.resolve(json({ status: "not-ready", name: SERVE_SERVICE_NAME }, 503));
7655
+ }
7656
+ }
7657
+ if (url.pathname === "/version" && req.method === "GET") {
7658
+ return Promise.resolve(json({ version: packageVersion() }));
7659
+ }
7660
+ if (url.pathname === "/openapi.json" && req.method === "GET") {
7661
+ return Promise.resolve(json(openApiDocument));
7662
+ }
7198
7663
  if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
7199
7664
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
7200
7665
  }
@@ -7273,6 +7738,7 @@ var init_serve = __esm(() => {
7273
7738
  init_store();
7274
7739
  init_resolve();
7275
7740
  init_config();
7741
+ init_openapi();
7276
7742
  init_semver();
7277
7743
  if (false) {}
7278
7744
  });
@@ -7373,9 +7839,9 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
7373
7839
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7374
7840
  import { z as z2 } from "zod";
7375
7841
  import { createServer } from "http";
7376
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
7377
- import { join as join11, dirname as dirname6 } from "path";
7378
- import { fileURLToPath as fileURLToPath3 } from "url";
7842
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
7843
+ import { join as join13, dirname as dirname7 } from "path";
7844
+ import { fileURLToPath as fileURLToPath4 } from "url";
7379
7845
  async function runVerifiedHook(name, stdin, timeoutMs) {
7380
7846
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
7381
7847
  const resolved = resolveHook2(name);
@@ -7383,7 +7849,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
7383
7849
  if (!resolved || !hookScript || !existsSync12(hookScript)) {
7384
7850
  throw new Error(`Hook '${name}' not found`);
7385
7851
  }
7386
- const content = readFileSync9(hookScript);
7852
+ const content = readFileSync11(hookScript);
7387
7853
  const sha = sha256Of(content);
7388
7854
  const check = checkScriptHash(name, sha);
7389
7855
  if (!check.ok) {
@@ -7608,14 +8074,14 @@ function createHooksServer() {
7608
8074
  continue;
7609
8075
  }
7610
8076
  const hookDir = getHookPath(name);
7611
- if (!existsSync12(join11(hookDir, "src", "hook.ts"))) {
8077
+ if (!existsSync12(join13(hookDir, "src", "hook.ts"))) {
7612
8078
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
7613
8079
  hookHealthy = false;
7614
8080
  }
7615
8081
  }
7616
8082
  if (meta && settingsExist) {
7617
8083
  try {
7618
- const settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
8084
+ const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
7619
8085
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
7620
8086
  const eventName = meta.event.split(":")[0];
7621
8087
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -7645,10 +8111,10 @@ function createHooksServer() {
7645
8111
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
7646
8112
  }
7647
8113
  const hookPath = getHookPath(name);
7648
- const readmePath = join11(hookPath, "README.md");
8114
+ const readmePath = join13(hookPath, "README.md");
7649
8115
  let readme = "";
7650
8116
  if (existsSync12(readmePath)) {
7651
- readme = readFileSync9(readmePath, "utf-8");
8117
+ readme = readFileSync11(readmePath, "utf-8");
7652
8118
  }
7653
8119
  if (verbose) {
7654
8120
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
@@ -7978,7 +8444,7 @@ function createHooksServer() {
7978
8444
  let settings = {};
7979
8445
  try {
7980
8446
  if (existsSync12(settingsPath))
7981
- settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
8447
+ settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
7982
8448
  } catch {}
7983
8449
  if (!settings.hooks)
7984
8450
  settings.hooks = {};
@@ -7987,8 +8453,8 @@ function createHooksServer() {
7987
8453
  disabled.push(name);
7988
8454
  settings.hooks.__disabled = disabled;
7989
8455
  const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
7990
- const { dirname: dirname7 } = await import("path");
7991
- mkdirSync8(dirname7(settingsPath), { recursive: true });
8456
+ const { dirname: dirname8 } = await import("path");
8457
+ mkdirSync8(dirname8(settingsPath), { recursive: true });
7992
8458
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
7993
8459
  `);
7994
8460
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -8001,7 +8467,7 @@ function createHooksServer() {
8001
8467
  let settings = {};
8002
8468
  try {
8003
8469
  if (existsSync12(settingsPath))
8004
- settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
8470
+ settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
8005
8471
  } catch {}
8006
8472
  if (settings.hooks?.__disabled) {
8007
8473
  settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
@@ -8361,13 +8827,13 @@ var init_server = __esm(() => {
8361
8827
  init_store();
8362
8828
  init_redact();
8363
8829
  init_storage();
8364
- __dirname3 = dirname6(fileURLToPath3(import.meta.url));
8830
+ __dirname3 = dirname7(fileURLToPath4(import.meta.url));
8365
8831
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
8366
8832
  try {
8367
8833
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
8368
- const p = join11(__dirname3, rel);
8834
+ const p = join13(__dirname3, rel);
8369
8835
  if (existsSync12(p)) {
8370
- pkg = JSON.parse(readFileSync9(p, "utf-8"));
8836
+ pkg = JSON.parse(readFileSync11(p, "utf-8"));
8371
8837
  break;
8372
8838
  }
8373
8839
  }
@@ -9638,9 +10104,9 @@ var {
9638
10104
 
9639
10105
  // src/cli/index.tsx
9640
10106
  import chalk2 from "chalk";
9641
- import { existsSync as existsSync13, readFileSync as readFileSync10, rmSync as rmSync5 } from "fs";
9642
- import { join as join12, dirname as dirname7 } from "path";
9643
- import { fileURLToPath as fileURLToPath4 } from "url";
10107
+ import { existsSync as existsSync13, readFileSync as readFileSync12, rmSync as rmSync5 } from "fs";
10108
+ import { join as join14, dirname as dirname8 } from "path";
10109
+ import { fileURLToPath as fileURLToPath5 } from "url";
9644
10110
 
9645
10111
  // src/cli/components/App.tsx
9646
10112
  import { useState as useState7 } from "react";
@@ -10821,9 +11287,9 @@ init_resolve();
10821
11287
  init_store();
10822
11288
  init_semver();
10823
11289
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
10824
- var __dirname4 = dirname7(fileURLToPath4(import.meta.url));
10825
- var pkgPath = existsSync13(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
10826
- var pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
11290
+ var __dirname4 = dirname8(fileURLToPath5(import.meta.url));
11291
+ var pkgPath = existsSync13(join14(__dirname4, "..", "package.json")) ? join14(__dirname4, "..", "package.json") : join14(__dirname4, "..", "..", "package.json");
11292
+ var pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
10827
11293
  var program2 = new Command;
10828
11294
  function resolveScope(options) {
10829
11295
  if (options.project)
@@ -10857,7 +11323,7 @@ function projectLogRows2(rows) {
10857
11323
  function readToken(tokenFile) {
10858
11324
  if (tokenFile) {
10859
11325
  try {
10860
- return readFileSync10(tokenFile, "utf-8").trim();
11326
+ return readFileSync12(tokenFile, "utf-8").trim();
10861
11327
  } catch {
10862
11328
  return;
10863
11329
  }
@@ -10994,7 +11460,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
10994
11460
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
10995
11461
  process.exit(1);
10996
11462
  }
10997
- const content = readFileSync10(hookScript);
11463
+ const content = readFileSync12(hookScript);
10998
11464
  const sha = sha256Of2(content);
10999
11465
  const check = checkScriptHash2(hook, sha);
11000
11466
  if (!check.ok) {
@@ -11100,7 +11566,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
11100
11566
  process.stderr.write(stderr);
11101
11567
  process.exit(exitCode);
11102
11568
  });
11103
- 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, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
11569
+ 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 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, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
11104
11570
  const scope = resolveScope(options);
11105
11571
  const target = resolveTarget(options);
11106
11572
  let toInstall = hooks;
@@ -11288,7 +11754,7 @@ Dry run \u2014 would install (${scope}, ${target}):
11288
11754
  return;
11289
11755
  }
11290
11756
  const successCount = results.filter((r) => r.success).length;
11291
- const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (open-configs should apply)" : scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
11757
+ const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
11292
11758
  console.log(chalk2.bold(`
11293
11759
  Installing hooks (${scope}, ${target})...
11294
11760
  `));
@@ -11570,7 +12036,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11570
12036
  let wiringCount = 0;
11571
12037
  if (settingsExist) {
11572
12038
  try {
11573
- const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
12039
+ const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
11574
12040
  wiringCount = countSettingsWiring(settings);
11575
12041
  } catch {}
11576
12042
  }
@@ -11590,7 +12056,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11590
12056
  continue;
11591
12057
  }
11592
12058
  const hookDir = getHookPath(name);
11593
- const hookScript = join12(hookDir, "src", "hook.ts");
12059
+ const hookScript = join14(hookDir, "src", "hook.ts");
11594
12060
  if (!existsSync13(hookScript)) {
11595
12061
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
11596
12062
  hookHealthy = false;
@@ -11598,7 +12064,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11598
12064
  }
11599
12065
  if (meta && settingsExist) {
11600
12066
  try {
11601
- const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
12067
+ const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
11602
12068
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
11603
12069
  const eventName = meta.event.split(":")[0];
11604
12070
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -11765,10 +12231,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
11765
12231
  return;
11766
12232
  }
11767
12233
  const hookPath = getHookPath(hook);
11768
- const readmePath = join12(hookPath, "README.md");
12234
+ const readmePath = join14(hookPath, "README.md");
11769
12235
  let readme = "";
11770
12236
  if (existsSync13(readmePath)) {
11771
- readme = readFileSync10(readmePath, "utf-8");
12237
+ readme = readFileSync12(readmePath, "utf-8");
11772
12238
  }
11773
12239
  if (options.json) {
11774
12240
  console.log(JSON.stringify({ ...meta, readme }));
@@ -11822,7 +12288,7 @@ ${meta.displayName} v${meta.version}
11822
12288
  installation: {
11823
12289
  global: "hooks install gitguard",
11824
12290
  project: "hooks install gitguard --project",
11825
- codewith: "hooks install session-start --target codewith # emits TOML for open-configs to apply",
12291
+ codewith: "hooks install session-start --target codewith # emits TOML for configs to apply",
11826
12292
  category: 'hooks install --category "Git Safety"',
11827
12293
  all: "hooks install --all"
11828
12294
  },
@@ -11965,9 +12431,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
11965
12431
  if (file === "-") {
11966
12432
  raw = await new Response(Bun.stdin.stream()).text();
11967
12433
  } else {
11968
- const { readFileSync: readFileSync11 } = await import("fs");
12434
+ const { readFileSync: readFileSync13 } = await import("fs");
11969
12435
  try {
11970
- raw = readFileSync11(file, "utf-8");
12436
+ raw = readFileSync13(file, "utf-8");
11971
12437
  } catch {
11972
12438
  if (options.json) {
11973
12439
  console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));