@hasna/hooks 0.4.1 → 0.6.0
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 +96 -12
- package/bin/index.js +2089 -470
- package/bin/serve.js +5248 -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 +5341 -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 +1521 -3
- package/hooks/codewith-native-common.ts +1627 -37
- 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/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/hooks/pre-bash/README.md +72 -2
- package/hooks/worktree-guard/README.md +9 -2
- 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();
|
|
@@ -2045,6 +2045,16 @@ var init_registry = __esm(() => {
|
|
|
2045
2045
|
matcher: "Bash",
|
|
2046
2046
|
tags: ["codewith", "bash", "secrets", "gitleaks", "risky-ops"]
|
|
2047
2047
|
},
|
|
2048
|
+
{
|
|
2049
|
+
name: "scanoutput",
|
|
2050
|
+
displayName: "Scan Output",
|
|
2051
|
+
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",
|
|
2052
|
+
version: "0.1.0",
|
|
2053
|
+
category: "Security",
|
|
2054
|
+
event: "PostToolUse",
|
|
2055
|
+
matcher: "Bash",
|
|
2056
|
+
tags: ["security", "credentials", "secrets", "detection", "audit"]
|
|
2057
|
+
},
|
|
2048
2058
|
{
|
|
2049
2059
|
name: "phonenotify",
|
|
2050
2060
|
displayName: "Phone Notify",
|
|
@@ -2399,7 +2409,7 @@ var init_registry = __esm(() => {
|
|
|
2399
2409
|
];
|
|
2400
2410
|
});
|
|
2401
2411
|
|
|
2402
|
-
// node_modules/cli-spinners/spinners.json
|
|
2412
|
+
// ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/spinners.json
|
|
2403
2413
|
var require_spinners = __commonJS((exports, module) => {
|
|
2404
2414
|
module.exports = {
|
|
2405
2415
|
dots: {
|
|
@@ -4025,7 +4035,7 @@ var require_spinners = __commonJS((exports, module) => {
|
|
|
4025
4035
|
};
|
|
4026
4036
|
});
|
|
4027
4037
|
|
|
4028
|
-
// node_modules/cli-spinners/index.js
|
|
4038
|
+
// ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/index.js
|
|
4029
4039
|
var require_cli_spinners = __commonJS((exports, module) => {
|
|
4030
4040
|
var spinners = Object.assign({}, require_spinners());
|
|
4031
4041
|
var spinnersList = Object.keys(spinners);
|
|
@@ -4039,10 +4049,320 @@ var require_cli_spinners = __commonJS((exports, module) => {
|
|
|
4039
4049
|
module.exports = spinners;
|
|
4040
4050
|
});
|
|
4041
4051
|
|
|
4042
|
-
// src/
|
|
4052
|
+
// src/config.ts
|
|
4053
|
+
var exports_config = {};
|
|
4054
|
+
__export(exports_config, {
|
|
4055
|
+
writeConfig: () => writeConfig,
|
|
4056
|
+
resolveApiUrl: () => resolveApiUrl,
|
|
4057
|
+
resolveApiKey: () => resolveApiKey,
|
|
4058
|
+
readConfig: () => readConfig,
|
|
4059
|
+
getLockPath: () => getLockPath,
|
|
4060
|
+
getHooksDataDir: () => getHooksDataDir,
|
|
4061
|
+
getCustomHooksDir: () => getCustomHooksDir,
|
|
4062
|
+
getConfigPath: () => getConfigPath
|
|
4063
|
+
});
|
|
4043
4064
|
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4044
|
-
import { join as join2
|
|
4065
|
+
import { join as join2 } from "path";
|
|
4045
4066
|
import { homedir as homedir2 } from "os";
|
|
4067
|
+
function getHooksDataDir() {
|
|
4068
|
+
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
4069
|
+
if (explicit)
|
|
4070
|
+
return explicit;
|
|
4071
|
+
return join2(homedir2(), ".hasna", "hooks");
|
|
4072
|
+
}
|
|
4073
|
+
function getCustomHooksDir() {
|
|
4074
|
+
return join2(getHooksDataDir(), "hooks");
|
|
4075
|
+
}
|
|
4076
|
+
function getLockPath() {
|
|
4077
|
+
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4078
|
+
if (explicit)
|
|
4079
|
+
return explicit;
|
|
4080
|
+
return join2(getHooksDataDir(), "hooks.lock");
|
|
4081
|
+
}
|
|
4082
|
+
function getConfigPath() {
|
|
4083
|
+
const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
|
|
4084
|
+
if (explicit)
|
|
4085
|
+
return explicit;
|
|
4086
|
+
return join2(getHooksDataDir(), "config.json");
|
|
4087
|
+
}
|
|
4088
|
+
function readConfig() {
|
|
4089
|
+
try {
|
|
4090
|
+
const path = getConfigPath();
|
|
4091
|
+
if (!existsSync2(path))
|
|
4092
|
+
return {};
|
|
4093
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
4094
|
+
} catch {
|
|
4095
|
+
return {};
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
function writeConfig(config) {
|
|
4099
|
+
const path = getConfigPath();
|
|
4100
|
+
mkdirSync(join2(path, ".."), { recursive: true });
|
|
4101
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
4102
|
+
`, "utf-8");
|
|
4103
|
+
return path;
|
|
4104
|
+
}
|
|
4105
|
+
function resolveApiUrl() {
|
|
4106
|
+
const envUrl = process.env.HASNA_HOOKS_API_URL ?? process.env.HOOKS_API_URL ?? process.env.HASNA_HOOKS_REGISTRY_URL ?? process.env.HOOKS_REGISTRY_URL;
|
|
4107
|
+
if (envUrl)
|
|
4108
|
+
return envUrl.replace(/\/+$/, "");
|
|
4109
|
+
const cfg = readConfig();
|
|
4110
|
+
return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
|
|
4111
|
+
}
|
|
4112
|
+
function resolveApiKey(flagKey) {
|
|
4113
|
+
if (flagKey && flagKey.length > 0)
|
|
4114
|
+
return flagKey;
|
|
4115
|
+
return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
|
|
4116
|
+
}
|
|
4117
|
+
var init_config = () => {};
|
|
4118
|
+
|
|
4119
|
+
// src/lib/manifest.ts
|
|
4120
|
+
var exports_manifest = {};
|
|
4121
|
+
__export(exports_manifest, {
|
|
4122
|
+
writeCustomHook: () => writeCustomHook,
|
|
4123
|
+
shortManifestName: () => shortManifestName,
|
|
4124
|
+
resolveScript: () => resolveScript,
|
|
4125
|
+
readCustomManifest: () => readCustomManifest,
|
|
4126
|
+
parseManifest: () => parseManifest,
|
|
4127
|
+
normalizeManifestName: () => normalizeManifestName,
|
|
4128
|
+
manifestSchema: () => manifestSchema,
|
|
4129
|
+
listCustomHooks: () => listCustomHooks,
|
|
4130
|
+
customHookDir: () => customHookDir,
|
|
4131
|
+
assertContained: () => assertContained,
|
|
4132
|
+
ScriptContainmentError: () => ScriptContainmentError,
|
|
4133
|
+
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4134
|
+
});
|
|
4135
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4136
|
+
import { basename, dirname, isAbsolute, join as join3, normalize, resolve, sep } from "path";
|
|
4137
|
+
import { z } from "zod";
|
|
4138
|
+
function parseManifest(json) {
|
|
4139
|
+
const parsed = JSON.parse(json);
|
|
4140
|
+
return manifestSchema.parse(parsed);
|
|
4141
|
+
}
|
|
4142
|
+
function normalizeManifestName(name) {
|
|
4143
|
+
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4144
|
+
}
|
|
4145
|
+
function shortManifestName(name) {
|
|
4146
|
+
return name.startsWith("hook-") ? name.slice("hook-".length) : name;
|
|
4147
|
+
}
|
|
4148
|
+
function isWithin(child, parent) {
|
|
4149
|
+
return child === parent || child.startsWith(parent + sep);
|
|
4150
|
+
}
|
|
4151
|
+
function assertContainedLexical(target, root, raw) {
|
|
4152
|
+
const lexical = resolve(target);
|
|
4153
|
+
const rootLexical = resolve(root);
|
|
4154
|
+
if (!isWithin(lexical, rootLexical)) {
|
|
4155
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4156
|
+
}
|
|
4157
|
+
return lexical;
|
|
4158
|
+
}
|
|
4159
|
+
function realpathNearest(path) {
|
|
4160
|
+
const tail = [];
|
|
4161
|
+
let cur = path;
|
|
4162
|
+
while (!existsSync3(cur)) {
|
|
4163
|
+
const parent = dirname(cur);
|
|
4164
|
+
if (parent === cur)
|
|
4165
|
+
break;
|
|
4166
|
+
tail.unshift(basename(cur));
|
|
4167
|
+
cur = parent;
|
|
4168
|
+
}
|
|
4169
|
+
return join3(realpathSync(cur), ...tail);
|
|
4170
|
+
}
|
|
4171
|
+
function assertContainedReal(root, target, raw) {
|
|
4172
|
+
const rootReal = realpathSync(root);
|
|
4173
|
+
const targetReal = realpathNearest(target);
|
|
4174
|
+
if (!isWithin(targetReal, rootReal)) {
|
|
4175
|
+
throw new ScriptContainmentError(target, root, raw);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
function assertContained(target, root, raw) {
|
|
4179
|
+
const lexical = assertContainedLexical(target, root, raw);
|
|
4180
|
+
assertContainedReal(root, lexical, raw);
|
|
4181
|
+
return lexical;
|
|
4182
|
+
}
|
|
4183
|
+
function resolveScript(manifest, manifestDir) {
|
|
4184
|
+
if (manifest.script.includes(`
|
|
4185
|
+
`)) {
|
|
4186
|
+
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4187
|
+
return { path: `script${ext}`, content: manifest.script };
|
|
4188
|
+
}
|
|
4189
|
+
const rel = normalize(manifest.script);
|
|
4190
|
+
if (isAbsolute(rel)) {
|
|
4191
|
+
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4192
|
+
}
|
|
4193
|
+
const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
|
|
4194
|
+
if (!existsSync3(resolved)) {
|
|
4195
|
+
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4196
|
+
}
|
|
4197
|
+
return { path: rel, content: readFileSync2(resolved, "utf-8") };
|
|
4198
|
+
}
|
|
4199
|
+
function customHookDir(name) {
|
|
4200
|
+
return join3(getCustomHooksDir(), shortManifestName(name));
|
|
4201
|
+
}
|
|
4202
|
+
function readCustomManifest(name) {
|
|
4203
|
+
const dir = customHookDir(name);
|
|
4204
|
+
const manifestPath = join3(dir, "manifest.json");
|
|
4205
|
+
if (!existsSync3(manifestPath))
|
|
4206
|
+
return;
|
|
4207
|
+
try {
|
|
4208
|
+
const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
|
|
4209
|
+
const script = resolveScript(manifest, dir);
|
|
4210
|
+
return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline: manifest.script.includes(`
|
|
4211
|
+
`) };
|
|
4212
|
+
} catch (err) {
|
|
4213
|
+
if (err instanceof ScriptContainmentError)
|
|
4214
|
+
throw err;
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
function listCustomHooks() {
|
|
4219
|
+
const dir = getCustomHooksDir();
|
|
4220
|
+
if (!existsSync3(dir))
|
|
4221
|
+
return [];
|
|
4222
|
+
const results = [];
|
|
4223
|
+
for (const entry of readdirSafe(dir)) {
|
|
4224
|
+
const parsed = readCustomManifest(entry);
|
|
4225
|
+
if (parsed)
|
|
4226
|
+
results.push(parsed);
|
|
4227
|
+
}
|
|
4228
|
+
return results.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
|
4229
|
+
}
|
|
4230
|
+
function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
4231
|
+
const dir = customHookDir(name);
|
|
4232
|
+
if (isAbsolute(scriptRelPath)) {
|
|
4233
|
+
throw new ScriptContainmentError(scriptRelPath, dir);
|
|
4234
|
+
}
|
|
4235
|
+
const scriptPath = scriptRelPath.includes("/") ? join3(dir, scriptRelPath) : join3(dir, scriptRelPath);
|
|
4236
|
+
assertContainedLexical(scriptPath, dir, scriptRelPath);
|
|
4237
|
+
mkdirSync2(dir, { recursive: true });
|
|
4238
|
+
assertContainedReal(dir, scriptPath, scriptRelPath);
|
|
4239
|
+
const scriptDir = dirname(scriptPath);
|
|
4240
|
+
if (scriptDir !== dir)
|
|
4241
|
+
mkdirSync2(scriptDir, { recursive: true });
|
|
4242
|
+
writeFileSync2(scriptPath, scriptContent, "utf-8");
|
|
4243
|
+
writeFileSync2(join3(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
4244
|
+
`, "utf-8");
|
|
4245
|
+
return { dir, scriptPath };
|
|
4246
|
+
}
|
|
4247
|
+
function readdirSafe(dir) {
|
|
4248
|
+
try {
|
|
4249
|
+
return readdirSync(dir);
|
|
4250
|
+
} catch {
|
|
4251
|
+
return [];
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
var HOOK_NAME_RE, manifestSchema, ScriptContainmentError;
|
|
4255
|
+
var init_manifest = __esm(() => {
|
|
4256
|
+
init_config();
|
|
4257
|
+
HOOK_NAME_RE = /^[\w-]+$/;
|
|
4258
|
+
manifestSchema = z.object({
|
|
4259
|
+
name: z.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4260
|
+
version: z.string().regex(/^\d+\.\d+\.\d+/, "version must be semver (e.g. 1.2.3)"),
|
|
4261
|
+
description: z.string().optional(),
|
|
4262
|
+
events: z.array(z.string()).min(1, "at least one event is required"),
|
|
4263
|
+
script: z.string().min(1, "script must name a relative path or inline content"),
|
|
4264
|
+
args: z.array(z.string()).optional(),
|
|
4265
|
+
timeout_ms: z.number().int().positive().optional()
|
|
4266
|
+
});
|
|
4267
|
+
ScriptContainmentError = class ScriptContainmentError extends Error {
|
|
4268
|
+
constructor(path, hookDir, raw) {
|
|
4269
|
+
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}'`);
|
|
4270
|
+
this.name = "ScriptContainmentError";
|
|
4271
|
+
}
|
|
4272
|
+
};
|
|
4273
|
+
});
|
|
4274
|
+
|
|
4275
|
+
// src/lib/resolve.ts
|
|
4276
|
+
var exports_resolve = {};
|
|
4277
|
+
__export(exports_resolve, {
|
|
4278
|
+
resolveScriptPath: () => resolveScriptPath,
|
|
4279
|
+
resolveHookMeta: () => resolveHookMeta,
|
|
4280
|
+
resolveHookDir: () => resolveHookDir,
|
|
4281
|
+
resolveHook: () => resolveHook
|
|
4282
|
+
});
|
|
4283
|
+
import { join as join4 } from "path";
|
|
4284
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4285
|
+
function resolveHookDir(name) {
|
|
4286
|
+
const custom = readCustomManifest(name);
|
|
4287
|
+
if (custom)
|
|
4288
|
+
return customHookDir(name);
|
|
4289
|
+
const meta = getHook(shortManifestName(name));
|
|
4290
|
+
if (!meta)
|
|
4291
|
+
return;
|
|
4292
|
+
const short = shortManifestName(name);
|
|
4293
|
+
const bundledBase = join4(__dirname, "..", "..", "hooks");
|
|
4294
|
+
const direct = join4(bundledBase, short);
|
|
4295
|
+
if (existsSync4(direct))
|
|
4296
|
+
return direct;
|
|
4297
|
+
const prefixed = join4(bundledBase, `hook-${short}`);
|
|
4298
|
+
return existsSync4(prefixed) ? prefixed : undefined;
|
|
4299
|
+
}
|
|
4300
|
+
function resolveScriptPath(name) {
|
|
4301
|
+
const custom = readCustomManifest(name);
|
|
4302
|
+
if (custom)
|
|
4303
|
+
return custom.scriptPath;
|
|
4304
|
+
const dir = resolveHookDir(name);
|
|
4305
|
+
if (!dir)
|
|
4306
|
+
return;
|
|
4307
|
+
const script = join4(dir, "src", "hook.ts");
|
|
4308
|
+
return existsSync4(script) ? script : undefined;
|
|
4309
|
+
}
|
|
4310
|
+
function resolveHookMeta(name) {
|
|
4311
|
+
const custom = readCustomManifest(name);
|
|
4312
|
+
if (custom) {
|
|
4313
|
+
const { manifest } = custom;
|
|
4314
|
+
return {
|
|
4315
|
+
name: shortManifestName(manifest.name),
|
|
4316
|
+
displayName: manifest.description ? manifest.name : manifest.name,
|
|
4317
|
+
description: manifest.description ?? "Custom hook",
|
|
4318
|
+
version: manifest.version,
|
|
4319
|
+
category: "Workflow Automation",
|
|
4320
|
+
event: manifest.events[0] ?? "PostToolUse",
|
|
4321
|
+
events: manifest.events,
|
|
4322
|
+
matcher: "",
|
|
4323
|
+
tags: ["custom"]
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
return getHook(shortManifestName(name));
|
|
4327
|
+
}
|
|
4328
|
+
function resolveHook(name) {
|
|
4329
|
+
const custom = readCustomManifest(name);
|
|
4330
|
+
if (custom) {
|
|
4331
|
+
const meta2 = resolveHookMeta(name);
|
|
4332
|
+
return {
|
|
4333
|
+
name: shortManifestName(custom.manifest.name),
|
|
4334
|
+
source: "custom",
|
|
4335
|
+
version: custom.manifest.version,
|
|
4336
|
+
events: custom.manifest.events,
|
|
4337
|
+
description: custom.manifest.description ?? "Custom hook",
|
|
4338
|
+
scriptPath: custom.scriptPath,
|
|
4339
|
+
meta: meta2
|
|
4340
|
+
};
|
|
4341
|
+
}
|
|
4342
|
+
const meta = getHook(shortManifestName(name));
|
|
4343
|
+
const scriptPath = resolveScriptPath(name);
|
|
4344
|
+
if (!meta || !scriptPath)
|
|
4345
|
+
return;
|
|
4346
|
+
return {
|
|
4347
|
+
name: shortManifestName(name),
|
|
4348
|
+
source: "bundled",
|
|
4349
|
+
version: meta.version,
|
|
4350
|
+
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
4351
|
+
description: meta.description,
|
|
4352
|
+
scriptPath,
|
|
4353
|
+
meta
|
|
4354
|
+
};
|
|
4355
|
+
}
|
|
4356
|
+
var __dirname = "/home/hasna/.hasna/repos/worktrees/apps/1ec6ba31-cdbc-42ea-8b1e-705e4198abbf/apps/hooks/src/lib";
|
|
4357
|
+
var init_resolve = __esm(() => {
|
|
4358
|
+
init_registry();
|
|
4359
|
+
init_manifest();
|
|
4360
|
+
});
|
|
4361
|
+
|
|
4362
|
+
// src/lib/installer.ts
|
|
4363
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4364
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
4365
|
+
import { homedir as homedir3 } from "os";
|
|
4046
4366
|
import { fileURLToPath } from "url";
|
|
4047
4367
|
function normalizeHookName(name) {
|
|
4048
4368
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
@@ -4063,33 +4383,43 @@ function getTargetSettingsDir(target) {
|
|
|
4063
4383
|
return ".gemini";
|
|
4064
4384
|
return ".claude";
|
|
4065
4385
|
}
|
|
4386
|
+
function getGlobalSettingsPathOverride(target) {
|
|
4387
|
+
if (target === "claude")
|
|
4388
|
+
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
4389
|
+
if (target === "gemini")
|
|
4390
|
+
return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4066
4393
|
function getSettingsPath(scope = "global", target = "claude", codewithConfigPath) {
|
|
4067
4394
|
if (target === "codewith" && codewithConfigPath)
|
|
4068
4395
|
return codewithConfigPath;
|
|
4069
4396
|
if (target === "codewith" && process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH) {
|
|
4070
4397
|
return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH;
|
|
4071
4398
|
}
|
|
4399
|
+
const globalOverride = scope === "global" ? getGlobalSettingsPathOverride(target) : undefined;
|
|
4400
|
+
if (globalOverride)
|
|
4401
|
+
return globalOverride;
|
|
4072
4402
|
const dir = getTargetSettingsDir(target);
|
|
4073
4403
|
if (scope === "project") {
|
|
4074
|
-
return target === "codewith" ?
|
|
4404
|
+
return target === "codewith" ? join5(process.cwd(), dir, "config.toml") : join5(process.cwd(), dir, "settings.json");
|
|
4075
4405
|
}
|
|
4076
|
-
return target === "codewith" ?
|
|
4406
|
+
return target === "codewith" ? join5(homedir3(), dir, "config.toml") : join5(homedir3(), dir, "settings.json");
|
|
4077
4407
|
}
|
|
4078
4408
|
function getHookPath(name) {
|
|
4079
4409
|
const shortName = shortHookName(name);
|
|
4080
|
-
const
|
|
4081
|
-
if (
|
|
4082
|
-
return
|
|
4083
|
-
return
|
|
4410
|
+
const resolved = resolveHookDir(shortName);
|
|
4411
|
+
if (resolved)
|
|
4412
|
+
return resolved;
|
|
4413
|
+
return join5(HOOKS_DIR, normalizeHookName(shortName));
|
|
4084
4414
|
}
|
|
4085
4415
|
function hookExists(name) {
|
|
4086
|
-
return
|
|
4416
|
+
return existsSync5(getHookPath(name));
|
|
4087
4417
|
}
|
|
4088
4418
|
function readSettings(scope = "global", target = "claude") {
|
|
4089
4419
|
const path = getSettingsPath(scope, target);
|
|
4090
4420
|
try {
|
|
4091
|
-
if (
|
|
4092
|
-
return JSON.parse(
|
|
4421
|
+
if (existsSync5(path)) {
|
|
4422
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
4093
4423
|
}
|
|
4094
4424
|
} catch (error) {
|
|
4095
4425
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -4098,11 +4428,11 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
4098
4428
|
}
|
|
4099
4429
|
function writeSettings(settings, scope = "global", target = "claude") {
|
|
4100
4430
|
const path = getSettingsPath(scope, target);
|
|
4101
|
-
const dir =
|
|
4102
|
-
if (!
|
|
4103
|
-
|
|
4431
|
+
const dir = dirname2(path);
|
|
4432
|
+
if (!existsSync5(dir)) {
|
|
4433
|
+
mkdirSync3(dir, { recursive: true });
|
|
4104
4434
|
}
|
|
4105
|
-
|
|
4435
|
+
writeFileSync3(path, JSON.stringify(settings, null, 2) + `
|
|
4106
4436
|
`);
|
|
4107
4437
|
}
|
|
4108
4438
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -4160,7 +4490,7 @@ function codewithStatusMessage(name) {
|
|
|
4160
4490
|
}
|
|
4161
4491
|
function buildCodewithTomlFragment(name, profile) {
|
|
4162
4492
|
const shortName = shortHookName(name);
|
|
4163
|
-
const meta =
|
|
4493
|
+
const meta = resolveHookMeta(shortName);
|
|
4164
4494
|
if (!meta)
|
|
4165
4495
|
throw new Error(`Hook '${shortName}' not found`);
|
|
4166
4496
|
const command = profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`;
|
|
@@ -4188,25 +4518,25 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
4188
4518
|
function readCodewithConfig(scope, configPath) {
|
|
4189
4519
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
4190
4520
|
try {
|
|
4191
|
-
return
|
|
4521
|
+
return existsSync5(path) ? readFileSync3(path, "utf-8") : "";
|
|
4192
4522
|
} catch {
|
|
4193
4523
|
return "";
|
|
4194
4524
|
}
|
|
4195
4525
|
}
|
|
4196
4526
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
4197
4527
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
4198
|
-
const dir =
|
|
4199
|
-
if (!
|
|
4200
|
-
|
|
4201
|
-
const existing =
|
|
4202
|
-
const
|
|
4528
|
+
const dir = dirname2(path);
|
|
4529
|
+
if (!existsSync5(dir))
|
|
4530
|
+
mkdirSync3(dir, { recursive: true });
|
|
4531
|
+
const existing = existsSync5(path) ? readFileSync3(path, "utf-8") : "";
|
|
4532
|
+
const sep2 = existing.trim() ? `
|
|
4203
4533
|
|
|
4204
4534
|
` : "";
|
|
4205
|
-
|
|
4535
|
+
writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
4206
4536
|
return path;
|
|
4207
4537
|
}
|
|
4208
4538
|
function detectConflict(name, scope, target) {
|
|
4209
|
-
const meta =
|
|
4539
|
+
const meta = resolveHookMeta(name);
|
|
4210
4540
|
if (!meta || !meta.matcher)
|
|
4211
4541
|
return;
|
|
4212
4542
|
const events = new Set(getHookEvents(meta));
|
|
@@ -4302,7 +4632,7 @@ function installHook(name, options = {}) {
|
|
|
4302
4632
|
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
|
|
4303
4633
|
if (target === "all") {
|
|
4304
4634
|
const shortName = shortHookName(name);
|
|
4305
|
-
const meta =
|
|
4635
|
+
const meta = resolveHookMeta(shortName);
|
|
4306
4636
|
if (meta) {
|
|
4307
4637
|
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
4308
4638
|
if (unsupportedTargets.length > 0) {
|
|
@@ -4335,7 +4665,7 @@ function installHook(name, options = {}) {
|
|
|
4335
4665
|
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath);
|
|
4336
4666
|
}
|
|
4337
4667
|
function registerHook(name, scope = "global", target = "claude", profile) {
|
|
4338
|
-
const meta =
|
|
4668
|
+
const meta = resolveHookMeta(name);
|
|
4339
4669
|
if (!meta)
|
|
4340
4670
|
return;
|
|
4341
4671
|
const eventKeys = getHookEvents(meta).map((event) => {
|
|
@@ -4378,7 +4708,7 @@ function removeHookFromAllEvents(settings, name) {
|
|
|
4378
4708
|
}
|
|
4379
4709
|
}
|
|
4380
4710
|
function unregisterHook(name, scope = "global", target = "claude") {
|
|
4381
|
-
const meta =
|
|
4711
|
+
const meta = resolveHookMeta(name);
|
|
4382
4712
|
if (!meta)
|
|
4383
4713
|
return;
|
|
4384
4714
|
const settings = readSettings(scope, target);
|
|
@@ -4445,8 +4775,9 @@ function removeHookForTarget(name, scope, target) {
|
|
|
4445
4775
|
var __dirname2, HOOKS_DIR, EVENT_MAP, getInstalledHooks;
|
|
4446
4776
|
var init_installer = __esm(() => {
|
|
4447
4777
|
init_registry();
|
|
4448
|
-
|
|
4449
|
-
|
|
4778
|
+
init_resolve();
|
|
4779
|
+
__dirname2 = dirname2(fileURLToPath(import.meta.url));
|
|
4780
|
+
HOOKS_DIR = existsSync5(join5(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join5(__dirname2, "..", "..", "hooks") : join5(__dirname2, "..", "hooks");
|
|
4450
4781
|
EVENT_MAP = {
|
|
4451
4782
|
claude: {
|
|
4452
4783
|
PreToolUse: "PreToolUse",
|
|
@@ -4483,25 +4814,25 @@ var init_installer = __esm(() => {
|
|
|
4483
4814
|
});
|
|
4484
4815
|
|
|
4485
4816
|
// src/lib/profiles.ts
|
|
4486
|
-
import { existsSync as
|
|
4487
|
-
import { join as
|
|
4488
|
-
import { homedir as
|
|
4817
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, readdirSync as readdirSync2, rmSync, cpSync } from "fs";
|
|
4818
|
+
import { join as join6 } from "path";
|
|
4819
|
+
import { homedir as homedir4 } from "os";
|
|
4489
4820
|
function resolveProfilesDir() {
|
|
4490
|
-
const newDir =
|
|
4491
|
-
const oldDir =
|
|
4492
|
-
if (!
|
|
4493
|
-
|
|
4821
|
+
const newDir = join6(homedir4(), ".hasna", "hooks", "profiles");
|
|
4822
|
+
const oldDir = join6(homedir4(), ".hooks", "profiles");
|
|
4823
|
+
if (!existsSync6(newDir) && existsSync6(oldDir)) {
|
|
4824
|
+
mkdirSync4(join6(homedir4(), ".hasna", "hooks"), { recursive: true });
|
|
4494
4825
|
cpSync(oldDir, newDir, { recursive: true });
|
|
4495
4826
|
}
|
|
4496
4827
|
return newDir;
|
|
4497
4828
|
}
|
|
4498
4829
|
function ensureProfilesDir() {
|
|
4499
|
-
if (!
|
|
4500
|
-
|
|
4830
|
+
if (!existsSync6(PROFILES_DIR)) {
|
|
4831
|
+
mkdirSync4(PROFILES_DIR, { recursive: true });
|
|
4501
4832
|
}
|
|
4502
4833
|
}
|
|
4503
4834
|
function profilePath(id) {
|
|
4504
|
-
return
|
|
4835
|
+
return join6(PROFILES_DIR, `${id}.json`);
|
|
4505
4836
|
}
|
|
4506
4837
|
function shortUuid() {
|
|
4507
4838
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -4520,29 +4851,29 @@ function createProfile(input) {
|
|
|
4520
4851
|
if (input.name) {
|
|
4521
4852
|
profile.name = input.name;
|
|
4522
4853
|
}
|
|
4523
|
-
|
|
4854
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4524
4855
|
`);
|
|
4525
4856
|
return profile;
|
|
4526
4857
|
}
|
|
4527
4858
|
function getProfile(id) {
|
|
4528
4859
|
const path = profilePath(id);
|
|
4529
4860
|
try {
|
|
4530
|
-
if (!
|
|
4861
|
+
if (!existsSync6(path))
|
|
4531
4862
|
return null;
|
|
4532
|
-
return JSON.parse(
|
|
4863
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
4533
4864
|
} catch {
|
|
4534
4865
|
return null;
|
|
4535
4866
|
}
|
|
4536
4867
|
}
|
|
4537
4868
|
function listProfiles() {
|
|
4538
|
-
if (!
|
|
4869
|
+
if (!existsSync6(PROFILES_DIR))
|
|
4539
4870
|
return [];
|
|
4540
4871
|
try {
|
|
4541
|
-
const files =
|
|
4872
|
+
const files = readdirSync2(PROFILES_DIR).filter((f) => f.endsWith(".json"));
|
|
4542
4873
|
const profiles = [];
|
|
4543
4874
|
for (const file of files) {
|
|
4544
4875
|
try {
|
|
4545
|
-
const content =
|
|
4876
|
+
const content = readFileSync4(join6(PROFILES_DIR, file), "utf-8");
|
|
4546
4877
|
profiles.push(JSON.parse(content));
|
|
4547
4878
|
} catch {}
|
|
4548
4879
|
}
|
|
@@ -4556,7 +4887,7 @@ function touchProfile(id) {
|
|
|
4556
4887
|
if (!profile)
|
|
4557
4888
|
return;
|
|
4558
4889
|
profile.last_seen_at = new Date().toISOString();
|
|
4559
|
-
|
|
4890
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
4560
4891
|
`);
|
|
4561
4892
|
}
|
|
4562
4893
|
function exportProfiles() {
|
|
@@ -4572,11 +4903,11 @@ function importProfiles(profiles) {
|
|
|
4572
4903
|
continue;
|
|
4573
4904
|
}
|
|
4574
4905
|
const path = profilePath(profile.agent_id);
|
|
4575
|
-
if (
|
|
4906
|
+
if (existsSync6(path)) {
|
|
4576
4907
|
skipped++;
|
|
4577
4908
|
continue;
|
|
4578
4909
|
}
|
|
4579
|
-
|
|
4910
|
+
writeFileSync4(path, JSON.stringify(profile, null, 2) + `
|
|
4580
4911
|
`);
|
|
4581
4912
|
imported++;
|
|
4582
4913
|
}
|
|
@@ -4683,6 +5014,32 @@ var init_003_user_prompt_submit_event = __esm(() => {
|
|
|
4683
5014
|
init_schema();
|
|
4684
5015
|
});
|
|
4685
5016
|
|
|
5017
|
+
// src/db/migrations/004_hooks_table.ts
|
|
5018
|
+
function up4(db) {
|
|
5019
|
+
db.exec(CREATE_HOOKS_TABLE);
|
|
5020
|
+
for (const idx of CREATE_HOOKS_INDEXES) {
|
|
5021
|
+
db.exec(idx);
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
var CREATE_HOOKS_TABLE = `
|
|
5025
|
+
CREATE TABLE IF NOT EXISTS hooks (
|
|
5026
|
+
id TEXT PRIMARY KEY,
|
|
5027
|
+
name TEXT NOT NULL,
|
|
5028
|
+
version TEXT NOT NULL,
|
|
5029
|
+
sha256 TEXT NOT NULL,
|
|
5030
|
+
source_type TEXT NOT NULL,
|
|
5031
|
+
source_ref TEXT,
|
|
5032
|
+
installed_at TEXT NOT NULL,
|
|
5033
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
5034
|
+
last_verified_at TEXT
|
|
5035
|
+
)
|
|
5036
|
+
`, CREATE_HOOKS_INDEXES;
|
|
5037
|
+
var init_004_hooks_table = __esm(() => {
|
|
5038
|
+
CREATE_HOOKS_INDEXES = [
|
|
5039
|
+
`CREATE INDEX IF NOT EXISTS idx_hooks_name ON hooks (name)`
|
|
5040
|
+
];
|
|
5041
|
+
});
|
|
5042
|
+
|
|
4686
5043
|
// src/db/migrations/index.ts
|
|
4687
5044
|
function ensureMigrationsTable(db) {
|
|
4688
5045
|
db.exec(`
|
|
@@ -4714,17 +5071,19 @@ var init_migrations = __esm(() => {
|
|
|
4714
5071
|
init_001_initial();
|
|
4715
5072
|
init_002_session_events();
|
|
4716
5073
|
init_003_user_prompt_submit_event();
|
|
5074
|
+
init_004_hooks_table();
|
|
4717
5075
|
MIGRATIONS = [
|
|
4718
5076
|
{ version: "001_initial", up },
|
|
4719
5077
|
{ version: "002_session_events", up: up2 },
|
|
4720
|
-
{ version: "003_user_prompt_submit_event", up: up3 }
|
|
5078
|
+
{ version: "003_user_prompt_submit_event", up: up3 },
|
|
5079
|
+
{ version: "004_hooks_table", up: up4 }
|
|
4721
5080
|
];
|
|
4722
5081
|
});
|
|
4723
5082
|
|
|
4724
5083
|
// src/db/legacy-import.ts
|
|
4725
|
-
import { existsSync as
|
|
4726
|
-
import { join as
|
|
4727
|
-
import { homedir as
|
|
5084
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
5085
|
+
import { join as join7 } from "path";
|
|
5086
|
+
import { homedir as homedir5 } from "os";
|
|
4728
5087
|
function ensureMetaTable(db) {
|
|
4729
5088
|
db.exec(`
|
|
4730
5089
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -4747,7 +5106,7 @@ function nanoid() {
|
|
|
4747
5106
|
function importJsonlFile(db, filePath) {
|
|
4748
5107
|
let count = 0;
|
|
4749
5108
|
try {
|
|
4750
|
-
const lines =
|
|
5109
|
+
const lines = readFileSync5(filePath, "utf-8").split(`
|
|
4751
5110
|
`).filter(Boolean);
|
|
4752
5111
|
for (const line of lines) {
|
|
4753
5112
|
try {
|
|
@@ -4773,7 +5132,7 @@ function importJsonlFile(db, filePath) {
|
|
|
4773
5132
|
function importErrorsLog(db, filePath) {
|
|
4774
5133
|
let count = 0;
|
|
4775
5134
|
try {
|
|
4776
|
-
const lines =
|
|
5135
|
+
const lines = readFileSync5(filePath, "utf-8").split(`
|
|
4777
5136
|
`).filter(Boolean);
|
|
4778
5137
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
4779
5138
|
for (const line of lines) {
|
|
@@ -4798,25 +5157,25 @@ function importErrorsLog(db, filePath) {
|
|
|
4798
5157
|
} catch {}
|
|
4799
5158
|
return count;
|
|
4800
5159
|
}
|
|
4801
|
-
function runLegacyImport(db) {
|
|
5160
|
+
function runLegacyImport(db, homeDir = homedir5()) {
|
|
4802
5161
|
try {
|
|
4803
5162
|
if (isAlreadyDone(db))
|
|
4804
5163
|
return;
|
|
4805
5164
|
let total = 0;
|
|
4806
|
-
const claudeProjectsDir =
|
|
4807
|
-
if (
|
|
5165
|
+
const claudeProjectsDir = join7(homeDir, ".claude", "projects");
|
|
5166
|
+
if (existsSync7(claudeProjectsDir)) {
|
|
4808
5167
|
try {
|
|
4809
|
-
const projectDirs =
|
|
5168
|
+
const projectDirs = readdirSync3(claudeProjectsDir);
|
|
4810
5169
|
for (const dir of projectDirs) {
|
|
4811
|
-
const projectDir =
|
|
5170
|
+
const projectDir = join7(claudeProjectsDir, dir);
|
|
4812
5171
|
try {
|
|
4813
|
-
const files =
|
|
5172
|
+
const files = readdirSync3(projectDir);
|
|
4814
5173
|
for (const file of files) {
|
|
4815
5174
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
4816
|
-
total += importJsonlFile(db,
|
|
5175
|
+
total += importJsonlFile(db, join7(projectDir, file));
|
|
4817
5176
|
}
|
|
4818
5177
|
if (file === "errors.log") {
|
|
4819
|
-
total += importErrorsLog(db,
|
|
5178
|
+
total += importErrorsLog(db, join7(projectDir, file));
|
|
4820
5179
|
}
|
|
4821
5180
|
}
|
|
4822
5181
|
} catch {}
|
|
@@ -4859,17 +5218,17 @@ __export(exports_db, {
|
|
|
4859
5218
|
closeDb: () => closeDb
|
|
4860
5219
|
});
|
|
4861
5220
|
import { Database } from "bun:sqlite";
|
|
4862
|
-
import { existsSync as
|
|
4863
|
-
import { join as
|
|
4864
|
-
import { homedir as
|
|
5221
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, cpSync as cpSync2 } from "fs";
|
|
5222
|
+
import { join as join8 } from "path";
|
|
5223
|
+
import { homedir as homedir6 } from "os";
|
|
4865
5224
|
function resolveDataDir() {
|
|
4866
5225
|
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
4867
5226
|
if (explicit)
|
|
4868
5227
|
return explicit;
|
|
4869
|
-
const newDir =
|
|
4870
|
-
const oldDir =
|
|
4871
|
-
if (!
|
|
4872
|
-
|
|
5228
|
+
const newDir = join8(homedir6(), ".hasna", "hooks");
|
|
5229
|
+
const oldDir = join8(homedir6(), ".hooks");
|
|
5230
|
+
if (!existsSync8(newDir) && existsSync8(oldDir)) {
|
|
5231
|
+
mkdirSync5(join8(homedir6(), ".hasna"), { recursive: true });
|
|
4873
5232
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
4874
5233
|
}
|
|
4875
5234
|
return newDir;
|
|
@@ -4879,19 +5238,19 @@ function getDbPath() {
|
|
|
4879
5238
|
if (explicitDb)
|
|
4880
5239
|
return explicitDb;
|
|
4881
5240
|
const dataDir = resolveDataDir();
|
|
4882
|
-
return
|
|
5241
|
+
return join8(dataDir, "hooks.db");
|
|
4883
5242
|
}
|
|
4884
5243
|
function ensureDir(dbPath) {
|
|
4885
5244
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
4886
|
-
if (dir && !
|
|
4887
|
-
|
|
5245
|
+
if (dir && !existsSync8(dir)) {
|
|
5246
|
+
mkdirSync5(dir, { recursive: true });
|
|
4888
5247
|
}
|
|
4889
5248
|
}
|
|
4890
5249
|
function getDb() {
|
|
4891
5250
|
if (instance)
|
|
4892
5251
|
return instance;
|
|
4893
5252
|
const dbPath = getDbPath();
|
|
4894
|
-
const isNew = dbPath === ":memory:" || !
|
|
5253
|
+
const isNew = dbPath === ":memory:" || !existsSync8(dbPath);
|
|
4895
5254
|
ensureDir(dbPath);
|
|
4896
5255
|
instance = new Database(dbPath);
|
|
4897
5256
|
instance.exec("PRAGMA journal_mode=WAL");
|
|
@@ -4930,6 +5289,313 @@ var init_db = __esm(() => {
|
|
|
4930
5289
|
init_legacy_import();
|
|
4931
5290
|
});
|
|
4932
5291
|
|
|
5292
|
+
// src/lib/store.ts
|
|
5293
|
+
var exports_store = {};
|
|
5294
|
+
__export(exports_store, {
|
|
5295
|
+
writeLock: () => writeLock,
|
|
5296
|
+
verifyScriptHash: () => verifyScriptHash,
|
|
5297
|
+
upsertHookRecord: () => upsertHookRecord,
|
|
5298
|
+
sha256Of: () => sha256Of,
|
|
5299
|
+
sha256File: () => sha256File,
|
|
5300
|
+
setPinnedHook: () => setPinnedHook,
|
|
5301
|
+
retrustHook: () => retrustHook,
|
|
5302
|
+
removePinnedHook: () => removePinnedHook,
|
|
5303
|
+
removeHookRecord: () => removeHookRecord,
|
|
5304
|
+
readLock: () => readLock,
|
|
5305
|
+
listHookRecords: () => listHookRecords,
|
|
5306
|
+
getPinnedHook: () => getPinnedHook,
|
|
5307
|
+
getHookRecord: () => getHookRecord,
|
|
5308
|
+
checkScriptHash: () => checkScriptHash
|
|
5309
|
+
});
|
|
5310
|
+
import { createHash } from "crypto";
|
|
5311
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
5312
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
5313
|
+
function sha256Of(input) {
|
|
5314
|
+
return createHash("sha256").update(input).digest("hex");
|
|
5315
|
+
}
|
|
5316
|
+
async function sha256File(path) {
|
|
5317
|
+
const buf = await readFile2(path);
|
|
5318
|
+
return sha256Of(buf);
|
|
5319
|
+
}
|
|
5320
|
+
function getHookRecord(db, name) {
|
|
5321
|
+
return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
|
|
5322
|
+
}
|
|
5323
|
+
function listHookRecords(db) {
|
|
5324
|
+
return db.query("SELECT * FROM hooks ORDER BY name").all();
|
|
5325
|
+
}
|
|
5326
|
+
function upsertHookRecord(db, record) {
|
|
5327
|
+
const existing = getHookRecord(db, record.name);
|
|
5328
|
+
const id = existing?.id ?? record.name;
|
|
5329
|
+
const installedAt = existing?.installed_at ?? new Date().toISOString();
|
|
5330
|
+
const enabled = record.enabled ?? existing?.enabled ?? 1;
|
|
5331
|
+
db.run(`INSERT INTO hooks (id, name, version, sha256, source_type, source_ref, installed_at, enabled, last_verified_at)
|
|
5332
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5333
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
5334
|
+
version = excluded.version,
|
|
5335
|
+
sha256 = excluded.sha256,
|
|
5336
|
+
source_type = excluded.source_type,
|
|
5337
|
+
source_ref = excluded.source_ref,
|
|
5338
|
+
enabled = excluded.enabled,
|
|
5339
|
+
last_verified_at = excluded.last_verified_at`, [
|
|
5340
|
+
id,
|
|
5341
|
+
record.name,
|
|
5342
|
+
record.version,
|
|
5343
|
+
record.sha256,
|
|
5344
|
+
record.source_type,
|
|
5345
|
+
record.source_ref ?? existing?.source_ref ?? null,
|
|
5346
|
+
installedAt,
|
|
5347
|
+
enabled,
|
|
5348
|
+
record.last_verified_at ?? null
|
|
5349
|
+
]);
|
|
5350
|
+
}
|
|
5351
|
+
function removeHookRecord(db, name) {
|
|
5352
|
+
const res = db.run("DELETE FROM hooks WHERE name = ?", [name]);
|
|
5353
|
+
return res.changes > 0;
|
|
5354
|
+
}
|
|
5355
|
+
function readLock() {
|
|
5356
|
+
try {
|
|
5357
|
+
const path = getLockPath();
|
|
5358
|
+
if (!existsSync9(path))
|
|
5359
|
+
return { hooks: {} };
|
|
5360
|
+
const parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
5361
|
+
return { hooks: parsed.hooks ?? {} };
|
|
5362
|
+
} catch {
|
|
5363
|
+
return { hooks: {} };
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
function writeLock(lock) {
|
|
5367
|
+
const path = getLockPath();
|
|
5368
|
+
mkdirSync6(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
|
|
5369
|
+
const sorted = {};
|
|
5370
|
+
for (const name of Object.keys(lock.hooks).sort()) {
|
|
5371
|
+
sorted[name] = lock.hooks[name];
|
|
5372
|
+
}
|
|
5373
|
+
writeFileSync5(path, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
5374
|
+
`, "utf-8");
|
|
5375
|
+
return path;
|
|
5376
|
+
}
|
|
5377
|
+
function setPinnedHook(name, entry) {
|
|
5378
|
+
const lock = readLock();
|
|
5379
|
+
lock.hooks[name] = entry;
|
|
5380
|
+
return writeLock(lock);
|
|
5381
|
+
}
|
|
5382
|
+
function getPinnedHook(name) {
|
|
5383
|
+
return readLock().hooks[name];
|
|
5384
|
+
}
|
|
5385
|
+
function removePinnedHook(name) {
|
|
5386
|
+
const lock = readLock();
|
|
5387
|
+
if (!(name in lock.hooks))
|
|
5388
|
+
return false;
|
|
5389
|
+
delete lock.hooks[name];
|
|
5390
|
+
writeLock(lock);
|
|
5391
|
+
return true;
|
|
5392
|
+
}
|
|
5393
|
+
function checkScriptHash(name, actual) {
|
|
5394
|
+
const db = getDb();
|
|
5395
|
+
const record = getHookRecord(db, name);
|
|
5396
|
+
const pin = getPinnedHook(name);
|
|
5397
|
+
const expected = record?.sha256 ?? pin?.sha256 ?? null;
|
|
5398
|
+
if (expected !== null && expected !== actual) {
|
|
5399
|
+
return { ok: false, pinned: true, expected, actual, name };
|
|
5400
|
+
}
|
|
5401
|
+
if (expected === null) {
|
|
5402
|
+
const now3 = new Date().toISOString();
|
|
5403
|
+
const version = pin?.version ?? record?.version ?? "0.0.0";
|
|
5404
|
+
const source = pin?.source ?? record?.source_type ?? "local";
|
|
5405
|
+
upsertHookRecord(db, {
|
|
5406
|
+
name,
|
|
5407
|
+
version,
|
|
5408
|
+
sha256: actual,
|
|
5409
|
+
source_type: source,
|
|
5410
|
+
last_verified_at: now3
|
|
5411
|
+
});
|
|
5412
|
+
setPinnedHook(name, { version, sha256: actual, source });
|
|
5413
|
+
return { ok: true, pinned: false, expected: null, actual, name };
|
|
5414
|
+
}
|
|
5415
|
+
const now2 = new Date().toISOString();
|
|
5416
|
+
upsertHookRecord(db, {
|
|
5417
|
+
name,
|
|
5418
|
+
version: record?.version ?? pin?.version ?? "0.0.0",
|
|
5419
|
+
sha256: expected,
|
|
5420
|
+
source_type: record?.source_type ?? pin?.source ?? "local",
|
|
5421
|
+
last_verified_at: now2
|
|
5422
|
+
});
|
|
5423
|
+
return { ok: true, pinned: true, expected, actual, name };
|
|
5424
|
+
}
|
|
5425
|
+
async function verifyScriptHash(name, scriptPath) {
|
|
5426
|
+
return checkScriptHash(name, await sha256File(scriptPath));
|
|
5427
|
+
}
|
|
5428
|
+
function retrustHook(name, scriptPath, version, source) {
|
|
5429
|
+
const actual = sha256Of(readFileSync6(scriptPath));
|
|
5430
|
+
const db = getDb();
|
|
5431
|
+
upsertHookRecord(db, {
|
|
5432
|
+
name,
|
|
5433
|
+
version,
|
|
5434
|
+
sha256: actual,
|
|
5435
|
+
source_type: source,
|
|
5436
|
+
last_verified_at: new Date().toISOString()
|
|
5437
|
+
});
|
|
5438
|
+
setPinnedHook(name, { version, sha256: actual, source });
|
|
5439
|
+
return { ok: true, pinned: true, expected: actual, actual, name };
|
|
5440
|
+
}
|
|
5441
|
+
var init_store = __esm(() => {
|
|
5442
|
+
init_db();
|
|
5443
|
+
init_config();
|
|
5444
|
+
});
|
|
5445
|
+
|
|
5446
|
+
// src/lib/run.ts
|
|
5447
|
+
var exports_run = {};
|
|
5448
|
+
__export(exports_run, {
|
|
5449
|
+
executeVerifiedScript: () => executeVerifiedScript
|
|
5450
|
+
});
|
|
5451
|
+
import { randomBytes } from "crypto";
|
|
5452
|
+
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
5453
|
+
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
5454
|
+
function extensionOf(scriptPath) {
|
|
5455
|
+
const base = basename2(scriptPath);
|
|
5456
|
+
const dot = base.lastIndexOf(".");
|
|
5457
|
+
return dot > 0 ? base.slice(dot + 1) : "ts";
|
|
5458
|
+
}
|
|
5459
|
+
async function executeVerifiedScript(options) {
|
|
5460
|
+
const scriptDir = dirname3(options.scriptPath);
|
|
5461
|
+
const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes(12).toString("hex")}.${extensionOf(options.scriptPath)}`);
|
|
5462
|
+
try {
|
|
5463
|
+
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
5464
|
+
const proc = Bun.spawn(["bun", "run", isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
5465
|
+
stdin: new Response(options.stdin),
|
|
5466
|
+
stdout: "pipe",
|
|
5467
|
+
stderr: "pipe",
|
|
5468
|
+
env: options.env ?? process.env,
|
|
5469
|
+
...options.timeout ? { timeout: options.timeout } : {}
|
|
5470
|
+
});
|
|
5471
|
+
const [stdoutText, stderrText, exitCode] = await Promise.all([
|
|
5472
|
+
new Response(proc.stdout).text(),
|
|
5473
|
+
new Response(proc.stderr).text(),
|
|
5474
|
+
proc.exited
|
|
5475
|
+
]);
|
|
5476
|
+
return { stdout: stdoutText, stderr: stderrText, exitCode };
|
|
5477
|
+
} finally {
|
|
5478
|
+
try {
|
|
5479
|
+
rmSync2(tempPath, { force: true });
|
|
5480
|
+
} catch {}
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
var init_run = () => {};
|
|
5484
|
+
|
|
5485
|
+
// src/lib/custom-install.ts
|
|
5486
|
+
var exports_custom_install = {};
|
|
5487
|
+
__export(exports_custom_install, {
|
|
5488
|
+
isCustomSource: () => isCustomSource,
|
|
5489
|
+
installCustomSource: () => installCustomSource,
|
|
5490
|
+
ensureCustomDir: () => ensureCustomDir
|
|
5491
|
+
});
|
|
5492
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
5493
|
+
import { join as join10 } from "path";
|
|
5494
|
+
import { tmpdir } from "os";
|
|
5495
|
+
function manifestAt(dir) {
|
|
5496
|
+
const path = join10(dir, "manifest.json");
|
|
5497
|
+
if (!existsSync10(path))
|
|
5498
|
+
return;
|
|
5499
|
+
try {
|
|
5500
|
+
return parseManifest(readFileSync7(path, "utf-8"));
|
|
5501
|
+
} catch {
|
|
5502
|
+
return;
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
function copyManifestDir(srcDir, source, kind) {
|
|
5506
|
+
const manifest = manifestAt(srcDir);
|
|
5507
|
+
if (!manifest) {
|
|
5508
|
+
throw new Error(`No valid manifest.json found at ${srcDir}`);
|
|
5509
|
+
}
|
|
5510
|
+
return writeFromManifest(manifest, srcDir, source, kind);
|
|
5511
|
+
}
|
|
5512
|
+
function writeFromManifest(manifest, srcDir, source, kind) {
|
|
5513
|
+
const script = resolveScript(manifest, srcDir);
|
|
5514
|
+
const name = shortManifestName(manifest.name);
|
|
5515
|
+
const { dir, scriptPath } = writeCustomHook(name, manifest, script.content, script.path);
|
|
5516
|
+
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
5517
|
+
}
|
|
5518
|
+
async function cloneToTemp(gitUrl) {
|
|
5519
|
+
const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
|
|
5520
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
|
|
5521
|
+
stdout: "pipe",
|
|
5522
|
+
stderr: "pipe"
|
|
5523
|
+
});
|
|
5524
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
5525
|
+
new Response(proc.stdout).text(),
|
|
5526
|
+
new Response(proc.stderr).text(),
|
|
5527
|
+
proc.exited
|
|
5528
|
+
]);
|
|
5529
|
+
if (exitCode !== 0) {
|
|
5530
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
5531
|
+
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
5532
|
+
}
|
|
5533
|
+
return join10(tmp, "repo");
|
|
5534
|
+
}
|
|
5535
|
+
function isGitUrl(value) {
|
|
5536
|
+
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
5537
|
+
}
|
|
5538
|
+
function isCustomSource(value) {
|
|
5539
|
+
if (isGitUrl(value))
|
|
5540
|
+
return true;
|
|
5541
|
+
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
5542
|
+
return true;
|
|
5543
|
+
return existsSync10(value);
|
|
5544
|
+
}
|
|
5545
|
+
async function installCustomSource(source) {
|
|
5546
|
+
if (isGitUrl(source)) {
|
|
5547
|
+
const repoDir = await cloneToTemp(source);
|
|
5548
|
+
try {
|
|
5549
|
+
return copyManifestDir(repoDir, source, "git");
|
|
5550
|
+
} finally {
|
|
5551
|
+
rmSync3(repoDir, { recursive: true, force: true });
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
5555
|
+
return installManifestUrl(source);
|
|
5556
|
+
}
|
|
5557
|
+
if (!existsSync10(source)) {
|
|
5558
|
+
throw new Error(`No such local path: ${source}`);
|
|
5559
|
+
}
|
|
5560
|
+
if (source.endsWith("manifest.json")) {
|
|
5561
|
+
const manifest = parseManifest(readFileSync7(source, "utf-8"));
|
|
5562
|
+
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
5563
|
+
}
|
|
5564
|
+
return copyManifestDir(source, source, "local");
|
|
5565
|
+
}
|
|
5566
|
+
async function installManifestUrl(url) {
|
|
5567
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
5568
|
+
if (!res.ok) {
|
|
5569
|
+
throw new Error(`Failed to fetch manifest (${res.status}): ${url}`);
|
|
5570
|
+
}
|
|
5571
|
+
const manifest = parseManifest(await res.text());
|
|
5572
|
+
let script;
|
|
5573
|
+
if (manifest.script.includes(`
|
|
5574
|
+
`)) {
|
|
5575
|
+
script = { path: "script.ts", content: manifest.script };
|
|
5576
|
+
} else {
|
|
5577
|
+
const base = url.substring(0, url.lastIndexOf("/"));
|
|
5578
|
+
const scriptUrl = `${base}/${manifest.script}`;
|
|
5579
|
+
const scriptRes = await fetch(scriptUrl, { redirect: "follow" });
|
|
5580
|
+
if (!scriptRes.ok) {
|
|
5581
|
+
throw new Error(`Failed to fetch script (${scriptRes.status}): ${scriptUrl}`);
|
|
5582
|
+
}
|
|
5583
|
+
script = { path: manifest.script, content: await scriptRes.text() };
|
|
5584
|
+
}
|
|
5585
|
+
const name = shortManifestName(manifest.name);
|
|
5586
|
+
const { dir, scriptPath } = writeCustomHook(name, manifest, script.content, script.path);
|
|
5587
|
+
return { name, version: manifest.version, kind: "url", source: url, dir, scriptPath };
|
|
5588
|
+
}
|
|
5589
|
+
function ensureCustomDir() {
|
|
5590
|
+
const dir = getCustomHooksDir();
|
|
5591
|
+
mkdirSync7(dir, { recursive: true });
|
|
5592
|
+
return dir;
|
|
5593
|
+
}
|
|
5594
|
+
var init_custom_install = __esm(() => {
|
|
5595
|
+
init_config();
|
|
5596
|
+
init_manifest();
|
|
5597
|
+
});
|
|
5598
|
+
|
|
4933
5599
|
// src/db/pg-migrations.ts
|
|
4934
5600
|
var PG_MIGRATIONS;
|
|
4935
5601
|
var init_pg_migrations = __esm(() => {
|
|
@@ -5014,11 +5680,30 @@ function readEnv(name) {
|
|
|
5014
5680
|
const value = process.env[name]?.trim();
|
|
5015
5681
|
return value || undefined;
|
|
5016
5682
|
}
|
|
5017
|
-
function
|
|
5018
|
-
const
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5683
|
+
function assertNoRetiredModeEnv() {
|
|
5684
|
+
for (const name of RETIRED_STORAGE_MODE_ENV) {
|
|
5685
|
+
const value = readEnv(name);
|
|
5686
|
+
if (!value)
|
|
5687
|
+
continue;
|
|
5688
|
+
const replacement = RETIRED_MODE_REPLACEMENT[value.trim().toLowerCase()];
|
|
5689
|
+
const mapping = replacement ? `${value} maps to ${replacement}` : `use one of ${STORAGE_BACKENDS.join(", ")}`;
|
|
5690
|
+
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}.`);
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
function normalizeStorageBackend(value, envName) {
|
|
5694
|
+
if (value === undefined)
|
|
5695
|
+
return;
|
|
5696
|
+
const normalized = value.trim().toLowerCase();
|
|
5697
|
+
if (normalized === "")
|
|
5698
|
+
return;
|
|
5699
|
+
const backend = BACKEND_ALIASES[normalized];
|
|
5700
|
+
if (backend)
|
|
5701
|
+
return backend;
|
|
5702
|
+
const replacement = RETIRED_MODE_REPLACEMENT[normalized];
|
|
5703
|
+
if (replacement) {
|
|
5704
|
+
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.`);
|
|
5705
|
+
}
|
|
5706
|
+
throw new Error(`${envName}=${value} is not a known hooks storage backend. ` + `Set ${HOOKS_STORAGE_BACKEND_ENV} to one of ${STORAGE_BACKENDS.join(", ")}.`);
|
|
5022
5707
|
}
|
|
5023
5708
|
function getStorageDatabaseEnvName() {
|
|
5024
5709
|
for (const name of STORAGE_DATABASE_ENV) {
|
|
@@ -5035,11 +5720,12 @@ function getStorageDatabaseUrl() {
|
|
|
5035
5720
|
const env = getStorageDatabaseEnv();
|
|
5036
5721
|
return env ? readEnv(env.name) ?? null : null;
|
|
5037
5722
|
}
|
|
5038
|
-
function
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5723
|
+
function getStorageBackend() {
|
|
5724
|
+
assertNoRetiredModeEnv();
|
|
5725
|
+
const backend = normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_ENV), HOOKS_STORAGE_BACKEND_ENV) ?? normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_FALLBACK_ENV), HOOKS_STORAGE_BACKEND_FALLBACK_ENV);
|
|
5726
|
+
if (backend)
|
|
5727
|
+
return backend;
|
|
5728
|
+
return getStorageDatabaseUrl() ? "postgresql" : "sqlite";
|
|
5043
5729
|
}
|
|
5044
5730
|
async function getStoragePg() {
|
|
5045
5731
|
const url = getStorageDatabaseUrl();
|
|
@@ -5097,7 +5783,7 @@ function getStorageStatus() {
|
|
|
5097
5783
|
const activeEnv = getStorageDatabaseEnv();
|
|
5098
5784
|
return {
|
|
5099
5785
|
configured: Boolean(activeEnv),
|
|
5100
|
-
|
|
5786
|
+
backend: getStorageBackend(),
|
|
5101
5787
|
env: STORAGE_DATABASE_ENV,
|
|
5102
5788
|
activeEnv: activeEnv?.name ?? null,
|
|
5103
5789
|
service: "hooks",
|
|
@@ -5265,7 +5951,7 @@ function coerceForSqlite(value) {
|
|
|
5265
5951
|
return JSON.stringify(value);
|
|
5266
5952
|
return String(value);
|
|
5267
5953
|
}
|
|
5268
|
-
var STORAGE_TABLES, HOOKS_STORAGE_TABLES, HOOKS_STORAGE_ENV = "HASNA_HOOKS_DATABASE_URL", HOOKS_STORAGE_FALLBACK_ENV = "HOOKS_DATABASE_URL",
|
|
5954
|
+
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
5955
|
var init_storage_sync = __esm(() => {
|
|
5270
5956
|
init_db();
|
|
5271
5957
|
init_pg_migrations();
|
|
@@ -5277,14 +5963,32 @@ var init_storage_sync = __esm(() => {
|
|
|
5277
5963
|
"feedback"
|
|
5278
5964
|
];
|
|
5279
5965
|
HOOKS_STORAGE_TABLES = STORAGE_TABLES;
|
|
5966
|
+
STORAGE_BACKENDS = ["sqlite", "postgresql"];
|
|
5280
5967
|
STORAGE_DATABASE_ENV = [HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV];
|
|
5281
|
-
|
|
5968
|
+
STORAGE_BACKEND_ENV = [HOOKS_STORAGE_BACKEND_ENV, HOOKS_STORAGE_BACKEND_FALLBACK_ENV];
|
|
5969
|
+
RETIRED_STORAGE_MODE_ENV = ["HASNA_HOOKS_STORAGE_MODE", "HOOKS_STORAGE_MODE"];
|
|
5282
5970
|
PRIMARY_KEYS = {
|
|
5283
5971
|
hook_events: ["id"],
|
|
5284
5972
|
schema_migrations: ["version"],
|
|
5285
5973
|
_meta: ["key"],
|
|
5286
5974
|
feedback: ["id"]
|
|
5287
5975
|
};
|
|
5976
|
+
RETIRED_MODE_REPLACEMENT = {
|
|
5977
|
+
local: "sqlite",
|
|
5978
|
+
hybrid: "postgresql",
|
|
5979
|
+
remote: "postgresql",
|
|
5980
|
+
self_hosted: "postgresql",
|
|
5981
|
+
"self-hosted": "postgresql",
|
|
5982
|
+
selfhosted: "postgresql",
|
|
5983
|
+
cloud: "postgresql"
|
|
5984
|
+
};
|
|
5985
|
+
BACKEND_ALIASES = {
|
|
5986
|
+
sqlite: "sqlite",
|
|
5987
|
+
sqlite3: "sqlite",
|
|
5988
|
+
postgresql: "postgresql",
|
|
5989
|
+
postgres: "postgresql",
|
|
5990
|
+
pg: "postgresql"
|
|
5991
|
+
};
|
|
5288
5992
|
});
|
|
5289
5993
|
|
|
5290
5994
|
// src/storage.ts
|
|
@@ -5299,20 +6003,22 @@ __export(exports_storage, {
|
|
|
5299
6003
|
getSyncMetaAll: () => getSyncMetaAll,
|
|
5300
6004
|
getStorageStatus: () => getStorageStatus,
|
|
5301
6005
|
getStoragePg: () => getStoragePg,
|
|
5302
|
-
getStorageMode: () => getStorageMode,
|
|
5303
6006
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
5304
6007
|
getStorageDatabaseEnvName: () => getStorageDatabaseEnvName,
|
|
5305
6008
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
6009
|
+
getStorageBackend: () => getStorageBackend,
|
|
5306
6010
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
5307
|
-
STORAGE_MODE_ENV: () => STORAGE_MODE_ENV,
|
|
5308
6011
|
STORAGE_DATABASE_ENV: () => STORAGE_DATABASE_ENV,
|
|
6012
|
+
STORAGE_BACKEND_ENV: () => STORAGE_BACKEND_ENV,
|
|
6013
|
+
STORAGE_BACKENDS: () => STORAGE_BACKENDS,
|
|
6014
|
+
RETIRED_STORAGE_MODE_ENV: () => RETIRED_STORAGE_MODE_ENV,
|
|
5309
6015
|
PgAdapterAsync: () => PgAdapterAsync,
|
|
5310
6016
|
PG_MIGRATIONS: () => PG_MIGRATIONS,
|
|
5311
6017
|
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
6018
|
HOOKS_STORAGE_FALLBACK_ENV: () => HOOKS_STORAGE_FALLBACK_ENV,
|
|
5315
|
-
HOOKS_STORAGE_ENV: () => HOOKS_STORAGE_ENV
|
|
6019
|
+
HOOKS_STORAGE_ENV: () => HOOKS_STORAGE_ENV,
|
|
6020
|
+
HOOKS_STORAGE_BACKEND_FALLBACK_ENV: () => HOOKS_STORAGE_BACKEND_FALLBACK_ENV,
|
|
6021
|
+
HOOKS_STORAGE_BACKEND_ENV: () => HOOKS_STORAGE_BACKEND_ENV
|
|
5316
6022
|
});
|
|
5317
6023
|
var init_storage = __esm(() => {
|
|
5318
6024
|
init_storage_sync();
|
|
@@ -5320,56 +6026,433 @@ var init_storage = __esm(() => {
|
|
|
5320
6026
|
init_pg_migrations();
|
|
5321
6027
|
});
|
|
5322
6028
|
|
|
5323
|
-
// src/
|
|
5324
|
-
var
|
|
5325
|
-
__export(
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
6029
|
+
// src/serve.ts
|
|
6030
|
+
var exports_serve = {};
|
|
6031
|
+
__export(exports_serve, {
|
|
6032
|
+
startServeServer: () => startServeServer,
|
|
6033
|
+
handleServeRequest: () => handleServeRequest,
|
|
6034
|
+
SERVE_SERVICE_NAME: () => SERVE_SERVICE_NAME,
|
|
6035
|
+
SERVE_HOST: () => SERVE_HOST,
|
|
6036
|
+
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
5330
6037
|
});
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
6038
|
+
async function buildCatalog() {
|
|
6039
|
+
const byName = new Map;
|
|
6040
|
+
for (const meta of HOOKS) {
|
|
6041
|
+
const scriptPath = resolveHook(meta.name)?.scriptPath;
|
|
6042
|
+
if (!scriptPath)
|
|
6043
|
+
continue;
|
|
6044
|
+
byName.set(meta.name, {
|
|
6045
|
+
name: meta.name,
|
|
6046
|
+
version: meta.version,
|
|
6047
|
+
sha256: await sha256File(scriptPath),
|
|
6048
|
+
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
6049
|
+
description: meta.description,
|
|
6050
|
+
source: "bundled"
|
|
6051
|
+
});
|
|
6052
|
+
}
|
|
6053
|
+
for (const custom of listCustomHooks()) {
|
|
6054
|
+
const name = shortManifestName(custom.manifest.name);
|
|
6055
|
+
byName.set(name, {
|
|
6056
|
+
name,
|
|
6057
|
+
version: custom.manifest.version,
|
|
6058
|
+
sha256: await sha256File(custom.scriptPath),
|
|
6059
|
+
events: custom.manifest.events,
|
|
6060
|
+
description: custom.manifest.description ?? "Custom hook",
|
|
6061
|
+
source: byName.has(name) ? "custom-overrides-bundled" : "custom"
|
|
6062
|
+
});
|
|
6063
|
+
}
|
|
6064
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
6065
|
+
}
|
|
6066
|
+
async function artifactFor(name, version) {
|
|
6067
|
+
const resolved = resolveHook(name);
|
|
6068
|
+
if (!resolved)
|
|
6069
|
+
return null;
|
|
6070
|
+
if (version !== resolved.version)
|
|
6071
|
+
return null;
|
|
6072
|
+
const custom = listCustomHooks().find((c) => shortManifestName(c.manifest.name) === name);
|
|
6073
|
+
const manifest = {
|
|
6074
|
+
name,
|
|
6075
|
+
version: resolved.version,
|
|
6076
|
+
description: resolved.description,
|
|
6077
|
+
events: resolved.events,
|
|
6078
|
+
script: custom ? custom.manifest.script : `src/hook.ts`,
|
|
6079
|
+
args: custom?.manifest.args,
|
|
6080
|
+
timeout_ms: custom?.manifest.timeout_ms
|
|
5347
6081
|
};
|
|
6082
|
+
const script = await Bun.file(resolved.scriptPath).text();
|
|
6083
|
+
return { manifest, script };
|
|
5348
6084
|
}
|
|
5349
|
-
function
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
6085
|
+
function authorized(req, apiKey) {
|
|
6086
|
+
if (!apiKey)
|
|
6087
|
+
return false;
|
|
6088
|
+
const header = req.headers.get("authorization") ?? "";
|
|
6089
|
+
if (header.startsWith("Bearer "))
|
|
6090
|
+
return header.slice("Bearer ".length) === apiKey;
|
|
6091
|
+
return req.headers.get("x-api-key") === apiKey;
|
|
5354
6092
|
}
|
|
5355
|
-
function
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
6093
|
+
function json(body, status = 200, headers = {}) {
|
|
6094
|
+
return new Response(JSON.stringify(body), {
|
|
6095
|
+
status,
|
|
6096
|
+
headers: { "content-type": "application/json", ...headers }
|
|
6097
|
+
});
|
|
5359
6098
|
}
|
|
5360
|
-
function
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
6099
|
+
function handleServeRequest(req, apiKey) {
|
|
6100
|
+
const url = new URL(req.url);
|
|
6101
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
6102
|
+
return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
|
|
6103
|
+
}
|
|
6104
|
+
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
6105
|
+
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
6106
|
+
}
|
|
6107
|
+
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
6108
|
+
return Promise.resolve(json(readLock()));
|
|
6109
|
+
}
|
|
6110
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
|
|
6111
|
+
if (artifactMatch && req.method === "GET") {
|
|
6112
|
+
const [, name, version] = artifactMatch;
|
|
6113
|
+
return artifactFor(name, version).then(async (artifact) => {
|
|
6114
|
+
if (!artifact)
|
|
6115
|
+
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
6116
|
+
const resolved = resolveHook(name);
|
|
6117
|
+
const sha = resolved ? await sha256File(resolved.scriptPath) : artifact.manifest.name;
|
|
6118
|
+
return json(artifact, 200, { "x-hook-sha256": sha });
|
|
6119
|
+
});
|
|
6120
|
+
}
|
|
6121
|
+
if (url.pathname === "/api/v1/hooks" && req.method === "PUT") {
|
|
6122
|
+
if (!authorized(req, apiKey)) {
|
|
6123
|
+
return Promise.resolve(json({ error: "unauthorized: valid API key required to publish" }, 401));
|
|
6124
|
+
}
|
|
6125
|
+
return req.json().then((body) => {
|
|
6126
|
+
const name = body.name ?? "";
|
|
6127
|
+
const version = body.version ?? "";
|
|
6128
|
+
const resolved = resolveHook(name);
|
|
6129
|
+
if (!resolved)
|
|
6130
|
+
return json({ error: `Hook '${name}' not found in local store` }, 404);
|
|
6131
|
+
if (version && version !== resolved.version) {
|
|
6132
|
+
return json({ error: `Version mismatch: local '${name}' is ${resolved.version}, requested ${version}` }, 409);
|
|
6133
|
+
}
|
|
6134
|
+
const check = retrustHook(name, resolved.scriptPath, resolved.version, "serve");
|
|
6135
|
+
return json({ ok: true, hook: { name, version: resolved.version, sha256: check.actual } });
|
|
6136
|
+
}, () => json({ error: "invalid JSON body" }, 400));
|
|
6137
|
+
}
|
|
6138
|
+
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
6139
|
+
}
|
|
6140
|
+
function startServeServer(options) {
|
|
6141
|
+
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
6142
|
+
const host = options.host ?? SERVE_HOST;
|
|
6143
|
+
const apiKey = resolveApiKey(options.apiKey);
|
|
6144
|
+
const server = Bun.serve({
|
|
6145
|
+
hostname: host,
|
|
6146
|
+
port,
|
|
6147
|
+
async fetch(req) {
|
|
6148
|
+
return handleServeRequest(req, apiKey);
|
|
6149
|
+
}
|
|
6150
|
+
});
|
|
6151
|
+
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
6152
|
+
return server;
|
|
5367
6153
|
}
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
6154
|
+
var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
|
|
6155
|
+
var init_serve = __esm(() => {
|
|
6156
|
+
init_registry();
|
|
6157
|
+
init_manifest();
|
|
6158
|
+
init_store();
|
|
6159
|
+
init_resolve();
|
|
6160
|
+
init_config();
|
|
6161
|
+
if (false) {}
|
|
6162
|
+
});
|
|
6163
|
+
|
|
6164
|
+
// src/lib/sync.ts
|
|
6165
|
+
var exports_sync = {};
|
|
6166
|
+
__export(exports_sync, {
|
|
6167
|
+
syncHooks: () => syncHooks,
|
|
6168
|
+
planSync: () => planSync
|
|
6169
|
+
});
|
|
6170
|
+
import { createHash as createHash2 } from "crypto";
|
|
6171
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
6172
|
+
function sha256OfText(text) {
|
|
6173
|
+
return createHash2("sha256").update(text).digest("hex");
|
|
6174
|
+
}
|
|
6175
|
+
function sha256OfPath(path) {
|
|
6176
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
6177
|
+
}
|
|
6178
|
+
function collectBundledCatalog() {
|
|
6179
|
+
const out = [];
|
|
6180
|
+
for (const meta of HOOKS) {
|
|
6181
|
+
const scriptPath = resolveScriptPath(meta.name);
|
|
6182
|
+
if (!scriptPath)
|
|
6183
|
+
continue;
|
|
6184
|
+
out.push({
|
|
6185
|
+
name: meta.name,
|
|
6186
|
+
version: meta.version,
|
|
6187
|
+
sha256: sha256OfPath(scriptPath)
|
|
6188
|
+
});
|
|
6189
|
+
}
|
|
6190
|
+
return out;
|
|
6191
|
+
}
|
|
6192
|
+
async function fetchJson(base, path) {
|
|
6193
|
+
const res = await fetch(`${base}${path}`, {
|
|
6194
|
+
headers: { accept: "application/json" },
|
|
6195
|
+
signal: AbortSignal.timeout(15000)
|
|
6196
|
+
});
|
|
6197
|
+
if (!res.ok) {
|
|
6198
|
+
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
6199
|
+
}
|
|
6200
|
+
return res.json();
|
|
6201
|
+
}
|
|
6202
|
+
async function fetchRemoteState(apiUrl) {
|
|
6203
|
+
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
6204
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
6205
|
+
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
6206
|
+
throw new Error("remote catalog is malformed: missing hooks array");
|
|
6207
|
+
}
|
|
6208
|
+
if (!remoteLock.hooks || typeof remoteLock.hooks !== "object") {
|
|
6209
|
+
throw new Error("remote lock is malformed: missing hooks map");
|
|
6210
|
+
}
|
|
6211
|
+
return { catalog: catalog.hooks, remoteLock };
|
|
6212
|
+
}
|
|
6213
|
+
function computeDiff(catalog, remoteLock) {
|
|
6214
|
+
const local = readLock();
|
|
6215
|
+
const diff = { added: [], updated: [], unchanged: [], skipped: [] };
|
|
6216
|
+
for (const entry of catalog) {
|
|
6217
|
+
if (!entry.name || !entry.version || !entry.sha256)
|
|
6218
|
+
continue;
|
|
6219
|
+
const pin = local.hooks[entry.name];
|
|
6220
|
+
const remotePin = remoteLock ? remoteLock.hooks[entry.name] : entry;
|
|
6221
|
+
if (remoteLock && !remotePin) {
|
|
6222
|
+
diff.skipped.push(entry.name);
|
|
6223
|
+
continue;
|
|
6224
|
+
}
|
|
6225
|
+
const pinnedLocally = pin && pin.version === entry.version && pin.sha256 === entry.sha256;
|
|
6226
|
+
if (pinnedLocally) {
|
|
6227
|
+
diff.unchanged.push(entry.name);
|
|
6228
|
+
} else if (pin) {
|
|
6229
|
+
diff.updated.push(entry.name);
|
|
6230
|
+
} else {
|
|
6231
|
+
diff.added.push(entry.name);
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
return diff;
|
|
6235
|
+
}
|
|
6236
|
+
async function planSync() {
|
|
6237
|
+
const apiUrl = resolveApiUrl();
|
|
6238
|
+
if (apiUrl) {
|
|
6239
|
+
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6240
|
+
return { apiUrl, dryRun: false, diff: computeDiff(catalog, remoteLock) };
|
|
6241
|
+
}
|
|
6242
|
+
const diff = computeDiff(collectBundledCatalog(), null);
|
|
6243
|
+
return { apiUrl: null, dryRun: false, diff };
|
|
6244
|
+
}
|
|
6245
|
+
async function syncHooks(options = {}) {
|
|
6246
|
+
const apiUrl = resolveApiUrl();
|
|
6247
|
+
const plan = await planSync();
|
|
6248
|
+
if (options.dryRun)
|
|
6249
|
+
return { ...plan, dryRun: true };
|
|
6250
|
+
if (!apiUrl) {
|
|
6251
|
+
const db2 = getDb();
|
|
6252
|
+
for (const entry of collectBundledCatalog()) {
|
|
6253
|
+
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
6254
|
+
upsertHookRecord(db2, {
|
|
6255
|
+
name: entry.name,
|
|
6256
|
+
version: entry.version,
|
|
6257
|
+
sha256: entry.sha256,
|
|
6258
|
+
source_type: "bundled",
|
|
6259
|
+
source_ref: "apps/hooks/hooks",
|
|
6260
|
+
last_verified_at: new Date().toISOString()
|
|
6261
|
+
});
|
|
6262
|
+
}
|
|
6263
|
+
return plan;
|
|
6264
|
+
}
|
|
6265
|
+
const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
|
|
6266
|
+
const diff = computeDiff(catalog, remoteLock);
|
|
6267
|
+
const toFetch = [...diff.added, ...diff.updated];
|
|
6268
|
+
const artifacts = new Map;
|
|
6269
|
+
for (const name of toFetch) {
|
|
6270
|
+
const entry = remoteLock.hooks[name];
|
|
6271
|
+
if (!entry)
|
|
6272
|
+
continue;
|
|
6273
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${name}/${entry.version}`);
|
|
6274
|
+
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
6275
|
+
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
6276
|
+
}
|
|
6277
|
+
const actualSha = sha256OfText(artifact.script);
|
|
6278
|
+
if (actualSha !== entry.sha256) {
|
|
6279
|
+
throw new Error(`sha256 mismatch for '${name}@${entry.version}': lock says ${entry.sha256}, artifact has ${actualSha}`);
|
|
6280
|
+
}
|
|
6281
|
+
artifacts.set(name, artifact);
|
|
6282
|
+
}
|
|
6283
|
+
const db = getDb();
|
|
6284
|
+
for (const name of toFetch) {
|
|
6285
|
+
const artifact = artifacts.get(name);
|
|
6286
|
+
const entry = remoteLock.hooks[name];
|
|
6287
|
+
if (!artifact || !entry)
|
|
6288
|
+
continue;
|
|
6289
|
+
const manifest = parseManifest(JSON.stringify({ ...artifact.manifest, name: shortManifestName(artifact.manifest.name || name) }));
|
|
6290
|
+
const scriptRel = artifact.manifest.script.includes(`
|
|
6291
|
+
`) ? "script.ts" : artifact.manifest.script;
|
|
6292
|
+
writeCustomHook(name, manifest, artifact.script, scriptRel);
|
|
6293
|
+
const scriptPath = resolveScriptPath(name);
|
|
6294
|
+
const actual = await sha256File(scriptPath);
|
|
6295
|
+
setPinnedHook(name, { version: entry.version, sha256: actual, source: entry.source ?? "remote" });
|
|
6296
|
+
upsertHookRecord(db, {
|
|
6297
|
+
name,
|
|
6298
|
+
version: entry.version,
|
|
6299
|
+
sha256: actual,
|
|
6300
|
+
source_type: entry.source ?? "remote",
|
|
6301
|
+
source_ref: apiUrl,
|
|
6302
|
+
last_verified_at: new Date().toISOString()
|
|
6303
|
+
});
|
|
6304
|
+
}
|
|
6305
|
+
return { apiUrl, dryRun: false, diff };
|
|
6306
|
+
}
|
|
6307
|
+
var init_sync = __esm(() => {
|
|
6308
|
+
init_registry();
|
|
6309
|
+
init_config();
|
|
6310
|
+
init_db();
|
|
6311
|
+
init_store();
|
|
6312
|
+
init_manifest();
|
|
6313
|
+
init_resolve();
|
|
6314
|
+
});
|
|
6315
|
+
|
|
6316
|
+
// src/cf/provision.ts
|
|
6317
|
+
var exports_provision = {};
|
|
6318
|
+
__export(exports_provision, {
|
|
6319
|
+
provisionCloudflareResources: () => provisionCloudflareResources
|
|
6320
|
+
});
|
|
6321
|
+
async function cfFetch(fetchFn, token, path, init) {
|
|
6322
|
+
const res = await fetchFn(`${CF_API}${path}`, {
|
|
6323
|
+
...init,
|
|
6324
|
+
headers: {
|
|
6325
|
+
authorization: `Bearer ${token}`,
|
|
6326
|
+
"content-type": "application/json",
|
|
6327
|
+
...init?.headers ?? {}
|
|
6328
|
+
}
|
|
6329
|
+
});
|
|
6330
|
+
const data = await res.json().catch(() => null);
|
|
6331
|
+
return { ok: res.ok, status: res.status, data };
|
|
6332
|
+
}
|
|
6333
|
+
async function provisionCloudflareResources(options) {
|
|
6334
|
+
const { token, accountId, databaseName, bucketName, dryRun, fetchFn = fetch } = options;
|
|
6335
|
+
const result = {
|
|
6336
|
+
d1DatabaseId: null,
|
|
6337
|
+
d1Created: false,
|
|
6338
|
+
d1Exists: false,
|
|
6339
|
+
r2BucketExists: false,
|
|
6340
|
+
r2Created: false,
|
|
6341
|
+
commands: []
|
|
6342
|
+
};
|
|
6343
|
+
if (dryRun) {
|
|
6344
|
+
result.commands = [
|
|
6345
|
+
`POST ${CF_API}/accounts/${accountId}/d1/database {"name": "${databaseName}"} (if missing)`,
|
|
6346
|
+
`PUT ${CF_API}/accounts/${accountId}/r2/buckets/${bucketName} (if missing)`,
|
|
6347
|
+
`wrangler d1 migrations apply ${databaseName} --remote`,
|
|
6348
|
+
`wrangler deploy --config wrangler.toml`
|
|
6349
|
+
];
|
|
6350
|
+
return result;
|
|
6351
|
+
}
|
|
6352
|
+
const d1List = await cfFetch(fetchFn, token, `/accounts/${accountId}/d1/database?name=${encodeURIComponent(databaseName)}`);
|
|
6353
|
+
if (!d1List.ok) {
|
|
6354
|
+
throw new Error(`failed to list D1 databases (${d1List.status})`);
|
|
6355
|
+
}
|
|
6356
|
+
const d1Rows = d1List.data?.result ?? [];
|
|
6357
|
+
if (d1Rows.length > 0) {
|
|
6358
|
+
result.d1Exists = true;
|
|
6359
|
+
result.d1DatabaseId = d1Rows[0].id;
|
|
6360
|
+
} else {
|
|
6361
|
+
const created = await cfFetch(fetchFn, token, `/accounts/${accountId}/d1/database`, {
|
|
6362
|
+
method: "POST",
|
|
6363
|
+
body: JSON.stringify({ name: databaseName })
|
|
6364
|
+
});
|
|
6365
|
+
if (!created.ok) {
|
|
6366
|
+
throw new Error(`failed to create D1 database '${databaseName}' (${created.status})`);
|
|
6367
|
+
}
|
|
6368
|
+
result.d1Created = true;
|
|
6369
|
+
result.d1DatabaseId = created.data?.result?.id ?? null;
|
|
6370
|
+
}
|
|
6371
|
+
const r2List = await cfFetch(fetchFn, token, `/accounts/${accountId}/r2/buckets`);
|
|
6372
|
+
if (!r2List.ok) {
|
|
6373
|
+
throw new Error(`failed to list R2 buckets (${r2List.status})`);
|
|
6374
|
+
}
|
|
6375
|
+
const bucketNames = (r2List.data?.result?.buckets ?? []).map((b) => b.name);
|
|
6376
|
+
if (bucketNames.includes(bucketName)) {
|
|
6377
|
+
result.r2BucketExists = true;
|
|
6378
|
+
} else {
|
|
6379
|
+
const created = await cfFetch(fetchFn, token, `/accounts/${accountId}/r2/buckets/${bucketName}`, {
|
|
6380
|
+
method: "PUT",
|
|
6381
|
+
body: JSON.stringify({ name: bucketName })
|
|
6382
|
+
});
|
|
6383
|
+
if (!created.ok) {
|
|
6384
|
+
throw new Error(`failed to create R2 bucket '${bucketName}' (${created.status})`);
|
|
6385
|
+
}
|
|
6386
|
+
result.r2Created = true;
|
|
6387
|
+
}
|
|
6388
|
+
result.commands = [
|
|
6389
|
+
`wrangler d1 migrations apply ${databaseName} --remote`,
|
|
6390
|
+
`wrangler secret put HOOKS_API_KEY`,
|
|
6391
|
+
`wrangler deploy --config wrangler.toml`
|
|
6392
|
+
];
|
|
6393
|
+
return result;
|
|
6394
|
+
}
|
|
6395
|
+
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
6396
|
+
|
|
6397
|
+
// src/mcp/server.ts
|
|
6398
|
+
var exports_server = {};
|
|
6399
|
+
__export(exports_server, {
|
|
6400
|
+
startStdioServer: () => startStdioServer,
|
|
6401
|
+
startSSEServer: () => startSSEServer,
|
|
6402
|
+
createHooksServer: () => createHooksServer,
|
|
6403
|
+
MCP_PORT: () => MCP_PORT
|
|
6404
|
+
});
|
|
6405
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6406
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
6407
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6408
|
+
import { z as z2 } from "zod";
|
|
6409
|
+
import { createServer } from "http";
|
|
6410
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
6411
|
+
import { join as join11, dirname as dirname4 } from "path";
|
|
6412
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6413
|
+
async function runVerifiedHook(name, scriptPath, stdin, timeoutMs) {
|
|
6414
|
+
const content = readFileSync9(scriptPath);
|
|
6415
|
+
const check = checkScriptHash(name, sha256Of(content));
|
|
6416
|
+
if (!check.ok) {
|
|
6417
|
+
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.`);
|
|
6418
|
+
}
|
|
6419
|
+
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
6420
|
+
return executeVerifiedScript2({ name, scriptPath, content, stdin, env: process.env, timeout: timeoutMs });
|
|
6421
|
+
}
|
|
6422
|
+
function formatInstallResults(results, extra) {
|
|
6423
|
+
const installed = results.filter((r) => r.success).map((r) => r.hook);
|
|
6424
|
+
const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
|
|
6425
|
+
return {
|
|
6426
|
+
content: [{
|
|
6427
|
+
type: "text",
|
|
6428
|
+
text: JSON.stringify({ installed, failed, total: results.length, success: installed.length, ...extra })
|
|
6429
|
+
}]
|
|
6430
|
+
};
|
|
6431
|
+
}
|
|
6432
|
+
function truncateText(value, max = 160) {
|
|
6433
|
+
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
6434
|
+
if (text.length <= max)
|
|
6435
|
+
return text;
|
|
6436
|
+
return `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
6437
|
+
}
|
|
6438
|
+
function boundedLimit(value, fallback, max) {
|
|
6439
|
+
if (!Number.isFinite(value) || !value || value <= 0)
|
|
6440
|
+
return fallback;
|
|
6441
|
+
return Math.min(Math.floor(value), max);
|
|
6442
|
+
}
|
|
6443
|
+
function compactHook(hook) {
|
|
6444
|
+
return {
|
|
6445
|
+
name: hook.name,
|
|
6446
|
+
event: hook.event,
|
|
6447
|
+
matcher: hook.matcher,
|
|
6448
|
+
category: hook.category
|
|
6449
|
+
};
|
|
6450
|
+
}
|
|
6451
|
+
function compactHookResult(hooks, limit, detail) {
|
|
6452
|
+
const visible = hooks.slice(0, limit).map(compactHook);
|
|
6453
|
+
const omitted = Math.max(0, hooks.length - visible.length);
|
|
6454
|
+
return {
|
|
6455
|
+
hooks: visible,
|
|
5373
6456
|
count: visible.length,
|
|
5374
6457
|
total: hooks.length,
|
|
5375
6458
|
omitted,
|
|
@@ -5394,9 +6477,9 @@ function createHooksServer() {
|
|
|
5394
6477
|
});
|
|
5395
6478
|
const defineTool = (name, description, schema, handler) => server.tool(name, description, schema, handler);
|
|
5396
6479
|
defineTool("hooks_list", "List available hooks. Compact by default; pass compact:false for full HookMeta objects.", {
|
|
5397
|
-
category:
|
|
5398
|
-
compact:
|
|
5399
|
-
limit:
|
|
6480
|
+
category: z2.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security')"),
|
|
6481
|
+
compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
|
|
6482
|
+
limit: z2.number().default(25).describe("Max compact rows to return")
|
|
5400
6483
|
}, async ({ category, compact, limit }) => {
|
|
5401
6484
|
const maxRows = boundedLimit(limit, 25, 100);
|
|
5402
6485
|
if (category) {
|
|
@@ -5414,15 +6497,15 @@ function createHooksServer() {
|
|
|
5414
6497
|
return { content: [{ type: "text", text: JSON.stringify(compact ? compactHookResult(HOOKS, maxRows, "hooks_info") : result) }] };
|
|
5415
6498
|
});
|
|
5416
6499
|
defineTool("hooks_search", "Search hooks. Compact by default; pass compact:false for full HookMeta objects.", {
|
|
5417
|
-
query:
|
|
5418
|
-
compact:
|
|
5419
|
-
limit:
|
|
6500
|
+
query: z2.string().describe("Search query"),
|
|
6501
|
+
compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
|
|
6502
|
+
limit: z2.number().default(10).describe("Max compact rows to return")
|
|
5420
6503
|
}, async ({ query, compact, limit }) => {
|
|
5421
6504
|
const results = searchHooks(query);
|
|
5422
6505
|
const out = compact ? compactHookResult(results, boundedLimit(limit, 10, 100), "hooks_info") : results;
|
|
5423
6506
|
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
5424
6507
|
});
|
|
5425
|
-
defineTool("hooks_info", "Get detailed information about a specific hook including install status", { name:
|
|
6508
|
+
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
6509
|
const meta = getHook(name);
|
|
5427
6510
|
if (!meta) {
|
|
5428
6511
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
@@ -5432,18 +6515,18 @@ function createHooksServer() {
|
|
|
5432
6515
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }) }] };
|
|
5433
6516
|
});
|
|
5434
6517
|
defineTool("hooks_install", "Install one or more hooks by registering them in agent settings", {
|
|
5435
|
-
hooks:
|
|
5436
|
-
scope:
|
|
5437
|
-
overwrite:
|
|
5438
|
-
profile:
|
|
6518
|
+
hooks: z2.array(z2.string()).describe("Hook names to install"),
|
|
6519
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Install scope"),
|
|
6520
|
+
overwrite: z2.boolean().default(false).describe("Overwrite if already installed"),
|
|
6521
|
+
profile: z2.string().optional().describe("Agent profile ID to scope hooks to")
|
|
5439
6522
|
}, async ({ hooks, scope, overwrite, profile }) => {
|
|
5440
6523
|
const results = hooks.map((name) => installHook(name, { scope, overwrite, profile }));
|
|
5441
6524
|
return formatInstallResults(results, { scope, profile });
|
|
5442
6525
|
});
|
|
5443
6526
|
defineTool("hooks_install_category", "Install all hooks in a category", {
|
|
5444
|
-
category:
|
|
5445
|
-
scope:
|
|
5446
|
-
overwrite:
|
|
6527
|
+
category: z2.string().describe("Category name"),
|
|
6528
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Install scope"),
|
|
6529
|
+
overwrite: z2.boolean().default(false).describe("Overwrite if already installed")
|
|
5447
6530
|
}, async ({ category, scope, overwrite }) => {
|
|
5448
6531
|
const cat = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
|
|
5449
6532
|
if (!cat) {
|
|
@@ -5454,45 +6537,53 @@ function createHooksServer() {
|
|
|
5454
6537
|
return formatInstallResults(results, { category: cat, scope });
|
|
5455
6538
|
});
|
|
5456
6539
|
defineTool("hooks_install_all", "Install all available hooks", {
|
|
5457
|
-
scope:
|
|
5458
|
-
overwrite:
|
|
6540
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Install scope"),
|
|
6541
|
+
overwrite: z2.boolean().default(false).describe("Overwrite if already installed")
|
|
5459
6542
|
}, async ({ scope, overwrite }) => {
|
|
5460
6543
|
const results = HOOKS.map((h) => installHook(h.name, { scope, overwrite }));
|
|
5461
6544
|
return formatInstallResults(results, { scope });
|
|
5462
6545
|
});
|
|
5463
6546
|
defineTool("hooks_remove", "Remove (unregister) a hook from agent settings", {
|
|
5464
|
-
name:
|
|
5465
|
-
scope:
|
|
6547
|
+
name: z2.string().describe("Hook name to remove"),
|
|
6548
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to remove from")
|
|
5466
6549
|
}, async ({ name, scope }) => {
|
|
5467
6550
|
const removed = removeHook(name, scope);
|
|
5468
6551
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, removed, scope }) }] };
|
|
5469
6552
|
});
|
|
5470
6553
|
defineTool("hooks_doctor", "Check health of installed hooks \u2014 verifies hook source exists, settings are correct", {
|
|
5471
|
-
scope:
|
|
6554
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to check")
|
|
5472
6555
|
}, async ({ scope }) => {
|
|
5473
6556
|
const settingsPath = getSettingsPath(scope);
|
|
5474
6557
|
const issues = [];
|
|
5475
6558
|
const healthy = [];
|
|
5476
|
-
const settingsExist =
|
|
6559
|
+
const settingsExist = existsSync11(settingsPath);
|
|
5477
6560
|
if (!settingsExist) {
|
|
5478
6561
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
5479
6562
|
}
|
|
5480
6563
|
const registered = getRegisteredHooks(scope);
|
|
5481
6564
|
for (const name of registered) {
|
|
5482
|
-
const
|
|
6565
|
+
const custom = readCustomManifest(name);
|
|
6566
|
+
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
5483
6567
|
let hookHealthy = true;
|
|
5484
|
-
if (
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
6568
|
+
if (custom) {
|
|
6569
|
+
if (!existsSync11(custom.scriptPath)) {
|
|
6570
|
+
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
6571
|
+
hookHealthy = false;
|
|
6572
|
+
}
|
|
6573
|
+
} else {
|
|
6574
|
+
if (!hookExists(name)) {
|
|
6575
|
+
issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
|
|
6576
|
+
continue;
|
|
6577
|
+
}
|
|
6578
|
+
const hookDir = getHookPath(name);
|
|
6579
|
+
if (!existsSync11(join11(hookDir, "src", "hook.ts"))) {
|
|
6580
|
+
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
6581
|
+
hookHealthy = false;
|
|
6582
|
+
}
|
|
5492
6583
|
}
|
|
5493
6584
|
if (meta && settingsExist) {
|
|
5494
6585
|
try {
|
|
5495
|
-
const settings = JSON.parse(
|
|
6586
|
+
const settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
|
|
5496
6587
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
5497
6588
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
5498
6589
|
const match = h.command?.match(/^hooks run ([\w-]+)/);
|
|
@@ -5517,8 +6608,8 @@ function createHooksServer() {
|
|
|
5517
6608
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
5518
6609
|
});
|
|
5519
6610
|
defineTool("hooks_docs", "Get documentation. Hook README content is summarized by default; pass verbose:true for the full README.", {
|
|
5520
|
-
name:
|
|
5521
|
-
verbose:
|
|
6611
|
+
name: z2.string().optional().describe("Hook name for specific docs, omit for general docs"),
|
|
6612
|
+
verbose: z2.boolean().default(false).describe("Return full hook README when true")
|
|
5522
6613
|
}, async ({ name, verbose }) => {
|
|
5523
6614
|
if (name) {
|
|
5524
6615
|
const meta = getHook(name);
|
|
@@ -5526,10 +6617,10 @@ function createHooksServer() {
|
|
|
5526
6617
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
5527
6618
|
}
|
|
5528
6619
|
const hookPath = getHookPath(name);
|
|
5529
|
-
const readmePath =
|
|
6620
|
+
const readmePath = join11(hookPath, "README.md");
|
|
5530
6621
|
let readme = "";
|
|
5531
|
-
if (
|
|
5532
|
-
readme =
|
|
6622
|
+
if (existsSync11(readmePath)) {
|
|
6623
|
+
readme = readFileSync9(readmePath, "utf-8");
|
|
5533
6624
|
}
|
|
5534
6625
|
if (verbose) {
|
|
5535
6626
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -5577,9 +6668,9 @@ function createHooksServer() {
|
|
|
5577
6668
|
};
|
|
5578
6669
|
});
|
|
5579
6670
|
defineTool("hooks_registered", "Get currently registered hooks for a scope. Compact by default.", {
|
|
5580
|
-
scope:
|
|
5581
|
-
compact:
|
|
5582
|
-
limit:
|
|
6671
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to check"),
|
|
6672
|
+
compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for descriptions."),
|
|
6673
|
+
limit: z2.number().default(25).describe("Max compact rows to return")
|
|
5583
6674
|
}, async ({ scope, compact, limit }) => {
|
|
5584
6675
|
const registered = getRegisteredHooks(scope);
|
|
5585
6676
|
const result = registered.map((name) => {
|
|
@@ -5609,18 +6700,18 @@ function createHooksServer() {
|
|
|
5609
6700
|
};
|
|
5610
6701
|
});
|
|
5611
6702
|
defineTool("hooks_run", "Execute a hook programmatically with the given input and return its output", {
|
|
5612
|
-
name:
|
|
5613
|
-
input:
|
|
5614
|
-
profile:
|
|
5615
|
-
timeout_ms:
|
|
6703
|
+
name: z2.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')"),
|
|
6704
|
+
input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input as JSON object (HookInput)"),
|
|
6705
|
+
profile: z2.string().optional().describe("Agent profile ID to inject into hook input"),
|
|
6706
|
+
timeout_ms: z2.number().default(1e4).describe("Timeout in milliseconds (default: 10000)")
|
|
5616
6707
|
}, async ({ name, input, profile, timeout_ms }) => {
|
|
5617
6708
|
const meta = getHook(name);
|
|
5618
6709
|
if (!meta) {
|
|
5619
6710
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
5620
6711
|
}
|
|
5621
6712
|
const hookDir = getHookPath(name);
|
|
5622
|
-
const hookScript =
|
|
5623
|
-
if (!
|
|
6713
|
+
const hookScript = join11(hookDir, "src", "hook.ts");
|
|
6714
|
+
if (!existsSync11(hookScript)) {
|
|
5624
6715
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
|
|
5625
6716
|
}
|
|
5626
6717
|
let hookInput = { ...input };
|
|
@@ -5635,29 +6726,28 @@ function createHooksServer() {
|
|
|
5635
6726
|
};
|
|
5636
6727
|
}
|
|
5637
6728
|
}
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
]);
|
|
6729
|
+
let ran = null;
|
|
6730
|
+
try {
|
|
6731
|
+
ran = await Promise.race([
|
|
6732
|
+
runVerifiedHook(name, hookScript, JSON.stringify(hookInput), timeout_ms),
|
|
6733
|
+
new Promise((resolve2) => setTimeout(() => resolve2(null), timeout_ms))
|
|
6734
|
+
]);
|
|
6735
|
+
} catch (err) {
|
|
6736
|
+
return {
|
|
6737
|
+
content: [{
|
|
6738
|
+
type: "text",
|
|
6739
|
+
text: JSON.stringify({ hook: name, error: err instanceof Error ? err.message : String(err), trust_failed: true })
|
|
6740
|
+
}]
|
|
6741
|
+
};
|
|
6742
|
+
}
|
|
6743
|
+
const timedOut = ran === null;
|
|
6744
|
+
const stdoutText = ran?.stdout ?? "";
|
|
6745
|
+
const stderrText = ran?.stderr ?? "";
|
|
5656
6746
|
let output = {};
|
|
5657
6747
|
try {
|
|
5658
|
-
output = JSON.parse(
|
|
6748
|
+
output = JSON.parse(stdoutText);
|
|
5659
6749
|
} catch {
|
|
5660
|
-
output =
|
|
6750
|
+
output = stdoutText ? { raw: stdoutText } : {};
|
|
5661
6751
|
}
|
|
5662
6752
|
return {
|
|
5663
6753
|
content: [{
|
|
@@ -5665,16 +6755,16 @@ function createHooksServer() {
|
|
|
5665
6755
|
text: JSON.stringify({
|
|
5666
6756
|
hook: name,
|
|
5667
6757
|
output,
|
|
5668
|
-
stderr:
|
|
5669
|
-
exitCode:
|
|
5670
|
-
...
|
|
6758
|
+
stderr: stderrText || undefined,
|
|
6759
|
+
exitCode: ran?.exitCode ?? -1,
|
|
6760
|
+
...timedOut ? { timedOut: true, timeout_ms } : {}
|
|
5671
6761
|
})
|
|
5672
6762
|
}]
|
|
5673
6763
|
};
|
|
5674
6764
|
});
|
|
5675
6765
|
defineTool("hooks_update", "Re-register installed hooks to pick up new package version (reinstalls with overwrite)", {
|
|
5676
|
-
hooks:
|
|
5677
|
-
scope:
|
|
6766
|
+
hooks: z2.array(z2.string()).optional().describe("Hook names to update (omit to update all installed hooks)"),
|
|
6767
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to update")
|
|
5678
6768
|
}, async ({ hooks, scope }) => {
|
|
5679
6769
|
const installed = getRegisteredHooks(scope);
|
|
5680
6770
|
const toUpdate = hooks && hooks.length > 0 ? hooks : installed;
|
|
@@ -5692,9 +6782,9 @@ function createHooksServer() {
|
|
|
5692
6782
|
return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
|
|
5693
6783
|
});
|
|
5694
6784
|
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:
|
|
6785
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to inspect"),
|
|
6786
|
+
profile: z2.string().optional().describe("Agent profile ID to include in context"),
|
|
6787
|
+
verbose: z2.boolean().default(false).describe("Include full profile preferences/details when true")
|
|
5698
6788
|
}, async ({ scope, profile, verbose }) => {
|
|
5699
6789
|
const settingsPath = getSettingsPath(scope);
|
|
5700
6790
|
const registered = getRegisteredHooks(scope);
|
|
@@ -5712,7 +6802,7 @@ function createHooksServer() {
|
|
|
5712
6802
|
const ctx = {
|
|
5713
6803
|
scope,
|
|
5714
6804
|
settings_path: settingsPath,
|
|
5715
|
-
settings_exists:
|
|
6805
|
+
settings_exists: existsSync11(settingsPath),
|
|
5716
6806
|
registered_hooks: hooks,
|
|
5717
6807
|
hook_count: hooks.length,
|
|
5718
6808
|
healthy,
|
|
@@ -5728,10 +6818,10 @@ function createHooksServer() {
|
|
|
5728
6818
|
return { content: [{ type: "text", text: JSON.stringify(ctx) }] };
|
|
5729
6819
|
});
|
|
5730
6820
|
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:
|
|
6821
|
+
tool_name: z2.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
|
|
6822
|
+
tool_input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
|
|
6823
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope to check"),
|
|
6824
|
+
timeout_ms: z2.number().default(5000).describe("Per-hook timeout in milliseconds")
|
|
5735
6825
|
}, async ({ tool_name, tool_input, scope, timeout_ms }) => {
|
|
5736
6826
|
const registered = getRegisteredHooks(scope);
|
|
5737
6827
|
const matchingHooks = registered.filter((name) => {
|
|
@@ -5752,28 +6842,23 @@ function createHooksServer() {
|
|
|
5752
6842
|
const input = { tool_name, tool_input };
|
|
5753
6843
|
const results = await Promise.all(matchingHooks.map(async (name) => {
|
|
5754
6844
|
const hookDir = getHookPath(name);
|
|
5755
|
-
const hookScript =
|
|
5756
|
-
if (!
|
|
6845
|
+
const hookScript = join11(hookDir, "src", "hook.ts");
|
|
6846
|
+
if (!existsSync11(hookScript))
|
|
5757
6847
|
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)
|
|
6848
|
+
let ran = null;
|
|
6849
|
+
try {
|
|
6850
|
+
ran = await Promise.race([
|
|
6851
|
+
runVerifiedHook(name, hookScript, JSON.stringify(input), timeout_ms),
|
|
6852
|
+
new Promise((r) => setTimeout(() => r(null), timeout_ms))
|
|
6853
|
+
]);
|
|
6854
|
+
} catch (err) {
|
|
6855
|
+
return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
|
|
6856
|
+
}
|
|
6857
|
+
if (ran === null)
|
|
5773
6858
|
return { name, decision: "approve", timedOut: true };
|
|
5774
6859
|
let output = {};
|
|
5775
6860
|
try {
|
|
5776
|
-
output = JSON.parse(
|
|
6861
|
+
output = JSON.parse(ran.stdout);
|
|
5777
6862
|
} catch {}
|
|
5778
6863
|
return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
|
|
5779
6864
|
}));
|
|
@@ -5793,10 +6878,10 @@ function createHooksServer() {
|
|
|
5793
6878
|
};
|
|
5794
6879
|
});
|
|
5795
6880
|
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:
|
|
6881
|
+
agent_type: z2.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
|
|
6882
|
+
name: z2.string().optional().describe("Optional display name for the agent"),
|
|
6883
|
+
hooks: z2.array(z2.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
|
|
6884
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Install scope")
|
|
5800
6885
|
}, async ({ agent_type, name, hooks, scope }) => {
|
|
5801
6886
|
const profile = createProfile({ agent_type, name });
|
|
5802
6887
|
const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
|
|
@@ -5811,52 +6896,49 @@ function createHooksServer() {
|
|
|
5811
6896
|
};
|
|
5812
6897
|
});
|
|
5813
6898
|
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:
|
|
6899
|
+
hooks: z2.array(z2.object({
|
|
6900
|
+
name: z2.string().describe("Hook name"),
|
|
6901
|
+
input: z2.record(z2.string(), z2.unknown()).default(() => ({})).describe("Hook input JSON")
|
|
5817
6902
|
})).describe("List of hooks to run with their inputs"),
|
|
5818
|
-
timeout_ms:
|
|
6903
|
+
timeout_ms: z2.number().default(1e4).describe("Per-hook timeout in milliseconds")
|
|
5819
6904
|
}, async ({ hooks, timeout_ms }) => {
|
|
5820
6905
|
const results = await Promise.all(hooks.map(async ({ name, input }) => {
|
|
5821
6906
|
const meta = getHook(name);
|
|
5822
6907
|
if (!meta)
|
|
5823
6908
|
return { name, error: `Hook '${name}' not found` };
|
|
5824
|
-
const hookScript =
|
|
5825
|
-
if (!
|
|
6909
|
+
const hookScript = join11(getHookPath(name), "src", "hook.ts");
|
|
6910
|
+
if (!existsSync11(hookScript))
|
|
5826
6911
|
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
|
-
]);
|
|
6912
|
+
let ran = null;
|
|
6913
|
+
try {
|
|
6914
|
+
ran = await Promise.race([
|
|
6915
|
+
runVerifiedHook(name, hookScript, JSON.stringify(input), timeout_ms),
|
|
6916
|
+
new Promise((r) => setTimeout(() => r(null), timeout_ms))
|
|
6917
|
+
]);
|
|
6918
|
+
} catch (err) {
|
|
6919
|
+
return { name, error: err instanceof Error ? err.message : String(err) };
|
|
6920
|
+
}
|
|
6921
|
+
const stdoutText = ran?.stdout ?? "";
|
|
6922
|
+
const timedOut = ran === null;
|
|
5841
6923
|
let output = {};
|
|
5842
6924
|
try {
|
|
5843
|
-
output = JSON.parse(
|
|
6925
|
+
output = JSON.parse(stdoutText);
|
|
5844
6926
|
} catch {
|
|
5845
|
-
output =
|
|
6927
|
+
output = stdoutText ? { raw: stdoutText } : {};
|
|
5846
6928
|
}
|
|
5847
|
-
return { name, output, exitCode:
|
|
6929
|
+
return { name, output, exitCode: ran?.exitCode ?? -1, ...timedOut ? { timedOut: true } : {} };
|
|
5848
6930
|
}));
|
|
5849
6931
|
return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }) }] };
|
|
5850
6932
|
});
|
|
5851
6933
|
defineTool("hooks_disable", "Temporarily disable a registered hook without removing it. Stores disabled list in settings under hooks.__disabled.", {
|
|
5852
|
-
name:
|
|
5853
|
-
scope:
|
|
6934
|
+
name: z2.string().describe("Hook name to disable"),
|
|
6935
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope")
|
|
5854
6936
|
}, async ({ name, scope }) => {
|
|
5855
6937
|
const settingsPath = getSettingsPath(scope);
|
|
5856
6938
|
let settings = {};
|
|
5857
6939
|
try {
|
|
5858
|
-
if (
|
|
5859
|
-
settings = JSON.parse(
|
|
6940
|
+
if (existsSync11(settingsPath))
|
|
6941
|
+
settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
|
|
5860
6942
|
} catch {}
|
|
5861
6943
|
if (!settings.hooks)
|
|
5862
6944
|
settings.hooks = {};
|
|
@@ -5864,43 +6946,43 @@ function createHooksServer() {
|
|
|
5864
6946
|
if (!disabled.includes(name))
|
|
5865
6947
|
disabled.push(name);
|
|
5866
6948
|
settings.hooks.__disabled = disabled;
|
|
5867
|
-
const { writeFileSync:
|
|
5868
|
-
const { dirname:
|
|
5869
|
-
|
|
5870
|
-
|
|
6949
|
+
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
|
|
6950
|
+
const { dirname: dirname5 } = await import("path");
|
|
6951
|
+
mkdirSync8(dirname5(settingsPath), { recursive: true });
|
|
6952
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
5871
6953
|
`);
|
|
5872
6954
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
5873
6955
|
});
|
|
5874
6956
|
defineTool("hooks_enable", "Re-enable a previously disabled hook.", {
|
|
5875
|
-
name:
|
|
5876
|
-
scope:
|
|
6957
|
+
name: z2.string().describe("Hook name to enable"),
|
|
6958
|
+
scope: z2.enum(["global", "project"]).default("global").describe("Scope")
|
|
5877
6959
|
}, async ({ name, scope }) => {
|
|
5878
6960
|
const settingsPath = getSettingsPath(scope);
|
|
5879
6961
|
let settings = {};
|
|
5880
6962
|
try {
|
|
5881
|
-
if (
|
|
5882
|
-
settings = JSON.parse(
|
|
6963
|
+
if (existsSync11(settingsPath))
|
|
6964
|
+
settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
|
|
5883
6965
|
} catch {}
|
|
5884
6966
|
if (settings.hooks?.__disabled) {
|
|
5885
6967
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
5886
6968
|
if (settings.hooks.__disabled.length === 0)
|
|
5887
6969
|
delete settings.hooks.__disabled;
|
|
5888
|
-
const { writeFileSync:
|
|
5889
|
-
|
|
6970
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
6971
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
5890
6972
|
`);
|
|
5891
6973
|
}
|
|
5892
6974
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
|
|
5893
6975
|
});
|
|
5894
6976
|
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:
|
|
6977
|
+
agent_type: z2.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
|
|
6978
|
+
name: z2.string().optional().describe("Optional display name for the agent")
|
|
5897
6979
|
}, async ({ agent_type, name }) => {
|
|
5898
6980
|
const profile = createProfile({ agent_type, name });
|
|
5899
6981
|
return { content: [{ type: "text", text: JSON.stringify(profile) }] };
|
|
5900
6982
|
});
|
|
5901
6983
|
defineTool("hooks_profiles", "List registered agent profiles. Compact by default.", {
|
|
5902
|
-
compact:
|
|
5903
|
-
limit:
|
|
6984
|
+
compact: z2.boolean().default(true).describe("Return compact summaries by default. Set false for full profiles."),
|
|
6985
|
+
limit: z2.number().default(25).describe("Max compact rows to return")
|
|
5904
6986
|
}, async ({ compact, limit }) => {
|
|
5905
6987
|
const profiles = listProfiles();
|
|
5906
6988
|
if (!compact)
|
|
@@ -5926,11 +7008,11 @@ function createHooksServer() {
|
|
|
5926
7008
|
};
|
|
5927
7009
|
});
|
|
5928
7010
|
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:
|
|
7011
|
+
hook_name: z2.string().optional().describe("Filter by hook name (e.g. 'sessionlog', 'costwatch')"),
|
|
7012
|
+
session_id: z2.string().optional().describe("Filter by session ID prefix"),
|
|
7013
|
+
limit: z2.number().optional().describe("Max number of events to return. Defaults to 20 compact rows or 50 full rows."),
|
|
7014
|
+
since: z2.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from"),
|
|
7015
|
+
compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
|
|
5934
7016
|
}, async ({ hook_name, session_id, limit, since, compact }) => {
|
|
5935
7017
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
5936
7018
|
const db = getDb2();
|
|
@@ -5976,8 +7058,8 @@ function createHooksServer() {
|
|
|
5976
7058
|
};
|
|
5977
7059
|
});
|
|
5978
7060
|
defineTool("hooks_log_tail", "Show recent hook events from SQLite. Compact summaries by default.", {
|
|
5979
|
-
n:
|
|
5980
|
-
compact:
|
|
7061
|
+
n: z2.number().default(20).describe("Number of most recent events to return"),
|
|
7062
|
+
compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
|
|
5981
7063
|
}, async ({ n, compact }) => {
|
|
5982
7064
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
5983
7065
|
const db = getDb2();
|
|
@@ -5996,9 +7078,9 @@ function createHooksServer() {
|
|
|
5996
7078
|
};
|
|
5997
7079
|
});
|
|
5998
7080
|
defineTool("hooks_log_errors", "Show hook events that contain errors. Compact summaries by default.", {
|
|
5999
|
-
since:
|
|
6000
|
-
limit:
|
|
6001
|
-
compact:
|
|
7081
|
+
since: z2.string().default("24h").describe("Duration string (e.g. '1h', '30m', '7d') or ISO timestamp"),
|
|
7082
|
+
limit: z2.number().optional().describe("Max number of error events to return. Defaults to 20 compact rows or 50 full rows."),
|
|
7083
|
+
compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
|
|
6002
7084
|
}, async ({ since, limit, compact }) => {
|
|
6003
7085
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
6004
7086
|
const db = getDb2();
|
|
@@ -6026,7 +7108,7 @@ function createHooksServer() {
|
|
|
6026
7108
|
};
|
|
6027
7109
|
});
|
|
6028
7110
|
defineTool("hooks_log_summary", "Summarize hook execution: counts per hook, error rates, and recent activity.", {
|
|
6029
|
-
since:
|
|
7111
|
+
since: z2.string().default("24h").describe("Duration string (e.g. '1h', '24h', '7d') or ISO timestamp")
|
|
6030
7112
|
}, async ({ since }) => {
|
|
6031
7113
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
6032
7114
|
const db = getDb2();
|
|
@@ -6060,13 +7142,13 @@ function createHooksServer() {
|
|
|
6060
7142
|
};
|
|
6061
7143
|
});
|
|
6062
7144
|
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:
|
|
7145
|
+
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)) }] }));
|
|
7146
|
+
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)) }] }));
|
|
7147
|
+
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
7148
|
defineTool("send_feedback", "Send feedback about this service", {
|
|
6067
|
-
message:
|
|
6068
|
-
email:
|
|
6069
|
-
category:
|
|
7149
|
+
message: z2.string(),
|
|
7150
|
+
email: z2.string().optional(),
|
|
7151
|
+
category: z2.enum(["bug", "feature", "general"]).optional()
|
|
6070
7152
|
}, async (params) => {
|
|
6071
7153
|
try {
|
|
6072
7154
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
@@ -6083,8 +7165,8 @@ function createHooksServer() {
|
|
|
6083
7165
|
}
|
|
6084
7166
|
});
|
|
6085
7167
|
defineTool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
|
|
6086
|
-
name:
|
|
6087
|
-
session_id:
|
|
7168
|
+
name: z2.string(),
|
|
7169
|
+
session_id: z2.string().optional()
|
|
6088
7170
|
}, async (params) => {
|
|
6089
7171
|
const existing = [..._hooksAgents.values()].find((a) => a.name === params.name);
|
|
6090
7172
|
if (existing) {
|
|
@@ -6099,7 +7181,7 @@ function createHooksServer() {
|
|
|
6099
7181
|
return { content: [{ type: "text", text: JSON.stringify(ag) }] };
|
|
6100
7182
|
});
|
|
6101
7183
|
defineTool("heartbeat", "Update last_seen_at to signal agent is active.", {
|
|
6102
|
-
agent_id:
|
|
7184
|
+
agent_id: z2.string()
|
|
6103
7185
|
}, async (params) => {
|
|
6104
7186
|
const ag = _hooksAgents.get(params.agent_id);
|
|
6105
7187
|
if (!ag)
|
|
@@ -6108,8 +7190,8 @@ function createHooksServer() {
|
|
|
6108
7190
|
return { content: [{ type: "text", text: JSON.stringify({ agent_id: ag.id, last_seen_at: ag.last_seen_at }) }] };
|
|
6109
7191
|
});
|
|
6110
7192
|
defineTool("set_focus", "Set active project context for this agent session.", {
|
|
6111
|
-
agent_id:
|
|
6112
|
-
project_id:
|
|
7193
|
+
agent_id: z2.string(),
|
|
7194
|
+
project_id: z2.string().optional()
|
|
6113
7195
|
}, async (params) => {
|
|
6114
7196
|
const ag = _hooksAgents.get(params.agent_id);
|
|
6115
7197
|
if (!ag)
|
|
@@ -6118,8 +7200,8 @@ function createHooksServer() {
|
|
|
6118
7200
|
return { content: [{ type: "text", text: JSON.stringify({ agent_id: ag.id, project_id: ag.project_id ?? null }) }] };
|
|
6119
7201
|
});
|
|
6120
7202
|
defineTool("list_agents", "List registered agents. Compact by default.", {
|
|
6121
|
-
compact:
|
|
6122
|
-
limit:
|
|
7203
|
+
compact: z2.boolean().default(true).describe("Return compact agent summaries by default. Set false for full records."),
|
|
7204
|
+
limit: z2.number().default(25).describe("Max compact rows to return")
|
|
6123
7205
|
}, async ({ compact, limit }) => {
|
|
6124
7206
|
const agents = [..._hooksAgents.values()];
|
|
6125
7207
|
if (!compact)
|
|
@@ -6194,14 +7276,17 @@ var init_server = __esm(() => {
|
|
|
6194
7276
|
init_registry();
|
|
6195
7277
|
init_installer();
|
|
6196
7278
|
init_profiles();
|
|
7279
|
+
init_manifest();
|
|
7280
|
+
init_resolve();
|
|
7281
|
+
init_store();
|
|
6197
7282
|
init_storage();
|
|
6198
|
-
__dirname3 =
|
|
7283
|
+
__dirname3 = dirname4(fileURLToPath2(import.meta.url));
|
|
6199
7284
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
6200
7285
|
try {
|
|
6201
7286
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
6202
|
-
const p =
|
|
6203
|
-
if (
|
|
6204
|
-
pkg = JSON.parse(
|
|
7287
|
+
const p = join11(__dirname3, rel);
|
|
7288
|
+
if (existsSync11(p)) {
|
|
7289
|
+
pkg = JSON.parse(readFileSync9(p, "utf-8"));
|
|
6205
7290
|
break;
|
|
6206
7291
|
}
|
|
6207
7292
|
}
|
|
@@ -6268,8 +7353,9 @@ function startMcpHttpServer(options) {
|
|
|
6268
7353
|
var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
|
|
6269
7354
|
var init_http = () => {};
|
|
6270
7355
|
|
|
6271
|
-
// node_modules/@hasna/events/dist/commander.js
|
|
7356
|
+
// ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
|
|
6272
7357
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
7358
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
6273
7359
|
import { existsSync } from "fs";
|
|
6274
7360
|
import { homedir } from "os";
|
|
6275
7361
|
import { join } from "path";
|
|
@@ -6374,6 +7460,9 @@ function channelMatchesEvent(channel, event) {
|
|
|
6374
7460
|
}
|
|
6375
7461
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
6376
7462
|
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
7463
|
+
var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
|
|
7464
|
+
var DEFAULT_EVENT_PAGE_LIMIT = 100;
|
|
7465
|
+
var MAX_EVENT_PAGE_LIMIT = 1000;
|
|
6377
7466
|
function getEventsDataDir(override) {
|
|
6378
7467
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
6379
7468
|
}
|
|
@@ -6387,11 +7476,13 @@ function getActiveEventsDirEnv() {
|
|
|
6387
7476
|
|
|
6388
7477
|
class JsonEventsStore {
|
|
6389
7478
|
dataDir;
|
|
7479
|
+
runtime;
|
|
6390
7480
|
channelsPath;
|
|
6391
7481
|
eventsPath;
|
|
6392
7482
|
deliveriesPath;
|
|
6393
7483
|
constructor(dataDir = getEventsDataDir()) {
|
|
6394
7484
|
this.dataDir = dataDir;
|
|
7485
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
6395
7486
|
this.channelsPath = join(dataDir, "channels.json");
|
|
6396
7487
|
this.eventsPath = join(dataDir, "events.json");
|
|
6397
7488
|
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
@@ -6439,13 +7530,58 @@ class JsonEventsStore {
|
|
|
6439
7530
|
await this.writeJson(this.eventsPath, events);
|
|
6440
7531
|
return event;
|
|
6441
7532
|
}
|
|
6442
|
-
async
|
|
7533
|
+
async appendEventOnce(event, options = {}) {
|
|
6443
7534
|
await this.init();
|
|
6444
|
-
|
|
7535
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
7536
|
+
const dedupe = options.dedupe !== false;
|
|
7537
|
+
if (dedupe) {
|
|
7538
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
7539
|
+
if (existing) {
|
|
7540
|
+
return {
|
|
7541
|
+
event: existing,
|
|
7542
|
+
stored: false,
|
|
7543
|
+
deduped: true,
|
|
7544
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
7545
|
+
};
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
events.push(event);
|
|
7549
|
+
await this.writeJson(this.eventsPath, events);
|
|
7550
|
+
return {
|
|
7551
|
+
event,
|
|
7552
|
+
stored: true,
|
|
7553
|
+
deduped: false,
|
|
7554
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
7555
|
+
};
|
|
7556
|
+
}
|
|
7557
|
+
async listEvents(options = {}) {
|
|
7558
|
+
await this.init();
|
|
7559
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
7560
|
+
return queryEvents(events, options);
|
|
7561
|
+
}
|
|
7562
|
+
async listEventsPage(options = {}) {
|
|
7563
|
+
await this.init();
|
|
7564
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
7565
|
+
const queried = queryEvents(events, {
|
|
7566
|
+
eventId: options.eventId,
|
|
7567
|
+
source: options.source,
|
|
7568
|
+
type: options.type
|
|
7569
|
+
});
|
|
7570
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
7571
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
7572
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
7573
|
+
const nextOffset = offset + pageEvents.length;
|
|
7574
|
+
const hasMore = nextOffset < queried.length;
|
|
7575
|
+
return {
|
|
7576
|
+
events: pageEvents,
|
|
7577
|
+
cursor: options.cursor,
|
|
7578
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
7579
|
+
hasMore
|
|
7580
|
+
};
|
|
6445
7581
|
}
|
|
6446
7582
|
async findEventByIdentity(identity) {
|
|
6447
7583
|
const events = await this.listEvents();
|
|
6448
|
-
return events
|
|
7584
|
+
return findEventByIdentity(events, identity);
|
|
6449
7585
|
}
|
|
6450
7586
|
async appendDelivery(result) {
|
|
6451
7587
|
await this.init();
|
|
@@ -6496,6 +7632,83 @@ class JsonEventsStore {
|
|
|
6496
7632
|
});
|
|
6497
7633
|
}
|
|
6498
7634
|
}
|
|
7635
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
7636
|
+
return {
|
|
7637
|
+
mode: "local-files",
|
|
7638
|
+
name: "json-events-store",
|
|
7639
|
+
remote: false,
|
|
7640
|
+
localFiles: true,
|
|
7641
|
+
localSqlite: false,
|
|
7642
|
+
postgres: false,
|
|
7643
|
+
s3: false,
|
|
7644
|
+
aws: false,
|
|
7645
|
+
durable: true,
|
|
7646
|
+
idempotency: "best-effort-local",
|
|
7647
|
+
replayCursors: true,
|
|
7648
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
7649
|
+
};
|
|
7650
|
+
}
|
|
7651
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
7652
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
7653
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
7654
|
+
const payload = {
|
|
7655
|
+
offset,
|
|
7656
|
+
eventId: options.eventId,
|
|
7657
|
+
source: options.source,
|
|
7658
|
+
type: options.type
|
|
7659
|
+
};
|
|
7660
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
7661
|
+
}
|
|
7662
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
7663
|
+
if (!cursor)
|
|
7664
|
+
return 0;
|
|
7665
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
7666
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
7667
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
7668
|
+
let payload;
|
|
7669
|
+
try {
|
|
7670
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
7671
|
+
} catch {
|
|
7672
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
7673
|
+
}
|
|
7674
|
+
const offset = payload.offset;
|
|
7675
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
7676
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
7677
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
7678
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
7679
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
7680
|
+
return offset;
|
|
7681
|
+
}
|
|
7682
|
+
function normalizeEventPageLimit(limit) {
|
|
7683
|
+
if (limit === undefined)
|
|
7684
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
7685
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
7686
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
7687
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
7688
|
+
}
|
|
7689
|
+
function queryEvents(events, options) {
|
|
7690
|
+
let rows = events;
|
|
7691
|
+
if (options.eventId)
|
|
7692
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
7693
|
+
if (options.source)
|
|
7694
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
7695
|
+
if (options.type)
|
|
7696
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
7697
|
+
if (options.cursor) {
|
|
7698
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
7699
|
+
rows = rows.slice(offset);
|
|
7700
|
+
}
|
|
7701
|
+
if (options.limit !== undefined)
|
|
7702
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
7703
|
+
return rows;
|
|
7704
|
+
}
|
|
7705
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
7706
|
+
if (cursorValue !== optionValue)
|
|
7707
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
7708
|
+
}
|
|
7709
|
+
function findEventByIdentity(events, identity) {
|
|
7710
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
7711
|
+
}
|
|
6499
7712
|
async function getEventsStatus(dataDir) {
|
|
6500
7713
|
const store = new JsonEventsStore(dataDir);
|
|
6501
7714
|
await store.init();
|
|
@@ -6512,6 +7725,7 @@ async function getEventsStatus(dataDir) {
|
|
|
6512
7725
|
service: "events",
|
|
6513
7726
|
schemaVersion: "1.0",
|
|
6514
7727
|
dataDir: store.dataDir,
|
|
7728
|
+
storage: store.runtime,
|
|
6515
7729
|
env: {
|
|
6516
7730
|
primary: HASNA_EVENTS_DIR_ENV,
|
|
6517
7731
|
fallback: HASNA_EVENTS_HOME_ENV,
|
|
@@ -6556,21 +7770,27 @@ function now() {
|
|
|
6556
7770
|
function truncate(value, max = 4096) {
|
|
6557
7771
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
6558
7772
|
}
|
|
6559
|
-
function buildWebhookRequest(event, channel) {
|
|
7773
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
6560
7774
|
if (!channel.webhook)
|
|
6561
7775
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
7776
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
7777
|
+
if (/^x-hasna-/i.test(name)) {
|
|
7778
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
6562
7781
|
const body = JSON.stringify(event);
|
|
6563
|
-
const timestamp =
|
|
7782
|
+
const timestamp = options.timestamp ?? new Date().toISOString();
|
|
6564
7783
|
const headers = {
|
|
6565
7784
|
"Content-Type": "application/json",
|
|
6566
7785
|
"User-Agent": "@hasna/events",
|
|
6567
7786
|
"X-Hasna-Event-Id": event.id,
|
|
6568
7787
|
"X-Hasna-Event-Type": event.type,
|
|
6569
|
-
|
|
6570
|
-
|
|
7788
|
+
...channel.webhook.headers,
|
|
7789
|
+
"X-Hasna-Timestamp": timestamp
|
|
6571
7790
|
};
|
|
6572
|
-
|
|
6573
|
-
|
|
7791
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
7792
|
+
if (secret) {
|
|
7793
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
|
|
6574
7794
|
}
|
|
6575
7795
|
return { body, headers };
|
|
6576
7796
|
}
|
|
@@ -6578,7 +7798,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6578
7798
|
if (!channel.webhook)
|
|
6579
7799
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
6580
7800
|
const startedAt = now();
|
|
6581
|
-
|
|
7801
|
+
let secret = channel.webhook.secret;
|
|
7802
|
+
if (channel.webhook.secretRef) {
|
|
7803
|
+
if (!options.secretResolver) {
|
|
7804
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
7805
|
+
}
|
|
7806
|
+
try {
|
|
7807
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
7808
|
+
} catch {
|
|
7809
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
7810
|
+
}
|
|
7811
|
+
if (!secret)
|
|
7812
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
7813
|
+
}
|
|
7814
|
+
const timestamp = (options.now?.() ?? new Date).toISOString();
|
|
7815
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
|
|
6582
7816
|
const controller = new AbortController;
|
|
6583
7817
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
6584
7818
|
try {
|
|
@@ -6610,6 +7844,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6610
7844
|
clearTimeout(timeout);
|
|
6611
7845
|
}
|
|
6612
7846
|
}
|
|
7847
|
+
function failedAttempt(startedAt, error) {
|
|
7848
|
+
return {
|
|
7849
|
+
attempt: 1,
|
|
7850
|
+
status: "failed",
|
|
7851
|
+
startedAt,
|
|
7852
|
+
completedAt: now(),
|
|
7853
|
+
error
|
|
7854
|
+
};
|
|
7855
|
+
}
|
|
6613
7856
|
async function dispatchCommand(event, channel) {
|
|
6614
7857
|
if (!channel.command)
|
|
6615
7858
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -6698,6 +7941,90 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
6698
7941
|
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
6699
7942
|
};
|
|
6700
7943
|
}
|
|
7944
|
+
|
|
7945
|
+
class EventValidationError extends Error {
|
|
7946
|
+
eventType;
|
|
7947
|
+
issues;
|
|
7948
|
+
constructor(eventType, issues) {
|
|
7949
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
7950
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
7951
|
+
this.name = "EventValidationError";
|
|
7952
|
+
this.eventType = eventType;
|
|
7953
|
+
this.issues = issues;
|
|
7954
|
+
}
|
|
7955
|
+
}
|
|
7956
|
+
|
|
7957
|
+
class EventTypeCatalog {
|
|
7958
|
+
definitions = new Map;
|
|
7959
|
+
register(definition) {
|
|
7960
|
+
this.definitions.set(definition.type, definition);
|
|
7961
|
+
return this;
|
|
7962
|
+
}
|
|
7963
|
+
unregister(type) {
|
|
7964
|
+
return this.definitions.delete(type);
|
|
7965
|
+
}
|
|
7966
|
+
has(type) {
|
|
7967
|
+
return this.definitions.has(type);
|
|
7968
|
+
}
|
|
7969
|
+
get(type) {
|
|
7970
|
+
return this.definitions.get(type);
|
|
7971
|
+
}
|
|
7972
|
+
list() {
|
|
7973
|
+
return [...this.definitions.values()];
|
|
7974
|
+
}
|
|
7975
|
+
validateEvent(event) {
|
|
7976
|
+
const definition = this.definitions.get(event.type);
|
|
7977
|
+
if (!definition)
|
|
7978
|
+
return { ok: true };
|
|
7979
|
+
return definition.validate(event.data, event);
|
|
7980
|
+
}
|
|
7981
|
+
assertEventValid(event) {
|
|
7982
|
+
const result = this.validateEvent(event);
|
|
7983
|
+
if (!result.ok) {
|
|
7984
|
+
throw new EventValidationError(event.type, result.issues);
|
|
7985
|
+
}
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
var defaultEventTypeCatalog = new EventTypeCatalog;
|
|
7989
|
+
var APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
7990
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
7991
|
+
if (paths.length === 0)
|
|
7992
|
+
return event;
|
|
7993
|
+
const copy = structuredClone(event);
|
|
7994
|
+
for (const path of paths) {
|
|
7995
|
+
setPath(copy, path, replacement);
|
|
7996
|
+
}
|
|
7997
|
+
return copy;
|
|
7998
|
+
}
|
|
7999
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
8000
|
+
return redactValue(event, replacement);
|
|
8001
|
+
}
|
|
8002
|
+
function shouldRedactKey(key) {
|
|
8003
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
8004
|
+
}
|
|
8005
|
+
function redactValue(value, replacement) {
|
|
8006
|
+
if (Array.isArray(value))
|
|
8007
|
+
return value.map((item) => redactValue(item, replacement));
|
|
8008
|
+
if (!value || typeof value !== "object")
|
|
8009
|
+
return value;
|
|
8010
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
8011
|
+
key,
|
|
8012
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
8013
|
+
]));
|
|
8014
|
+
}
|
|
8015
|
+
function setPath(input, path, replacement) {
|
|
8016
|
+
const parts = path.split(".");
|
|
8017
|
+
let cursor = input;
|
|
8018
|
+
for (const part of parts.slice(0, -1)) {
|
|
8019
|
+
const next = cursor[part];
|
|
8020
|
+
if (!next || typeof next !== "object")
|
|
8021
|
+
return;
|
|
8022
|
+
cursor = next;
|
|
8023
|
+
}
|
|
8024
|
+
const last = parts.at(-1);
|
|
8025
|
+
if (last && last in cursor)
|
|
8026
|
+
cursor[last] = replacement;
|
|
8027
|
+
}
|
|
6701
8028
|
function createEvent(input) {
|
|
6702
8029
|
return {
|
|
6703
8030
|
id: input.id ?? randomUUID2(),
|
|
@@ -6718,10 +8045,18 @@ class EventsClient {
|
|
|
6718
8045
|
store;
|
|
6719
8046
|
redactors;
|
|
6720
8047
|
transportOptions;
|
|
8048
|
+
catalog;
|
|
8049
|
+
validateCatalogTypes;
|
|
6721
8050
|
constructor(options = {}) {
|
|
6722
8051
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
6723
8052
|
this.redactors = options.redactors ?? [];
|
|
6724
|
-
this.transportOptions = {
|
|
8053
|
+
this.transportOptions = {
|
|
8054
|
+
fetchImpl: options.fetchImpl,
|
|
8055
|
+
secretResolver: options.secretResolver,
|
|
8056
|
+
now: options.now
|
|
8057
|
+
};
|
|
8058
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
8059
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
6725
8060
|
}
|
|
6726
8061
|
async addChannel(input) {
|
|
6727
8062
|
const timestamp = new Date().toISOString();
|
|
@@ -6739,18 +8074,40 @@ class EventsClient {
|
|
|
6739
8074
|
}
|
|
6740
8075
|
async emit(input, options = {}) {
|
|
6741
8076
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
6742
|
-
if (options.
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
6750
|
-
return { event, deliveries, deduped: false };
|
|
6751
|
-
}
|
|
6752
|
-
async listEvents() {
|
|
6753
|
-
|
|
8077
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
8078
|
+
this.catalog.assertEventValid(event);
|
|
8079
|
+
}
|
|
8080
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
8081
|
+
if (append.deduped) {
|
|
8082
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
8083
|
+
}
|
|
8084
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
8085
|
+
return { event: append.event, deliveries, deduped: false };
|
|
8086
|
+
}
|
|
8087
|
+
async listEvents(options = {}) {
|
|
8088
|
+
if (Object.keys(options).length === 0)
|
|
8089
|
+
return this.store.listEvents();
|
|
8090
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
8091
|
+
}
|
|
8092
|
+
async listEventsPage(options = {}) {
|
|
8093
|
+
if (this.store.listEventsPage)
|
|
8094
|
+
return this.store.listEventsPage(options);
|
|
8095
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
8096
|
+
eventId: options.eventId,
|
|
8097
|
+
source: options.source,
|
|
8098
|
+
type: options.type
|
|
8099
|
+
});
|
|
8100
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
8101
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
8102
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
8103
|
+
const nextOffset = offset + pageEvents.length;
|
|
8104
|
+
const hasMore = nextOffset < events.length;
|
|
8105
|
+
return {
|
|
8106
|
+
events: pageEvents,
|
|
8107
|
+
cursor: options.cursor,
|
|
8108
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
8109
|
+
hasMore
|
|
8110
|
+
};
|
|
6754
8111
|
}
|
|
6755
8112
|
async listDeliveries() {
|
|
6756
8113
|
return this.store.listDeliveries();
|
|
@@ -6818,22 +8175,37 @@ class EventsClient {
|
|
|
6818
8175
|
return result;
|
|
6819
8176
|
}
|
|
6820
8177
|
async replay(options = {}) {
|
|
6821
|
-
const
|
|
6822
|
-
if (options.eventId && event.id !== options.eventId)
|
|
6823
|
-
return false;
|
|
6824
|
-
if (options.source && event.source !== options.source)
|
|
6825
|
-
return false;
|
|
6826
|
-
if (options.type && event.type !== options.type)
|
|
6827
|
-
return false;
|
|
6828
|
-
return true;
|
|
6829
|
-
});
|
|
8178
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
6830
8179
|
if (options.dryRun)
|
|
6831
|
-
return { events, deliveries: [] };
|
|
8180
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
6832
8181
|
const deliveries = [];
|
|
6833
|
-
for (const event of events) {
|
|
8182
|
+
for (const event of page.events) {
|
|
6834
8183
|
deliveries.push(...await this.deliver(event));
|
|
6835
8184
|
}
|
|
6836
|
-
return { events, deliveries };
|
|
8185
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
8186
|
+
}
|
|
8187
|
+
async appendEvent(event, options) {
|
|
8188
|
+
if (this.store.appendEventOnce) {
|
|
8189
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
8190
|
+
}
|
|
8191
|
+
if (options.dedupe) {
|
|
8192
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
8193
|
+
if (existing) {
|
|
8194
|
+
return {
|
|
8195
|
+
event: existing,
|
|
8196
|
+
stored: false,
|
|
8197
|
+
deduped: true,
|
|
8198
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
8199
|
+
};
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
const stored = await this.store.appendEvent(event);
|
|
8203
|
+
return {
|
|
8204
|
+
event: stored,
|
|
8205
|
+
stored: true,
|
|
8206
|
+
deduped: false,
|
|
8207
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
8208
|
+
};
|
|
6837
8209
|
}
|
|
6838
8210
|
async applyRedaction(event, channel) {
|
|
6839
8211
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -6860,15 +8232,6 @@ class EventsClient {
|
|
|
6860
8232
|
return createDeliveryResult(event, channel, attempts);
|
|
6861
8233
|
}
|
|
6862
8234
|
}
|
|
6863
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
6864
|
-
if (paths.length === 0)
|
|
6865
|
-
return event;
|
|
6866
|
-
const copy = structuredClone(event);
|
|
6867
|
-
for (const path of paths) {
|
|
6868
|
-
setPath(copy, path, replacement);
|
|
6869
|
-
}
|
|
6870
|
-
return copy;
|
|
6871
|
-
}
|
|
6872
8235
|
function sanitizeChannelForOutput(channel) {
|
|
6873
8236
|
const copy = structuredClone(channel);
|
|
6874
8237
|
if (copy.webhook?.secret)
|
|
@@ -6881,34 +8244,19 @@ function sanitizeChannelForOutput(channel) {
|
|
|
6881
8244
|
function sanitizeChannelsForOutput(channels) {
|
|
6882
8245
|
return channels.map(sanitizeChannelForOutput);
|
|
6883
8246
|
}
|
|
6884
|
-
function
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
6898
|
-
]));
|
|
6899
|
-
}
|
|
6900
|
-
function setPath(input, path, replacement) {
|
|
6901
|
-
const parts = path.split(".");
|
|
6902
|
-
let cursor = input;
|
|
6903
|
-
for (const part of parts.slice(0, -1)) {
|
|
6904
|
-
const next = cursor[part];
|
|
6905
|
-
if (!next || typeof next !== "object")
|
|
6906
|
-
return;
|
|
6907
|
-
cursor = next;
|
|
6908
|
-
}
|
|
6909
|
-
const last = parts.at(-1);
|
|
6910
|
-
if (last && last in cursor)
|
|
6911
|
-
cursor[last] = replacement;
|
|
8247
|
+
function queryClientEvents(events, options) {
|
|
8248
|
+
let rows = events;
|
|
8249
|
+
if (options.eventId)
|
|
8250
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
8251
|
+
if (options.source)
|
|
8252
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
8253
|
+
if (options.type)
|
|
8254
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
8255
|
+
if (options.cursor)
|
|
8256
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
8257
|
+
if (options.limit !== undefined)
|
|
8258
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
8259
|
+
return rows;
|
|
6912
8260
|
}
|
|
6913
8261
|
function normalizeTime(value) {
|
|
6914
8262
|
if (!value)
|
|
@@ -6992,6 +8340,7 @@ function parseMatcherExpression(value, label) {
|
|
|
6992
8340
|
negated: false
|
|
6993
8341
|
};
|
|
6994
8342
|
}
|
|
8343
|
+
var DEFAULT_EVENT_LIST_LIMIT = 100;
|
|
6995
8344
|
function parseJsonObject(value, fallback) {
|
|
6996
8345
|
if (!value)
|
|
6997
8346
|
return fallback;
|
|
@@ -7024,6 +8373,14 @@ function print(value, json, text) {
|
|
|
7024
8373
|
else
|
|
7025
8374
|
console.log(text);
|
|
7026
8375
|
}
|
|
8376
|
+
function fail(error, json) {
|
|
8377
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8378
|
+
if (json)
|
|
8379
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
8380
|
+
else
|
|
8381
|
+
console.error(message);
|
|
8382
|
+
process.exitCode = 1;
|
|
8383
|
+
}
|
|
7027
8384
|
function hasJsonOption(options) {
|
|
7028
8385
|
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
7029
8386
|
}
|
|
@@ -7078,26 +8435,36 @@ function registerChannelCommands(program, options) {
|
|
|
7078
8435
|
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
7079
8436
|
});
|
|
7080
8437
|
channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
7081
|
-
const
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
8438
|
+
const json = wantsJson(actionOptions, command);
|
|
8439
|
+
try {
|
|
8440
|
+
const result = await createClient(options).testChannel(id, {
|
|
8441
|
+
source: actionOptions.source ?? options.source,
|
|
8442
|
+
type: actionOptions.type,
|
|
8443
|
+
subject: actionOptions.subject ?? id,
|
|
8444
|
+
message: actionOptions.message,
|
|
8445
|
+
data: parseJsonObject(actionOptions.data, { test: true }),
|
|
8446
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
8447
|
+
}, { honorFilters: actionOptions.honorFilters });
|
|
8448
|
+
print(result, json, `${result.status}: ${result.channelId}`);
|
|
8449
|
+
} catch (error) {
|
|
8450
|
+
fail(error, json);
|
|
8451
|
+
}
|
|
7090
8452
|
});
|
|
7091
8453
|
channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
7092
|
-
const
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
8454
|
+
const json = wantsJson(actionOptions, command);
|
|
8455
|
+
try {
|
|
8456
|
+
const result = await createClient(options).matchChannel(id, {
|
|
8457
|
+
source: actionOptions.source ?? options.source,
|
|
8458
|
+
type: actionOptions.type,
|
|
8459
|
+
subject: actionOptions.subject ?? id,
|
|
8460
|
+
message: actionOptions.message,
|
|
8461
|
+
data: parseJsonObject(actionOptions.data, { test: true }),
|
|
8462
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
8463
|
+
});
|
|
8464
|
+
print(result, json, `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
|
|
8465
|
+
} catch (error) {
|
|
8466
|
+
fail(error, json);
|
|
8467
|
+
}
|
|
7101
8468
|
});
|
|
7102
8469
|
return channels;
|
|
7103
8470
|
}
|
|
@@ -7116,7 +8483,8 @@ function registerEventCommands(program, options) {
|
|
|
7116
8483
|
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
7117
8484
|
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
7118
8485
|
});
|
|
7119
|
-
|
|
8486
|
+
const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
|
|
8487
|
+
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
7120
8488
|
let rows = await createClient(options).listEvents();
|
|
7121
8489
|
if (actionOptions.source)
|
|
7122
8490
|
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
@@ -7135,14 +8503,16 @@ function registerEventCommands(program, options) {
|
|
|
7135
8503
|
for (const event of rows)
|
|
7136
8504
|
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
7137
8505
|
});
|
|
7138
|
-
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
8506
|
+
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
7139
8507
|
const result = await createClient(options).replay({
|
|
7140
8508
|
eventId: actionOptions.id,
|
|
7141
8509
|
source: actionOptions.source,
|
|
7142
8510
|
type: actionOptions.type,
|
|
8511
|
+
cursor: actionOptions.cursor,
|
|
8512
|
+
limit: actionOptions.limit,
|
|
7143
8513
|
dryRun: actionOptions.dryRun
|
|
7144
8514
|
});
|
|
7145
|
-
print(result, wantsJson(actionOptions, command),
|
|
8515
|
+
print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
|
|
7146
8516
|
});
|
|
7147
8517
|
return events;
|
|
7148
8518
|
}
|
|
@@ -7160,11 +8530,15 @@ function collectValues(value, previous) {
|
|
|
7160
8530
|
previous.push(value);
|
|
7161
8531
|
return previous;
|
|
7162
8532
|
}
|
|
8533
|
+
function replaySummary(events, deliveries, nextCursor) {
|
|
8534
|
+
const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
|
|
8535
|
+
return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
|
|
8536
|
+
}
|
|
7163
8537
|
|
|
7164
8538
|
// src/cli/index.tsx
|
|
7165
8539
|
import { render } from "ink";
|
|
7166
8540
|
|
|
7167
|
-
// node_modules/commander/esm.mjs
|
|
8541
|
+
// ../../node_modules/.bun/commander@12.1.0/node_modules/commander/esm.mjs
|
|
7168
8542
|
var import__ = __toESM(require_commander(), 1);
|
|
7169
8543
|
var {
|
|
7170
8544
|
program,
|
|
@@ -7182,19 +8556,19 @@ var {
|
|
|
7182
8556
|
|
|
7183
8557
|
// src/cli/index.tsx
|
|
7184
8558
|
import chalk2 from "chalk";
|
|
7185
|
-
import { existsSync as
|
|
7186
|
-
import { join as
|
|
8559
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
8560
|
+
import { join as join12, dirname as dirname5 } from "path";
|
|
7187
8561
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7188
8562
|
|
|
7189
8563
|
// src/cli/components/App.tsx
|
|
7190
8564
|
import { useState as useState7 } from "react";
|
|
7191
8565
|
import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
|
|
7192
8566
|
|
|
7193
|
-
// node_modules/ink-select-input/build/Indicator.js
|
|
8567
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
|
|
7194
8568
|
import React from "react";
|
|
7195
8569
|
import { Box, Text } from "ink";
|
|
7196
8570
|
|
|
7197
|
-
// node_modules/is-unicode-supported/index.js
|
|
8571
|
+
// ../../node_modules/.bun/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js
|
|
7198
8572
|
import process2 from "process";
|
|
7199
8573
|
function isUnicodeSupported() {
|
|
7200
8574
|
const { env } = process2;
|
|
@@ -7205,7 +8579,7 @@ function isUnicodeSupported() {
|
|
|
7205
8579
|
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";
|
|
7206
8580
|
}
|
|
7207
8581
|
|
|
7208
|
-
// node_modules/figures/index.js
|
|
8582
|
+
// ../../node_modules/.bun/figures@6.1.0/node_modules/figures/index.js
|
|
7209
8583
|
var common = {
|
|
7210
8584
|
circleQuestionMark: "(?)",
|
|
7211
8585
|
questionMarkPrefix: "(?)",
|
|
@@ -7481,23 +8855,23 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
|
|
|
7481
8855
|
var figures_default = figures;
|
|
7482
8856
|
var replacements = Object.entries(specialMainSymbols);
|
|
7483
8857
|
|
|
7484
|
-
// node_modules/ink-select-input/build/Indicator.js
|
|
8858
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
|
|
7485
8859
|
function Indicator({ isSelected = false }) {
|
|
7486
8860
|
return React.createElement(Box, { marginRight: 1 }, isSelected ? React.createElement(Text, { color: "blue" }, figures_default.pointer) : React.createElement(Text, null, " "));
|
|
7487
8861
|
}
|
|
7488
8862
|
var Indicator_default = Indicator;
|
|
7489
|
-
// node_modules/ink-select-input/build/Item.js
|
|
8863
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Item.js
|
|
7490
8864
|
import * as React2 from "react";
|
|
7491
8865
|
import { Text as Text2 } from "ink";
|
|
7492
8866
|
function Item({ isSelected = false, label }) {
|
|
7493
8867
|
return React2.createElement(Text2, { color: isSelected ? "blue" : undefined }, label);
|
|
7494
8868
|
}
|
|
7495
8869
|
var Item_default = Item;
|
|
7496
|
-
// node_modules/ink-select-input/build/SelectInput.js
|
|
8870
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
|
|
7497
8871
|
import { isDeepStrictEqual } from "util";
|
|
7498
8872
|
import React3, { useState, useEffect, useRef, useCallback } from "react";
|
|
7499
8873
|
|
|
7500
|
-
// node_modules/to-rotated/index.js
|
|
8874
|
+
// ../../node_modules/.bun/to-rotated@1.0.0/node_modules/to-rotated/index.js
|
|
7501
8875
|
function toRotated(array, steps) {
|
|
7502
8876
|
if (!Array.isArray(array)) {
|
|
7503
8877
|
throw new TypeError(`Expected an array, got \`${typeof array}\`.`);
|
|
@@ -7519,7 +8893,7 @@ function toRotated(array, steps) {
|
|
|
7519
8893
|
];
|
|
7520
8894
|
}
|
|
7521
8895
|
|
|
7522
|
-
// node_modules/ink-select-input/build/SelectInput.js
|
|
8896
|
+
// ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
|
|
7523
8897
|
import { Box as Box2, useInput } from "ink";
|
|
7524
8898
|
function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent = Indicator_default, itemComponent = Item_default, limit: customLimit, onSelect, onHighlight }) {
|
|
7525
8899
|
const hasLimit = typeof customLimit === "number" && items.length > customLimit;
|
|
@@ -7815,7 +9189,7 @@ function HookSelect({
|
|
|
7815
9189
|
import { useState as useState4, useEffect as useEffect3 } from "react";
|
|
7816
9190
|
import { Box as Box6, Text as Text7, useInput as useInput4 } from "ink";
|
|
7817
9191
|
|
|
7818
|
-
// node_modules/ink-text-input/build/index.js
|
|
9192
|
+
// ../../node_modules/.bun/ink-text-input@6.0.0+fa875fef9f949ebd/node_modules/ink-text-input/build/index.js
|
|
7819
9193
|
import React5, { useState as useState3, useEffect as useEffect2 } from "react";
|
|
7820
9194
|
import { Text as Text6, useInput as useInput3 } from "ink";
|
|
7821
9195
|
import chalk from "chalk";
|
|
@@ -7993,7 +9367,7 @@ function SearchView({
|
|
|
7993
9367
|
import { useState as useState6, useEffect as useEffect5 } from "react";
|
|
7994
9368
|
import { Box as Box7, Text as Text9 } from "ink";
|
|
7995
9369
|
|
|
7996
|
-
// node_modules/ink-spinner/build/index.js
|
|
9370
|
+
// ../../node_modules/.bun/ink-spinner@5.0.0+fa875fef9f949ebd/node_modules/ink-spinner/build/index.js
|
|
7997
9371
|
var import_cli_spinners = __toESM(require_cli_spinners(), 1);
|
|
7998
9372
|
import React7, { useState as useState5, useEffect as useEffect4 } from "react";
|
|
7999
9373
|
import { Text as Text8 } from "ink";
|
|
@@ -8359,10 +9733,12 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
8359
9733
|
init_registry();
|
|
8360
9734
|
init_installer();
|
|
8361
9735
|
init_profiles();
|
|
9736
|
+
init_manifest();
|
|
9737
|
+
init_resolve();
|
|
8362
9738
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
8363
|
-
var __dirname4 =
|
|
8364
|
-
var pkgPath =
|
|
8365
|
-
var pkg2 = JSON.parse(
|
|
9739
|
+
var __dirname4 = dirname5(fileURLToPath3(import.meta.url));
|
|
9740
|
+
var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
|
|
9741
|
+
var pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
8366
9742
|
var program2 = new Command;
|
|
8367
9743
|
function resolveScope(options) {
|
|
8368
9744
|
if (options.project)
|
|
@@ -8390,6 +9766,16 @@ function truncateText2(value, max = 96) {
|
|
|
8390
9766
|
return text;
|
|
8391
9767
|
return `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
8392
9768
|
}
|
|
9769
|
+
function readToken(tokenFile) {
|
|
9770
|
+
if (tokenFile) {
|
|
9771
|
+
try {
|
|
9772
|
+
return readFileSync10(tokenFile, "utf-8").trim();
|
|
9773
|
+
} catch {
|
|
9774
|
+
return;
|
|
9775
|
+
}
|
|
9776
|
+
}
|
|
9777
|
+
return process.env.CF_API_TOKEN;
|
|
9778
|
+
}
|
|
8393
9779
|
function readmePreview(readme, max = 280) {
|
|
8394
9780
|
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));
|
|
8395
9781
|
const preview = blocks[0] ?? readme.split(`
|
|
@@ -8430,7 +9816,38 @@ program2.name("hooks").description("Install hooks for AI coding agents").version
|
|
|
8430
9816
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
8431
9817
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
8432
9818
|
});
|
|
8433
|
-
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) => {
|
|
9819
|
+
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) => {
|
|
9820
|
+
if (options.cloudflare) {
|
|
9821
|
+
const { writeConfig: writeConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
9822
|
+
if (!options.apiUrl) {
|
|
9823
|
+
const message = "--cloudflare requires --api-url <url>";
|
|
9824
|
+
if (options.json)
|
|
9825
|
+
console.log(JSON.stringify({ error: message }));
|
|
9826
|
+
else
|
|
9827
|
+
console.log(chalk2.red(message));
|
|
9828
|
+
return;
|
|
9829
|
+
}
|
|
9830
|
+
const config = { api_url: options.apiUrl.replace(/\/+$/, ""), api_key_ref: options.apiKey };
|
|
9831
|
+
const path = writeConfig2(config);
|
|
9832
|
+
if (options.json) {
|
|
9833
|
+
console.log(JSON.stringify({ ok: true, api_url: config.api_url, api_key_ref: config.api_key_ref ?? null, config_path: path }));
|
|
9834
|
+
return;
|
|
9835
|
+
}
|
|
9836
|
+
console.log(chalk2.green(`
|
|
9837
|
+
\u2713 Remote registry configured
|
|
9838
|
+
`));
|
|
9839
|
+
console.log(` ${chalk2.dim("API URL:")} ${config.api_url}`);
|
|
9840
|
+
console.log(` ${chalk2.dim("API key ref:")} ${config.api_key_ref ?? "(none; serve will refuse publishes)"}`);
|
|
9841
|
+
console.log(` ${chalk2.dim("Config:")} ${path}`);
|
|
9842
|
+
console.log();
|
|
9843
|
+
console.log(chalk2.dim(" Presence of an API URL selects the remote registry; absence means local."));
|
|
9844
|
+
if (config.api_key_ref) {
|
|
9845
|
+
console.log(chalk2.dim(` Run the server with the key resolved from the vault, never the value:`));
|
|
9846
|
+
console.log(` secrets exec ${config.api_key_ref} --as HASNA_HOOKS_API_KEY -- hooks serve`);
|
|
9847
|
+
}
|
|
9848
|
+
console.log();
|
|
9849
|
+
return;
|
|
9850
|
+
}
|
|
8434
9851
|
const agentType = options.agent;
|
|
8435
9852
|
if (!["claude", "gemini", "custom"].includes(agentType)) {
|
|
8436
9853
|
if (options.json) {
|
|
@@ -8461,15 +9878,27 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
8461
9878
|
console.log();
|
|
8462
9879
|
});
|
|
8463
9880
|
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) => {
|
|
8464
|
-
const
|
|
8465
|
-
|
|
9881
|
+
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
9882
|
+
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
9883
|
+
const resolved = resolveHook2(hook);
|
|
9884
|
+
if (!resolved) {
|
|
8466
9885
|
console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
8467
9886
|
process.exit(1);
|
|
8468
9887
|
}
|
|
8469
|
-
const
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
9888
|
+
const hookScript = resolveScriptPath2(hook);
|
|
9889
|
+
if (!hookScript || !existsSync12(hookScript)) {
|
|
9890
|
+
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
9891
|
+
process.exit(1);
|
|
9892
|
+
}
|
|
9893
|
+
const content = readFileSync10(hookScript);
|
|
9894
|
+
const check = checkScriptHash2(hook, sha256Of2(content));
|
|
9895
|
+
if (!check.ok) {
|
|
9896
|
+
console.error(JSON.stringify({
|
|
9897
|
+
error: `Hook '${hook}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${hook}' to trust the new content.`,
|
|
9898
|
+
hook,
|
|
9899
|
+
expected_sha256: check.expected,
|
|
9900
|
+
actual_sha256: check.actual
|
|
9901
|
+
}));
|
|
8473
9902
|
process.exit(1);
|
|
8474
9903
|
}
|
|
8475
9904
|
const stdin = await new Response(Bun.stdin.stream()).text();
|
|
@@ -8490,22 +9919,27 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
8490
9919
|
} catch {}
|
|
8491
9920
|
}
|
|
8492
9921
|
}
|
|
8493
|
-
const
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
9922
|
+
const { readCustomManifest: readCustomManifest2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
9923
|
+
const { executeVerifiedScript: executeVerifiedScript2 } = await Promise.resolve().then(() => (init_run(), exports_run));
|
|
9924
|
+
const custom = readCustomManifest2(hook);
|
|
9925
|
+
const args = custom?.manifest.args ?? [];
|
|
9926
|
+
const timeout = custom?.manifest.timeout_ms;
|
|
9927
|
+
const { stdout, stderr, exitCode } = await executeVerifiedScript2({
|
|
9928
|
+
name: hook,
|
|
9929
|
+
scriptPath: hookScript,
|
|
9930
|
+
content,
|
|
9931
|
+
args,
|
|
9932
|
+
stdin: hookStdin,
|
|
9933
|
+
env: process.env,
|
|
9934
|
+
timeout
|
|
8498
9935
|
});
|
|
8499
|
-
const stdout = await new Response(proc.stdout).text();
|
|
8500
|
-
const stderr = await new Response(proc.stderr).text();
|
|
8501
|
-
const exitCode = await proc.exited;
|
|
8502
9936
|
if (stdout)
|
|
8503
9937
|
process.stdout.write(stdout);
|
|
8504
9938
|
if (stderr)
|
|
8505
9939
|
process.stderr.write(stderr);
|
|
8506
9940
|
process.exit(exitCode);
|
|
8507
9941
|
});
|
|
8508
|
-
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) => {
|
|
9942
|
+
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) => {
|
|
8509
9943
|
const scope = resolveScope(options);
|
|
8510
9944
|
const target = resolveTarget(options);
|
|
8511
9945
|
let toInstall = hooks;
|
|
@@ -8537,11 +9971,21 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
8537
9971
|
}
|
|
8538
9972
|
return;
|
|
8539
9973
|
}
|
|
9974
|
+
const { isCustomSource: isCustomSource2, installCustomSource: installCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
|
|
9975
|
+
const customSources = toInstall.filter((n) => !getHook(n) && isCustomSource2(n));
|
|
8540
9976
|
if (options.dryRun) {
|
|
8541
9977
|
const known = toInstall.filter((n) => getHook(n));
|
|
8542
|
-
const unknown = toInstall.filter((n) => !getHook(n));
|
|
9978
|
+
const unknown = toInstall.filter((n) => !getHook(n) && !isCustomSource2(n));
|
|
8543
9979
|
if (options.json) {
|
|
8544
|
-
console.log(JSON.stringify({
|
|
9980
|
+
console.log(JSON.stringify({
|
|
9981
|
+
dryRun: true,
|
|
9982
|
+
would_install: known,
|
|
9983
|
+
custom_sources: customSources,
|
|
9984
|
+
unknown,
|
|
9985
|
+
scope,
|
|
9986
|
+
target,
|
|
9987
|
+
mode: target === "codewith" ? "fragment" : "write"
|
|
9988
|
+
}));
|
|
8545
9989
|
return;
|
|
8546
9990
|
}
|
|
8547
9991
|
console.log(chalk2.bold(`
|
|
@@ -8551,6 +9995,9 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
8551
9995
|
const meta = getHook(name);
|
|
8552
9996
|
console.log(chalk2.cyan(` ${name}`) + chalk2.dim(` [${meta.event}${meta.matcher ? ` ${meta.matcher}` : ""}]`));
|
|
8553
9997
|
}
|
|
9998
|
+
for (const source of customSources) {
|
|
9999
|
+
console.log(chalk2.cyan(` ${source}`) + chalk2.dim(" [custom source: would fetch and install]"));
|
|
10000
|
+
}
|
|
8554
10001
|
if (unknown.length > 0) {
|
|
8555
10002
|
console.log();
|
|
8556
10003
|
for (const name of unknown) {
|
|
@@ -8561,8 +10008,29 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
8561
10008
|
return;
|
|
8562
10009
|
}
|
|
8563
10010
|
const results = [];
|
|
8564
|
-
|
|
8565
|
-
|
|
10011
|
+
const installedCustom = [];
|
|
10012
|
+
for (const source of customSources) {
|
|
10013
|
+
try {
|
|
10014
|
+
const custom = await installCustomSource2(source);
|
|
10015
|
+
installedCustom.push(custom.name);
|
|
10016
|
+
if (options.json) {
|
|
10017
|
+
console.log(JSON.stringify({ custom: { source, name: custom.name, version: custom.version } }));
|
|
10018
|
+
} else {
|
|
10019
|
+
console.log(chalk2.green(`\u2713 Installed custom hook '${custom.name}' v${custom.version} from ${source}`));
|
|
10020
|
+
}
|
|
10021
|
+
} catch (error) {
|
|
10022
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10023
|
+
results.push({ hook: source, success: false, error: message });
|
|
10024
|
+
if (options.json) {
|
|
10025
|
+
console.log(JSON.stringify({ error: `Custom hook install failed for ${source}: ${message}` }));
|
|
10026
|
+
} else {
|
|
10027
|
+
console.log(chalk2.red(`\u2717 ${message}`));
|
|
10028
|
+
}
|
|
10029
|
+
}
|
|
10030
|
+
}
|
|
10031
|
+
const namesToRegister = [...new Set([...toInstall.filter((n) => !isCustomSource2(n)), ...installedCustom])];
|
|
10032
|
+
for (const name of namesToRegister) {
|
|
10033
|
+
if (!getHook(name) && !installedCustom.includes(name)) {
|
|
8566
10034
|
const suggestions = suggestHooks(name);
|
|
8567
10035
|
const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
|
|
8568
10036
|
results.push({ hook: name, success: false, error: `Hook '${name}' not found${hint}` });
|
|
@@ -8752,8 +10220,9 @@ Categories:
|
|
|
8752
10220
|
console.log(` ${category} (${count})`);
|
|
8753
10221
|
}
|
|
8754
10222
|
});
|
|
8755
|
-
program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action((hook, options) => {
|
|
8756
|
-
const
|
|
10223
|
+
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) => {
|
|
10224
|
+
const { resolveHookMeta: resolveHookMeta2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
10225
|
+
const meta = resolveHookMeta2(hook) ?? getHook(hook);
|
|
8757
10226
|
if (!meta) {
|
|
8758
10227
|
const suggestions = suggestHooks(hook);
|
|
8759
10228
|
const hint = suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : "";
|
|
@@ -8766,8 +10235,12 @@ program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "O
|
|
|
8766
10235
|
}
|
|
8767
10236
|
const globalInstalled = getRegisteredHooks("global").includes(meta.name);
|
|
8768
10237
|
const projectInstalled = getRegisteredHooks("project").includes(meta.name);
|
|
10238
|
+
const { readCustomManifest: readCustomManifest2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
|
|
10239
|
+
const custom = readCustomManifest2(meta.name);
|
|
10240
|
+
const source = custom ? "custom" : "bundled";
|
|
10241
|
+
const sourceNote = custom ? "custom (~/.hasna/hooks/hooks/) overrides the bundled registry" : "bundled registry";
|
|
8769
10242
|
if (options.json) {
|
|
8770
|
-
console.log(JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }));
|
|
10243
|
+
console.log(JSON.stringify({ ...meta, source, source_note: sourceNote, global: globalInstalled, project: projectInstalled }));
|
|
8771
10244
|
return;
|
|
8772
10245
|
}
|
|
8773
10246
|
console.log(chalk2.bold(`
|
|
@@ -8780,6 +10253,7 @@ ${meta.displayName}
|
|
|
8780
10253
|
console.log(` ${chalk2.dim("Event:")} ${meta.event}`);
|
|
8781
10254
|
console.log(` ${chalk2.dim("Matcher:")} ${meta.matcher || "(none)"}`);
|
|
8782
10255
|
console.log(` ${chalk2.dim("Tags:")} ${meta.tags.join(", ")}`);
|
|
10256
|
+
console.log(` ${chalk2.dim("Source:")} ${source} (${sourceNote})`);
|
|
8783
10257
|
console.log(` ${chalk2.dim("Command:")} hooks run ${meta.name}`);
|
|
8784
10258
|
console.log();
|
|
8785
10259
|
if (globalInstalled) {
|
|
@@ -8798,28 +10272,36 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
8798
10272
|
const settingsPath = getSettingsPath(scope);
|
|
8799
10273
|
const issues = [];
|
|
8800
10274
|
const healthy = [];
|
|
8801
|
-
const settingsExist =
|
|
10275
|
+
const settingsExist = existsSync12(settingsPath);
|
|
8802
10276
|
if (!settingsExist) {
|
|
8803
10277
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
8804
10278
|
}
|
|
8805
10279
|
const registered = getRegisteredHooks(scope);
|
|
8806
10280
|
for (const name of registered) {
|
|
8807
|
-
const
|
|
10281
|
+
const custom = readCustomManifest(name);
|
|
10282
|
+
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
8808
10283
|
let hookHealthy = true;
|
|
8809
|
-
if (
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
10284
|
+
if (custom) {
|
|
10285
|
+
if (!existsSync12(custom.scriptPath)) {
|
|
10286
|
+
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
10287
|
+
hookHealthy = false;
|
|
10288
|
+
}
|
|
10289
|
+
} else {
|
|
10290
|
+
if (!hookExists(name)) {
|
|
10291
|
+
issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
|
|
10292
|
+
hookHealthy = false;
|
|
10293
|
+
continue;
|
|
10294
|
+
}
|
|
10295
|
+
const hookDir = getHookPath(name);
|
|
10296
|
+
const hookScript = join12(hookDir, "src", "hook.ts");
|
|
10297
|
+
if (!existsSync12(hookScript)) {
|
|
10298
|
+
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
10299
|
+
hookHealthy = false;
|
|
10300
|
+
}
|
|
8819
10301
|
}
|
|
8820
10302
|
if (meta && settingsExist) {
|
|
8821
10303
|
try {
|
|
8822
|
-
const settings = JSON.parse(
|
|
10304
|
+
const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
8823
10305
|
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
8824
10306
|
const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
|
|
8825
10307
|
const match = h.command?.match(/^hooks run ([\w-]+)/);
|
|
@@ -8866,7 +10348,7 @@ Hook Health Check (${scope})
|
|
|
8866
10348
|
}
|
|
8867
10349
|
console.log();
|
|
8868
10350
|
});
|
|
8869
|
-
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) => {
|
|
10351
|
+
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) => {
|
|
8870
10352
|
const scope = resolveScope(options);
|
|
8871
10353
|
const installed = getInstalledHooks(scope);
|
|
8872
10354
|
const toUpdate = hooks.length > 0 ? hooks : installed;
|
|
@@ -8878,6 +10360,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
8878
10360
|
}
|
|
8879
10361
|
return;
|
|
8880
10362
|
}
|
|
10363
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
10364
|
+
const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10365
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
8881
10366
|
const results = [];
|
|
8882
10367
|
for (const name of toUpdate) {
|
|
8883
10368
|
if (!installed.includes(name)) {
|
|
@@ -8885,6 +10370,20 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
8885
10370
|
continue;
|
|
8886
10371
|
}
|
|
8887
10372
|
const result = installHook(name, { scope, overwrite: true });
|
|
10373
|
+
const resolved = resolveHook2(name);
|
|
10374
|
+
if (result.success && resolved) {
|
|
10375
|
+
const hash = await sha256File2(resolved.scriptPath);
|
|
10376
|
+
setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
|
|
10377
|
+
upsertHookRecord2(getDb2(), {
|
|
10378
|
+
name: resolved.name,
|
|
10379
|
+
version: resolved.version,
|
|
10380
|
+
sha256: hash,
|
|
10381
|
+
source_type: resolved.source,
|
|
10382
|
+
last_verified_at: new Date().toISOString()
|
|
10383
|
+
});
|
|
10384
|
+
results.push({ ...result, pinned: { version: resolved.version, sha256: hash } });
|
|
10385
|
+
continue;
|
|
10386
|
+
}
|
|
8888
10387
|
results.push(result);
|
|
8889
10388
|
}
|
|
8890
10389
|
if (options.json) {
|
|
@@ -8899,7 +10398,7 @@ Updating hooks...
|
|
|
8899
10398
|
`));
|
|
8900
10399
|
for (const result of results) {
|
|
8901
10400
|
if (result.success) {
|
|
8902
|
-
console.log(chalk2.green(`\u2713 ${result.hook} updated`));
|
|
10401
|
+
console.log(chalk2.green(`\u2713 ${result.hook} updated`) + (result.pinned ? chalk2.dim(` (pinned ${result.pinned.version})`) : ""));
|
|
8903
10402
|
} else {
|
|
8904
10403
|
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
8905
10404
|
}
|
|
@@ -8917,10 +10416,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
8917
10416
|
return;
|
|
8918
10417
|
}
|
|
8919
10418
|
const hookPath = getHookPath(hook);
|
|
8920
|
-
const readmePath =
|
|
10419
|
+
const readmePath = join12(hookPath, "README.md");
|
|
8921
10420
|
let readme = "";
|
|
8922
|
-
if (
|
|
8923
|
-
readme =
|
|
10421
|
+
if (existsSync12(readmePath)) {
|
|
10422
|
+
readme = readFileSync10(readmePath, "utf-8");
|
|
8924
10423
|
}
|
|
8925
10424
|
if (options.json) {
|
|
8926
10425
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -9102,14 +10601,14 @@ Upgrading @hasna/hooks (${pm})...
|
|
|
9102
10601
|
});
|
|
9103
10602
|
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) => {
|
|
9104
10603
|
const profiles = exportProfiles();
|
|
9105
|
-
const
|
|
10604
|
+
const json2 = JSON.stringify(profiles, null, 2);
|
|
9106
10605
|
if (options.output) {
|
|
9107
|
-
const { writeFileSync:
|
|
9108
|
-
|
|
10606
|
+
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
10607
|
+
writeFileSync7(options.output, json2 + `
|
|
9109
10608
|
`);
|
|
9110
10609
|
console.log(chalk2.green(`\u2713 Exported ${profiles.length} profile(s) to ${options.output}`));
|
|
9111
10610
|
} else {
|
|
9112
|
-
console.log(
|
|
10611
|
+
console.log(json2);
|
|
9113
10612
|
}
|
|
9114
10613
|
});
|
|
9115
10614
|
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) => {
|
|
@@ -9117,9 +10616,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
9117
10616
|
if (file === "-") {
|
|
9118
10617
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
9119
10618
|
} else {
|
|
9120
|
-
const { readFileSync:
|
|
10619
|
+
const { readFileSync: readFileSync11 } = await import("fs");
|
|
9121
10620
|
try {
|
|
9122
|
-
raw =
|
|
10621
|
+
raw = readFileSync11(file, "utf-8");
|
|
9123
10622
|
} catch {
|
|
9124
10623
|
if (options.json) {
|
|
9125
10624
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -9314,7 +10813,7 @@ storageCmd.command("status").description("Show storage sync status").option("-j,
|
|
|
9314
10813
|
Storage Status
|
|
9315
10814
|
`));
|
|
9316
10815
|
console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
|
|
9317
|
-
console.log(`
|
|
10816
|
+
console.log(` Backend: ${status.backend}`);
|
|
9318
10817
|
console.log(` Tables: ${status.tables.join(", ")}`);
|
|
9319
10818
|
console.log(` Sync rows: ${status.sync.length}`);
|
|
9320
10819
|
});
|
|
@@ -9376,6 +10875,126 @@ storageCmd.command("sync").description("Bidirectional storage sync: pull then pu
|
|
|
9376
10875
|
process.exitCode = 1;
|
|
9377
10876
|
}
|
|
9378
10877
|
});
|
|
10878
|
+
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) => {
|
|
10879
|
+
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
10880
|
+
const { retrustHook: retrustHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
10881
|
+
const resolved = resolveHook2(hook);
|
|
10882
|
+
if (!resolved) {
|
|
10883
|
+
if (options.json)
|
|
10884
|
+
console.log(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
10885
|
+
else
|
|
10886
|
+
console.log(chalk2.red(`Hook '${hook}' not found`));
|
|
10887
|
+
return;
|
|
10888
|
+
}
|
|
10889
|
+
const result = retrustHook2(resolved.name, resolved.scriptPath, resolved.version, resolved.source);
|
|
10890
|
+
if (options.json) {
|
|
10891
|
+
console.log(JSON.stringify({ ok: true, hook: resolved.name, version: resolved.version, sha256: result.actual, source: resolved.source }));
|
|
10892
|
+
return;
|
|
10893
|
+
}
|
|
10894
|
+
console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
|
|
10895
|
+
console.log(chalk2.dim(` sha256 ${result.actual}`));
|
|
10896
|
+
});
|
|
10897
|
+
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) => {
|
|
10898
|
+
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
10899
|
+
startServeServer2({
|
|
10900
|
+
port: options.port ? parseInt(options.port, 10) : undefined,
|
|
10901
|
+
host: options.host,
|
|
10902
|
+
apiKey: options.apiKey
|
|
10903
|
+
});
|
|
10904
|
+
});
|
|
10905
|
+
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) => {
|
|
10906
|
+
const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
10907
|
+
try {
|
|
10908
|
+
const plan = options.dryRun ? await planSync2() : await syncHooks2();
|
|
10909
|
+
const diff = plan.diff;
|
|
10910
|
+
if (options.json) {
|
|
10911
|
+
console.log(JSON.stringify({ dry_run: plan.dryRun, api_url: plan.apiUrl ?? null, diff }));
|
|
10912
|
+
return;
|
|
10913
|
+
}
|
|
10914
|
+
const source = plan.apiUrl ? plan.apiUrl : "bundled registry";
|
|
10915
|
+
console.log(chalk2.bold(`
|
|
10916
|
+
Hook sync (${source})${plan.dryRun ? " \u2014 dry run" : ""}
|
|
10917
|
+
`));
|
|
10918
|
+
if (diff.added.length > 0) {
|
|
10919
|
+
console.log(chalk2.green(` + ${diff.added.length} to add:`));
|
|
10920
|
+
for (const name of diff.added)
|
|
10921
|
+
console.log(chalk2.green(` ${name}`));
|
|
10922
|
+
}
|
|
10923
|
+
if (diff.updated.length > 0) {
|
|
10924
|
+
console.log(chalk2.yellow(` ~ ${diff.updated.length} to update:`));
|
|
10925
|
+
for (const name of diff.updated)
|
|
10926
|
+
console.log(chalk2.yellow(` ${name}`));
|
|
10927
|
+
}
|
|
10928
|
+
if (diff.skipped.length > 0) {
|
|
10929
|
+
console.log(chalk2.dim(` - ${diff.skipped.length} not in remote lock (left untouched)`));
|
|
10930
|
+
}
|
|
10931
|
+
console.log(chalk2.dim(` = ${diff.unchanged.length} unchanged`));
|
|
10932
|
+
if (plan.dryRun)
|
|
10933
|
+
console.log(chalk2.dim(`
|
|
10934
|
+
Dry run: nothing was written.`));
|
|
10935
|
+
else
|
|
10936
|
+
console.log(chalk2.green(`
|
|
10937
|
+
\u2713 Synced (${diff.added.length} added, ${diff.updated.length} updated, ${diff.unchanged.length} unchanged)`));
|
|
10938
|
+
} catch (error) {
|
|
10939
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10940
|
+
if (options.json) {
|
|
10941
|
+
console.log(JSON.stringify({ error: message, changed: false }));
|
|
10942
|
+
} else {
|
|
10943
|
+
console.error(chalk2.red(`\u2717 ${message}`));
|
|
10944
|
+
console.error(chalk2.dim(" Nothing was changed (fail-closed)."));
|
|
10945
|
+
}
|
|
10946
|
+
process.exitCode = 1;
|
|
10947
|
+
}
|
|
10948
|
+
});
|
|
10949
|
+
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) => {
|
|
10950
|
+
const token = readToken(options.tokenFile);
|
|
10951
|
+
const accountId = options.accountId ?? process.env.CF_ACCOUNT_ID;
|
|
10952
|
+
if (!options.dryRun) {
|
|
10953
|
+
if (!token) {
|
|
10954
|
+
console.error(chalk2.red("CF_API_TOKEN is not set (or --token-file path is unreadable)."));
|
|
10955
|
+
console.error(chalk2.dim(" Resolve it from the vault, never paste the value:"));
|
|
10956
|
+
console.error(chalk2.dim(" secrets exec <vault-key> --as CF_API_TOKEN -- hooks cf deploy"));
|
|
10957
|
+
process.exit(1);
|
|
10958
|
+
}
|
|
10959
|
+
if (!accountId) {
|
|
10960
|
+
console.error(chalk2.red("CF_ACCOUNT_ID is not set (or pass --account-id)."));
|
|
10961
|
+
process.exit(1);
|
|
10962
|
+
}
|
|
10963
|
+
}
|
|
10964
|
+
const { provisionCloudflareResources: provisionCloudflareResources2 } = await Promise.resolve().then(() => exports_provision);
|
|
10965
|
+
const result = await provisionCloudflareResources2({
|
|
10966
|
+
token: token ?? "",
|
|
10967
|
+
accountId: accountId ?? "",
|
|
10968
|
+
databaseName: options.databaseName ?? "hooks-registry",
|
|
10969
|
+
bucketName: options.bucketName ?? "hooks-registry-artifacts",
|
|
10970
|
+
dryRun: options.dryRun
|
|
10971
|
+
});
|
|
10972
|
+
if (options.json) {
|
|
10973
|
+
console.log(JSON.stringify({ dry_run: options.dryRun, ...result }));
|
|
10974
|
+
return;
|
|
10975
|
+
}
|
|
10976
|
+
if (options.dryRun) {
|
|
10977
|
+
console.log(chalk2.bold(`
|
|
10978
|
+
cf deploy dry run \u2014 would provision:
|
|
10979
|
+
`));
|
|
10980
|
+
} else {
|
|
10981
|
+
console.log(chalk2.bold(`
|
|
10982
|
+
Cloudflare provisioning complete:
|
|
10983
|
+
`));
|
|
10984
|
+
console.log(` ${chalk2.dim("D1:")} ${result.d1Created ? chalk2.green(`created '${options.databaseName ?? "hooks-registry"}'`) : chalk2.dim(`exists (${result.d1DatabaseId})`)}`);
|
|
10985
|
+
console.log(` ${chalk2.dim("R2:")} ${result.r2Created ? chalk2.green(`created '${options.bucketName ?? "hooks-registry-artifacts"}'`) : chalk2.dim("exists")}`);
|
|
10986
|
+
}
|
|
10987
|
+
console.log(`
|
|
10988
|
+
${chalk2.bold("Worker upload (run these with wrangler):")}`);
|
|
10989
|
+
for (const cmd of result.commands) {
|
|
10990
|
+
console.log(` ${cmd}`);
|
|
10991
|
+
}
|
|
10992
|
+
console.log();
|
|
10993
|
+
console.log(chalk2.dim(" Worker upload is not performed by this command \u2014 the worker needs the workerd"));
|
|
10994
|
+
console.log(chalk2.dim(" target, which only wrangler can bundle. Copy src/cf/wrangler.toml.example to"));
|
|
10995
|
+
console.log(chalk2.dim(" wrangler.toml, fill in the D1 database id, then run the commands above."));
|
|
10996
|
+
console.log();
|
|
10997
|
+
});
|
|
9379
10998
|
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) => {
|
|
9380
10999
|
if (options.stdio) {
|
|
9381
11000
|
const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|