@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/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,216 @@ 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, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
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, { leaderAlive: false });
|
|
4906
5002
|
return { stdout: stdoutText, stderr: stderrText, exitCode };
|
|
4907
5003
|
} finally {
|
|
5004
|
+
if (timeoutTimer)
|
|
5005
|
+
clearTimeout(timeoutTimer);
|
|
4908
5006
|
try {
|
|
4909
|
-
|
|
5007
|
+
rmSync4(tempPath, { force: true });
|
|
4910
5008
|
} catch {}
|
|
4911
5009
|
}
|
|
4912
5010
|
}
|
|
4913
|
-
|
|
5011
|
+
function killGroup(proc, options) {
|
|
5012
|
+
if (!proc)
|
|
5013
|
+
return;
|
|
5014
|
+
const pgid = proc.pid;
|
|
5015
|
+
for (let pass = 0;pass < 5; pass++) {
|
|
5016
|
+
let members = [];
|
|
5017
|
+
try {
|
|
5018
|
+
const res = Bun.spawnSync(["ps", "-eo", "pid=,pgid="], { stdout: "pipe", stderr: "pipe" });
|
|
5019
|
+
if (res.exitCode === 0) {
|
|
5020
|
+
members = res.stdout.toString().split(`
|
|
5021
|
+
`).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);
|
|
5022
|
+
}
|
|
5023
|
+
} catch {}
|
|
5024
|
+
if (members.length === 0)
|
|
5025
|
+
break;
|
|
5026
|
+
let killed = 0;
|
|
5027
|
+
for (const pid of members) {
|
|
5028
|
+
try {
|
|
5029
|
+
const probe = Bun.spawnSync(["ps", "-o", "pid=,pgid=", "-p", String(pid)], { stdout: "pipe", stderr: "pipe" });
|
|
5030
|
+
const cols = probe.stdout.toString().trim().split(/\s+/);
|
|
5031
|
+
if (cols.length === 2 && cols[1] === String(pgid)) {
|
|
5032
|
+
process.kill(pid, "SIGKILL");
|
|
5033
|
+
killed++;
|
|
5034
|
+
}
|
|
5035
|
+
} catch {}
|
|
5036
|
+
}
|
|
5037
|
+
if (killed === 0)
|
|
5038
|
+
break;
|
|
5039
|
+
}
|
|
5040
|
+
if (options.leaderAlive) {
|
|
5041
|
+
try {
|
|
5042
|
+
process.kill(proc.pid, "SIGKILL");
|
|
5043
|
+
} catch {}
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
4914
5047
|
var init_run = __esm(() => {
|
|
5048
|
+
HookTimeoutError = class HookTimeoutError extends Error {
|
|
5049
|
+
timeoutMs;
|
|
5050
|
+
constructor(timeoutMs) {
|
|
5051
|
+
super(`Hook timed out after ${timeoutMs}ms (process group killed)`);
|
|
5052
|
+
this.name = "HookTimeoutError";
|
|
5053
|
+
this.timeoutMs = timeoutMs;
|
|
5054
|
+
}
|
|
5055
|
+
};
|
|
4915
5056
|
BASH_EXTENSIONS = new Set(["sh", "bash"]);
|
|
4916
5057
|
BUN_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]);
|
|
4917
5058
|
});
|
|
4918
5059
|
|
|
5060
|
+
// src/lib/db-writer.ts
|
|
5061
|
+
var exports_db_writer = {};
|
|
5062
|
+
__export(exports_db_writer, {
|
|
5063
|
+
writeHookEvent: () => writeHookEvent,
|
|
5064
|
+
resolveEventType: () => resolveEventType,
|
|
5065
|
+
recordHookRun: () => recordHookRun,
|
|
5066
|
+
normalizeEventType: () => normalizeEventType
|
|
5067
|
+
});
|
|
5068
|
+
function nanoid2() {
|
|
5069
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
|
|
5070
|
+
}
|
|
5071
|
+
function writeHookEvent(event) {
|
|
5072
|
+
try {
|
|
5073
|
+
const db = getDb();
|
|
5074
|
+
const id = nanoid2();
|
|
5075
|
+
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
5076
|
+
db.run(`INSERT INTO hook_events
|
|
5077
|
+
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, result, error, duration_ms, project_dir, metadata)
|
|
5078
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5079
|
+
id,
|
|
5080
|
+
timestamp,
|
|
5081
|
+
event.session_id,
|
|
5082
|
+
event.hook_name,
|
|
5083
|
+
event.event_type,
|
|
5084
|
+
event.tool_name ?? null,
|
|
5085
|
+
event.tool_input ? event.tool_input.slice(0, 500) : null,
|
|
5086
|
+
event.result ?? null,
|
|
5087
|
+
event.error ?? null,
|
|
5088
|
+
event.duration_ms ?? null,
|
|
5089
|
+
event.project_dir ?? null,
|
|
5090
|
+
event.metadata ?? null
|
|
5091
|
+
]);
|
|
5092
|
+
} catch (err) {
|
|
5093
|
+
process.stderr.write(`[hooks db-writer] failed to write event: ${err}
|
|
5094
|
+
`);
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
function normalizeEventType(value) {
|
|
5098
|
+
if (typeof value !== "string")
|
|
5099
|
+
return null;
|
|
5100
|
+
if (ALLOWED_EVENT_TYPES.has(value))
|
|
5101
|
+
return value;
|
|
5102
|
+
const bare = value.split(":")[0] ?? "";
|
|
5103
|
+
return ALLOWED_EVENT_TYPES.has(bare) ? bare : null;
|
|
5104
|
+
}
|
|
5105
|
+
function resolveEventType(inputEvent, fallbackEvent) {
|
|
5106
|
+
const fromInput = normalizeEventType(inputEvent);
|
|
5107
|
+
if (fromInput)
|
|
5108
|
+
return fromInput;
|
|
5109
|
+
return normalizeEventType(fallbackEvent ?? null);
|
|
5110
|
+
}
|
|
5111
|
+
function recordHookRun(record) {
|
|
5112
|
+
const eventType = record.eventType ? normalizeEventType(record.eventType) : null;
|
|
5113
|
+
if (!eventType) {
|
|
5114
|
+
process.stderr.write(`[hooks db-writer] skipped event for '${record.hookName}': unsupported event type '${String(record.eventType)}'
|
|
5115
|
+
`);
|
|
5116
|
+
return;
|
|
5117
|
+
}
|
|
5118
|
+
const metadata = {
|
|
5119
|
+
...record.metadata ?? {},
|
|
5120
|
+
version: record.version ?? null,
|
|
5121
|
+
sha256: record.sha256 ?? null,
|
|
5122
|
+
exit_code: record.exitCode
|
|
5123
|
+
};
|
|
5124
|
+
writeHookEvent({
|
|
5125
|
+
session_id: record.sessionId ?? "cli",
|
|
5126
|
+
hook_name: record.hookName,
|
|
5127
|
+
event_type: eventType,
|
|
5128
|
+
tool_name: record.toolName ?? null,
|
|
5129
|
+
tool_input: record.toolInput !== undefined ? JSON.stringify(record.toolInput) : null,
|
|
5130
|
+
result: record.result ?? null,
|
|
5131
|
+
error: record.error ?? null,
|
|
5132
|
+
duration_ms: record.durationMs,
|
|
5133
|
+
project_dir: record.projectDir ?? null,
|
|
5134
|
+
metadata: JSON.stringify(metadata)
|
|
5135
|
+
});
|
|
5136
|
+
}
|
|
5137
|
+
var ALLOWED_EVENT_TYPES;
|
|
5138
|
+
var init_db_writer = __esm(() => {
|
|
5139
|
+
init_db();
|
|
5140
|
+
ALLOWED_EVENT_TYPES = new Set([
|
|
5141
|
+
"PreToolUse",
|
|
5142
|
+
"PostToolUse",
|
|
5143
|
+
"Stop",
|
|
5144
|
+
"Notification",
|
|
5145
|
+
"SessionStart",
|
|
5146
|
+
"SessionEnd",
|
|
5147
|
+
"UserPromptSubmit",
|
|
5148
|
+
"SubagentStart"
|
|
5149
|
+
]);
|
|
5150
|
+
});
|
|
5151
|
+
|
|
4919
5152
|
// src/db/storage-sync.ts
|
|
4920
5153
|
init_db();
|
|
4921
5154
|
|
|
@@ -5847,7 +6080,7 @@ function getHook(name) {
|
|
|
5847
6080
|
return HOOKS.find((h) => h.name === name);
|
|
5848
6081
|
}
|
|
5849
6082
|
// src/lib/installer.ts
|
|
5850
|
-
import { existsSync as
|
|
6083
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
5851
6084
|
import { join as join6, dirname as dirname3 } from "path";
|
|
5852
6085
|
import { homedir as homedir4 } from "os";
|
|
5853
6086
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -5917,7 +6150,8 @@ function resolveHook(name) {
|
|
|
5917
6150
|
events: custom2.manifest.events,
|
|
5918
6151
|
description: custom2.manifest.description ?? "Custom hook",
|
|
5919
6152
|
scriptPath: custom2.scriptPath,
|
|
5920
|
-
meta: meta2
|
|
6153
|
+
meta: meta2,
|
|
6154
|
+
timeoutMs: custom2.manifest.timeout_ms ?? null
|
|
5921
6155
|
};
|
|
5922
6156
|
}
|
|
5923
6157
|
const meta = getHook(shortManifestName(name));
|
|
@@ -5931,13 +6165,16 @@ function resolveHook(name) {
|
|
|
5931
6165
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
5932
6166
|
description: meta.description,
|
|
5933
6167
|
scriptPath,
|
|
5934
|
-
meta
|
|
6168
|
+
meta,
|
|
6169
|
+
timeoutMs: null
|
|
5935
6170
|
};
|
|
5936
6171
|
}
|
|
5937
6172
|
|
|
5938
6173
|
// src/lib/installer.ts
|
|
6174
|
+
init_manifest();
|
|
6175
|
+
init_store();
|
|
5939
6176
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
5940
|
-
var HOOKS_DIR =
|
|
6177
|
+
var HOOKS_DIR = existsSync7(join6(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join6(__dirname2, "..", "..", "hooks") : join6(__dirname2, "..", "hooks");
|
|
5941
6178
|
function normalizeHookName(name) {
|
|
5942
6179
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
5943
6180
|
}
|
|
@@ -6019,13 +6256,13 @@ function getHookPath(name) {
|
|
|
6019
6256
|
return join6(HOOKS_DIR, normalizeHookName(shortName));
|
|
6020
6257
|
}
|
|
6021
6258
|
function hookExists(name) {
|
|
6022
|
-
return
|
|
6259
|
+
return existsSync7(getHookPath(name));
|
|
6023
6260
|
}
|
|
6024
6261
|
function readSettings(scope = "global", target = "claude") {
|
|
6025
6262
|
const path = getSettingsPath(scope, target);
|
|
6026
6263
|
try {
|
|
6027
|
-
if (
|
|
6028
|
-
return JSON.parse(
|
|
6264
|
+
if (existsSync7(path)) {
|
|
6265
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
6029
6266
|
}
|
|
6030
6267
|
} catch (error) {
|
|
6031
6268
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -6035,10 +6272,10 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
6035
6272
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
6036
6273
|
const path = getSettingsPath(scope, target);
|
|
6037
6274
|
const dir = dirname3(path);
|
|
6038
|
-
if (!
|
|
6039
|
-
|
|
6275
|
+
if (!existsSync7(dir)) {
|
|
6276
|
+
mkdirSync5(dir, { recursive: true });
|
|
6040
6277
|
}
|
|
6041
|
-
|
|
6278
|
+
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
6042
6279
|
`);
|
|
6043
6280
|
}
|
|
6044
6281
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -6124,7 +6361,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
6124
6361
|
function readCodewithConfig(scope, configPath) {
|
|
6125
6362
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6126
6363
|
try {
|
|
6127
|
-
return
|
|
6364
|
+
return existsSync7(path) ? readFileSync5(path, "utf-8") : "";
|
|
6128
6365
|
} catch {
|
|
6129
6366
|
return "";
|
|
6130
6367
|
}
|
|
@@ -6132,13 +6369,13 @@ function readCodewithConfig(scope, configPath) {
|
|
|
6132
6369
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
6133
6370
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6134
6371
|
const dir = dirname3(path);
|
|
6135
|
-
if (!
|
|
6136
|
-
|
|
6137
|
-
const existing =
|
|
6372
|
+
if (!existsSync7(dir))
|
|
6373
|
+
mkdirSync5(dir, { recursive: true });
|
|
6374
|
+
const existing = existsSync7(path) ? readFileSync5(path, "utf-8") : "";
|
|
6138
6375
|
const sep2 = existing.trim() ? `
|
|
6139
6376
|
|
|
6140
6377
|
` : "";
|
|
6141
|
-
|
|
6378
|
+
writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
6142
6379
|
return path;
|
|
6143
6380
|
}
|
|
6144
6381
|
function detectConflict(name, scope, target) {
|
|
@@ -6314,9 +6551,6 @@ function removeHookFromAllEvents(settings, name) {
|
|
|
6314
6551
|
}
|
|
6315
6552
|
}
|
|
6316
6553
|
function unregisterHook(name, scope = "global", target = "claude") {
|
|
6317
|
-
const meta = resolveHookMeta(name);
|
|
6318
|
-
if (!meta)
|
|
6319
|
-
return;
|
|
6320
6554
|
const settings = readSettings(scope, target);
|
|
6321
6555
|
if (!settings.hooks)
|
|
6322
6556
|
return;
|
|
@@ -6382,6 +6616,136 @@ function removeHookForTarget(name, scope, target) {
|
|
|
6382
6616
|
unregisterHook(name, scope, target);
|
|
6383
6617
|
return true;
|
|
6384
6618
|
}
|
|
6619
|
+
function removeCodewithHookEntry(configText, name) {
|
|
6620
|
+
const sections = configText.split(/\n\s*\n/);
|
|
6621
|
+
const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
|
|
6622
|
+
const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
|
|
6623
|
+
const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
|
|
6624
|
+
const eventsWithEntries = new Set;
|
|
6625
|
+
const kept = [];
|
|
6626
|
+
let removed = false;
|
|
6627
|
+
for (const section of sections) {
|
|
6628
|
+
const lines = section.split(`
|
|
6629
|
+
`);
|
|
6630
|
+
const entryMatch = entryHeaderRe.exec(lines[0] ?? "");
|
|
6631
|
+
if (entryMatch) {
|
|
6632
|
+
const event = entryMatch[1];
|
|
6633
|
+
eventsWithEntries.add(event);
|
|
6634
|
+
if (lines.some((line) => nameRe.test(line.trim()))) {
|
|
6635
|
+
removed = true;
|
|
6636
|
+
continue;
|
|
6637
|
+
}
|
|
6638
|
+
kept.push({ text: section });
|
|
6639
|
+
continue;
|
|
6640
|
+
}
|
|
6641
|
+
const eventMatch = eventHeaderRe.exec(lines[0] ?? "");
|
|
6642
|
+
kept.push({ text: section, eventHeaderFor: eventMatch ? eventMatch[1] : undefined });
|
|
6643
|
+
}
|
|
6644
|
+
const keptEntriesByEvent = new Set;
|
|
6645
|
+
for (const item of kept) {
|
|
6646
|
+
const m = entryHeaderRe.exec(item.text.split(`
|
|
6647
|
+
`)[0] ?? "");
|
|
6648
|
+
if (m)
|
|
6649
|
+
keptEntriesByEvent.add(m[1]);
|
|
6650
|
+
}
|
|
6651
|
+
const finalSections = kept.filter((item) => {
|
|
6652
|
+
if (item.eventHeaderFor === undefined)
|
|
6653
|
+
return true;
|
|
6654
|
+
if (!eventsWithEntries.has(item.eventHeaderFor))
|
|
6655
|
+
return true;
|
|
6656
|
+
return keptEntriesByEvent.has(item.eventHeaderFor);
|
|
6657
|
+
}).map((item) => item.text);
|
|
6658
|
+
return {
|
|
6659
|
+
text: finalSections.join(`
|
|
6660
|
+
|
|
6661
|
+
`) + (configText.endsWith(`
|
|
6662
|
+
`) ? `
|
|
6663
|
+
` : ""),
|
|
6664
|
+
removed
|
|
6665
|
+
};
|
|
6666
|
+
}
|
|
6667
|
+
function escapeRegExp(value) {
|
|
6668
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6669
|
+
}
|
|
6670
|
+
function codewithHasHookEntryInText(configText, name) {
|
|
6671
|
+
return new RegExp(`hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?(?:$|\\s|")`).test(configText);
|
|
6672
|
+
}
|
|
6673
|
+
function codewithHasHookEntry(name) {
|
|
6674
|
+
const path = getSettingsPath("global", "codewith");
|
|
6675
|
+
if (!existsSync7(path))
|
|
6676
|
+
return false;
|
|
6677
|
+
try {
|
|
6678
|
+
return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
|
|
6679
|
+
} catch {
|
|
6680
|
+
return false;
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
function uninstallHook(name, scope = "global", target = "claude") {
|
|
6684
|
+
const shortName = shortHookName(name);
|
|
6685
|
+
const custom2 = readCustomManifest(shortName);
|
|
6686
|
+
const bundledMeta = custom2 ? undefined : getHook(shortName);
|
|
6687
|
+
const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
|
|
6688
|
+
const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
|
|
6689
|
+
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
6690
|
+
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
6691
|
+
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName);
|
|
6692
|
+
if (!custom2 && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
6693
|
+
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
6694
|
+
}
|
|
6695
|
+
const writableTargets = target === "all" ? ["claude", "gemini"] : target === "codewith" ? [] : [target];
|
|
6696
|
+
const scopes = [];
|
|
6697
|
+
for (const s of ["global", "project"]) {
|
|
6698
|
+
for (const t of writableTargets) {
|
|
6699
|
+
if (getRegisteredHooksForTarget(s, t).includes(shortName)) {
|
|
6700
|
+
unregisterHook(shortName, s, t);
|
|
6701
|
+
if (!scopes.includes(s))
|
|
6702
|
+
scopes.push(s);
|
|
6703
|
+
}
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
6706
|
+
const registrationsRemaining = [];
|
|
6707
|
+
if (target === "codewith" || target === "all") {
|
|
6708
|
+
const codewithPath = getSettingsPath("global", "codewith");
|
|
6709
|
+
if (existsSync7(codewithPath)) {
|
|
6710
|
+
const before = readFileSync5(codewithPath, "utf-8");
|
|
6711
|
+
const after = removeCodewithHookEntry(before, shortName);
|
|
6712
|
+
if (after.removed) {
|
|
6713
|
+
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
6714
|
+
}
|
|
6715
|
+
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
6716
|
+
registrationsRemaining.push("codewith");
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
if (custom2) {
|
|
6721
|
+
try {
|
|
6722
|
+
rmSync(customHookDir(shortName), { recursive: true, force: true });
|
|
6723
|
+
} catch {
|
|
6724
|
+
return {
|
|
6725
|
+
name: shortName,
|
|
6726
|
+
removed: false,
|
|
6727
|
+
source: "custom",
|
|
6728
|
+
settingsScopes: scopes,
|
|
6729
|
+
storeDirRemoved: false,
|
|
6730
|
+
pinRemoved: false,
|
|
6731
|
+
dbRecordRemoved: false,
|
|
6732
|
+
registrationsRemaining,
|
|
6733
|
+
error: `Hook '${shortName}' store directory could not be removed; trust records preserved (no fail-open)`
|
|
6734
|
+
};
|
|
6735
|
+
}
|
|
6736
|
+
}
|
|
6737
|
+
const { removedPin, removedRecord } = removeHookFromStore(shortName);
|
|
6738
|
+
return {
|
|
6739
|
+
name: shortName,
|
|
6740
|
+
removed: true,
|
|
6741
|
+
source: custom2 ? "custom" : bundledMeta ? "bundled" : "registered-only",
|
|
6742
|
+
settingsScopes: scopes,
|
|
6743
|
+
storeDirRemoved: custom2 ? true : false,
|
|
6744
|
+
pinRemoved: removedPin,
|
|
6745
|
+
dbRecordRemoved: removedRecord,
|
|
6746
|
+
registrationsRemaining
|
|
6747
|
+
};
|
|
6748
|
+
}
|
|
6385
6749
|
// src/index.ts
|
|
6386
6750
|
init_profiles();
|
|
6387
6751
|
init_config();
|
|
@@ -6392,7 +6756,7 @@ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
|
6392
6756
|
// src/lib/custom-install.ts
|
|
6393
6757
|
init_config();
|
|
6394
6758
|
init_manifest();
|
|
6395
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as
|
|
6759
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
6396
6760
|
import { join as join8 } from "path";
|
|
6397
6761
|
import { tmpdir } from "os";
|
|
6398
6762
|
function manifestAt(dir) {
|
|
@@ -6430,7 +6794,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
6430
6794
|
proc.exited
|
|
6431
6795
|
]);
|
|
6432
6796
|
if (exitCode !== 0) {
|
|
6433
|
-
|
|
6797
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
6434
6798
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
6435
6799
|
}
|
|
6436
6800
|
return join8(tmp, "repo");
|
|
@@ -6451,7 +6815,7 @@ async function installCustomSource(source) {
|
|
|
6451
6815
|
try {
|
|
6452
6816
|
return copyManifestDir(repoDir, source, "git");
|
|
6453
6817
|
} finally {
|
|
6454
|
-
|
|
6818
|
+
rmSync3(repoDir, { recursive: true, force: true });
|
|
6455
6819
|
}
|
|
6456
6820
|
}
|
|
6457
6821
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
@@ -6523,6 +6887,7 @@ async function fetchJson(base, path) {
|
|
|
6523
6887
|
headers["x-api-key"] = apiKey;
|
|
6524
6888
|
const res = await fetch(`${base}${path}`, {
|
|
6525
6889
|
headers,
|
|
6890
|
+
redirect: "error",
|
|
6526
6891
|
signal: AbortSignal.timeout(15000)
|
|
6527
6892
|
});
|
|
6528
6893
|
if (res.status === 401) {
|
|
@@ -6626,11 +6991,14 @@ async function syncHooks(options = {}) {
|
|
|
6626
6991
|
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6627
6992
|
const scriptPath = resolveScriptPath(name);
|
|
6628
6993
|
const actual = await sha256File(scriptPath);
|
|
6629
|
-
|
|
6994
|
+
if (actual !== entry.sha256) {
|
|
6995
|
+
throw new Error(`sha256 mismatch after write for '${name}@${entry.version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
6996
|
+
}
|
|
6997
|
+
setPinnedHook(name, { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" });
|
|
6630
6998
|
upsertHookRecord(db, {
|
|
6631
6999
|
name,
|
|
6632
7000
|
version: entry.version,
|
|
6633
|
-
sha256:
|
|
7001
|
+
sha256: entry.sha256,
|
|
6634
7002
|
source_type: entry.source ?? "remote",
|
|
6635
7003
|
source_ref: apiUrl,
|
|
6636
7004
|
last_verified_at: new Date().toISOString()
|
|
@@ -6638,6 +7006,59 @@ async function syncHooks(options = {}) {
|
|
|
6638
7006
|
}
|
|
6639
7007
|
return { apiUrl, dryRun: false, diff };
|
|
6640
7008
|
}
|
|
7009
|
+
async function fetchPinnedHook(name, version, apiUrl) {
|
|
7010
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
7011
|
+
const entry = remoteLock.hooks?.[name];
|
|
7012
|
+
if (!entry) {
|
|
7013
|
+
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
7014
|
+
}
|
|
7015
|
+
if (entry.version !== version) {
|
|
7016
|
+
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${entry.version})`);
|
|
7017
|
+
}
|
|
7018
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${version}`);
|
|
7019
|
+
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
7020
|
+
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
7021
|
+
}
|
|
7022
|
+
const manifestName = shortManifestName(artifact.manifest.name ?? name);
|
|
7023
|
+
if (manifestName !== name) {
|
|
7024
|
+
throw new Error(`artifact for '${name}@${version}' declares a different hook name ('${manifestName}')`);
|
|
7025
|
+
}
|
|
7026
|
+
if (artifact.manifest.version !== version) {
|
|
7027
|
+
throw new Error(`artifact for '${name}@${version}' declares a different version ('${artifact.manifest.version}')`);
|
|
7028
|
+
}
|
|
7029
|
+
const actualSha = sha256OfText(artifact.script);
|
|
7030
|
+
if (actualSha !== entry.sha256) {
|
|
7031
|
+
throw new Error(`sha256 mismatch for '${name}@${version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
7032
|
+
}
|
|
7033
|
+
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
7034
|
+
const scriptRel = artifact.manifest.script.includes(`
|
|
7035
|
+
`) ? "script.ts" : artifact.manifest.script;
|
|
7036
|
+
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
7037
|
+
const scriptPath = resolveScriptPath(name);
|
|
7038
|
+
const actual = await sha256File(scriptPath);
|
|
7039
|
+
if (actual !== entry.sha256) {
|
|
7040
|
+
throw new Error(`sha256 mismatch after write for '${name}@${version}': lock says ${entry.sha256}, on disk ${actual}`);
|
|
7041
|
+
}
|
|
7042
|
+
const db = getDb();
|
|
7043
|
+
setPinnedHook(name, { version, sha256: entry.sha256, source: entry.source ?? "remote" });
|
|
7044
|
+
upsertHookRecord(db, {
|
|
7045
|
+
name,
|
|
7046
|
+
version,
|
|
7047
|
+
sha256: entry.sha256,
|
|
7048
|
+
source_type: entry.source ?? "remote",
|
|
7049
|
+
source_ref: apiUrl,
|
|
7050
|
+
last_verified_at: new Date().toISOString()
|
|
7051
|
+
});
|
|
7052
|
+
return {
|
|
7053
|
+
name,
|
|
7054
|
+
version,
|
|
7055
|
+
sha256: entry.sha256,
|
|
7056
|
+
source: entry.source ?? "remote",
|
|
7057
|
+
source_ref: apiUrl,
|
|
7058
|
+
artifact,
|
|
7059
|
+
scriptPath
|
|
7060
|
+
};
|
|
7061
|
+
}
|
|
6641
7062
|
// src/serve.ts
|
|
6642
7063
|
init_manifest();
|
|
6643
7064
|
init_store();
|
|
@@ -6897,16 +7318,68 @@ async function runHook(name, input, options = {}) {
|
|
|
6897
7318
|
};
|
|
6898
7319
|
}
|
|
6899
7320
|
}
|
|
6900
|
-
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
6901
|
-
const
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
7321
|
+
const { executeVerifiedScript: executeVerifiedScript2, HookTimeoutError: HookTimeoutError2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
7322
|
+
const manifestTimeout = custom2?.manifest.timeout_ms ?? null;
|
|
7323
|
+
const effectiveTimeout = options.timeout !== undefined && options.timeout > 0 ? options.timeout : manifestTimeout ?? null;
|
|
7324
|
+
const started = Date.now();
|
|
7325
|
+
let stdoutText = "";
|
|
7326
|
+
let stderrText = "";
|
|
7327
|
+
let exitCode = 0;
|
|
7328
|
+
try {
|
|
7329
|
+
({ stdout: stdoutText, stderr: stderrText, exitCode } = await executeVerifiedScript2({
|
|
7330
|
+
name,
|
|
7331
|
+
scriptPath: script,
|
|
7332
|
+
content,
|
|
7333
|
+
args: custom2?.manifest.args ?? [],
|
|
7334
|
+
stdin: JSON.stringify(hookInput),
|
|
7335
|
+
env: process.env,
|
|
7336
|
+
timeout: effectiveTimeout ?? undefined
|
|
7337
|
+
}));
|
|
7338
|
+
} catch (err) {
|
|
7339
|
+
if (err instanceof HookTimeoutError2) {
|
|
7340
|
+
try {
|
|
7341
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
7342
|
+
recordHookRun2({
|
|
7343
|
+
hookName: name,
|
|
7344
|
+
eventType: resolveEventType2(input.hook_event_name, custom2?.manifest.events[0] ?? "PostToolUse"),
|
|
7345
|
+
version: custom2?.manifest.version,
|
|
7346
|
+
sha256: sha256Of2(content),
|
|
7347
|
+
sessionId: typeof input.session_id === "string" ? input.session_id : null,
|
|
7348
|
+
toolName: typeof input.tool_name === "string" ? input.tool_name : null,
|
|
7349
|
+
toolInput: input.tool_input,
|
|
7350
|
+
error: err.message.slice(0, 500),
|
|
7351
|
+
exitCode: -1,
|
|
7352
|
+
durationMs: Date.now() - started,
|
|
7353
|
+
projectDir: process.cwd()
|
|
7354
|
+
});
|
|
7355
|
+
} catch {}
|
|
7356
|
+
throw err;
|
|
7357
|
+
}
|
|
7358
|
+
throw err;
|
|
7359
|
+
}
|
|
7360
|
+
const durationMs = Date.now() - started;
|
|
7361
|
+
try {
|
|
7362
|
+
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
7363
|
+
let outputJson = {};
|
|
7364
|
+
try {
|
|
7365
|
+
outputJson = JSON.parse(stdoutText);
|
|
7366
|
+
} catch {}
|
|
7367
|
+
const blocked = outputJson.decision === "block" || outputJson.continue === false;
|
|
7368
|
+
recordHookRun2({
|
|
7369
|
+
hookName: name,
|
|
7370
|
+
eventType: resolveEventType2(input.hook_event_name, custom2?.manifest.events[0] ?? "PostToolUse"),
|
|
7371
|
+
version: custom2?.manifest.version,
|
|
7372
|
+
sha256: sha256Of2(content),
|
|
7373
|
+
sessionId: typeof input.session_id === "string" ? input.session_id : null,
|
|
7374
|
+
toolName: typeof input.tool_name === "string" ? input.tool_name : null,
|
|
7375
|
+
toolInput: input.tool_input,
|
|
7376
|
+
result: blocked ? "block" : "continue",
|
|
7377
|
+
error: exitCode !== 0 ? (stderrText || `hook exited with code ${exitCode}`).slice(0, 500) : null,
|
|
7378
|
+
exitCode,
|
|
7379
|
+
durationMs,
|
|
7380
|
+
projectDir: process.cwd()
|
|
7381
|
+
});
|
|
7382
|
+
} catch {}
|
|
6910
7383
|
let output = {};
|
|
6911
7384
|
try {
|
|
6912
7385
|
output = JSON.parse(stdoutText);
|
|
@@ -6922,6 +7395,7 @@ export {
|
|
|
6922
7395
|
verifyScriptHash,
|
|
6923
7396
|
upsertHookRecord,
|
|
6924
7397
|
updateProfile,
|
|
7398
|
+
uninstallHook,
|
|
6925
7399
|
touchProfile,
|
|
6926
7400
|
syncHooks,
|
|
6927
7401
|
storageSync,
|
|
@@ -6946,12 +7420,14 @@ export {
|
|
|
6946
7420
|
removeProjectHook,
|
|
6947
7421
|
removePinnedHook,
|
|
6948
7422
|
removeHookRecord,
|
|
7423
|
+
removeHookFromStore,
|
|
6949
7424
|
removeHook,
|
|
6950
7425
|
readLock,
|
|
6951
7426
|
readCustomManifest,
|
|
6952
7427
|
readConfig,
|
|
6953
7428
|
provisionCloudflareResources,
|
|
6954
7429
|
planSync,
|
|
7430
|
+
pinInstalledHook,
|
|
6955
7431
|
parseStorageTables,
|
|
6956
7432
|
parseManifest,
|
|
6957
7433
|
listProjectHooks,
|
|
@@ -6991,6 +7467,7 @@ export {
|
|
|
6991
7467
|
getHook,
|
|
6992
7468
|
getCustomHooksDir,
|
|
6993
7469
|
getConfigPath,
|
|
7470
|
+
fetchPinnedHook,
|
|
6994
7471
|
exportProfiles,
|
|
6995
7472
|
deleteProfile,
|
|
6996
7473
|
createProfile,
|