@hasna/hooks 0.7.9 → 0.7.10

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/serve.js CHANGED
@@ -15,6 +15,128 @@ var __export = (target, all) => {
15
15
  });
16
16
  };
17
17
 
18
+ // src/lib/app-home.ts
19
+ import { existsSync } from "fs";
20
+ import { homedir as homedir2 } from "os";
21
+ import { join as join2, resolve } from "path";
22
+
23
+ // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
24
+ import { homedir } from "os";
25
+ import { join } from "path";
26
+ var KIND_ENV = {
27
+ config: "HASNA_CONFIG_HOME",
28
+ data: "HASNA_DATA_HOME",
29
+ state: "HASNA_STATE_HOME",
30
+ cache: "HASNA_CACHE_HOME"
31
+ };
32
+ var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
33
+ function assertApp(app) {
34
+ if (typeof app !== "string" || app.length === 0) {
35
+ throw new TypeError("paths: app must be a non-empty string");
36
+ }
37
+ if (!APP_SLUG_RE.test(app)) {
38
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
39
+ }
40
+ }
41
+ function envOf(options) {
42
+ return options.env ?? process.env;
43
+ }
44
+ function envValue(options, kind) {
45
+ const value = envOf(options)[KIND_ENV[kind]];
46
+ return typeof value === "string" && value.length > 0 ? value : undefined;
47
+ }
48
+ function isMacOS(platform) {
49
+ return platform === "darwin";
50
+ }
51
+ function baseDir(kind, options) {
52
+ const override = envValue(options, kind);
53
+ if (override)
54
+ return override;
55
+ const home = options.home ?? homedir();
56
+ const platform = options.platform ?? process.platform;
57
+ if (isMacOS(platform)) {
58
+ switch (kind) {
59
+ case "config":
60
+ case "data":
61
+ return join(home, "Library", "Application Support", "Hasna");
62
+ case "cache":
63
+ return join(home, "Library", "Caches", "Hasna");
64
+ case "state":
65
+ return join(home, "Library", "Logs", "Hasna");
66
+ }
67
+ }
68
+ switch (kind) {
69
+ case "config":
70
+ return join(home, ".config", "hasna");
71
+ case "data":
72
+ return join(home, ".local", "share", "hasna");
73
+ case "state":
74
+ return join(home, ".local", "state", "hasna");
75
+ case "cache":
76
+ return join(home, ".cache", "hasna");
77
+ }
78
+ }
79
+ function resolvePath(kind, options) {
80
+ assertApp(options.app);
81
+ const appSegment = options.internal === true ? join("internal", options.app) : options.app;
82
+ return join(baseDir(kind, options), appSegment);
83
+ }
84
+ function dataDir(options) {
85
+ return resolvePath("data", options);
86
+ }
87
+
88
+ // src/lib/app-home.ts
89
+ var STORE_MARKER = "hooks.db";
90
+ function getHomeDir(env = process.env) {
91
+ return env.HOME || env.USERPROFILE || homedir2();
92
+ }
93
+ function getResolverDataRoot(env = process.env) {
94
+ return dataDir({ app: "hooks", home: getHomeDir(env), env });
95
+ }
96
+ function getLegacyDataRoot(env = process.env) {
97
+ return join2(getHomeDir(env), ".hasna", "hooks");
98
+ }
99
+ function adoptResolverDataRoot(resolved, env = process.env) {
100
+ const dataOverride = env.HASNA_DATA_HOME;
101
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
102
+ return true;
103
+ return existsSync(join2(resolved, STORE_MARKER));
104
+ }
105
+ function getExplicitDataDir(env = process.env) {
106
+ const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
107
+ if (typeof dir === "string" && dir.trim().length > 0)
108
+ return dir.trim();
109
+ return;
110
+ }
111
+ function getExactDataRoot(env = process.env) {
112
+ const dir = env.HASNA_HOOKS_HOME?.trim() || env.HOOKS_HOME?.trim();
113
+ if (dir)
114
+ return resolve(dir);
115
+ return;
116
+ }
117
+ function getEffectiveDataRoot(env = process.env) {
118
+ const explicit = getExplicitDataDir(env);
119
+ if (explicit)
120
+ return explicit;
121
+ const exact = getExactDataRoot(env);
122
+ if (exact)
123
+ return exact;
124
+ const resolved = getResolverDataRoot(env);
125
+ return adoptResolverDataRoot(resolved, env) ? resolve(resolved) : getLegacyDataRoot(env);
126
+ }
127
+ function getReportedDbPath(env = process.env) {
128
+ const explicit = getExplicitDbPath(env);
129
+ if (explicit)
130
+ return explicit;
131
+ return join2(getEffectiveDataRoot(env), "hooks.db");
132
+ }
133
+ function getExplicitDbPath(env = process.env) {
134
+ const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
135
+ if (typeof db === "string" && db.trim().length > 0)
136
+ return db.trim();
137
+ return;
138
+ }
139
+
18
140
  // src/lib/registry.ts
19
141
  var HOOKS = [
20
142
  {
@@ -331,7 +453,7 @@ var HOOKS = [
331
453
  {
332
454
  name: "sessionlog",
333
455
  displayName: "Session Log",
334
- description: "Logs every tool call to SQLite (~/.hasna/hooks/hooks.db)",
456
+ description: `Logs every tool call to SQLite (${getReportedDbPath()})`,
335
457
  version: "0.1.0",
336
458
  category: "Observability",
337
459
  event: "PostToolUse",
@@ -341,7 +463,7 @@ var HOOKS = [
341
463
  {
342
464
  name: "commandlog",
343
465
  displayName: "Command Log",
344
- description: "Logs every Bash command to SQLite (~/.hasna/hooks/hooks.db)",
466
+ description: `Logs every Bash command to SQLite (${getReportedDbPath()})`,
345
467
  version: "0.1.0",
346
468
  category: "Observability",
347
469
  event: "PostToolUse",
@@ -361,7 +483,7 @@ var HOOKS = [
361
483
  {
362
484
  name: "errornotify",
363
485
  displayName: "Error Notify",
364
- description: "Detects tool failures and logs errors to SQLite (~/.hasna/hooks/hooks.db)",
486
+ description: `Detects tool failures and logs errors to SQLite (${getReportedDbPath()})`,
365
487
  version: "0.1.0",
366
488
  category: "Observability",
367
489
  event: "PostToolUse",
@@ -534,8 +656,8 @@ function getHook(name) {
534
656
  }
535
657
 
536
658
  // src/lib/manifest.ts
537
- import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, realpathSync, writeFileSync } from "fs";
538
- import { basename, dirname, isAbsolute, join as join3, normalize, resolve, sep } from "path";
659
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, realpathSync, writeFileSync } from "fs";
660
+ import { basename, dirname, isAbsolute, join as join5, normalize, resolve as resolve2, sep } from "path";
539
661
 
540
662
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
541
663
  var exports_external = {};
@@ -4512,7 +4634,8 @@ var coerce = {
4512
4634
  var NEVER = INVALID;
4513
4635
  // ../contracts/dist/client/transport.js
4514
4636
  import { readFileSync, statSync } from "fs";
4515
- import { join } from "path";
4637
+ import { createRequire } from "module";
4638
+ import { join as join3 } from "path";
4516
4639
  function envToken(name) {
4517
4640
  return name.toUpperCase().replace(/-/g, "_");
4518
4641
  }
@@ -4527,6 +4650,9 @@ function credentialOverrideEnvKey(name) {
4527
4650
  return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
4528
4651
  }
4529
4652
  var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
4653
+ function credentialPointerEnvKey(name) {
4654
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
4655
+ }
4530
4656
 
4531
4657
  class CredentialResolutionError extends Error {
4532
4658
  appName;
@@ -4539,31 +4665,55 @@ class CredentialResolutionError extends Error {
4539
4665
  }
4540
4666
  }
4541
4667
  var HASNA_STATE_DIR = ".hasna";
4542
- var FLEET_CREDENTIAL_DIR = "cloud";
4668
+ var FLEET_CREDENTIAL_DIR = "fleet-env";
4669
+ var LEGACY_CLOUD_DIR = "cloud";
4543
4670
  var CONFIG_DIR = ".config";
4544
4671
  var CONFIG_NAMESPACE = "hasna";
4672
+ var LEGACY_CLOUD_REMOVAL_DEADLINE = "2026-10-01";
4545
4673
  var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
4546
4674
  var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
4547
4675
  var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
4548
4676
  var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
4677
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
4549
4678
  function homeDir(env) {
4550
4679
  const home = env.HOME?.trim();
4551
4680
  return home ? home : null;
4552
4681
  }
4553
- function credentialDiskSources(name, env) {
4554
- return profileDiskSources(name, env, null);
4555
- }
4556
- function profileDiskSources(name, env, profile) {
4682
+ function credentialDiskSourceList(name, env, profile = null) {
4557
4683
  const home = homeDir(env);
4558
4684
  if (!home || !SAFE_APP_SLUG.test(name))
4559
4685
  return [];
4560
4686
  const stem = profile ? `${name}.${profile}` : name;
4561
4687
  const configStem = profile ? `${name}-${profile}` : name;
4562
4688
  return [
4563
- join(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
4564
- join(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
4689
+ {
4690
+ path: join3(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
4691
+ tier: "fleet-env",
4692
+ deprecated: false
4693
+ },
4694
+ {
4695
+ path: join3(home, HASNA_STATE_DIR, LEGACY_CLOUD_DIR, `${stem}.env`),
4696
+ tier: "legacy-cloud",
4697
+ deprecated: true
4698
+ },
4699
+ {
4700
+ path: join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}.env`),
4701
+ tier: "config",
4702
+ deprecated: false
4703
+ },
4704
+ {
4705
+ path: join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`),
4706
+ tier: "config-legacy",
4707
+ deprecated: true
4708
+ }
4565
4709
  ];
4566
4710
  }
4711
+ function credentialDiskSources(name, env) {
4712
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
4713
+ }
4714
+ function profileDiskSources(name, env, profile) {
4715
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
4716
+ }
4567
4717
  function parseEnvFile(text) {
4568
4718
  const values = new Map;
4569
4719
  for (const rawLine of text.split(/\r?\n/)) {
@@ -4614,6 +4764,9 @@ function readCredentialFile(path, apiKeyKeys) {
4614
4764
  return null;
4615
4765
  }
4616
4766
  function assertUsableCredential(appName, source, value) {
4767
+ if (VAULT_POINTER_SHAPE.test(value)) {
4768
+ throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
4769
+ }
4617
4770
  if (!ILLEGAL_IN_HEADER_VALUE.test(value))
4618
4771
  return;
4619
4772
  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]);
@@ -4637,6 +4790,14 @@ function sealCredential(fields) {
4637
4790
  writable: false,
4638
4791
  configurable: false
4639
4792
  });
4793
+ if (fields.pointerVaultKey !== undefined) {
4794
+ Object.defineProperty(sealed, "pointerVaultKey", {
4795
+ value: fields.pointerVaultKey,
4796
+ enumerable: false,
4797
+ writable: false,
4798
+ configurable: false
4799
+ });
4800
+ }
4640
4801
  Object.defineProperty(sealed, INSPECT_CUSTOM, {
4641
4802
  value: () => ({ ...visible, apiKey: "[redacted]" }),
4642
4803
  enumerable: false,
@@ -4709,6 +4870,27 @@ function resolveCredential(name, env, options = {}) {
4709
4870
  warning: null
4710
4871
  });
4711
4872
  }
4873
+ const pointerKeyName = credentialPointerEnvKey(name);
4874
+ const pointerRaw = env[pointerKeyName];
4875
+ if (pointerRaw !== undefined) {
4876
+ const pointer = pointerRaw.trim();
4877
+ if (!pointer) {
4878
+ throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
4879
+ }
4880
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
4881
+ throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
4882
+ }
4883
+ return sealCredential({
4884
+ apiKey: "",
4885
+ pointerVaultKey: pointer,
4886
+ tier: "pointer",
4887
+ source: pointerKeyName,
4888
+ deliberate: true,
4889
+ deprecated: false,
4890
+ diskCandidates: diskPaths,
4891
+ warning: null
4892
+ });
4893
+ }
4712
4894
  const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
4713
4895
  if (profile) {
4714
4896
  const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
@@ -4733,26 +4915,42 @@ function resolveCredential(name, env, options = {}) {
4733
4915
  }
4734
4916
  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);
4735
4917
  }
4736
- const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
4918
+ const diskSourceList = credentialDiskSourceList(name, env, null);
4919
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
4737
4920
  if (diskHits.length > 0) {
4738
4921
  const winner = diskHits[0];
4739
- assertUsableCredential(name, winner.path, winner.value);
4922
+ assertUsableCredential(name, winner.src.path, winner.value);
4740
4923
  const divergentSources = [
4741
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
4924
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
4742
4925
  ...(() => {
4743
4926
  const legacyHit = firstEnvValue(env, apiKeyKeys);
4744
4927
  return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
4745
4928
  })()
4746
4929
  ];
4747
- 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;
4930
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.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;
4931
+ let deprecated = winner.src.deprecated;
4932
+ let finalWarning = warning;
4933
+ if (winner.src.deprecated) {
4934
+ deprecated = true;
4935
+ const sink = options.onDeprecation ?? defaultDeprecationSink;
4936
+ const notified = deprecationNotified();
4937
+ const noticeKey = `${name}:${winner.src.path}`;
4938
+ if (!notified.has(noticeKey)) {
4939
+ notified.add(noticeKey);
4940
+ const target = diskSourceList[0]?.path ?? "<none>";
4941
+ const message = `[${name}] DEPRECATED: the API key came from ${winner.src.path} \u2014 a legacy credential location. ` + `The primary location is ${target} (~/.hasna/fleet-env/<app>.env). The legacy 'cloud' tiers are ` + `removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}. Migrate the key to the primary location.`;
4942
+ sink(message);
4943
+ }
4944
+ finalWarning = [warning, `Legacy credential source: ${winner.src.path}. Removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}.`].filter(Boolean).join(" ") || null;
4945
+ }
4748
4946
  return sealCredential({
4749
4947
  apiKey: winner.value,
4750
- tier: "disk",
4751
- source: winner.path,
4948
+ tier: winner.src.tier,
4949
+ source: winner.src.path,
4752
4950
  deliberate: false,
4753
- deprecated: false,
4951
+ deprecated,
4754
4952
  diskCandidates: diskPaths,
4755
- warning
4953
+ warning: finalWarning
4756
4954
  });
4757
4955
  }
4758
4956
  const legacy = firstEnvValue(env, apiKeyKeys);
@@ -4778,6 +4976,8 @@ function resolveCredential(name, env, options = {}) {
4778
4976
  }
4779
4977
  return null;
4780
4978
  }
4979
+ var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
4980
+ var requireSecretsSdk = createRequire(import.meta.url);
4781
4981
  var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
4782
4982
  var AUTHORITY_OVERRIDE_HEADERS = new Set([
4783
4983
  "host",
@@ -4788,22 +4988,18 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
4788
4988
  ]);
4789
4989
 
4790
4990
  // src/config.ts
4791
- import { join as join2 } from "path";
4792
- import { homedir } from "os";
4991
+ import { join as join4 } from "path";
4793
4992
  function getHooksDataDir() {
4794
- const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4795
- if (explicit)
4796
- return explicit;
4797
- return join2(homedir(), ".hasna", "hooks");
4993
+ return getEffectiveDataRoot();
4798
4994
  }
4799
4995
  function getCustomHooksDir() {
4800
- return join2(getHooksDataDir(), "hooks");
4996
+ return join4(getHooksDataDir(), "hooks");
4801
4997
  }
4802
4998
  function getLockPath() {
4803
4999
  const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
4804
5000
  if (explicit)
4805
5001
  return explicit;
4806
- return join2(getHooksDataDir(), "hooks.lock");
5002
+ return join4(getHooksDataDir(), "hooks.lock");
4807
5003
  }
4808
5004
  function resolveApiKey() {
4809
5005
  return resolveCredential("hooks", process.env)?.apiKey;
@@ -4845,8 +5041,8 @@ function isWithin(child, parent) {
4845
5041
  return child === parent || child.startsWith(parent + sep);
4846
5042
  }
4847
5043
  function assertContainedLexical(target, root, raw) {
4848
- const lexical = resolve(target);
4849
- const rootLexical = resolve(root);
5044
+ const lexical = resolve2(target);
5045
+ const rootLexical = resolve2(root);
4850
5046
  if (!isWithin(lexical, rootLexical)) {
4851
5047
  throw new ScriptContainmentError(target, root, raw);
4852
5048
  }
@@ -4855,14 +5051,14 @@ function assertContainedLexical(target, root, raw) {
4855
5051
  function realpathNearest(path) {
4856
5052
  const tail = [];
4857
5053
  let cur = path;
4858
- while (!existsSync(cur)) {
5054
+ while (!existsSync2(cur)) {
4859
5055
  const parent = dirname(cur);
4860
5056
  if (parent === cur)
4861
5057
  break;
4862
5058
  tail.unshift(basename(cur));
4863
5059
  cur = parent;
4864
5060
  }
4865
- return join3(realpathSync(cur), ...tail);
5061
+ return join5(realpathSync(cur), ...tail);
4866
5062
  }
4867
5063
  function assertContainedReal(root, target, raw) {
4868
5064
  const rootReal = realpathSync(root);
@@ -4895,25 +5091,25 @@ function resolveScript(manifest, manifestDir) {
4895
5091
  if (isInlineScript(manifest))
4896
5092
  return { path: scriptRelFor(manifest), content: manifest.script };
4897
5093
  const rel = scriptRelFor(manifest);
4898
- const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
4899
- if (!existsSync(resolved)) {
5094
+ const resolved = assertContained(join5(manifestDir, rel), manifestDir, manifest.script);
5095
+ if (!existsSync2(resolved)) {
4900
5096
  throw new Error(`manifest script file not found: ${resolved}`);
4901
5097
  }
4902
5098
  return { path: rel, content: readFileSync2(resolved, "utf-8") };
4903
5099
  }
4904
5100
  function customHookDir(name) {
4905
- return join3(getCustomHooksDir(), shortManifestName(name));
5101
+ return join5(getCustomHooksDir(), shortManifestName(name));
4906
5102
  }
4907
5103
  function readCustomManifest(name) {
4908
5104
  const dir = customHookDir(name);
4909
- const manifestPath = join3(dir, "manifest.json");
4910
- if (!existsSync(manifestPath))
5105
+ const manifestPath = join5(dir, "manifest.json");
5106
+ if (!existsSync2(manifestPath))
4911
5107
  return;
4912
5108
  try {
4913
5109
  const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
4914
5110
  const script = resolveScript(manifest, dir);
4915
5111
  const scriptIsInline = isInlineScript(manifest);
4916
- return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline };
5112
+ return { manifest, scriptPath: join5(dir, script.path), scriptContent: script.content, scriptIsInline };
4917
5113
  } catch (err) {
4918
5114
  if (err instanceof ScriptContainmentError)
4919
5115
  throw err;
@@ -4922,7 +5118,7 @@ function readCustomManifest(name) {
4922
5118
  }
4923
5119
  function listCustomHooks() {
4924
5120
  const dir = getCustomHooksDir();
4925
- if (!existsSync(dir))
5121
+ if (!existsSync2(dir))
4926
5122
  return [];
4927
5123
  const results = [];
4928
5124
  for (const entry of readdirSafe(dir)) {
@@ -4942,14 +5138,13 @@ function readdirSafe(dir) {
4942
5138
 
4943
5139
  // src/lib/store.ts
4944
5140
  import { createHash } from "crypto";
4945
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
5141
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
4946
5142
  import { readFile } from "fs/promises";
4947
5143
 
4948
5144
  // src/db/index.ts
4949
5145
  import { Database } from "bun:sqlite";
4950
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, cpSync } from "fs";
4951
- import { join as join5 } from "path";
4952
- import { homedir as homedir3 } from "os";
5146
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, cpSync } from "fs";
5147
+ import { join as join7 } from "path";
4953
5148
 
4954
5149
  // src/db/schema.ts
4955
5150
  var CREATE_HOOK_EVENTS_TABLE = `
@@ -5121,9 +5316,9 @@ function runMigrations(db) {
5121
5316
  }
5122
5317
 
5123
5318
  // src/db/legacy-import.ts
5124
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
5125
- import { join as join4 } from "path";
5126
- import { homedir as homedir2 } from "os";
5319
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
5320
+ import { join as join6 } from "path";
5321
+ import { homedir as homedir3 } from "os";
5127
5322
  var META_KEY = "legacy_import_done";
5128
5323
  function ensureMetaTable(db) {
5129
5324
  db.exec(`
@@ -5198,25 +5393,25 @@ function importErrorsLog(db, filePath) {
5198
5393
  } catch {}
5199
5394
  return count;
5200
5395
  }
5201
- function runLegacyImport(db, homeDir2 = homedir2()) {
5396
+ function runLegacyImport(db, homeDir2 = homedir3()) {
5202
5397
  try {
5203
5398
  if (isAlreadyDone(db))
5204
5399
  return;
5205
5400
  let total = 0;
5206
- const claudeProjectsDir = join4(homeDir2, ".claude", "projects");
5207
- if (existsSync2(claudeProjectsDir)) {
5401
+ const claudeProjectsDir = join6(homeDir2, ".claude", "projects");
5402
+ if (existsSync3(claudeProjectsDir)) {
5208
5403
  try {
5209
5404
  const projectDirs = readdirSync2(claudeProjectsDir);
5210
5405
  for (const dir of projectDirs) {
5211
- const projectDir = join4(claudeProjectsDir, dir);
5406
+ const projectDir = join6(claudeProjectsDir, dir);
5212
5407
  try {
5213
5408
  const files = readdirSync2(projectDir);
5214
5409
  for (const file of files) {
5215
5410
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
5216
- total += importJsonlFile(db, join4(projectDir, file));
5411
+ total += importJsonlFile(db, join6(projectDir, file));
5217
5412
  }
5218
5413
  if (file === "errors.log") {
5219
- total += importErrorsLog(db, join4(projectDir, file));
5414
+ total += importErrorsLog(db, join6(projectDir, file));
5220
5415
  }
5221
5416
  }
5222
5417
  } catch {}
@@ -5251,27 +5446,24 @@ function runRetention(db, days) {
5251
5446
  // src/db/index.ts
5252
5447
  var instance = null;
5253
5448
  function resolveDataDir() {
5254
- const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
5255
- if (explicit)
5256
- return explicit;
5257
- const newDir = join5(homedir3(), ".hasna", "hooks");
5258
- const oldDir = join5(homedir3(), ".hooks");
5259
- if (!existsSync3(newDir) && existsSync3(oldDir)) {
5260
- mkdirSync2(join5(homedir3(), ".hasna"), { recursive: true });
5261
- cpSync(oldDir, newDir, { recursive: true });
5449
+ const effective = getEffectiveDataRoot();
5450
+ const oldDir = join7(getHomeDir(), ".hooks");
5451
+ if (!existsSync4(effective) && existsSync4(oldDir)) {
5452
+ mkdirSync2(effective, { recursive: true });
5453
+ cpSync(oldDir, effective, { recursive: true });
5262
5454
  }
5263
- return newDir;
5455
+ return effective;
5264
5456
  }
5265
5457
  function getDbPath() {
5266
5458
  const explicitDb = process.env.HASNA_HOOKS_DB_PATH ?? process.env.HOOKS_DB_PATH;
5267
5459
  if (explicitDb)
5268
5460
  return explicitDb;
5269
- const dataDir = resolveDataDir();
5270
- return join5(dataDir, "hooks.db");
5461
+ const dataDir2 = resolveDataDir();
5462
+ return join7(dataDir2, "hooks.db");
5271
5463
  }
5272
5464
  function ensureDir(dbPath) {
5273
5465
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
5274
- if (dir && !existsSync3(dir)) {
5466
+ if (dir && !existsSync4(dir)) {
5275
5467
  mkdirSync2(dir, { recursive: true });
5276
5468
  }
5277
5469
  }
@@ -5279,7 +5471,7 @@ function getDb() {
5279
5471
  if (instance)
5280
5472
  return instance;
5281
5473
  const dbPath = getDbPath();
5282
- const isNew = dbPath === ":memory:" || !existsSync3(dbPath);
5474
+ const isNew = dbPath === ":memory:" || !existsSync4(dbPath);
5283
5475
  ensureDir(dbPath);
5284
5476
  instance = new Database(dbPath);
5285
5477
  instance.exec("PRAGMA busy_timeout=5000");
@@ -5362,7 +5554,7 @@ function assertLockShape(parsed, path) {
5362
5554
  }
5363
5555
  function readLock() {
5364
5556
  const path = getLockPath();
5365
- if (!existsSync4(path))
5557
+ if (!existsSync5(path))
5366
5558
  return { hooks: {} };
5367
5559
  let raw;
5368
5560
  try {
@@ -5412,14 +5604,14 @@ function retrustHook(name, scriptPath, version, source) {
5412
5604
  }
5413
5605
 
5414
5606
  // src/lib/resolve.ts
5415
- import { dirname as dirname2, join as join6 } from "path";
5416
- import { existsSync as existsSync5 } from "fs";
5607
+ import { dirname as dirname2, join as join8 } from "path";
5608
+ import { existsSync as existsSync6 } from "fs";
5417
5609
  import { fileURLToPath } from "url";
5418
5610
  function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
5419
- const sourceLayout = join6(moduleDir, "..", "..", "hooks");
5420
- if (existsSync5(join6(sourceLayout, "hook-gitguard")))
5611
+ const sourceLayout = join8(moduleDir, "..", "..", "hooks");
5612
+ if (existsSync6(join8(sourceLayout, "hook-gitguard")))
5421
5613
  return sourceLayout;
5422
- return join6(moduleDir, "..", "hooks");
5614
+ return join8(moduleDir, "..", "hooks");
5423
5615
  }
5424
5616
  function resolveHookDir(name) {
5425
5617
  const custom2 = readCustomManifest(name);
@@ -5430,11 +5622,11 @@ function resolveHookDir(name) {
5430
5622
  return;
5431
5623
  const short = shortManifestName(name);
5432
5624
  const bundledBase = resolveBundledHooksDir();
5433
- const direct = join6(bundledBase, short);
5434
- if (existsSync5(direct))
5625
+ const direct = join8(bundledBase, short);
5626
+ if (existsSync6(direct))
5435
5627
  return direct;
5436
- const prefixed = join6(bundledBase, `hook-${short}`);
5437
- return existsSync5(prefixed) ? prefixed : undefined;
5628
+ const prefixed = join8(bundledBase, `hook-${short}`);
5629
+ return existsSync6(prefixed) ? prefixed : undefined;
5438
5630
  }
5439
5631
  function resolveScriptPath(name) {
5440
5632
  const custom2 = readCustomManifest(name);
@@ -5443,8 +5635,8 @@ function resolveScriptPath(name) {
5443
5635
  const dir = resolveHookDir(name);
5444
5636
  if (!dir)
5445
5637
  return;
5446
- const script = join6(dir, "src", "hook.ts");
5447
- return existsSync5(script) ? script : undefined;
5638
+ const script = join8(dir, "src", "hook.ts");
5639
+ return existsSync6(script) ? script : undefined;
5448
5640
  }
5449
5641
  function resolveHookMeta(name) {
5450
5642
  const custom2 = readCustomManifest(name);
@@ -5497,7 +5689,7 @@ function resolveHook(name) {
5497
5689
 
5498
5690
  // src/serve.ts
5499
5691
  import { readFileSync as readFileSync5 } from "fs";
5500
- import { dirname as dirname3, join as join7 } from "path";
5692
+ import { dirname as dirname3, join as join9 } from "path";
5501
5693
  import { fileURLToPath as fileURLToPath2 } from "url";
5502
5694
 
5503
5695
  // src/lib/secure-compare.ts
@@ -5670,7 +5862,7 @@ var SERVE_HOST = "127.0.0.1";
5670
5862
  var SERVE_SERVICE_NAME = "hooks-registry";
5671
5863
  function packageVersion() {
5672
5864
  try {
5673
- const pkgPath = join7(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json");
5865
+ const pkgPath = join9(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json");
5674
5866
  return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
5675
5867
  } catch {
5676
5868
  return "0.0.0";
@@ -5819,9 +6011,15 @@ function handleServeRequest(req, apiKey) {
5819
6011
  }
5820
6012
  return Promise.resolve(json({ error: "Not Found" }, 404));
5821
6013
  }
6014
+ function resolveServeOptions(options) {
6015
+ const envPort = process.env.PORT?.trim();
6016
+ const envHost = process.env.HOST?.trim();
6017
+ const port = options.port ?? (envPort && /^[0-9]+$/.test(envPort) ? parseInt(envPort, 10) : DEFAULT_SERVE_PORT);
6018
+ const host = options.host ?? (envHost && envHost.length > 0 ? envHost : SERVE_HOST);
6019
+ return { port, host };
6020
+ }
5822
6021
  function startServeServer(options) {
5823
- const port = options.port ?? DEFAULT_SERVE_PORT;
5824
- const host = options.host ?? SERVE_HOST;
6022
+ const { port, host } = resolveServeOptions(options);
5825
6023
  const apiKey = resolveApiKey();
5826
6024
  const server = Bun.serve({
5827
6025
  hostname: host,
@@ -5862,6 +6060,7 @@ options:
5862
6060
  }
5863
6061
  export {
5864
6062
  startServeServer,
6063
+ resolveServeOptions,
5865
6064
  handleServeRequest,
5866
6065
  SERVE_SERVICE_NAME,
5867
6066
  SERVE_HOST,
@@ -1,5 +1,7 @@
1
1
  /**
2
- * SQLite DB module for hooks — persistent storage at ~/.hasna/hooks/hooks.db
2
+ * SQLite DB module for hooks — persistent storage at the effective data
3
+ * root resolved through @hasna/paths (legacy ~/.hasna/hooks until adopted,
4
+ * then the XDG data home) at hooks.db.
3
5
  *
4
6
  * Uses bun:sqlite with WAL mode for concurrent reads.
5
7
  * Supports HASNA_HOOKS_DATA_DIR / HOOKS_DATA_DIR and HASNA_HOOKS_DB_PATH / HOOKS_DB_PATH env overrides.