@hasna/hooks 0.2.15 → 0.2.17

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/index.js CHANGED
@@ -861,7 +861,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
861
861
  this._exitCallback = (err) => {
862
862
  if (err.code !== "commander.executeSubCommandAsync") {
863
863
  throw err;
864
- } else {}
864
+ }
865
865
  };
866
866
  }
867
867
  return this;
@@ -3946,9 +3946,9 @@ var require_cli_spinners = __commonJS((exports, module) => {
3946
3946
  });
3947
3947
 
3948
3948
  // src/lib/installer.ts
3949
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3950
- import { join, dirname } from "path";
3951
- import { homedir } from "os";
3949
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
3950
+ import { join as join2, dirname } from "path";
3951
+ import { homedir as homedir2 } from "os";
3952
3952
  import { fileURLToPath } from "url";
3953
3953
  function normalizeHookName(name) {
3954
3954
  return name.startsWith("hook-") ? name : `hook-${name}`;
@@ -3970,20 +3970,20 @@ function getTargetSettingsDir(target) {
3970
3970
  function getSettingsPath(scope = "global", target = "claude") {
3971
3971
  const dir = getTargetSettingsDir(target);
3972
3972
  if (scope === "project") {
3973
- return join(process.cwd(), dir, "settings.json");
3973
+ return join2(process.cwd(), dir, "settings.json");
3974
3974
  }
3975
- return join(homedir(), dir, "settings.json");
3975
+ return join2(homedir2(), dir, "settings.json");
3976
3976
  }
3977
3977
  function getHookPath(name) {
3978
- return join(HOOKS_DIR, normalizeHookName(name));
3978
+ return join2(HOOKS_DIR, normalizeHookName(name));
3979
3979
  }
3980
3980
  function hookExists(name) {
3981
- return existsSync(getHookPath(name));
3981
+ return existsSync2(getHookPath(name));
3982
3982
  }
3983
3983
  function readSettings(scope = "global", target = "claude") {
3984
3984
  const path = getSettingsPath(scope, target);
3985
3985
  try {
3986
- if (existsSync(path)) {
3986
+ if (existsSync2(path)) {
3987
3987
  return JSON.parse(readFileSync(path, "utf-8"));
3988
3988
  }
3989
3989
  } catch (error) {
@@ -3994,7 +3994,7 @@ function readSettings(scope = "global", target = "claude") {
3994
3994
  function writeSettings(settings, scope = "global", target = "claude") {
3995
3995
  const path = getSettingsPath(scope, target);
3996
3996
  const dir = dirname(path);
3997
- if (!existsSync(dir)) {
3997
+ if (!existsSync2(dir)) {
3998
3998
  mkdirSync(dir, { recursive: true });
3999
3999
  }
4000
4000
  writeFileSync(path, JSON.stringify(settings, null, 2) + `
@@ -4136,7 +4136,7 @@ var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks;
4136
4136
  var init_installer = __esm(() => {
4137
4137
  init_registry();
4138
4138
  __dirname2 = dirname(fileURLToPath(import.meta.url));
4139
- HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
4139
+ HOOKS_DIR = existsSync2(join2(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join2(__dirname2, "..", "..", "hooks") : join2(__dirname2, "..", "hooks");
4140
4140
  EVENT_MAP = {
4141
4141
  claude: {
4142
4142
  PreToolUse: "PreToolUse",
@@ -4155,25 +4155,25 @@ var init_installer = __esm(() => {
4155
4155
  });
4156
4156
 
4157
4157
  // src/lib/profiles.ts
4158
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync, rmSync, cpSync } from "fs";
4159
- import { join as join2 } from "path";
4160
- import { homedir as homedir2 } from "os";
4158
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync, rmSync, cpSync } from "fs";
4159
+ import { join as join3 } from "path";
4160
+ import { homedir as homedir3 } from "os";
4161
4161
  function resolveProfilesDir() {
4162
- const newDir = join2(homedir2(), ".hasna", "hooks", "profiles");
4163
- const oldDir = join2(homedir2(), ".hooks", "profiles");
4164
- if (!existsSync2(newDir) && existsSync2(oldDir)) {
4165
- mkdirSync2(join2(homedir2(), ".hasna", "hooks"), { recursive: true });
4162
+ const newDir = join3(homedir3(), ".hasna", "hooks", "profiles");
4163
+ const oldDir = join3(homedir3(), ".hooks", "profiles");
4164
+ if (!existsSync3(newDir) && existsSync3(oldDir)) {
4165
+ mkdirSync2(join3(homedir3(), ".hasna", "hooks"), { recursive: true });
4166
4166
  cpSync(oldDir, newDir, { recursive: true });
4167
4167
  }
4168
4168
  return newDir;
4169
4169
  }
4170
4170
  function ensureProfilesDir() {
4171
- if (!existsSync2(PROFILES_DIR)) {
4171
+ if (!existsSync3(PROFILES_DIR)) {
4172
4172
  mkdirSync2(PROFILES_DIR, { recursive: true });
4173
4173
  }
4174
4174
  }
4175
4175
  function profilePath(id) {
4176
- return join2(PROFILES_DIR, `${id}.json`);
4176
+ return join3(PROFILES_DIR, `${id}.json`);
4177
4177
  }
4178
4178
  function shortUuid() {
4179
4179
  return crypto.randomUUID().slice(0, 8);
@@ -4181,12 +4181,12 @@ function shortUuid() {
4181
4181
  function createProfile(input) {
4182
4182
  ensureProfilesDir();
4183
4183
  const id = shortUuid();
4184
- const now = new Date().toISOString();
4184
+ const now2 = new Date().toISOString();
4185
4185
  const profile = {
4186
4186
  agent_id: id,
4187
4187
  agent_type: input.agent_type,
4188
- created_at: now,
4189
- last_seen_at: now,
4188
+ created_at: now2,
4189
+ last_seen_at: now2,
4190
4190
  preferences: {}
4191
4191
  };
4192
4192
  if (input.name) {
@@ -4199,7 +4199,7 @@ function createProfile(input) {
4199
4199
  function getProfile(id) {
4200
4200
  const path = profilePath(id);
4201
4201
  try {
4202
- if (!existsSync2(path))
4202
+ if (!existsSync3(path))
4203
4203
  return null;
4204
4204
  return JSON.parse(readFileSync2(path, "utf-8"));
4205
4205
  } catch {
@@ -4207,14 +4207,14 @@ function getProfile(id) {
4207
4207
  }
4208
4208
  }
4209
4209
  function listProfiles() {
4210
- if (!existsSync2(PROFILES_DIR))
4210
+ if (!existsSync3(PROFILES_DIR))
4211
4211
  return [];
4212
4212
  try {
4213
4213
  const files = readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".json"));
4214
4214
  const profiles = [];
4215
4215
  for (const file of files) {
4216
4216
  try {
4217
- const content = readFileSync2(join2(PROFILES_DIR, file), "utf-8");
4217
+ const content = readFileSync2(join3(PROFILES_DIR, file), "utf-8");
4218
4218
  profiles.push(JSON.parse(content));
4219
4219
  } catch {}
4220
4220
  }
@@ -4244,7 +4244,7 @@ function importProfiles(profiles) {
4244
4244
  continue;
4245
4245
  }
4246
4246
  const path = profilePath(profile.agent_id);
4247
- if (existsSync2(path)) {
4247
+ if (existsSync3(path)) {
4248
4248
  skipped++;
4249
4249
  continue;
4250
4250
  }
@@ -4263,23 +4263,23 @@ var init_profiles = __esm(() => {
4263
4263
  import { createRequire } from "module";
4264
4264
  import { Database } from "bun:sqlite";
4265
4265
  import {
4266
- existsSync as existsSync3,
4266
+ existsSync as existsSync4,
4267
4267
  mkdirSync as mkdirSync3,
4268
4268
  readdirSync as readdirSync2,
4269
4269
  copyFileSync
4270
4270
  } from "fs";
4271
- import { homedir as homedir3 } from "os";
4272
- import { join as join3, relative } from "path";
4271
+ import { homedir as homedir4 } from "os";
4272
+ import { join as join4, relative } from "path";
4273
4273
  import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4274
4274
  import { homedir as homedir22 } from "os";
4275
4275
  import { join as join22 } from "path";
4276
- import { readdirSync as readdirSync22, existsSync as existsSync32 } from "fs";
4277
- import { join as join32 } from "path";
4276
+ import { readdirSync as readdirSync3, existsSync as existsSync6 } from "fs";
4277
+ import { join as join6 } from "path";
4278
+ import { homedir as homedir5 } from "os";
4278
4279
  import { homedir as homedir32 } from "os";
4279
- import { homedir as homedir4 } from "os";
4280
- import { join as join4 } from "path";
4281
- import { join as join6, dirname as dirname2 } from "path";
4282
- import { homedir as homedir5, platform } from "os";
4280
+ import { join as join32 } from "path";
4281
+ import { join as join5, dirname as dirname2 } from "path";
4282
+ import { homedir as homedir42, platform } from "os";
4283
4283
  function __accessProp2(key) {
4284
4284
  return this[key];
4285
4285
  }
@@ -5093,13 +5093,13 @@ function custom(check, _params = {}, fatal) {
5093
5093
  return ZodAny.create();
5094
5094
  }
5095
5095
  function getDataDir(serviceName) {
5096
- const dir = join3(HASNA_DIR, serviceName);
5096
+ const dir = join4(HASNA_DIR, serviceName);
5097
5097
  mkdirSync3(dir, { recursive: true });
5098
5098
  return dir;
5099
5099
  }
5100
5100
  function getDbPath(serviceName) {
5101
5101
  const dir = getDataDir(serviceName);
5102
- return join3(dir, `${serviceName}.db`);
5102
+ return join4(dir, `${serviceName}.db`);
5103
5103
  }
5104
5104
  function getConfigDir() {
5105
5105
  return CONFIG_DIR;
@@ -5150,11 +5150,11 @@ function isSyncExcludedTable(table) {
5150
5150
  return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
5151
5151
  }
5152
5152
  function discoverServices() {
5153
- const dataDir = join32(homedir32(), ".hasna");
5154
- if (!existsSync32(dataDir))
5153
+ const dataDir = join6(homedir5(), ".hasna");
5154
+ if (!existsSync6(dataDir))
5155
5155
  return [];
5156
5156
  try {
5157
- const entries = readdirSync22(dataDir, { withFileTypes: true });
5157
+ const entries = readdirSync3(dataDir, { withFileTypes: true });
5158
5158
  return entries.filter((e) => {
5159
5159
  if (!e.isDirectory())
5160
5160
  return false;
@@ -5166,30 +5166,30 @@ function discoverServices() {
5166
5166
  return [];
5167
5167
  }
5168
5168
  }
5169
- function discoverSyncableServices() {
5169
+ function discoverSyncableServices2() {
5170
5170
  const local = discoverServices();
5171
5171
  const pgSet = new Set(KNOWN_PG_SERVICES);
5172
5172
  return local.filter((s) => pgSet.has(s));
5173
5173
  }
5174
5174
  function getServiceDbPath(service) {
5175
- const dataDir = join32(homedir32(), ".hasna", service);
5176
- if (!existsSync32(dataDir))
5175
+ const dataDir = join6(homedir5(), ".hasna", service);
5176
+ if (!existsSync6(dataDir))
5177
5177
  return null;
5178
5178
  const candidates = [
5179
- join32(dataDir, `${service}.db`),
5180
- join32(dataDir, "data.db"),
5181
- join32(dataDir, "database.db")
5179
+ join6(dataDir, `${service}.db`),
5180
+ join6(dataDir, "data.db"),
5181
+ join6(dataDir, "database.db")
5182
5182
  ];
5183
5183
  try {
5184
- const files = readdirSync22(dataDir);
5184
+ const files = readdirSync3(dataDir);
5185
5185
  for (const f of files) {
5186
5186
  if (f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm")) {
5187
- candidates.push(join32(dataDir, f));
5187
+ candidates.push(join6(dataDir, f));
5188
5188
  }
5189
5189
  }
5190
5190
  } catch {}
5191
5191
  for (const p of candidates) {
5192
- if (existsSync32(p))
5192
+ if (existsSync6(p))
5193
5193
  return p;
5194
5194
  }
5195
5195
  return null;
@@ -5218,8 +5218,8 @@ class SyncProgressTracker {
5218
5218
  }
5219
5219
  start(table, total, direction) {
5220
5220
  const resumed = this.canResume(table);
5221
- const now = Date.now();
5222
- this.startTimes.set(table, now);
5221
+ const now2 = Date.now();
5222
+ this.startTimes.set(table, now2);
5223
5223
  const status = resumed ? "resumed" : "in_progress";
5224
5224
  const info = {
5225
5225
  table,
@@ -13528,7 +13528,7 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
13528
13528
  init_external();
13529
13529
  });
13530
13530
  init_dotfile = __esm2(() => {
13531
- HASNA_DIR = join3(homedir3(), ".hasna");
13531
+ HASNA_DIR = join4(homedir4(), ".hasna");
13532
13532
  });
13533
13533
  exports_config = {};
13534
13534
  __export2(exports_config, {
@@ -13566,7 +13566,7 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
13566
13566
  __export2(exports_discover, {
13567
13567
  isSyncExcludedTable: () => isSyncExcludedTable,
13568
13568
  getServiceDbPath: () => getServiceDbPath,
13569
- discoverSyncableServices: () => discoverSyncableServices,
13569
+ discoverSyncableServices: () => discoverSyncableServices2,
13570
13570
  discoverServices: () => discoverServices,
13571
13571
  SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
13572
13572
  KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
@@ -13621,15 +13621,13 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
13621
13621
  init_config();
13622
13622
  init_config();
13623
13623
  init_dotfile();
13624
- init_adapter();
13625
13624
  init_config();
13626
- init_discover();
13627
- AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
13625
+ AUTO_SYNC_CONFIG_PATH = join32(homedir32(), ".hasna", "cloud", "config.json");
13628
13626
  init_config();
13629
13627
  init_adapter();
13630
13628
  init_dotfile();
13631
13629
  init_config();
13632
- CONFIG_DIR2 = join6(homedir5(), ".hasna", "cloud");
13630
+ CONFIG_DIR2 = join5(homedir42(), ".hasna", "cloud");
13633
13631
  init_adapter();
13634
13632
  init_config();
13635
13633
  init_discover();
@@ -13713,8 +13711,8 @@ var init_migrations = __esm(() => {
13713
13711
  });
13714
13712
 
13715
13713
  // src/db/legacy-import.ts
13716
- import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "fs";
13717
- import { join as join5 } from "path";
13714
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "fs";
13715
+ import { join as join7 } from "path";
13718
13716
  import { homedir as homedir6 } from "os";
13719
13717
  function ensureMetaTable(db) {
13720
13718
  db.exec(`
@@ -13794,20 +13792,20 @@ function runLegacyImport(db) {
13794
13792
  if (isAlreadyDone(db))
13795
13793
  return;
13796
13794
  let total = 0;
13797
- const claudeProjectsDir = join5(homedir6(), ".claude", "projects");
13798
- if (existsSync4(claudeProjectsDir)) {
13795
+ const claudeProjectsDir = join7(homedir6(), ".claude", "projects");
13796
+ if (existsSync5(claudeProjectsDir)) {
13799
13797
  try {
13800
- const projectDirs = readdirSync3(claudeProjectsDir);
13798
+ const projectDirs = readdirSync4(claudeProjectsDir);
13801
13799
  for (const dir of projectDirs) {
13802
- const projectDir = join5(claudeProjectsDir, dir);
13800
+ const projectDir = join7(claudeProjectsDir, dir);
13803
13801
  try {
13804
- const files = readdirSync3(projectDir);
13802
+ const files = readdirSync4(projectDir);
13805
13803
  for (const file of files) {
13806
13804
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
13807
- total += importJsonlFile(db, join5(projectDir, file));
13805
+ total += importJsonlFile(db, join7(projectDir, file));
13808
13806
  }
13809
13807
  if (file === "errors.log") {
13810
- total += importErrorsLog(db, join5(projectDir, file));
13808
+ total += importErrorsLog(db, join7(projectDir, file));
13811
13809
  }
13812
13810
  }
13813
13811
  } catch {}
@@ -13849,17 +13847,17 @@ __export(exports_db, {
13849
13847
  createTestDb: () => createTestDb,
13850
13848
  closeDb: () => closeDb
13851
13849
  });
13852
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, cpSync as cpSync2 } from "fs";
13853
- import { join as join7 } from "path";
13850
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, cpSync as cpSync2 } from "fs";
13851
+ import { join as join9 } from "path";
13854
13852
  import { homedir as homedir8 } from "os";
13855
13853
  function resolveDataDir() {
13856
13854
  const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
13857
13855
  if (explicit)
13858
13856
  return explicit;
13859
- const newDir = join7(homedir8(), ".hasna", "hooks");
13860
- const oldDir = join7(homedir8(), ".hooks");
13861
- if (!existsSync5(newDir) && existsSync5(oldDir)) {
13862
- mkdirSync4(join7(homedir8(), ".hasna"), { recursive: true });
13857
+ const newDir = join9(homedir8(), ".hasna", "hooks");
13858
+ const oldDir = join9(homedir8(), ".hooks");
13859
+ if (!existsSync7(newDir) && existsSync7(oldDir)) {
13860
+ mkdirSync4(join9(homedir8(), ".hasna"), { recursive: true });
13863
13861
  cpSync2(oldDir, newDir, { recursive: true });
13864
13862
  }
13865
13863
  return newDir;
@@ -13869,11 +13867,11 @@ function getDbPath2() {
13869
13867
  if (explicitDb)
13870
13868
  return explicitDb;
13871
13869
  const dataDir = resolveDataDir();
13872
- return join7(dataDir, "hooks.db");
13870
+ return join9(dataDir, "hooks.db");
13873
13871
  }
13874
13872
  function ensureDir(dbPath) {
13875
13873
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
13876
- if (dir && !existsSync5(dir)) {
13874
+ if (dir && !existsSync7(dir)) {
13877
13875
  mkdirSync4(dir, { recursive: true });
13878
13876
  }
13879
13877
  }
@@ -13881,7 +13879,7 @@ function getDb() {
13881
13879
  if (instance)
13882
13880
  return instance;
13883
13881
  const dbPath = getDbPath2();
13884
- const isNew = dbPath === ":memory:" || !existsSync5(dbPath);
13882
+ const isNew = dbPath === ":memory:" || !existsSync7(dbPath);
13885
13883
  ensureDir(dbPath);
13886
13884
  instance = new SqliteAdapter(dbPath);
13887
13885
  instance.exec("PRAGMA journal_mode=WAL");
@@ -13934,8 +13932,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
13934
13932
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13935
13933
  import { z } from "zod";
13936
13934
  import { createServer } from "http";
13937
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
13938
- import { join as join8, dirname as dirname3 } from "path";
13935
+ import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
13936
+ import { join as join10, dirname as dirname3 } from "path";
13939
13937
  import { fileURLToPath as fileURLToPath2 } from "url";
13940
13938
  function formatInstallResults(results, extra) {
13941
13939
  const installed = results.filter((r) => r.success).map((r) => r.hook);
@@ -14032,7 +14030,7 @@ function createHooksServer() {
14032
14030
  const settingsPath = getSettingsPath(scope);
14033
14031
  const issues = [];
14034
14032
  const healthy = [];
14035
- const settingsExist = existsSync6(settingsPath);
14033
+ const settingsExist = existsSync9(settingsPath);
14036
14034
  if (!settingsExist) {
14037
14035
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
14038
14036
  }
@@ -14045,7 +14043,7 @@ function createHooksServer() {
14045
14043
  continue;
14046
14044
  }
14047
14045
  const hookDir = getHookPath(name);
14048
- if (!existsSync6(join8(hookDir, "src", "hook.ts"))) {
14046
+ if (!existsSync9(join10(hookDir, "src", "hook.ts"))) {
14049
14047
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
14050
14048
  hookHealthy = false;
14051
14049
  }
@@ -14082,9 +14080,9 @@ function createHooksServer() {
14082
14080
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
14083
14081
  }
14084
14082
  const hookPath = getHookPath(name);
14085
- const readmePath = join8(hookPath, "README.md");
14083
+ const readmePath = join10(hookPath, "README.md");
14086
14084
  let readme = "";
14087
- if (existsSync6(readmePath)) {
14085
+ if (existsSync9(readmePath)) {
14088
14086
  readme = readFileSync5(readmePath, "utf-8");
14089
14087
  }
14090
14088
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
@@ -14134,8 +14132,8 @@ function createHooksServer() {
14134
14132
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
14135
14133
  }
14136
14134
  const hookDir = getHookPath(name);
14137
- const hookScript = join8(hookDir, "src", "hook.ts");
14138
- if (!existsSync6(hookScript)) {
14135
+ const hookScript = join10(hookDir, "src", "hook.ts");
14136
+ if (!existsSync9(hookScript)) {
14139
14137
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
14140
14138
  }
14141
14139
  let hookInput = { ...input };
@@ -14226,7 +14224,7 @@ function createHooksServer() {
14226
14224
  const ctx = {
14227
14225
  scope,
14228
14226
  settings_path: settingsPath,
14229
- settings_exists: existsSync6(settingsPath),
14227
+ settings_exists: existsSync9(settingsPath),
14230
14228
  registered_hooks: hooks,
14231
14229
  hook_count: hooks.length,
14232
14230
  healthy,
@@ -14264,8 +14262,8 @@ function createHooksServer() {
14264
14262
  const input = { tool_name, tool_input };
14265
14263
  const results = await Promise.all(matchingHooks.map(async (name) => {
14266
14264
  const hookDir = getHookPath(name);
14267
- const hookScript = join8(hookDir, "src", "hook.ts");
14268
- if (!existsSync6(hookScript))
14265
+ const hookScript = join10(hookDir, "src", "hook.ts");
14266
+ if (!existsSync9(hookScript))
14269
14267
  return { name, decision: "approve", error: "script not found" };
14270
14268
  const proc = Bun.spawn(["bun", "run", hookScript], {
14271
14269
  stdin: new Response(JSON.stringify(input)),
@@ -14333,8 +14331,8 @@ function createHooksServer() {
14333
14331
  const meta = getHook(name);
14334
14332
  if (!meta)
14335
14333
  return { name, error: `Hook '${name}' not found` };
14336
- const hookScript = join8(getHookPath(name), "src", "hook.ts");
14337
- if (!existsSync6(hookScript))
14334
+ const hookScript = join10(getHookPath(name), "src", "hook.ts");
14335
+ if (!existsSync9(hookScript))
14338
14336
  return { name, error: "script not found" };
14339
14337
  const proc = Bun.spawn(["bun", "run", hookScript], {
14340
14338
  stdin: new Response(JSON.stringify(input)),
@@ -14367,7 +14365,7 @@ function createHooksServer() {
14367
14365
  const settingsPath = getSettingsPath(scope);
14368
14366
  let settings = {};
14369
14367
  try {
14370
- if (existsSync6(settingsPath))
14368
+ if (existsSync9(settingsPath))
14371
14369
  settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
14372
14370
  } catch {}
14373
14371
  if (!settings.hooks)
@@ -14390,7 +14388,7 @@ function createHooksServer() {
14390
14388
  const settingsPath = getSettingsPath(scope);
14391
14389
  let settings = {};
14392
14390
  try {
14393
- if (existsSync6(settingsPath))
14391
+ if (existsSync9(settingsPath))
14394
14392
  settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
14395
14393
  } catch {}
14396
14394
  if (settings.hooks?.__disabled) {
@@ -14623,8 +14621,8 @@ var init_server = __esm(() => {
14623
14621
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
14624
14622
  try {
14625
14623
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
14626
- const p = join8(__dirname3, rel);
14627
- if (existsSync6(p)) {
14624
+ const p = join10(__dirname3, rel);
14625
+ if (existsSync9(p)) {
14628
14626
  pkg = JSON.parse(readFileSync5(p, "utf-8"));
14629
14627
  break;
14630
14628
  }
@@ -14692,6 +14690,690 @@ function startMcpHttpServer(options) {
14692
14690
  var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
14693
14691
  var init_http = () => {};
14694
14692
 
14693
+ // node_modules/@hasna/events/dist/commander.js
14694
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
14695
+ import { existsSync } from "fs";
14696
+ import { homedir } from "os";
14697
+ import { join } from "path";
14698
+ import { createHmac, timingSafeEqual } from "crypto";
14699
+ import { randomUUID } from "crypto";
14700
+ import { spawn } from "child_process";
14701
+ import { randomUUID as randomUUID2 } from "crypto";
14702
+ function getPathValue(input, path) {
14703
+ return path.split(".").reduce((value, part) => {
14704
+ if (value && typeof value === "object" && part in value) {
14705
+ return value[part];
14706
+ }
14707
+ return;
14708
+ }, input);
14709
+ }
14710
+ function wildcardToRegExp(pattern) {
14711
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
14712
+ return new RegExp(`^${escaped}$`);
14713
+ }
14714
+ function matchString(value, matcher) {
14715
+ if (matcher === undefined)
14716
+ return true;
14717
+ if (value === undefined)
14718
+ return false;
14719
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
14720
+ return matchers.some((item) => wildcardToRegExp(item).test(value));
14721
+ }
14722
+ function matchRecord(input, matcher) {
14723
+ if (!matcher)
14724
+ return true;
14725
+ return Object.entries(matcher).every(([path, expected]) => {
14726
+ const actual = getPathValue(input, path);
14727
+ if (typeof expected === "string" || Array.isArray(expected)) {
14728
+ return matchString(actual === undefined ? undefined : String(actual), expected);
14729
+ }
14730
+ return actual === expected;
14731
+ });
14732
+ }
14733
+ function eventMatchesFilter(event, filter) {
14734
+ return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
14735
+ }
14736
+ function channelMatchesEvent(channel, event) {
14737
+ if (!channel.enabled)
14738
+ return false;
14739
+ if (!channel.filters || channel.filters.length === 0)
14740
+ return true;
14741
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
14742
+ }
14743
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
14744
+ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
14745
+ function getEventsDataDir(override) {
14746
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
14747
+ }
14748
+
14749
+ class JsonEventsStore {
14750
+ dataDir;
14751
+ channelsPath;
14752
+ eventsPath;
14753
+ deliveriesPath;
14754
+ constructor(dataDir = getEventsDataDir()) {
14755
+ this.dataDir = dataDir;
14756
+ this.channelsPath = join(dataDir, "channels.json");
14757
+ this.eventsPath = join(dataDir, "events.json");
14758
+ this.deliveriesPath = join(dataDir, "deliveries.json");
14759
+ }
14760
+ async init() {
14761
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
14762
+ await chmod(this.dataDir, 448).catch(() => {
14763
+ return;
14764
+ });
14765
+ await this.ensureArrayFile(this.channelsPath);
14766
+ await this.ensureArrayFile(this.eventsPath);
14767
+ await this.ensureArrayFile(this.deliveriesPath);
14768
+ }
14769
+ async addChannel(channel) {
14770
+ await this.init();
14771
+ const channels = await this.readJson(this.channelsPath, []);
14772
+ const index = channels.findIndex((item) => item.id === channel.id);
14773
+ if (index >= 0) {
14774
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
14775
+ } else {
14776
+ channels.push(channel);
14777
+ }
14778
+ await this.writeJson(this.channelsPath, channels);
14779
+ return index >= 0 ? channels[index] : channel;
14780
+ }
14781
+ async listChannels() {
14782
+ await this.init();
14783
+ return this.readJson(this.channelsPath, []);
14784
+ }
14785
+ async getChannel(id) {
14786
+ const channels = await this.listChannels();
14787
+ return channels.find((channel) => channel.id === id);
14788
+ }
14789
+ async removeChannel(id) {
14790
+ await this.init();
14791
+ const channels = await this.readJson(this.channelsPath, []);
14792
+ const next = channels.filter((channel) => channel.id !== id);
14793
+ await this.writeJson(this.channelsPath, next);
14794
+ return next.length !== channels.length;
14795
+ }
14796
+ async appendEvent(event) {
14797
+ await this.init();
14798
+ const events = await this.readJson(this.eventsPath, []);
14799
+ events.push(event);
14800
+ await this.writeJson(this.eventsPath, events);
14801
+ return event;
14802
+ }
14803
+ async listEvents() {
14804
+ await this.init();
14805
+ return this.readJson(this.eventsPath, []);
14806
+ }
14807
+ async findEventByIdentity(identity) {
14808
+ const events = await this.listEvents();
14809
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
14810
+ }
14811
+ async appendDelivery(result) {
14812
+ await this.init();
14813
+ const deliveries = await this.readJson(this.deliveriesPath, []);
14814
+ deliveries.push(result);
14815
+ await this.writeJson(this.deliveriesPath, deliveries);
14816
+ return result;
14817
+ }
14818
+ async listDeliveries() {
14819
+ await this.init();
14820
+ return this.readJson(this.deliveriesPath, []);
14821
+ }
14822
+ async exportData() {
14823
+ return {
14824
+ channels: await this.listChannels(),
14825
+ events: await this.listEvents(),
14826
+ deliveries: await this.listDeliveries()
14827
+ };
14828
+ }
14829
+ async ensureArrayFile(path) {
14830
+ if (!existsSync(path)) {
14831
+ await writeFile(path, `[]
14832
+ `, { encoding: "utf-8", mode: 384 });
14833
+ }
14834
+ await chmod(path, 384).catch(() => {
14835
+ return;
14836
+ });
14837
+ }
14838
+ async readJson(path, fallback) {
14839
+ try {
14840
+ const raw = await readFile(path, "utf-8");
14841
+ if (!raw.trim())
14842
+ return fallback;
14843
+ return JSON.parse(raw);
14844
+ } catch (error) {
14845
+ if (error.code === "ENOENT")
14846
+ return fallback;
14847
+ throw error;
14848
+ }
14849
+ }
14850
+ async writeJson(path, value) {
14851
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
14852
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
14853
+ `, { encoding: "utf-8", mode: 384 });
14854
+ await rename(tempPath, path);
14855
+ await chmod(path, 384).catch(() => {
14856
+ return;
14857
+ });
14858
+ }
14859
+ }
14860
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
14861
+ function buildSignatureBase(timestamp, body) {
14862
+ return `${timestamp}.${body}`;
14863
+ }
14864
+ function signPayload(secret, timestamp, body) {
14865
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
14866
+ return `sha256=${digest}`;
14867
+ }
14868
+ function now() {
14869
+ return new Date().toISOString();
14870
+ }
14871
+ function truncate(value, max = 4096) {
14872
+ return value.length > max ? `${value.slice(0, max)}...` : value;
14873
+ }
14874
+ function buildWebhookRequest(event, channel) {
14875
+ if (!channel.webhook)
14876
+ throw new Error(`Channel ${channel.id} has no webhook config`);
14877
+ const body = JSON.stringify(event);
14878
+ const timestamp = event.time;
14879
+ const headers = {
14880
+ "Content-Type": "application/json",
14881
+ "User-Agent": "@hasna/events",
14882
+ "X-Hasna-Event-Id": event.id,
14883
+ "X-Hasna-Event-Type": event.type,
14884
+ "X-Hasna-Timestamp": timestamp,
14885
+ ...channel.webhook.headers
14886
+ };
14887
+ if (channel.webhook.secret) {
14888
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
14889
+ }
14890
+ return { body, headers };
14891
+ }
14892
+ async function dispatchWebhook(event, channel, options = {}) {
14893
+ if (!channel.webhook)
14894
+ throw new Error(`Channel ${channel.id} has no webhook config`);
14895
+ const startedAt = now();
14896
+ const { body, headers } = buildWebhookRequest(event, channel);
14897
+ const controller = new AbortController;
14898
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
14899
+ try {
14900
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
14901
+ method: "POST",
14902
+ headers,
14903
+ body,
14904
+ signal: controller.signal
14905
+ });
14906
+ const responseBody = truncate(await response.text());
14907
+ return {
14908
+ attempt: 1,
14909
+ status: response.ok ? "success" : "failed",
14910
+ startedAt,
14911
+ completedAt: now(),
14912
+ responseStatus: response.status,
14913
+ responseBody,
14914
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
14915
+ };
14916
+ } catch (error) {
14917
+ return {
14918
+ attempt: 1,
14919
+ status: "failed",
14920
+ startedAt,
14921
+ completedAt: now(),
14922
+ error: error instanceof Error ? error.message : String(error)
14923
+ };
14924
+ } finally {
14925
+ clearTimeout(timeout);
14926
+ }
14927
+ }
14928
+ async function dispatchCommand(event, channel) {
14929
+ if (!channel.command)
14930
+ throw new Error(`Channel ${channel.id} has no command config`);
14931
+ const startedAt = now();
14932
+ const eventJson = JSON.stringify(event);
14933
+ const env = {
14934
+ ...process.env,
14935
+ ...channel.command.env,
14936
+ HASNA_CHANNEL_ID: channel.id,
14937
+ HASNA_EVENT_ID: event.id,
14938
+ HASNA_EVENT_TYPE: event.type,
14939
+ HASNA_EVENT_SOURCE: event.source,
14940
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
14941
+ HASNA_EVENT_SEVERITY: event.severity,
14942
+ HASNA_EVENT_TIME: event.time,
14943
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
14944
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
14945
+ HASNA_EVENT_JSON: eventJson
14946
+ };
14947
+ return new Promise((resolve) => {
14948
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
14949
+ cwd: channel.command.cwd,
14950
+ env,
14951
+ stdio: ["pipe", "pipe", "pipe"]
14952
+ });
14953
+ let stdout = "";
14954
+ let stderr = "";
14955
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
14956
+ child.stdin.end(eventJson);
14957
+ child.stdout.on("data", (chunk) => {
14958
+ stdout += chunk.toString();
14959
+ });
14960
+ child.stderr.on("data", (chunk) => {
14961
+ stderr += chunk.toString();
14962
+ });
14963
+ child.on("error", (error) => {
14964
+ clearTimeout(timeout);
14965
+ resolve({
14966
+ attempt: 1,
14967
+ status: "failed",
14968
+ startedAt,
14969
+ completedAt: now(),
14970
+ stdout: truncate(stdout),
14971
+ stderr: truncate(stderr),
14972
+ error: error.message
14973
+ });
14974
+ });
14975
+ child.on("close", (code, signal) => {
14976
+ clearTimeout(timeout);
14977
+ const success = code === 0;
14978
+ resolve({
14979
+ attempt: 1,
14980
+ status: success ? "success" : "failed",
14981
+ startedAt,
14982
+ completedAt: now(),
14983
+ stdout: truncate(stdout),
14984
+ stderr: truncate(stderr),
14985
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
14986
+ });
14987
+ });
14988
+ });
14989
+ }
14990
+ async function dispatchChannel(event, channel, options = {}) {
14991
+ if (channel.transport === "webhook")
14992
+ return dispatchWebhook(event, channel, options);
14993
+ if (channel.transport === "command")
14994
+ return dispatchCommand(event, channel);
14995
+ return {
14996
+ attempt: 1,
14997
+ status: "skipped",
14998
+ startedAt: now(),
14999
+ completedAt: now(),
15000
+ error: `Unsupported transport: ${channel.transport}`
15001
+ };
15002
+ }
15003
+ function createDeliveryResult(event, channel, attempts) {
15004
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
15005
+ return {
15006
+ id: randomUUID(),
15007
+ eventId: event.id,
15008
+ channelId: channel.id,
15009
+ transport: channel.transport,
15010
+ status,
15011
+ attempts,
15012
+ createdAt: attempts[0]?.startedAt ?? now(),
15013
+ completedAt: attempts.at(-1)?.completedAt ?? now()
15014
+ };
15015
+ }
15016
+ function createEvent(input) {
15017
+ return {
15018
+ id: input.id ?? randomUUID2(),
15019
+ source: input.source,
15020
+ type: input.type,
15021
+ time: normalizeTime(input.time),
15022
+ subject: input.subject,
15023
+ severity: input.severity ?? "info",
15024
+ data: input.data ?? {},
15025
+ message: input.message,
15026
+ dedupeKey: input.dedupeKey,
15027
+ schemaVersion: input.schemaVersion ?? "1.0",
15028
+ metadata: input.metadata ?? {}
15029
+ };
15030
+ }
15031
+
15032
+ class EventsClient {
15033
+ store;
15034
+ redactors;
15035
+ transportOptions;
15036
+ constructor(options = {}) {
15037
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
15038
+ this.redactors = options.redactors ?? [];
15039
+ this.transportOptions = { fetchImpl: options.fetchImpl };
15040
+ }
15041
+ async addChannel(input) {
15042
+ const timestamp = new Date().toISOString();
15043
+ return this.store.addChannel({
15044
+ ...input,
15045
+ createdAt: input.createdAt ?? timestamp,
15046
+ updatedAt: input.updatedAt ?? timestamp
15047
+ });
15048
+ }
15049
+ async listChannels() {
15050
+ return this.store.listChannels();
15051
+ }
15052
+ async removeChannel(id) {
15053
+ return this.store.removeChannel(id);
15054
+ }
15055
+ async emit(input, options = {}) {
15056
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
15057
+ if (options.dedupe !== false) {
15058
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
15059
+ if (existing) {
15060
+ return { event: existing, deliveries: [], deduped: true };
15061
+ }
15062
+ }
15063
+ await this.store.appendEvent(event);
15064
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
15065
+ return { event, deliveries, deduped: false };
15066
+ }
15067
+ async listEvents() {
15068
+ return this.store.listEvents();
15069
+ }
15070
+ async listDeliveries() {
15071
+ return this.store.listDeliveries();
15072
+ }
15073
+ async deliver(event) {
15074
+ const channels = await this.store.listChannels();
15075
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
15076
+ const deliveries = [];
15077
+ for (const channel of selected) {
15078
+ const eventForChannel = await this.applyRedaction(event, channel);
15079
+ const result = await this.deliverWithRetry(eventForChannel, channel);
15080
+ await this.store.appendDelivery(result);
15081
+ deliveries.push(result);
15082
+ }
15083
+ return deliveries;
15084
+ }
15085
+ async testChannel(id, input = {}) {
15086
+ const channel = await this.store.getChannel(id);
15087
+ if (!channel)
15088
+ throw new Error(`Channel not found: ${id}`);
15089
+ const event = createEvent({
15090
+ source: input.source ?? "hasna.events",
15091
+ type: input.type ?? "events.test",
15092
+ subject: input.subject ?? id,
15093
+ severity: input.severity ?? "info",
15094
+ data: input.data ?? { test: true },
15095
+ message: input.message ?? "Hasna events test delivery",
15096
+ dedupeKey: input.dedupeKey,
15097
+ schemaVersion: input.schemaVersion,
15098
+ metadata: input.metadata,
15099
+ time: input.time,
15100
+ id: input.id
15101
+ });
15102
+ const eventForChannel = await this.applyRedaction(event, channel);
15103
+ const result = await this.deliverWithRetry(eventForChannel, channel);
15104
+ await this.store.appendDelivery(result);
15105
+ return result;
15106
+ }
15107
+ async replay(options = {}) {
15108
+ const events = (await this.store.listEvents()).filter((event) => {
15109
+ if (options.eventId && event.id !== options.eventId)
15110
+ return false;
15111
+ if (options.source && event.source !== options.source)
15112
+ return false;
15113
+ if (options.type && event.type !== options.type)
15114
+ return false;
15115
+ return true;
15116
+ });
15117
+ if (options.dryRun)
15118
+ return { events, deliveries: [] };
15119
+ const deliveries = [];
15120
+ for (const event of events) {
15121
+ deliveries.push(...await this.deliver(event));
15122
+ }
15123
+ return { events, deliveries };
15124
+ }
15125
+ async applyRedaction(event, channel) {
15126
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
15127
+ for (const redactor of this.redactors) {
15128
+ next = await redactor(next, channel);
15129
+ }
15130
+ return next;
15131
+ }
15132
+ async deliverWithRetry(event, channel) {
15133
+ const policy = normalizeRetryPolicy(channel.retry);
15134
+ const attempts = [];
15135
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
15136
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
15137
+ attempt.attempt = index + 1;
15138
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
15139
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
15140
+ }
15141
+ attempts.push(attempt);
15142
+ if (attempt.status !== "failed")
15143
+ break;
15144
+ if (attempt.nextBackoffMs)
15145
+ await Bun.sleep(attempt.nextBackoffMs);
15146
+ }
15147
+ return createDeliveryResult(event, channel, attempts);
15148
+ }
15149
+ }
15150
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
15151
+ if (paths.length === 0)
15152
+ return event;
15153
+ const copy = structuredClone(event);
15154
+ for (const path of paths) {
15155
+ setPath(copy, path, replacement);
15156
+ }
15157
+ return copy;
15158
+ }
15159
+ function sanitizeChannelForOutput(channel) {
15160
+ const copy = structuredClone(channel);
15161
+ if (copy.webhook?.secret)
15162
+ copy.webhook.secret = "[REDACTED]";
15163
+ if (copy.command?.env) {
15164
+ copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
15165
+ }
15166
+ return copy;
15167
+ }
15168
+ function sanitizeChannelsForOutput(channels) {
15169
+ return channels.map(sanitizeChannelForOutput);
15170
+ }
15171
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15172
+ return redactValue(event, replacement);
15173
+ }
15174
+ function shouldRedactKey(key) {
15175
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
15176
+ }
15177
+ function redactValue(value, replacement) {
15178
+ if (Array.isArray(value))
15179
+ return value.map((item) => redactValue(item, replacement));
15180
+ if (!value || typeof value !== "object")
15181
+ return value;
15182
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15183
+ key,
15184
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
15185
+ ]));
15186
+ }
15187
+ function setPath(input, path, replacement) {
15188
+ const parts = path.split(".");
15189
+ let cursor = input;
15190
+ for (const part of parts.slice(0, -1)) {
15191
+ const next = cursor[part];
15192
+ if (!next || typeof next !== "object")
15193
+ return;
15194
+ cursor = next;
15195
+ }
15196
+ const last = parts.at(-1);
15197
+ if (last && last in cursor)
15198
+ cursor[last] = replacement;
15199
+ }
15200
+ function normalizeTime(value) {
15201
+ if (!value)
15202
+ return new Date().toISOString();
15203
+ return value instanceof Date ? value.toISOString() : value;
15204
+ }
15205
+ function normalizeRetryPolicy(policy) {
15206
+ return {
15207
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
15208
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
15209
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
15210
+ };
15211
+ }
15212
+ function parseJsonObject(value, fallback) {
15213
+ if (!value)
15214
+ return fallback;
15215
+ const parsed = JSON.parse(value);
15216
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15217
+ throw new Error("Expected a JSON object");
15218
+ }
15219
+ return parsed;
15220
+ }
15221
+ function parseHeaders(values) {
15222
+ if (!values?.length)
15223
+ return;
15224
+ const headers = {};
15225
+ for (const value of values) {
15226
+ const separator = value.indexOf("=");
15227
+ if (separator === -1)
15228
+ throw new Error(`Invalid header, expected name=value: ${value}`);
15229
+ headers[value.slice(0, separator)] = value.slice(separator + 1);
15230
+ }
15231
+ return headers;
15232
+ }
15233
+ function parseFilter(options) {
15234
+ const filter2 = {};
15235
+ if (options.source)
15236
+ filter2.source = options.source;
15237
+ if (options.type)
15238
+ filter2.type = options.type;
15239
+ if (options.subject)
15240
+ filter2.subject = options.subject;
15241
+ if (options.severity)
15242
+ filter2.severity = options.severity;
15243
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
15244
+ }
15245
+ function createClient(options) {
15246
+ if (options.createClient)
15247
+ return options.createClient();
15248
+ return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
15249
+ }
15250
+ function print(value, json, text) {
15251
+ if (json)
15252
+ console.log(JSON.stringify(value, null, 2));
15253
+ else
15254
+ console.log(text);
15255
+ }
15256
+ function hasJsonOption(options) {
15257
+ return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
15258
+ }
15259
+ function wantsJson(actionOptions, command) {
15260
+ return hasJsonOption(actionOptions) || hasJsonOption(command);
15261
+ }
15262
+ function registerWebhookCommands(program, options) {
15263
+ const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
15264
+ webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
15265
+ const timestamp = new Date().toISOString();
15266
+ const channel = {
15267
+ id: actionOptions.id,
15268
+ name: actionOptions.name,
15269
+ enabled: !actionOptions.disabled,
15270
+ transport: actionOptions.transport,
15271
+ filters: parseFilter(actionOptions),
15272
+ retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
15273
+ redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
15274
+ createdAt: timestamp,
15275
+ updatedAt: timestamp
15276
+ };
15277
+ if (actionOptions.transport === "webhook") {
15278
+ channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
15279
+ } else if (actionOptions.transport === "command") {
15280
+ channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
15281
+ } else {
15282
+ throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
15283
+ }
15284
+ const saved = await createClient(options).addChannel(channel);
15285
+ print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
15286
+ });
15287
+ webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
15288
+ const channels = await createClient(options).listChannels();
15289
+ if (wantsJson(actionOptions, command)) {
15290
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
15291
+ return;
15292
+ }
15293
+ if (!channels.length) {
15294
+ console.log("No channels configured.");
15295
+ return;
15296
+ }
15297
+ for (const channel of channels) {
15298
+ console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
15299
+ }
15300
+ });
15301
+ webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
15302
+ const removed = await createClient(options).removeChannel(id);
15303
+ print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
15304
+ });
15305
+ webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
15306
+ const result = await createClient(options).testChannel(id, {
15307
+ source: options.source,
15308
+ type: actionOptions.type,
15309
+ subject: actionOptions.subject ?? id,
15310
+ message: actionOptions.message,
15311
+ data: parseJsonObject(actionOptions.data, { test: true })
15312
+ });
15313
+ print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
15314
+ });
15315
+ return webhooks;
15316
+ }
15317
+ function registerEventCommands(program, options) {
15318
+ const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
15319
+ events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions, command) => {
15320
+ const result = await createClient(options).emit({
15321
+ source: actionOptions.source ?? options.source,
15322
+ type,
15323
+ subject: actionOptions.subject,
15324
+ severity: actionOptions.severity,
15325
+ message: actionOptions.message,
15326
+ dedupeKey: actionOptions.dedupeKey,
15327
+ data: parseJsonObject(actionOptions.data, {}),
15328
+ metadata: parseJsonObject(actionOptions.metadata, {})
15329
+ }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
15330
+ print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
15331
+ });
15332
+ events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
15333
+ let rows = await createClient(options).listEvents();
15334
+ if (actionOptions.source)
15335
+ rows = rows.filter((event) => event.source === actionOptions.source);
15336
+ if (actionOptions.type)
15337
+ rows = rows.filter((event) => event.type === actionOptions.type);
15338
+ if (actionOptions.limit)
15339
+ rows = rows.slice(-actionOptions.limit);
15340
+ if (wantsJson(actionOptions, command)) {
15341
+ console.log(JSON.stringify(rows, null, 2));
15342
+ return;
15343
+ }
15344
+ if (!rows.length) {
15345
+ console.log("No events recorded.");
15346
+ return;
15347
+ }
15348
+ for (const event of rows)
15349
+ console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
15350
+ });
15351
+ events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
15352
+ const result = await createClient(options).replay({
15353
+ eventId: actionOptions.id,
15354
+ source: actionOptions.source,
15355
+ type: actionOptions.type,
15356
+ dryRun: actionOptions.dryRun
15357
+ });
15358
+ print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
15359
+ });
15360
+ return events;
15361
+ }
15362
+ function registerEventsCommands(program, options) {
15363
+ registerWebhookCommands(program, options);
15364
+ registerEventCommands(program, options);
15365
+ }
15366
+ function parseNumber(value) {
15367
+ const parsed = Number(value);
15368
+ if (!Number.isFinite(parsed))
15369
+ throw new Error(`Expected a number, got ${value}`);
15370
+ return parsed;
15371
+ }
15372
+ function collectValues(value, previous) {
15373
+ previous.push(value);
15374
+ return previous;
15375
+ }
15376
+
14695
15377
  // src/cli/index.tsx
14696
15378
  import { render } from "ink";
14697
15379
 
@@ -14713,8 +15395,8 @@ var {
14713
15395
 
14714
15396
  // src/cli/index.tsx
14715
15397
  import chalk2 from "chalk";
14716
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
14717
- import { join as join9, dirname as dirname4 } from "path";
15398
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
15399
+ import { join as join11, dirname as dirname4 } from "path";
14718
15400
  import { fileURLToPath as fileURLToPath3 } from "url";
14719
15401
 
14720
15402
  // src/cli/components/App.tsx
@@ -15191,7 +15873,7 @@ var COL_NAME = 18;
15191
15873
  var COL_VERSION = 9;
15192
15874
  var COL_EVENT = 14;
15193
15875
  var COL_DESC = 40;
15194
- function truncate(str, max) {
15876
+ function truncate2(str, max) {
15195
15877
  return str.length > max ? str.slice(0, max - 3) + "..." : str;
15196
15878
  }
15197
15879
  function pad(str, width) {
@@ -15268,7 +15950,7 @@ function DataTable({
15268
15950
  pad(hook.displayName, COL_NAME),
15269
15951
  pad(hook.version, COL_VERSION),
15270
15952
  pad(hook.event, COL_EVENT),
15271
- truncate(hook.description, COL_DESC)
15953
+ truncate2(hook.description, COL_DESC)
15272
15954
  ]
15273
15955
  }, undefined, true, undefined, this)
15274
15956
  }, hook.name, false, undefined, this);
@@ -15892,7 +16574,7 @@ init_installer();
15892
16574
  init_profiles();
15893
16575
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
15894
16576
  var __dirname4 = dirname4(fileURLToPath3(import.meta.url));
15895
- var pkgPath = existsSync7(join9(__dirname4, "..", "package.json")) ? join9(__dirname4, "..", "package.json") : join9(__dirname4, "..", "..", "package.json");
16577
+ var pkgPath = existsSync10(join11(__dirname4, "..", "package.json")) ? join11(__dirname4, "..", "package.json") : join11(__dirname4, "..", "..", "package.json");
15896
16578
  var pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
15897
16579
  var program2 = new Command;
15898
16580
  function resolveScope(options) {
@@ -15963,8 +16645,8 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
15963
16645
  process.exit(1);
15964
16646
  }
15965
16647
  const hookDir = getHookPath(hook);
15966
- const hookScript = join9(hookDir, "src", "hook.ts");
15967
- if (!existsSync7(hookScript)) {
16648
+ const hookScript = join11(hookDir, "src", "hook.ts");
16649
+ if (!existsSync10(hookScript)) {
15968
16650
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
15969
16651
  process.exit(1);
15970
16652
  }
@@ -16264,7 +16946,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
16264
16946
  const settingsPath = getSettingsPath(scope);
16265
16947
  const issues = [];
16266
16948
  const healthy = [];
16267
- const settingsExist = existsSync7(settingsPath);
16949
+ const settingsExist = existsSync10(settingsPath);
16268
16950
  if (!settingsExist) {
16269
16951
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
16270
16952
  }
@@ -16278,8 +16960,8 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
16278
16960
  continue;
16279
16961
  }
16280
16962
  const hookDir = getHookPath(name);
16281
- const hookScript = join9(hookDir, "src", "hook.ts");
16282
- if (!existsSync7(hookScript)) {
16963
+ const hookScript = join11(hookDir, "src", "hook.ts");
16964
+ if (!existsSync10(hookScript)) {
16283
16965
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
16284
16966
  hookHealthy = false;
16285
16967
  }
@@ -16383,9 +17065,9 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
16383
17065
  return;
16384
17066
  }
16385
17067
  const hookPath = getHookPath(hook);
16386
- const readmePath = join9(hookPath, "README.md");
17068
+ const readmePath = join11(hookPath, "README.md");
16387
17069
  let readme = "";
16388
- if (existsSync7(readmePath)) {
17070
+ if (existsSync10(readmePath)) {
16389
17071
  readme = readFileSync6(readmePath, "utf-8");
16390
17072
  }
16391
17073
  if (options.json) {
@@ -16763,4 +17445,5 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
16763
17445
  startMcpHttpServer2({ name: "hooks", port: resolveMcpHttpPort2(args), buildServer: createHooksServer2 });
16764
17446
  }
16765
17447
  });
17448
+ registerEventsCommands(program2, { source: "hooks" });
16766
17449
  program2.parse();