@hasna/hooks 0.7.10 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +32 -6
  2. package/bin/index.js +1872 -1300
  3. package/bin/serve.js +730 -551
  4. package/dist/config.d.ts +25 -19
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +1198 -712
  7. package/dist/lib/app-home.d.ts +9 -0
  8. package/dist/lib/installer.d.ts +0 -9
  9. package/dist/lib/local-opt-in.d.ts +80 -0
  10. package/dist/lib/profiles.d.ts +10 -0
  11. package/dist/lib/resolver-types.d.ts +45 -0
  12. package/dist/lib/sync.d.ts +13 -8
  13. package/dist/lib/transport.d.ts +87 -0
  14. package/dist/openapi.d.ts +1 -1
  15. package/dist/storage.js +54 -63
  16. package/hooks/hook-affected-tests/package.json +1 -1
  17. package/hooks/hook-agent-rules-version-check/package.json +1 -1
  18. package/hooks/hook-agentmessages/package.json +1 -1
  19. package/hooks/hook-announce-start/package.json +1 -1
  20. package/hooks/hook-announce-stop/package.json +1 -1
  21. package/hooks/hook-autoformat/package.json +1 -1
  22. package/hooks/hook-branchprotect/package.json +1 -1
  23. package/hooks/hook-checkbugs/package.json +1 -1
  24. package/hooks/hook-checkdocs/package.json +1 -1
  25. package/hooks/hook-checkfiles/package.json +1 -1
  26. package/hooks/hook-checklint/package.json +1 -1
  27. package/hooks/hook-checkpoint/package.json +1 -1
  28. package/hooks/hook-checksecurity/package.json +1 -1
  29. package/hooks/hook-checktasks/package.json +1 -1
  30. package/hooks/hook-checktests/package.json +1 -1
  31. package/hooks/hook-conflict-detect/package.json +1 -1
  32. package/hooks/hook-contextrefresh/package.json +1 -1
  33. package/hooks/hook-desktopnotify/package.json +1 -1
  34. package/hooks/hook-dm-inject/package.json +1 -1
  35. package/hooks/hook-envsetup/package.json +1 -1
  36. package/hooks/hook-failure-to-task/package.json +1 -1
  37. package/hooks/hook-filelock/package.json +1 -1
  38. package/hooks/hook-fleet-blockers-gate/package.json +1 -1
  39. package/hooks/hook-fleet-catchup/package.json +1 -1
  40. package/hooks/hook-gitguard/package.json +1 -1
  41. package/hooks/hook-packageage/package.json +1 -1
  42. package/hooks/hook-permissionguard/package.json +1 -1
  43. package/hooks/hook-phonenotify/package.json +1 -1
  44. package/hooks/hook-precompact/package.json +1 -1
  45. package/hooks/hook-protectfiles/package.json +1 -1
  46. package/hooks/hook-stylescheck/package.json +1 -1
  47. package/hooks/hook-typecheck-gate/package.json +1 -1
  48. package/package.json +4 -9
  49. package/scripts/ensure-profiles-dir.mjs +52 -1
@@ -11,6 +11,15 @@
11
11
  * physically migrated there, so a live store at the legacy home never becomes
12
12
  * invisible on upgrade.
13
13
  */
14
+ export type PathKind = "config" | "data" | "state" | "cache";
15
+ export interface PathsResolverOptions {
16
+ app: string;
17
+ internal?: boolean;
18
+ platform?: string;
19
+ home?: string;
20
+ env?: Record<string, string | undefined>;
21
+ }
22
+ export declare function dataDir(options: PathsResolverOptions): string;
14
23
  /** Resolve the user's home directory: $HOME, then $USERPROFILE, then the OS user database. */
15
24
  export declare function getHomeDir(env?: NodeJS.ProcessEnv): string;
16
25
  /**
@@ -81,14 +81,5 @@ export declare function removeCodewithHookEntry(configText: string, name: string
81
81
  text: string;
82
82
  removed: boolean;
83
83
  };
84
- /**
85
- * Full uninstall — the settings registration, the store directory (custom
86
- * hooks), the lock pin and the DB record are all removed. Bundled hooks keep
87
- * their package files (they belong to the package, not the store).
88
- *
89
- * Resolves custom and registry-synced hooks, which live in the custom store
90
- * dir, as well as bundled ones (QA-1 BUG-A / QA-4: remove was bundled-only
91
- * and never cleaned the store/lock/DB).
92
- */
93
84
  export declare function uninstallHook(name: string, scope?: Scope, target?: Target): UninstallResult;
94
85
  export {};
@@ -0,0 +1,80 @@
1
+ import type { HooksCredentialOptions, HooksLocalOptInEnv } from "./resolver-types.js";
2
+ /** The deliberate unhosted opt-in, canonical name first. */
3
+ export declare const HOOKS_LOCAL_OPT_IN_ENV_KEYS: readonly ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
4
+ /** True when the operator deliberately asked for the unhosted local store. */
5
+ export declare function isHooksLocalOptIn(env?: HooksLocalOptInEnv): boolean;
6
+ /** Every env name that can configure a hooks authority or credential, resolver-derived. */
7
+ export declare function hooksAuthorityEnvKeys(): string[];
8
+ /**
9
+ * Does the ENVIRONMENT itself configure a hooks authority or credential?
10
+ *
11
+ * Deliberately narrower than "does a credential resolve": answering it must not
12
+ * touch the Keychain or the filesystem, because doing so would defeat the
13
+ * isolation the opt-in short-circuit exists to provide. It reads the env
14
+ * dictionary and nothing else.
15
+ *
16
+ * A DECLARED-BUT-BLANK variable counts as absent HERE — a blank has always been
17
+ * this package's spelling for "not configured", and helpers in the wild blank
18
+ * rather than delete. It is NOT absent once we do go hosted: the resolver
19
+ * refuses a blank loudly rather than falling through to another identity, which
20
+ * is the behaviour that matters at that point.
21
+ */
22
+ export declare function hasHooksEnvAuthorityIntent(env?: HooksLocalOptInEnv): boolean;
23
+ /** True when this environment should be served by the on-box bundled store. */
24
+ export declare function selectsHooksLocalStore(env?: HooksLocalOptInEnv): boolean;
25
+ /**
26
+ * The environment as the resolver should see it: every authority/credential
27
+ * variable that is DECLARED BUT BLANK removed.
28
+ *
29
+ * A blank has always been this package's spelling for "not configured".
30
+ * @hasna/contracts takes the opposite and, for its purposes, correct view: a
31
+ * declared-but-blank credential is a misconfiguration it refuses loudly rather
32
+ * than resolving around, because a blank that fell through would authenticate
33
+ * as a different principal than the operator named.
34
+ *
35
+ * Both are right at their own layer, and the mismatch is not hypothetical: an
36
+ * environment carrying a real `HASNA_HOOKS_API_KEY` alongside a blank legacy
37
+ * alias — the exact shape a scrubbed-then-overridden fixture produces — is a
38
+ * complete, unambiguous configuration that would otherwise be refused for the
39
+ * alias nobody set. Normalising here keeps "blank means unset" true at the
40
+ * hooks seam while leaving the resolver's stricter rule intact for everything
41
+ * it does receive: a value that is present is still policed, and two aliases
42
+ * that actually disagree still refuse.
43
+ */
44
+ export declare function hooksResolverEnv<T extends HooksLocalOptInEnv>(env: T): T;
45
+ /** The env object and credential options a hooks surface hands @hasna/contracts. */
46
+ export interface HooksResolverInputs<T extends HooksLocalOptInEnv> {
47
+ /** The environment with every declared-but-blank authority variable removed. */
48
+ env: T;
49
+ /** The chain options, with the Keychain tier's ambient gate already decided. */
50
+ credentials: HooksCredentialOptions;
51
+ }
52
+ /**
53
+ * Build the resolver's inputs: the normalised environment AND the credential
54
+ * options that keep the machine's Keychain tier reachable across it.
55
+ *
56
+ * WHY THIS IS NOT JUST {@link hooksResolverEnv}. Blanking a variable and
57
+ * deleting it are not the same operation to @hasna/contracts, because dropping
58
+ * a key forces us to hand the resolver a COPY, and the resolver gates its
59
+ * ambient tiers on OBJECT IDENTITY (`env === process.env`, or the registry
60
+ * symbol its own snapshot carries). A copy is, by that test, a caller-built
61
+ * world — the hermetic seam — so the Keychain is outside it and tier 3 turns
62
+ * itself off. Silently: there is no error, no warning and no diagnostic.
63
+ *
64
+ * The consequence is the one failure this whole ruling exists to prevent. On a
65
+ * station whose Keychain holds `hasna.credentials.hooks.api-key`, ONE
66
+ * declared-but-blank authority variable dropped the run from the Keychain
67
+ * identity to whatever came next: to `~/.hasna/hooks/config/credentials`, a
68
+ * DIFFERENT principal, with no notice; or, with nothing on disk, to a bare
69
+ * REMOTE_API_CONFIG_MISSING on a station that is in fact configured. A
70
+ * deliberate tier must never fall through to another identity, so the gate is
71
+ * decided HERE, on the original env, and carried across the copy as the
72
+ * documented `keychain.enabled` control rather than being left to an identity
73
+ * test the copy cannot pass.
74
+ *
75
+ * An explicit `enabled` from the caller still wins, and an injected `run`
76
+ * (which @hasna/contracts already treats as "enabled") is left alone, so the
77
+ * hermetic seam tests rely on is untouched. When there is no blank to remove
78
+ * the inputs pass through by identity, exactly as before.
79
+ */
80
+ export declare function hooksResolverInputs<T extends HooksLocalOptInEnv>(env: T, credentials?: HooksCredentialOptions): HooksResolverInputs<T>;
@@ -20,6 +20,16 @@ export interface CreateProfileInput {
20
20
  agent_type: "claude" | "gemini" | "custom";
21
21
  name?: string;
22
22
  }
23
+ /**
24
+ * The profiles directory under the effective data root.
25
+ *
26
+ * Resolved on every call, never snapshotted at module load: the effective
27
+ * data root is env-driven (HASNA_HOOKS_DATA_DIR / HASNA_HOOKS_HOME / the XDG
28
+ * adoption in app-home), and bun test runs test files in one process with a
29
+ * SHARED process.env — a module-load snapshot froze the dir to whatever the
30
+ * env held at import time, so a concurrent file pinning a data-root override
31
+ * silently moved every profile operation of this module with it.
32
+ */
23
33
  export declare function getProfilesDir(): string;
24
34
  export declare function createProfile(input: CreateProfileInput): AgentProfile;
25
35
  export declare function getProfile(id: string): AgentProfile | null;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The @hasna/contracts client types that @hasna/hooks PUBLISHES or hands back
3
+ * across its public seam, spelled locally.
4
+ *
5
+ * WHY THIS FILE EXISTS. `@hasna/contracts` is a BUILD-TIME dependency of this
6
+ * package (pinned 1.0.2, inlined by `bun build --target bun`), so the emitted
7
+ * `dist/**&#47;*.d.ts` must never `import ... from "@hasna/contracts/client"` —
8
+ * a published declaration that references a package consumers do not have is
9
+ * a broken install (hasna/apps#1782). Every public signature therefore uses
10
+ * these local spellings, which are structurally identical to the resolver's
11
+ * own types (`CredentialChainOptions` / `KeychainCommandResult`). The
12
+ * `credential chain shape` test in `transport.test.ts` pins the local
13
+ * spellings against the real @hasna/contracts types so they cannot drift.
14
+ */
15
+ export type HooksLocalOptInEnv = Record<string, string | undefined>;
16
+ /** What an injected `security` runner returns: an exit status plus output. */
17
+ export interface HooksKeychainCommandResult {
18
+ /** Exit status; null when the tool could not be started or was killed. */
19
+ status: number | null;
20
+ stdout: string;
21
+ stderr: string;
22
+ }
23
+ /** Tier 3 (Keychain) controls — structurally `KeychainTierOptions`. */
24
+ export interface HooksKeychainTierOptions {
25
+ /** Whether the Keychain is consulted for a caller-built env object. */
26
+ enabled?: boolean;
27
+ /** Defaults to `process.platform`; the tier exists only on `"darwin"`. */
28
+ platform?: string;
29
+ /** The machine's host name when `HASNA_STATION` is unset. */
30
+ hostname?: () => string;
31
+ /** The `security` runner. Defaults to spawning `/usr/bin/security`. */
32
+ run?: (argv: readonly string[]) => HooksKeychainCommandResult;
33
+ }
34
+ /**
35
+ * Tier-1 credential inputs and Keychain-tier controls — structurally
36
+ * `CredentialChainOptions` from @hasna/contracts/client, spelled locally.
37
+ */
38
+ export interface HooksCredentialOptions {
39
+ /** Tier 1: an explicit key, e.g. from `--api-key`. */
40
+ apiKey?: string;
41
+ /** Tier 1: an explicit profile name. Beats `HASNA_PROFILE`. */
42
+ profile?: string;
43
+ /** Tier 3: Keychain controls — a fake `security` runner in tests, an opt-out on CI. */
44
+ keychain?: HooksKeychainTierOptions;
45
+ }
@@ -6,7 +6,16 @@
6
6
  * failure mid-sync leaves the local store untouched. Local-only hooks are
7
7
  * never deleted by a remote sync.
8
8
  */
9
+ import type { HooksCredentialOptions } from "./resolver-types.js";
9
10
  import { parseManifest } from "./manifest.js";
11
+ type Env = Record<string, string | undefined>;
12
+ export interface HooksSyncOptions {
13
+ dryRun?: boolean;
14
+ /** Injectable environment (tests). Defaults to `process.env`. */
15
+ env?: Env;
16
+ /** Injectable credential-chain inputs (tests: fake `security` runner). */
17
+ credentials?: HooksCredentialOptions;
18
+ }
10
19
  export interface SyncDiff {
11
20
  added: string[];
12
21
  updated: string[];
@@ -39,12 +48,8 @@ interface RemoteLock {
39
48
  versions?: string[];
40
49
  }>;
41
50
  }
42
- export declare function planSync(options?: {
43
- dryRun?: boolean;
44
- }): Promise<SyncPlan>;
45
- export declare function syncHooks(options?: {
46
- dryRun?: boolean;
47
- }): Promise<SyncPlan>;
51
+ export declare function planSync(options?: HooksSyncOptions): Promise<SyncPlan>;
52
+ export declare function syncHooks(options?: HooksSyncOptions): Promise<SyncPlan>;
48
53
  /**
49
54
  * One fully-validated artifact ready to be committed. Nothing has touched
50
55
  * the store at this point: every fetch, sha check and manifest parse has
@@ -64,7 +69,7 @@ export interface StagedSyncArtifact {
64
69
  * written. A failure here (network, sha mismatch, malformed manifest,
65
70
  * containment violation) throws with the store untouched.
66
71
  */
67
- export declare function stageSyncArtifacts(apiUrl: string, names: string[], remoteLock: RemoteLock): Promise<StagedSyncArtifact[]>;
72
+ export declare function stageSyncArtifacts(apiUrl: string, apiKey: string, names: string[], remoteLock: RemoteLock): Promise<StagedSyncArtifact[]>;
68
73
  /**
69
74
  * P1-9 commit phase, ordered so a mid-commit failure cannot leave a partial
70
75
  * store that reads as trusted:
@@ -101,5 +106,5 @@ export interface PinnedHookInstall {
101
106
  * the user is fetched from the versioned registry — older-than-latest pins
102
107
  * are first-class, never rejected as "not the latest".
103
108
  */
104
- export declare function fetchPinnedHook(name: string, version: string, apiUrl: string): Promise<PinnedHookInstall>;
109
+ export declare function fetchPinnedHook(name: string, version: string, apiUrl: string, apiKey: string): Promise<PinnedHookInstall>;
105
110
  export {};
@@ -0,0 +1,87 @@
1
+ import type { HooksCredentialOptions, HooksLocalOptInEnv } from "./resolver-types.js";
2
+ /** The unhosted mode: bundled registry + local SQLite store. Never a default. */
3
+ export type HooksTransportMode = "remote" | "local";
4
+ /** The resolved remote-registry authority pair. Never carries a value besides `apiKey`. */
5
+ export interface HooksRemoteAuthority {
6
+ /**
7
+ * Registry origin WITHOUT the `/v1` suffix the resolver appends — the sync
8
+ * client composes `/api/v1/...` routes on top of it.
9
+ */
10
+ origin: string;
11
+ /** The credential, resolved together with the authority it will be sent to. */
12
+ apiKey: string;
13
+ /** WHERE the authority came from: an env key NAME, a Keychain item reference, a file PATH, or "default". */
14
+ apiUrlSource: string | null;
15
+ /** WHERE the credential came from: an env key NAME, a Keychain item reference, or a file PATH. Never a value. */
16
+ apiKeySource: string | null;
17
+ /** Which tier of the chain supplied the credential. */
18
+ apiKeyTier: string | null;
19
+ /** The resolver's `<origin>/v1` base, for diagnostics. */
20
+ v1BaseUrl: string;
21
+ /** Human-readable warning, or null. Never contains secret values. */
22
+ warning: string | null;
23
+ }
24
+ /**
25
+ * The transport decision every hosted surface makes: remote (resolver-backed)
26
+ * or local (explicit opt-in, resolver never consulted).
27
+ *
28
+ * Local mode returns `authority: null` and carries no key. Remote mode always
29
+ * carries a fully resolved pair — the seam throws before returning a
30
+ * half-configured one.
31
+ */
32
+ export interface HooksTransportResolution {
33
+ mode: HooksTransportMode;
34
+ /** `"local-opt-in"` for the deliberate unhosted store, else `"<api key source>+<api url source>"`. */
35
+ source: string;
36
+ /** The resolved remote pair; null in local mode. */
37
+ authority: HooksRemoteAuthority | null;
38
+ }
39
+ /** Where the one-line local-mode notice goes. Defaults to `process.stderr`. */
40
+ export type HooksTransportNotice = (line: string) => void;
41
+ /** Reset the once-per-process local-mode notice. Test seam only. */
42
+ export declare function __resetHooksLocalNotice(): void;
43
+ /**
44
+ * Announce the unhosted mode on stderr, once per process. Surfaces that never
45
+ * resolve the transport (the CLI gate opening through the opt-in) still have
46
+ * to SAY the run is local — the "local on stderr" doctrine (2026-09-04).
47
+ */
48
+ export declare function announceHooksLocalMode(reason?: string, notice?: HooksTransportNotice): void;
49
+ /** Strip the `/v1` suffix the resolver normalised onto the authority. */
50
+ export declare function hooksRegistryOrigin(v1BaseUrl: string): string;
51
+ /**
52
+ * Re-throw a `@hasna/contracts` resolution failure as hooks' own fail-closed
53
+ * diagnostic, preserving the resolver's message (which names every tier it
54
+ * consulted) behind the stable `REMOTE_API_*` code callers match on. Nothing
55
+ * here ever returns a client or a local store: every arm throws.
56
+ */
57
+ export declare function rethrowHooksAuthorityFailure(error: unknown): never;
58
+ /** Tier-1 credential inputs and Keychain-tier controls, forwarded verbatim. */
59
+ export type { HooksCredentialOptions } from "./resolver-types.js";
60
+ export interface HooksTransportOptions {
61
+ /** Tier-1 credential inputs (`--api-key` / `--profile`) and the injectable `security` runner tests use. */
62
+ credentials?: HooksCredentialOptions;
63
+ /** Where the one-line local-mode notice goes. Defaults to `process.stderr`. */
64
+ notice?: HooksTransportNotice;
65
+ }
66
+ /**
67
+ * Resolve the hooks transport. The deliberate unhosted opt-in is answered
68
+ * first and WITHOUT consulting the resolver; otherwise `@hasna/contracts`
69
+ * resolves the credential AND the authority together as one strict pair, and
70
+ * any failure to do so is a throw — the client never defaults to the on-box
71
+ * store (owner ruling 2026-09-04). Resolved fresh on every call.
72
+ */
73
+ export declare function resolveHooksTransport(env?: HooksLocalOptInEnv, options?: HooksTransportOptions): HooksTransportResolution;
74
+ /**
75
+ * The registry keys the publish surface checks. Local-by-design commands
76
+ * (`hooks serve`) never pick a transport; they only need to know whether a
77
+ * credential exists to honour a PUT. Resolved fresh on every call, through the
78
+ * same chain as {@link resolveHooksTransport}, so a key rotation heals a
79
+ * long-lived server without a restart.
80
+ *
81
+ * Returns `undefined` when no credential resolves — reads stay open and
82
+ * publish refuses — and THROWS on a deliberate tier that exists but cannot be
83
+ * honoured (a locked Keychain, an unsafe credential file), because falling
84
+ * through to another identity is the one silent failure the chain exists to
85
+ * end.
86
+ */
87
+ export declare function resolveHooksServePublishKey(env?: HooksLocalOptInEnv, options?: HooksTransportOptions): string | undefined;
package/dist/openapi.d.ts CHANGED
@@ -209,7 +209,7 @@ export declare const openApiDocument: {
209
209
  readonly apiKey: {
210
210
  readonly type: "http";
211
211
  readonly scheme: "bearer";
212
- readonly description: "The registry API key (HASNA_HOOKS_API_KEY / HOOKS_API_KEY).";
212
+ readonly description: "The registry API key — resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value.";
213
213
  };
214
214
  };
215
215
  };
package/dist/storage.js CHANGED
@@ -15,92 +15,77 @@ var __export = (target, all) => {
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
- // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
18
+ // src/lib/app-home.ts
19
+ import { existsSync } from "fs";
19
20
  import { homedir } from "os";
20
- import { join } from "path";
21
- function assertApp(app) {
21
+ import { join, resolve } from "path";
22
+ import { homedir as pathsResolverHomedir } from "os";
23
+ import { join as pathsResolverJoin } from "path";
24
+ function pathsResolverAssertApp(app) {
22
25
  if (typeof app !== "string" || app.length === 0) {
23
26
  throw new TypeError("paths: app must be a non-empty string");
24
27
  }
25
- if (!APP_SLUG_RE.test(app)) {
28
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
26
29
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
27
30
  }
28
31
  }
29
- function envOf(options) {
30
- return options.env ?? process.env;
31
- }
32
- function envValue(options, kind) {
33
- const value = envOf(options)[KIND_ENV[kind]];
34
- return typeof value === "string" && value.length > 0 ? value : undefined;
35
- }
36
- function isMacOS(platform) {
37
- return platform === "darwin";
32
+ function pathsResolverAssertKind(kind) {
33
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
34
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
35
+ }
38
36
  }
39
- function baseDir(kind, options) {
40
- const override = envValue(options, kind);
41
- if (override)
37
+ function pathsResolverBaseDir(kind, options) {
38
+ pathsResolverAssertKind(kind);
39
+ const env = options.env ?? process.env;
40
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
41
+ if (typeof override === "string" && override.length > 0)
42
42
  return override;
43
- const home = options.home ?? homedir();
43
+ const home = options.home ?? pathsResolverHomedir();
44
44
  const platform = options.platform ?? process.platform;
45
- if (isMacOS(platform)) {
45
+ if (platform === "darwin") {
46
46
  switch (kind) {
47
47
  case "config":
48
48
  case "data":
49
- return join(home, "Library", "Application Support", "Hasna");
49
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
50
50
  case "cache":
51
- return join(home, "Library", "Caches", "Hasna");
51
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
52
52
  case "state":
53
- return join(home, "Library", "Logs", "Hasna");
53
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
54
54
  }
55
55
  }
56
56
  switch (kind) {
57
57
  case "config":
58
- return join(home, ".config", "hasna");
58
+ return pathsResolverJoin(home, ".config", "hasna");
59
59
  case "data":
60
- return join(home, ".local", "share", "hasna");
60
+ return pathsResolverJoin(home, ".local", "share", "hasna");
61
61
  case "state":
62
- return join(home, ".local", "state", "hasna");
62
+ return pathsResolverJoin(home, ".local", "state", "hasna");
63
63
  case "cache":
64
- return join(home, ".cache", "hasna");
64
+ return pathsResolverJoin(home, ".cache", "hasna");
65
65
  }
66
66
  }
67
- function resolvePath(kind, options) {
68
- assertApp(options.app);
69
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
70
- return join(baseDir(kind, options), appSegment);
67
+ function pathsResolverResolve(kind, options) {
68
+ pathsResolverAssertApp(options.app);
69
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
70
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
71
71
  }
72
72
  function dataDir(options) {
73
- return resolvePath("data", options);
73
+ return pathsResolverResolve("data", options);
74
74
  }
75
- var KIND_ENV, APP_SLUG_RE;
76
- var init_dist = __esm(() => {
77
- KIND_ENV = {
78
- config: "HASNA_CONFIG_HOME",
79
- data: "HASNA_DATA_HOME",
80
- state: "HASNA_STATE_HOME",
81
- cache: "HASNA_CACHE_HOME"
82
- };
83
- APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
84
- });
85
-
86
- // src/lib/app-home.ts
87
- import { existsSync } from "fs";
88
- import { homedir as homedir2 } from "os";
89
- import { join as join2, resolve } from "path";
90
75
  function getHomeDir(env = process.env) {
91
- return env.HOME || env.USERPROFILE || homedir2();
76
+ return env.HOME || env.USERPROFILE || homedir();
92
77
  }
93
78
  function getResolverDataRoot(env = process.env) {
94
79
  return dataDir({ app: "hooks", home: getHomeDir(env), env });
95
80
  }
96
81
  function getLegacyDataRoot(env = process.env) {
97
- return join2(getHomeDir(env), ".hasna", "hooks");
82
+ return join(getHomeDir(env), ".hasna", "hooks");
98
83
  }
99
84
  function adoptResolverDataRoot(resolved, env = process.env) {
100
85
  const dataOverride = env.HASNA_DATA_HOME;
101
86
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
102
87
  return true;
103
- return existsSync(join2(resolved, STORE_MARKER));
88
+ return existsSync(join(resolved, STORE_MARKER));
104
89
  }
105
90
  function getExplicitDataDir(env = process.env) {
106
91
  const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
@@ -128,7 +113,7 @@ function getReportedDbPath(env = process.env) {
128
113
  const explicit = getExplicitDbPath(env);
129
114
  if (explicit)
130
115
  return explicit;
131
- return join2(getEffectiveDataRoot(env), "hooks.db");
116
+ return join(getEffectiveDataRoot(env), "hooks.db");
132
117
  }
133
118
  function getExplicitDbPath(env = process.env) {
134
119
  const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
@@ -136,9 +121,15 @@ function getExplicitDbPath(env = process.env) {
136
121
  return db.trim();
137
122
  return;
138
123
  }
139
- var STORE_MARKER = "hooks.db";
124
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, STORE_MARKER = "hooks.db";
140
125
  var init_app_home = __esm(() => {
141
- init_dist();
126
+ PATHS_RESOLVER_KIND_ENV = {
127
+ config: "HASNA_CONFIG_HOME",
128
+ data: "HASNA_DATA_HOME",
129
+ state: "HASNA_STATE_HOME",
130
+ cache: "HASNA_CACHE_HOME"
131
+ };
132
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
142
133
  });
143
134
 
144
135
  // src/db/schema.ts
@@ -336,8 +327,8 @@ var init_migrations = __esm(() => {
336
327
 
337
328
  // src/db/legacy-import.ts
338
329
  import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
339
- import { join as join3 } from "path";
340
- import { homedir as homedir3 } from "os";
330
+ import { join as join2 } from "path";
331
+ import { homedir as homedir2 } from "os";
341
332
  function ensureMetaTable(db) {
342
333
  db.exec(`
343
334
  CREATE TABLE IF NOT EXISTS _meta (
@@ -411,25 +402,25 @@ function importErrorsLog(db, filePath) {
411
402
  } catch {}
412
403
  return count;
413
404
  }
414
- function runLegacyImport(db, homeDir = homedir3()) {
405
+ function runLegacyImport(db, homeDir = homedir2()) {
415
406
  try {
416
407
  if (isAlreadyDone(db))
417
408
  return;
418
409
  let total = 0;
419
- const claudeProjectsDir = join3(homeDir, ".claude", "projects");
410
+ const claudeProjectsDir = join2(homeDir, ".claude", "projects");
420
411
  if (existsSync2(claudeProjectsDir)) {
421
412
  try {
422
413
  const projectDirs = readdirSync(claudeProjectsDir);
423
414
  for (const dir of projectDirs) {
424
- const projectDir = join3(claudeProjectsDir, dir);
415
+ const projectDir = join2(claudeProjectsDir, dir);
425
416
  try {
426
417
  const files = readdirSync(projectDir);
427
418
  for (const file of files) {
428
419
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
429
- total += importJsonlFile(db, join3(projectDir, file));
420
+ total += importJsonlFile(db, join2(projectDir, file));
430
421
  }
431
422
  if (file === "errors.log") {
432
- total += importErrorsLog(db, join3(projectDir, file));
423
+ total += importErrorsLog(db, join2(projectDir, file));
433
424
  }
434
425
  }
435
426
  } catch {}
@@ -466,10 +457,10 @@ function runRetention(db, days) {
466
457
  // src/db/index.ts
467
458
  import { Database } from "bun:sqlite";
468
459
  import { existsSync as existsSync3, mkdirSync, cpSync } from "fs";
469
- import { join as join4 } from "path";
460
+ import { join as join3 } from "path";
470
461
  function resolveDataDir() {
471
462
  const effective = getEffectiveDataRoot();
472
- const oldDir = join4(getHomeDir(), ".hooks");
463
+ const oldDir = join3(getHomeDir(), ".hooks");
473
464
  if (!existsSync3(effective) && existsSync3(oldDir)) {
474
465
  mkdirSync(effective, { recursive: true });
475
466
  cpSync(oldDir, effective, { recursive: true });
@@ -481,7 +472,7 @@ function getDbPath() {
481
472
  if (explicitDb)
482
473
  return explicitDb;
483
474
  const dataDir2 = resolveDataDir();
484
- return join4(dataDir2, "hooks.db");
475
+ return join3(dataDir2, "hooks.db");
485
476
  }
486
477
  function ensureDir(dbPath) {
487
478
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
@@ -568,8 +559,8 @@ var init_redact = __esm(() => {
568
559
  /AKIA[0-9A-Z]{16}/g,
569
560
  /ASIA[0-9A-Z]{16}/g,
570
561
  /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
571
- /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
572
- /-----BEGIN OPENSSH PRIVATE KEY-----/g,
562
+ /-----BEGIN [A-Z0-9 ]*PRIVAT[E] KEY-----/g,
563
+ /-----BEGIN OPENSSH PRIVAT[E] KEY-----/g,
573
564
  /\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
574
565
  /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
575
566
  /\b(?:password|passwd|pwd|token|api[_-]?key|secret|access[_-]?key|client[_-]?secret|authorization|auth|credential|database_url|db_url|connection_string|dsn)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']{6,})/gi
@@ -43,7 +43,7 @@
43
43
  "bun": ">=1.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@types/bun": "^1.3.8",
46
+ "@types/bun": "1.3.14",
47
47
  "@types/node": "^20",
48
48
  "typescript": "^5.0.0"
49
49
  }
@@ -46,7 +46,7 @@
46
46
  "bun": ">=1.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@types/bun": "^1.3.8",
49
+ "@types/bun": "1.3.14",
50
50
  "@types/node": "^20",
51
51
  "typescript": "^5.0.0"
52
52
  }
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {},
25
25
  "devDependencies": {
26
- "@types/bun": "^1.3.8",
26
+ "@types/bun": "1.3.14",
27
27
  "typescript": "^5.7.2"
28
28
  },
29
29
  "author": "Hasna",
@@ -44,7 +44,7 @@
44
44
  "bun": ">=1.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@types/bun": "^1.3.8",
47
+ "@types/bun": "1.3.14",
48
48
  "@types/node": "^20",
49
49
  "typescript": "^5.0.0"
50
50
  }
@@ -44,7 +44,7 @@
44
44
  "bun": ">=1.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@types/bun": "^1.3.8",
47
+ "@types/bun": "1.3.14",
48
48
  "@types/node": "^20",
49
49
  "typescript": "^5.0.0"
50
50
  }
@@ -51,7 +51,7 @@
51
51
  "bun": ">=1.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/bun": "^1.3.8",
54
+ "@types/bun": "1.3.14",
55
55
  "@types/node": "^20",
56
56
  "typescript": "^5.0.0"
57
57
  }
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "engines": { "node": ">=18", "bun": ">=1.0" },
37
37
  "devDependencies": {
38
- "@types/bun": "^1.3.8",
38
+ "@types/bun": "1.3.14",
39
39
  "@types/node": "^20",
40
40
  "typescript": "^5.0.0"
41
41
  }
@@ -51,7 +51,7 @@
51
51
  "bun": ">=1.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/bun": "^1.3.8",
54
+ "@types/bun": "1.3.14",
55
55
  "@types/node": "^20",
56
56
  "typescript": "^5.0.0"
57
57
  }
@@ -50,7 +50,7 @@
50
50
  "bun": ">=1.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/bun": "^1.3.8",
53
+ "@types/bun": "1.3.14",
54
54
  "@types/node": "^20",
55
55
  "typescript": "^5.0.0"
56
56
  }
@@ -49,7 +49,7 @@
49
49
  "bun": ">=1.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@types/bun": "^1.3.8",
52
+ "@types/bun": "1.3.14",
53
53
  "@types/node": "^20",
54
54
  "typescript": "^5.0.0"
55
55
  }