@hasna/hooks 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/index.js +2154 -1400
- package/bin/serve.js +34 -4
- package/dist/db/migrations/005_subagent_start_event.d.ts +10 -0
- package/dist/db/schema.d.ts +2 -2
- package/dist/index.d.ts +9 -5
- package/dist/index.js +683 -195
- package/dist/lib/db-writer.d.ts +40 -0
- package/dist/lib/installer.d.ts +37 -0
- package/dist/lib/resolve.d.ts +2 -0
- package/dist/lib/run.d.ts +8 -0
- package/dist/lib/store.d.ts +15 -0
- package/dist/lib/sync.d.ts +19 -0
- package/dist/storage.js +34 -2
- package/package.json +1 -1
package/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,44 +4752,378 @@ 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);
|
|
4855
4909
|
const dot = base.lastIndexOf(".");
|
|
4856
4910
|
return dot > 0 ? base.slice(dot + 1) : "ts";
|
|
4857
4911
|
}
|
|
4912
|
+
function shebangInterpreter(firstLine) {
|
|
4913
|
+
if (!firstLine.startsWith("#!"))
|
|
4914
|
+
return null;
|
|
4915
|
+
const rest = firstLine.slice(2).trim();
|
|
4916
|
+
if (!rest)
|
|
4917
|
+
return null;
|
|
4918
|
+
const parts = rest.split(/\s+/);
|
|
4919
|
+
if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
|
|
4920
|
+
return parts[1] && parts[1] !== "-S" ? basename2(parts[1]) : parts[2] ? basename2(parts[2]) : null;
|
|
4921
|
+
}
|
|
4922
|
+
return basename2(parts[0]);
|
|
4923
|
+
}
|
|
4924
|
+
function interpreterFor(name, scriptPath, content) {
|
|
4925
|
+
const ext = extensionOf(scriptPath);
|
|
4926
|
+
const firstLine = content.subarray(0, 512).toString("utf8").split(`
|
|
4927
|
+
`, 1)[0] ?? "";
|
|
4928
|
+
const interp = shebangInterpreter(firstLine);
|
|
4929
|
+
if (interp) {
|
|
4930
|
+
if (interp === "bash" || interp === "sh")
|
|
4931
|
+
return { command: ["/bin/bash"], tempExt: "sh" };
|
|
4932
|
+
if (interp === "node" || interp === "bun")
|
|
4933
|
+
return { command: ["bun", "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
|
|
4934
|
+
throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
|
|
4935
|
+
}
|
|
4936
|
+
if (BASH_EXTENSIONS.has(ext))
|
|
4937
|
+
return { command: ["/bin/bash"], tempExt: "sh" };
|
|
4938
|
+
if (BUN_EXTENSIONS.has(ext))
|
|
4939
|
+
return { command: ["bun", "run"], tempExt: ext };
|
|
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)`);
|
|
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
|
+
}
|
|
4858
4974
|
async function executeVerifiedScript(options) {
|
|
4859
4975
|
const scriptDir = dirname4(options.scriptPath);
|
|
4860
|
-
const
|
|
4976
|
+
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
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;
|
|
4861
4984
|
try {
|
|
4862
4985
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
4863
|
-
|
|
4986
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
4864
4987
|
stdin: new Response(options.stdin),
|
|
4865
4988
|
stdout: "pipe",
|
|
4866
4989
|
stderr: "pipe",
|
|
4867
4990
|
env: options.env ?? process.env,
|
|
4868
|
-
|
|
4991
|
+
detached: true
|
|
4869
4992
|
});
|
|
4870
|
-
const
|
|
4871
|
-
|
|
4872
|
-
new
|
|
4873
|
-
|
|
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)
|
|
4874
5000
|
]);
|
|
5001
|
+
killGroup(proc);
|
|
4875
5002
|
return { stdout: stdoutText, stderr: stderrText, exitCode };
|
|
4876
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 {
|
|
4877
5017
|
try {
|
|
4878
|
-
|
|
5018
|
+
process.kill(proc.pid, "SIGKILL");
|
|
4879
5019
|
} catch {}
|
|
4880
5020
|
}
|
|
4881
5021
|
}
|
|
4882
|
-
var
|
|
5022
|
+
var HookTimeoutError, BASH_EXTENSIONS, BUN_EXTENSIONS;
|
|
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
|
+
};
|
|
5032
|
+
BASH_EXTENSIONS = new Set(["sh", "bash"]);
|
|
5033
|
+
BUN_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"]);
|
|
5034
|
+
});
|
|
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
|
+
});
|
|
4883
5127
|
|
|
4884
5128
|
// src/db/storage-sync.ts
|
|
4885
5129
|
init_db();
|
|
@@ -5812,7 +6056,7 @@ function getHook(name) {
|
|
|
5812
6056
|
return HOOKS.find((h) => h.name === name);
|
|
5813
6057
|
}
|
|
5814
6058
|
// src/lib/installer.ts
|
|
5815
|
-
import { existsSync as
|
|
6059
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
5816
6060
|
import { join as join6, dirname as dirname3 } from "path";
|
|
5817
6061
|
import { homedir as homedir4 } from "os";
|
|
5818
6062
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -5882,7 +6126,8 @@ function resolveHook(name) {
|
|
|
5882
6126
|
events: custom2.manifest.events,
|
|
5883
6127
|
description: custom2.manifest.description ?? "Custom hook",
|
|
5884
6128
|
scriptPath: custom2.scriptPath,
|
|
5885
|
-
meta: meta2
|
|
6129
|
+
meta: meta2,
|
|
6130
|
+
timeoutMs: custom2.manifest.timeout_ms ?? null
|
|
5886
6131
|
};
|
|
5887
6132
|
}
|
|
5888
6133
|
const meta = getHook(shortManifestName(name));
|
|
@@ -5896,13 +6141,16 @@ function resolveHook(name) {
|
|
|
5896
6141
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
5897
6142
|
description: meta.description,
|
|
5898
6143
|
scriptPath,
|
|
5899
|
-
meta
|
|
6144
|
+
meta,
|
|
6145
|
+
timeoutMs: null
|
|
5900
6146
|
};
|
|
5901
6147
|
}
|
|
5902
6148
|
|
|
5903
6149
|
// src/lib/installer.ts
|
|
6150
|
+
init_manifest();
|
|
6151
|
+
init_store();
|
|
5904
6152
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
5905
|
-
var HOOKS_DIR =
|
|
6153
|
+
var HOOKS_DIR = existsSync7(join6(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join6(__dirname2, "..", "..", "hooks") : join6(__dirname2, "..", "hooks");
|
|
5906
6154
|
function normalizeHookName(name) {
|
|
5907
6155
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
5908
6156
|
}
|
|
@@ -5984,13 +6232,13 @@ function getHookPath(name) {
|
|
|
5984
6232
|
return join6(HOOKS_DIR, normalizeHookName(shortName));
|
|
5985
6233
|
}
|
|
5986
6234
|
function hookExists(name) {
|
|
5987
|
-
return
|
|
6235
|
+
return existsSync7(getHookPath(name));
|
|
5988
6236
|
}
|
|
5989
6237
|
function readSettings(scope = "global", target = "claude") {
|
|
5990
6238
|
const path = getSettingsPath(scope, target);
|
|
5991
6239
|
try {
|
|
5992
|
-
if (
|
|
5993
|
-
return JSON.parse(
|
|
6240
|
+
if (existsSync7(path)) {
|
|
6241
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
5994
6242
|
}
|
|
5995
6243
|
} catch (error) {
|
|
5996
6244
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -6000,10 +6248,10 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
6000
6248
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
6001
6249
|
const path = getSettingsPath(scope, target);
|
|
6002
6250
|
const dir = dirname3(path);
|
|
6003
|
-
if (!
|
|
6004
|
-
|
|
6251
|
+
if (!existsSync7(dir)) {
|
|
6252
|
+
mkdirSync5(dir, { recursive: true });
|
|
6005
6253
|
}
|
|
6006
|
-
|
|
6254
|
+
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
6007
6255
|
`);
|
|
6008
6256
|
}
|
|
6009
6257
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -6089,7 +6337,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
6089
6337
|
function readCodewithConfig(scope, configPath) {
|
|
6090
6338
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6091
6339
|
try {
|
|
6092
|
-
return
|
|
6340
|
+
return existsSync7(path) ? readFileSync5(path, "utf-8") : "";
|
|
6093
6341
|
} catch {
|
|
6094
6342
|
return "";
|
|
6095
6343
|
}
|
|
@@ -6097,13 +6345,13 @@ function readCodewithConfig(scope, configPath) {
|
|
|
6097
6345
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
6098
6346
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6099
6347
|
const dir = dirname3(path);
|
|
6100
|
-
if (!
|
|
6101
|
-
|
|
6102
|
-
const existing =
|
|
6348
|
+
if (!existsSync7(dir))
|
|
6349
|
+
mkdirSync5(dir, { recursive: true });
|
|
6350
|
+
const existing = existsSync7(path) ? readFileSync5(path, "utf-8") : "";
|
|
6103
6351
|
const sep2 = existing.trim() ? `
|
|
6104
6352
|
|
|
6105
6353
|
` : "";
|
|
6106
|
-
|
|
6354
|
+
writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
6107
6355
|
return path;
|
|
6108
6356
|
}
|
|
6109
6357
|
function detectConflict(name, scope, target) {
|
|
@@ -6279,9 +6527,6 @@ function removeHookFromAllEvents(settings, name) {
|
|
|
6279
6527
|
}
|
|
6280
6528
|
}
|
|
6281
6529
|
function unregisterHook(name, scope = "global", target = "claude") {
|
|
6282
|
-
const meta = resolveHookMeta(name);
|
|
6283
|
-
if (!meta)
|
|
6284
|
-
return;
|
|
6285
6530
|
const settings = readSettings(scope, target);
|
|
6286
6531
|
if (!settings.hooks)
|
|
6287
6532
|
return;
|
|
@@ -6347,6 +6592,136 @@ function removeHookForTarget(name, scope, target) {
|
|
|
6347
6592
|
unregisterHook(name, scope, target);
|
|
6348
6593
|
return true;
|
|
6349
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
|
+
}
|
|
6350
6725
|
// src/index.ts
|
|
6351
6726
|
init_profiles();
|
|
6352
6727
|
init_config();
|
|
@@ -6357,7 +6732,7 @@ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
|
6357
6732
|
// src/lib/custom-install.ts
|
|
6358
6733
|
init_config();
|
|
6359
6734
|
init_manifest();
|
|
6360
|
-
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";
|
|
6361
6736
|
import { join as join8 } from "path";
|
|
6362
6737
|
import { tmpdir } from "os";
|
|
6363
6738
|
function manifestAt(dir) {
|
|
@@ -6395,7 +6770,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
6395
6770
|
proc.exited
|
|
6396
6771
|
]);
|
|
6397
6772
|
if (exitCode !== 0) {
|
|
6398
|
-
|
|
6773
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
6399
6774
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
6400
6775
|
}
|
|
6401
6776
|
return join8(tmp, "repo");
|
|
@@ -6416,7 +6791,7 @@ async function installCustomSource(source) {
|
|
|
6416
6791
|
try {
|
|
6417
6792
|
return copyManifestDir(repoDir, source, "git");
|
|
6418
6793
|
} finally {
|
|
6419
|
-
|
|
6794
|
+
rmSync3(repoDir, { recursive: true, force: true });
|
|
6420
6795
|
}
|
|
6421
6796
|
}
|
|
6422
6797
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
@@ -6488,6 +6863,7 @@ async function fetchJson(base, path) {
|
|
|
6488
6863
|
headers["x-api-key"] = apiKey;
|
|
6489
6864
|
const res = await fetch(`${base}${path}`, {
|
|
6490
6865
|
headers,
|
|
6866
|
+
redirect: "error",
|
|
6491
6867
|
signal: AbortSignal.timeout(15000)
|
|
6492
6868
|
});
|
|
6493
6869
|
if (res.status === 401) {
|
|
@@ -6591,11 +6967,14 @@ async function syncHooks(options = {}) {
|
|
|
6591
6967
|
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6592
6968
|
const scriptPath = resolveScriptPath(name);
|
|
6593
6969
|
const actual = await sha256File(scriptPath);
|
|
6594
|
-
|
|
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" });
|
|
6595
6974
|
upsertHookRecord(db, {
|
|
6596
6975
|
name,
|
|
6597
6976
|
version: entry.version,
|
|
6598
|
-
sha256:
|
|
6977
|
+
sha256: entry.sha256,
|
|
6599
6978
|
source_type: entry.source ?? "remote",
|
|
6600
6979
|
source_ref: apiUrl,
|
|
6601
6980
|
last_verified_at: new Date().toISOString()
|
|
@@ -6603,6 +6982,59 @@ async function syncHooks(options = {}) {
|
|
|
6603
6982
|
}
|
|
6604
6983
|
return { apiUrl, dryRun: false, diff };
|
|
6605
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
|
+
}
|
|
6606
7038
|
// src/serve.ts
|
|
6607
7039
|
init_manifest();
|
|
6608
7040
|
init_store();
|
|
@@ -6862,16 +7294,68 @@ async function runHook(name, input, options = {}) {
|
|
|
6862
7294
|
};
|
|
6863
7295
|
}
|
|
6864
7296
|
}
|
|
6865
|
-
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
6866
|
-
const
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
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 {}
|
|
6875
7359
|
let output = {};
|
|
6876
7360
|
try {
|
|
6877
7361
|
output = JSON.parse(stdoutText);
|
|
@@ -6887,6 +7371,7 @@ export {
|
|
|
6887
7371
|
verifyScriptHash,
|
|
6888
7372
|
upsertHookRecord,
|
|
6889
7373
|
updateProfile,
|
|
7374
|
+
uninstallHook,
|
|
6890
7375
|
touchProfile,
|
|
6891
7376
|
syncHooks,
|
|
6892
7377
|
storageSync,
|
|
@@ -6911,12 +7396,14 @@ export {
|
|
|
6911
7396
|
removeProjectHook,
|
|
6912
7397
|
removePinnedHook,
|
|
6913
7398
|
removeHookRecord,
|
|
7399
|
+
removeHookFromStore,
|
|
6914
7400
|
removeHook,
|
|
6915
7401
|
readLock,
|
|
6916
7402
|
readCustomManifest,
|
|
6917
7403
|
readConfig,
|
|
6918
7404
|
provisionCloudflareResources,
|
|
6919
7405
|
planSync,
|
|
7406
|
+
pinInstalledHook,
|
|
6920
7407
|
parseStorageTables,
|
|
6921
7408
|
parseManifest,
|
|
6922
7409
|
listProjectHooks,
|
|
@@ -6956,6 +7443,7 @@ export {
|
|
|
6956
7443
|
getHook,
|
|
6957
7444
|
getCustomHooksDir,
|
|
6958
7445
|
getConfigPath,
|
|
7446
|
+
fetchPinnedHook,
|
|
6959
7447
|
exportProfiles,
|
|
6960
7448
|
deleteProfile,
|
|
6961
7449
|
createProfile,
|