@hasna/hooks 0.6.3 → 0.6.5

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/dist/index.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);
@@ -4545,149 +4577,6 @@ var init_manifest = __esm(() => {
4545
4577
  };
4546
4578
  });
4547
4579
 
4548
- // src/lib/profiles.ts
4549
- var exports_profiles = {};
4550
- __export(exports_profiles, {
4551
- updateProfile: () => updateProfile,
4552
- touchProfile: () => touchProfile,
4553
- listProfiles: () => listProfiles,
4554
- importProfiles: () => importProfiles,
4555
- getProfilesDir: () => getProfilesDir,
4556
- getProfile: () => getProfile,
4557
- exportProfiles: () => exportProfiles,
4558
- deleteProfile: () => deleteProfile,
4559
- createProfile: () => createProfile
4560
- });
4561
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync3, rmSync, cpSync as cpSync2 } from "fs";
4562
- import { join as join7 } from "path";
4563
- import { homedir as homedir5 } from "os";
4564
- function resolveProfilesDir() {
4565
- const newDir = join7(homedir5(), ".hasna", "hooks", "profiles");
4566
- const oldDir = join7(homedir5(), ".hooks", "profiles");
4567
- if (!existsSync7(newDir) && existsSync7(oldDir)) {
4568
- mkdirSync5(join7(homedir5(), ".hasna", "hooks"), { recursive: true });
4569
- cpSync2(oldDir, newDir, { recursive: true });
4570
- }
4571
- return newDir;
4572
- }
4573
- function ensureProfilesDir() {
4574
- if (!existsSync7(PROFILES_DIR)) {
4575
- mkdirSync5(PROFILES_DIR, { recursive: true });
4576
- }
4577
- }
4578
- function profilePath(id) {
4579
- return join7(PROFILES_DIR, `${id}.json`);
4580
- }
4581
- function shortUuid() {
4582
- return crypto.randomUUID().slice(0, 8);
4583
- }
4584
- function getProfilesDir() {
4585
- return PROFILES_DIR;
4586
- }
4587
- function createProfile(input) {
4588
- ensureProfilesDir();
4589
- const id = shortUuid();
4590
- const now = new Date().toISOString();
4591
- const profile = {
4592
- agent_id: id,
4593
- agent_type: input.agent_type,
4594
- created_at: now,
4595
- last_seen_at: now,
4596
- preferences: {}
4597
- };
4598
- if (input.name) {
4599
- profile.name = input.name;
4600
- }
4601
- writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
4602
- `);
4603
- return profile;
4604
- }
4605
- function getProfile(id) {
4606
- const path = profilePath(id);
4607
- try {
4608
- if (!existsSync7(path))
4609
- return null;
4610
- return JSON.parse(readFileSync5(path, "utf-8"));
4611
- } catch {
4612
- return null;
4613
- }
4614
- }
4615
- function listProfiles() {
4616
- if (!existsSync7(PROFILES_DIR))
4617
- return [];
4618
- try {
4619
- const files = readdirSync3(PROFILES_DIR).filter((f) => f.endsWith(".json"));
4620
- const profiles = [];
4621
- for (const file of files) {
4622
- try {
4623
- const content = readFileSync5(join7(PROFILES_DIR, file), "utf-8");
4624
- profiles.push(JSON.parse(content));
4625
- } catch {}
4626
- }
4627
- return profiles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
4628
- } catch {
4629
- return [];
4630
- }
4631
- }
4632
- function updateProfile(id, data) {
4633
- const profile = getProfile(id);
4634
- if (!profile)
4635
- return null;
4636
- if (data.name !== undefined)
4637
- profile.name = data.name;
4638
- if (data.preferences !== undefined)
4639
- profile.preferences = data.preferences;
4640
- writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
4641
- `);
4642
- return profile;
4643
- }
4644
- function deleteProfile(id) {
4645
- const path = profilePath(id);
4646
- if (!existsSync7(path))
4647
- return false;
4648
- try {
4649
- rmSync(path);
4650
- return true;
4651
- } catch {
4652
- return false;
4653
- }
4654
- }
4655
- function touchProfile(id) {
4656
- const profile = getProfile(id);
4657
- if (!profile)
4658
- return;
4659
- profile.last_seen_at = new Date().toISOString();
4660
- writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
4661
- `);
4662
- }
4663
- function exportProfiles() {
4664
- return listProfiles();
4665
- }
4666
- function importProfiles(profiles) {
4667
- ensureProfilesDir();
4668
- let imported = 0;
4669
- let skipped = 0;
4670
- for (const profile of profiles) {
4671
- if (!profile.agent_id || !profile.agent_type) {
4672
- skipped++;
4673
- continue;
4674
- }
4675
- const path = profilePath(profile.agent_id);
4676
- if (existsSync7(path)) {
4677
- skipped++;
4678
- continue;
4679
- }
4680
- writeFileSync4(path, JSON.stringify(profile, null, 2) + `
4681
- `);
4682
- imported++;
4683
- }
4684
- return { imported, skipped };
4685
- }
4686
- var PROFILES_DIR;
4687
- var init_profiles = __esm(() => {
4688
- PROFILES_DIR = resolveProfilesDir();
4689
- });
4690
-
4691
4580
  // src/lib/store.ts
4692
4581
  var exports_store = {};
4693
4582
  __export(exports_store, {
@@ -4700,14 +4589,16 @@ __export(exports_store, {
4700
4589
  retrustHook: () => retrustHook,
4701
4590
  removePinnedHook: () => removePinnedHook,
4702
4591
  removeHookRecord: () => removeHookRecord,
4592
+ removeHookFromStore: () => removeHookFromStore,
4703
4593
  readLock: () => readLock,
4594
+ pinInstalledHook: () => pinInstalledHook,
4704
4595
  listHookRecords: () => listHookRecords,
4705
4596
  getPinnedHook: () => getPinnedHook,
4706
4597
  getHookRecord: () => getHookRecord,
4707
4598
  checkScriptHash: () => checkScriptHash
4708
4599
  });
4709
4600
  import { createHash } from "crypto";
4710
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
4601
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4711
4602
  import { readFile } from "fs/promises";
4712
4603
  function sha256Of(input) {
4713
4604
  return createHash("sha256").update(input).digest("hex");
@@ -4754,9 +4645,9 @@ function removeHookRecord(db, name) {
4754
4645
  function readLock() {
4755
4646
  try {
4756
4647
  const path = getLockPath();
4757
- if (!existsSync8(path))
4648
+ if (!existsSync6(path))
4758
4649
  return { hooks: {} };
4759
- const parsed = JSON.parse(readFileSync6(path, "utf-8"));
4650
+ const parsed = JSON.parse(readFileSync4(path, "utf-8"));
4760
4651
  return { hooks: parsed.hooks ?? {} };
4761
4652
  } catch {
4762
4653
  return { hooks: {} };
@@ -4764,12 +4655,12 @@ function readLock() {
4764
4655
  }
4765
4656
  function writeLock(lock) {
4766
4657
  const path = getLockPath();
4767
- mkdirSync6(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
4658
+ mkdirSync4(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
4768
4659
  const sorted = {};
4769
4660
  for (const name of Object.keys(lock.hooks).sort()) {
4770
4661
  sorted[name] = lock.hooks[name];
4771
4662
  }
4772
- writeFileSync5(path, JSON.stringify({ hooks: sorted }, null, 2) + `
4663
+ writeFileSync3(path, JSON.stringify({ hooks: sorted }, null, 2) + `
4773
4664
  `, "utf-8");
4774
4665
  return path;
4775
4666
  }
@@ -4789,6 +4680,25 @@ function removePinnedHook(name) {
4789
4680
  writeLock(lock);
4790
4681
  return true;
4791
4682
  }
4683
+ function pinInstalledHook(name, version, sha256, source, sourceRef) {
4684
+ const now = new Date().toISOString();
4685
+ const db = getDb();
4686
+ upsertHookRecord(db, {
4687
+ name,
4688
+ version,
4689
+ sha256,
4690
+ source_type: source,
4691
+ source_ref: sourceRef ?? null,
4692
+ last_verified_at: now
4693
+ });
4694
+ setPinnedHook(name, { version, sha256, source });
4695
+ }
4696
+ function removeHookFromStore(name) {
4697
+ const removedPin = removePinnedHook(name);
4698
+ const db = getDb();
4699
+ const removedRecord = removeHookRecord(db, name);
4700
+ return { removedPin, removedRecord };
4701
+ }
4792
4702
  function checkScriptHash(name, actual) {
4793
4703
  const db = getDb();
4794
4704
  const record = getHookRecord(db, name);
@@ -4825,7 +4735,7 @@ async function verifyScriptHash(name, scriptPath) {
4825
4735
  return checkScriptHash(name, await sha256File(scriptPath));
4826
4736
  }
4827
4737
  function retrustHook(name, scriptPath, version, source) {
4828
- const actual = sha256Of(readFileSync6(scriptPath));
4738
+ const actual = sha256Of(readFileSync4(scriptPath));
4829
4739
  const db = getDb();
4830
4740
  upsertHookRecord(db, {
4831
4741
  name,
@@ -4842,13 +4752,157 @@ var init_store = __esm(() => {
4842
4752
  init_config();
4843
4753
  });
4844
4754
 
4755
+ // src/lib/profiles.ts
4756
+ var exports_profiles = {};
4757
+ __export(exports_profiles, {
4758
+ updateProfile: () => updateProfile,
4759
+ touchProfile: () => touchProfile,
4760
+ listProfiles: () => listProfiles,
4761
+ importProfiles: () => importProfiles,
4762
+ getProfilesDir: () => getProfilesDir,
4763
+ getProfile: () => getProfile,
4764
+ exportProfiles: () => exportProfiles,
4765
+ deleteProfile: () => deleteProfile,
4766
+ createProfile: () => createProfile
4767
+ });
4768
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
4769
+ import { join as join7 } from "path";
4770
+ import { homedir as homedir5 } from "os";
4771
+ function resolveProfilesDir() {
4772
+ const newDir = join7(homedir5(), ".hasna", "hooks", "profiles");
4773
+ const oldDir = join7(homedir5(), ".hooks", "profiles");
4774
+ if (!existsSync8(newDir) && existsSync8(oldDir)) {
4775
+ mkdirSync6(join7(homedir5(), ".hasna", "hooks"), { recursive: true });
4776
+ cpSync2(oldDir, newDir, { recursive: true });
4777
+ }
4778
+ return newDir;
4779
+ }
4780
+ function ensureProfilesDir() {
4781
+ if (!existsSync8(PROFILES_DIR)) {
4782
+ mkdirSync6(PROFILES_DIR, { recursive: true });
4783
+ }
4784
+ }
4785
+ function profilePath(id) {
4786
+ return join7(PROFILES_DIR, `${id}.json`);
4787
+ }
4788
+ function shortUuid() {
4789
+ return crypto.randomUUID().slice(0, 8);
4790
+ }
4791
+ function getProfilesDir() {
4792
+ return PROFILES_DIR;
4793
+ }
4794
+ function createProfile(input) {
4795
+ ensureProfilesDir();
4796
+ const id = shortUuid();
4797
+ const now = new Date().toISOString();
4798
+ const profile = {
4799
+ agent_id: id,
4800
+ agent_type: input.agent_type,
4801
+ created_at: now,
4802
+ last_seen_at: now,
4803
+ preferences: {}
4804
+ };
4805
+ if (input.name) {
4806
+ profile.name = input.name;
4807
+ }
4808
+ writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
4809
+ `);
4810
+ return profile;
4811
+ }
4812
+ function getProfile(id) {
4813
+ const path = profilePath(id);
4814
+ try {
4815
+ if (!existsSync8(path))
4816
+ return null;
4817
+ return JSON.parse(readFileSync6(path, "utf-8"));
4818
+ } catch {
4819
+ return null;
4820
+ }
4821
+ }
4822
+ function listProfiles() {
4823
+ if (!existsSync8(PROFILES_DIR))
4824
+ return [];
4825
+ try {
4826
+ const files = readdirSync3(PROFILES_DIR).filter((f) => f.endsWith(".json"));
4827
+ const profiles = [];
4828
+ for (const file of files) {
4829
+ try {
4830
+ const content = readFileSync6(join7(PROFILES_DIR, file), "utf-8");
4831
+ profiles.push(JSON.parse(content));
4832
+ } catch {}
4833
+ }
4834
+ return profiles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
4835
+ } catch {
4836
+ return [];
4837
+ }
4838
+ }
4839
+ function updateProfile(id, data) {
4840
+ const profile = getProfile(id);
4841
+ if (!profile)
4842
+ return null;
4843
+ if (data.name !== undefined)
4844
+ profile.name = data.name;
4845
+ if (data.preferences !== undefined)
4846
+ profile.preferences = data.preferences;
4847
+ writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
4848
+ `);
4849
+ return profile;
4850
+ }
4851
+ function deleteProfile(id) {
4852
+ const path = profilePath(id);
4853
+ if (!existsSync8(path))
4854
+ return false;
4855
+ try {
4856
+ rmSync2(path);
4857
+ return true;
4858
+ } catch {
4859
+ return false;
4860
+ }
4861
+ }
4862
+ function touchProfile(id) {
4863
+ const profile = getProfile(id);
4864
+ if (!profile)
4865
+ return;
4866
+ profile.last_seen_at = new Date().toISOString();
4867
+ writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
4868
+ `);
4869
+ }
4870
+ function exportProfiles() {
4871
+ return listProfiles();
4872
+ }
4873
+ function importProfiles(profiles) {
4874
+ ensureProfilesDir();
4875
+ let imported = 0;
4876
+ let skipped = 0;
4877
+ for (const profile of profiles) {
4878
+ if (!profile.agent_id || !profile.agent_type) {
4879
+ skipped++;
4880
+ continue;
4881
+ }
4882
+ const path = profilePath(profile.agent_id);
4883
+ if (existsSync8(path)) {
4884
+ skipped++;
4885
+ continue;
4886
+ }
4887
+ writeFileSync5(path, JSON.stringify(profile, null, 2) + `
4888
+ `);
4889
+ imported++;
4890
+ }
4891
+ return { imported, skipped };
4892
+ }
4893
+ var PROFILES_DIR;
4894
+ var init_profiles = __esm(() => {
4895
+ PROFILES_DIR = resolveProfilesDir();
4896
+ });
4897
+
4845
4898
  // src/lib/run.ts
4846
4899
  var exports_run = {};
4847
4900
  __export(exports_run, {
4848
- executeVerifiedScript: () => executeVerifiedScript
4901
+ executeVerifiedScript: () => executeVerifiedScript,
4902
+ HookTimeoutError: () => HookTimeoutError
4849
4903
  });
4850
4904
  import { randomBytes } from "crypto";
4851
- import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
4905
+ import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
4852
4906
  import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
4853
4907
  function extensionOf(scriptPath) {
4854
4908
  const base = basename2(scriptPath);
@@ -4885,37 +4939,216 @@ function interpreterFor(name, scriptPath, content) {
4885
4939
  return { command: ["bun", "run"], tempExt: ext };
4886
4940
  throw new Error(`Refusing to run hook '${name}': unsupported script extension '.${ext}' (supported: .sh .bash .ts .tsx .js .jsx .mjs .cjs .mts .cts, or a recognized shebang)`);
4887
4941
  }
4942
+ async function readPipeWithDeadline(stream, deadlineMs) {
4943
+ const reader = stream.getReader();
4944
+ const chunks = [];
4945
+ const deadline = Date.now() + deadlineMs;
4946
+ try {
4947
+ while (true) {
4948
+ const remaining = deadline - Date.now();
4949
+ if (remaining <= 0)
4950
+ break;
4951
+ let result;
4952
+ try {
4953
+ result = await Promise.race([
4954
+ reader.read(),
4955
+ new Promise((_, reject) => {
4956
+ const t = setTimeout(() => reject(new Error("pipe-drain-deadline")), remaining);
4957
+ t.unref?.();
4958
+ })
4959
+ ]);
4960
+ } catch {
4961
+ break;
4962
+ }
4963
+ if (result.done)
4964
+ break;
4965
+ chunks.push(new TextDecoder().decode(result.value));
4966
+ }
4967
+ } finally {
4968
+ try {
4969
+ await reader.cancel();
4970
+ } catch {}
4971
+ }
4972
+ return chunks.join("");
4973
+ }
4888
4974
  async function executeVerifiedScript(options) {
4889
4975
  const scriptDir = dirname4(options.scriptPath);
4890
4976
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
4891
4977
  const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${interpreter.tempExt}`);
4978
+ let proc = null;
4979
+ let timedOut = false;
4980
+ const timeoutTimer = options.timeout ? setTimeout(() => {
4981
+ timedOut = true;
4982
+ killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
4983
+ }, options.timeout) : null;
4892
4984
  try {
4893
4985
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
4894
- const proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
4986
+ proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
4895
4987
  stdin: new Response(options.stdin),
4896
4988
  stdout: "pipe",
4897
4989
  stderr: "pipe",
4898
4990
  env: options.env ?? process.env,
4899
- ...options.timeout ? { timeout: options.timeout } : {}
4991
+ detached: true
4900
4992
  });
4901
- const [stdoutText, stderrText, exitCode] = await Promise.all([
4902
- new Response(proc.stdout).text(),
4903
- new Response(proc.stderr).text(),
4904
- proc.exited
4993
+ const exitCode = await proc.exited;
4994
+ if (timedOut) {
4995
+ throw new HookTimeoutError(options.timeout);
4996
+ }
4997
+ const [stdoutText, stderrText] = await Promise.all([
4998
+ readPipeWithDeadline(proc.stdout, 750),
4999
+ readPipeWithDeadline(proc.stderr, 750)
4905
5000
  ]);
5001
+ killGroup(proc, { leaderAlive: false });
4906
5002
  return { stdout: stdoutText, stderr: stderrText, exitCode };
4907
5003
  } finally {
5004
+ if (timeoutTimer)
5005
+ clearTimeout(timeoutTimer);
4908
5006
  try {
4909
- rmSync3(tempPath, { force: true });
5007
+ rmSync4(tempPath, { force: true });
4910
5008
  } catch {}
4911
5009
  }
4912
5010
  }
4913
- var BASH_EXTENSIONS, BUN_EXTENSIONS;
5011
+ function killGroup(proc, options) {
5012
+ if (!proc)
5013
+ return;
5014
+ const pgid = proc.pid;
5015
+ for (let pass = 0;pass < 5; pass++) {
5016
+ let members = [];
5017
+ try {
5018
+ const res = Bun.spawnSync(["ps", "-eo", "pid=,pgid="], { stdout: "pipe", stderr: "pipe" });
5019
+ if (res.exitCode === 0) {
5020
+ members = res.stdout.toString().split(`
5021
+ `).map((line) => line.trim().split(/\s+/)).filter((cols) => cols.length === 2 && cols[1] === String(pgid)).map((cols) => Number(cols[0])).filter((pid) => Number.isInteger(pid) && pid > 1).filter((pid) => options.leaderAlive || pid !== proc.pid);
5022
+ }
5023
+ } catch {}
5024
+ if (members.length === 0)
5025
+ break;
5026
+ let killed = 0;
5027
+ for (const pid of members) {
5028
+ try {
5029
+ const probe = Bun.spawnSync(["ps", "-o", "pid=,pgid=", "-p", String(pid)], { stdout: "pipe", stderr: "pipe" });
5030
+ const cols = probe.stdout.toString().trim().split(/\s+/);
5031
+ if (cols.length === 2 && cols[1] === String(pgid)) {
5032
+ process.kill(pid, "SIGKILL");
5033
+ killed++;
5034
+ }
5035
+ } catch {}
5036
+ }
5037
+ if (killed === 0)
5038
+ break;
5039
+ }
5040
+ if (options.leaderAlive) {
5041
+ try {
5042
+ process.kill(proc.pid, "SIGKILL");
5043
+ } catch {}
5044
+ }
5045
+ }
5046
+ var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
4914
5047
  var init_run = __esm(() => {
5048
+ HookTimeoutError = class HookTimeoutError extends Error {
5049
+ timeoutMs;
5050
+ constructor(timeoutMs) {
5051
+ super(`Hook timed out after ${timeoutMs}ms (process group killed)`);
5052
+ this.name = "HookTimeoutError";
5053
+ this.timeoutMs = timeoutMs;
5054
+ }
5055
+ };
4915
5056
  BASH_EXTENSIONS = new Set(["sh", "bash"]);
4916
5057
  BUN_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]);
4917
5058
  });
4918
5059
 
5060
+ // src/lib/db-writer.ts
5061
+ var exports_db_writer = {};
5062
+ __export(exports_db_writer, {
5063
+ writeHookEvent: () => writeHookEvent,
5064
+ resolveEventType: () => resolveEventType,
5065
+ recordHookRun: () => recordHookRun,
5066
+ normalizeEventType: () => normalizeEventType
5067
+ });
5068
+ function nanoid2() {
5069
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
5070
+ }
5071
+ function writeHookEvent(event) {
5072
+ try {
5073
+ const db = getDb();
5074
+ const id = nanoid2();
5075
+ const timestamp = event.timestamp ?? new Date().toISOString();
5076
+ db.run(`INSERT INTO hook_events
5077
+ (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
5078
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5079
+ id,
5080
+ timestamp,
5081
+ event.session_id,
5082
+ event.hook_name,
5083
+ event.event_type,
5084
+ event.tool_name ?? null,
5085
+ event.tool_input ? event.tool_input.slice(0, 500) : null,
5086
+ event.result ?? null,
5087
+ event.error ?? null,
5088
+ event.duration_ms ?? null,
5089
+ event.project_dir ?? null,
5090
+ event.metadata ?? null
5091
+ ]);
5092
+ } catch (err) {
5093
+ process.stderr.write(`[hooks db-writer] failed to write event: ${err}
5094
+ `);
5095
+ }
5096
+ }
5097
+ function normalizeEventType(value) {
5098
+ if (typeof value !== "string")
5099
+ return null;
5100
+ if (ALLOWED_EVENT_TYPES.has(value))
5101
+ return value;
5102
+ const bare = value.split(":")[0] ?? "";
5103
+ return ALLOWED_EVENT_TYPES.has(bare) ? bare : null;
5104
+ }
5105
+ function resolveEventType(inputEvent, fallbackEvent) {
5106
+ const fromInput = normalizeEventType(inputEvent);
5107
+ if (fromInput)
5108
+ return fromInput;
5109
+ return normalizeEventType(fallbackEvent ?? null);
5110
+ }
5111
+ function recordHookRun(record) {
5112
+ const eventType = record.eventType ? normalizeEventType(record.eventType) : null;
5113
+ if (!eventType) {
5114
+ process.stderr.write(`[hooks db-writer] skipped event for '${record.hookName}': unsupported event type '${String(record.eventType)}'
5115
+ `);
5116
+ return;
5117
+ }
5118
+ const metadata = {
5119
+ ...record.metadata ?? {},
5120
+ version: record.version ?? null,
5121
+ sha256: record.sha256 ?? null,
5122
+ exit_code: record.exitCode
5123
+ };
5124
+ writeHookEvent({
5125
+ session_id: record.sessionId ?? "cli",
5126
+ hook_name: record.hookName,
5127
+ event_type: eventType,
5128
+ tool_name: record.toolName ?? null,
5129
+ tool_input: record.toolInput !== undefined ? JSON.stringify(record.toolInput) : null,
5130
+ result: record.result ?? null,
5131
+ error: record.error ?? null,
5132
+ duration_ms: record.durationMs,
5133
+ project_dir: record.projectDir ?? null,
5134
+ metadata: JSON.stringify(metadata)
5135
+ });
5136
+ }
5137
+ var ALLOWED_EVENT_TYPES;
5138
+ var init_db_writer = __esm(() => {
5139
+ init_db();
5140
+ ALLOWED_EVENT_TYPES = new Set([
5141
+ "PreToolUse",
5142
+ "PostToolUse",
5143
+ "Stop",
5144
+ "Notification",
5145
+ "SessionStart",
5146
+ "SessionEnd",
5147
+ "UserPromptSubmit",
5148
+ "SubagentStart"
5149
+ ]);
5150
+ });
5151
+
4919
5152
  // src/db/storage-sync.ts
4920
5153
  init_db();
4921
5154
 
@@ -5847,7 +6080,7 @@ function getHook(name) {
5847
6080
  return HOOKS.find((h) => h.name === name);
5848
6081
  }
5849
6082
  // src/lib/installer.ts
5850
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
6083
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
5851
6084
  import { join as join6, dirname as dirname3 } from "path";
5852
6085
  import { homedir as homedir4 } from "os";
5853
6086
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -5917,7 +6150,8 @@ function resolveHook(name) {
5917
6150
  events: custom2.manifest.events,
5918
6151
  description: custom2.manifest.description ?? "Custom hook",
5919
6152
  scriptPath: custom2.scriptPath,
5920
- meta: meta2
6153
+ meta: meta2,
6154
+ timeoutMs: custom2.manifest.timeout_ms ?? null
5921
6155
  };
5922
6156
  }
5923
6157
  const meta = getHook(shortManifestName(name));
@@ -5931,13 +6165,16 @@ function resolveHook(name) {
5931
6165
  events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
5932
6166
  description: meta.description,
5933
6167
  scriptPath,
5934
- meta
6168
+ meta,
6169
+ timeoutMs: null
5935
6170
  };
5936
6171
  }
5937
6172
 
5938
6173
  // src/lib/installer.ts
6174
+ init_manifest();
6175
+ init_store();
5939
6176
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
5940
- var HOOKS_DIR = existsSync6(join6(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join6(__dirname2, "..", "..", "hooks") : join6(__dirname2, "..", "hooks");
6177
+ var HOOKS_DIR = existsSync7(join6(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join6(__dirname2, "..", "..", "hooks") : join6(__dirname2, "..", "hooks");
5941
6178
  function normalizeHookName(name) {
5942
6179
  return name.startsWith("hook-") ? name : `hook-${name}`;
5943
6180
  }
@@ -6019,13 +6256,13 @@ function getHookPath(name) {
6019
6256
  return join6(HOOKS_DIR, normalizeHookName(shortName));
6020
6257
  }
6021
6258
  function hookExists(name) {
6022
- return existsSync6(getHookPath(name));
6259
+ return existsSync7(getHookPath(name));
6023
6260
  }
6024
6261
  function readSettings(scope = "global", target = "claude") {
6025
6262
  const path = getSettingsPath(scope, target);
6026
6263
  try {
6027
- if (existsSync6(path)) {
6028
- return JSON.parse(readFileSync4(path, "utf-8"));
6264
+ if (existsSync7(path)) {
6265
+ return JSON.parse(readFileSync5(path, "utf-8"));
6029
6266
  }
6030
6267
  } catch (error) {
6031
6268
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -6035,10 +6272,10 @@ function readSettings(scope = "global", target = "claude") {
6035
6272
  function writeSettings(settings, scope = "global", target = "claude") {
6036
6273
  const path = getSettingsPath(scope, target);
6037
6274
  const dir = dirname3(path);
6038
- if (!existsSync6(dir)) {
6039
- mkdirSync4(dir, { recursive: true });
6275
+ if (!existsSync7(dir)) {
6276
+ mkdirSync5(dir, { recursive: true });
6040
6277
  }
6041
- writeFileSync3(path, JSON.stringify(settings, null, 2) + `
6278
+ writeFileSync4(path, JSON.stringify(settings, null, 2) + `
6042
6279
  `);
6043
6280
  }
6044
6281
  function getTargetEventName(internalEvent, target) {
@@ -6124,7 +6361,7 @@ function buildCodewithTomlFragment(name, profile) {
6124
6361
  function readCodewithConfig(scope, configPath) {
6125
6362
  const path = getSettingsPath(scope, "codewith", configPath);
6126
6363
  try {
6127
- return existsSync6(path) ? readFileSync4(path, "utf-8") : "";
6364
+ return existsSync7(path) ? readFileSync5(path, "utf-8") : "";
6128
6365
  } catch {
6129
6366
  return "";
6130
6367
  }
@@ -6132,13 +6369,13 @@ function readCodewithConfig(scope, configPath) {
6132
6369
  function appendCodewithFragment(fragment, scope, configPath) {
6133
6370
  const path = getSettingsPath(scope, "codewith", configPath);
6134
6371
  const dir = dirname3(path);
6135
- if (!existsSync6(dir))
6136
- mkdirSync4(dir, { recursive: true });
6137
- const existing = existsSync6(path) ? readFileSync4(path, "utf-8") : "";
6372
+ if (!existsSync7(dir))
6373
+ mkdirSync5(dir, { recursive: true });
6374
+ const existing = existsSync7(path) ? readFileSync5(path, "utf-8") : "";
6138
6375
  const sep2 = existing.trim() ? `
6139
6376
 
6140
6377
  ` : "";
6141
- writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
6378
+ writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
6142
6379
  return path;
6143
6380
  }
6144
6381
  function detectConflict(name, scope, target) {
@@ -6314,9 +6551,6 @@ function removeHookFromAllEvents(settings, name) {
6314
6551
  }
6315
6552
  }
6316
6553
  function unregisterHook(name, scope = "global", target = "claude") {
6317
- const meta = resolveHookMeta(name);
6318
- if (!meta)
6319
- return;
6320
6554
  const settings = readSettings(scope, target);
6321
6555
  if (!settings.hooks)
6322
6556
  return;
@@ -6382,6 +6616,136 @@ function removeHookForTarget(name, scope, target) {
6382
6616
  unregisterHook(name, scope, target);
6383
6617
  return true;
6384
6618
  }
6619
+ function removeCodewithHookEntry(configText, name) {
6620
+ const sections = configText.split(/\n\s*\n/);
6621
+ const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
6622
+ const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
6623
+ const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
6624
+ const eventsWithEntries = new Set;
6625
+ const kept = [];
6626
+ let removed = false;
6627
+ for (const section of sections) {
6628
+ const lines = section.split(`
6629
+ `);
6630
+ const entryMatch = entryHeaderRe.exec(lines[0] ?? "");
6631
+ if (entryMatch) {
6632
+ const event = entryMatch[1];
6633
+ eventsWithEntries.add(event);
6634
+ if (lines.some((line) => nameRe.test(line.trim()))) {
6635
+ removed = true;
6636
+ continue;
6637
+ }
6638
+ kept.push({ text: section });
6639
+ continue;
6640
+ }
6641
+ const eventMatch = eventHeaderRe.exec(lines[0] ?? "");
6642
+ kept.push({ text: section, eventHeaderFor: eventMatch ? eventMatch[1] : undefined });
6643
+ }
6644
+ const keptEntriesByEvent = new Set;
6645
+ for (const item of kept) {
6646
+ const m = entryHeaderRe.exec(item.text.split(`
6647
+ `)[0] ?? "");
6648
+ if (m)
6649
+ keptEntriesByEvent.add(m[1]);
6650
+ }
6651
+ const finalSections = kept.filter((item) => {
6652
+ if (item.eventHeaderFor === undefined)
6653
+ return true;
6654
+ if (!eventsWithEntries.has(item.eventHeaderFor))
6655
+ return true;
6656
+ return keptEntriesByEvent.has(item.eventHeaderFor);
6657
+ }).map((item) => item.text);
6658
+ return {
6659
+ text: finalSections.join(`
6660
+
6661
+ `) + (configText.endsWith(`
6662
+ `) ? `
6663
+ ` : ""),
6664
+ removed
6665
+ };
6666
+ }
6667
+ function escapeRegExp(value) {
6668
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6669
+ }
6670
+ function codewithHasHookEntryInText(configText, name) {
6671
+ return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
6672
+ }
6673
+ function codewithHasHookEntry(name) {
6674
+ const path = getSettingsPath("global", "codewith");
6675
+ if (!existsSync7(path))
6676
+ return false;
6677
+ try {
6678
+ return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
6679
+ } catch {
6680
+ return false;
6681
+ }
6682
+ }
6683
+ function uninstallHook(name, scope = "global", target = "claude") {
6684
+ const shortName = shortHookName(name);
6685
+ const custom2 = readCustomManifest(shortName);
6686
+ const bundledMeta = custom2 ? undefined : getHook(shortName);
6687
+ const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
6688
+ const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
6689
+ const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
6690
+ const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
6691
+ const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
6692
+ if (!custom2 && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
6693
+ return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
6694
+ }
6695
+ const writableTargets = target === "all" ? ["claude", "gemini"] : target === "codewith" ? [] : [target];
6696
+ const scopes = [];
6697
+ for (const s of ["global", "project"]) {
6698
+ for (const t of writableTargets) {
6699
+ if (getRegisteredHooksForTarget(s, t).includes(shortName)) {
6700
+ unregisterHook(shortName, s, t);
6701
+ if (!scopes.includes(s))
6702
+ scopes.push(s);
6703
+ }
6704
+ }
6705
+ }
6706
+ const registrationsRemaining = [];
6707
+ if (target === "codewith" || target === "all") {
6708
+ const codewithPath = getSettingsPath("global", "codewith");
6709
+ if (existsSync7(codewithPath)) {
6710
+ const before = readFileSync5(codewithPath, "utf-8");
6711
+ const after = removeCodewithHookEntry(before, shortName);
6712
+ if (after.removed) {
6713
+ writeFileSync4(codewithPath, after.text, "utf-8");
6714
+ }
6715
+ if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
6716
+ registrationsRemaining.push("codewith");
6717
+ }
6718
+ }
6719
+ }
6720
+ if (custom2) {
6721
+ try {
6722
+ rmSync(customHookDir(shortName), { recursive: true, force: true });
6723
+ } catch {
6724
+ return {
6725
+ name: shortName,
6726
+ removed: false,
6727
+ source: "custom",
6728
+ settingsScopes: scopes,
6729
+ storeDirRemoved: false,
6730
+ pinRemoved: false,
6731
+ dbRecordRemoved: false,
6732
+ registrationsRemaining,
6733
+ error: `Hook '${shortName}' store directory could not be removed; trust records preserved (no fail-open)`
6734
+ };
6735
+ }
6736
+ }
6737
+ const { removedPin, removedRecord } = removeHookFromStore(shortName);
6738
+ return {
6739
+ name: shortName,
6740
+ removed: true,
6741
+ source: custom2 ? "custom" : bundledMeta ? "bundled" : "registered-only",
6742
+ settingsScopes: scopes,
6743
+ storeDirRemoved: custom2 ? true : false,
6744
+ pinRemoved: removedPin,
6745
+ dbRecordRemoved: removedRecord,
6746
+ registrationsRemaining
6747
+ };
6748
+ }
6385
6749
  // src/index.ts
6386
6750
  init_profiles();
6387
6751
  init_config();
@@ -6392,7 +6756,7 @@ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
6392
6756
  // src/lib/custom-install.ts
6393
6757
  init_config();
6394
6758
  init_manifest();
6395
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
6759
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
6396
6760
  import { join as join8 } from "path";
6397
6761
  import { tmpdir } from "os";
6398
6762
  function manifestAt(dir) {
@@ -6430,7 +6794,7 @@ async function cloneToTemp(gitUrl) {
6430
6794
  proc.exited
6431
6795
  ]);
6432
6796
  if (exitCode !== 0) {
6433
- rmSync2(tmp, { recursive: true, force: true });
6797
+ rmSync3(tmp, { recursive: true, force: true });
6434
6798
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
6435
6799
  }
6436
6800
  return join8(tmp, "repo");
@@ -6451,7 +6815,7 @@ async function installCustomSource(source) {
6451
6815
  try {
6452
6816
  return copyManifestDir(repoDir, source, "git");
6453
6817
  } finally {
6454
- rmSync2(repoDir, { recursive: true, force: true });
6818
+ rmSync3(repoDir, { recursive: true, force: true });
6455
6819
  }
6456
6820
  }
6457
6821
  if (source.startsWith("http://") || source.startsWith("https://")) {
@@ -6523,6 +6887,7 @@ async function fetchJson(base, path) {
6523
6887
  headers["x-api-key"] = apiKey;
6524
6888
  const res = await fetch(`${base}${path}`, {
6525
6889
  headers,
6890
+ redirect: "error",
6526
6891
  signal: AbortSignal.timeout(15000)
6527
6892
  });
6528
6893
  if (res.status === 401) {
@@ -6626,11 +6991,14 @@ async function syncHooks(options = {}) {
6626
6991
  writeCustomHook(name, manifest, artifact.script, scriptRel);
6627
6992
  const scriptPath = resolveScriptPath(name);
6628
6993
  const actual = await sha256File(scriptPath);
6629
- setPinnedHook(name, { version: entry.version, sha256: actual, source: entry.source ?? "remote" });
6994
+ if (actual !== entry.sha256) {
6995
+ throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
6996
+ }
6997
+ setPinnedHook(name, { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" });
6630
6998
  upsertHookRecord(db, {
6631
6999
  name,
6632
7000
  version: entry.version,
6633
- sha256: actual,
7001
+ sha256: entry.sha256,
6634
7002
  source_type: entry.source ?? "remote",
6635
7003
  source_ref: apiUrl,
6636
7004
  last_verified_at: new Date().toISOString()
@@ -6638,6 +7006,59 @@ async function syncHooks(options = {}) {
6638
7006
  }
6639
7007
  return { apiUrl, dryRun: false, diff };
6640
7008
  }
7009
+ async function fetchPinnedHook(name, version, apiUrl) {
7010
+ const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
7011
+ const entry = remoteLock.hooks?.[name];
7012
+ if (!entry) {
7013
+ throw new Error(`Hook '${name}' is not in the remote registry lock`);
7014
+ }
7015
+ if (entry.version !== version) {
7016
+ throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${entry.version})`);
7017
+ }
7018
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}`);
7019
+ if (!artifact.manifest || typeof artifact.script !== "string") {
7020
+ throw new Error(`artifact for '${name}@${version}' is malformed`);
7021
+ }
7022
+ const manifestName = shortManifestName(artifact.manifest.name ?? name);
7023
+ if (manifestName !== name) {
7024
+ throw new Error(`artifact for '${name}@${version}' declares a different hook name ('${manifestName}')`);
7025
+ }
7026
+ if (artifact.manifest.version !== version) {
7027
+ throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
7028
+ }
7029
+ const actualSha = sha256OfText(artifact.script);
7030
+ if (actualSha !== entry.sha256) {
7031
+ throw new Error(`sha256 mismatch for '${name}@${version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
7032
+ }
7033
+ const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
7034
+ const scriptRel = artifact.manifest.script.includes(`
7035
+ `) ? "script.ts" : artifact.manifest.script;
7036
+ writeCustomHook(name, manifest, artifact.script, scriptRel);
7037
+ const scriptPath = resolveScriptPath(name);
7038
+ const actual = await sha256File(scriptPath);
7039
+ if (actual !== entry.sha256) {
7040
+ throw new Error(`sha256 mismatch after write for '${name}@${version}': lock says ${entry.sha256}, on disk ${actual}`);
7041
+ }
7042
+ const db = getDb();
7043
+ setPinnedHook(name, { version, sha256: entry.sha256, source: entry.source ?? "remote" });
7044
+ upsertHookRecord(db, {
7045
+ name,
7046
+ version,
7047
+ sha256: entry.sha256,
7048
+ source_type: entry.source ?? "remote",
7049
+ source_ref: apiUrl,
7050
+ last_verified_at: new Date().toISOString()
7051
+ });
7052
+ return {
7053
+ name,
7054
+ version,
7055
+ sha256: entry.sha256,
7056
+ source: entry.source ?? "remote",
7057
+ source_ref: apiUrl,
7058
+ artifact,
7059
+ scriptPath
7060
+ };
7061
+ }
6641
7062
  // src/serve.ts
6642
7063
  init_manifest();
6643
7064
  init_store();
@@ -6897,16 +7318,68 @@ async function runHook(name, input, options = {}) {
6897
7318
  };
6898
7319
  }
6899
7320
  }
6900
- const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
6901
- const { stdout: stdoutText, stderr: stderrText, exitCode } = await executeVerifiedScript2({
6902
- name,
6903
- scriptPath: script,
6904
- content,
6905
- args: custom2?.manifest.args ?? [],
6906
- stdin: JSON.stringify(hookInput),
6907
- env: process.env,
6908
- timeout: custom2?.manifest.timeout_ms
6909
- });
7321
+ const { executeVerifiedScript: executeVerifiedScript2, HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
7322
+ const manifestTimeout = custom2?.manifest.timeout_ms ?? null;
7323
+ const effectiveTimeout = options.timeout !== undefined && options.timeout > 0 ? options.timeout : manifestTimeout ?? null;
7324
+ const started = Date.now();
7325
+ let stdoutText = "";
7326
+ let stderrText = "";
7327
+ let exitCode = 0;
7328
+ try {
7329
+ ({ stdout: stdoutText, stderr: stderrText, exitCode } = await executeVerifiedScript2({
7330
+ name,
7331
+ scriptPath: script,
7332
+ content,
7333
+ args: custom2?.manifest.args ?? [],
7334
+ stdin: JSON.stringify(hookInput),
7335
+ env: process.env,
7336
+ timeout: effectiveTimeout ?? undefined
7337
+ }));
7338
+ } catch (err) {
7339
+ if (err instanceof HookTimeoutError2) {
7340
+ try {
7341
+ const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
7342
+ recordHookRun2({
7343
+ hookName: name,
7344
+ eventType: resolveEventType2(input.hook_event_name, custom2?.manifest.events[0] ?? "PostToolUse"),
7345
+ version: custom2?.manifest.version,
7346
+ sha256: sha256Of2(content),
7347
+ sessionId: typeof input.session_id === "string" ? input.session_id : null,
7348
+ toolName: typeof input.tool_name === "string" ? input.tool_name : null,
7349
+ toolInput: input.tool_input,
7350
+ error: err.message.slice(0, 500),
7351
+ exitCode: -1,
7352
+ durationMs: Date.now() - started,
7353
+ projectDir: process.cwd()
7354
+ });
7355
+ } catch {}
7356
+ throw err;
7357
+ }
7358
+ throw err;
7359
+ }
7360
+ const durationMs = Date.now() - started;
7361
+ try {
7362
+ const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
7363
+ let outputJson = {};
7364
+ try {
7365
+ outputJson = JSON.parse(stdoutText);
7366
+ } catch {}
7367
+ const blocked = outputJson.decision === "block" || outputJson.continue === false;
7368
+ recordHookRun2({
7369
+ hookName: name,
7370
+ eventType: resolveEventType2(input.hook_event_name, custom2?.manifest.events[0] ?? "PostToolUse"),
7371
+ version: custom2?.manifest.version,
7372
+ sha256: sha256Of2(content),
7373
+ sessionId: typeof input.session_id === "string" ? input.session_id : null,
7374
+ toolName: typeof input.tool_name === "string" ? input.tool_name : null,
7375
+ toolInput: input.tool_input,
7376
+ result: blocked ? "block" : "continue",
7377
+ error: exitCode !== 0 ? (stderrText || `hook exited with code ${exitCode}`).slice(0, 500) : null,
7378
+ exitCode,
7379
+ durationMs,
7380
+ projectDir: process.cwd()
7381
+ });
7382
+ } catch {}
6910
7383
  let output = {};
6911
7384
  try {
6912
7385
  output = JSON.parse(stdoutText);
@@ -6922,6 +7395,7 @@ export {
6922
7395
  verifyScriptHash,
6923
7396
  upsertHookRecord,
6924
7397
  updateProfile,
7398
+ uninstallHook,
6925
7399
  touchProfile,
6926
7400
  syncHooks,
6927
7401
  storageSync,
@@ -6946,12 +7420,14 @@ export {
6946
7420
  removeProjectHook,
6947
7421
  removePinnedHook,
6948
7422
  removeHookRecord,
7423
+ removeHookFromStore,
6949
7424
  removeHook,
6950
7425
  readLock,
6951
7426
  readCustomManifest,
6952
7427
  readConfig,
6953
7428
  provisionCloudflareResources,
6954
7429
  planSync,
7430
+ pinInstalledHook,
6955
7431
  parseStorageTables,
6956
7432
  parseManifest,
6957
7433
  listProjectHooks,
@@ -6991,6 +7467,7 @@ export {
6991
7467
  getHook,
6992
7468
  getCustomHooksDir,
6993
7469
  getConfigPath,
7470
+ fetchPinnedHook,
6994
7471
  exportProfiles,
6995
7472
  deleteProfile,
6996
7473
  createProfile,