@hasna/hooks 0.10.7 → 0.10.8
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 +39 -1
- package/bin/hooks-mcp.js +52 -19
- package/bin/index.js +239 -80
- package/bin/native-safety-entry.js +196 -15
- package/dist/index.js +52 -19
- package/dist/lib/native-safety.d.ts +4 -0
- package/dist/native-safety.d.ts +6 -0
- package/dist/native-safety.js +18 -0
- package/hooks/hook-workspace-repos-guard/README.md +12 -0
- package/hooks/hook-workspace-repos-guard/src/hook.ts +247 -14
- package/package.json +2 -2
- package/scripts/ensure-profiles-dir.mjs +65 -1
package/bin/index.js
CHANGED
|
@@ -1079,6 +1079,19 @@ function pinEligibleFile(file, maximum, executable = false) {
|
|
|
1079
1079
|
closeSync(fd);
|
|
1080
1080
|
}
|
|
1081
1081
|
}
|
|
1082
|
+
function pinVerified(file, expected, maximum, executable = false) {
|
|
1083
|
+
try {
|
|
1084
|
+
return pinEligibleFile(file, maximum, executable) === expected;
|
|
1085
|
+
} catch {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function nativeSafetyBindingVerified(command) {
|
|
1090
|
+
const binding = nativeSafetyCommandBinding(command);
|
|
1091
|
+
if (!binding)
|
|
1092
|
+
return;
|
|
1093
|
+
return pinVerified(binding.runtime, binding.runtimeSHA256, 200000000, true) && pinVerified(binding.worker, binding.workerSHA256, 256000);
|
|
1094
|
+
}
|
|
1082
1095
|
function installedNativeSafetyCommand(name) {
|
|
1083
1096
|
const home = realpathSync2(homedir2());
|
|
1084
1097
|
const runtime = realpathSync2(process.execPath);
|
|
@@ -2068,7 +2081,7 @@ var init_codex_settings = __esm(() => {
|
|
|
2068
2081
|
snapshots = new WeakMap;
|
|
2069
2082
|
});
|
|
2070
2083
|
|
|
2071
|
-
// ../../node_modules/.bun/@hasna+skills@0.9.
|
|
2084
|
+
// ../../node_modules/.bun/@hasna+skills@0.9.17+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
|
|
2072
2085
|
import { existsSync as existsSync9 } from "fs";
|
|
2073
2086
|
import { homedir as homedir4 } from "os";
|
|
2074
2087
|
import { join as join8, resolve as resolve3 } from "path";
|
|
@@ -2081,6 +2094,7 @@ import { basename as basename2, isAbsolute as isAbsolute22, resolve as resolve22
|
|
|
2081
2094
|
import { createHash as createHash16 } from "crypto";
|
|
2082
2095
|
import { lstatSync as lstatSync20, readFileSync as readFileSync16, readdirSync as readdirSync9, realpathSync as realpathSync4 } from "fs";
|
|
2083
2096
|
import { basename as basename8, dirname as dirname16, isAbsolute as isAbsolute14, join as join26, resolve as resolve14, sep as sep5 } from "path";
|
|
2097
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
|
|
2084
2098
|
import { dirname as dirname32, isAbsolute as isAbsolute3, join as join72, resolve as resolve32 } from "path";
|
|
2085
2099
|
import { lstatSync as lstatSync3, readFileSync as readFileSync6, opendirSync } from "fs";
|
|
2086
2100
|
import { createHash as createHash22 } from "crypto";
|
|
@@ -2579,8 +2593,8 @@ function assertAgentPolicyCollections(policy) {
|
|
|
2579
2593
|
text(source.path);
|
|
2580
2594
|
requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
|
|
2581
2595
|
if (source.hashMode !== undefined)
|
|
2582
|
-
requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
|
|
2583
|
-
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
|
|
2596
|
+
requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
|
|
2597
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
|
|
2584
2598
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "settings.json");
|
|
2585
2599
|
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
|
|
2586
2600
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
|
|
@@ -3494,7 +3508,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
|
|
|
3494
3508
|
}
|
|
3495
3509
|
need2(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
|
|
3496
3510
|
}
|
|
3497
|
-
function inferencePreference(key, child) {
|
|
3511
|
+
function inferencePreference(key, child, version) {
|
|
3498
3512
|
if (key === "skipDangerousModePermissionPrompt") {
|
|
3499
3513
|
need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
|
|
3500
3514
|
return false;
|
|
@@ -3507,7 +3521,7 @@ function inferencePreference(key, child) {
|
|
|
3507
3521
|
return false;
|
|
3508
3522
|
need2(child.kind === "object", "requires an object of model preferences");
|
|
3509
3523
|
child.entries = child.entries.filter(([model, preferences]) => {
|
|
3510
|
-
if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
|
|
3524
|
+
if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
|
|
3511
3525
|
return true;
|
|
3512
3526
|
const effort = preferences.entries[0][1];
|
|
3513
3527
|
need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
|
|
@@ -3518,7 +3532,7 @@ function inferencePreference(key, child) {
|
|
|
3518
3532
|
function settingsDigest(text2, version = 1) {
|
|
3519
3533
|
const value = parse3(text2);
|
|
3520
3534
|
value.entries = value.entries.filter(([key, child]) => {
|
|
3521
|
-
if (version
|
|
3535
|
+
if (version !== 1 && inferencePreference(key, child, version))
|
|
3522
3536
|
return false;
|
|
3523
3537
|
if (key === "model") {
|
|
3524
3538
|
need2(child.kind === "string", "contains an invalid model selection");
|
|
@@ -3556,6 +3570,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
|
|
|
3556
3570
|
budget.remaining -= bytes;
|
|
3557
3571
|
return settingsDigest(text2, 2);
|
|
3558
3572
|
}
|
|
3573
|
+
function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
3574
|
+
return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
|
|
3575
|
+
}
|
|
3576
|
+
function hashClaudeSettingsReplacementV3(text2, budget) {
|
|
3577
|
+
need2(typeof text2 === "string", "requires settings text");
|
|
3578
|
+
const bytes = Buffer.byteLength(text2);
|
|
3579
|
+
need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
|
|
3580
|
+
budget.remaining -= bytes;
|
|
3581
|
+
return settingsDigest(text2, 3);
|
|
3582
|
+
}
|
|
3559
3583
|
function assertSafePath(path) {
|
|
3560
3584
|
let cursor = resolve9(path);
|
|
3561
3585
|
while (true) {
|
|
@@ -4007,6 +4031,8 @@ function applyAgentIntegration(plan) {
|
|
|
4007
4031
|
parseManagedSkillPolicy(change.after);
|
|
4008
4032
|
const aliases = plan.rootAliases ?? [];
|
|
4009
4033
|
recheckRootAliases(aliases);
|
|
4034
|
+
for (const agent of plan.retainedReviewChecks?.agents ?? [])
|
|
4035
|
+
assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
|
|
4010
4036
|
for (const agent of plan.managedAgentChecks?.agents ?? [])
|
|
4011
4037
|
assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
|
|
4012
4038
|
for (const binding of plan.discoveryBefore ?? []) {
|
|
@@ -5009,10 +5035,11 @@ function unchanged2(before) {
|
|
|
5009
5035
|
need4(sameStat(before.stat, after.stat) && after.sha256 === before.sha256, "INPUT_CHANGED");
|
|
5010
5036
|
}
|
|
5011
5037
|
function assertCodexHookDiscoveryRecovery(binding, witness) {
|
|
5012
|
-
const proof =
|
|
5038
|
+
const proof = proofs2.get(witness);
|
|
5013
5039
|
need4(!!proof && binding.agent === "codex" && witness.discoverySha256 === sha2(JSON.stringify(binding)), "RECONCILE_DISCOVERY_RECOVERY_CHANGED");
|
|
5014
5040
|
unchanged2(proof.before);
|
|
5015
5041
|
unchanged2(proof.current);
|
|
5042
|
+
proof.recheckAdditions();
|
|
5016
5043
|
}
|
|
5017
5044
|
function verifiesCodexHookDiscoverySource(binding, source, currentDigest, witness) {
|
|
5018
5045
|
if (!witness || source.path !== witness.configPath || source.hashMode !== "bytes")
|
|
@@ -5053,13 +5080,13 @@ function read2(path, changes) {
|
|
|
5053
5080
|
return readFileSync16(path, "utf8");
|
|
5054
5081
|
}
|
|
5055
5082
|
function projected(source, changes, budget = discoveryByteBudget()) {
|
|
5056
|
-
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
|
|
5083
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
|
|
5057
5084
|
if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
|
|
5058
5085
|
throw new Error("Claude settings witnesses require exact typed metadata");
|
|
5059
|
-
const current2 = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
|
|
5086
|
+
const current2 = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
|
|
5060
5087
|
if (current2 !== source.sha256)
|
|
5061
5088
|
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
5062
|
-
return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
|
|
5089
|
+
return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
|
|
5063
5090
|
}
|
|
5064
5091
|
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
|
|
5065
5092
|
if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
|
|
@@ -5121,7 +5148,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
|
|
|
5121
5148
|
safe4(root2);
|
|
5122
5149
|
}
|
|
5123
5150
|
function assertMarketplaceBinding(binding, source) {
|
|
5124
|
-
if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
|
|
5151
|
+
if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
|
|
5125
5152
|
throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
|
|
5126
5153
|
if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
|
|
5127
5154
|
throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
|
|
@@ -5130,7 +5157,10 @@ function rebindAgentDiscovery(binding, changes) {
|
|
|
5130
5157
|
const budget = discoveryByteBudget();
|
|
5131
5158
|
return { ...binding, sources: binding.sources.map((source) => {
|
|
5132
5159
|
assertMarketplaceBinding(binding, source);
|
|
5133
|
-
|
|
5160
|
+
const sha2562 = projected(source, changes, budget);
|
|
5161
|
+
if (!changes.has(source.path) && sha2562 !== source.sha256)
|
|
5162
|
+
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
5163
|
+
return { ...source, sha256: sha2562 };
|
|
5134
5164
|
}) };
|
|
5135
5165
|
}
|
|
5136
5166
|
function assertProjectDiscovery(agent, directories, home, canonical4 = resolve14, reviewed) {
|
|
@@ -5242,6 +5272,19 @@ function resolveAgentDiscovery(options) {
|
|
|
5242
5272
|
}
|
|
5243
5273
|
}
|
|
5244
5274
|
}
|
|
5275
|
+
if (options.reviewed === undefined && options.retainedReview !== undefined) {
|
|
5276
|
+
const retained = options.retainedReview;
|
|
5277
|
+
if (retained.agent !== agent || retained.method !== "reviewed")
|
|
5278
|
+
unresolved("invalid retained review");
|
|
5279
|
+
verifyAgentDiscovery(retained);
|
|
5280
|
+
if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
|
|
5281
|
+
unresolved("retained review is missing the full agent configuration source");
|
|
5282
|
+
if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
|
|
5283
|
+
unresolved("retained settings witness names another configuration source");
|
|
5284
|
+
if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual3(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
|
|
5285
|
+
unresolved("retained review is missing current runtime discovery coverage");
|
|
5286
|
+
return retained;
|
|
5287
|
+
}
|
|
5245
5288
|
if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
|
|
5246
5289
|
throw new Error("Invalid --discovery-inputs version or agents");
|
|
5247
5290
|
const review = options.reviewed?.agents.find((item) => item.agent === agent);
|
|
@@ -5252,7 +5295,7 @@ function resolveAgentDiscovery(options) {
|
|
|
5252
5295
|
if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
|
|
5253
5296
|
throw new Error("Explicit discovery reviews require full source-file hashes");
|
|
5254
5297
|
verifyAgentDiscovery(supplied);
|
|
5255
|
-
if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
|
|
5298
|
+
if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
|
|
5256
5299
|
throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
|
|
5257
5300
|
if (!review.sources.some((source) => source.path === canonical4(configPath)))
|
|
5258
5301
|
throw new Error("Discovery review must include the agent configuration source");
|
|
@@ -12042,7 +12085,7 @@ Use the Skills CLI as the skill authority for this station.
|
|
|
12042
12085
|
- Run a skill only when the user's task calls for execution. Use \`skills run --help\` and select the intended local or cloud target explicitly.
|
|
12043
12086
|
|
|
12044
12087
|
This bridge contains no skill payloads. Do not copy skill content into agent-native skill directories or load other native skill copies. If a Skills command refuses authority, selection, integrity, or native drift, report that refusal and use its repair guidance; do not substitute bundled or stale local content. Explicit local draft authoring remains available.
|
|
12045
|
-
`, CLI_BRIDGE_DIGEST, CLI_BRIDGE_FILES, CLAUDE_SETTINGS_WITNESS_LIMITS, sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, BOOLEAN_PREFERENCES, ENUM_PREFERENCES, BUILTIN_MODEL_SELECTIONS, CLAUDE_COORDINATED_HOOK_EVENTS, PERSISTED_EFFORT_LEVELS, sha = (data) => createHash10("sha256").update(data).digest("hex"), ROOTS, PLUGIN_PROJECTION_LIMITS, pluginHash = (bytes) => createHash11("sha256").update(bytes).digest("hex"), READ_FLAGS, WRITE_FLAGS, uid = () => typeof process.getuid === "function" ? BigInt(process.getuid()) : undefined, missing = (error) => Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"), quote2 = (value) => `'${value.replaceAll("'", "'\\''")}'`, resolverGuard = 'exec 9<"$1"; actual=$(/usr/bin/sha256sum /proc/self/fd/9); actual=${actual%% *}; [ "sha256:$actual" = "$2" ]; exec /proc/self/fd/9 integration plugin resolve --binding "$3"', CLAUDE_MARKETPLACE_REGISTRY_LIMITS, sameFile2 = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, field = (object4, key) => object4.entries.find(([name]) => name === key)?.[1], sameStat = (a, b) => ["dev", "ino", "uid", "nlink", "mode", "size", "mtimeNs", "ctimeNs"].every((key) => a[key] === b[key]), sha2 = (text2) => createHash15("sha256").update(text2).digest("hex"),
|
|
12088
|
+
`, CLI_BRIDGE_DIGEST, CLI_BRIDGE_FILES, CLAUDE_SETTINGS_WITNESS_LIMITS, sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, BOOLEAN_PREFERENCES, ENUM_PREFERENCES, BUILTIN_MODEL_SELECTIONS, CLAUDE_COORDINATED_HOOK_EVENTS, PERSISTED_EFFORT_LEVELS, sha = (data) => createHash10("sha256").update(data).digest("hex"), ROOTS, PLUGIN_PROJECTION_LIMITS, pluginHash = (bytes) => createHash11("sha256").update(bytes).digest("hex"), READ_FLAGS, WRITE_FLAGS, uid = () => typeof process.getuid === "function" ? BigInt(process.getuid()) : undefined, missing = (error) => Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"), quote2 = (value) => `'${value.replaceAll("'", "'\\''")}'`, resolverGuard = 'exec 9<"$1"; actual=$(/usr/bin/sha256sum /proc/self/fd/9); actual=${actual%% *}; [ "sha256:$actual" = "$2" ]; exec /proc/self/fd/9 integration plugin resolve --binding "$3"', CLAUDE_MARKETPLACE_REGISTRY_LIMITS, sameFile2 = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, field = (object4, key) => object4.entries.find(([name]) => name === key)?.[1], sameStat = (a, b) => ["dev", "ino", "uid", "nlink", "mode", "size", "mtimeNs", "ctimeNs"].every((key) => a[key] === b[key]), events, matchedEvents, proofs, sha2 = (text2) => createHash15("sha256").update(text2).digest("hex"), proofs2, digest = (text2) => createHash16("sha256").update(text2).digest("hex"), NATIVE_DISCOVERY_ROOTS, OFFICIAL_SKILL_NAMES, exports_external, exports_core2, exports_util, EVALUATING, captureStackTrace, allowsEval, getParsedType = (data) => {
|
|
12046
12089
|
const t2 = typeof data;
|
|
12047
12090
|
switch (t2) {
|
|
12048
12091
|
case "undefined":
|
|
@@ -26445,7 +26488,7 @@ ${end.comment}` : end.comment;
|
|
|
26445
26488
|
$visitAsync = visit.visitAsync;
|
|
26446
26489
|
package_default = {
|
|
26447
26490
|
name: "@hasna/skills",
|
|
26448
|
-
version: "0.9.
|
|
26491
|
+
version: "0.9.17",
|
|
26449
26492
|
description: "Skills library for AI coding agents",
|
|
26450
26493
|
type: "module",
|
|
26451
26494
|
bin: {
|
|
@@ -26817,7 +26860,10 @@ ${end.comment}` : end.comment;
|
|
|
26817
26860
|
READ_FLAGS = constants8.O_RDONLY | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
|
|
26818
26861
|
WRITE_FLAGS = constants8.O_WRONLY | constants8.O_CREAT | constants8.O_EXCL | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
|
|
26819
26862
|
CLAUDE_MARKETPLACE_REGISTRY_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
|
|
26863
|
+
events = { UserPromptSubmit: ["userPromptSubmit", "user_prompt_submit"], SessionStart: ["sessionStart", "session_start"], SubagentStart: ["subagentStart", "subagent_start"] };
|
|
26864
|
+
matchedEvents = { ...events, PreToolUse: ["preToolUse", "pre_tool_use"] };
|
|
26820
26865
|
proofs = new WeakMap;
|
|
26866
|
+
proofs2 = new WeakMap;
|
|
26821
26867
|
NATIVE_DISCOVERY_ROOTS = NATIVE_SKILL_ROOTS.map(([, path]) => path.split("/"));
|
|
26822
26868
|
OFFICIAL_SKILL_NAMES = new Set(SKILLS.map((skill) => skill.name));
|
|
26823
26869
|
exports_external = {};
|
|
@@ -32999,8 +33045,8 @@ CREATE TABLE IF NOT EXISTS skills_sync_cursors (
|
|
|
32999
33045
|
import { createHash as createHash5 } from "crypto";
|
|
33000
33046
|
function coordinateClaudeSettings(before, replacement, options) {
|
|
33001
33047
|
const prior = JSON.parse(before ?? "{}"), next = JSON.parse(replacement);
|
|
33002
|
-
const
|
|
33003
|
-
const changed = [...
|
|
33048
|
+
const events2 = new Set([...Object.keys(prior.hooks ?? {}), ...Object.keys(next.hooks ?? {})]);
|
|
33049
|
+
const changed = [...events2].filter((event) => JSON.stringify(prior.hooks?.[event]) !== JSON.stringify(next.hooks?.[event]));
|
|
33004
33050
|
const selected = changed.length ? changed : ["PreToolUse"];
|
|
33005
33051
|
const plan = planClaudeHookEventsUpdate({
|
|
33006
33052
|
...options,
|
|
@@ -33270,7 +33316,7 @@ function detectConflict(name, scope, target) {
|
|
|
33270
33316
|
const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
33271
33317
|
if (!meta3 || !meta3.matcher)
|
|
33272
33318
|
return;
|
|
33273
|
-
const
|
|
33319
|
+
const events2 = new Set(getHookEvents(meta3));
|
|
33274
33320
|
const registered = getRegisteredHooksForTarget(scope, target);
|
|
33275
33321
|
for (const existingName of registered) {
|
|
33276
33322
|
if (existingName === name)
|
|
@@ -33278,7 +33324,7 @@ function detectConflict(name, scope, target) {
|
|
|
33278
33324
|
const existing = getHook(existingName);
|
|
33279
33325
|
if (!existing || !existing.matcher)
|
|
33280
33326
|
continue;
|
|
33281
|
-
if (!getHookEvents(existing).some((event) =>
|
|
33327
|
+
if (!getHookEvents(existing).some((event) => events2.has(event)))
|
|
33282
33328
|
continue;
|
|
33283
33329
|
if (matchersOverlap(meta3.matcher.toLowerCase(), existing.matcher.toLowerCase())) {
|
|
33284
33330
|
return `conflicts with '${existingName}' (same event ${meta3.event}, overlapping matcher '${existing.matcher}')`;
|
|
@@ -37436,10 +37482,10 @@ var init_redact = __esm(() => {
|
|
|
37436
37482
|
|
|
37437
37483
|
// src/lib/profiles.ts
|
|
37438
37484
|
import { existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync20, writeFileSync as writeFileSync12, readdirSync as readdirSync8, rmSync, cpSync as cpSync2 } from "fs";
|
|
37439
|
-
import { join as
|
|
37485
|
+
import { join as join29 } from "path";
|
|
37440
37486
|
function getProfilesDir() {
|
|
37441
|
-
const newDir =
|
|
37442
|
-
const oldDir =
|
|
37487
|
+
const newDir = join29(getEffectiveDataRoot(), "profiles");
|
|
37488
|
+
const oldDir = join29(getHomeDir(), ".hooks", "profiles");
|
|
37443
37489
|
if (!existsSync13(newDir) && existsSync13(oldDir)) {
|
|
37444
37490
|
mkdirSync13(newDir, { recursive: true });
|
|
37445
37491
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
@@ -37453,7 +37499,7 @@ function ensureProfilesDir() {
|
|
|
37453
37499
|
}
|
|
37454
37500
|
}
|
|
37455
37501
|
function profilePath(id) {
|
|
37456
|
-
return
|
|
37502
|
+
return join29(getProfilesDir(), `${id}.json`);
|
|
37457
37503
|
}
|
|
37458
37504
|
function shortUuid() {
|
|
37459
37505
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -37495,7 +37541,7 @@ function listProfiles() {
|
|
|
37495
37541
|
const profiles = [];
|
|
37496
37542
|
for (const file2 of files) {
|
|
37497
37543
|
try {
|
|
37498
|
-
const content = readFileSync20(
|
|
37544
|
+
const content = readFileSync20(join29(dir, file2), "utf-8");
|
|
37499
37545
|
profiles.push(JSON.parse(content));
|
|
37500
37546
|
} catch {}
|
|
37501
37547
|
}
|
|
@@ -37550,7 +37596,7 @@ import { createRequire as createRequire3 } from "module";
|
|
|
37550
37596
|
import { fileURLToPath as fileURLToPath22 } from "url";
|
|
37551
37597
|
import { closeSync as closeSync18, fstatSync as fstatSync17, lstatSync as lstatSync24, openSync as openSync18, readFileSync as readFileSync21, realpathSync as realpathSync13, statSync as statSync2 } from "fs";
|
|
37552
37598
|
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
37553
|
-
import { basename as basename9, dirname as dirname23, join as
|
|
37599
|
+
import { basename as basename9, dirname as dirname23, join as join30, relative as relative3, sep as sep6 } from "path";
|
|
37554
37600
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
37555
37601
|
function envToken2(name) {
|
|
37556
37602
|
return name.toUpperCase().replace(/-/g, "_");
|
|
@@ -37625,7 +37671,7 @@ function o(e2, r2, t2) {
|
|
|
37625
37671
|
}
|
|
37626
37672
|
function entryPoint(directory2) {
|
|
37627
37673
|
const root = realpathSync13(directory2);
|
|
37628
|
-
const fd = openSync18(
|
|
37674
|
+
const fd = openSync18(join30(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
37629
37675
|
let pkg;
|
|
37630
37676
|
try {
|
|
37631
37677
|
const stat2 = fstatSync17(fd);
|
|
@@ -37645,9 +37691,9 @@ function entryPoint(directory2) {
|
|
|
37645
37691
|
if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
|
|
37646
37692
|
throw new Error("Invalid Secrets import export");
|
|
37647
37693
|
}
|
|
37648
|
-
const file2 = realpathSync13(
|
|
37694
|
+
const file2 = realpathSync13(join30(root, ...parts));
|
|
37649
37695
|
const within = relative3(root, file2);
|
|
37650
|
-
if (!within || within === ".." || within.startsWith(`..${
|
|
37696
|
+
if (!within || within === ".." || within.startsWith(`..${sep6}`) || !statSync2(file2).isFile()) {
|
|
37651
37697
|
throw new Error("Secrets import export escapes its package or is not a file");
|
|
37652
37698
|
}
|
|
37653
37699
|
return pathToFileURL(file2).href;
|
|
@@ -37656,7 +37702,7 @@ function resolveInstalledSecrets(parentUrl) {
|
|
|
37656
37702
|
let directory2 = dirname23(fileURLToPath3(parentUrl));
|
|
37657
37703
|
for (;; ) {
|
|
37658
37704
|
if (basename9(directory2) !== "node_modules") {
|
|
37659
|
-
const candidate =
|
|
37705
|
+
const candidate = join30(directory2, "node_modules", PACKAGE);
|
|
37660
37706
|
let absent = false;
|
|
37661
37707
|
try {
|
|
37662
37708
|
lstatSync24(candidate);
|
|
@@ -38963,7 +39009,7 @@ __export(exports_run, {
|
|
|
38963
39009
|
});
|
|
38964
39010
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
38965
39011
|
import { rmSync as rmSync2, writeFileSync as writeFileSync13 } from "fs";
|
|
38966
|
-
import { basename as basename11, dirname as dirname25, isAbsolute as isAbsolute25, join as
|
|
39012
|
+
import { basename as basename11, dirname as dirname25, isAbsolute as isAbsolute25, join as join31 } from "path";
|
|
38967
39013
|
function extensionOf(scriptPath) {
|
|
38968
39014
|
const base = basename11(scriptPath);
|
|
38969
39015
|
const dot = base.lastIndexOf(".");
|
|
@@ -39034,7 +39080,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
39034
39080
|
async function executeVerifiedScript(options) {
|
|
39035
39081
|
const scriptDir = dirname25(options.scriptPath);
|
|
39036
39082
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
39037
|
-
const tempPath =
|
|
39083
|
+
const tempPath = join31(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
39038
39084
|
let proc = null;
|
|
39039
39085
|
let timedOut = false;
|
|
39040
39086
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -39126,10 +39172,10 @@ __export(exports_custom_install, {
|
|
|
39126
39172
|
ensureCustomDir: () => ensureCustomDir
|
|
39127
39173
|
});
|
|
39128
39174
|
import { existsSync as existsSync15, mkdirSync as mkdirSync14, mkdtempSync, readFileSync as readFileSync23, rmSync as rmSync4 } from "fs";
|
|
39129
|
-
import { join as
|
|
39175
|
+
import { join as join32 } from "path";
|
|
39130
39176
|
import { tmpdir as tmpdir2 } from "os";
|
|
39131
39177
|
function manifestAt(dir) {
|
|
39132
|
-
const path =
|
|
39178
|
+
const path = join32(dir, "manifest.json");
|
|
39133
39179
|
if (!existsSync15(path))
|
|
39134
39180
|
return;
|
|
39135
39181
|
try {
|
|
@@ -39152,8 +39198,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
39152
39198
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
39153
39199
|
}
|
|
39154
39200
|
async function cloneToTemp(gitUrl) {
|
|
39155
|
-
const tmp = mkdtempSync(
|
|
39156
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
39201
|
+
const tmp = mkdtempSync(join32(tmpdir2(), "hooks-install-"));
|
|
39202
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join32(tmp, "repo")], {
|
|
39157
39203
|
stdout: "pipe",
|
|
39158
39204
|
stderr: "pipe"
|
|
39159
39205
|
});
|
|
@@ -39166,7 +39212,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
39166
39212
|
rmSync4(tmp, { recursive: true, force: true });
|
|
39167
39213
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
39168
39214
|
}
|
|
39169
|
-
return
|
|
39215
|
+
return join32(tmp, "repo");
|
|
39170
39216
|
}
|
|
39171
39217
|
function isGitUrl(value) {
|
|
39172
39218
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -40405,11 +40451,11 @@ __export(exports_serve, {
|
|
|
40405
40451
|
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
40406
40452
|
});
|
|
40407
40453
|
import { readFileSync as readFileSync26 } from "fs";
|
|
40408
|
-
import { dirname as dirname26, join as
|
|
40454
|
+
import { dirname as dirname26, join as join33 } from "path";
|
|
40409
40455
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
40410
40456
|
function packageVersion() {
|
|
40411
40457
|
try {
|
|
40412
|
-
const pkgPath =
|
|
40458
|
+
const pkgPath = join33(dirname26(fileURLToPath4(import.meta.url)), "..", "package.json");
|
|
40413
40459
|
return JSON.parse(readFileSync26(pkgPath, "utf-8")).version || "0.0.0";
|
|
40414
40460
|
} catch {
|
|
40415
40461
|
return "0.0.0";
|
|
@@ -40711,7 +40757,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
40711
40757
|
import { z as z3 } from "zod";
|
|
40712
40758
|
import { createServer } from "http";
|
|
40713
40759
|
import { existsSync as existsSync16, readFileSync as readFileSync27 } from "fs";
|
|
40714
|
-
import { join as
|
|
40760
|
+
import { join as join34, dirname as dirname27 } from "path";
|
|
40715
40761
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
40716
40762
|
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
40717
40763
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
@@ -40945,7 +40991,7 @@ function createHooksServer() {
|
|
|
40945
40991
|
continue;
|
|
40946
40992
|
}
|
|
40947
40993
|
const hookDir = getHookPath(name);
|
|
40948
|
-
if (!existsSync16(
|
|
40994
|
+
if (!existsSync16(join34(hookDir, "src", "hook.ts"))) {
|
|
40949
40995
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
40950
40996
|
hookHealthy = false;
|
|
40951
40997
|
}
|
|
@@ -40989,7 +41035,7 @@ function createHooksServer() {
|
|
|
40989
41035
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
40990
41036
|
}
|
|
40991
41037
|
const hookPath = getHookPath(name);
|
|
40992
|
-
const readmePath =
|
|
41038
|
+
const readmePath = join34(hookPath, "README.md");
|
|
40993
41039
|
let readme = "";
|
|
40994
41040
|
if (existsSync16(readmePath)) {
|
|
40995
41041
|
readme = readFileSync27(readmePath, "utf-8");
|
|
@@ -41719,7 +41765,7 @@ var init_server = __esm(() => {
|
|
|
41719
41765
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
41720
41766
|
try {
|
|
41721
41767
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
41722
|
-
const p =
|
|
41768
|
+
const p = join34(__dirname3, rel);
|
|
41723
41769
|
if (existsSync16(p)) {
|
|
41724
41770
|
pkg = JSON.parse(readFileSync27(p, "utf-8"));
|
|
41725
41771
|
break;
|
|
@@ -41876,6 +41922,23 @@ function readNativeSafetyRegistration(home = homedir6()) {
|
|
|
41876
41922
|
throw new NativeSafetyError(error61?.code === "ENOENT" ? "binding_missing" : "binding_invalid");
|
|
41877
41923
|
}
|
|
41878
41924
|
}
|
|
41925
|
+
function nativeSafetyBindingIntegrityReason(home = homedir6()) {
|
|
41926
|
+
try {
|
|
41927
|
+
const path = join12(canonicalHome(home), ".hasna/hooks/native", filename);
|
|
41928
|
+
const stat2 = lstatSync7(path);
|
|
41929
|
+
if (!stat2.isFile())
|
|
41930
|
+
return "the saved binding is not a regular file";
|
|
41931
|
+
if (!owner2(stat2.uid))
|
|
41932
|
+
return "the saved binding is not owned by the current user";
|
|
41933
|
+
if (stat2.mode & 63)
|
|
41934
|
+
return `the saved binding is accessible by group or others (mode ${(stat2.mode & 511).toString(8).padStart(3, "0")})`;
|
|
41935
|
+
if (stat2.size > 32768)
|
|
41936
|
+
return "the saved binding exceeds its size bound";
|
|
41937
|
+
return;
|
|
41938
|
+
} catch (error61) {
|
|
41939
|
+
return error61?.code === "ENOENT" ? "the saved binding is missing" : undefined;
|
|
41940
|
+
}
|
|
41941
|
+
}
|
|
41879
41942
|
function registerNativeSafety(options = {}) {
|
|
41880
41943
|
const home = canonicalHome(options.home ?? homedir6());
|
|
41881
41944
|
const record3 = { schema: "hasna.hooks.native-safety.v1", harness: "sumi", command: options.command ?? installedNativeSafetyCommand("trash-guard") };
|
|
@@ -43205,17 +43268,17 @@ class JsonEventsStore {
|
|
|
43205
43268
|
}
|
|
43206
43269
|
async appendEvent(event) {
|
|
43207
43270
|
await this.init();
|
|
43208
|
-
const
|
|
43209
|
-
|
|
43210
|
-
await this.writeJson(this.eventsPath,
|
|
43271
|
+
const events2 = await this.readJson(this.eventsPath, []);
|
|
43272
|
+
events2.push(event);
|
|
43273
|
+
await this.writeJson(this.eventsPath, events2);
|
|
43211
43274
|
return event;
|
|
43212
43275
|
}
|
|
43213
43276
|
async appendEventOnce(event, options = {}) {
|
|
43214
43277
|
await this.init();
|
|
43215
|
-
const
|
|
43278
|
+
const events2 = await this.readJson(this.eventsPath, []);
|
|
43216
43279
|
const dedupe = options.dedupe !== false;
|
|
43217
43280
|
if (dedupe) {
|
|
43218
|
-
const existing = findEventByIdentity(
|
|
43281
|
+
const existing = findEventByIdentity(events2, { id: event.id, dedupeKey: event.dedupeKey });
|
|
43219
43282
|
if (existing) {
|
|
43220
43283
|
return {
|
|
43221
43284
|
event: existing,
|
|
@@ -43225,8 +43288,8 @@ class JsonEventsStore {
|
|
|
43225
43288
|
};
|
|
43226
43289
|
}
|
|
43227
43290
|
}
|
|
43228
|
-
|
|
43229
|
-
await this.writeJson(this.eventsPath,
|
|
43291
|
+
events2.push(event);
|
|
43292
|
+
await this.writeJson(this.eventsPath, events2);
|
|
43230
43293
|
return {
|
|
43231
43294
|
event,
|
|
43232
43295
|
stored: true,
|
|
@@ -43236,13 +43299,13 @@ class JsonEventsStore {
|
|
|
43236
43299
|
}
|
|
43237
43300
|
async listEvents(options = {}) {
|
|
43238
43301
|
await this.init();
|
|
43239
|
-
const
|
|
43240
|
-
return queryEvents(
|
|
43302
|
+
const events2 = await this.readJson(this.eventsPath, []);
|
|
43303
|
+
return queryEvents(events2, options);
|
|
43241
43304
|
}
|
|
43242
43305
|
async listEventsPage(options = {}) {
|
|
43243
43306
|
await this.init();
|
|
43244
|
-
const
|
|
43245
|
-
const queried = queryEvents(
|
|
43307
|
+
const events2 = await this.readJson(this.eventsPath, []);
|
|
43308
|
+
const queried = queryEvents(events2, {
|
|
43246
43309
|
eventId: options.eventId,
|
|
43247
43310
|
source: options.source,
|
|
43248
43311
|
type: options.type
|
|
@@ -43260,8 +43323,8 @@ class JsonEventsStore {
|
|
|
43260
43323
|
};
|
|
43261
43324
|
}
|
|
43262
43325
|
async findEventByIdentity(identity3) {
|
|
43263
|
-
const
|
|
43264
|
-
return findEventByIdentity(
|
|
43326
|
+
const events2 = await this.listEvents();
|
|
43327
|
+
return findEventByIdentity(events2, identity3);
|
|
43265
43328
|
}
|
|
43266
43329
|
async appendDelivery(result) {
|
|
43267
43330
|
await this.init();
|
|
@@ -43366,8 +43429,8 @@ function normalizeEventPageLimit(limit) {
|
|
|
43366
43429
|
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
43367
43430
|
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
43368
43431
|
}
|
|
43369
|
-
function queryEvents(
|
|
43370
|
-
let rows =
|
|
43432
|
+
function queryEvents(events2, options) {
|
|
43433
|
+
let rows = events2;
|
|
43371
43434
|
if (options.eventId)
|
|
43372
43435
|
rows = rows.filter((event) => event.id === options.eventId);
|
|
43373
43436
|
if (options.source)
|
|
@@ -43386,13 +43449,13 @@ function assertCursorFilter(name, cursorValue, optionValue) {
|
|
|
43386
43449
|
if (cursorValue !== optionValue)
|
|
43387
43450
|
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
43388
43451
|
}
|
|
43389
|
-
function findEventByIdentity(
|
|
43390
|
-
return
|
|
43452
|
+
function findEventByIdentity(events2, identity3) {
|
|
43453
|
+
return events2.find((event) => identity3.id !== undefined && event.id === identity3.id || identity3.dedupeKey !== undefined && event.dedupeKey === identity3.dedupeKey);
|
|
43391
43454
|
}
|
|
43392
43455
|
async function getEventsStatus(dataDir22) {
|
|
43393
43456
|
const store = new JsonEventsStore(dataDir22);
|
|
43394
43457
|
await store.init();
|
|
43395
|
-
const [channels,
|
|
43458
|
+
const [channels, events2, deliveries] = await Promise.all([
|
|
43396
43459
|
store.listChannels(),
|
|
43397
43460
|
store.listEvents(),
|
|
43398
43461
|
store.listDeliveries()
|
|
@@ -43413,14 +43476,14 @@ async function getEventsStatus(dataDir22) {
|
|
|
43413
43476
|
},
|
|
43414
43477
|
files: {
|
|
43415
43478
|
channels: statusFile(store.dataDir, "channels.json", channels.length),
|
|
43416
|
-
events: statusFile(store.dataDir, "events.json",
|
|
43479
|
+
events: statusFile(store.dataDir, "events.json", events2.length),
|
|
43417
43480
|
deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
|
|
43418
43481
|
},
|
|
43419
43482
|
counts: {
|
|
43420
43483
|
channels: channels.length,
|
|
43421
43484
|
enabledChannels: channels.filter((channel) => channel.enabled).length,
|
|
43422
43485
|
disabledChannels: channels.filter((channel) => !channel.enabled).length,
|
|
43423
|
-
events:
|
|
43486
|
+
events: events2.length,
|
|
43424
43487
|
deliveries: deliveries.length
|
|
43425
43488
|
},
|
|
43426
43489
|
transports,
|
|
@@ -44135,16 +44198,16 @@ class EventsClient {
|
|
|
44135
44198
|
async listEventsPage(options = {}) {
|
|
44136
44199
|
if (this.store.listEventsPage)
|
|
44137
44200
|
return this.store.listEventsPage(options);
|
|
44138
|
-
const
|
|
44201
|
+
const events2 = queryClientEvents(await this.store.listEvents(), {
|
|
44139
44202
|
eventId: options.eventId,
|
|
44140
44203
|
source: options.source,
|
|
44141
44204
|
type: options.type
|
|
44142
44205
|
});
|
|
44143
44206
|
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
44144
44207
|
const limit = normalizeEventPageLimit(options.limit);
|
|
44145
|
-
const pageEvents =
|
|
44208
|
+
const pageEvents = events2.slice(offset, offset + limit);
|
|
44146
44209
|
const nextOffset = offset + pageEvents.length;
|
|
44147
|
-
const hasMore = nextOffset <
|
|
44210
|
+
const hasMore = nextOffset < events2.length;
|
|
44148
44211
|
return {
|
|
44149
44212
|
events: pageEvents,
|
|
44150
44213
|
cursor: options.cursor,
|
|
@@ -44287,8 +44350,8 @@ function sanitizeChannelForOutput(channel) {
|
|
|
44287
44350
|
function sanitizeChannelsForOutput(channels) {
|
|
44288
44351
|
return channels.map(sanitizeChannelForOutput);
|
|
44289
44352
|
}
|
|
44290
|
-
function queryClientEvents(
|
|
44291
|
-
let rows =
|
|
44353
|
+
function queryClientEvents(events2, options) {
|
|
44354
|
+
let rows = events2;
|
|
44292
44355
|
if (options.eventId)
|
|
44293
44356
|
rows = rows.filter((event) => event.id === options.eventId);
|
|
44294
44357
|
if (options.source)
|
|
@@ -44521,8 +44584,8 @@ function registerChannelCommands(program2, options) {
|
|
|
44521
44584
|
return channels;
|
|
44522
44585
|
}
|
|
44523
44586
|
function registerEventCommands(program2, options) {
|
|
44524
|
-
const
|
|
44525
|
-
|
|
44587
|
+
const events2 = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
44588
|
+
events2.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions, command) => {
|
|
44526
44589
|
const result = await createClient(options).emit({
|
|
44527
44590
|
source: actionOptions.source ?? options.source,
|
|
44528
44591
|
type,
|
|
@@ -44536,7 +44599,7 @@ function registerEventCommands(program2, options) {
|
|
|
44536
44599
|
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
44537
44600
|
});
|
|
44538
44601
|
const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
|
|
44539
|
-
|
|
44602
|
+
events2.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) => {
|
|
44540
44603
|
let rows = await createClient(options).listEvents();
|
|
44541
44604
|
if (actionOptions.source)
|
|
44542
44605
|
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
@@ -44555,7 +44618,7 @@ function registerEventCommands(program2, options) {
|
|
|
44555
44618
|
for (const event of rows)
|
|
44556
44619
|
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
44557
44620
|
});
|
|
44558
|
-
|
|
44621
|
+
events2.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) => {
|
|
44559
44622
|
const result = await createClient(options).replay({
|
|
44560
44623
|
eventId: actionOptions.id,
|
|
44561
44624
|
source: actionOptions.source,
|
|
@@ -44566,7 +44629,7 @@ function registerEventCommands(program2, options) {
|
|
|
44566
44629
|
});
|
|
44567
44630
|
print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
|
|
44568
44631
|
});
|
|
44569
|
-
return
|
|
44632
|
+
return events2;
|
|
44570
44633
|
}
|
|
44571
44634
|
function registerEventsCommands(program2, options) {
|
|
44572
44635
|
registerChannelCommands(program2, options);
|
|
@@ -44582,9 +44645,9 @@ function collectValues(value, previous) {
|
|
|
44582
44645
|
previous.push(value);
|
|
44583
44646
|
return previous;
|
|
44584
44647
|
}
|
|
44585
|
-
function replaySummary(
|
|
44648
|
+
function replaySummary(events2, deliveries, nextCursor) {
|
|
44586
44649
|
const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
|
|
44587
|
-
return `Replayed ${
|
|
44650
|
+
return `Replayed ${events2} event(s), ${deliveries} delivery result(s)${suffix}`;
|
|
44588
44651
|
}
|
|
44589
44652
|
|
|
44590
44653
|
// src/cli/index.tsx
|
|
@@ -44609,7 +44672,7 @@ var {
|
|
|
44609
44672
|
// src/cli/index.tsx
|
|
44610
44673
|
import chalk2 from "chalk";
|
|
44611
44674
|
import { existsSync as existsSync17, readFileSync as readFileSync28, rmSync as rmSync6 } from "fs";
|
|
44612
|
-
import { join as
|
|
44675
|
+
import { join as join35, dirname as dirname28 } from "path";
|
|
44613
44676
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
44614
44677
|
|
|
44615
44678
|
// src/cli/components/App.tsx
|
|
@@ -45784,6 +45847,10 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
45784
45847
|
// src/cli/index.tsx
|
|
45785
45848
|
init_native_safety();
|
|
45786
45849
|
|
|
45850
|
+
// src/lib/sumi-binding.ts
|
|
45851
|
+
import { homedir as homedir12 } from "os";
|
|
45852
|
+
import { join as join27, sep as sep3 } from "path";
|
|
45853
|
+
|
|
45787
45854
|
// hooks/hook-trash-guard/src/binary.ts
|
|
45788
45855
|
import { dirname as dirname22, isAbsolute as isAbsolute20, join as join24, resolve as resolve21 } from "path";
|
|
45789
45856
|
import { accessSync, closeSync as closeSync17, constants as constants16, fstatSync as fstatSync15, lstatSync as lstatSync23, openSync as openSync17, readFileSync as readFileSync19, readlinkSync as readlinkSync3, readSync as readSync2, realpathSync as realpathSync12, statSync } from "fs";
|
|
@@ -45958,6 +46025,68 @@ function inspectTrashBinary(env = process.env) {
|
|
|
45958
46025
|
return { ok: false, code: failure };
|
|
45959
46026
|
}
|
|
45960
46027
|
|
|
46028
|
+
// src/lib/sumi-binding.ts
|
|
46029
|
+
init_native_safety();
|
|
46030
|
+
function failure(code2, reason) {
|
|
46031
|
+
return { ok: false, target: "sumi", code: code2, ...reason ? { reason: reason.slice(0, 300) } : {} };
|
|
46032
|
+
}
|
|
46033
|
+
function nativeSafetyBindingDurable(worker, home) {
|
|
46034
|
+
return !(worker.startsWith(join27(home, ".bun") + sep3) || worker.includes(`${sep3}node_modules${sep3}.bun${sep3}`));
|
|
46035
|
+
}
|
|
46036
|
+
async function provisionSumiSafetyBinding(options = {}) {
|
|
46037
|
+
const home = options.home ?? homedir12();
|
|
46038
|
+
let desired;
|
|
46039
|
+
try {
|
|
46040
|
+
desired = options.command ?? installedNativeSafetyCommand("trash-guard");
|
|
46041
|
+
} catch (error61) {
|
|
46042
|
+
const message = error61 instanceof Error ? error61.message : String(error61);
|
|
46043
|
+
return failure(/Bundled native safety runner is missing/.test(message) ? "runner_missing" : "guard_failed", message);
|
|
46044
|
+
}
|
|
46045
|
+
try {
|
|
46046
|
+
if (nativeSafetyCommandBinding(desired)?.name !== "trash-guard")
|
|
46047
|
+
throw new NativeSafetyError("input_invalid");
|
|
46048
|
+
const durable = nativeSafetyBindingDurable(nativeSafetyCommandBinding(desired).worker, home);
|
|
46049
|
+
let current2;
|
|
46050
|
+
try {
|
|
46051
|
+
current2 = readNativeSafetyRegistration(home);
|
|
46052
|
+
} catch (error61) {
|
|
46053
|
+
if (!(error61 instanceof NativeSafetyError) || error61.code !== "binding_missing")
|
|
46054
|
+
throw error61;
|
|
46055
|
+
}
|
|
46056
|
+
if (current2) {
|
|
46057
|
+
if (current2.record.command === desired) {
|
|
46058
|
+
return { ok: true, target: "sumi", action: "unchanged", path: current2.path, sha256: current2.sha256, changed: false, durable };
|
|
46059
|
+
}
|
|
46060
|
+
if (nativeSafetyBindingVerified(current2.record.command)) {
|
|
46061
|
+
return { ok: true, target: "sumi", action: "kept", path: current2.path, sha256: current2.sha256, changed: false, durable };
|
|
46062
|
+
}
|
|
46063
|
+
}
|
|
46064
|
+
const binding = nativeSafetyCommandBinding(desired);
|
|
46065
|
+
const trash = inspectTrashBinary({ PATH: binding.path });
|
|
46066
|
+
if (!trash.ok)
|
|
46067
|
+
return failure(trash.code);
|
|
46068
|
+
await verifyNativeSafetyCommand(desired, home);
|
|
46069
|
+
const registered = registerNativeSafety({ home, command: desired, expectedSHA256: current2?.sha256 });
|
|
46070
|
+
const verified = await verifyNativeSafetyRegistration(home);
|
|
46071
|
+
if (verified.sha256 !== registered.sha256)
|
|
46072
|
+
throw new NativeSafetyError("binding_changed");
|
|
46073
|
+
return {
|
|
46074
|
+
ok: true,
|
|
46075
|
+
target: "sumi",
|
|
46076
|
+
action: current2 ? "repaired" : "created",
|
|
46077
|
+
path: registered.path,
|
|
46078
|
+
sha256: registered.sha256,
|
|
46079
|
+
changed: true,
|
|
46080
|
+
durable,
|
|
46081
|
+
...current2 ? { backup: `${registered.path}.before-${current2.sha256}` } : {}
|
|
46082
|
+
};
|
|
46083
|
+
} catch (error61) {
|
|
46084
|
+
if (!(error61 instanceof NativeSafetyError))
|
|
46085
|
+
return failure("guard_failed", error61 instanceof Error ? error61.message : String(error61));
|
|
46086
|
+
return failure(error61.code, error61.reason ?? (error61.code === "binding_invalid" ? nativeSafetyBindingIntegrityReason(home) : undefined));
|
|
46087
|
+
}
|
|
46088
|
+
}
|
|
46089
|
+
|
|
45961
46090
|
// src/cli/index.tsx
|
|
45962
46091
|
init_registry();
|
|
45963
46092
|
init_installer();
|
|
@@ -45975,7 +46104,7 @@ init_transport2();
|
|
|
45975
46104
|
init_db();
|
|
45976
46105
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
45977
46106
|
var __dirname4 = dirname28(fileURLToPath6(import.meta.url));
|
|
45978
|
-
var pkgPath = existsSync17(
|
|
46107
|
+
var pkgPath = existsSync17(join35(__dirname4, "..", "package.json")) ? join35(__dirname4, "..", "package.json") : join35(__dirname4, "..", "..", "package.json");
|
|
45979
46108
|
var pkg2 = JSON.parse(readFileSync28(pkgPath, "utf-8"));
|
|
45980
46109
|
var program3 = new Command;
|
|
45981
46110
|
function resolveScope(options) {
|
|
@@ -46003,6 +46132,20 @@ function resolveConcreteTarget(options) {
|
|
|
46003
46132
|
return "codewith";
|
|
46004
46133
|
return "claude";
|
|
46005
46134
|
}
|
|
46135
|
+
function reportSumiBinding(result) {
|
|
46136
|
+
if (result.ok) {
|
|
46137
|
+
if (result.changed) {
|
|
46138
|
+
console.log(chalk2.green(`\u2713 Sumi native-safety binding ${result.action}`) + chalk2.dim(` (${result.path})`));
|
|
46139
|
+
if (result.backup)
|
|
46140
|
+
console.log(chalk2.dim(` previous binding preserved at ${result.backup}`));
|
|
46141
|
+
}
|
|
46142
|
+
if (!result.durable) {
|
|
46143
|
+
console.error(chalk2.yellow("\u26A0 The Sumi binding pins a worker in a tree a package upgrade rewrites") + chalk2.dim(" \u2014 re-run `hooks safety provision` after upgrading that tree, or provision from a stable versioned install tree."));
|
|
46144
|
+
}
|
|
46145
|
+
return;
|
|
46146
|
+
}
|
|
46147
|
+
console.error(chalk2.yellow(`\u26A0 Sumi native-safety binding was not provisioned (${result.code})`) + chalk2.dim(` \u2014 run \`hooks safety provision\` on this station before a Sumi seat needs its shell.`));
|
|
46148
|
+
}
|
|
46006
46149
|
function formatSettingsPath(scope, target) {
|
|
46007
46150
|
if (target === "all")
|
|
46008
46151
|
return "target-specific settings";
|
|
@@ -46194,7 +46337,7 @@ program3.command("init").description("Register a new agent profile with a unique
|
|
|
46194
46337
|
if (profile.name) {
|
|
46195
46338
|
console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
|
|
46196
46339
|
}
|
|
46197
|
-
console.log(` ${chalk2.dim("Profile:")} ${
|
|
46340
|
+
console.log(` ${chalk2.dim("Profile:")} ${join35(getProfilesDir(), `${profile.agent_id}.json`)}`);
|
|
46198
46341
|
console.log();
|
|
46199
46342
|
console.log(chalk2.dim(" Install hooks with this profile:"));
|
|
46200
46343
|
console.log(` hooks install gitguard --profile ${profile.agent_id}`);
|
|
@@ -46512,6 +46655,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
46512
46655
|
});
|
|
46513
46656
|
results2.push(result);
|
|
46514
46657
|
}
|
|
46658
|
+
const sumiBinding = await provisionSumiSafetyBinding();
|
|
46515
46659
|
if (options.json) {
|
|
46516
46660
|
console.log(JSON.stringify({
|
|
46517
46661
|
installed: results2.filter((r2) => r2.success).map((r2) => r2.hook),
|
|
@@ -46522,6 +46666,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
46522
46666
|
scope,
|
|
46523
46667
|
target,
|
|
46524
46668
|
applied: results2.some((r2) => r2.applied),
|
|
46669
|
+
sumiBinding,
|
|
46525
46670
|
...target === "codex" ? { notes: results2.filter((r2) => r2.success && r2.note).map((r2) => r2.note) } : {}
|
|
46526
46671
|
}));
|
|
46527
46672
|
if (results2.some((r2) => !r2.success)) {
|
|
@@ -46571,6 +46716,7 @@ Installing hooks (${scope}, ${target})...
|
|
|
46571
46716
|
}
|
|
46572
46717
|
console.log(chalk2.dim(`
|
|
46573
46718
|
Registered in ${settingsFile}`));
|
|
46719
|
+
reportSumiBinding(sumiBinding);
|
|
46574
46720
|
});
|
|
46575
46721
|
program3.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks (bundled + custom/registry)").action((options) => {
|
|
46576
46722
|
const scope = resolveScope(options);
|
|
@@ -46846,7 +46992,7 @@ program3.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
46846
46992
|
continue;
|
|
46847
46993
|
}
|
|
46848
46994
|
const hookDir = getHookPath(name);
|
|
46849
|
-
const hookScript =
|
|
46995
|
+
const hookScript = join35(hookDir, "src", "hook.ts");
|
|
46850
46996
|
if (!existsSync17(hookScript)) {
|
|
46851
46997
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
46852
46998
|
hookHealthy = false;
|
|
@@ -47057,7 +47203,7 @@ program3.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
47057
47203
|
return;
|
|
47058
47204
|
}
|
|
47059
47205
|
const hookPath = getHookPath(hook);
|
|
47060
|
-
const readmePath =
|
|
47206
|
+
const readmePath = join35(hookPath, "README.md");
|
|
47061
47207
|
let readme = "";
|
|
47062
47208
|
if (existsSync17(readmePath)) {
|
|
47063
47209
|
readme = readFileSync28(readmePath, "utf-8");
|
|
@@ -47233,11 +47379,13 @@ Upgrading @hasna/hooks (${pm})...
|
|
|
47233
47379
|
}
|
|
47234
47380
|
const versionProc = Bun.spawnSync(["npm", "view", "@hasna/hooks", "version"]);
|
|
47235
47381
|
const latest = new TextDecoder().decode(versionProc.stdout).trim() || "unknown";
|
|
47382
|
+
const sumiBinding = await provisionSumiSafetyBinding();
|
|
47236
47383
|
if (options.json) {
|
|
47237
|
-
console.log(JSON.stringify({ current: current2, latest, updated: true }));
|
|
47384
|
+
console.log(JSON.stringify({ current: current2, latest, updated: true, sumiBinding }));
|
|
47238
47385
|
} else {
|
|
47239
47386
|
console.log(chalk2.green(`
|
|
47240
47387
|
\u2713 Upgraded: ${current2} \u2192 ${latest}`));
|
|
47388
|
+
reportSumiBinding(sumiBinding);
|
|
47241
47389
|
}
|
|
47242
47390
|
});
|
|
47243
47391
|
program3.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) => {
|
|
@@ -47810,6 +47958,17 @@ safety.command("verify").requiredOption("--target <target>", "sumi, codex or cla
|
|
|
47810
47958
|
process.exitCode = 1;
|
|
47811
47959
|
}
|
|
47812
47960
|
});
|
|
47961
|
+
safety.command("provision").option("--target <target>", "sumi", "sumi").description("Idempotently provision the required native binding from this installed package").action(async (options) => {
|
|
47962
|
+
if (options.target !== "sumi") {
|
|
47963
|
+
console.log(JSON.stringify({ ok: false, target: options.target, code: "input_invalid" }));
|
|
47964
|
+
process.exitCode = 1;
|
|
47965
|
+
return;
|
|
47966
|
+
}
|
|
47967
|
+
const result = await provisionSumiSafetyBinding();
|
|
47968
|
+
console.log(JSON.stringify(result));
|
|
47969
|
+
if (!result.ok)
|
|
47970
|
+
process.exitCode = 1;
|
|
47971
|
+
});
|
|
47813
47972
|
var API_INDEPENDENT_COMMANDS = new Set([
|
|
47814
47973
|
"safety",
|
|
47815
47974
|
"serve",
|