javi-forge 1.40.0 → 1.42.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/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +51 -16
- package/assets/claude-hooks/manifest.json +6 -2
- package/assets/grok-hooks/manifest.json +9 -0
- package/dist/cli/dispatch/hooks.d.ts +1 -1
- package/dist/cli/dispatch/hooks.js +4 -2
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +20 -3
- package/dist/commands/cursor-hooks.d.ts +15 -0
- package/dist/commands/cursor-hooks.js +44 -0
- package/dist/commands/grok-hooks.d.ts +15 -0
- package/dist/commands/grok-hooks.js +45 -0
- package/dist/lib/agent-adapter.d.ts +5 -3
- package/dist/lib/agent-adapter.js +65 -1
- package/dist/lib/cursor-hook-manager.d.ts +75 -0
- package/dist/lib/cursor-hook-manager.js +379 -0
- package/dist/lib/grok-hook-manager.d.ts +88 -0
- package/dist/lib/grok-hook-manager.js +327 -0
- package/package.json +1 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build SkillGuard hook ownership manager. Grok reads trusted global hook
|
|
3
|
+
* definitions from ~/.grok/hooks, so installation is a two-file transaction:
|
|
4
|
+
* the PreToolUse JSON registration and the adjacent policy runtime. Doctor
|
|
5
|
+
* deliberately reports installed bytes/registration only, not runtime loading.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { CLAUDE_HOOK_ASSETS_DIR, FORGE_ROOT } from "../constants.js";
|
|
12
|
+
import { safeReadFile } from "./safe-read.js";
|
|
13
|
+
import { selectSecureFs } from "./secure-fs-posix.js";
|
|
14
|
+
import { runTransaction, } from "./secure-fs-transaction.js";
|
|
15
|
+
const MAX_BYTES = 1024 * 1024;
|
|
16
|
+
const READ_OPTS = {
|
|
17
|
+
maxBytes: MAX_BYTES,
|
|
18
|
+
hardRejectOverBytes: MAX_BYTES,
|
|
19
|
+
maxLineLength: Number.POSITIVE_INFINITY,
|
|
20
|
+
};
|
|
21
|
+
export const GROK_HOOK_NAME = "javi-forge-skillguard-pre-tool-use.json";
|
|
22
|
+
export const GROK_POLICY_NAME = "javi-forge-skillguard-pre-tool-use.mjs";
|
|
23
|
+
export const GROK_HOOK_MARKER = "javi-forge-managed: grok-pretooluse v1";
|
|
24
|
+
export const GROK_POLICY_MARKER = "// javi-forge-managed: claude-pretooluse v1";
|
|
25
|
+
export const GROK_HOOK_VERSION = 1;
|
|
26
|
+
export const GROK_TOOL_MATCHER = "run_terminal_command|read_file|search_replace";
|
|
27
|
+
export const GROK_ASSETS_DIR = path.join(FORGE_ROOT, "assets", "grok-hooks");
|
|
28
|
+
export const SHIPPED_GROK_POLICY = path.join(CLAUDE_HOOK_ASSETS_DIR, GROK_POLICY_NAME);
|
|
29
|
+
export function resolveGrokHomeRoot(baseDir, env = process.env) {
|
|
30
|
+
const override = env.GROK_HOME;
|
|
31
|
+
return override && override.length > 0
|
|
32
|
+
? path.resolve(override)
|
|
33
|
+
: path.join(baseDir, ".grok");
|
|
34
|
+
}
|
|
35
|
+
export function grokConfigPaths(baseDir, env = process.env) {
|
|
36
|
+
const grokDir = resolveGrokHomeRoot(baseDir, env);
|
|
37
|
+
const hooksDir = path.join(grokDir, "hooks");
|
|
38
|
+
return {
|
|
39
|
+
grokDir,
|
|
40
|
+
hooksDir,
|
|
41
|
+
hookFile: path.join(hooksDir, GROK_HOOK_NAME),
|
|
42
|
+
policyFile: path.join(hooksDir, GROK_POLICY_NAME),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Generate the complete standalone Grok hook registration for an adjacent asset. */
|
|
46
|
+
export function expectedGrokHookJson(policyFile) {
|
|
47
|
+
const definition = {
|
|
48
|
+
"javi-forge-managed": GROK_HOOK_MARKER,
|
|
49
|
+
version: GROK_HOOK_VERSION,
|
|
50
|
+
hooks: {
|
|
51
|
+
PreToolUse: [
|
|
52
|
+
{
|
|
53
|
+
matcher: GROK_TOOL_MATCHER,
|
|
54
|
+
hooks: [
|
|
55
|
+
{
|
|
56
|
+
type: "command",
|
|
57
|
+
command: `${JSON.stringify(process.execPath)} ${JSON.stringify(policyFile)} --agent=grok`,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
return `${JSON.stringify(definition, null, 2)}\n`;
|
|
65
|
+
}
|
|
66
|
+
async function fileKind(target) {
|
|
67
|
+
try {
|
|
68
|
+
const stat = await lstat(target);
|
|
69
|
+
if (stat.isSymbolicLink())
|
|
70
|
+
return { kind: "symlink" };
|
|
71
|
+
return stat.isFile() ? { kind: "file" } : { kind: "non-regular" };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const code = error.code;
|
|
75
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
76
|
+
return { kind: "absent" };
|
|
77
|
+
return { kind: "error", detail: code ?? String(error) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function sha256(content) {
|
|
81
|
+
return createHash("sha256")
|
|
82
|
+
.update(Buffer.from(content, "utf8"))
|
|
83
|
+
.digest("hex");
|
|
84
|
+
}
|
|
85
|
+
/** Classify an installed policy runtime with the packaged manifest identity. */
|
|
86
|
+
export async function classifyGrokAsset(target, marker, manifest) {
|
|
87
|
+
const stat = await fileKind(target);
|
|
88
|
+
if (stat.kind === "absent")
|
|
89
|
+
return { state: "absent" };
|
|
90
|
+
if (stat.kind === "symlink")
|
|
91
|
+
return { state: "symlink" };
|
|
92
|
+
if (stat.kind !== "file")
|
|
93
|
+
return { state: "non-regular", detail: stat.detail };
|
|
94
|
+
const read = await safeReadFile(target, READ_OPTS);
|
|
95
|
+
if (!read.ok) {
|
|
96
|
+
if (read.reason === "not-found")
|
|
97
|
+
return { state: "absent" };
|
|
98
|
+
if (read.reason === "binary" || read.reason === "too-large")
|
|
99
|
+
return { state: "foreign", detail: read.reason };
|
|
100
|
+
return { state: "non-regular", detail: read.reason };
|
|
101
|
+
}
|
|
102
|
+
if (!read.content.startsWith(`${marker}\n`))
|
|
103
|
+
return { state: "foreign" };
|
|
104
|
+
const observed = sha256(read.content);
|
|
105
|
+
if (observed === manifest.sha256)
|
|
106
|
+
return {
|
|
107
|
+
state: "managed-current",
|
|
108
|
+
sha256: observed,
|
|
109
|
+
version: manifest.version,
|
|
110
|
+
};
|
|
111
|
+
if (manifest.historical.includes(observed))
|
|
112
|
+
return {
|
|
113
|
+
state: "released-outdated",
|
|
114
|
+
sha256: observed,
|
|
115
|
+
version: manifest.version,
|
|
116
|
+
};
|
|
117
|
+
return { state: "edited-managed", sha256: observed };
|
|
118
|
+
}
|
|
119
|
+
function isObject(value) {
|
|
120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Classify a generated hook registration semantically. JSON whitespace does not
|
|
124
|
+
* matter; the command is compared against the supplied baseDir's adjacent asset.
|
|
125
|
+
*/
|
|
126
|
+
export async function classifyGrokHookJson(target, policyFile, manifest) {
|
|
127
|
+
const stat = await fileKind(target);
|
|
128
|
+
if (stat.kind === "absent")
|
|
129
|
+
return { state: "absent" };
|
|
130
|
+
if (stat.kind === "symlink")
|
|
131
|
+
return { state: "symlink" };
|
|
132
|
+
if (stat.kind !== "file")
|
|
133
|
+
return { state: "non-regular", detail: stat.detail };
|
|
134
|
+
const read = await safeReadFile(target, READ_OPTS);
|
|
135
|
+
if (!read.ok) {
|
|
136
|
+
if (read.reason === "not-found")
|
|
137
|
+
return { state: "absent" };
|
|
138
|
+
if (read.reason === "binary" || read.reason === "too-large")
|
|
139
|
+
return { state: "foreign", detail: read.reason };
|
|
140
|
+
return { state: "non-regular", detail: read.reason };
|
|
141
|
+
}
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(read.content);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { state: "malformed" };
|
|
148
|
+
}
|
|
149
|
+
if (!isObject(parsed))
|
|
150
|
+
return { state: "malformed" };
|
|
151
|
+
if (parsed["javi-forge-managed"] !== GROK_HOOK_MARKER)
|
|
152
|
+
return { state: "foreign" };
|
|
153
|
+
if (typeof parsed.version !== "number")
|
|
154
|
+
return { state: "edited-managed" };
|
|
155
|
+
if (manifest.historicalVersions.includes(parsed.version))
|
|
156
|
+
return { state: "released-outdated", version: parsed.version };
|
|
157
|
+
const expected = JSON.parse(expectedGrokHookJson(policyFile));
|
|
158
|
+
if (parsed.version === manifest.version &&
|
|
159
|
+
JSON.stringify(parsed) === JSON.stringify(expected))
|
|
160
|
+
return { state: "managed-current", version: parsed.version };
|
|
161
|
+
return { state: "edited-managed", version: parsed.version };
|
|
162
|
+
}
|
|
163
|
+
async function readManifest() {
|
|
164
|
+
const [hookRead, policyRead] = await Promise.all([
|
|
165
|
+
safeReadFile(path.join(GROK_ASSETS_DIR, "manifest.json"), READ_OPTS),
|
|
166
|
+
safeReadFile(path.join(CLAUDE_HOOK_ASSETS_DIR, "manifest.json"), READ_OPTS),
|
|
167
|
+
]);
|
|
168
|
+
if (!hookRead.ok)
|
|
169
|
+
throw new Error(`unreadable grok hook manifest: ${hookRead.reason}`);
|
|
170
|
+
if (!policyRead.ok)
|
|
171
|
+
throw new Error(`unreadable claude-hooks manifest: ${policyRead.reason}`);
|
|
172
|
+
const hookManifest = JSON.parse(hookRead.content);
|
|
173
|
+
const policyManifest = JSON.parse(policyRead.content);
|
|
174
|
+
return { hook: hookManifest.hook, policy: policyManifest.asset };
|
|
175
|
+
}
|
|
176
|
+
export async function doctorGrokSkillGuard(baseDir = os.homedir(), options = {}) {
|
|
177
|
+
const manifest = options.manifest ?? (await readManifest());
|
|
178
|
+
const paths = grokConfigPaths(baseDir, options.env);
|
|
179
|
+
const [hook, policy] = await Promise.all([
|
|
180
|
+
classifyGrokHookJson(paths.hookFile, paths.policyFile, manifest.hook),
|
|
181
|
+
classifyGrokAsset(paths.policyFile, GROK_POLICY_MARKER, manifest.policy),
|
|
182
|
+
]);
|
|
183
|
+
const remediation = [];
|
|
184
|
+
for (const [name, asset] of [
|
|
185
|
+
["hook registration", hook],
|
|
186
|
+
["policy runtime", policy],
|
|
187
|
+
]) {
|
|
188
|
+
if (asset.state === "absent" || asset.state === "released-outdated") {
|
|
189
|
+
remediation.push(`install the Grok ${name} with: javi-forge hooks install grok`);
|
|
190
|
+
}
|
|
191
|
+
else if (asset.state === "edited-managed") {
|
|
192
|
+
remediation.push(`repair the edited Grok ${name} with: javi-forge hooks repair grok --force`);
|
|
193
|
+
}
|
|
194
|
+
else if (asset.state !== "managed-current") {
|
|
195
|
+
remediation.push(`manually review the Grok ${name} (${asset.state}); it will not be overwritten`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
healthy: hook.state === "managed-current" && policy.state === "managed-current",
|
|
200
|
+
hook,
|
|
201
|
+
policy,
|
|
202
|
+
remediation: [...new Set(remediation)],
|
|
203
|
+
runtimeEvidence: "not-implemented",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function writePlan(state, force, name) {
|
|
207
|
+
if (state === "absent" || state === "released-outdated")
|
|
208
|
+
return { write: true };
|
|
209
|
+
if (state === "managed-current")
|
|
210
|
+
return { write: false };
|
|
211
|
+
if (state === "edited-managed" && force)
|
|
212
|
+
return { write: true };
|
|
213
|
+
if (state === "edited-managed")
|
|
214
|
+
return {
|
|
215
|
+
write: false,
|
|
216
|
+
refusal: `refuse edited ${name}; rerun repair with --force`,
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
write: false,
|
|
220
|
+
refusal: `refuse ${name} in state ${state} — manual review`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export async function _runGrok(baseDir, mode, options, deps = {}) {
|
|
224
|
+
const manifest = deps.manifest ?? (await readManifest());
|
|
225
|
+
const paths = grokConfigPaths(baseDir, deps.env);
|
|
226
|
+
const doctorFn = deps.doctor ?? doctorGrokSkillGuard;
|
|
227
|
+
const doctor = () => doctorFn(baseDir, {
|
|
228
|
+
env: deps.env,
|
|
229
|
+
manifest,
|
|
230
|
+
policySource: deps.policySource,
|
|
231
|
+
});
|
|
232
|
+
const force = mode === "repair" && options.force === true;
|
|
233
|
+
const [hook, policy] = await Promise.all([
|
|
234
|
+
classifyGrokHookJson(paths.hookFile, paths.policyFile, manifest.hook),
|
|
235
|
+
classifyGrokAsset(paths.policyFile, GROK_POLICY_MARKER, manifest.policy),
|
|
236
|
+
]);
|
|
237
|
+
const hookPlan = writePlan(hook.state, force, "Grok hook registration");
|
|
238
|
+
const policyPlan = writePlan(policy.state, force, "Grok policy runtime");
|
|
239
|
+
if (hookPlan.refusal || policyPlan.refusal) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
changed: [],
|
|
243
|
+
backups: [],
|
|
244
|
+
errors: [hookPlan.refusal ?? policyPlan.refusal ?? "refused"],
|
|
245
|
+
warnings: [],
|
|
246
|
+
report: await doctor(),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (!hookPlan.write && !policyPlan.write) {
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
changed: [],
|
|
253
|
+
backups: [],
|
|
254
|
+
errors: [],
|
|
255
|
+
warnings: [],
|
|
256
|
+
report: await doctor(),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const secureFs = deps.secureFs !== undefined
|
|
260
|
+
? deps.secureFs
|
|
261
|
+
: selectSecureFs(process.platform);
|
|
262
|
+
if (!secureFs) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
changed: [],
|
|
266
|
+
backups: [],
|
|
267
|
+
errors: ["windows-secure-object-unavailable"],
|
|
268
|
+
warnings: [],
|
|
269
|
+
report: await doctor(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const policySource = deps.policySource ?? SHIPPED_GROK_POLICY;
|
|
273
|
+
const policyBytes = policyPlan.write ? await readFile(policySource) : null;
|
|
274
|
+
if (policyBytes &&
|
|
275
|
+
createHash("sha256").update(policyBytes).digest("hex") !==
|
|
276
|
+
manifest.policy.sha256) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
changed: [],
|
|
280
|
+
backups: [],
|
|
281
|
+
errors: ["refuse packaged Grok policy asset with manifest hash mismatch"],
|
|
282
|
+
warnings: [],
|
|
283
|
+
report: await doctor(),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const hookBytes = hookPlan.write
|
|
287
|
+
? Buffer.from(expectedGrokHookJson(paths.policyFile), "utf8")
|
|
288
|
+
: null;
|
|
289
|
+
const components = [
|
|
290
|
+
{
|
|
291
|
+
path: paths.hookFile,
|
|
292
|
+
desired: hookBytes,
|
|
293
|
+
capturePrior: hookPlan.write && hook.state !== "absent",
|
|
294
|
+
forceBackup: force && hook.state === "edited-managed",
|
|
295
|
+
wasAbsent: hook.state === "absent",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
path: paths.policyFile,
|
|
299
|
+
desired: policyBytes,
|
|
300
|
+
capturePrior: policyPlan.write && policy.state !== "absent",
|
|
301
|
+
forceBackup: force && policy.state === "edited-managed",
|
|
302
|
+
wasAbsent: policy.state === "absent",
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
const tx = await runTransaction({
|
|
306
|
+
secureFs,
|
|
307
|
+
clock: deps.clock ?? (() => new Date()),
|
|
308
|
+
nonce: deps.nonce ?? (() => randomBytes(4).toString("hex")),
|
|
309
|
+
projectDir: path.dirname(paths.grokDir),
|
|
310
|
+
layout: { containers: [paths.grokDir, paths.hooksDir], components },
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
ok: tx.ok,
|
|
314
|
+
changed: tx.committed,
|
|
315
|
+
backups: tx.backups,
|
|
316
|
+
errors: tx.errors,
|
|
317
|
+
warnings: [],
|
|
318
|
+
report: await doctor(),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
export function installGrokSkillGuard(baseDir, deps = {}) {
|
|
322
|
+
return _runGrok(baseDir ?? os.homedir(), "install", {}, deps);
|
|
323
|
+
}
|
|
324
|
+
export function repairGrokSkillGuard(baseDir, options, deps = {}) {
|
|
325
|
+
return _runGrok(baseDir ?? os.homedir(), "repair", options ?? {}, deps);
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=grok-hook-manager.js.map
|