@hasna/hooks 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/index.js +2154 -1400
- package/bin/serve.js +34 -4
- package/dist/db/migrations/005_subagent_start_event.d.ts +10 -0
- package/dist/db/schema.d.ts +2 -2
- package/dist/index.d.ts +9 -5
- package/dist/index.js +683 -195
- package/dist/lib/db-writer.d.ts +40 -0
- package/dist/lib/installer.d.ts +37 -0
- package/dist/lib/resolve.d.ts +2 -0
- package/dist/lib/run.d.ts +8 -0
- package/dist/lib/store.d.ts +15 -0
- package/dist/lib/sync.d.ts +19 -0
- package/dist/storage.js +34 -2
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -4354,7 +4354,8 @@ function resolveHook(name) {
|
|
|
4354
4354
|
events: custom.manifest.events,
|
|
4355
4355
|
description: custom.manifest.description ?? "Custom hook",
|
|
4356
4356
|
scriptPath: custom.scriptPath,
|
|
4357
|
-
meta: meta2
|
|
4357
|
+
meta: meta2,
|
|
4358
|
+
timeoutMs: custom.manifest.timeout_ms ?? null
|
|
4358
4359
|
};
|
|
4359
4360
|
}
|
|
4360
4361
|
const meta = getHook(shortManifestName(name));
|
|
@@ -4368,7 +4369,8 @@ function resolveHook(name) {
|
|
|
4368
4369
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
4369
4370
|
description: meta.description,
|
|
4370
4371
|
scriptPath,
|
|
4371
|
-
meta
|
|
4372
|
+
meta,
|
|
4373
|
+
timeoutMs: null
|
|
4372
4374
|
};
|
|
4373
4375
|
}
|
|
4374
4376
|
var init_resolve = __esm(() => {
|
|
@@ -4376,1157 +4378,1549 @@ var init_resolve = __esm(() => {
|
|
|
4376
4378
|
init_manifest();
|
|
4377
4379
|
});
|
|
4378
4380
|
|
|
4379
|
-
// src/
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4381
|
+
// src/db/schema.ts
|
|
4382
|
+
var CREATE_HOOK_EVENTS_TABLE = `
|
|
4383
|
+
CREATE TABLE IF NOT EXISTS hook_events (
|
|
4384
|
+
id TEXT PRIMARY KEY,
|
|
4385
|
+
timestamp TEXT NOT NULL,
|
|
4386
|
+
session_id TEXT NOT NULL,
|
|
4387
|
+
hook_name TEXT NOT NULL,
|
|
4388
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
|
|
4389
|
+
tool_name TEXT,
|
|
4390
|
+
tool_input TEXT,
|
|
4391
|
+
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
4392
|
+
error TEXT,
|
|
4393
|
+
duration_ms INTEGER,
|
|
4394
|
+
project_dir TEXT,
|
|
4395
|
+
metadata TEXT
|
|
4396
|
+
)
|
|
4397
|
+
`, CREATE_INDEXES;
|
|
4398
|
+
var init_schema = __esm(() => {
|
|
4399
|
+
CREATE_INDEXES = [
|
|
4400
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
|
|
4401
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
|
|
4402
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
|
|
4403
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
|
|
4404
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`
|
|
4405
|
+
];
|
|
4406
|
+
});
|
|
4407
|
+
|
|
4408
|
+
// src/db/migrations/001_initial.ts
|
|
4409
|
+
function up(db) {
|
|
4410
|
+
db.exec(CREATE_HOOK_EVENTS_TABLE);
|
|
4411
|
+
for (const idx of CREATE_INDEXES) {
|
|
4412
|
+
db.exec(idx);
|
|
4413
|
+
}
|
|
4389
4414
|
}
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4415
|
+
var init_001_initial = __esm(() => {
|
|
4416
|
+
init_schema();
|
|
4417
|
+
});
|
|
4418
|
+
|
|
4419
|
+
// src/db/migrations/002_session_events.ts
|
|
4420
|
+
function up2(db) {
|
|
4421
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
|
|
4422
|
+
if (!row?.sql)
|
|
4423
|
+
return;
|
|
4424
|
+
if (row.sql.includes("SessionStart"))
|
|
4425
|
+
return;
|
|
4426
|
+
db.exec("BEGIN");
|
|
4427
|
+
try {
|
|
4428
|
+
db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
|
|
4429
|
+
db.exec(CREATE_HOOK_EVENTS_TABLE);
|
|
4430
|
+
db.exec(`INSERT INTO hook_events
|
|
4431
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
4432
|
+
SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
|
|
4433
|
+
FROM hook_events_old`);
|
|
4434
|
+
db.exec("DROP TABLE hook_events_old");
|
|
4435
|
+
for (const idx of CREATE_INDEXES) {
|
|
4436
|
+
db.exec(idx);
|
|
4437
|
+
}
|
|
4438
|
+
db.exec("COMMIT");
|
|
4439
|
+
} catch (err) {
|
|
4440
|
+
db.exec("ROLLBACK");
|
|
4441
|
+
throw err;
|
|
4442
|
+
}
|
|
4395
4443
|
}
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4444
|
+
var init_002_session_events = __esm(() => {
|
|
4445
|
+
init_schema();
|
|
4446
|
+
});
|
|
4447
|
+
|
|
4448
|
+
// src/db/migrations/003_user_prompt_submit_event.ts
|
|
4449
|
+
function up3(db) {
|
|
4450
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
|
|
4451
|
+
if (!row?.sql)
|
|
4452
|
+
return;
|
|
4453
|
+
if (row.sql.includes("UserPromptSubmit"))
|
|
4454
|
+
return;
|
|
4455
|
+
db.exec("BEGIN");
|
|
4456
|
+
try {
|
|
4457
|
+
db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
|
|
4458
|
+
db.exec(CREATE_HOOK_EVENTS_TABLE);
|
|
4459
|
+
db.exec(`INSERT INTO hook_events
|
|
4460
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
4461
|
+
SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
|
|
4462
|
+
FROM hook_events_old`);
|
|
4463
|
+
db.exec("DROP TABLE hook_events_old");
|
|
4464
|
+
for (const idx of CREATE_INDEXES) {
|
|
4465
|
+
db.exec(idx);
|
|
4466
|
+
}
|
|
4467
|
+
db.exec("COMMIT");
|
|
4468
|
+
} catch (err) {
|
|
4469
|
+
db.exec("ROLLBACK");
|
|
4470
|
+
throw err;
|
|
4471
|
+
}
|
|
4402
4472
|
}
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4473
|
+
var init_003_user_prompt_submit_event = __esm(() => {
|
|
4474
|
+
init_schema();
|
|
4475
|
+
});
|
|
4476
|
+
|
|
4477
|
+
// src/db/migrations/004_hooks_table.ts
|
|
4478
|
+
function up4(db) {
|
|
4479
|
+
db.exec(CREATE_HOOKS_TABLE);
|
|
4480
|
+
for (const idx of CREATE_HOOKS_INDEXES) {
|
|
4481
|
+
db.exec(idx);
|
|
4482
|
+
}
|
|
4409
4483
|
}
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4484
|
+
var CREATE_HOOKS_TABLE = `
|
|
4485
|
+
CREATE TABLE IF NOT EXISTS hooks (
|
|
4486
|
+
id TEXT PRIMARY KEY,
|
|
4487
|
+
name TEXT NOT NULL,
|
|
4488
|
+
version TEXT NOT NULL,
|
|
4489
|
+
sha256 TEXT NOT NULL,
|
|
4490
|
+
source_type TEXT NOT NULL,
|
|
4491
|
+
source_ref TEXT,
|
|
4492
|
+
installed_at TEXT NOT NULL,
|
|
4493
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
4494
|
+
last_verified_at TEXT
|
|
4495
|
+
)
|
|
4496
|
+
`, CREATE_HOOKS_INDEXES;
|
|
4497
|
+
var init_004_hooks_table = __esm(() => {
|
|
4498
|
+
CREATE_HOOKS_INDEXES = [
|
|
4499
|
+
`CREATE INDEX IF NOT EXISTS idx_hooks_name ON hooks (name)`
|
|
4500
|
+
];
|
|
4501
|
+
});
|
|
4502
|
+
|
|
4503
|
+
// src/db/migrations/005_subagent_start_event.ts
|
|
4504
|
+
function up5(db) {
|
|
4505
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
|
|
4506
|
+
if (!row?.sql)
|
|
4507
|
+
return;
|
|
4508
|
+
if (row.sql.includes("SubagentStart"))
|
|
4509
|
+
return;
|
|
4510
|
+
db.exec("BEGIN");
|
|
4511
|
+
try {
|
|
4512
|
+
db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
|
|
4513
|
+
db.exec(CREATE_HOOK_EVENTS_TABLE);
|
|
4514
|
+
db.exec(`INSERT INTO hook_events
|
|
4515
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
4516
|
+
SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
|
|
4517
|
+
FROM hook_events_old`);
|
|
4518
|
+
db.exec("DROP TABLE hook_events_old");
|
|
4519
|
+
for (const idx of CREATE_INDEXES) {
|
|
4520
|
+
db.exec(idx);
|
|
4521
|
+
}
|
|
4522
|
+
db.exec("COMMIT");
|
|
4523
|
+
} catch (err) {
|
|
4524
|
+
db.exec("ROLLBACK");
|
|
4525
|
+
throw err;
|
|
4415
4526
|
}
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4527
|
+
}
|
|
4528
|
+
var init_005_subagent_start_event = __esm(() => {
|
|
4529
|
+
init_schema();
|
|
4530
|
+
});
|
|
4531
|
+
|
|
4532
|
+
// src/db/migrations/index.ts
|
|
4533
|
+
function ensureMigrationsTable(db) {
|
|
4534
|
+
db.exec(`
|
|
4535
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
4536
|
+
version TEXT PRIMARY KEY,
|
|
4537
|
+
applied_at TEXT NOT NULL
|
|
4538
|
+
)
|
|
4539
|
+
`);
|
|
4540
|
+
}
|
|
4541
|
+
function getApplied(db) {
|
|
4542
|
+
const rows = db.query("SELECT version FROM schema_migrations").all();
|
|
4543
|
+
return new Set(rows.map((r) => r.version));
|
|
4544
|
+
}
|
|
4545
|
+
function runMigrations(db) {
|
|
4546
|
+
ensureMigrationsTable(db);
|
|
4547
|
+
const applied = getApplied(db);
|
|
4548
|
+
for (const migration of MIGRATIONS) {
|
|
4549
|
+
if (applied.has(migration.version))
|
|
4550
|
+
continue;
|
|
4551
|
+
migration.up(db);
|
|
4552
|
+
db.run("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", [
|
|
4553
|
+
migration.version,
|
|
4554
|
+
new Date().toISOString()
|
|
4555
|
+
]);
|
|
4422
4556
|
}
|
|
4423
|
-
return target === "codewith" ? join5(homedir3(), dir, "config.toml") : join5(homedir3(), dir, "settings.json");
|
|
4424
4557
|
}
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4558
|
+
var MIGRATIONS;
|
|
4559
|
+
var init_migrations = __esm(() => {
|
|
4560
|
+
init_001_initial();
|
|
4561
|
+
init_002_session_events();
|
|
4562
|
+
init_003_user_prompt_submit_event();
|
|
4563
|
+
init_004_hooks_table();
|
|
4564
|
+
init_005_subagent_start_event();
|
|
4565
|
+
MIGRATIONS = [
|
|
4566
|
+
{ version: "001_initial", up },
|
|
4567
|
+
{ version: "002_session_events", up: up2 },
|
|
4568
|
+
{ version: "003_user_prompt_submit_event", up: up3 },
|
|
4569
|
+
{ version: "004_hooks_table", up: up4 },
|
|
4570
|
+
{ version: "005_subagent_start_event", up: up5 }
|
|
4571
|
+
];
|
|
4572
|
+
});
|
|
4573
|
+
|
|
4574
|
+
// src/db/legacy-import.ts
|
|
4575
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
4576
|
+
import { join as join5 } from "path";
|
|
4577
|
+
import { homedir as homedir3 } from "os";
|
|
4578
|
+
function ensureMetaTable(db) {
|
|
4579
|
+
db.exec(`
|
|
4580
|
+
CREATE TABLE IF NOT EXISTS _meta (
|
|
4581
|
+
key TEXT PRIMARY KEY,
|
|
4582
|
+
value TEXT NOT NULL
|
|
4583
|
+
)
|
|
4584
|
+
`);
|
|
4431
4585
|
}
|
|
4432
|
-
function
|
|
4433
|
-
|
|
4586
|
+
function isAlreadyDone(db) {
|
|
4587
|
+
ensureMetaTable(db);
|
|
4588
|
+
const row = db.query("SELECT value FROM _meta WHERE key = ?").get(META_KEY);
|
|
4589
|
+
return row?.value === "1";
|
|
4434
4590
|
}
|
|
4435
|
-
function
|
|
4436
|
-
|
|
4591
|
+
function markDone(db) {
|
|
4592
|
+
db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", [META_KEY, "1"]);
|
|
4593
|
+
}
|
|
4594
|
+
function nanoid() {
|
|
4595
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
|
|
4596
|
+
}
|
|
4597
|
+
function importJsonlFile(db, filePath) {
|
|
4598
|
+
let count = 0;
|
|
4599
|
+
try {
|
|
4600
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4601
|
+
`).filter(Boolean);
|
|
4602
|
+
for (const line of lines) {
|
|
4603
|
+
try {
|
|
4604
|
+
const entry = JSON.parse(line);
|
|
4605
|
+
db.run(`INSERT OR IGNORE INTO hook_events
|
|
4606
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, project_dir)
|
|
4607
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4608
|
+
nanoid(),
|
|
4609
|
+
entry.timestamp ?? new Date().toISOString(),
|
|
4610
|
+
entry.session_id ?? "legacy",
|
|
4611
|
+
"sessionlog",
|
|
4612
|
+
"PostToolUse",
|
|
4613
|
+
entry.tool_name ?? null,
|
|
4614
|
+
entry.tool_input ? String(entry.tool_input).slice(0, 500) : null,
|
|
4615
|
+
null
|
|
4616
|
+
]);
|
|
4617
|
+
count++;
|
|
4618
|
+
} catch {}
|
|
4619
|
+
}
|
|
4620
|
+
} catch {}
|
|
4621
|
+
return count;
|
|
4622
|
+
}
|
|
4623
|
+
function importErrorsLog(db, filePath) {
|
|
4624
|
+
let count = 0;
|
|
4437
4625
|
try {
|
|
4438
|
-
|
|
4439
|
-
|
|
4626
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4627
|
+
`).filter(Boolean);
|
|
4628
|
+
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
4629
|
+
for (const line of lines) {
|
|
4630
|
+
try {
|
|
4631
|
+
const m = line.match(linePattern);
|
|
4632
|
+
if (!m)
|
|
4633
|
+
continue;
|
|
4634
|
+
const [, timestamp, sessionPrefix, , errorMsg] = m;
|
|
4635
|
+
db.run(`INSERT OR IGNORE INTO hook_events
|
|
4636
|
+
(id, timestamp, session_id, hook_name, event_type, error)
|
|
4637
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
4638
|
+
nanoid(),
|
|
4639
|
+
timestamp,
|
|
4640
|
+
sessionPrefix ? `legacy-${sessionPrefix}` : "legacy",
|
|
4641
|
+
"errornotify",
|
|
4642
|
+
"PostToolUse",
|
|
4643
|
+
errorMsg.slice(0, 500)
|
|
4644
|
+
]);
|
|
4645
|
+
count++;
|
|
4646
|
+
} catch {}
|
|
4440
4647
|
}
|
|
4441
|
-
} catch
|
|
4442
|
-
|
|
4648
|
+
} catch {}
|
|
4649
|
+
return count;
|
|
4650
|
+
}
|
|
4651
|
+
function runLegacyImport(db, homeDir = homedir3()) {
|
|
4652
|
+
try {
|
|
4653
|
+
if (isAlreadyDone(db))
|
|
4654
|
+
return;
|
|
4655
|
+
let total = 0;
|
|
4656
|
+
const claudeProjectsDir = join5(homeDir, ".claude", "projects");
|
|
4657
|
+
if (existsSync5(claudeProjectsDir)) {
|
|
4658
|
+
try {
|
|
4659
|
+
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
4660
|
+
for (const dir of projectDirs) {
|
|
4661
|
+
const projectDir = join5(claudeProjectsDir, dir);
|
|
4662
|
+
try {
|
|
4663
|
+
const files = readdirSync2(projectDir);
|
|
4664
|
+
for (const file of files) {
|
|
4665
|
+
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
4666
|
+
total += importJsonlFile(db, join5(projectDir, file));
|
|
4667
|
+
}
|
|
4668
|
+
if (file === "errors.log") {
|
|
4669
|
+
total += importErrorsLog(db, join5(projectDir, file));
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
} catch {}
|
|
4673
|
+
}
|
|
4674
|
+
} catch {}
|
|
4675
|
+
}
|
|
4676
|
+
markDone(db);
|
|
4677
|
+
if (total > 0) {
|
|
4678
|
+
process.stderr.write(`[hooks] Imported ${total} legacy log entries into SQLite.
|
|
4679
|
+
`);
|
|
4680
|
+
}
|
|
4681
|
+
} catch (err) {
|
|
4682
|
+
process.stderr.write(`[hooks] Legacy import failed (non-fatal): ${err}
|
|
4683
|
+
`);
|
|
4443
4684
|
}
|
|
4444
|
-
return {};
|
|
4445
4685
|
}
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4686
|
+
var META_KEY = "legacy_import_done";
|
|
4687
|
+
var init_legacy_import = () => {};
|
|
4688
|
+
|
|
4689
|
+
// src/db/retention.ts
|
|
4690
|
+
function runRetention(db, days) {
|
|
4691
|
+
const envDays = parseInt(process.env.HOOKS_RETENTION_DAYS ?? "30");
|
|
4692
|
+
const retentionDays = days ?? (isNaN(envDays) || envDays <= 0 ? 30 : envDays);
|
|
4693
|
+
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
|
4694
|
+
try {
|
|
4695
|
+
db.run("DELETE FROM hook_events WHERE timestamp < ?", [cutoff]);
|
|
4696
|
+
const changes = db.query("SELECT changes() as changes").get()?.changes ?? 0;
|
|
4697
|
+
return changes;
|
|
4698
|
+
} catch {
|
|
4699
|
+
return 0;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
// src/db/index.ts
|
|
4704
|
+
var exports_db = {};
|
|
4705
|
+
__export(exports_db, {
|
|
4706
|
+
getDbPath: () => getDbPath,
|
|
4707
|
+
getDb: () => getDb,
|
|
4708
|
+
createTestDb: () => createTestDb,
|
|
4709
|
+
closeDb: () => closeDb
|
|
4710
|
+
});
|
|
4711
|
+
import { Database } from "bun:sqlite";
|
|
4712
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, cpSync } from "fs";
|
|
4713
|
+
import { join as join6 } from "path";
|
|
4714
|
+
import { homedir as homedir4 } from "os";
|
|
4715
|
+
function resolveDataDir() {
|
|
4716
|
+
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
4717
|
+
if (explicit)
|
|
4718
|
+
return explicit;
|
|
4719
|
+
const newDir = join6(homedir4(), ".hasna", "hooks");
|
|
4720
|
+
const oldDir = join6(homedir4(), ".hooks");
|
|
4721
|
+
if (!existsSync6(newDir) && existsSync6(oldDir)) {
|
|
4722
|
+
mkdirSync3(join6(homedir4(), ".hasna"), { recursive: true });
|
|
4723
|
+
cpSync(oldDir, newDir, { recursive: true });
|
|
4724
|
+
}
|
|
4725
|
+
return newDir;
|
|
4726
|
+
}
|
|
4727
|
+
function getDbPath() {
|
|
4728
|
+
const explicitDb = process.env.HASNA_HOOKS_DB_PATH ?? process.env.HOOKS_DB_PATH;
|
|
4729
|
+
if (explicitDb)
|
|
4730
|
+
return explicitDb;
|
|
4731
|
+
const dataDir = resolveDataDir();
|
|
4732
|
+
return join6(dataDir, "hooks.db");
|
|
4733
|
+
}
|
|
4734
|
+
function ensureDir(dbPath) {
|
|
4735
|
+
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
4736
|
+
if (dir && !existsSync6(dir)) {
|
|
4450
4737
|
mkdirSync3(dir, { recursive: true });
|
|
4451
4738
|
}
|
|
4452
|
-
writeFileSync3(path, JSON.stringify(settings, null, 2) + `
|
|
4453
|
-
`);
|
|
4454
4739
|
}
|
|
4455
|
-
function
|
|
4456
|
-
|
|
4740
|
+
function getDb() {
|
|
4741
|
+
if (instance)
|
|
4742
|
+
return instance;
|
|
4743
|
+
const dbPath = getDbPath();
|
|
4744
|
+
const isNew = dbPath === ":memory:" || !existsSync6(dbPath);
|
|
4745
|
+
ensureDir(dbPath);
|
|
4746
|
+
instance = new Database(dbPath);
|
|
4747
|
+
instance.exec("PRAGMA busy_timeout=5000");
|
|
4748
|
+
instance.exec("PRAGMA journal_mode=WAL");
|
|
4749
|
+
instance.exec("PRAGMA foreign_keys=ON");
|
|
4750
|
+
runMigrations(instance);
|
|
4751
|
+
runRetention(instance);
|
|
4752
|
+
instance.exec(`CREATE TABLE IF NOT EXISTS feedback (
|
|
4753
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
4754
|
+
message TEXT NOT NULL,
|
|
4755
|
+
email TEXT,
|
|
4756
|
+
category TEXT DEFAULT 'general',
|
|
4757
|
+
version TEXT,
|
|
4758
|
+
machine_id TEXT,
|
|
4759
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4760
|
+
)`);
|
|
4761
|
+
if (isNew) {
|
|
4762
|
+
runLegacyImport(instance);
|
|
4763
|
+
}
|
|
4764
|
+
return instance;
|
|
4765
|
+
}
|
|
4766
|
+
function closeDb() {
|
|
4767
|
+
if (instance) {
|
|
4768
|
+
instance.close();
|
|
4769
|
+
instance = null;
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
function createTestDb() {
|
|
4773
|
+
const db = new Database(":memory:");
|
|
4774
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
4775
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
4776
|
+
return db;
|
|
4457
4777
|
}
|
|
4458
|
-
|
|
4459
|
-
|
|
4778
|
+
var instance = null;
|
|
4779
|
+
var init_db = __esm(() => {
|
|
4780
|
+
init_migrations();
|
|
4781
|
+
init_legacy_import();
|
|
4782
|
+
});
|
|
4783
|
+
|
|
4784
|
+
// src/lib/store.ts
|
|
4785
|
+
var exports_store = {};
|
|
4786
|
+
__export(exports_store, {
|
|
4787
|
+
writeLock: () => writeLock,
|
|
4788
|
+
verifyScriptHash: () => verifyScriptHash,
|
|
4789
|
+
upsertHookRecord: () => upsertHookRecord,
|
|
4790
|
+
sha256Of: () => sha256Of,
|
|
4791
|
+
sha256File: () => sha256File,
|
|
4792
|
+
setPinnedHook: () => setPinnedHook,
|
|
4793
|
+
retrustHook: () => retrustHook,
|
|
4794
|
+
removePinnedHook: () => removePinnedHook,
|
|
4795
|
+
removeHookRecord: () => removeHookRecord,
|
|
4796
|
+
removeHookFromStore: () => removeHookFromStore,
|
|
4797
|
+
readLock: () => readLock,
|
|
4798
|
+
pinInstalledHook: () => pinInstalledHook,
|
|
4799
|
+
listHookRecords: () => listHookRecords,
|
|
4800
|
+
getPinnedHook: () => getPinnedHook,
|
|
4801
|
+
getHookRecord: () => getHookRecord,
|
|
4802
|
+
checkScriptHash: () => checkScriptHash
|
|
4803
|
+
});
|
|
4804
|
+
import { createHash } from "crypto";
|
|
4805
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4806
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
4807
|
+
function sha256Of(input) {
|
|
4808
|
+
return createHash("sha256").update(input).digest("hex");
|
|
4460
4809
|
}
|
|
4461
|
-
function
|
|
4462
|
-
|
|
4810
|
+
async function sha256File(path) {
|
|
4811
|
+
const buf = await readFile2(path);
|
|
4812
|
+
return sha256Of(buf);
|
|
4463
4813
|
}
|
|
4464
|
-
function
|
|
4465
|
-
|
|
4466
|
-
return;
|
|
4467
|
-
if (matcher.startsWith("^"))
|
|
4468
|
-
return matcher;
|
|
4469
|
-
if (matcher.includes("|"))
|
|
4470
|
-
return `^(${matcher})$`;
|
|
4471
|
-
return `^${matcher}$`;
|
|
4814
|
+
function getHookRecord(db, name) {
|
|
4815
|
+
return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
|
|
4472
4816
|
}
|
|
4473
|
-
function
|
|
4474
|
-
|
|
4475
|
-
case "knowledge-context":
|
|
4476
|
-
return 6;
|
|
4477
|
-
case "session-start":
|
|
4478
|
-
return 8;
|
|
4479
|
-
case "pre-bash":
|
|
4480
|
-
return 20;
|
|
4481
|
-
case "prompt-guard":
|
|
4482
|
-
return 3;
|
|
4483
|
-
case "worktree-guard":
|
|
4484
|
-
case "stop-sync":
|
|
4485
|
-
return 5;
|
|
4486
|
-
default:
|
|
4487
|
-
return 10;
|
|
4488
|
-
}
|
|
4817
|
+
function listHookRecords(db) {
|
|
4818
|
+
return db.query("SELECT * FROM hooks ORDER BY name").all();
|
|
4489
4819
|
}
|
|
4490
|
-
function
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4820
|
+
function upsertHookRecord(db, record) {
|
|
4821
|
+
const existing = getHookRecord(db, record.name);
|
|
4822
|
+
const id = existing?.id ?? record.name;
|
|
4823
|
+
const installedAt = existing?.installed_at ?? new Date().toISOString();
|
|
4824
|
+
const enabled = record.enabled ?? existing?.enabled ?? 1;
|
|
4825
|
+
db.run(`INSERT INTO hooks (id, name, version, sha256, source_type, source_ref, installed_at, enabled, last_verified_at)
|
|
4826
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4827
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
4828
|
+
version = excluded.version,
|
|
4829
|
+
sha256 = excluded.sha256,
|
|
4830
|
+
source_type = excluded.source_type,
|
|
4831
|
+
source_ref = excluded.source_ref,
|
|
4832
|
+
enabled = excluded.enabled,
|
|
4833
|
+
last_verified_at = excluded.last_verified_at`, [
|
|
4834
|
+
id,
|
|
4835
|
+
record.name,
|
|
4836
|
+
record.version,
|
|
4837
|
+
record.sha256,
|
|
4838
|
+
record.source_type,
|
|
4839
|
+
record.source_ref ?? existing?.source_ref ?? null,
|
|
4840
|
+
installedAt,
|
|
4841
|
+
enabled,
|
|
4842
|
+
record.last_verified_at ?? null
|
|
4843
|
+
]);
|
|
4507
4844
|
}
|
|
4508
|
-
function
|
|
4509
|
-
const
|
|
4510
|
-
|
|
4511
|
-
if (!meta)
|
|
4512
|
-
throw new Error(`Hook '${shortName}' not found`);
|
|
4513
|
-
const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
|
|
4514
|
-
const matcher = codewithMatcher(meta.matcher);
|
|
4515
|
-
const fragments = [];
|
|
4516
|
-
for (const event of getHookEvents(meta)) {
|
|
4517
|
-
const eventKey = getTargetEventName(event, "codewith");
|
|
4518
|
-
if (!eventKey) {
|
|
4519
|
-
throw new Error(`Hook '${shortName}' uses event '${event}', which is not supported by the Codewith target`);
|
|
4520
|
-
}
|
|
4521
|
-
const lines = [
|
|
4522
|
-
`[[hooks.${eventKey}]]`
|
|
4523
|
-
];
|
|
4524
|
-
if (matcher)
|
|
4525
|
-
lines.push(`matcher = ${tomlString(matcher)}`);
|
|
4526
|
-
lines.push("", `[[hooks.${eventKey}.hooks]]`, `type = "command"`, `command = ${tomlString(command)}`, `timeout = ${codewithTimeout(shortName)}`, `statusMessage = ${tomlString(codewithStatusMessage(shortName))}`);
|
|
4527
|
-
fragments.push(lines.join(`
|
|
4528
|
-
`));
|
|
4529
|
-
}
|
|
4530
|
-
return `${fragments.join(`
|
|
4531
|
-
|
|
4532
|
-
`)}
|
|
4533
|
-
`;
|
|
4845
|
+
function removeHookRecord(db, name) {
|
|
4846
|
+
const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
|
|
4847
|
+
return res.changes > 0;
|
|
4534
4848
|
}
|
|
4535
|
-
function
|
|
4536
|
-
const path = getSettingsPath(scope, "codewith", configPath);
|
|
4849
|
+
function readLock() {
|
|
4537
4850
|
try {
|
|
4538
|
-
|
|
4851
|
+
const path = getLockPath();
|
|
4852
|
+
if (!existsSync7(path))
|
|
4853
|
+
return { hooks: {} };
|
|
4854
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
4855
|
+
return { hooks: parsed.hooks ?? {} };
|
|
4539
4856
|
} catch {
|
|
4540
|
-
return
|
|
4857
|
+
return { hooks: {} };
|
|
4541
4858
|
}
|
|
4542
4859
|
}
|
|
4543
|
-
function
|
|
4544
|
-
const path =
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
4860
|
+
function writeLock(lock) {
|
|
4861
|
+
const path = getLockPath();
|
|
4862
|
+
mkdirSync4(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
|
|
4863
|
+
const sorted = {};
|
|
4864
|
+
for (const name of Object.keys(lock.hooks).sort()) {
|
|
4865
|
+
sorted[name] = lock.hooks[name];
|
|
4866
|
+
}
|
|
4867
|
+
writeFileSync3(path, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
4868
|
+
`, "utf-8");
|
|
4553
4869
|
return path;
|
|
4554
4870
|
}
|
|
4555
|
-
function
|
|
4556
|
-
const
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
const events = new Set(getHookEvents(meta));
|
|
4560
|
-
const registered = getRegisteredHooksForTarget(scope, target);
|
|
4561
|
-
for (const existingName of registered) {
|
|
4562
|
-
if (existingName === name)
|
|
4563
|
-
continue;
|
|
4564
|
-
const existing = getHook(existingName);
|
|
4565
|
-
if (!existing || !existing.matcher)
|
|
4566
|
-
continue;
|
|
4567
|
-
if (!getHookEvents(existing).some((event) => events.has(event)))
|
|
4568
|
-
continue;
|
|
4569
|
-
const a = meta.matcher.toLowerCase();
|
|
4570
|
-
const b = existing.matcher.toLowerCase();
|
|
4571
|
-
if (a === b || a.includes(b) || b.includes(a)) {
|
|
4572
|
-
return `conflicts with '${existingName}' (same event ${meta.event}, overlapping matcher '${existing.matcher}')`;
|
|
4573
|
-
}
|
|
4574
|
-
}
|
|
4575
|
-
return;
|
|
4871
|
+
function setPinnedHook(name, entry) {
|
|
4872
|
+
const lock = readLock();
|
|
4873
|
+
lock.hooks[name] = entry;
|
|
4874
|
+
return writeLock(lock);
|
|
4576
4875
|
}
|
|
4577
|
-
function
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
if (
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
success: true,
|
|
4615
|
-
scope,
|
|
4616
|
-
target,
|
|
4617
|
-
fragment,
|
|
4618
|
-
applied: true,
|
|
4619
|
-
configPath: path,
|
|
4620
|
-
note: "Direct Codewith config write was explicitly requested; prefer open-configs for managed machines."
|
|
4621
|
-
};
|
|
4622
|
-
} catch (error) {
|
|
4623
|
-
return {
|
|
4624
|
-
hook: shortName,
|
|
4625
|
-
success: false,
|
|
4626
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
4627
|
-
target
|
|
4628
|
-
};
|
|
4629
|
-
}
|
|
4630
|
-
}
|
|
4631
|
-
const registered = getRegisteredHooksForTarget(scope, target);
|
|
4632
|
-
if (registered.includes(shortName) && !overwrite) {
|
|
4633
|
-
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
4876
|
+
function getPinnedHook(name) {
|
|
4877
|
+
return readLock().hooks[name];
|
|
4878
|
+
}
|
|
4879
|
+
function removePinnedHook(name) {
|
|
4880
|
+
const lock = readLock();
|
|
4881
|
+
if (!(name in lock.hooks))
|
|
4882
|
+
return false;
|
|
4883
|
+
delete lock.hooks[name];
|
|
4884
|
+
writeLock(lock);
|
|
4885
|
+
return true;
|
|
4886
|
+
}
|
|
4887
|
+
function pinInstalledHook(name, version, sha256, source, sourceRef) {
|
|
4888
|
+
const now2 = new Date().toISOString();
|
|
4889
|
+
const db = getDb();
|
|
4890
|
+
upsertHookRecord(db, {
|
|
4891
|
+
name,
|
|
4892
|
+
version,
|
|
4893
|
+
sha256,
|
|
4894
|
+
source_type: source,
|
|
4895
|
+
source_ref: sourceRef ?? null,
|
|
4896
|
+
last_verified_at: now2
|
|
4897
|
+
});
|
|
4898
|
+
setPinnedHook(name, { version, sha256, source });
|
|
4899
|
+
}
|
|
4900
|
+
function removeHookFromStore(name) {
|
|
4901
|
+
const removedPin = removePinnedHook(name);
|
|
4902
|
+
const db = getDb();
|
|
4903
|
+
const removedRecord = removeHookRecord(db, name);
|
|
4904
|
+
return { removedPin, removedRecord };
|
|
4905
|
+
}
|
|
4906
|
+
function checkScriptHash(name, actual) {
|
|
4907
|
+
const db = getDb();
|
|
4908
|
+
const record = getHookRecord(db, name);
|
|
4909
|
+
const pin = getPinnedHook(name);
|
|
4910
|
+
const expected = record?.sha256 ?? pin?.sha256 ?? null;
|
|
4911
|
+
if (expected !== null && expected !== actual) {
|
|
4912
|
+
return { ok: false, pinned: true, expected, actual, name };
|
|
4634
4913
|
}
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
};
|
|
4914
|
+
if (expected === null) {
|
|
4915
|
+
const now3 = new Date().toISOString();
|
|
4916
|
+
const version = pin?.version ?? record?.version ?? "0.0.0";
|
|
4917
|
+
const source = pin?.source ?? record?.source_type ?? "local";
|
|
4918
|
+
upsertHookRecord(db, {
|
|
4919
|
+
name,
|
|
4920
|
+
version,
|
|
4921
|
+
sha256: actual,
|
|
4922
|
+
source_type: source,
|
|
4923
|
+
last_verified_at: now3
|
|
4924
|
+
});
|
|
4925
|
+
setPinnedHook(name, { version, sha256: actual, source });
|
|
4926
|
+
return { ok: true, pinned: false, expected: null, actual, name };
|
|
4646
4927
|
}
|
|
4928
|
+
const now2 = new Date().toISOString();
|
|
4929
|
+
upsertHookRecord(db, {
|
|
4930
|
+
name,
|
|
4931
|
+
version: record?.version ?? pin?.version ?? "0.0.0",
|
|
4932
|
+
sha256: expected,
|
|
4933
|
+
source_type: record?.source_type ?? pin?.source ?? "local",
|
|
4934
|
+
last_verified_at: now2
|
|
4935
|
+
});
|
|
4936
|
+
return { ok: true, pinned: true, expected, actual, name };
|
|
4647
4937
|
}
|
|
4648
|
-
function
|
|
4649
|
-
|
|
4650
|
-
if (target === "all") {
|
|
4651
|
-
const shortName = shortHookName(name);
|
|
4652
|
-
const meta = resolveHookMeta(shortName);
|
|
4653
|
-
if (meta) {
|
|
4654
|
-
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
4655
|
-
if (unsupportedTargets.length > 0) {
|
|
4656
|
-
return {
|
|
4657
|
-
hook: shortName,
|
|
4658
|
-
success: false,
|
|
4659
|
-
error: `Event(s) '${getHookEvents(meta).join(", ")}' are not supported by target(s): ${unsupportedTargets.join(", ")}`,
|
|
4660
|
-
target: "all"
|
|
4661
|
-
};
|
|
4662
|
-
}
|
|
4663
|
-
}
|
|
4664
|
-
const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
|
|
4665
|
-
if (!claudeResult.success) {
|
|
4666
|
-
return {
|
|
4667
|
-
...claudeResult,
|
|
4668
|
-
error: `Failed for target 'claude': ${claudeResult.error}`,
|
|
4669
|
-
target: "all"
|
|
4670
|
-
};
|
|
4671
|
-
}
|
|
4672
|
-
const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
|
|
4673
|
-
if (!codewithResult.success) {
|
|
4674
|
-
return {
|
|
4675
|
-
...codewithResult,
|
|
4676
|
-
error: `Failed for target 'codewith': ${codewithResult.error}`,
|
|
4677
|
-
target: "all"
|
|
4678
|
-
};
|
|
4679
|
-
}
|
|
4680
|
-
return { ...claudeResult, target: "all" };
|
|
4681
|
-
}
|
|
4682
|
-
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
|
|
4938
|
+
async function verifyScriptHash(name, scriptPath) {
|
|
4939
|
+
return checkScriptHash(name, await sha256File(scriptPath));
|
|
4683
4940
|
}
|
|
4684
|
-
function
|
|
4685
|
-
const
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
return eventKey;
|
|
4941
|
+
function retrustHook(name, scriptPath, version, source) {
|
|
4942
|
+
const actual = sha256Of(readFileSync4(scriptPath));
|
|
4943
|
+
const db = getDb();
|
|
4944
|
+
upsertHookRecord(db, {
|
|
4945
|
+
name,
|
|
4946
|
+
version,
|
|
4947
|
+
sha256: actual,
|
|
4948
|
+
source_type: source,
|
|
4949
|
+
last_verified_at: new Date().toISOString()
|
|
4694
4950
|
});
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4951
|
+
setPinnedHook(name, { version, sha256: actual, source });
|
|
4952
|
+
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
4953
|
+
}
|
|
4954
|
+
var init_store = __esm(() => {
|
|
4955
|
+
init_db();
|
|
4956
|
+
init_config();
|
|
4957
|
+
});
|
|
4958
|
+
|
|
4959
|
+
// src/lib/installer.ts
|
|
4960
|
+
var exports_installer = {};
|
|
4961
|
+
__export(exports_installer, {
|
|
4962
|
+
uninstallHook: () => uninstallHook,
|
|
4963
|
+
removeHook: () => removeHook,
|
|
4964
|
+
removeCodewithHookEntry: () => removeCodewithHookEntry,
|
|
4965
|
+
isEventSupported: () => isEventSupported,
|
|
4966
|
+
installHooks: () => installHooks,
|
|
4967
|
+
installHook: () => installHook,
|
|
4968
|
+
hookExists: () => hookExists,
|
|
4969
|
+
getSettingsPath: () => getSettingsPath,
|
|
4970
|
+
getRegisteredHooksForTarget: () => getRegisteredHooksForTarget,
|
|
4971
|
+
getRegisteredHooks: () => getRegisteredHooks,
|
|
4972
|
+
getInstalledHooks: () => getInstalledHooks,
|
|
4973
|
+
getHookPath: () => getHookPath,
|
|
4974
|
+
buildCodewithTomlFragment: () => buildCodewithTomlFragment
|
|
4975
|
+
});
|
|
4976
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
4977
|
+
import { join as join7, dirname as dirname3 } from "path";
|
|
4978
|
+
import { homedir as homedir5 } from "os";
|
|
4979
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4980
|
+
function normalizeHookName(name) {
|
|
4981
|
+
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4982
|
+
}
|
|
4983
|
+
function shortHookName(name) {
|
|
4984
|
+
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
4985
|
+
}
|
|
4986
|
+
function removeHookEntriesByName(entries, hookName) {
|
|
4987
|
+
return entries.filter((entry) => !entry.hooks?.some((h) => {
|
|
4988
|
+
const match = h.command?.match(/^hooks run ([\w-]+)/);
|
|
4989
|
+
return match && match[1] === hookName;
|
|
4990
|
+
}));
|
|
4991
|
+
}
|
|
4992
|
+
function getTargetSettingsDir(target) {
|
|
4993
|
+
if (target === "codewith")
|
|
4994
|
+
return ".codewith";
|
|
4995
|
+
if (target === "gemini")
|
|
4996
|
+
return ".gemini";
|
|
4997
|
+
return ".claude";
|
|
4998
|
+
}
|
|
4999
|
+
function getGlobalSettingsPathOverride(target) {
|
|
5000
|
+
if (target === "claude")
|
|
5001
|
+
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
5002
|
+
if (target === "gemini")
|
|
5003
|
+
return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
|
|
5004
|
+
return;
|
|
5005
|
+
}
|
|
5006
|
+
function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
|
|
5007
|
+
if (target === "codewith" && codewithConfigPath)
|
|
5008
|
+
return codewithConfigPath;
|
|
5009
|
+
if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
5010
|
+
return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
|
|
4698
5011
|
}
|
|
4699
|
-
const
|
|
4700
|
-
if (
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
if (!settings.hooks[eventKey])
|
|
4706
|
-
settings.hooks[eventKey] = [];
|
|
4707
|
-
const entry = {
|
|
4708
|
-
hooks: [{ type: "command", command: hookCommand }]
|
|
4709
|
-
};
|
|
4710
|
-
if (meta.matcher) {
|
|
4711
|
-
entry.matcher = meta.matcher;
|
|
4712
|
-
}
|
|
4713
|
-
settings.hooks[eventKey].push(entry);
|
|
5012
|
+
const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
|
|
5013
|
+
if (globalOverride)
|
|
5014
|
+
return globalOverride;
|
|
5015
|
+
const dir = getTargetSettingsDir(target);
|
|
5016
|
+
if (scope === "project") {
|
|
5017
|
+
return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
|
|
4714
5018
|
}
|
|
4715
|
-
|
|
5019
|
+
return target === "codewith" ? join7(homedir5(), dir, "config.toml") : join7(homedir5(), dir, "settings.json");
|
|
4716
5020
|
}
|
|
4717
|
-
function
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
delete settings.hooks[key];
|
|
4724
|
-
}
|
|
4725
|
-
}
|
|
5021
|
+
function getHookPath(name) {
|
|
5022
|
+
const shortName = shortHookName(name);
|
|
5023
|
+
const resolved = resolveHookDir(shortName);
|
|
5024
|
+
if (resolved)
|
|
5025
|
+
return resolved;
|
|
5026
|
+
return join7(HOOKS_DIR, normalizeHookName(shortName));
|
|
4726
5027
|
}
|
|
4727
|
-
function
|
|
4728
|
-
|
|
4729
|
-
if (!meta)
|
|
4730
|
-
return;
|
|
4731
|
-
const settings = readSettings(scope, target);
|
|
4732
|
-
if (!settings.hooks)
|
|
4733
|
-
return;
|
|
4734
|
-
removeHookFromAllEvents(settings, name);
|
|
4735
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
4736
|
-
delete settings.hooks;
|
|
4737
|
-
}
|
|
4738
|
-
writeSettings(settings, scope, target);
|
|
5028
|
+
function hookExists(name) {
|
|
5029
|
+
return existsSync8(getHookPath(name));
|
|
4739
5030
|
}
|
|
4740
|
-
function
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
let match;
|
|
4746
|
-
while (match = re.exec(config)) {
|
|
4747
|
-
registered2.push(match[1]);
|
|
5031
|
+
function readSettings(scope = "global", target = "claude") {
|
|
5032
|
+
const path = getSettingsPath(scope, target);
|
|
5033
|
+
try {
|
|
5034
|
+
if (existsSync8(path)) {
|
|
5035
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
4748
5036
|
}
|
|
4749
|
-
|
|
5037
|
+
} catch (error) {
|
|
5038
|
+
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4750
5039
|
}
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
const
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
|
|
4759
|
-
const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
|
|
4760
|
-
const match = newMatch || oldMatch;
|
|
4761
|
-
if (match) {
|
|
4762
|
-
registered.push(match[1]);
|
|
4763
|
-
}
|
|
4764
|
-
}
|
|
4765
|
-
}
|
|
5040
|
+
return {};
|
|
5041
|
+
}
|
|
5042
|
+
function writeSettings(settings, scope = "global", target = "claude") {
|
|
5043
|
+
const path = getSettingsPath(scope, target);
|
|
5044
|
+
const dir = dirname3(path);
|
|
5045
|
+
if (!existsSync8(dir)) {
|
|
5046
|
+
mkdirSync5(dir, { recursive: true });
|
|
4766
5047
|
}
|
|
4767
|
-
|
|
5048
|
+
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
5049
|
+
`);
|
|
4768
5050
|
}
|
|
4769
|
-
function
|
|
4770
|
-
return
|
|
5051
|
+
function getTargetEventName(internalEvent, target) {
|
|
5052
|
+
return EVENT_MAP[target]?.[internalEvent] ?? null;
|
|
5053
|
+
}
|
|
5054
|
+
function isEventSupported(internalEvent, target) {
|
|
5055
|
+
return getTargetEventName(internalEvent, target) !== null;
|
|
5056
|
+
}
|
|
5057
|
+
function tomlString(value) {
|
|
5058
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
5059
|
+
}
|
|
5060
|
+
function codewithMatcher(matcher) {
|
|
5061
|
+
if (!matcher)
|
|
5062
|
+
return;
|
|
5063
|
+
if (matcher.startsWith("^"))
|
|
5064
|
+
return matcher;
|
|
5065
|
+
if (matcher.includes("|"))
|
|
5066
|
+
return `^(${matcher})$`;
|
|
5067
|
+
return `^${matcher}$`;
|
|
4771
5068
|
}
|
|
4772
|
-
function
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
5069
|
+
function codewithTimeout(name) {
|
|
5070
|
+
switch (name) {
|
|
5071
|
+
case "knowledge-context":
|
|
5072
|
+
return 6;
|
|
5073
|
+
case "session-start":
|
|
5074
|
+
return 8;
|
|
5075
|
+
case "pre-bash":
|
|
5076
|
+
return 20;
|
|
5077
|
+
case "prompt-guard":
|
|
5078
|
+
return 3;
|
|
5079
|
+
case "worktree-guard":
|
|
5080
|
+
case "stop-sync":
|
|
5081
|
+
return 5;
|
|
5082
|
+
default:
|
|
5083
|
+
return 10;
|
|
4781
5084
|
}
|
|
4782
|
-
return removeHookForTarget(shortName, scope, target);
|
|
4783
5085
|
}
|
|
4784
|
-
function
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
5086
|
+
function codewithStatusMessage(name) {
|
|
5087
|
+
switch (name) {
|
|
5088
|
+
case "knowledge-context":
|
|
5089
|
+
return "Loading Knowledge context";
|
|
5090
|
+
case "session-start":
|
|
5091
|
+
return "Checking Hasna session context";
|
|
5092
|
+
case "pre-bash":
|
|
5093
|
+
return "Checking Bash safety";
|
|
5094
|
+
case "prompt-guard":
|
|
5095
|
+
return "Checking prompt safety";
|
|
5096
|
+
case "worktree-guard":
|
|
5097
|
+
return "Checking worktree safety";
|
|
5098
|
+
case "stop-sync":
|
|
5099
|
+
return "Syncing turn-end heartbeat";
|
|
5100
|
+
default:
|
|
5101
|
+
return `Running ${name}`;
|
|
4788
5102
|
}
|
|
4789
|
-
unregisterHook(name, scope, target);
|
|
4790
|
-
return true;
|
|
4791
5103
|
}
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
SessionStart: "SessionStart",
|
|
4805
|
-
SessionEnd: "SessionEnd",
|
|
4806
|
-
UserPromptSubmit: null,
|
|
4807
|
-
SubagentStart: null
|
|
4808
|
-
},
|
|
4809
|
-
gemini: {
|
|
4810
|
-
PreToolUse: "BeforeTool",
|
|
4811
|
-
PostToolUse: "AfterTool",
|
|
4812
|
-
Stop: "AfterAgent",
|
|
4813
|
-
Notification: "Notification",
|
|
4814
|
-
SessionStart: null,
|
|
4815
|
-
SessionEnd: null,
|
|
4816
|
-
UserPromptSubmit: null,
|
|
4817
|
-
SubagentStart: null
|
|
4818
|
-
},
|
|
4819
|
-
codewith: {
|
|
4820
|
-
PreToolUse: "PreToolUse",
|
|
4821
|
-
PostToolUse: "PostToolUse",
|
|
4822
|
-
Stop: "Stop",
|
|
4823
|
-
Notification: null,
|
|
4824
|
-
SessionStart: "SessionStart",
|
|
4825
|
-
SessionEnd: null,
|
|
4826
|
-
UserPromptSubmit: "UserPromptSubmit",
|
|
4827
|
-
SubagentStart: "SubagentStart"
|
|
5104
|
+
function buildCodewithTomlFragment(name, profile) {
|
|
5105
|
+
const shortName = shortHookName(name);
|
|
5106
|
+
const meta = resolveHookMeta(shortName);
|
|
5107
|
+
if (!meta)
|
|
5108
|
+
throw new Error(`Hook '${shortName}' not found`);
|
|
5109
|
+
const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
|
|
5110
|
+
const matcher = codewithMatcher(meta.matcher);
|
|
5111
|
+
const fragments = [];
|
|
5112
|
+
for (const event of getHookEvents(meta)) {
|
|
5113
|
+
const eventKey = getTargetEventName(event, "codewith");
|
|
5114
|
+
if (!eventKey) {
|
|
5115
|
+
throw new Error(`Hook '${shortName}' uses event '${event}', which is not supported by the Codewith target`);
|
|
4828
5116
|
}
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
5117
|
+
const lines = [
|
|
5118
|
+
`[[hooks.${eventKey}]]`
|
|
5119
|
+
];
|
|
5120
|
+
if (matcher)
|
|
5121
|
+
lines.push(`matcher = ${tomlString(matcher)}`);
|
|
5122
|
+
lines.push("", `[[hooks.${eventKey}.hooks]]`, `type = "command"`, `command = ${tomlString(command)}`, `timeout = ${codewithTimeout(shortName)}`, `statusMessage = ${tomlString(codewithStatusMessage(shortName))}`);
|
|
5123
|
+
fragments.push(lines.join(`
|
|
5124
|
+
`));
|
|
5125
|
+
}
|
|
5126
|
+
return `${fragments.join(`
|
|
4832
5127
|
|
|
4833
|
-
|
|
4834
|
-
|
|
5128
|
+
`)}
|
|
5129
|
+
`;
|
|
5130
|
+
}
|
|
5131
|
+
function readCodewithConfig(scope, configPath) {
|
|
5132
|
+
const path = getSettingsPath(scope, "codewith", configPath);
|
|
4835
5133
|
try {
|
|
4836
|
-
return
|
|
5134
|
+
return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
|
|
4837
5135
|
} catch {
|
|
4838
|
-
return
|
|
5136
|
+
return "";
|
|
4839
5137
|
}
|
|
4840
5138
|
}
|
|
4841
|
-
function
|
|
4842
|
-
const
|
|
4843
|
-
const
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
const
|
|
4847
|
-
|
|
4848
|
-
return false;
|
|
4849
|
-
return hooks.some((entry) => entry?.hooks?.some((h) => {
|
|
4850
|
-
const match = h?.command?.match(/^hooks run ([\w-]+)/);
|
|
4851
|
-
if (!match || match[1] !== name)
|
|
4852
|
-
return false;
|
|
4853
|
-
if (!matcher)
|
|
4854
|
-
return true;
|
|
4855
|
-
const entryMatcher = typeof entry.matcher === "string" ? entry.matcher : "";
|
|
4856
|
-
if (!entryMatcher)
|
|
4857
|
-
return true;
|
|
4858
|
-
return entryMatcher === matcher || safeMatcherTest(entryMatcher, matcher) || safeMatcherTest(matcher, entryMatcher);
|
|
4859
|
-
}));
|
|
4860
|
-
}
|
|
5139
|
+
function appendCodewithFragment(fragment, scope, configPath) {
|
|
5140
|
+
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5141
|
+
const dir = dirname3(path);
|
|
5142
|
+
if (!existsSync8(dir))
|
|
5143
|
+
mkdirSync5(dir, { recursive: true });
|
|
5144
|
+
const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
|
|
5145
|
+
const sep2 = existing.trim() ? `
|
|
4861
5146
|
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
import { homedir as homedir4 } from "os";
|
|
4866
|
-
function resolveProfilesDir() {
|
|
4867
|
-
const newDir = join6(homedir4(), ".hasna", "hooks", "profiles");
|
|
4868
|
-
const oldDir = join6(homedir4(), ".hooks", "profiles");
|
|
4869
|
-
if (!existsSync6(newDir) && existsSync6(oldDir)) {
|
|
4870
|
-
mkdirSync4(join6(homedir4(), ".hasna", "hooks"), { recursive: true });
|
|
4871
|
-
cpSync(oldDir, newDir, { recursive: true });
|
|
4872
|
-
}
|
|
4873
|
-
return newDir;
|
|
5147
|
+
` : "";
|
|
5148
|
+
writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
5149
|
+
return path;
|
|
4874
5150
|
}
|
|
4875
|
-
function
|
|
4876
|
-
|
|
4877
|
-
|
|
5151
|
+
function detectConflict(name, scope, target) {
|
|
5152
|
+
const meta = resolveHookMeta(name);
|
|
5153
|
+
if (!meta || !meta.matcher)
|
|
5154
|
+
return;
|
|
5155
|
+
const events = new Set(getHookEvents(meta));
|
|
5156
|
+
const registered = getRegisteredHooksForTarget(scope, target);
|
|
5157
|
+
for (const existingName of registered) {
|
|
5158
|
+
if (existingName === name)
|
|
5159
|
+
continue;
|
|
5160
|
+
const existing = getHook(existingName);
|
|
5161
|
+
if (!existing || !existing.matcher)
|
|
5162
|
+
continue;
|
|
5163
|
+
if (!getHookEvents(existing).some((event) => events.has(event)))
|
|
5164
|
+
continue;
|
|
5165
|
+
const a = meta.matcher.toLowerCase();
|
|
5166
|
+
const b = existing.matcher.toLowerCase();
|
|
5167
|
+
if (a === b || a.includes(b) || b.includes(a)) {
|
|
5168
|
+
return `conflicts with '${existingName}' (same event ${meta.event}, overlapping matcher '${existing.matcher}')`;
|
|
5169
|
+
}
|
|
4878
5170
|
}
|
|
5171
|
+
return;
|
|
4879
5172
|
}
|
|
4880
|
-
function
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
return crypto.randomUUID().slice(0, 8);
|
|
4885
|
-
}
|
|
4886
|
-
function createProfile(input) {
|
|
4887
|
-
ensureProfilesDir();
|
|
4888
|
-
const id = shortUuid();
|
|
4889
|
-
const now2 = new Date().toISOString();
|
|
4890
|
-
const profile = {
|
|
4891
|
-
agent_id: id,
|
|
4892
|
-
agent_type: input.agent_type,
|
|
4893
|
-
created_at: now2,
|
|
4894
|
-
last_seen_at: now2,
|
|
4895
|
-
preferences: {}
|
|
4896
|
-
};
|
|
4897
|
-
if (input.name) {
|
|
4898
|
-
profile.name = input.name;
|
|
5173
|
+
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
|
|
5174
|
+
const shortName = shortHookName(name);
|
|
5175
|
+
if (!hookExists(shortName)) {
|
|
5176
|
+
return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
|
|
4899
5177
|
}
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
5178
|
+
if (target === "codewith") {
|
|
5179
|
+
try {
|
|
5180
|
+
const fragment = buildCodewithTomlFragment(shortName, profile);
|
|
5181
|
+
if (codewithMode !== "write") {
|
|
5182
|
+
return {
|
|
5183
|
+
hook: shortName,
|
|
5184
|
+
success: true,
|
|
5185
|
+
scope,
|
|
5186
|
+
target,
|
|
5187
|
+
fragment,
|
|
5188
|
+
applied: false,
|
|
5189
|
+
note: "Codewith install is fragment-only by default; open-configs should own applying this TOML."
|
|
5190
|
+
};
|
|
5191
|
+
}
|
|
5192
|
+
if (!codewithConfigPath) {
|
|
5193
|
+
return {
|
|
5194
|
+
hook: shortName,
|
|
5195
|
+
success: false,
|
|
5196
|
+
error: "Direct Codewith writes require an explicit --codewith-config path; refusing to write default ~/.codewith/config.toml.",
|
|
5197
|
+
scope,
|
|
5198
|
+
target,
|
|
5199
|
+
fragment,
|
|
5200
|
+
applied: false
|
|
5201
|
+
};
|
|
5202
|
+
}
|
|
5203
|
+
const existing = readCodewithConfig(scope, codewithConfigPath);
|
|
5204
|
+
if (!overwrite && new RegExp(`command\\s*=\\s*["']hooks run ${shortName}(?:\\s|["'])`).test(existing)) {
|
|
5205
|
+
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to append another fragment.", scope, target };
|
|
5206
|
+
}
|
|
5207
|
+
const path = appendCodewithFragment(fragment, scope, codewithConfigPath);
|
|
5208
|
+
return {
|
|
5209
|
+
hook: shortName,
|
|
5210
|
+
success: true,
|
|
5211
|
+
scope,
|
|
5212
|
+
target,
|
|
5213
|
+
fragment,
|
|
5214
|
+
applied: true,
|
|
5215
|
+
configPath: path,
|
|
5216
|
+
note: "Direct Codewith config write was explicitly requested; prefer open-configs for managed machines."
|
|
5217
|
+
};
|
|
5218
|
+
} catch (error) {
|
|
5219
|
+
return {
|
|
5220
|
+
hook: shortName,
|
|
5221
|
+
success: false,
|
|
5222
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
5223
|
+
target
|
|
5224
|
+
};
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
const registered = getRegisteredHooksForTarget(scope, target);
|
|
5228
|
+
if (registered.includes(shortName) && !overwrite) {
|
|
5229
|
+
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
5230
|
+
}
|
|
5231
|
+
const conflict = detectConflict(shortName, scope, target);
|
|
4906
5232
|
try {
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
5233
|
+
registerHook(shortName, scope, target, profile);
|
|
5234
|
+
return { hook: shortName, success: true, scope, target, ...conflict ? { conflict } : {} };
|
|
5235
|
+
} catch (error) {
|
|
5236
|
+
return {
|
|
5237
|
+
hook: shortName,
|
|
5238
|
+
success: false,
|
|
5239
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
5240
|
+
target
|
|
5241
|
+
};
|
|
4912
5242
|
}
|
|
4913
5243
|
}
|
|
4914
|
-
function
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
const
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
5244
|
+
function installHook(name, options = {}) {
|
|
5245
|
+
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
|
|
5246
|
+
if (target === "all") {
|
|
5247
|
+
const shortName = shortHookName(name);
|
|
5248
|
+
const meta = resolveHookMeta(shortName);
|
|
5249
|
+
if (meta) {
|
|
5250
|
+
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
5251
|
+
if (unsupportedTargets.length > 0) {
|
|
5252
|
+
return {
|
|
5253
|
+
hook: shortName,
|
|
5254
|
+
success: false,
|
|
5255
|
+
error: `Event(s) '${getHookEvents(meta).join(", ")}' are not supported by target(s): ${unsupportedTargets.join(", ")}`,
|
|
5256
|
+
target: "all"
|
|
5257
|
+
};
|
|
5258
|
+
}
|
|
4925
5259
|
}
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
5260
|
+
const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
|
|
5261
|
+
if (!claudeResult.success) {
|
|
5262
|
+
return {
|
|
5263
|
+
...claudeResult,
|
|
5264
|
+
error: `Failed for target 'claude': ${claudeResult.error}`,
|
|
5265
|
+
target: "all"
|
|
5266
|
+
};
|
|
5267
|
+
}
|
|
5268
|
+
const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
|
|
5269
|
+
if (!codewithResult.success) {
|
|
5270
|
+
return {
|
|
5271
|
+
...codewithResult,
|
|
5272
|
+
error: `Failed for target 'codewith': ${codewithResult.error}`,
|
|
5273
|
+
target: "all"
|
|
5274
|
+
};
|
|
5275
|
+
}
|
|
5276
|
+
return { ...claudeResult, target: "all" };
|
|
4929
5277
|
}
|
|
5278
|
+
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
|
|
4930
5279
|
}
|
|
4931
|
-
function
|
|
4932
|
-
const
|
|
4933
|
-
if (!
|
|
5280
|
+
function registerHook(name, scope = "global", target = "claude", profile) {
|
|
5281
|
+
const meta = resolveHookMeta(name);
|
|
5282
|
+
if (!meta)
|
|
4934
5283
|
return;
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
}
|
|
4939
|
-
function exportProfiles() {
|
|
4940
|
-
return listProfiles();
|
|
4941
|
-
}
|
|
4942
|
-
function importProfiles(profiles) {
|
|
4943
|
-
ensureProfilesDir();
|
|
4944
|
-
let imported = 0;
|
|
4945
|
-
let skipped = 0;
|
|
4946
|
-
for (const profile of profiles) {
|
|
4947
|
-
if (!profile.agent_id || !profile.agent_type) {
|
|
4948
|
-
skipped++;
|
|
4949
|
-
continue;
|
|
4950
|
-
}
|
|
4951
|
-
const path = profilePath(profile.agent_id);
|
|
4952
|
-
if (existsSync6(path)) {
|
|
4953
|
-
skipped++;
|
|
4954
|
-
continue;
|
|
5284
|
+
const eventKeys = getHookEvents(meta).map((event) => {
|
|
5285
|
+
const eventKey = getTargetEventName(event, target);
|
|
5286
|
+
if (eventKey === null) {
|
|
5287
|
+
throw new Error(`Event '${event}' is not supported by target '${target}'`);
|
|
4955
5288
|
}
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
5289
|
+
return eventKey;
|
|
5290
|
+
});
|
|
5291
|
+
const uniqueEventKeys = [...new Set(eventKeys)];
|
|
5292
|
+
if (uniqueEventKeys.length === 0) {
|
|
5293
|
+
throw new Error(`Hook '${name}' has no installable events for target '${target}'`);
|
|
4959
5294
|
}
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
tool_name TEXT,
|
|
4976
|
-
tool_input TEXT,
|
|
4977
|
-
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
4978
|
-
error TEXT,
|
|
4979
|
-
duration_ms INTEGER,
|
|
4980
|
-
project_dir TEXT,
|
|
4981
|
-
metadata TEXT
|
|
4982
|
-
)
|
|
4983
|
-
`, CREATE_INDEXES;
|
|
4984
|
-
var init_schema = __esm(() => {
|
|
4985
|
-
CREATE_INDEXES = [
|
|
4986
|
-
`CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
|
|
4987
|
-
`CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
|
|
4988
|
-
`CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
|
|
4989
|
-
`CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
|
|
4990
|
-
`CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`
|
|
4991
|
-
];
|
|
4992
|
-
});
|
|
4993
|
-
|
|
4994
|
-
// src/db/migrations/001_initial.ts
|
|
4995
|
-
function up(db) {
|
|
4996
|
-
db.exec(CREATE_HOOK_EVENTS_TABLE);
|
|
4997
|
-
for (const idx of CREATE_INDEXES) {
|
|
4998
|
-
db.exec(idx);
|
|
5295
|
+
const settings = readSettings(scope, target);
|
|
5296
|
+
if (!settings.hooks)
|
|
5297
|
+
settings.hooks = {};
|
|
5298
|
+
removeHookFromAllEvents(settings, name);
|
|
5299
|
+
const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`;
|
|
5300
|
+
for (const eventKey of uniqueEventKeys) {
|
|
5301
|
+
if (!settings.hooks[eventKey])
|
|
5302
|
+
settings.hooks[eventKey] = [];
|
|
5303
|
+
const entry = {
|
|
5304
|
+
hooks: [{ type: "command", command: hookCommand }]
|
|
5305
|
+
};
|
|
5306
|
+
if (meta.matcher) {
|
|
5307
|
+
entry.matcher = meta.matcher;
|
|
5308
|
+
}
|
|
5309
|
+
settings.hooks[eventKey].push(entry);
|
|
4999
5310
|
}
|
|
5311
|
+
writeSettings(settings, scope, target);
|
|
5000
5312
|
}
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
});
|
|
5004
|
-
|
|
5005
|
-
// src/db/migrations/002_session_events.ts
|
|
5006
|
-
function up2(db) {
|
|
5007
|
-
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
|
|
5008
|
-
if (!row?.sql)
|
|
5009
|
-
return;
|
|
5010
|
-
if (row.sql.includes("SessionStart"))
|
|
5313
|
+
function removeHookFromAllEvents(settings, name) {
|
|
5314
|
+
if (!settings.hooks)
|
|
5011
5315
|
return;
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
db.exec(`INSERT INTO hook_events
|
|
5017
|
-
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
5018
|
-
SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
|
|
5019
|
-
FROM hook_events_old`);
|
|
5020
|
-
db.exec("DROP TABLE hook_events_old");
|
|
5021
|
-
for (const idx of CREATE_INDEXES) {
|
|
5022
|
-
db.exec(idx);
|
|
5316
|
+
for (const key of Object.keys(settings.hooks)) {
|
|
5317
|
+
settings.hooks[key] = removeHookEntriesByName(settings.hooks[key], name);
|
|
5318
|
+
if (settings.hooks[key].length === 0) {
|
|
5319
|
+
delete settings.hooks[key];
|
|
5023
5320
|
}
|
|
5024
|
-
db.exec("COMMIT");
|
|
5025
|
-
} catch (err) {
|
|
5026
|
-
db.exec("ROLLBACK");
|
|
5027
|
-
throw err;
|
|
5028
5321
|
}
|
|
5029
5322
|
}
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
// src/db/migrations/003_user_prompt_submit_event.ts
|
|
5035
|
-
function up3(db) {
|
|
5036
|
-
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
|
|
5037
|
-
if (!row?.sql)
|
|
5038
|
-
return;
|
|
5039
|
-
if (row.sql.includes("UserPromptSubmit"))
|
|
5323
|
+
function unregisterHook(name, scope = "global", target = "claude") {
|
|
5324
|
+
const settings = readSettings(scope, target);
|
|
5325
|
+
if (!settings.hooks)
|
|
5040
5326
|
return;
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5327
|
+
removeHookFromAllEvents(settings, name);
|
|
5328
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
5329
|
+
delete settings.hooks;
|
|
5330
|
+
}
|
|
5331
|
+
writeSettings(settings, scope, target);
|
|
5332
|
+
}
|
|
5333
|
+
function installHooks(names, options = {}) {
|
|
5334
|
+
return names.map((name) => installHook(name, options));
|
|
5335
|
+
}
|
|
5336
|
+
function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
5337
|
+
if (target === "codewith") {
|
|
5338
|
+
const config = readCodewithConfig(scope);
|
|
5339
|
+
const registered2 = [];
|
|
5340
|
+
const re = /command\s*=\s*["']hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?["']/g;
|
|
5341
|
+
let match;
|
|
5342
|
+
while (match = re.exec(config)) {
|
|
5343
|
+
registered2.push(match[1]);
|
|
5052
5344
|
}
|
|
5053
|
-
|
|
5054
|
-
}
|
|
5055
|
-
|
|
5056
|
-
|
|
5345
|
+
return [...new Set(registered2)];
|
|
5346
|
+
}
|
|
5347
|
+
const settings = readSettings(scope, target);
|
|
5348
|
+
if (!settings.hooks)
|
|
5349
|
+
return [];
|
|
5350
|
+
const registered = [];
|
|
5351
|
+
for (const eventKey of Object.keys(settings.hooks)) {
|
|
5352
|
+
for (const entry of settings.hooks[eventKey]) {
|
|
5353
|
+
for (const hook of entry.hooks || []) {
|
|
5354
|
+
const newMatch = hook.command?.match(/^hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?$/);
|
|
5355
|
+
const oldMatch = hook.command?.match(/^hook-([\w-]+)$/);
|
|
5356
|
+
const match = newMatch || oldMatch;
|
|
5357
|
+
if (match) {
|
|
5358
|
+
registered.push(match[1]);
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
5363
|
+
return [...new Set(registered)];
|
|
5364
|
+
}
|
|
5365
|
+
function getRegisteredHooks(scope = "global") {
|
|
5366
|
+
return getRegisteredHooksForTarget(scope, "claude");
|
|
5367
|
+
}
|
|
5368
|
+
function removeHook(name, scope = "global", target = "claude") {
|
|
5369
|
+
const shortName = shortHookName(name);
|
|
5370
|
+
if (target === "all") {
|
|
5371
|
+
const claudeRemoved = removeHookForTarget(shortName, scope, "claude");
|
|
5372
|
+
const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
|
|
5373
|
+
return claudeRemoved || geminiRemoved;
|
|
5374
|
+
}
|
|
5375
|
+
if (target === "codewith") {
|
|
5376
|
+
return false;
|
|
5057
5377
|
}
|
|
5378
|
+
return removeHookForTarget(shortName, scope, target);
|
|
5058
5379
|
}
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
// src/db/migrations/004_hooks_table.ts
|
|
5064
|
-
function up4(db) {
|
|
5065
|
-
db.exec(CREATE_HOOKS_TABLE);
|
|
5066
|
-
for (const idx of CREATE_HOOKS_INDEXES) {
|
|
5067
|
-
db.exec(idx);
|
|
5380
|
+
function removeHookForTarget(name, scope, target) {
|
|
5381
|
+
const registered = getRegisteredHooksForTarget(scope, target);
|
|
5382
|
+
if (!registered.includes(name)) {
|
|
5383
|
+
return false;
|
|
5068
5384
|
}
|
|
5385
|
+
unregisterHook(name, scope, target);
|
|
5386
|
+
return true;
|
|
5069
5387
|
}
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
function ensureMigrationsTable(db) {
|
|
5091
|
-
db.exec(`
|
|
5092
|
-
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
5093
|
-
version TEXT PRIMARY KEY,
|
|
5094
|
-
applied_at TEXT NOT NULL
|
|
5095
|
-
)
|
|
5096
|
-
`);
|
|
5097
|
-
}
|
|
5098
|
-
function getApplied(db) {
|
|
5099
|
-
const rows = db.query("SELECT version FROM schema_migrations").all();
|
|
5100
|
-
return new Set(rows.map((r) => r.version));
|
|
5101
|
-
}
|
|
5102
|
-
function runMigrations(db) {
|
|
5103
|
-
ensureMigrationsTable(db);
|
|
5104
|
-
const applied = getApplied(db);
|
|
5105
|
-
for (const migration of MIGRATIONS) {
|
|
5106
|
-
if (applied.has(migration.version))
|
|
5388
|
+
function removeCodewithHookEntry(configText, name) {
|
|
5389
|
+
const sections = configText.split(/\n\s*\n/);
|
|
5390
|
+
const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
|
|
5391
|
+
const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
|
|
5392
|
+
const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
|
|
5393
|
+
const eventsWithEntries = new Set;
|
|
5394
|
+
const kept = [];
|
|
5395
|
+
let removed = false;
|
|
5396
|
+
for (const section of sections) {
|
|
5397
|
+
const lines = section.split(`
|
|
5398
|
+
`);
|
|
5399
|
+
const entryMatch = entryHeaderRe.exec(lines[0] ?? "");
|
|
5400
|
+
if (entryMatch) {
|
|
5401
|
+
const event = entryMatch[1];
|
|
5402
|
+
eventsWithEntries.add(event);
|
|
5403
|
+
if (lines.some((line) => nameRe.test(line.trim()))) {
|
|
5404
|
+
removed = true;
|
|
5405
|
+
continue;
|
|
5406
|
+
}
|
|
5407
|
+
kept.push({ text: section });
|
|
5107
5408
|
continue;
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
new Date().toISOString()
|
|
5112
|
-
]);
|
|
5409
|
+
}
|
|
5410
|
+
const eventMatch = eventHeaderRe.exec(lines[0] ?? "");
|
|
5411
|
+
kept.push({ text: section, eventHeaderFor: eventMatch ? eventMatch[1] : undefined });
|
|
5113
5412
|
}
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
});
|
|
5413
|
+
const keptEntriesByEvent = new Set;
|
|
5414
|
+
for (const item of kept) {
|
|
5415
|
+
const m = entryHeaderRe.exec(item.text.split(`
|
|
5416
|
+
`)[0] ?? "");
|
|
5417
|
+
if (m)
|
|
5418
|
+
keptEntriesByEvent.add(m[1]);
|
|
5419
|
+
}
|
|
5420
|
+
const finalSections = kept.filter((item) => {
|
|
5421
|
+
if (item.eventHeaderFor === undefined)
|
|
5422
|
+
return true;
|
|
5423
|
+
if (!eventsWithEntries.has(item.eventHeaderFor))
|
|
5424
|
+
return true;
|
|
5425
|
+
return keptEntriesByEvent.has(item.eventHeaderFor);
|
|
5426
|
+
}).map((item) => item.text);
|
|
5427
|
+
return {
|
|
5428
|
+
text: finalSections.join(`
|
|
5128
5429
|
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
db.exec(`
|
|
5135
|
-
CREATE TABLE IF NOT EXISTS _meta (
|
|
5136
|
-
key TEXT PRIMARY KEY,
|
|
5137
|
-
value TEXT NOT NULL
|
|
5138
|
-
)
|
|
5139
|
-
`);
|
|
5140
|
-
}
|
|
5141
|
-
function isAlreadyDone(db) {
|
|
5142
|
-
ensureMetaTable(db);
|
|
5143
|
-
const row = db.query("SELECT value FROM _meta WHERE key = ?").get(META_KEY);
|
|
5144
|
-
return row?.value === "1";
|
|
5430
|
+
`) + (configText.endsWith(`
|
|
5431
|
+
`) ? `
|
|
5432
|
+
` : ""),
|
|
5433
|
+
removed
|
|
5434
|
+
};
|
|
5145
5435
|
}
|
|
5146
|
-
function
|
|
5147
|
-
|
|
5436
|
+
function escapeRegExp(value) {
|
|
5437
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5148
5438
|
}
|
|
5149
|
-
function
|
|
5150
|
-
return
|
|
5439
|
+
function codewithHasHookEntryInText(configText, name) {
|
|
5440
|
+
return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
|
|
5151
5441
|
}
|
|
5152
|
-
function
|
|
5153
|
-
|
|
5442
|
+
function codewithHasHookEntry(name) {
|
|
5443
|
+
const path = getSettingsPath("global", "codewith");
|
|
5444
|
+
if (!existsSync8(path))
|
|
5445
|
+
return false;
|
|
5154
5446
|
try {
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
const entry = JSON.parse(line);
|
|
5160
|
-
db.run(`INSERT OR IGNORE INTO hook_events
|
|
5161
|
-
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, project_dir)
|
|
5162
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5163
|
-
nanoid(),
|
|
5164
|
-
entry.timestamp ?? new Date().toISOString(),
|
|
5165
|
-
entry.session_id ?? "legacy",
|
|
5166
|
-
"sessionlog",
|
|
5167
|
-
"PostToolUse",
|
|
5168
|
-
entry.tool_name ?? null,
|
|
5169
|
-
entry.tool_input ? String(entry.tool_input).slice(0, 500) : null,
|
|
5170
|
-
null
|
|
5171
|
-
]);
|
|
5172
|
-
count++;
|
|
5173
|
-
} catch {}
|
|
5174
|
-
}
|
|
5175
|
-
} catch {}
|
|
5176
|
-
return count;
|
|
5447
|
+
return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
|
|
5448
|
+
} catch {
|
|
5449
|
+
return false;
|
|
5450
|
+
}
|
|
5177
5451
|
}
|
|
5178
|
-
function
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
]);
|
|
5200
|
-
count++;
|
|
5201
|
-
} catch {}
|
|
5452
|
+
function uninstallHook(name, scope = "global", target = "claude") {
|
|
5453
|
+
const shortName = shortHookName(name);
|
|
5454
|
+
const custom = readCustomManifest(shortName);
|
|
5455
|
+
const bundledMeta = custom ? undefined : getHook(shortName);
|
|
5456
|
+
const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
|
|
5457
|
+
const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
|
|
5458
|
+
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
5459
|
+
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
5460
|
+
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
|
|
5461
|
+
if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
5462
|
+
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
5463
|
+
}
|
|
5464
|
+
const writableTargets = target === "all" ? ["claude", "gemini"] : target === "codewith" ? [] : [target];
|
|
5465
|
+
const scopes = [];
|
|
5466
|
+
for (const s of ["global", "project"]) {
|
|
5467
|
+
for (const t of writableTargets) {
|
|
5468
|
+
if (getRegisteredHooksForTarget(s, t).includes(shortName)) {
|
|
5469
|
+
unregisterHook(shortName, s, t);
|
|
5470
|
+
if (!scopes.includes(s))
|
|
5471
|
+
scopes.push(s);
|
|
5472
|
+
}
|
|
5202
5473
|
}
|
|
5203
|
-
}
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
const projectDir = join7(claudeProjectsDir, dir);
|
|
5217
|
-
try {
|
|
5218
|
-
const files = readdirSync3(projectDir);
|
|
5219
|
-
for (const file of files) {
|
|
5220
|
-
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
5221
|
-
total += importJsonlFile(db, join7(projectDir, file));
|
|
5222
|
-
}
|
|
5223
|
-
if (file === "errors.log") {
|
|
5224
|
-
total += importErrorsLog(db, join7(projectDir, file));
|
|
5225
|
-
}
|
|
5226
|
-
}
|
|
5227
|
-
} catch {}
|
|
5228
|
-
}
|
|
5229
|
-
} catch {}
|
|
5474
|
+
}
|
|
5475
|
+
const registrationsRemaining = [];
|
|
5476
|
+
if (target === "codewith" || target === "all") {
|
|
5477
|
+
const codewithPath = getSettingsPath("global", "codewith");
|
|
5478
|
+
if (existsSync8(codewithPath)) {
|
|
5479
|
+
const before = readFileSync5(codewithPath, "utf-8");
|
|
5480
|
+
const after = removeCodewithHookEntry(before, shortName);
|
|
5481
|
+
if (after.removed) {
|
|
5482
|
+
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
5483
|
+
}
|
|
5484
|
+
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
5485
|
+
registrationsRemaining.push("codewith");
|
|
5486
|
+
}
|
|
5230
5487
|
}
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5488
|
+
}
|
|
5489
|
+
if (custom) {
|
|
5490
|
+
try {
|
|
5491
|
+
rmSync(customHookDir(shortName), { recursive: true, force: true });
|
|
5492
|
+
} catch {
|
|
5493
|
+
return {
|
|
5494
|
+
name: shortName,
|
|
5495
|
+
removed: false,
|
|
5496
|
+
source: "custom",
|
|
5497
|
+
settingsScopes: scopes,
|
|
5498
|
+
storeDirRemoved: false,
|
|
5499
|
+
pinRemoved: false,
|
|
5500
|
+
dbRecordRemoved: false,
|
|
5501
|
+
registrationsRemaining,
|
|
5502
|
+
error: `Hook '${shortName}' store directory could not be removed; trust records preserved (no fail-open)`
|
|
5503
|
+
};
|
|
5235
5504
|
}
|
|
5236
|
-
} catch (err) {
|
|
5237
|
-
process.stderr.write(`[hooks] Legacy import failed (non-fatal): ${err}
|
|
5238
|
-
`);
|
|
5239
5505
|
}
|
|
5506
|
+
const { removedPin, removedRecord } = removeHookFromStore(shortName);
|
|
5507
|
+
return {
|
|
5508
|
+
name: shortName,
|
|
5509
|
+
removed: true,
|
|
5510
|
+
source: custom ? "custom" : bundledMeta ? "bundled" : "registered-only",
|
|
5511
|
+
settingsScopes: scopes,
|
|
5512
|
+
storeDirRemoved: custom ? true : false,
|
|
5513
|
+
pinRemoved: removedPin,
|
|
5514
|
+
dbRecordRemoved: removedRecord,
|
|
5515
|
+
registrationsRemaining
|
|
5516
|
+
};
|
|
5240
5517
|
}
|
|
5241
|
-
var
|
|
5242
|
-
var
|
|
5518
|
+
var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks;
|
|
5519
|
+
var init_installer = __esm(() => {
|
|
5520
|
+
init_registry();
|
|
5521
|
+
init_resolve();
|
|
5522
|
+
init_manifest();
|
|
5523
|
+
init_store();
|
|
5524
|
+
__dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
5525
|
+
HOOKS_DIR = existsSync8(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
|
|
5526
|
+
EVENT_MAP = {
|
|
5527
|
+
claude: {
|
|
5528
|
+
PreToolUse: "PreToolUse",
|
|
5529
|
+
PostToolUse: "PostToolUse",
|
|
5530
|
+
Stop: "Stop",
|
|
5531
|
+
Notification: "Notification",
|
|
5532
|
+
SessionStart: "SessionStart",
|
|
5533
|
+
SessionEnd: "SessionEnd",
|
|
5534
|
+
UserPromptSubmit: null,
|
|
5535
|
+
SubagentStart: null
|
|
5536
|
+
},
|
|
5537
|
+
gemini: {
|
|
5538
|
+
PreToolUse: "BeforeTool",
|
|
5539
|
+
PostToolUse: "AfterTool",
|
|
5540
|
+
Stop: "AfterAgent",
|
|
5541
|
+
Notification: "Notification",
|
|
5542
|
+
SessionStart: null,
|
|
5543
|
+
SessionEnd: null,
|
|
5544
|
+
UserPromptSubmit: null,
|
|
5545
|
+
SubagentStart: null
|
|
5546
|
+
},
|
|
5547
|
+
codewith: {
|
|
5548
|
+
PreToolUse: "PreToolUse",
|
|
5549
|
+
PostToolUse: "PostToolUse",
|
|
5550
|
+
Stop: "Stop",
|
|
5551
|
+
Notification: null,
|
|
5552
|
+
SessionStart: "SessionStart",
|
|
5553
|
+
SessionEnd: null,
|
|
5554
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
5555
|
+
SubagentStart: "SubagentStart"
|
|
5556
|
+
}
|
|
5557
|
+
};
|
|
5558
|
+
getInstalledHooks = getRegisteredHooks;
|
|
5559
|
+
});
|
|
5243
5560
|
|
|
5244
|
-
// src/
|
|
5245
|
-
function
|
|
5246
|
-
const envDays = parseInt(process.env.HOOKS_RETENTION_DAYS ?? "30");
|
|
5247
|
-
const retentionDays = days ?? (isNaN(envDays) || envDays <= 0 ? 30 : envDays);
|
|
5248
|
-
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
|
5561
|
+
// src/lib/registration.ts
|
|
5562
|
+
function safeMatcherTest(pattern, value) {
|
|
5249
5563
|
try {
|
|
5250
|
-
|
|
5251
|
-
const changes = db.query("SELECT changes() as changes").get()?.changes ?? 0;
|
|
5252
|
-
return changes;
|
|
5564
|
+
return new RegExp(pattern).test(value);
|
|
5253
5565
|
} catch {
|
|
5254
|
-
return
|
|
5566
|
+
return false;
|
|
5255
5567
|
}
|
|
5256
5568
|
}
|
|
5569
|
+
function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
5570
|
+
const separator = eventSpec.indexOf(":");
|
|
5571
|
+
const eventName = separator === -1 ? eventSpec : eventSpec.slice(0, separator);
|
|
5572
|
+
const specMatcher = separator === -1 ? "" : eventSpec.slice(separator + 1);
|
|
5573
|
+
const matcher = specMatcher || hookMatcher || "";
|
|
5574
|
+
const hooks = settings.hooks?.[eventName];
|
|
5575
|
+
if (!Array.isArray(hooks))
|
|
5576
|
+
return false;
|
|
5577
|
+
return hooks.some((entry) => entry?.hooks?.some((h) => {
|
|
5578
|
+
const match = h?.command?.match(/^hooks run ([\w-]+)/);
|
|
5579
|
+
if (!match || match[1] !== name)
|
|
5580
|
+
return false;
|
|
5581
|
+
if (!matcher)
|
|
5582
|
+
return true;
|
|
5583
|
+
const entryMatcher = typeof entry.matcher === "string" ? entry.matcher : "";
|
|
5584
|
+
if (!entryMatcher)
|
|
5585
|
+
return true;
|
|
5586
|
+
return entryMatcher === matcher || safeMatcherTest(entryMatcher, matcher) || safeMatcherTest(matcher, entryMatcher);
|
|
5587
|
+
}));
|
|
5588
|
+
}
|
|
5257
5589
|
|
|
5258
|
-
// src/
|
|
5259
|
-
|
|
5260
|
-
__export(exports_db, {
|
|
5261
|
-
getDbPath: () => getDbPath,
|
|
5262
|
-
getDb: () => getDb,
|
|
5263
|
-
createTestDb: () => createTestDb,
|
|
5264
|
-
closeDb: () => closeDb
|
|
5265
|
-
});
|
|
5266
|
-
import { Database } from "bun:sqlite";
|
|
5267
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync5, cpSync as cpSync2 } from "fs";
|
|
5590
|
+
// src/lib/profiles.ts
|
|
5591
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
|
|
5268
5592
|
import { join as join8 } from "path";
|
|
5269
5593
|
import { homedir as homedir6 } from "os";
|
|
5270
|
-
function
|
|
5271
|
-
const
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
const oldDir = join8(homedir6(), ".hooks");
|
|
5276
|
-
if (!existsSync8(newDir) && existsSync8(oldDir)) {
|
|
5277
|
-
mkdirSync5(join8(homedir6(), ".hasna"), { recursive: true });
|
|
5594
|
+
function resolveProfilesDir() {
|
|
5595
|
+
const newDir = join8(homedir6(), ".hasna", "hooks", "profiles");
|
|
5596
|
+
const oldDir = join8(homedir6(), ".hooks", "profiles");
|
|
5597
|
+
if (!existsSync9(newDir) && existsSync9(oldDir)) {
|
|
5598
|
+
mkdirSync6(join8(homedir6(), ".hasna", "hooks"), { recursive: true });
|
|
5278
5599
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
5279
5600
|
}
|
|
5280
5601
|
return newDir;
|
|
5281
5602
|
}
|
|
5282
|
-
function
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
return explicitDb;
|
|
5286
|
-
const dataDir = resolveDataDir();
|
|
5287
|
-
return join8(dataDir, "hooks.db");
|
|
5288
|
-
}
|
|
5289
|
-
function ensureDir(dbPath) {
|
|
5290
|
-
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
5291
|
-
if (dir && !existsSync8(dir)) {
|
|
5292
|
-
mkdirSync5(dir, { recursive: true });
|
|
5293
|
-
}
|
|
5294
|
-
}
|
|
5295
|
-
function getDb() {
|
|
5296
|
-
if (instance)
|
|
5297
|
-
return instance;
|
|
5298
|
-
const dbPath = getDbPath();
|
|
5299
|
-
const isNew = dbPath === ":memory:" || !existsSync8(dbPath);
|
|
5300
|
-
ensureDir(dbPath);
|
|
5301
|
-
instance = new Database(dbPath);
|
|
5302
|
-
instance.exec("PRAGMA journal_mode=WAL");
|
|
5303
|
-
instance.exec("PRAGMA foreign_keys=ON");
|
|
5304
|
-
runMigrations(instance);
|
|
5305
|
-
runRetention(instance);
|
|
5306
|
-
instance.exec(`CREATE TABLE IF NOT EXISTS feedback (
|
|
5307
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
5308
|
-
message TEXT NOT NULL,
|
|
5309
|
-
email TEXT,
|
|
5310
|
-
category TEXT DEFAULT 'general',
|
|
5311
|
-
version TEXT,
|
|
5312
|
-
machine_id TEXT,
|
|
5313
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
5314
|
-
)`);
|
|
5315
|
-
if (isNew) {
|
|
5316
|
-
runLegacyImport(instance);
|
|
5317
|
-
}
|
|
5318
|
-
return instance;
|
|
5319
|
-
}
|
|
5320
|
-
function closeDb() {
|
|
5321
|
-
if (instance) {
|
|
5322
|
-
instance.close();
|
|
5323
|
-
instance = null;
|
|
5603
|
+
function ensureProfilesDir() {
|
|
5604
|
+
if (!existsSync9(PROFILES_DIR)) {
|
|
5605
|
+
mkdirSync6(PROFILES_DIR, { recursive: true });
|
|
5324
5606
|
}
|
|
5325
5607
|
}
|
|
5326
|
-
function
|
|
5327
|
-
|
|
5328
|
-
db.exec("PRAGMA journal_mode=WAL");
|
|
5329
|
-
db.exec("PRAGMA foreign_keys=ON");
|
|
5330
|
-
return db;
|
|
5331
|
-
}
|
|
5332
|
-
var instance = null;
|
|
5333
|
-
var init_db = __esm(() => {
|
|
5334
|
-
init_migrations();
|
|
5335
|
-
init_legacy_import();
|
|
5336
|
-
});
|
|
5337
|
-
|
|
5338
|
-
// src/lib/store.ts
|
|
5339
|
-
var exports_store = {};
|
|
5340
|
-
__export(exports_store, {
|
|
5341
|
-
writeLock: () => writeLock,
|
|
5342
|
-
verifyScriptHash: () => verifyScriptHash,
|
|
5343
|
-
upsertHookRecord: () => upsertHookRecord,
|
|
5344
|
-
sha256Of: () => sha256Of,
|
|
5345
|
-
sha256File: () => sha256File,
|
|
5346
|
-
setPinnedHook: () => setPinnedHook,
|
|
5347
|
-
retrustHook: () => retrustHook,
|
|
5348
|
-
removePinnedHook: () => removePinnedHook,
|
|
5349
|
-
removeHookRecord: () => removeHookRecord,
|
|
5350
|
-
readLock: () => readLock,
|
|
5351
|
-
listHookRecords: () => listHookRecords,
|
|
5352
|
-
getPinnedHook: () => getPinnedHook,
|
|
5353
|
-
getHookRecord: () => getHookRecord,
|
|
5354
|
-
checkScriptHash: () => checkScriptHash
|
|
5355
|
-
});
|
|
5356
|
-
import { createHash } from "crypto";
|
|
5357
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
5358
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
5359
|
-
function sha256Of(input) {
|
|
5360
|
-
return createHash("sha256").update(input).digest("hex");
|
|
5361
|
-
}
|
|
5362
|
-
async function sha256File(path) {
|
|
5363
|
-
const buf = await readFile2(path);
|
|
5364
|
-
return sha256Of(buf);
|
|
5365
|
-
}
|
|
5366
|
-
function getHookRecord(db, name) {
|
|
5367
|
-
return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
|
|
5368
|
-
}
|
|
5369
|
-
function listHookRecords(db) {
|
|
5370
|
-
return db.query("SELECT * FROM hooks ORDER BY name").all();
|
|
5608
|
+
function profilePath(id) {
|
|
5609
|
+
return join8(PROFILES_DIR, `${id}.json`);
|
|
5371
5610
|
}
|
|
5372
|
-
function
|
|
5373
|
-
|
|
5374
|
-
const id = existing?.id ?? record.name;
|
|
5375
|
-
const installedAt = existing?.installed_at ?? new Date().toISOString();
|
|
5376
|
-
const enabled = record.enabled ?? existing?.enabled ?? 1;
|
|
5377
|
-
db.run(`INSERT INTO hooks (id, name, version, sha256, source_type, source_ref, installed_at, enabled, last_verified_at)
|
|
5378
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5379
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
5380
|
-
version = excluded.version,
|
|
5381
|
-
sha256 = excluded.sha256,
|
|
5382
|
-
source_type = excluded.source_type,
|
|
5383
|
-
source_ref = excluded.source_ref,
|
|
5384
|
-
enabled = excluded.enabled,
|
|
5385
|
-
last_verified_at = excluded.last_verified_at`, [
|
|
5386
|
-
id,
|
|
5387
|
-
record.name,
|
|
5388
|
-
record.version,
|
|
5389
|
-
record.sha256,
|
|
5390
|
-
record.source_type,
|
|
5391
|
-
record.source_ref ?? existing?.source_ref ?? null,
|
|
5392
|
-
installedAt,
|
|
5393
|
-
enabled,
|
|
5394
|
-
record.last_verified_at ?? null
|
|
5395
|
-
]);
|
|
5611
|
+
function shortUuid() {
|
|
5612
|
+
return crypto.randomUUID().slice(0, 8);
|
|
5396
5613
|
}
|
|
5397
|
-
function
|
|
5398
|
-
|
|
5399
|
-
|
|
5614
|
+
function createProfile(input) {
|
|
5615
|
+
ensureProfilesDir();
|
|
5616
|
+
const id = shortUuid();
|
|
5617
|
+
const now2 = new Date().toISOString();
|
|
5618
|
+
const profile = {
|
|
5619
|
+
agent_id: id,
|
|
5620
|
+
agent_type: input.agent_type,
|
|
5621
|
+
created_at: now2,
|
|
5622
|
+
last_seen_at: now2,
|
|
5623
|
+
preferences: {}
|
|
5624
|
+
};
|
|
5625
|
+
if (input.name) {
|
|
5626
|
+
profile.name = input.name;
|
|
5627
|
+
}
|
|
5628
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5629
|
+
`);
|
|
5630
|
+
return profile;
|
|
5400
5631
|
}
|
|
5401
|
-
function
|
|
5632
|
+
function getProfile(id) {
|
|
5633
|
+
const path = profilePath(id);
|
|
5402
5634
|
try {
|
|
5403
|
-
const path = getLockPath();
|
|
5404
5635
|
if (!existsSync9(path))
|
|
5405
|
-
return
|
|
5406
|
-
|
|
5407
|
-
return { hooks: parsed.hooks ?? {} };
|
|
5636
|
+
return null;
|
|
5637
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
5408
5638
|
} catch {
|
|
5409
|
-
return
|
|
5639
|
+
return null;
|
|
5410
5640
|
}
|
|
5411
5641
|
}
|
|
5412
|
-
function
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5642
|
+
function listProfiles() {
|
|
5643
|
+
if (!existsSync9(PROFILES_DIR))
|
|
5644
|
+
return [];
|
|
5645
|
+
try {
|
|
5646
|
+
const files = readdirSync3(PROFILES_DIR).filter((f) => f.endsWith(".json"));
|
|
5647
|
+
const profiles = [];
|
|
5648
|
+
for (const file of files) {
|
|
5649
|
+
try {
|
|
5650
|
+
const content = readFileSync6(join8(PROFILES_DIR, file), "utf-8");
|
|
5651
|
+
profiles.push(JSON.parse(content));
|
|
5652
|
+
} catch {}
|
|
5653
|
+
}
|
|
5654
|
+
return profiles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
5655
|
+
} catch {
|
|
5656
|
+
return [];
|
|
5418
5657
|
}
|
|
5419
|
-
writeFileSync5(path, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
5420
|
-
`, "utf-8");
|
|
5421
|
-
return path;
|
|
5422
|
-
}
|
|
5423
|
-
function setPinnedHook(name, entry) {
|
|
5424
|
-
const lock = readLock();
|
|
5425
|
-
lock.hooks[name] = entry;
|
|
5426
|
-
return writeLock(lock);
|
|
5427
5658
|
}
|
|
5428
|
-
function
|
|
5429
|
-
|
|
5659
|
+
function touchProfile(id) {
|
|
5660
|
+
const profile = getProfile(id);
|
|
5661
|
+
if (!profile)
|
|
5662
|
+
return;
|
|
5663
|
+
profile.last_seen_at = new Date().toISOString();
|
|
5664
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5665
|
+
`);
|
|
5430
5666
|
}
|
|
5431
|
-
function
|
|
5432
|
-
|
|
5433
|
-
if (!(name in lock.hooks))
|
|
5434
|
-
return false;
|
|
5435
|
-
delete lock.hooks[name];
|
|
5436
|
-
writeLock(lock);
|
|
5437
|
-
return true;
|
|
5667
|
+
function exportProfiles() {
|
|
5668
|
+
return listProfiles();
|
|
5438
5669
|
}
|
|
5439
|
-
function
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
const
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
const
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
last_verified_at: now3
|
|
5457
|
-
});
|
|
5458
|
-
setPinnedHook(name, { version, sha256: actual, source });
|
|
5459
|
-
return { ok: true, pinned: false, expected: null, actual, name };
|
|
5670
|
+
function importProfiles(profiles) {
|
|
5671
|
+
ensureProfilesDir();
|
|
5672
|
+
let imported = 0;
|
|
5673
|
+
let skipped = 0;
|
|
5674
|
+
for (const profile of profiles) {
|
|
5675
|
+
if (!profile.agent_id || !profile.agent_type) {
|
|
5676
|
+
skipped++;
|
|
5677
|
+
continue;
|
|
5678
|
+
}
|
|
5679
|
+
const path = profilePath(profile.agent_id);
|
|
5680
|
+
if (existsSync9(path)) {
|
|
5681
|
+
skipped++;
|
|
5682
|
+
continue;
|
|
5683
|
+
}
|
|
5684
|
+
writeFileSync5(path, JSON.stringify(profile, null, 2) + `
|
|
5685
|
+
`);
|
|
5686
|
+
imported++;
|
|
5460
5687
|
}
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5688
|
+
return { imported, skipped };
|
|
5689
|
+
}
|
|
5690
|
+
var PROFILES_DIR;
|
|
5691
|
+
var init_profiles = __esm(() => {
|
|
5692
|
+
PROFILES_DIR = resolveProfilesDir();
|
|
5693
|
+
});
|
|
5694
|
+
|
|
5695
|
+
// src/lib/db-writer.ts
|
|
5696
|
+
var exports_db_writer = {};
|
|
5697
|
+
__export(exports_db_writer, {
|
|
5698
|
+
writeHookEvent: () => writeHookEvent,
|
|
5699
|
+
resolveEventType: () => resolveEventType,
|
|
5700
|
+
recordHookRun: () => recordHookRun,
|
|
5701
|
+
normalizeEventType: () => normalizeEventType
|
|
5702
|
+
});
|
|
5703
|
+
function nanoid2() {
|
|
5704
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
|
|
5470
5705
|
}
|
|
5471
|
-
|
|
5472
|
-
|
|
5706
|
+
function writeHookEvent(event) {
|
|
5707
|
+
try {
|
|
5708
|
+
const db = getDb();
|
|
5709
|
+
const id = nanoid2();
|
|
5710
|
+
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
5711
|
+
db.run(`INSERT INTO hook_events
|
|
5712
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
5713
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5714
|
+
id,
|
|
5715
|
+
timestamp,
|
|
5716
|
+
event.session_id,
|
|
5717
|
+
event.hook_name,
|
|
5718
|
+
event.event_type,
|
|
5719
|
+
event.tool_name ?? null,
|
|
5720
|
+
event.tool_input ? event.tool_input.slice(0, 500) : null,
|
|
5721
|
+
event.result ?? null,
|
|
5722
|
+
event.error ?? null,
|
|
5723
|
+
event.duration_ms ?? null,
|
|
5724
|
+
event.project_dir ?? null,
|
|
5725
|
+
event.metadata ?? null
|
|
5726
|
+
]);
|
|
5727
|
+
} catch (err) {
|
|
5728
|
+
process.stderr.write(`[hooks db-writer] failed to write event: ${err}
|
|
5729
|
+
`);
|
|
5730
|
+
}
|
|
5473
5731
|
}
|
|
5474
|
-
function
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5732
|
+
function normalizeEventType(value) {
|
|
5733
|
+
if (typeof value !== "string")
|
|
5734
|
+
return null;
|
|
5735
|
+
if (ALLOWED_EVENT_TYPES.has(value))
|
|
5736
|
+
return value;
|
|
5737
|
+
const bare = value.split(":")[0] ?? "";
|
|
5738
|
+
return ALLOWED_EVENT_TYPES.has(bare) ? bare : null;
|
|
5739
|
+
}
|
|
5740
|
+
function resolveEventType(inputEvent, fallbackEvent) {
|
|
5741
|
+
const fromInput = normalizeEventType(inputEvent);
|
|
5742
|
+
if (fromInput)
|
|
5743
|
+
return fromInput;
|
|
5744
|
+
return normalizeEventType(fallbackEvent ?? null);
|
|
5745
|
+
}
|
|
5746
|
+
function recordHookRun(record) {
|
|
5747
|
+
const eventType = record.eventType ? normalizeEventType(record.eventType) : null;
|
|
5748
|
+
if (!eventType) {
|
|
5749
|
+
process.stderr.write(`[hooks db-writer] skipped event for '${record.hookName}': unsupported event type '${String(record.eventType)}'
|
|
5750
|
+
`);
|
|
5751
|
+
return;
|
|
5752
|
+
}
|
|
5753
|
+
const metadata = {
|
|
5754
|
+
...record.metadata ?? {},
|
|
5755
|
+
version: record.version ?? null,
|
|
5756
|
+
sha256: record.sha256 ?? null,
|
|
5757
|
+
exit_code: record.exitCode
|
|
5758
|
+
};
|
|
5759
|
+
writeHookEvent({
|
|
5760
|
+
session_id: record.sessionId ?? "cli",
|
|
5761
|
+
hook_name: record.hookName,
|
|
5762
|
+
event_type: eventType,
|
|
5763
|
+
tool_name: record.toolName ?? null,
|
|
5764
|
+
tool_input: record.toolInput !== undefined ? JSON.stringify(record.toolInput) : null,
|
|
5765
|
+
result: record.result ?? null,
|
|
5766
|
+
error: record.error ?? null,
|
|
5767
|
+
duration_ms: record.durationMs,
|
|
5768
|
+
project_dir: record.projectDir ?? null,
|
|
5769
|
+
metadata: JSON.stringify(metadata)
|
|
5483
5770
|
});
|
|
5484
|
-
setPinnedHook(name, { version, sha256: actual, source });
|
|
5485
|
-
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
5486
5771
|
}
|
|
5487
|
-
var
|
|
5772
|
+
var ALLOWED_EVENT_TYPES;
|
|
5773
|
+
var init_db_writer = __esm(() => {
|
|
5488
5774
|
init_db();
|
|
5489
|
-
|
|
5775
|
+
ALLOWED_EVENT_TYPES = new Set([
|
|
5776
|
+
"PreToolUse",
|
|
5777
|
+
"PostToolUse",
|
|
5778
|
+
"Stop",
|
|
5779
|
+
"Notification",
|
|
5780
|
+
"SessionStart",
|
|
5781
|
+
"SessionEnd",
|
|
5782
|
+
"UserPromptSubmit",
|
|
5783
|
+
"SubagentStart"
|
|
5784
|
+
]);
|
|
5490
5785
|
});
|
|
5491
5786
|
|
|
5492
5787
|
// src/lib/run.ts
|
|
5493
5788
|
var exports_run = {};
|
|
5494
5789
|
__export(exports_run, {
|
|
5495
|
-
executeVerifiedScript: () => executeVerifiedScript
|
|
5790
|
+
executeVerifiedScript: () => executeVerifiedScript,
|
|
5791
|
+
HookTimeoutError: () => HookTimeoutError
|
|
5496
5792
|
});
|
|
5497
5793
|
import { randomBytes } from "crypto";
|
|
5498
|
-
import { rmSync as
|
|
5794
|
+
import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
5499
5795
|
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
5500
5796
|
function extensionOf(scriptPath) {
|
|
5501
5797
|
const base = basename2(scriptPath);
|
|
5502
5798
|
const dot = base.lastIndexOf(".");
|
|
5503
5799
|
return dot > 0 ? base.slice(dot + 1) : "ts";
|
|
5504
5800
|
}
|
|
5801
|
+
function shebangInterpreter(firstLine) {
|
|
5802
|
+
if (!firstLine.startsWith("#!"))
|
|
5803
|
+
return null;
|
|
5804
|
+
const rest = firstLine.slice(2).trim();
|
|
5805
|
+
if (!rest)
|
|
5806
|
+
return null;
|
|
5807
|
+
const parts = rest.split(/\s+/);
|
|
5808
|
+
if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
|
|
5809
|
+
return parts[1] && parts[1] !== "-S" ? basename2(parts[1]) : parts[2] ? basename2(parts[2]) : null;
|
|
5810
|
+
}
|
|
5811
|
+
return basename2(parts[0]);
|
|
5812
|
+
}
|
|
5813
|
+
function interpreterFor(name, scriptPath, content) {
|
|
5814
|
+
const ext = extensionOf(scriptPath);
|
|
5815
|
+
const firstLine = content.subarray(0, 512).toString("utf8").split(`
|
|
5816
|
+
`, 1)[0] ?? "";
|
|
5817
|
+
const interp = shebangInterpreter(firstLine);
|
|
5818
|
+
if (interp) {
|
|
5819
|
+
if (interp === "bash" || interp === "sh")
|
|
5820
|
+
return { command: ["/bin/bash"], tempExt: "sh" };
|
|
5821
|
+
if (interp === "node" || interp === "bun")
|
|
5822
|
+
return { command: ["bun", "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
|
|
5823
|
+
throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
|
|
5824
|
+
}
|
|
5825
|
+
if (BASH_EXTENSIONS.has(ext))
|
|
5826
|
+
return { command: ["/bin/bash"], tempExt: "sh" };
|
|
5827
|
+
if (BUN_EXTENSIONS.has(ext))
|
|
5828
|
+
return { command: ["bun", "run"], tempExt: ext };
|
|
5829
|
+
throw new Error(`Refusing to run hook '${name}': unsupported script extension '.${ext}' (supported: .sh .bash .ts .tsx .js .jsx .mjs .cjs .mts .cts, or a recognized shebang)`);
|
|
5830
|
+
}
|
|
5831
|
+
async function readPipeWithDeadline(stream, deadlineMs) {
|
|
5832
|
+
const reader = stream.getReader();
|
|
5833
|
+
const chunks = [];
|
|
5834
|
+
const deadline = Date.now() + deadlineMs;
|
|
5835
|
+
try {
|
|
5836
|
+
while (true) {
|
|
5837
|
+
const remaining = deadline - Date.now();
|
|
5838
|
+
if (remaining <= 0)
|
|
5839
|
+
break;
|
|
5840
|
+
let result;
|
|
5841
|
+
try {
|
|
5842
|
+
result = await Promise.race([
|
|
5843
|
+
reader.read(),
|
|
5844
|
+
new Promise((_, reject) => {
|
|
5845
|
+
const t = setTimeout(() => reject(new Error("pipe-drain-deadline")), remaining);
|
|
5846
|
+
t.unref?.();
|
|
5847
|
+
})
|
|
5848
|
+
]);
|
|
5849
|
+
} catch {
|
|
5850
|
+
break;
|
|
5851
|
+
}
|
|
5852
|
+
if (result.done)
|
|
5853
|
+
break;
|
|
5854
|
+
chunks.push(new TextDecoder().decode(result.value));
|
|
5855
|
+
}
|
|
5856
|
+
} finally {
|
|
5857
|
+
try {
|
|
5858
|
+
await reader.cancel();
|
|
5859
|
+
} catch {}
|
|
5860
|
+
}
|
|
5861
|
+
return chunks.join("");
|
|
5862
|
+
}
|
|
5505
5863
|
async function executeVerifiedScript(options) {
|
|
5506
5864
|
const scriptDir = dirname4(options.scriptPath);
|
|
5507
|
-
const
|
|
5865
|
+
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
5866
|
+
const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${interpreter.tempExt}`);
|
|
5867
|
+
let proc = null;
|
|
5868
|
+
let timedOut = false;
|
|
5869
|
+
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
5870
|
+
timedOut = true;
|
|
5871
|
+
killGroup(proc);
|
|
5872
|
+
}, options.timeout) : null;
|
|
5508
5873
|
try {
|
|
5509
5874
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
5510
|
-
|
|
5875
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
5511
5876
|
stdin: new Response(options.stdin),
|
|
5512
5877
|
stdout: "pipe",
|
|
5513
5878
|
stderr: "pipe",
|
|
5514
5879
|
env: options.env ?? process.env,
|
|
5515
|
-
|
|
5880
|
+
detached: true
|
|
5516
5881
|
});
|
|
5517
|
-
const
|
|
5518
|
-
|
|
5519
|
-
new
|
|
5520
|
-
|
|
5882
|
+
const exitCode = await proc.exited;
|
|
5883
|
+
if (timedOut) {
|
|
5884
|
+
throw new HookTimeoutError(options.timeout);
|
|
5885
|
+
}
|
|
5886
|
+
const [stdoutText, stderrText] = await Promise.all([
|
|
5887
|
+
readPipeWithDeadline(proc.stdout, 750),
|
|
5888
|
+
readPipeWithDeadline(proc.stderr, 750)
|
|
5521
5889
|
]);
|
|
5890
|
+
killGroup(proc);
|
|
5522
5891
|
return { stdout: stdoutText, stderr: stderrText, exitCode };
|
|
5523
5892
|
} finally {
|
|
5893
|
+
if (timeoutTimer)
|
|
5894
|
+
clearTimeout(timeoutTimer);
|
|
5895
|
+
try {
|
|
5896
|
+
rmSync3(tempPath, { force: true });
|
|
5897
|
+
} catch {}
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
function killGroup(proc) {
|
|
5901
|
+
if (!proc)
|
|
5902
|
+
return;
|
|
5903
|
+
try {
|
|
5904
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
5905
|
+
} catch {
|
|
5524
5906
|
try {
|
|
5525
|
-
|
|
5907
|
+
process.kill(proc.pid, "SIGKILL");
|
|
5526
5908
|
} catch {}
|
|
5527
5909
|
}
|
|
5528
5910
|
}
|
|
5529
|
-
var
|
|
5911
|
+
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
5912
|
+
var init_run = __esm(() => {
|
|
5913
|
+
HookTimeoutError = class HookTimeoutError extends Error {
|
|
5914
|
+
timeoutMs;
|
|
5915
|
+
constructor(timeoutMs) {
|
|
5916
|
+
super(`Hook timed out after ${timeoutMs}ms (process group killed)`);
|
|
5917
|
+
this.name = "HookTimeoutError";
|
|
5918
|
+
this.timeoutMs = timeoutMs;
|
|
5919
|
+
}
|
|
5920
|
+
};
|
|
5921
|
+
BASH_EXTENSIONS = new Set(["sh", "bash"]);
|
|
5922
|
+
BUN_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]);
|
|
5923
|
+
});
|
|
5530
5924
|
|
|
5531
5925
|
// src/lib/custom-install.ts
|
|
5532
5926
|
var exports_custom_install = {};
|
|
@@ -5535,7 +5929,7 @@ __export(exports_custom_install, {
|
|
|
5535
5929
|
installCustomSource: () => installCustomSource,
|
|
5536
5930
|
ensureCustomDir: () => ensureCustomDir
|
|
5537
5931
|
});
|
|
5538
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as
|
|
5932
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
|
|
5539
5933
|
import { join as join10 } from "path";
|
|
5540
5934
|
import { tmpdir } from "os";
|
|
5541
5935
|
function manifestAt(dir) {
|
|
@@ -5573,7 +5967,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
5573
5967
|
proc.exited
|
|
5574
5968
|
]);
|
|
5575
5969
|
if (exitCode !== 0) {
|
|
5576
|
-
|
|
5970
|
+
rmSync4(tmp, { recursive: true, force: true });
|
|
5577
5971
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
5578
5972
|
}
|
|
5579
5973
|
return join10(tmp, "repo");
|
|
@@ -5588,58 +5982,275 @@ function isCustomSource(value) {
|
|
|
5588
5982
|
return true;
|
|
5589
5983
|
return existsSync10(value);
|
|
5590
5984
|
}
|
|
5591
|
-
async function installCustomSource(source) {
|
|
5592
|
-
if (isGitUrl(source)) {
|
|
5593
|
-
const repoDir = await cloneToTemp(source);
|
|
5594
|
-
try {
|
|
5595
|
-
return copyManifestDir(repoDir, source, "git");
|
|
5596
|
-
} finally {
|
|
5597
|
-
|
|
5598
|
-
}
|
|
5985
|
+
async function installCustomSource(source) {
|
|
5986
|
+
if (isGitUrl(source)) {
|
|
5987
|
+
const repoDir = await cloneToTemp(source);
|
|
5988
|
+
try {
|
|
5989
|
+
return copyManifestDir(repoDir, source, "git");
|
|
5990
|
+
} finally {
|
|
5991
|
+
rmSync4(repoDir, { recursive: true, force: true });
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
5995
|
+
return installManifestUrl(source);
|
|
5996
|
+
}
|
|
5997
|
+
if (!existsSync10(source)) {
|
|
5998
|
+
throw new Error(`No such local path: ${source}`);
|
|
5999
|
+
}
|
|
6000
|
+
if (source.endsWith("manifest.json")) {
|
|
6001
|
+
const manifest = parseManifest(readFileSync7(source, "utf-8"));
|
|
6002
|
+
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
6003
|
+
}
|
|
6004
|
+
return copyManifestDir(source, source, "local");
|
|
6005
|
+
}
|
|
6006
|
+
async function installManifestUrl(url) {
|
|
6007
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
6008
|
+
if (!res.ok) {
|
|
6009
|
+
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
6010
|
+
}
|
|
6011
|
+
const manifest = parseManifest(await res.text());
|
|
6012
|
+
let script;
|
|
6013
|
+
if (manifest.script.includes(`
|
|
6014
|
+
`)) {
|
|
6015
|
+
script = { path: "script.ts", content: manifest.script };
|
|
6016
|
+
} else {
|
|
6017
|
+
const base = url.substring(0, url.lastIndexOf("/"));
|
|
6018
|
+
const scriptUrl = `${base}/${manifest.script}`;
|
|
6019
|
+
const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
|
|
6020
|
+
if (!scriptRes.ok) {
|
|
6021
|
+
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
6022
|
+
}
|
|
6023
|
+
script = { path: manifest.script, content: await scriptRes.text() };
|
|
6024
|
+
}
|
|
6025
|
+
const name = shortManifestName(manifest.name);
|
|
6026
|
+
const { dir, scriptPath } = writeCustomHook(name, manifest, script.content, script.path);
|
|
6027
|
+
return { name, version: manifest.version, kind: "url", source: url, dir, scriptPath };
|
|
6028
|
+
}
|
|
6029
|
+
function ensureCustomDir() {
|
|
6030
|
+
const dir = getCustomHooksDir();
|
|
6031
|
+
mkdirSync7(dir, { recursive: true });
|
|
6032
|
+
return dir;
|
|
6033
|
+
}
|
|
6034
|
+
var init_custom_install = __esm(() => {
|
|
6035
|
+
init_config();
|
|
6036
|
+
init_manifest();
|
|
6037
|
+
});
|
|
6038
|
+
|
|
6039
|
+
// src/lib/sync.ts
|
|
6040
|
+
var exports_sync = {};
|
|
6041
|
+
__export(exports_sync, {
|
|
6042
|
+
syncHooks: () => syncHooks,
|
|
6043
|
+
planSync: () => planSync,
|
|
6044
|
+
fetchPinnedHook: () => fetchPinnedHook
|
|
6045
|
+
});
|
|
6046
|
+
import { createHash as createHash2 } from "crypto";
|
|
6047
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
6048
|
+
function sha256OfText(text) {
|
|
6049
|
+
return createHash2("sha256").update(text).digest("hex");
|
|
6050
|
+
}
|
|
6051
|
+
function sha256OfPath(path) {
|
|
6052
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
6053
|
+
}
|
|
6054
|
+
function collectBundledCatalog() {
|
|
6055
|
+
const out = [];
|
|
6056
|
+
for (const meta of HOOKS) {
|
|
6057
|
+
const scriptPath = resolveScriptPath(meta.name);
|
|
6058
|
+
if (!scriptPath)
|
|
6059
|
+
continue;
|
|
6060
|
+
out.push({
|
|
6061
|
+
name: meta.name,
|
|
6062
|
+
version: meta.version,
|
|
6063
|
+
sha256: sha256OfPath(scriptPath)
|
|
6064
|
+
});
|
|
6065
|
+
}
|
|
6066
|
+
return out;
|
|
6067
|
+
}
|
|
6068
|
+
async function fetchJson(base, path) {
|
|
6069
|
+
const headers = { accept: "application/json" };
|
|
6070
|
+
const apiKey = resolveApiKey();
|
|
6071
|
+
if (apiKey)
|
|
6072
|
+
headers["x-api-key"] = apiKey;
|
|
6073
|
+
const res = await fetch(`${base}${path}`, {
|
|
6074
|
+
headers,
|
|
6075
|
+
redirect: "error",
|
|
6076
|
+
signal: AbortSignal.timeout(15000)
|
|
6077
|
+
});
|
|
6078
|
+
if (res.status === 401) {
|
|
6079
|
+
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
|
|
6080
|
+
}
|
|
6081
|
+
if (!res.ok) {
|
|
6082
|
+
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6083
|
+
}
|
|
6084
|
+
return res.json();
|
|
6085
|
+
}
|
|
6086
|
+
async function fetchRemoteState(apiUrl) {
|
|
6087
|
+
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
6088
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
6089
|
+
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
6090
|
+
throw new Error("remote catalog is malformed: missing hooks array");
|
|
6091
|
+
}
|
|
6092
|
+
if (!remoteLock.hooks || typeof remoteLock.hooks !== "object") {
|
|
6093
|
+
throw new Error("remote lock is malformed: missing hooks map");
|
|
6094
|
+
}
|
|
6095
|
+
return { catalog: catalog.hooks, remoteLock };
|
|
6096
|
+
}
|
|
6097
|
+
function computeDiff(catalog, remoteLock) {
|
|
6098
|
+
const local = readLock();
|
|
6099
|
+
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
6100
|
+
for (const entry of catalog) {
|
|
6101
|
+
if (!entry.name || !entry.version || !entry.sha256)
|
|
6102
|
+
continue;
|
|
6103
|
+
const pin = local.hooks[entry.name];
|
|
6104
|
+
const remotePin = remoteLock ? remoteLock.hooks[entry.name] : entry;
|
|
6105
|
+
if (remoteLock && !remotePin) {
|
|
6106
|
+
diff.skipped.push(entry.name);
|
|
6107
|
+
continue;
|
|
6108
|
+
}
|
|
6109
|
+
const pinnedLocally = pin && pin.version === entry.version && pin.sha256 === entry.sha256;
|
|
6110
|
+
if (pinnedLocally) {
|
|
6111
|
+
diff.unchanged.push(entry.name);
|
|
6112
|
+
} else if (pin) {
|
|
6113
|
+
diff.updated.push(entry.name);
|
|
6114
|
+
} else {
|
|
6115
|
+
diff.added.push(entry.name);
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
return diff;
|
|
6119
|
+
}
|
|
6120
|
+
async function planSync() {
|
|
6121
|
+
const apiUrl = resolveApiUrl();
|
|
6122
|
+
if (apiUrl) {
|
|
6123
|
+
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6124
|
+
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
6125
|
+
}
|
|
6126
|
+
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6127
|
+
return { apiUrl: null, dryRun: false, diff };
|
|
6128
|
+
}
|
|
6129
|
+
async function syncHooks(options = {}) {
|
|
6130
|
+
const apiUrl = resolveApiUrl();
|
|
6131
|
+
const plan = await planSync();
|
|
6132
|
+
if (options.dryRun)
|
|
6133
|
+
return { ...plan, dryRun: true };
|
|
6134
|
+
if (!apiUrl) {
|
|
6135
|
+
const db2 = getDb();
|
|
6136
|
+
for (const entry of collectBundledCatalog()) {
|
|
6137
|
+
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6138
|
+
upsertHookRecord(db2, {
|
|
6139
|
+
name: entry.name,
|
|
6140
|
+
version: entry.version,
|
|
6141
|
+
sha256: entry.sha256,
|
|
6142
|
+
source_type: "bundled",
|
|
6143
|
+
source_ref: "apps/hooks/hooks",
|
|
6144
|
+
last_verified_at: new Date().toISOString()
|
|
6145
|
+
});
|
|
6146
|
+
}
|
|
6147
|
+
return plan;
|
|
6148
|
+
}
|
|
6149
|
+
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6150
|
+
const diff = computeDiff(catalog, remoteLock);
|
|
6151
|
+
const toFetch = [...diff.added, ...diff.updated];
|
|
6152
|
+
const artifacts = new Map;
|
|
6153
|
+
for (const name of toFetch) {
|
|
6154
|
+
const entry = remoteLock.hooks[name];
|
|
6155
|
+
if (!entry)
|
|
6156
|
+
continue;
|
|
6157
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
6158
|
+
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6159
|
+
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6160
|
+
}
|
|
6161
|
+
const actualSha = sha256OfText(artifact.script);
|
|
6162
|
+
if (actualSha !== entry.sha256) {
|
|
6163
|
+
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6164
|
+
}
|
|
6165
|
+
artifacts.set(name, artifact);
|
|
6166
|
+
}
|
|
6167
|
+
const db = getDb();
|
|
6168
|
+
for (const name of toFetch) {
|
|
6169
|
+
const artifact = artifacts.get(name);
|
|
6170
|
+
const entry = remoteLock.hooks[name];
|
|
6171
|
+
if (!artifact || !entry)
|
|
6172
|
+
continue;
|
|
6173
|
+
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6174
|
+
const scriptRel = artifact.manifest.script.includes(`
|
|
6175
|
+
`) ? "script.ts" : artifact.manifest.script;
|
|
6176
|
+
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6177
|
+
const scriptPath = resolveScriptPath(name);
|
|
6178
|
+
const actual = await sha256File(scriptPath);
|
|
6179
|
+
if (actual !== entry.sha256) {
|
|
6180
|
+
throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6181
|
+
}
|
|
6182
|
+
setPinnedHook(name, { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" });
|
|
6183
|
+
upsertHookRecord(db, {
|
|
6184
|
+
name,
|
|
6185
|
+
version: entry.version,
|
|
6186
|
+
sha256: entry.sha256,
|
|
6187
|
+
source_type: entry.source ?? "remote",
|
|
6188
|
+
source_ref: apiUrl,
|
|
6189
|
+
last_verified_at: new Date().toISOString()
|
|
6190
|
+
});
|
|
6191
|
+
}
|
|
6192
|
+
return { apiUrl, dryRun: false, diff };
|
|
6193
|
+
}
|
|
6194
|
+
async function fetchPinnedHook(name, version, apiUrl) {
|
|
6195
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
6196
|
+
const entry = remoteLock.hooks?.[name];
|
|
6197
|
+
if (!entry) {
|
|
6198
|
+
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
5599
6199
|
}
|
|
5600
|
-
if (
|
|
5601
|
-
|
|
6200
|
+
if (entry.version !== version) {
|
|
6201
|
+
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${entry.version})`);
|
|
5602
6202
|
}
|
|
5603
|
-
|
|
5604
|
-
|
|
6203
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}`);
|
|
6204
|
+
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6205
|
+
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
5605
6206
|
}
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
6207
|
+
const manifestName = shortManifestName(artifact.manifest.name ?? name);
|
|
6208
|
+
if (manifestName !== name) {
|
|
6209
|
+
throw new Error(`artifact for '${name}@${version}' declares a different hook name ('${manifestName}')`);
|
|
5609
6210
|
}
|
|
5610
|
-
|
|
5611
|
-
}
|
|
5612
|
-
async function installManifestUrl(url) {
|
|
5613
|
-
const res = await fetch(url, { redirect: "follow" });
|
|
5614
|
-
if (!res.ok) {
|
|
5615
|
-
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
6211
|
+
if (artifact.manifest.version !== version) {
|
|
6212
|
+
throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
|
|
5616
6213
|
}
|
|
5617
|
-
const
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
`)) {
|
|
5621
|
-
script = { path: "script.ts", content: manifest.script };
|
|
5622
|
-
} else {
|
|
5623
|
-
const base = url.substring(0, url.lastIndexOf("/"));
|
|
5624
|
-
const scriptUrl = `${base}/${manifest.script}`;
|
|
5625
|
-
const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
|
|
5626
|
-
if (!scriptRes.ok) {
|
|
5627
|
-
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
5628
|
-
}
|
|
5629
|
-
script = { path: manifest.script, content: await scriptRes.text() };
|
|
6214
|
+
const actualSha = sha256OfText(artifact.script);
|
|
6215
|
+
if (actualSha !== entry.sha256) {
|
|
6216
|
+
throw new Error(`sha256 mismatch for '${name}@${version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
5630
6217
|
}
|
|
5631
|
-
const
|
|
5632
|
-
const
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
const
|
|
5637
|
-
|
|
5638
|
-
|
|
6218
|
+
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6219
|
+
const scriptRel = artifact.manifest.script.includes(`
|
|
6220
|
+
`) ? "script.ts" : artifact.manifest.script;
|
|
6221
|
+
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6222
|
+
const scriptPath = resolveScriptPath(name);
|
|
6223
|
+
const actual = await sha256File(scriptPath);
|
|
6224
|
+
if (actual !== entry.sha256) {
|
|
6225
|
+
throw new Error(`sha256 mismatch after write for '${name}@${version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6226
|
+
}
|
|
6227
|
+
const db = getDb();
|
|
6228
|
+
setPinnedHook(name, { version, sha256: entry.sha256, source: entry.source ?? "remote" });
|
|
6229
|
+
upsertHookRecord(db, {
|
|
6230
|
+
name,
|
|
6231
|
+
version,
|
|
6232
|
+
sha256: entry.sha256,
|
|
6233
|
+
source_type: entry.source ?? "remote",
|
|
6234
|
+
source_ref: apiUrl,
|
|
6235
|
+
last_verified_at: new Date().toISOString()
|
|
6236
|
+
});
|
|
6237
|
+
return {
|
|
6238
|
+
name,
|
|
6239
|
+
version,
|
|
6240
|
+
sha256: entry.sha256,
|
|
6241
|
+
source: entry.source ?? "remote",
|
|
6242
|
+
source_ref: apiUrl,
|
|
6243
|
+
artifact,
|
|
6244
|
+
scriptPath
|
|
6245
|
+
};
|
|
5639
6246
|
}
|
|
5640
|
-
var
|
|
6247
|
+
var init_sync = __esm(() => {
|
|
6248
|
+
init_registry();
|
|
5641
6249
|
init_config();
|
|
6250
|
+
init_db();
|
|
6251
|
+
init_store();
|
|
5642
6252
|
init_manifest();
|
|
6253
|
+
init_resolve();
|
|
5643
6254
|
});
|
|
5644
6255
|
|
|
5645
6256
|
// src/db/pg-migrations.ts
|
|
@@ -6149,234 +6760,75 @@ function authorized(req, apiKey) {
|
|
|
6149
6760
|
return secureEqual(header.slice("Bearer ".length), apiKey);
|
|
6150
6761
|
return secureEqual(req.headers.get("x-api-key") ?? "", apiKey);
|
|
6151
6762
|
}
|
|
6152
|
-
function json(body, status = 200, headers = {}) {
|
|
6153
|
-
return new Response(JSON.stringify(body), {
|
|
6154
|
-
status,
|
|
6155
|
-
headers: { "content-type": "application/json", ...headers }
|
|
6156
|
-
});
|
|
6157
|
-
}
|
|
6158
|
-
function handleServeRequest(req, apiKey) {
|
|
6159
|
-
const url = new URL(req.url);
|
|
6160
|
-
if (url.pathname === "/health" && req.method === "GET") {
|
|
6161
|
-
return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
|
|
6162
|
-
}
|
|
6163
|
-
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
6164
|
-
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
6165
|
-
}
|
|
6166
|
-
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
6167
|
-
return Promise.resolve(json(readLock()));
|
|
6168
|
-
}
|
|
6169
|
-
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
|
|
6170
|
-
if (artifactMatch && req.method === "GET") {
|
|
6171
|
-
const [, name, version] = artifactMatch;
|
|
6172
|
-
return artifactFor(name, version).then(async (artifact) => {
|
|
6173
|
-
if (!artifact)
|
|
6174
|
-
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
6175
|
-
const resolved = resolveHook(name);
|
|
6176
|
-
const sha = resolved ? await sha256File(resolved.scriptPath) : artifact.manifest.name;
|
|
6177
|
-
return json(artifact, 200, { "x-hook-sha256": sha });
|
|
6178
|
-
});
|
|
6179
|
-
}
|
|
6180
|
-
if (url.pathname === "/api/v1/hooks" && req.method === "PUT") {
|
|
6181
|
-
if (!authorized(req, apiKey)) {
|
|
6182
|
-
return Promise.resolve(json({ error: "unauthorized: valid API key required to publish" }, 401));
|
|
6183
|
-
}
|
|
6184
|
-
return req.json().then((body) => {
|
|
6185
|
-
const name = body.name ?? "";
|
|
6186
|
-
const version = body.version ?? "";
|
|
6187
|
-
const resolved = resolveHook(name);
|
|
6188
|
-
if (!resolved)
|
|
6189
|
-
return json({ error: `Hook '${name}' not found in local store` }, 404);
|
|
6190
|
-
if (version && version !== resolved.version) {
|
|
6191
|
-
return json({ error: `Version mismatch: local '${name}' is ${resolved.version}, requested ${version}` }, 409);
|
|
6192
|
-
}
|
|
6193
|
-
const check = retrustHook(name, resolved.scriptPath, resolved.version, "serve");
|
|
6194
|
-
return json({ ok: true, hook: { name, version: resolved.version, sha256: check.actual } });
|
|
6195
|
-
}, () => json({ error: "invalid JSON body" }, 400));
|
|
6196
|
-
}
|
|
6197
|
-
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
6198
|
-
}
|
|
6199
|
-
function startServeServer(options) {
|
|
6200
|
-
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
6201
|
-
const host = options.host ?? SERVE_HOST;
|
|
6202
|
-
const apiKey = resolveApiKey(options.apiKey);
|
|
6203
|
-
const server = Bun.serve({
|
|
6204
|
-
hostname: host,
|
|
6205
|
-
port,
|
|
6206
|
-
async fetch(req) {
|
|
6207
|
-
return handleServeRequest(req, apiKey);
|
|
6208
|
-
}
|
|
6209
|
-
});
|
|
6210
|
-
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
6211
|
-
return server;
|
|
6212
|
-
}
|
|
6213
|
-
var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
|
|
6214
|
-
var init_serve = __esm(() => {
|
|
6215
|
-
init_registry();
|
|
6216
|
-
init_manifest();
|
|
6217
|
-
init_store();
|
|
6218
|
-
init_resolve();
|
|
6219
|
-
init_config();
|
|
6220
|
-
if (false) {}
|
|
6221
|
-
});
|
|
6222
|
-
|
|
6223
|
-
// src/lib/sync.ts
|
|
6224
|
-
var exports_sync = {};
|
|
6225
|
-
__export(exports_sync, {
|
|
6226
|
-
syncHooks: () => syncHooks,
|
|
6227
|
-
planSync: () => planSync
|
|
6228
|
-
});
|
|
6229
|
-
import { createHash as createHash2 } from "crypto";
|
|
6230
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
6231
|
-
function sha256OfText(text) {
|
|
6232
|
-
return createHash2("sha256").update(text).digest("hex");
|
|
6233
|
-
}
|
|
6234
|
-
function sha256OfPath(path) {
|
|
6235
|
-
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
6236
|
-
}
|
|
6237
|
-
function collectBundledCatalog() {
|
|
6238
|
-
const out = [];
|
|
6239
|
-
for (const meta of HOOKS) {
|
|
6240
|
-
const scriptPath = resolveScriptPath(meta.name);
|
|
6241
|
-
if (!scriptPath)
|
|
6242
|
-
continue;
|
|
6243
|
-
out.push({
|
|
6244
|
-
name: meta.name,
|
|
6245
|
-
version: meta.version,
|
|
6246
|
-
sha256: sha256OfPath(scriptPath)
|
|
6247
|
-
});
|
|
6248
|
-
}
|
|
6249
|
-
return out;
|
|
6250
|
-
}
|
|
6251
|
-
async function fetchJson(base, path) {
|
|
6252
|
-
const headers = { accept: "application/json" };
|
|
6253
|
-
const apiKey = resolveApiKey();
|
|
6254
|
-
if (apiKey)
|
|
6255
|
-
headers["x-api-key"] = apiKey;
|
|
6256
|
-
const res = await fetch(`${base}${path}`, {
|
|
6257
|
-
headers,
|
|
6258
|
-
signal: AbortSignal.timeout(15000)
|
|
6259
|
-
});
|
|
6260
|
-
if (res.status === 401) {
|
|
6261
|
-
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
|
|
6262
|
-
}
|
|
6263
|
-
if (!res.ok) {
|
|
6264
|
-
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6265
|
-
}
|
|
6266
|
-
return res.json();
|
|
6267
|
-
}
|
|
6268
|
-
async function fetchRemoteState(apiUrl) {
|
|
6269
|
-
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
6270
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
6271
|
-
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
6272
|
-
throw new Error("remote catalog is malformed: missing hooks array");
|
|
6273
|
-
}
|
|
6274
|
-
if (!remoteLock.hooks || typeof remoteLock.hooks !== "object") {
|
|
6275
|
-
throw new Error("remote lock is malformed: missing hooks map");
|
|
6276
|
-
}
|
|
6277
|
-
return { catalog: catalog.hooks, remoteLock };
|
|
6278
|
-
}
|
|
6279
|
-
function computeDiff(catalog, remoteLock) {
|
|
6280
|
-
const local = readLock();
|
|
6281
|
-
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
6282
|
-
for (const entry of catalog) {
|
|
6283
|
-
if (!entry.name || !entry.version || !entry.sha256)
|
|
6284
|
-
continue;
|
|
6285
|
-
const pin = local.hooks[entry.name];
|
|
6286
|
-
const remotePin = remoteLock ? remoteLock.hooks[entry.name] : entry;
|
|
6287
|
-
if (remoteLock && !remotePin) {
|
|
6288
|
-
diff.skipped.push(entry.name);
|
|
6289
|
-
continue;
|
|
6290
|
-
}
|
|
6291
|
-
const pinnedLocally = pin && pin.version === entry.version && pin.sha256 === entry.sha256;
|
|
6292
|
-
if (pinnedLocally) {
|
|
6293
|
-
diff.unchanged.push(entry.name);
|
|
6294
|
-
} else if (pin) {
|
|
6295
|
-
diff.updated.push(entry.name);
|
|
6296
|
-
} else {
|
|
6297
|
-
diff.added.push(entry.name);
|
|
6298
|
-
}
|
|
6299
|
-
}
|
|
6300
|
-
return diff;
|
|
6301
|
-
}
|
|
6302
|
-
async function planSync() {
|
|
6303
|
-
const apiUrl = resolveApiUrl();
|
|
6304
|
-
if (apiUrl) {
|
|
6305
|
-
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6306
|
-
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
6307
|
-
}
|
|
6308
|
-
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6309
|
-
return { apiUrl: null, dryRun: false, diff };
|
|
6310
|
-
}
|
|
6311
|
-
async function syncHooks(options = {}) {
|
|
6312
|
-
const apiUrl = resolveApiUrl();
|
|
6313
|
-
const plan = await planSync();
|
|
6314
|
-
if (options.dryRun)
|
|
6315
|
-
return { ...plan, dryRun: true };
|
|
6316
|
-
if (!apiUrl) {
|
|
6317
|
-
const db2 = getDb();
|
|
6318
|
-
for (const entry of collectBundledCatalog()) {
|
|
6319
|
-
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6320
|
-
upsertHookRecord(db2, {
|
|
6321
|
-
name: entry.name,
|
|
6322
|
-
version: entry.version,
|
|
6323
|
-
sha256: entry.sha256,
|
|
6324
|
-
source_type: "bundled",
|
|
6325
|
-
source_ref: "apps/hooks/hooks",
|
|
6326
|
-
last_verified_at: new Date().toISOString()
|
|
6327
|
-
});
|
|
6328
|
-
}
|
|
6329
|
-
return plan;
|
|
6330
|
-
}
|
|
6331
|
-
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6332
|
-
const diff = computeDiff(catalog, remoteLock);
|
|
6333
|
-
const toFetch = [...diff.added, ...diff.updated];
|
|
6334
|
-
const artifacts = new Map;
|
|
6335
|
-
for (const name of toFetch) {
|
|
6336
|
-
const entry = remoteLock.hooks[name];
|
|
6337
|
-
if (!entry)
|
|
6338
|
-
continue;
|
|
6339
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
6340
|
-
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6341
|
-
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6342
|
-
}
|
|
6343
|
-
const actualSha = sha256OfText(artifact.script);
|
|
6344
|
-
if (actualSha !== entry.sha256) {
|
|
6345
|
-
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6346
|
-
}
|
|
6347
|
-
artifacts.set(name, artifact);
|
|
6763
|
+
function json(body, status = 200, headers = {}) {
|
|
6764
|
+
return new Response(JSON.stringify(body), {
|
|
6765
|
+
status,
|
|
6766
|
+
headers: { "content-type": "application/json", ...headers }
|
|
6767
|
+
});
|
|
6768
|
+
}
|
|
6769
|
+
function handleServeRequest(req, apiKey) {
|
|
6770
|
+
const url = new URL(req.url);
|
|
6771
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
6772
|
+
return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
|
|
6348
6773
|
}
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
version: entry.version,
|
|
6365
|
-
sha256: actual,
|
|
6366
|
-
source_type: entry.source ?? "remote",
|
|
6367
|
-
source_ref: apiUrl,
|
|
6368
|
-
last_verified_at: new Date().toISOString()
|
|
6774
|
+
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
6775
|
+
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
6776
|
+
}
|
|
6777
|
+
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
6778
|
+
return Promise.resolve(json(readLock()));
|
|
6779
|
+
}
|
|
6780
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
|
|
6781
|
+
if (artifactMatch && req.method === "GET") {
|
|
6782
|
+
const [, name, version] = artifactMatch;
|
|
6783
|
+
return artifactFor(name, version).then(async (artifact) => {
|
|
6784
|
+
if (!artifact)
|
|
6785
|
+
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
6786
|
+
const resolved = resolveHook(name);
|
|
6787
|
+
const sha = resolved ? await sha256File(resolved.scriptPath) : artifact.manifest.name;
|
|
6788
|
+
return json(artifact, 200, { "x-hook-sha256": sha });
|
|
6369
6789
|
});
|
|
6370
6790
|
}
|
|
6371
|
-
|
|
6791
|
+
if (url.pathname === "/api/v1/hooks" && req.method === "PUT") {
|
|
6792
|
+
if (!authorized(req, apiKey)) {
|
|
6793
|
+
return Promise.resolve(json({ error: "unauthorized: valid API key required to publish" }, 401));
|
|
6794
|
+
}
|
|
6795
|
+
return req.json().then((body) => {
|
|
6796
|
+
const name = body.name ?? "";
|
|
6797
|
+
const version = body.version ?? "";
|
|
6798
|
+
const resolved = resolveHook(name);
|
|
6799
|
+
if (!resolved)
|
|
6800
|
+
return json({ error: `Hook '${name}' not found in local store` }, 404);
|
|
6801
|
+
if (version && version !== resolved.version) {
|
|
6802
|
+
return json({ error: `Version mismatch: local '${name}' is ${resolved.version}, requested ${version}` }, 409);
|
|
6803
|
+
}
|
|
6804
|
+
const check = retrustHook(name, resolved.scriptPath, resolved.version, "serve");
|
|
6805
|
+
return json({ ok: true, hook: { name, version: resolved.version, sha256: check.actual } });
|
|
6806
|
+
}, () => json({ error: "invalid JSON body" }, 400));
|
|
6807
|
+
}
|
|
6808
|
+
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
6372
6809
|
}
|
|
6373
|
-
|
|
6810
|
+
function startServeServer(options) {
|
|
6811
|
+
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
6812
|
+
const host = options.host ?? SERVE_HOST;
|
|
6813
|
+
const apiKey = resolveApiKey(options.apiKey);
|
|
6814
|
+
const server = Bun.serve({
|
|
6815
|
+
hostname: host,
|
|
6816
|
+
port,
|
|
6817
|
+
async fetch(req) {
|
|
6818
|
+
return handleServeRequest(req, apiKey);
|
|
6819
|
+
}
|
|
6820
|
+
});
|
|
6821
|
+
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
6822
|
+
return server;
|
|
6823
|
+
}
|
|
6824
|
+
var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
|
|
6825
|
+
var init_serve = __esm(() => {
|
|
6374
6826
|
init_registry();
|
|
6375
|
-
init_config();
|
|
6376
|
-
init_db();
|
|
6377
|
-
init_store();
|
|
6378
6827
|
init_manifest();
|
|
6828
|
+
init_store();
|
|
6379
6829
|
init_resolve();
|
|
6830
|
+
init_config();
|
|
6831
|
+
if (false) {}
|
|
6380
6832
|
});
|
|
6381
6833
|
|
|
6382
6834
|
// src/cf/provision.ts
|
|
@@ -6476,14 +6928,77 @@ import { createServer } from "http";
|
|
|
6476
6928
|
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
6477
6929
|
import { join as join11, dirname as dirname5 } from "path";
|
|
6478
6930
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6479
|
-
async function runVerifiedHook(name,
|
|
6480
|
-
const
|
|
6481
|
-
const
|
|
6931
|
+
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
6932
|
+
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
6933
|
+
const resolved = resolveHook2(name);
|
|
6934
|
+
const hookScript = resolveScriptPath2(name);
|
|
6935
|
+
if (!resolved || !hookScript || !existsSync11(hookScript)) {
|
|
6936
|
+
throw new Error(`Hook '${name}' not found`);
|
|
6937
|
+
}
|
|
6938
|
+
const content = readFileSync9(hookScript);
|
|
6939
|
+
const sha = sha256Of(content);
|
|
6940
|
+
const check = checkScriptHash(name, sha);
|
|
6482
6941
|
if (!check.ok) {
|
|
6483
6942
|
throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
|
|
6484
6943
|
}
|
|
6485
|
-
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
6486
|
-
|
|
6944
|
+
const { executeVerifiedScript: executeVerifiedScript2, HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
6945
|
+
const started = Date.now();
|
|
6946
|
+
let ran;
|
|
6947
|
+
try {
|
|
6948
|
+
ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, env: process.env, timeout: timeoutMs });
|
|
6949
|
+
} catch (err) {
|
|
6950
|
+
try {
|
|
6951
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
6952
|
+
let inputJson = {};
|
|
6953
|
+
try {
|
|
6954
|
+
inputJson = JSON.parse(stdin);
|
|
6955
|
+
} catch {}
|
|
6956
|
+
recordHookRun2({
|
|
6957
|
+
hookName: name,
|
|
6958
|
+
eventType: resolveEventType2(inputJson.hook_event_name, resolved.events[0] ?? "PostToolUse"),
|
|
6959
|
+
version: resolved.version,
|
|
6960
|
+
sha256: sha,
|
|
6961
|
+
sessionId: typeof inputJson.session_id === "string" ? inputJson.session_id : null,
|
|
6962
|
+
toolName: typeof inputJson.tool_name === "string" ? inputJson.tool_name : null,
|
|
6963
|
+
toolInput: inputJson.tool_input,
|
|
6964
|
+
error: err instanceof Error ? err.message.slice(0, 500) : String(err),
|
|
6965
|
+
exitCode: -1,
|
|
6966
|
+
durationMs: Date.now() - started,
|
|
6967
|
+
projectDir: process.cwd()
|
|
6968
|
+
});
|
|
6969
|
+
} catch {}
|
|
6970
|
+
if (err instanceof HookTimeoutError2)
|
|
6971
|
+
throw err;
|
|
6972
|
+
throw err;
|
|
6973
|
+
}
|
|
6974
|
+
const durationMs = Date.now() - started;
|
|
6975
|
+
try {
|
|
6976
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
6977
|
+
let inputJson = {};
|
|
6978
|
+
try {
|
|
6979
|
+
inputJson = JSON.parse(stdin);
|
|
6980
|
+
} catch {}
|
|
6981
|
+
let outputJson = {};
|
|
6982
|
+
try {
|
|
6983
|
+
outputJson = JSON.parse(ran.stdout);
|
|
6984
|
+
} catch {}
|
|
6985
|
+
const blocked = outputJson.decision === "block" || outputJson.continue === false;
|
|
6986
|
+
recordHookRun2({
|
|
6987
|
+
hookName: name,
|
|
6988
|
+
eventType: resolveEventType2(inputJson.hook_event_name, resolved.events[0] ?? "PostToolUse"),
|
|
6989
|
+
version: resolved.version,
|
|
6990
|
+
sha256: sha,
|
|
6991
|
+
sessionId: typeof inputJson.session_id === "string" ? inputJson.session_id : null,
|
|
6992
|
+
toolName: typeof inputJson.tool_name === "string" ? inputJson.tool_name : null,
|
|
6993
|
+
toolInput: inputJson.tool_input,
|
|
6994
|
+
result: blocked ? "block" : "continue",
|
|
6995
|
+
error: ran.exitCode !== 0 ? (ran.stderr || `hook exited with code ${ran.exitCode}`).slice(0, 500) : null,
|
|
6996
|
+
exitCode: ran.exitCode,
|
|
6997
|
+
durationMs,
|
|
6998
|
+
projectDir: process.cwd()
|
|
6999
|
+
});
|
|
7000
|
+
} catch {}
|
|
7001
|
+
return ran;
|
|
6487
7002
|
}
|
|
6488
7003
|
function formatInstallResults(results, extra) {
|
|
6489
7004
|
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
@@ -6765,17 +7280,14 @@ function createHooksServer() {
|
|
|
6765
7280
|
name: z2.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')"),
|
|
6766
7281
|
input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input as JSON object (HookInput)"),
|
|
6767
7282
|
profile: z2.string().optional().describe("Agent profile ID to inject into hook input"),
|
|
6768
|
-
timeout_ms: z2.number().
|
|
7283
|
+
timeout_ms: z2.number().int().positive().max(600000).optional().describe("Timeout in milliseconds (default: the hook's manifest timeout_ms or 10000)")
|
|
6769
7284
|
}, async ({ name, input, profile, timeout_ms }) => {
|
|
6770
|
-
const
|
|
6771
|
-
|
|
7285
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
7286
|
+
const resolved = resolveHook2(name);
|
|
7287
|
+
if (!resolved) {
|
|
6772
7288
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
6773
7289
|
}
|
|
6774
|
-
const
|
|
6775
|
-
const hookScript = join11(hookDir, "src", "hook.ts");
|
|
6776
|
-
if (!existsSync11(hookScript)) {
|
|
6777
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
|
|
6778
|
-
}
|
|
7290
|
+
const effectiveTimeout = timeout_ms ?? resolved.timeoutMs ?? 1e4;
|
|
6779
7291
|
let hookInput = { ...input };
|
|
6780
7292
|
if (profile) {
|
|
6781
7293
|
const p = getProfile(profile);
|
|
@@ -6788,13 +7300,38 @@ function createHooksServer() {
|
|
|
6788
7300
|
};
|
|
6789
7301
|
}
|
|
6790
7302
|
}
|
|
6791
|
-
let ran = null;
|
|
6792
7303
|
try {
|
|
6793
|
-
ran = await
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
7304
|
+
const ran = await runVerifiedHook(name, JSON.stringify(hookInput), effectiveTimeout);
|
|
7305
|
+
const stdoutText = ran.stdout;
|
|
7306
|
+
const stderrText = ran.stderr;
|
|
7307
|
+
let output = {};
|
|
7308
|
+
try {
|
|
7309
|
+
output = JSON.parse(stdoutText);
|
|
7310
|
+
} catch {
|
|
7311
|
+
output = stdoutText ? { raw: stdoutText } : {};
|
|
7312
|
+
}
|
|
7313
|
+
return {
|
|
7314
|
+
content: [{
|
|
7315
|
+
type: "text",
|
|
7316
|
+
text: JSON.stringify({
|
|
7317
|
+
hook: name,
|
|
7318
|
+
output,
|
|
7319
|
+
stderr: stderrText || undefined,
|
|
7320
|
+
exitCode: ran.exitCode,
|
|
7321
|
+
timeout_ms: effectiveTimeout
|
|
7322
|
+
})
|
|
7323
|
+
}]
|
|
7324
|
+
};
|
|
6797
7325
|
} catch (err) {
|
|
7326
|
+
const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7327
|
+
if (err instanceof HookTimeoutError2) {
|
|
7328
|
+
return {
|
|
7329
|
+
content: [{
|
|
7330
|
+
type: "text",
|
|
7331
|
+
text: JSON.stringify({ hook: name, timedOut: true, timeout_ms: effectiveTimeout, error: err.message })
|
|
7332
|
+
}]
|
|
7333
|
+
};
|
|
7334
|
+
}
|
|
6798
7335
|
return {
|
|
6799
7336
|
content: [{
|
|
6800
7337
|
type: "text",
|
|
@@ -6802,27 +7339,6 @@ function createHooksServer() {
|
|
|
6802
7339
|
}]
|
|
6803
7340
|
};
|
|
6804
7341
|
}
|
|
6805
|
-
const timedOut = ran === null;
|
|
6806
|
-
const stdoutText = ran?.stdout ?? "";
|
|
6807
|
-
const stderrText = ran?.stderr ?? "";
|
|
6808
|
-
let output = {};
|
|
6809
|
-
try {
|
|
6810
|
-
output = JSON.parse(stdoutText);
|
|
6811
|
-
} catch {
|
|
6812
|
-
output = stdoutText ? { raw: stdoutText } : {};
|
|
6813
|
-
}
|
|
6814
|
-
return {
|
|
6815
|
-
content: [{
|
|
6816
|
-
type: "text",
|
|
6817
|
-
text: JSON.stringify({
|
|
6818
|
-
hook: name,
|
|
6819
|
-
output,
|
|
6820
|
-
stderr: stderrText || undefined,
|
|
6821
|
-
exitCode: ran?.exitCode ?? -1,
|
|
6822
|
-
...timedOut ? { timedOut: true, timeout_ms } : {}
|
|
6823
|
-
})
|
|
6824
|
-
}]
|
|
6825
|
-
};
|
|
6826
7342
|
});
|
|
6827
7343
|
defineTool("hooks_update", "Re-register installed hooks to pick up new package version (reinstalls with overwrite)", {
|
|
6828
7344
|
hooks: z2.array(z2.string()).optional().describe("Hook names to update (omit to update all installed hooks)"),
|
|
@@ -6883,11 +7399,11 @@ function createHooksServer() {
|
|
|
6883
7399
|
tool_name: z2.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
|
|
6884
7400
|
tool_input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
|
|
6885
7401
|
scope: z2.enum(["global", "project"]).default("global").describe("Scope to check"),
|
|
6886
|
-
timeout_ms: z2.number().
|
|
7402
|
+
timeout_ms: z2.number().int().positive().max(600000).optional().describe("Per-hook timeout in milliseconds (default: the hook's manifest timeout_ms or 5000)")
|
|
6887
7403
|
}, async ({ tool_name, tool_input, scope, timeout_ms }) => {
|
|
6888
7404
|
const registered = getRegisteredHooks(scope);
|
|
6889
7405
|
const matchingHooks = registered.filter((name) => {
|
|
6890
|
-
const meta = getHook(name);
|
|
7406
|
+
const meta = resolveHookMeta(name) ?? getHook(name);
|
|
6891
7407
|
if (!meta || meta.event !== "PreToolUse")
|
|
6892
7408
|
return false;
|
|
6893
7409
|
if (!meta.matcher)
|
|
@@ -6903,26 +7419,23 @@ function createHooksServer() {
|
|
|
6903
7419
|
}
|
|
6904
7420
|
const input = { tool_name, tool_input };
|
|
6905
7421
|
const results = await Promise.all(matchingHooks.map(async (name) => {
|
|
6906
|
-
const
|
|
6907
|
-
const
|
|
6908
|
-
|
|
6909
|
-
return { name, decision: "approve", error: "script not found" };
|
|
6910
|
-
let ran = null;
|
|
7422
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
7423
|
+
const resolved = resolveHook2(name);
|
|
7424
|
+
const effectiveTimeout = timeout_ms ?? resolved?.timeoutMs ?? 5000;
|
|
6911
7425
|
try {
|
|
6912
|
-
ran = await
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
7426
|
+
const ran = await runVerifiedHook(name, JSON.stringify(input), effectiveTimeout);
|
|
7427
|
+
let output = {};
|
|
7428
|
+
try {
|
|
7429
|
+
output = JSON.parse(ran.stdout);
|
|
7430
|
+
} catch {}
|
|
7431
|
+
return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
|
|
6916
7432
|
} catch (err) {
|
|
7433
|
+
const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7434
|
+
if (err instanceof HookTimeoutError2) {
|
|
7435
|
+
return { name, decision: "approve", timedOut: true, timeout_ms: effectiveTimeout };
|
|
7436
|
+
}
|
|
6917
7437
|
return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
|
|
6918
7438
|
}
|
|
6919
|
-
if (ran === null)
|
|
6920
|
-
return { name, decision: "approve", timedOut: true };
|
|
6921
|
-
let output = {};
|
|
6922
|
-
try {
|
|
6923
|
-
output = JSON.parse(ran.stdout);
|
|
6924
|
-
} catch {}
|
|
6925
|
-
return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
|
|
6926
7439
|
}));
|
|
6927
7440
|
const blocked = results.find((r) => r.decision === "block");
|
|
6928
7441
|
return {
|
|
@@ -6962,33 +7475,31 @@ function createHooksServer() {
|
|
|
6962
7475
|
name: z2.string().describe("Hook name"),
|
|
6963
7476
|
input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input JSON")
|
|
6964
7477
|
})).describe("List of hooks to run with their inputs"),
|
|
6965
|
-
timeout_ms: z2.number().
|
|
7478
|
+
timeout_ms: z2.number().int().positive().max(600000).optional().describe("Per-hook timeout in milliseconds (default: the hook's manifest timeout_ms or 10000)")
|
|
6966
7479
|
}, async ({ hooks, timeout_ms }) => {
|
|
6967
7480
|
const results = await Promise.all(hooks.map(async ({ name, input }) => {
|
|
6968
|
-
const
|
|
6969
|
-
|
|
7481
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
7482
|
+
const resolved = resolveHook2(name);
|
|
7483
|
+
if (!resolved)
|
|
6970
7484
|
return { name, error: `Hook '${name}' not found` };
|
|
6971
|
-
const
|
|
6972
|
-
if (!existsSync11(hookScript))
|
|
6973
|
-
return { name, error: "script not found" };
|
|
6974
|
-
let ran = null;
|
|
7485
|
+
const effectiveTimeout = timeout_ms ?? resolved.timeoutMs ?? 1e4;
|
|
6975
7486
|
try {
|
|
6976
|
-
ran = await
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
7487
|
+
const ran = await runVerifiedHook(name, JSON.stringify(input), effectiveTimeout);
|
|
7488
|
+
const stdoutText = ran.stdout;
|
|
7489
|
+
let output = {};
|
|
7490
|
+
try {
|
|
7491
|
+
output = JSON.parse(stdoutText);
|
|
7492
|
+
} catch {
|
|
7493
|
+
output = stdoutText ? { raw: stdoutText } : {};
|
|
7494
|
+
}
|
|
7495
|
+
return { name, output, exitCode: ran.exitCode, timeout_ms: effectiveTimeout };
|
|
6980
7496
|
} catch (err) {
|
|
7497
|
+
const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7498
|
+
if (err instanceof HookTimeoutError2) {
|
|
7499
|
+
return { name, error: err.message, timedOut: true, timeout_ms: effectiveTimeout };
|
|
7500
|
+
}
|
|
6981
7501
|
return { name, error: err instanceof Error ? err.message : String(err) };
|
|
6982
7502
|
}
|
|
6983
|
-
const stdoutText = ran?.stdout ?? "";
|
|
6984
|
-
const timedOut = ran === null;
|
|
6985
|
-
let output = {};
|
|
6986
|
-
try {
|
|
6987
|
-
output = JSON.parse(stdoutText);
|
|
6988
|
-
} catch {
|
|
6989
|
-
output = stdoutText ? { raw: stdoutText } : {};
|
|
6990
|
-
}
|
|
6991
|
-
return { name, output, exitCode: ran?.exitCode ?? -1, ...timedOut ? { timedOut: true } : {} };
|
|
6992
7503
|
}));
|
|
6993
7504
|
return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }) }] };
|
|
6994
7505
|
});
|
|
@@ -8618,7 +9129,7 @@ var {
|
|
|
8618
9129
|
|
|
8619
9130
|
// src/cli/index.tsx
|
|
8620
9131
|
import chalk2 from "chalk";
|
|
8621
|
-
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
9132
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync5 } from "fs";
|
|
8622
9133
|
import { join as join12, dirname as dirname6 } from "path";
|
|
8623
9134
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8624
9135
|
|
|
@@ -9797,6 +10308,7 @@ init_installer();
|
|
|
9797
10308
|
init_profiles();
|
|
9798
10309
|
init_manifest();
|
|
9799
10310
|
init_resolve();
|
|
10311
|
+
init_store();
|
|
9800
10312
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
9801
10313
|
var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
|
|
9802
10314
|
var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
|
|
@@ -9849,6 +10361,22 @@ function hookSummaryLine(hook, options = {}) {
|
|
|
9849
10361
|
const description = options.verbose ? ` - ${truncateText2(hook.description, 110)}` : "";
|
|
9850
10362
|
return ` ${chalk2.cyan(hook.name.padEnd(17))} ${chalk2.dim(`[${hook.event}${matcher}]`)} ${chalk2.dim(hook.category)}${description}`;
|
|
9851
10363
|
}
|
|
10364
|
+
function listStoreHooks() {
|
|
10365
|
+
const out = [];
|
|
10366
|
+
for (const parsed of listCustomHooks()) {
|
|
10367
|
+
const name = parsed.manifest.name;
|
|
10368
|
+
const pin = getPinnedHook(name);
|
|
10369
|
+
const source = pin?.source === "remote" || pin?.source === "registry" ? "registry" : "custom";
|
|
10370
|
+
const meta = resolveHookMeta(name);
|
|
10371
|
+
if (!meta)
|
|
10372
|
+
continue;
|
|
10373
|
+
out.push({
|
|
10374
|
+
meta: { ...meta, version: pin?.version ?? meta.version },
|
|
10375
|
+
source
|
|
10376
|
+
});
|
|
10377
|
+
}
|
|
10378
|
+
return out.sort((a, b) => a.meta.name.localeCompare(b.meta.name));
|
|
10379
|
+
}
|
|
9852
10380
|
function printDisclosureHint(hidden, detailCommand, options = {}) {
|
|
9853
10381
|
const rowControls = options.includeAll ? "--limit, --all, --verbose" : "--limit, --verbose";
|
|
9854
10382
|
if (hidden > 0) {
|
|
@@ -9889,24 +10417,23 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
9889
10417
|
console.log(chalk2.red(message));
|
|
9890
10418
|
return;
|
|
9891
10419
|
}
|
|
9892
|
-
const
|
|
10420
|
+
const apiKeyRef = options.apiKey ?? "hasna/hooks/live/api-key";
|
|
10421
|
+
const config = { api_url: options.apiUrl.replace(/\/+$/, ""), api_key_ref: apiKeyRef };
|
|
9893
10422
|
const path = writeConfig2(config);
|
|
9894
10423
|
if (options.json) {
|
|
9895
|
-
console.log(JSON.stringify({ ok: true, api_url: config.api_url, api_key_ref: config.api_key_ref
|
|
10424
|
+
console.log(JSON.stringify({ ok: true, api_url: config.api_url, api_key_ref: config.api_key_ref, config_path: path }));
|
|
9896
10425
|
return;
|
|
9897
10426
|
}
|
|
9898
10427
|
console.log(chalk2.green(`
|
|
9899
10428
|
\u2713 Remote registry configured
|
|
9900
10429
|
`));
|
|
9901
10430
|
console.log(` ${chalk2.dim("API URL:")} ${config.api_url}`);
|
|
9902
|
-
console.log(` ${chalk2.dim("API key ref:")} ${config.api_key_ref
|
|
10431
|
+
console.log(` ${chalk2.dim("API key ref:")} ${config.api_key_ref}`);
|
|
9903
10432
|
console.log(` ${chalk2.dim("Config:")} ${path}`);
|
|
9904
10433
|
console.log();
|
|
9905
10434
|
console.log(chalk2.dim(" Presence of an API URL selects the remote registry; absence means local."));
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
console.log(` secrets exec ${config.api_key_ref} --as HASNA_HOOKS_API_KEY -- hooks serve`);
|
|
9909
|
-
}
|
|
10435
|
+
console.log(chalk2.dim(` Run the server with the key resolved from the vault, never the value:`));
|
|
10436
|
+
console.log(` secrets exec ${config.api_key_ref} --as HASNA_HOOKS_API_KEY -- hooks serve`);
|
|
9910
10437
|
console.log();
|
|
9911
10438
|
return;
|
|
9912
10439
|
}
|
|
@@ -9942,6 +10469,7 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
9942
10469
|
program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
|
|
9943
10470
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
9944
10471
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10472
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
9945
10473
|
const resolved = resolveHook2(hook);
|
|
9946
10474
|
if (!resolved) {
|
|
9947
10475
|
console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
@@ -9953,7 +10481,8 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
9953
10481
|
process.exit(1);
|
|
9954
10482
|
}
|
|
9955
10483
|
const content = readFileSync10(hookScript);
|
|
9956
|
-
const
|
|
10484
|
+
const sha = sha256Of2(content);
|
|
10485
|
+
const check = checkScriptHash2(hook, sha);
|
|
9957
10486
|
if (!check.ok) {
|
|
9958
10487
|
console.error(JSON.stringify({
|
|
9959
10488
|
error: `Hook '${hook}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${hook}' to trust the new content.`,
|
|
@@ -9982,18 +10511,74 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
9982
10511
|
}
|
|
9983
10512
|
}
|
|
9984
10513
|
const { readCustomManifest: readCustomManifest2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
9985
|
-
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
10514
|
+
const { executeVerifiedScript: executeVerifiedScript2, HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
9986
10515
|
const custom = readCustomManifest2(hook);
|
|
9987
10516
|
const args = custom?.manifest.args ?? [];
|
|
9988
10517
|
const timeout = custom?.manifest.timeout_ms;
|
|
9989
|
-
const
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
10518
|
+
const started = Date.now();
|
|
10519
|
+
let stdout = "";
|
|
10520
|
+
let stderr = "";
|
|
10521
|
+
let exitCode = 0;
|
|
10522
|
+
try {
|
|
10523
|
+
({ stdout, stderr, exitCode } = await executeVerifiedScript2({
|
|
10524
|
+
name: hook,
|
|
10525
|
+
scriptPath: hookScript,
|
|
10526
|
+
content,
|
|
10527
|
+
args,
|
|
10528
|
+
stdin: hookStdin,
|
|
10529
|
+
env: process.env,
|
|
10530
|
+
timeout
|
|
10531
|
+
}));
|
|
10532
|
+
} catch (err) {
|
|
10533
|
+
if (err instanceof HookTimeoutError2) {
|
|
10534
|
+
try {
|
|
10535
|
+
const { recordHookRun: recordHookRun3, resolveEventType: resolveEventType3 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
10536
|
+
let inputJson2 = {};
|
|
10537
|
+
try {
|
|
10538
|
+
inputJson2 = JSON.parse(stdin);
|
|
10539
|
+
} catch {}
|
|
10540
|
+
recordHookRun3({
|
|
10541
|
+
hookName: hook,
|
|
10542
|
+
eventType: resolveEventType3(inputJson2.hook_event_name, resolved.events[0] ?? "PostToolUse"),
|
|
10543
|
+
version: resolved.version,
|
|
10544
|
+
sha256: sha,
|
|
10545
|
+
sessionId: typeof inputJson2.session_id === "string" ? inputJson2.session_id : null,
|
|
10546
|
+
toolName: typeof inputJson2.tool_name === "string" ? inputJson2.tool_name : null,
|
|
10547
|
+
toolInput: inputJson2.tool_input,
|
|
10548
|
+
error: err.message.slice(0, 500),
|
|
10549
|
+
exitCode: -1,
|
|
10550
|
+
durationMs: Date.now() - started,
|
|
10551
|
+
projectDir: process.cwd()
|
|
10552
|
+
});
|
|
10553
|
+
} catch {}
|
|
10554
|
+
console.error(JSON.stringify({ error: err.message, hook, timedOut: true, timeout_ms: timeout ?? null }));
|
|
10555
|
+
process.exit(1);
|
|
10556
|
+
}
|
|
10557
|
+
throw err;
|
|
10558
|
+
}
|
|
10559
|
+
const durationMs = Date.now() - started;
|
|
10560
|
+
let inputJson = {};
|
|
10561
|
+
try {
|
|
10562
|
+
inputJson = JSON.parse(stdin);
|
|
10563
|
+
} catch {}
|
|
10564
|
+
let outputJson = {};
|
|
10565
|
+
try {
|
|
10566
|
+
outputJson = JSON.parse(stdout);
|
|
10567
|
+
} catch {}
|
|
10568
|
+
const blocked = outputJson.decision === "block" || outputJson.continue === false;
|
|
10569
|
+
recordHookRun2({
|
|
10570
|
+
hookName: hook,
|
|
10571
|
+
eventType: resolveEventType2(inputJson.hook_event_name, resolved.events[0] ?? "PostToolUse"),
|
|
10572
|
+
version: resolved.version,
|
|
10573
|
+
sha256: sha,
|
|
10574
|
+
sessionId: typeof inputJson.session_id === "string" ? inputJson.session_id : null,
|
|
10575
|
+
toolName: typeof inputJson.tool_name === "string" ? inputJson.tool_name : null,
|
|
10576
|
+
toolInput: inputJson.tool_input,
|
|
10577
|
+
result: blocked ? "block" : "continue",
|
|
10578
|
+
error: exitCode !== 0 ? (stderr || `hook exited with code ${exitCode}`).slice(0, 500) : null,
|
|
10579
|
+
exitCode,
|
|
10580
|
+
durationMs,
|
|
10581
|
+
projectDir: process.cwd()
|
|
9997
10582
|
});
|
|
9998
10583
|
if (stdout)
|
|
9999
10584
|
process.stdout.write(stdout);
|
|
@@ -10001,7 +10586,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
10001
10586
|
process.stderr.write(stderr);
|
|
10002
10587
|
process.exit(exitCode);
|
|
10003
10588
|
});
|
|
10004
|
-
program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer open-configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, or
|
|
10589
|
+
program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer open-configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
|
|
10005
10590
|
const scope = resolveScope(options);
|
|
10006
10591
|
const target = resolveTarget(options);
|
|
10007
10592
|
let toInstall = hooks;
|
|
@@ -10034,15 +10619,35 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
10034
10619
|
return;
|
|
10035
10620
|
}
|
|
10036
10621
|
const { isCustomSource: isCustomSource2, installCustomSource: installCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
10622
|
+
const { readCustomManifest: readCustomManifest2, HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
10623
|
+
const { resolveApiUrl: resolveApiUrl2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
10624
|
+
const { sha256Of: sha256Of2, pinInstalledHook: pinInstalledHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10625
|
+
const { readFileSync: readFileSyncFs } = await import("fs");
|
|
10626
|
+
function parseNameVersion(arg) {
|
|
10627
|
+
if (isCustomSource2(arg))
|
|
10628
|
+
return null;
|
|
10629
|
+
const at = arg.lastIndexOf("@");
|
|
10630
|
+
if (at <= 0 || at === arg.length - 1)
|
|
10631
|
+
return null;
|
|
10632
|
+
const name = arg.slice(0, at);
|
|
10633
|
+
const version = arg.slice(at + 1);
|
|
10634
|
+
if (!HOOK_NAME_RE2.test(name))
|
|
10635
|
+
return null;
|
|
10636
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
|
|
10637
|
+
return null;
|
|
10638
|
+
return { name, version };
|
|
10639
|
+
}
|
|
10640
|
+
const pinnedRequests = toInstall.map((arg) => ({ arg, pinned: parseNameVersion(arg) })).filter((x) => x.pinned !== null);
|
|
10037
10641
|
const customSources = toInstall.filter((n) => !getHook(n) && isCustomSource2(n));
|
|
10038
10642
|
if (options.dryRun) {
|
|
10039
10643
|
const known = toInstall.filter((n) => getHook(n));
|
|
10040
|
-
const unknown = toInstall.filter((n) => !getHook(n) && !isCustomSource2(n));
|
|
10644
|
+
const unknown = toInstall.filter((n) => !getHook(n) && !isCustomSource2(n) && !parseNameVersion(n) && !readCustomManifest2(n));
|
|
10041
10645
|
if (options.json) {
|
|
10042
10646
|
console.log(JSON.stringify({
|
|
10043
10647
|
dryRun: true,
|
|
10044
10648
|
would_install: known,
|
|
10045
10649
|
custom_sources: customSources,
|
|
10650
|
+
pinned: pinnedRequests.map((p) => `${p.pinned.name}@${p.pinned.version}`),
|
|
10046
10651
|
unknown,
|
|
10047
10652
|
scope,
|
|
10048
10653
|
target,
|
|
@@ -10060,6 +10665,9 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10060
10665
|
for (const source of customSources) {
|
|
10061
10666
|
console.log(chalk2.cyan(` ${source}`) + chalk2.dim(" [custom source: would fetch and install]"));
|
|
10062
10667
|
}
|
|
10668
|
+
for (const p of pinnedRequests) {
|
|
10669
|
+
console.log(chalk2.cyan(` ${p.pinned.name}@${p.pinned.version}`) + chalk2.dim(" [pinned registry install]"));
|
|
10670
|
+
}
|
|
10063
10671
|
if (unknown.length > 0) {
|
|
10064
10672
|
console.log();
|
|
10065
10673
|
for (const name of unknown) {
|
|
@@ -10074,9 +10682,21 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10074
10682
|
for (const source of customSources) {
|
|
10075
10683
|
try {
|
|
10076
10684
|
const custom = await installCustomSource2(source);
|
|
10685
|
+
try {
|
|
10686
|
+
const scriptBytes = readFileSyncFs(custom.scriptPath);
|
|
10687
|
+
pinInstalledHook2(custom.name, custom.version, sha256Of2(scriptBytes), "custom", source);
|
|
10688
|
+
} catch (pinError) {
|
|
10689
|
+
const { customHookDir: customHookDir2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
10690
|
+
const { removeHookFromStore: removeHookFromStore2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10691
|
+
try {
|
|
10692
|
+
rmSync5(customHookDir2(custom.name), { recursive: true, force: true });
|
|
10693
|
+
} catch {}
|
|
10694
|
+
removeHookFromStore2(custom.name);
|
|
10695
|
+
throw pinError;
|
|
10696
|
+
}
|
|
10077
10697
|
installedCustom.push(custom.name);
|
|
10078
10698
|
if (options.json) {
|
|
10079
|
-
console.log(JSON.stringify({ custom: { source, name: custom.name, version: custom.version } }));
|
|
10699
|
+
console.log(JSON.stringify({ custom: { source, name: custom.name, version: custom.version, pinned: true } }));
|
|
10080
10700
|
} else {
|
|
10081
10701
|
console.log(chalk2.green(`\u2713 Installed custom hook '${custom.name}' v${custom.version} from ${source}`));
|
|
10082
10702
|
}
|
|
@@ -10090,9 +10710,37 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10090
10710
|
}
|
|
10091
10711
|
}
|
|
10092
10712
|
}
|
|
10093
|
-
const
|
|
10713
|
+
const installedPinned = [];
|
|
10714
|
+
for (const { arg, pinned } of pinnedRequests) {
|
|
10715
|
+
try {
|
|
10716
|
+
const apiUrl = resolveApiUrl2();
|
|
10717
|
+
if (!apiUrl)
|
|
10718
|
+
throw new Error(`Cannot install '${arg}': no remote registry configured (set api_url or HASNA_HOOKS_API_URL)`);
|
|
10719
|
+
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
10720
|
+
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, apiUrl);
|
|
10721
|
+
installedPinned.push(pinnedInstall.name);
|
|
10722
|
+
if (options.json) {
|
|
10723
|
+
console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
|
|
10724
|
+
} else {
|
|
10725
|
+
console.log(chalk2.green(`\u2713 Installed '${pinnedInstall.name}' v${pinnedInstall.version} from registry (sha256 ${pinnedInstall.sha256.slice(0, 12)}\u2026)`));
|
|
10726
|
+
}
|
|
10727
|
+
} catch (error) {
|
|
10728
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10729
|
+
results.push({ hook: arg, success: false, error: message });
|
|
10730
|
+
if (options.json) {
|
|
10731
|
+
console.log(JSON.stringify({ error: `Pinned install failed for ${arg}: ${message}` }));
|
|
10732
|
+
} else {
|
|
10733
|
+
console.log(chalk2.red(`\u2717 ${message}`));
|
|
10734
|
+
}
|
|
10735
|
+
}
|
|
10736
|
+
}
|
|
10737
|
+
const namesToRegister = [...new Set([
|
|
10738
|
+
...toInstall.filter((n) => !isCustomSource2(n) && !parseNameVersion(n)),
|
|
10739
|
+
...installedCustom,
|
|
10740
|
+
...installedPinned
|
|
10741
|
+
])];
|
|
10094
10742
|
for (const name of namesToRegister) {
|
|
10095
|
-
if (!getHook(name) && !installedCustom.includes(name)) {
|
|
10743
|
+
if (!getHook(name) && !installedCustom.includes(name) && !installedPinned.includes(name) && !readCustomManifest2(name)) {
|
|
10096
10744
|
const suggestions = suggestHooks(name);
|
|
10097
10745
|
const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
|
|
10098
10746
|
results.push({ hook: name, success: false, error: `Hook '${name}' not found${hint}` });
|
|
@@ -10119,8 +10767,12 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10119
10767
|
target,
|
|
10120
10768
|
applied: results.some((r) => r.applied)
|
|
10121
10769
|
}));
|
|
10770
|
+
if (results.length > 0 && results.every((r) => !r.success)) {
|
|
10771
|
+
process.exitCode = 1;
|
|
10772
|
+
}
|
|
10122
10773
|
return;
|
|
10123
10774
|
}
|
|
10775
|
+
const successCount = results.filter((r) => r.success).length;
|
|
10124
10776
|
const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (open-configs should apply)" : scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
|
|
10125
10777
|
console.log(chalk2.bold(`
|
|
10126
10778
|
Installing hooks (${scope}, ${target})...
|
|
@@ -10147,38 +10799,59 @@ Installing hooks (${scope}, ${target})...
|
|
|
10147
10799
|
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
10148
10800
|
}
|
|
10149
10801
|
}
|
|
10802
|
+
if (results.length > 0 && successCount === 0) {
|
|
10803
|
+
console.log(chalk2.red(`
|
|
10804
|
+
\u2717 Nothing was registered \u2014 all ${results.length} hook(s) failed.`));
|
|
10805
|
+
process.exitCode = 1;
|
|
10806
|
+
return;
|
|
10807
|
+
}
|
|
10150
10808
|
console.log(chalk2.dim(`
|
|
10151
10809
|
Registered in ${settingsFile}`));
|
|
10152
10810
|
});
|
|
10153
|
-
program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
|
|
10811
|
+
program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks (bundled + custom/registry)").action((options) => {
|
|
10154
10812
|
const scope = resolveScope(options);
|
|
10155
10813
|
const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
|
|
10814
|
+
const storeHooks = listStoreHooks();
|
|
10156
10815
|
if (options.registered || options.installed) {
|
|
10157
10816
|
const target = options.target === "gemini" ? "gemini" : options.target === "codewith" ? "codewith" : "claude";
|
|
10158
10817
|
const registered = getRegisteredHooksForTarget(scope, target);
|
|
10818
|
+
const installed = [...new Set([...registered, ...storeHooks.map((s) => s.meta.name)])];
|
|
10819
|
+
const shown = options.registered ? registered : installed;
|
|
10159
10820
|
if (options.json) {
|
|
10160
|
-
console.log(JSON.stringify(
|
|
10161
|
-
const
|
|
10162
|
-
|
|
10821
|
+
console.log(JSON.stringify(shown.map((name) => {
|
|
10822
|
+
const store = storeHooks.find((s) => s.meta.name === name);
|
|
10823
|
+
const meta = store?.meta ?? getHook(name);
|
|
10824
|
+
return {
|
|
10825
|
+
name,
|
|
10826
|
+
event: meta?.event,
|
|
10827
|
+
version: meta?.version,
|
|
10828
|
+
description: meta?.description,
|
|
10829
|
+
source: store?.source ?? (store ? "custom" : undefined),
|
|
10830
|
+
scope,
|
|
10831
|
+
target
|
|
10832
|
+
};
|
|
10163
10833
|
})));
|
|
10164
10834
|
return;
|
|
10165
10835
|
}
|
|
10166
|
-
if (
|
|
10167
|
-
console.log(chalk2.dim(`No hooks registered (${scope}, ${target})`));
|
|
10836
|
+
if (shown.length === 0) {
|
|
10837
|
+
console.log(chalk2.dim(`No hooks ${options.registered ? "registered" : "installed"} (${scope}, ${target})`));
|
|
10168
10838
|
return;
|
|
10169
10839
|
}
|
|
10170
|
-
const
|
|
10840
|
+
const label = options.registered ? "Registered" : "Installed";
|
|
10841
|
+
const visible2 = shown.slice(0, limit);
|
|
10171
10842
|
console.log(chalk2.bold(`
|
|
10172
|
-
|
|
10843
|
+
${label} hooks \u2014 ${scope}/${target} (${shown.length}, showing ${visible2.length}):
|
|
10173
10844
|
`));
|
|
10174
10845
|
for (const name of visible2) {
|
|
10175
|
-
const
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10846
|
+
const store = storeHooks.find((s) => s.meta.name === name);
|
|
10847
|
+
const meta = store?.meta ?? getHook(name);
|
|
10848
|
+
if (meta) {
|
|
10849
|
+
console.log(hookSummaryLine(meta, { verbose: options.verbose }) + (store ? chalk2.dim(` (${store.source})`) : ""));
|
|
10850
|
+
} else {
|
|
10179
10851
|
console.log(` ${chalk2.cyan(name)} ${chalk2.dim("[unknown]")}`);
|
|
10852
|
+
}
|
|
10180
10853
|
}
|
|
10181
|
-
printDisclosureHint(
|
|
10854
|
+
printDisclosureHint(shown.length - visible2.length, "hooks info <name>", { includeAll: true });
|
|
10182
10855
|
return;
|
|
10183
10856
|
}
|
|
10184
10857
|
if (options.category) {
|
|
@@ -10211,6 +10884,18 @@ ${category} (${hooks.length}, showing ${visible2.length}):
|
|
|
10211
10884
|
for (const category of CATEGORIES) {
|
|
10212
10885
|
result[category] = getHooksByCategory(category);
|
|
10213
10886
|
}
|
|
10887
|
+
result["Custom / Registry"] = storeHooks.map((s) => ({
|
|
10888
|
+
name: s.meta.name,
|
|
10889
|
+
displayName: s.meta.displayName,
|
|
10890
|
+
description: s.meta.description,
|
|
10891
|
+
version: s.meta.version,
|
|
10892
|
+
category: "Custom / Registry",
|
|
10893
|
+
event: s.meta.event,
|
|
10894
|
+
events: s.meta.events,
|
|
10895
|
+
matcher: s.meta.matcher,
|
|
10896
|
+
tags: [...new Set([...s.meta.tags, s.source])],
|
|
10897
|
+
source: s.source
|
|
10898
|
+
}));
|
|
10214
10899
|
console.log(JSON.stringify(result));
|
|
10215
10900
|
return;
|
|
10216
10901
|
}
|
|
@@ -10221,6 +10906,16 @@ Available hooks (${HOOKS.length}, showing ${visible.length}):
|
|
|
10221
10906
|
for (const h of visible)
|
|
10222
10907
|
console.log(hookSummaryLine(h, { verbose: options.verbose }));
|
|
10223
10908
|
printDisclosureHint(HOOKS.length - visible.length, "hooks info <name>", { includeAll: true });
|
|
10909
|
+
if (storeHooks.length > 0) {
|
|
10910
|
+
const storeVisible = storeHooks.slice(0, limit);
|
|
10911
|
+
console.log(chalk2.bold(`
|
|
10912
|
+
Custom / Registry hooks (${storeHooks.length}, showing ${storeVisible.length}):
|
|
10913
|
+
`));
|
|
10914
|
+
for (const s of storeVisible) {
|
|
10915
|
+
console.log(hookSummaryLine(s.meta, { verbose: options.verbose }) + chalk2.dim(` (${s.source})`));
|
|
10916
|
+
}
|
|
10917
|
+
printDisclosureHint(storeHooks.length - storeVisible.length, "hooks info <name>", { includeAll: true });
|
|
10918
|
+
}
|
|
10224
10919
|
});
|
|
10225
10920
|
program2.command("search").argument("<query>", "Search term").option("-n, --limit <n>", "Max rows to show in compact output", "10").option("--verbose", "Show descriptions for search results", false).option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
|
|
10226
10921
|
const results = searchHooks(query);
|
|
@@ -10241,28 +10936,40 @@ Found ${results.length} hook(s), showing ${visible.length}:
|
|
|
10241
10936
|
console.log(hookSummaryLine(h, { verbose: options.verbose }));
|
|
10242
10937
|
printDisclosureHint(results.length - visible.length, "hooks info <name>");
|
|
10243
10938
|
});
|
|
10244
|
-
program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
|
|
10939
|
+
program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook (settings registration + store + lock + DB)").action(async (hook, options) => {
|
|
10245
10940
|
const scope = resolveScope(options);
|
|
10246
10941
|
const target = resolveTarget(options);
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
console.log(JSON.stringify({ hook, removed: false, scope, target, error: `Hook '${hook}' not found${hint}`, suggestions }));
|
|
10252
|
-
} else {
|
|
10253
|
-
console.log(chalk2.red(`\u2717 Hook '${hook}' not found${hint}`));
|
|
10254
|
-
}
|
|
10255
|
-
return;
|
|
10942
|
+
const { uninstallHook: uninstallHook2 } = await Promise.resolve().then(() => (init_installer(), exports_installer));
|
|
10943
|
+
const result = uninstallHook2(hook, scope, target);
|
|
10944
|
+
if (!result.removed) {
|
|
10945
|
+
process.exitCode = 1;
|
|
10256
10946
|
}
|
|
10257
|
-
const removed = removeHook(hook, scope, target);
|
|
10258
10947
|
if (options.json) {
|
|
10259
|
-
console.log(JSON.stringify({
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10948
|
+
console.log(JSON.stringify({
|
|
10949
|
+
hook: result.name,
|
|
10950
|
+
removed: result.removed,
|
|
10951
|
+
source: result.source,
|
|
10952
|
+
settings_scopes: result.settingsScopes,
|
|
10953
|
+
store_dir_removed: result.storeDirRemoved,
|
|
10954
|
+
pin_removed: result.pinRemoved,
|
|
10955
|
+
db_record_removed: result.dbRecordRemoved,
|
|
10956
|
+
registrations_remaining: result.registrationsRemaining,
|
|
10957
|
+
scope,
|
|
10958
|
+
target,
|
|
10959
|
+
...result.error ? { error: result.error } : {}
|
|
10960
|
+
}));
|
|
10961
|
+
} else if (result.removed) {
|
|
10962
|
+
console.log(chalk2.green(`\u2713 Removed ${result.name} (${scope}, ${target})`));
|
|
10963
|
+
if (result.source === "custom" && result.storeDirRemoved) {
|
|
10964
|
+
console.log(chalk2.dim(` store dir removed; lock pin ${result.pinRemoved ? "removed" : "absent"}; DB record ${result.dbRecordRemoved ? "removed" : "absent"}`));
|
|
10965
|
+
}
|
|
10966
|
+
if (result.registrationsRemaining && result.registrationsRemaining.length > 0) {
|
|
10967
|
+
console.log(chalk2.yellow(` \u26A0 registrations remaining: ${result.registrationsRemaining.join(", ")} \u2014 remove them in that target's own config`));
|
|
10968
|
+
}
|
|
10264
10969
|
} else {
|
|
10265
|
-
|
|
10970
|
+
const suggestions = suggestHooks(hook);
|
|
10971
|
+
const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
|
|
10972
|
+
console.log(chalk2.red(`\u2717 Hook '${hook}' not found${hint}`));
|
|
10266
10973
|
}
|
|
10267
10974
|
});
|
|
10268
10975
|
program2.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
|
|
@@ -10406,7 +11113,7 @@ Hook Health Check (${scope})
|
|
|
10406
11113
|
}
|
|
10407
11114
|
console.log();
|
|
10408
11115
|
});
|
|
10409
|
-
program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version)
|
|
11116
|
+
program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version), refresh lock pins, or install a pinned registry version (<name>@<version>)").action(async (hooks, options) => {
|
|
10410
11117
|
const scope = resolveScope(options);
|
|
10411
11118
|
const installed = getInstalledHooks(scope);
|
|
10412
11119
|
const toUpdate = hooks.length > 0 ? hooks : installed;
|
|
@@ -10416,19 +11123,60 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
10416
11123
|
} else {
|
|
10417
11124
|
console.log(chalk2.dim("No hooks installed to update."));
|
|
10418
11125
|
}
|
|
11126
|
+
process.exitCode = 1;
|
|
10419
11127
|
return;
|
|
10420
11128
|
}
|
|
10421
11129
|
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
10422
11130
|
const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10423
11131
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
11132
|
+
const { resolveApiUrl: resolveApiUrl2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
11133
|
+
const { isCustomSource: isCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
11134
|
+
const { HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
11135
|
+
function parseNameVersion(arg) {
|
|
11136
|
+
if (isCustomSource2(arg))
|
|
11137
|
+
return null;
|
|
11138
|
+
const at = arg.lastIndexOf("@");
|
|
11139
|
+
if (at <= 0 || at === arg.length - 1)
|
|
11140
|
+
return null;
|
|
11141
|
+
const name = arg.slice(0, at);
|
|
11142
|
+
const version = arg.slice(at + 1);
|
|
11143
|
+
if (!HOOK_NAME_RE2.test(name))
|
|
11144
|
+
return null;
|
|
11145
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
|
|
11146
|
+
return null;
|
|
11147
|
+
return { name, version };
|
|
11148
|
+
}
|
|
10424
11149
|
const results = [];
|
|
10425
|
-
for (const
|
|
10426
|
-
|
|
10427
|
-
|
|
11150
|
+
for (const arg of toUpdate) {
|
|
11151
|
+
const pinned = parseNameVersion(arg);
|
|
11152
|
+
if (pinned) {
|
|
11153
|
+
try {
|
|
11154
|
+
const apiUrl = resolveApiUrl2();
|
|
11155
|
+
if (!apiUrl)
|
|
11156
|
+
throw new Error(`Cannot update '${arg}': no remote registry configured (set api_url or HASNA_HOOKS_API_URL)`);
|
|
11157
|
+
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
11158
|
+
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, apiUrl);
|
|
11159
|
+
const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
|
|
11160
|
+
if (!registered.success) {
|
|
11161
|
+
results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
|
|
11162
|
+
continue;
|
|
11163
|
+
}
|
|
11164
|
+
results.push({
|
|
11165
|
+
hook: pinnedInstall.name,
|
|
11166
|
+
success: true,
|
|
11167
|
+
pinned: { version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source }
|
|
11168
|
+
});
|
|
11169
|
+
} catch (error) {
|
|
11170
|
+
results.push({ hook: arg, success: false, error: error instanceof Error ? error.message : String(error) });
|
|
11171
|
+
}
|
|
10428
11172
|
continue;
|
|
10429
11173
|
}
|
|
10430
|
-
|
|
10431
|
-
|
|
11174
|
+
if (!installed.includes(arg)) {
|
|
11175
|
+
results.push({ hook: arg, success: false, error: "Not installed" });
|
|
11176
|
+
continue;
|
|
11177
|
+
}
|
|
11178
|
+
const result = installHook(arg, { scope, overwrite: true });
|
|
11179
|
+
const resolved = resolveHook2(arg);
|
|
10432
11180
|
if (result.success && resolved) {
|
|
10433
11181
|
const hash = await sha256File2(resolved.scriptPath);
|
|
10434
11182
|
setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
|
|
@@ -10449,6 +11197,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
10449
11197
|
updated: results.filter((r) => r.success).map((r) => r.hook),
|
|
10450
11198
|
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }))
|
|
10451
11199
|
}));
|
|
11200
|
+
if (results.some((r) => !r.success)) {
|
|
11201
|
+
process.exitCode = 1;
|
|
11202
|
+
}
|
|
10452
11203
|
return;
|
|
10453
11204
|
}
|
|
10454
11205
|
console.log(chalk2.bold(`
|
|
@@ -10461,6 +11212,9 @@ Updating hooks...
|
|
|
10461
11212
|
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
10462
11213
|
}
|
|
10463
11214
|
}
|
|
11215
|
+
if (results.some((r) => !r.success)) {
|
|
11216
|
+
process.exitCode = 1;
|
|
11217
|
+
}
|
|
10464
11218
|
});
|
|
10465
11219
|
program2.command("docs").argument("[hook]", "Hook name (shows general docs if omitted)").option("--verbose", "Print full hook README content", false).option("-j, --json", "Output as JSON", false).description("Show documentation for hooks").action((hook, options) => {
|
|
10466
11220
|
if (hook) {
|