@hasna/hooks 0.6.2 → 0.6.4

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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared hook DB writer — single write path for all observability hooks.
3
+ * Never throws: errors are written to stderr only.
4
+ */
5
+ import type { HookEventRow } from "../db/schema";
6
+ export type HookEventInput = Omit<HookEventRow, "id" | "timestamp"> & {
7
+ timestamp?: string;
8
+ };
9
+ export declare function writeHookEvent(event: HookEventInput): void;
10
+ /** Normalize a hook_event_name from hook input to a value the schema accepts. */
11
+ export declare function normalizeEventType(value: unknown): string | null;
12
+ /**
13
+ * Pick the event type for a run record: the hook input's hook_event_name when
14
+ * it is valid, else the hook's declared event. Guarantees a row lands for
15
+ * every execution even when the agent passes an unknown event name.
16
+ */
17
+ export declare function resolveEventType(inputEvent: unknown, fallbackEvent: string | null | undefined): string | null;
18
+ /**
19
+ * Record one hook execution in hook_events — the row `hooks log` reads.
20
+ * Written by every run path (CLI run, SDK runHook, MCP run tools) so a real
21
+ * fire is always observable (bug ef58dcb7: 0 rows after real fires).
22
+ *
23
+ * Never throws: observability must not break execution. An invalid event type
24
+ * or a missing DB is reported on stderr and skipped.
25
+ */
26
+ export declare function recordHookRun(record: {
27
+ hookName: string;
28
+ eventType: string | null;
29
+ version?: string | null;
30
+ sha256?: string | null;
31
+ sessionId?: string | null;
32
+ toolName?: string | null;
33
+ toolInput?: unknown;
34
+ result?: "continue" | "block" | null;
35
+ error?: string | null;
36
+ exitCode: number;
37
+ durationMs: number;
38
+ projectDir?: string | null;
39
+ metadata?: Record<string, unknown>;
40
+ }): void;
@@ -54,4 +54,41 @@ export declare function getRegisteredHooks(scope?: Scope): string[];
54
54
  /** @deprecated Use getRegisteredHooks instead */
55
55
  export declare const getInstalledHooks: typeof getRegisteredHooks;
56
56
  export declare function removeHook(name: string, scope?: Scope, target?: Target): boolean;
57
+ export interface UninstallResult {
58
+ name: string;
59
+ removed: boolean;
60
+ source: "custom" | "bundled" | "registered-only" | null;
61
+ settingsScopes: Scope[];
62
+ storeDirRemoved: boolean;
63
+ pinRemoved: boolean;
64
+ dbRecordRemoved: boolean;
65
+ /** Targets whose config still registers the hook after removal (e.g. codewith TOML the caller must edit itself). */
66
+ registrationsRemaining: string[];
67
+ error?: string;
68
+ }
69
+ /**
70
+ * Lossless removal of a `hooks run <name>` entry from a Codewith config.toml.
71
+ *
72
+ * Works on sections split by blank lines — the shape buildCodewithTomlFragment
73
+ * writes: a `[[hooks.EVENT]]` header section (with optional matcher), then one
74
+ * `[[hooks.EVENT.hooks]]` entry section per hook containing
75
+ * `command = "hooks run <name>"`. Only sections positively identified as this
76
+ * hook's entries are removed; the enclosing EVENT header is dropped only when
77
+ * every one of its entries belonged to this hook. Anything ambiguous is
78
+ * preserved verbatim.
79
+ */
80
+ export declare function removeCodewithHookEntry(configText: string, name: string): {
81
+ text: string;
82
+ removed: boolean;
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
+ export declare function uninstallHook(name: string, scope?: Scope, target?: Target): UninstallResult;
57
94
  export {};
@@ -12,6 +12,8 @@ export interface ResolvedHook {
12
12
  description: string;
13
13
  scriptPath: string;
14
14
  meta: HookMeta;
15
+ /** The hook's own declared timeout (manifest timeout_ms), or null. */
16
+ timeoutMs: number | null;
15
17
  }
16
18
  /**
17
19
  * Locate the bundled hooks directory at runtime from the executing module's
package/dist/lib/run.d.ts CHANGED
@@ -34,4 +34,12 @@ export interface VerifiedRunResult {
34
34
  stderr: string;
35
35
  exitCode: number;
36
36
  }
37
+ /**
38
+ * Raised when a verified script exceeds its timeout. The whole process group
39
+ * was killed, so no descendant survives (see executeVerifiedScript).
40
+ */
41
+ export declare class HookTimeoutError extends Error {
42
+ readonly timeoutMs: number;
43
+ constructor(timeoutMs: number);
44
+ }
37
45
  export declare function executeVerifiedScript(options: VerifiedRunOptions): Promise<VerifiedRunResult>;
@@ -44,6 +44,21 @@ export declare function writeLock(lock: LockFile): string;
44
44
  export declare function setPinnedHook(name: string, entry: LockEntry): string;
45
45
  export declare function getPinnedHook(name: string): LockEntry | undefined;
46
46
  export declare function removePinnedHook(name: string): boolean;
47
+ /**
48
+ * Pin a hook at install time — the ACTUAL installed version and sha, so the
49
+ * first run is trusted with real provenance instead of a 0.0.0 placeholder
50
+ * (QA-1 P3: install pinned 0.0.0 until trust).
51
+ */
52
+ export declare function pinInstalledHook(name: string, version: string, sha256: string, source: string, sourceRef?: string | null): void;
53
+ /**
54
+ * Remove every store-side record of a hook: the lock pin and the DB row.
55
+ * Does not touch hook files on disk — callers decide whether the hook lives
56
+ * in the package (bundled, keep) or the custom store dir (remove).
57
+ */
58
+ export declare function removeHookFromStore(name: string): {
59
+ removedPin: boolean;
60
+ removedRecord: boolean;
61
+ };
47
62
  export interface TrustCheck {
48
63
  ok: boolean;
49
64
  pinned: boolean;
@@ -33,3 +33,22 @@ export declare function planSync(): Promise<SyncPlan>;
33
33
  export declare function syncHooks(options?: {
34
34
  dryRun?: boolean;
35
35
  }): Promise<SyncPlan>;
36
+ export interface PinnedHookInstall {
37
+ name: string;
38
+ version: string;
39
+ sha256: string;
40
+ source: string;
41
+ source_ref: string;
42
+ artifact: ArtifactResponse;
43
+ scriptPath: string;
44
+ }
45
+ /**
46
+ * Fetch one exact hook version from the remote registry, verify its sha
47
+ * against the remote lock, write it to the custom store, and pin it.
48
+ * Powers `hooks install <name>@<version>` / `hooks update <name>@<version>`
49
+ * (QA-2 finding: pinned-version install/update was unsupported).
50
+ *
51
+ * Requires an api_url (remote registry). The sha check is against the remote
52
+ * lock — the exact version named by the user, never a mutable latest.
53
+ */
54
+ export declare function fetchPinnedHook(name: string, version: string, apiUrl: string): Promise<PinnedHookInstall>;
package/dist/storage.js CHANGED
@@ -22,7 +22,7 @@ var CREATE_HOOK_EVENTS_TABLE = `
22
22
  timestamp TEXT NOT NULL,
23
23
  session_id TEXT NOT NULL,
24
24
  hook_name TEXT NOT NULL,
25
- event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
25
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
26
26
  tool_name TEXT,
27
27
  tool_input TEXT,
28
28
  result TEXT CHECK (result IN ('continue', 'block', NULL)),
@@ -137,6 +137,35 @@ var init_004_hooks_table = __esm(() => {
137
137
  ];
138
138
  });
139
139
 
140
+ // src/db/migrations/005_subagent_start_event.ts
141
+ function up5(db) {
142
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
143
+ if (!row?.sql)
144
+ return;
145
+ if (row.sql.includes("SubagentStart"))
146
+ return;
147
+ db.exec("BEGIN");
148
+ try {
149
+ db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
150
+ db.exec(CREATE_HOOK_EVENTS_TABLE);
151
+ db.exec(`INSERT INTO hook_events
152
+ (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
153
+ SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
154
+ FROM hook_events_old`);
155
+ db.exec("DROP TABLE hook_events_old");
156
+ for (const idx of CREATE_INDEXES) {
157
+ db.exec(idx);
158
+ }
159
+ db.exec("COMMIT");
160
+ } catch (err) {
161
+ db.exec("ROLLBACK");
162
+ throw err;
163
+ }
164
+ }
165
+ var init_005_subagent_start_event = __esm(() => {
166
+ init_schema();
167
+ });
168
+
140
169
  // src/db/migrations/index.ts
141
170
  function ensureMigrationsTable(db) {
142
171
  db.exec(`
@@ -169,11 +198,13 @@ var init_migrations = __esm(() => {
169
198
  init_002_session_events();
170
199
  init_003_user_prompt_submit_event();
171
200
  init_004_hooks_table();
201
+ init_005_subagent_start_event();
172
202
  MIGRATIONS = [
173
203
  { version: "001_initial", up },
174
204
  { version: "002_session_events", up: up2 },
175
205
  { version: "003_user_prompt_submit_event", up: up3 },
176
- { version: "004_hooks_table", up: up4 }
206
+ { version: "004_hooks_table", up: up4 },
207
+ { version: "005_subagent_start_event", up: up5 }
177
208
  ];
178
209
  });
179
210
 
@@ -343,6 +374,7 @@ function getDb() {
343
374
  const isNew = dbPath === ":memory:" || !existsSync2(dbPath);
344
375
  ensureDir(dbPath);
345
376
  instance = new Database(dbPath);
377
+ instance.exec("PRAGMA busy_timeout=5000");
346
378
  instance.exec("PRAGMA journal_mode=WAL");
347
379
  instance.exec("PRAGMA foreign_keys=ON");
348
380
  runMigrations(instance);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {