@hasna/hooks 0.5.0 → 0.6.1
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 +81 -8
- package/bin/index.js +1756 -382
- package/bin/serve.js +5258 -0
- package/dist/cf/provision.d.ts +24 -0
- package/dist/config.d.ts +18 -0
- package/dist/db/legacy-import.d.ts +1 -1
- package/dist/db/migrations/004_hooks_table.d.ts +9 -0
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/db/storage-sync.d.ts +26 -6
- package/dist/index.d.ts +18 -2
- package/dist/index.js +5351 -286
- package/dist/lib/custom-install.d.ts +19 -0
- package/dist/lib/manifest.d.ts +70 -0
- package/dist/lib/resolve.d.ts +19 -0
- package/dist/lib/run.d.ts +37 -0
- package/dist/lib/store.d.ts +69 -0
- package/dist/lib/sync.d.ts +35 -0
- package/dist/serve.d.ts +36 -0
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +133 -42
- package/hooks/codewith-native-common.test.ts +15 -2
- package/hooks/hook-scanoutput/README.md +151 -0
- package/hooks/hook-scanoutput/package.json +12 -0
- package/hooks/hook-scanoutput/src/hook.test.ts +217 -0
- package/hooks/hook-scanoutput/src/hook.ts +319 -0
- package/hooks/hook-workspace-repos-guard/README.md +63 -0
- package/hooks/hook-workspace-repos-guard/package.json +12 -0
- package/hooks/hook-workspace-repos-guard/src/hook.test.ts +312 -0
- package/hooks/hook-workspace-repos-guard/src/hook.ts +352 -0
- package/hooks/hook-workspace-repos-guard/tsconfig.json +21 -0
- package/hooks/mention-context/README.md +109 -0
- package/hooks/mention-context/package.json +9 -0
- package/hooks/mention-context/src/hasna-mention-context.py +1218 -0
- package/hooks/mention-context/src/hasna-mention-warm.py +521 -0
- package/hooks/mention-context/src/hook.test.ts +68 -0
- package/hooks/mention-context/src/test_run_capture.py +345 -0
- package/package.json +9 -5
package/bin/index.js
CHANGED
|
@@ -47,7 +47,7 @@ var __export = (target, all) => {
|
|
|
47
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
48
48
|
var __require = import.meta.require;
|
|
49
49
|
|
|
50
|
-
// node_modules/commander/lib/error.js
|
|
50
|
+
// ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/error.js
|
|
51
51
|
var require_error = __commonJS((exports) => {
|
|
52
52
|
class CommanderError extends Error {
|
|
53
53
|
constructor(exitCode, code, message) {
|
|
@@ -71,7 +71,7 @@ var require_error = __commonJS((exports) => {
|
|
|
71
71
|
exports.InvalidArgumentError = InvalidArgumentError;
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
// node_modules/commander/lib/argument.js
|
|
74
|
+
// ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/argument.js
|
|
75
75
|
var require_argument = __commonJS((exports) => {
|
|
76
76
|
var { InvalidArgumentError } = require_error();
|
|
77
77
|
|
|
@@ -150,7 +150,7 @@ var require_argument = __commonJS((exports) => {
|
|
|
150
150
|
exports.humanReadableArgName = humanReadableArgName;
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
// node_modules/commander/lib/help.js
|
|
153
|
+
// ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/help.js
|
|
154
154
|
var require_help = __commonJS((exports) => {
|
|
155
155
|
var { humanReadableArgName } = require_argument();
|
|
156
156
|
|
|
@@ -399,7 +399,7 @@ var require_help = __commonJS((exports) => {
|
|
|
399
399
|
exports.Help = Help;
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
-
// node_modules/commander/lib/option.js
|
|
402
|
+
// ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/option.js
|
|
403
403
|
var require_option = __commonJS((exports) => {
|
|
404
404
|
var { InvalidArgumentError } = require_error();
|
|
405
405
|
|
|
@@ -550,7 +550,7 @@ var require_option = __commonJS((exports) => {
|
|
|
550
550
|
exports.DualOptions = DualOptions;
|
|
551
551
|
});
|
|
552
552
|
|
|
553
|
-
// node_modules/commander/lib/suggestSimilar.js
|
|
553
|
+
// ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js
|
|
554
554
|
var require_suggestSimilar = __commonJS((exports) => {
|
|
555
555
|
var maxDistance = 3;
|
|
556
556
|
function editDistance(a, b) {
|
|
@@ -623,7 +623,7 @@ var require_suggestSimilar = __commonJS((exports) => {
|
|
|
623
623
|
exports.suggestSimilar = suggestSimilar;
|
|
624
624
|
});
|
|
625
625
|
|
|
626
|
-
// node_modules/commander/lib/command.js
|
|
626
|
+
// ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/command.js
|
|
627
627
|
var require_command = __commonJS((exports) => {
|
|
628
628
|
var EventEmitter = __require("events").EventEmitter;
|
|
629
629
|
var childProcess = __require("child_process");
|
|
@@ -1866,7 +1866,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1866
1866
|
exports.Command = Command;
|
|
1867
1867
|
});
|
|
1868
1868
|
|
|
1869
|
-
// node_modules/commander/index.js
|
|
1869
|
+
// ../../node_modules/.bun/commander@12.1.0/node_modules/commander/index.js
|
|
1870
1870
|
var require_commander = __commonJS((exports) => {
|
|
1871
1871
|
var { Argument } = require_argument();
|
|
1872
1872
|
var { Command } = require_command();
|
|
@@ -1955,6 +1955,16 @@ var init_registry = __esm(() => {
|
|
|
1955
1955
|
matcher: "^(Bash|Write|Edit|MultiEdit|NotebookEdit|apply_patch|ApplyPatch|functions\\.apply_patch|mcp__.*)$",
|
|
1956
1956
|
tags: ["git", "worktree", "repos", "multi-agent", "safety", "dangerous-ops"]
|
|
1957
1957
|
},
|
|
1958
|
+
{
|
|
1959
|
+
name: "workspace-repos-guard",
|
|
1960
|
+
displayName: "Workspace Repos Guard",
|
|
1961
|
+
description: "Blocks writes and deletes that break the ~/workspace/repos structure: writes to the repos root, top-level entry creation, non-org folders, and any delete under repos",
|
|
1962
|
+
version: "0.1.0",
|
|
1963
|
+
category: "Git Safety",
|
|
1964
|
+
event: "PreToolUse",
|
|
1965
|
+
matcher: "^(Bash|Write|Edit|MultiEdit|NotebookEdit|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
1966
|
+
tags: ["workspace", "repos", "structure", "guard", "safety", "orgs", "multi-agent"]
|
|
1967
|
+
},
|
|
1958
1968
|
{
|
|
1959
1969
|
name: "checktests",
|
|
1960
1970
|
displayName: "Check Tests",
|
|
@@ -2045,6 +2055,16 @@ var init_registry = __esm(() => {
|
|
|
2045
2055
|
matcher: "Bash",
|
|
2046
2056
|
tags: ["codewith", "bash", "secrets", "gitleaks", "risky-ops"]
|
|
2047
2057
|
},
|
|
2058
|
+
{
|
|
2059
|
+
name: "scanoutput",
|
|
2060
|
+
displayName: "Scan Output",
|
|
2061
|
+
description: "Scans tool output for credential shapes and warns. Detection only \u2014 the output is already persisted when this runs; it cannot redact or prevent",
|
|
2062
|
+
version: "0.1.0",
|
|
2063
|
+
category: "Security",
|
|
2064
|
+
event: "PostToolUse",
|
|
2065
|
+
matcher: "Bash",
|
|
2066
|
+
tags: ["security", "credentials", "secrets", "detection", "audit"]
|
|
2067
|
+
},
|
|
2048
2068
|
{
|
|
2049
2069
|
name: "phonenotify",
|
|
2050
2070
|
displayName: "Phone Notify",
|
|
@@ -2399,7 +2419,7 @@ var init_registry = __esm(() => {
|
|
|
2399
2419
|
];
|
|
2400
2420
|
});
|
|
2401
2421
|
|
|
2402
|
-
// node_modules/cli-spinners/spinners.json
|
|
2422
|
+
// ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/spinners.json
|
|
2403
2423
|
var require_spinners = __commonJS((exports, module) => {
|
|
2404
2424
|
module.exports = {
|
|
2405
2425
|
dots: {
|
|
@@ -4025,7 +4045,7 @@ var require_spinners = __commonJS((exports, module) => {
|
|
|
4025
4045
|
};
|
|
4026
4046
|
});
|
|
4027
4047
|
|
|
4028
|
-
// node_modules/cli-spinners/index.js
|
|
4048
|
+
// ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/index.js
|
|
4029
4049
|
var require_cli_spinners = __commonJS((exports, module) => {
|
|
4030
4050
|
var spinners = Object.assign({}, require_spinners());
|
|
4031
4051
|
var spinnersList = Object.keys(spinners);
|
|
@@ -4039,10 +4059,320 @@ var require_cli_spinners = __commonJS((exports, module) => {
|
|
|
4039
4059
|
module.exports = spinners;
|
|
4040
4060
|
});
|
|
4041
4061
|
|
|
4042
|
-
// src/
|
|
4062
|
+
// src/config.ts
|
|
4063
|
+
var exports_config = {};
|
|
4064
|
+
__export(exports_config, {
|
|
4065
|
+
writeConfig: () => writeConfig,
|
|
4066
|
+
resolveApiUrl: () => resolveApiUrl,
|
|
4067
|
+
resolveApiKey: () => resolveApiKey,
|
|
4068
|
+
readConfig: () => readConfig,
|
|
4069
|
+
getLockPath: () => getLockPath,
|
|
4070
|
+
getHooksDataDir: () => getHooksDataDir,
|
|
4071
|
+
getCustomHooksDir: () => getCustomHooksDir,
|
|
4072
|
+
getConfigPath: () => getConfigPath
|
|
4073
|
+
});
|
|
4043
4074
|
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4044
|
-
import { join as join2
|
|
4075
|
+
import { join as join2 } from "path";
|
|
4045
4076
|
import { homedir as homedir2 } from "os";
|
|
4077
|
+
function getHooksDataDir() {
|
|
4078
|
+
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
4079
|
+
if (explicit)
|
|
4080
|
+
return explicit;
|
|
4081
|
+
return join2(homedir2(), ".hasna", "hooks");
|
|
4082
|
+
}
|
|
4083
|
+
function getCustomHooksDir() {
|
|
4084
|
+
return join2(getHooksDataDir(), "hooks");
|
|
4085
|
+
}
|
|
4086
|
+
function getLockPath() {
|
|
4087
|
+
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4088
|
+
if (explicit)
|
|
4089
|
+
return explicit;
|
|
4090
|
+
return join2(getHooksDataDir(), "hooks.lock");
|
|
4091
|
+
}
|
|
4092
|
+
function getConfigPath() {
|
|
4093
|
+
const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
|
|
4094
|
+
if (explicit)
|
|
4095
|
+
return explicit;
|
|
4096
|
+
return join2(getHooksDataDir(), "config.json");
|
|
4097
|
+
}
|
|
4098
|
+
function readConfig() {
|
|
4099
|
+
try {
|
|
4100
|
+
const path = getConfigPath();
|
|
4101
|
+
if (!existsSync2(path))
|
|
4102
|
+
return {};
|
|
4103
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
4104
|
+
} catch {
|
|
4105
|
+
return {};
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
function writeConfig(config) {
|
|
4109
|
+
const path = getConfigPath();
|
|
4110
|
+
mkdirSync(join2(path, ".."), { recursive: true });
|
|
4111
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
4112
|
+
`, "utf-8");
|
|
4113
|
+
return path;
|
|
4114
|
+
}
|
|
4115
|
+
function resolveApiUrl() {
|
|
4116
|
+
const envUrl = process.env.HASNA_HOOKS_API_URL ?? process.env.HOOKS_API_URL ?? process.env.HASNA_HOOKS_REGISTRY_URL ?? process.env.HOOKS_REGISTRY_URL;
|
|
4117
|
+
if (envUrl)
|
|
4118
|
+
return envUrl.replace(/\/+$/, "");
|
|
4119
|
+
const cfg = readConfig();
|
|
4120
|
+
return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
|
|
4121
|
+
}
|
|
4122
|
+
function resolveApiKey(flagKey) {
|
|
4123
|
+
if (flagKey && flagKey.length > 0)
|
|
4124
|
+
return flagKey;
|
|
4125
|
+
return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
|
|
4126
|
+
}
|
|
4127
|
+
var init_config = () => {};
|
|
4128
|
+
|
|
4129
|
+
// src/lib/manifest.ts
|
|
4130
|
+
var exports_manifest = {};
|
|
4131
|
+
__export(exports_manifest, {
|
|
4132
|
+
writeCustomHook: () => writeCustomHook,
|
|
4133
|
+
shortManifestName: () => shortManifestName,
|
|
4134
|
+
resolveScript: () => resolveScript,
|
|
4135
|
+
readCustomManifest: () => readCustomManifest,
|
|
4136
|
+
parseManifest: () => parseManifest,
|
|
4137
|
+
normalizeManifestName: () => normalizeManifestName,
|
|
4138
|
+
manifestSchema: () => manifestSchema,
|
|
4139
|
+
listCustomHooks: () => listCustomHooks,
|
|
4140
|
+
customHookDir: () => customHookDir,
|
|
4141
|
+
assertContained: () => assertContained,
|
|
4142
|
+
ScriptContainmentError: () => ScriptContainmentError,
|
|
4143
|
+
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4144
|
+
});
|
|
4145
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4146
|
+
import { basename, dirname, isAbsolute, join as join3, normalize, resolve, sep } from "path";
|
|
4147
|
+
import { z } from "zod";
|
|
4148
|
+
function parseManifest(json) {
|
|
4149
|
+
const parsed = JSON.parse(json);
|
|
4150
|
+
return manifestSchema.parse(parsed);
|
|
4151
|
+
}
|
|
4152
|
+
function normalizeManifestName(name) {
|
|
4153
|
+
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4154
|
+
}
|
|
4155
|
+
function shortManifestName(name) {
|
|
4156
|
+
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
4157
|
+
}
|
|
4158
|
+
function isWithin(child, parent) {
|
|
4159
|
+
return child === parent || child.startsWith(parent + sep);
|
|
4160
|
+
}
|
|
4161
|
+
function assertContainedLexical(target, root, raw) {
|
|
4162
|
+
const lexical = resolve(target);
|
|
4163
|
+
const rootLexical = resolve(root);
|
|
4164
|
+
if (!isWithin(lexical, rootLexical)) {
|
|
4165
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4166
|
+
}
|
|
4167
|
+
return lexical;
|
|
4168
|
+
}
|
|
4169
|
+
function realpathNearest(path) {
|
|
4170
|
+
const tail = [];
|
|
4171
|
+
let cur = path;
|
|
4172
|
+
while (!existsSync3(cur)) {
|
|
4173
|
+
const parent = dirname(cur);
|
|
4174
|
+
if (parent === cur)
|
|
4175
|
+
break;
|
|
4176
|
+
tail.unshift(basename(cur));
|
|
4177
|
+
cur = parent;
|
|
4178
|
+
}
|
|
4179
|
+
return join3(realpathSync(cur), ...tail);
|
|
4180
|
+
}
|
|
4181
|
+
function assertContainedReal(root, target, raw) {
|
|
4182
|
+
const rootReal = realpathSync(root);
|
|
4183
|
+
const targetReal = realpathNearest(target);
|
|
4184
|
+
if (!isWithin(targetReal, rootReal)) {
|
|
4185
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
function assertContained(target, root, raw) {
|
|
4189
|
+
const lexical = assertContainedLexical(target, root, raw);
|
|
4190
|
+
assertContainedReal(root, lexical, raw);
|
|
4191
|
+
return lexical;
|
|
4192
|
+
}
|
|
4193
|
+
function resolveScript(manifest, manifestDir) {
|
|
4194
|
+
if (manifest.script.includes(`
|
|
4195
|
+
`)) {
|
|
4196
|
+
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4197
|
+
return { path: `script${ext}`, content: manifest.script };
|
|
4198
|
+
}
|
|
4199
|
+
const rel = normalize(manifest.script);
|
|
4200
|
+
if (isAbsolute(rel)) {
|
|
4201
|
+
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4202
|
+
}
|
|
4203
|
+
const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
|
|
4204
|
+
if (!existsSync3(resolved)) {
|
|
4205
|
+
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4206
|
+
}
|
|
4207
|
+
return { path: rel, content: readFileSync2(resolved, "utf-8") };
|
|
4208
|
+
}
|
|
4209
|
+
function customHookDir(name) {
|
|
4210
|
+
return join3(getCustomHooksDir(), shortManifestName(name));
|
|
4211
|
+
}
|
|
4212
|
+
function readCustomManifest(name) {
|
|
4213
|
+
const dir = customHookDir(name);
|
|
4214
|
+
const manifestPath = join3(dir, "manifest.json");
|
|
4215
|
+
if (!existsSync3(manifestPath))
|
|
4216
|
+
return;
|
|
4217
|
+
try {
|
|
4218
|
+
const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
|
|
4219
|
+
const script = resolveScript(manifest, dir);
|
|
4220
|
+
return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline: manifest.script.includes(`
|
|
4221
|
+
`) };
|
|
4222
|
+
} catch (err) {
|
|
4223
|
+
if (err instanceof ScriptContainmentError)
|
|
4224
|
+
throw err;
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
function listCustomHooks() {
|
|
4229
|
+
const dir = getCustomHooksDir();
|
|
4230
|
+
if (!existsSync3(dir))
|
|
4231
|
+
return [];
|
|
4232
|
+
const results = [];
|
|
4233
|
+
for (const entry of readdirSafe(dir)) {
|
|
4234
|
+
const parsed = readCustomManifest(entry);
|
|
4235
|
+
if (parsed)
|
|
4236
|
+
results.push(parsed);
|
|
4237
|
+
}
|
|
4238
|
+
return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
4239
|
+
}
|
|
4240
|
+
function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
4241
|
+
const dir = customHookDir(name);
|
|
4242
|
+
if (isAbsolute(scriptRelPath)) {
|
|
4243
|
+
throw new ScriptContainmentError(scriptRelPath, dir);
|
|
4244
|
+
}
|
|
4245
|
+
const scriptPath = scriptRelPath.includes("/") ? join3(dir, scriptRelPath) : join3(dir, scriptRelPath);
|
|
4246
|
+
assertContainedLexical(scriptPath, dir, scriptRelPath);
|
|
4247
|
+
mkdirSync2(dir, { recursive: true });
|
|
4248
|
+
assertContainedReal(dir, scriptPath, scriptRelPath);
|
|
4249
|
+
const scriptDir = dirname(scriptPath);
|
|
4250
|
+
if (scriptDir !== dir)
|
|
4251
|
+
mkdirSync2(scriptDir, { recursive: true });
|
|
4252
|
+
writeFileSync2(scriptPath, scriptContent, "utf-8");
|
|
4253
|
+
writeFileSync2(join3(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
4254
|
+
`, "utf-8");
|
|
4255
|
+
return { dir, scriptPath };
|
|
4256
|
+
}
|
|
4257
|
+
function readdirSafe(dir) {
|
|
4258
|
+
try {
|
|
4259
|
+
return readdirSync(dir);
|
|
4260
|
+
} catch {
|
|
4261
|
+
return [];
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
var HOOK_NAME_RE, manifestSchema, ScriptContainmentError;
|
|
4265
|
+
var init_manifest = __esm(() => {
|
|
4266
|
+
init_config();
|
|
4267
|
+
HOOK_NAME_RE = /^[\w-]+$/;
|
|
4268
|
+
manifestSchema = z.object({
|
|
4269
|
+
name: z.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4270
|
+
version: z.string().regex(/^\d+\.\d+\.\d+/, "version must be semver (e.g. 1.2.3)"),
|
|
4271
|
+
description: z.string().optional(),
|
|
4272
|
+
events: z.array(z.string()).min(1, "at least one event is required"),
|
|
4273
|
+
script: z.string().min(1, "script must name a relative path or inline content"),
|
|
4274
|
+
args: z.array(z.string()).optional(),
|
|
4275
|
+
timeout_ms: z.number().int().positive().optional()
|
|
4276
|
+
});
|
|
4277
|
+
ScriptContainmentError = class ScriptContainmentError extends Error {
|
|
4278
|
+
constructor(path, hookDir, raw) {
|
|
4279
|
+
super(raw && raw !== path ? `manifest script escapes the hook directory: '${path}' (from '${raw}') resolves outside '${hookDir}'` : `manifest script escapes the hook directory: '${path}' resolves outside '${hookDir}'`);
|
|
4280
|
+
this.name = "ScriptContainmentError";
|
|
4281
|
+
}
|
|
4282
|
+
};
|
|
4283
|
+
});
|
|
4284
|
+
|
|
4285
|
+
// src/lib/resolve.ts
|
|
4286
|
+
var exports_resolve = {};
|
|
4287
|
+
__export(exports_resolve, {
|
|
4288
|
+
resolveScriptPath: () => resolveScriptPath,
|
|
4289
|
+
resolveHookMeta: () => resolveHookMeta,
|
|
4290
|
+
resolveHookDir: () => resolveHookDir,
|
|
4291
|
+
resolveHook: () => resolveHook
|
|
4292
|
+
});
|
|
4293
|
+
import { join as join4 } from "path";
|
|
4294
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4295
|
+
function resolveHookDir(name) {
|
|
4296
|
+
const custom = readCustomManifest(name);
|
|
4297
|
+
if (custom)
|
|
4298
|
+
return customHookDir(name);
|
|
4299
|
+
const meta = getHook(shortManifestName(name));
|
|
4300
|
+
if (!meta)
|
|
4301
|
+
return;
|
|
4302
|
+
const short = shortManifestName(name);
|
|
4303
|
+
const bundledBase = join4(__dirname, "..", "..", "hooks");
|
|
4304
|
+
const direct = join4(bundledBase, short);
|
|
4305
|
+
if (existsSync4(direct))
|
|
4306
|
+
return direct;
|
|
4307
|
+
const prefixed = join4(bundledBase, `hook-${short}`);
|
|
4308
|
+
return existsSync4(prefixed) ? prefixed : undefined;
|
|
4309
|
+
}
|
|
4310
|
+
function resolveScriptPath(name) {
|
|
4311
|
+
const custom = readCustomManifest(name);
|
|
4312
|
+
if (custom)
|
|
4313
|
+
return custom.scriptPath;
|
|
4314
|
+
const dir = resolveHookDir(name);
|
|
4315
|
+
if (!dir)
|
|
4316
|
+
return;
|
|
4317
|
+
const script = join4(dir, "src", "hook.ts");
|
|
4318
|
+
return existsSync4(script) ? script : undefined;
|
|
4319
|
+
}
|
|
4320
|
+
function resolveHookMeta(name) {
|
|
4321
|
+
const custom = readCustomManifest(name);
|
|
4322
|
+
if (custom) {
|
|
4323
|
+
const { manifest } = custom;
|
|
4324
|
+
return {
|
|
4325
|
+
name: shortManifestName(manifest.name),
|
|
4326
|
+
displayName: manifest.description ? manifest.name : manifest.name,
|
|
4327
|
+
description: manifest.description ?? "Custom hook",
|
|
4328
|
+
version: manifest.version,
|
|
4329
|
+
category: "Workflow Automation",
|
|
4330
|
+
event: manifest.events[0] ?? "PostToolUse",
|
|
4331
|
+
events: manifest.events,
|
|
4332
|
+
matcher: "",
|
|
4333
|
+
tags: ["custom"]
|
|
4334
|
+
};
|
|
4335
|
+
}
|
|
4336
|
+
return getHook(shortManifestName(name));
|
|
4337
|
+
}
|
|
4338
|
+
function resolveHook(name) {
|
|
4339
|
+
const custom = readCustomManifest(name);
|
|
4340
|
+
if (custom) {
|
|
4341
|
+
const meta2 = resolveHookMeta(name);
|
|
4342
|
+
return {
|
|
4343
|
+
name: shortManifestName(custom.manifest.name),
|
|
4344
|
+
source: "custom",
|
|
4345
|
+
version: custom.manifest.version,
|
|
4346
|
+
events: custom.manifest.events,
|
|
4347
|
+
description: custom.manifest.description ?? "Custom hook",
|
|
4348
|
+
scriptPath: custom.scriptPath,
|
|
4349
|
+
meta: meta2
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
4352
|
+
const meta = getHook(shortManifestName(name));
|
|
4353
|
+
const scriptPath = resolveScriptPath(name);
|
|
4354
|
+
if (!meta || !scriptPath)
|
|
4355
|
+
return;
|
|
4356
|
+
return {
|
|
4357
|
+
name: shortManifestName(name),
|
|
4358
|
+
source: "bundled",
|
|
4359
|
+
version: meta.version,
|
|
4360
|
+
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
4361
|
+
description: meta.description,
|
|
4362
|
+
scriptPath,
|
|
4363
|
+
meta
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
var __dirname = "/home/hasna/.hasna/repos/worktrees/hasna-apps/workspace-repos-guard/apps/hooks/src/lib";
|
|
4367
|
+
var init_resolve = __esm(() => {
|
|
4368
|
+
init_registry();
|
|
4369
|
+
init_manifest();
|
|
4370
|
+
});
|
|
4371
|
+
|
|
4372
|
+
// src/lib/installer.ts
|
|
4373
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4374
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
4375
|
+
import { homedir as homedir3 } from "os";
|
|
4046
4376
|
import { fileURLToPath } from "url";
|
|
4047
4377
|
function normalizeHookName(name) {
|
|
4048
4378
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
@@ -4063,33 +4393,43 @@ function getTargetSettingsDir(target) {
|
|
|
4063
4393
|
return ".gemini";
|
|
4064
4394
|
return ".claude";
|
|
4065
4395
|
}
|
|
4396
|
+
function getGlobalSettingsPathOverride(target) {
|
|
4397
|
+
if (target === "claude")
|
|
4398
|
+
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
4399
|
+
if (target === "gemini")
|
|
4400
|
+
return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4066
4403
|
function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
|
|
4067
4404
|
if (target === "codewith" && codewithConfigPath)
|
|
4068
4405
|
return codewithConfigPath;
|
|
4069
4406
|
if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
4070
4407
|
return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
|
|
4071
4408
|
}
|
|
4409
|
+
const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
|
|
4410
|
+
if (globalOverride)
|
|
4411
|
+
return globalOverride;
|
|
4072
4412
|
const dir = getTargetSettingsDir(target);
|
|
4073
4413
|
if (scope === "project") {
|
|
4074
|
-
return target === "codewith" ?
|
|
4414
|
+
return target === "codewith" ? join5(process.cwd(), dir, "config.toml") : join5(process.cwd(), dir, "settings.json");
|
|
4075
4415
|
}
|
|
4076
|
-
return target === "codewith" ?
|
|
4416
|
+
return target === "codewith" ? join5(homedir3(), dir, "config.toml") : join5(homedir3(), dir, "settings.json");
|
|
4077
4417
|
}
|
|
4078
4418
|
function getHookPath(name) {
|
|
4079
4419
|
const shortName = shortHookName(name);
|
|
4080
|
-
const
|
|
4081
|
-
if (
|
|
4082
|
-
return
|
|
4083
|
-
return
|
|
4420
|
+
const resolved = resolveHookDir(shortName);
|
|
4421
|
+
if (resolved)
|
|
4422
|
+
return resolved;
|
|
4423
|
+
return join5(HOOKS_DIR, normalizeHookName(shortName));
|
|
4084
4424
|
}
|
|
4085
4425
|
function hookExists(name) {
|
|
4086
|
-
return
|
|
4426
|
+
return existsSync5(getHookPath(name));
|
|
4087
4427
|
}
|
|
4088
4428
|
function readSettings(scope = "global", target = "claude") {
|
|
4089
4429
|
const path = getSettingsPath(scope, target);
|
|
4090
4430
|
try {
|
|
4091
|
-
if (
|
|
4092
|
-
return JSON.parse(
|
|
4431
|
+
if (existsSync5(path)) {
|
|
4432
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
4093
4433
|
}
|
|
4094
4434
|
} catch (error) {
|
|
4095
4435
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -4098,11 +4438,11 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
4098
4438
|
}
|
|
4099
4439
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
4100
4440
|
const path = getSettingsPath(scope, target);
|
|
4101
|
-
const dir =
|
|
4102
|
-
if (!
|
|
4103
|
-
|
|
4441
|
+
const dir = dirname2(path);
|
|
4442
|
+
if (!existsSync5(dir)) {
|
|
4443
|
+
mkdirSync3(dir, { recursive: true });
|
|
4104
4444
|
}
|
|
4105
|
-
|
|
4445
|
+
writeFileSync3(path, JSON.stringify(settings, null, 2) + `
|
|
4106
4446
|
`);
|
|
4107
4447
|
}
|
|
4108
4448
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -4160,7 +4500,7 @@ function codewithStatusMessage(name) {
|
|
|
4160
4500
|
}
|
|
4161
4501
|
function buildCodewithTomlFragment(name, profile) {
|
|
4162
4502
|
const shortName = shortHookName(name);
|
|
4163
|
-
const meta =
|
|
4503
|
+
const meta = resolveHookMeta(shortName);
|
|
4164
4504
|
if (!meta)
|
|
4165
4505
|
throw new Error(`Hook '${shortName}' not found`);
|
|
4166
4506
|
const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
|
|
@@ -4188,25 +4528,25 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
4188
4528
|
function readCodewithConfig(scope, configPath) {
|
|
4189
4529
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
4190
4530
|
try {
|
|
4191
|
-
return
|
|
4531
|
+
return existsSync5(path) ? readFileSync3(path, "utf-8") : "";
|
|
4192
4532
|
} catch {
|
|
4193
4533
|
return "";
|
|
4194
4534
|
}
|
|
4195
4535
|
}
|
|
4196
4536
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
4197
4537
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
4198
|
-
const dir =
|
|
4199
|
-
if (!
|
|
4200
|
-
|
|
4201
|
-
const existing =
|
|
4202
|
-
const
|
|
4538
|
+
const dir = dirname2(path);
|
|
4539
|
+
if (!existsSync5(dir))
|
|
4540
|
+
mkdirSync3(dir, { recursive: true });
|
|
4541
|
+
const existing = existsSync5(path) ? readFileSync3(path, "utf-8") : "";
|
|
4542
|
+
const sep2 = existing.trim() ? `
|
|
4203
4543
|
|
|
4204
4544
|
` : "";
|
|
4205
|
-
|
|
4545
|
+
writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
4206
4546
|
return path;
|
|
4207
4547
|
}
|
|
4208
4548
|
function detectConflict(name, scope, target) {
|
|
4209
|
-
const meta =
|
|
4549
|
+
const meta = resolveHookMeta(name);
|
|
4210
4550
|
if (!meta || !meta.matcher)
|
|
4211
4551
|
return;
|
|
4212
4552
|
const events = new Set(getHookEvents(meta));
|
|
@@ -4302,7 +4642,7 @@ function installHook(name, options = {}) {
|
|
|
4302
4642
|
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
|
|
4303
4643
|
if (target === "all") {
|
|
4304
4644
|
const shortName = shortHookName(name);
|
|
4305
|
-
const meta =
|
|
4645
|
+
const meta = resolveHookMeta(shortName);
|
|
4306
4646
|
if (meta) {
|
|
4307
4647
|
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
4308
4648
|
if (unsupportedTargets.length > 0) {
|
|
@@ -4335,7 +4675,7 @@ function installHook(name, options = {}) {
|
|
|
4335
4675
|
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
|
|
4336
4676
|
}
|
|
4337
4677
|
function registerHook(name, scope = "global", target = "claude", profile) {
|
|
4338
|
-
const meta =
|
|
4678
|
+
const meta = resolveHookMeta(name);
|
|
4339
4679
|
if (!meta)
|
|
4340
4680
|
return;
|
|
4341
4681
|
const eventKeys = getHookEvents(meta).map((event) => {
|
|
@@ -4378,7 +4718,7 @@ function removeHookFromAllEvents(settings, name) {
|
|
|
4378
4718
|
}
|
|
4379
4719
|
}
|
|
4380
4720
|
function unregisterHook(name, scope = "global", target = "claude") {
|
|
4381
|
-
const meta =
|
|
4721
|
+
const meta = resolveHookMeta(name);
|
|
4382
4722
|
if (!meta)
|
|
4383
4723
|
return;
|
|
4384
4724
|
const settings = readSettings(scope, target);
|
|
@@ -4445,8 +4785,9 @@ function removeHookForTarget(name, scope, target) {
|
|
|
4445
4785
|
var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks;
|
|
4446
4786
|
var init_installer = __esm(() => {
|
|
4447
4787
|
init_registry();
|
|
4448
|
-
|
|
4449
|
-
|
|
4788
|
+
init_resolve();
|
|
4789
|
+
__dirname2 = dirname2(fileURLToPath(import.meta.url));
|
|
4790
|
+
HOOKS_DIR = existsSync5(join5(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join5(__dirname2, "..", "..", "hooks") : join5(__dirname2, "..", "hooks");
|
|
4450
4791
|
EVENT_MAP = {
|
|
4451
4792
|
claude: {
|
|
4452
4793
|
PreToolUse: "PreToolUse",
|
|
@@ -4483,25 +4824,25 @@ var init_installer = __esm(() => {
|
|
|
4483
4824
|
});
|
|
4484
4825
|
|
|
4485
4826
|
// src/lib/profiles.ts
|
|
4486
|
-
import { existsSync as
|
|
4487
|
-
import { join as
|
|
4488
|
-
import { homedir as
|
|
4827
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, readdirSync as readdirSync2, rmSync, cpSync } from "fs";
|
|
4828
|
+
import { join as join6 } from "path";
|
|
4829
|
+
import { homedir as homedir4 } from "os";
|
|
4489
4830
|
function resolveProfilesDir() {
|
|
4490
|
-
const newDir =
|
|
4491
|
-
const oldDir =
|
|
4492
|
-
if (!
|
|
4493
|
-
|
|
4831
|
+
const newDir = join6(homedir4(), ".hasna", "hooks", "profiles");
|
|
4832
|
+
const oldDir = join6(homedir4(), ".hooks", "profiles");
|
|
4833
|
+
if (!existsSync6(newDir) && existsSync6(oldDir)) {
|
|
4834
|
+
mkdirSync4(join6(homedir4(), ".hasna", "hooks"), { recursive: true });
|
|
4494
4835
|
cpSync(oldDir, newDir, { recursive: true });
|
|
4495
4836
|
}
|
|
4496
4837
|
return newDir;
|
|
4497
4838
|
}
|
|
4498
4839
|
function ensureProfilesDir() {
|
|
4499
|
-
if (!
|
|
4500
|
-
|
|
4840
|
+
if (!existsSync6(PROFILES_DIR)) {
|
|
4841
|
+
mkdirSync4(PROFILES_DIR, { recursive: true });
|
|
4501
4842
|
}
|
|
4502
4843
|
}
|
|
4503
4844
|
function profilePath(id) {
|
|
4504
|
-
return
|
|
4845
|
+
return join6(PROFILES_DIR, `${id}.json`);
|
|
4505
4846
|
}
|
|
4506
4847
|
function shortUuid() {
|
|
4507
4848
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -4520,29 +4861,29 @@ function createProfile(input) {
|
|
|
4520
4861
|
if (input.name) {
|
|
4521
4862
|
profile.name = input.name;
|
|
4522
4863
|
}
|
|
4523
|
-
|
|
4864
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4524
4865
|
`);
|
|
4525
4866
|
return profile;
|
|
4526
4867
|
}
|
|
4527
4868
|
function getProfile(id) {
|
|
4528
4869
|
const path = profilePath(id);
|
|
4529
4870
|
try {
|
|
4530
|
-
if (!
|
|
4871
|
+
if (!existsSync6(path))
|
|
4531
4872
|
return null;
|
|
4532
|
-
return JSON.parse(
|
|
4873
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
4533
4874
|
} catch {
|
|
4534
4875
|
return null;
|
|
4535
4876
|
}
|
|
4536
4877
|
}
|
|
4537
4878
|
function listProfiles() {
|
|
4538
|
-
if (!
|
|
4879
|
+
if (!existsSync6(PROFILES_DIR))
|
|
4539
4880
|
return [];
|
|
4540
4881
|
try {
|
|
4541
|
-
const files =
|
|
4882
|
+
const files = readdirSync2(PROFILES_DIR).filter((f) => f.endsWith(".json"));
|
|
4542
4883
|
const profiles = [];
|
|
4543
4884
|
for (const file of files) {
|
|
4544
4885
|
try {
|
|
4545
|
-
const content =
|
|
4886
|
+
const content = readFileSync4(join6(PROFILES_DIR, file), "utf-8");
|
|
4546
4887
|
profiles.push(JSON.parse(content));
|
|
4547
4888
|
} catch {}
|
|
4548
4889
|
}
|
|
@@ -4556,7 +4897,7 @@ function touchProfile(id) {
|
|
|
4556
4897
|
if (!profile)
|
|
4557
4898
|
return;
|
|
4558
4899
|
profile.last_seen_at = new Date().toISOString();
|
|
4559
|
-
|
|
4900
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4560
4901
|
`);
|
|
4561
4902
|
}
|
|
4562
4903
|
function exportProfiles() {
|
|
@@ -4572,11 +4913,11 @@ function importProfiles(profiles) {
|
|
|
4572
4913
|
continue;
|
|
4573
4914
|
}
|
|
4574
4915
|
const path = profilePath(profile.agent_id);
|
|
4575
|
-
if (
|
|
4916
|
+
if (existsSync6(path)) {
|
|
4576
4917
|
skipped++;
|
|
4577
4918
|
continue;
|
|
4578
4919
|
}
|
|
4579
|
-
|
|
4920
|
+
writeFileSync4(path, JSON.stringify(profile, null, 2) + `
|
|
4580
4921
|
`);
|
|
4581
4922
|
imported++;
|
|
4582
4923
|
}
|
|
@@ -4683,6 +5024,32 @@ var init_003_user_prompt_submit_event = __esm(() => {
|
|
|
4683
5024
|
init_schema();
|
|
4684
5025
|
});
|
|
4685
5026
|
|
|
5027
|
+
// src/db/migrations/004_hooks_table.ts
|
|
5028
|
+
function up4(db) {
|
|
5029
|
+
db.exec(CREATE_HOOKS_TABLE);
|
|
5030
|
+
for (const idx of CREATE_HOOKS_INDEXES) {
|
|
5031
|
+
db.exec(idx);
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
var CREATE_HOOKS_TABLE = `
|
|
5035
|
+
CREATE TABLE IF NOT EXISTS hooks (
|
|
5036
|
+
id TEXT PRIMARY KEY,
|
|
5037
|
+
name TEXT NOT NULL,
|
|
5038
|
+
version TEXT NOT NULL,
|
|
5039
|
+
sha256 TEXT NOT NULL,
|
|
5040
|
+
source_type TEXT NOT NULL,
|
|
5041
|
+
source_ref TEXT,
|
|
5042
|
+
installed_at TEXT NOT NULL,
|
|
5043
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
5044
|
+
last_verified_at TEXT
|
|
5045
|
+
)
|
|
5046
|
+
`, CREATE_HOOKS_INDEXES;
|
|
5047
|
+
var init_004_hooks_table = __esm(() => {
|
|
5048
|
+
CREATE_HOOKS_INDEXES = [
|
|
5049
|
+
`CREATE INDEX IF NOT EXISTS idx_hooks_name ON hooks (name)`
|
|
5050
|
+
];
|
|
5051
|
+
});
|
|
5052
|
+
|
|
4686
5053
|
// src/db/migrations/index.ts
|
|
4687
5054
|
function ensureMigrationsTable(db) {
|
|
4688
5055
|
db.exec(`
|
|
@@ -4714,17 +5081,19 @@ var init_migrations = __esm(() => {
|
|
|
4714
5081
|
init_001_initial();
|
|
4715
5082
|
init_002_session_events();
|
|
4716
5083
|
init_003_user_prompt_submit_event();
|
|
5084
|
+
init_004_hooks_table();
|
|
4717
5085
|
MIGRATIONS = [
|
|
4718
5086
|
{ version: "001_initial", up },
|
|
4719
5087
|
{ version: "002_session_events", up: up2 },
|
|
4720
|
-
{ version: "003_user_prompt_submit_event", up: up3 }
|
|
5088
|
+
{ version: "003_user_prompt_submit_event", up: up3 },
|
|
5089
|
+
{ version: "004_hooks_table", up: up4 }
|
|
4721
5090
|
];
|
|
4722
5091
|
});
|
|
4723
5092
|
|
|
4724
5093
|
// src/db/legacy-import.ts
|
|
4725
|
-
import { existsSync as
|
|
4726
|
-
import { join as
|
|
4727
|
-
import { homedir as
|
|
5094
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
5095
|
+
import { join as join7 } from "path";
|
|
5096
|
+
import { homedir as homedir5 } from "os";
|
|
4728
5097
|
function ensureMetaTable(db) {
|
|
4729
5098
|
db.exec(`
|
|
4730
5099
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -4747,7 +5116,7 @@ function nanoid() {
|
|
|
4747
5116
|
function importJsonlFile(db, filePath) {
|
|
4748
5117
|
let count = 0;
|
|
4749
5118
|
try {
|
|
4750
|
-
const lines =
|
|
5119
|
+
const lines = readFileSync5(filePath, "utf-8").split(`
|
|
4751
5120
|
`).filter(Boolean);
|
|
4752
5121
|
for (const line of lines) {
|
|
4753
5122
|
try {
|
|
@@ -4773,7 +5142,7 @@ function importJsonlFile(db, filePath) {
|
|
|
4773
5142
|
function importErrorsLog(db, filePath) {
|
|
4774
5143
|
let count = 0;
|
|
4775
5144
|
try {
|
|
4776
|
-
const lines =
|
|
5145
|
+
const lines = readFileSync5(filePath, "utf-8").split(`
|
|
4777
5146
|
`).filter(Boolean);
|
|
4778
5147
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
4779
5148
|
for (const line of lines) {
|
|
@@ -4798,25 +5167,25 @@ function importErrorsLog(db, filePath) {
|
|
|
4798
5167
|
} catch {}
|
|
4799
5168
|
return count;
|
|
4800
5169
|
}
|
|
4801
|
-
function runLegacyImport(db) {
|
|
5170
|
+
function runLegacyImport(db, homeDir = homedir5()) {
|
|
4802
5171
|
try {
|
|
4803
5172
|
if (isAlreadyDone(db))
|
|
4804
5173
|
return;
|
|
4805
5174
|
let total = 0;
|
|
4806
|
-
const claudeProjectsDir =
|
|
4807
|
-
if (
|
|
5175
|
+
const claudeProjectsDir = join7(homeDir, ".claude", "projects");
|
|
5176
|
+
if (existsSync7(claudeProjectsDir)) {
|
|
4808
5177
|
try {
|
|
4809
|
-
const projectDirs =
|
|
5178
|
+
const projectDirs = readdirSync3(claudeProjectsDir);
|
|
4810
5179
|
for (const dir of projectDirs) {
|
|
4811
|
-
const projectDir =
|
|
5180
|
+
const projectDir = join7(claudeProjectsDir, dir);
|
|
4812
5181
|
try {
|
|
4813
|
-
const files =
|
|
5182
|
+
const files = readdirSync3(projectDir);
|
|
4814
5183
|
for (const file of files) {
|
|
4815
5184
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
4816
|
-
total += importJsonlFile(db,
|
|
5185
|
+
total += importJsonlFile(db, join7(projectDir, file));
|
|
4817
5186
|
}
|
|
4818
5187
|
if (file === "errors.log") {
|
|
4819
|
-
total += importErrorsLog(db,
|
|
5188
|
+
total += importErrorsLog(db, join7(projectDir, file));
|
|
4820
5189
|
}
|
|
4821
5190
|
}
|
|
4822
5191
|
} catch {}
|
|
@@ -4859,17 +5228,17 @@ __export(exports_db, {
|
|
|
4859
5228
|
closeDb: () => closeDb
|
|
4860
5229
|
});
|
|
4861
5230
|
import { Database } from "bun:sqlite";
|
|
4862
|
-
import { existsSync as
|
|
4863
|
-
import { join as
|
|
4864
|
-
import { homedir as
|
|
5231
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, cpSync as cpSync2 } from "fs";
|
|
5232
|
+
import { join as join8 } from "path";
|
|
5233
|
+
import { homedir as homedir6 } from "os";
|
|
4865
5234
|
function resolveDataDir() {
|
|
4866
5235
|
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
4867
5236
|
if (explicit)
|
|
4868
5237
|
return explicit;
|
|
4869
|
-
const newDir =
|
|
4870
|
-
const oldDir =
|
|
4871
|
-
if (!
|
|
4872
|
-
|
|
5238
|
+
const newDir = join8(homedir6(), ".hasna", "hooks");
|
|
5239
|
+
const oldDir = join8(homedir6(), ".hooks");
|
|
5240
|
+
if (!existsSync8(newDir) && existsSync8(oldDir)) {
|
|
5241
|
+
mkdirSync5(join8(homedir6(), ".hasna"), { recursive: true });
|
|
4873
5242
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
4874
5243
|
}
|
|
4875
5244
|
return newDir;
|
|
@@ -4879,19 +5248,19 @@ function getDbPath() {
|
|
|
4879
5248
|
if (explicitDb)
|
|
4880
5249
|
return explicitDb;
|
|
4881
5250
|
const dataDir = resolveDataDir();
|
|
4882
|
-
return
|
|
5251
|
+
return join8(dataDir, "hooks.db");
|
|
4883
5252
|
}
|
|
4884
5253
|
function ensureDir(dbPath) {
|
|
4885
5254
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
4886
|
-
if (dir && !
|
|
4887
|
-
|
|
5255
|
+
if (dir && !existsSync8(dir)) {
|
|
5256
|
+
mkdirSync5(dir, { recursive: true });
|
|
4888
5257
|
}
|
|
4889
5258
|
}
|
|
4890
5259
|
function getDb() {
|
|
4891
5260
|
if (instance)
|
|
4892
5261
|
return instance;
|
|
4893
5262
|
const dbPath = getDbPath();
|
|
4894
|
-
const isNew = dbPath === ":memory:" || !
|
|
5263
|
+
const isNew = dbPath === ":memory:" || !existsSync8(dbPath);
|
|
4895
5264
|
ensureDir(dbPath);
|
|
4896
5265
|
instance = new Database(dbPath);
|
|
4897
5266
|
instance.exec("PRAGMA journal_mode=WAL");
|
|
@@ -4930,6 +5299,313 @@ var init_db = __esm(() => {
|
|
|
4930
5299
|
init_legacy_import();
|
|
4931
5300
|
});
|
|
4932
5301
|
|
|
5302
|
+
// src/lib/store.ts
|
|
5303
|
+
var exports_store = {};
|
|
5304
|
+
__export(exports_store, {
|
|
5305
|
+
writeLock: () => writeLock,
|
|
5306
|
+
verifyScriptHash: () => verifyScriptHash,
|
|
5307
|
+
upsertHookRecord: () => upsertHookRecord,
|
|
5308
|
+
sha256Of: () => sha256Of,
|
|
5309
|
+
sha256File: () => sha256File,
|
|
5310
|
+
setPinnedHook: () => setPinnedHook,
|
|
5311
|
+
retrustHook: () => retrustHook,
|
|
5312
|
+
removePinnedHook: () => removePinnedHook,
|
|
5313
|
+
removeHookRecord: () => removeHookRecord,
|
|
5314
|
+
readLock: () => readLock,
|
|
5315
|
+
listHookRecords: () => listHookRecords,
|
|
5316
|
+
getPinnedHook: () => getPinnedHook,
|
|
5317
|
+
getHookRecord: () => getHookRecord,
|
|
5318
|
+
checkScriptHash: () => checkScriptHash
|
|
5319
|
+
});
|
|
5320
|
+
import { createHash } from "crypto";
|
|
5321
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
5322
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
5323
|
+
function sha256Of(input) {
|
|
5324
|
+
return createHash("sha256").update(input).digest("hex");
|
|
5325
|
+
}
|
|
5326
|
+
async function sha256File(path) {
|
|
5327
|
+
const buf = await readFile2(path);
|
|
5328
|
+
return sha256Of(buf);
|
|
5329
|
+
}
|
|
5330
|
+
function getHookRecord(db, name) {
|
|
5331
|
+
return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
|
|
5332
|
+
}
|
|
5333
|
+
function listHookRecords(db) {
|
|
5334
|
+
return db.query("SELECT * FROM hooks ORDER BY name").all();
|
|
5335
|
+
}
|
|
5336
|
+
function upsertHookRecord(db, record) {
|
|
5337
|
+
const existing = getHookRecord(db, record.name);
|
|
5338
|
+
const id = existing?.id ?? record.name;
|
|
5339
|
+
const installedAt = existing?.installed_at ?? new Date().toISOString();
|
|
5340
|
+
const enabled = record.enabled ?? existing?.enabled ?? 1;
|
|
5341
|
+
db.run(`INSERT INTO hooks (id, name, version, sha256, source_type, source_ref, installed_at, enabled, last_verified_at)
|
|
5342
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5343
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
5344
|
+
version = excluded.version,
|
|
5345
|
+
sha256 = excluded.sha256,
|
|
5346
|
+
source_type = excluded.source_type,
|
|
5347
|
+
source_ref = excluded.source_ref,
|
|
5348
|
+
enabled = excluded.enabled,
|
|
5349
|
+
last_verified_at = excluded.last_verified_at`, [
|
|
5350
|
+
id,
|
|
5351
|
+
record.name,
|
|
5352
|
+
record.version,
|
|
5353
|
+
record.sha256,
|
|
5354
|
+
record.source_type,
|
|
5355
|
+
record.source_ref ?? existing?.source_ref ?? null,
|
|
5356
|
+
installedAt,
|
|
5357
|
+
enabled,
|
|
5358
|
+
record.last_verified_at ?? null
|
|
5359
|
+
]);
|
|
5360
|
+
}
|
|
5361
|
+
function removeHookRecord(db, name) {
|
|
5362
|
+
const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
|
|
5363
|
+
return res.changes > 0;
|
|
5364
|
+
}
|
|
5365
|
+
function readLock() {
|
|
5366
|
+
try {
|
|
5367
|
+
const path = getLockPath();
|
|
5368
|
+
if (!existsSync9(path))
|
|
5369
|
+
return { hooks: {} };
|
|
5370
|
+
const parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
5371
|
+
return { hooks: parsed.hooks ?? {} };
|
|
5372
|
+
} catch {
|
|
5373
|
+
return { hooks: {} };
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
function writeLock(lock) {
|
|
5377
|
+
const path = getLockPath();
|
|
5378
|
+
mkdirSync6(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
|
|
5379
|
+
const sorted = {};
|
|
5380
|
+
for (const name of Object.keys(lock.hooks).sort()) {
|
|
5381
|
+
sorted[name] = lock.hooks[name];
|
|
5382
|
+
}
|
|
5383
|
+
writeFileSync5(path, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
5384
|
+
`, "utf-8");
|
|
5385
|
+
return path;
|
|
5386
|
+
}
|
|
5387
|
+
function setPinnedHook(name, entry) {
|
|
5388
|
+
const lock = readLock();
|
|
5389
|
+
lock.hooks[name] = entry;
|
|
5390
|
+
return writeLock(lock);
|
|
5391
|
+
}
|
|
5392
|
+
function getPinnedHook(name) {
|
|
5393
|
+
return readLock().hooks[name];
|
|
5394
|
+
}
|
|
5395
|
+
function removePinnedHook(name) {
|
|
5396
|
+
const lock = readLock();
|
|
5397
|
+
if (!(name in lock.hooks))
|
|
5398
|
+
return false;
|
|
5399
|
+
delete lock.hooks[name];
|
|
5400
|
+
writeLock(lock);
|
|
5401
|
+
return true;
|
|
5402
|
+
}
|
|
5403
|
+
function checkScriptHash(name, actual) {
|
|
5404
|
+
const db = getDb();
|
|
5405
|
+
const record = getHookRecord(db, name);
|
|
5406
|
+
const pin = getPinnedHook(name);
|
|
5407
|
+
const expected = record?.sha256 ?? pin?.sha256 ?? null;
|
|
5408
|
+
if (expected !== null && expected !== actual) {
|
|
5409
|
+
return { ok: false, pinned: true, expected, actual, name };
|
|
5410
|
+
}
|
|
5411
|
+
if (expected === null) {
|
|
5412
|
+
const now3 = new Date().toISOString();
|
|
5413
|
+
const version = pin?.version ?? record?.version ?? "0.0.0";
|
|
5414
|
+
const source = pin?.source ?? record?.source_type ?? "local";
|
|
5415
|
+
upsertHookRecord(db, {
|
|
5416
|
+
name,
|
|
5417
|
+
version,
|
|
5418
|
+
sha256: actual,
|
|
5419
|
+
source_type: source,
|
|
5420
|
+
last_verified_at: now3
|
|
5421
|
+
});
|
|
5422
|
+
setPinnedHook(name, { version, sha256: actual, source });
|
|
5423
|
+
return { ok: true, pinned: false, expected: null, actual, name };
|
|
5424
|
+
}
|
|
5425
|
+
const now2 = new Date().toISOString();
|
|
5426
|
+
upsertHookRecord(db, {
|
|
5427
|
+
name,
|
|
5428
|
+
version: record?.version ?? pin?.version ?? "0.0.0",
|
|
5429
|
+
sha256: expected,
|
|
5430
|
+
source_type: record?.source_type ?? pin?.source ?? "local",
|
|
5431
|
+
last_verified_at: now2
|
|
5432
|
+
});
|
|
5433
|
+
return { ok: true, pinned: true, expected, actual, name };
|
|
5434
|
+
}
|
|
5435
|
+
async function verifyScriptHash(name, scriptPath) {
|
|
5436
|
+
return checkScriptHash(name, await sha256File(scriptPath));
|
|
5437
|
+
}
|
|
5438
|
+
function retrustHook(name, scriptPath, version, source) {
|
|
5439
|
+
const actual = sha256Of(readFileSync6(scriptPath));
|
|
5440
|
+
const db = getDb();
|
|
5441
|
+
upsertHookRecord(db, {
|
|
5442
|
+
name,
|
|
5443
|
+
version,
|
|
5444
|
+
sha256: actual,
|
|
5445
|
+
source_type: source,
|
|
5446
|
+
last_verified_at: new Date().toISOString()
|
|
5447
|
+
});
|
|
5448
|
+
setPinnedHook(name, { version, sha256: actual, source });
|
|
5449
|
+
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
5450
|
+
}
|
|
5451
|
+
var init_store = __esm(() => {
|
|
5452
|
+
init_db();
|
|
5453
|
+
init_config();
|
|
5454
|
+
});
|
|
5455
|
+
|
|
5456
|
+
// src/lib/run.ts
|
|
5457
|
+
var exports_run = {};
|
|
5458
|
+
__export(exports_run, {
|
|
5459
|
+
executeVerifiedScript: () => executeVerifiedScript
|
|
5460
|
+
});
|
|
5461
|
+
import { randomBytes } from "crypto";
|
|
5462
|
+
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
5463
|
+
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
5464
|
+
function extensionOf(scriptPath) {
|
|
5465
|
+
const base = basename2(scriptPath);
|
|
5466
|
+
const dot = base.lastIndexOf(".");
|
|
5467
|
+
return dot > 0 ? base.slice(dot + 1) : "ts";
|
|
5468
|
+
}
|
|
5469
|
+
async function executeVerifiedScript(options) {
|
|
5470
|
+
const scriptDir = dirname3(options.scriptPath);
|
|
5471
|
+
const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${extensionOf(options.scriptPath)}`);
|
|
5472
|
+
try {
|
|
5473
|
+
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
5474
|
+
const proc = Bun.spawn(["bun", "run", isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
5475
|
+
stdin: new Response(options.stdin),
|
|
5476
|
+
stdout: "pipe",
|
|
5477
|
+
stderr: "pipe",
|
|
5478
|
+
env: options.env ?? process.env,
|
|
5479
|
+
...options.timeout ? { timeout: options.timeout } : {}
|
|
5480
|
+
});
|
|
5481
|
+
const [stdoutText, stderrText, exitCode] = await Promise.all([
|
|
5482
|
+
new Response(proc.stdout).text(),
|
|
5483
|
+
new Response(proc.stderr).text(),
|
|
5484
|
+
proc.exited
|
|
5485
|
+
]);
|
|
5486
|
+
return { stdout: stdoutText, stderr: stderrText, exitCode };
|
|
5487
|
+
} finally {
|
|
5488
|
+
try {
|
|
5489
|
+
rmSync2(tempPath, { force: true });
|
|
5490
|
+
} catch {}
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
var init_run = () => {};
|
|
5494
|
+
|
|
5495
|
+
// src/lib/custom-install.ts
|
|
5496
|
+
var exports_custom_install = {};
|
|
5497
|
+
__export(exports_custom_install, {
|
|
5498
|
+
isCustomSource: () => isCustomSource,
|
|
5499
|
+
installCustomSource: () => installCustomSource,
|
|
5500
|
+
ensureCustomDir: () => ensureCustomDir
|
|
5501
|
+
});
|
|
5502
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
5503
|
+
import { join as join10 } from "path";
|
|
5504
|
+
import { tmpdir } from "os";
|
|
5505
|
+
function manifestAt(dir) {
|
|
5506
|
+
const path = join10(dir, "manifest.json");
|
|
5507
|
+
if (!existsSync10(path))
|
|
5508
|
+
return;
|
|
5509
|
+
try {
|
|
5510
|
+
return parseManifest(readFileSync7(path, "utf-8"));
|
|
5511
|
+
} catch {
|
|
5512
|
+
return;
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
function copyManifestDir(srcDir, source, kind) {
|
|
5516
|
+
const manifest = manifestAt(srcDir);
|
|
5517
|
+
if (!manifest) {
|
|
5518
|
+
throw new Error(`No valid manifest.json found at ${srcDir}`);
|
|
5519
|
+
}
|
|
5520
|
+
return writeFromManifest(manifest, srcDir, source, kind);
|
|
5521
|
+
}
|
|
5522
|
+
function writeFromManifest(manifest, srcDir, source, kind) {
|
|
5523
|
+
const script = resolveScript(manifest, srcDir);
|
|
5524
|
+
const name = shortManifestName(manifest.name);
|
|
5525
|
+
const { dir, scriptPath } = writeCustomHook(name, manifest, script.content, script.path);
|
|
5526
|
+
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
5527
|
+
}
|
|
5528
|
+
async function cloneToTemp(gitUrl) {
|
|
5529
|
+
const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
|
|
5530
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
|
|
5531
|
+
stdout: "pipe",
|
|
5532
|
+
stderr: "pipe"
|
|
5533
|
+
});
|
|
5534
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
5535
|
+
new Response(proc.stdout).text(),
|
|
5536
|
+
new Response(proc.stderr).text(),
|
|
5537
|
+
proc.exited
|
|
5538
|
+
]);
|
|
5539
|
+
if (exitCode !== 0) {
|
|
5540
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
5541
|
+
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
5542
|
+
}
|
|
5543
|
+
return join10(tmp, "repo");
|
|
5544
|
+
}
|
|
5545
|
+
function isGitUrl(value) {
|
|
5546
|
+
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
5547
|
+
}
|
|
5548
|
+
function isCustomSource(value) {
|
|
5549
|
+
if (isGitUrl(value))
|
|
5550
|
+
return true;
|
|
5551
|
+
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
5552
|
+
return true;
|
|
5553
|
+
return existsSync10(value);
|
|
5554
|
+
}
|
|
5555
|
+
async function installCustomSource(source) {
|
|
5556
|
+
if (isGitUrl(source)) {
|
|
5557
|
+
const repoDir = await cloneToTemp(source);
|
|
5558
|
+
try {
|
|
5559
|
+
return copyManifestDir(repoDir, source, "git");
|
|
5560
|
+
} finally {
|
|
5561
|
+
rmSync3(repoDir, { recursive: true, force: true });
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
5565
|
+
return installManifestUrl(source);
|
|
5566
|
+
}
|
|
5567
|
+
if (!existsSync10(source)) {
|
|
5568
|
+
throw new Error(`No such local path: ${source}`);
|
|
5569
|
+
}
|
|
5570
|
+
if (source.endsWith("manifest.json")) {
|
|
5571
|
+
const manifest = parseManifest(readFileSync7(source, "utf-8"));
|
|
5572
|
+
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
5573
|
+
}
|
|
5574
|
+
return copyManifestDir(source, source, "local");
|
|
5575
|
+
}
|
|
5576
|
+
async function installManifestUrl(url) {
|
|
5577
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
5578
|
+
if (!res.ok) {
|
|
5579
|
+
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
5580
|
+
}
|
|
5581
|
+
const manifest = parseManifest(await res.text());
|
|
5582
|
+
let script;
|
|
5583
|
+
if (manifest.script.includes(`
|
|
5584
|
+
`)) {
|
|
5585
|
+
script = { path: "script.ts", content: manifest.script };
|
|
5586
|
+
} else {
|
|
5587
|
+
const base = url.substring(0, url.lastIndexOf("/"));
|
|
5588
|
+
const scriptUrl = `${base}/${manifest.script}`;
|
|
5589
|
+
const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
|
|
5590
|
+
if (!scriptRes.ok) {
|
|
5591
|
+
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
5592
|
+
}
|
|
5593
|
+
script = { path: manifest.script, content: await scriptRes.text() };
|
|
5594
|
+
}
|
|
5595
|
+
const name = shortManifestName(manifest.name);
|
|
5596
|
+
const { dir, scriptPath } = writeCustomHook(name, manifest, script.content, script.path);
|
|
5597
|
+
return { name, version: manifest.version, kind: "url", source: url, dir, scriptPath };
|
|
5598
|
+
}
|
|
5599
|
+
function ensureCustomDir() {
|
|
5600
|
+
const dir = getCustomHooksDir();
|
|
5601
|
+
mkdirSync7(dir, { recursive: true });
|
|
5602
|
+
return dir;
|
|
5603
|
+
}
|
|
5604
|
+
var init_custom_install = __esm(() => {
|
|
5605
|
+
init_config();
|
|
5606
|
+
init_manifest();
|
|
5607
|
+
});
|
|
5608
|
+
|
|
4933
5609
|
// src/db/pg-migrations.ts
|
|
4934
5610
|
var PG_MIGRATIONS;
|
|
4935
5611
|
var init_pg_migrations = __esm(() => {
|
|
@@ -5014,11 +5690,30 @@ function readEnv(name) {
|
|
|
5014
5690
|
const value = process.env[name]?.trim();
|
|
5015
5691
|
return value || undefined;
|
|
5016
5692
|
}
|
|
5017
|
-
function
|
|
5018
|
-
const
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5693
|
+
function assertNoRetiredModeEnv() {
|
|
5694
|
+
for (const name of RETIRED_STORAGE_MODE_ENV) {
|
|
5695
|
+
const value = readEnv(name);
|
|
5696
|
+
if (!value)
|
|
5697
|
+
continue;
|
|
5698
|
+
const replacement = RETIRED_MODE_REPLACEMENT[value.trim().toLowerCase()];
|
|
5699
|
+
const mapping = replacement ? `${value} maps to ${replacement}` : `use one of ${STORAGE_BACKENDS.join(", ")}`;
|
|
5700
|
+
throw new Error(`${name} is a retired deployment-mode variable and is no longer read. ` + `Hooks storage is a data-backend switch, not a deployment mode: ` + `set ${HOOKS_STORAGE_BACKEND_ENV} to ${STORAGE_BACKENDS.join(" or ")} instead (${mapping}), ` + `then unset ${name}.`);
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
function normalizeStorageBackend(value, envName) {
|
|
5704
|
+
if (value === undefined)
|
|
5705
|
+
return;
|
|
5706
|
+
const normalized = value.trim().toLowerCase();
|
|
5707
|
+
if (normalized === "")
|
|
5708
|
+
return;
|
|
5709
|
+
const backend = BACKEND_ALIASES[normalized];
|
|
5710
|
+
if (backend)
|
|
5711
|
+
return backend;
|
|
5712
|
+
const replacement = RETIRED_MODE_REPLACEMENT[normalized];
|
|
5713
|
+
if (replacement) {
|
|
5714
|
+
throw new Error(`${envName}=${value} names a retired deployment mode. ` + `local/hybrid/remote/self_hosted/cloud were removed: hooks storage now selects a data ` + `backend only. Set ${envName}=${replacement} instead.`);
|
|
5715
|
+
}
|
|
5716
|
+
throw new Error(`${envName}=${value} is not a known hooks storage backend. ` + `Set ${HOOKS_STORAGE_BACKEND_ENV} to one of ${STORAGE_BACKENDS.join(", ")}.`);
|
|
5022
5717
|
}
|
|
5023
5718
|
function getStorageDatabaseEnvName() {
|
|
5024
5719
|
for (const name of STORAGE_DATABASE_ENV) {
|
|
@@ -5035,11 +5730,12 @@ function getStorageDatabaseUrl() {
|
|
|
5035
5730
|
const env = getStorageDatabaseEnv();
|
|
5036
5731
|
return env ? readEnv(env.name) ?? null : null;
|
|
5037
5732
|
}
|
|
5038
|
-
function
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5733
|
+
function getStorageBackend() {
|
|
5734
|
+
assertNoRetiredModeEnv();
|
|
5735
|
+
const backend = normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_ENV), HOOKS_STORAGE_BACKEND_ENV) ?? normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_FALLBACK_ENV), HOOKS_STORAGE_BACKEND_FALLBACK_ENV);
|
|
5736
|
+
if (backend)
|
|
5737
|
+
return backend;
|
|
5738
|
+
return getStorageDatabaseUrl() ? "postgresql" : "sqlite";
|
|
5043
5739
|
}
|
|
5044
5740
|
async function getStoragePg() {
|
|
5045
5741
|
const url = getStorageDatabaseUrl();
|
|
@@ -5097,7 +5793,7 @@ function getStorageStatus() {
|
|
|
5097
5793
|
const activeEnv = getStorageDatabaseEnv();
|
|
5098
5794
|
return {
|
|
5099
5795
|
configured: Boolean(activeEnv),
|
|
5100
|
-
|
|
5796
|
+
backend: getStorageBackend(),
|
|
5101
5797
|
env: STORAGE_DATABASE_ENV,
|
|
5102
5798
|
activeEnv: activeEnv?.name ?? null,
|
|
5103
5799
|
service: "hooks",
|
|
@@ -5265,7 +5961,7 @@ function coerceForSqlite(value) {
|
|
|
5265
5961
|
return JSON.stringify(value);
|
|
5266
5962
|
return String(value);
|
|
5267
5963
|
}
|
|
5268
|
-
var STORAGE_TABLES, HOOKS_STORAGE_TABLES, HOOKS_STORAGE_ENV = "HASNA_HOOKS_DATABASE_URL", HOOKS_STORAGE_FALLBACK_ENV = "HOOKS_DATABASE_URL",
|
|
5964
|
+
var STORAGE_TABLES, HOOKS_STORAGE_TABLES, STORAGE_BACKENDS, HOOKS_STORAGE_ENV = "HASNA_HOOKS_DATABASE_URL", HOOKS_STORAGE_FALLBACK_ENV = "HOOKS_DATABASE_URL", HOOKS_STORAGE_BACKEND_ENV = "HASNA_HOOKS_STORAGE_BACKEND", HOOKS_STORAGE_BACKEND_FALLBACK_ENV = "HOOKS_STORAGE_BACKEND", STORAGE_DATABASE_ENV, STORAGE_BACKEND_ENV, RETIRED_STORAGE_MODE_ENV, PRIMARY_KEYS, RETIRED_MODE_REPLACEMENT, BACKEND_ALIASES;
|
|
5269
5965
|
var init_storage_sync = __esm(() => {
|
|
5270
5966
|
init_db();
|
|
5271
5967
|
init_pg_migrations();
|
|
@@ -5277,14 +5973,32 @@ var init_storage_sync = __esm(() => {
|
|
|
5277
5973
|
"feedback"
|
|
5278
5974
|
];
|
|
5279
5975
|
HOOKS_STORAGE_TABLES = STORAGE_TABLES;
|
|
5976
|
+
STORAGE_BACKENDS = ["sqlite", "postgresql"];
|
|
5280
5977
|
STORAGE_DATABASE_ENV = [HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV];
|
|
5281
|
-
|
|
5978
|
+
STORAGE_BACKEND_ENV = [HOOKS_STORAGE_BACKEND_ENV, HOOKS_STORAGE_BACKEND_FALLBACK_ENV];
|
|
5979
|
+
RETIRED_STORAGE_MODE_ENV = ["HASNA_HOOKS_STORAGE_MODE", "HOOKS_STORAGE_MODE"];
|
|
5282
5980
|
PRIMARY_KEYS = {
|
|
5283
5981
|
hook_events: ["id"],
|
|
5284
5982
|
schema_migrations: ["version"],
|
|
5285
5983
|
_meta: ["key"],
|
|
5286
5984
|
feedback: ["id"]
|
|
5287
5985
|
};
|
|
5986
|
+
RETIRED_MODE_REPLACEMENT = {
|
|
5987
|
+
local: "sqlite",
|
|
5988
|
+
hybrid: "postgresql",
|
|
5989
|
+
remote: "postgresql",
|
|
5990
|
+
self_hosted: "postgresql",
|
|
5991
|
+
"self-hosted": "postgresql",
|
|
5992
|
+
selfhosted: "postgresql",
|
|
5993
|
+
cloud: "postgresql"
|
|
5994
|
+
};
|
|
5995
|
+
BACKEND_ALIASES = {
|
|
5996
|
+
sqlite: "sqlite",
|
|
5997
|
+
sqlite3: "sqlite",
|
|
5998
|
+
postgresql: "postgresql",
|
|
5999
|
+
postgres: "postgresql",
|
|
6000
|
+
pg: "postgresql"
|
|
6001
|
+
};
|
|
5288
6002
|
});
|
|
5289
6003
|
|
|
5290
6004
|
// src/storage.ts
|
|
@@ -5299,20 +6013,22 @@ __export(exports_storage, {
|
|
|
5299
6013
|
getSyncMetaAll: () => getSyncMetaAll,
|
|
5300
6014
|
getStorageStatus: () => getStorageStatus,
|
|
5301
6015
|
getStoragePg: () => getStoragePg,
|
|
5302
|
-
getStorageMode: () => getStorageMode,
|
|
5303
6016
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
5304
6017
|
getStorageDatabaseEnvName: () => getStorageDatabaseEnvName,
|
|
5305
6018
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
6019
|
+
getStorageBackend: () => getStorageBackend,
|
|
5306
6020
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
5307
|
-
STORAGE_MODE_ENV: () => STORAGE_MODE_ENV,
|
|
5308
6021
|
STORAGE_DATABASE_ENV: () => STORAGE_DATABASE_ENV,
|
|
6022
|
+
STORAGE_BACKEND_ENV: () => STORAGE_BACKEND_ENV,
|
|
6023
|
+
STORAGE_BACKENDS: () => STORAGE_BACKENDS,
|
|
6024
|
+
RETIRED_STORAGE_MODE_ENV: () => RETIRED_STORAGE_MODE_ENV,
|
|
5309
6025
|
PgAdapterAsync: () => PgAdapterAsync,
|
|
5310
6026
|
PG_MIGRATIONS: () => PG_MIGRATIONS,
|
|
5311
6027
|
HOOKS_STORAGE_TABLES: () => HOOKS_STORAGE_TABLES,
|
|
5312
|
-
HOOKS_STORAGE_MODE_FALLBACK_ENV: () => HOOKS_STORAGE_MODE_FALLBACK_ENV,
|
|
5313
|
-
HOOKS_STORAGE_MODE_ENV: () => HOOKS_STORAGE_MODE_ENV,
|
|
5314
6028
|
HOOKS_STORAGE_FALLBACK_ENV: () => HOOKS_STORAGE_FALLBACK_ENV,
|
|
5315
|
-
HOOKS_STORAGE_ENV: () => HOOKS_STORAGE_ENV
|
|
6029
|
+
HOOKS_STORAGE_ENV: () => HOOKS_STORAGE_ENV,
|
|
6030
|
+
HOOKS_STORAGE_BACKEND_FALLBACK_ENV: () => HOOKS_STORAGE_BACKEND_FALLBACK_ENV,
|
|
6031
|
+
HOOKS_STORAGE_BACKEND_ENV: () => HOOKS_STORAGE_BACKEND_ENV
|
|
5316
6032
|
});
|
|
5317
6033
|
var init_storage = __esm(() => {
|
|
5318
6034
|
init_storage_sync();
|
|
@@ -5320,6 +6036,374 @@ var init_storage = __esm(() => {
|
|
|
5320
6036
|
init_pg_migrations();
|
|
5321
6037
|
});
|
|
5322
6038
|
|
|
6039
|
+
// src/serve.ts
|
|
6040
|
+
var exports_serve = {};
|
|
6041
|
+
__export(exports_serve, {
|
|
6042
|
+
startServeServer: () => startServeServer,
|
|
6043
|
+
handleServeRequest: () => handleServeRequest,
|
|
6044
|
+
SERVE_SERVICE_NAME: () => SERVE_SERVICE_NAME,
|
|
6045
|
+
SERVE_HOST: () => SERVE_HOST,
|
|
6046
|
+
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
6047
|
+
});
|
|
6048
|
+
async function buildCatalog() {
|
|
6049
|
+
const byName = new Map;
|
|
6050
|
+
for (const meta of HOOKS) {
|
|
6051
|
+
const scriptPath = resolveHook(meta.name)?.scriptPath;
|
|
6052
|
+
if (!scriptPath)
|
|
6053
|
+
continue;
|
|
6054
|
+
byName.set(meta.name, {
|
|
6055
|
+
name: meta.name,
|
|
6056
|
+
version: meta.version,
|
|
6057
|
+
sha256: await sha256File(scriptPath),
|
|
6058
|
+
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
6059
|
+
description: meta.description,
|
|
6060
|
+
source: "bundled"
|
|
6061
|
+
});
|
|
6062
|
+
}
|
|
6063
|
+
for (const custom of listCustomHooks()) {
|
|
6064
|
+
const name = shortManifestName(custom.manifest.name);
|
|
6065
|
+
byName.set(name, {
|
|
6066
|
+
name,
|
|
6067
|
+
version: custom.manifest.version,
|
|
6068
|
+
sha256: await sha256File(custom.scriptPath),
|
|
6069
|
+
events: custom.manifest.events,
|
|
6070
|
+
description: custom.manifest.description ?? "Custom hook",
|
|
6071
|
+
source: byName.has(name) ? "custom-overrides-bundled" : "custom"
|
|
6072
|
+
});
|
|
6073
|
+
}
|
|
6074
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
6075
|
+
}
|
|
6076
|
+
async function artifactFor(name, version) {
|
|
6077
|
+
const resolved = resolveHook(name);
|
|
6078
|
+
if (!resolved)
|
|
6079
|
+
return null;
|
|
6080
|
+
if (version !== resolved.version)
|
|
6081
|
+
return null;
|
|
6082
|
+
const custom = listCustomHooks().find((c) => shortManifestName(c.manifest.name) === name);
|
|
6083
|
+
const manifest = {
|
|
6084
|
+
name,
|
|
6085
|
+
version: resolved.version,
|
|
6086
|
+
description: resolved.description,
|
|
6087
|
+
events: resolved.events,
|
|
6088
|
+
script: custom ? custom.manifest.script : `src/hook.ts`,
|
|
6089
|
+
args: custom?.manifest.args,
|
|
6090
|
+
timeout_ms: custom?.manifest.timeout_ms
|
|
6091
|
+
};
|
|
6092
|
+
const script = await Bun.file(resolved.scriptPath).text();
|
|
6093
|
+
return { manifest, script };
|
|
6094
|
+
}
|
|
6095
|
+
function authorized(req, apiKey) {
|
|
6096
|
+
if (!apiKey)
|
|
6097
|
+
return false;
|
|
6098
|
+
const header = req.headers.get("authorization") ?? "";
|
|
6099
|
+
if (header.startsWith("Bearer "))
|
|
6100
|
+
return header.slice("Bearer ".length) === apiKey;
|
|
6101
|
+
return req.headers.get("x-api-key") === apiKey;
|
|
6102
|
+
}
|
|
6103
|
+
function json(body, status = 200, headers = {}) {
|
|
6104
|
+
return new Response(JSON.stringify(body), {
|
|
6105
|
+
status,
|
|
6106
|
+
headers: { "content-type": "application/json", ...headers }
|
|
6107
|
+
});
|
|
6108
|
+
}
|
|
6109
|
+
function handleServeRequest(req, apiKey) {
|
|
6110
|
+
const url = new URL(req.url);
|
|
6111
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
6112
|
+
return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
|
|
6113
|
+
}
|
|
6114
|
+
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
6115
|
+
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
6116
|
+
}
|
|
6117
|
+
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
6118
|
+
return Promise.resolve(json(readLock()));
|
|
6119
|
+
}
|
|
6120
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
|
|
6121
|
+
if (artifactMatch && req.method === "GET") {
|
|
6122
|
+
const [, name, version] = artifactMatch;
|
|
6123
|
+
return artifactFor(name, version).then(async (artifact) => {
|
|
6124
|
+
if (!artifact)
|
|
6125
|
+
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
6126
|
+
const resolved = resolveHook(name);
|
|
6127
|
+
const sha = resolved ? await sha256File(resolved.scriptPath) : artifact.manifest.name;
|
|
6128
|
+
return json(artifact, 200, { "x-hook-sha256": sha });
|
|
6129
|
+
});
|
|
6130
|
+
}
|
|
6131
|
+
if (url.pathname === "/api/v1/hooks" && req.method === "PUT") {
|
|
6132
|
+
if (!authorized(req, apiKey)) {
|
|
6133
|
+
return Promise.resolve(json({ error: "unauthorized: valid API key required to publish" }, 401));
|
|
6134
|
+
}
|
|
6135
|
+
return req.json().then((body) => {
|
|
6136
|
+
const name = body.name ?? "";
|
|
6137
|
+
const version = body.version ?? "";
|
|
6138
|
+
const resolved = resolveHook(name);
|
|
6139
|
+
if (!resolved)
|
|
6140
|
+
return json({ error: `Hook '${name}' not found in local store` }, 404);
|
|
6141
|
+
if (version && version !== resolved.version) {
|
|
6142
|
+
return json({ error: `Version mismatch: local '${name}' is ${resolved.version}, requested ${version}` }, 409);
|
|
6143
|
+
}
|
|
6144
|
+
const check = retrustHook(name, resolved.scriptPath, resolved.version, "serve");
|
|
6145
|
+
return json({ ok: true, hook: { name, version: resolved.version, sha256: check.actual } });
|
|
6146
|
+
}, () => json({ error: "invalid JSON body" }, 400));
|
|
6147
|
+
}
|
|
6148
|
+
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
6149
|
+
}
|
|
6150
|
+
function startServeServer(options) {
|
|
6151
|
+
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
6152
|
+
const host = options.host ?? SERVE_HOST;
|
|
6153
|
+
const apiKey = resolveApiKey(options.apiKey);
|
|
6154
|
+
const server = Bun.serve({
|
|
6155
|
+
hostname: host,
|
|
6156
|
+
port,
|
|
6157
|
+
async fetch(req) {
|
|
6158
|
+
return handleServeRequest(req, apiKey);
|
|
6159
|
+
}
|
|
6160
|
+
});
|
|
6161
|
+
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
6162
|
+
return server;
|
|
6163
|
+
}
|
|
6164
|
+
var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
|
|
6165
|
+
var init_serve = __esm(() => {
|
|
6166
|
+
init_registry();
|
|
6167
|
+
init_manifest();
|
|
6168
|
+
init_store();
|
|
6169
|
+
init_resolve();
|
|
6170
|
+
init_config();
|
|
6171
|
+
if (false) {}
|
|
6172
|
+
});
|
|
6173
|
+
|
|
6174
|
+
// src/lib/sync.ts
|
|
6175
|
+
var exports_sync = {};
|
|
6176
|
+
__export(exports_sync, {
|
|
6177
|
+
syncHooks: () => syncHooks,
|
|
6178
|
+
planSync: () => planSync
|
|
6179
|
+
});
|
|
6180
|
+
import { createHash as createHash2 } from "crypto";
|
|
6181
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
6182
|
+
function sha256OfText(text) {
|
|
6183
|
+
return createHash2("sha256").update(text).digest("hex");
|
|
6184
|
+
}
|
|
6185
|
+
function sha256OfPath(path) {
|
|
6186
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
6187
|
+
}
|
|
6188
|
+
function collectBundledCatalog() {
|
|
6189
|
+
const out = [];
|
|
6190
|
+
for (const meta of HOOKS) {
|
|
6191
|
+
const scriptPath = resolveScriptPath(meta.name);
|
|
6192
|
+
if (!scriptPath)
|
|
6193
|
+
continue;
|
|
6194
|
+
out.push({
|
|
6195
|
+
name: meta.name,
|
|
6196
|
+
version: meta.version,
|
|
6197
|
+
sha256: sha256OfPath(scriptPath)
|
|
6198
|
+
});
|
|
6199
|
+
}
|
|
6200
|
+
return out;
|
|
6201
|
+
}
|
|
6202
|
+
async function fetchJson(base, path) {
|
|
6203
|
+
const res = await fetch(`${base}${path}`, {
|
|
6204
|
+
headers: { accept: "application/json" },
|
|
6205
|
+
signal: AbortSignal.timeout(15000)
|
|
6206
|
+
});
|
|
6207
|
+
if (!res.ok) {
|
|
6208
|
+
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6209
|
+
}
|
|
6210
|
+
return res.json();
|
|
6211
|
+
}
|
|
6212
|
+
async function fetchRemoteState(apiUrl) {
|
|
6213
|
+
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
6214
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
6215
|
+
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
6216
|
+
throw new Error("remote catalog is malformed: missing hooks array");
|
|
6217
|
+
}
|
|
6218
|
+
if (!remoteLock.hooks || typeof remoteLock.hooks !== "object") {
|
|
6219
|
+
throw new Error("remote lock is malformed: missing hooks map");
|
|
6220
|
+
}
|
|
6221
|
+
return { catalog: catalog.hooks, remoteLock };
|
|
6222
|
+
}
|
|
6223
|
+
function computeDiff(catalog, remoteLock) {
|
|
6224
|
+
const local = readLock();
|
|
6225
|
+
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
6226
|
+
for (const entry of catalog) {
|
|
6227
|
+
if (!entry.name || !entry.version || !entry.sha256)
|
|
6228
|
+
continue;
|
|
6229
|
+
const pin = local.hooks[entry.name];
|
|
6230
|
+
const remotePin = remoteLock ? remoteLock.hooks[entry.name] : entry;
|
|
6231
|
+
if (remoteLock && !remotePin) {
|
|
6232
|
+
diff.skipped.push(entry.name);
|
|
6233
|
+
continue;
|
|
6234
|
+
}
|
|
6235
|
+
const pinnedLocally = pin && pin.version === entry.version && pin.sha256 === entry.sha256;
|
|
6236
|
+
if (pinnedLocally) {
|
|
6237
|
+
diff.unchanged.push(entry.name);
|
|
6238
|
+
} else if (pin) {
|
|
6239
|
+
diff.updated.push(entry.name);
|
|
6240
|
+
} else {
|
|
6241
|
+
diff.added.push(entry.name);
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
6244
|
+
return diff;
|
|
6245
|
+
}
|
|
6246
|
+
async function planSync() {
|
|
6247
|
+
const apiUrl = resolveApiUrl();
|
|
6248
|
+
if (apiUrl) {
|
|
6249
|
+
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6250
|
+
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
6251
|
+
}
|
|
6252
|
+
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6253
|
+
return { apiUrl: null, dryRun: false, diff };
|
|
6254
|
+
}
|
|
6255
|
+
async function syncHooks(options = {}) {
|
|
6256
|
+
const apiUrl = resolveApiUrl();
|
|
6257
|
+
const plan = await planSync();
|
|
6258
|
+
if (options.dryRun)
|
|
6259
|
+
return { ...plan, dryRun: true };
|
|
6260
|
+
if (!apiUrl) {
|
|
6261
|
+
const db2 = getDb();
|
|
6262
|
+
for (const entry of collectBundledCatalog()) {
|
|
6263
|
+
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6264
|
+
upsertHookRecord(db2, {
|
|
6265
|
+
name: entry.name,
|
|
6266
|
+
version: entry.version,
|
|
6267
|
+
sha256: entry.sha256,
|
|
6268
|
+
source_type: "bundled",
|
|
6269
|
+
source_ref: "apps/hooks/hooks",
|
|
6270
|
+
last_verified_at: new Date().toISOString()
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
return plan;
|
|
6274
|
+
}
|
|
6275
|
+
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6276
|
+
const diff = computeDiff(catalog, remoteLock);
|
|
6277
|
+
const toFetch = [...diff.added, ...diff.updated];
|
|
6278
|
+
const artifacts = new Map;
|
|
6279
|
+
for (const name of toFetch) {
|
|
6280
|
+
const entry = remoteLock.hooks[name];
|
|
6281
|
+
if (!entry)
|
|
6282
|
+
continue;
|
|
6283
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
6284
|
+
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6285
|
+
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6286
|
+
}
|
|
6287
|
+
const actualSha = sha256OfText(artifact.script);
|
|
6288
|
+
if (actualSha !== entry.sha256) {
|
|
6289
|
+
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6290
|
+
}
|
|
6291
|
+
artifacts.set(name, artifact);
|
|
6292
|
+
}
|
|
6293
|
+
const db = getDb();
|
|
6294
|
+
for (const name of toFetch) {
|
|
6295
|
+
const artifact = artifacts.get(name);
|
|
6296
|
+
const entry = remoteLock.hooks[name];
|
|
6297
|
+
if (!artifact || !entry)
|
|
6298
|
+
continue;
|
|
6299
|
+
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6300
|
+
const scriptRel = artifact.manifest.script.includes(`
|
|
6301
|
+
`) ? "script.ts" : artifact.manifest.script;
|
|
6302
|
+
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6303
|
+
const scriptPath = resolveScriptPath(name);
|
|
6304
|
+
const actual = await sha256File(scriptPath);
|
|
6305
|
+
setPinnedHook(name, { version: entry.version, sha256: actual, source: entry.source ?? "remote" });
|
|
6306
|
+
upsertHookRecord(db, {
|
|
6307
|
+
name,
|
|
6308
|
+
version: entry.version,
|
|
6309
|
+
sha256: actual,
|
|
6310
|
+
source_type: entry.source ?? "remote",
|
|
6311
|
+
source_ref: apiUrl,
|
|
6312
|
+
last_verified_at: new Date().toISOString()
|
|
6313
|
+
});
|
|
6314
|
+
}
|
|
6315
|
+
return { apiUrl, dryRun: false, diff };
|
|
6316
|
+
}
|
|
6317
|
+
var init_sync = __esm(() => {
|
|
6318
|
+
init_registry();
|
|
6319
|
+
init_config();
|
|
6320
|
+
init_db();
|
|
6321
|
+
init_store();
|
|
6322
|
+
init_manifest();
|
|
6323
|
+
init_resolve();
|
|
6324
|
+
});
|
|
6325
|
+
|
|
6326
|
+
// src/cf/provision.ts
|
|
6327
|
+
var exports_provision = {};
|
|
6328
|
+
__export(exports_provision, {
|
|
6329
|
+
provisionCloudflareResources: () => provisionCloudflareResources
|
|
6330
|
+
});
|
|
6331
|
+
async function cfFetch(fetchFn, token, path, init) {
|
|
6332
|
+
const res = await fetchFn(`${CF_API}${path}`, {
|
|
6333
|
+
...init,
|
|
6334
|
+
headers: {
|
|
6335
|
+
authorization: `Bearer ${token}`,
|
|
6336
|
+
"content-type": "application/json",
|
|
6337
|
+
...init?.headers ?? {}
|
|
6338
|
+
}
|
|
6339
|
+
});
|
|
6340
|
+
const data = await res.json().catch(() => null);
|
|
6341
|
+
return { ok: res.ok, status: res.status, data };
|
|
6342
|
+
}
|
|
6343
|
+
async function provisionCloudflareResources(options) {
|
|
6344
|
+
const { token, accountId, databaseName, bucketName, dryRun, fetchFn = fetch } = options;
|
|
6345
|
+
const result = {
|
|
6346
|
+
d1DatabaseId: null,
|
|
6347
|
+
d1Created: false,
|
|
6348
|
+
d1Exists: false,
|
|
6349
|
+
r2BucketExists: false,
|
|
6350
|
+
r2Created: false,
|
|
6351
|
+
commands: []
|
|
6352
|
+
};
|
|
6353
|
+
if (dryRun) {
|
|
6354
|
+
result.commands = [
|
|
6355
|
+
`POST ${CF_API}/accounts/${accountId}/d1/database {"name": "${databaseName}"} (if missing)`,
|
|
6356
|
+
`PUT ${CF_API}/accounts/${accountId}/r2/buckets/${bucketName} (if missing)`,
|
|
6357
|
+
`wrangler d1 migrations apply ${databaseName} --remote`,
|
|
6358
|
+
`wrangler deploy --config wrangler.toml`
|
|
6359
|
+
];
|
|
6360
|
+
return result;
|
|
6361
|
+
}
|
|
6362
|
+
const d1List = await cfFetch(fetchFn, token, `/accounts/${accountId}/d1/database?name=${encodeURIComponent(databaseName)}`);
|
|
6363
|
+
if (!d1List.ok) {
|
|
6364
|
+
throw new Error(`failed to list D1 databases (${d1List.status})`);
|
|
6365
|
+
}
|
|
6366
|
+
const d1Rows = d1List.data?.result ?? [];
|
|
6367
|
+
if (d1Rows.length > 0) {
|
|
6368
|
+
result.d1Exists = true;
|
|
6369
|
+
result.d1DatabaseId = d1Rows[0].id;
|
|
6370
|
+
} else {
|
|
6371
|
+
const created = await cfFetch(fetchFn, token, `/accounts/${accountId}/d1/database`, {
|
|
6372
|
+
method: "POST",
|
|
6373
|
+
body: JSON.stringify({ name: databaseName })
|
|
6374
|
+
});
|
|
6375
|
+
if (!created.ok) {
|
|
6376
|
+
throw new Error(`failed to create D1 database '${databaseName}' (${created.status})`);
|
|
6377
|
+
}
|
|
6378
|
+
result.d1Created = true;
|
|
6379
|
+
result.d1DatabaseId = created.data?.result?.id ?? null;
|
|
6380
|
+
}
|
|
6381
|
+
const r2List = await cfFetch(fetchFn, token, `/accounts/${accountId}/r2/buckets`);
|
|
6382
|
+
if (!r2List.ok) {
|
|
6383
|
+
throw new Error(`failed to list R2 buckets (${r2List.status})`);
|
|
6384
|
+
}
|
|
6385
|
+
const bucketNames = (r2List.data?.result?.buckets ?? []).map((b) => b.name);
|
|
6386
|
+
if (bucketNames.includes(bucketName)) {
|
|
6387
|
+
result.r2BucketExists = true;
|
|
6388
|
+
} else {
|
|
6389
|
+
const created = await cfFetch(fetchFn, token, `/accounts/${accountId}/r2/buckets/${bucketName}`, {
|
|
6390
|
+
method: "PUT",
|
|
6391
|
+
body: JSON.stringify({ name: bucketName })
|
|
6392
|
+
});
|
|
6393
|
+
if (!created.ok) {
|
|
6394
|
+
throw new Error(`failed to create R2 bucket '${bucketName}' (${created.status})`);
|
|
6395
|
+
}
|
|
6396
|
+
result.r2Created = true;
|
|
6397
|
+
}
|
|
6398
|
+
result.commands = [
|
|
6399
|
+
`wrangler d1 migrations apply ${databaseName} --remote`,
|
|
6400
|
+
`wrangler secret put HOOKS_API_KEY`,
|
|
6401
|
+
`wrangler deploy --config wrangler.toml`
|
|
6402
|
+
];
|
|
6403
|
+
return result;
|
|
6404
|
+
}
|
|
6405
|
+
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
6406
|
+
|
|
5323
6407
|
// src/mcp/server.ts
|
|
5324
6408
|
var exports_server = {};
|
|
5325
6409
|
__export(exports_server, {
|
|
@@ -5331,11 +6415,20 @@ __export(exports_server, {
|
|
|
5331
6415
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5332
6416
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5333
6417
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5334
|
-
import { z } from "zod";
|
|
6418
|
+
import { z as z2 } from "zod";
|
|
5335
6419
|
import { createServer } from "http";
|
|
5336
|
-
import { existsSync as
|
|
5337
|
-
import { join as
|
|
6420
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
6421
|
+
import { join as join11, dirname as dirname4 } from "path";
|
|
5338
6422
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6423
|
+
async function runVerifiedHook(name, scriptPath, stdin, timeoutMs) {
|
|
6424
|
+
const content = readFileSync9(scriptPath);
|
|
6425
|
+
const check = checkScriptHash(name, sha256Of(content));
|
|
6426
|
+
if (!check.ok) {
|
|
6427
|
+
throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
|
|
6428
|
+
}
|
|
6429
|
+
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
6430
|
+
return executeVerifiedScript2({ name, scriptPath, content, stdin, env: process.env, timeout: timeoutMs });
|
|
6431
|
+
}
|
|
5339
6432
|
function formatInstallResults(results, extra) {
|
|
5340
6433
|
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
5341
6434
|
const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
|
|
@@ -5394,9 +6487,9 @@ function createHooksServer() {
|
|
|
5394
6487
|
});
|
|
5395
6488
|
const defineTool = (name, description, schema, handler) => server.tool(name, description, schema, handler);
|
|
5396
6489
|
defineTool("hooks_list", "List available hooks. Compact by default; pass compact:false for full HookMeta objects.", {
|
|
5397
|
-
category:
|
|
5398
|
-
compact:
|
|
5399
|
-
limit:
|
|
6490
|
+
category: z2.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security')"),
|
|
6491
|
+
compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
|
|
6492
|
+
limit: z2.number().default(25).describe("Max compact rows to return")
|
|
5400
6493
|
}, async ({ category, compact, limit }) => {
|
|
5401
6494
|
const maxRows = boundedLimit(limit, 25, 100);
|
|
5402
6495
|
if (category) {
|
|
@@ -5414,15 +6507,15 @@ function createHooksServer() {
|
|
|
5414
6507
|
return { content: [{ type: "text", text: JSON.stringify(compact ? compactHookResult(HOOKS, maxRows, "hooks_info") : result) }] };
|
|
5415
6508
|
});
|
|
5416
6509
|
defineTool("hooks_search", "Search hooks. Compact by default; pass compact:false for full HookMeta objects.", {
|
|
5417
|
-
query:
|
|
5418
|
-
compact:
|
|
5419
|
-
limit:
|
|
6510
|
+
query: z2.string().describe("Search query"),
|
|
6511
|
+
compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
|
|
6512
|
+
limit: z2.number().default(10).describe("Max compact rows to return")
|
|
5420
6513
|
}, async ({ query, compact, limit }) => {
|
|
5421
6514
|
const results = searchHooks(query);
|
|
5422
6515
|
const out = compact ? compactHookResult(results, boundedLimit(limit, 10, 100), "hooks_info") : results;
|
|
5423
6516
|
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
5424
6517
|
});
|
|
5425
|
-
defineTool("hooks_info", "Get detailed information about a specific hook including install status", { name:
|
|
6518
|
+
defineTool("hooks_info", "Get detailed information about a specific hook including install status", { name: z2.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')") }, async ({ name }) => {
|
|
5426
6519
|
const meta = getHook(name);
|
|
5427
6520
|
if (!meta) {
|
|
5428
6521
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
@@ -5432,18 +6525,18 @@ function createHooksServer() {
|
|
|
5432
6525
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }) }] };
|
|
5433
6526
|
});
|
|
5434
6527
|
defineTool("hooks_install", "Install one or more hooks by registering them in agent settings", {
|
|
5435
|
-
hooks:
|
|
5436
|
-
scope:
|
|
5437
|
-
overwrite:
|
|
5438
|
-
profile:
|
|
6528
|
+
hooks: z2.array(z2.string()).describe("Hook names to install"),
|
|
6529
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Install scope"),
|
|
6530
|
+
overwrite: z2.boolean().default(false).describe("Overwrite if already installed"),
|
|
6531
|
+
profile: z2.string().optional().describe("Agent profile ID to scope hooks to")
|
|
5439
6532
|
}, async ({ hooks, scope, overwrite, profile }) => {
|
|
5440
6533
|
const results = hooks.map((name) => installHook(name, { scope, overwrite, profile }));
|
|
5441
6534
|
return formatInstallResults(results, { scope, profile });
|
|
5442
6535
|
});
|
|
5443
6536
|
defineTool("hooks_install_category", "Install all hooks in a category", {
|
|
5444
|
-
category:
|
|
5445
|
-
scope:
|
|
5446
|
-
overwrite:
|
|
6537
|
+
category: z2.string().describe("Category name"),
|
|
6538
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Install scope"),
|
|
6539
|
+
overwrite: z2.boolean().default(false).describe("Overwrite if already installed")
|
|
5447
6540
|
}, async ({ category, scope, overwrite }) => {
|
|
5448
6541
|
const cat = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
|
|
5449
6542
|
if (!cat) {
|
|
@@ -5454,45 +6547,53 @@ function createHooksServer() {
|
|
|
5454
6547
|
return formatInstallResults(results, { category: cat, scope });
|
|
5455
6548
|
});
|
|
5456
6549
|
defineTool("hooks_install_all", "Install all available hooks", {
|
|
5457
|
-
scope:
|
|
5458
|
-
overwrite:
|
|
6550
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Install scope"),
|
|
6551
|
+
overwrite: z2.boolean().default(false).describe("Overwrite if already installed")
|
|
5459
6552
|
}, async ({ scope, overwrite }) => {
|
|
5460
6553
|
const results = HOOKS.map((h) => installHook(h.name, { scope, overwrite }));
|
|
5461
6554
|
return formatInstallResults(results, { scope });
|
|
5462
6555
|
});
|
|
5463
6556
|
defineTool("hooks_remove", "Remove (unregister) a hook from agent settings", {
|
|
5464
|
-
name:
|
|
5465
|
-
scope:
|
|
6557
|
+
name: z2.string().describe("Hook name to remove"),
|
|
6558
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to remove from")
|
|
5466
6559
|
}, async ({ name, scope }) => {
|
|
5467
6560
|
const removed = removeHook(name, scope);
|
|
5468
6561
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, removed, scope }) }] };
|
|
5469
6562
|
});
|
|
5470
6563
|
defineTool("hooks_doctor", "Check health of installed hooks \u2014 verifies hook source exists, settings are correct", {
|
|
5471
|
-
scope:
|
|
6564
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to check")
|
|
5472
6565
|
}, async ({ scope }) => {
|
|
5473
6566
|
const settingsPath = getSettingsPath(scope);
|
|
5474
6567
|
const issues = [];
|
|
5475
6568
|
const healthy = [];
|
|
5476
|
-
const settingsExist =
|
|
6569
|
+
const settingsExist = existsSync11(settingsPath);
|
|
5477
6570
|
if (!settingsExist) {
|
|
5478
6571
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
5479
6572
|
}
|
|
5480
6573
|
const registered = getRegisteredHooks(scope);
|
|
5481
6574
|
for (const name of registered) {
|
|
5482
|
-
const
|
|
6575
|
+
const custom = readCustomManifest(name);
|
|
6576
|
+
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
5483
6577
|
let hookHealthy = true;
|
|
5484
|
-
if (
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
6578
|
+
if (custom) {
|
|
6579
|
+
if (!existsSync11(custom.scriptPath)) {
|
|
6580
|
+
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
6581
|
+
hookHealthy = false;
|
|
6582
|
+
}
|
|
6583
|
+
} else {
|
|
6584
|
+
if (!hookExists(name)) {
|
|
6585
|
+
issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
|
|
6586
|
+
continue;
|
|
6587
|
+
}
|
|
6588
|
+
const hookDir = getHookPath(name);
|
|
6589
|
+
if (!existsSync11(join11(hookDir, "src", "hook.ts"))) {
|
|
6590
|
+
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
6591
|
+
hookHealthy = false;
|
|
6592
|
+
}
|
|
5492
6593
|
}
|
|
5493
6594
|
if (meta && settingsExist) {
|
|
5494
6595
|
try {
|
|
5495
|
-
const settings = JSON.parse(
|
|
6596
|
+
const settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
|
|
5496
6597
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
5497
6598
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
5498
6599
|
const match = h.command?.match(/^hooks run ([\w-]+)/);
|
|
@@ -5517,8 +6618,8 @@ function createHooksServer() {
|
|
|
5517
6618
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
5518
6619
|
});
|
|
5519
6620
|
defineTool("hooks_docs", "Get documentation. Hook README content is summarized by default; pass verbose:true for the full README.", {
|
|
5520
|
-
name:
|
|
5521
|
-
verbose:
|
|
6621
|
+
name: z2.string().optional().describe("Hook name for specific docs, omit for general docs"),
|
|
6622
|
+
verbose: z2.boolean().default(false).describe("Return full hook README when true")
|
|
5522
6623
|
}, async ({ name, verbose }) => {
|
|
5523
6624
|
if (name) {
|
|
5524
6625
|
const meta = getHook(name);
|
|
@@ -5526,10 +6627,10 @@ function createHooksServer() {
|
|
|
5526
6627
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
5527
6628
|
}
|
|
5528
6629
|
const hookPath = getHookPath(name);
|
|
5529
|
-
const readmePath =
|
|
6630
|
+
const readmePath = join11(hookPath, "README.md");
|
|
5530
6631
|
let readme = "";
|
|
5531
|
-
if (
|
|
5532
|
-
readme =
|
|
6632
|
+
if (existsSync11(readmePath)) {
|
|
6633
|
+
readme = readFileSync9(readmePath, "utf-8");
|
|
5533
6634
|
}
|
|
5534
6635
|
if (verbose) {
|
|
5535
6636
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -5577,9 +6678,9 @@ function createHooksServer() {
|
|
|
5577
6678
|
};
|
|
5578
6679
|
});
|
|
5579
6680
|
defineTool("hooks_registered", "Get currently registered hooks for a scope. Compact by default.", {
|
|
5580
|
-
scope:
|
|
5581
|
-
compact:
|
|
5582
|
-
limit:
|
|
6681
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to check"),
|
|
6682
|
+
compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for descriptions."),
|
|
6683
|
+
limit: z2.number().default(25).describe("Max compact rows to return")
|
|
5583
6684
|
}, async ({ scope, compact, limit }) => {
|
|
5584
6685
|
const registered = getRegisteredHooks(scope);
|
|
5585
6686
|
const result = registered.map((name) => {
|
|
@@ -5609,18 +6710,18 @@ function createHooksServer() {
|
|
|
5609
6710
|
};
|
|
5610
6711
|
});
|
|
5611
6712
|
defineTool("hooks_run", "Execute a hook programmatically with the given input and return its output", {
|
|
5612
|
-
name:
|
|
5613
|
-
input:
|
|
5614
|
-
profile:
|
|
5615
|
-
timeout_ms:
|
|
6713
|
+
name: z2.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')"),
|
|
6714
|
+
input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input as JSON object (HookInput)"),
|
|
6715
|
+
profile: z2.string().optional().describe("Agent profile ID to inject into hook input"),
|
|
6716
|
+
timeout_ms: z2.number().default(1e4).describe("Timeout in milliseconds (default: 10000)")
|
|
5616
6717
|
}, async ({ name, input, profile, timeout_ms }) => {
|
|
5617
6718
|
const meta = getHook(name);
|
|
5618
6719
|
if (!meta) {
|
|
5619
6720
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
5620
6721
|
}
|
|
5621
6722
|
const hookDir = getHookPath(name);
|
|
5622
|
-
const hookScript =
|
|
5623
|
-
if (!
|
|
6723
|
+
const hookScript = join11(hookDir, "src", "hook.ts");
|
|
6724
|
+
if (!existsSync11(hookScript)) {
|
|
5624
6725
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
|
|
5625
6726
|
}
|
|
5626
6727
|
let hookInput = { ...input };
|
|
@@ -5635,29 +6736,28 @@ function createHooksServer() {
|
|
|
5635
6736
|
};
|
|
5636
6737
|
}
|
|
5637
6738
|
}
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
]);
|
|
6739
|
+
let ran = null;
|
|
6740
|
+
try {
|
|
6741
|
+
ran = await Promise.race([
|
|
6742
|
+
runVerifiedHook(name, hookScript, JSON.stringify(hookInput), timeout_ms),
|
|
6743
|
+
new Promise((resolve2) => setTimeout(() => resolve2(null), timeout_ms))
|
|
6744
|
+
]);
|
|
6745
|
+
} catch (err) {
|
|
6746
|
+
return {
|
|
6747
|
+
content: [{
|
|
6748
|
+
type: "text",
|
|
6749
|
+
text: JSON.stringify({ hook: name, error: err instanceof Error ? err.message : String(err), trust_failed: true })
|
|
6750
|
+
}]
|
|
6751
|
+
};
|
|
6752
|
+
}
|
|
6753
|
+
const timedOut = ran === null;
|
|
6754
|
+
const stdoutText = ran?.stdout ?? "";
|
|
6755
|
+
const stderrText = ran?.stderr ?? "";
|
|
5656
6756
|
let output = {};
|
|
5657
6757
|
try {
|
|
5658
|
-
output = JSON.parse(
|
|
6758
|
+
output = JSON.parse(stdoutText);
|
|
5659
6759
|
} catch {
|
|
5660
|
-
output =
|
|
6760
|
+
output = stdoutText ? { raw: stdoutText } : {};
|
|
5661
6761
|
}
|
|
5662
6762
|
return {
|
|
5663
6763
|
content: [{
|
|
@@ -5665,16 +6765,16 @@ function createHooksServer() {
|
|
|
5665
6765
|
text: JSON.stringify({
|
|
5666
6766
|
hook: name,
|
|
5667
6767
|
output,
|
|
5668
|
-
stderr:
|
|
5669
|
-
exitCode:
|
|
5670
|
-
...
|
|
6768
|
+
stderr: stderrText || undefined,
|
|
6769
|
+
exitCode: ran?.exitCode ?? -1,
|
|
6770
|
+
...timedOut ? { timedOut: true, timeout_ms } : {}
|
|
5671
6771
|
})
|
|
5672
6772
|
}]
|
|
5673
6773
|
};
|
|
5674
6774
|
});
|
|
5675
6775
|
defineTool("hooks_update", "Re-register installed hooks to pick up new package version (reinstalls with overwrite)", {
|
|
5676
|
-
hooks:
|
|
5677
|
-
scope:
|
|
6776
|
+
hooks: z2.array(z2.string()).optional().describe("Hook names to update (omit to update all installed hooks)"),
|
|
6777
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to update")
|
|
5678
6778
|
}, async ({ hooks, scope }) => {
|
|
5679
6779
|
const installed = getRegisteredHooks(scope);
|
|
5680
6780
|
const toUpdate = hooks && hooks.length > 0 ? hooks : installed;
|
|
@@ -5692,9 +6792,9 @@ function createHooksServer() {
|
|
|
5692
6792
|
return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
|
|
5693
6793
|
});
|
|
5694
6794
|
defineTool("hooks_context", "Get compact agent context in one call: installed hooks, active profile summary, settings path, and doctor status.", {
|
|
5695
|
-
scope:
|
|
5696
|
-
profile:
|
|
5697
|
-
verbose:
|
|
6795
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to inspect"),
|
|
6796
|
+
profile: z2.string().optional().describe("Agent profile ID to include in context"),
|
|
6797
|
+
verbose: z2.boolean().default(false).describe("Include full profile preferences/details when true")
|
|
5698
6798
|
}, async ({ scope, profile, verbose }) => {
|
|
5699
6799
|
const settingsPath = getSettingsPath(scope);
|
|
5700
6800
|
const registered = getRegisteredHooks(scope);
|
|
@@ -5712,7 +6812,7 @@ function createHooksServer() {
|
|
|
5712
6812
|
const ctx = {
|
|
5713
6813
|
scope,
|
|
5714
6814
|
settings_path: settingsPath,
|
|
5715
|
-
settings_exists:
|
|
6815
|
+
settings_exists: existsSync11(settingsPath),
|
|
5716
6816
|
registered_hooks: hooks,
|
|
5717
6817
|
hook_count: hooks.length,
|
|
5718
6818
|
healthy,
|
|
@@ -5728,10 +6828,10 @@ function createHooksServer() {
|
|
|
5728
6828
|
return { content: [{ type: "text", text: JSON.stringify(ctx) }] };
|
|
5729
6829
|
});
|
|
5730
6830
|
defineTool("hooks_preview", "Simulate which installed PreToolUse hooks would fire for a given tool call and what decision each returns. Use this to understand your hook environment before taking an action.", {
|
|
5731
|
-
tool_name:
|
|
5732
|
-
tool_input:
|
|
5733
|
-
scope:
|
|
5734
|
-
timeout_ms:
|
|
6831
|
+
tool_name: z2.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
|
|
6832
|
+
tool_input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
|
|
6833
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to check"),
|
|
6834
|
+
timeout_ms: z2.number().default(5000).describe("Per-hook timeout in milliseconds")
|
|
5735
6835
|
}, async ({ tool_name, tool_input, scope, timeout_ms }) => {
|
|
5736
6836
|
const registered = getRegisteredHooks(scope);
|
|
5737
6837
|
const matchingHooks = registered.filter((name) => {
|
|
@@ -5752,28 +6852,23 @@ function createHooksServer() {
|
|
|
5752
6852
|
const input = { tool_name, tool_input };
|
|
5753
6853
|
const results = await Promise.all(matchingHooks.map(async (name) => {
|
|
5754
6854
|
const hookDir = getHookPath(name);
|
|
5755
|
-
const hookScript =
|
|
5756
|
-
if (!
|
|
6855
|
+
const hookScript = join11(hookDir, "src", "hook.ts");
|
|
6856
|
+
if (!existsSync11(hookScript))
|
|
5757
6857
|
return { name, decision: "approve", error: "script not found" };
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
proc.kill();
|
|
5769
|
-
return { stdout: "", timedOut: true };
|
|
5770
|
-
})
|
|
5771
|
-
]);
|
|
5772
|
-
if (res.timedOut)
|
|
6858
|
+
let ran = null;
|
|
6859
|
+
try {
|
|
6860
|
+
ran = await Promise.race([
|
|
6861
|
+
runVerifiedHook(name, hookScript, JSON.stringify(input), timeout_ms),
|
|
6862
|
+
new Promise((r) => setTimeout(() => r(null), timeout_ms))
|
|
6863
|
+
]);
|
|
6864
|
+
} catch (err) {
|
|
6865
|
+
return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
|
|
6866
|
+
}
|
|
6867
|
+
if (ran === null)
|
|
5773
6868
|
return { name, decision: "approve", timedOut: true };
|
|
5774
6869
|
let output = {};
|
|
5775
6870
|
try {
|
|
5776
|
-
output = JSON.parse(
|
|
6871
|
+
output = JSON.parse(ran.stdout);
|
|
5777
6872
|
} catch {}
|
|
5778
6873
|
return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
|
|
5779
6874
|
}));
|
|
@@ -5793,10 +6888,10 @@ function createHooksServer() {
|
|
|
5793
6888
|
};
|
|
5794
6889
|
});
|
|
5795
6890
|
defineTool("hooks_setup", "Single-shot agent onboarding: create an agent profile + install recommended hooks in one call. Ideal for agents setting up hooks at session start.", {
|
|
5796
|
-
agent_type:
|
|
5797
|
-
name:
|
|
5798
|
-
hooks:
|
|
5799
|
-
scope:
|
|
6891
|
+
agent_type: z2.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
|
|
6892
|
+
name: z2.string().optional().describe("Optional display name for the agent"),
|
|
6893
|
+
hooks: z2.array(z2.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
|
|
6894
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Install scope")
|
|
5800
6895
|
}, async ({ agent_type, name, hooks, scope }) => {
|
|
5801
6896
|
const profile = createProfile({ agent_type, name });
|
|
5802
6897
|
const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
|
|
@@ -5811,52 +6906,49 @@ function createHooksServer() {
|
|
|
5811
6906
|
};
|
|
5812
6907
|
});
|
|
5813
6908
|
defineTool("hooks_batch_run", "Run multiple hooks in parallel in a single call. Returns all results at once \u2014 more efficient than N separate hooks_run calls.", {
|
|
5814
|
-
hooks:
|
|
5815
|
-
name:
|
|
5816
|
-
input:
|
|
6909
|
+
hooks: z2.array(z2.object({
|
|
6910
|
+
name: z2.string().describe("Hook name"),
|
|
6911
|
+
input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input JSON")
|
|
5817
6912
|
})).describe("List of hooks to run with their inputs"),
|
|
5818
|
-
timeout_ms:
|
|
6913
|
+
timeout_ms: z2.number().default(1e4).describe("Per-hook timeout in milliseconds")
|
|
5819
6914
|
}, async ({ hooks, timeout_ms }) => {
|
|
5820
6915
|
const results = await Promise.all(hooks.map(async ({ name, input }) => {
|
|
5821
6916
|
const meta = getHook(name);
|
|
5822
6917
|
if (!meta)
|
|
5823
6918
|
return { name, error: `Hook '${name}' not found` };
|
|
5824
|
-
const hookScript =
|
|
5825
|
-
if (!
|
|
6919
|
+
const hookScript = join11(getHookPath(name), "src", "hook.ts");
|
|
6920
|
+
if (!existsSync11(hookScript))
|
|
5826
6921
|
return { name, error: "script not found" };
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
return { stdout: "", stderr: "", exitCode: -1, timedOut: true };
|
|
5839
|
-
})
|
|
5840
|
-
]);
|
|
6922
|
+
let ran = null;
|
|
6923
|
+
try {
|
|
6924
|
+
ran = await Promise.race([
|
|
6925
|
+
runVerifiedHook(name, hookScript, JSON.stringify(input), timeout_ms),
|
|
6926
|
+
new Promise((r) => setTimeout(() => r(null), timeout_ms))
|
|
6927
|
+
]);
|
|
6928
|
+
} catch (err) {
|
|
6929
|
+
return { name, error: err instanceof Error ? err.message : String(err) };
|
|
6930
|
+
}
|
|
6931
|
+
const stdoutText = ran?.stdout ?? "";
|
|
6932
|
+
const timedOut = ran === null;
|
|
5841
6933
|
let output = {};
|
|
5842
6934
|
try {
|
|
5843
|
-
output = JSON.parse(
|
|
6935
|
+
output = JSON.parse(stdoutText);
|
|
5844
6936
|
} catch {
|
|
5845
|
-
output =
|
|
6937
|
+
output = stdoutText ? { raw: stdoutText } : {};
|
|
5846
6938
|
}
|
|
5847
|
-
return { name, output, exitCode:
|
|
6939
|
+
return { name, output, exitCode: ran?.exitCode ?? -1, ...timedOut ? { timedOut: true } : {} };
|
|
5848
6940
|
}));
|
|
5849
6941
|
return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }) }] };
|
|
5850
6942
|
});
|
|
5851
6943
|
defineTool("hooks_disable", "Temporarily disable a registered hook without removing it. Stores disabled list in settings under hooks.__disabled.", {
|
|
5852
|
-
name:
|
|
5853
|
-
scope:
|
|
6944
|
+
name: z2.string().describe("Hook name to disable"),
|
|
6945
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope")
|
|
5854
6946
|
}, async ({ name, scope }) => {
|
|
5855
6947
|
const settingsPath = getSettingsPath(scope);
|
|
5856
6948
|
let settings = {};
|
|
5857
6949
|
try {
|
|
5858
|
-
if (
|
|
5859
|
-
settings = JSON.parse(
|
|
6950
|
+
if (existsSync11(settingsPath))
|
|
6951
|
+
settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
|
|
5860
6952
|
} catch {}
|
|
5861
6953
|
if (!settings.hooks)
|
|
5862
6954
|
settings.hooks = {};
|
|
@@ -5864,43 +6956,43 @@ function createHooksServer() {
|
|
|
5864
6956
|
if (!disabled.includes(name))
|
|
5865
6957
|
disabled.push(name);
|
|
5866
6958
|
settings.hooks.__disabled = disabled;
|
|
5867
|
-
const { writeFileSync:
|
|
5868
|
-
const { dirname:
|
|
5869
|
-
|
|
5870
|
-
|
|
6959
|
+
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
|
|
6960
|
+
const { dirname: dirname5 } = await import("path");
|
|
6961
|
+
mkdirSync8(dirname5(settingsPath), { recursive: true });
|
|
6962
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
5871
6963
|
`);
|
|
5872
6964
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
5873
6965
|
});
|
|
5874
6966
|
defineTool("hooks_enable", "Re-enable a previously disabled hook.", {
|
|
5875
|
-
name:
|
|
5876
|
-
scope:
|
|
6967
|
+
name: z2.string().describe("Hook name to enable"),
|
|
6968
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope")
|
|
5877
6969
|
}, async ({ name, scope }) => {
|
|
5878
6970
|
const settingsPath = getSettingsPath(scope);
|
|
5879
6971
|
let settings = {};
|
|
5880
6972
|
try {
|
|
5881
|
-
if (
|
|
5882
|
-
settings = JSON.parse(
|
|
6973
|
+
if (existsSync11(settingsPath))
|
|
6974
|
+
settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
|
|
5883
6975
|
} catch {}
|
|
5884
6976
|
if (settings.hooks?.__disabled) {
|
|
5885
6977
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
5886
6978
|
if (settings.hooks.__disabled.length === 0)
|
|
5887
6979
|
delete settings.hooks.__disabled;
|
|
5888
|
-
const { writeFileSync:
|
|
5889
|
-
|
|
6980
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
6981
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
5890
6982
|
`);
|
|
5891
6983
|
}
|
|
5892
6984
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
|
|
5893
6985
|
});
|
|
5894
6986
|
defineTool("hooks_init", "Register a new agent profile \u2014 returns a unique agent_id for use with hook installation and execution", {
|
|
5895
|
-
agent_type:
|
|
5896
|
-
name:
|
|
6987
|
+
agent_type: z2.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
|
|
6988
|
+
name: z2.string().optional().describe("Optional display name for the agent")
|
|
5897
6989
|
}, async ({ agent_type, name }) => {
|
|
5898
6990
|
const profile = createProfile({ agent_type, name });
|
|
5899
6991
|
return { content: [{ type: "text", text: JSON.stringify(profile) }] };
|
|
5900
6992
|
});
|
|
5901
6993
|
defineTool("hooks_profiles", "List registered agent profiles. Compact by default.", {
|
|
5902
|
-
compact:
|
|
5903
|
-
limit:
|
|
6994
|
+
compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for full profiles."),
|
|
6995
|
+
limit: z2.number().default(25).describe("Max compact rows to return")
|
|
5904
6996
|
}, async ({ compact, limit }) => {
|
|
5905
6997
|
const profiles = listProfiles();
|
|
5906
6998
|
if (!compact)
|
|
@@ -5926,11 +7018,11 @@ function createHooksServer() {
|
|
|
5926
7018
|
};
|
|
5927
7019
|
});
|
|
5928
7020
|
defineTool("hooks_log_list", "List hook events from SQLite. Compact summaries by default; set compact:false for full event rows.", {
|
|
5929
|
-
hook_name:
|
|
5930
|
-
session_id:
|
|
5931
|
-
limit:
|
|
5932
|
-
since:
|
|
5933
|
-
compact:
|
|
7021
|
+
hook_name: z2.string().optional().describe("Filter by hook name (e.g. 'sessionlog', 'costwatch')"),
|
|
7022
|
+
session_id: z2.string().optional().describe("Filter by session ID prefix"),
|
|
7023
|
+
limit: z2.number().optional().describe("Max number of events to return. Defaults to 20 compact rows or 50 full rows."),
|
|
7024
|
+
since: z2.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from"),
|
|
7025
|
+
compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
|
|
5934
7026
|
}, async ({ hook_name, session_id, limit, since, compact }) => {
|
|
5935
7027
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
5936
7028
|
const db = getDb2();
|
|
@@ -5976,8 +7068,8 @@ function createHooksServer() {
|
|
|
5976
7068
|
};
|
|
5977
7069
|
});
|
|
5978
7070
|
defineTool("hooks_log_tail", "Show recent hook events from SQLite. Compact summaries by default.", {
|
|
5979
|
-
n:
|
|
5980
|
-
compact:
|
|
7071
|
+
n: z2.number().default(20).describe("Number of most recent events to return"),
|
|
7072
|
+
compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
|
|
5981
7073
|
}, async ({ n, compact }) => {
|
|
5982
7074
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
5983
7075
|
const db = getDb2();
|
|
@@ -5996,9 +7088,9 @@ function createHooksServer() {
|
|
|
5996
7088
|
};
|
|
5997
7089
|
});
|
|
5998
7090
|
defineTool("hooks_log_errors", "Show hook events that contain errors. Compact summaries by default.", {
|
|
5999
|
-
since:
|
|
6000
|
-
limit:
|
|
6001
|
-
compact:
|
|
7091
|
+
since: z2.string().default("24h").describe("Duration string (e.g. '1h', '30m', '7d') or ISO timestamp"),
|
|
7092
|
+
limit: z2.number().optional().describe("Max number of error events to return. Defaults to 20 compact rows or 50 full rows."),
|
|
7093
|
+
compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
|
|
6002
7094
|
}, async ({ since, limit, compact }) => {
|
|
6003
7095
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
6004
7096
|
const db = getDb2();
|
|
@@ -6026,7 +7118,7 @@ function createHooksServer() {
|
|
|
6026
7118
|
};
|
|
6027
7119
|
});
|
|
6028
7120
|
defineTool("hooks_log_summary", "Summarize hook execution: counts per hook, error rates, and recent activity.", {
|
|
6029
|
-
since:
|
|
7121
|
+
since: z2.string().default("24h").describe("Duration string (e.g. '1h', '24h', '7d') or ISO timestamp")
|
|
6030
7122
|
}, async ({ since }) => {
|
|
6031
7123
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
6032
7124
|
const db = getDb2();
|
|
@@ -6060,13 +7152,13 @@ function createHooksServer() {
|
|
|
6060
7152
|
};
|
|
6061
7153
|
});
|
|
6062
7154
|
defineTool("storage_status", "Show hooks storage sync configuration and local sync history.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(getStorageStatus()) }] }));
|
|
6063
|
-
defineTool("storage_push", "Push local hook data to storage PostgreSQL.", { tables:
|
|
6064
|
-
defineTool("storage_pull", "Pull hook data from storage PostgreSQL to local SQLite.", { tables:
|
|
6065
|
-
defineTool("storage_sync", "Bidirectional hooks storage sync: pull then push.", { tables:
|
|
7155
|
+
defineTool("storage_push", "Push local hook data to storage PostgreSQL.", { tables: z2.array(z2.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePush(params.tables ? { tables: params.tables } : undefined)) }] }));
|
|
7156
|
+
defineTool("storage_pull", "Pull hook data from storage PostgreSQL to local SQLite.", { tables: z2.array(z2.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePull(params.tables ? { tables: params.tables } : undefined)) }] }));
|
|
7157
|
+
defineTool("storage_sync", "Bidirectional hooks storage sync: pull then push.", { tables: z2.array(z2.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storageSync(params.tables ? { tables: params.tables } : undefined)) }] }));
|
|
6066
7158
|
defineTool("send_feedback", "Send feedback about this service", {
|
|
6067
|
-
message:
|
|
6068
|
-
email:
|
|
6069
|
-
category:
|
|
7159
|
+
message: z2.string(),
|
|
7160
|
+
email: z2.string().optional(),
|
|
7161
|
+
category: z2.enum(["bug", "feature", "general"]).optional()
|
|
6070
7162
|
}, async (params) => {
|
|
6071
7163
|
try {
|
|
6072
7164
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
@@ -6083,8 +7175,8 @@ function createHooksServer() {
|
|
|
6083
7175
|
}
|
|
6084
7176
|
});
|
|
6085
7177
|
defineTool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
|
|
6086
|
-
name:
|
|
6087
|
-
session_id:
|
|
7178
|
+
name: z2.string(),
|
|
7179
|
+
session_id: z2.string().optional()
|
|
6088
7180
|
}, async (params) => {
|
|
6089
7181
|
const existing = [..._hooksAgents.values()].find((a) => a.name === params.name);
|
|
6090
7182
|
if (existing) {
|
|
@@ -6099,7 +7191,7 @@ function createHooksServer() {
|
|
|
6099
7191
|
return { content: [{ type: "text", text: JSON.stringify(ag) }] };
|
|
6100
7192
|
});
|
|
6101
7193
|
defineTool("heartbeat", "Update last_seen_at to signal agent is active.", {
|
|
6102
|
-
agent_id:
|
|
7194
|
+
agent_id: z2.string()
|
|
6103
7195
|
}, async (params) => {
|
|
6104
7196
|
const ag = _hooksAgents.get(params.agent_id);
|
|
6105
7197
|
if (!ag)
|
|
@@ -6108,8 +7200,8 @@ function createHooksServer() {
|
|
|
6108
7200
|
return { content: [{ type: "text", text: JSON.stringify({ agent_id: ag.id, last_seen_at: ag.last_seen_at }) }] };
|
|
6109
7201
|
});
|
|
6110
7202
|
defineTool("set_focus", "Set active project context for this agent session.", {
|
|
6111
|
-
agent_id:
|
|
6112
|
-
project_id:
|
|
7203
|
+
agent_id: z2.string(),
|
|
7204
|
+
project_id: z2.string().optional()
|
|
6113
7205
|
}, async (params) => {
|
|
6114
7206
|
const ag = _hooksAgents.get(params.agent_id);
|
|
6115
7207
|
if (!ag)
|
|
@@ -6118,8 +7210,8 @@ function createHooksServer() {
|
|
|
6118
7210
|
return { content: [{ type: "text", text: JSON.stringify({ agent_id: ag.id, project_id: ag.project_id ?? null }) }] };
|
|
6119
7211
|
});
|
|
6120
7212
|
defineTool("list_agents", "List registered agents. Compact by default.", {
|
|
6121
|
-
compact:
|
|
6122
|
-
limit:
|
|
7213
|
+
compact: z2.boolean().default(true).describe("Return compact agent summaries by default. Set false for full records."),
|
|
7214
|
+
limit: z2.number().default(25).describe("Max compact rows to return")
|
|
6123
7215
|
}, async ({ compact, limit }) => {
|
|
6124
7216
|
const agents = [..._hooksAgents.values()];
|
|
6125
7217
|
if (!compact)
|
|
@@ -6194,14 +7286,17 @@ var init_server = __esm(() => {
|
|
|
6194
7286
|
init_registry();
|
|
6195
7287
|
init_installer();
|
|
6196
7288
|
init_profiles();
|
|
7289
|
+
init_manifest();
|
|
7290
|
+
init_resolve();
|
|
7291
|
+
init_store();
|
|
6197
7292
|
init_storage();
|
|
6198
|
-
__dirname3 =
|
|
7293
|
+
__dirname3 = dirname4(fileURLToPath2(import.meta.url));
|
|
6199
7294
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
6200
7295
|
try {
|
|
6201
7296
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
6202
|
-
const p =
|
|
6203
|
-
if (
|
|
6204
|
-
pkg = JSON.parse(
|
|
7297
|
+
const p = join11(__dirname3, rel);
|
|
7298
|
+
if (existsSync11(p)) {
|
|
7299
|
+
pkg = JSON.parse(readFileSync9(p, "utf-8"));
|
|
6205
7300
|
break;
|
|
6206
7301
|
}
|
|
6207
7302
|
}
|
|
@@ -6268,7 +7363,7 @@ function startMcpHttpServer(options) {
|
|
|
6268
7363
|
var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
|
|
6269
7364
|
var init_http = () => {};
|
|
6270
7365
|
|
|
6271
|
-
// node_modules/@hasna/events/dist/commander.js
|
|
7366
|
+
// ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
|
|
6272
7367
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6273
7368
|
import { Buffer as Buffer2 } from "buffer";
|
|
6274
7369
|
import { existsSync } from "fs";
|
|
@@ -6685,21 +7780,27 @@ function now() {
|
|
|
6685
7780
|
function truncate(value, max = 4096) {
|
|
6686
7781
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
6687
7782
|
}
|
|
6688
|
-
function buildWebhookRequest(event, channel) {
|
|
7783
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
6689
7784
|
if (!channel.webhook)
|
|
6690
7785
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
7786
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
7787
|
+
if (/^x-hasna-/i.test(name)) {
|
|
7788
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
7789
|
+
}
|
|
7790
|
+
}
|
|
6691
7791
|
const body = JSON.stringify(event);
|
|
6692
|
-
const timestamp =
|
|
7792
|
+
const timestamp = options.timestamp ?? new Date().toISOString();
|
|
6693
7793
|
const headers = {
|
|
6694
7794
|
"Content-Type": "application/json",
|
|
6695
7795
|
"User-Agent": "@hasna/events",
|
|
6696
7796
|
"X-Hasna-Event-Id": event.id,
|
|
6697
7797
|
"X-Hasna-Event-Type": event.type,
|
|
6698
|
-
|
|
6699
|
-
|
|
7798
|
+
...channel.webhook.headers,
|
|
7799
|
+
"X-Hasna-Timestamp": timestamp
|
|
6700
7800
|
};
|
|
6701
|
-
|
|
6702
|
-
|
|
7801
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
7802
|
+
if (secret) {
|
|
7803
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
|
|
6703
7804
|
}
|
|
6704
7805
|
return { body, headers };
|
|
6705
7806
|
}
|
|
@@ -6707,7 +7808,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6707
7808
|
if (!channel.webhook)
|
|
6708
7809
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
6709
7810
|
const startedAt = now();
|
|
6710
|
-
|
|
7811
|
+
let secret = channel.webhook.secret;
|
|
7812
|
+
if (channel.webhook.secretRef) {
|
|
7813
|
+
if (!options.secretResolver) {
|
|
7814
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
7815
|
+
}
|
|
7816
|
+
try {
|
|
7817
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
7818
|
+
} catch {
|
|
7819
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
7820
|
+
}
|
|
7821
|
+
if (!secret)
|
|
7822
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
7823
|
+
}
|
|
7824
|
+
const timestamp = (options.now?.() ?? new Date).toISOString();
|
|
7825
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
|
|
6711
7826
|
const controller = new AbortController;
|
|
6712
7827
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
6713
7828
|
try {
|
|
@@ -6739,6 +7854,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6739
7854
|
clearTimeout(timeout);
|
|
6740
7855
|
}
|
|
6741
7856
|
}
|
|
7857
|
+
function failedAttempt(startedAt, error) {
|
|
7858
|
+
return {
|
|
7859
|
+
attempt: 1,
|
|
7860
|
+
status: "failed",
|
|
7861
|
+
startedAt,
|
|
7862
|
+
completedAt: now(),
|
|
7863
|
+
error
|
|
7864
|
+
};
|
|
7865
|
+
}
|
|
6742
7866
|
async function dispatchCommand(event, channel) {
|
|
6743
7867
|
if (!channel.command)
|
|
6744
7868
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -6872,6 +7996,45 @@ class EventTypeCatalog {
|
|
|
6872
7996
|
}
|
|
6873
7997
|
}
|
|
6874
7998
|
var defaultEventTypeCatalog = new EventTypeCatalog;
|
|
7999
|
+
var APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
8000
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
8001
|
+
if (paths.length === 0)
|
|
8002
|
+
return event;
|
|
8003
|
+
const copy = structuredClone(event);
|
|
8004
|
+
for (const path of paths) {
|
|
8005
|
+
setPath(copy, path, replacement);
|
|
8006
|
+
}
|
|
8007
|
+
return copy;
|
|
8008
|
+
}
|
|
8009
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
8010
|
+
return redactValue(event, replacement);
|
|
8011
|
+
}
|
|
8012
|
+
function shouldRedactKey(key) {
|
|
8013
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
8014
|
+
}
|
|
8015
|
+
function redactValue(value, replacement) {
|
|
8016
|
+
if (Array.isArray(value))
|
|
8017
|
+
return value.map((item) => redactValue(item, replacement));
|
|
8018
|
+
if (!value || typeof value !== "object")
|
|
8019
|
+
return value;
|
|
8020
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
8021
|
+
key,
|
|
8022
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
8023
|
+
]));
|
|
8024
|
+
}
|
|
8025
|
+
function setPath(input, path, replacement) {
|
|
8026
|
+
const parts = path.split(".");
|
|
8027
|
+
let cursor = input;
|
|
8028
|
+
for (const part of parts.slice(0, -1)) {
|
|
8029
|
+
const next = cursor[part];
|
|
8030
|
+
if (!next || typeof next !== "object")
|
|
8031
|
+
return;
|
|
8032
|
+
cursor = next;
|
|
8033
|
+
}
|
|
8034
|
+
const last = parts.at(-1);
|
|
8035
|
+
if (last && last in cursor)
|
|
8036
|
+
cursor[last] = replacement;
|
|
8037
|
+
}
|
|
6875
8038
|
function createEvent(input) {
|
|
6876
8039
|
return {
|
|
6877
8040
|
id: input.id ?? randomUUID2(),
|
|
@@ -6897,7 +8060,11 @@ class EventsClient {
|
|
|
6897
8060
|
constructor(options = {}) {
|
|
6898
8061
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
6899
8062
|
this.redactors = options.redactors ?? [];
|
|
6900
|
-
this.transportOptions = {
|
|
8063
|
+
this.transportOptions = {
|
|
8064
|
+
fetchImpl: options.fetchImpl,
|
|
8065
|
+
secretResolver: options.secretResolver,
|
|
8066
|
+
now: options.now
|
|
8067
|
+
};
|
|
6901
8068
|
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
6902
8069
|
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
6903
8070
|
}
|
|
@@ -7075,15 +8242,6 @@ class EventsClient {
|
|
|
7075
8242
|
return createDeliveryResult(event, channel, attempts);
|
|
7076
8243
|
}
|
|
7077
8244
|
}
|
|
7078
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
7079
|
-
if (paths.length === 0)
|
|
7080
|
-
return event;
|
|
7081
|
-
const copy = structuredClone(event);
|
|
7082
|
-
for (const path of paths) {
|
|
7083
|
-
setPath(copy, path, replacement);
|
|
7084
|
-
}
|
|
7085
|
-
return copy;
|
|
7086
|
-
}
|
|
7087
8245
|
function sanitizeChannelForOutput(channel) {
|
|
7088
8246
|
const copy = structuredClone(channel);
|
|
7089
8247
|
if (copy.webhook?.secret)
|
|
@@ -7096,35 +8254,6 @@ function sanitizeChannelForOutput(channel) {
|
|
|
7096
8254
|
function sanitizeChannelsForOutput(channels) {
|
|
7097
8255
|
return channels.map(sanitizeChannelForOutput);
|
|
7098
8256
|
}
|
|
7099
|
-
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
7100
|
-
return redactValue(event, replacement);
|
|
7101
|
-
}
|
|
7102
|
-
function shouldRedactKey(key) {
|
|
7103
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
7104
|
-
}
|
|
7105
|
-
function redactValue(value, replacement) {
|
|
7106
|
-
if (Array.isArray(value))
|
|
7107
|
-
return value.map((item) => redactValue(item, replacement));
|
|
7108
|
-
if (!value || typeof value !== "object")
|
|
7109
|
-
return value;
|
|
7110
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
7111
|
-
key,
|
|
7112
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
7113
|
-
]));
|
|
7114
|
-
}
|
|
7115
|
-
function setPath(input, path, replacement) {
|
|
7116
|
-
const parts = path.split(".");
|
|
7117
|
-
let cursor = input;
|
|
7118
|
-
for (const part of parts.slice(0, -1)) {
|
|
7119
|
-
const next = cursor[part];
|
|
7120
|
-
if (!next || typeof next !== "object")
|
|
7121
|
-
return;
|
|
7122
|
-
cursor = next;
|
|
7123
|
-
}
|
|
7124
|
-
const last = parts.at(-1);
|
|
7125
|
-
if (last && last in cursor)
|
|
7126
|
-
cursor[last] = replacement;
|
|
7127
|
-
}
|
|
7128
8257
|
function queryClientEvents(events, options) {
|
|
7129
8258
|
let rows = events;
|
|
7130
8259
|
if (options.eventId)
|
|
@@ -7419,7 +8548,7 @@ function replaySummary(events, deliveries, nextCursor) {
|
|
|
7419
8548
|
// src/cli/index.tsx
|
|
7420
8549
|
import { render } from "ink";
|
|
7421
8550
|
|
|
7422
|
-
// node_modules/commander/esm.mjs
|
|
8551
|
+
// ../../node_modules/.bun/commander@12.1.0/node_modules/commander/esm.mjs
|
|
7423
8552
|
var import__ = __toESM(require_commander(), 1);
|
|
7424
8553
|
var {
|
|
7425
8554
|
program,
|
|
@@ -7437,19 +8566,19 @@ var {
|
|
|
7437
8566
|
|
|
7438
8567
|
// src/cli/index.tsx
|
|
7439
8568
|
import chalk2 from "chalk";
|
|
7440
|
-
import { existsSync as
|
|
7441
|
-
import { join as
|
|
8569
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
8570
|
+
import { join as join12, dirname as dirname5 } from "path";
|
|
7442
8571
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7443
8572
|
|
|
7444
8573
|
// src/cli/components/App.tsx
|
|
7445
8574
|
import { useState as useState7 } from "react";
|
|
7446
8575
|
import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
|
|
7447
8576
|
|
|
7448
|
-
// node_modules/ink-select-input/build/Indicator.js
|
|
8577
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
|
|
7449
8578
|
import React from "react";
|
|
7450
8579
|
import { Box, Text } from "ink";
|
|
7451
8580
|
|
|
7452
|
-
// node_modules/is-unicode-supported/index.js
|
|
8581
|
+
// ../../node_modules/.bun/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js
|
|
7453
8582
|
import process2 from "process";
|
|
7454
8583
|
function isUnicodeSupported() {
|
|
7455
8584
|
const { env } = process2;
|
|
@@ -7460,7 +8589,7 @@ function isUnicodeSupported() {
|
|
|
7460
8589
|
return Boolean(env.WT_SESSION) || Boolean(env.TERMINUS_SUBLIME) || env.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
|
|
7461
8590
|
}
|
|
7462
8591
|
|
|
7463
|
-
// node_modules/figures/index.js
|
|
8592
|
+
// ../../node_modules/.bun/figures@6.1.0/node_modules/figures/index.js
|
|
7464
8593
|
var common = {
|
|
7465
8594
|
circleQuestionMark: "(?)",
|
|
7466
8595
|
questionMarkPrefix: "(?)",
|
|
@@ -7736,23 +8865,23 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
|
|
|
7736
8865
|
var figures_default = figures;
|
|
7737
8866
|
var replacements = Object.entries(specialMainSymbols);
|
|
7738
8867
|
|
|
7739
|
-
// node_modules/ink-select-input/build/Indicator.js
|
|
8868
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
|
|
7740
8869
|
function Indicator({ isSelected = false }) {
|
|
7741
8870
|
return React.createElement(Box, { marginRight: 1 }, isSelected ? React.createElement(Text, { color: "blue" }, figures_default.pointer) : React.createElement(Text, null, " "));
|
|
7742
8871
|
}
|
|
7743
8872
|
var Indicator_default = Indicator;
|
|
7744
|
-
// node_modules/ink-select-input/build/Item.js
|
|
8873
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Item.js
|
|
7745
8874
|
import * as React2 from "react";
|
|
7746
8875
|
import { Text as Text2 } from "ink";
|
|
7747
8876
|
function Item({ isSelected = false, label }) {
|
|
7748
8877
|
return React2.createElement(Text2, { color: isSelected ? "blue" : undefined }, label);
|
|
7749
8878
|
}
|
|
7750
8879
|
var Item_default = Item;
|
|
7751
|
-
// node_modules/ink-select-input/build/SelectInput.js
|
|
8880
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
|
|
7752
8881
|
import { isDeepStrictEqual } from "util";
|
|
7753
8882
|
import React3, { useState, useEffect, useRef, useCallback } from "react";
|
|
7754
8883
|
|
|
7755
|
-
// node_modules/to-rotated/index.js
|
|
8884
|
+
// ../../node_modules/.bun/to-rotated@1.0.0/node_modules/to-rotated/index.js
|
|
7756
8885
|
function toRotated(array, steps) {
|
|
7757
8886
|
if (!Array.isArray(array)) {
|
|
7758
8887
|
throw new TypeError(`Expected an array, got \`${typeof array}\`.`);
|
|
@@ -7774,7 +8903,7 @@ function toRotated(array, steps) {
|
|
|
7774
8903
|
];
|
|
7775
8904
|
}
|
|
7776
8905
|
|
|
7777
|
-
// node_modules/ink-select-input/build/SelectInput.js
|
|
8906
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
|
|
7778
8907
|
import { Box as Box2, useInput } from "ink";
|
|
7779
8908
|
function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent = Indicator_default, itemComponent = Item_default, limit: customLimit, onSelect, onHighlight }) {
|
|
7780
8909
|
const hasLimit = typeof customLimit === "number" && items.length > customLimit;
|
|
@@ -8070,7 +9199,7 @@ function HookSelect({
|
|
|
8070
9199
|
import { useState as useState4, useEffect as useEffect3 } from "react";
|
|
8071
9200
|
import { Box as Box6, Text as Text7, useInput as useInput4 } from "ink";
|
|
8072
9201
|
|
|
8073
|
-
// node_modules/ink-text-input/build/index.js
|
|
9202
|
+
// ../../node_modules/.bun/ink-text-input@6.0.0+fa875fef9f949ebd/node_modules/ink-text-input/build/index.js
|
|
8074
9203
|
import React5, { useState as useState3, useEffect as useEffect2 } from "react";
|
|
8075
9204
|
import { Text as Text6, useInput as useInput3 } from "ink";
|
|
8076
9205
|
import chalk from "chalk";
|
|
@@ -8248,7 +9377,7 @@ function SearchView({
|
|
|
8248
9377
|
import { useState as useState6, useEffect as useEffect5 } from "react";
|
|
8249
9378
|
import { Box as Box7, Text as Text9 } from "ink";
|
|
8250
9379
|
|
|
8251
|
-
// node_modules/ink-spinner/build/index.js
|
|
9380
|
+
// ../../node_modules/.bun/ink-spinner@5.0.0+fa875fef9f949ebd/node_modules/ink-spinner/build/index.js
|
|
8252
9381
|
var import_cli_spinners = __toESM(require_cli_spinners(), 1);
|
|
8253
9382
|
import React7, { useState as useState5, useEffect as useEffect4 } from "react";
|
|
8254
9383
|
import { Text as Text8 } from "ink";
|
|
@@ -8614,10 +9743,12 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
8614
9743
|
init_registry();
|
|
8615
9744
|
init_installer();
|
|
8616
9745
|
init_profiles();
|
|
9746
|
+
init_manifest();
|
|
9747
|
+
init_resolve();
|
|
8617
9748
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
8618
|
-
var __dirname4 =
|
|
8619
|
-
var pkgPath =
|
|
8620
|
-
var pkg2 = JSON.parse(
|
|
9749
|
+
var __dirname4 = dirname5(fileURLToPath3(import.meta.url));
|
|
9750
|
+
var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
|
|
9751
|
+
var pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
8621
9752
|
var program2 = new Command;
|
|
8622
9753
|
function resolveScope(options) {
|
|
8623
9754
|
if (options.project)
|
|
@@ -8645,6 +9776,16 @@ function truncateText2(value, max = 96) {
|
|
|
8645
9776
|
return text;
|
|
8646
9777
|
return `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
8647
9778
|
}
|
|
9779
|
+
function readToken(tokenFile) {
|
|
9780
|
+
if (tokenFile) {
|
|
9781
|
+
try {
|
|
9782
|
+
return readFileSync10(tokenFile, "utf-8").trim();
|
|
9783
|
+
} catch {
|
|
9784
|
+
return;
|
|
9785
|
+
}
|
|
9786
|
+
}
|
|
9787
|
+
return process.env.CF_API_TOKEN;
|
|
9788
|
+
}
|
|
8648
9789
|
function readmePreview(readme, max = 280) {
|
|
8649
9790
|
const blocks = readme.split(/\n\s*\n/).map((part) => part.trim()).filter(Boolean).filter((part) => !part.startsWith("#")).filter((part) => !part.startsWith("```")).filter((part) => !/^\[!\[/.test(part));
|
|
8650
9791
|
const preview = blocks[0] ?? readme.split(`
|
|
@@ -8685,7 +9826,38 @@ program2.name("hooks").description("Install hooks for AI coding agents").version
|
|
|
8685
9826
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
8686
9827
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
8687
9828
|
});
|
|
8688
|
-
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).action((options) => {
|
|
9829
|
+
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) => {
|
|
9830
|
+
if (options.cloudflare) {
|
|
9831
|
+
const { writeConfig: writeConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
9832
|
+
if (!options.apiUrl) {
|
|
9833
|
+
const message = "--cloudflare requires --api-url <url>";
|
|
9834
|
+
if (options.json)
|
|
9835
|
+
console.log(JSON.stringify({ error: message }));
|
|
9836
|
+
else
|
|
9837
|
+
console.log(chalk2.red(message));
|
|
9838
|
+
return;
|
|
9839
|
+
}
|
|
9840
|
+
const config = { api_url: options.apiUrl.replace(/\/+$/, ""), api_key_ref: options.apiKey };
|
|
9841
|
+
const path = writeConfig2(config);
|
|
9842
|
+
if (options.json) {
|
|
9843
|
+
console.log(JSON.stringify({ ok: true, api_url: config.api_url, api_key_ref: config.api_key_ref ?? null, config_path: path }));
|
|
9844
|
+
return;
|
|
9845
|
+
}
|
|
9846
|
+
console.log(chalk2.green(`
|
|
9847
|
+
\u2713 Remote registry configured
|
|
9848
|
+
`));
|
|
9849
|
+
console.log(` ${chalk2.dim("API URL:")} ${config.api_url}`);
|
|
9850
|
+
console.log(` ${chalk2.dim("API key ref:")} ${config.api_key_ref ?? "(none; serve will refuse publishes)"}`);
|
|
9851
|
+
console.log(` ${chalk2.dim("Config:")} ${path}`);
|
|
9852
|
+
console.log();
|
|
9853
|
+
console.log(chalk2.dim(" Presence of an API URL selects the remote registry; absence means local."));
|
|
9854
|
+
if (config.api_key_ref) {
|
|
9855
|
+
console.log(chalk2.dim(` Run the server with the key resolved from the vault, never the value:`));
|
|
9856
|
+
console.log(` secrets exec ${config.api_key_ref} --as HASNA_HOOKS_API_KEY -- hooks serve`);
|
|
9857
|
+
}
|
|
9858
|
+
console.log();
|
|
9859
|
+
return;
|
|
9860
|
+
}
|
|
8689
9861
|
const agentType = options.agent;
|
|
8690
9862
|
if (!["claude", "gemini", "custom"].includes(agentType)) {
|
|
8691
9863
|
if (options.json) {
|
|
@@ -8716,15 +9888,27 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
8716
9888
|
console.log();
|
|
8717
9889
|
});
|
|
8718
9890
|
program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
|
|
8719
|
-
const
|
|
8720
|
-
|
|
9891
|
+
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
9892
|
+
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
9893
|
+
const resolved = resolveHook2(hook);
|
|
9894
|
+
if (!resolved) {
|
|
8721
9895
|
console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
8722
9896
|
process.exit(1);
|
|
8723
9897
|
}
|
|
8724
|
-
const
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
9898
|
+
const hookScript = resolveScriptPath2(hook);
|
|
9899
|
+
if (!hookScript || !existsSync12(hookScript)) {
|
|
9900
|
+
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
9901
|
+
process.exit(1);
|
|
9902
|
+
}
|
|
9903
|
+
const content = readFileSync10(hookScript);
|
|
9904
|
+
const check = checkScriptHash2(hook, sha256Of2(content));
|
|
9905
|
+
if (!check.ok) {
|
|
9906
|
+
console.error(JSON.stringify({
|
|
9907
|
+
error: `Hook '${hook}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${hook}' to trust the new content.`,
|
|
9908
|
+
hook,
|
|
9909
|
+
expected_sha256: check.expected,
|
|
9910
|
+
actual_sha256: check.actual
|
|
9911
|
+
}));
|
|
8728
9912
|
process.exit(1);
|
|
8729
9913
|
}
|
|
8730
9914
|
const stdin = await new Response(Bun.stdin.stream()).text();
|
|
@@ -8745,22 +9929,27 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
8745
9929
|
} catch {}
|
|
8746
9930
|
}
|
|
8747
9931
|
}
|
|
8748
|
-
const
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
9932
|
+
const { readCustomManifest: readCustomManifest2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
9933
|
+
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
9934
|
+
const custom = readCustomManifest2(hook);
|
|
9935
|
+
const args = custom?.manifest.args ?? [];
|
|
9936
|
+
const timeout = custom?.manifest.timeout_ms;
|
|
9937
|
+
const { stdout, stderr, exitCode } = await executeVerifiedScript2({
|
|
9938
|
+
name: hook,
|
|
9939
|
+
scriptPath: hookScript,
|
|
9940
|
+
content,
|
|
9941
|
+
args,
|
|
9942
|
+
stdin: hookStdin,
|
|
9943
|
+
env: process.env,
|
|
9944
|
+
timeout
|
|
8753
9945
|
});
|
|
8754
|
-
const stdout = await new Response(proc.stdout).text();
|
|
8755
|
-
const stderr = await new Response(proc.stderr).text();
|
|
8756
|
-
const exitCode = await proc.exited;
|
|
8757
9946
|
if (stdout)
|
|
8758
9947
|
process.stdout.write(stdout);
|
|
8759
9948
|
if (stderr)
|
|
8760
9949
|
process.stderr.write(stderr);
|
|
8761
9950
|
process.exit(exitCode);
|
|
8762
9951
|
});
|
|
8763
|
-
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 open-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").action((hooks, options) => {
|
|
9952
|
+
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 open-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, or manifest URLs)").action(async (hooks, options) => {
|
|
8764
9953
|
const scope = resolveScope(options);
|
|
8765
9954
|
const target = resolveTarget(options);
|
|
8766
9955
|
let toInstall = hooks;
|
|
@@ -8792,11 +9981,21 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
8792
9981
|
}
|
|
8793
9982
|
return;
|
|
8794
9983
|
}
|
|
9984
|
+
const { isCustomSource: isCustomSource2, installCustomSource: installCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
9985
|
+
const customSources = toInstall.filter((n) => !getHook(n) && isCustomSource2(n));
|
|
8795
9986
|
if (options.dryRun) {
|
|
8796
9987
|
const known = toInstall.filter((n) => getHook(n));
|
|
8797
|
-
const unknown = toInstall.filter((n) => !getHook(n));
|
|
9988
|
+
const unknown = toInstall.filter((n) => !getHook(n) && !isCustomSource2(n));
|
|
8798
9989
|
if (options.json) {
|
|
8799
|
-
console.log(JSON.stringify({
|
|
9990
|
+
console.log(JSON.stringify({
|
|
9991
|
+
dryRun: true,
|
|
9992
|
+
would_install: known,
|
|
9993
|
+
custom_sources: customSources,
|
|
9994
|
+
unknown,
|
|
9995
|
+
scope,
|
|
9996
|
+
target,
|
|
9997
|
+
mode: target === "codewith" ? "fragment" : "write"
|
|
9998
|
+
}));
|
|
8800
9999
|
return;
|
|
8801
10000
|
}
|
|
8802
10001
|
console.log(chalk2.bold(`
|
|
@@ -8806,6 +10005,9 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
8806
10005
|
const meta = getHook(name);
|
|
8807
10006
|
console.log(chalk2.cyan(` ${name}`) + chalk2.dim(` [${meta.event}${meta.matcher ? ` ${meta.matcher}` : ""}]`));
|
|
8808
10007
|
}
|
|
10008
|
+
for (const source of customSources) {
|
|
10009
|
+
console.log(chalk2.cyan(` ${source}`) + chalk2.dim(" [custom source: would fetch and install]"));
|
|
10010
|
+
}
|
|
8809
10011
|
if (unknown.length > 0) {
|
|
8810
10012
|
console.log();
|
|
8811
10013
|
for (const name of unknown) {
|
|
@@ -8816,8 +10018,29 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
8816
10018
|
return;
|
|
8817
10019
|
}
|
|
8818
10020
|
const results = [];
|
|
8819
|
-
|
|
8820
|
-
|
|
10021
|
+
const installedCustom = [];
|
|
10022
|
+
for (const source of customSources) {
|
|
10023
|
+
try {
|
|
10024
|
+
const custom = await installCustomSource2(source);
|
|
10025
|
+
installedCustom.push(custom.name);
|
|
10026
|
+
if (options.json) {
|
|
10027
|
+
console.log(JSON.stringify({ custom: { source, name: custom.name, version: custom.version } }));
|
|
10028
|
+
} else {
|
|
10029
|
+
console.log(chalk2.green(`\u2713 Installed custom hook '${custom.name}' v${custom.version} from ${source}`));
|
|
10030
|
+
}
|
|
10031
|
+
} catch (error) {
|
|
10032
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10033
|
+
results.push({ hook: source, success: false, error: message });
|
|
10034
|
+
if (options.json) {
|
|
10035
|
+
console.log(JSON.stringify({ error: `Custom hook install failed for ${source}: ${message}` }));
|
|
10036
|
+
} else {
|
|
10037
|
+
console.log(chalk2.red(`\u2717 ${message}`));
|
|
10038
|
+
}
|
|
10039
|
+
}
|
|
10040
|
+
}
|
|
10041
|
+
const namesToRegister = [...new Set([...toInstall.filter((n) => !isCustomSource2(n)), ...installedCustom])];
|
|
10042
|
+
for (const name of namesToRegister) {
|
|
10043
|
+
if (!getHook(name) && !installedCustom.includes(name)) {
|
|
8821
10044
|
const suggestions = suggestHooks(name);
|
|
8822
10045
|
const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
|
|
8823
10046
|
results.push({ hook: name, success: false, error: `Hook '${name}' not found${hint}` });
|
|
@@ -9007,8 +10230,9 @@ Categories:
|
|
|
9007
10230
|
console.log(` ${category} (${count})`);
|
|
9008
10231
|
}
|
|
9009
10232
|
});
|
|
9010
|
-
program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action((hook, options) => {
|
|
9011
|
-
const
|
|
10233
|
+
program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action(async (hook, options) => {
|
|
10234
|
+
const { resolveHookMeta: resolveHookMeta2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
10235
|
+
const meta = resolveHookMeta2(hook) ?? getHook(hook);
|
|
9012
10236
|
if (!meta) {
|
|
9013
10237
|
const suggestions = suggestHooks(hook);
|
|
9014
10238
|
const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
|
|
@@ -9021,8 +10245,12 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
|
|
|
9021
10245
|
}
|
|
9022
10246
|
const globalInstalled = getRegisteredHooks("global").includes(meta.name);
|
|
9023
10247
|
const projectInstalled = getRegisteredHooks("project").includes(meta.name);
|
|
10248
|
+
const { readCustomManifest: readCustomManifest2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
10249
|
+
const custom = readCustomManifest2(meta.name);
|
|
10250
|
+
const source = custom ? "custom" : "bundled";
|
|
10251
|
+
const sourceNote = custom ? "custom (~/.hasna/hooks/hooks/) overrides the bundled registry" : "bundled registry";
|
|
9024
10252
|
if (options.json) {
|
|
9025
|
-
console.log(JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }));
|
|
10253
|
+
console.log(JSON.stringify({ ...meta, source, source_note: sourceNote, global: globalInstalled, project: projectInstalled }));
|
|
9026
10254
|
return;
|
|
9027
10255
|
}
|
|
9028
10256
|
console.log(chalk2.bold(`
|
|
@@ -9035,6 +10263,7 @@ ${meta.displayName}
|
|
|
9035
10263
|
console.log(` ${chalk2.dim("Event:")} ${meta.event}`);
|
|
9036
10264
|
console.log(` ${chalk2.dim("Matcher:")} ${meta.matcher || "(none)"}`);
|
|
9037
10265
|
console.log(` ${chalk2.dim("Tags:")} ${meta.tags.join(", ")}`);
|
|
10266
|
+
console.log(` ${chalk2.dim("Source:")} ${source} (${sourceNote})`);
|
|
9038
10267
|
console.log(` ${chalk2.dim("Command:")} hooks run ${meta.name}`);
|
|
9039
10268
|
console.log();
|
|
9040
10269
|
if (globalInstalled) {
|
|
@@ -9053,28 +10282,36 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
9053
10282
|
const settingsPath = getSettingsPath(scope);
|
|
9054
10283
|
const issues = [];
|
|
9055
10284
|
const healthy = [];
|
|
9056
|
-
const settingsExist =
|
|
10285
|
+
const settingsExist = existsSync12(settingsPath);
|
|
9057
10286
|
if (!settingsExist) {
|
|
9058
10287
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
9059
10288
|
}
|
|
9060
10289
|
const registered = getRegisteredHooks(scope);
|
|
9061
10290
|
for (const name of registered) {
|
|
9062
|
-
const
|
|
10291
|
+
const custom = readCustomManifest(name);
|
|
10292
|
+
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
9063
10293
|
let hookHealthy = true;
|
|
9064
|
-
if (
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
10294
|
+
if (custom) {
|
|
10295
|
+
if (!existsSync12(custom.scriptPath)) {
|
|
10296
|
+
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
10297
|
+
hookHealthy = false;
|
|
10298
|
+
}
|
|
10299
|
+
} else {
|
|
10300
|
+
if (!hookExists(name)) {
|
|
10301
|
+
issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
|
|
10302
|
+
hookHealthy = false;
|
|
10303
|
+
continue;
|
|
10304
|
+
}
|
|
10305
|
+
const hookDir = getHookPath(name);
|
|
10306
|
+
const hookScript = join12(hookDir, "src", "hook.ts");
|
|
10307
|
+
if (!existsSync12(hookScript)) {
|
|
10308
|
+
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
10309
|
+
hookHealthy = false;
|
|
10310
|
+
}
|
|
9074
10311
|
}
|
|
9075
10312
|
if (meta && settingsExist) {
|
|
9076
10313
|
try {
|
|
9077
|
-
const settings = JSON.parse(
|
|
10314
|
+
const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
9078
10315
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
9079
10316
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
9080
10317
|
const match = h.command?.match(/^hooks run ([\w-]+)/);
|
|
@@ -9121,7 +10358,7 @@ Hook Health Check (${scope})
|
|
|
9121
10358
|
}
|
|
9122
10359
|
console.log();
|
|
9123
10360
|
});
|
|
9124
|
-
program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version)").action((hooks, options) => {
|
|
10361
|
+
program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version) and refresh the lock pins").action(async (hooks, options) => {
|
|
9125
10362
|
const scope = resolveScope(options);
|
|
9126
10363
|
const installed = getInstalledHooks(scope);
|
|
9127
10364
|
const toUpdate = hooks.length > 0 ? hooks : installed;
|
|
@@ -9133,6 +10370,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
9133
10370
|
}
|
|
9134
10371
|
return;
|
|
9135
10372
|
}
|
|
10373
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
10374
|
+
const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10375
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
9136
10376
|
const results = [];
|
|
9137
10377
|
for (const name of toUpdate) {
|
|
9138
10378
|
if (!installed.includes(name)) {
|
|
@@ -9140,6 +10380,20 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
9140
10380
|
continue;
|
|
9141
10381
|
}
|
|
9142
10382
|
const result = installHook(name, { scope, overwrite: true });
|
|
10383
|
+
const resolved = resolveHook2(name);
|
|
10384
|
+
if (result.success && resolved) {
|
|
10385
|
+
const hash = await sha256File2(resolved.scriptPath);
|
|
10386
|
+
setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
|
|
10387
|
+
upsertHookRecord2(getDb2(), {
|
|
10388
|
+
name: resolved.name,
|
|
10389
|
+
version: resolved.version,
|
|
10390
|
+
sha256: hash,
|
|
10391
|
+
source_type: resolved.source,
|
|
10392
|
+
last_verified_at: new Date().toISOString()
|
|
10393
|
+
});
|
|
10394
|
+
results.push({ ...result, pinned: { version: resolved.version, sha256: hash } });
|
|
10395
|
+
continue;
|
|
10396
|
+
}
|
|
9143
10397
|
results.push(result);
|
|
9144
10398
|
}
|
|
9145
10399
|
if (options.json) {
|
|
@@ -9154,7 +10408,7 @@ Updating hooks...
|
|
|
9154
10408
|
`));
|
|
9155
10409
|
for (const result of results) {
|
|
9156
10410
|
if (result.success) {
|
|
9157
|
-
console.log(chalk2.green(`\u2713 ${result.hook} updated`));
|
|
10411
|
+
console.log(chalk2.green(`\u2713 ${result.hook} updated`) + (result.pinned ? chalk2.dim(` (pinned ${result.pinned.version})`) : ""));
|
|
9158
10412
|
} else {
|
|
9159
10413
|
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
9160
10414
|
}
|
|
@@ -9172,10 +10426,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
9172
10426
|
return;
|
|
9173
10427
|
}
|
|
9174
10428
|
const hookPath = getHookPath(hook);
|
|
9175
|
-
const readmePath =
|
|
10429
|
+
const readmePath = join12(hookPath, "README.md");
|
|
9176
10430
|
let readme = "";
|
|
9177
|
-
if (
|
|
9178
|
-
readme =
|
|
10431
|
+
if (existsSync12(readmePath)) {
|
|
10432
|
+
readme = readFileSync10(readmePath, "utf-8");
|
|
9179
10433
|
}
|
|
9180
10434
|
if (options.json) {
|
|
9181
10435
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -9357,14 +10611,14 @@ Upgrading @hasna/hooks (${pm})...
|
|
|
9357
10611
|
});
|
|
9358
10612
|
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) => {
|
|
9359
10613
|
const profiles = exportProfiles();
|
|
9360
|
-
const
|
|
10614
|
+
const json2 = JSON.stringify(profiles, null, 2);
|
|
9361
10615
|
if (options.output) {
|
|
9362
|
-
const { writeFileSync:
|
|
9363
|
-
|
|
10616
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
10617
|
+
writeFileSync7(options.output, json2 + `
|
|
9364
10618
|
`);
|
|
9365
10619
|
console.log(chalk2.green(`\u2713 Exported ${profiles.length} profile(s) to ${options.output}`));
|
|
9366
10620
|
} else {
|
|
9367
|
-
console.log(
|
|
10621
|
+
console.log(json2);
|
|
9368
10622
|
}
|
|
9369
10623
|
});
|
|
9370
10624
|
program2.command("profile-import").argument("<file>", "JSON file to import profiles from (use - for stdin)").description("Import agent profiles from a JSON export file").option("-j, --json", "Output result as JSON", false).action(async (file, options) => {
|
|
@@ -9372,9 +10626,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
9372
10626
|
if (file === "-") {
|
|
9373
10627
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
9374
10628
|
} else {
|
|
9375
|
-
const { readFileSync:
|
|
10629
|
+
const { readFileSync: readFileSync11 } = await import("fs");
|
|
9376
10630
|
try {
|
|
9377
|
-
raw =
|
|
10631
|
+
raw = readFileSync11(file, "utf-8");
|
|
9378
10632
|
} catch {
|
|
9379
10633
|
if (options.json) {
|
|
9380
10634
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -9569,7 +10823,7 @@ storageCmd.command("status").description("Show storage sync status").option("-j,
|
|
|
9569
10823
|
Storage Status
|
|
9570
10824
|
`));
|
|
9571
10825
|
console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
|
|
9572
|
-
console.log(`
|
|
10826
|
+
console.log(` Backend: ${status.backend}`);
|
|
9573
10827
|
console.log(` Tables: ${status.tables.join(", ")}`);
|
|
9574
10828
|
console.log(` Sync rows: ${status.sync.length}`);
|
|
9575
10829
|
});
|
|
@@ -9631,6 +10885,126 @@ storageCmd.command("sync").description("Bidirectional storage sync: pull then pu
|
|
|
9631
10885
|
process.exitCode = 1;
|
|
9632
10886
|
}
|
|
9633
10887
|
});
|
|
10888
|
+
program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json", "Output as JSON", false).description("Trust the current script content of a hook (re-pins its sha256)").action(async (hook, options) => {
|
|
10889
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
10890
|
+
const { retrustHook: retrustHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10891
|
+
const resolved = resolveHook2(hook);
|
|
10892
|
+
if (!resolved) {
|
|
10893
|
+
if (options.json)
|
|
10894
|
+
console.log(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
10895
|
+
else
|
|
10896
|
+
console.log(chalk2.red(`Hook '${hook}' not found`));
|
|
10897
|
+
return;
|
|
10898
|
+
}
|
|
10899
|
+
const result = retrustHook2(resolved.name, resolved.scriptPath, resolved.version, resolved.source);
|
|
10900
|
+
if (options.json) {
|
|
10901
|
+
console.log(JSON.stringify({ ok: true, hook: resolved.name, version: resolved.version, sha256: result.actual, source: resolved.source }));
|
|
10902
|
+
return;
|
|
10903
|
+
}
|
|
10904
|
+
console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
|
|
10905
|
+
console.log(chalk2.dim(` sha256 ${result.actual}`));
|
|
10906
|
+
});
|
|
10907
|
+
program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").option("--api-key <key>", "API key for publish (defaults to HASNA_HOOKS_API_KEY / HOOKS_API_KEY)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock)").action(async (options) => {
|
|
10908
|
+
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
10909
|
+
startServeServer2({
|
|
10910
|
+
port: options.port ? parseInt(options.port, 10) : undefined,
|
|
10911
|
+
host: options.host,
|
|
10912
|
+
apiKey: options.apiKey
|
|
10913
|
+
});
|
|
10914
|
+
});
|
|
10915
|
+
program2.command("sync").option("--dry-run", "Print the plan without changing anything", false).option("-j, --json", "Output as JSON", false).description("Sync local hooks with the remote registry (or bundled registry when no API URL is configured)").action(async (options) => {
|
|
10916
|
+
const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
10917
|
+
try {
|
|
10918
|
+
const plan = options.dryRun ? await planSync2() : await syncHooks2();
|
|
10919
|
+
const diff = plan.diff;
|
|
10920
|
+
if (options.json) {
|
|
10921
|
+
console.log(JSON.stringify({ dry_run: plan.dryRun, api_url: plan.apiUrl ?? null, diff }));
|
|
10922
|
+
return;
|
|
10923
|
+
}
|
|
10924
|
+
const source = plan.apiUrl ? plan.apiUrl : "bundled registry";
|
|
10925
|
+
console.log(chalk2.bold(`
|
|
10926
|
+
Hook sync (${source})${plan.dryRun ? " \u2014 dry run" : ""}
|
|
10927
|
+
`));
|
|
10928
|
+
if (diff.added.length > 0) {
|
|
10929
|
+
console.log(chalk2.green(` + ${diff.added.length} to add:`));
|
|
10930
|
+
for (const name of diff.added)
|
|
10931
|
+
console.log(chalk2.green(` ${name}`));
|
|
10932
|
+
}
|
|
10933
|
+
if (diff.updated.length > 0) {
|
|
10934
|
+
console.log(chalk2.yellow(` ~ ${diff.updated.length} to update:`));
|
|
10935
|
+
for (const name of diff.updated)
|
|
10936
|
+
console.log(chalk2.yellow(` ${name}`));
|
|
10937
|
+
}
|
|
10938
|
+
if (diff.skipped.length > 0) {
|
|
10939
|
+
console.log(chalk2.dim(` - ${diff.skipped.length} not in remote lock (left untouched)`));
|
|
10940
|
+
}
|
|
10941
|
+
console.log(chalk2.dim(` = ${diff.unchanged.length} unchanged`));
|
|
10942
|
+
if (plan.dryRun)
|
|
10943
|
+
console.log(chalk2.dim(`
|
|
10944
|
+
Dry run: nothing was written.`));
|
|
10945
|
+
else
|
|
10946
|
+
console.log(chalk2.green(`
|
|
10947
|
+
\u2713 Synced (${diff.added.length} added, ${diff.updated.length} updated, ${diff.unchanged.length} unchanged)`));
|
|
10948
|
+
} catch (error) {
|
|
10949
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10950
|
+
if (options.json) {
|
|
10951
|
+
console.log(JSON.stringify({ error: message, changed: false }));
|
|
10952
|
+
} else {
|
|
10953
|
+
console.error(chalk2.red(`\u2717 ${message}`));
|
|
10954
|
+
console.error(chalk2.dim(" Nothing was changed (fail-closed)."));
|
|
10955
|
+
}
|
|
10956
|
+
process.exitCode = 1;
|
|
10957
|
+
}
|
|
10958
|
+
});
|
|
10959
|
+
program2.command("cf").description("Provision and deploy the hooks registry to Cloudflare").option("--deploy", "Provision D1 + R2 via the Cloudflare API", true).option("--dry-run", "Print the plan without calling the Cloudflare API", false).option("--account-id <id>", "Cloudflare account id (defaults to CF_ACCOUNT_ID env)").option("--database-name <name>", "D1 database name (default hooks-registry)").option("--bucket-name <name>", "R2 bucket name (default hooks-registry-artifacts)").option("--token-file <path>", "Read the API token from a file (defaults to CF_API_TOKEN env)").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
10960
|
+
const token = readToken(options.tokenFile);
|
|
10961
|
+
const accountId = options.accountId ?? process.env.CF_ACCOUNT_ID;
|
|
10962
|
+
if (!options.dryRun) {
|
|
10963
|
+
if (!token) {
|
|
10964
|
+
console.error(chalk2.red("CF_API_TOKEN is not set (or --token-file path is unreadable)."));
|
|
10965
|
+
console.error(chalk2.dim(" Resolve it from the vault, never paste the value:"));
|
|
10966
|
+
console.error(chalk2.dim(" secrets exec <vault-key> --as CF_API_TOKEN -- hooks cf deploy"));
|
|
10967
|
+
process.exit(1);
|
|
10968
|
+
}
|
|
10969
|
+
if (!accountId) {
|
|
10970
|
+
console.error(chalk2.red("CF_ACCOUNT_ID is not set (or pass --account-id)."));
|
|
10971
|
+
process.exit(1);
|
|
10972
|
+
}
|
|
10973
|
+
}
|
|
10974
|
+
const { provisionCloudflareResources: provisionCloudflareResources2 } = await Promise.resolve().then(() => exports_provision);
|
|
10975
|
+
const result = await provisionCloudflareResources2({
|
|
10976
|
+
token: token ?? "",
|
|
10977
|
+
accountId: accountId ?? "",
|
|
10978
|
+
databaseName: options.databaseName ?? "hooks-registry",
|
|
10979
|
+
bucketName: options.bucketName ?? "hooks-registry-artifacts",
|
|
10980
|
+
dryRun: options.dryRun
|
|
10981
|
+
});
|
|
10982
|
+
if (options.json) {
|
|
10983
|
+
console.log(JSON.stringify({ dry_run: options.dryRun, ...result }));
|
|
10984
|
+
return;
|
|
10985
|
+
}
|
|
10986
|
+
if (options.dryRun) {
|
|
10987
|
+
console.log(chalk2.bold(`
|
|
10988
|
+
cf deploy dry run \u2014 would provision:
|
|
10989
|
+
`));
|
|
10990
|
+
} else {
|
|
10991
|
+
console.log(chalk2.bold(`
|
|
10992
|
+
Cloudflare provisioning complete:
|
|
10993
|
+
`));
|
|
10994
|
+
console.log(` ${chalk2.dim("D1:")} ${result.d1Created ? chalk2.green(`created '${options.databaseName ?? "hooks-registry"}'`) : chalk2.dim(`exists (${result.d1DatabaseId})`)}`);
|
|
10995
|
+
console.log(` ${chalk2.dim("R2:")} ${result.r2Created ? chalk2.green(`created '${options.bucketName ?? "hooks-registry-artifacts"}'`) : chalk2.dim("exists")}`);
|
|
10996
|
+
}
|
|
10997
|
+
console.log(`
|
|
10998
|
+
${chalk2.bold("Worker upload (run these with wrangler):")}`);
|
|
10999
|
+
for (const cmd of result.commands) {
|
|
11000
|
+
console.log(` ${cmd}`);
|
|
11001
|
+
}
|
|
11002
|
+
console.log();
|
|
11003
|
+
console.log(chalk2.dim(" Worker upload is not performed by this command \u2014 the worker needs the workerd"));
|
|
11004
|
+
console.log(chalk2.dim(" target, which only wrangler can bundle. Copy src/cf/wrangler.toml.example to"));
|
|
11005
|
+
console.log(chalk2.dim(" wrangler.toml, fill in the D1 database id, then run the commands above."));
|
|
11006
|
+
console.log();
|
|
11007
|
+
});
|
|
9634
11008
|
program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process per agent)", false).option("--sse", "Use legacy SSE transport (port 39427)", false).option("--http", "Use Streamable HTTP transport (explicit; this is also the default)", false).option("-p, --port <port>", "Port for HTTP/SSE transport (defaults to 8847 for HTTP, 39427 for SSE)").description("Start MCP server for AI agent integration (default: shared Streamable HTTP)").action(async (options) => {
|
|
9635
11009
|
if (options.stdio) {
|
|
9636
11010
|
const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|