@hasna/hooks 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/index.js +2124 -1405
- 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 +647 -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/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ var CREATE_HOOK_EVENTS_TABLE = `
|
|
|
22
22
|
timestamp TEXT NOT NULL,
|
|
23
23
|
session_id TEXT NOT NULL,
|
|
24
24
|
hook_name TEXT NOT NULL,
|
|
25
|
-
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
|
|
25
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
|
|
26
26
|
tool_name TEXT,
|
|
27
27
|
tool_input TEXT,
|
|
28
28
|
result TEXT CHECK (result IN ('continue', 'block', NULL)),
|
|
@@ -137,6 +137,35 @@ var init_004_hooks_table = __esm(() => {
|
|
|
137
137
|
];
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
// src/db/migrations/005_subagent_start_event.ts
|
|
141
|
+
function up5(db) {
|
|
142
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("hook_events");
|
|
143
|
+
if (!row?.sql)
|
|
144
|
+
return;
|
|
145
|
+
if (row.sql.includes("SubagentStart"))
|
|
146
|
+
return;
|
|
147
|
+
db.exec("BEGIN");
|
|
148
|
+
try {
|
|
149
|
+
db.exec("ALTER TABLE hook_events RENAME TO hook_events_old");
|
|
150
|
+
db.exec(CREATE_HOOK_EVENTS_TABLE);
|
|
151
|
+
db.exec(`INSERT INTO hook_events
|
|
152
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
153
|
+
SELECT id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata
|
|
154
|
+
FROM hook_events_old`);
|
|
155
|
+
db.exec("DROP TABLE hook_events_old");
|
|
156
|
+
for (const idx of CREATE_INDEXES) {
|
|
157
|
+
db.exec(idx);
|
|
158
|
+
}
|
|
159
|
+
db.exec("COMMIT");
|
|
160
|
+
} catch (err) {
|
|
161
|
+
db.exec("ROLLBACK");
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
var init_005_subagent_start_event = __esm(() => {
|
|
166
|
+
init_schema();
|
|
167
|
+
});
|
|
168
|
+
|
|
140
169
|
// src/db/migrations/index.ts
|
|
141
170
|
function ensureMigrationsTable(db) {
|
|
142
171
|
db.exec(`
|
|
@@ -169,11 +198,13 @@ var init_migrations = __esm(() => {
|
|
|
169
198
|
init_002_session_events();
|
|
170
199
|
init_003_user_prompt_submit_event();
|
|
171
200
|
init_004_hooks_table();
|
|
201
|
+
init_005_subagent_start_event();
|
|
172
202
|
MIGRATIONS = [
|
|
173
203
|
{ version: "001_initial", up },
|
|
174
204
|
{ version: "002_session_events", up: up2 },
|
|
175
205
|
{ version: "003_user_prompt_submit_event", up: up3 },
|
|
176
|
-
{ version: "004_hooks_table", up: up4 }
|
|
206
|
+
{ version: "004_hooks_table", up: up4 },
|
|
207
|
+
{ version: "005_subagent_start_event", up: up5 }
|
|
177
208
|
];
|
|
178
209
|
});
|
|
179
210
|
|
|
@@ -343,6 +374,7 @@ function getDb() {
|
|
|
343
374
|
const isNew = dbPath === ":memory:" || !existsSync2(dbPath);
|
|
344
375
|
ensureDir(dbPath);
|
|
345
376
|
instance = new Database(dbPath);
|
|
377
|
+
instance.exec("PRAGMA busy_timeout=5000");
|
|
346
378
|
instance.exec("PRAGMA journal_mode=WAL");
|
|
347
379
|
instance.exec("PRAGMA foreign_keys=ON");
|
|
348
380
|
runMigrations(instance);
|
|
@@ -4545,149 +4577,6 @@ var init_manifest = __esm(() => {
|
|
|
4545
4577
|
};
|
|
4546
4578
|
});
|
|
4547
4579
|
|
|
4548
|
-
// src/lib/profiles.ts
|
|
4549
|
-
var exports_profiles = {};
|
|
4550
|
-
__export(exports_profiles, {
|
|
4551
|
-
updateProfile: () => updateProfile,
|
|
4552
|
-
touchProfile: () => touchProfile,
|
|
4553
|
-
listProfiles: () => listProfiles,
|
|
4554
|
-
importProfiles: () => importProfiles,
|
|
4555
|
-
getProfilesDir: () => getProfilesDir,
|
|
4556
|
-
getProfile: () => getProfile,
|
|
4557
|
-
exportProfiles: () => exportProfiles,
|
|
4558
|
-
deleteProfile: () => deleteProfile,
|
|
4559
|
-
createProfile: () => createProfile
|
|
4560
|
-
});
|
|
4561
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync3, rmSync, cpSync as cpSync2 } from "fs";
|
|
4562
|
-
import { join as join7 } from "path";
|
|
4563
|
-
import { homedir as homedir5 } from "os";
|
|
4564
|
-
function resolveProfilesDir() {
|
|
4565
|
-
const newDir = join7(homedir5(), ".hasna", "hooks", "profiles");
|
|
4566
|
-
const oldDir = join7(homedir5(), ".hooks", "profiles");
|
|
4567
|
-
if (!existsSync7(newDir) && existsSync7(oldDir)) {
|
|
4568
|
-
mkdirSync5(join7(homedir5(), ".hasna", "hooks"), { recursive: true });
|
|
4569
|
-
cpSync2(oldDir, newDir, { recursive: true });
|
|
4570
|
-
}
|
|
4571
|
-
return newDir;
|
|
4572
|
-
}
|
|
4573
|
-
function ensureProfilesDir() {
|
|
4574
|
-
if (!existsSync7(PROFILES_DIR)) {
|
|
4575
|
-
mkdirSync5(PROFILES_DIR, { recursive: true });
|
|
4576
|
-
}
|
|
4577
|
-
}
|
|
4578
|
-
function profilePath(id) {
|
|
4579
|
-
return join7(PROFILES_DIR, `${id}.json`);
|
|
4580
|
-
}
|
|
4581
|
-
function shortUuid() {
|
|
4582
|
-
return crypto.randomUUID().slice(0, 8);
|
|
4583
|
-
}
|
|
4584
|
-
function getProfilesDir() {
|
|
4585
|
-
return PROFILES_DIR;
|
|
4586
|
-
}
|
|
4587
|
-
function createProfile(input) {
|
|
4588
|
-
ensureProfilesDir();
|
|
4589
|
-
const id = shortUuid();
|
|
4590
|
-
const now = new Date().toISOString();
|
|
4591
|
-
const profile = {
|
|
4592
|
-
agent_id: id,
|
|
4593
|
-
agent_type: input.agent_type,
|
|
4594
|
-
created_at: now,
|
|
4595
|
-
last_seen_at: now,
|
|
4596
|
-
preferences: {}
|
|
4597
|
-
};
|
|
4598
|
-
if (input.name) {
|
|
4599
|
-
profile.name = input.name;
|
|
4600
|
-
}
|
|
4601
|
-
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4602
|
-
`);
|
|
4603
|
-
return profile;
|
|
4604
|
-
}
|
|
4605
|
-
function getProfile(id) {
|
|
4606
|
-
const path = profilePath(id);
|
|
4607
|
-
try {
|
|
4608
|
-
if (!existsSync7(path))
|
|
4609
|
-
return null;
|
|
4610
|
-
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
4611
|
-
} catch {
|
|
4612
|
-
return null;
|
|
4613
|
-
}
|
|
4614
|
-
}
|
|
4615
|
-
function listProfiles() {
|
|
4616
|
-
if (!existsSync7(PROFILES_DIR))
|
|
4617
|
-
return [];
|
|
4618
|
-
try {
|
|
4619
|
-
const files = readdirSync3(PROFILES_DIR).filter((f) => f.endsWith(".json"));
|
|
4620
|
-
const profiles = [];
|
|
4621
|
-
for (const file of files) {
|
|
4622
|
-
try {
|
|
4623
|
-
const content = readFileSync5(join7(PROFILES_DIR, file), "utf-8");
|
|
4624
|
-
profiles.push(JSON.parse(content));
|
|
4625
|
-
} catch {}
|
|
4626
|
-
}
|
|
4627
|
-
return profiles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
4628
|
-
} catch {
|
|
4629
|
-
return [];
|
|
4630
|
-
}
|
|
4631
|
-
}
|
|
4632
|
-
function updateProfile(id, data) {
|
|
4633
|
-
const profile = getProfile(id);
|
|
4634
|
-
if (!profile)
|
|
4635
|
-
return null;
|
|
4636
|
-
if (data.name !== undefined)
|
|
4637
|
-
profile.name = data.name;
|
|
4638
|
-
if (data.preferences !== undefined)
|
|
4639
|
-
profile.preferences = data.preferences;
|
|
4640
|
-
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4641
|
-
`);
|
|
4642
|
-
return profile;
|
|
4643
|
-
}
|
|
4644
|
-
function deleteProfile(id) {
|
|
4645
|
-
const path = profilePath(id);
|
|
4646
|
-
if (!existsSync7(path))
|
|
4647
|
-
return false;
|
|
4648
|
-
try {
|
|
4649
|
-
rmSync(path);
|
|
4650
|
-
return true;
|
|
4651
|
-
} catch {
|
|
4652
|
-
return false;
|
|
4653
|
-
}
|
|
4654
|
-
}
|
|
4655
|
-
function touchProfile(id) {
|
|
4656
|
-
const profile = getProfile(id);
|
|
4657
|
-
if (!profile)
|
|
4658
|
-
return;
|
|
4659
|
-
profile.last_seen_at = new Date().toISOString();
|
|
4660
|
-
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4661
|
-
`);
|
|
4662
|
-
}
|
|
4663
|
-
function exportProfiles() {
|
|
4664
|
-
return listProfiles();
|
|
4665
|
-
}
|
|
4666
|
-
function importProfiles(profiles) {
|
|
4667
|
-
ensureProfilesDir();
|
|
4668
|
-
let imported = 0;
|
|
4669
|
-
let skipped = 0;
|
|
4670
|
-
for (const profile of profiles) {
|
|
4671
|
-
if (!profile.agent_id || !profile.agent_type) {
|
|
4672
|
-
skipped++;
|
|
4673
|
-
continue;
|
|
4674
|
-
}
|
|
4675
|
-
const path = profilePath(profile.agent_id);
|
|
4676
|
-
if (existsSync7(path)) {
|
|
4677
|
-
skipped++;
|
|
4678
|
-
continue;
|
|
4679
|
-
}
|
|
4680
|
-
writeFileSync4(path, JSON.stringify(profile, null, 2) + `
|
|
4681
|
-
`);
|
|
4682
|
-
imported++;
|
|
4683
|
-
}
|
|
4684
|
-
return { imported, skipped };
|
|
4685
|
-
}
|
|
4686
|
-
var PROFILES_DIR;
|
|
4687
|
-
var init_profiles = __esm(() => {
|
|
4688
|
-
PROFILES_DIR = resolveProfilesDir();
|
|
4689
|
-
});
|
|
4690
|
-
|
|
4691
4580
|
// src/lib/store.ts
|
|
4692
4581
|
var exports_store = {};
|
|
4693
4582
|
__export(exports_store, {
|
|
@@ -4700,14 +4589,16 @@ __export(exports_store, {
|
|
|
4700
4589
|
retrustHook: () => retrustHook,
|
|
4701
4590
|
removePinnedHook: () => removePinnedHook,
|
|
4702
4591
|
removeHookRecord: () => removeHookRecord,
|
|
4592
|
+
removeHookFromStore: () => removeHookFromStore,
|
|
4703
4593
|
readLock: () => readLock,
|
|
4594
|
+
pinInstalledHook: () => pinInstalledHook,
|
|
4704
4595
|
listHookRecords: () => listHookRecords,
|
|
4705
4596
|
getPinnedHook: () => getPinnedHook,
|
|
4706
4597
|
getHookRecord: () => getHookRecord,
|
|
4707
4598
|
checkScriptHash: () => checkScriptHash
|
|
4708
4599
|
});
|
|
4709
4600
|
import { createHash } from "crypto";
|
|
4710
|
-
import { existsSync as
|
|
4601
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4711
4602
|
import { readFile } from "fs/promises";
|
|
4712
4603
|
function sha256Of(input) {
|
|
4713
4604
|
return createHash("sha256").update(input).digest("hex");
|
|
@@ -4754,9 +4645,9 @@ function removeHookRecord(db, name) {
|
|
|
4754
4645
|
function readLock() {
|
|
4755
4646
|
try {
|
|
4756
4647
|
const path = getLockPath();
|
|
4757
|
-
if (!
|
|
4648
|
+
if (!existsSync6(path))
|
|
4758
4649
|
return { hooks: {} };
|
|
4759
|
-
const parsed = JSON.parse(
|
|
4650
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
4760
4651
|
return { hooks: parsed.hooks ?? {} };
|
|
4761
4652
|
} catch {
|
|
4762
4653
|
return { hooks: {} };
|
|
@@ -4764,12 +4655,12 @@ function readLock() {
|
|
|
4764
4655
|
}
|
|
4765
4656
|
function writeLock(lock) {
|
|
4766
4657
|
const path = getLockPath();
|
|
4767
|
-
|
|
4658
|
+
mkdirSync4(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
|
|
4768
4659
|
const sorted = {};
|
|
4769
4660
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
4770
4661
|
sorted[name] = lock.hooks[name];
|
|
4771
4662
|
}
|
|
4772
|
-
|
|
4663
|
+
writeFileSync3(path, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
4773
4664
|
`, "utf-8");
|
|
4774
4665
|
return path;
|
|
4775
4666
|
}
|
|
@@ -4789,6 +4680,25 @@ function removePinnedHook(name) {
|
|
|
4789
4680
|
writeLock(lock);
|
|
4790
4681
|
return true;
|
|
4791
4682
|
}
|
|
4683
|
+
function pinInstalledHook(name, version, sha256, source, sourceRef) {
|
|
4684
|
+
const now = new Date().toISOString();
|
|
4685
|
+
const db = getDb();
|
|
4686
|
+
upsertHookRecord(db, {
|
|
4687
|
+
name,
|
|
4688
|
+
version,
|
|
4689
|
+
sha256,
|
|
4690
|
+
source_type: source,
|
|
4691
|
+
source_ref: sourceRef ?? null,
|
|
4692
|
+
last_verified_at: now
|
|
4693
|
+
});
|
|
4694
|
+
setPinnedHook(name, { version, sha256, source });
|
|
4695
|
+
}
|
|
4696
|
+
function removeHookFromStore(name) {
|
|
4697
|
+
const removedPin = removePinnedHook(name);
|
|
4698
|
+
const db = getDb();
|
|
4699
|
+
const removedRecord = removeHookRecord(db, name);
|
|
4700
|
+
return { removedPin, removedRecord };
|
|
4701
|
+
}
|
|
4792
4702
|
function checkScriptHash(name, actual) {
|
|
4793
4703
|
const db = getDb();
|
|
4794
4704
|
const record = getHookRecord(db, name);
|
|
@@ -4825,7 +4735,7 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
4825
4735
|
return checkScriptHash(name, await sha256File(scriptPath));
|
|
4826
4736
|
}
|
|
4827
4737
|
function retrustHook(name, scriptPath, version, source) {
|
|
4828
|
-
const actual = sha256Of(
|
|
4738
|
+
const actual = sha256Of(readFileSync4(scriptPath));
|
|
4829
4739
|
const db = getDb();
|
|
4830
4740
|
upsertHookRecord(db, {
|
|
4831
4741
|
name,
|
|
@@ -4842,13 +4752,157 @@ var init_store = __esm(() => {
|
|
|
4842
4752
|
init_config();
|
|
4843
4753
|
});
|
|
4844
4754
|
|
|
4755
|
+
// src/lib/profiles.ts
|
|
4756
|
+
var exports_profiles = {};
|
|
4757
|
+
__export(exports_profiles, {
|
|
4758
|
+
updateProfile: () => updateProfile,
|
|
4759
|
+
touchProfile: () => touchProfile,
|
|
4760
|
+
listProfiles: () => listProfiles,
|
|
4761
|
+
importProfiles: () => importProfiles,
|
|
4762
|
+
getProfilesDir: () => getProfilesDir,
|
|
4763
|
+
getProfile: () => getProfile,
|
|
4764
|
+
exportProfiles: () => exportProfiles,
|
|
4765
|
+
deleteProfile: () => deleteProfile,
|
|
4766
|
+
createProfile: () => createProfile
|
|
4767
|
+
});
|
|
4768
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
|
|
4769
|
+
import { join as join7 } from "path";
|
|
4770
|
+
import { homedir as homedir5 } from "os";
|
|
4771
|
+
function resolveProfilesDir() {
|
|
4772
|
+
const newDir = join7(homedir5(), ".hasna", "hooks", "profiles");
|
|
4773
|
+
const oldDir = join7(homedir5(), ".hooks", "profiles");
|
|
4774
|
+
if (!existsSync8(newDir) && existsSync8(oldDir)) {
|
|
4775
|
+
mkdirSync6(join7(homedir5(), ".hasna", "hooks"), { recursive: true });
|
|
4776
|
+
cpSync2(oldDir, newDir, { recursive: true });
|
|
4777
|
+
}
|
|
4778
|
+
return newDir;
|
|
4779
|
+
}
|
|
4780
|
+
function ensureProfilesDir() {
|
|
4781
|
+
if (!existsSync8(PROFILES_DIR)) {
|
|
4782
|
+
mkdirSync6(PROFILES_DIR, { recursive: true });
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
function profilePath(id) {
|
|
4786
|
+
return join7(PROFILES_DIR, `${id}.json`);
|
|
4787
|
+
}
|
|
4788
|
+
function shortUuid() {
|
|
4789
|
+
return crypto.randomUUID().slice(0, 8);
|
|
4790
|
+
}
|
|
4791
|
+
function getProfilesDir() {
|
|
4792
|
+
return PROFILES_DIR;
|
|
4793
|
+
}
|
|
4794
|
+
function createProfile(input) {
|
|
4795
|
+
ensureProfilesDir();
|
|
4796
|
+
const id = shortUuid();
|
|
4797
|
+
const now = new Date().toISOString();
|
|
4798
|
+
const profile = {
|
|
4799
|
+
agent_id: id,
|
|
4800
|
+
agent_type: input.agent_type,
|
|
4801
|
+
created_at: now,
|
|
4802
|
+
last_seen_at: now,
|
|
4803
|
+
preferences: {}
|
|
4804
|
+
};
|
|
4805
|
+
if (input.name) {
|
|
4806
|
+
profile.name = input.name;
|
|
4807
|
+
}
|
|
4808
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4809
|
+
`);
|
|
4810
|
+
return profile;
|
|
4811
|
+
}
|
|
4812
|
+
function getProfile(id) {
|
|
4813
|
+
const path = profilePath(id);
|
|
4814
|
+
try {
|
|
4815
|
+
if (!existsSync8(path))
|
|
4816
|
+
return null;
|
|
4817
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
4818
|
+
} catch {
|
|
4819
|
+
return null;
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
function listProfiles() {
|
|
4823
|
+
if (!existsSync8(PROFILES_DIR))
|
|
4824
|
+
return [];
|
|
4825
|
+
try {
|
|
4826
|
+
const files = readdirSync3(PROFILES_DIR).filter((f) => f.endsWith(".json"));
|
|
4827
|
+
const profiles = [];
|
|
4828
|
+
for (const file of files) {
|
|
4829
|
+
try {
|
|
4830
|
+
const content = readFileSync6(join7(PROFILES_DIR, file), "utf-8");
|
|
4831
|
+
profiles.push(JSON.parse(content));
|
|
4832
|
+
} catch {}
|
|
4833
|
+
}
|
|
4834
|
+
return profiles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
4835
|
+
} catch {
|
|
4836
|
+
return [];
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
function updateProfile(id, data) {
|
|
4840
|
+
const profile = getProfile(id);
|
|
4841
|
+
if (!profile)
|
|
4842
|
+
return null;
|
|
4843
|
+
if (data.name !== undefined)
|
|
4844
|
+
profile.name = data.name;
|
|
4845
|
+
if (data.preferences !== undefined)
|
|
4846
|
+
profile.preferences = data.preferences;
|
|
4847
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4848
|
+
`);
|
|
4849
|
+
return profile;
|
|
4850
|
+
}
|
|
4851
|
+
function deleteProfile(id) {
|
|
4852
|
+
const path = profilePath(id);
|
|
4853
|
+
if (!existsSync8(path))
|
|
4854
|
+
return false;
|
|
4855
|
+
try {
|
|
4856
|
+
rmSync2(path);
|
|
4857
|
+
return true;
|
|
4858
|
+
} catch {
|
|
4859
|
+
return false;
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4862
|
+
function touchProfile(id) {
|
|
4863
|
+
const profile = getProfile(id);
|
|
4864
|
+
if (!profile)
|
|
4865
|
+
return;
|
|
4866
|
+
profile.last_seen_at = new Date().toISOString();
|
|
4867
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4868
|
+
`);
|
|
4869
|
+
}
|
|
4870
|
+
function exportProfiles() {
|
|
4871
|
+
return listProfiles();
|
|
4872
|
+
}
|
|
4873
|
+
function importProfiles(profiles) {
|
|
4874
|
+
ensureProfilesDir();
|
|
4875
|
+
let imported = 0;
|
|
4876
|
+
let skipped = 0;
|
|
4877
|
+
for (const profile of profiles) {
|
|
4878
|
+
if (!profile.agent_id || !profile.agent_type) {
|
|
4879
|
+
skipped++;
|
|
4880
|
+
continue;
|
|
4881
|
+
}
|
|
4882
|
+
const path = profilePath(profile.agent_id);
|
|
4883
|
+
if (existsSync8(path)) {
|
|
4884
|
+
skipped++;
|
|
4885
|
+
continue;
|
|
4886
|
+
}
|
|
4887
|
+
writeFileSync5(path, JSON.stringify(profile, null, 2) + `
|
|
4888
|
+
`);
|
|
4889
|
+
imported++;
|
|
4890
|
+
}
|
|
4891
|
+
return { imported, skipped };
|
|
4892
|
+
}
|
|
4893
|
+
var PROFILES_DIR;
|
|
4894
|
+
var init_profiles = __esm(() => {
|
|
4895
|
+
PROFILES_DIR = resolveProfilesDir();
|
|
4896
|
+
});
|
|
4897
|
+
|
|
4845
4898
|
// src/lib/run.ts
|
|
4846
4899
|
var exports_run = {};
|
|
4847
4900
|
__export(exports_run, {
|
|
4848
|
-
executeVerifiedScript: () => executeVerifiedScript
|
|
4901
|
+
executeVerifiedScript: () => executeVerifiedScript,
|
|
4902
|
+
HookTimeoutError: () => HookTimeoutError
|
|
4849
4903
|
});
|
|
4850
4904
|
import { randomBytes } from "crypto";
|
|
4851
|
-
import { rmSync as
|
|
4905
|
+
import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
4852
4906
|
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
4853
4907
|
function extensionOf(scriptPath) {
|
|
4854
4908
|
const base = basename2(scriptPath);
|
|
@@ -4885,37 +4939,192 @@ function interpreterFor(name, scriptPath, content) {
|
|
|
4885
4939
|
return { command: ["bun", "run"], tempExt: ext };
|
|
4886
4940
|
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)`);
|
|
4887
4941
|
}
|
|
4942
|
+
async function readPipeWithDeadline(stream, deadlineMs) {
|
|
4943
|
+
const reader = stream.getReader();
|
|
4944
|
+
const chunks = [];
|
|
4945
|
+
const deadline = Date.now() + deadlineMs;
|
|
4946
|
+
try {
|
|
4947
|
+
while (true) {
|
|
4948
|
+
const remaining = deadline - Date.now();
|
|
4949
|
+
if (remaining <= 0)
|
|
4950
|
+
break;
|
|
4951
|
+
let result;
|
|
4952
|
+
try {
|
|
4953
|
+
result = await Promise.race([
|
|
4954
|
+
reader.read(),
|
|
4955
|
+
new Promise((_, reject) => {
|
|
4956
|
+
const t = setTimeout(() => reject(new Error("pipe-drain-deadline")), remaining);
|
|
4957
|
+
t.unref?.();
|
|
4958
|
+
})
|
|
4959
|
+
]);
|
|
4960
|
+
} catch {
|
|
4961
|
+
break;
|
|
4962
|
+
}
|
|
4963
|
+
if (result.done)
|
|
4964
|
+
break;
|
|
4965
|
+
chunks.push(new TextDecoder().decode(result.value));
|
|
4966
|
+
}
|
|
4967
|
+
} finally {
|
|
4968
|
+
try {
|
|
4969
|
+
await reader.cancel();
|
|
4970
|
+
} catch {}
|
|
4971
|
+
}
|
|
4972
|
+
return chunks.join("");
|
|
4973
|
+
}
|
|
4888
4974
|
async function executeVerifiedScript(options) {
|
|
4889
4975
|
const scriptDir = dirname4(options.scriptPath);
|
|
4890
4976
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
4891
4977
|
const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${interpreter.tempExt}`);
|
|
4978
|
+
let proc = null;
|
|
4979
|
+
let timedOut = false;
|
|
4980
|
+
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
4981
|
+
timedOut = true;
|
|
4982
|
+
killGroup(proc);
|
|
4983
|
+
}, options.timeout) : null;
|
|
4892
4984
|
try {
|
|
4893
4985
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
4894
|
-
|
|
4986
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
4895
4987
|
stdin: new Response(options.stdin),
|
|
4896
4988
|
stdout: "pipe",
|
|
4897
4989
|
stderr: "pipe",
|
|
4898
4990
|
env: options.env ?? process.env,
|
|
4899
|
-
|
|
4991
|
+
detached: true
|
|
4900
4992
|
});
|
|
4901
|
-
const
|
|
4902
|
-
|
|
4903
|
-
new
|
|
4904
|
-
|
|
4993
|
+
const exitCode = await proc.exited;
|
|
4994
|
+
if (timedOut) {
|
|
4995
|
+
throw new HookTimeoutError(options.timeout);
|
|
4996
|
+
}
|
|
4997
|
+
const [stdoutText, stderrText] = await Promise.all([
|
|
4998
|
+
readPipeWithDeadline(proc.stdout, 750),
|
|
4999
|
+
readPipeWithDeadline(proc.stderr, 750)
|
|
4905
5000
|
]);
|
|
5001
|
+
killGroup(proc);
|
|
4906
5002
|
return { stdout: stdoutText, stderr: stderrText, exitCode };
|
|
4907
5003
|
} finally {
|
|
5004
|
+
if (timeoutTimer)
|
|
5005
|
+
clearTimeout(timeoutTimer);
|
|
5006
|
+
try {
|
|
5007
|
+
rmSync4(tempPath, { force: true });
|
|
5008
|
+
} catch {}
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
function killGroup(proc) {
|
|
5012
|
+
if (!proc)
|
|
5013
|
+
return;
|
|
5014
|
+
try {
|
|
5015
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
5016
|
+
} catch {
|
|
4908
5017
|
try {
|
|
4909
|
-
|
|
5018
|
+
process.kill(proc.pid, "SIGKILL");
|
|
4910
5019
|
} catch {}
|
|
4911
5020
|
}
|
|
4912
5021
|
}
|
|
4913
|
-
var BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
5022
|
+
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
4914
5023
|
var init_run = __esm(() => {
|
|
5024
|
+
HookTimeoutError = class HookTimeoutError extends Error {
|
|
5025
|
+
timeoutMs;
|
|
5026
|
+
constructor(timeoutMs) {
|
|
5027
|
+
super(`Hook timed out after ${timeoutMs}ms (process group killed)`);
|
|
5028
|
+
this.name = "HookTimeoutError";
|
|
5029
|
+
this.timeoutMs = timeoutMs;
|
|
5030
|
+
}
|
|
5031
|
+
};
|
|
4915
5032
|
BASH_EXTENSIONS = new Set(["sh", "bash"]);
|
|
4916
5033
|
BUN_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]);
|
|
4917
5034
|
});
|
|
4918
5035
|
|
|
5036
|
+
// src/lib/db-writer.ts
|
|
5037
|
+
var exports_db_writer = {};
|
|
5038
|
+
__export(exports_db_writer, {
|
|
5039
|
+
writeHookEvent: () => writeHookEvent,
|
|
5040
|
+
resolveEventType: () => resolveEventType,
|
|
5041
|
+
recordHookRun: () => recordHookRun,
|
|
5042
|
+
normalizeEventType: () => normalizeEventType
|
|
5043
|
+
});
|
|
5044
|
+
function nanoid2() {
|
|
5045
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
|
|
5046
|
+
}
|
|
5047
|
+
function writeHookEvent(event) {
|
|
5048
|
+
try {
|
|
5049
|
+
const db = getDb();
|
|
5050
|
+
const id = nanoid2();
|
|
5051
|
+
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
5052
|
+
db.run(`INSERT INTO hook_events
|
|
5053
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
5054
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5055
|
+
id,
|
|
5056
|
+
timestamp,
|
|
5057
|
+
event.session_id,
|
|
5058
|
+
event.hook_name,
|
|
5059
|
+
event.event_type,
|
|
5060
|
+
event.tool_name ?? null,
|
|
5061
|
+
event.tool_input ? event.tool_input.slice(0, 500) : null,
|
|
5062
|
+
event.result ?? null,
|
|
5063
|
+
event.error ?? null,
|
|
5064
|
+
event.duration_ms ?? null,
|
|
5065
|
+
event.project_dir ?? null,
|
|
5066
|
+
event.metadata ?? null
|
|
5067
|
+
]);
|
|
5068
|
+
} catch (err) {
|
|
5069
|
+
process.stderr.write(`[hooks db-writer] failed to write event: ${err}
|
|
5070
|
+
`);
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
function normalizeEventType(value) {
|
|
5074
|
+
if (typeof value !== "string")
|
|
5075
|
+
return null;
|
|
5076
|
+
if (ALLOWED_EVENT_TYPES.has(value))
|
|
5077
|
+
return value;
|
|
5078
|
+
const bare = value.split(":")[0] ?? "";
|
|
5079
|
+
return ALLOWED_EVENT_TYPES.has(bare) ? bare : null;
|
|
5080
|
+
}
|
|
5081
|
+
function resolveEventType(inputEvent, fallbackEvent) {
|
|
5082
|
+
const fromInput = normalizeEventType(inputEvent);
|
|
5083
|
+
if (fromInput)
|
|
5084
|
+
return fromInput;
|
|
5085
|
+
return normalizeEventType(fallbackEvent ?? null);
|
|
5086
|
+
}
|
|
5087
|
+
function recordHookRun(record) {
|
|
5088
|
+
const eventType = record.eventType ? normalizeEventType(record.eventType) : null;
|
|
5089
|
+
if (!eventType) {
|
|
5090
|
+
process.stderr.write(`[hooks db-writer] skipped event for '${record.hookName}': unsupported event type '${String(record.eventType)}'
|
|
5091
|
+
`);
|
|
5092
|
+
return;
|
|
5093
|
+
}
|
|
5094
|
+
const metadata = {
|
|
5095
|
+
...record.metadata ?? {},
|
|
5096
|
+
version: record.version ?? null,
|
|
5097
|
+
sha256: record.sha256 ?? null,
|
|
5098
|
+
exit_code: record.exitCode
|
|
5099
|
+
};
|
|
5100
|
+
writeHookEvent({
|
|
5101
|
+
session_id: record.sessionId ?? "cli",
|
|
5102
|
+
hook_name: record.hookName,
|
|
5103
|
+
event_type: eventType,
|
|
5104
|
+
tool_name: record.toolName ?? null,
|
|
5105
|
+
tool_input: record.toolInput !== undefined ? JSON.stringify(record.toolInput) : null,
|
|
5106
|
+
result: record.result ?? null,
|
|
5107
|
+
error: record.error ?? null,
|
|
5108
|
+
duration_ms: record.durationMs,
|
|
5109
|
+
project_dir: record.projectDir ?? null,
|
|
5110
|
+
metadata: JSON.stringify(metadata)
|
|
5111
|
+
});
|
|
5112
|
+
}
|
|
5113
|
+
var ALLOWED_EVENT_TYPES;
|
|
5114
|
+
var init_db_writer = __esm(() => {
|
|
5115
|
+
init_db();
|
|
5116
|
+
ALLOWED_EVENT_TYPES = new Set([
|
|
5117
|
+
"PreToolUse",
|
|
5118
|
+
"PostToolUse",
|
|
5119
|
+
"Stop",
|
|
5120
|
+
"Notification",
|
|
5121
|
+
"SessionStart",
|
|
5122
|
+
"SessionEnd",
|
|
5123
|
+
"UserPromptSubmit",
|
|
5124
|
+
"SubagentStart"
|
|
5125
|
+
]);
|
|
5126
|
+
});
|
|
5127
|
+
|
|
4919
5128
|
// src/db/storage-sync.ts
|
|
4920
5129
|
init_db();
|
|
4921
5130
|
|
|
@@ -5847,7 +6056,7 @@ function getHook(name) {
|
|
|
5847
6056
|
return HOOKS.find((h) => h.name === name);
|
|
5848
6057
|
}
|
|
5849
6058
|
// src/lib/installer.ts
|
|
5850
|
-
import { existsSync as
|
|
6059
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
5851
6060
|
import { join as join6, dirname as dirname3 } from "path";
|
|
5852
6061
|
import { homedir as homedir4 } from "os";
|
|
5853
6062
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -5917,7 +6126,8 @@ function resolveHook(name) {
|
|
|
5917
6126
|
events: custom2.manifest.events,
|
|
5918
6127
|
description: custom2.manifest.description ?? "Custom hook",
|
|
5919
6128
|
scriptPath: custom2.scriptPath,
|
|
5920
|
-
meta: meta2
|
|
6129
|
+
meta: meta2,
|
|
6130
|
+
timeoutMs: custom2.manifest.timeout_ms ?? null
|
|
5921
6131
|
};
|
|
5922
6132
|
}
|
|
5923
6133
|
const meta = getHook(shortManifestName(name));
|
|
@@ -5931,13 +6141,16 @@ function resolveHook(name) {
|
|
|
5931
6141
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
5932
6142
|
description: meta.description,
|
|
5933
6143
|
scriptPath,
|
|
5934
|
-
meta
|
|
6144
|
+
meta,
|
|
6145
|
+
timeoutMs: null
|
|
5935
6146
|
};
|
|
5936
6147
|
}
|
|
5937
6148
|
|
|
5938
6149
|
// src/lib/installer.ts
|
|
6150
|
+
init_manifest();
|
|
6151
|
+
init_store();
|
|
5939
6152
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
5940
|
-
var HOOKS_DIR =
|
|
6153
|
+
var HOOKS_DIR = existsSync7(join6(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join6(__dirname2, "..", "..", "hooks") : join6(__dirname2, "..", "hooks");
|
|
5941
6154
|
function normalizeHookName(name) {
|
|
5942
6155
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
5943
6156
|
}
|
|
@@ -6019,13 +6232,13 @@ function getHookPath(name) {
|
|
|
6019
6232
|
return join6(HOOKS_DIR, normalizeHookName(shortName));
|
|
6020
6233
|
}
|
|
6021
6234
|
function hookExists(name) {
|
|
6022
|
-
return
|
|
6235
|
+
return existsSync7(getHookPath(name));
|
|
6023
6236
|
}
|
|
6024
6237
|
function readSettings(scope = "global", target = "claude") {
|
|
6025
6238
|
const path = getSettingsPath(scope, target);
|
|
6026
6239
|
try {
|
|
6027
|
-
if (
|
|
6028
|
-
return JSON.parse(
|
|
6240
|
+
if (existsSync7(path)) {
|
|
6241
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
6029
6242
|
}
|
|
6030
6243
|
} catch (error) {
|
|
6031
6244
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -6035,10 +6248,10 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
6035
6248
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
6036
6249
|
const path = getSettingsPath(scope, target);
|
|
6037
6250
|
const dir = dirname3(path);
|
|
6038
|
-
if (!
|
|
6039
|
-
|
|
6251
|
+
if (!existsSync7(dir)) {
|
|
6252
|
+
mkdirSync5(dir, { recursive: true });
|
|
6040
6253
|
}
|
|
6041
|
-
|
|
6254
|
+
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
6042
6255
|
`);
|
|
6043
6256
|
}
|
|
6044
6257
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -6124,7 +6337,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
6124
6337
|
function readCodewithConfig(scope, configPath) {
|
|
6125
6338
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6126
6339
|
try {
|
|
6127
|
-
return
|
|
6340
|
+
return existsSync7(path) ? readFileSync5(path, "utf-8") : "";
|
|
6128
6341
|
} catch {
|
|
6129
6342
|
return "";
|
|
6130
6343
|
}
|
|
@@ -6132,13 +6345,13 @@ function readCodewithConfig(scope, configPath) {
|
|
|
6132
6345
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
6133
6346
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6134
6347
|
const dir = dirname3(path);
|
|
6135
|
-
if (!
|
|
6136
|
-
|
|
6137
|
-
const existing =
|
|
6348
|
+
if (!existsSync7(dir))
|
|
6349
|
+
mkdirSync5(dir, { recursive: true });
|
|
6350
|
+
const existing = existsSync7(path) ? readFileSync5(path, "utf-8") : "";
|
|
6138
6351
|
const sep2 = existing.trim() ? `
|
|
6139
6352
|
|
|
6140
6353
|
` : "";
|
|
6141
|
-
|
|
6354
|
+
writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
6142
6355
|
return path;
|
|
6143
6356
|
}
|
|
6144
6357
|
function detectConflict(name, scope, target) {
|
|
@@ -6314,9 +6527,6 @@ function removeHookFromAllEvents(settings, name) {
|
|
|
6314
6527
|
}
|
|
6315
6528
|
}
|
|
6316
6529
|
function unregisterHook(name, scope = "global", target = "claude") {
|
|
6317
|
-
const meta = resolveHookMeta(name);
|
|
6318
|
-
if (!meta)
|
|
6319
|
-
return;
|
|
6320
6530
|
const settings = readSettings(scope, target);
|
|
6321
6531
|
if (!settings.hooks)
|
|
6322
6532
|
return;
|
|
@@ -6382,6 +6592,136 @@ function removeHookForTarget(name, scope, target) {
|
|
|
6382
6592
|
unregisterHook(name, scope, target);
|
|
6383
6593
|
return true;
|
|
6384
6594
|
}
|
|
6595
|
+
function removeCodewithHookEntry(configText, name) {
|
|
6596
|
+
const sections = configText.split(/\n\s*\n/);
|
|
6597
|
+
const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
|
|
6598
|
+
const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
|
|
6599
|
+
const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
|
|
6600
|
+
const eventsWithEntries = new Set;
|
|
6601
|
+
const kept = [];
|
|
6602
|
+
let removed = false;
|
|
6603
|
+
for (const section of sections) {
|
|
6604
|
+
const lines = section.split(`
|
|
6605
|
+
`);
|
|
6606
|
+
const entryMatch = entryHeaderRe.exec(lines[0] ?? "");
|
|
6607
|
+
if (entryMatch) {
|
|
6608
|
+
const event = entryMatch[1];
|
|
6609
|
+
eventsWithEntries.add(event);
|
|
6610
|
+
if (lines.some((line) => nameRe.test(line.trim()))) {
|
|
6611
|
+
removed = true;
|
|
6612
|
+
continue;
|
|
6613
|
+
}
|
|
6614
|
+
kept.push({ text: section });
|
|
6615
|
+
continue;
|
|
6616
|
+
}
|
|
6617
|
+
const eventMatch = eventHeaderRe.exec(lines[0] ?? "");
|
|
6618
|
+
kept.push({ text: section, eventHeaderFor: eventMatch ? eventMatch[1] : undefined });
|
|
6619
|
+
}
|
|
6620
|
+
const keptEntriesByEvent = new Set;
|
|
6621
|
+
for (const item of kept) {
|
|
6622
|
+
const m = entryHeaderRe.exec(item.text.split(`
|
|
6623
|
+
`)[0] ?? "");
|
|
6624
|
+
if (m)
|
|
6625
|
+
keptEntriesByEvent.add(m[1]);
|
|
6626
|
+
}
|
|
6627
|
+
const finalSections = kept.filter((item) => {
|
|
6628
|
+
if (item.eventHeaderFor === undefined)
|
|
6629
|
+
return true;
|
|
6630
|
+
if (!eventsWithEntries.has(item.eventHeaderFor))
|
|
6631
|
+
return true;
|
|
6632
|
+
return keptEntriesByEvent.has(item.eventHeaderFor);
|
|
6633
|
+
}).map((item) => item.text);
|
|
6634
|
+
return {
|
|
6635
|
+
text: finalSections.join(`
|
|
6636
|
+
|
|
6637
|
+
`) + (configText.endsWith(`
|
|
6638
|
+
`) ? `
|
|
6639
|
+
` : ""),
|
|
6640
|
+
removed
|
|
6641
|
+
};
|
|
6642
|
+
}
|
|
6643
|
+
function escapeRegExp(value) {
|
|
6644
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6645
|
+
}
|
|
6646
|
+
function codewithHasHookEntryInText(configText, name) {
|
|
6647
|
+
return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
|
|
6648
|
+
}
|
|
6649
|
+
function codewithHasHookEntry(name) {
|
|
6650
|
+
const path = getSettingsPath("global", "codewith");
|
|
6651
|
+
if (!existsSync7(path))
|
|
6652
|
+
return false;
|
|
6653
|
+
try {
|
|
6654
|
+
return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
|
|
6655
|
+
} catch {
|
|
6656
|
+
return false;
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
function uninstallHook(name, scope = "global", target = "claude") {
|
|
6660
|
+
const shortName = shortHookName(name);
|
|
6661
|
+
const custom2 = readCustomManifest(shortName);
|
|
6662
|
+
const bundledMeta = custom2 ? undefined : getHook(shortName);
|
|
6663
|
+
const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
|
|
6664
|
+
const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
|
|
6665
|
+
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
6666
|
+
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
6667
|
+
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
|
|
6668
|
+
if (!custom2 && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
6669
|
+
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
6670
|
+
}
|
|
6671
|
+
const writableTargets = target === "all" ? ["claude", "gemini"] : target === "codewith" ? [] : [target];
|
|
6672
|
+
const scopes = [];
|
|
6673
|
+
for (const s of ["global", "project"]) {
|
|
6674
|
+
for (const t of writableTargets) {
|
|
6675
|
+
if (getRegisteredHooksForTarget(s, t).includes(shortName)) {
|
|
6676
|
+
unregisterHook(shortName, s, t);
|
|
6677
|
+
if (!scopes.includes(s))
|
|
6678
|
+
scopes.push(s);
|
|
6679
|
+
}
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
const registrationsRemaining = [];
|
|
6683
|
+
if (target === "codewith" || target === "all") {
|
|
6684
|
+
const codewithPath = getSettingsPath("global", "codewith");
|
|
6685
|
+
if (existsSync7(codewithPath)) {
|
|
6686
|
+
const before = readFileSync5(codewithPath, "utf-8");
|
|
6687
|
+
const after = removeCodewithHookEntry(before, shortName);
|
|
6688
|
+
if (after.removed) {
|
|
6689
|
+
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
6690
|
+
}
|
|
6691
|
+
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
6692
|
+
registrationsRemaining.push("codewith");
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
}
|
|
6696
|
+
if (custom2) {
|
|
6697
|
+
try {
|
|
6698
|
+
rmSync(customHookDir(shortName), { recursive: true, force: true });
|
|
6699
|
+
} catch {
|
|
6700
|
+
return {
|
|
6701
|
+
name: shortName,
|
|
6702
|
+
removed: false,
|
|
6703
|
+
source: "custom",
|
|
6704
|
+
settingsScopes: scopes,
|
|
6705
|
+
storeDirRemoved: false,
|
|
6706
|
+
pinRemoved: false,
|
|
6707
|
+
dbRecordRemoved: false,
|
|
6708
|
+
registrationsRemaining,
|
|
6709
|
+
error: `Hook '${shortName}' store directory could not be removed; trust records preserved (no fail-open)`
|
|
6710
|
+
};
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
const { removedPin, removedRecord } = removeHookFromStore(shortName);
|
|
6714
|
+
return {
|
|
6715
|
+
name: shortName,
|
|
6716
|
+
removed: true,
|
|
6717
|
+
source: custom2 ? "custom" : bundledMeta ? "bundled" : "registered-only",
|
|
6718
|
+
settingsScopes: scopes,
|
|
6719
|
+
storeDirRemoved: custom2 ? true : false,
|
|
6720
|
+
pinRemoved: removedPin,
|
|
6721
|
+
dbRecordRemoved: removedRecord,
|
|
6722
|
+
registrationsRemaining
|
|
6723
|
+
};
|
|
6724
|
+
}
|
|
6385
6725
|
// src/index.ts
|
|
6386
6726
|
init_profiles();
|
|
6387
6727
|
init_config();
|
|
@@ -6392,7 +6732,7 @@ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
|
6392
6732
|
// src/lib/custom-install.ts
|
|
6393
6733
|
init_config();
|
|
6394
6734
|
init_manifest();
|
|
6395
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as
|
|
6735
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
6396
6736
|
import { join as join8 } from "path";
|
|
6397
6737
|
import { tmpdir } from "os";
|
|
6398
6738
|
function manifestAt(dir) {
|
|
@@ -6430,7 +6770,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
6430
6770
|
proc.exited
|
|
6431
6771
|
]);
|
|
6432
6772
|
if (exitCode !== 0) {
|
|
6433
|
-
|
|
6773
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
6434
6774
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
6435
6775
|
}
|
|
6436
6776
|
return join8(tmp, "repo");
|
|
@@ -6451,7 +6791,7 @@ async function installCustomSource(source) {
|
|
|
6451
6791
|
try {
|
|
6452
6792
|
return copyManifestDir(repoDir, source, "git");
|
|
6453
6793
|
} finally {
|
|
6454
|
-
|
|
6794
|
+
rmSync3(repoDir, { recursive: true, force: true });
|
|
6455
6795
|
}
|
|
6456
6796
|
}
|
|
6457
6797
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
@@ -6523,6 +6863,7 @@ async function fetchJson(base, path) {
|
|
|
6523
6863
|
headers["x-api-key"] = apiKey;
|
|
6524
6864
|
const res = await fetch(`${base}${path}`, {
|
|
6525
6865
|
headers,
|
|
6866
|
+
redirect: "error",
|
|
6526
6867
|
signal: AbortSignal.timeout(15000)
|
|
6527
6868
|
});
|
|
6528
6869
|
if (res.status === 401) {
|
|
@@ -6626,11 +6967,14 @@ async function syncHooks(options = {}) {
|
|
|
6626
6967
|
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6627
6968
|
const scriptPath = resolveScriptPath(name);
|
|
6628
6969
|
const actual = await sha256File(scriptPath);
|
|
6629
|
-
|
|
6970
|
+
if (actual !== entry.sha256) {
|
|
6971
|
+
throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6972
|
+
}
|
|
6973
|
+
setPinnedHook(name, { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" });
|
|
6630
6974
|
upsertHookRecord(db, {
|
|
6631
6975
|
name,
|
|
6632
6976
|
version: entry.version,
|
|
6633
|
-
sha256:
|
|
6977
|
+
sha256: entry.sha256,
|
|
6634
6978
|
source_type: entry.source ?? "remote",
|
|
6635
6979
|
source_ref: apiUrl,
|
|
6636
6980
|
last_verified_at: new Date().toISOString()
|
|
@@ -6638,6 +6982,59 @@ async function syncHooks(options = {}) {
|
|
|
6638
6982
|
}
|
|
6639
6983
|
return { apiUrl, dryRun: false, diff };
|
|
6640
6984
|
}
|
|
6985
|
+
async function fetchPinnedHook(name, version, apiUrl) {
|
|
6986
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
6987
|
+
const entry = remoteLock.hooks?.[name];
|
|
6988
|
+
if (!entry) {
|
|
6989
|
+
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
6990
|
+
}
|
|
6991
|
+
if (entry.version !== version) {
|
|
6992
|
+
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${entry.version})`);
|
|
6993
|
+
}
|
|
6994
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}`);
|
|
6995
|
+
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6996
|
+
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
6997
|
+
}
|
|
6998
|
+
const manifestName = shortManifestName(artifact.manifest.name ?? name);
|
|
6999
|
+
if (manifestName !== name) {
|
|
7000
|
+
throw new Error(`artifact for '${name}@${version}' declares a different hook name ('${manifestName}')`);
|
|
7001
|
+
}
|
|
7002
|
+
if (artifact.manifest.version !== version) {
|
|
7003
|
+
throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
|
|
7004
|
+
}
|
|
7005
|
+
const actualSha = sha256OfText(artifact.script);
|
|
7006
|
+
if (actualSha !== entry.sha256) {
|
|
7007
|
+
throw new Error(`sha256 mismatch for '${name}@${version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
7008
|
+
}
|
|
7009
|
+
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
7010
|
+
const scriptRel = artifact.manifest.script.includes(`
|
|
7011
|
+
`) ? "script.ts" : artifact.manifest.script;
|
|
7012
|
+
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
7013
|
+
const scriptPath = resolveScriptPath(name);
|
|
7014
|
+
const actual = await sha256File(scriptPath);
|
|
7015
|
+
if (actual !== entry.sha256) {
|
|
7016
|
+
throw new Error(`sha256 mismatch after write for '${name}@${version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
7017
|
+
}
|
|
7018
|
+
const db = getDb();
|
|
7019
|
+
setPinnedHook(name, { version, sha256: entry.sha256, source: entry.source ?? "remote" });
|
|
7020
|
+
upsertHookRecord(db, {
|
|
7021
|
+
name,
|
|
7022
|
+
version,
|
|
7023
|
+
sha256: entry.sha256,
|
|
7024
|
+
source_type: entry.source ?? "remote",
|
|
7025
|
+
source_ref: apiUrl,
|
|
7026
|
+
last_verified_at: new Date().toISOString()
|
|
7027
|
+
});
|
|
7028
|
+
return {
|
|
7029
|
+
name,
|
|
7030
|
+
version,
|
|
7031
|
+
sha256: entry.sha256,
|
|
7032
|
+
source: entry.source ?? "remote",
|
|
7033
|
+
source_ref: apiUrl,
|
|
7034
|
+
artifact,
|
|
7035
|
+
scriptPath
|
|
7036
|
+
};
|
|
7037
|
+
}
|
|
6641
7038
|
// src/serve.ts
|
|
6642
7039
|
init_manifest();
|
|
6643
7040
|
init_store();
|
|
@@ -6897,16 +7294,68 @@ async function runHook(name, input, options = {}) {
|
|
|
6897
7294
|
};
|
|
6898
7295
|
}
|
|
6899
7296
|
}
|
|
6900
|
-
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
6901
|
-
const
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
7297
|
+
const { executeVerifiedScript: executeVerifiedScript2, HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7298
|
+
const manifestTimeout = custom2?.manifest.timeout_ms ?? null;
|
|
7299
|
+
const effectiveTimeout = options.timeout !== undefined && options.timeout > 0 ? options.timeout : manifestTimeout ?? null;
|
|
7300
|
+
const started = Date.now();
|
|
7301
|
+
let stdoutText = "";
|
|
7302
|
+
let stderrText = "";
|
|
7303
|
+
let exitCode = 0;
|
|
7304
|
+
try {
|
|
7305
|
+
({ stdout: stdoutText, stderr: stderrText, exitCode } = await executeVerifiedScript2({
|
|
7306
|
+
name,
|
|
7307
|
+
scriptPath: script,
|
|
7308
|
+
content,
|
|
7309
|
+
args: custom2?.manifest.args ?? [],
|
|
7310
|
+
stdin: JSON.stringify(hookInput),
|
|
7311
|
+
env: process.env,
|
|
7312
|
+
timeout: effectiveTimeout ?? undefined
|
|
7313
|
+
}));
|
|
7314
|
+
} catch (err) {
|
|
7315
|
+
if (err instanceof HookTimeoutError2) {
|
|
7316
|
+
try {
|
|
7317
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
7318
|
+
recordHookRun2({
|
|
7319
|
+
hookName: name,
|
|
7320
|
+
eventType: resolveEventType2(input.hook_event_name, custom2?.manifest.events[0] ?? "PostToolUse"),
|
|
7321
|
+
version: custom2?.manifest.version,
|
|
7322
|
+
sha256: sha256Of2(content),
|
|
7323
|
+
sessionId: typeof input.session_id === "string" ? input.session_id : null,
|
|
7324
|
+
toolName: typeof input.tool_name === "string" ? input.tool_name : null,
|
|
7325
|
+
toolInput: input.tool_input,
|
|
7326
|
+
error: err.message.slice(0, 500),
|
|
7327
|
+
exitCode: -1,
|
|
7328
|
+
durationMs: Date.now() - started,
|
|
7329
|
+
projectDir: process.cwd()
|
|
7330
|
+
});
|
|
7331
|
+
} catch {}
|
|
7332
|
+
throw err;
|
|
7333
|
+
}
|
|
7334
|
+
throw err;
|
|
7335
|
+
}
|
|
7336
|
+
const durationMs = Date.now() - started;
|
|
7337
|
+
try {
|
|
7338
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
7339
|
+
let outputJson = {};
|
|
7340
|
+
try {
|
|
7341
|
+
outputJson = JSON.parse(stdoutText);
|
|
7342
|
+
} catch {}
|
|
7343
|
+
const blocked = outputJson.decision === "block" || outputJson.continue === false;
|
|
7344
|
+
recordHookRun2({
|
|
7345
|
+
hookName: name,
|
|
7346
|
+
eventType: resolveEventType2(input.hook_event_name, custom2?.manifest.events[0] ?? "PostToolUse"),
|
|
7347
|
+
version: custom2?.manifest.version,
|
|
7348
|
+
sha256: sha256Of2(content),
|
|
7349
|
+
sessionId: typeof input.session_id === "string" ? input.session_id : null,
|
|
7350
|
+
toolName: typeof input.tool_name === "string" ? input.tool_name : null,
|
|
7351
|
+
toolInput: input.tool_input,
|
|
7352
|
+
result: blocked ? "block" : "continue",
|
|
7353
|
+
error: exitCode !== 0 ? (stderrText || `hook exited with code ${exitCode}`).slice(0, 500) : null,
|
|
7354
|
+
exitCode,
|
|
7355
|
+
durationMs,
|
|
7356
|
+
projectDir: process.cwd()
|
|
7357
|
+
});
|
|
7358
|
+
} catch {}
|
|
6910
7359
|
let output = {};
|
|
6911
7360
|
try {
|
|
6912
7361
|
output = JSON.parse(stdoutText);
|
|
@@ -6922,6 +7371,7 @@ export {
|
|
|
6922
7371
|
verifyScriptHash,
|
|
6923
7372
|
upsertHookRecord,
|
|
6924
7373
|
updateProfile,
|
|
7374
|
+
uninstallHook,
|
|
6925
7375
|
touchProfile,
|
|
6926
7376
|
syncHooks,
|
|
6927
7377
|
storageSync,
|
|
@@ -6946,12 +7396,14 @@ export {
|
|
|
6946
7396
|
removeProjectHook,
|
|
6947
7397
|
removePinnedHook,
|
|
6948
7398
|
removeHookRecord,
|
|
7399
|
+
removeHookFromStore,
|
|
6949
7400
|
removeHook,
|
|
6950
7401
|
readLock,
|
|
6951
7402
|
readCustomManifest,
|
|
6952
7403
|
readConfig,
|
|
6953
7404
|
provisionCloudflareResources,
|
|
6954
7405
|
planSync,
|
|
7406
|
+
pinInstalledHook,
|
|
6955
7407
|
parseStorageTables,
|
|
6956
7408
|
parseManifest,
|
|
6957
7409
|
listProjectHooks,
|
|
@@ -6991,6 +7443,7 @@ export {
|
|
|
6991
7443
|
getHook,
|
|
6992
7444
|
getCustomHooksDir,
|
|
6993
7445
|
getConfigPath,
|
|
7446
|
+
fetchPinnedHook,
|
|
6994
7447
|
exportProfiles,
|
|
6995
7448
|
deleteProfile,
|
|
6996
7449
|
createProfile,
|