@hasna/hooks 0.2.20 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +46 -2
  2. package/bin/index.js +1205 -93
  3. package/dist/db/migrations/002_session_events.d.ts +10 -0
  4. package/dist/db/migrations/003_user_prompt_submit_event.d.ts +10 -0
  5. package/dist/db/pg-migrations.d.ts +1 -1
  6. package/dist/db/remote-storage.d.ts +9 -0
  7. package/dist/db/schema.d.ts +4 -3
  8. package/dist/db/storage-sync.d.ts +57 -0
  9. package/dist/index.d.ts +16 -3
  10. package/dist/index.js +1092 -67
  11. package/dist/lib/installer.d.ts +26 -5
  12. package/dist/lib/registry.d.ts +5 -1
  13. package/dist/storage.d.ts +4 -0
  14. package/dist/storage.js +696 -0
  15. package/hooks/codewith-native-common.test.ts +61 -0
  16. package/hooks/codewith-native-common.ts +478 -0
  17. package/hooks/hook-agent-rules-version-check/LICENSE +191 -0
  18. package/hooks/hook-agent-rules-version-check/README.md +50 -0
  19. package/hooks/hook-agent-rules-version-check/package.json +53 -0
  20. package/hooks/hook-agent-rules-version-check/src/hook.test.ts +104 -0
  21. package/hooks/hook-agent-rules-version-check/src/hook.ts +215 -0
  22. package/hooks/hook-agent-rules-version-check/tsconfig.json +25 -0
  23. package/hooks/hook-announce-start/README.md +7 -5
  24. package/hooks/hook-announce-start/package.json +2 -2
  25. package/hooks/hook-announce-start/src/hook.ts +33 -14
  26. package/hooks/hook-fleet-blockers-gate/LICENSE +191 -0
  27. package/hooks/hook-fleet-blockers-gate/README.md +39 -0
  28. package/hooks/hook-fleet-blockers-gate/package.json +52 -0
  29. package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +106 -0
  30. package/hooks/hook-fleet-blockers-gate/src/hook.ts +242 -0
  31. package/hooks/hook-fleet-blockers-gate/tsconfig.json +25 -0
  32. package/hooks/hook-fleet-catchup/LICENSE +191 -0
  33. package/hooks/hook-fleet-catchup/README.md +40 -0
  34. package/hooks/hook-fleet-catchup/package.json +52 -0
  35. package/hooks/hook-fleet-catchup/src/hook.test.ts +156 -0
  36. package/hooks/hook-fleet-catchup/src/hook.ts +283 -0
  37. package/hooks/hook-fleet-catchup/tsconfig.json +25 -0
  38. package/hooks/hook-knowledge-context/README.md +53 -0
  39. package/hooks/hook-knowledge-context/package.json +52 -0
  40. package/hooks/hook-knowledge-context/src/hook.test.ts +168 -0
  41. package/hooks/hook-knowledge-context/src/hook.ts +355 -0
  42. package/hooks/hook-knowledge-context/tsconfig.json +25 -0
  43. package/hooks/pre-bash/README.md +17 -0
  44. package/hooks/pre-bash/package.json +12 -0
  45. package/hooks/pre-bash/src/hook.ts +103 -0
  46. package/hooks/prompt-guard/README.md +17 -0
  47. package/hooks/prompt-guard/package.json +12 -0
  48. package/hooks/prompt-guard/src/hook.ts +76 -0
  49. package/hooks/session-start/README.md +17 -0
  50. package/hooks/session-start/package.json +12 -0
  51. package/hooks/session-start/src/hook.ts +150 -0
  52. package/hooks/stop-sync/README.md +17 -0
  53. package/hooks/stop-sync/package.json +12 -0
  54. package/hooks/stop-sync/src/hook.ts +69 -0
  55. package/hooks/worktree-guard/README.md +17 -0
  56. package/hooks/worktree-guard/package.json +12 -0
  57. package/hooks/worktree-guard/src/hook.ts +88 -0
  58. package/package.json +14 -6
  59. package/dist/cli/components/App.d.ts +0 -7
  60. package/dist/cli/components/CategorySelect.d.ts +0 -7
  61. package/dist/cli/components/DataTable.d.ts +0 -11
  62. package/dist/cli/components/Header.d.ts +0 -7
  63. package/dist/cli/components/HookSelect.d.ts +0 -11
  64. package/dist/cli/components/InstallProgress.d.ts +0 -9
  65. package/dist/cli/components/SearchView.d.ts +0 -9
  66. package/dist/cli/index.d.ts +0 -2
  67. package/dist/lib/db-writer.d.ts +0 -9
  68. package/dist/mcp/http.d.ts +0 -16
  69. package/dist/mcp/server.d.ts +0 -17
package/dist/index.js CHANGED
@@ -28,25 +28,25 @@ __export(exports_profiles, {
28
28
  deleteProfile: () => deleteProfile,
29
29
  createProfile: () => createProfile
30
30
  });
31
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync, rmSync, cpSync } from "fs";
32
- import { join as join2 } from "path";
33
- import { homedir as homedir2 } from "os";
31
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync2, rmSync, cpSync as cpSync2 } from "fs";
32
+ import { join as join4 } from "path";
33
+ import { homedir as homedir4 } from "os";
34
34
  function resolveProfilesDir() {
35
- const newDir = join2(homedir2(), ".hasna", "hooks", "profiles");
36
- const oldDir = join2(homedir2(), ".hooks", "profiles");
37
- if (!existsSync2(newDir) && existsSync2(oldDir)) {
38
- mkdirSync2(join2(homedir2(), ".hasna", "hooks"), { recursive: true });
39
- cpSync(oldDir, newDir, { recursive: true });
35
+ const newDir = join4(homedir4(), ".hasna", "hooks", "profiles");
36
+ const oldDir = join4(homedir4(), ".hooks", "profiles");
37
+ if (!existsSync4(newDir) && existsSync4(oldDir)) {
38
+ mkdirSync3(join4(homedir4(), ".hasna", "hooks"), { recursive: true });
39
+ cpSync2(oldDir, newDir, { recursive: true });
40
40
  }
41
41
  return newDir;
42
42
  }
43
43
  function ensureProfilesDir() {
44
- if (!existsSync2(PROFILES_DIR)) {
45
- mkdirSync2(PROFILES_DIR, { recursive: true });
44
+ if (!existsSync4(PROFILES_DIR)) {
45
+ mkdirSync3(PROFILES_DIR, { recursive: true });
46
46
  }
47
47
  }
48
48
  function profilePath(id) {
49
- return join2(PROFILES_DIR, `${id}.json`);
49
+ return join4(PROFILES_DIR, `${id}.json`);
50
50
  }
51
51
  function shortUuid() {
52
52
  return crypto.randomUUID().slice(0, 8);
@@ -75,22 +75,22 @@ function createProfile(input) {
75
75
  function getProfile(id) {
76
76
  const path = profilePath(id);
77
77
  try {
78
- if (!existsSync2(path))
78
+ if (!existsSync4(path))
79
79
  return null;
80
- return JSON.parse(readFileSync2(path, "utf-8"));
80
+ return JSON.parse(readFileSync3(path, "utf-8"));
81
81
  } catch {
82
82
  return null;
83
83
  }
84
84
  }
85
85
  function listProfiles() {
86
- if (!existsSync2(PROFILES_DIR))
86
+ if (!existsSync4(PROFILES_DIR))
87
87
  return [];
88
88
  try {
89
- const files = readdirSync(PROFILES_DIR).filter((f) => f.endsWith(".json"));
89
+ const files = readdirSync2(PROFILES_DIR).filter((f) => f.endsWith(".json"));
90
90
  const profiles = [];
91
91
  for (const file of files) {
92
92
  try {
93
- const content = readFileSync2(join2(PROFILES_DIR, file), "utf-8");
93
+ const content = readFileSync3(join4(PROFILES_DIR, file), "utf-8");
94
94
  profiles.push(JSON.parse(content));
95
95
  } catch {}
96
96
  }
@@ -113,7 +113,7 @@ function updateProfile(id, data) {
113
113
  }
114
114
  function deleteProfile(id) {
115
115
  const path = profilePath(id);
116
- if (!existsSync2(path))
116
+ if (!existsSync4(path))
117
117
  return false;
118
118
  try {
119
119
  rmSync(path);
@@ -143,7 +143,7 @@ function importProfiles(profiles) {
143
143
  continue;
144
144
  }
145
145
  const path = profilePath(profile.agent_id);
146
- if (existsSync2(path)) {
146
+ if (existsSync4(path)) {
147
147
  skipped++;
148
148
  continue;
149
149
  }
@@ -158,7 +158,671 @@ var init_profiles = __esm(() => {
158
158
  PROFILES_DIR = resolveProfilesDir();
159
159
  });
160
160
 
161
+ // src/db/index.ts
162
+ import { Database } from "bun:sqlite";
163
+ import { existsSync as existsSync2, mkdirSync, cpSync } from "fs";
164
+ import { join as join2 } from "path";
165
+ import { homedir as homedir2 } from "os";
166
+
167
+ // src/db/schema.ts
168
+ var CREATE_HOOK_EVENTS_TABLE = `
169
+ CREATE TABLE IF NOT EXISTS hook_events (
170
+ id TEXT PRIMARY KEY,
171
+ timestamp TEXT NOT NULL,
172
+ session_id TEXT NOT NULL,
173
+ hook_name TEXT NOT NULL,
174
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
175
+ tool_name TEXT,
176
+ tool_input TEXT,
177
+ result TEXT CHECK (result IN ('continue', 'block', NULL)),
178
+ error TEXT,
179
+ duration_ms INTEGER,
180
+ project_dir TEXT,
181
+ metadata TEXT
182
+ )
183
+ `;
184
+ var CREATE_INDEXES = [
185
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
186
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
187
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
188
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
189
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`
190
+ ];
191
+
192
+ // src/db/migrations/001_initial.ts
193
+ function up(db) {
194
+ db.exec(CREATE_HOOK_EVENTS_TABLE);
195
+ for (const idx of CREATE_INDEXES) {
196
+ db.exec(idx);
197
+ }
198
+ }
199
+
200
+ // src/db/migrations/002_session_events.ts
201
+ function up2(db) {
202
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
203
+ if (!row?.sql)
204
+ return;
205
+ if (row.sql.includes("SessionStart"))
206
+ return;
207
+ db.exec("BEGIN");
208
+ try {
209
+ db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
210
+ db.exec(CREATE_HOOK_EVENTS_TABLE);
211
+ db.exec(`INSERT INTO hook_events
212
+ (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
213
+ SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
214
+ FROM hook_events_old`);
215
+ db.exec("DROP TABLE hook_events_old");
216
+ for (const idx of CREATE_INDEXES) {
217
+ db.exec(idx);
218
+ }
219
+ db.exec("COMMIT");
220
+ } catch (err) {
221
+ db.exec("ROLLBACK");
222
+ throw err;
223
+ }
224
+ }
225
+
226
+ // src/db/migrations/003_user_prompt_submit_event.ts
227
+ function up3(db) {
228
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
229
+ if (!row?.sql)
230
+ return;
231
+ if (row.sql.includes("UserPromptSubmit"))
232
+ return;
233
+ db.exec("BEGIN");
234
+ try {
235
+ db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
236
+ db.exec(CREATE_HOOK_EVENTS_TABLE);
237
+ db.exec(`INSERT INTO hook_events
238
+ (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
239
+ SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
240
+ FROM hook_events_old`);
241
+ db.exec("DROP TABLE hook_events_old");
242
+ for (const idx of CREATE_INDEXES) {
243
+ db.exec(idx);
244
+ }
245
+ db.exec("COMMIT");
246
+ } catch (err) {
247
+ db.exec("ROLLBACK");
248
+ throw err;
249
+ }
250
+ }
251
+
252
+ // src/db/migrations/index.ts
253
+ var MIGRATIONS = [
254
+ { version: "001_initial", up },
255
+ { version: "002_session_events", up: up2 },
256
+ { version: "003_user_prompt_submit_event", up: up3 }
257
+ ];
258
+ function ensureMigrationsTable(db) {
259
+ db.exec(`
260
+ CREATE TABLE IF NOT EXISTS schema_migrations (
261
+ version TEXT PRIMARY KEY,
262
+ applied_at TEXT NOT NULL
263
+ )
264
+ `);
265
+ }
266
+ function getApplied(db) {
267
+ const rows = db.query("SELECT version FROM schema_migrations").all();
268
+ return new Set(rows.map((r) => r.version));
269
+ }
270
+ function runMigrations(db) {
271
+ ensureMigrationsTable(db);
272
+ const applied = getApplied(db);
273
+ for (const migration of MIGRATIONS) {
274
+ if (applied.has(migration.version))
275
+ continue;
276
+ migration.up(db);
277
+ db.run("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", [
278
+ migration.version,
279
+ new Date().toISOString()
280
+ ]);
281
+ }
282
+ }
283
+
284
+ // src/db/legacy-import.ts
285
+ import { existsSync, readdirSync, readFileSync } from "fs";
286
+ import { join } from "path";
287
+ import { homedir } from "os";
288
+ var META_KEY = "legacy_import_done";
289
+ function ensureMetaTable(db) {
290
+ db.exec(`
291
+ CREATE TABLE IF NOT EXISTS _meta (
292
+ key TEXT PRIMARY KEY,
293
+ value TEXT NOT NULL
294
+ )
295
+ `);
296
+ }
297
+ function isAlreadyDone(db) {
298
+ ensureMetaTable(db);
299
+ const row = db.query("SELECT value FROM _meta WHERE key = ?").get(META_KEY);
300
+ return row?.value === "1";
301
+ }
302
+ function markDone(db) {
303
+ db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", [META_KEY, "1"]);
304
+ }
305
+ function nanoid() {
306
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
307
+ }
308
+ function importJsonlFile(db, filePath) {
309
+ let count = 0;
310
+ try {
311
+ const lines = readFileSync(filePath, "utf-8").split(`
312
+ `).filter(Boolean);
313
+ for (const line of lines) {
314
+ try {
315
+ const entry = JSON.parse(line);
316
+ db.run(`INSERT OR IGNORE INTO hook_events
317
+ (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, project_dir)
318
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
319
+ nanoid(),
320
+ entry.timestamp ?? new Date().toISOString(),
321
+ entry.session_id ?? "legacy",
322
+ "sessionlog",
323
+ "PostToolUse",
324
+ entry.tool_name ?? null,
325
+ entry.tool_input ? String(entry.tool_input).slice(0, 500) : null,
326
+ null
327
+ ]);
328
+ count++;
329
+ } catch {}
330
+ }
331
+ } catch {}
332
+ return count;
333
+ }
334
+ function importErrorsLog(db, filePath) {
335
+ let count = 0;
336
+ try {
337
+ const lines = readFileSync(filePath, "utf-8").split(`
338
+ `).filter(Boolean);
339
+ const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
340
+ for (const line of lines) {
341
+ try {
342
+ const m = line.match(linePattern);
343
+ if (!m)
344
+ continue;
345
+ const [, timestamp, sessionPrefix, , errorMsg] = m;
346
+ db.run(`INSERT OR IGNORE INTO hook_events
347
+ (id, timestamp, session_id, hook_name, event_type, error)
348
+ VALUES (?, ?, ?, ?, ?, ?)`, [
349
+ nanoid(),
350
+ timestamp,
351
+ sessionPrefix ? `legacy-${sessionPrefix}` : "legacy",
352
+ "errornotify",
353
+ "PostToolUse",
354
+ errorMsg.slice(0, 500)
355
+ ]);
356
+ count++;
357
+ } catch {}
358
+ }
359
+ } catch {}
360
+ return count;
361
+ }
362
+ function runLegacyImport(db) {
363
+ try {
364
+ if (isAlreadyDone(db))
365
+ return;
366
+ let total = 0;
367
+ const claudeProjectsDir = join(homedir(), ".claude", "projects");
368
+ if (existsSync(claudeProjectsDir)) {
369
+ try {
370
+ const projectDirs = readdirSync(claudeProjectsDir);
371
+ for (const dir of projectDirs) {
372
+ const projectDir = join(claudeProjectsDir, dir);
373
+ try {
374
+ const files = readdirSync(projectDir);
375
+ for (const file of files) {
376
+ if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
377
+ total += importJsonlFile(db, join(projectDir, file));
378
+ }
379
+ if (file === "errors.log") {
380
+ total += importErrorsLog(db, join(projectDir, file));
381
+ }
382
+ }
383
+ } catch {}
384
+ }
385
+ } catch {}
386
+ }
387
+ markDone(db);
388
+ if (total > 0) {
389
+ process.stderr.write(`[hooks] Imported ${total} legacy log entries into SQLite.
390
+ `);
391
+ }
392
+ } catch (err) {
393
+ process.stderr.write(`[hooks] Legacy import failed (non-fatal): ${err}
394
+ `);
395
+ }
396
+ }
397
+
398
+ // src/db/retention.ts
399
+ function runRetention(db, days) {
400
+ const envDays = parseInt(process.env.HOOKS_RETENTION_DAYS ?? "30");
401
+ const retentionDays = days ?? (isNaN(envDays) || envDays <= 0 ? 30 : envDays);
402
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
403
+ try {
404
+ db.run("DELETE FROM hook_events WHERE timestamp < ?", [cutoff]);
405
+ const changes = db.query("SELECT changes() as changes").get()?.changes ?? 0;
406
+ return changes;
407
+ } catch {
408
+ return 0;
409
+ }
410
+ }
411
+
412
+ // src/db/index.ts
413
+ var instance = null;
414
+ function resolveDataDir() {
415
+ const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
416
+ if (explicit)
417
+ return explicit;
418
+ const newDir = join2(homedir2(), ".hasna", "hooks");
419
+ const oldDir = join2(homedir2(), ".hooks");
420
+ if (!existsSync2(newDir) && existsSync2(oldDir)) {
421
+ mkdirSync(join2(homedir2(), ".hasna"), { recursive: true });
422
+ cpSync(oldDir, newDir, { recursive: true });
423
+ }
424
+ return newDir;
425
+ }
426
+ function getDbPath() {
427
+ const explicitDb = process.env.HASNA_HOOKS_DB_PATH ?? process.env.HOOKS_DB_PATH;
428
+ if (explicitDb)
429
+ return explicitDb;
430
+ const dataDir = resolveDataDir();
431
+ return join2(dataDir, "hooks.db");
432
+ }
433
+ function ensureDir(dbPath) {
434
+ const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
435
+ if (dir && !existsSync2(dir)) {
436
+ mkdirSync(dir, { recursive: true });
437
+ }
438
+ }
439
+ function getDb() {
440
+ if (instance)
441
+ return instance;
442
+ const dbPath = getDbPath();
443
+ const isNew = dbPath === ":memory:" || !existsSync2(dbPath);
444
+ ensureDir(dbPath);
445
+ instance = new Database(dbPath);
446
+ instance.exec("PRAGMA journal_mode=WAL");
447
+ instance.exec("PRAGMA foreign_keys=ON");
448
+ runMigrations(instance);
449
+ runRetention(instance);
450
+ instance.exec(`CREATE TABLE IF NOT EXISTS feedback (
451
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
452
+ message TEXT NOT NULL,
453
+ email TEXT,
454
+ category TEXT DEFAULT 'general',
455
+ version TEXT,
456
+ machine_id TEXT,
457
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
458
+ )`);
459
+ if (isNew) {
460
+ runLegacyImport(instance);
461
+ }
462
+ return instance;
463
+ }
464
+
465
+ // src/db/pg-migrations.ts
466
+ var PG_MIGRATIONS = [
467
+ `CREATE TABLE IF NOT EXISTS hook_events (
468
+ id TEXT PRIMARY KEY,
469
+ timestamp TEXT NOT NULL,
470
+ session_id TEXT NOT NULL,
471
+ hook_name TEXT NOT NULL,
472
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
473
+ tool_name TEXT,
474
+ tool_input TEXT,
475
+ result TEXT CHECK (result IN ('continue', 'block', NULL)),
476
+ error TEXT,
477
+ duration_ms INTEGER,
478
+ project_dir TEXT,
479
+ metadata TEXT
480
+ )`,
481
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
482
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
483
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
484
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
485
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`,
486
+ `CREATE TABLE IF NOT EXISTS schema_migrations (
487
+ version TEXT PRIMARY KEY,
488
+ applied_at TEXT NOT NULL
489
+ )`,
490
+ `CREATE TABLE IF NOT EXISTS _meta (
491
+ key TEXT PRIMARY KEY,
492
+ value TEXT NOT NULL
493
+ )`,
494
+ `CREATE TABLE IF NOT EXISTS feedback (
495
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
496
+ message TEXT NOT NULL,
497
+ email TEXT,
498
+ category TEXT DEFAULT 'general',
499
+ version TEXT,
500
+ machine_id TEXT,
501
+ created_at TEXT NOT NULL DEFAULT NOW()::text
502
+ )`,
503
+ `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
504
+ `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
505
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`
506
+ ];
507
+
508
+ // src/db/remote-storage.ts
509
+ import pg from "pg";
510
+ function translatePlaceholders(sql) {
511
+ let index = 0;
512
+ return sql.replace(/\?/g, () => `$${++index}`);
513
+ }
514
+ function normalizeParams(params) {
515
+ const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
516
+ return flat.map((value) => value === undefined ? null : value);
517
+ }
518
+ function sslConfigFor(connectionString) {
519
+ return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
520
+ }
521
+
522
+ class PgAdapterAsync {
523
+ pool;
524
+ constructor(connectionString) {
525
+ this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) });
526
+ }
527
+ async run(sql, ...params) {
528
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
529
+ return { changes: result.rowCount ?? 0 };
530
+ }
531
+ async all(sql, ...params) {
532
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
533
+ return result.rows;
534
+ }
535
+ async close() {
536
+ await this.pool.end();
537
+ }
538
+ }
539
+
540
+ // src/db/storage-sync.ts
541
+ var STORAGE_TABLES = [
542
+ "hook_events",
543
+ "schema_migrations",
544
+ "_meta",
545
+ "feedback"
546
+ ];
547
+ var HOOKS_STORAGE_TABLES = STORAGE_TABLES;
548
+ var HOOKS_STORAGE_ENV = "HASNA_HOOKS_DATABASE_URL";
549
+ var HOOKS_STORAGE_FALLBACK_ENV = "HOOKS_DATABASE_URL";
550
+ var HOOKS_STORAGE_MODE_ENV = "HASNA_HOOKS_STORAGE_MODE";
551
+ var HOOKS_STORAGE_MODE_FALLBACK_ENV = "HOOKS_STORAGE_MODE";
552
+ var STORAGE_DATABASE_ENV = [HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV];
553
+ var STORAGE_MODE_ENV = [HOOKS_STORAGE_MODE_ENV, HOOKS_STORAGE_MODE_FALLBACK_ENV];
554
+ var PRIMARY_KEYS = {
555
+ hook_events: ["id"],
556
+ schema_migrations: ["version"],
557
+ _meta: ["key"],
558
+ feedback: ["id"]
559
+ };
560
+ function readEnv(name) {
561
+ const value = process.env[name]?.trim();
562
+ return value || undefined;
563
+ }
564
+ function normalizeStorageMode(value) {
565
+ const normalized = value?.trim().toLowerCase();
566
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
567
+ return normalized;
568
+ return;
569
+ }
570
+ function getStorageDatabaseEnvName() {
571
+ for (const name of STORAGE_DATABASE_ENV) {
572
+ if (readEnv(name))
573
+ return name;
574
+ }
575
+ return null;
576
+ }
577
+ function getStorageDatabaseEnv() {
578
+ const name = getStorageDatabaseEnvName();
579
+ return name ? { name } : null;
580
+ }
581
+ function getStorageDatabaseUrl() {
582
+ const env = getStorageDatabaseEnv();
583
+ return env ? readEnv(env.name) ?? null : null;
584
+ }
585
+ function getStorageMode() {
586
+ const mode = normalizeStorageMode(readEnv(HOOKS_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv(HOOKS_STORAGE_MODE_FALLBACK_ENV));
587
+ if (mode)
588
+ return mode;
589
+ return getStorageDatabaseUrl() ? "hybrid" : "local";
590
+ }
591
+ async function getStoragePg() {
592
+ const url = getStorageDatabaseUrl();
593
+ if (!url) {
594
+ throw new Error("Missing HASNA_HOOKS_DATABASE_URL or HOOKS_DATABASE_URL");
595
+ }
596
+ return new PgAdapterAsync(url);
597
+ }
598
+ async function runStorageMigrations(remote) {
599
+ await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
600
+ for (const sql of PG_MIGRATIONS)
601
+ await remote.run(sql);
602
+ }
603
+ async function storagePush(options) {
604
+ const remote = await getStoragePg();
605
+ const db = getDb();
606
+ try {
607
+ await runStorageMigrations(remote);
608
+ const results = [];
609
+ for (const table of resolveTables(options?.tables)) {
610
+ results.push(await pushTable(db, remote, table));
611
+ }
612
+ recordSyncMeta(db, "push", results);
613
+ return results;
614
+ } finally {
615
+ await remote.close();
616
+ }
617
+ }
618
+ async function storagePull(options) {
619
+ const remote = await getStoragePg();
620
+ const db = getDb();
621
+ try {
622
+ await runStorageMigrations(remote);
623
+ const results = [];
624
+ for (const table of resolveTables(options?.tables)) {
625
+ results.push(await pullTable(remote, db, table));
626
+ }
627
+ recordSyncMeta(db, "pull", results);
628
+ return results;
629
+ } finally {
630
+ await remote.close();
631
+ }
632
+ }
633
+ async function storageSync(options) {
634
+ const pull = await storagePull(options);
635
+ const push = await storagePush(options);
636
+ return { pull, push };
637
+ }
638
+ function getSyncMetaAll() {
639
+ const db = getDb();
640
+ ensureSyncMetaTable(db);
641
+ return db.query("SELECT table_name, last_synced_at, direction FROM _hooks_sync_meta ORDER BY table_name, direction").all();
642
+ }
643
+ function getStorageStatus() {
644
+ const activeEnv = getStorageDatabaseEnv();
645
+ return {
646
+ configured: Boolean(activeEnv),
647
+ mode: getStorageMode(),
648
+ env: STORAGE_DATABASE_ENV,
649
+ activeEnv: activeEnv?.name ?? null,
650
+ service: "hooks",
651
+ tables: STORAGE_TABLES,
652
+ sync: getSyncMetaAll()
653
+ };
654
+ }
655
+ function resolveTables(tables) {
656
+ if (!tables || tables.length === 0)
657
+ return [...STORAGE_TABLES];
658
+ const allowed = new Set(STORAGE_TABLES);
659
+ const requested = tables.map((table) => table.trim()).filter(Boolean);
660
+ const invalid = requested.filter((table) => !allowed.has(table));
661
+ if (invalid.length > 0)
662
+ throw new Error(`Unknown hooks sync table(s): ${invalid.join(", ")}`);
663
+ return requested;
664
+ }
665
+ function parseStorageTables(value) {
666
+ if (!value)
667
+ return;
668
+ return resolveTables(Array.isArray(value) ? value : value.split(","));
669
+ }
670
+ async function pushTable(db, remote, table) {
671
+ const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
672
+ try {
673
+ if (!tableExists(db, table))
674
+ return result;
675
+ const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
676
+ result.rowsRead = rows.length;
677
+ if (rows.length === 0)
678
+ return result;
679
+ const remoteColumns = await getRemoteColumns(remote, table);
680
+ const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
681
+ result.rowsWritten = await upsertPg(remote, table, columns, rows, remoteColumns);
682
+ } catch (error) {
683
+ result.errors.push(error instanceof Error ? error.message : String(error));
684
+ }
685
+ return result;
686
+ }
687
+ async function pullTable(remote, db, table) {
688
+ const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
689
+ try {
690
+ if (!tableExists(db, table))
691
+ return result;
692
+ const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
693
+ result.rowsRead = rows.length;
694
+ if (rows.length === 0)
695
+ return result;
696
+ const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
697
+ result.rowsWritten = upsertSqlite(db, table, columns, rows);
698
+ } catch (error) {
699
+ result.errors.push(error instanceof Error ? error.message : String(error));
700
+ }
701
+ return result;
702
+ }
703
+ async function getRemoteColumns(remote, table) {
704
+ const rows = await remote.all("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
705
+ return new Map(rows.map((row) => [row.column_name, row.data_type]));
706
+ }
707
+ function filterRemoteColumns(remoteColumns, columns) {
708
+ if (remoteColumns.size === 0)
709
+ return columns;
710
+ return columns.filter((column) => remoteColumns.has(column));
711
+ }
712
+ function filterLocalColumns(db, table, columns) {
713
+ const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
714
+ const allowed = new Set(rows.map((row) => row.name));
715
+ return columns.filter((column) => allowed.has(column));
716
+ }
717
+ async function upsertPg(remote, table, columns, rows, remoteColumns) {
718
+ if (columns.length === 0)
719
+ return 0;
720
+ const primaryKeys = PRIMARY_KEYS[table];
721
+ const columnList = columns.map(quoteIdent).join(", ");
722
+ const placeholders = columns.map(() => "?").join(", ");
723
+ const keyList = primaryKeys.map(quoteIdent).join(", ");
724
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
725
+ const fallbackKey = primaryKeys[0];
726
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
727
+ for (const row of rows) {
728
+ await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
729
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => coerceForPg(row[column], remoteColumns.get(column))));
730
+ }
731
+ return rows.length;
732
+ }
733
+ function upsertSqlite(db, table, columns, rows) {
734
+ if (columns.length === 0)
735
+ return 0;
736
+ const primaryKeys = PRIMARY_KEYS[table];
737
+ const columnList = columns.map(quoteIdent).join(", ");
738
+ const placeholders = columns.map(() => "?").join(", ");
739
+ const keyList = primaryKeys.map(quoteIdent).join(", ");
740
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
741
+ const fallbackKey = primaryKeys[0];
742
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
743
+ const statement = db.query(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
744
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
745
+ const insert = db.transaction((batch) => {
746
+ for (const row of batch)
747
+ statement.run(...columns.map((column) => coerceForSqlite(row[column])));
748
+ });
749
+ insert(rows);
750
+ return rows.length;
751
+ }
752
+ function recordSyncMeta(db, direction, results) {
753
+ ensureSyncMetaTable(db);
754
+ const now = new Date().toISOString();
755
+ const statement = db.query(`
756
+ INSERT INTO _hooks_sync_meta (table_name, last_synced_at, direction)
757
+ VALUES (?, ?, ?)
758
+ ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at
759
+ `);
760
+ for (const result of results) {
761
+ if (result.errors.length > 0)
762
+ continue;
763
+ statement.run(result.table, now, direction);
764
+ }
765
+ }
766
+ function ensureSyncMetaTable(db) {
767
+ db.exec(`
768
+ CREATE TABLE IF NOT EXISTS _hooks_sync_meta (
769
+ table_name TEXT NOT NULL,
770
+ last_synced_at TEXT,
771
+ direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')),
772
+ PRIMARY KEY (table_name, direction)
773
+ )
774
+ `);
775
+ }
776
+ function tableExists(db, table) {
777
+ const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
778
+ return Boolean(row);
779
+ }
780
+ function quoteIdent(identifier) {
781
+ return `"${identifier.replace(/"/g, '""')}"`;
782
+ }
783
+ function coerceForPg(value, dataType) {
784
+ if (value === undefined || value === null)
785
+ return null;
786
+ if (dataType === "boolean") {
787
+ if (typeof value === "boolean")
788
+ return value;
789
+ if (typeof value === "number")
790
+ return value !== 0;
791
+ if (typeof value === "string")
792
+ return value === "1" || value.toLowerCase() === "true";
793
+ }
794
+ if (value instanceof Date)
795
+ return value.toISOString();
796
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
797
+ return value;
798
+ if (typeof value === "object")
799
+ return JSON.stringify(value);
800
+ return value;
801
+ }
802
+ function coerceForSqlite(value) {
803
+ if (value === undefined || value === null)
804
+ return null;
805
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
806
+ return value;
807
+ if (value instanceof Date)
808
+ return value.toISOString();
809
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
810
+ return value;
811
+ if (typeof value === "object")
812
+ return JSON.stringify(value);
813
+ return String(value);
814
+ }
161
815
  // src/lib/registry.ts
816
+ var HOOK_EVENTS = [
817
+ "PreToolUse",
818
+ "PostToolUse",
819
+ "Stop",
820
+ "Notification",
821
+ "SessionStart",
822
+ "SessionEnd",
823
+ "UserPromptSubmit",
824
+ "SubagentStart"
825
+ ];
162
826
  var CATEGORIES = [
163
827
  "Git Safety",
164
828
  "Code Quality",
@@ -202,6 +866,16 @@ var HOOKS = [
202
866
  matcher: "Write|Edit|NotebookEdit",
203
867
  tags: ["git", "snapshot", "rollback", "backup"]
204
868
  },
869
+ {
870
+ name: "worktree-guard",
871
+ displayName: "Worktree Guard",
872
+ description: "Warns on feature work outside managed repos worktrees and blocks commit/push from shared checkouts",
873
+ version: "0.1.0",
874
+ category: "Git Safety",
875
+ event: "PreToolUse",
876
+ matcher: "Bash|Write|Edit|MultiEdit|NotebookEdit",
877
+ tags: ["git", "worktree", "repos", "multi-agent", "safety"]
878
+ },
205
879
  {
206
880
  name: "checktests",
207
881
  displayName: "Check Tests",
@@ -282,6 +956,16 @@ var HOOKS = [
282
956
  matcher: "Bash",
283
957
  tags: ["npm", "packages", "typosquatting", "supply-chain"]
284
958
  },
959
+ {
960
+ name: "pre-bash",
961
+ displayName: "Pre Bash",
962
+ description: "Codewith Bash gate for staged secrets scans before commit/push and comms rechecks before risky operations",
963
+ version: "0.1.0",
964
+ category: "Security",
965
+ event: "PreToolUse",
966
+ matcher: "Bash",
967
+ tags: ["codewith", "bash", "secrets", "gitleaks", "risky-ops"]
968
+ },
285
969
  {
286
970
  name: "phonenotify",
287
971
  displayName: "Phone Notify",
@@ -322,6 +1006,17 @@ var HOOKS = [
322
1006
  matcher: "",
323
1007
  tags: ["context", "compaction", "state", "backup"]
324
1008
  },
1009
+ {
1010
+ name: "knowledge-context",
1011
+ displayName: "Knowledge Context",
1012
+ description: "Injects deterministic Knowledge context packs into Codewith SessionStart, UserPromptSubmit, and SubagentStart",
1013
+ version: "0.1.0",
1014
+ category: "Context Management",
1015
+ event: "SessionStart",
1016
+ events: ["SessionStart", "UserPromptSubmit", "SubagentStart"],
1017
+ matcher: "",
1018
+ tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
1019
+ },
325
1020
  {
326
1021
  name: "autoformat",
327
1022
  displayName: "Auto Format",
@@ -392,6 +1087,16 @@ var HOOKS = [
392
1087
  matcher: "",
393
1088
  tags: ["prompt", "injection", "security", "validation", "guard"]
394
1089
  },
1090
+ {
1091
+ name: "prompt-guard",
1092
+ displayName: "Prompt Guard (Codewith)",
1093
+ description: "Codewith UserPromptSubmit guard for obvious pasted prompt-injection and fake policy/freeze instructions",
1094
+ version: "0.1.0",
1095
+ category: "Permissions",
1096
+ event: "UserPromptSubmit",
1097
+ matcher: "",
1098
+ tags: ["codewith", "prompt", "injection", "policy", "freeze"]
1099
+ },
395
1100
  {
396
1101
  name: "desktopnotify",
397
1102
  displayName: "Desktop Notify",
@@ -482,6 +1187,26 @@ var HOOKS = [
482
1187
  matcher: "",
483
1188
  tags: ["tasks", "completion", "gate", "quality", "agent-teams"]
484
1189
  },
1190
+ {
1191
+ name: "session-start",
1192
+ displayName: "Session Start",
1193
+ description: "Codewith SessionStart digest for blockers, announcements, identity registration, and heartbeat context",
1194
+ version: "0.1.0",
1195
+ category: "Agent Teams",
1196
+ event: "SessionStart",
1197
+ matcher: "",
1198
+ tags: ["codewith", "session", "context", "conversations", "heartbeat"]
1199
+ },
1200
+ {
1201
+ name: "stop-sync",
1202
+ displayName: "Stop Sync",
1203
+ description: "Codewith Stop turn-end heartbeat and best-effort task evidence helper",
1204
+ version: "0.1.0",
1205
+ category: "Agent Teams",
1206
+ event: "Stop",
1207
+ matcher: "",
1208
+ tags: ["codewith", "stop", "heartbeat", "todos", "turn-end"]
1209
+ },
485
1210
  {
486
1211
  name: "typecheck-gate",
487
1212
  displayName: "Typecheck Gate",
@@ -535,10 +1260,10 @@ var HOOKS = [
535
1260
  {
536
1261
  name: "announce-start",
537
1262
  displayName: "Announce Start",
538
- description: "On session start, auto-registers agent, reads messages, and announces to space",
539
- version: "0.1.0",
1263
+ description: "On SessionStart, auto-registers agent, reads messages, and announces to space",
1264
+ version: "0.2.0",
540
1265
  category: "Agent Teams",
541
- event: "Notification",
1266
+ event: "SessionStart",
542
1267
  matcher: "",
543
1268
  tags: ["announcement", "start", "register", "messages", "agent-teams"]
544
1269
  },
@@ -561,11 +1286,44 @@ var HOOKS = [
561
1286
  event: "Notification",
562
1287
  matcher: "",
563
1288
  tags: ["messages", "dm", "context", "inject", "agent-teams"]
1289
+ },
1290
+ {
1291
+ name: "fleet-catchup",
1292
+ displayName: "Fleet Catchup",
1293
+ description: "On SessionStart, injects unread blockers, channel notifications, and the announcements digest into context (deterministic CLI reads, fail-open)",
1294
+ version: "0.1.0",
1295
+ category: "Agent Teams",
1296
+ event: "SessionStart",
1297
+ matcher: "",
1298
+ tags: ["fleet", "catchup", "blockers", "announcements", "context", "agent-teams"]
1299
+ },
1300
+ {
1301
+ name: "agent-rules-version-check",
1302
+ displayName: "Agent Rules Version Check",
1303
+ description: "On SessionStart, compares the rendered hasna:agent-operating-rules sentinel version against configs state and warns on mismatch",
1304
+ version: "0.1.0",
1305
+ category: "Agent Teams",
1306
+ event: "SessionStart",
1307
+ matcher: "",
1308
+ tags: ["fleet", "rules", "version", "sentinel", "configs", "drift", "agent-teams"]
1309
+ },
1310
+ {
1311
+ name: "fleet-blockers-gate",
1312
+ displayName: "Fleet Blockers Gate",
1313
+ description: "Before tool calls, blocks mutating tools while an unread [FREEZE] blocker is active (TTL-cached conversations check, fail-open <500ms)",
1314
+ version: "0.1.0",
1315
+ category: "Agent Teams",
1316
+ event: "PreToolUse",
1317
+ matcher: "",
1318
+ tags: ["fleet", "freeze", "blockers", "gate", "safety", "agent-teams"]
564
1319
  }
565
1320
  ];
566
1321
  function getHooksByCategory(category) {
567
1322
  return HOOKS.filter((h) => h.category === category);
568
1323
  }
1324
+ function getHookEvents(hook) {
1325
+ return hook.events && hook.events.length > 0 ? hook.events : [hook.event];
1326
+ }
569
1327
  function searchHooks(query) {
570
1328
  const q = query.toLowerCase();
571
1329
  return HOOKS.filter((h) => h.name.toLowerCase().includes(q) || h.displayName.toLowerCase().includes(q) || h.description.toLowerCase().includes(q) || h.tags.some((t) => t.includes(q)));
@@ -574,21 +1332,21 @@ function getHook(name) {
574
1332
  return HOOKS.find((h) => h.name === name);
575
1333
  }
576
1334
  // src/lib/installer.ts
577
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
578
- import { join, dirname } from "path";
579
- import { homedir } from "os";
1335
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1336
+ import { join as join3, dirname } from "path";
1337
+ import { homedir as homedir3 } from "os";
580
1338
  import { fileURLToPath } from "url";
581
1339
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
582
- var HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
1340
+ var HOOKS_DIR = existsSync3(join3(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join3(__dirname2, "..", "..", "hooks") : join3(__dirname2, "..", "hooks");
583
1341
  function normalizeHookName(name) {
584
1342
  return name.startsWith("hook-") ? name : `hook-${name}`;
585
1343
  }
586
1344
  function shortHookName(name) {
587
- return normalizeHookName(name).replace("hook-", "");
1345
+ return name.startsWith("hook-") ? name.slice("hook-".length) : name;
588
1346
  }
589
1347
  function removeHookEntriesByName(entries, hookName) {
590
1348
  return entries.filter((entry) => !entry.hooks?.some((h) => {
591
- const match = h.command?.match(/^hooks run (\w+)/);
1349
+ const match = h.command?.match(/^hooks run ([\w-]+)/);
592
1350
  return match && match[1] === hookName;
593
1351
  }));
594
1352
  }
@@ -597,38 +1355,67 @@ var EVENT_MAP = {
597
1355
  PreToolUse: "PreToolUse",
598
1356
  PostToolUse: "PostToolUse",
599
1357
  Stop: "Stop",
600
- Notification: "Notification"
1358
+ Notification: "Notification",
1359
+ SessionStart: "SessionStart",
1360
+ SessionEnd: "SessionEnd",
1361
+ UserPromptSubmit: null,
1362
+ SubagentStart: null
601
1363
  },
602
1364
  gemini: {
603
1365
  PreToolUse: "BeforeTool",
604
1366
  PostToolUse: "AfterTool",
605
1367
  Stop: "AfterAgent",
606
- Notification: "Notification"
1368
+ Notification: "Notification",
1369
+ SessionStart: null,
1370
+ SessionEnd: null,
1371
+ UserPromptSubmit: null,
1372
+ SubagentStart: null
1373
+ },
1374
+ codewith: {
1375
+ PreToolUse: "PreToolUse",
1376
+ PostToolUse: "PostToolUse",
1377
+ Stop: "Stop",
1378
+ Notification: null,
1379
+ SessionStart: "SessionStart",
1380
+ SessionEnd: null,
1381
+ UserPromptSubmit: "UserPromptSubmit",
1382
+ SubagentStart: "SubagentStart"
607
1383
  }
608
1384
  };
609
1385
  function getTargetSettingsDir(target) {
1386
+ if (target === "codewith")
1387
+ return ".codewith";
610
1388
  if (target === "gemini")
611
1389
  return ".gemini";
612
1390
  return ".claude";
613
1391
  }
614
- function getSettingsPath(scope = "global", target = "claude") {
1392
+ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
1393
+ if (target === "codewith" && codewithConfigPath)
1394
+ return codewithConfigPath;
1395
+ if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
1396
+ return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
1397
+ }
615
1398
  const dir = getTargetSettingsDir(target);
616
1399
  if (scope === "project") {
617
- return join(process.cwd(), dir, "settings.json");
1400
+ return target === "codewith" ? join3(process.cwd(), dir, "config.toml") : join3(process.cwd(), dir, "settings.json");
618
1401
  }
619
- return join(homedir(), dir, "settings.json");
1402
+ return target === "codewith" ? join3(homedir3(), dir, "config.toml") : join3(homedir3(), dir, "settings.json");
620
1403
  }
621
1404
  function getHookPath(name) {
622
- return join(HOOKS_DIR, normalizeHookName(name));
1405
+ const shortName = shortHookName(name);
1406
+ const direct = join3(HOOKS_DIR, shortName);
1407
+ if (existsSync3(direct))
1408
+ return direct;
1409
+ return join3(HOOKS_DIR, normalizeHookName(shortName));
623
1410
  }
624
1411
  function hookExists(name) {
625
- return existsSync(getHookPath(name));
1412
+ return existsSync3(getHookPath(name));
626
1413
  }
627
1414
  function readSettings(scope = "global", target = "claude") {
628
1415
  const path = getSettingsPath(scope, target);
629
1416
  try {
630
- if (existsSync(path)) {
631
- return JSON.parse(readFileSync(path, "utf-8"));
1417
+ if (existsSync3(path)) {
1418
+ return JSON.parse(readFileSync2(path, "utf-8"));
632
1419
  }
633
1420
  } catch (error) {
634
1421
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -638,25 +1425,125 @@ function readSettings(scope = "global", target = "claude") {
638
1425
  function writeSettings(settings, scope = "global", target = "claude") {
639
1426
  const path = getSettingsPath(scope, target);
640
1427
  const dir = dirname(path);
641
- if (!existsSync(dir)) {
642
- mkdirSync(dir, { recursive: true });
1428
+ if (!existsSync3(dir)) {
1429
+ mkdirSync2(dir, { recursive: true });
643
1430
  }
644
1431
  writeFileSync(path, JSON.stringify(settings, null, 2) + `
645
1432
  `);
646
1433
  }
647
1434
  function getTargetEventName(internalEvent, target) {
648
- return EVENT_MAP[target]?.[internalEvent] || internalEvent;
1435
+ return EVENT_MAP[target]?.[internalEvent] ?? null;
1436
+ }
1437
+ function isEventSupported(internalEvent, target) {
1438
+ return getTargetEventName(internalEvent, target) !== null;
1439
+ }
1440
+ function tomlString(value) {
1441
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
1442
+ }
1443
+ function codewithMatcher(matcher) {
1444
+ if (!matcher)
1445
+ return;
1446
+ if (matcher.startsWith("^"))
1447
+ return matcher;
1448
+ if (matcher.includes("|"))
1449
+ return `^(${matcher})$`;
1450
+ return `^${matcher}$`;
1451
+ }
1452
+ function codewithTimeout(name) {
1453
+ switch (name) {
1454
+ case "knowledge-context":
1455
+ return 2;
1456
+ case "session-start":
1457
+ return 8;
1458
+ case "pre-bash":
1459
+ return 20;
1460
+ case "prompt-guard":
1461
+ return 3;
1462
+ case "worktree-guard":
1463
+ case "stop-sync":
1464
+ return 5;
1465
+ default:
1466
+ return 10;
1467
+ }
1468
+ }
1469
+ function codewithStatusMessage(name) {
1470
+ switch (name) {
1471
+ case "knowledge-context":
1472
+ return "Loading Knowledge context";
1473
+ case "session-start":
1474
+ return "Checking Hasna session context";
1475
+ case "pre-bash":
1476
+ return "Checking Bash safety";
1477
+ case "prompt-guard":
1478
+ return "Checking prompt safety";
1479
+ case "worktree-guard":
1480
+ return "Checking worktree safety";
1481
+ case "stop-sync":
1482
+ return "Syncing turn-end heartbeat";
1483
+ default:
1484
+ return `Running ${name}`;
1485
+ }
1486
+ }
1487
+ function buildCodewithTomlFragment(name, profile) {
1488
+ const shortName = shortHookName(name);
1489
+ const meta = getHook(shortName);
1490
+ if (!meta)
1491
+ throw new Error(`Hook '${shortName}' not found`);
1492
+ const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
1493
+ const matcher = codewithMatcher(meta.matcher);
1494
+ const fragments = [];
1495
+ for (const event of getHookEvents(meta)) {
1496
+ const eventKey = getTargetEventName(event, "codewith");
1497
+ if (!eventKey) {
1498
+ throw new Error(`Hook '${shortName}' uses event '${event}', which is not supported by the Codewith target`);
1499
+ }
1500
+ const lines = [
1501
+ `[[hooks.${eventKey}]]`
1502
+ ];
1503
+ if (matcher)
1504
+ lines.push(`matcher = ${tomlString(matcher)}`);
1505
+ lines.push("", `[[hooks.${eventKey}.hooks]]`, `type = "command"`, `command = ${tomlString(command)}`, `timeout = ${codewithTimeout(shortName)}`, `statusMessage = ${tomlString(codewithStatusMessage(shortName))}`);
1506
+ fragments.push(lines.join(`
1507
+ `));
1508
+ }
1509
+ return `${fragments.join(`
1510
+
1511
+ `)}
1512
+ `;
1513
+ }
1514
+ function readCodewithConfig(scope, configPath) {
1515
+ const path = getSettingsPath(scope, "codewith", configPath);
1516
+ try {
1517
+ return existsSync3(path) ? readFileSync2(path, "utf-8") : "";
1518
+ } catch {
1519
+ return "";
1520
+ }
1521
+ }
1522
+ function appendCodewithFragment(fragment, scope, configPath) {
1523
+ const path = getSettingsPath(scope, "codewith", configPath);
1524
+ const dir = dirname(path);
1525
+ if (!existsSync3(dir))
1526
+ mkdirSync2(dir, { recursive: true });
1527
+ const existing = existsSync3(path) ? readFileSync2(path, "utf-8") : "";
1528
+ const sep = existing.trim() ? `
1529
+
1530
+ ` : "";
1531
+ writeFileSync(path, `${existing.replace(/\s*$/, "")}${sep}${fragment}`);
1532
+ return path;
649
1533
  }
650
1534
  function detectConflict(name, scope, target) {
651
1535
  const meta = getHook(name);
652
1536
  if (!meta || !meta.matcher)
653
1537
  return;
1538
+ const events = new Set(getHookEvents(meta));
654
1539
  const registered = getRegisteredHooksForTarget(scope, target);
655
1540
  for (const existingName of registered) {
656
1541
  if (existingName === name)
657
1542
  continue;
658
1543
  const existing = getHook(existingName);
659
- if (!existing || existing.event !== meta.event || !existing.matcher)
1544
+ if (!existing || !existing.matcher)
1545
+ continue;
1546
+ if (!getHookEvents(existing).some((event) => events.has(event)))
660
1547
  continue;
661
1548
  const a = meta.matcher.toLowerCase();
662
1549
  const b = existing.matcher.toLowerCase();
@@ -666,11 +1553,60 @@ function detectConflict(name, scope, target) {
666
1553
  }
667
1554
  return;
668
1555
  }
669
- function installForTarget(name, scope, overwrite, target, profile) {
1556
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
670
1557
  const shortName = shortHookName(name);
671
1558
  if (!hookExists(shortName)) {
672
1559
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
673
1560
  }
1561
+ if (target === "codewith") {
1562
+ try {
1563
+ const fragment = buildCodewithTomlFragment(shortName, profile);
1564
+ if (codewithMode !== "write") {
1565
+ return {
1566
+ hook: shortName,
1567
+ success: true,
1568
+ scope,
1569
+ target,
1570
+ fragment,
1571
+ applied: false,
1572
+ note: "Codewith install is fragment-only by default; open-configs should own applying this TOML."
1573
+ };
1574
+ }
1575
+ if (!codewithConfigPath) {
1576
+ return {
1577
+ hook: shortName,
1578
+ success: false,
1579
+ error: "Direct Codewith writes require an explicit --codewith-config path; refusing to write default ~/.codewith/config.toml.",
1580
+ scope,
1581
+ target,
1582
+ fragment,
1583
+ applied: false
1584
+ };
1585
+ }
1586
+ const existing = readCodewithConfig(scope, codewithConfigPath);
1587
+ if (!overwrite && new RegExp(`command\\s*=\\s*["']hooks run ${shortName}(?:\\s|["'])`).test(existing)) {
1588
+ return { hook: shortName, success: false, error: "Already installed. Use --overwrite to append another fragment.", scope, target };
1589
+ }
1590
+ const path = appendCodewithFragment(fragment, scope, codewithConfigPath);
1591
+ return {
1592
+ hook: shortName,
1593
+ success: true,
1594
+ scope,
1595
+ target,
1596
+ fragment,
1597
+ applied: true,
1598
+ configPath: path,
1599
+ note: "Direct Codewith config write was explicitly requested; prefer open-configs for managed machines."
1600
+ };
1601
+ } catch (error) {
1602
+ return {
1603
+ hook: shortName,
1604
+ success: false,
1605
+ error: error instanceof Error ? error.message : "Unknown error",
1606
+ target
1607
+ };
1608
+ }
1609
+ }
674
1610
  const registered = getRegisteredHooksForTarget(scope, target);
675
1611
  if (registered.includes(shortName) && !overwrite) {
676
1612
  return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
@@ -689,35 +1625,92 @@ function installForTarget(name, scope, overwrite, target, profile) {
689
1625
  }
690
1626
  }
691
1627
  function installHook(name, options = {}) {
692
- const { scope = "global", overwrite = false, target = "claude", profile } = options;
1628
+ const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
693
1629
  if (target === "all") {
1630
+ const shortName = shortHookName(name);
1631
+ const meta = getHook(shortName);
1632
+ if (meta) {
1633
+ const unsupportedTargets = ["claude", "gemini", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
1634
+ if (unsupportedTargets.length > 0) {
1635
+ return {
1636
+ hook: shortName,
1637
+ success: false,
1638
+ error: `Event(s) '${getHookEvents(meta).join(", ")}' are not supported by target(s): ${unsupportedTargets.join(", ")}`,
1639
+ target: "all"
1640
+ };
1641
+ }
1642
+ }
694
1643
  const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
695
- installForTarget(name, scope, overwrite, "gemini", profile);
1644
+ if (!claudeResult.success) {
1645
+ return {
1646
+ ...claudeResult,
1647
+ error: `Failed for target 'claude': ${claudeResult.error}`,
1648
+ target: "all"
1649
+ };
1650
+ }
1651
+ const geminiResult = installForTarget(name, scope, overwrite, "gemini", profile);
1652
+ if (!geminiResult.success) {
1653
+ return {
1654
+ ...geminiResult,
1655
+ error: `Failed for target 'gemini': ${geminiResult.error}`,
1656
+ target: "all"
1657
+ };
1658
+ }
1659
+ const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
1660
+ if (!codewithResult.success) {
1661
+ return {
1662
+ ...codewithResult,
1663
+ error: `Failed for target 'codewith': ${codewithResult.error}`,
1664
+ target: "all"
1665
+ };
1666
+ }
696
1667
  return { ...claudeResult, target: "all" };
697
1668
  }
698
- return installForTarget(name, scope, overwrite, target, profile);
1669
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
699
1670
  }
700
1671
  function registerHook(name, scope = "global", target = "claude", profile) {
701
1672
  const meta = getHook(name);
702
1673
  if (!meta)
703
1674
  return;
1675
+ const eventKeys = getHookEvents(meta).map((event) => {
1676
+ const eventKey = getTargetEventName(event, target);
1677
+ if (eventKey === null) {
1678
+ throw new Error(`Event '${event}' is not supported by target '${target}'`);
1679
+ }
1680
+ return eventKey;
1681
+ });
1682
+ const uniqueEventKeys = [...new Set(eventKeys)];
1683
+ if (uniqueEventKeys.length === 0) {
1684
+ throw new Error(`Hook '${name}' has no installable events for target '${target}'`);
1685
+ }
704
1686
  const settings = readSettings(scope, target);
705
1687
  if (!settings.hooks)
706
1688
  settings.hooks = {};
707
- const eventKey = getTargetEventName(meta.event, target);
708
- if (!settings.hooks[eventKey])
709
- settings.hooks[eventKey] = [];
710
- settings.hooks[eventKey] = removeHookEntriesByName(settings.hooks[eventKey], name);
1689
+ removeHookFromAllEvents(settings, name);
711
1690
  const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
712
- const entry = {
713
- hooks: [{ type: "command", command: hookCommand }]
714
- };
715
- if (meta.matcher) {
716
- entry.matcher = meta.matcher;
1691
+ for (const eventKey of uniqueEventKeys) {
1692
+ if (!settings.hooks[eventKey])
1693
+ settings.hooks[eventKey] = [];
1694
+ const entry = {
1695
+ hooks: [{ type: "command", command: hookCommand }]
1696
+ };
1697
+ if (meta.matcher) {
1698
+ entry.matcher = meta.matcher;
1699
+ }
1700
+ settings.hooks[eventKey].push(entry);
717
1701
  }
718
- settings.hooks[eventKey].push(entry);
719
1702
  writeSettings(settings, scope, target);
720
1703
  }
1704
+ function removeHookFromAllEvents(settings, name) {
1705
+ if (!settings.hooks)
1706
+ return;
1707
+ for (const key of Object.keys(settings.hooks)) {
1708
+ settings.hooks[key] = removeHookEntriesByName(settings.hooks[key], name);
1709
+ if (settings.hooks[key].length === 0) {
1710
+ delete settings.hooks[key];
1711
+ }
1712
+ }
1713
+ }
721
1714
  function unregisterHook(name, scope = "global", target = "claude") {
722
1715
  const meta = getHook(name);
723
1716
  if (!meta)
@@ -725,13 +1718,7 @@ function unregisterHook(name, scope = "global", target = "claude") {
725
1718
  const settings = readSettings(scope, target);
726
1719
  if (!settings.hooks)
727
1720
  return;
728
- const eventKey = getTargetEventName(meta.event, target);
729
- if (!settings.hooks[eventKey])
730
- return;
731
- settings.hooks[eventKey] = removeHookEntriesByName(settings.hooks[eventKey], name);
732
- if (settings.hooks[eventKey].length === 0) {
733
- delete settings.hooks[eventKey];
734
- }
1721
+ removeHookFromAllEvents(settings, name);
735
1722
  if (Object.keys(settings.hooks).length === 0) {
736
1723
  delete settings.hooks;
737
1724
  }
@@ -741,6 +1728,16 @@ function installHooks(names, options = {}) {
741
1728
  return names.map((name) => installHook(name, options));
742
1729
  }
743
1730
  function getRegisteredHooksForTarget(scope = "global", target = "claude") {
1731
+ if (target === "codewith") {
1732
+ const config = readCodewithConfig(scope);
1733
+ const registered2 = [];
1734
+ const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
1735
+ let match;
1736
+ while (match = re.exec(config)) {
1737
+ registered2.push(match[1]);
1738
+ }
1739
+ return [...new Set(registered2)];
1740
+ }
744
1741
  const settings = readSettings(scope, target);
745
1742
  if (!settings.hooks)
746
1743
  return [];
@@ -748,8 +1745,8 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
748
1745
  for (const eventKey of Object.keys(settings.hooks)) {
749
1746
  for (const entry of settings.hooks[eventKey]) {
750
1747
  for (const hook of entry.hooks || []) {
751
- const newMatch = hook.command?.match(/^hooks run (\w+)(?:\s+--profile\s+\w+)?$/);
752
- const oldMatch = hook.command?.match(/^hook-(\w+)$/);
1748
+ const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
1749
+ const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
753
1750
  const match = newMatch || oldMatch;
754
1751
  if (match) {
755
1752
  registered.push(match[1]);
@@ -770,6 +1767,9 @@ function removeHook(name, scope = "global", target = "claude") {
770
1767
  const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
771
1768
  return claudeRemoved || geminiRemoved;
772
1769
  }
1770
+ if (target === "codewith") {
1771
+ return false;
1772
+ }
773
1773
  return removeHookForTarget(shortName, scope, target);
774
1774
  }
775
1775
  function removeHookForTarget(name, scope, target) {
@@ -782,8 +1782,8 @@ function removeHookForTarget(name, scope, target) {
782
1782
  }
783
1783
  // src/index.ts
784
1784
  init_profiles();
785
- import { join as join3 } from "path";
786
- import { existsSync as existsSync3 } from "fs";
1785
+ import { join as join5 } from "path";
1786
+ import { existsSync as existsSync5 } from "fs";
787
1787
  function installHookForProject(name, options = {}) {
788
1788
  return installHook(name, { ...options, scope: "project" });
789
1789
  }
@@ -801,8 +1801,8 @@ async function runHook(name, input, options = {}) {
801
1801
  if (!meta)
802
1802
  throw new Error(`Hook '${name}' not found`);
803
1803
  const hookDir = getHookPath(name);
804
- const hookScript = join3(hookDir, "src", "hook.ts");
805
- if (!existsSync3(hookScript))
1804
+ const hookScript = join5(hookDir, "src", "hook.ts");
1805
+ if (!existsSync5(hookScript))
806
1806
  throw new Error(`Hook script not found: ${hookScript}`);
807
1807
  let hookInput = { ...input };
808
1808
  if (options.profile) {
@@ -839,18 +1839,32 @@ async function runHook(name, input, options = {}) {
839
1839
  export {
840
1840
  updateProfile,
841
1841
  touchProfile,
1842
+ storageSync,
1843
+ storagePush,
1844
+ storagePull,
842
1845
  searchHooks,
1846
+ runStorageMigrations,
843
1847
  runHook,
1848
+ resolveTables,
844
1849
  removeProjectHook,
845
1850
  removeHook,
1851
+ parseStorageTables,
846
1852
  listProjectHooks,
847
1853
  listProfiles,
1854
+ isEventSupported,
848
1855
  installHooksForProject,
849
1856
  installHooks,
850
1857
  installHookForProject,
851
1858
  installHook,
852
1859
  importProfiles,
853
1860
  hookExists,
1861
+ getSyncMetaAll,
1862
+ getStorageStatus,
1863
+ getStoragePg,
1864
+ getStorageMode,
1865
+ getStorageDatabaseUrl,
1866
+ getStorageDatabaseEnvName,
1867
+ getStorageDatabaseEnv,
854
1868
  getSettingsPath,
855
1869
  getRegisteredHooksForTarget,
856
1870
  getRegisteredHooks,
@@ -859,10 +1873,21 @@ export {
859
1873
  getInstalledHooks,
860
1874
  getHooksByCategory,
861
1875
  getHookPath,
1876
+ getHookEvents,
862
1877
  getHook,
863
1878
  exportProfiles,
864
1879
  deleteProfile,
865
1880
  createProfile,
1881
+ buildCodewithTomlFragment,
1882
+ STORAGE_TABLES,
1883
+ STORAGE_MODE_ENV,
1884
+ STORAGE_DATABASE_ENV,
1885
+ HOOK_EVENTS,
1886
+ HOOKS_STORAGE_TABLES,
1887
+ HOOKS_STORAGE_MODE_FALLBACK_ENV,
1888
+ HOOKS_STORAGE_MODE_ENV,
1889
+ HOOKS_STORAGE_FALLBACK_ENV,
1890
+ HOOKS_STORAGE_ENV,
866
1891
  HOOKS,
867
1892
  CATEGORIES
868
1893
  };