@hasna/hooks 0.9.1 → 0.9.2
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 +10 -0
- package/bin/hooks-mcp.js +197 -79
- package/bin/index.js +304 -144
- package/bin/serve.js +11 -0
- package/dist/index.js +204 -83
- package/dist/lib/codex-settings.d.ts +3 -0
- package/dist/lib/installer.d.ts +1 -1
- package/hooks/hook-mementos-context/README.md +80 -0
- package/hooks/hook-mementos-context/src/hook.ts +116 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -48,6 +48,16 @@ tools return compact summaries by default, while explicit flags such as
|
|
|
48
48
|
`compact:false`, `verbose:true`, or a detail tool like `hooks_info` return full
|
|
49
49
|
records.
|
|
50
50
|
|
|
51
|
+
## Optional Mementos prompt context
|
|
52
|
+
|
|
53
|
+
`hooks install mementos-context --target codex` registers a native prompt hook.
|
|
54
|
+
Enable it in the calling shell with `HOOKS_MEMENTOS_ENABLED=1`; set
|
|
55
|
+
`HOOKS_MEMENTOS_PROJECT` to an existing Mementos project. Provider judgments are
|
|
56
|
+
separately opt-in. The hook returns bounded, quoted memory data and continues
|
|
57
|
+
on errors. See [setup, scope and provider options](hooks/hook-mementos-context/README.md).
|
|
58
|
+
Codex requires review and trust of the exact registration through `/hooks`.
|
|
59
|
+
Claude Code and Codewith also support this hook through their explicit targets.
|
|
60
|
+
|
|
51
61
|
## Codewith-native hooks
|
|
52
62
|
|
|
53
63
|
@hasna/hooks includes unprefixed Codewith-native hook names:
|
package/bin/hooks-mcp.js
CHANGED
|
@@ -377,6 +377,17 @@ var init_registry = __esm(() => {
|
|
|
377
377
|
matcher: "",
|
|
378
378
|
tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
|
|
379
379
|
},
|
|
380
|
+
{
|
|
381
|
+
name: "mementos-context",
|
|
382
|
+
displayName: "Mementos Prompt Context",
|
|
383
|
+
description: "Retrieves scoped memories for a user prompt, with optional relevance judgments",
|
|
384
|
+
version: "0.1.0",
|
|
385
|
+
category: "Context Management",
|
|
386
|
+
event: "UserPromptSubmit",
|
|
387
|
+
matcher: "",
|
|
388
|
+
timeoutSeconds: 12,
|
|
389
|
+
tags: ["mementos", "memory", "prompt", "codex", "claude", "codewith", "opt-in"]
|
|
390
|
+
},
|
|
380
391
|
{
|
|
381
392
|
name: "autoformat",
|
|
382
393
|
displayName: "Auto Format",
|
|
@@ -1410,7 +1421,7 @@ var init_redact = __esm(() => {
|
|
|
1410
1421
|
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
|
|
1411
1422
|
import { isIP } from "net";
|
|
1412
1423
|
import { spawnSync } from "child_process";
|
|
1413
|
-
import { closeSync, fstatSync, openSync, readFileSync as
|
|
1424
|
+
import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
|
|
1414
1425
|
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
1415
1426
|
import { createRequire } from "module";
|
|
1416
1427
|
import { hostname as osHostname } from "os";
|
|
@@ -1514,7 +1525,7 @@ function readAppConfigFile(path) {
|
|
|
1514
1525
|
};
|
|
1515
1526
|
let fd = -1;
|
|
1516
1527
|
try {
|
|
1517
|
-
fd =
|
|
1528
|
+
fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
1518
1529
|
} catch (error) {
|
|
1519
1530
|
const code = error.code;
|
|
1520
1531
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -1535,7 +1546,7 @@ function readAppConfigFile(path) {
|
|
|
1535
1546
|
unsafe("the file is not owned by the current user");
|
|
1536
1547
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
1537
1548
|
unsafe("the file exceeds the size limit");
|
|
1538
|
-
const bytes =
|
|
1549
|
+
const bytes = readFileSync7(fd);
|
|
1539
1550
|
const after = fstatSync(fd);
|
|
1540
1551
|
if (!configFileReadsCoherent(before, after)) {
|
|
1541
1552
|
unsafe("the file changed while being read");
|
|
@@ -1543,7 +1554,7 @@ function readAppConfigFile(path) {
|
|
|
1543
1554
|
return parseEnvFile(bytes.toString("utf8"));
|
|
1544
1555
|
} finally {
|
|
1545
1556
|
if (fd !== -1)
|
|
1546
|
-
|
|
1557
|
+
closeSync2(fd);
|
|
1547
1558
|
}
|
|
1548
1559
|
}
|
|
1549
1560
|
function readCredentialFile(path, apiKeyKeys) {
|
|
@@ -2198,8 +2209,8 @@ var init_local_opt_in = __esm(() => {
|
|
|
2198
2209
|
});
|
|
2199
2210
|
|
|
2200
2211
|
// src/lib/hook-env.ts
|
|
2201
|
-
import { dirname as
|
|
2202
|
-
import { existsSync as
|
|
2212
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
|
|
2213
|
+
import { existsSync as existsSync10, realpathSync as realpathSync2, statSync } from "fs";
|
|
2203
2214
|
import { tmpdir } from "os";
|
|
2204
2215
|
function isDeniedEnvName(name) {
|
|
2205
2216
|
const upper = name.toUpperCase();
|
|
@@ -2247,9 +2258,9 @@ function isUnsafePathEntry(entry, home) {
|
|
|
2247
2258
|
}
|
|
2248
2259
|
function sanitizeHookPath(pathValue, home) {
|
|
2249
2260
|
const baseline = PATH_BASELINE.slice();
|
|
2250
|
-
if (process.platform === "darwin" &&
|
|
2261
|
+
if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
|
|
2251
2262
|
baseline.push("/opt/homebrew/bin");
|
|
2252
|
-
const bunDir =
|
|
2263
|
+
const bunDir = dirname5(process.execPath);
|
|
2253
2264
|
const seen = new Set;
|
|
2254
2265
|
const out = [];
|
|
2255
2266
|
for (const dir of baseline) {
|
|
@@ -2373,8 +2384,8 @@ __export(exports_run, {
|
|
|
2373
2384
|
HookTimeoutError: () => HookTimeoutError
|
|
2374
2385
|
});
|
|
2375
2386
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2376
|
-
import { rmSync as rmSync2, writeFileSync as
|
|
2377
|
-
import { basename as basename2, dirname as
|
|
2387
|
+
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2388
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join10 } from "path";
|
|
2378
2389
|
function extensionOf(scriptPath) {
|
|
2379
2390
|
const base = basename2(scriptPath);
|
|
2380
2391
|
const dot = base.lastIndexOf(".");
|
|
@@ -2443,7 +2454,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
2443
2454
|
return chunks.join("");
|
|
2444
2455
|
}
|
|
2445
2456
|
async function executeVerifiedScript(options) {
|
|
2446
|
-
const scriptDir =
|
|
2457
|
+
const scriptDir = dirname6(options.scriptPath);
|
|
2447
2458
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
2448
2459
|
const tempPath = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
2449
2460
|
let proc = null;
|
|
@@ -2453,7 +2464,7 @@ async function executeVerifiedScript(options) {
|
|
|
2453
2464
|
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
2454
2465
|
}, options.timeout) : null;
|
|
2455
2466
|
try {
|
|
2456
|
-
|
|
2467
|
+
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
2457
2468
|
proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
2458
2469
|
stdin: new Response(options.stdin),
|
|
2459
2470
|
stdout: "pipe",
|
|
@@ -2653,16 +2664,16 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
2653
2664
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2654
2665
|
import { z as z2 } from "zod";
|
|
2655
2666
|
init_registry();
|
|
2656
|
-
import { existsSync as
|
|
2657
|
-
import { join as join11, dirname as
|
|
2667
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
2668
|
+
import { join as join11, dirname as dirname7 } from "path";
|
|
2658
2669
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2659
2670
|
|
|
2660
2671
|
// src/lib/installer.ts
|
|
2661
2672
|
init_registry();
|
|
2662
2673
|
init_resolve();
|
|
2663
2674
|
init_manifest();
|
|
2664
|
-
import { existsSync as
|
|
2665
|
-
import { join as join7, dirname as
|
|
2675
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2676
|
+
import { join as join7, dirname as dirname4 } from "path";
|
|
2666
2677
|
import { homedir as homedir3 } from "os";
|
|
2667
2678
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2668
2679
|
|
|
@@ -2861,9 +2872,78 @@ function checkScriptHash(name, actual) {
|
|
|
2861
2872
|
return { ok: true, pinned: true, expected, actual, name };
|
|
2862
2873
|
}
|
|
2863
2874
|
|
|
2875
|
+
// src/lib/codex-settings.ts
|
|
2876
|
+
import { closeSync, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
2877
|
+
import { dirname as dirname3 } from "path";
|
|
2878
|
+
import { randomUUID } from "crypto";
|
|
2879
|
+
var snapshots = new WeakMap;
|
|
2880
|
+
function current(path) {
|
|
2881
|
+
try {
|
|
2882
|
+
const stat = lstatSync(path);
|
|
2883
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
2884
|
+
throw new Error("invalid_file");
|
|
2885
|
+
return readFileSync4(path, "utf8");
|
|
2886
|
+
} catch (error) {
|
|
2887
|
+
if (error.code === "ENOENT")
|
|
2888
|
+
return null;
|
|
2889
|
+
throw new Error("Cannot safely read Codex hooks.json; no settings were changed.");
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
function readCodexSettings(path) {
|
|
2893
|
+
const raw = current(path);
|
|
2894
|
+
let settings;
|
|
2895
|
+
try {
|
|
2896
|
+
settings = raw === null ? {} : JSON.parse(raw);
|
|
2897
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
2898
|
+
throw new Error;
|
|
2899
|
+
if (settings.hooks !== undefined) {
|
|
2900
|
+
if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks))
|
|
2901
|
+
throw new Error;
|
|
2902
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
2903
|
+
if (!Array.isArray(entries) || entries.some((entry) => !entry || !Array.isArray(entry.hooks) || entry.hooks.some((hook) => !hook || typeof hook !== "object")))
|
|
2904
|
+
throw new Error;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
} catch {
|
|
2908
|
+
throw new Error("Invalid Codex hooks.json; repair it before installing or removing hooks.");
|
|
2909
|
+
}
|
|
2910
|
+
snapshots.set(settings, { path, raw });
|
|
2911
|
+
return settings;
|
|
2912
|
+
}
|
|
2913
|
+
function writeCodexSettings(path, settings) {
|
|
2914
|
+
const snapshot = snapshots.get(settings);
|
|
2915
|
+
if (!snapshot || snapshot.path !== path)
|
|
2916
|
+
throw new Error("Codex settings require a complete current read before writing.");
|
|
2917
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
2918
|
+
const lock = `${path}.hooks-write.lock`;
|
|
2919
|
+
let fd;
|
|
2920
|
+
try {
|
|
2921
|
+
fd = openSync(lock, "wx", 384);
|
|
2922
|
+
} catch {
|
|
2923
|
+
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
2924
|
+
}
|
|
2925
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
2926
|
+
try {
|
|
2927
|
+
if (current(path) !== snapshot.raw)
|
|
2928
|
+
throw new Error("Codex settings changed since reading; re-read before retrying.");
|
|
2929
|
+
const next = JSON.stringify(settings, null, 2) + `
|
|
2930
|
+
`;
|
|
2931
|
+
writeFileSync3(temp, next, { flag: "wx", mode: 384 });
|
|
2932
|
+
if (current(path) !== snapshot.raw)
|
|
2933
|
+
throw new Error("Codex settings changed since reading; re-read before retrying.");
|
|
2934
|
+
renameSync2(temp, path);
|
|
2935
|
+
snapshots.set(settings, { path, raw: next });
|
|
2936
|
+
} finally {
|
|
2937
|
+
if (existsSync7(temp))
|
|
2938
|
+
unlinkSync(temp);
|
|
2939
|
+
closeSync(fd);
|
|
2940
|
+
unlinkSync(lock);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2864
2944
|
// src/lib/installer.ts
|
|
2865
|
-
var __dirname2 =
|
|
2866
|
-
var HOOKS_DIR =
|
|
2945
|
+
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
2946
|
+
var HOOKS_DIR = existsSync8(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
|
|
2867
2947
|
function normalizeHookName(name) {
|
|
2868
2948
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
2869
2949
|
}
|
|
@@ -2871,10 +2951,15 @@ function shortHookName(name) {
|
|
|
2871
2951
|
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
2872
2952
|
}
|
|
2873
2953
|
function removeHookEntriesByName(entries, hookName) {
|
|
2874
|
-
return entries.
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2954
|
+
return entries.flatMap((entry) => {
|
|
2955
|
+
if (!Array.isArray(entry.hooks))
|
|
2956
|
+
return [entry];
|
|
2957
|
+
const kept = entry.hooks.filter((h) => {
|
|
2958
|
+
const match = h.command?.match(/^hooks run ([\w-]+)/);
|
|
2959
|
+
return !(match && match[1] === hookName);
|
|
2960
|
+
});
|
|
2961
|
+
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
2962
|
+
});
|
|
2878
2963
|
}
|
|
2879
2964
|
var EVENT_MAP = {
|
|
2880
2965
|
claude: {
|
|
@@ -2884,7 +2969,7 @@ var EVENT_MAP = {
|
|
|
2884
2969
|
Notification: "Notification",
|
|
2885
2970
|
SessionStart: "SessionStart",
|
|
2886
2971
|
SessionEnd: "SessionEnd",
|
|
2887
|
-
UserPromptSubmit:
|
|
2972
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
2888
2973
|
SubagentStart: null
|
|
2889
2974
|
},
|
|
2890
2975
|
gemini: {
|
|
@@ -2906,9 +2991,21 @@ var EVENT_MAP = {
|
|
|
2906
2991
|
SessionEnd: null,
|
|
2907
2992
|
UserPromptSubmit: "UserPromptSubmit",
|
|
2908
2993
|
SubagentStart: "SubagentStart"
|
|
2994
|
+
},
|
|
2995
|
+
codex: {
|
|
2996
|
+
PreToolUse: "PreToolUse",
|
|
2997
|
+
PostToolUse: "PostToolUse",
|
|
2998
|
+
Stop: "Stop",
|
|
2999
|
+
Notification: null,
|
|
3000
|
+
SessionStart: "SessionStart",
|
|
3001
|
+
SessionEnd: "SessionEnd",
|
|
3002
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
3003
|
+
SubagentStart: "SubagentStart"
|
|
2909
3004
|
}
|
|
2910
3005
|
};
|
|
2911
3006
|
function getTargetSettingsDir(target) {
|
|
3007
|
+
if (target === "codex")
|
|
3008
|
+
return ".codex";
|
|
2912
3009
|
if (target === "codewith")
|
|
2913
3010
|
return ".codewith";
|
|
2914
3011
|
if (target === "gemini")
|
|
@@ -2916,6 +3013,8 @@ function getTargetSettingsDir(target) {
|
|
|
2916
3013
|
return ".claude";
|
|
2917
3014
|
}
|
|
2918
3015
|
function getGlobalSettingsPathOverride(target) {
|
|
3016
|
+
if (target === "codex")
|
|
3017
|
+
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
|
|
2919
3018
|
if (target === "claude")
|
|
2920
3019
|
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
2921
3020
|
if (target === "gemini")
|
|
@@ -2932,6 +3031,8 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
2932
3031
|
if (globalOverride)
|
|
2933
3032
|
return globalOverride;
|
|
2934
3033
|
const dir = getTargetSettingsDir(target);
|
|
3034
|
+
if (target === "codex")
|
|
3035
|
+
return join7(scope === "project" ? process.cwd() : homedir3(), dir, "hooks.json");
|
|
2935
3036
|
if (scope === "project") {
|
|
2936
3037
|
return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
|
|
2937
3038
|
}
|
|
@@ -2945,13 +3046,15 @@ function getHookPath(name) {
|
|
|
2945
3046
|
return join7(HOOKS_DIR, normalizeHookName(shortName));
|
|
2946
3047
|
}
|
|
2947
3048
|
function hookExists(name) {
|
|
2948
|
-
return
|
|
3049
|
+
return existsSync8(getHookPath(name));
|
|
2949
3050
|
}
|
|
2950
3051
|
function readSettings(scope = "global", target = "claude") {
|
|
2951
3052
|
const path = getSettingsPath(scope, target);
|
|
3053
|
+
if (target === "codex")
|
|
3054
|
+
return readCodexSettings(path);
|
|
2952
3055
|
try {
|
|
2953
|
-
if (
|
|
2954
|
-
return JSON.parse(
|
|
3056
|
+
if (existsSync8(path)) {
|
|
3057
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
2955
3058
|
}
|
|
2956
3059
|
} catch (error) {
|
|
2957
3060
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -2960,11 +3063,13 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
2960
3063
|
}
|
|
2961
3064
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
2962
3065
|
const path = getSettingsPath(scope, target);
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3066
|
+
if (target === "codex")
|
|
3067
|
+
return writeCodexSettings(path, settings);
|
|
3068
|
+
const dir = dirname4(path);
|
|
3069
|
+
if (!existsSync8(dir)) {
|
|
3070
|
+
mkdirSync5(dir, { recursive: true });
|
|
2966
3071
|
}
|
|
2967
|
-
|
|
3072
|
+
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
2968
3073
|
`);
|
|
2969
3074
|
}
|
|
2970
3075
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -2987,6 +3092,8 @@ function codewithMatcher(matcher) {
|
|
|
2987
3092
|
}
|
|
2988
3093
|
function codewithTimeout(name) {
|
|
2989
3094
|
switch (name) {
|
|
3095
|
+
case "mementos-context":
|
|
3096
|
+
return 12;
|
|
2990
3097
|
case "knowledge-context":
|
|
2991
3098
|
return 6;
|
|
2992
3099
|
case "session-start":
|
|
@@ -3053,21 +3160,21 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
3053
3160
|
function readCodewithConfig(scope, configPath) {
|
|
3054
3161
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
3055
3162
|
try {
|
|
3056
|
-
return
|
|
3163
|
+
return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
|
|
3057
3164
|
} catch {
|
|
3058
3165
|
return "";
|
|
3059
3166
|
}
|
|
3060
3167
|
}
|
|
3061
3168
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
3062
3169
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
3063
|
-
const dir =
|
|
3064
|
-
if (!
|
|
3065
|
-
|
|
3066
|
-
const existing =
|
|
3170
|
+
const dir = dirname4(path);
|
|
3171
|
+
if (!existsSync8(dir))
|
|
3172
|
+
mkdirSync5(dir, { recursive: true });
|
|
3173
|
+
const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
|
|
3067
3174
|
const sep2 = existing.trim() ? `
|
|
3068
3175
|
|
|
3069
3176
|
` : "";
|
|
3070
|
-
|
|
3177
|
+
writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
3071
3178
|
return path;
|
|
3072
3179
|
}
|
|
3073
3180
|
function detectConflict(name, scope, target) {
|
|
@@ -3164,18 +3271,25 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
3164
3271
|
};
|
|
3165
3272
|
}
|
|
3166
3273
|
}
|
|
3167
|
-
const registered = getRegisteredHooksForTarget(scope, target);
|
|
3168
|
-
if (registered.includes(shortName) && !overwrite) {
|
|
3169
|
-
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
3170
|
-
}
|
|
3171
|
-
const conflict = detectConflict(shortName, scope, target);
|
|
3172
|
-
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
3173
|
-
if (rewriteConflict) {
|
|
3174
|
-
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
3175
|
-
}
|
|
3176
3274
|
try {
|
|
3275
|
+
const registered = getRegisteredHooksForTarget(scope, target);
|
|
3276
|
+
if (registered.includes(shortName) && !overwrite) {
|
|
3277
|
+
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
3278
|
+
}
|
|
3279
|
+
const conflict = detectConflict(shortName, scope, target);
|
|
3280
|
+
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
3281
|
+
if (rewriteConflict) {
|
|
3282
|
+
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
3283
|
+
}
|
|
3177
3284
|
registerHook(shortName, scope, target, profile);
|
|
3178
|
-
return {
|
|
3285
|
+
return {
|
|
3286
|
+
hook: shortName,
|
|
3287
|
+
success: true,
|
|
3288
|
+
scope,
|
|
3289
|
+
target,
|
|
3290
|
+
...conflict ? { conflict } : {},
|
|
3291
|
+
...target === "codex" ? { applied: true, configPath: getSettingsPath(scope, target), note: "Registered in Codex hooks.json. Review and trust the exact definition in Codex /hooks; project hooks also require a trusted project. Registration alone does not prove execution." } : {}
|
|
3292
|
+
};
|
|
3179
3293
|
} catch (error) {
|
|
3180
3294
|
return {
|
|
3181
3295
|
hook: shortName,
|
|
@@ -3187,6 +3301,9 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
3187
3301
|
}
|
|
3188
3302
|
function installHook(name, options = {}) {
|
|
3189
3303
|
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
|
|
3304
|
+
if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
|
|
3305
|
+
return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
|
|
3306
|
+
}
|
|
3190
3307
|
if (target === "all") {
|
|
3191
3308
|
const shortName = shortHookName(name);
|
|
3192
3309
|
const meta = resolveHookMeta(shortName);
|
|
@@ -3315,7 +3432,8 @@ function removeHook(name, scope = "global", target = "claude") {
|
|
|
3315
3432
|
if (target === "all") {
|
|
3316
3433
|
const claudeRemoved = removeHookForTarget(shortName, scope, "claude");
|
|
3317
3434
|
const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
|
|
3318
|
-
|
|
3435
|
+
const codexRemoved = removeHookForTarget(shortName, scope, "codex");
|
|
3436
|
+
return claudeRemoved || geminiRemoved || codexRemoved;
|
|
3319
3437
|
}
|
|
3320
3438
|
if (target === "codewith") {
|
|
3321
3439
|
return false;
|
|
@@ -3333,21 +3451,21 @@ function removeHookForTarget(name, scope, target) {
|
|
|
3333
3451
|
|
|
3334
3452
|
// src/lib/profiles.ts
|
|
3335
3453
|
init_app_home();
|
|
3336
|
-
import { existsSync as
|
|
3454
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
3337
3455
|
import { join as join8 } from "path";
|
|
3338
3456
|
function getProfilesDir() {
|
|
3339
3457
|
const newDir = join8(getEffectiveDataRoot(), "profiles");
|
|
3340
3458
|
const oldDir = join8(getHomeDir(), ".hooks", "profiles");
|
|
3341
|
-
if (!
|
|
3342
|
-
|
|
3459
|
+
if (!existsSync9(newDir) && existsSync9(oldDir)) {
|
|
3460
|
+
mkdirSync6(newDir, { recursive: true });
|
|
3343
3461
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
3344
3462
|
}
|
|
3345
3463
|
return newDir;
|
|
3346
3464
|
}
|
|
3347
3465
|
function ensureProfilesDir() {
|
|
3348
3466
|
const dir = getProfilesDir();
|
|
3349
|
-
if (!
|
|
3350
|
-
|
|
3467
|
+
if (!existsSync9(dir)) {
|
|
3468
|
+
mkdirSync6(dir, { recursive: true });
|
|
3351
3469
|
}
|
|
3352
3470
|
}
|
|
3353
3471
|
function profilePath(id) {
|
|
@@ -3370,30 +3488,30 @@ function createProfile(input) {
|
|
|
3370
3488
|
if (input.name) {
|
|
3371
3489
|
profile.name = input.name;
|
|
3372
3490
|
}
|
|
3373
|
-
|
|
3491
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
3374
3492
|
`);
|
|
3375
3493
|
return profile;
|
|
3376
3494
|
}
|
|
3377
3495
|
function getProfile(id) {
|
|
3378
3496
|
const path = profilePath(id);
|
|
3379
3497
|
try {
|
|
3380
|
-
if (!
|
|
3498
|
+
if (!existsSync9(path))
|
|
3381
3499
|
return null;
|
|
3382
|
-
return JSON.parse(
|
|
3500
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
3383
3501
|
} catch {
|
|
3384
3502
|
return null;
|
|
3385
3503
|
}
|
|
3386
3504
|
}
|
|
3387
3505
|
function listProfiles() {
|
|
3388
3506
|
const dir = getProfilesDir();
|
|
3389
|
-
if (!
|
|
3507
|
+
if (!existsSync9(dir))
|
|
3390
3508
|
return [];
|
|
3391
3509
|
try {
|
|
3392
3510
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
3393
3511
|
const profiles = [];
|
|
3394
3512
|
for (const file of files) {
|
|
3395
3513
|
try {
|
|
3396
|
-
const content =
|
|
3514
|
+
const content = readFileSync6(join8(dir, file), "utf-8");
|
|
3397
3515
|
profiles.push(JSON.parse(content));
|
|
3398
3516
|
} catch {}
|
|
3399
3517
|
}
|
|
@@ -3925,13 +4043,13 @@ function coerceForSqlite(value) {
|
|
|
3925
4043
|
return String(value);
|
|
3926
4044
|
}
|
|
3927
4045
|
// src/mcp/server.ts
|
|
3928
|
-
var __dirname3 =
|
|
4046
|
+
var __dirname3 = dirname7(fileURLToPath3(import.meta.url));
|
|
3929
4047
|
var pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
3930
4048
|
try {
|
|
3931
4049
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
3932
4050
|
const p = join11(__dirname3, rel);
|
|
3933
|
-
if (
|
|
3934
|
-
pkg = JSON.parse(
|
|
4051
|
+
if (existsSync11(p)) {
|
|
4052
|
+
pkg = JSON.parse(readFileSync8(p, "utf-8"));
|
|
3935
4053
|
break;
|
|
3936
4054
|
}
|
|
3937
4055
|
}
|
|
@@ -3940,10 +4058,10 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
|
3940
4058
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
3941
4059
|
const resolved = resolveHook2(name);
|
|
3942
4060
|
const hookScript = resolveScriptPath2(name);
|
|
3943
|
-
if (!resolved || !hookScript || !
|
|
4061
|
+
if (!resolved || !hookScript || !existsSync11(hookScript)) {
|
|
3944
4062
|
throw new Error(`Hook '${name}' not found`);
|
|
3945
4063
|
}
|
|
3946
|
-
const content =
|
|
4064
|
+
const content = readFileSync8(hookScript);
|
|
3947
4065
|
const sha = sha256Of(content);
|
|
3948
4066
|
const check = checkScriptHash(name, sha);
|
|
3949
4067
|
if (!check.ok) {
|
|
@@ -4149,7 +4267,7 @@ function createHooksServer() {
|
|
|
4149
4267
|
const settingsPath = getSettingsPath(scope);
|
|
4150
4268
|
const issues = [];
|
|
4151
4269
|
const healthy = [];
|
|
4152
|
-
const settingsExist =
|
|
4270
|
+
const settingsExist = existsSync11(settingsPath);
|
|
4153
4271
|
if (!settingsExist) {
|
|
4154
4272
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
4155
4273
|
}
|
|
@@ -4159,7 +4277,7 @@ function createHooksServer() {
|
|
|
4159
4277
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
4160
4278
|
let hookHealthy = true;
|
|
4161
4279
|
if (custom) {
|
|
4162
|
-
if (!
|
|
4280
|
+
if (!existsSync11(custom.scriptPath)) {
|
|
4163
4281
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
4164
4282
|
hookHealthy = false;
|
|
4165
4283
|
}
|
|
@@ -4169,14 +4287,14 @@ function createHooksServer() {
|
|
|
4169
4287
|
continue;
|
|
4170
4288
|
}
|
|
4171
4289
|
const hookDir = getHookPath(name);
|
|
4172
|
-
if (!
|
|
4290
|
+
if (!existsSync11(join11(hookDir, "src", "hook.ts"))) {
|
|
4173
4291
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
4174
4292
|
hookHealthy = false;
|
|
4175
4293
|
}
|
|
4176
4294
|
}
|
|
4177
4295
|
if (meta && settingsExist) {
|
|
4178
4296
|
try {
|
|
4179
|
-
const settings = JSON.parse(
|
|
4297
|
+
const settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
4180
4298
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
4181
4299
|
const eventName = meta.event.split(":")[0];
|
|
4182
4300
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -4215,8 +4333,8 @@ function createHooksServer() {
|
|
|
4215
4333
|
const hookPath = getHookPath(name);
|
|
4216
4334
|
const readmePath = join11(hookPath, "README.md");
|
|
4217
4335
|
let readme = "";
|
|
4218
|
-
if (
|
|
4219
|
-
readme =
|
|
4336
|
+
if (existsSync11(readmePath)) {
|
|
4337
|
+
readme = readFileSync8(readmePath, "utf-8");
|
|
4220
4338
|
}
|
|
4221
4339
|
if (verbose) {
|
|
4222
4340
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -4399,7 +4517,7 @@ function createHooksServer() {
|
|
|
4399
4517
|
const ctx = {
|
|
4400
4518
|
scope,
|
|
4401
4519
|
settings_path: settingsPath,
|
|
4402
|
-
settings_exists:
|
|
4520
|
+
settings_exists: existsSync11(settingsPath),
|
|
4403
4521
|
registered_hooks: hooks,
|
|
4404
4522
|
hook_count: hooks.length,
|
|
4405
4523
|
healthy,
|
|
@@ -4545,8 +4663,8 @@ function createHooksServer() {
|
|
|
4545
4663
|
const settingsPath = getSettingsPath(scope);
|
|
4546
4664
|
let settings = {};
|
|
4547
4665
|
try {
|
|
4548
|
-
if (
|
|
4549
|
-
settings = JSON.parse(
|
|
4666
|
+
if (existsSync11(settingsPath))
|
|
4667
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
4550
4668
|
} catch {}
|
|
4551
4669
|
if (!settings.hooks)
|
|
4552
4670
|
settings.hooks = {};
|
|
@@ -4554,10 +4672,10 @@ function createHooksServer() {
|
|
|
4554
4672
|
if (!disabled.includes(name))
|
|
4555
4673
|
disabled.push(name);
|
|
4556
4674
|
settings.hooks.__disabled = disabled;
|
|
4557
|
-
const { writeFileSync:
|
|
4558
|
-
const { dirname:
|
|
4559
|
-
|
|
4560
|
-
|
|
4675
|
+
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync7 } = await import("fs");
|
|
4676
|
+
const { dirname: dirname8 } = await import("path");
|
|
4677
|
+
mkdirSync7(dirname8(settingsPath), { recursive: true });
|
|
4678
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4561
4679
|
`);
|
|
4562
4680
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
4563
4681
|
});
|
|
@@ -4568,15 +4686,15 @@ function createHooksServer() {
|
|
|
4568
4686
|
const settingsPath = getSettingsPath(scope);
|
|
4569
4687
|
let settings = {};
|
|
4570
4688
|
try {
|
|
4571
|
-
if (
|
|
4572
|
-
settings = JSON.parse(
|
|
4689
|
+
if (existsSync11(settingsPath))
|
|
4690
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
4573
4691
|
} catch {}
|
|
4574
4692
|
if (settings.hooks?.__disabled) {
|
|
4575
4693
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
4576
4694
|
if (settings.hooks.__disabled.length === 0)
|
|
4577
4695
|
delete settings.hooks.__disabled;
|
|
4578
|
-
const { writeFileSync:
|
|
4579
|
-
|
|
4696
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
4697
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4580
4698
|
`);
|
|
4581
4699
|
}
|
|
4582
4700
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
|