@hasna/hooks 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/index.js +2141 -1398
- 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 +671 -194
- 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,1126 +4378,1420 @@ 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]);
|
|
5344
|
+
}
|
|
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
|
+
}
|
|
5052
5361
|
}
|
|
5053
|
-
db.exec("COMMIT");
|
|
5054
|
-
} catch (err) {
|
|
5055
|
-
db.exec("ROLLBACK");
|
|
5056
|
-
throw err;
|
|
5057
5362
|
}
|
|
5363
|
+
return [...new Set(registered)];
|
|
5058
5364
|
}
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
}
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
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;
|
|
5068
5374
|
}
|
|
5375
|
+
if (target === "codewith") {
|
|
5376
|
+
return false;
|
|
5377
|
+
}
|
|
5378
|
+
return removeHookForTarget(shortName, scope, target);
|
|
5069
5379
|
}
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
source_ref TEXT,
|
|
5078
|
-
installed_at TEXT NOT NULL,
|
|
5079
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
5080
|
-
last_verified_at TEXT
|
|
5081
|
-
)
|
|
5082
|
-
`, CREATE_HOOKS_INDEXES;
|
|
5083
|
-
var init_004_hooks_table = __esm(() => {
|
|
5084
|
-
CREATE_HOOKS_INDEXES = [
|
|
5085
|
-
`CREATE INDEX IF NOT EXISTS idx_hooks_name ON hooks (name)`
|
|
5086
|
-
];
|
|
5087
|
-
});
|
|
5088
|
-
|
|
5089
|
-
// src/db/migrations/index.ts
|
|
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));
|
|
5380
|
+
function removeHookForTarget(name, scope, target) {
|
|
5381
|
+
const registered = getRegisteredHooksForTarget(scope, target);
|
|
5382
|
+
if (!registered.includes(name)) {
|
|
5383
|
+
return false;
|
|
5384
|
+
}
|
|
5385
|
+
unregisterHook(name, scope, target);
|
|
5386
|
+
return true;
|
|
5101
5387
|
}
|
|
5102
|
-
function
|
|
5103
|
-
|
|
5104
|
-
const
|
|
5105
|
-
|
|
5106
|
-
|
|
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
5658
|
}
|
|
5423
|
-
function
|
|
5424
|
-
const
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
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
|
-
upsertHookRecord(db, {
|
|
5463
|
-
name,
|
|
5464
|
-
version: record?.version ?? pin?.version ?? "0.0.0",
|
|
5465
|
-
sha256: expected,
|
|
5466
|
-
source_type: record?.source_type ?? pin?.source ?? "local",
|
|
5467
|
-
last_verified_at: now2
|
|
5468
|
-
});
|
|
5469
|
-
return { ok: true, pinned: true, expected, actual, name };
|
|
5688
|
+
return { imported, skipped };
|
|
5470
5689
|
}
|
|
5471
|
-
|
|
5472
|
-
|
|
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);
|
|
5705
|
+
}
|
|
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);
|
|
@@ -5532,33 +5828,120 @@ function interpreterFor(name, scriptPath, content) {
|
|
|
5532
5828
|
return { command: ["bun", "run"], tempExt: ext };
|
|
5533
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)`);
|
|
5534
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
|
+
}
|
|
5535
5863
|
async function executeVerifiedScript(options) {
|
|
5536
5864
|
const scriptDir = dirname4(options.scriptPath);
|
|
5537
5865
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
5538
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, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
5872
|
+
}, options.timeout) : null;
|
|
5539
5873
|
try {
|
|
5540
5874
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
5541
|
-
|
|
5875
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
5542
5876
|
stdin: new Response(options.stdin),
|
|
5543
5877
|
stdout: "pipe",
|
|
5544
5878
|
stderr: "pipe",
|
|
5545
5879
|
env: options.env ?? process.env,
|
|
5546
|
-
|
|
5880
|
+
detached: true
|
|
5547
5881
|
});
|
|
5548
|
-
const
|
|
5549
|
-
|
|
5550
|
-
new
|
|
5551
|
-
|
|
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)
|
|
5552
5889
|
]);
|
|
5890
|
+
killGroup(proc, { leaderAlive: false });
|
|
5553
5891
|
return { stdout: stdoutText, stderr: stderrText, exitCode };
|
|
5554
5892
|
} finally {
|
|
5893
|
+
if (timeoutTimer)
|
|
5894
|
+
clearTimeout(timeoutTimer);
|
|
5895
|
+
try {
|
|
5896
|
+
rmSync3(tempPath, { force: true });
|
|
5897
|
+
} catch {}
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
function killGroup(proc, options) {
|
|
5901
|
+
if (!proc)
|
|
5902
|
+
return;
|
|
5903
|
+
const pgid = proc.pid;
|
|
5904
|
+
for (let pass = 0;pass < 5; pass++) {
|
|
5905
|
+
let members = [];
|
|
5906
|
+
try {
|
|
5907
|
+
const res = Bun.spawnSync(["ps", "-eo", "pid=,pgid="], { stdout: "pipe", stderr: "pipe" });
|
|
5908
|
+
if (res.exitCode === 0) {
|
|
5909
|
+
members = res.stdout.toString().split(`
|
|
5910
|
+
`).map((line) => line.trim().split(/\s+/)).filter((cols) => cols.length === 2 && cols[1] === String(pgid)).map((cols) => Number(cols[0])).filter((pid) => Number.isInteger(pid) && pid > 1).filter((pid) => options.leaderAlive || pid !== proc.pid);
|
|
5911
|
+
}
|
|
5912
|
+
} catch {}
|
|
5913
|
+
if (members.length === 0)
|
|
5914
|
+
break;
|
|
5915
|
+
let killed = 0;
|
|
5916
|
+
for (const pid of members) {
|
|
5917
|
+
try {
|
|
5918
|
+
const probe = Bun.spawnSync(["ps", "-o", "pid=,pgid=", "-p", String(pid)], { stdout: "pipe", stderr: "pipe" });
|
|
5919
|
+
const cols = probe.stdout.toString().trim().split(/\s+/);
|
|
5920
|
+
if (cols.length === 2 && cols[1] === String(pgid)) {
|
|
5921
|
+
process.kill(pid, "SIGKILL");
|
|
5922
|
+
killed++;
|
|
5923
|
+
}
|
|
5924
|
+
} catch {}
|
|
5925
|
+
}
|
|
5926
|
+
if (killed === 0)
|
|
5927
|
+
break;
|
|
5928
|
+
}
|
|
5929
|
+
if (options.leaderAlive) {
|
|
5555
5930
|
try {
|
|
5556
|
-
|
|
5931
|
+
process.kill(proc.pid, "SIGKILL");
|
|
5557
5932
|
} catch {}
|
|
5558
5933
|
}
|
|
5559
5934
|
}
|
|
5560
|
-
var BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
5935
|
+
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
5561
5936
|
var init_run = __esm(() => {
|
|
5937
|
+
HookTimeoutError = class HookTimeoutError extends Error {
|
|
5938
|
+
timeoutMs;
|
|
5939
|
+
constructor(timeoutMs) {
|
|
5940
|
+
super(`Hook timed out after ${timeoutMs}ms (process group killed)`);
|
|
5941
|
+
this.name = "HookTimeoutError";
|
|
5942
|
+
this.timeoutMs = timeoutMs;
|
|
5943
|
+
}
|
|
5944
|
+
};
|
|
5562
5945
|
BASH_EXTENSIONS = new Set(["sh", "bash"]);
|
|
5563
5946
|
BUN_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]);
|
|
5564
5947
|
});
|
|
@@ -5570,7 +5953,7 @@ __export(exports_custom_install, {
|
|
|
5570
5953
|
installCustomSource: () => installCustomSource,
|
|
5571
5954
|
ensureCustomDir: () => ensureCustomDir
|
|
5572
5955
|
});
|
|
5573
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as
|
|
5956
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
|
|
5574
5957
|
import { join as join10 } from "path";
|
|
5575
5958
|
import { tmpdir } from "os";
|
|
5576
5959
|
function manifestAt(dir) {
|
|
@@ -5608,7 +5991,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
5608
5991
|
proc.exited
|
|
5609
5992
|
]);
|
|
5610
5993
|
if (exitCode !== 0) {
|
|
5611
|
-
|
|
5994
|
+
rmSync4(tmp, { recursive: true, force: true });
|
|
5612
5995
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
5613
5996
|
}
|
|
5614
5997
|
return join10(tmp, "repo");
|
|
@@ -5623,58 +6006,275 @@ function isCustomSource(value) {
|
|
|
5623
6006
|
return true;
|
|
5624
6007
|
return existsSync10(value);
|
|
5625
6008
|
}
|
|
5626
|
-
async function installCustomSource(source) {
|
|
5627
|
-
if (isGitUrl(source)) {
|
|
5628
|
-
const repoDir = await cloneToTemp(source);
|
|
5629
|
-
try {
|
|
5630
|
-
return copyManifestDir(repoDir, source, "git");
|
|
5631
|
-
} finally {
|
|
5632
|
-
|
|
5633
|
-
}
|
|
6009
|
+
async function installCustomSource(source) {
|
|
6010
|
+
if (isGitUrl(source)) {
|
|
6011
|
+
const repoDir = await cloneToTemp(source);
|
|
6012
|
+
try {
|
|
6013
|
+
return copyManifestDir(repoDir, source, "git");
|
|
6014
|
+
} finally {
|
|
6015
|
+
rmSync4(repoDir, { recursive: true, force: true });
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
6019
|
+
return installManifestUrl(source);
|
|
6020
|
+
}
|
|
6021
|
+
if (!existsSync10(source)) {
|
|
6022
|
+
throw new Error(`No such local path: ${source}`);
|
|
6023
|
+
}
|
|
6024
|
+
if (source.endsWith("manifest.json")) {
|
|
6025
|
+
const manifest = parseManifest(readFileSync7(source, "utf-8"));
|
|
6026
|
+
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
6027
|
+
}
|
|
6028
|
+
return copyManifestDir(source, source, "local");
|
|
6029
|
+
}
|
|
6030
|
+
async function installManifestUrl(url) {
|
|
6031
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
6032
|
+
if (!res.ok) {
|
|
6033
|
+
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
6034
|
+
}
|
|
6035
|
+
const manifest = parseManifest(await res.text());
|
|
6036
|
+
let script;
|
|
6037
|
+
if (manifest.script.includes(`
|
|
6038
|
+
`)) {
|
|
6039
|
+
script = { path: "script.ts", content: manifest.script };
|
|
6040
|
+
} else {
|
|
6041
|
+
const base = url.substring(0, url.lastIndexOf("/"));
|
|
6042
|
+
const scriptUrl = `${base}/${manifest.script}`;
|
|
6043
|
+
const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
|
|
6044
|
+
if (!scriptRes.ok) {
|
|
6045
|
+
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
6046
|
+
}
|
|
6047
|
+
script = { path: manifest.script, content: await scriptRes.text() };
|
|
6048
|
+
}
|
|
6049
|
+
const name = shortManifestName(manifest.name);
|
|
6050
|
+
const { dir, scriptPath } = writeCustomHook(name, manifest, script.content, script.path);
|
|
6051
|
+
return { name, version: manifest.version, kind: "url", source: url, dir, scriptPath };
|
|
6052
|
+
}
|
|
6053
|
+
function ensureCustomDir() {
|
|
6054
|
+
const dir = getCustomHooksDir();
|
|
6055
|
+
mkdirSync7(dir, { recursive: true });
|
|
6056
|
+
return dir;
|
|
6057
|
+
}
|
|
6058
|
+
var init_custom_install = __esm(() => {
|
|
6059
|
+
init_config();
|
|
6060
|
+
init_manifest();
|
|
6061
|
+
});
|
|
6062
|
+
|
|
6063
|
+
// src/lib/sync.ts
|
|
6064
|
+
var exports_sync = {};
|
|
6065
|
+
__export(exports_sync, {
|
|
6066
|
+
syncHooks: () => syncHooks,
|
|
6067
|
+
planSync: () => planSync,
|
|
6068
|
+
fetchPinnedHook: () => fetchPinnedHook
|
|
6069
|
+
});
|
|
6070
|
+
import { createHash as createHash2 } from "crypto";
|
|
6071
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
6072
|
+
function sha256OfText(text) {
|
|
6073
|
+
return createHash2("sha256").update(text).digest("hex");
|
|
6074
|
+
}
|
|
6075
|
+
function sha256OfPath(path) {
|
|
6076
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
6077
|
+
}
|
|
6078
|
+
function collectBundledCatalog() {
|
|
6079
|
+
const out = [];
|
|
6080
|
+
for (const meta of HOOKS) {
|
|
6081
|
+
const scriptPath = resolveScriptPath(meta.name);
|
|
6082
|
+
if (!scriptPath)
|
|
6083
|
+
continue;
|
|
6084
|
+
out.push({
|
|
6085
|
+
name: meta.name,
|
|
6086
|
+
version: meta.version,
|
|
6087
|
+
sha256: sha256OfPath(scriptPath)
|
|
6088
|
+
});
|
|
6089
|
+
}
|
|
6090
|
+
return out;
|
|
6091
|
+
}
|
|
6092
|
+
async function fetchJson(base, path) {
|
|
6093
|
+
const headers = { accept: "application/json" };
|
|
6094
|
+
const apiKey = resolveApiKey();
|
|
6095
|
+
if (apiKey)
|
|
6096
|
+
headers["x-api-key"] = apiKey;
|
|
6097
|
+
const res = await fetch(`${base}${path}`, {
|
|
6098
|
+
headers,
|
|
6099
|
+
redirect: "error",
|
|
6100
|
+
signal: AbortSignal.timeout(15000)
|
|
6101
|
+
});
|
|
6102
|
+
if (res.status === 401) {
|
|
6103
|
+
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
|
|
6104
|
+
}
|
|
6105
|
+
if (!res.ok) {
|
|
6106
|
+
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6107
|
+
}
|
|
6108
|
+
return res.json();
|
|
6109
|
+
}
|
|
6110
|
+
async function fetchRemoteState(apiUrl) {
|
|
6111
|
+
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
6112
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
6113
|
+
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
6114
|
+
throw new Error("remote catalog is malformed: missing hooks array");
|
|
6115
|
+
}
|
|
6116
|
+
if (!remoteLock.hooks || typeof remoteLock.hooks !== "object") {
|
|
6117
|
+
throw new Error("remote lock is malformed: missing hooks map");
|
|
6118
|
+
}
|
|
6119
|
+
return { catalog: catalog.hooks, remoteLock };
|
|
6120
|
+
}
|
|
6121
|
+
function computeDiff(catalog, remoteLock) {
|
|
6122
|
+
const local = readLock();
|
|
6123
|
+
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
6124
|
+
for (const entry of catalog) {
|
|
6125
|
+
if (!entry.name || !entry.version || !entry.sha256)
|
|
6126
|
+
continue;
|
|
6127
|
+
const pin = local.hooks[entry.name];
|
|
6128
|
+
const remotePin = remoteLock ? remoteLock.hooks[entry.name] : entry;
|
|
6129
|
+
if (remoteLock && !remotePin) {
|
|
6130
|
+
diff.skipped.push(entry.name);
|
|
6131
|
+
continue;
|
|
6132
|
+
}
|
|
6133
|
+
const pinnedLocally = pin && pin.version === entry.version && pin.sha256 === entry.sha256;
|
|
6134
|
+
if (pinnedLocally) {
|
|
6135
|
+
diff.unchanged.push(entry.name);
|
|
6136
|
+
} else if (pin) {
|
|
6137
|
+
diff.updated.push(entry.name);
|
|
6138
|
+
} else {
|
|
6139
|
+
diff.added.push(entry.name);
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
return diff;
|
|
6143
|
+
}
|
|
6144
|
+
async function planSync() {
|
|
6145
|
+
const apiUrl = resolveApiUrl();
|
|
6146
|
+
if (apiUrl) {
|
|
6147
|
+
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6148
|
+
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
6149
|
+
}
|
|
6150
|
+
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6151
|
+
return { apiUrl: null, dryRun: false, diff };
|
|
6152
|
+
}
|
|
6153
|
+
async function syncHooks(options = {}) {
|
|
6154
|
+
const apiUrl = resolveApiUrl();
|
|
6155
|
+
const plan = await planSync();
|
|
6156
|
+
if (options.dryRun)
|
|
6157
|
+
return { ...plan, dryRun: true };
|
|
6158
|
+
if (!apiUrl) {
|
|
6159
|
+
const db2 = getDb();
|
|
6160
|
+
for (const entry of collectBundledCatalog()) {
|
|
6161
|
+
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6162
|
+
upsertHookRecord(db2, {
|
|
6163
|
+
name: entry.name,
|
|
6164
|
+
version: entry.version,
|
|
6165
|
+
sha256: entry.sha256,
|
|
6166
|
+
source_type: "bundled",
|
|
6167
|
+
source_ref: "apps/hooks/hooks",
|
|
6168
|
+
last_verified_at: new Date().toISOString()
|
|
6169
|
+
});
|
|
6170
|
+
}
|
|
6171
|
+
return plan;
|
|
6172
|
+
}
|
|
6173
|
+
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6174
|
+
const diff = computeDiff(catalog, remoteLock);
|
|
6175
|
+
const toFetch = [...diff.added, ...diff.updated];
|
|
6176
|
+
const artifacts = new Map;
|
|
6177
|
+
for (const name of toFetch) {
|
|
6178
|
+
const entry = remoteLock.hooks[name];
|
|
6179
|
+
if (!entry)
|
|
6180
|
+
continue;
|
|
6181
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
6182
|
+
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6183
|
+
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6184
|
+
}
|
|
6185
|
+
const actualSha = sha256OfText(artifact.script);
|
|
6186
|
+
if (actualSha !== entry.sha256) {
|
|
6187
|
+
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6188
|
+
}
|
|
6189
|
+
artifacts.set(name, artifact);
|
|
6190
|
+
}
|
|
6191
|
+
const db = getDb();
|
|
6192
|
+
for (const name of toFetch) {
|
|
6193
|
+
const artifact = artifacts.get(name);
|
|
6194
|
+
const entry = remoteLock.hooks[name];
|
|
6195
|
+
if (!artifact || !entry)
|
|
6196
|
+
continue;
|
|
6197
|
+
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6198
|
+
const scriptRel = artifact.manifest.script.includes(`
|
|
6199
|
+
`) ? "script.ts" : artifact.manifest.script;
|
|
6200
|
+
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6201
|
+
const scriptPath = resolveScriptPath(name);
|
|
6202
|
+
const actual = await sha256File(scriptPath);
|
|
6203
|
+
if (actual !== entry.sha256) {
|
|
6204
|
+
throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6205
|
+
}
|
|
6206
|
+
setPinnedHook(name, { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" });
|
|
6207
|
+
upsertHookRecord(db, {
|
|
6208
|
+
name,
|
|
6209
|
+
version: entry.version,
|
|
6210
|
+
sha256: entry.sha256,
|
|
6211
|
+
source_type: entry.source ?? "remote",
|
|
6212
|
+
source_ref: apiUrl,
|
|
6213
|
+
last_verified_at: new Date().toISOString()
|
|
6214
|
+
});
|
|
6215
|
+
}
|
|
6216
|
+
return { apiUrl, dryRun: false, diff };
|
|
6217
|
+
}
|
|
6218
|
+
async function fetchPinnedHook(name, version, apiUrl) {
|
|
6219
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
6220
|
+
const entry = remoteLock.hooks?.[name];
|
|
6221
|
+
if (!entry) {
|
|
6222
|
+
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
5634
6223
|
}
|
|
5635
|
-
if (
|
|
5636
|
-
|
|
6224
|
+
if (entry.version !== version) {
|
|
6225
|
+
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${entry.version})`);
|
|
5637
6226
|
}
|
|
5638
|
-
|
|
5639
|
-
|
|
6227
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}`);
|
|
6228
|
+
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6229
|
+
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
5640
6230
|
}
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
6231
|
+
const manifestName = shortManifestName(artifact.manifest.name ?? name);
|
|
6232
|
+
if (manifestName !== name) {
|
|
6233
|
+
throw new Error(`artifact for '${name}@${version}' declares a different hook name ('${manifestName}')`);
|
|
5644
6234
|
}
|
|
5645
|
-
|
|
5646
|
-
}
|
|
5647
|
-
async function installManifestUrl(url) {
|
|
5648
|
-
const res = await fetch(url, { redirect: "follow" });
|
|
5649
|
-
if (!res.ok) {
|
|
5650
|
-
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
6235
|
+
if (artifact.manifest.version !== version) {
|
|
6236
|
+
throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
|
|
5651
6237
|
}
|
|
5652
|
-
const
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
`)) {
|
|
5656
|
-
script = { path: "script.ts", content: manifest.script };
|
|
5657
|
-
} else {
|
|
5658
|
-
const base = url.substring(0, url.lastIndexOf("/"));
|
|
5659
|
-
const scriptUrl = `${base}/${manifest.script}`;
|
|
5660
|
-
const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
|
|
5661
|
-
if (!scriptRes.ok) {
|
|
5662
|
-
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
5663
|
-
}
|
|
5664
|
-
script = { path: manifest.script, content: await scriptRes.text() };
|
|
6238
|
+
const actualSha = sha256OfText(artifact.script);
|
|
6239
|
+
if (actualSha !== entry.sha256) {
|
|
6240
|
+
throw new Error(`sha256 mismatch for '${name}@${version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
5665
6241
|
}
|
|
5666
|
-
const
|
|
5667
|
-
const
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
const
|
|
5672
|
-
|
|
5673
|
-
|
|
6242
|
+
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6243
|
+
const scriptRel = artifact.manifest.script.includes(`
|
|
6244
|
+
`) ? "script.ts" : artifact.manifest.script;
|
|
6245
|
+
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6246
|
+
const scriptPath = resolveScriptPath(name);
|
|
6247
|
+
const actual = await sha256File(scriptPath);
|
|
6248
|
+
if (actual !== entry.sha256) {
|
|
6249
|
+
throw new Error(`sha256 mismatch after write for '${name}@${version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6250
|
+
}
|
|
6251
|
+
const db = getDb();
|
|
6252
|
+
setPinnedHook(name, { version, sha256: entry.sha256, source: entry.source ?? "remote" });
|
|
6253
|
+
upsertHookRecord(db, {
|
|
6254
|
+
name,
|
|
6255
|
+
version,
|
|
6256
|
+
sha256: entry.sha256,
|
|
6257
|
+
source_type: entry.source ?? "remote",
|
|
6258
|
+
source_ref: apiUrl,
|
|
6259
|
+
last_verified_at: new Date().toISOString()
|
|
6260
|
+
});
|
|
6261
|
+
return {
|
|
6262
|
+
name,
|
|
6263
|
+
version,
|
|
6264
|
+
sha256: entry.sha256,
|
|
6265
|
+
source: entry.source ?? "remote",
|
|
6266
|
+
source_ref: apiUrl,
|
|
6267
|
+
artifact,
|
|
6268
|
+
scriptPath
|
|
6269
|
+
};
|
|
5674
6270
|
}
|
|
5675
|
-
var
|
|
6271
|
+
var init_sync = __esm(() => {
|
|
6272
|
+
init_registry();
|
|
5676
6273
|
init_config();
|
|
6274
|
+
init_db();
|
|
6275
|
+
init_store();
|
|
5677
6276
|
init_manifest();
|
|
6277
|
+
init_resolve();
|
|
5678
6278
|
});
|
|
5679
6279
|
|
|
5680
6280
|
// src/db/pg-migrations.ts
|
|
@@ -6184,234 +6784,75 @@ function authorized(req, apiKey) {
|
|
|
6184
6784
|
return secureEqual(header.slice("Bearer ".length), apiKey);
|
|
6185
6785
|
return secureEqual(req.headers.get("x-api-key") ?? "", apiKey);
|
|
6186
6786
|
}
|
|
6187
|
-
function json(body, status = 200, headers = {}) {
|
|
6188
|
-
return new Response(JSON.stringify(body), {
|
|
6189
|
-
status,
|
|
6190
|
-
headers: { "content-type": "application/json", ...headers }
|
|
6191
|
-
});
|
|
6192
|
-
}
|
|
6193
|
-
function handleServeRequest(req, apiKey) {
|
|
6194
|
-
const url = new URL(req.url);
|
|
6195
|
-
if (url.pathname === "/health" && req.method === "GET") {
|
|
6196
|
-
return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
|
|
6197
|
-
}
|
|
6198
|
-
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
6199
|
-
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
6200
|
-
}
|
|
6201
|
-
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
6202
|
-
return Promise.resolve(json(readLock()));
|
|
6203
|
-
}
|
|
6204
|
-
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
|
|
6205
|
-
if (artifactMatch && req.method === "GET") {
|
|
6206
|
-
const [, name, version] = artifactMatch;
|
|
6207
|
-
return artifactFor(name, version).then(async (artifact) => {
|
|
6208
|
-
if (!artifact)
|
|
6209
|
-
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
6210
|
-
const resolved = resolveHook(name);
|
|
6211
|
-
const sha = resolved ? await sha256File(resolved.scriptPath) : artifact.manifest.name;
|
|
6212
|
-
return json(artifact, 200, { "x-hook-sha256": sha });
|
|
6213
|
-
});
|
|
6214
|
-
}
|
|
6215
|
-
if (url.pathname === "/api/v1/hooks" && req.method === "PUT") {
|
|
6216
|
-
if (!authorized(req, apiKey)) {
|
|
6217
|
-
return Promise.resolve(json({ error: "unauthorized: valid API key required to publish" }, 401));
|
|
6218
|
-
}
|
|
6219
|
-
return req.json().then((body) => {
|
|
6220
|
-
const name = body.name ?? "";
|
|
6221
|
-
const version = body.version ?? "";
|
|
6222
|
-
const resolved = resolveHook(name);
|
|
6223
|
-
if (!resolved)
|
|
6224
|
-
return json({ error: `Hook '${name}' not found in local store` }, 404);
|
|
6225
|
-
if (version && version !== resolved.version) {
|
|
6226
|
-
return json({ error: `Version mismatch: local '${name}' is ${resolved.version}, requested ${version}` }, 409);
|
|
6227
|
-
}
|
|
6228
|
-
const check = retrustHook(name, resolved.scriptPath, resolved.version, "serve");
|
|
6229
|
-
return json({ ok: true, hook: { name, version: resolved.version, sha256: check.actual } });
|
|
6230
|
-
}, () => json({ error: "invalid JSON body" }, 400));
|
|
6231
|
-
}
|
|
6232
|
-
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
6233
|
-
}
|
|
6234
|
-
function startServeServer(options) {
|
|
6235
|
-
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
6236
|
-
const host = options.host ?? SERVE_HOST;
|
|
6237
|
-
const apiKey = resolveApiKey(options.apiKey);
|
|
6238
|
-
const server = Bun.serve({
|
|
6239
|
-
hostname: host,
|
|
6240
|
-
port,
|
|
6241
|
-
async fetch(req) {
|
|
6242
|
-
return handleServeRequest(req, apiKey);
|
|
6243
|
-
}
|
|
6244
|
-
});
|
|
6245
|
-
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
6246
|
-
return server;
|
|
6247
|
-
}
|
|
6248
|
-
var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
|
|
6249
|
-
var init_serve = __esm(() => {
|
|
6250
|
-
init_registry();
|
|
6251
|
-
init_manifest();
|
|
6252
|
-
init_store();
|
|
6253
|
-
init_resolve();
|
|
6254
|
-
init_config();
|
|
6255
|
-
if (false) {}
|
|
6256
|
-
});
|
|
6257
|
-
|
|
6258
|
-
// src/lib/sync.ts
|
|
6259
|
-
var exports_sync = {};
|
|
6260
|
-
__export(exports_sync, {
|
|
6261
|
-
syncHooks: () => syncHooks,
|
|
6262
|
-
planSync: () => planSync
|
|
6263
|
-
});
|
|
6264
|
-
import { createHash as createHash2 } from "crypto";
|
|
6265
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
6266
|
-
function sha256OfText(text) {
|
|
6267
|
-
return createHash2("sha256").update(text).digest("hex");
|
|
6268
|
-
}
|
|
6269
|
-
function sha256OfPath(path) {
|
|
6270
|
-
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
6271
|
-
}
|
|
6272
|
-
function collectBundledCatalog() {
|
|
6273
|
-
const out = [];
|
|
6274
|
-
for (const meta of HOOKS) {
|
|
6275
|
-
const scriptPath = resolveScriptPath(meta.name);
|
|
6276
|
-
if (!scriptPath)
|
|
6277
|
-
continue;
|
|
6278
|
-
out.push({
|
|
6279
|
-
name: meta.name,
|
|
6280
|
-
version: meta.version,
|
|
6281
|
-
sha256: sha256OfPath(scriptPath)
|
|
6282
|
-
});
|
|
6283
|
-
}
|
|
6284
|
-
return out;
|
|
6285
|
-
}
|
|
6286
|
-
async function fetchJson(base, path) {
|
|
6287
|
-
const headers = { accept: "application/json" };
|
|
6288
|
-
const apiKey = resolveApiKey();
|
|
6289
|
-
if (apiKey)
|
|
6290
|
-
headers["x-api-key"] = apiKey;
|
|
6291
|
-
const res = await fetch(`${base}${path}`, {
|
|
6292
|
-
headers,
|
|
6293
|
-
signal: AbortSignal.timeout(15000)
|
|
6294
|
-
});
|
|
6295
|
-
if (res.status === 401) {
|
|
6296
|
-
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
|
|
6297
|
-
}
|
|
6298
|
-
if (!res.ok) {
|
|
6299
|
-
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6300
|
-
}
|
|
6301
|
-
return res.json();
|
|
6302
|
-
}
|
|
6303
|
-
async function fetchRemoteState(apiUrl) {
|
|
6304
|
-
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
6305
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
6306
|
-
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
6307
|
-
throw new Error("remote catalog is malformed: missing hooks array");
|
|
6308
|
-
}
|
|
6309
|
-
if (!remoteLock.hooks || typeof remoteLock.hooks !== "object") {
|
|
6310
|
-
throw new Error("remote lock is malformed: missing hooks map");
|
|
6311
|
-
}
|
|
6312
|
-
return { catalog: catalog.hooks, remoteLock };
|
|
6313
|
-
}
|
|
6314
|
-
function computeDiff(catalog, remoteLock) {
|
|
6315
|
-
const local = readLock();
|
|
6316
|
-
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
6317
|
-
for (const entry of catalog) {
|
|
6318
|
-
if (!entry.name || !entry.version || !entry.sha256)
|
|
6319
|
-
continue;
|
|
6320
|
-
const pin = local.hooks[entry.name];
|
|
6321
|
-
const remotePin = remoteLock ? remoteLock.hooks[entry.name] : entry;
|
|
6322
|
-
if (remoteLock && !remotePin) {
|
|
6323
|
-
diff.skipped.push(entry.name);
|
|
6324
|
-
continue;
|
|
6325
|
-
}
|
|
6326
|
-
const pinnedLocally = pin && pin.version === entry.version && pin.sha256 === entry.sha256;
|
|
6327
|
-
if (pinnedLocally) {
|
|
6328
|
-
diff.unchanged.push(entry.name);
|
|
6329
|
-
} else if (pin) {
|
|
6330
|
-
diff.updated.push(entry.name);
|
|
6331
|
-
} else {
|
|
6332
|
-
diff.added.push(entry.name);
|
|
6333
|
-
}
|
|
6334
|
-
}
|
|
6335
|
-
return diff;
|
|
6336
|
-
}
|
|
6337
|
-
async function planSync() {
|
|
6338
|
-
const apiUrl = resolveApiUrl();
|
|
6339
|
-
if (apiUrl) {
|
|
6340
|
-
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6341
|
-
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
6342
|
-
}
|
|
6343
|
-
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6344
|
-
return { apiUrl: null, dryRun: false, diff };
|
|
6345
|
-
}
|
|
6346
|
-
async function syncHooks(options = {}) {
|
|
6347
|
-
const apiUrl = resolveApiUrl();
|
|
6348
|
-
const plan = await planSync();
|
|
6349
|
-
if (options.dryRun)
|
|
6350
|
-
return { ...plan, dryRun: true };
|
|
6351
|
-
if (!apiUrl) {
|
|
6352
|
-
const db2 = getDb();
|
|
6353
|
-
for (const entry of collectBundledCatalog()) {
|
|
6354
|
-
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6355
|
-
upsertHookRecord(db2, {
|
|
6356
|
-
name: entry.name,
|
|
6357
|
-
version: entry.version,
|
|
6358
|
-
sha256: entry.sha256,
|
|
6359
|
-
source_type: "bundled",
|
|
6360
|
-
source_ref: "apps/hooks/hooks",
|
|
6361
|
-
last_verified_at: new Date().toISOString()
|
|
6362
|
-
});
|
|
6363
|
-
}
|
|
6364
|
-
return plan;
|
|
6365
|
-
}
|
|
6366
|
-
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6367
|
-
const diff = computeDiff(catalog, remoteLock);
|
|
6368
|
-
const toFetch = [...diff.added, ...diff.updated];
|
|
6369
|
-
const artifacts = new Map;
|
|
6370
|
-
for (const name of toFetch) {
|
|
6371
|
-
const entry = remoteLock.hooks[name];
|
|
6372
|
-
if (!entry)
|
|
6373
|
-
continue;
|
|
6374
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
6375
|
-
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6376
|
-
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6377
|
-
}
|
|
6378
|
-
const actualSha = sha256OfText(artifact.script);
|
|
6379
|
-
if (actualSha !== entry.sha256) {
|
|
6380
|
-
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6381
|
-
}
|
|
6382
|
-
artifacts.set(name, artifact);
|
|
6787
|
+
function json(body, status = 200, headers = {}) {
|
|
6788
|
+
return new Response(JSON.stringify(body), {
|
|
6789
|
+
status,
|
|
6790
|
+
headers: { "content-type": "application/json", ...headers }
|
|
6791
|
+
});
|
|
6792
|
+
}
|
|
6793
|
+
function handleServeRequest(req, apiKey) {
|
|
6794
|
+
const url = new URL(req.url);
|
|
6795
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
6796
|
+
return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
|
|
6383
6797
|
}
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
version: entry.version,
|
|
6400
|
-
sha256: actual,
|
|
6401
|
-
source_type: entry.source ?? "remote",
|
|
6402
|
-
source_ref: apiUrl,
|
|
6403
|
-
last_verified_at: new Date().toISOString()
|
|
6798
|
+
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
6799
|
+
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
6800
|
+
}
|
|
6801
|
+
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
6802
|
+
return Promise.resolve(json(readLock()));
|
|
6803
|
+
}
|
|
6804
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
|
|
6805
|
+
if (artifactMatch && req.method === "GET") {
|
|
6806
|
+
const [, name, version] = artifactMatch;
|
|
6807
|
+
return artifactFor(name, version).then(async (artifact) => {
|
|
6808
|
+
if (!artifact)
|
|
6809
|
+
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
6810
|
+
const resolved = resolveHook(name);
|
|
6811
|
+
const sha = resolved ? await sha256File(resolved.scriptPath) : artifact.manifest.name;
|
|
6812
|
+
return json(artifact, 200, { "x-hook-sha256": sha });
|
|
6404
6813
|
});
|
|
6405
6814
|
}
|
|
6406
|
-
|
|
6815
|
+
if (url.pathname === "/api/v1/hooks" && req.method === "PUT") {
|
|
6816
|
+
if (!authorized(req, apiKey)) {
|
|
6817
|
+
return Promise.resolve(json({ error: "unauthorized: valid API key required to publish" }, 401));
|
|
6818
|
+
}
|
|
6819
|
+
return req.json().then((body) => {
|
|
6820
|
+
const name = body.name ?? "";
|
|
6821
|
+
const version = body.version ?? "";
|
|
6822
|
+
const resolved = resolveHook(name);
|
|
6823
|
+
if (!resolved)
|
|
6824
|
+
return json({ error: `Hook '${name}' not found in local store` }, 404);
|
|
6825
|
+
if (version && version !== resolved.version) {
|
|
6826
|
+
return json({ error: `Version mismatch: local '${name}' is ${resolved.version}, requested ${version}` }, 409);
|
|
6827
|
+
}
|
|
6828
|
+
const check = retrustHook(name, resolved.scriptPath, resolved.version, "serve");
|
|
6829
|
+
return json({ ok: true, hook: { name, version: resolved.version, sha256: check.actual } });
|
|
6830
|
+
}, () => json({ error: "invalid JSON body" }, 400));
|
|
6831
|
+
}
|
|
6832
|
+
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
6407
6833
|
}
|
|
6408
|
-
|
|
6834
|
+
function startServeServer(options) {
|
|
6835
|
+
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
6836
|
+
const host = options.host ?? SERVE_HOST;
|
|
6837
|
+
const apiKey = resolveApiKey(options.apiKey);
|
|
6838
|
+
const server = Bun.serve({
|
|
6839
|
+
hostname: host,
|
|
6840
|
+
port,
|
|
6841
|
+
async fetch(req) {
|
|
6842
|
+
return handleServeRequest(req, apiKey);
|
|
6843
|
+
}
|
|
6844
|
+
});
|
|
6845
|
+
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
6846
|
+
return server;
|
|
6847
|
+
}
|
|
6848
|
+
var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
|
|
6849
|
+
var init_serve = __esm(() => {
|
|
6409
6850
|
init_registry();
|
|
6410
|
-
init_config();
|
|
6411
|
-
init_db();
|
|
6412
|
-
init_store();
|
|
6413
6851
|
init_manifest();
|
|
6852
|
+
init_store();
|
|
6414
6853
|
init_resolve();
|
|
6854
|
+
init_config();
|
|
6855
|
+
if (false) {}
|
|
6415
6856
|
});
|
|
6416
6857
|
|
|
6417
6858
|
// src/cf/provision.ts
|
|
@@ -6511,14 +6952,77 @@ import { createServer } from "http";
|
|
|
6511
6952
|
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
6512
6953
|
import { join as join11, dirname as dirname5 } from "path";
|
|
6513
6954
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6514
|
-
async function runVerifiedHook(name,
|
|
6515
|
-
const
|
|
6516
|
-
const
|
|
6955
|
+
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
6956
|
+
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
6957
|
+
const resolved = resolveHook2(name);
|
|
6958
|
+
const hookScript = resolveScriptPath2(name);
|
|
6959
|
+
if (!resolved || !hookScript || !existsSync11(hookScript)) {
|
|
6960
|
+
throw new Error(`Hook '${name}' not found`);
|
|
6961
|
+
}
|
|
6962
|
+
const content = readFileSync9(hookScript);
|
|
6963
|
+
const sha = sha256Of(content);
|
|
6964
|
+
const check = checkScriptHash(name, sha);
|
|
6517
6965
|
if (!check.ok) {
|
|
6518
6966
|
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.`);
|
|
6519
6967
|
}
|
|
6520
|
-
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
6521
|
-
|
|
6968
|
+
const { executeVerifiedScript: executeVerifiedScript2, HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
6969
|
+
const started = Date.now();
|
|
6970
|
+
let ran;
|
|
6971
|
+
try {
|
|
6972
|
+
ran = await executeVerifiedScript2({ name, scriptPath: hookScript, content, stdin, env: process.env, timeout: timeoutMs });
|
|
6973
|
+
} catch (err) {
|
|
6974
|
+
try {
|
|
6975
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
6976
|
+
let inputJson = {};
|
|
6977
|
+
try {
|
|
6978
|
+
inputJson = JSON.parse(stdin);
|
|
6979
|
+
} catch {}
|
|
6980
|
+
recordHookRun2({
|
|
6981
|
+
hookName: name,
|
|
6982
|
+
eventType: resolveEventType2(inputJson.hook_event_name, resolved.events[0] ?? "PostToolUse"),
|
|
6983
|
+
version: resolved.version,
|
|
6984
|
+
sha256: sha,
|
|
6985
|
+
sessionId: typeof inputJson.session_id === "string" ? inputJson.session_id : null,
|
|
6986
|
+
toolName: typeof inputJson.tool_name === "string" ? inputJson.tool_name : null,
|
|
6987
|
+
toolInput: inputJson.tool_input,
|
|
6988
|
+
error: err instanceof Error ? err.message.slice(0, 500) : String(err),
|
|
6989
|
+
exitCode: -1,
|
|
6990
|
+
durationMs: Date.now() - started,
|
|
6991
|
+
projectDir: process.cwd()
|
|
6992
|
+
});
|
|
6993
|
+
} catch {}
|
|
6994
|
+
if (err instanceof HookTimeoutError2)
|
|
6995
|
+
throw err;
|
|
6996
|
+
throw err;
|
|
6997
|
+
}
|
|
6998
|
+
const durationMs = Date.now() - started;
|
|
6999
|
+
try {
|
|
7000
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
7001
|
+
let inputJson = {};
|
|
7002
|
+
try {
|
|
7003
|
+
inputJson = JSON.parse(stdin);
|
|
7004
|
+
} catch {}
|
|
7005
|
+
let outputJson = {};
|
|
7006
|
+
try {
|
|
7007
|
+
outputJson = JSON.parse(ran.stdout);
|
|
7008
|
+
} catch {}
|
|
7009
|
+
const blocked = outputJson.decision === "block" || outputJson.continue === false;
|
|
7010
|
+
recordHookRun2({
|
|
7011
|
+
hookName: name,
|
|
7012
|
+
eventType: resolveEventType2(inputJson.hook_event_name, resolved.events[0] ?? "PostToolUse"),
|
|
7013
|
+
version: resolved.version,
|
|
7014
|
+
sha256: sha,
|
|
7015
|
+
sessionId: typeof inputJson.session_id === "string" ? inputJson.session_id : null,
|
|
7016
|
+
toolName: typeof inputJson.tool_name === "string" ? inputJson.tool_name : null,
|
|
7017
|
+
toolInput: inputJson.tool_input,
|
|
7018
|
+
result: blocked ? "block" : "continue",
|
|
7019
|
+
error: ran.exitCode !== 0 ? (ran.stderr || `hook exited with code ${ran.exitCode}`).slice(0, 500) : null,
|
|
7020
|
+
exitCode: ran.exitCode,
|
|
7021
|
+
durationMs,
|
|
7022
|
+
projectDir: process.cwd()
|
|
7023
|
+
});
|
|
7024
|
+
} catch {}
|
|
7025
|
+
return ran;
|
|
6522
7026
|
}
|
|
6523
7027
|
function formatInstallResults(results, extra) {
|
|
6524
7028
|
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
@@ -6800,17 +7304,14 @@ function createHooksServer() {
|
|
|
6800
7304
|
name: z2.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')"),
|
|
6801
7305
|
input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input as JSON object (HookInput)"),
|
|
6802
7306
|
profile: z2.string().optional().describe("Agent profile ID to inject into hook input"),
|
|
6803
|
-
timeout_ms: z2.number().
|
|
7307
|
+
timeout_ms: z2.number().int().positive().max(600000).optional().describe("Timeout in milliseconds (default: the hook's manifest timeout_ms or 10000)")
|
|
6804
7308
|
}, async ({ name, input, profile, timeout_ms }) => {
|
|
6805
|
-
const
|
|
6806
|
-
|
|
7309
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
7310
|
+
const resolved = resolveHook2(name);
|
|
7311
|
+
if (!resolved) {
|
|
6807
7312
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
6808
7313
|
}
|
|
6809
|
-
const
|
|
6810
|
-
const hookScript = join11(hookDir, "src", "hook.ts");
|
|
6811
|
-
if (!existsSync11(hookScript)) {
|
|
6812
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
|
|
6813
|
-
}
|
|
7314
|
+
const effectiveTimeout = timeout_ms ?? resolved.timeoutMs ?? 1e4;
|
|
6814
7315
|
let hookInput = { ...input };
|
|
6815
7316
|
if (profile) {
|
|
6816
7317
|
const p = getProfile(profile);
|
|
@@ -6823,13 +7324,38 @@ function createHooksServer() {
|
|
|
6823
7324
|
};
|
|
6824
7325
|
}
|
|
6825
7326
|
}
|
|
6826
|
-
let ran = null;
|
|
6827
7327
|
try {
|
|
6828
|
-
ran = await
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
7328
|
+
const ran = await runVerifiedHook(name, JSON.stringify(hookInput), effectiveTimeout);
|
|
7329
|
+
const stdoutText = ran.stdout;
|
|
7330
|
+
const stderrText = ran.stderr;
|
|
7331
|
+
let output = {};
|
|
7332
|
+
try {
|
|
7333
|
+
output = JSON.parse(stdoutText);
|
|
7334
|
+
} catch {
|
|
7335
|
+
output = stdoutText ? { raw: stdoutText } : {};
|
|
7336
|
+
}
|
|
7337
|
+
return {
|
|
7338
|
+
content: [{
|
|
7339
|
+
type: "text",
|
|
7340
|
+
text: JSON.stringify({
|
|
7341
|
+
hook: name,
|
|
7342
|
+
output,
|
|
7343
|
+
stderr: stderrText || undefined,
|
|
7344
|
+
exitCode: ran.exitCode,
|
|
7345
|
+
timeout_ms: effectiveTimeout
|
|
7346
|
+
})
|
|
7347
|
+
}]
|
|
7348
|
+
};
|
|
6832
7349
|
} catch (err) {
|
|
7350
|
+
const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7351
|
+
if (err instanceof HookTimeoutError2) {
|
|
7352
|
+
return {
|
|
7353
|
+
content: [{
|
|
7354
|
+
type: "text",
|
|
7355
|
+
text: JSON.stringify({ hook: name, timedOut: true, timeout_ms: effectiveTimeout, error: err.message })
|
|
7356
|
+
}]
|
|
7357
|
+
};
|
|
7358
|
+
}
|
|
6833
7359
|
return {
|
|
6834
7360
|
content: [{
|
|
6835
7361
|
type: "text",
|
|
@@ -6837,27 +7363,6 @@ function createHooksServer() {
|
|
|
6837
7363
|
}]
|
|
6838
7364
|
};
|
|
6839
7365
|
}
|
|
6840
|
-
const timedOut = ran === null;
|
|
6841
|
-
const stdoutText = ran?.stdout ?? "";
|
|
6842
|
-
const stderrText = ran?.stderr ?? "";
|
|
6843
|
-
let output = {};
|
|
6844
|
-
try {
|
|
6845
|
-
output = JSON.parse(stdoutText);
|
|
6846
|
-
} catch {
|
|
6847
|
-
output = stdoutText ? { raw: stdoutText } : {};
|
|
6848
|
-
}
|
|
6849
|
-
return {
|
|
6850
|
-
content: [{
|
|
6851
|
-
type: "text",
|
|
6852
|
-
text: JSON.stringify({
|
|
6853
|
-
hook: name,
|
|
6854
|
-
output,
|
|
6855
|
-
stderr: stderrText || undefined,
|
|
6856
|
-
exitCode: ran?.exitCode ?? -1,
|
|
6857
|
-
...timedOut ? { timedOut: true, timeout_ms } : {}
|
|
6858
|
-
})
|
|
6859
|
-
}]
|
|
6860
|
-
};
|
|
6861
7366
|
});
|
|
6862
7367
|
defineTool("hooks_update", "Re-register installed hooks to pick up new package version (reinstalls with overwrite)", {
|
|
6863
7368
|
hooks: z2.array(z2.string()).optional().describe("Hook names to update (omit to update all installed hooks)"),
|
|
@@ -6918,11 +7423,11 @@ function createHooksServer() {
|
|
|
6918
7423
|
tool_name: z2.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
|
|
6919
7424
|
tool_input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
|
|
6920
7425
|
scope: z2.enum(["global", "project"]).default("global").describe("Scope to check"),
|
|
6921
|
-
timeout_ms: z2.number().
|
|
7426
|
+
timeout_ms: z2.number().int().positive().max(600000).optional().describe("Per-hook timeout in milliseconds (default: the hook's manifest timeout_ms or 5000)")
|
|
6922
7427
|
}, async ({ tool_name, tool_input, scope, timeout_ms }) => {
|
|
6923
7428
|
const registered = getRegisteredHooks(scope);
|
|
6924
7429
|
const matchingHooks = registered.filter((name) => {
|
|
6925
|
-
const meta = getHook(name);
|
|
7430
|
+
const meta = resolveHookMeta(name) ?? getHook(name);
|
|
6926
7431
|
if (!meta || meta.event !== "PreToolUse")
|
|
6927
7432
|
return false;
|
|
6928
7433
|
if (!meta.matcher)
|
|
@@ -6938,26 +7443,23 @@ function createHooksServer() {
|
|
|
6938
7443
|
}
|
|
6939
7444
|
const input = { tool_name, tool_input };
|
|
6940
7445
|
const results = await Promise.all(matchingHooks.map(async (name) => {
|
|
6941
|
-
const
|
|
6942
|
-
const
|
|
6943
|
-
|
|
6944
|
-
return { name, decision: "approve", error: "script not found" };
|
|
6945
|
-
let ran = null;
|
|
7446
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
7447
|
+
const resolved = resolveHook2(name);
|
|
7448
|
+
const effectiveTimeout = timeout_ms ?? resolved?.timeoutMs ?? 5000;
|
|
6946
7449
|
try {
|
|
6947
|
-
ran = await
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
7450
|
+
const ran = await runVerifiedHook(name, JSON.stringify(input), effectiveTimeout);
|
|
7451
|
+
let output = {};
|
|
7452
|
+
try {
|
|
7453
|
+
output = JSON.parse(ran.stdout);
|
|
7454
|
+
} catch {}
|
|
7455
|
+
return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
|
|
6951
7456
|
} catch (err) {
|
|
7457
|
+
const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7458
|
+
if (err instanceof HookTimeoutError2) {
|
|
7459
|
+
return { name, decision: "approve", timedOut: true, timeout_ms: effectiveTimeout };
|
|
7460
|
+
}
|
|
6952
7461
|
return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
|
|
6953
7462
|
}
|
|
6954
|
-
if (ran === null)
|
|
6955
|
-
return { name, decision: "approve", timedOut: true };
|
|
6956
|
-
let output = {};
|
|
6957
|
-
try {
|
|
6958
|
-
output = JSON.parse(ran.stdout);
|
|
6959
|
-
} catch {}
|
|
6960
|
-
return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
|
|
6961
7463
|
}));
|
|
6962
7464
|
const blocked = results.find((r) => r.decision === "block");
|
|
6963
7465
|
return {
|
|
@@ -6997,33 +7499,31 @@ function createHooksServer() {
|
|
|
6997
7499
|
name: z2.string().describe("Hook name"),
|
|
6998
7500
|
input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input JSON")
|
|
6999
7501
|
})).describe("List of hooks to run with their inputs"),
|
|
7000
|
-
timeout_ms: z2.number().
|
|
7502
|
+
timeout_ms: z2.number().int().positive().max(600000).optional().describe("Per-hook timeout in milliseconds (default: the hook's manifest timeout_ms or 10000)")
|
|
7001
7503
|
}, async ({ hooks, timeout_ms }) => {
|
|
7002
7504
|
const results = await Promise.all(hooks.map(async ({ name, input }) => {
|
|
7003
|
-
const
|
|
7004
|
-
|
|
7505
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
7506
|
+
const resolved = resolveHook2(name);
|
|
7507
|
+
if (!resolved)
|
|
7005
7508
|
return { name, error: `Hook '${name}' not found` };
|
|
7006
|
-
const
|
|
7007
|
-
if (!existsSync11(hookScript))
|
|
7008
|
-
return { name, error: "script not found" };
|
|
7009
|
-
let ran = null;
|
|
7509
|
+
const effectiveTimeout = timeout_ms ?? resolved.timeoutMs ?? 1e4;
|
|
7010
7510
|
try {
|
|
7011
|
-
ran = await
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7511
|
+
const ran = await runVerifiedHook(name, JSON.stringify(input), effectiveTimeout);
|
|
7512
|
+
const stdoutText = ran.stdout;
|
|
7513
|
+
let output = {};
|
|
7514
|
+
try {
|
|
7515
|
+
output = JSON.parse(stdoutText);
|
|
7516
|
+
} catch {
|
|
7517
|
+
output = stdoutText ? { raw: stdoutText } : {};
|
|
7518
|
+
}
|
|
7519
|
+
return { name, output, exitCode: ran.exitCode, timeout_ms: effectiveTimeout };
|
|
7015
7520
|
} catch (err) {
|
|
7521
|
+
const { HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7522
|
+
if (err instanceof HookTimeoutError2) {
|
|
7523
|
+
return { name, error: err.message, timedOut: true, timeout_ms: effectiveTimeout };
|
|
7524
|
+
}
|
|
7016
7525
|
return { name, error: err instanceof Error ? err.message : String(err) };
|
|
7017
7526
|
}
|
|
7018
|
-
const stdoutText = ran?.stdout ?? "";
|
|
7019
|
-
const timedOut = ran === null;
|
|
7020
|
-
let output = {};
|
|
7021
|
-
try {
|
|
7022
|
-
output = JSON.parse(stdoutText);
|
|
7023
|
-
} catch {
|
|
7024
|
-
output = stdoutText ? { raw: stdoutText } : {};
|
|
7025
|
-
}
|
|
7026
|
-
return { name, output, exitCode: ran?.exitCode ?? -1, ...timedOut ? { timedOut: true } : {} };
|
|
7027
7527
|
}));
|
|
7028
7528
|
return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }) }] };
|
|
7029
7529
|
});
|
|
@@ -8653,7 +9153,7 @@ var {
|
|
|
8653
9153
|
|
|
8654
9154
|
// src/cli/index.tsx
|
|
8655
9155
|
import chalk2 from "chalk";
|
|
8656
|
-
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
9156
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync5 } from "fs";
|
|
8657
9157
|
import { join as join12, dirname as dirname6 } from "path";
|
|
8658
9158
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8659
9159
|
|
|
@@ -9832,6 +10332,7 @@ init_installer();
|
|
|
9832
10332
|
init_profiles();
|
|
9833
10333
|
init_manifest();
|
|
9834
10334
|
init_resolve();
|
|
10335
|
+
init_store();
|
|
9835
10336
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
9836
10337
|
var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
|
|
9837
10338
|
var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
|
|
@@ -9884,6 +10385,22 @@ function hookSummaryLine(hook, options = {}) {
|
|
|
9884
10385
|
const description = options.verbose ? ` - ${truncateText2(hook.description, 110)}` : "";
|
|
9885
10386
|
return ` ${chalk2.cyan(hook.name.padEnd(17))} ${chalk2.dim(`[${hook.event}${matcher}]`)} ${chalk2.dim(hook.category)}${description}`;
|
|
9886
10387
|
}
|
|
10388
|
+
function listStoreHooks() {
|
|
10389
|
+
const out = [];
|
|
10390
|
+
for (const parsed of listCustomHooks()) {
|
|
10391
|
+
const name = parsed.manifest.name;
|
|
10392
|
+
const pin = getPinnedHook(name);
|
|
10393
|
+
const source = pin?.source === "remote" || pin?.source === "registry" ? "registry" : "custom";
|
|
10394
|
+
const meta = resolveHookMeta(name);
|
|
10395
|
+
if (!meta)
|
|
10396
|
+
continue;
|
|
10397
|
+
out.push({
|
|
10398
|
+
meta: { ...meta, version: pin?.version ?? meta.version },
|
|
10399
|
+
source
|
|
10400
|
+
});
|
|
10401
|
+
}
|
|
10402
|
+
return out.sort((a, b) => a.meta.name.localeCompare(b.meta.name));
|
|
10403
|
+
}
|
|
9887
10404
|
function printDisclosureHint(hidden, detailCommand, options = {}) {
|
|
9888
10405
|
const rowControls = options.includeAll ? "--limit, --all, --verbose" : "--limit, --verbose";
|
|
9889
10406
|
if (hidden > 0) {
|
|
@@ -9924,24 +10441,23 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
9924
10441
|
console.log(chalk2.red(message));
|
|
9925
10442
|
return;
|
|
9926
10443
|
}
|
|
9927
|
-
const
|
|
10444
|
+
const apiKeyRef = options.apiKey ?? "hasna/hooks/live/api-key";
|
|
10445
|
+
const config = { api_url: options.apiUrl.replace(/\/+$/, ""), api_key_ref: apiKeyRef };
|
|
9928
10446
|
const path = writeConfig2(config);
|
|
9929
10447
|
if (options.json) {
|
|
9930
|
-
console.log(JSON.stringify({ ok: true, api_url: config.api_url, api_key_ref: config.api_key_ref
|
|
10448
|
+
console.log(JSON.stringify({ ok: true, api_url: config.api_url, api_key_ref: config.api_key_ref, config_path: path }));
|
|
9931
10449
|
return;
|
|
9932
10450
|
}
|
|
9933
10451
|
console.log(chalk2.green(`
|
|
9934
10452
|
\u2713 Remote registry configured
|
|
9935
10453
|
`));
|
|
9936
10454
|
console.log(` ${chalk2.dim("API URL:")} ${config.api_url}`);
|
|
9937
|
-
console.log(` ${chalk2.dim("API key ref:")} ${config.api_key_ref
|
|
10455
|
+
console.log(` ${chalk2.dim("API key ref:")} ${config.api_key_ref}`);
|
|
9938
10456
|
console.log(` ${chalk2.dim("Config:")} ${path}`);
|
|
9939
10457
|
console.log();
|
|
9940
10458
|
console.log(chalk2.dim(" Presence of an API URL selects the remote registry; absence means local."));
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
console.log(` secrets exec ${config.api_key_ref} --as HASNA_HOOKS_API_KEY -- hooks serve`);
|
|
9944
|
-
}
|
|
10459
|
+
console.log(chalk2.dim(` Run the server with the key resolved from the vault, never the value:`));
|
|
10460
|
+
console.log(` secrets exec ${config.api_key_ref} --as HASNA_HOOKS_API_KEY -- hooks serve`);
|
|
9945
10461
|
console.log();
|
|
9946
10462
|
return;
|
|
9947
10463
|
}
|
|
@@ -9977,6 +10493,7 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
9977
10493
|
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) => {
|
|
9978
10494
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
9979
10495
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10496
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
9980
10497
|
const resolved = resolveHook2(hook);
|
|
9981
10498
|
if (!resolved) {
|
|
9982
10499
|
console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
@@ -9988,7 +10505,8 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
9988
10505
|
process.exit(1);
|
|
9989
10506
|
}
|
|
9990
10507
|
const content = readFileSync10(hookScript);
|
|
9991
|
-
const
|
|
10508
|
+
const sha = sha256Of2(content);
|
|
10509
|
+
const check = checkScriptHash2(hook, sha);
|
|
9992
10510
|
if (!check.ok) {
|
|
9993
10511
|
console.error(JSON.stringify({
|
|
9994
10512
|
error: `Hook '${hook}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${hook}' to trust the new content.`,
|
|
@@ -10017,18 +10535,74 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
10017
10535
|
}
|
|
10018
10536
|
}
|
|
10019
10537
|
const { readCustomManifest: readCustomManifest2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
10020
|
-
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
10538
|
+
const { executeVerifiedScript: executeVerifiedScript2, HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
10021
10539
|
const custom = readCustomManifest2(hook);
|
|
10022
10540
|
const args = custom?.manifest.args ?? [];
|
|
10023
10541
|
const timeout = custom?.manifest.timeout_ms;
|
|
10024
|
-
const
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10542
|
+
const started = Date.now();
|
|
10543
|
+
let stdout = "";
|
|
10544
|
+
let stderr = "";
|
|
10545
|
+
let exitCode = 0;
|
|
10546
|
+
try {
|
|
10547
|
+
({ stdout, stderr, exitCode } = await executeVerifiedScript2({
|
|
10548
|
+
name: hook,
|
|
10549
|
+
scriptPath: hookScript,
|
|
10550
|
+
content,
|
|
10551
|
+
args,
|
|
10552
|
+
stdin: hookStdin,
|
|
10553
|
+
env: process.env,
|
|
10554
|
+
timeout
|
|
10555
|
+
}));
|
|
10556
|
+
} catch (err) {
|
|
10557
|
+
if (err instanceof HookTimeoutError2) {
|
|
10558
|
+
try {
|
|
10559
|
+
const { recordHookRun: recordHookRun3, resolveEventType: resolveEventType3 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
10560
|
+
let inputJson2 = {};
|
|
10561
|
+
try {
|
|
10562
|
+
inputJson2 = JSON.parse(stdin);
|
|
10563
|
+
} catch {}
|
|
10564
|
+
recordHookRun3({
|
|
10565
|
+
hookName: hook,
|
|
10566
|
+
eventType: resolveEventType3(inputJson2.hook_event_name, resolved.events[0] ?? "PostToolUse"),
|
|
10567
|
+
version: resolved.version,
|
|
10568
|
+
sha256: sha,
|
|
10569
|
+
sessionId: typeof inputJson2.session_id === "string" ? inputJson2.session_id : null,
|
|
10570
|
+
toolName: typeof inputJson2.tool_name === "string" ? inputJson2.tool_name : null,
|
|
10571
|
+
toolInput: inputJson2.tool_input,
|
|
10572
|
+
error: err.message.slice(0, 500),
|
|
10573
|
+
exitCode: -1,
|
|
10574
|
+
durationMs: Date.now() - started,
|
|
10575
|
+
projectDir: process.cwd()
|
|
10576
|
+
});
|
|
10577
|
+
} catch {}
|
|
10578
|
+
console.error(JSON.stringify({ error: err.message, hook, timedOut: true, timeout_ms: timeout ?? null }));
|
|
10579
|
+
process.exit(1);
|
|
10580
|
+
}
|
|
10581
|
+
throw err;
|
|
10582
|
+
}
|
|
10583
|
+
const durationMs = Date.now() - started;
|
|
10584
|
+
let inputJson = {};
|
|
10585
|
+
try {
|
|
10586
|
+
inputJson = JSON.parse(stdin);
|
|
10587
|
+
} catch {}
|
|
10588
|
+
let outputJson = {};
|
|
10589
|
+
try {
|
|
10590
|
+
outputJson = JSON.parse(stdout);
|
|
10591
|
+
} catch {}
|
|
10592
|
+
const blocked = outputJson.decision === "block" || outputJson.continue === false;
|
|
10593
|
+
recordHookRun2({
|
|
10594
|
+
hookName: hook,
|
|
10595
|
+
eventType: resolveEventType2(inputJson.hook_event_name, resolved.events[0] ?? "PostToolUse"),
|
|
10596
|
+
version: resolved.version,
|
|
10597
|
+
sha256: sha,
|
|
10598
|
+
sessionId: typeof inputJson.session_id === "string" ? inputJson.session_id : null,
|
|
10599
|
+
toolName: typeof inputJson.tool_name === "string" ? inputJson.tool_name : null,
|
|
10600
|
+
toolInput: inputJson.tool_input,
|
|
10601
|
+
result: blocked ? "block" : "continue",
|
|
10602
|
+
error: exitCode !== 0 ? (stderr || `hook exited with code ${exitCode}`).slice(0, 500) : null,
|
|
10603
|
+
exitCode,
|
|
10604
|
+
durationMs,
|
|
10605
|
+
projectDir: process.cwd()
|
|
10032
10606
|
});
|
|
10033
10607
|
if (stdout)
|
|
10034
10608
|
process.stdout.write(stdout);
|
|
@@ -10036,7 +10610,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
10036
10610
|
process.stderr.write(stderr);
|
|
10037
10611
|
process.exit(exitCode);
|
|
10038
10612
|
});
|
|
10039
|
-
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
|
|
10613
|
+
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) => {
|
|
10040
10614
|
const scope = resolveScope(options);
|
|
10041
10615
|
const target = resolveTarget(options);
|
|
10042
10616
|
let toInstall = hooks;
|
|
@@ -10069,15 +10643,35 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
10069
10643
|
return;
|
|
10070
10644
|
}
|
|
10071
10645
|
const { isCustomSource: isCustomSource2, installCustomSource: installCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
10646
|
+
const { readCustomManifest: readCustomManifest2, HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
10647
|
+
const { resolveApiUrl: resolveApiUrl2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
10648
|
+
const { sha256Of: sha256Of2, pinInstalledHook: pinInstalledHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10649
|
+
const { readFileSync: readFileSyncFs } = await import("fs");
|
|
10650
|
+
function parseNameVersion(arg) {
|
|
10651
|
+
if (isCustomSource2(arg))
|
|
10652
|
+
return null;
|
|
10653
|
+
const at = arg.lastIndexOf("@");
|
|
10654
|
+
if (at <= 0 || at === arg.length - 1)
|
|
10655
|
+
return null;
|
|
10656
|
+
const name = arg.slice(0, at);
|
|
10657
|
+
const version = arg.slice(at + 1);
|
|
10658
|
+
if (!HOOK_NAME_RE2.test(name))
|
|
10659
|
+
return null;
|
|
10660
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
|
|
10661
|
+
return null;
|
|
10662
|
+
return { name, version };
|
|
10663
|
+
}
|
|
10664
|
+
const pinnedRequests = toInstall.map((arg) => ({ arg, pinned: parseNameVersion(arg) })).filter((x) => x.pinned !== null);
|
|
10072
10665
|
const customSources = toInstall.filter((n) => !getHook(n) && isCustomSource2(n));
|
|
10073
10666
|
if (options.dryRun) {
|
|
10074
10667
|
const known = toInstall.filter((n) => getHook(n));
|
|
10075
|
-
const unknown = toInstall.filter((n) => !getHook(n) && !isCustomSource2(n));
|
|
10668
|
+
const unknown = toInstall.filter((n) => !getHook(n) && !isCustomSource2(n) && !parseNameVersion(n) && !readCustomManifest2(n));
|
|
10076
10669
|
if (options.json) {
|
|
10077
10670
|
console.log(JSON.stringify({
|
|
10078
10671
|
dryRun: true,
|
|
10079
10672
|
would_install: known,
|
|
10080
10673
|
custom_sources: customSources,
|
|
10674
|
+
pinned: pinnedRequests.map((p) => `${p.pinned.name}@${p.pinned.version}`),
|
|
10081
10675
|
unknown,
|
|
10082
10676
|
scope,
|
|
10083
10677
|
target,
|
|
@@ -10095,6 +10689,9 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10095
10689
|
for (const source of customSources) {
|
|
10096
10690
|
console.log(chalk2.cyan(` ${source}`) + chalk2.dim(" [custom source: would fetch and install]"));
|
|
10097
10691
|
}
|
|
10692
|
+
for (const p of pinnedRequests) {
|
|
10693
|
+
console.log(chalk2.cyan(` ${p.pinned.name}@${p.pinned.version}`) + chalk2.dim(" [pinned registry install]"));
|
|
10694
|
+
}
|
|
10098
10695
|
if (unknown.length > 0) {
|
|
10099
10696
|
console.log();
|
|
10100
10697
|
for (const name of unknown) {
|
|
@@ -10109,9 +10706,21 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10109
10706
|
for (const source of customSources) {
|
|
10110
10707
|
try {
|
|
10111
10708
|
const custom = await installCustomSource2(source);
|
|
10709
|
+
try {
|
|
10710
|
+
const scriptBytes = readFileSyncFs(custom.scriptPath);
|
|
10711
|
+
pinInstalledHook2(custom.name, custom.version, sha256Of2(scriptBytes), "custom", source);
|
|
10712
|
+
} catch (pinError) {
|
|
10713
|
+
const { customHookDir: customHookDir2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
10714
|
+
const { removeHookFromStore: removeHookFromStore2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10715
|
+
try {
|
|
10716
|
+
rmSync5(customHookDir2(custom.name), { recursive: true, force: true });
|
|
10717
|
+
} catch {}
|
|
10718
|
+
removeHookFromStore2(custom.name);
|
|
10719
|
+
throw pinError;
|
|
10720
|
+
}
|
|
10112
10721
|
installedCustom.push(custom.name);
|
|
10113
10722
|
if (options.json) {
|
|
10114
|
-
console.log(JSON.stringify({ custom: { source, name: custom.name, version: custom.version } }));
|
|
10723
|
+
console.log(JSON.stringify({ custom: { source, name: custom.name, version: custom.version, pinned: true } }));
|
|
10115
10724
|
} else {
|
|
10116
10725
|
console.log(chalk2.green(`\u2713 Installed custom hook '${custom.name}' v${custom.version} from ${source}`));
|
|
10117
10726
|
}
|
|
@@ -10125,9 +10734,37 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10125
10734
|
}
|
|
10126
10735
|
}
|
|
10127
10736
|
}
|
|
10128
|
-
const
|
|
10737
|
+
const installedPinned = [];
|
|
10738
|
+
for (const { arg, pinned } of pinnedRequests) {
|
|
10739
|
+
try {
|
|
10740
|
+
const apiUrl = resolveApiUrl2();
|
|
10741
|
+
if (!apiUrl)
|
|
10742
|
+
throw new Error(`Cannot install '${arg}': no remote registry configured (set api_url or HASNA_HOOKS_API_URL)`);
|
|
10743
|
+
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
10744
|
+
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, apiUrl);
|
|
10745
|
+
installedPinned.push(pinnedInstall.name);
|
|
10746
|
+
if (options.json) {
|
|
10747
|
+
console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
|
|
10748
|
+
} else {
|
|
10749
|
+
console.log(chalk2.green(`\u2713 Installed '${pinnedInstall.name}' v${pinnedInstall.version} from registry (sha256 ${pinnedInstall.sha256.slice(0, 12)}\u2026)`));
|
|
10750
|
+
}
|
|
10751
|
+
} catch (error) {
|
|
10752
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10753
|
+
results.push({ hook: arg, success: false, error: message });
|
|
10754
|
+
if (options.json) {
|
|
10755
|
+
console.log(JSON.stringify({ error: `Pinned install failed for ${arg}: ${message}` }));
|
|
10756
|
+
} else {
|
|
10757
|
+
console.log(chalk2.red(`\u2717 ${message}`));
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
const namesToRegister = [...new Set([
|
|
10762
|
+
...toInstall.filter((n) => !isCustomSource2(n) && !parseNameVersion(n)),
|
|
10763
|
+
...installedCustom,
|
|
10764
|
+
...installedPinned
|
|
10765
|
+
])];
|
|
10129
10766
|
for (const name of namesToRegister) {
|
|
10130
|
-
if (!getHook(name) && !installedCustom.includes(name)) {
|
|
10767
|
+
if (!getHook(name) && !installedCustom.includes(name) && !installedPinned.includes(name) && !readCustomManifest2(name)) {
|
|
10131
10768
|
const suggestions = suggestHooks(name);
|
|
10132
10769
|
const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
|
|
10133
10770
|
results.push({ hook: name, success: false, error: `Hook '${name}' not found${hint}` });
|
|
@@ -10154,8 +10791,12 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
10154
10791
|
target,
|
|
10155
10792
|
applied: results.some((r) => r.applied)
|
|
10156
10793
|
}));
|
|
10794
|
+
if (results.length > 0 && results.every((r) => !r.success)) {
|
|
10795
|
+
process.exitCode = 1;
|
|
10796
|
+
}
|
|
10157
10797
|
return;
|
|
10158
10798
|
}
|
|
10799
|
+
const successCount = results.filter((r) => r.success).length;
|
|
10159
10800
|
const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (open-configs should apply)" : scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
|
|
10160
10801
|
console.log(chalk2.bold(`
|
|
10161
10802
|
Installing hooks (${scope}, ${target})...
|
|
@@ -10182,38 +10823,59 @@ Installing hooks (${scope}, ${target})...
|
|
|
10182
10823
|
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
10183
10824
|
}
|
|
10184
10825
|
}
|
|
10826
|
+
if (results.length > 0 && successCount === 0) {
|
|
10827
|
+
console.log(chalk2.red(`
|
|
10828
|
+
\u2717 Nothing was registered \u2014 all ${results.length} hook(s) failed.`));
|
|
10829
|
+
process.exitCode = 1;
|
|
10830
|
+
return;
|
|
10831
|
+
}
|
|
10185
10832
|
console.log(chalk2.dim(`
|
|
10186
10833
|
Registered in ${settingsFile}`));
|
|
10187
10834
|
});
|
|
10188
|
-
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) => {
|
|
10835
|
+
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) => {
|
|
10189
10836
|
const scope = resolveScope(options);
|
|
10190
10837
|
const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
|
|
10838
|
+
const storeHooks = listStoreHooks();
|
|
10191
10839
|
if (options.registered || options.installed) {
|
|
10192
10840
|
const target = options.target === "gemini" ? "gemini" : options.target === "codewith" ? "codewith" : "claude";
|
|
10193
10841
|
const registered = getRegisteredHooksForTarget(scope, target);
|
|
10842
|
+
const installed = [...new Set([...registered, ...storeHooks.map((s) => s.meta.name)])];
|
|
10843
|
+
const shown = options.registered ? registered : installed;
|
|
10194
10844
|
if (options.json) {
|
|
10195
|
-
console.log(JSON.stringify(
|
|
10196
|
-
const
|
|
10197
|
-
|
|
10845
|
+
console.log(JSON.stringify(shown.map((name) => {
|
|
10846
|
+
const store = storeHooks.find((s) => s.meta.name === name);
|
|
10847
|
+
const meta = store?.meta ?? getHook(name);
|
|
10848
|
+
return {
|
|
10849
|
+
name,
|
|
10850
|
+
event: meta?.event,
|
|
10851
|
+
version: meta?.version,
|
|
10852
|
+
description: meta?.description,
|
|
10853
|
+
source: store?.source ?? (store ? "custom" : undefined),
|
|
10854
|
+
scope,
|
|
10855
|
+
target
|
|
10856
|
+
};
|
|
10198
10857
|
})));
|
|
10199
10858
|
return;
|
|
10200
10859
|
}
|
|
10201
|
-
if (
|
|
10202
|
-
console.log(chalk2.dim(`No hooks registered (${scope}, ${target})`));
|
|
10860
|
+
if (shown.length === 0) {
|
|
10861
|
+
console.log(chalk2.dim(`No hooks ${options.registered ? "registered" : "installed"} (${scope}, ${target})`));
|
|
10203
10862
|
return;
|
|
10204
10863
|
}
|
|
10205
|
-
const
|
|
10864
|
+
const label = options.registered ? "Registered" : "Installed";
|
|
10865
|
+
const visible2 = shown.slice(0, limit);
|
|
10206
10866
|
console.log(chalk2.bold(`
|
|
10207
|
-
|
|
10867
|
+
${label} hooks \u2014 ${scope}/${target} (${shown.length}, showing ${visible2.length}):
|
|
10208
10868
|
`));
|
|
10209
10869
|
for (const name of visible2) {
|
|
10210
|
-
const
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10870
|
+
const store = storeHooks.find((s) => s.meta.name === name);
|
|
10871
|
+
const meta = store?.meta ?? getHook(name);
|
|
10872
|
+
if (meta) {
|
|
10873
|
+
console.log(hookSummaryLine(meta, { verbose: options.verbose }) + (store ? chalk2.dim(` (${store.source})`) : ""));
|
|
10874
|
+
} else {
|
|
10214
10875
|
console.log(` ${chalk2.cyan(name)} ${chalk2.dim("[unknown]")}`);
|
|
10876
|
+
}
|
|
10215
10877
|
}
|
|
10216
|
-
printDisclosureHint(
|
|
10878
|
+
printDisclosureHint(shown.length - visible2.length, "hooks info <name>", { includeAll: true });
|
|
10217
10879
|
return;
|
|
10218
10880
|
}
|
|
10219
10881
|
if (options.category) {
|
|
@@ -10246,6 +10908,18 @@ ${category} (${hooks.length}, showing ${visible2.length}):
|
|
|
10246
10908
|
for (const category of CATEGORIES) {
|
|
10247
10909
|
result[category] = getHooksByCategory(category);
|
|
10248
10910
|
}
|
|
10911
|
+
result["Custom / Registry"] = storeHooks.map((s) => ({
|
|
10912
|
+
name: s.meta.name,
|
|
10913
|
+
displayName: s.meta.displayName,
|
|
10914
|
+
description: s.meta.description,
|
|
10915
|
+
version: s.meta.version,
|
|
10916
|
+
category: "Custom / Registry",
|
|
10917
|
+
event: s.meta.event,
|
|
10918
|
+
events: s.meta.events,
|
|
10919
|
+
matcher: s.meta.matcher,
|
|
10920
|
+
tags: [...new Set([...s.meta.tags, s.source])],
|
|
10921
|
+
source: s.source
|
|
10922
|
+
}));
|
|
10249
10923
|
console.log(JSON.stringify(result));
|
|
10250
10924
|
return;
|
|
10251
10925
|
}
|
|
@@ -10256,6 +10930,16 @@ Available hooks (${HOOKS.length}, showing ${visible.length}):
|
|
|
10256
10930
|
for (const h of visible)
|
|
10257
10931
|
console.log(hookSummaryLine(h, { verbose: options.verbose }));
|
|
10258
10932
|
printDisclosureHint(HOOKS.length - visible.length, "hooks info <name>", { includeAll: true });
|
|
10933
|
+
if (storeHooks.length > 0) {
|
|
10934
|
+
const storeVisible = storeHooks.slice(0, limit);
|
|
10935
|
+
console.log(chalk2.bold(`
|
|
10936
|
+
Custom / Registry hooks (${storeHooks.length}, showing ${storeVisible.length}):
|
|
10937
|
+
`));
|
|
10938
|
+
for (const s of storeVisible) {
|
|
10939
|
+
console.log(hookSummaryLine(s.meta, { verbose: options.verbose }) + chalk2.dim(` (${s.source})`));
|
|
10940
|
+
}
|
|
10941
|
+
printDisclosureHint(storeHooks.length - storeVisible.length, "hooks info <name>", { includeAll: true });
|
|
10942
|
+
}
|
|
10259
10943
|
});
|
|
10260
10944
|
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) => {
|
|
10261
10945
|
const results = searchHooks(query);
|
|
@@ -10276,28 +10960,40 @@ Found ${results.length} hook(s), showing ${visible.length}:
|
|
|
10276
10960
|
console.log(hookSummaryLine(h, { verbose: options.verbose }));
|
|
10277
10961
|
printDisclosureHint(results.length - visible.length, "hooks info <name>");
|
|
10278
10962
|
});
|
|
10279
|
-
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) => {
|
|
10963
|
+
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) => {
|
|
10280
10964
|
const scope = resolveScope(options);
|
|
10281
10965
|
const target = resolveTarget(options);
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
console.log(JSON.stringify({ hook, removed: false, scope, target, error: `Hook '${hook}' not found${hint}`, suggestions }));
|
|
10287
|
-
} else {
|
|
10288
|
-
console.log(chalk2.red(`\u2717 Hook '${hook}' not found${hint}`));
|
|
10289
|
-
}
|
|
10290
|
-
return;
|
|
10966
|
+
const { uninstallHook: uninstallHook2 } = await Promise.resolve().then(() => (init_installer(), exports_installer));
|
|
10967
|
+
const result = uninstallHook2(hook, scope, target);
|
|
10968
|
+
if (!result.removed) {
|
|
10969
|
+
process.exitCode = 1;
|
|
10291
10970
|
}
|
|
10292
|
-
const removed = removeHook(hook, scope, target);
|
|
10293
10971
|
if (options.json) {
|
|
10294
|
-
console.log(JSON.stringify({
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10972
|
+
console.log(JSON.stringify({
|
|
10973
|
+
hook: result.name,
|
|
10974
|
+
removed: result.removed,
|
|
10975
|
+
source: result.source,
|
|
10976
|
+
settings_scopes: result.settingsScopes,
|
|
10977
|
+
store_dir_removed: result.storeDirRemoved,
|
|
10978
|
+
pin_removed: result.pinRemoved,
|
|
10979
|
+
db_record_removed: result.dbRecordRemoved,
|
|
10980
|
+
registrations_remaining: result.registrationsRemaining,
|
|
10981
|
+
scope,
|
|
10982
|
+
target,
|
|
10983
|
+
...result.error ? { error: result.error } : {}
|
|
10984
|
+
}));
|
|
10985
|
+
} else if (result.removed) {
|
|
10986
|
+
console.log(chalk2.green(`\u2713 Removed ${result.name} (${scope}, ${target})`));
|
|
10987
|
+
if (result.source === "custom" && result.storeDirRemoved) {
|
|
10988
|
+
console.log(chalk2.dim(` store dir removed; lock pin ${result.pinRemoved ? "removed" : "absent"}; DB record ${result.dbRecordRemoved ? "removed" : "absent"}`));
|
|
10989
|
+
}
|
|
10990
|
+
if (result.registrationsRemaining && result.registrationsRemaining.length > 0) {
|
|
10991
|
+
console.log(chalk2.yellow(` \u26A0 registrations remaining: ${result.registrationsRemaining.join(", ")} \u2014 remove them in that target's own config`));
|
|
10992
|
+
}
|
|
10299
10993
|
} else {
|
|
10300
|
-
|
|
10994
|
+
const suggestions = suggestHooks(hook);
|
|
10995
|
+
const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
|
|
10996
|
+
console.log(chalk2.red(`\u2717 Hook '${hook}' not found${hint}`));
|
|
10301
10997
|
}
|
|
10302
10998
|
});
|
|
10303
10999
|
program2.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
|
|
@@ -10441,7 +11137,7 @@ Hook Health Check (${scope})
|
|
|
10441
11137
|
}
|
|
10442
11138
|
console.log();
|
|
10443
11139
|
});
|
|
10444
|
-
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)
|
|
11140
|
+
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) => {
|
|
10445
11141
|
const scope = resolveScope(options);
|
|
10446
11142
|
const installed = getInstalledHooks(scope);
|
|
10447
11143
|
const toUpdate = hooks.length > 0 ? hooks : installed;
|
|
@@ -10451,19 +11147,60 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
10451
11147
|
} else {
|
|
10452
11148
|
console.log(chalk2.dim("No hooks installed to update."));
|
|
10453
11149
|
}
|
|
11150
|
+
process.exitCode = 1;
|
|
10454
11151
|
return;
|
|
10455
11152
|
}
|
|
10456
11153
|
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
10457
11154
|
const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10458
11155
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
11156
|
+
const { resolveApiUrl: resolveApiUrl2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
11157
|
+
const { isCustomSource: isCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
11158
|
+
const { HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
11159
|
+
function parseNameVersion(arg) {
|
|
11160
|
+
if (isCustomSource2(arg))
|
|
11161
|
+
return null;
|
|
11162
|
+
const at = arg.lastIndexOf("@");
|
|
11163
|
+
if (at <= 0 || at === arg.length - 1)
|
|
11164
|
+
return null;
|
|
11165
|
+
const name = arg.slice(0, at);
|
|
11166
|
+
const version = arg.slice(at + 1);
|
|
11167
|
+
if (!HOOK_NAME_RE2.test(name))
|
|
11168
|
+
return null;
|
|
11169
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
|
|
11170
|
+
return null;
|
|
11171
|
+
return { name, version };
|
|
11172
|
+
}
|
|
10459
11173
|
const results = [];
|
|
10460
|
-
for (const
|
|
10461
|
-
|
|
10462
|
-
|
|
11174
|
+
for (const arg of toUpdate) {
|
|
11175
|
+
const pinned = parseNameVersion(arg);
|
|
11176
|
+
if (pinned) {
|
|
11177
|
+
try {
|
|
11178
|
+
const apiUrl = resolveApiUrl2();
|
|
11179
|
+
if (!apiUrl)
|
|
11180
|
+
throw new Error(`Cannot update '${arg}': no remote registry configured (set api_url or HASNA_HOOKS_API_URL)`);
|
|
11181
|
+
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
11182
|
+
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, apiUrl);
|
|
11183
|
+
const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
|
|
11184
|
+
if (!registered.success) {
|
|
11185
|
+
results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
|
|
11186
|
+
continue;
|
|
11187
|
+
}
|
|
11188
|
+
results.push({
|
|
11189
|
+
hook: pinnedInstall.name,
|
|
11190
|
+
success: true,
|
|
11191
|
+
pinned: { version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source }
|
|
11192
|
+
});
|
|
11193
|
+
} catch (error) {
|
|
11194
|
+
results.push({ hook: arg, success: false, error: error instanceof Error ? error.message : String(error) });
|
|
11195
|
+
}
|
|
10463
11196
|
continue;
|
|
10464
11197
|
}
|
|
10465
|
-
|
|
10466
|
-
|
|
11198
|
+
if (!installed.includes(arg)) {
|
|
11199
|
+
results.push({ hook: arg, success: false, error: "Not installed" });
|
|
11200
|
+
continue;
|
|
11201
|
+
}
|
|
11202
|
+
const result = installHook(arg, { scope, overwrite: true });
|
|
11203
|
+
const resolved = resolveHook2(arg);
|
|
10467
11204
|
if (result.success && resolved) {
|
|
10468
11205
|
const hash = await sha256File2(resolved.scriptPath);
|
|
10469
11206
|
setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
|
|
@@ -10484,6 +11221,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
10484
11221
|
updated: results.filter((r) => r.success).map((r) => r.hook),
|
|
10485
11222
|
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }))
|
|
10486
11223
|
}));
|
|
11224
|
+
if (results.some((r) => !r.success)) {
|
|
11225
|
+
process.exitCode = 1;
|
|
11226
|
+
}
|
|
10487
11227
|
return;
|
|
10488
11228
|
}
|
|
10489
11229
|
console.log(chalk2.bold(`
|
|
@@ -10496,6 +11236,9 @@ Updating hooks...
|
|
|
10496
11236
|
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
10497
11237
|
}
|
|
10498
11238
|
}
|
|
11239
|
+
if (results.some((r) => !r.success)) {
|
|
11240
|
+
process.exitCode = 1;
|
|
11241
|
+
}
|
|
10499
11242
|
});
|
|
10500
11243
|
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) => {
|
|
10501
11244
|
if (hook) {
|