@hasna/hooks 0.9.0 → 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 +43 -5
- package/bin/hooks-mcp.js +4980 -0
- package/bin/index.js +586 -252
- package/bin/serve.js +34 -12
- package/dist/db/index.d.ts +8 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3016 -2782
- package/dist/lib/codex-settings.d.ts +3 -0
- package/dist/lib/db-writer.d.ts +30 -1
- package/dist/lib/installer.d.ts +1 -1
- package/dist/lib/local-opt-in.d.ts +26 -0
- package/dist/lib/sync.d.ts +17 -1
- package/dist/sdk/index.d.ts +103 -0
- package/dist/sdk/index.js +1050 -0
- package/dist/storage.js +15 -1
- package/hooks/hook-mementos-context/README.md +80 -0
- package/hooks/hook-mementos-context/src/hook.ts +116 -0
- package/hooks/hook-trash-guard/README.md +20 -5
- package/hooks/hook-trash-guard/package.json +1 -1
- package/hooks/hook-trash-guard/src/hook.ts +45 -17
- package/package.json +11 -5
- package/scripts/validate-package.ts +31 -4
package/bin/index.js
CHANGED
|
@@ -2087,10 +2087,10 @@ var init_registry = __esm(() => {
|
|
|
2087
2087
|
name: "trash-guard",
|
|
2088
2088
|
displayName: "Trash Guard",
|
|
2089
2089
|
description: "Rewrites rm issued through Bash into `trash guard`, so the delete lands in a recoverable trash store; refuses the delete when there is nothing to redirect to (trash absent, a delete verb it cannot rewrite, or the protected class: /, ~, ~/.hasna, ~/.ssh, ~/.aws)",
|
|
2090
|
-
version: "0.1.
|
|
2090
|
+
version: "0.1.1",
|
|
2091
2091
|
category: "Git Safety",
|
|
2092
2092
|
event: "PreToolUse",
|
|
2093
|
-
matcher: "Bash",
|
|
2093
|
+
matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
2094
2094
|
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
2095
2095
|
rewritesInput: true,
|
|
2096
2096
|
timeoutSeconds: 5
|
|
@@ -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",
|
|
@@ -4919,14 +4930,30 @@ function runRetention(db, days) {
|
|
|
4919
4930
|
// src/db/index.ts
|
|
4920
4931
|
var exports_db = {};
|
|
4921
4932
|
__export(exports_db, {
|
|
4933
|
+
refuseLocalStore: () => refuseLocalStore,
|
|
4934
|
+
localStoreRefusalMessage: () => localStoreRefusalMessage,
|
|
4935
|
+
isLocalStoreRefused: () => isLocalStoreRefused,
|
|
4922
4936
|
getDbPath: () => getDbPath,
|
|
4923
4937
|
getDb: () => getDb,
|
|
4924
4938
|
createTestDb: () => createTestDb,
|
|
4925
|
-
closeDb: () => closeDb
|
|
4939
|
+
closeDb: () => closeDb,
|
|
4940
|
+
allowLocalStore: () => allowLocalStore
|
|
4926
4941
|
});
|
|
4927
4942
|
import { Database } from "bun:sqlite";
|
|
4928
4943
|
import { existsSync as existsSync8, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
4929
4944
|
import { join as join9 } from "path";
|
|
4945
|
+
function refuseLocalStore(message) {
|
|
4946
|
+
localStoreRefusal = message;
|
|
4947
|
+
}
|
|
4948
|
+
function allowLocalStore() {
|
|
4949
|
+
localStoreRefusal = null;
|
|
4950
|
+
}
|
|
4951
|
+
function isLocalStoreRefused() {
|
|
4952
|
+
return localStoreRefusal !== null;
|
|
4953
|
+
}
|
|
4954
|
+
function localStoreRefusalMessage() {
|
|
4955
|
+
return localStoreRefusal;
|
|
4956
|
+
}
|
|
4930
4957
|
function resolveDataDir() {
|
|
4931
4958
|
const effective = getEffectiveDataRoot();
|
|
4932
4959
|
const oldDir = join9(getHomeDir(), ".hooks");
|
|
@@ -4950,6 +4977,8 @@ function ensureDir(dbPath) {
|
|
|
4950
4977
|
}
|
|
4951
4978
|
}
|
|
4952
4979
|
function getDb() {
|
|
4980
|
+
if (localStoreRefusal !== null)
|
|
4981
|
+
throw new Error(localStoreRefusal);
|
|
4953
4982
|
if (instance)
|
|
4954
4983
|
return instance;
|
|
4955
4984
|
const dbPath = getDbPath();
|
|
@@ -4987,7 +5016,7 @@ function createTestDb() {
|
|
|
4987
5016
|
db.exec("PRAGMA foreign_keys=ON");
|
|
4988
5017
|
return db;
|
|
4989
5018
|
}
|
|
4990
|
-
var instance = null;
|
|
5019
|
+
var instance = null, localStoreRefusal = null;
|
|
4991
5020
|
var init_db = __esm(() => {
|
|
4992
5021
|
init_app_home();
|
|
4993
5022
|
init_migrations();
|
|
@@ -5026,6 +5055,9 @@ async function sha256File(path) {
|
|
|
5026
5055
|
const buf = await readFile2(path);
|
|
5027
5056
|
return sha256Of(buf);
|
|
5028
5057
|
}
|
|
5058
|
+
function storeDb() {
|
|
5059
|
+
return isLocalStoreRefused() ? null : getDb();
|
|
5060
|
+
}
|
|
5029
5061
|
function getHookRecord(db, name) {
|
|
5030
5062
|
return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
|
|
5031
5063
|
}
|
|
@@ -5126,26 +5158,28 @@ function removePinnedHook(name) {
|
|
|
5126
5158
|
}
|
|
5127
5159
|
function pinInstalledHook(name, version, sha256, source, sourceRef) {
|
|
5128
5160
|
const now2 = new Date().toISOString();
|
|
5129
|
-
const db =
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5161
|
+
const db = storeDb();
|
|
5162
|
+
if (db) {
|
|
5163
|
+
upsertHookRecord(db, {
|
|
5164
|
+
name,
|
|
5165
|
+
version,
|
|
5166
|
+
sha256,
|
|
5167
|
+
source_type: source,
|
|
5168
|
+
source_ref: sourceRef ?? null,
|
|
5169
|
+
last_verified_at: now2
|
|
5170
|
+
});
|
|
5171
|
+
}
|
|
5138
5172
|
setPinnedHook(name, { version, sha256, source });
|
|
5139
5173
|
}
|
|
5140
5174
|
function removeHookFromStore(name) {
|
|
5141
5175
|
const removedPin = removePinnedHook(name);
|
|
5142
|
-
const db =
|
|
5143
|
-
const removedRecord = removeHookRecord(db, name);
|
|
5176
|
+
const db = storeDb();
|
|
5177
|
+
const removedRecord = db ? removeHookRecord(db, name) : false;
|
|
5144
5178
|
return { removedPin, removedRecord };
|
|
5145
5179
|
}
|
|
5146
5180
|
function checkScriptHash(name, actual) {
|
|
5147
|
-
const db =
|
|
5148
|
-
const record = getHookRecord(db, name);
|
|
5181
|
+
const db = storeDb();
|
|
5182
|
+
const record = db ? getHookRecord(db, name) : null;
|
|
5149
5183
|
const pin = getPinnedHook(name);
|
|
5150
5184
|
const expected = record?.sha256 ?? pin?.sha256 ?? null;
|
|
5151
5185
|
if (expected !== null && expected !== actual) {
|
|
@@ -5155,24 +5189,28 @@ function checkScriptHash(name, actual) {
|
|
|
5155
5189
|
const now3 = new Date().toISOString();
|
|
5156
5190
|
const version = pin?.version ?? record?.version ?? "0.0.0";
|
|
5157
5191
|
const source = pin?.source ?? record?.source_type ?? "local";
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5192
|
+
if (db) {
|
|
5193
|
+
upsertHookRecord(db, {
|
|
5194
|
+
name,
|
|
5195
|
+
version,
|
|
5196
|
+
sha256: actual,
|
|
5197
|
+
source_type: source,
|
|
5198
|
+
last_verified_at: now3
|
|
5199
|
+
});
|
|
5200
|
+
}
|
|
5165
5201
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
5166
5202
|
return { ok: true, pinned: false, expected: null, actual, name };
|
|
5167
5203
|
}
|
|
5168
5204
|
const now2 = new Date().toISOString();
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5205
|
+
if (db) {
|
|
5206
|
+
upsertHookRecord(db, {
|
|
5207
|
+
name,
|
|
5208
|
+
version: record?.version ?? pin?.version ?? "0.0.0",
|
|
5209
|
+
sha256: expected,
|
|
5210
|
+
source_type: record?.source_type ?? pin?.source ?? "local",
|
|
5211
|
+
last_verified_at: now2
|
|
5212
|
+
});
|
|
5213
|
+
}
|
|
5176
5214
|
return { ok: true, pinned: true, expected, actual, name };
|
|
5177
5215
|
}
|
|
5178
5216
|
async function verifyScriptHash(name, scriptPath) {
|
|
@@ -5180,14 +5218,16 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
5180
5218
|
}
|
|
5181
5219
|
function retrustHook(name, scriptPath, version, source) {
|
|
5182
5220
|
const actual = sha256Of(readFileSync3(scriptPath));
|
|
5183
|
-
const db =
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5221
|
+
const db = storeDb();
|
|
5222
|
+
if (db) {
|
|
5223
|
+
upsertHookRecord(db, {
|
|
5224
|
+
name,
|
|
5225
|
+
version,
|
|
5226
|
+
sha256: actual,
|
|
5227
|
+
source_type: source,
|
|
5228
|
+
last_verified_at: new Date().toISOString()
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5191
5231
|
setPinnedHook(name, { version, sha256: actual, source });
|
|
5192
5232
|
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
5193
5233
|
}
|
|
@@ -5203,6 +5243,78 @@ var init_store = __esm(() => {
|
|
|
5203
5243
|
};
|
|
5204
5244
|
});
|
|
5205
5245
|
|
|
5246
|
+
// src/lib/codex-settings.ts
|
|
5247
|
+
import { closeSync, existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5248
|
+
import { dirname as dirname3 } from "path";
|
|
5249
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5250
|
+
function current(path) {
|
|
5251
|
+
try {
|
|
5252
|
+
const stat = lstatSync(path);
|
|
5253
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
5254
|
+
throw new Error("invalid_file");
|
|
5255
|
+
return readFileSync4(path, "utf8");
|
|
5256
|
+
} catch (error) {
|
|
5257
|
+
if (error.code === "ENOENT")
|
|
5258
|
+
return null;
|
|
5259
|
+
throw new Error("Cannot safely read Codex hooks.json; no settings were changed.");
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
function readCodexSettings(path) {
|
|
5263
|
+
const raw = current(path);
|
|
5264
|
+
let settings;
|
|
5265
|
+
try {
|
|
5266
|
+
settings = raw === null ? {} : JSON.parse(raw);
|
|
5267
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
5268
|
+
throw new Error;
|
|
5269
|
+
if (settings.hooks !== undefined) {
|
|
5270
|
+
if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks))
|
|
5271
|
+
throw new Error;
|
|
5272
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
5273
|
+
if (!Array.isArray(entries) || entries.some((entry) => !entry || !Array.isArray(entry.hooks) || entry.hooks.some((hook) => !hook || typeof hook !== "object")))
|
|
5274
|
+
throw new Error;
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
} catch {
|
|
5278
|
+
throw new Error("Invalid Codex hooks.json; repair it before installing or removing hooks.");
|
|
5279
|
+
}
|
|
5280
|
+
snapshots.set(settings, { path, raw });
|
|
5281
|
+
return settings;
|
|
5282
|
+
}
|
|
5283
|
+
function writeCodexSettings(path, settings) {
|
|
5284
|
+
const snapshot = snapshots.get(settings);
|
|
5285
|
+
if (!snapshot || snapshot.path !== path)
|
|
5286
|
+
throw new Error("Codex settings require a complete current read before writing.");
|
|
5287
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
5288
|
+
const lock = `${path}.hooks-write.lock`;
|
|
5289
|
+
let fd;
|
|
5290
|
+
try {
|
|
5291
|
+
fd = openSync(lock, "wx", 384);
|
|
5292
|
+
} catch {
|
|
5293
|
+
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
5294
|
+
}
|
|
5295
|
+
const temp = `${path}.${randomUUID3()}.tmp`;
|
|
5296
|
+
try {
|
|
5297
|
+
if (current(path) !== snapshot.raw)
|
|
5298
|
+
throw new Error("Codex settings changed since reading; re-read before retrying.");
|
|
5299
|
+
const next = JSON.stringify(settings, null, 2) + `
|
|
5300
|
+
`;
|
|
5301
|
+
writeFileSync3(temp, next, { flag: "wx", mode: 384 });
|
|
5302
|
+
if (current(path) !== snapshot.raw)
|
|
5303
|
+
throw new Error("Codex settings changed since reading; re-read before retrying.");
|
|
5304
|
+
renameSync2(temp, path);
|
|
5305
|
+
snapshots.set(settings, { path, raw: next });
|
|
5306
|
+
} finally {
|
|
5307
|
+
if (existsSync10(temp))
|
|
5308
|
+
unlinkSync(temp);
|
|
5309
|
+
closeSync(fd);
|
|
5310
|
+
unlinkSync(lock);
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
var snapshots;
|
|
5314
|
+
var init_codex_settings = __esm(() => {
|
|
5315
|
+
snapshots = new WeakMap;
|
|
5316
|
+
});
|
|
5317
|
+
|
|
5206
5318
|
// src/lib/installer.ts
|
|
5207
5319
|
var exports_installer = {};
|
|
5208
5320
|
__export(exports_installer, {
|
|
@@ -5223,8 +5335,8 @@ __export(exports_installer, {
|
|
|
5223
5335
|
detectRewriteConflict: () => detectRewriteConflict,
|
|
5224
5336
|
buildCodewithTomlFragment: () => buildCodewithTomlFragment
|
|
5225
5337
|
});
|
|
5226
|
-
import { existsSync as
|
|
5227
|
-
import { join as join10, dirname as
|
|
5338
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
5339
|
+
import { join as join10, dirname as dirname4 } from "path";
|
|
5228
5340
|
import { homedir as homedir4 } from "os";
|
|
5229
5341
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5230
5342
|
function normalizeHookName(name) {
|
|
@@ -5234,12 +5346,19 @@ function shortHookName(name) {
|
|
|
5234
5346
|
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
5235
5347
|
}
|
|
5236
5348
|
function removeHookEntriesByName(entries, hookName) {
|
|
5237
|
-
return entries.
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5349
|
+
return entries.flatMap((entry) => {
|
|
5350
|
+
if (!Array.isArray(entry.hooks))
|
|
5351
|
+
return [entry];
|
|
5352
|
+
const kept = entry.hooks.filter((h) => {
|
|
5353
|
+
const match = h.command?.match(/^hooks run ([\w-]+)/);
|
|
5354
|
+
return !(match && match[1] === hookName);
|
|
5355
|
+
});
|
|
5356
|
+
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
5357
|
+
});
|
|
5241
5358
|
}
|
|
5242
5359
|
function getTargetSettingsDir(target) {
|
|
5360
|
+
if (target === "codex")
|
|
5361
|
+
return ".codex";
|
|
5243
5362
|
if (target === "codewith")
|
|
5244
5363
|
return ".codewith";
|
|
5245
5364
|
if (target === "gemini")
|
|
@@ -5247,6 +5366,8 @@ function getTargetSettingsDir(target) {
|
|
|
5247
5366
|
return ".claude";
|
|
5248
5367
|
}
|
|
5249
5368
|
function getGlobalSettingsPathOverride(target) {
|
|
5369
|
+
if (target === "codex")
|
|
5370
|
+
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
|
|
5250
5371
|
if (target === "claude")
|
|
5251
5372
|
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
5252
5373
|
if (target === "gemini")
|
|
@@ -5263,6 +5384,8 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
5263
5384
|
if (globalOverride)
|
|
5264
5385
|
return globalOverride;
|
|
5265
5386
|
const dir = getTargetSettingsDir(target);
|
|
5387
|
+
if (target === "codex")
|
|
5388
|
+
return join10(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
|
|
5266
5389
|
if (scope === "project") {
|
|
5267
5390
|
return target === "codewith" ? join10(process.cwd(), dir, "config.toml") : join10(process.cwd(), dir, "settings.json");
|
|
5268
5391
|
}
|
|
@@ -5276,13 +5399,15 @@ function getHookPath(name) {
|
|
|
5276
5399
|
return join10(HOOKS_DIR, normalizeHookName(shortName));
|
|
5277
5400
|
}
|
|
5278
5401
|
function hookExists(name) {
|
|
5279
|
-
return
|
|
5402
|
+
return existsSync11(getHookPath(name));
|
|
5280
5403
|
}
|
|
5281
5404
|
function readSettings(scope = "global", target = "claude") {
|
|
5282
5405
|
const path = getSettingsPath(scope, target);
|
|
5406
|
+
if (target === "codex")
|
|
5407
|
+
return readCodexSettings(path);
|
|
5283
5408
|
try {
|
|
5284
|
-
if (
|
|
5285
|
-
return JSON.parse(
|
|
5409
|
+
if (existsSync11(path)) {
|
|
5410
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
5286
5411
|
}
|
|
5287
5412
|
} catch (error) {
|
|
5288
5413
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5291,11 +5416,13 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
5291
5416
|
}
|
|
5292
5417
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
5293
5418
|
const path = getSettingsPath(scope, target);
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5419
|
+
if (target === "codex")
|
|
5420
|
+
return writeCodexSettings(path, settings);
|
|
5421
|
+
const dir = dirname4(path);
|
|
5422
|
+
if (!existsSync11(dir)) {
|
|
5423
|
+
mkdirSync5(dir, { recursive: true });
|
|
5297
5424
|
}
|
|
5298
|
-
|
|
5425
|
+
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
5299
5426
|
`);
|
|
5300
5427
|
}
|
|
5301
5428
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -5318,6 +5445,8 @@ function codewithMatcher(matcher) {
|
|
|
5318
5445
|
}
|
|
5319
5446
|
function codewithTimeout(name) {
|
|
5320
5447
|
switch (name) {
|
|
5448
|
+
case "mementos-context":
|
|
5449
|
+
return 12;
|
|
5321
5450
|
case "knowledge-context":
|
|
5322
5451
|
return 6;
|
|
5323
5452
|
case "session-start":
|
|
@@ -5384,21 +5513,21 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
5384
5513
|
function readCodewithConfig(scope, configPath) {
|
|
5385
5514
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5386
5515
|
try {
|
|
5387
|
-
return
|
|
5516
|
+
return existsSync11(path) ? readFileSync5(path, "utf-8") : "";
|
|
5388
5517
|
} catch {
|
|
5389
5518
|
return "";
|
|
5390
5519
|
}
|
|
5391
5520
|
}
|
|
5392
5521
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
5393
5522
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5394
|
-
const dir =
|
|
5395
|
-
if (!
|
|
5396
|
-
|
|
5397
|
-
const existing =
|
|
5523
|
+
const dir = dirname4(path);
|
|
5524
|
+
if (!existsSync11(dir))
|
|
5525
|
+
mkdirSync5(dir, { recursive: true });
|
|
5526
|
+
const existing = existsSync11(path) ? readFileSync5(path, "utf-8") : "";
|
|
5398
5527
|
const sep2 = existing.trim() ? `
|
|
5399
5528
|
|
|
5400
5529
|
` : "";
|
|
5401
|
-
|
|
5530
|
+
writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
5402
5531
|
return path;
|
|
5403
5532
|
}
|
|
5404
5533
|
function detectConflict(name, scope, target) {
|
|
@@ -5495,18 +5624,25 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
5495
5624
|
};
|
|
5496
5625
|
}
|
|
5497
5626
|
}
|
|
5498
|
-
const registered = getRegisteredHooksForTarget(scope, target);
|
|
5499
|
-
if (registered.includes(shortName) && !overwrite) {
|
|
5500
|
-
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
5501
|
-
}
|
|
5502
|
-
const conflict = detectConflict(shortName, scope, target);
|
|
5503
|
-
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
5504
|
-
if (rewriteConflict) {
|
|
5505
|
-
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
5506
|
-
}
|
|
5507
5627
|
try {
|
|
5628
|
+
const registered = getRegisteredHooksForTarget(scope, target);
|
|
5629
|
+
if (registered.includes(shortName) && !overwrite) {
|
|
5630
|
+
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
|
|
5631
|
+
}
|
|
5632
|
+
const conflict = detectConflict(shortName, scope, target);
|
|
5633
|
+
const rewriteConflict = detectRewriteConflict(shortName, scope, target);
|
|
5634
|
+
if (rewriteConflict) {
|
|
5635
|
+
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
5636
|
+
}
|
|
5508
5637
|
registerHook(shortName, scope, target, profile);
|
|
5509
|
-
return {
|
|
5638
|
+
return {
|
|
5639
|
+
hook: shortName,
|
|
5640
|
+
success: true,
|
|
5641
|
+
scope,
|
|
5642
|
+
target,
|
|
5643
|
+
...conflict ? { conflict } : {},
|
|
5644
|
+
...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." } : {}
|
|
5645
|
+
};
|
|
5510
5646
|
} catch (error) {
|
|
5511
5647
|
return {
|
|
5512
5648
|
hook: shortName,
|
|
@@ -5518,6 +5654,9 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
5518
5654
|
}
|
|
5519
5655
|
function installHook(name, options = {}) {
|
|
5520
5656
|
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
|
|
5657
|
+
if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
|
|
5658
|
+
return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
|
|
5659
|
+
}
|
|
5521
5660
|
if (target === "all") {
|
|
5522
5661
|
const shortName = shortHookName(name);
|
|
5523
5662
|
const meta = resolveHookMeta(shortName);
|
|
@@ -5649,7 +5788,8 @@ function removeHook(name, scope = "global", target = "claude") {
|
|
|
5649
5788
|
if (target === "all") {
|
|
5650
5789
|
const claudeRemoved = removeHookForTarget(shortName, scope, "claude");
|
|
5651
5790
|
const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
|
|
5652
|
-
|
|
5791
|
+
const codexRemoved = removeHookForTarget(shortName, scope, "codex");
|
|
5792
|
+
return claudeRemoved || geminiRemoved || codexRemoved;
|
|
5653
5793
|
}
|
|
5654
5794
|
if (target === "codewith") {
|
|
5655
5795
|
return false;
|
|
@@ -5666,9 +5806,9 @@ function removeHookForTarget(name, scope, target) {
|
|
|
5666
5806
|
}
|
|
5667
5807
|
function readSettingsAt(path) {
|
|
5668
5808
|
try {
|
|
5669
|
-
if (!
|
|
5809
|
+
if (!existsSync11(path))
|
|
5670
5810
|
return;
|
|
5671
|
-
const raw =
|
|
5811
|
+
const raw = readFileSync5(path, "utf-8");
|
|
5672
5812
|
return { raw, settings: JSON.parse(raw) };
|
|
5673
5813
|
} catch {
|
|
5674
5814
|
return;
|
|
@@ -5701,7 +5841,9 @@ function pruneStaleRegistrations(scope = "global") {
|
|
|
5701
5841
|
commands.add(finding.command);
|
|
5702
5842
|
staleCommandsByEvent.set(finding.event, commands);
|
|
5703
5843
|
}
|
|
5704
|
-
const settings = parsed.settings;
|
|
5844
|
+
const settings = target === "codex" ? readCodexSettings(path) : parsed.settings;
|
|
5845
|
+
if (target === "codex" && readFileSync5(path, "utf8") !== parsed.raw)
|
|
5846
|
+
throw new Error("Codex settings changed before pruning; retry.");
|
|
5705
5847
|
for (const [eventKey, commands] of staleCommandsByEvent) {
|
|
5706
5848
|
const entries = settings.hooks?.[eventKey];
|
|
5707
5849
|
if (!Array.isArray(entries))
|
|
@@ -5726,7 +5868,7 @@ function pruneStaleRegistrations(scope = "global") {
|
|
|
5726
5868
|
if (settings.hooks && Object.keys(settings.hooks).length === 0)
|
|
5727
5869
|
delete settings.hooks;
|
|
5728
5870
|
const backupPath = `${path}.bak`;
|
|
5729
|
-
|
|
5871
|
+
writeFileSync4(backupPath, parsed.raw);
|
|
5730
5872
|
writeSettings(settings, scope, target);
|
|
5731
5873
|
results.push({ file: path, removed: stale, backupPath });
|
|
5732
5874
|
}
|
|
@@ -5788,10 +5930,10 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
5788
5930
|
}
|
|
5789
5931
|
function codewithHasHookEntry(name, scope = "global") {
|
|
5790
5932
|
const path = getSettingsPath(scope, "codewith");
|
|
5791
|
-
if (!
|
|
5933
|
+
if (!existsSync11(path))
|
|
5792
5934
|
return false;
|
|
5793
5935
|
try {
|
|
5794
|
-
return codewithHasHookEntryInText(
|
|
5936
|
+
return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
|
|
5795
5937
|
} catch {
|
|
5796
5938
|
return false;
|
|
5797
5939
|
}
|
|
@@ -5805,7 +5947,7 @@ function removeStoreDirFailClosed(dir) {
|
|
|
5805
5947
|
}
|
|
5806
5948
|
for (const entry of entries) {
|
|
5807
5949
|
if (!entry.isDirectory()) {
|
|
5808
|
-
|
|
5950
|
+
unlinkSync2(join10(dir, entry.name));
|
|
5809
5951
|
}
|
|
5810
5952
|
}
|
|
5811
5953
|
rmdirSync(dir);
|
|
@@ -5815,14 +5957,14 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5815
5957
|
const custom = readCustomManifest(shortName);
|
|
5816
5958
|
const bundledMeta = custom ? undefined : getHook(shortName);
|
|
5817
5959
|
const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
|
|
5818
|
-
const registeredClaudeOrGemini = target === "all" ?
|
|
5960
|
+
const registeredClaudeOrGemini = target === "all" ? JSON_SETTINGS_TARGETS.some(existsInTarget) : false;
|
|
5819
5961
|
const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
|
|
5820
5962
|
const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
|
|
5821
5963
|
const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
|
|
5822
5964
|
if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
|
|
5823
5965
|
return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
|
|
5824
5966
|
}
|
|
5825
|
-
const writableTargets = target === "all" ?
|
|
5967
|
+
const writableTargets = target === "all" ? JSON_SETTINGS_TARGETS : target === "codewith" ? [] : [target];
|
|
5826
5968
|
const scopes = [];
|
|
5827
5969
|
for (const s of ["global", "project"]) {
|
|
5828
5970
|
for (const t of writableTargets) {
|
|
@@ -5836,11 +5978,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5836
5978
|
const registrationsRemaining = [];
|
|
5837
5979
|
if (target === "codewith" || target === "all") {
|
|
5838
5980
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
5839
|
-
if (
|
|
5840
|
-
const before =
|
|
5981
|
+
if (existsSync11(codewithPath)) {
|
|
5982
|
+
const before = readFileSync5(codewithPath, "utf-8");
|
|
5841
5983
|
const after = removeCodewithHookEntry(before, shortName);
|
|
5842
5984
|
if (after.removed) {
|
|
5843
|
-
|
|
5985
|
+
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
5844
5986
|
}
|
|
5845
5987
|
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
5846
5988
|
registrationsRemaining.push("codewith");
|
|
@@ -5883,8 +6025,9 @@ var init_installer = __esm(() => {
|
|
|
5883
6025
|
init_manifest();
|
|
5884
6026
|
init_registration();
|
|
5885
6027
|
init_store();
|
|
5886
|
-
|
|
5887
|
-
|
|
6028
|
+
init_codex_settings();
|
|
6029
|
+
__dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
6030
|
+
HOOKS_DIR = existsSync11(join10(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join10(__dirname2, "..", "..", "hooks") : join10(__dirname2, "..", "hooks");
|
|
5888
6031
|
EVENT_MAP = {
|
|
5889
6032
|
claude: {
|
|
5890
6033
|
PreToolUse: "PreToolUse",
|
|
@@ -5893,7 +6036,7 @@ var init_installer = __esm(() => {
|
|
|
5893
6036
|
Notification: "Notification",
|
|
5894
6037
|
SessionStart: "SessionStart",
|
|
5895
6038
|
SessionEnd: "SessionEnd",
|
|
5896
|
-
UserPromptSubmit:
|
|
6039
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
5897
6040
|
SubagentStart: null
|
|
5898
6041
|
},
|
|
5899
6042
|
gemini: {
|
|
@@ -5915,10 +6058,20 @@ var init_installer = __esm(() => {
|
|
|
5915
6058
|
SessionEnd: null,
|
|
5916
6059
|
UserPromptSubmit: "UserPromptSubmit",
|
|
5917
6060
|
SubagentStart: "SubagentStart"
|
|
6061
|
+
},
|
|
6062
|
+
codex: {
|
|
6063
|
+
PreToolUse: "PreToolUse",
|
|
6064
|
+
PostToolUse: "PostToolUse",
|
|
6065
|
+
Stop: "Stop",
|
|
6066
|
+
Notification: null,
|
|
6067
|
+
SessionStart: "SessionStart",
|
|
6068
|
+
SessionEnd: "SessionEnd",
|
|
6069
|
+
UserPromptSubmit: "UserPromptSubmit",
|
|
6070
|
+
SubagentStart: "SubagentStart"
|
|
5918
6071
|
}
|
|
5919
6072
|
};
|
|
5920
6073
|
getInstalledHooks = getRegisteredHooks;
|
|
5921
|
-
JSON_SETTINGS_TARGETS = ["claude", "gemini"];
|
|
6074
|
+
JSON_SETTINGS_TARGETS = ["claude", "gemini", "codex"];
|
|
5922
6075
|
});
|
|
5923
6076
|
|
|
5924
6077
|
// src/lib/redact.ts
|
|
@@ -5987,21 +6140,21 @@ var init_redact = __esm(() => {
|
|
|
5987
6140
|
});
|
|
5988
6141
|
|
|
5989
6142
|
// src/lib/profiles.ts
|
|
5990
|
-
import { existsSync as
|
|
6143
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
5991
6144
|
import { join as join11 } from "path";
|
|
5992
6145
|
function getProfilesDir() {
|
|
5993
6146
|
const newDir = join11(getEffectiveDataRoot(), "profiles");
|
|
5994
6147
|
const oldDir = join11(getHomeDir(), ".hooks", "profiles");
|
|
5995
|
-
if (!
|
|
5996
|
-
|
|
6148
|
+
if (!existsSync12(newDir) && existsSync12(oldDir)) {
|
|
6149
|
+
mkdirSync6(newDir, { recursive: true });
|
|
5997
6150
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
5998
6151
|
}
|
|
5999
6152
|
return newDir;
|
|
6000
6153
|
}
|
|
6001
6154
|
function ensureProfilesDir() {
|
|
6002
6155
|
const dir = getProfilesDir();
|
|
6003
|
-
if (!
|
|
6004
|
-
|
|
6156
|
+
if (!existsSync12(dir)) {
|
|
6157
|
+
mkdirSync6(dir, { recursive: true });
|
|
6005
6158
|
}
|
|
6006
6159
|
}
|
|
6007
6160
|
function profilePath(id) {
|
|
@@ -6024,30 +6177,30 @@ function createProfile(input) {
|
|
|
6024
6177
|
if (input.name) {
|
|
6025
6178
|
profile.name = input.name;
|
|
6026
6179
|
}
|
|
6027
|
-
|
|
6180
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
6028
6181
|
`);
|
|
6029
6182
|
return profile;
|
|
6030
6183
|
}
|
|
6031
6184
|
function getProfile(id) {
|
|
6032
6185
|
const path = profilePath(id);
|
|
6033
6186
|
try {
|
|
6034
|
-
if (!
|
|
6187
|
+
if (!existsSync12(path))
|
|
6035
6188
|
return null;
|
|
6036
|
-
return JSON.parse(
|
|
6189
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
6037
6190
|
} catch {
|
|
6038
6191
|
return null;
|
|
6039
6192
|
}
|
|
6040
6193
|
}
|
|
6041
6194
|
function listProfiles() {
|
|
6042
6195
|
const dir = getProfilesDir();
|
|
6043
|
-
if (!
|
|
6196
|
+
if (!existsSync12(dir))
|
|
6044
6197
|
return [];
|
|
6045
6198
|
try {
|
|
6046
6199
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
6047
6200
|
const profiles = [];
|
|
6048
6201
|
for (const file of files) {
|
|
6049
6202
|
try {
|
|
6050
|
-
const content =
|
|
6203
|
+
const content = readFileSync6(join11(dir, file), "utf-8");
|
|
6051
6204
|
profiles.push(JSON.parse(content));
|
|
6052
6205
|
} catch {}
|
|
6053
6206
|
}
|
|
@@ -6061,7 +6214,7 @@ function touchProfile(id) {
|
|
|
6061
6214
|
if (!profile)
|
|
6062
6215
|
return;
|
|
6063
6216
|
profile.last_seen_at = new Date().toISOString();
|
|
6064
|
-
|
|
6217
|
+
writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
6065
6218
|
`);
|
|
6066
6219
|
}
|
|
6067
6220
|
function exportProfiles() {
|
|
@@ -6077,11 +6230,11 @@ function importProfiles(profiles) {
|
|
|
6077
6230
|
continue;
|
|
6078
6231
|
}
|
|
6079
6232
|
const path = profilePath(profile.agent_id);
|
|
6080
|
-
if (
|
|
6233
|
+
if (existsSync12(path)) {
|
|
6081
6234
|
skipped++;
|
|
6082
6235
|
continue;
|
|
6083
6236
|
}
|
|
6084
|
-
|
|
6237
|
+
writeFileSync5(path, JSON.stringify(profile, null, 2) + `
|
|
6085
6238
|
`);
|
|
6086
6239
|
imported++;
|
|
6087
6240
|
}
|
|
@@ -6091,10 +6244,10 @@ var init_profiles = __esm(() => {
|
|
|
6091
6244
|
init_app_home();
|
|
6092
6245
|
});
|
|
6093
6246
|
|
|
6094
|
-
//
|
|
6247
|
+
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
|
|
6095
6248
|
import { isIP as isIP2 } from "net";
|
|
6096
6249
|
import { spawnSync } from "child_process";
|
|
6097
|
-
import { closeSync, fstatSync, openSync, readFileSync as
|
|
6250
|
+
import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
|
|
6098
6251
|
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
6099
6252
|
import { createRequire as createRequire2 } from "module";
|
|
6100
6253
|
import { hostname as osHostname } from "os";
|
|
@@ -6198,7 +6351,7 @@ function readAppConfigFile(path) {
|
|
|
6198
6351
|
};
|
|
6199
6352
|
let fd = -1;
|
|
6200
6353
|
try {
|
|
6201
|
-
fd =
|
|
6354
|
+
fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
6202
6355
|
} catch (error) {
|
|
6203
6356
|
const code = error.code;
|
|
6204
6357
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -6219,7 +6372,7 @@ function readAppConfigFile(path) {
|
|
|
6219
6372
|
unsafe("the file is not owned by the current user");
|
|
6220
6373
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
|
|
6221
6374
|
unsafe("the file exceeds the size limit");
|
|
6222
|
-
const bytes =
|
|
6375
|
+
const bytes = readFileSync7(fd);
|
|
6223
6376
|
const after = fstatSync(fd);
|
|
6224
6377
|
if (!configFileReadsCoherent(before, after)) {
|
|
6225
6378
|
unsafe("the file changed while being read");
|
|
@@ -6227,7 +6380,7 @@ function readAppConfigFile(path) {
|
|
|
6227
6380
|
return parseEnvFile(bytes.toString("utf8"));
|
|
6228
6381
|
} finally {
|
|
6229
6382
|
if (fd !== -1)
|
|
6230
|
-
|
|
6383
|
+
closeSync2(fd);
|
|
6231
6384
|
}
|
|
6232
6385
|
}
|
|
6233
6386
|
function readCredentialFile(path, apiKeyKeys) {
|
|
@@ -6771,7 +6924,7 @@ function credentialDiskSourcesForMessage(name, env) {
|
|
|
6771
6924
|
const paths = credentialDiskSources(name, env);
|
|
6772
6925
|
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
6773
6926
|
}
|
|
6774
|
-
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2,
|
|
6927
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
|
|
6775
6928
|
var init_transport = __esm(() => {
|
|
6776
6929
|
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
6777
6930
|
appName;
|
|
@@ -6800,7 +6953,7 @@ var init_transport = __esm(() => {
|
|
|
6800
6953
|
INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
6801
6954
|
CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
6802
6955
|
AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
6803
|
-
|
|
6956
|
+
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
6804
6957
|
requireSecretsSdk2 = createRequire2(import.meta.url);
|
|
6805
6958
|
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
6806
6959
|
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -6844,6 +6997,15 @@ function hasHooksEnvAuthorityIntent(env = process.env) {
|
|
|
6844
6997
|
function selectsHooksLocalStore(env = process.env) {
|
|
6845
6998
|
return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
|
|
6846
6999
|
}
|
|
7000
|
+
function hooksFailClosedLine(detail) {
|
|
7001
|
+
return `REMOTE_API_CONFIG_MISSING: hooks: no registry credential resolved from ${HOOKS_CREDENTIAL_TIERS} ` + `(with HASNA_HOOKS_API_URL or the fleet gateway default), and local mode is not enabled ` + `(${HOOKS_LOCAL_OPT_IN_HINT}) \u2014 refusing to silently fall back to local storage.` + (detail ? ` ${detail}` : "");
|
|
7002
|
+
}
|
|
7003
|
+
function hooksHostedRouteLocalStoreRefusal(authority) {
|
|
7004
|
+
return "REMOTE_COMMAND_UNSUPPORTED: this command is local-only \u2014 it reads the on-box SQLite store " + `(hooks.db), which the hosted hooks registry authority (${authority}) does not serve; local SQLite ` + `is opt-in only (${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default \u2014 refusing to open a local ` + "store on the hosted route.";
|
|
7005
|
+
}
|
|
7006
|
+
function hooksEventSinkRefusal() {
|
|
7007
|
+
return "REMOTE_COMMAND_UNSUPPORTED: hook event not recorded \u2014 the hook event log is local-only (on-box " + "SQLite hooks.db) and the hosted hooks registry has no event route; local SQLite is opt-in only " + `(${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default. Nothing was written.`;
|
|
7008
|
+
}
|
|
6847
7009
|
function hooksResolverEnv(env) {
|
|
6848
7010
|
const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
6849
7011
|
if (blanks.length === 0)
|
|
@@ -6868,7 +7030,7 @@ function hooksResolverInputs(env, credentials = {}) {
|
|
|
6868
7030
|
}
|
|
6869
7031
|
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
6870
7032
|
}
|
|
6871
|
-
var HOOKS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
7033
|
+
var HOOKS_LOCAL_OPT_IN_ENV_KEYS, HOOKS_CREDENTIAL_TIERS = "the Keychain item hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY", HOOKS_LOCAL_OPT_IN_HINT = "HASNA_HOOKS_LOCAL=1 (alias HOOKS_LOCAL=1)", CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
6872
7034
|
var init_local_opt_in = __esm(() => {
|
|
6873
7035
|
init_transport();
|
|
6874
7036
|
HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
|
|
@@ -6971,14 +7133,35 @@ var init_transport2 = __esm(() => {
|
|
|
6971
7133
|
var exports_db_writer = {};
|
|
6972
7134
|
__export(exports_db_writer, {
|
|
6973
7135
|
writeHookEvent: () => writeHookEvent,
|
|
7136
|
+
resolveHookEventSink: () => resolveHookEventSink,
|
|
6974
7137
|
resolveEventType: () => resolveEventType,
|
|
6975
7138
|
recordHookRun: () => recordHookRun,
|
|
6976
|
-
normalizeEventType: () => normalizeEventType
|
|
7139
|
+
normalizeEventType: () => normalizeEventType,
|
|
7140
|
+
__resetHookEventSinkNotice: () => __resetHookEventSinkNotice
|
|
6977
7141
|
});
|
|
7142
|
+
function resolveHookEventSink(env = process.env) {
|
|
7143
|
+
if (isLocalStoreRefused())
|
|
7144
|
+
return { kind: "refused", reason: localStoreRefusalMessage() ?? hooksEventSinkRefusal() };
|
|
7145
|
+
if (selectsHooksLocalStore(env))
|
|
7146
|
+
return { kind: "local" };
|
|
7147
|
+
return { kind: "refused", reason: hooksEventSinkRefusal() };
|
|
7148
|
+
}
|
|
7149
|
+
function __resetHookEventSinkNotice() {
|
|
7150
|
+
sinkRefusalPrinted = false;
|
|
7151
|
+
}
|
|
6978
7152
|
function nanoid2() {
|
|
6979
7153
|
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
|
|
6980
7154
|
}
|
|
6981
7155
|
function writeHookEvent(event) {
|
|
7156
|
+
const sink = resolveHookEventSink();
|
|
7157
|
+
if (sink.kind !== "local") {
|
|
7158
|
+
if (!sinkRefusalPrinted) {
|
|
7159
|
+
sinkRefusalPrinted = true;
|
|
7160
|
+
process.stderr.write(`[hooks db-writer] ${sink.reason}
|
|
7161
|
+
`);
|
|
7162
|
+
}
|
|
7163
|
+
return;
|
|
7164
|
+
}
|
|
6982
7165
|
try {
|
|
6983
7166
|
const db = getDb();
|
|
6984
7167
|
const id = nanoid2();
|
|
@@ -7047,9 +7230,10 @@ function recordHookRun(record) {
|
|
|
7047
7230
|
metadata: JSON.stringify(metadata)
|
|
7048
7231
|
});
|
|
7049
7232
|
}
|
|
7050
|
-
var ALLOWED_EVENT_TYPES;
|
|
7233
|
+
var sinkRefusalPrinted = false, ALLOWED_EVENT_TYPES;
|
|
7051
7234
|
var init_db_writer = __esm(() => {
|
|
7052
7235
|
init_db();
|
|
7236
|
+
init_local_opt_in();
|
|
7053
7237
|
init_redact();
|
|
7054
7238
|
ALLOWED_EVENT_TYPES = new Set([
|
|
7055
7239
|
"PreToolUse",
|
|
@@ -7064,8 +7248,9 @@ var init_db_writer = __esm(() => {
|
|
|
7064
7248
|
});
|
|
7065
7249
|
|
|
7066
7250
|
// src/lib/hook-env.ts
|
|
7067
|
-
import { dirname as
|
|
7068
|
-
import { existsSync as
|
|
7251
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
|
|
7252
|
+
import { existsSync as existsSync13, realpathSync as realpathSync2, statSync } from "fs";
|
|
7253
|
+
import { tmpdir } from "os";
|
|
7069
7254
|
function isDeniedEnvName(name) {
|
|
7070
7255
|
const upper = name.toUpperCase();
|
|
7071
7256
|
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
@@ -7091,6 +7276,19 @@ function isUnsafePathEntry(entry, home) {
|
|
|
7091
7276
|
return true;
|
|
7092
7277
|
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
7093
7278
|
return true;
|
|
7279
|
+
const canonical = (path) => {
|
|
7280
|
+
try {
|
|
7281
|
+
return realpathSync2(path);
|
|
7282
|
+
} catch {
|
|
7283
|
+
return path;
|
|
7284
|
+
}
|
|
7285
|
+
};
|
|
7286
|
+
const resolved = canonical(entry);
|
|
7287
|
+
for (const root of [home, tmpdir(), "/tmp", "/var/tmp"].filter((value) => Boolean(value))) {
|
|
7288
|
+
const blocked = canonical(root);
|
|
7289
|
+
if (resolved === blocked || resolved.startsWith(blocked + "/"))
|
|
7290
|
+
return true;
|
|
7291
|
+
}
|
|
7094
7292
|
try {
|
|
7095
7293
|
if ((statSync(entry).mode & 2) !== 0)
|
|
7096
7294
|
return true;
|
|
@@ -7099,9 +7297,9 @@ function isUnsafePathEntry(entry, home) {
|
|
|
7099
7297
|
}
|
|
7100
7298
|
function sanitizeHookPath(pathValue, home) {
|
|
7101
7299
|
const baseline = PATH_BASELINE.slice();
|
|
7102
|
-
if (process.platform === "darwin" &&
|
|
7300
|
+
if (process.platform === "darwin" && existsSync13("/opt/homebrew/bin"))
|
|
7103
7301
|
baseline.push("/opt/homebrew/bin");
|
|
7104
|
-
const bunDir =
|
|
7302
|
+
const bunDir = dirname5(process.execPath);
|
|
7105
7303
|
const seen = new Set;
|
|
7106
7304
|
const out = [];
|
|
7107
7305
|
for (const dir of baseline) {
|
|
@@ -7210,6 +7408,7 @@ var init_hook_env = __esm(() => {
|
|
|
7210
7408
|
DENY_CONTAINS = ["DATABASE_URL"];
|
|
7211
7409
|
PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
|
|
7212
7410
|
CONFIG_PROJECTIONS = [
|
|
7411
|
+
["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"],
|
|
7213
7412
|
["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
|
|
7214
7413
|
["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
|
|
7215
7414
|
["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
|
|
@@ -7224,8 +7423,8 @@ __export(exports_run, {
|
|
|
7224
7423
|
HookTimeoutError: () => HookTimeoutError
|
|
7225
7424
|
});
|
|
7226
7425
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
7227
|
-
import { rmSync as rmSync2, writeFileSync as
|
|
7228
|
-
import { basename as basename2, dirname as
|
|
7426
|
+
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
7427
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join13 } from "path";
|
|
7229
7428
|
function extensionOf(scriptPath) {
|
|
7230
7429
|
const base = basename2(scriptPath);
|
|
7231
7430
|
const dot = base.lastIndexOf(".");
|
|
@@ -7294,7 +7493,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
7294
7493
|
return chunks.join("");
|
|
7295
7494
|
}
|
|
7296
7495
|
async function executeVerifiedScript(options) {
|
|
7297
|
-
const scriptDir =
|
|
7496
|
+
const scriptDir = dirname6(options.scriptPath);
|
|
7298
7497
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
7299
7498
|
const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
7300
7499
|
let proc = null;
|
|
@@ -7304,7 +7503,7 @@ async function executeVerifiedScript(options) {
|
|
|
7304
7503
|
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
7305
7504
|
}, options.timeout) : null;
|
|
7306
7505
|
try {
|
|
7307
|
-
|
|
7506
|
+
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
7308
7507
|
proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
7309
7508
|
stdin: new Response(options.stdin),
|
|
7310
7509
|
stdout: "pipe",
|
|
@@ -7387,15 +7586,15 @@ __export(exports_custom_install, {
|
|
|
7387
7586
|
installCustomSource: () => installCustomSource,
|
|
7388
7587
|
ensureCustomDir: () => ensureCustomDir
|
|
7389
7588
|
});
|
|
7390
|
-
import { existsSync as
|
|
7589
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
|
|
7391
7590
|
import { join as join14 } from "path";
|
|
7392
|
-
import { tmpdir } from "os";
|
|
7591
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
7393
7592
|
function manifestAt(dir) {
|
|
7394
7593
|
const path = join14(dir, "manifest.json");
|
|
7395
|
-
if (!
|
|
7594
|
+
if (!existsSync14(path))
|
|
7396
7595
|
return;
|
|
7397
7596
|
try {
|
|
7398
|
-
return parseManifest(
|
|
7597
|
+
return parseManifest(readFileSync8(path, "utf-8"));
|
|
7399
7598
|
} catch {
|
|
7400
7599
|
return;
|
|
7401
7600
|
}
|
|
@@ -7414,7 +7613,7 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
7414
7613
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
7415
7614
|
}
|
|
7416
7615
|
async function cloneToTemp(gitUrl) {
|
|
7417
|
-
const tmp = mkdtempSync(join14(
|
|
7616
|
+
const tmp = mkdtempSync(join14(tmpdir2(), "hooks-install-"));
|
|
7418
7617
|
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join14(tmp, "repo")], {
|
|
7419
7618
|
stdout: "pipe",
|
|
7420
7619
|
stderr: "pipe"
|
|
@@ -7438,7 +7637,7 @@ function isCustomSource(value) {
|
|
|
7438
7637
|
return true;
|
|
7439
7638
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
7440
7639
|
return true;
|
|
7441
|
-
return
|
|
7640
|
+
return existsSync14(value);
|
|
7442
7641
|
}
|
|
7443
7642
|
async function installCustomSource(source) {
|
|
7444
7643
|
if (isGitUrl(source)) {
|
|
@@ -7452,11 +7651,11 @@ async function installCustomSource(source) {
|
|
|
7452
7651
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
7453
7652
|
return installManifestUrl(source);
|
|
7454
7653
|
}
|
|
7455
|
-
if (!
|
|
7654
|
+
if (!existsSync14(source)) {
|
|
7456
7655
|
throw new Error(`No such local path: ${source}`);
|
|
7457
7656
|
}
|
|
7458
7657
|
if (source.endsWith("manifest.json")) {
|
|
7459
|
-
const manifest = parseManifest(
|
|
7658
|
+
const manifest = parseManifest(readFileSync8(source, "utf-8"));
|
|
7460
7659
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
7461
7660
|
}
|
|
7462
7661
|
return copyManifestDir(source, source, "local");
|
|
@@ -7487,7 +7686,7 @@ async function installManifestUrl(url) {
|
|
|
7487
7686
|
}
|
|
7488
7687
|
function ensureCustomDir() {
|
|
7489
7688
|
const dir = getCustomHooksDir();
|
|
7490
|
-
|
|
7689
|
+
mkdirSync7(dir, { recursive: true });
|
|
7491
7690
|
return dir;
|
|
7492
7691
|
}
|
|
7493
7692
|
var init_custom_install = __esm(() => {
|
|
@@ -7501,16 +7700,17 @@ __export(exports_sync, {
|
|
|
7501
7700
|
syncHooks: () => syncHooks,
|
|
7502
7701
|
stageSyncArtifacts: () => stageSyncArtifacts,
|
|
7503
7702
|
planSync: () => planSync,
|
|
7703
|
+
installPinnedFromBundled: () => installPinnedFromBundled,
|
|
7504
7704
|
fetchPinnedHook: () => fetchPinnedHook,
|
|
7505
7705
|
commitSyncArtifacts: () => commitSyncArtifacts
|
|
7506
7706
|
});
|
|
7507
7707
|
import { createHash as createHash2 } from "crypto";
|
|
7508
|
-
import { readFileSync as
|
|
7708
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
7509
7709
|
function sha256OfText(text) {
|
|
7510
7710
|
return createHash2("sha256").update(text).digest("hex");
|
|
7511
7711
|
}
|
|
7512
7712
|
function sha256OfPath(path) {
|
|
7513
|
-
return createHash2("sha256").update(
|
|
7713
|
+
return createHash2("sha256").update(readFileSync9(path)).digest("hex");
|
|
7514
7714
|
}
|
|
7515
7715
|
function collectBundledCatalog() {
|
|
7516
7716
|
const out = [];
|
|
@@ -7664,7 +7864,7 @@ async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
|
|
|
7664
7864
|
return staged;
|
|
7665
7865
|
}
|
|
7666
7866
|
async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
7667
|
-
const db = getDb();
|
|
7867
|
+
const db = isLocalStoreRefused() ? null : getDb();
|
|
7668
7868
|
for (const item of staged) {
|
|
7669
7869
|
const entry = remoteLock.hooks[item.name];
|
|
7670
7870
|
if (!entry)
|
|
@@ -7684,6 +7884,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
|
7684
7884
|
lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
|
|
7685
7885
|
}
|
|
7686
7886
|
writeLock(lock);
|
|
7887
|
+
if (!db)
|
|
7888
|
+
return;
|
|
7687
7889
|
db.exec("BEGIN");
|
|
7688
7890
|
try {
|
|
7689
7891
|
for (const item of staged) {
|
|
@@ -7746,23 +7948,56 @@ async function fetchPinnedHook(name, version, apiUrl, apiKey) {
|
|
|
7746
7948
|
if (actual !== expectedSha) {
|
|
7747
7949
|
throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
|
|
7748
7950
|
}
|
|
7749
|
-
const db = getDb();
|
|
7750
7951
|
setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
|
|
7751
|
-
|
|
7952
|
+
if (!isLocalStoreRefused()) {
|
|
7953
|
+
upsertHookRecord(getDb(), {
|
|
7954
|
+
name,
|
|
7955
|
+
version,
|
|
7956
|
+
sha256: expectedSha,
|
|
7957
|
+
source_type: entry.source ?? "remote",
|
|
7958
|
+
source_ref: apiUrl,
|
|
7959
|
+
last_verified_at: new Date().toISOString()
|
|
7960
|
+
});
|
|
7961
|
+
}
|
|
7962
|
+
return {
|
|
7752
7963
|
name,
|
|
7753
7964
|
version,
|
|
7754
7965
|
sha256: expectedSha,
|
|
7755
|
-
|
|
7966
|
+
source: entry.source ?? "remote",
|
|
7756
7967
|
source_ref: apiUrl,
|
|
7968
|
+
artifact,
|
|
7969
|
+
scriptPath
|
|
7970
|
+
};
|
|
7971
|
+
}
|
|
7972
|
+
async function installPinnedFromBundled(name, version) {
|
|
7973
|
+
const bundled = collectBundledCatalog().find((entry) => entry.name === name);
|
|
7974
|
+
if (!bundled) {
|
|
7975
|
+
throw new Error(`Hook '${name}' is not in the bundled registry`);
|
|
7976
|
+
}
|
|
7977
|
+
if (bundled.version !== version) {
|
|
7978
|
+
throw new Error(`Hook '${name}' version ${version} is not in the bundled registry (available: ${bundled.version})`);
|
|
7979
|
+
}
|
|
7980
|
+
const scriptPath = resolveScriptPath(name);
|
|
7981
|
+
if (!scriptPath) {
|
|
7982
|
+
throw new Error(`Hook '${name}' has no script in the bundled registry`);
|
|
7983
|
+
}
|
|
7984
|
+
const db = getDb();
|
|
7985
|
+
setPinnedHook(name, { version, sha256: bundled.sha256, source: "bundled", pinned: true });
|
|
7986
|
+
upsertHookRecord(db, {
|
|
7987
|
+
name,
|
|
7988
|
+
version,
|
|
7989
|
+
sha256: bundled.sha256,
|
|
7990
|
+
source_type: "bundled",
|
|
7991
|
+
source_ref: "bundled registry",
|
|
7757
7992
|
last_verified_at: new Date().toISOString()
|
|
7758
7993
|
});
|
|
7759
7994
|
return {
|
|
7760
7995
|
name,
|
|
7761
7996
|
version,
|
|
7762
|
-
sha256:
|
|
7763
|
-
source:
|
|
7764
|
-
source_ref:
|
|
7765
|
-
artifact,
|
|
7997
|
+
sha256: bundled.sha256,
|
|
7998
|
+
source: "bundled",
|
|
7999
|
+
source_ref: "bundled registry",
|
|
8000
|
+
artifact: null,
|
|
7766
8001
|
scriptPath
|
|
7767
8002
|
};
|
|
7768
8003
|
}
|
|
@@ -8425,13 +8660,13 @@ __export(exports_serve, {
|
|
|
8425
8660
|
SERVE_HOST: () => SERVE_HOST,
|
|
8426
8661
|
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
8427
8662
|
});
|
|
8428
|
-
import { readFileSync as
|
|
8429
|
-
import { dirname as
|
|
8663
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
8664
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
8430
8665
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8431
8666
|
function packageVersion() {
|
|
8432
8667
|
try {
|
|
8433
|
-
const pkgPath = join15(
|
|
8434
|
-
return JSON.parse(
|
|
8668
|
+
const pkgPath = join15(dirname7(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
8669
|
+
return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
|
|
8435
8670
|
} catch {
|
|
8436
8671
|
return "0.0.0";
|
|
8437
8672
|
}
|
|
@@ -8595,7 +8830,7 @@ function startServeServer(options) {
|
|
|
8595
8830
|
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
8596
8831
|
}
|
|
8597
8832
|
});
|
|
8598
|
-
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
8833
|
+
console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);
|
|
8599
8834
|
return server;
|
|
8600
8835
|
}
|
|
8601
8836
|
var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
|
|
@@ -8691,6 +8926,32 @@ async function provisionCloudflareResources(options) {
|
|
|
8691
8926
|
}
|
|
8692
8927
|
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
8693
8928
|
|
|
8929
|
+
// src/mcp/authority.ts
|
|
8930
|
+
var exports_authority = {};
|
|
8931
|
+
__export(exports_authority, {
|
|
8932
|
+
resolveHooksMcpAuthority: () => resolveHooksMcpAuthority,
|
|
8933
|
+
decideHooksMcpAuthority: () => decideHooksMcpAuthority
|
|
8934
|
+
});
|
|
8935
|
+
function resolveHooksMcpAuthority(env = process.env) {
|
|
8936
|
+
const transport = resolveHooksTransport(env);
|
|
8937
|
+
if (transport.mode === "local" || !transport.authority) {
|
|
8938
|
+
return { route: "local", source: "local-opt-in" };
|
|
8939
|
+
}
|
|
8940
|
+
return { route: "hosted", v1BaseUrl: transport.authority.v1BaseUrl, source: transport.source };
|
|
8941
|
+
}
|
|
8942
|
+
function decideHooksMcpAuthority(env = process.env) {
|
|
8943
|
+
const authority = resolveHooksMcpAuthority(env);
|
|
8944
|
+
if (authority.route === "hosted") {
|
|
8945
|
+
refuseLocalStore(hooksHostedRouteLocalStoreRefusal(authority.v1BaseUrl));
|
|
8946
|
+
}
|
|
8947
|
+
return authority;
|
|
8948
|
+
}
|
|
8949
|
+
var init_authority = __esm(() => {
|
|
8950
|
+
init_db();
|
|
8951
|
+
init_local_opt_in();
|
|
8952
|
+
init_transport2();
|
|
8953
|
+
});
|
|
8954
|
+
|
|
8694
8955
|
// src/mcp/server.ts
|
|
8695
8956
|
var exports_server = {};
|
|
8696
8957
|
__export(exports_server, {
|
|
@@ -8706,17 +8967,17 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
8706
8967
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8707
8968
|
import { z as z2 } from "zod";
|
|
8708
8969
|
import { createServer } from "http";
|
|
8709
|
-
import { existsSync as
|
|
8710
|
-
import { join as join16, dirname as
|
|
8970
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
|
|
8971
|
+
import { join as join16, dirname as dirname8 } from "path";
|
|
8711
8972
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8712
8973
|
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
8713
8974
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
8714
8975
|
const resolved = resolveHook2(name);
|
|
8715
8976
|
const hookScript = resolveScriptPath2(name);
|
|
8716
|
-
if (!resolved || !hookScript || !
|
|
8977
|
+
if (!resolved || !hookScript || !existsSync15(hookScript)) {
|
|
8717
8978
|
throw new Error(`Hook '${name}' not found`);
|
|
8718
8979
|
}
|
|
8719
|
-
const content =
|
|
8980
|
+
const content = readFileSync11(hookScript);
|
|
8720
8981
|
const sha = sha256Of(content);
|
|
8721
8982
|
const check = checkScriptHash(name, sha);
|
|
8722
8983
|
if (!check.ok) {
|
|
@@ -8921,7 +9182,7 @@ function createHooksServer() {
|
|
|
8921
9182
|
const settingsPath = getSettingsPath(scope);
|
|
8922
9183
|
const issues = [];
|
|
8923
9184
|
const healthy = [];
|
|
8924
|
-
const settingsExist =
|
|
9185
|
+
const settingsExist = existsSync15(settingsPath);
|
|
8925
9186
|
if (!settingsExist) {
|
|
8926
9187
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
8927
9188
|
}
|
|
@@ -8931,7 +9192,7 @@ function createHooksServer() {
|
|
|
8931
9192
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
8932
9193
|
let hookHealthy = true;
|
|
8933
9194
|
if (custom) {
|
|
8934
|
-
if (!
|
|
9195
|
+
if (!existsSync15(custom.scriptPath)) {
|
|
8935
9196
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
8936
9197
|
hookHealthy = false;
|
|
8937
9198
|
}
|
|
@@ -8941,14 +9202,14 @@ function createHooksServer() {
|
|
|
8941
9202
|
continue;
|
|
8942
9203
|
}
|
|
8943
9204
|
const hookDir = getHookPath(name);
|
|
8944
|
-
if (!
|
|
9205
|
+
if (!existsSync15(join16(hookDir, "src", "hook.ts"))) {
|
|
8945
9206
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
8946
9207
|
hookHealthy = false;
|
|
8947
9208
|
}
|
|
8948
9209
|
}
|
|
8949
9210
|
if (meta && settingsExist) {
|
|
8950
9211
|
try {
|
|
8951
|
-
const settings = JSON.parse(
|
|
9212
|
+
const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
8952
9213
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
8953
9214
|
const eventName = meta.event.split(":")[0];
|
|
8954
9215
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -8987,8 +9248,8 @@ function createHooksServer() {
|
|
|
8987
9248
|
const hookPath = getHookPath(name);
|
|
8988
9249
|
const readmePath = join16(hookPath, "README.md");
|
|
8989
9250
|
let readme = "";
|
|
8990
|
-
if (
|
|
8991
|
-
readme =
|
|
9251
|
+
if (existsSync15(readmePath)) {
|
|
9252
|
+
readme = readFileSync11(readmePath, "utf-8");
|
|
8992
9253
|
}
|
|
8993
9254
|
if (verbose) {
|
|
8994
9255
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -9171,7 +9432,7 @@ function createHooksServer() {
|
|
|
9171
9432
|
const ctx = {
|
|
9172
9433
|
scope,
|
|
9173
9434
|
settings_path: settingsPath,
|
|
9174
|
-
settings_exists:
|
|
9435
|
+
settings_exists: existsSync15(settingsPath),
|
|
9175
9436
|
registered_hooks: hooks,
|
|
9176
9437
|
hook_count: hooks.length,
|
|
9177
9438
|
healthy,
|
|
@@ -9317,8 +9578,8 @@ function createHooksServer() {
|
|
|
9317
9578
|
const settingsPath = getSettingsPath(scope);
|
|
9318
9579
|
let settings = {};
|
|
9319
9580
|
try {
|
|
9320
|
-
if (
|
|
9321
|
-
settings = JSON.parse(
|
|
9581
|
+
if (existsSync15(settingsPath))
|
|
9582
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
9322
9583
|
} catch {}
|
|
9323
9584
|
if (!settings.hooks)
|
|
9324
9585
|
settings.hooks = {};
|
|
@@ -9326,10 +9587,10 @@ function createHooksServer() {
|
|
|
9326
9587
|
if (!disabled.includes(name))
|
|
9327
9588
|
disabled.push(name);
|
|
9328
9589
|
settings.hooks.__disabled = disabled;
|
|
9329
|
-
const { writeFileSync:
|
|
9330
|
-
const { dirname:
|
|
9331
|
-
|
|
9332
|
-
|
|
9590
|
+
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
|
|
9591
|
+
const { dirname: dirname9 } = await import("path");
|
|
9592
|
+
mkdirSync8(dirname9(settingsPath), { recursive: true });
|
|
9593
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
9333
9594
|
`);
|
|
9334
9595
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
9335
9596
|
});
|
|
@@ -9340,15 +9601,15 @@ function createHooksServer() {
|
|
|
9340
9601
|
const settingsPath = getSettingsPath(scope);
|
|
9341
9602
|
let settings = {};
|
|
9342
9603
|
try {
|
|
9343
|
-
if (
|
|
9344
|
-
settings = JSON.parse(
|
|
9604
|
+
if (existsSync15(settingsPath))
|
|
9605
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
9345
9606
|
} catch {}
|
|
9346
9607
|
if (settings.hooks?.__disabled) {
|
|
9347
9608
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
9348
9609
|
if (settings.hooks.__disabled.length === 0)
|
|
9349
9610
|
delete settings.hooks.__disabled;
|
|
9350
|
-
const { writeFileSync:
|
|
9351
|
-
|
|
9611
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
9612
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
9352
9613
|
`);
|
|
9353
9614
|
}
|
|
9354
9615
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
|
|
@@ -9632,6 +9893,7 @@ async function startSSEServer(options = {}) {
|
|
|
9632
9893
|
if (!loopback && !authToken) {
|
|
9633
9894
|
throw new Error(`Refusing to serve MCP SSE on non-loopback host '${host}' without an auth token. Set HASNA_HOOKS_MCP_TOKEN (or HOOKS_MCP_TOKEN) to authorize a non-loopback bind.`);
|
|
9634
9895
|
}
|
|
9896
|
+
decideHooksMcpAuthority();
|
|
9635
9897
|
const server = createHooksServer();
|
|
9636
9898
|
const transports = new Map;
|
|
9637
9899
|
function authorized2(headers) {
|
|
@@ -9681,6 +9943,13 @@ async function startSSEServer(options = {}) {
|
|
|
9681
9943
|
});
|
|
9682
9944
|
}
|
|
9683
9945
|
async function startStdioServer() {
|
|
9946
|
+
try {
|
|
9947
|
+
decideHooksMcpAuthority();
|
|
9948
|
+
} catch (err) {
|
|
9949
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
9950
|
+
`);
|
|
9951
|
+
process.exit(1);
|
|
9952
|
+
}
|
|
9684
9953
|
try {
|
|
9685
9954
|
const server = createHooksServer();
|
|
9686
9955
|
const transport = new StdioServerTransport;
|
|
@@ -9701,14 +9970,15 @@ var init_server = __esm(() => {
|
|
|
9701
9970
|
init_registration();
|
|
9702
9971
|
init_store();
|
|
9703
9972
|
init_redact();
|
|
9973
|
+
init_authority();
|
|
9704
9974
|
init_storage();
|
|
9705
|
-
__dirname3 =
|
|
9975
|
+
__dirname3 = dirname8(fileURLToPath4(import.meta.url));
|
|
9706
9976
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
9707
9977
|
try {
|
|
9708
9978
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
9709
9979
|
const p = join16(__dirname3, rel);
|
|
9710
|
-
if (
|
|
9711
|
-
pkg = JSON.parse(
|
|
9980
|
+
if (existsSync15(p)) {
|
|
9981
|
+
pkg = JSON.parse(readFileSync11(p, "utf-8"));
|
|
9712
9982
|
break;
|
|
9713
9983
|
}
|
|
9714
9984
|
}
|
|
@@ -10892,11 +11162,11 @@ function setPath(input, path, replacement) {
|
|
|
10892
11162
|
if (last && last in cursor)
|
|
10893
11163
|
cursor[last] = replacement;
|
|
10894
11164
|
}
|
|
11165
|
+
var MAX_PACKAGE_BYTES = 1024 * 1024;
|
|
10895
11166
|
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
10896
11167
|
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
10897
11168
|
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
10898
11169
|
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
10899
|
-
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
10900
11170
|
var requireSecretsSdk = createRequire(import.meta.url);
|
|
10901
11171
|
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
10902
11172
|
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
@@ -11450,8 +11720,8 @@ var {
|
|
|
11450
11720
|
|
|
11451
11721
|
// src/cli/index.tsx
|
|
11452
11722
|
import chalk2 from "chalk";
|
|
11453
|
-
import { existsSync as
|
|
11454
|
-
import { join as join17, dirname as
|
|
11723
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
|
|
11724
|
+
import { join as join17, dirname as dirname9 } from "path";
|
|
11455
11725
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
11456
11726
|
|
|
11457
11727
|
// src/cli/components/App.tsx
|
|
@@ -12293,7 +12563,7 @@ function InstallProgress({
|
|
|
12293
12563
|
onComplete
|
|
12294
12564
|
}) {
|
|
12295
12565
|
const [results, setResults] = useState6([]);
|
|
12296
|
-
const [
|
|
12566
|
+
const [current2, setCurrent] = useState6(0);
|
|
12297
12567
|
const [installing, setInstalling] = useState6(true);
|
|
12298
12568
|
useEffect5(() => {
|
|
12299
12569
|
const install = async () => {
|
|
@@ -12317,12 +12587,12 @@ function InstallProgress({
|
|
|
12317
12587
|
marginBottom: 1,
|
|
12318
12588
|
children: /* @__PURE__ */ jsxDEV6(Text9, {
|
|
12319
12589
|
bold: true,
|
|
12320
|
-
children: installing ? `Installing hooks (${
|
|
12590
|
+
children: installing ? `Installing hooks (${current2 + 1}/${hooks.length})...` : "Installation complete!"
|
|
12321
12591
|
}, undefined, false, undefined, this)
|
|
12322
12592
|
}, undefined, false, undefined, this),
|
|
12323
12593
|
hooks.map((name, i) => {
|
|
12324
12594
|
const result = results[i];
|
|
12325
|
-
const isCurrent = i ===
|
|
12595
|
+
const isCurrent = i === current2 && installing;
|
|
12326
12596
|
const meta = getHook(name);
|
|
12327
12597
|
return /* @__PURE__ */ jsxDEV6(Box7, {
|
|
12328
12598
|
flexDirection: "column",
|
|
@@ -12637,10 +12907,11 @@ init_app_home();
|
|
|
12637
12907
|
init_config();
|
|
12638
12908
|
init_local_opt_in();
|
|
12639
12909
|
init_transport2();
|
|
12910
|
+
init_db();
|
|
12640
12911
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
12641
|
-
var __dirname4 =
|
|
12642
|
-
var pkgPath =
|
|
12643
|
-
var pkg2 = JSON.parse(
|
|
12912
|
+
var __dirname4 = dirname9(fileURLToPath5(import.meta.url));
|
|
12913
|
+
var pkgPath = existsSync16(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
|
|
12914
|
+
var pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
12644
12915
|
var program2 = new Command;
|
|
12645
12916
|
function resolveScope(options) {
|
|
12646
12917
|
if (options.project)
|
|
@@ -12650,12 +12921,45 @@ function resolveScope(options) {
|
|
|
12650
12921
|
function resolveTarget(options) {
|
|
12651
12922
|
if (options.target === "gemini")
|
|
12652
12923
|
return "gemini";
|
|
12924
|
+
if (options.target === "codex")
|
|
12925
|
+
return "codex";
|
|
12653
12926
|
if (options.target === "codewith")
|
|
12654
12927
|
return "codewith";
|
|
12655
12928
|
if (options.target === "all")
|
|
12656
12929
|
return "all";
|
|
12657
12930
|
return "claude";
|
|
12658
12931
|
}
|
|
12932
|
+
function resolveConcreteTarget(options) {
|
|
12933
|
+
if (options.target === "gemini")
|
|
12934
|
+
return "gemini";
|
|
12935
|
+
if (options.target === "codex")
|
|
12936
|
+
return "codex";
|
|
12937
|
+
if (options.target === "codewith")
|
|
12938
|
+
return "codewith";
|
|
12939
|
+
return "claude";
|
|
12940
|
+
}
|
|
12941
|
+
function formatSettingsPath(scope, target) {
|
|
12942
|
+
if (target === "all")
|
|
12943
|
+
return "target-specific settings";
|
|
12944
|
+
const actual = getSettingsPath(scope, target);
|
|
12945
|
+
if (scope === "project") {
|
|
12946
|
+
if (target === "codewith")
|
|
12947
|
+
return ".codewith/config.toml";
|
|
12948
|
+
if (target === "gemini")
|
|
12949
|
+
return ".gemini/settings.json";
|
|
12950
|
+
if (target === "codex")
|
|
12951
|
+
return ".codex/hooks.json";
|
|
12952
|
+
return ".claude/settings.json";
|
|
12953
|
+
}
|
|
12954
|
+
if (target === "codewith") {
|
|
12955
|
+
return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH ? "$HASNA_HOOKS_CODEWITH_CONFIG_PATH" : "~/.codewith/config.toml";
|
|
12956
|
+
}
|
|
12957
|
+
if (target === "gemini")
|
|
12958
|
+
return "~/.gemini/settings.json";
|
|
12959
|
+
if (target === "codex")
|
|
12960
|
+
return "~/.codex/hooks.json";
|
|
12961
|
+
return actual === getSettingsPath("global", "claude") ? "~/.claude/settings.json" : actual;
|
|
12962
|
+
}
|
|
12659
12963
|
function parseLimit(value, fallback, max) {
|
|
12660
12964
|
const parsed = value ? parseInt(value, 10) : fallback;
|
|
12661
12965
|
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
@@ -12671,10 +12975,23 @@ function truncateText2(value, max = 96) {
|
|
|
12671
12975
|
function projectLogRows2(rows) {
|
|
12672
12976
|
return rows.map((row) => projectEventRowForRead({ ...row }));
|
|
12673
12977
|
}
|
|
12978
|
+
async function openLocalStoreOrExit(json2) {
|
|
12979
|
+
try {
|
|
12980
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
12981
|
+
return getDb2();
|
|
12982
|
+
} catch (error) {
|
|
12983
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12984
|
+
if (json2)
|
|
12985
|
+
console.log(JSON.stringify({ error: message }));
|
|
12986
|
+
else
|
|
12987
|
+
console.error(chalk2.red(`\u2717 ${message}`));
|
|
12988
|
+
process.exit(1);
|
|
12989
|
+
}
|
|
12990
|
+
}
|
|
12674
12991
|
function readToken(tokenFile) {
|
|
12675
12992
|
if (tokenFile) {
|
|
12676
12993
|
try {
|
|
12677
|
-
return
|
|
12994
|
+
return readFileSync12(tokenFile, "utf-8").trim();
|
|
12678
12995
|
} catch {
|
|
12679
12996
|
return;
|
|
12680
12997
|
}
|
|
@@ -12735,6 +13052,11 @@ function suggestHooks(name, max = 3) {
|
|
|
12735
13052
|
}
|
|
12736
13053
|
program2.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
|
|
12737
13054
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
13055
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
13056
|
+
console.error(chalk2.red("The interactive hook browser requires a TTY terminal.") + `
|
|
13057
|
+
` + chalk2.dim("Use `hooks search <query>`, `hooks list`, or `hooks categories` for non-interactive use."));
|
|
13058
|
+
process.exit(1);
|
|
13059
|
+
}
|
|
12738
13060
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
12739
13061
|
});
|
|
12740
13062
|
program2.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).option("--cloudflare", "Configure a remote registry (Cloudflare worker)", false).option("--api-url <url>", "Remote registry API URL (with --cloudflare)").option("--api-key <ref>", "Vault key NAME for the registry API key (never the value; with --cloudflare)").action(async (options) => {
|
|
@@ -12823,11 +13145,11 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
12823
13145
|
process.exit(1);
|
|
12824
13146
|
}
|
|
12825
13147
|
const hookScript = resolveScriptPath2(hook);
|
|
12826
|
-
if (!hookScript || !
|
|
13148
|
+
if (!hookScript || !existsSync16(hookScript)) {
|
|
12827
13149
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
12828
13150
|
process.exit(1);
|
|
12829
13151
|
}
|
|
12830
|
-
const content =
|
|
13152
|
+
const content = readFileSync12(hookScript);
|
|
12831
13153
|
const sha = sha256Of2(content);
|
|
12832
13154
|
const check = checkScriptHash2(hook, sha);
|
|
12833
13155
|
if (!check.ok) {
|
|
@@ -12933,7 +13255,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
12933
13255
|
process.stderr.write(stderr);
|
|
12934
13256
|
process.exit(exitCode);
|
|
12935
13257
|
});
|
|
12936
|
-
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) => {
|
|
13258
|
+
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("--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) => {
|
|
12937
13259
|
const scope = resolveScope(options);
|
|
12938
13260
|
const target = resolveTarget(options);
|
|
12939
13261
|
let toInstall = hooks;
|
|
@@ -12970,6 +13292,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
12970
13292
|
const { readCustomManifest: readCustomManifest2, HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
12971
13293
|
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
12972
13294
|
const { sha256Of: sha256Of2, pinInstalledHook: pinInstalledHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
13295
|
+
const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
12973
13296
|
const { readFileSync: readFileSyncFs } = await import("fs");
|
|
12974
13297
|
function parseNameVersion(arg) {
|
|
12975
13298
|
if (isCustomSource2(arg))
|
|
@@ -13062,17 +13385,13 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13062
13385
|
for (const { arg, pinned } of pinnedRequests) {
|
|
13063
13386
|
try {
|
|
13064
13387
|
const transport = resolveHooksTransport2();
|
|
13065
|
-
|
|
13066
|
-
throw new Error(`Cannot install '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
|
|
13067
|
-
}
|
|
13068
|
-
const { origin, apiKey } = transport.authority;
|
|
13069
|
-
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13070
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
13388
|
+
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
|
|
13071
13389
|
installedPinned.push(pinnedInstall.name);
|
|
13072
13390
|
if (options.json) {
|
|
13073
13391
|
console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
|
|
13074
13392
|
} else {
|
|
13075
|
-
|
|
13393
|
+
const registryLabel = transport.mode === "remote" ? "registry" : "bundled registry";
|
|
13394
|
+
console.log(chalk2.green(`\u2713 Installed '${pinnedInstall.name}' v${pinnedInstall.version} from ${registryLabel} (sha256 ${pinnedInstall.sha256.slice(0, 12)}\u2026)`));
|
|
13076
13395
|
}
|
|
13077
13396
|
} catch (error) {
|
|
13078
13397
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -13115,7 +13434,8 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13115
13434
|
success: results.filter((r) => r.success).length,
|
|
13116
13435
|
scope,
|
|
13117
13436
|
target,
|
|
13118
|
-
applied: results.some((r) => r.applied)
|
|
13437
|
+
applied: results.some((r) => r.applied),
|
|
13438
|
+
...target === "codex" ? { notes: results.filter((r) => r.success && r.note).map((r) => r.note) } : {}
|
|
13119
13439
|
}));
|
|
13120
13440
|
if (results.some((r) => !r.success)) {
|
|
13121
13441
|
process.exitCode = 1;
|
|
@@ -13123,7 +13443,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13123
13443
|
return;
|
|
13124
13444
|
}
|
|
13125
13445
|
const successCount = results.filter((r) => r.success).length;
|
|
13126
|
-
const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : scope
|
|
13446
|
+
const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : formatSettingsPath(scope, target);
|
|
13127
13447
|
console.log(chalk2.bold(`
|
|
13128
13448
|
Installing hooks (${scope}, ${target})...
|
|
13129
13449
|
`));
|
|
@@ -13137,6 +13457,8 @@ Installing hooks (${scope}, ${target})...
|
|
|
13137
13457
|
if (result.conflict) {
|
|
13138
13458
|
console.log(chalk2.yellow(` \u26A0 Warning: ${result.conflict}`));
|
|
13139
13459
|
}
|
|
13460
|
+
if (result.note && target === "codex")
|
|
13461
|
+
console.log(chalk2.dim(` ${result.note}`));
|
|
13140
13462
|
if (result.fragment && target === "codewith") {
|
|
13141
13463
|
console.log(chalk2.dim(" Codewith TOML fragment:"));
|
|
13142
13464
|
console.log(chalk2.cyan(result.fragment.trimEnd().split(`
|
|
@@ -13163,12 +13485,12 @@ Installing hooks (${scope}, ${target})...
|
|
|
13163
13485
|
console.log(chalk2.dim(`
|
|
13164
13486
|
Registered in ${settingsFile}`));
|
|
13165
13487
|
});
|
|
13166
|
-
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) => {
|
|
13488
|
+
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) => {
|
|
13167
13489
|
const scope = resolveScope(options);
|
|
13168
13490
|
const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
|
|
13169
13491
|
const storeHooks = listStoreHooks();
|
|
13170
13492
|
if (options.registered || options.installed) {
|
|
13171
|
-
const target = options
|
|
13493
|
+
const target = resolveConcreteTarget(options);
|
|
13172
13494
|
const registered = getRegisteredHooksForTarget(scope, target);
|
|
13173
13495
|
const installed = [...new Set([...registered, ...storeHooks.map((s) => s.meta.name)])];
|
|
13174
13496
|
const shown = options.registered ? registered : installed;
|
|
@@ -13291,7 +13613,7 @@ Found ${results.length} hook(s), showing ${visible.length}:
|
|
|
13291
13613
|
console.log(hookSummaryLine(h, { verbose: options.verbose }));
|
|
13292
13614
|
printDisclosureHint(results.length - visible.length, "hooks info <name>");
|
|
13293
13615
|
});
|
|
13294
|
-
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) => {
|
|
13616
|
+
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) => {
|
|
13295
13617
|
const scope = resolveScope(options);
|
|
13296
13618
|
const target = resolveTarget(options);
|
|
13297
13619
|
const { uninstallHook: uninstallHook2 } = await Promise.resolve().then(() => (init_installer(), exports_installer));
|
|
@@ -13407,7 +13729,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13407
13729
|
});
|
|
13408
13730
|
}
|
|
13409
13731
|
const staleNames = new Set(stale.map((finding) => finding.hook));
|
|
13410
|
-
const settingsExist =
|
|
13732
|
+
const settingsExist = existsSync16(settingsPath);
|
|
13411
13733
|
if (!settingsExist) {
|
|
13412
13734
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
13413
13735
|
}
|
|
@@ -13415,7 +13737,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13415
13737
|
let wiringCount = 0;
|
|
13416
13738
|
if (settingsExist) {
|
|
13417
13739
|
try {
|
|
13418
|
-
const settings = JSON.parse(
|
|
13740
|
+
const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
13419
13741
|
wiringCount = countSettingsWiring(settings);
|
|
13420
13742
|
} catch {}
|
|
13421
13743
|
}
|
|
@@ -13426,7 +13748,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13426
13748
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
13427
13749
|
let hookHealthy = true;
|
|
13428
13750
|
if (custom) {
|
|
13429
|
-
if (!
|
|
13751
|
+
if (!existsSync16(custom.scriptPath)) {
|
|
13430
13752
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
13431
13753
|
hookHealthy = false;
|
|
13432
13754
|
}
|
|
@@ -13438,14 +13760,14 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13438
13760
|
}
|
|
13439
13761
|
const hookDir = getHookPath(name);
|
|
13440
13762
|
const hookScript = join17(hookDir, "src", "hook.ts");
|
|
13441
|
-
if (!
|
|
13763
|
+
if (!existsSync16(hookScript)) {
|
|
13442
13764
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
13443
13765
|
hookHealthy = false;
|
|
13444
13766
|
}
|
|
13445
13767
|
}
|
|
13446
13768
|
if (meta && settingsExist) {
|
|
13447
13769
|
try {
|
|
13448
|
-
const settings = JSON.parse(
|
|
13770
|
+
const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
13449
13771
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
13450
13772
|
const eventName = meta.event.split(":")[0];
|
|
13451
13773
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -13547,8 +13869,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
13547
13869
|
}
|
|
13548
13870
|
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
13549
13871
|
const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
13550
|
-
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
13872
|
+
const { getDb: getDb2, isLocalStoreRefused: isLocalStoreRefused2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
13551
13873
|
const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
|
|
13874
|
+
const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13552
13875
|
const { isCustomSource: isCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
13553
13876
|
const { HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
13554
13877
|
function parseNameVersion(arg) {
|
|
@@ -13571,12 +13894,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
13571
13894
|
if (pinned) {
|
|
13572
13895
|
try {
|
|
13573
13896
|
const transport = resolveHooksTransport2();
|
|
13574
|
-
|
|
13575
|
-
throw new Error(`Cannot update '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
|
|
13576
|
-
}
|
|
13577
|
-
const { origin, apiKey } = transport.authority;
|
|
13578
|
-
const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
13579
|
-
const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
|
|
13897
|
+
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
|
|
13580
13898
|
const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
|
|
13581
13899
|
if (!registered.success) {
|
|
13582
13900
|
results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
|
|
@@ -13601,13 +13919,15 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
13601
13919
|
if (result.success && resolved) {
|
|
13602
13920
|
const hash = await sha256File2(resolved.scriptPath);
|
|
13603
13921
|
setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13922
|
+
if (!isLocalStoreRefused2()) {
|
|
13923
|
+
upsertHookRecord2(getDb2(), {
|
|
13924
|
+
name: resolved.name,
|
|
13925
|
+
version: resolved.version,
|
|
13926
|
+
sha256: hash,
|
|
13927
|
+
source_type: resolved.source,
|
|
13928
|
+
last_verified_at: new Date().toISOString()
|
|
13929
|
+
});
|
|
13930
|
+
}
|
|
13611
13931
|
results.push({ ...result, pinned: { version: resolved.version, sha256: hash } });
|
|
13612
13932
|
continue;
|
|
13613
13933
|
}
|
|
@@ -13652,8 +13972,8 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
13652
13972
|
const hookPath = getHookPath(hook);
|
|
13653
13973
|
const readmePath = join17(hookPath, "README.md");
|
|
13654
13974
|
let readme = "";
|
|
13655
|
-
if (
|
|
13656
|
-
readme =
|
|
13975
|
+
if (existsSync16(readmePath)) {
|
|
13976
|
+
readme = readFileSync12(readmePath, "utf-8");
|
|
13657
13977
|
}
|
|
13658
13978
|
if (options.json) {
|
|
13659
13979
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -13772,7 +14092,7 @@ ${meta.displayName} v${meta.version}
|
|
|
13772
14092
|
console.log();
|
|
13773
14093
|
});
|
|
13774
14094
|
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) => {
|
|
13775
|
-
const
|
|
14095
|
+
const current2 = pkg2.version;
|
|
13776
14096
|
let pm = "npm";
|
|
13777
14097
|
try {
|
|
13778
14098
|
const which = Bun.spawnSync(["which", "bun"]);
|
|
@@ -13790,13 +14110,13 @@ program2.command("upgrade").option("-c, --check", "Check for updates without ins
|
|
|
13790
14110
|
}
|
|
13791
14111
|
process.exit(1);
|
|
13792
14112
|
}
|
|
13793
|
-
const upToDate =
|
|
14113
|
+
const upToDate = current2 === latest2;
|
|
13794
14114
|
if (options.json) {
|
|
13795
|
-
console.log(JSON.stringify({ current, latest: latest2, upToDate }));
|
|
14115
|
+
console.log(JSON.stringify({ current: current2, latest: latest2, upToDate }));
|
|
13796
14116
|
} else if (upToDate) {
|
|
13797
|
-
console.log(chalk2.green(`\u2713 Already on latest version (${
|
|
14117
|
+
console.log(chalk2.green(`\u2713 Already on latest version (${current2})`));
|
|
13798
14118
|
} else {
|
|
13799
|
-
console.log(chalk2.yellow(`Update available: ${
|
|
14119
|
+
console.log(chalk2.yellow(`Update available: ${current2} \u2192 ${latest2}`));
|
|
13800
14120
|
console.log(chalk2.dim(` Run: hooks upgrade`));
|
|
13801
14121
|
}
|
|
13802
14122
|
return;
|
|
@@ -13817,7 +14137,7 @@ Upgrading @hasna/hooks (${pm})...
|
|
|
13817
14137
|
const exitCode = await proc.exited;
|
|
13818
14138
|
if (exitCode !== 0) {
|
|
13819
14139
|
if (options.json) {
|
|
13820
|
-
console.log(JSON.stringify({ current, updated: false, error: `${pm} exited with code ${exitCode}` }));
|
|
14140
|
+
console.log(JSON.stringify({ current: current2, updated: false, error: `${pm} exited with code ${exitCode}` }));
|
|
13821
14141
|
} else {
|
|
13822
14142
|
console.log(chalk2.red(`
|
|
13823
14143
|
\u2717 Upgrade failed (exit code ${exitCode})`));
|
|
@@ -13827,18 +14147,18 @@ Upgrading @hasna/hooks (${pm})...
|
|
|
13827
14147
|
const versionProc = Bun.spawnSync(["npm", "view", "@hasna/hooks", "version"]);
|
|
13828
14148
|
const latest = new TextDecoder().decode(versionProc.stdout).trim() || "unknown";
|
|
13829
14149
|
if (options.json) {
|
|
13830
|
-
console.log(JSON.stringify({ current, latest, updated: true }));
|
|
14150
|
+
console.log(JSON.stringify({ current: current2, latest, updated: true }));
|
|
13831
14151
|
} else {
|
|
13832
14152
|
console.log(chalk2.green(`
|
|
13833
|
-
\u2713 Upgraded: ${
|
|
14153
|
+
\u2713 Upgraded: ${current2} \u2192 ${latest}`));
|
|
13834
14154
|
}
|
|
13835
14155
|
});
|
|
13836
14156
|
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) => {
|
|
13837
14157
|
const profiles = exportProfiles();
|
|
13838
14158
|
const json2 = JSON.stringify(profiles, null, 2);
|
|
13839
14159
|
if (options.output) {
|
|
13840
|
-
const { writeFileSync:
|
|
13841
|
-
|
|
14160
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
14161
|
+
writeFileSync7(options.output, json2 + `
|
|
13842
14162
|
`);
|
|
13843
14163
|
console.log(chalk2.green(`\u2713 Exported ${profiles.length} profile(s) to ${options.output}`));
|
|
13844
14164
|
} else {
|
|
@@ -13850,9 +14170,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
13850
14170
|
if (file === "-") {
|
|
13851
14171
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
13852
14172
|
} else {
|
|
13853
|
-
const { readFileSync:
|
|
14173
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
13854
14174
|
try {
|
|
13855
|
-
raw =
|
|
14175
|
+
raw = readFileSync13(file, "utf-8");
|
|
13856
14176
|
} catch {
|
|
13857
14177
|
if (options.json) {
|
|
13858
14178
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -13885,8 +14205,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
13885
14205
|
});
|
|
13886
14206
|
var logCmd = program2.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
|
|
13887
14207
|
logCmd.command("list").description("List hook events").option("--hook <name>", "Filter by hook name").option("--session <id>", "Filter by session ID").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13888
|
-
const
|
|
13889
|
-
const db = getDb2();
|
|
14208
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13890
14209
|
const limit = parseInt(options.limit) || 50;
|
|
13891
14210
|
let sql = "SELECT * FROM hook_events WHERE 1=1";
|
|
13892
14211
|
const params = [];
|
|
@@ -13923,8 +14242,7 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
|
|
|
13923
14242
|
Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
|
|
13924
14243
|
});
|
|
13925
14244
|
logCmd.command("search <text>").description("Search hook events by tool_input or error text").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (text, options) => {
|
|
13926
|
-
const
|
|
13927
|
-
const db = getDb2();
|
|
14245
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13928
14246
|
const limit = parseInt(options.limit) || 50;
|
|
13929
14247
|
const q = `%${text}%`;
|
|
13930
14248
|
const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
|
|
@@ -13949,8 +14267,7 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
|
|
|
13949
14267
|
Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
|
|
13950
14268
|
});
|
|
13951
14269
|
logCmd.command("tail").description("Show most recent hook events").option("-n <n>", "Number of rows", "20").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13952
|
-
const
|
|
13953
|
-
const db = getDb2();
|
|
14270
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13954
14271
|
const limit = parseInt(options.n) || 20;
|
|
13955
14272
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
|
|
13956
14273
|
const projected = projectLogRows2(rows);
|
|
@@ -13975,8 +14292,7 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
|
|
|
13975
14292
|
Compact rows shown. Use --json for full event records or -n <n> to change row count.`));
|
|
13976
14293
|
});
|
|
13977
14294
|
logCmd.command("errors").description("Show hook events that contain errors").option("--since <duration>", "Only show errors since this duration (e.g. 1h, 30m, 7d)", "24h").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
13978
|
-
const
|
|
13979
|
-
const db = getDb2();
|
|
14295
|
+
const db = await openLocalStoreOrExit(options.json);
|
|
13980
14296
|
const limit = parseInt(options.limit) || 50;
|
|
13981
14297
|
function parseDuration(s) {
|
|
13982
14298
|
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
@@ -14018,8 +14334,7 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
|
|
|
14018
14334
|
Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
|
|
14019
14335
|
});
|
|
14020
14336
|
logCmd.command("clear").description("Delete hook event logs").option("--hook <name>", "Only delete events for this hook").option("-y, --yes", "Skip confirmation prompt", false).action(async (options) => {
|
|
14021
|
-
const
|
|
14022
|
-
const db = getDb2();
|
|
14337
|
+
const db = await openLocalStoreOrExit(false);
|
|
14023
14338
|
const countRow = options.hook ? db.query("SELECT COUNT(*) as n FROM hook_events WHERE hook_name = ?").get(options.hook) : db.query("SELECT COUNT(*) as n FROM hook_events").get();
|
|
14024
14339
|
const count = countRow?.n ?? 0;
|
|
14025
14340
|
if (count === 0) {
|
|
@@ -14041,19 +14356,28 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
|
|
|
14041
14356
|
});
|
|
14042
14357
|
var storageCmd = program2.command("storage").description("Sync local hook data with storage PostgreSQL");
|
|
14043
14358
|
storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14044
|
-
|
|
14045
|
-
|
|
14046
|
-
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
|
|
14359
|
+
try {
|
|
14360
|
+
const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14361
|
+
const status = getStorageStatus2();
|
|
14362
|
+
if (options.json) {
|
|
14363
|
+
console.log(JSON.stringify(status, null, 2));
|
|
14364
|
+
return;
|
|
14365
|
+
}
|
|
14366
|
+
console.log(chalk2.bold(`
|
|
14051
14367
|
Storage Status
|
|
14052
14368
|
`));
|
|
14053
|
-
|
|
14054
|
-
|
|
14055
|
-
|
|
14056
|
-
|
|
14369
|
+
console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
|
|
14370
|
+
console.log(` Backend: ${status.backend}`);
|
|
14371
|
+
console.log(` Tables: ${status.tables.join(", ")}`);
|
|
14372
|
+
console.log(` Sync rows: ${status.sync.length}`);
|
|
14373
|
+
} catch (error) {
|
|
14374
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14375
|
+
if (options.json)
|
|
14376
|
+
console.log(JSON.stringify({ error: message }));
|
|
14377
|
+
else
|
|
14378
|
+
console.error(chalk2.red(`\u2717 ${message}`));
|
|
14379
|
+
process.exitCode = 1;
|
|
14380
|
+
}
|
|
14057
14381
|
});
|
|
14058
14382
|
storageCmd.command("push").description("Push local hook data to storage PostgreSQL").option("-t, --tables <tables>", "Comma-separated table names").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14059
14383
|
try {
|
|
@@ -14263,6 +14587,13 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
14263
14587
|
const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
14264
14588
|
await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
|
|
14265
14589
|
} else {
|
|
14590
|
+
const { decideHooksMcpAuthority: decideHooksMcpAuthority2 } = await Promise.resolve().then(() => (init_authority(), exports_authority));
|
|
14591
|
+
try {
|
|
14592
|
+
decideHooksMcpAuthority2();
|
|
14593
|
+
} catch (error) {
|
|
14594
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
14595
|
+
process.exit(1);
|
|
14596
|
+
}
|
|
14266
14597
|
const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
14267
14598
|
const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
14268
14599
|
const args = options.port ? ["--port", options.port] : [];
|
|
@@ -14271,7 +14602,6 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
14271
14602
|
});
|
|
14272
14603
|
registerEventsCommands(program2, { source: "hooks" });
|
|
14273
14604
|
var API_INDEPENDENT_COMMANDS = new Set([
|
|
14274
|
-
"run",
|
|
14275
14605
|
"serve",
|
|
14276
14606
|
"mcp",
|
|
14277
14607
|
"cf",
|
|
@@ -14283,11 +14613,11 @@ var API_INDEPENDENT_COMMANDS = new Set([
|
|
|
14283
14613
|
"events"
|
|
14284
14614
|
]);
|
|
14285
14615
|
function failClosedForMissingApiEnv(detail) {
|
|
14286
|
-
console.error(chalk2.red(
|
|
14616
|
+
console.error(chalk2.red(hooksFailClosedLine() + `
|
|
14287
14617
|
` + `The remote registry requires a STRICT pair \u2014 set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or the
|
|
14288
14618
|
` + `Keychain items hasna.credentials.hooks.api-url / .api-key, or ~/.hasna/hooks/config/credentials.
|
|
14289
|
-
|
|
14290
|
-
|
|
14619
|
+
config.json (api_url / api_key_ref) is RETIRED and no longer read. Or set
|
|
14620
|
+
HASNA_HOOKS_LOCAL=1 to explicitly opt into local mode (bundled registry + local store).
|
|
14291
14621
|
` + (detail ? `${detail}
|
|
14292
14622
|
` : "") + "Refusing to silently fall back to local storage."));
|
|
14293
14623
|
process.exit(1);
|
|
@@ -14311,21 +14641,25 @@ function enforceTransportGate() {
|
|
|
14311
14641
|
return;
|
|
14312
14642
|
if (token !== null && API_INDEPENDENT_COMMANDS.has(token))
|
|
14313
14643
|
return;
|
|
14314
|
-
if (hasHooksEnvAuthorityIntent(process.env))
|
|
14644
|
+
if (hasHooksEnvAuthorityIntent(process.env)) {
|
|
14645
|
+
refuseLocalStore(hooksHostedRouteLocalStoreRefusal("configured by HASNA_HOOKS_API_URL / HASNA_HOOKS_API_KEY"));
|
|
14315
14646
|
return;
|
|
14647
|
+
}
|
|
14316
14648
|
if (isHooksLocalOptIn(process.env)) {
|
|
14317
14649
|
announceHooksLocalMode();
|
|
14318
14650
|
return;
|
|
14319
14651
|
}
|
|
14652
|
+
let transport;
|
|
14320
14653
|
try {
|
|
14321
|
-
|
|
14322
|
-
if (transport.mode !== "remote" || !transport.authority) {
|
|
14323
|
-
failClosedForMissingApiEnv();
|
|
14324
|
-
}
|
|
14654
|
+
transport = resolveHooksTransport(process.env);
|
|
14325
14655
|
} catch (error) {
|
|
14326
14656
|
const message = error instanceof Error ? error.message : String(error);
|
|
14327
14657
|
failClosedForMissingApiEnv(message);
|
|
14328
14658
|
}
|
|
14659
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
14660
|
+
failClosedForMissingApiEnv();
|
|
14661
|
+
}
|
|
14662
|
+
refuseLocalStore(hooksHostedRouteLocalStoreRefusal(transport.authority.v1BaseUrl));
|
|
14329
14663
|
}
|
|
14330
14664
|
enforceTransportGate();
|
|
14331
14665
|
program2.parse();
|