@hasna/hooks 0.9.1 → 0.9.3
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 +295 -98
- package/bin/index.js +430 -168
- package/bin/serve.js +11 -0
- package/dist/index.js +302 -104
- package/dist/lib/codex-settings.d.ts +3 -0
- package/dist/lib/installer.d.ts +5 -2
- package/dist/lib/mementos-options.d.ts +16 -0
- package/dist/lib/registration.d.ts +1 -0
- package/hooks/hook-mementos-context/README.md +106 -0
- package/hooks/hook-mementos-context/src/hook.ts +116 -0
- package/package.json +5 -4
package/bin/index.js
CHANGED
|
@@ -2246,6 +2246,17 @@ var init_registry = __esm(() => {
|
|
|
2246
2246
|
matcher: "",
|
|
2247
2247
|
tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
|
|
2248
2248
|
},
|
|
2249
|
+
{
|
|
2250
|
+
name: "mementos-context",
|
|
2251
|
+
displayName: "Mementos Prompt Context",
|
|
2252
|
+
description: "Retrieves scoped memories for a user prompt, with optional relevance judgments",
|
|
2253
|
+
version: "0.1.0",
|
|
2254
|
+
category: "Context Management",
|
|
2255
|
+
event: "UserPromptSubmit",
|
|
2256
|
+
matcher: "",
|
|
2257
|
+
timeoutSeconds: 12,
|
|
2258
|
+
tags: ["mementos", "memory", "prompt", "codex", "claude", "codewith", "opt-in"]
|
|
2259
|
+
},
|
|
2249
2260
|
{
|
|
2250
2261
|
name: "autoformat",
|
|
2251
2262
|
displayName: "Auto Format",
|
|
@@ -4489,10 +4500,84 @@ var init_resolve = __esm(() => {
|
|
|
4489
4500
|
init_manifest();
|
|
4490
4501
|
});
|
|
4491
4502
|
|
|
4503
|
+
// src/lib/mementos-options.ts
|
|
4504
|
+
function validateMementosRegistration(name, value) {
|
|
4505
|
+
if (value === undefined)
|
|
4506
|
+
return;
|
|
4507
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)) || !Object.hasOwn(value, "enabled") || Object.keys(value).some((key) => key !== "enabled" && key !== "secretRef") || name.replace(/^hook-/, "") !== "mementos-context" || typeof value.enabled !== "boolean") {
|
|
4508
|
+
throw new Error("Mementos options require the mementos-context hook and an explicit enable/disable choice.");
|
|
4509
|
+
}
|
|
4510
|
+
if (value.secretRef !== undefined && (!value.enabled || typeof value.secretRef !== "string" || value.secretRef.length > 256 || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*(?:\/[a-zA-Z0-9][a-zA-Z0-9_.-]*){1,12}$/.test(value.secretRef))) {
|
|
4511
|
+
throw new Error("OpenRouter requires an enabled Mementos hook and a valid Secrets reference, never a key value.");
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
function mementosRegistrationFromFlags(name, flags) {
|
|
4515
|
+
if (!flags.enableMementos && !flags.disableMementos && flags.openrouterSecretRef === undefined)
|
|
4516
|
+
return;
|
|
4517
|
+
if (Boolean(flags.enableMementos) === Boolean(flags.disableMementos)) {
|
|
4518
|
+
throw new Error("Choose exactly one of --enable-mementos or --disable-mementos.");
|
|
4519
|
+
}
|
|
4520
|
+
const value = { enabled: flags.enableMementos === true, ...flags.openrouterSecretRef !== undefined ? { secretRef: flags.openrouterSecretRef } : {} };
|
|
4521
|
+
validateMementosRegistration(name, value);
|
|
4522
|
+
return value;
|
|
4523
|
+
}
|
|
4524
|
+
function mementosCommandSuffix(value) {
|
|
4525
|
+
validateMementosRegistration("mementos-context", value);
|
|
4526
|
+
if (!value)
|
|
4527
|
+
return "";
|
|
4528
|
+
return value.enabled ? ` --enable-mementos${value.secretRef ? ` --openrouter-secret-ref ${value.secretRef}` : ""}` : " --disable-mementos";
|
|
4529
|
+
}
|
|
4530
|
+
function retainedMementosRegistration(commands) {
|
|
4531
|
+
const values = commands.filter((command) => /^hooks run mementos-context(?:\s|$)/.test(command)).map((command) => {
|
|
4532
|
+
const args = command.trim().split(/\s+/).slice(3);
|
|
4533
|
+
const flags = {};
|
|
4534
|
+
const seen = new Set;
|
|
4535
|
+
while (args.length) {
|
|
4536
|
+
const key = args.shift();
|
|
4537
|
+
if (seen.has(key))
|
|
4538
|
+
throw new Error("Ambiguous Mementos registration; specify its enablement explicitly.");
|
|
4539
|
+
seen.add(key);
|
|
4540
|
+
if (key === "--profile" && /^[\w-]+$/.test(args[0] ?? ""))
|
|
4541
|
+
args.shift();
|
|
4542
|
+
else if (key === "--enable-mementos")
|
|
4543
|
+
flags.enableMementos = true;
|
|
4544
|
+
else if (key === "--disable-mementos")
|
|
4545
|
+
flags.disableMementos = true;
|
|
4546
|
+
else if (key === "--openrouter-secret-ref" && args.length)
|
|
4547
|
+
flags.openrouterSecretRef = args.shift();
|
|
4548
|
+
else
|
|
4549
|
+
throw new Error("Unrecognized Mementos registration options; specify its enablement explicitly.");
|
|
4550
|
+
}
|
|
4551
|
+
return mementosRegistrationFromFlags("mementos-context", flags);
|
|
4552
|
+
});
|
|
4553
|
+
if (new Set(values.map((value) => JSON.stringify(value))).size > 1) {
|
|
4554
|
+
throw new Error("Conflicting Mementos registrations; specify its enablement explicitly.");
|
|
4555
|
+
}
|
|
4556
|
+
return values[0];
|
|
4557
|
+
}
|
|
4558
|
+
function mementosRunEnvironment(env, value) {
|
|
4559
|
+
validateMementosRegistration("mementos-context", value);
|
|
4560
|
+
if (!value)
|
|
4561
|
+
return env;
|
|
4562
|
+
return {
|
|
4563
|
+
...env,
|
|
4564
|
+
HOOKS_MEMENTOS_ENABLED: value.enabled && env.HOOKS_MEMENTOS_ENABLED !== "0" ? "1" : "0",
|
|
4565
|
+
...value.secretRef ? { HOOKS_MEMENTOS_OPENROUTER_SECRET_REF: value.secretRef } : {}
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4492
4569
|
// src/lib/registration.ts
|
|
4493
4570
|
function registrationName(command) {
|
|
4494
4571
|
if (typeof command !== "string")
|
|
4495
4572
|
return;
|
|
4573
|
+
if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
|
|
4574
|
+
try {
|
|
4575
|
+
retainedMementosRegistration([command.trim()]);
|
|
4576
|
+
return "mementos-context";
|
|
4577
|
+
} catch {
|
|
4578
|
+
return;
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4496
4581
|
const match = command.trim().match(REGISTRATION_COMMAND_RE);
|
|
4497
4582
|
return match ? match[1] ?? match[2] : undefined;
|
|
4498
4583
|
}
|
|
@@ -5232,6 +5317,78 @@ var init_store = __esm(() => {
|
|
|
5232
5317
|
};
|
|
5233
5318
|
});
|
|
5234
5319
|
|
|
5320
|
+
// src/lib/codex-settings.ts
|
|
5321
|
+
import { closeSync, existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5322
|
+
import { dirname as dirname3 } from "path";
|
|
5323
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5324
|
+
function current(path) {
|
|
5325
|
+
try {
|
|
5326
|
+
const stat = lstatSync(path);
|
|
5327
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
5328
|
+
throw new Error("invalid_file");
|
|
5329
|
+
return readFileSync4(path, "utf8");
|
|
5330
|
+
} catch (error) {
|
|
5331
|
+
if (error.code === "ENOENT")
|
|
5332
|
+
return null;
|
|
5333
|
+
throw new Error("Cannot safely read Codex hooks.json; no settings were changed.");
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
function readCodexSettings(path) {
|
|
5337
|
+
const raw = current(path);
|
|
5338
|
+
let settings;
|
|
5339
|
+
try {
|
|
5340
|
+
settings = raw === null ? {} : JSON.parse(raw);
|
|
5341
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
5342
|
+
throw new Error;
|
|
5343
|
+
if (settings.hooks !== undefined) {
|
|
5344
|
+
if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks))
|
|
5345
|
+
throw new Error;
|
|
5346
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
5347
|
+
if (!Array.isArray(entries) || entries.some((entry) => !entry || !Array.isArray(entry.hooks) || entry.hooks.some((hook) => !hook || typeof hook !== "object")))
|
|
5348
|
+
throw new Error;
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
} catch {
|
|
5352
|
+
throw new Error("Invalid Codex hooks.json; repair it before installing or removing hooks.");
|
|
5353
|
+
}
|
|
5354
|
+
snapshots.set(settings, { path, raw });
|
|
5355
|
+
return settings;
|
|
5356
|
+
}
|
|
5357
|
+
function writeCodexSettings(path, settings) {
|
|
5358
|
+
const snapshot = snapshots.get(settings);
|
|
5359
|
+
if (!snapshot || snapshot.path !== path)
|
|
5360
|
+
throw new Error("Codex settings require a complete current read before writing.");
|
|
5361
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
5362
|
+
const lock = `${path}.hooks-write.lock`;
|
|
5363
|
+
let fd;
|
|
5364
|
+
try {
|
|
5365
|
+
fd = openSync(lock, "wx", 384);
|
|
5366
|
+
} catch {
|
|
5367
|
+
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
5368
|
+
}
|
|
5369
|
+
const temp = `${path}.${randomUUID3()}.tmp`;
|
|
5370
|
+
try {
|
|
5371
|
+
if (current(path) !== snapshot.raw)
|
|
5372
|
+
throw new Error("Codex settings changed since reading; re-read before retrying.");
|
|
5373
|
+
const next = JSON.stringify(settings, null, 2) + `
|
|
5374
|
+
`;
|
|
5375
|
+
writeFileSync3(temp, next, { flag: "wx", mode: 384 });
|
|
5376
|
+
if (current(path) !== snapshot.raw)
|
|
5377
|
+
throw new Error("Codex settings changed since reading; re-read before retrying.");
|
|
5378
|
+
renameSync2(temp, path);
|
|
5379
|
+
snapshots.set(settings, { path, raw: next });
|
|
5380
|
+
} finally {
|
|
5381
|
+
if (existsSync10(temp))
|
|
5382
|
+
unlinkSync(temp);
|
|
5383
|
+
closeSync(fd);
|
|
5384
|
+
unlinkSync(lock);
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
var snapshots;
|
|
5388
|
+
var init_codex_settings = __esm(() => {
|
|
5389
|
+
snapshots = new WeakMap;
|
|
5390
|
+
});
|
|
5391
|
+
|
|
5235
5392
|
// src/lib/installer.ts
|
|
5236
5393
|
var exports_installer = {};
|
|
5237
5394
|
__export(exports_installer, {
|
|
@@ -5252,8 +5409,8 @@ __export(exports_installer, {
|
|
|
5252
5409
|
detectRewriteConflict: () => detectRewriteConflict,
|
|
5253
5410
|
buildCodewithTomlFragment: () => buildCodewithTomlFragment
|
|
5254
5411
|
});
|
|
5255
|
-
import { existsSync as
|
|
5256
|
-
import { join as join10, dirname as
|
|
5412
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
5413
|
+
import { join as join10, dirname as dirname4 } from "path";
|
|
5257
5414
|
import { homedir as homedir4 } from "os";
|
|
5258
5415
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5259
5416
|
function normalizeHookName(name) {
|
|
@@ -5263,12 +5420,19 @@ function shortHookName(name) {
|
|
|
5263
5420
|
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
5264
5421
|
}
|
|
5265
5422
|
function removeHookEntriesByName(entries, hookName) {
|
|
5266
|
-
return entries.
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5423
|
+
return entries.flatMap((entry) => {
|
|
5424
|
+
if (!Array.isArray(entry.hooks))
|
|
5425
|
+
return [entry];
|
|
5426
|
+
const kept = entry.hooks.filter((h) => {
|
|
5427
|
+
const match = h.command?.match(/^hooks run ([\w-]+)/);
|
|
5428
|
+
return !(match && match[1] === hookName);
|
|
5429
|
+
});
|
|
5430
|
+
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
5431
|
+
});
|
|
5270
5432
|
}
|
|
5271
5433
|
function getTargetSettingsDir(target) {
|
|
5434
|
+
if (target === "codex")
|
|
5435
|
+
return ".codex";
|
|
5272
5436
|
if (target === "codewith")
|
|
5273
5437
|
return ".codewith";
|
|
5274
5438
|
if (target === "gemini")
|
|
@@ -5276,6 +5440,8 @@ function getTargetSettingsDir(target) {
|
|
|
5276
5440
|
return ".claude";
|
|
5277
5441
|
}
|
|
5278
5442
|
function getGlobalSettingsPathOverride(target) {
|
|
5443
|
+
if (target === "codex")
|
|
5444
|
+
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
|
|
5279
5445
|
if (target === "claude")
|
|
5280
5446
|
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
5281
5447
|
if (target === "gemini")
|
|
@@ -5292,6 +5458,8 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
5292
5458
|
if (globalOverride)
|
|
5293
5459
|
return globalOverride;
|
|
5294
5460
|
const dir = getTargetSettingsDir(target);
|
|
5461
|
+
if (target === "codex")
|
|
5462
|
+
return join10(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
|
|
5295
5463
|
if (scope === "project") {
|
|
5296
5464
|
return target === "codewith" ? join10(process.cwd(), dir, "config.toml") : join10(process.cwd(), dir, "settings.json");
|
|
5297
5465
|
}
|
|
@@ -5305,13 +5473,15 @@ function getHookPath(name) {
|
|
|
5305
5473
|
return join10(HOOKS_DIR, normalizeHookName(shortName));
|
|
5306
5474
|
}
|
|
5307
5475
|
function hookExists(name) {
|
|
5308
|
-
return
|
|
5476
|
+
return existsSync11(getHookPath(name));
|
|
5309
5477
|
}
|
|
5310
5478
|
function readSettings(scope = "global", target = "claude") {
|
|
5311
5479
|
const path = getSettingsPath(scope, target);
|
|
5480
|
+
if (target === "codex")
|
|
5481
|
+
return readCodexSettings(path);
|
|
5312
5482
|
try {
|
|
5313
|
-
if (
|
|
5314
|
-
return JSON.parse(
|
|
5483
|
+
if (existsSync11(path)) {
|
|
5484
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
5315
5485
|
}
|
|
5316
5486
|
} catch (error) {
|
|
5317
5487
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5320,11 +5490,13 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
5320
5490
|
}
|
|
5321
5491
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
5322
5492
|
const path = getSettingsPath(scope, target);
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5493
|
+
if (target === "codex")
|
|
5494
|
+
return writeCodexSettings(path, settings);
|
|
5495
|
+
const dir = dirname4(path);
|
|
5496
|
+
if (!existsSync11(dir)) {
|
|
5497
|
+
mkdirSync5(dir, { recursive: true });
|
|
5326
5498
|
}
|
|
5327
|
-
|
|
5499
|
+
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
5328
5500
|
`);
|
|
5329
5501
|
}
|
|
5330
5502
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -5347,6 +5519,8 @@ function codewithMatcher(matcher) {
|
|
|
5347
5519
|
}
|
|
5348
5520
|
function codewithTimeout(name) {
|
|
5349
5521
|
switch (name) {
|
|
5522
|
+
case "mementos-context":
|
|
5523
|
+
return 12;
|
|
5350
5524
|
case "knowledge-context":
|
|
5351
5525
|
return 6;
|
|
5352
5526
|
case "session-start":
|
|
@@ -5383,12 +5557,13 @@ function codewithStatusMessage(name) {
|
|
|
5383
5557
|
return `Running ${name}`;
|
|
5384
5558
|
}
|
|
5385
5559
|
}
|
|
5386
|
-
function buildCodewithTomlFragment(name, profile) {
|
|
5560
|
+
function buildCodewithTomlFragment(name, profile, mementos) {
|
|
5387
5561
|
const shortName = shortHookName(name);
|
|
5388
5562
|
const meta = resolveHookMeta(shortName);
|
|
5389
5563
|
if (!meta)
|
|
5390
5564
|
throw new Error(`Hook '${shortName}' not found`);
|
|
5391
|
-
|
|
5565
|
+
validateMementosRegistration(shortName, mementos);
|
|
5566
|
+
const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
5392
5567
|
const matcher = codewithMatcher(meta.matcher);
|
|
5393
5568
|
const fragments = [];
|
|
5394
5569
|
for (const event of getHookEvents(meta)) {
|
|
@@ -5413,21 +5588,21 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
5413
5588
|
function readCodewithConfig(scope, configPath) {
|
|
5414
5589
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5415
5590
|
try {
|
|
5416
|
-
return
|
|
5591
|
+
return existsSync11(path) ? readFileSync5(path, "utf-8") : "";
|
|
5417
5592
|
} catch {
|
|
5418
5593
|
return "";
|
|
5419
5594
|
}
|
|
5420
5595
|
}
|
|
5421
5596
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
5422
5597
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5423
|
-
const dir =
|
|
5424
|
-
if (!
|
|
5425
|
-
|
|
5426
|
-
const existing =
|
|
5598
|
+
const dir = dirname4(path);
|
|
5599
|
+
if (!existsSync11(dir))
|
|
5600
|
+
mkdirSync5(dir, { recursive: true });
|
|
5601
|
+
const existing = existsSync11(path) ? readFileSync5(path, "utf-8") : "";
|
|
5427
5602
|
const sep2 = existing.trim() ? `
|
|
5428
5603
|
|
|
5429
5604
|
` : "";
|
|
5430
|
-
|
|
5605
|
+
writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
5431
5606
|
return path;
|
|
5432
5607
|
}
|
|
5433
5608
|
function detectConflict(name, scope, target) {
|
|
@@ -5470,14 +5645,14 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
5470
5645
|
}
|
|
5471
5646
|
return;
|
|
5472
5647
|
}
|
|
5473
|
-
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath) {
|
|
5648
|
+
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
5474
5649
|
const shortName = shortHookName(name);
|
|
5475
5650
|
if (!hookExists(shortName)) {
|
|
5476
5651
|
return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
|
|
5477
5652
|
}
|
|
5478
5653
|
if (target === "codewith") {
|
|
5479
5654
|
try {
|
|
5480
|
-
const fragment = buildCodewithTomlFragment(shortName, profile);
|
|
5655
|
+
const fragment = buildCodewithTomlFragment(shortName, profile, mementos);
|
|
5481
5656
|
if (codewithMode !== "write") {
|
|
5482
5657
|
return {
|
|
5483
5658
|
hook: shortName,
|
|
@@ -5524,18 +5699,25 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
5524
5699
|
};
|
|
5525
5700
|
}
|
|
5526
5701
|
}
|
|
5527
|
-
const registered = getRegisteredHooksForTarget(scope, target);
|
|
5528
|
-
if (registered.includes(shortName) && !overwrite) {
|
|
5529
|
-
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
5530
|
-
}
|
|
5531
|
-
const conflict = detectConflict(shortName, scope, target);
|
|
5532
|
-
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
5533
|
-
if (rewriteConflict) {
|
|
5534
|
-
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
5535
|
-
}
|
|
5536
5702
|
try {
|
|
5537
|
-
|
|
5538
|
-
|
|
5703
|
+
const registered = getRegisteredHooksForTarget(scope, target);
|
|
5704
|
+
if (registered.includes(shortName) && !overwrite) {
|
|
5705
|
+
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
5706
|
+
}
|
|
5707
|
+
const conflict = detectConflict(shortName, scope, target);
|
|
5708
|
+
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
5709
|
+
if (rewriteConflict) {
|
|
5710
|
+
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
5711
|
+
}
|
|
5712
|
+
registerHook(shortName, scope, target, profile, mementos);
|
|
5713
|
+
return {
|
|
5714
|
+
hook: shortName,
|
|
5715
|
+
success: true,
|
|
5716
|
+
scope,
|
|
5717
|
+
target,
|
|
5718
|
+
...conflict ? { conflict } : {},
|
|
5719
|
+
...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." } : {}
|
|
5720
|
+
};
|
|
5539
5721
|
} catch (error) {
|
|
5540
5722
|
return {
|
|
5541
5723
|
hook: shortName,
|
|
@@ -5546,7 +5728,15 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
5546
5728
|
}
|
|
5547
5729
|
}
|
|
5548
5730
|
function installHook(name, options = {}) {
|
|
5549
|
-
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
|
|
5731
|
+
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
|
|
5732
|
+
try {
|
|
5733
|
+
validateMementosRegistration(name, mementos);
|
|
5734
|
+
} catch (error) {
|
|
5735
|
+
return { hook: name, success: false, target, error: error instanceof Error ? error.message : "Invalid Mementos options" };
|
|
5736
|
+
}
|
|
5737
|
+
if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
|
|
5738
|
+
return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
|
|
5739
|
+
}
|
|
5550
5740
|
if (target === "all") {
|
|
5551
5741
|
const shortName = shortHookName(name);
|
|
5552
5742
|
const meta = resolveHookMeta(shortName);
|
|
@@ -5561,7 +5751,7 @@ function installHook(name, options = {}) {
|
|
|
5561
5751
|
};
|
|
5562
5752
|
}
|
|
5563
5753
|
}
|
|
5564
|
-
const claudeResult = installForTarget(name, scope, overwrite, "claude", profile);
|
|
5754
|
+
const claudeResult = installForTarget(name, scope, overwrite, "claude", profile, "fragment", undefined, mementos);
|
|
5565
5755
|
if (!claudeResult.success) {
|
|
5566
5756
|
return {
|
|
5567
5757
|
...claudeResult,
|
|
@@ -5569,7 +5759,7 @@ function installHook(name, options = {}) {
|
|
|
5569
5759
|
target: "all"
|
|
5570
5760
|
};
|
|
5571
5761
|
}
|
|
5572
|
-
const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath);
|
|
5762
|
+
const codewithResult = installForTarget(name, scope, overwrite, "codewith", profile, codewithMode, codewithConfigPath, mementos);
|
|
5573
5763
|
if (!codewithResult.success) {
|
|
5574
5764
|
return {
|
|
5575
5765
|
...codewithResult,
|
|
@@ -5579,9 +5769,9 @@ function installHook(name, options = {}) {
|
|
|
5579
5769
|
}
|
|
5580
5770
|
return { ...claudeResult, target: "all" };
|
|
5581
5771
|
}
|
|
5582
|
-
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
|
|
5772
|
+
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
5583
5773
|
}
|
|
5584
|
-
function registerHook(name, scope = "global", target = "claude", profile) {
|
|
5774
|
+
function registerHook(name, scope = "global", target = "claude", profile, mementos) {
|
|
5585
5775
|
const meta = resolveHookMeta(name);
|
|
5586
5776
|
if (!meta)
|
|
5587
5777
|
return;
|
|
@@ -5599,8 +5789,9 @@ function registerHook(name, scope = "global", target = "claude", profile) {
|
|
|
5599
5789
|
const settings = readSettings(scope, target);
|
|
5600
5790
|
if (!settings.hooks)
|
|
5601
5791
|
settings.hooks = {};
|
|
5792
|
+
const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
|
|
5602
5793
|
removeHookFromAllEvents(settings, name);
|
|
5603
|
-
const hookCommand = profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}
|
|
5794
|
+
const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
5604
5795
|
for (const eventKey of uniqueEventKeys) {
|
|
5605
5796
|
if (!settings.hooks[eventKey])
|
|
5606
5797
|
settings.hooks[eventKey] = [];
|
|
@@ -5645,10 +5836,12 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
|
5645
5836
|
if (target === "codewith") {
|
|
5646
5837
|
const config = readCodewithConfig(scope);
|
|
5647
5838
|
const registered2 = [];
|
|
5648
|
-
const re =
|
|
5839
|
+
const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
|
|
5649
5840
|
let match;
|
|
5650
5841
|
while (match = re.exec(config)) {
|
|
5651
|
-
|
|
5842
|
+
const name = registrationName(match[1]);
|
|
5843
|
+
if (name)
|
|
5844
|
+
registered2.push(name);
|
|
5652
5845
|
}
|
|
5653
5846
|
return [...new Set(registered2)];
|
|
5654
5847
|
}
|
|
@@ -5659,12 +5852,9 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
|
5659
5852
|
for (const eventKey of Object.keys(settings.hooks)) {
|
|
5660
5853
|
for (const entry of settings.hooks[eventKey]) {
|
|
5661
5854
|
for (const hook of entry.hooks || []) {
|
|
5662
|
-
const
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
if (match) {
|
|
5666
|
-
registered.push(match[1]);
|
|
5667
|
-
}
|
|
5855
|
+
const name = registrationName(hook.command);
|
|
5856
|
+
if (name)
|
|
5857
|
+
registered.push(name);
|
|
5668
5858
|
}
|
|
5669
5859
|
}
|
|
5670
5860
|
}
|
|
@@ -5678,7 +5868,8 @@ function removeHook(name, scope = "global", target = "claude") {
|
|
|
5678
5868
|
if (target === "all") {
|
|
5679
5869
|
const claudeRemoved = removeHookForTarget(shortName, scope, "claude");
|
|
5680
5870
|
const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
|
|
5681
|
-
|
|
5871
|
+
const codexRemoved = removeHookForTarget(shortName, scope, "codex");
|
|
5872
|
+
return claudeRemoved || geminiRemoved || codexRemoved;
|
|
5682
5873
|
}
|
|
5683
5874
|
if (target === "codewith") {
|
|
5684
5875
|
return false;
|
|
@@ -5695,9 +5886,9 @@ function removeHookForTarget(name, scope, target) {
|
|
|
5695
5886
|
}
|
|
5696
5887
|
function readSettingsAt(path) {
|
|
5697
5888
|
try {
|
|
5698
|
-
if (!
|
|
5889
|
+
if (!existsSync11(path))
|
|
5699
5890
|
return;
|
|
5700
|
-
const raw =
|
|
5891
|
+
const raw = readFileSync5(path, "utf-8");
|
|
5701
5892
|
return { raw, settings: JSON.parse(raw) };
|
|
5702
5893
|
} catch {
|
|
5703
5894
|
return;
|
|
@@ -5730,7 +5921,9 @@ function pruneStaleRegistrations(scope = "global") {
|
|
|
5730
5921
|
commands.add(finding.command);
|
|
5731
5922
|
staleCommandsByEvent.set(finding.event, commands);
|
|
5732
5923
|
}
|
|
5733
|
-
const settings = parsed.settings;
|
|
5924
|
+
const settings = target === "codex" ? readCodexSettings(path) : parsed.settings;
|
|
5925
|
+
if (target === "codex" && readFileSync5(path, "utf8") !== parsed.raw)
|
|
5926
|
+
throw new Error("Codex settings changed before pruning; retry.");
|
|
5734
5927
|
for (const [eventKey, commands] of staleCommandsByEvent) {
|
|
5735
5928
|
const entries = settings.hooks?.[eventKey];
|
|
5736
5929
|
if (!Array.isArray(entries))
|
|
@@ -5755,7 +5948,7 @@ function pruneStaleRegistrations(scope = "global") {
|
|
|
5755
5948
|
if (settings.hooks && Object.keys(settings.hooks).length === 0)
|
|
5756
5949
|
delete settings.hooks;
|
|
5757
5950
|
const backupPath = `${path}.bak`;
|
|
5758
|
-
|
|
5951
|
+
writeFileSync4(backupPath, parsed.raw);
|
|
5759
5952
|
writeSettings(settings, scope, target);
|
|
5760
5953
|
results.push({ file: path, removed: stale, backupPath });
|
|
5761
5954
|
}
|
|
@@ -5763,7 +5956,7 @@ function pruneStaleRegistrations(scope = "global") {
|
|
|
5763
5956
|
}
|
|
5764
5957
|
function removeCodewithHookEntry(configText, name) {
|
|
5765
5958
|
const sections = configText.split(/\n\s*\n/);
|
|
5766
|
-
const
|
|
5959
|
+
const commandRe = /^command\s*=\s*"([^"\n]+)"$/;
|
|
5767
5960
|
const entryHeaderRe = /^\[\[hooks\.([\w]+)\.hooks\]\]$/;
|
|
5768
5961
|
const eventHeaderRe = /^\[\[hooks\.([\w]+)\]\]$/;
|
|
5769
5962
|
const eventsWithEntries = new Set;
|
|
@@ -5776,7 +5969,7 @@ function removeCodewithHookEntry(configText, name) {
|
|
|
5776
5969
|
if (entryMatch) {
|
|
5777
5970
|
const event = entryMatch[1];
|
|
5778
5971
|
eventsWithEntries.add(event);
|
|
5779
|
-
if (lines.some((line) =>
|
|
5972
|
+
if (lines.some((line) => registrationName(commandRe.exec(line.trim())?.[1]) === name)) {
|
|
5780
5973
|
removed = true;
|
|
5781
5974
|
continue;
|
|
5782
5975
|
}
|
|
@@ -5817,10 +6010,10 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
5817
6010
|
}
|
|
5818
6011
|
function codewithHasHookEntry(name, scope = "global") {
|
|
5819
6012
|
const path = getSettingsPath(scope, "codewith");
|
|
5820
|
-
if (!
|
|
6013
|
+
if (!existsSync11(path))
|
|
5821
6014
|
return false;
|
|
5822
6015
|
try {
|
|
5823
|
-
return codewithHasHookEntryInText(
|
|
6016
|
+
return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
|
|
5824
6017
|
} catch {
|
|
5825
6018
|
return false;
|
|
5826
6019
|
}
|
|
@@ -5834,7 +6027,7 @@ function removeStoreDirFailClosed(dir) {
|
|
|
5834
6027
|
}
|
|
5835
6028
|
for (const entry of entries) {
|
|
5836
6029
|
if (!entry.isDirectory()) {
|
|
5837
|
-
|
|
6030
|
+
unlinkSync2(join10(dir, entry.name));
|
|
5838
6031
|
}
|
|
5839
6032
|
}
|
|
5840
6033
|
rmdirSync(dir);
|
|
@@ -5844,14 +6037,14 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5844
6037
|
const custom = readCustomManifest(shortName);
|
|
5845
6038
|
const bundledMeta = custom ? undefined : getHook(shortName);
|
|
5846
6039
|
const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
|
|
5847
|
-
const registeredClaudeOrGemini = target === "all" ?
|
|
6040
|
+
const registeredClaudeOrGemini = target === "all" ? JSON_SETTINGS_TARGETS.some(existsInTarget) : false;
|
|
5848
6041
|
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
5849
6042
|
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
5850
6043
|
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
|
|
5851
6044
|
if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
5852
6045
|
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
5853
6046
|
}
|
|
5854
|
-
const writableTargets = target === "all" ?
|
|
6047
|
+
const writableTargets = target === "all" ? JSON_SETTINGS_TARGETS : target === "codewith" ? [] : [target];
|
|
5855
6048
|
const scopes = [];
|
|
5856
6049
|
for (const s of ["global", "project"]) {
|
|
5857
6050
|
for (const t of writableTargets) {
|
|
@@ -5865,11 +6058,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5865
6058
|
const registrationsRemaining = [];
|
|
5866
6059
|
if (target === "codewith" || target === "all") {
|
|
5867
6060
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
5868
|
-
if (
|
|
5869
|
-
const before =
|
|
6061
|
+
if (existsSync11(codewithPath)) {
|
|
6062
|
+
const before = readFileSync5(codewithPath, "utf-8");
|
|
5870
6063
|
const after = removeCodewithHookEntry(before, shortName);
|
|
5871
6064
|
if (after.removed) {
|
|
5872
|
-
|
|
6065
|
+
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
5873
6066
|
}
|
|
5874
6067
|
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
5875
6068
|
registrationsRemaining.push("codewith");
|
|
@@ -5912,8 +6105,9 @@ var init_installer = __esm(() => {
|
|
|
5912
6105
|
init_manifest();
|
|
5913
6106
|
init_registration();
|
|
5914
6107
|
init_store();
|
|
5915
|
-
|
|
5916
|
-
|
|
6108
|
+
init_codex_settings();
|
|
6109
|
+
__dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
6110
|
+
HOOKS_DIR = existsSync11(join10(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join10(__dirname2, "..", "..", "hooks") : join10(__dirname2, "..", "hooks");
|
|
5917
6111
|
EVENT_MAP = {
|
|
5918
6112
|
claude: {
|
|
5919
6113
|
PreToolUse: "PreToolUse",
|
|
@@ -5922,7 +6116,7 @@ var init_installer = __esm(() => {
|
|
|
5922
6116
|
Notification: "Notification",
|
|
5923
6117
|
SessionStart: "SessionStart",
|
|
5924
6118
|
SessionEnd: "SessionEnd",
|
|
5925
|
-
UserPromptSubmit:
|
|
6119
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
5926
6120
|
SubagentStart: null
|
|
5927
6121
|
},
|
|
5928
6122
|
gemini: {
|
|
@@ -5944,10 +6138,20 @@ var init_installer = __esm(() => {
|
|
|
5944
6138
|
SessionEnd: null,
|
|
5945
6139
|
UserPromptSubmit: "UserPromptSubmit",
|
|
5946
6140
|
SubagentStart: "SubagentStart"
|
|
6141
|
+
},
|
|
6142
|
+
codex: {
|
|
6143
|
+
PreToolUse: "PreToolUse",
|
|
6144
|
+
PostToolUse: "PostToolUse",
|
|
6145
|
+
Stop: "Stop",
|
|
6146
|
+
Notification: null,
|
|
6147
|
+
SessionStart: "SessionStart",
|
|
6148
|
+
SessionEnd: "SessionEnd",
|
|
6149
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
6150
|
+
SubagentStart: "SubagentStart"
|
|
5947
6151
|
}
|
|
5948
6152
|
};
|
|
5949
6153
|
getInstalledHooks = getRegisteredHooks;
|
|
5950
|
-
JSON_SETTINGS_TARGETS = ["claude", "gemini"];
|
|
6154
|
+
JSON_SETTINGS_TARGETS = ["claude", "gemini", "codex"];
|
|
5951
6155
|
});
|
|
5952
6156
|
|
|
5953
6157
|
// src/lib/redact.ts
|
|
@@ -6016,21 +6220,21 @@ var init_redact = __esm(() => {
|
|
|
6016
6220
|
});
|
|
6017
6221
|
|
|
6018
6222
|
// src/lib/profiles.ts
|
|
6019
|
-
import { existsSync as
|
|
6223
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
6020
6224
|
import { join as join11 } from "path";
|
|
6021
6225
|
function getProfilesDir() {
|
|
6022
6226
|
const newDir = join11(getEffectiveDataRoot(), "profiles");
|
|
6023
6227
|
const oldDir = join11(getHomeDir(), ".hooks", "profiles");
|
|
6024
|
-
if (!
|
|
6025
|
-
|
|
6228
|
+
if (!existsSync12(newDir) && existsSync12(oldDir)) {
|
|
6229
|
+
mkdirSync6(newDir, { recursive: true });
|
|
6026
6230
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
6027
6231
|
}
|
|
6028
6232
|
return newDir;
|
|
6029
6233
|
}
|
|
6030
6234
|
function ensureProfilesDir() {
|
|
6031
6235
|
const dir = getProfilesDir();
|
|
6032
|
-
if (!
|
|
6033
|
-
|
|
6236
|
+
if (!existsSync12(dir)) {
|
|
6237
|
+
mkdirSync6(dir, { recursive: true });
|
|
6034
6238
|
}
|
|
6035
6239
|
}
|
|
6036
6240
|
function profilePath(id) {
|
|
@@ -6053,30 +6257,30 @@ function createProfile(input) {
|
|
|
6053
6257
|
if (input.name) {
|
|
6054
6258
|
profile.name = input.name;
|
|
6055
6259
|
}
|
|
6056
|
-
|
|
6260
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
6057
6261
|
`);
|
|
6058
6262
|
return profile;
|
|
6059
6263
|
}
|
|
6060
6264
|
function getProfile(id) {
|
|
6061
6265
|
const path = profilePath(id);
|
|
6062
6266
|
try {
|
|
6063
|
-
if (!
|
|
6267
|
+
if (!existsSync12(path))
|
|
6064
6268
|
return null;
|
|
6065
|
-
return JSON.parse(
|
|
6269
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
6066
6270
|
} catch {
|
|
6067
6271
|
return null;
|
|
6068
6272
|
}
|
|
6069
6273
|
}
|
|
6070
6274
|
function listProfiles() {
|
|
6071
6275
|
const dir = getProfilesDir();
|
|
6072
|
-
if (!
|
|
6276
|
+
if (!existsSync12(dir))
|
|
6073
6277
|
return [];
|
|
6074
6278
|
try {
|
|
6075
6279
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
6076
6280
|
const profiles = [];
|
|
6077
6281
|
for (const file of files) {
|
|
6078
6282
|
try {
|
|
6079
|
-
const content =
|
|
6283
|
+
const content = readFileSync6(join11(dir, file), "utf-8");
|
|
6080
6284
|
profiles.push(JSON.parse(content));
|
|
6081
6285
|
} catch {}
|
|
6082
6286
|
}
|
|
@@ -6090,7 +6294,7 @@ function touchProfile(id) {
|
|
|
6090
6294
|
if (!profile)
|
|
6091
6295
|
return;
|
|
6092
6296
|
profile.last_seen_at = new Date().toISOString();
|
|
6093
|
-
|
|
6297
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
6094
6298
|
`);
|
|
6095
6299
|
}
|
|
6096
6300
|
function exportProfiles() {
|
|
@@ -6106,11 +6310,11 @@ function importProfiles(profiles) {
|
|
|
6106
6310
|
continue;
|
|
6107
6311
|
}
|
|
6108
6312
|
const path = profilePath(profile.agent_id);
|
|
6109
|
-
if (
|
|
6313
|
+
if (existsSync12(path)) {
|
|
6110
6314
|
skipped++;
|
|
6111
6315
|
continue;
|
|
6112
6316
|
}
|
|
6113
|
-
|
|
6317
|
+
writeFileSync5(path, JSON.stringify(profile, null, 2) + `
|
|
6114
6318
|
`);
|
|
6115
6319
|
imported++;
|
|
6116
6320
|
}
|
|
@@ -6123,7 +6327,7 @@ var init_profiles = __esm(() => {
|
|
|
6123
6327
|
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
|
|
6124
6328
|
import { isIP as isIP2 } from "net";
|
|
6125
6329
|
import { spawnSync } from "child_process";
|
|
6126
|
-
import { closeSync, fstatSync, openSync, readFileSync as
|
|
6330
|
+
import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
|
|
6127
6331
|
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
6128
6332
|
import { createRequire as createRequire2 } from "module";
|
|
6129
6333
|
import { hostname as osHostname } from "os";
|
|
@@ -6227,7 +6431,7 @@ function readAppConfigFile(path) {
|
|
|
6227
6431
|
};
|
|
6228
6432
|
let fd = -1;
|
|
6229
6433
|
try {
|
|
6230
|
-
fd =
|
|
6434
|
+
fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
6231
6435
|
} catch (error) {
|
|
6232
6436
|
const code = error.code;
|
|
6233
6437
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -6248,7 +6452,7 @@ function readAppConfigFile(path) {
|
|
|
6248
6452
|
unsafe("the file is not owned by the current user");
|
|
6249
6453
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
|
|
6250
6454
|
unsafe("the file exceeds the size limit");
|
|
6251
|
-
const bytes =
|
|
6455
|
+
const bytes = readFileSync7(fd);
|
|
6252
6456
|
const after = fstatSync(fd);
|
|
6253
6457
|
if (!configFileReadsCoherent(before, after)) {
|
|
6254
6458
|
unsafe("the file changed while being read");
|
|
@@ -6256,7 +6460,7 @@ function readAppConfigFile(path) {
|
|
|
6256
6460
|
return parseEnvFile(bytes.toString("utf8"));
|
|
6257
6461
|
} finally {
|
|
6258
6462
|
if (fd !== -1)
|
|
6259
|
-
|
|
6463
|
+
closeSync2(fd);
|
|
6260
6464
|
}
|
|
6261
6465
|
}
|
|
6262
6466
|
function readCredentialFile(path, apiKeyKeys) {
|
|
@@ -7124,8 +7328,8 @@ var init_db_writer = __esm(() => {
|
|
|
7124
7328
|
});
|
|
7125
7329
|
|
|
7126
7330
|
// src/lib/hook-env.ts
|
|
7127
|
-
import { dirname as
|
|
7128
|
-
import { existsSync as
|
|
7331
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
|
|
7332
|
+
import { existsSync as existsSync13, realpathSync as realpathSync2, statSync } from "fs";
|
|
7129
7333
|
import { tmpdir } from "os";
|
|
7130
7334
|
function isDeniedEnvName(name) {
|
|
7131
7335
|
const upper = name.toUpperCase();
|
|
@@ -7173,9 +7377,9 @@ function isUnsafePathEntry(entry, home) {
|
|
|
7173
7377
|
}
|
|
7174
7378
|
function sanitizeHookPath(pathValue, home) {
|
|
7175
7379
|
const baseline = PATH_BASELINE.slice();
|
|
7176
|
-
if (process.platform === "darwin" &&
|
|
7380
|
+
if (process.platform === "darwin" && existsSync13("/opt/homebrew/bin"))
|
|
7177
7381
|
baseline.push("/opt/homebrew/bin");
|
|
7178
|
-
const bunDir =
|
|
7382
|
+
const bunDir = dirname5(process.execPath);
|
|
7179
7383
|
const seen = new Set;
|
|
7180
7384
|
const out = [];
|
|
7181
7385
|
for (const dir of baseline) {
|
|
@@ -7299,8 +7503,8 @@ __export(exports_run, {
|
|
|
7299
7503
|
HookTimeoutError: () => HookTimeoutError
|
|
7300
7504
|
});
|
|
7301
7505
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
7302
|
-
import { rmSync as rmSync2, writeFileSync as
|
|
7303
|
-
import { basename as basename2, dirname as
|
|
7506
|
+
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
7507
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join13 } from "path";
|
|
7304
7508
|
function extensionOf(scriptPath) {
|
|
7305
7509
|
const base = basename2(scriptPath);
|
|
7306
7510
|
const dot = base.lastIndexOf(".");
|
|
@@ -7369,7 +7573,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
7369
7573
|
return chunks.join("");
|
|
7370
7574
|
}
|
|
7371
7575
|
async function executeVerifiedScript(options) {
|
|
7372
|
-
const scriptDir =
|
|
7576
|
+
const scriptDir = dirname6(options.scriptPath);
|
|
7373
7577
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
7374
7578
|
const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
7375
7579
|
let proc = null;
|
|
@@ -7379,7 +7583,7 @@ async function executeVerifiedScript(options) {
|
|
|
7379
7583
|
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
7380
7584
|
}, options.timeout) : null;
|
|
7381
7585
|
try {
|
|
7382
|
-
|
|
7586
|
+
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
7383
7587
|
proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
7384
7588
|
stdin: new Response(options.stdin),
|
|
7385
7589
|
stdout: "pipe",
|
|
@@ -7462,15 +7666,15 @@ __export(exports_custom_install, {
|
|
|
7462
7666
|
installCustomSource: () => installCustomSource,
|
|
7463
7667
|
ensureCustomDir: () => ensureCustomDir
|
|
7464
7668
|
});
|
|
7465
|
-
import { existsSync as
|
|
7669
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
|
|
7466
7670
|
import { join as join14 } from "path";
|
|
7467
7671
|
import { tmpdir as tmpdir2 } from "os";
|
|
7468
7672
|
function manifestAt(dir) {
|
|
7469
7673
|
const path = join14(dir, "manifest.json");
|
|
7470
|
-
if (!
|
|
7674
|
+
if (!existsSync14(path))
|
|
7471
7675
|
return;
|
|
7472
7676
|
try {
|
|
7473
|
-
return parseManifest(
|
|
7677
|
+
return parseManifest(readFileSync8(path, "utf-8"));
|
|
7474
7678
|
} catch {
|
|
7475
7679
|
return;
|
|
7476
7680
|
}
|
|
@@ -7513,7 +7717,7 @@ function isCustomSource(value) {
|
|
|
7513
7717
|
return true;
|
|
7514
7718
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
7515
7719
|
return true;
|
|
7516
|
-
return
|
|
7720
|
+
return existsSync14(value);
|
|
7517
7721
|
}
|
|
7518
7722
|
async function installCustomSource(source) {
|
|
7519
7723
|
if (isGitUrl(source)) {
|
|
@@ -7527,11 +7731,11 @@ async function installCustomSource(source) {
|
|
|
7527
7731
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
7528
7732
|
return installManifestUrl(source);
|
|
7529
7733
|
}
|
|
7530
|
-
if (!
|
|
7734
|
+
if (!existsSync14(source)) {
|
|
7531
7735
|
throw new Error(`No such local path: ${source}`);
|
|
7532
7736
|
}
|
|
7533
7737
|
if (source.endsWith("manifest.json")) {
|
|
7534
|
-
const manifest = parseManifest(
|
|
7738
|
+
const manifest = parseManifest(readFileSync8(source, "utf-8"));
|
|
7535
7739
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
7536
7740
|
}
|
|
7537
7741
|
return copyManifestDir(source, source, "local");
|
|
@@ -7562,7 +7766,7 @@ async function installManifestUrl(url) {
|
|
|
7562
7766
|
}
|
|
7563
7767
|
function ensureCustomDir() {
|
|
7564
7768
|
const dir = getCustomHooksDir();
|
|
7565
|
-
|
|
7769
|
+
mkdirSync7(dir, { recursive: true });
|
|
7566
7770
|
return dir;
|
|
7567
7771
|
}
|
|
7568
7772
|
var init_custom_install = __esm(() => {
|
|
@@ -7581,12 +7785,12 @@ __export(exports_sync, {
|
|
|
7581
7785
|
commitSyncArtifacts: () => commitSyncArtifacts
|
|
7582
7786
|
});
|
|
7583
7787
|
import { createHash as createHash2 } from "crypto";
|
|
7584
|
-
import { readFileSync as
|
|
7788
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
7585
7789
|
function sha256OfText(text) {
|
|
7586
7790
|
return createHash2("sha256").update(text).digest("hex");
|
|
7587
7791
|
}
|
|
7588
7792
|
function sha256OfPath(path) {
|
|
7589
|
-
return createHash2("sha256").update(
|
|
7793
|
+
return createHash2("sha256").update(readFileSync9(path)).digest("hex");
|
|
7590
7794
|
}
|
|
7591
7795
|
function collectBundledCatalog() {
|
|
7592
7796
|
const out = [];
|
|
@@ -8536,13 +8740,13 @@ __export(exports_serve, {
|
|
|
8536
8740
|
SERVE_HOST: () => SERVE_HOST,
|
|
8537
8741
|
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
8538
8742
|
});
|
|
8539
|
-
import { readFileSync as
|
|
8540
|
-
import { dirname as
|
|
8743
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
8744
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
8541
8745
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8542
8746
|
function packageVersion() {
|
|
8543
8747
|
try {
|
|
8544
|
-
const pkgPath = join15(
|
|
8545
|
-
return JSON.parse(
|
|
8748
|
+
const pkgPath = join15(dirname7(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
8749
|
+
return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
|
|
8546
8750
|
} catch {
|
|
8547
8751
|
return "0.0.0";
|
|
8548
8752
|
}
|
|
@@ -8843,17 +9047,17 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
8843
9047
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8844
9048
|
import { z as z2 } from "zod";
|
|
8845
9049
|
import { createServer } from "http";
|
|
8846
|
-
import { existsSync as
|
|
8847
|
-
import { join as join16, dirname as
|
|
9050
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
|
|
9051
|
+
import { join as join16, dirname as dirname8 } from "path";
|
|
8848
9052
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8849
9053
|
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
8850
9054
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
8851
9055
|
const resolved = resolveHook2(name);
|
|
8852
9056
|
const hookScript = resolveScriptPath2(name);
|
|
8853
|
-
if (!resolved || !hookScript || !
|
|
9057
|
+
if (!resolved || !hookScript || !existsSync15(hookScript)) {
|
|
8854
9058
|
throw new Error(`Hook '${name}' not found`);
|
|
8855
9059
|
}
|
|
8856
|
-
const content =
|
|
9060
|
+
const content = readFileSync11(hookScript);
|
|
8857
9061
|
const sha = sha256Of(content);
|
|
8858
9062
|
const check = checkScriptHash(name, sha);
|
|
8859
9063
|
if (!check.ok) {
|
|
@@ -9058,7 +9262,7 @@ function createHooksServer() {
|
|
|
9058
9262
|
const settingsPath = getSettingsPath(scope);
|
|
9059
9263
|
const issues = [];
|
|
9060
9264
|
const healthy = [];
|
|
9061
|
-
const settingsExist =
|
|
9265
|
+
const settingsExist = existsSync15(settingsPath);
|
|
9062
9266
|
if (!settingsExist) {
|
|
9063
9267
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
9064
9268
|
}
|
|
@@ -9068,7 +9272,7 @@ function createHooksServer() {
|
|
|
9068
9272
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
9069
9273
|
let hookHealthy = true;
|
|
9070
9274
|
if (custom) {
|
|
9071
|
-
if (!
|
|
9275
|
+
if (!existsSync15(custom.scriptPath)) {
|
|
9072
9276
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
9073
9277
|
hookHealthy = false;
|
|
9074
9278
|
}
|
|
@@ -9078,14 +9282,14 @@ function createHooksServer() {
|
|
|
9078
9282
|
continue;
|
|
9079
9283
|
}
|
|
9080
9284
|
const hookDir = getHookPath(name);
|
|
9081
|
-
if (!
|
|
9285
|
+
if (!existsSync15(join16(hookDir, "src", "hook.ts"))) {
|
|
9082
9286
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
9083
9287
|
hookHealthy = false;
|
|
9084
9288
|
}
|
|
9085
9289
|
}
|
|
9086
9290
|
if (meta && settingsExist) {
|
|
9087
9291
|
try {
|
|
9088
|
-
const settings = JSON.parse(
|
|
9292
|
+
const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
9089
9293
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
9090
9294
|
const eventName = meta.event.split(":")[0];
|
|
9091
9295
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -9124,8 +9328,8 @@ function createHooksServer() {
|
|
|
9124
9328
|
const hookPath = getHookPath(name);
|
|
9125
9329
|
const readmePath = join16(hookPath, "README.md");
|
|
9126
9330
|
let readme = "";
|
|
9127
|
-
if (
|
|
9128
|
-
readme =
|
|
9331
|
+
if (existsSync15(readmePath)) {
|
|
9332
|
+
readme = readFileSync11(readmePath, "utf-8");
|
|
9129
9333
|
}
|
|
9130
9334
|
if (verbose) {
|
|
9131
9335
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -9308,7 +9512,7 @@ function createHooksServer() {
|
|
|
9308
9512
|
const ctx = {
|
|
9309
9513
|
scope,
|
|
9310
9514
|
settings_path: settingsPath,
|
|
9311
|
-
settings_exists:
|
|
9515
|
+
settings_exists: existsSync15(settingsPath),
|
|
9312
9516
|
registered_hooks: hooks,
|
|
9313
9517
|
hook_count: hooks.length,
|
|
9314
9518
|
healthy,
|
|
@@ -9454,8 +9658,8 @@ function createHooksServer() {
|
|
|
9454
9658
|
const settingsPath = getSettingsPath(scope);
|
|
9455
9659
|
let settings = {};
|
|
9456
9660
|
try {
|
|
9457
|
-
if (
|
|
9458
|
-
settings = JSON.parse(
|
|
9661
|
+
if (existsSync15(settingsPath))
|
|
9662
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
9459
9663
|
} catch {}
|
|
9460
9664
|
if (!settings.hooks)
|
|
9461
9665
|
settings.hooks = {};
|
|
@@ -9463,10 +9667,10 @@ function createHooksServer() {
|
|
|
9463
9667
|
if (!disabled.includes(name))
|
|
9464
9668
|
disabled.push(name);
|
|
9465
9669
|
settings.hooks.__disabled = disabled;
|
|
9466
|
-
const { writeFileSync:
|
|
9467
|
-
const { dirname:
|
|
9468
|
-
|
|
9469
|
-
|
|
9670
|
+
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
|
|
9671
|
+
const { dirname: dirname9 } = await import("path");
|
|
9672
|
+
mkdirSync8(dirname9(settingsPath), { recursive: true });
|
|
9673
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
9470
9674
|
`);
|
|
9471
9675
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
9472
9676
|
});
|
|
@@ -9477,15 +9681,15 @@ function createHooksServer() {
|
|
|
9477
9681
|
const settingsPath = getSettingsPath(scope);
|
|
9478
9682
|
let settings = {};
|
|
9479
9683
|
try {
|
|
9480
|
-
if (
|
|
9481
|
-
settings = JSON.parse(
|
|
9684
|
+
if (existsSync15(settingsPath))
|
|
9685
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
9482
9686
|
} catch {}
|
|
9483
9687
|
if (settings.hooks?.__disabled) {
|
|
9484
9688
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
9485
9689
|
if (settings.hooks.__disabled.length === 0)
|
|
9486
9690
|
delete settings.hooks.__disabled;
|
|
9487
|
-
const { writeFileSync:
|
|
9488
|
-
|
|
9691
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
9692
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
9489
9693
|
`);
|
|
9490
9694
|
}
|
|
9491
9695
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
|
|
@@ -9848,13 +10052,13 @@ var init_server = __esm(() => {
|
|
|
9848
10052
|
init_redact();
|
|
9849
10053
|
init_authority();
|
|
9850
10054
|
init_storage();
|
|
9851
|
-
__dirname3 =
|
|
10055
|
+
__dirname3 = dirname8(fileURLToPath4(import.meta.url));
|
|
9852
10056
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
9853
10057
|
try {
|
|
9854
10058
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
9855
10059
|
const p = join16(__dirname3, rel);
|
|
9856
|
-
if (
|
|
9857
|
-
pkg = JSON.parse(
|
|
10060
|
+
if (existsSync15(p)) {
|
|
10061
|
+
pkg = JSON.parse(readFileSync11(p, "utf-8"));
|
|
9858
10062
|
break;
|
|
9859
10063
|
}
|
|
9860
10064
|
}
|
|
@@ -11596,8 +11800,8 @@ var {
|
|
|
11596
11800
|
|
|
11597
11801
|
// src/cli/index.tsx
|
|
11598
11802
|
import chalk2 from "chalk";
|
|
11599
|
-
import { existsSync as
|
|
11600
|
-
import { join as join17, dirname as
|
|
11803
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
|
|
11804
|
+
import { join as join17, dirname as dirname9 } from "path";
|
|
11601
11805
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
11602
11806
|
|
|
11603
11807
|
// src/cli/components/App.tsx
|
|
@@ -12439,7 +12643,7 @@ function InstallProgress({
|
|
|
12439
12643
|
onComplete
|
|
12440
12644
|
}) {
|
|
12441
12645
|
const [results, setResults] = useState6([]);
|
|
12442
|
-
const [
|
|
12646
|
+
const [current2, setCurrent] = useState6(0);
|
|
12443
12647
|
const [installing, setInstalling] = useState6(true);
|
|
12444
12648
|
useEffect5(() => {
|
|
12445
12649
|
const install = async () => {
|
|
@@ -12463,12 +12667,12 @@ function InstallProgress({
|
|
|
12463
12667
|
marginBottom: 1,
|
|
12464
12668
|
children: /* @__PURE__ */ jsxDEV6(Text9, {
|
|
12465
12669
|
bold: true,
|
|
12466
|
-
children: installing ? `Installing hooks (${
|
|
12670
|
+
children: installing ? `Installing hooks (${current2 + 1}/${hooks.length})...` : "Installation complete!"
|
|
12467
12671
|
}, undefined, false, undefined, this)
|
|
12468
12672
|
}, undefined, false, undefined, this),
|
|
12469
12673
|
hooks.map((name, i) => {
|
|
12470
12674
|
const result = results[i];
|
|
12471
|
-
const isCurrent = i ===
|
|
12675
|
+
const isCurrent = i === current2 && installing;
|
|
12472
12676
|
const meta = getHook(name);
|
|
12473
12677
|
return /* @__PURE__ */ jsxDEV6(Box7, {
|
|
12474
12678
|
flexDirection: "column",
|
|
@@ -12785,9 +12989,9 @@ init_local_opt_in();
|
|
|
12785
12989
|
init_transport2();
|
|
12786
12990
|
init_db();
|
|
12787
12991
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
12788
|
-
var __dirname4 =
|
|
12789
|
-
var pkgPath =
|
|
12790
|
-
var pkg2 = JSON.parse(
|
|
12992
|
+
var __dirname4 = dirname9(fileURLToPath5(import.meta.url));
|
|
12993
|
+
var pkgPath = existsSync16(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
|
|
12994
|
+
var pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
12791
12995
|
var program2 = new Command;
|
|
12792
12996
|
function resolveScope(options) {
|
|
12793
12997
|
if (options.project)
|
|
@@ -12797,12 +13001,45 @@ function resolveScope(options) {
|
|
|
12797
13001
|
function resolveTarget(options) {
|
|
12798
13002
|
if (options.target === "gemini")
|
|
12799
13003
|
return "gemini";
|
|
13004
|
+
if (options.target === "codex")
|
|
13005
|
+
return "codex";
|
|
12800
13006
|
if (options.target === "codewith")
|
|
12801
13007
|
return "codewith";
|
|
12802
13008
|
if (options.target === "all")
|
|
12803
13009
|
return "all";
|
|
12804
13010
|
return "claude";
|
|
12805
13011
|
}
|
|
13012
|
+
function resolveConcreteTarget(options) {
|
|
13013
|
+
if (options.target === "gemini")
|
|
13014
|
+
return "gemini";
|
|
13015
|
+
if (options.target === "codex")
|
|
13016
|
+
return "codex";
|
|
13017
|
+
if (options.target === "codewith")
|
|
13018
|
+
return "codewith";
|
|
13019
|
+
return "claude";
|
|
13020
|
+
}
|
|
13021
|
+
function formatSettingsPath(scope, target) {
|
|
13022
|
+
if (target === "all")
|
|
13023
|
+
return "target-specific settings";
|
|
13024
|
+
const actual = getSettingsPath(scope, target);
|
|
13025
|
+
if (scope === "project") {
|
|
13026
|
+
if (target === "codewith")
|
|
13027
|
+
return ".codewith/config.toml";
|
|
13028
|
+
if (target === "gemini")
|
|
13029
|
+
return ".gemini/settings.json";
|
|
13030
|
+
if (target === "codex")
|
|
13031
|
+
return ".codex/hooks.json";
|
|
13032
|
+
return ".claude/settings.json";
|
|
13033
|
+
}
|
|
13034
|
+
if (target === "codewith") {
|
|
13035
|
+
return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH ? "$HASNA_HOOKS_CODEWITH_CONFIG_PATH" : "~/.codewith/config.toml";
|
|
13036
|
+
}
|
|
13037
|
+
if (target === "gemini")
|
|
13038
|
+
return "~/.gemini/settings.json";
|
|
13039
|
+
if (target === "codex")
|
|
13040
|
+
return "~/.codex/hooks.json";
|
|
13041
|
+
return actual === getSettingsPath("global", "claude") ? "~/.claude/settings.json" : actual;
|
|
13042
|
+
}
|
|
12806
13043
|
function parseLimit(value, fallback, max) {
|
|
12807
13044
|
const parsed = value ? parseInt(value, 10) : fallback;
|
|
12808
13045
|
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
@@ -12834,7 +13071,7 @@ async function openLocalStoreOrExit(json2) {
|
|
|
12834
13071
|
function readToken(tokenFile) {
|
|
12835
13072
|
if (tokenFile) {
|
|
12836
13073
|
try {
|
|
12837
|
-
return
|
|
13074
|
+
return readFileSync12(tokenFile, "utf-8").trim();
|
|
12838
13075
|
} catch {
|
|
12839
13076
|
return;
|
|
12840
13077
|
}
|
|
@@ -12978,7 +13215,15 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
12978
13215
|
console.log(` hooks install gitguard --profile ${profile.agent_id}`);
|
|
12979
13216
|
console.log();
|
|
12980
13217
|
});
|
|
12981
|
-
program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
|
|
13218
|
+
program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").option("--enable-mementos", "Enable mementos-context for this invocation (provider assistance remains separately opt-in)").option("--disable-mementos", "Disable mementos-context for this invocation").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
|
|
13219
|
+
let mementos;
|
|
13220
|
+
try {
|
|
13221
|
+
mementos = mementosRegistrationFromFlags(hook, options);
|
|
13222
|
+
} catch (error) {
|
|
13223
|
+
console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Invalid Mementos options" }));
|
|
13224
|
+
process.exitCode = 1;
|
|
13225
|
+
return;
|
|
13226
|
+
}
|
|
12982
13227
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
12983
13228
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
12984
13229
|
const { recordHookRun: recordHookRun2, resolveEventType: resolveEventType2 } = await Promise.resolve().then(() => (init_db_writer(), exports_db_writer));
|
|
@@ -12988,11 +13233,11 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
12988
13233
|
process.exit(1);
|
|
12989
13234
|
}
|
|
12990
13235
|
const hookScript = resolveScriptPath2(hook);
|
|
12991
|
-
if (!hookScript || !
|
|
13236
|
+
if (!hookScript || !existsSync16(hookScript)) {
|
|
12992
13237
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
12993
13238
|
process.exit(1);
|
|
12994
13239
|
}
|
|
12995
|
-
const content =
|
|
13240
|
+
const content = readFileSync12(hookScript);
|
|
12996
13241
|
const sha = sha256Of2(content);
|
|
12997
13242
|
const check = checkScriptHash2(hook, sha);
|
|
12998
13243
|
if (!check.ok) {
|
|
@@ -13038,7 +13283,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
13038
13283
|
content,
|
|
13039
13284
|
args,
|
|
13040
13285
|
stdin: hookStdin,
|
|
13041
|
-
env: process.env,
|
|
13286
|
+
env: mementosRunEnvironment(process.env, mementos),
|
|
13042
13287
|
timeout
|
|
13043
13288
|
}));
|
|
13044
13289
|
} catch (err) {
|
|
@@ -13098,7 +13343,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
13098
13343
|
process.stderr.write(stderr);
|
|
13099
13344
|
process.exit(exitCode);
|
|
13100
13345
|
});
|
|
13101
|
-
program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
|
|
13346
|
+
program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--enable-mementos", "Persistently enable mementos-context (provider assistance remains separately opt-in)").option("--disable-mementos", "Persistently disable mementos-context").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
|
|
13102
13347
|
const scope = resolveScope(options);
|
|
13103
13348
|
const target = resolveTarget(options);
|
|
13104
13349
|
let toInstall = hooks;
|
|
@@ -13122,6 +13367,18 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
13122
13367
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
13123
13368
|
return;
|
|
13124
13369
|
}
|
|
13370
|
+
let mementos;
|
|
13371
|
+
try {
|
|
13372
|
+
mementos = mementosRegistrationFromFlags(toInstall.length === 1 ? toInstall[0] : "", options);
|
|
13373
|
+
} catch (error) {
|
|
13374
|
+
const message = error instanceof Error ? error.message : "Invalid Mementos options";
|
|
13375
|
+
if (options.json)
|
|
13376
|
+
console.log(JSON.stringify({ error: message, applied: false }));
|
|
13377
|
+
else
|
|
13378
|
+
console.error(message);
|
|
13379
|
+
process.exitCode = 1;
|
|
13380
|
+
return;
|
|
13381
|
+
}
|
|
13125
13382
|
if (options.applyCodewith && (target === "codewith" || target === "all") && !options.codewithConfig) {
|
|
13126
13383
|
const message = "--apply-codewith requires --codewith-config <path>; refusing to write default ~/.codewith/config.toml.";
|
|
13127
13384
|
if (options.json) {
|
|
@@ -13160,6 +13417,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
13160
13417
|
console.log(JSON.stringify({
|
|
13161
13418
|
dryRun: true,
|
|
13162
13419
|
would_install: known,
|
|
13420
|
+
...mementos ? { mementos } : {},
|
|
13163
13421
|
custom_sources: customSources,
|
|
13164
13422
|
pinned: pinnedRequests.map((p) => `${p.pinned.name}@${p.pinned.version}`),
|
|
13165
13423
|
unknown,
|
|
@@ -13264,7 +13522,8 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13264
13522
|
target,
|
|
13265
13523
|
profile: options.profile,
|
|
13266
13524
|
codewithMode: options.applyCodewith ? "write" : "fragment",
|
|
13267
|
-
codewithConfigPath: options.codewithConfig
|
|
13525
|
+
codewithConfigPath: options.codewithConfig,
|
|
13526
|
+
mementos
|
|
13268
13527
|
});
|
|
13269
13528
|
results.push(result);
|
|
13270
13529
|
}
|
|
@@ -13277,7 +13536,8 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13277
13536
|
success: results.filter((r) => r.success).length,
|
|
13278
13537
|
scope,
|
|
13279
13538
|
target,
|
|
13280
|
-
applied: results.some((r) => r.applied)
|
|
13539
|
+
applied: results.some((r) => r.applied),
|
|
13540
|
+
...target === "codex" ? { notes: results.filter((r) => r.success && r.note).map((r) => r.note) } : {}
|
|
13281
13541
|
}));
|
|
13282
13542
|
if (results.some((r) => !r.success)) {
|
|
13283
13543
|
process.exitCode = 1;
|
|
@@ -13285,7 +13545,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13285
13545
|
return;
|
|
13286
13546
|
}
|
|
13287
13547
|
const successCount = results.filter((r) => r.success).length;
|
|
13288
|
-
const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : scope
|
|
13548
|
+
const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : formatSettingsPath(scope, target);
|
|
13289
13549
|
console.log(chalk2.bold(`
|
|
13290
13550
|
Installing hooks (${scope}, ${target})...
|
|
13291
13551
|
`));
|
|
@@ -13299,6 +13559,8 @@ Installing hooks (${scope}, ${target})...
|
|
|
13299
13559
|
if (result.conflict) {
|
|
13300
13560
|
console.log(chalk2.yellow(` \u26A0 Warning: ${result.conflict}`));
|
|
13301
13561
|
}
|
|
13562
|
+
if (result.note && target === "codex")
|
|
13563
|
+
console.log(chalk2.dim(` ${result.note}`));
|
|
13302
13564
|
if (result.fragment && target === "codewith") {
|
|
13303
13565
|
console.log(chalk2.dim(" Codewith TOML fragment:"));
|
|
13304
13566
|
console.log(chalk2.cyan(result.fragment.trimEnd().split(`
|
|
@@ -13325,12 +13587,12 @@ Installing hooks (${scope}, ${target})...
|
|
|
13325
13587
|
console.log(chalk2.dim(`
|
|
13326
13588
|
Registered in ${settingsFile}`));
|
|
13327
13589
|
});
|
|
13328
|
-
program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks (bundled + custom/registry)").action((options) => {
|
|
13590
|
+
program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks (bundled + custom/registry)").action((options) => {
|
|
13329
13591
|
const scope = resolveScope(options);
|
|
13330
13592
|
const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
|
|
13331
13593
|
const storeHooks = listStoreHooks();
|
|
13332
13594
|
if (options.registered || options.installed) {
|
|
13333
|
-
const target = options
|
|
13595
|
+
const target = resolveConcreteTarget(options);
|
|
13334
13596
|
const registered = getRegisteredHooksForTarget(scope, target);
|
|
13335
13597
|
const installed = [...new Set([...registered, ...storeHooks.map((s) => s.meta.name)])];
|
|
13336
13598
|
const shown = options.registered ? registered : installed;
|
|
@@ -13453,7 +13715,7 @@ Found ${results.length} hook(s), showing ${visible.length}:
|
|
|
13453
13715
|
console.log(hookSummaryLine(h, { verbose: options.verbose }));
|
|
13454
13716
|
printDisclosureHint(results.length - visible.length, "hooks info <name>");
|
|
13455
13717
|
});
|
|
13456
|
-
program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook (settings registration + store + lock + DB)").action(async (hook, options) => {
|
|
13718
|
+
program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook (settings registration + store + lock + DB)").action(async (hook, options) => {
|
|
13457
13719
|
const scope = resolveScope(options);
|
|
13458
13720
|
const target = resolveTarget(options);
|
|
13459
13721
|
const { uninstallHook: uninstallHook2 } = await Promise.resolve().then(() => (init_installer(), exports_installer));
|
|
@@ -13569,7 +13831,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13569
13831
|
});
|
|
13570
13832
|
}
|
|
13571
13833
|
const staleNames = new Set(stale.map((finding) => finding.hook));
|
|
13572
|
-
const settingsExist =
|
|
13834
|
+
const settingsExist = existsSync16(settingsPath);
|
|
13573
13835
|
if (!settingsExist) {
|
|
13574
13836
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
13575
13837
|
}
|
|
@@ -13577,7 +13839,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13577
13839
|
let wiringCount = 0;
|
|
13578
13840
|
if (settingsExist) {
|
|
13579
13841
|
try {
|
|
13580
|
-
const settings = JSON.parse(
|
|
13842
|
+
const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
13581
13843
|
wiringCount = countSettingsWiring(settings);
|
|
13582
13844
|
} catch {}
|
|
13583
13845
|
}
|
|
@@ -13588,7 +13850,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13588
13850
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
13589
13851
|
let hookHealthy = true;
|
|
13590
13852
|
if (custom) {
|
|
13591
|
-
if (!
|
|
13853
|
+
if (!existsSync16(custom.scriptPath)) {
|
|
13592
13854
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
13593
13855
|
hookHealthy = false;
|
|
13594
13856
|
}
|
|
@@ -13600,14 +13862,14 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13600
13862
|
}
|
|
13601
13863
|
const hookDir = getHookPath(name);
|
|
13602
13864
|
const hookScript = join17(hookDir, "src", "hook.ts");
|
|
13603
|
-
if (!
|
|
13865
|
+
if (!existsSync16(hookScript)) {
|
|
13604
13866
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
13605
13867
|
hookHealthy = false;
|
|
13606
13868
|
}
|
|
13607
13869
|
}
|
|
13608
13870
|
if (meta && settingsExist) {
|
|
13609
13871
|
try {
|
|
13610
|
-
const settings = JSON.parse(
|
|
13872
|
+
const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
13611
13873
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
13612
13874
|
const eventName = meta.event.split(":")[0];
|
|
13613
13875
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -13812,8 +14074,8 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
13812
14074
|
const hookPath = getHookPath(hook);
|
|
13813
14075
|
const readmePath = join17(hookPath, "README.md");
|
|
13814
14076
|
let readme = "";
|
|
13815
|
-
if (
|
|
13816
|
-
readme =
|
|
14077
|
+
if (existsSync16(readmePath)) {
|
|
14078
|
+
readme = readFileSync12(readmePath, "utf-8");
|
|
13817
14079
|
}
|
|
13818
14080
|
if (options.json) {
|
|
13819
14081
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -13932,7 +14194,7 @@ ${meta.displayName} v${meta.version}
|
|
|
13932
14194
|
console.log();
|
|
13933
14195
|
});
|
|
13934
14196
|
program2.command("upgrade").option("-c, --check", "Check for updates without installing", false).option("-j, --json", "Output as JSON", false).description("Update the @hasna/hooks package to the latest version").action(async (options) => {
|
|
13935
|
-
const
|
|
14197
|
+
const current2 = pkg2.version;
|
|
13936
14198
|
let pm = "npm";
|
|
13937
14199
|
try {
|
|
13938
14200
|
const which = Bun.spawnSync(["which", "bun"]);
|
|
@@ -13950,13 +14212,13 @@ program2.command("upgrade").option("-c, --check", "Check for updates without ins
|
|
|
13950
14212
|
}
|
|
13951
14213
|
process.exit(1);
|
|
13952
14214
|
}
|
|
13953
|
-
const upToDate =
|
|
14215
|
+
const upToDate = current2 === latest2;
|
|
13954
14216
|
if (options.json) {
|
|
13955
|
-
console.log(JSON.stringify({ current, latest: latest2, upToDate }));
|
|
14217
|
+
console.log(JSON.stringify({ current: current2, latest: latest2, upToDate }));
|
|
13956
14218
|
} else if (upToDate) {
|
|
13957
|
-
console.log(chalk2.green(`\u2713 Already on latest version (${
|
|
14219
|
+
console.log(chalk2.green(`\u2713 Already on latest version (${current2})`));
|
|
13958
14220
|
} else {
|
|
13959
|
-
console.log(chalk2.yellow(`Update available: ${
|
|
14221
|
+
console.log(chalk2.yellow(`Update available: ${current2} \u2192 ${latest2}`));
|
|
13960
14222
|
console.log(chalk2.dim(` Run: hooks upgrade`));
|
|
13961
14223
|
}
|
|
13962
14224
|
return;
|
|
@@ -13977,7 +14239,7 @@ Upgrading @hasna/hooks (${pm})...
|
|
|
13977
14239
|
const exitCode = await proc.exited;
|
|
13978
14240
|
if (exitCode !== 0) {
|
|
13979
14241
|
if (options.json) {
|
|
13980
|
-
console.log(JSON.stringify({ current, updated: false, error: `${pm} exited with code ${exitCode}` }));
|
|
14242
|
+
console.log(JSON.stringify({ current: current2, updated: false, error: `${pm} exited with code ${exitCode}` }));
|
|
13981
14243
|
} else {
|
|
13982
14244
|
console.log(chalk2.red(`
|
|
13983
14245
|
\u2717 Upgrade failed (exit code ${exitCode})`));
|
|
@@ -13987,18 +14249,18 @@ Upgrading @hasna/hooks (${pm})...
|
|
|
13987
14249
|
const versionProc = Bun.spawnSync(["npm", "view", "@hasna/hooks", "version"]);
|
|
13988
14250
|
const latest = new TextDecoder().decode(versionProc.stdout).trim() || "unknown";
|
|
13989
14251
|
if (options.json) {
|
|
13990
|
-
console.log(JSON.stringify({ current, latest, updated: true }));
|
|
14252
|
+
console.log(JSON.stringify({ current: current2, latest, updated: true }));
|
|
13991
14253
|
} else {
|
|
13992
14254
|
console.log(chalk2.green(`
|
|
13993
|
-
\u2713 Upgraded: ${
|
|
14255
|
+
\u2713 Upgraded: ${current2} \u2192 ${latest}`));
|
|
13994
14256
|
}
|
|
13995
14257
|
});
|
|
13996
14258
|
program2.command("profile-export").description("Export all agent profiles as JSON (for backup/cross-machine setup)").option("-o, --output <file>", "Write to file instead of stdout").option("-j, --json", "Output as JSON (default: true)", false).action(async (options) => {
|
|
13997
14259
|
const profiles = exportProfiles();
|
|
13998
14260
|
const json2 = JSON.stringify(profiles, null, 2);
|
|
13999
14261
|
if (options.output) {
|
|
14000
|
-
const { writeFileSync:
|
|
14001
|
-
|
|
14262
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
14263
|
+
writeFileSync7(options.output, json2 + `
|
|
14002
14264
|
`);
|
|
14003
14265
|
console.log(chalk2.green(`\u2713 Exported ${profiles.length} profile(s) to ${options.output}`));
|
|
14004
14266
|
} else {
|
|
@@ -14010,9 +14272,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
14010
14272
|
if (file === "-") {
|
|
14011
14273
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
14012
14274
|
} else {
|
|
14013
|
-
const { readFileSync:
|
|
14275
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
14014
14276
|
try {
|
|
14015
|
-
raw =
|
|
14277
|
+
raw = readFileSync13(file, "utf-8");
|
|
14016
14278
|
} catch {
|
|
14017
14279
|
if (options.json) {
|
|
14018
14280
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|