@hasna/hooks 0.2.19 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +45 -5
  2. package/bin/index.js +1262 -9569
  3. package/dist/db/index.d.ts +11 -0
  4. package/dist/db/legacy-import.d.ts +11 -0
  5. package/dist/db/migrations/001_initial.d.ts +6 -0
  6. package/dist/db/migrations/002_session_events.d.ts +10 -0
  7. package/dist/db/migrations/003_user_prompt_submit_event.d.ts +10 -0
  8. package/dist/db/migrations/index.d.ts +7 -0
  9. package/dist/db/pg-migrations.d.ts +7 -0
  10. package/dist/db/remote-storage.d.ts +9 -0
  11. package/dist/db/retention.d.ts +8 -0
  12. package/dist/db/schema.d.ts +22 -0
  13. package/dist/db/storage-sync.d.ts +57 -0
  14. package/dist/index.d.ts +72 -0
  15. package/dist/index.js +1042 -58
  16. package/dist/lib/installer.d.ts +56 -0
  17. package/dist/lib/profiles.d.ts +33 -0
  18. package/dist/lib/registry.d.ts +21 -0
  19. package/dist/storage.d.ts +4 -0
  20. package/dist/storage.js +696 -0
  21. package/hooks/codewith-native-common.test.ts +61 -0
  22. package/hooks/codewith-native-common.ts +478 -0
  23. package/hooks/hook-agent-rules-version-check/LICENSE +191 -0
  24. package/hooks/hook-agent-rules-version-check/README.md +50 -0
  25. package/hooks/hook-agent-rules-version-check/package.json +53 -0
  26. package/hooks/hook-agent-rules-version-check/src/hook.test.ts +104 -0
  27. package/hooks/hook-agent-rules-version-check/src/hook.ts +215 -0
  28. package/hooks/hook-agent-rules-version-check/tsconfig.json +25 -0
  29. package/hooks/hook-announce-start/README.md +7 -5
  30. package/hooks/hook-announce-start/package.json +2 -2
  31. package/hooks/hook-announce-start/src/hook.ts +33 -14
  32. package/hooks/hook-fleet-blockers-gate/LICENSE +191 -0
  33. package/hooks/hook-fleet-blockers-gate/README.md +39 -0
  34. package/hooks/hook-fleet-blockers-gate/package.json +52 -0
  35. package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +106 -0
  36. package/hooks/hook-fleet-blockers-gate/src/hook.ts +242 -0
  37. package/hooks/hook-fleet-blockers-gate/tsconfig.json +25 -0
  38. package/hooks/hook-fleet-catchup/LICENSE +191 -0
  39. package/hooks/hook-fleet-catchup/README.md +40 -0
  40. package/hooks/hook-fleet-catchup/package.json +52 -0
  41. package/hooks/hook-fleet-catchup/src/hook.test.ts +156 -0
  42. package/hooks/hook-fleet-catchup/src/hook.ts +283 -0
  43. package/hooks/hook-fleet-catchup/tsconfig.json +25 -0
  44. package/hooks/pre-bash/README.md +17 -0
  45. package/hooks/pre-bash/package.json +12 -0
  46. package/hooks/pre-bash/src/hook.ts +103 -0
  47. package/hooks/prompt-guard/README.md +17 -0
  48. package/hooks/prompt-guard/package.json +12 -0
  49. package/hooks/prompt-guard/src/hook.ts +76 -0
  50. package/hooks/session-start/README.md +17 -0
  51. package/hooks/session-start/package.json +12 -0
  52. package/hooks/session-start/src/hook.ts +150 -0
  53. package/hooks/stop-sync/README.md +17 -0
  54. package/hooks/stop-sync/package.json +12 -0
  55. package/hooks/stop-sync/src/hook.ts +69 -0
  56. package/hooks/worktree-guard/README.md +17 -0
  57. package/hooks/worktree-guard/package.json +12 -0
  58. package/hooks/worktree-guard/src/hook.ts +88 -0
  59. package/package.json +14 -7
  60. package/hooks/hook-affected-tests/dist/hook.js +0 -106
  61. package/hooks/hook-announce-start/dist/hook.js +0 -98
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,670 @@ 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
+ ];
162
825
  var CATEGORIES = [
163
826
  "Git Safety",
164
827
  "Code Quality",
@@ -202,6 +865,16 @@ var HOOKS = [
202
865
  matcher: "Write|Edit|NotebookEdit",
203
866
  tags: ["git", "snapshot", "rollback", "backup"]
204
867
  },
868
+ {
869
+ name: "worktree-guard",
870
+ displayName: "Worktree Guard",
871
+ description: "Warns on feature work outside managed repos worktrees and blocks commit/push from shared checkouts",
872
+ version: "0.1.0",
873
+ category: "Git Safety",
874
+ event: "PreToolUse",
875
+ matcher: "Bash|Write|Edit|MultiEdit|NotebookEdit",
876
+ tags: ["git", "worktree", "repos", "multi-agent", "safety"]
877
+ },
205
878
  {
206
879
  name: "checktests",
207
880
  displayName: "Check Tests",
@@ -282,6 +955,16 @@ var HOOKS = [
282
955
  matcher: "Bash",
283
956
  tags: ["npm", "packages", "typosquatting", "supply-chain"]
284
957
  },
958
+ {
959
+ name: "pre-bash",
960
+ displayName: "Pre Bash",
961
+ description: "Codewith Bash gate for staged secrets scans before commit/push and comms rechecks before risky operations",
962
+ version: "0.1.0",
963
+ category: "Security",
964
+ event: "PreToolUse",
965
+ matcher: "Bash",
966
+ tags: ["codewith", "bash", "secrets", "gitleaks", "risky-ops"]
967
+ },
285
968
  {
286
969
  name: "phonenotify",
287
970
  displayName: "Phone Notify",
@@ -392,6 +1075,16 @@ var HOOKS = [
392
1075
  matcher: "",
393
1076
  tags: ["prompt", "injection", "security", "validation", "guard"]
394
1077
  },
1078
+ {
1079
+ name: "prompt-guard",
1080
+ displayName: "Prompt Guard (Codewith)",
1081
+ description: "Codewith UserPromptSubmit guard for obvious pasted prompt-injection and fake policy/freeze instructions",
1082
+ version: "0.1.0",
1083
+ category: "Permissions",
1084
+ event: "UserPromptSubmit",
1085
+ matcher: "",
1086
+ tags: ["codewith", "prompt", "injection", "policy", "freeze"]
1087
+ },
395
1088
  {
396
1089
  name: "desktopnotify",
397
1090
  displayName: "Desktop Notify",
@@ -482,6 +1175,26 @@ var HOOKS = [
482
1175
  matcher: "",
483
1176
  tags: ["tasks", "completion", "gate", "quality", "agent-teams"]
484
1177
  },
1178
+ {
1179
+ name: "session-start",
1180
+ displayName: "Session Start",
1181
+ description: "Codewith SessionStart digest for blockers, announcements, identity registration, and heartbeat context",
1182
+ version: "0.1.0",
1183
+ category: "Agent Teams",
1184
+ event: "SessionStart",
1185
+ matcher: "",
1186
+ tags: ["codewith", "session", "context", "conversations", "heartbeat"]
1187
+ },
1188
+ {
1189
+ name: "stop-sync",
1190
+ displayName: "Stop Sync",
1191
+ description: "Codewith Stop turn-end heartbeat and best-effort task evidence helper",
1192
+ version: "0.1.0",
1193
+ category: "Agent Teams",
1194
+ event: "Stop",
1195
+ matcher: "",
1196
+ tags: ["codewith", "stop", "heartbeat", "todos", "turn-end"]
1197
+ },
485
1198
  {
486
1199
  name: "typecheck-gate",
487
1200
  displayName: "Typecheck Gate",
@@ -535,10 +1248,10 @@ var HOOKS = [
535
1248
  {
536
1249
  name: "announce-start",
537
1250
  displayName: "Announce Start",
538
- description: "On session start, auto-registers agent, reads messages, and announces to space",
539
- version: "0.1.0",
1251
+ description: "On SessionStart, auto-registers agent, reads messages, and announces to space",
1252
+ version: "0.2.0",
540
1253
  category: "Agent Teams",
541
- event: "Notification",
1254
+ event: "SessionStart",
542
1255
  matcher: "",
543
1256
  tags: ["announcement", "start", "register", "messages", "agent-teams"]
544
1257
  },
@@ -561,6 +1274,36 @@ var HOOKS = [
561
1274
  event: "Notification",
562
1275
  matcher: "",
563
1276
  tags: ["messages", "dm", "context", "inject", "agent-teams"]
1277
+ },
1278
+ {
1279
+ name: "fleet-catchup",
1280
+ displayName: "Fleet Catchup",
1281
+ description: "On SessionStart, injects unread blockers, channel notifications, and the announcements digest into context (deterministic CLI reads, fail-open)",
1282
+ version: "0.1.0",
1283
+ category: "Agent Teams",
1284
+ event: "SessionStart",
1285
+ matcher: "",
1286
+ tags: ["fleet", "catchup", "blockers", "announcements", "context", "agent-teams"]
1287
+ },
1288
+ {
1289
+ name: "agent-rules-version-check",
1290
+ displayName: "Agent Rules Version Check",
1291
+ description: "On SessionStart, compares the rendered hasna:agent-operating-rules sentinel version against configs state and warns on mismatch",
1292
+ version: "0.1.0",
1293
+ category: "Agent Teams",
1294
+ event: "SessionStart",
1295
+ matcher: "",
1296
+ tags: ["fleet", "rules", "version", "sentinel", "configs", "drift", "agent-teams"]
1297
+ },
1298
+ {
1299
+ name: "fleet-blockers-gate",
1300
+ displayName: "Fleet Blockers Gate",
1301
+ description: "Before tool calls, blocks mutating tools while an unread [FREEZE] blocker is active (TTL-cached conversations check, fail-open <500ms)",
1302
+ version: "0.1.0",
1303
+ category: "Agent Teams",
1304
+ event: "PreToolUse",
1305
+ matcher: "",
1306
+ tags: ["fleet", "freeze", "blockers", "gate", "safety", "agent-teams"]
564
1307
  }
565
1308
  ];
566
1309
  function getHooksByCategory(category) {
@@ -574,21 +1317,21 @@ function getHook(name) {
574
1317
  return HOOKS.find((h) => h.name === name);
575
1318
  }
576
1319
  // src/lib/installer.ts
577
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
578
- import { join, dirname } from "path";
579
- import { homedir } from "os";
1320
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1321
+ import { join as join3, dirname } from "path";
1322
+ import { homedir as homedir3 } from "os";
580
1323
  import { fileURLToPath } from "url";
581
1324
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
582
- var HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
1325
+ var HOOKS_DIR = existsSync3(join3(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join3(__dirname2, "..", "..", "hooks") : join3(__dirname2, "..", "hooks");
583
1326
  function normalizeHookName(name) {
584
1327
  return name.startsWith("hook-") ? name : `hook-${name}`;
585
1328
  }
586
1329
  function shortHookName(name) {
587
- return normalizeHookName(name).replace("hook-", "");
1330
+ return name.startsWith("hook-") ? name.slice("hook-".length) : name;
588
1331
  }
589
1332
  function removeHookEntriesByName(entries, hookName) {
590
1333
  return entries.filter((entry) => !entry.hooks?.some((h) => {
591
- const match = h.command?.match(/^hooks run (\w+)/);
1334
+ const match = h.command?.match(/^hooks run ([\w-]+)/);
592
1335
  return match && match[1] === hookName;
593
1336
  }));
594
1337
  }
@@ -597,38 +1340,64 @@ var EVENT_MAP = {
597
1340
  PreToolUse: "PreToolUse",
598
1341
  PostToolUse: "PostToolUse",
599
1342
  Stop: "Stop",
600
- Notification: "Notification"
1343
+ Notification: "Notification",
1344
+ SessionStart: "SessionStart",
1345
+ SessionEnd: "SessionEnd",
1346
+ UserPromptSubmit: null
601
1347
  },
602
1348
  gemini: {
603
1349
  PreToolUse: "BeforeTool",
604
1350
  PostToolUse: "AfterTool",
605
1351
  Stop: "AfterAgent",
606
- Notification: "Notification"
1352
+ Notification: "Notification",
1353
+ SessionStart: null,
1354
+ SessionEnd: null,
1355
+ UserPromptSubmit: null
1356
+ },
1357
+ codewith: {
1358
+ PreToolUse: "PreToolUse",
1359
+ PostToolUse: "PostToolUse",
1360
+ Stop: "Stop",
1361
+ Notification: null,
1362
+ SessionStart: "SessionStart",
1363
+ SessionEnd: null,
1364
+ UserPromptSubmit: "UserPromptSubmit"
607
1365
  }
608
1366
  };
609
1367
  function getTargetSettingsDir(target) {
1368
+ if (target === "codewith")
1369
+ return ".codewith";
610
1370
  if (target === "gemini")
611
1371
  return ".gemini";
612
1372
  return ".claude";
613
1373
  }
614
- function getSettingsPath(scope = "global", target = "claude") {
1374
+ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
1375
+ if (target === "codewith" && codewithConfigPath)
1376
+ return codewithConfigPath;
1377
+ if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
1378
+ return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
1379
+ }
615
1380
  const dir = getTargetSettingsDir(target);
616
1381
  if (scope === "project") {
617
- return join(process.cwd(), dir, "settings.json");
1382
+ return target === "codewith" ? join3(process.cwd(), dir, "config.toml") : join3(process.cwd(), dir, "settings.json");
618
1383
  }
619
- return join(homedir(), dir, "settings.json");
1384
+ return target === "codewith" ? join3(homedir3(), dir, "config.toml") : join3(homedir3(), dir, "settings.json");
620
1385
  }
621
1386
  function getHookPath(name) {
622
- return join(HOOKS_DIR, normalizeHookName(name));
1387
+ const shortName = shortHookName(name);
1388
+ const direct = join3(HOOKS_DIR, shortName);
1389
+ if (existsSync3(direct))
1390
+ return direct;
1391
+ return join3(HOOKS_DIR, normalizeHookName(shortName));
623
1392
  }
624
1393
  function hookExists(name) {
625
- return existsSync(getHookPath(name));
1394
+ return existsSync3(getHookPath(name));
626
1395
  }
627
1396
  function readSettings(scope = "global", target = "claude") {
628
1397
  const path = getSettingsPath(scope, target);
629
1398
  try {
630
- if (existsSync(path)) {
631
- return JSON.parse(readFileSync(path, "utf-8"));
1399
+ if (existsSync3(path)) {
1400
+ return JSON.parse(readFileSync2(path, "utf-8"));
632
1401
  }
633
1402
  } catch (error) {
634
1403
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -638,14 +1407,101 @@ function readSettings(scope = "global", target = "claude") {
638
1407
  function writeSettings(settings, scope = "global", target = "claude") {
639
1408
  const path = getSettingsPath(scope, target);
640
1409
  const dir = dirname(path);
641
- if (!existsSync(dir)) {
642
- mkdirSync(dir, { recursive: true });
1410
+ if (!existsSync3(dir)) {
1411
+ mkdirSync2(dir, { recursive: true });
643
1412
  }
644
1413
  writeFileSync(path, JSON.stringify(settings, null, 2) + `
645
1414
  `);
646
1415
  }
647
1416
  function getTargetEventName(internalEvent, target) {
648
- return EVENT_MAP[target]?.[internalEvent] || internalEvent;
1417
+ return EVENT_MAP[target]?.[internalEvent] ?? null;
1418
+ }
1419
+ function isEventSupported(internalEvent, target) {
1420
+ return getTargetEventName(internalEvent, target) !== null;
1421
+ }
1422
+ function tomlString(value) {
1423
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
1424
+ }
1425
+ function codewithMatcher(matcher) {
1426
+ if (!matcher)
1427
+ return;
1428
+ if (matcher.startsWith("^"))
1429
+ return matcher;
1430
+ if (matcher.includes("|"))
1431
+ return `^(${matcher})$`;
1432
+ return `^${matcher}$`;
1433
+ }
1434
+ function codewithTimeout(name) {
1435
+ switch (name) {
1436
+ case "session-start":
1437
+ return 8;
1438
+ case "pre-bash":
1439
+ return 20;
1440
+ case "prompt-guard":
1441
+ return 3;
1442
+ case "worktree-guard":
1443
+ case "stop-sync":
1444
+ return 5;
1445
+ default:
1446
+ return 10;
1447
+ }
1448
+ }
1449
+ function codewithStatusMessage(name) {
1450
+ switch (name) {
1451
+ case "session-start":
1452
+ return "Checking Hasna session context";
1453
+ case "pre-bash":
1454
+ return "Checking Bash safety";
1455
+ case "prompt-guard":
1456
+ return "Checking prompt safety";
1457
+ case "worktree-guard":
1458
+ return "Checking worktree safety";
1459
+ case "stop-sync":
1460
+ return "Syncing turn-end heartbeat";
1461
+ default:
1462
+ return `Running ${name}`;
1463
+ }
1464
+ }
1465
+ function buildCodewithTomlFragment(name, profile) {
1466
+ const shortName = shortHookName(name);
1467
+ const meta = getHook(shortName);
1468
+ if (!meta)
1469
+ throw new Error(`Hook '${shortName}' not found`);
1470
+ const eventKey = getTargetEventName(meta.event, "codewith");
1471
+ if (!eventKey) {
1472
+ throw new Error(`Hook '${shortName}' uses event '${meta.event}', which is not supported by the Codewith target`);
1473
+ }
1474
+ const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
1475
+ const matcher = codewithMatcher(meta.matcher);
1476
+ const lines = [
1477
+ `[[hooks.${eventKey}]]`
1478
+ ];
1479
+ if (matcher)
1480
+ lines.push(`matcher = ${tomlString(matcher)}`);
1481
+ lines.push("", `[[hooks.${eventKey}.hooks]]`, `type = "command"`, `command = ${tomlString(command)}`, `timeout = ${codewithTimeout(shortName)}`, `statusMessage = ${tomlString(codewithStatusMessage(shortName))}`);
1482
+ return `${lines.join(`
1483
+ `)}
1484
+ `;
1485
+ }
1486
+ function readCodewithConfig(scope, configPath) {
1487
+ const path = getSettingsPath(scope, "codewith", configPath);
1488
+ try {
1489
+ return existsSync3(path) ? readFileSync2(path, "utf-8") : "";
1490
+ } catch {
1491
+ return "";
1492
+ }
1493
+ }
1494
+ function appendCodewithFragment(fragment, scope, configPath) {
1495
+ const path = getSettingsPath(scope, "codewith", configPath);
1496
+ const dir = dirname(path);
1497
+ if (!existsSync3(dir))
1498
+ mkdirSync2(dir, { recursive: true });
1499
+ const existing = existsSync3(path) ? readFileSync2(path, "utf-8") : "";
1500
+ const sep = existing.trim() ? `
1501
+
1502
+ ` : "";
1503
+ writeFileSync(path, `${existing.replace(/\s*$/, "")}${sep}${fragment}`);
1504
+ return path;
649
1505
  }
650
1506
  function detectConflict(name, scope, target) {
651
1507
  const meta = getHook(name);
@@ -666,11 +1522,60 @@ function detectConflict(name, scope, target) {
666
1522
  }
667
1523
  return;
668
1524
  }
669
- function installForTarget(name, scope, overwrite, target, profile) {
1525
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
670
1526
  const shortName = shortHookName(name);
671
1527
  if (!hookExists(shortName)) {
672
1528
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
673
1529
  }
1530
+ if (target === "codewith") {
1531
+ try {
1532
+ const fragment = buildCodewithTomlFragment(shortName, profile);
1533
+ if (codewithMode !== "write") {
1534
+ return {
1535
+ hook: shortName,
1536
+ success: true,
1537
+ scope,
1538
+ target,
1539
+ fragment,
1540
+ applied: false,
1541
+ note: "Codewith install is fragment-only by default; open-configs should own applying this TOML."
1542
+ };
1543
+ }
1544
+ if (!codewithConfigPath) {
1545
+ return {
1546
+ hook: shortName,
1547
+ success: false,
1548
+ error: "Direct Codewith writes require an explicit --codewith-config path; refusing to write default ~/.codewith/config.toml.",
1549
+ scope,
1550
+ target,
1551
+ fragment,
1552
+ applied: false
1553
+ };
1554
+ }
1555
+ const existing = readCodewithConfig(scope, codewithConfigPath);
1556
+ if (!overwrite && new RegExp(`command\\s*=\\s*["']hooks run ${shortName}(?:\\s|["'])`).test(existing)) {
1557
+ return { hook: shortName, success: false, error: "Already installed. Use --overwrite to append another fragment.", scope, target };
1558
+ }
1559
+ const path = appendCodewithFragment(fragment, scope, codewithConfigPath);
1560
+ return {
1561
+ hook: shortName,
1562
+ success: true,
1563
+ scope,
1564
+ target,
1565
+ fragment,
1566
+ applied: true,
1567
+ configPath: path,
1568
+ note: "Direct Codewith config write was explicitly requested; prefer open-configs for managed machines."
1569
+ };
1570
+ } catch (error) {
1571
+ return {
1572
+ hook: shortName,
1573
+ success: false,
1574
+ error: error instanceof Error ? error.message : "Unknown error",
1575
+ target
1576
+ };
1577
+ }
1578
+ }
674
1579
  const registered = getRegisteredHooksForTarget(scope, target);
675
1580
  if (registered.includes(shortName) && !overwrite) {
676
1581
  return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
@@ -689,25 +1594,63 @@ function installForTarget(name, scope, overwrite, target, profile) {
689
1594
  }
690
1595
  }
691
1596
  function installHook(name, options = {}) {
692
- const { scope = "global", overwrite = false, target = "claude", profile } = options;
1597
+ const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
693
1598
  if (target === "all") {
1599
+ const shortName = shortHookName(name);
1600
+ const meta = getHook(shortName);
1601
+ if (meta) {
1602
+ const unsupportedTargets = ["claude", "gemini", "codewith"].filter((agentTarget) => !isEventSupported(meta.event, agentTarget));
1603
+ if (unsupportedTargets.length > 0) {
1604
+ return {
1605
+ hook: shortName,
1606
+ success: false,
1607
+ error: `Event '${meta.event}' is not supported by target(s): ${unsupportedTargets.join(", ")}`,
1608
+ target: "all"
1609
+ };
1610
+ }
1611
+ }
694
1612
  const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
695
- installForTarget(name, scope, overwrite, "gemini", profile);
1613
+ if (!claudeResult.success) {
1614
+ return {
1615
+ ...claudeResult,
1616
+ error: `Failed for target 'claude': ${claudeResult.error}`,
1617
+ target: "all"
1618
+ };
1619
+ }
1620
+ const geminiResult = installForTarget(name, scope, overwrite, "gemini", profile);
1621
+ if (!geminiResult.success) {
1622
+ return {
1623
+ ...geminiResult,
1624
+ error: `Failed for target 'gemini': ${geminiResult.error}`,
1625
+ target: "all"
1626
+ };
1627
+ }
1628
+ const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
1629
+ if (!codewithResult.success) {
1630
+ return {
1631
+ ...codewithResult,
1632
+ error: `Failed for target 'codewith': ${codewithResult.error}`,
1633
+ target: "all"
1634
+ };
1635
+ }
696
1636
  return { ...claudeResult, target: "all" };
697
1637
  }
698
- return installForTarget(name, scope, overwrite, target, profile);
1638
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
699
1639
  }
700
1640
  function registerHook(name, scope = "global", target = "claude", profile) {
701
1641
  const meta = getHook(name);
702
1642
  if (!meta)
703
1643
  return;
1644
+ const eventKey = getTargetEventName(meta.event, target);
1645
+ if (eventKey === null) {
1646
+ throw new Error(`Event '${meta.event}' is not supported by target '${target}'`);
1647
+ }
704
1648
  const settings = readSettings(scope, target);
705
1649
  if (!settings.hooks)
706
1650
  settings.hooks = {};
707
- const eventKey = getTargetEventName(meta.event, target);
1651
+ removeHookFromAllEvents(settings, name);
708
1652
  if (!settings.hooks[eventKey])
709
1653
  settings.hooks[eventKey] = [];
710
- settings.hooks[eventKey] = removeHookEntriesByName(settings.hooks[eventKey], name);
711
1654
  const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
712
1655
  const entry = {
713
1656
  hooks: [{ type: "command", command: hookCommand }]
@@ -718,6 +1661,16 @@ function registerHook(name, scope = "global", target = "claude", profile) {
718
1661
  settings.hooks[eventKey].push(entry);
719
1662
  writeSettings(settings, scope, target);
720
1663
  }
1664
+ function removeHookFromAllEvents(settings, name) {
1665
+ if (!settings.hooks)
1666
+ return;
1667
+ for (const key of Object.keys(settings.hooks)) {
1668
+ settings.hooks[key] = removeHookEntriesByName(settings.hooks[key], name);
1669
+ if (settings.hooks[key].length === 0) {
1670
+ delete settings.hooks[key];
1671
+ }
1672
+ }
1673
+ }
721
1674
  function unregisterHook(name, scope = "global", target = "claude") {
722
1675
  const meta = getHook(name);
723
1676
  if (!meta)
@@ -725,13 +1678,7 @@ function unregisterHook(name, scope = "global", target = "claude") {
725
1678
  const settings = readSettings(scope, target);
726
1679
  if (!settings.hooks)
727
1680
  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
- }
1681
+ removeHookFromAllEvents(settings, name);
735
1682
  if (Object.keys(settings.hooks).length === 0) {
736
1683
  delete settings.hooks;
737
1684
  }
@@ -741,6 +1688,16 @@ function installHooks(names, options = {}) {
741
1688
  return names.map((name) => installHook(name, options));
742
1689
  }
743
1690
  function getRegisteredHooksForTarget(scope = "global", target = "claude") {
1691
+ if (target === "codewith") {
1692
+ const config = readCodewithConfig(scope);
1693
+ const registered2 = [];
1694
+ const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
1695
+ let match;
1696
+ while (match = re.exec(config)) {
1697
+ registered2.push(match[1]);
1698
+ }
1699
+ return [...new Set(registered2)];
1700
+ }
744
1701
  const settings = readSettings(scope, target);
745
1702
  if (!settings.hooks)
746
1703
  return [];
@@ -748,8 +1705,8 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
748
1705
  for (const eventKey of Object.keys(settings.hooks)) {
749
1706
  for (const entry of settings.hooks[eventKey]) {
750
1707
  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+)$/);
1708
+ const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
1709
+ const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
753
1710
  const match = newMatch || oldMatch;
754
1711
  if (match) {
755
1712
  registered.push(match[1]);
@@ -770,6 +1727,9 @@ function removeHook(name, scope = "global", target = "claude") {
770
1727
  const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
771
1728
  return claudeRemoved || geminiRemoved;
772
1729
  }
1730
+ if (target === "codewith") {
1731
+ return false;
1732
+ }
773
1733
  return removeHookForTarget(shortName, scope, target);
774
1734
  }
775
1735
  function removeHookForTarget(name, scope, target) {
@@ -782,8 +1742,8 @@ function removeHookForTarget(name, scope, target) {
782
1742
  }
783
1743
  // src/index.ts
784
1744
  init_profiles();
785
- import { join as join3 } from "path";
786
- import { existsSync as existsSync3 } from "fs";
1745
+ import { join as join5 } from "path";
1746
+ import { existsSync as existsSync5 } from "fs";
787
1747
  function installHookForProject(name, options = {}) {
788
1748
  return installHook(name, { ...options, scope: "project" });
789
1749
  }
@@ -801,8 +1761,8 @@ async function runHook(name, input, options = {}) {
801
1761
  if (!meta)
802
1762
  throw new Error(`Hook '${name}' not found`);
803
1763
  const hookDir = getHookPath(name);
804
- const hookScript = join3(hookDir, "src", "hook.ts");
805
- if (!existsSync3(hookScript))
1764
+ const hookScript = join5(hookDir, "src", "hook.ts");
1765
+ if (!existsSync5(hookScript))
806
1766
  throw new Error(`Hook script not found: ${hookScript}`);
807
1767
  let hookInput = { ...input };
808
1768
  if (options.profile) {
@@ -839,18 +1799,32 @@ async function runHook(name, input, options = {}) {
839
1799
  export {
840
1800
  updateProfile,
841
1801
  touchProfile,
1802
+ storageSync,
1803
+ storagePush,
1804
+ storagePull,
842
1805
  searchHooks,
1806
+ runStorageMigrations,
843
1807
  runHook,
1808
+ resolveTables,
844
1809
  removeProjectHook,
845
1810
  removeHook,
1811
+ parseStorageTables,
846
1812
  listProjectHooks,
847
1813
  listProfiles,
1814
+ isEventSupported,
848
1815
  installHooksForProject,
849
1816
  installHooks,
850
1817
  installHookForProject,
851
1818
  installHook,
852
1819
  importProfiles,
853
1820
  hookExists,
1821
+ getSyncMetaAll,
1822
+ getStorageStatus,
1823
+ getStoragePg,
1824
+ getStorageMode,
1825
+ getStorageDatabaseUrl,
1826
+ getStorageDatabaseEnvName,
1827
+ getStorageDatabaseEnv,
854
1828
  getSettingsPath,
855
1829
  getRegisteredHooksForTarget,
856
1830
  getRegisteredHooks,
@@ -863,6 +1837,16 @@ export {
863
1837
  exportProfiles,
864
1838
  deleteProfile,
865
1839
  createProfile,
1840
+ buildCodewithTomlFragment,
1841
+ STORAGE_TABLES,
1842
+ STORAGE_MODE_ENV,
1843
+ STORAGE_DATABASE_ENV,
1844
+ HOOK_EVENTS,
1845
+ HOOKS_STORAGE_TABLES,
1846
+ HOOKS_STORAGE_MODE_FALLBACK_ENV,
1847
+ HOOKS_STORAGE_MODE_ENV,
1848
+ HOOKS_STORAGE_FALLBACK_ENV,
1849
+ HOOKS_STORAGE_ENV,
866
1850
  HOOKS,
867
1851
  CATEGORIES
868
1852
  };