@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/bin/serve.js
CHANGED
|
@@ -345,6 +345,17 @@ var HOOKS = [
|
|
|
345
345
|
matcher: "",
|
|
346
346
|
tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
|
|
347
347
|
},
|
|
348
|
+
{
|
|
349
|
+
name: "mementos-context",
|
|
350
|
+
displayName: "Mementos Prompt Context",
|
|
351
|
+
description: "Retrieves scoped memories for a user prompt, with optional relevance judgments",
|
|
352
|
+
version: "0.1.0",
|
|
353
|
+
category: "Context Management",
|
|
354
|
+
event: "UserPromptSubmit",
|
|
355
|
+
matcher: "",
|
|
356
|
+
timeoutSeconds: 12,
|
|
357
|
+
tags: ["mementos", "memory", "prompt", "codex", "claude", "codewith", "opt-in"]
|
|
358
|
+
},
|
|
348
359
|
{
|
|
349
360
|
name: "autoformat",
|
|
350
361
|
displayName: "Auto Format",
|
package/dist/index.js
CHANGED
|
@@ -4974,21 +4974,21 @@ __export(exports_profiles, {
|
|
|
4974
4974
|
deleteProfile: () => deleteProfile,
|
|
4975
4975
|
createProfile: () => createProfile
|
|
4976
4976
|
});
|
|
4977
|
-
import { existsSync as
|
|
4977
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
4978
4978
|
import { join as join8 } from "path";
|
|
4979
4979
|
function getProfilesDir() {
|
|
4980
4980
|
const newDir = join8(getEffectiveDataRoot(), "profiles");
|
|
4981
4981
|
const oldDir = join8(getHomeDir(), ".hooks", "profiles");
|
|
4982
|
-
if (!
|
|
4983
|
-
|
|
4982
|
+
if (!existsSync9(newDir) && existsSync9(oldDir)) {
|
|
4983
|
+
mkdirSync6(newDir, { recursive: true });
|
|
4984
4984
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
4985
4985
|
}
|
|
4986
4986
|
return newDir;
|
|
4987
4987
|
}
|
|
4988
4988
|
function ensureProfilesDir() {
|
|
4989
4989
|
const dir = getProfilesDir();
|
|
4990
|
-
if (!
|
|
4991
|
-
|
|
4990
|
+
if (!existsSync9(dir)) {
|
|
4991
|
+
mkdirSync6(dir, { recursive: true });
|
|
4992
4992
|
}
|
|
4993
4993
|
}
|
|
4994
4994
|
function profilePath(id) {
|
|
@@ -5011,30 +5011,30 @@ function createProfile(input) {
|
|
|
5011
5011
|
if (input.name) {
|
|
5012
5012
|
profile.name = input.name;
|
|
5013
5013
|
}
|
|
5014
|
-
|
|
5014
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5015
5015
|
`);
|
|
5016
5016
|
return profile;
|
|
5017
5017
|
}
|
|
5018
5018
|
function getProfile(id) {
|
|
5019
5019
|
const path = profilePath(id);
|
|
5020
5020
|
try {
|
|
5021
|
-
if (!
|
|
5021
|
+
if (!existsSync9(path))
|
|
5022
5022
|
return null;
|
|
5023
|
-
return JSON.parse(
|
|
5023
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
5024
5024
|
} catch {
|
|
5025
5025
|
return null;
|
|
5026
5026
|
}
|
|
5027
5027
|
}
|
|
5028
5028
|
function listProfiles() {
|
|
5029
5029
|
const dir = getProfilesDir();
|
|
5030
|
-
if (!
|
|
5030
|
+
if (!existsSync9(dir))
|
|
5031
5031
|
return [];
|
|
5032
5032
|
try {
|
|
5033
5033
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
5034
5034
|
const profiles = [];
|
|
5035
5035
|
for (const file of files) {
|
|
5036
5036
|
try {
|
|
5037
|
-
const content =
|
|
5037
|
+
const content = readFileSync6(join8(dir, file), "utf-8");
|
|
5038
5038
|
profiles.push(JSON.parse(content));
|
|
5039
5039
|
} catch {}
|
|
5040
5040
|
}
|
|
@@ -5051,13 +5051,13 @@ function updateProfile(id, data) {
|
|
|
5051
5051
|
profile.name = data.name;
|
|
5052
5052
|
if (data.preferences !== undefined)
|
|
5053
5053
|
profile.preferences = data.preferences;
|
|
5054
|
-
|
|
5054
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5055
5055
|
`);
|
|
5056
5056
|
return profile;
|
|
5057
5057
|
}
|
|
5058
5058
|
function deleteProfile(id) {
|
|
5059
5059
|
const path = profilePath(id);
|
|
5060
|
-
if (!
|
|
5060
|
+
if (!existsSync9(path))
|
|
5061
5061
|
return false;
|
|
5062
5062
|
try {
|
|
5063
5063
|
rmSync(path);
|
|
@@ -5071,7 +5071,7 @@ function touchProfile(id) {
|
|
|
5071
5071
|
if (!profile)
|
|
5072
5072
|
return;
|
|
5073
5073
|
profile.last_seen_at = new Date().toISOString();
|
|
5074
|
-
|
|
5074
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5075
5075
|
`);
|
|
5076
5076
|
}
|
|
5077
5077
|
function exportProfiles() {
|
|
@@ -5087,11 +5087,11 @@ function importProfiles(profiles) {
|
|
|
5087
5087
|
continue;
|
|
5088
5088
|
}
|
|
5089
5089
|
const path = profilePath(profile.agent_id);
|
|
5090
|
-
if (
|
|
5090
|
+
if (existsSync9(path)) {
|
|
5091
5091
|
skipped++;
|
|
5092
5092
|
continue;
|
|
5093
5093
|
}
|
|
5094
|
-
|
|
5094
|
+
writeFileSync5(path, JSON.stringify(profile, null, 2) + `
|
|
5095
5095
|
`);
|
|
5096
5096
|
imported++;
|
|
5097
5097
|
}
|
|
@@ -5104,7 +5104,7 @@ var init_profiles = __esm(() => {
|
|
|
5104
5104
|
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
|
|
5105
5105
|
import { isIP } from "net";
|
|
5106
5106
|
import { spawnSync } from "child_process";
|
|
5107
|
-
import { closeSync, fstatSync, openSync, readFileSync as
|
|
5107
|
+
import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
|
|
5108
5108
|
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
5109
5109
|
import { createRequire } from "module";
|
|
5110
5110
|
import { hostname as osHostname } from "os";
|
|
@@ -5208,7 +5208,7 @@ function readAppConfigFile(path) {
|
|
|
5208
5208
|
};
|
|
5209
5209
|
let fd = -1;
|
|
5210
5210
|
try {
|
|
5211
|
-
fd =
|
|
5211
|
+
fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
5212
5212
|
} catch (error) {
|
|
5213
5213
|
const code = error.code;
|
|
5214
5214
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -5229,7 +5229,7 @@ function readAppConfigFile(path) {
|
|
|
5229
5229
|
unsafe("the file is not owned by the current user");
|
|
5230
5230
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
5231
5231
|
unsafe("the file exceeds the size limit");
|
|
5232
|
-
const bytes =
|
|
5232
|
+
const bytes = readFileSync7(fd);
|
|
5233
5233
|
const after = fstatSync(fd);
|
|
5234
5234
|
if (!configFileReadsCoherent(before, after)) {
|
|
5235
5235
|
unsafe("the file changed while being read");
|
|
@@ -5237,7 +5237,7 @@ function readAppConfigFile(path) {
|
|
|
5237
5237
|
return parseEnvFile(bytes.toString("utf8"));
|
|
5238
5238
|
} finally {
|
|
5239
5239
|
if (fd !== -1)
|
|
5240
|
-
|
|
5240
|
+
closeSync2(fd);
|
|
5241
5241
|
}
|
|
5242
5242
|
}
|
|
5243
5243
|
function readCredentialFile(path, apiKeyKeys) {
|
|
@@ -6013,8 +6013,8 @@ var init_db_writer = __esm(() => {
|
|
|
6013
6013
|
});
|
|
6014
6014
|
|
|
6015
6015
|
// src/lib/hook-env.ts
|
|
6016
|
-
import { dirname as
|
|
6017
|
-
import { existsSync as
|
|
6016
|
+
import { dirname as dirname6, isAbsolute as isAbsolute3 } from "path";
|
|
6017
|
+
import { existsSync as existsSync11, realpathSync as realpathSync2, statSync } from "fs";
|
|
6018
6018
|
import { tmpdir as tmpdir2 } from "os";
|
|
6019
6019
|
function isDeniedEnvName(name) {
|
|
6020
6020
|
const upper = name.toUpperCase();
|
|
@@ -6062,9 +6062,9 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6062
6062
|
}
|
|
6063
6063
|
function sanitizeHookPath(pathValue, home) {
|
|
6064
6064
|
const baseline = PATH_BASELINE.slice();
|
|
6065
|
-
if (process.platform === "darwin" &&
|
|
6065
|
+
if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
|
|
6066
6066
|
baseline.push("/opt/homebrew/bin");
|
|
6067
|
-
const bunDir =
|
|
6067
|
+
const bunDir = dirname6(process.execPath);
|
|
6068
6068
|
const seen = new Set;
|
|
6069
6069
|
const out = [];
|
|
6070
6070
|
for (const dir of baseline) {
|
|
@@ -6188,8 +6188,8 @@ __export(exports_run, {
|
|
|
6188
6188
|
HookTimeoutError: () => HookTimeoutError
|
|
6189
6189
|
});
|
|
6190
6190
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
6191
|
-
import { rmSync as rmSync3, writeFileSync as
|
|
6192
|
-
import { basename as basename2, dirname as
|
|
6191
|
+
import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
6192
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute4, join as join12 } from "path";
|
|
6193
6193
|
function extensionOf(scriptPath) {
|
|
6194
6194
|
const base = basename2(scriptPath);
|
|
6195
6195
|
const dot = base.lastIndexOf(".");
|
|
@@ -6258,7 +6258,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
6258
6258
|
return chunks.join("");
|
|
6259
6259
|
}
|
|
6260
6260
|
async function executeVerifiedScript(options) {
|
|
6261
|
-
const scriptDir =
|
|
6261
|
+
const scriptDir = dirname7(options.scriptPath);
|
|
6262
6262
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
6263
6263
|
const tempPath = join12(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
6264
6264
|
let proc = null;
|
|
@@ -6268,7 +6268,7 @@ async function executeVerifiedScript(options) {
|
|
|
6268
6268
|
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
6269
6269
|
}, options.timeout) : null;
|
|
6270
6270
|
try {
|
|
6271
|
-
|
|
6271
|
+
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
6272
6272
|
proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
6273
6273
|
stdin: new Response(options.stdin),
|
|
6274
6274
|
stdout: "pipe",
|
|
@@ -7016,6 +7016,17 @@ var HOOKS = [
|
|
|
7016
7016
|
matcher: "",
|
|
7017
7017
|
tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
|
|
7018
7018
|
},
|
|
7019
|
+
{
|
|
7020
|
+
name: "mementos-context",
|
|
7021
|
+
displayName: "Mementos Prompt Context",
|
|
7022
|
+
description: "Retrieves scoped memories for a user prompt, with optional relevance judgments",
|
|
7023
|
+
version: "0.1.0",
|
|
7024
|
+
category: "Context Management",
|
|
7025
|
+
event: "UserPromptSubmit",
|
|
7026
|
+
matcher: "",
|
|
7027
|
+
timeoutSeconds: 12,
|
|
7028
|
+
tags: ["mementos", "memory", "prompt", "codex", "claude", "codewith", "opt-in"]
|
|
7029
|
+
},
|
|
7019
7030
|
{
|
|
7020
7031
|
name: "autoformat",
|
|
7021
7032
|
displayName: "Auto Format",
|
|
@@ -7341,8 +7352,8 @@ function getHook(name) {
|
|
|
7341
7352
|
return HOOKS.find((h) => h.name === name);
|
|
7342
7353
|
}
|
|
7343
7354
|
// src/lib/installer.ts
|
|
7344
|
-
import { existsSync as
|
|
7345
|
-
import { join as join7, dirname as
|
|
7355
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
7356
|
+
import { join as join7, dirname as dirname4 } from "path";
|
|
7346
7357
|
import { homedir as homedir3 } from "os";
|
|
7347
7358
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7348
7359
|
|
|
@@ -7441,8 +7452,79 @@ function matchersOverlap(a, b) {
|
|
|
7441
7452
|
|
|
7442
7453
|
// src/lib/installer.ts
|
|
7443
7454
|
init_store();
|
|
7444
|
-
|
|
7445
|
-
|
|
7455
|
+
|
|
7456
|
+
// src/lib/codex-settings.ts
|
|
7457
|
+
import { closeSync, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
7458
|
+
import { dirname as dirname3 } from "path";
|
|
7459
|
+
import { randomUUID } from "crypto";
|
|
7460
|
+
var snapshots = new WeakMap;
|
|
7461
|
+
function current(path) {
|
|
7462
|
+
try {
|
|
7463
|
+
const stat = lstatSync(path);
|
|
7464
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
7465
|
+
throw new Error("invalid_file");
|
|
7466
|
+
return readFileSync4(path, "utf8");
|
|
7467
|
+
} catch (error) {
|
|
7468
|
+
if (error.code === "ENOENT")
|
|
7469
|
+
return null;
|
|
7470
|
+
throw new Error("Cannot safely read Codex hooks.json; no settings were changed.");
|
|
7471
|
+
}
|
|
7472
|
+
}
|
|
7473
|
+
function readCodexSettings(path) {
|
|
7474
|
+
const raw = current(path);
|
|
7475
|
+
let settings;
|
|
7476
|
+
try {
|
|
7477
|
+
settings = raw === null ? {} : JSON.parse(raw);
|
|
7478
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
7479
|
+
throw new Error;
|
|
7480
|
+
if (settings.hooks !== undefined) {
|
|
7481
|
+
if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks))
|
|
7482
|
+
throw new Error;
|
|
7483
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
7484
|
+
if (!Array.isArray(entries) || entries.some((entry) => !entry || !Array.isArray(entry.hooks) || entry.hooks.some((hook) => !hook || typeof hook !== "object")))
|
|
7485
|
+
throw new Error;
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7488
|
+
} catch {
|
|
7489
|
+
throw new Error("Invalid Codex hooks.json; repair it before installing or removing hooks.");
|
|
7490
|
+
}
|
|
7491
|
+
snapshots.set(settings, { path, raw });
|
|
7492
|
+
return settings;
|
|
7493
|
+
}
|
|
7494
|
+
function writeCodexSettings(path, settings) {
|
|
7495
|
+
const snapshot = snapshots.get(settings);
|
|
7496
|
+
if (!snapshot || snapshot.path !== path)
|
|
7497
|
+
throw new Error("Codex settings require a complete current read before writing.");
|
|
7498
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
7499
|
+
const lock = `${path}.hooks-write.lock`;
|
|
7500
|
+
let fd;
|
|
7501
|
+
try {
|
|
7502
|
+
fd = openSync(lock, "wx", 384);
|
|
7503
|
+
} catch {
|
|
7504
|
+
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
7505
|
+
}
|
|
7506
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
7507
|
+
try {
|
|
7508
|
+
if (current(path) !== snapshot.raw)
|
|
7509
|
+
throw new Error("Codex settings changed since reading; re-read before retrying.");
|
|
7510
|
+
const next = JSON.stringify(settings, null, 2) + `
|
|
7511
|
+
`;
|
|
7512
|
+
writeFileSync3(temp, next, { flag: "wx", mode: 384 });
|
|
7513
|
+
if (current(path) !== snapshot.raw)
|
|
7514
|
+
throw new Error("Codex settings changed since reading; re-read before retrying.");
|
|
7515
|
+
renameSync2(temp, path);
|
|
7516
|
+
snapshots.set(settings, { path, raw: next });
|
|
7517
|
+
} finally {
|
|
7518
|
+
if (existsSync7(temp))
|
|
7519
|
+
unlinkSync(temp);
|
|
7520
|
+
closeSync(fd);
|
|
7521
|
+
unlinkSync(lock);
|
|
7522
|
+
}
|
|
7523
|
+
}
|
|
7524
|
+
|
|
7525
|
+
// src/lib/installer.ts
|
|
7526
|
+
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
7527
|
+
var HOOKS_DIR = existsSync8(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
|
|
7446
7528
|
function normalizeHookName(name) {
|
|
7447
7529
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
7448
7530
|
}
|
|
@@ -7450,10 +7532,15 @@ function shortHookName(name) {
|
|
|
7450
7532
|
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
7451
7533
|
}
|
|
7452
7534
|
function removeHookEntriesByName(entries, hookName) {
|
|
7453
|
-
return entries.
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7535
|
+
return entries.flatMap((entry) => {
|
|
7536
|
+
if (!Array.isArray(entry.hooks))
|
|
7537
|
+
return [entry];
|
|
7538
|
+
const kept = entry.hooks.filter((h) => {
|
|
7539
|
+
const match = h.command?.match(/^hooks run ([\w-]+)/);
|
|
7540
|
+
return !(match && match[1] === hookName);
|
|
7541
|
+
});
|
|
7542
|
+
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
7543
|
+
});
|
|
7457
7544
|
}
|
|
7458
7545
|
var EVENT_MAP = {
|
|
7459
7546
|
claude: {
|
|
@@ -7463,7 +7550,7 @@ var EVENT_MAP = {
|
|
|
7463
7550
|
Notification: "Notification",
|
|
7464
7551
|
SessionStart: "SessionStart",
|
|
7465
7552
|
SessionEnd: "SessionEnd",
|
|
7466
|
-
UserPromptSubmit:
|
|
7553
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
7467
7554
|
SubagentStart: null
|
|
7468
7555
|
},
|
|
7469
7556
|
gemini: {
|
|
@@ -7485,9 +7572,21 @@ var EVENT_MAP = {
|
|
|
7485
7572
|
SessionEnd: null,
|
|
7486
7573
|
UserPromptSubmit: "UserPromptSubmit",
|
|
7487
7574
|
SubagentStart: "SubagentStart"
|
|
7575
|
+
},
|
|
7576
|
+
codex: {
|
|
7577
|
+
PreToolUse: "PreToolUse",
|
|
7578
|
+
PostToolUse: "PostToolUse",
|
|
7579
|
+
Stop: "Stop",
|
|
7580
|
+
Notification: null,
|
|
7581
|
+
SessionStart: "SessionStart",
|
|
7582
|
+
SessionEnd: "SessionEnd",
|
|
7583
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
7584
|
+
SubagentStart: "SubagentStart"
|
|
7488
7585
|
}
|
|
7489
7586
|
};
|
|
7490
7587
|
function getTargetSettingsDir(target) {
|
|
7588
|
+
if (target === "codex")
|
|
7589
|
+
return ".codex";
|
|
7491
7590
|
if (target === "codewith")
|
|
7492
7591
|
return ".codewith";
|
|
7493
7592
|
if (target === "gemini")
|
|
@@ -7495,6 +7594,8 @@ function getTargetSettingsDir(target) {
|
|
|
7495
7594
|
return ".claude";
|
|
7496
7595
|
}
|
|
7497
7596
|
function getGlobalSettingsPathOverride(target) {
|
|
7597
|
+
if (target === "codex")
|
|
7598
|
+
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
|
|
7498
7599
|
if (target === "claude")
|
|
7499
7600
|
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
7500
7601
|
if (target === "gemini")
|
|
@@ -7511,6 +7612,8 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
7511
7612
|
if (globalOverride)
|
|
7512
7613
|
return globalOverride;
|
|
7513
7614
|
const dir = getTargetSettingsDir(target);
|
|
7615
|
+
if (target === "codex")
|
|
7616
|
+
return join7(scope === "project" ? process.cwd() : homedir3(), dir, "hooks.json");
|
|
7514
7617
|
if (scope === "project") {
|
|
7515
7618
|
return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
|
|
7516
7619
|
}
|
|
@@ -7524,13 +7627,15 @@ function getHookPath(name) {
|
|
|
7524
7627
|
return join7(HOOKS_DIR, normalizeHookName(shortName));
|
|
7525
7628
|
}
|
|
7526
7629
|
function hookExists(name) {
|
|
7527
|
-
return
|
|
7630
|
+
return existsSync8(getHookPath(name));
|
|
7528
7631
|
}
|
|
7529
7632
|
function readSettings(scope = "global", target = "claude") {
|
|
7530
7633
|
const path = getSettingsPath(scope, target);
|
|
7634
|
+
if (target === "codex")
|
|
7635
|
+
return readCodexSettings(path);
|
|
7531
7636
|
try {
|
|
7532
|
-
if (
|
|
7533
|
-
return JSON.parse(
|
|
7637
|
+
if (existsSync8(path)) {
|
|
7638
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
7534
7639
|
}
|
|
7535
7640
|
} catch (error) {
|
|
7536
7641
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -7539,11 +7644,13 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
7539
7644
|
}
|
|
7540
7645
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
7541
7646
|
const path = getSettingsPath(scope, target);
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7647
|
+
if (target === "codex")
|
|
7648
|
+
return writeCodexSettings(path, settings);
|
|
7649
|
+
const dir = dirname4(path);
|
|
7650
|
+
if (!existsSync8(dir)) {
|
|
7651
|
+
mkdirSync5(dir, { recursive: true });
|
|
7545
7652
|
}
|
|
7546
|
-
|
|
7653
|
+
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
7547
7654
|
`);
|
|
7548
7655
|
}
|
|
7549
7656
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -7566,6 +7673,8 @@ function codewithMatcher(matcher) {
|
|
|
7566
7673
|
}
|
|
7567
7674
|
function codewithTimeout(name) {
|
|
7568
7675
|
switch (name) {
|
|
7676
|
+
case "mementos-context":
|
|
7677
|
+
return 12;
|
|
7569
7678
|
case "knowledge-context":
|
|
7570
7679
|
return 6;
|
|
7571
7680
|
case "session-start":
|
|
@@ -7632,21 +7741,21 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
7632
7741
|
function readCodewithConfig(scope, configPath) {
|
|
7633
7742
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
7634
7743
|
try {
|
|
7635
|
-
return
|
|
7744
|
+
return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
|
|
7636
7745
|
} catch {
|
|
7637
7746
|
return "";
|
|
7638
7747
|
}
|
|
7639
7748
|
}
|
|
7640
7749
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
7641
7750
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
7642
|
-
const dir =
|
|
7643
|
-
if (!
|
|
7644
|
-
|
|
7645
|
-
const existing =
|
|
7751
|
+
const dir = dirname4(path);
|
|
7752
|
+
if (!existsSync8(dir))
|
|
7753
|
+
mkdirSync5(dir, { recursive: true });
|
|
7754
|
+
const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
|
|
7646
7755
|
const sep2 = existing.trim() ? `
|
|
7647
7756
|
|
|
7648
7757
|
` : "";
|
|
7649
|
-
|
|
7758
|
+
writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
7650
7759
|
return path;
|
|
7651
7760
|
}
|
|
7652
7761
|
function detectConflict(name, scope, target) {
|
|
@@ -7743,18 +7852,25 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
7743
7852
|
};
|
|
7744
7853
|
}
|
|
7745
7854
|
}
|
|
7746
|
-
const registered = getRegisteredHooksForTarget(scope, target);
|
|
7747
|
-
if (registered.includes(shortName) && !overwrite) {
|
|
7748
|
-
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
7749
|
-
}
|
|
7750
|
-
const conflict = detectConflict(shortName, scope, target);
|
|
7751
|
-
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
7752
|
-
if (rewriteConflict) {
|
|
7753
|
-
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
7754
|
-
}
|
|
7755
7855
|
try {
|
|
7856
|
+
const registered = getRegisteredHooksForTarget(scope, target);
|
|
7857
|
+
if (registered.includes(shortName) && !overwrite) {
|
|
7858
|
+
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
7859
|
+
}
|
|
7860
|
+
const conflict = detectConflict(shortName, scope, target);
|
|
7861
|
+
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
7862
|
+
if (rewriteConflict) {
|
|
7863
|
+
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
7864
|
+
}
|
|
7756
7865
|
registerHook(shortName, scope, target, profile);
|
|
7757
|
-
return {
|
|
7866
|
+
return {
|
|
7867
|
+
hook: shortName,
|
|
7868
|
+
success: true,
|
|
7869
|
+
scope,
|
|
7870
|
+
target,
|
|
7871
|
+
...conflict ? { conflict } : {},
|
|
7872
|
+
...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." } : {}
|
|
7873
|
+
};
|
|
7758
7874
|
} catch (error) {
|
|
7759
7875
|
return {
|
|
7760
7876
|
hook: shortName,
|
|
@@ -7766,6 +7882,9 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
7766
7882
|
}
|
|
7767
7883
|
function installHook(name, options = {}) {
|
|
7768
7884
|
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
|
|
7885
|
+
if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
|
|
7886
|
+
return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
|
|
7887
|
+
}
|
|
7769
7888
|
if (target === "all") {
|
|
7770
7889
|
const shortName = shortHookName(name);
|
|
7771
7890
|
const meta = resolveHookMeta(shortName);
|
|
@@ -7898,7 +8017,8 @@ function removeHook(name, scope = "global", target = "claude") {
|
|
|
7898
8017
|
if (target === "all") {
|
|
7899
8018
|
const claudeRemoved = removeHookForTarget(shortName, scope, "claude");
|
|
7900
8019
|
const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
|
|
7901
|
-
|
|
8020
|
+
const codexRemoved = removeHookForTarget(shortName, scope, "codex");
|
|
8021
|
+
return claudeRemoved || geminiRemoved || codexRemoved;
|
|
7902
8022
|
}
|
|
7903
8023
|
if (target === "codewith") {
|
|
7904
8024
|
return false;
|
|
@@ -7913,6 +8033,7 @@ function removeHookForTarget(name, scope, target) {
|
|
|
7913
8033
|
unregisterHook(name, scope, target);
|
|
7914
8034
|
return true;
|
|
7915
8035
|
}
|
|
8036
|
+
var JSON_SETTINGS_TARGETS = ["claude", "gemini", "codex"];
|
|
7916
8037
|
function removeCodewithHookEntry(configText, name) {
|
|
7917
8038
|
const sections = configText.split(/\n\s*\n/);
|
|
7918
8039
|
const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
|
|
@@ -7969,10 +8090,10 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
7969
8090
|
}
|
|
7970
8091
|
function codewithHasHookEntry(name, scope = "global") {
|
|
7971
8092
|
const path = getSettingsPath(scope, "codewith");
|
|
7972
|
-
if (!
|
|
8093
|
+
if (!existsSync8(path))
|
|
7973
8094
|
return false;
|
|
7974
8095
|
try {
|
|
7975
|
-
return codewithHasHookEntryInText(
|
|
8096
|
+
return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
|
|
7976
8097
|
} catch {
|
|
7977
8098
|
return false;
|
|
7978
8099
|
}
|
|
@@ -7986,7 +8107,7 @@ function removeStoreDirFailClosed(dir) {
|
|
|
7986
8107
|
}
|
|
7987
8108
|
for (const entry of entries) {
|
|
7988
8109
|
if (!entry.isDirectory()) {
|
|
7989
|
-
|
|
8110
|
+
unlinkSync2(join7(dir, entry.name));
|
|
7990
8111
|
}
|
|
7991
8112
|
}
|
|
7992
8113
|
rmdirSync(dir);
|
|
@@ -7996,14 +8117,14 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7996
8117
|
const custom2 = readCustomManifest(shortName);
|
|
7997
8118
|
const bundledMeta = custom2 ? undefined : getHook(shortName);
|
|
7998
8119
|
const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
|
|
7999
|
-
const registeredClaudeOrGemini = target === "all" ?
|
|
8120
|
+
const registeredClaudeOrGemini = target === "all" ? JSON_SETTINGS_TARGETS.some(existsInTarget) : false;
|
|
8000
8121
|
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
8001
8122
|
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
8002
8123
|
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
|
|
8003
8124
|
if (!custom2 && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
8004
8125
|
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
8005
8126
|
}
|
|
8006
|
-
const writableTargets = target === "all" ?
|
|
8127
|
+
const writableTargets = target === "all" ? JSON_SETTINGS_TARGETS : target === "codewith" ? [] : [target];
|
|
8007
8128
|
const scopes = [];
|
|
8008
8129
|
for (const s of ["global", "project"]) {
|
|
8009
8130
|
for (const t of writableTargets) {
|
|
@@ -8017,11 +8138,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
8017
8138
|
const registrationsRemaining = [];
|
|
8018
8139
|
if (target === "codewith" || target === "all") {
|
|
8019
8140
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
8020
|
-
if (
|
|
8021
|
-
const before =
|
|
8141
|
+
if (existsSync8(codewithPath)) {
|
|
8142
|
+
const before = readFileSync5(codewithPath, "utf-8");
|
|
8022
8143
|
const after = removeCodewithHookEntry(before, shortName);
|
|
8023
8144
|
if (after.removed) {
|
|
8024
|
-
|
|
8145
|
+
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
8025
8146
|
}
|
|
8026
8147
|
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
8027
8148
|
registrationsRemaining.push("codewith");
|
|
@@ -8060,7 +8181,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
8060
8181
|
// src/index.ts
|
|
8061
8182
|
init_profiles();
|
|
8062
8183
|
init_config();
|
|
8063
|
-
import { existsSync as
|
|
8184
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
8064
8185
|
|
|
8065
8186
|
// src/lib/transport.ts
|
|
8066
8187
|
init_transport();
|
|
@@ -8147,15 +8268,15 @@ init_manifest();
|
|
|
8147
8268
|
// src/lib/custom-install.ts
|
|
8148
8269
|
init_config();
|
|
8149
8270
|
init_manifest();
|
|
8150
|
-
import { existsSync as
|
|
8271
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2 } from "fs";
|
|
8151
8272
|
import { join as join10 } from "path";
|
|
8152
8273
|
import { tmpdir } from "os";
|
|
8153
8274
|
function manifestAt(dir) {
|
|
8154
8275
|
const path = join10(dir, "manifest.json");
|
|
8155
|
-
if (!
|
|
8276
|
+
if (!existsSync10(path))
|
|
8156
8277
|
return;
|
|
8157
8278
|
try {
|
|
8158
|
-
return parseManifest(
|
|
8279
|
+
return parseManifest(readFileSync8(path, "utf-8"));
|
|
8159
8280
|
} catch {
|
|
8160
8281
|
return;
|
|
8161
8282
|
}
|
|
@@ -8198,7 +8319,7 @@ function isCustomSource(value) {
|
|
|
8198
8319
|
return true;
|
|
8199
8320
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
8200
8321
|
return true;
|
|
8201
|
-
return
|
|
8322
|
+
return existsSync10(value);
|
|
8202
8323
|
}
|
|
8203
8324
|
async function installCustomSource(source) {
|
|
8204
8325
|
if (isGitUrl(source)) {
|
|
@@ -8212,11 +8333,11 @@ async function installCustomSource(source) {
|
|
|
8212
8333
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
8213
8334
|
return installManifestUrl(source);
|
|
8214
8335
|
}
|
|
8215
|
-
if (!
|
|
8336
|
+
if (!existsSync10(source)) {
|
|
8216
8337
|
throw new Error(`No such local path: ${source}`);
|
|
8217
8338
|
}
|
|
8218
8339
|
if (source.endsWith("manifest.json")) {
|
|
8219
|
-
const manifest = parseManifest(
|
|
8340
|
+
const manifest = parseManifest(readFileSync8(source, "utf-8"));
|
|
8220
8341
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
8221
8342
|
}
|
|
8222
8343
|
return copyManifestDir(source, source, "local");
|
|
@@ -8247,7 +8368,7 @@ async function installManifestUrl(url) {
|
|
|
8247
8368
|
}
|
|
8248
8369
|
// src/lib/sync.ts
|
|
8249
8370
|
import { createHash as createHash2 } from "crypto";
|
|
8250
|
-
import { readFileSync as
|
|
8371
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8251
8372
|
init_db();
|
|
8252
8373
|
init_store();
|
|
8253
8374
|
init_manifest();
|
|
@@ -8255,7 +8376,7 @@ function sha256OfText(text) {
|
|
|
8255
8376
|
return createHash2("sha256").update(text).digest("hex");
|
|
8256
8377
|
}
|
|
8257
8378
|
function sha256OfPath(path) {
|
|
8258
|
-
return createHash2("sha256").update(
|
|
8379
|
+
return createHash2("sha256").update(readFileSync9(path)).digest("hex");
|
|
8259
8380
|
}
|
|
8260
8381
|
function collectBundledCatalog() {
|
|
8261
8382
|
const out = [];
|
|
@@ -8549,8 +8670,8 @@ async function installPinnedFromBundled(name, version) {
|
|
|
8549
8670
|
// src/serve.ts
|
|
8550
8671
|
init_manifest();
|
|
8551
8672
|
init_store();
|
|
8552
|
-
import { readFileSync as
|
|
8553
|
-
import { dirname as
|
|
8673
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
8674
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
8554
8675
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8555
8676
|
|
|
8556
8677
|
// src/lib/secure-compare.ts
|
|
@@ -8724,8 +8845,8 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
8724
8845
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
8725
8846
|
function packageVersion() {
|
|
8726
8847
|
try {
|
|
8727
|
-
const pkgPath = join11(
|
|
8728
|
-
return JSON.parse(
|
|
8848
|
+
const pkgPath = join11(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
8849
|
+
return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
|
|
8729
8850
|
} catch {
|
|
8730
8851
|
return "0.0.0";
|
|
8731
8852
|
}
|
|
@@ -8991,11 +9112,11 @@ async function runHook(name, input, options = {}) {
|
|
|
8991
9112
|
throw new Error(`Hook '${name}' not found`);
|
|
8992
9113
|
const hookScript = getHookPath(name) + "/src/hook.ts";
|
|
8993
9114
|
const resolvedScript = custom2?.scriptPath;
|
|
8994
|
-
const script = resolvedScript ?? (
|
|
9115
|
+
const script = resolvedScript ?? (existsSync12(hookScript) ? hookScript : undefined);
|
|
8995
9116
|
if (!script)
|
|
8996
9117
|
throw new Error(`Hook script not found: ${name}`);
|
|
8997
9118
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
8998
|
-
const content =
|
|
9119
|
+
const content = readFileSync11(script);
|
|
8999
9120
|
const check = checkScriptHash2(name, sha256Of2(content));
|
|
9000
9121
|
if (!check.ok) {
|
|
9001
9122
|
throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
|