@scopebond/hook 0.6.0 → 0.8.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 +123 -13
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +825 -92
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts +2 -0
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +1 -1
- package/dist/cloud.js.map +1 -1
- package/dist/explain.d.ts +44 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +75 -0
- package/dist/explain.js.map +1 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +41 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +104 -21
- package/dist/init.js.map +1 -1
- package/dist/install.d.ts +70 -2
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +262 -10
- package/dist/install.js.map +1 -1
- package/dist/map.d.ts +6 -0
- package/dist/map.d.ts.map +1 -1
- package/dist/map.js +4 -1
- package/dist/map.js.map +1 -1
- package/dist/minimize.d.ts +11 -3
- package/dist/minimize.d.ts.map +1 -1
- package/dist/minimize.js +35 -6
- package/dist/minimize.js.map +1 -1
- package/dist/rules.d.ts +51 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +216 -0
- package/dist/rules.js.map +1 -0
- package/dist/runtime-install.d.ts +25 -0
- package/dist/runtime-install.d.ts.map +1 -0
- package/dist/runtime-install.js +106 -0
- package/dist/runtime-install.js.map +1 -0
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +52 -7
- package/dist/runtime.js.map +1 -1
- package/dist/version.d.ts +5 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +6 -2
- package/dist/version.js.map +1 -1
- package/package.json +2 -2
package/dist/install.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
// user-level install exists, a project-local `.scopebond` is used only if the user
|
|
6
6
|
// trusted that exact policy (`scopebond trust`, or `init` in the project): a cloned
|
|
7
7
|
// repository, or an agent inside it, must not be able to swap in its own policy.
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
8
9
|
import { createHash } from "node:crypto";
|
|
9
|
-
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
10
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, copyFileSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
12
13
|
/** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */
|
|
13
14
|
export function userHome() {
|
|
14
15
|
return process.env.SCOPEBOND_HOME ?? join(homedir(), ".scopebond");
|
|
@@ -22,6 +23,135 @@ export function userHarnessFile(harness) {
|
|
|
22
23
|
return join(homedir(), ".codex", "hooks.json");
|
|
23
24
|
return join(homedir(), ".claude", "settings.json");
|
|
24
25
|
}
|
|
26
|
+
/** The project-level agent config file for a harness — what `init` writes, so the
|
|
27
|
+
* hook travels with the repository rather than the machine. */
|
|
28
|
+
export function projectHarnessFile(harness, cwd = process.cwd()) {
|
|
29
|
+
if (harness === "cursor")
|
|
30
|
+
return join(cwd, ".cursor", "hooks.json");
|
|
31
|
+
if (harness === "codex")
|
|
32
|
+
return join(cwd, ".codex", "hooks.json");
|
|
33
|
+
return join(cwd, ".claude", "settings.json");
|
|
34
|
+
}
|
|
35
|
+
/** The personal, per-machine project config for a harness, or null when the harness
|
|
36
|
+
* has none. Claude Code reads `.claude/settings.local.json` beside `settings.json` and
|
|
37
|
+
* merges the hooks of both; it is the place for anything that names a path on this
|
|
38
|
+
* machine. Cursor and Codex have no such file. */
|
|
39
|
+
export function localHarnessFile(harness, cwd = process.cwd()) {
|
|
40
|
+
return harness === "claude" ? join(cwd, ".claude", "settings.local.json") : null;
|
|
41
|
+
}
|
|
42
|
+
export function harnessScopes(harness, cwd = process.cwd()) {
|
|
43
|
+
const project = projectHarnessFile(harness, cwd);
|
|
44
|
+
const local = localHarnessFile(harness, cwd);
|
|
45
|
+
const user = userHarnessFile(harness);
|
|
46
|
+
return {
|
|
47
|
+
project: isHarnessConfigured(project) ? project : null,
|
|
48
|
+
local: local && isHarnessConfigured(local) ? local : null,
|
|
49
|
+
user: isHarnessConfigured(user) ? user : null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** One word for where a harness is wired, for a status line. */
|
|
53
|
+
export function harnessScopeLabel(scopes) {
|
|
54
|
+
const where = [
|
|
55
|
+
scopes.project ? "project" : "",
|
|
56
|
+
scopes.local ? "this machine" : "",
|
|
57
|
+
scopes.user ? "user" : "",
|
|
58
|
+
].filter(Boolean);
|
|
59
|
+
if (where.length === 0)
|
|
60
|
+
return "";
|
|
61
|
+
if (where.length === 1 && scopes.project)
|
|
62
|
+
return "configured (this project)";
|
|
63
|
+
if (where.length === 1 && scopes.local)
|
|
64
|
+
return "configured (this project, this machine only)";
|
|
65
|
+
if (where.length === 1 && scopes.user)
|
|
66
|
+
return "configured (user-level)";
|
|
67
|
+
return `configured (${where.join(" + ")})`;
|
|
68
|
+
}
|
|
69
|
+
/** Whether a hook command names a path that exists only on this machine — the pinned
|
|
70
|
+
* `"<node>" "<…/cli.js>" claude` form — rather than the portable `npx` form. */
|
|
71
|
+
export function isMachineSpecificCommand(command) {
|
|
72
|
+
if (/(^|\s)npx(\.cmd)?\s/.test(command))
|
|
73
|
+
return false;
|
|
74
|
+
const first = (command.match(/"[^"]+"|\S+/) ?? [""])[0].replace(/^"|"$/g, "");
|
|
75
|
+
return isAbsolute(first) || /^[A-Za-z]:[\\/]/.test(first);
|
|
76
|
+
}
|
|
77
|
+
export function gitShareState(file) {
|
|
78
|
+
const cwd = dirname(file);
|
|
79
|
+
const git = (args) => {
|
|
80
|
+
try {
|
|
81
|
+
mkdirSync(cwd, { recursive: true });
|
|
82
|
+
execFileSync("git", args, { cwd, stdio: "ignore", timeout: 5000 });
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const status = error.status;
|
|
87
|
+
return typeof status === "number" ? status : -1;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
if (git(["rev-parse", "--is-inside-work-tree"]) !== 0)
|
|
91
|
+
return "none";
|
|
92
|
+
if (git(["ls-files", "--error-unmatch", "--", file]) === 0)
|
|
93
|
+
return "tracked";
|
|
94
|
+
return git(["check-ignore", "-q", "--", file]) === 0 ? "ignored" : "untracked";
|
|
95
|
+
}
|
|
96
|
+
/** Keep a file out of git for this clone only, through `.git/info/exclude` — never the
|
|
97
|
+
* repository's own `.gitignore`, which is the team's file. Returns true when the file is
|
|
98
|
+
* ignored afterwards. */
|
|
99
|
+
export function excludeFromGit(file) {
|
|
100
|
+
try {
|
|
101
|
+
const cwd = dirname(file);
|
|
102
|
+
mkdirSync(cwd, { recursive: true });
|
|
103
|
+
// The entry is built from git's own view of where this directory sits in the work
|
|
104
|
+
// tree (`--show-prefix`), not from comparing file-system paths: the same directory
|
|
105
|
+
// can be spelled two ways (a Windows 8.3 short name, a symlinked temp dir), and a
|
|
106
|
+
// path computed across the two spellings excludes the wrong file.
|
|
107
|
+
const prefix = execFileSync("git", ["rev-parse", "--show-prefix"], { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
108
|
+
const exclude = resolve(cwd, execFileSync("git", ["rev-parse", "--git-path", "info/exclude"], { cwd, encoding: "utf8", timeout: 5000 }).trim());
|
|
109
|
+
const entry = "/" + prefix + basename(file);
|
|
110
|
+
const existing = existsSync(exclude) ? readFileSync(exclude, "utf8") : "";
|
|
111
|
+
if (!existing.split(/\r?\n/).includes(entry)) {
|
|
112
|
+
mkdirSync(dirname(exclude), { recursive: true });
|
|
113
|
+
writeFileSync(exclude, `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${entry}\n`);
|
|
114
|
+
}
|
|
115
|
+
return gitShareState(file) === "ignored";
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Remove the Scopebond entries whose command `remove` selects, leaving every other
|
|
122
|
+
* setting alone. Returns the number of entries removed. */
|
|
123
|
+
export function pruneHarnessEntries(file, remove) {
|
|
124
|
+
if (!existsSync(file))
|
|
125
|
+
return 0;
|
|
126
|
+
let config;
|
|
127
|
+
try {
|
|
128
|
+
config = readHarnessConfig(file);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
const hooks = isRecord(config.hooks) ? config.hooks : null;
|
|
134
|
+
if (!hooks)
|
|
135
|
+
return 0;
|
|
136
|
+
let removed = 0;
|
|
137
|
+
const commandsOf = (e) => {
|
|
138
|
+
if (!isRecord(e))
|
|
139
|
+
return [];
|
|
140
|
+
const own = isScopebond(e.command) ? [String(e.command)] : [];
|
|
141
|
+
const nested = Array.isArray(e.hooks) ? e.hooks.flatMap(commandsOf) : [];
|
|
142
|
+
return [...own, ...nested];
|
|
143
|
+
};
|
|
144
|
+
for (const [event, value] of Object.entries(hooks)) {
|
|
145
|
+
if (!Array.isArray(value))
|
|
146
|
+
continue;
|
|
147
|
+
const kept = value.filter((e) => !(entryMatches(e) && commandsOf(e).some(remove)));
|
|
148
|
+
removed += value.length - kept.length;
|
|
149
|
+
hooks[event] = kept;
|
|
150
|
+
}
|
|
151
|
+
if (removed > 0)
|
|
152
|
+
writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
|
|
153
|
+
return removed;
|
|
154
|
+
}
|
|
25
155
|
/** The file in the user home that pins trusted project policies: absolute project
|
|
26
156
|
* config dir → SHA-256 of its policy.json. It lives under the protected home, so the
|
|
27
157
|
* governed agent cannot write it. */
|
|
@@ -96,11 +226,52 @@ export function untrustedProjectPolicy(payloadCwd) {
|
|
|
96
226
|
return dirs.find((d) => existsSync(join(d, "policy.json")) && !isTrustedProject(d)) ?? null;
|
|
97
227
|
}
|
|
98
228
|
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
/
|
|
103
|
-
|
|
229
|
+
/** Whether a harness command string is one of ours. This is the single matcher for
|
|
230
|
+
* every form the installer has ever written, and it is what makes re-running `init`
|
|
231
|
+
* or `install` replace the entry instead of appending a second one:
|
|
232
|
+
* - `npx -y @scopebond/hook@0.6.0 claude` (portable)
|
|
233
|
+
* - `"<node>" "<…/@scopebond/hook/dist/cli.js>" claude` (pinned, POSIX)
|
|
234
|
+
* - `"<node>" "<…\@scopebond\hook\dist\cli.js>" claude` (pinned, Windows)
|
|
235
|
+
* - `scopebond-hook claude` (legacy global binary)
|
|
236
|
+
* The separator class matters: a Windows pinned path spells the scope
|
|
237
|
+
* `@scopebond\hook`, and matching only `@scopebond/hook` let a second `init`
|
|
238
|
+
* install a duplicate hook — which would double-check every tool call.
|
|
239
|
+
*
|
|
240
|
+
* Written with substring and token tests rather than one regular expression: the
|
|
241
|
+
* previous `(^|["\s])scopebond(["'\s]).*\b(claude|cursor|codex)\b` backtracked
|
|
242
|
+
* polynomially on a long run of `\tscopebond\t` (CodeQL js/polynomial-redos). The
|
|
243
|
+
* input is a command out of a config file rather than a request, so this was not
|
|
244
|
+
* reachable by an attacker — but a linear check is also the clearer one. */
|
|
245
|
+
const HARNESS_SUBCOMMANDS = new Set(["claude", "cursor", "codex"]);
|
|
246
|
+
/** Long enough for any real command; a bound keeps the work linear and small. */
|
|
247
|
+
const MAX_COMMAND = 4096;
|
|
248
|
+
const unquote = (token) => token.replace(/^["']+|["']+$/g, "");
|
|
249
|
+
export const isScopebondHookCommand = (cmd) => {
|
|
250
|
+
if (typeof cmd !== "string" || cmd.length === 0 || cmd.length > MAX_COMMAND)
|
|
251
|
+
return false;
|
|
252
|
+
const lower = cmd.toLowerCase();
|
|
253
|
+
// The package itself, in either path spelling (`@scopebond/hook`, `@scopebond\hook`).
|
|
254
|
+
if (lower.includes("@scopebond/hook") || lower.includes("@scopebond\\hook"))
|
|
255
|
+
return true;
|
|
256
|
+
// The legacy global binary, as a whole token.
|
|
257
|
+
if (/(^|[\s"'])scopebond-hook([\s"']|$)/.test(cmd))
|
|
258
|
+
return true;
|
|
259
|
+
// An absolute or bare form whose last argument is one of our harness subcommands:
|
|
260
|
+
// `<node> <…/cli.js> claude`, `scopebond cursor`.
|
|
261
|
+
const tokens = cmd.split(/\s+/).filter(Boolean);
|
|
262
|
+
const last = unquote(tokens[tokens.length - 1] ?? "");
|
|
263
|
+
if (!HARNESS_SUBCOMMANDS.has(last))
|
|
264
|
+
return false;
|
|
265
|
+
return tokens.some((token) => {
|
|
266
|
+
const t = unquote(token).toLowerCase();
|
|
267
|
+
return t.includes("scopebond") || /cli\.(m|c)?js$/.test(t);
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
const isScopebond = isScopebondHookCommand;
|
|
271
|
+
/** Whether a harness hook entry (either shape: a bare command, or a group with a
|
|
272
|
+
* nested `hooks` array) is a Scopebond entry. */
|
|
273
|
+
export const harnessEntryMatches = (e) => isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));
|
|
274
|
+
const entryMatches = harnessEntryMatches;
|
|
104
275
|
/** Read an agent's JSON config for merging. A missing file is an empty config; a file
|
|
105
276
|
* that exists but does not parse as a JSON object is an error — rewriting it would
|
|
106
277
|
* silently delete the user's other settings, so the installer stops instead. */
|
|
@@ -121,10 +292,33 @@ export function readHarnessConfig(file) {
|
|
|
121
292
|
throw new Error(`${file} is not a JSON object; fix or move it, then run this again — it was left unchanged`);
|
|
122
293
|
return parsed;
|
|
123
294
|
}
|
|
295
|
+
/** Copy an agent config aside before changing it, once. Re-running an installer must not
|
|
296
|
+
* overwrite the backup with the already-modified file, or the original is lost on the
|
|
297
|
+
* second run — so an existing backup is left alone. Returns the backup path when one was
|
|
298
|
+
* written.
|
|
299
|
+
*
|
|
300
|
+
* This exists because these installers rewrite files the user did not create and may not
|
|
301
|
+
* know are involved (`~/.claude/settings.json` holds their theme, plugins and
|
|
302
|
+
* permissions), and the undo was previously "hope the merge was right". */
|
|
303
|
+
export function backupHarnessConfig(file) {
|
|
304
|
+
if (!existsSync(file))
|
|
305
|
+
return null;
|
|
306
|
+
const backup = `${file}.scopebond-backup`;
|
|
307
|
+
if (existsSync(backup))
|
|
308
|
+
return null;
|
|
309
|
+
try {
|
|
310
|
+
copyFileSync(file, backup);
|
|
311
|
+
return backup;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
124
317
|
/** Write (idempotently) a harness hook entry pointing at an explicit command string.
|
|
125
318
|
* Shared by the project installer (npx command) and the user installer (absolute path). */
|
|
126
319
|
export function writeHarnessConfig(file, harness, command) {
|
|
127
320
|
const config = readHarnessConfig(file);
|
|
321
|
+
backupHarnessConfig(file);
|
|
128
322
|
mkdirSync(dirname(file), { recursive: true });
|
|
129
323
|
const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});
|
|
130
324
|
if (harness === "cursor") {
|
|
@@ -183,10 +377,17 @@ export function cursorDetected() {
|
|
|
183
377
|
export function codexDetected() {
|
|
184
378
|
return existsSync(join(homedir(), ".codex"));
|
|
185
379
|
}
|
|
186
|
-
/** The absolute command a
|
|
187
|
-
*
|
|
380
|
+
/** The absolute command a harness entry runs: the current Node executable and the
|
|
381
|
+
* absolute path to this CLI, so it does not depend on PATH.
|
|
382
|
+
*
|
|
383
|
+
* Both paths are always quoted on Windows, even without a space in them. A harness
|
|
384
|
+
* runs this string through a shell, and that shell may be bash (Git Bash, WSL, a
|
|
385
|
+
* dev container), where an unquoted Windows path loses every backslash and the
|
|
386
|
+
* hook dies with MODULE_NOT_FOUND. Inside double quotes bash leaves a backslash
|
|
387
|
+
* alone unless it precedes $ ` " \ or a newline — none of which occur in a Windows
|
|
388
|
+
* path — and cmd.exe and PowerShell accept quoted paths too. */
|
|
188
389
|
export function absoluteHookCommand(cliPath, harness) {
|
|
189
|
-
const quote = (s) => (/\s/.test(s) ? `"${s}"` : s);
|
|
390
|
+
const quote = (s) => (process.platform === "win32" || /\s/.test(s) ? `"${s}"` : s);
|
|
190
391
|
return `${quote(process.execPath)} ${quote(cliPath)} ${harness}`;
|
|
191
392
|
}
|
|
192
393
|
export function isHarnessConfigured(file) {
|
|
@@ -202,6 +403,57 @@ export function isHarnessConfigured(file) {
|
|
|
202
403
|
return false;
|
|
203
404
|
}
|
|
204
405
|
}
|
|
406
|
+
/** Every Scopebond command string a harness config currently runs. `doctor` uses it to
|
|
407
|
+
* check that a pinned command still resolves: a hook entry that cannot start is worse
|
|
408
|
+
* than none, because a harness can read the failure as "no hook". */
|
|
409
|
+
export function configuredHookCommands(file) {
|
|
410
|
+
if (!existsSync(file))
|
|
411
|
+
return [];
|
|
412
|
+
const found = [];
|
|
413
|
+
const collect = (entry) => {
|
|
414
|
+
if (!isRecord(entry))
|
|
415
|
+
return;
|
|
416
|
+
if (isScopebond(entry.command))
|
|
417
|
+
found.push(String(entry.command));
|
|
418
|
+
if (Array.isArray(entry.hooks))
|
|
419
|
+
for (const h of entry.hooks)
|
|
420
|
+
collect(h);
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
const p = JSON.parse(readFileSync(file, "utf8"));
|
|
424
|
+
if (!isRecord(p) || !isRecord(p.hooks))
|
|
425
|
+
return [];
|
|
426
|
+
for (const list of Object.values(p.hooks))
|
|
427
|
+
if (Array.isArray(list))
|
|
428
|
+
for (const e of list)
|
|
429
|
+
collect(e);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
return found;
|
|
435
|
+
}
|
|
436
|
+
/** Whether a configured hook command can actually start. An `npx` form always can
|
|
437
|
+
* (npm resolves it); a pinned command names two absolute paths — the Node binary and
|
|
438
|
+
* the CLI — and either can go away: a Node version manager switching versions, or the
|
|
439
|
+
* Scopebond home being cleared. Both are exactly what this check exists to catch. */
|
|
440
|
+
export function hookCommandResolves(command) {
|
|
441
|
+
if (/(^|\s)npx(\.cmd)?\s/.test(command))
|
|
442
|
+
return true;
|
|
443
|
+
// `"<node>" "<cli.js>" claude`: check every token that names a file on disk.
|
|
444
|
+
const tokens = command.match(/"[^"]+"|\S+/g) ?? [];
|
|
445
|
+
let checked = 0;
|
|
446
|
+
for (const raw of tokens) {
|
|
447
|
+
const candidate = raw.replace(/^"|"$/g, "");
|
|
448
|
+
if (!/\.(m|c)?js$|\.exe$/i.test(candidate))
|
|
449
|
+
continue;
|
|
450
|
+
checked += 1;
|
|
451
|
+
if (!existsSync(candidate))
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
// A bare `node cli.js` form on POSIX has no .exe to check; the .js still counted.
|
|
455
|
+
return checked > 0 || !/[\\/]/.test(command.split(/\s+/)[0] ?? "");
|
|
456
|
+
}
|
|
205
457
|
/** Remove the user-level home (keys, policy, receipts). */
|
|
206
458
|
export function purgeHome() {
|
|
207
459
|
rmSync(userHome(), { recursive: true, force: true });
|
package/dist/install.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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","sourcesContent":["// SB112 — the user-level installer. One install per developer machine (not per repo):\n// keys and a starter policy live in a user-level home (~/.scopebond, override\n// SCOPEBOND_HOME), and the hook is registered by absolute path in the user-level agent\n// config (~/.claude/settings.json, ~/.cursor/hooks.json, ~/.codex/hooks.json). When a\n// user-level install exists, a project-local `.scopebond` is used only if the user\n// trusted that exact policy (`scopebond trust`, or `init` in the project): a cloned\n// repository, or an agent inside it, must not be able to swap in its own policy.\n\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\n\nexport type Harness = \"claude\" | \"cursor\" | \"codex\";\n\n/** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */\nexport function userHome(): string {\n return process.env.SCOPEBOND_HOME ?? join(homedir(), \".scopebond\");\n}\n\n/** The user-level agent config file for a harness. `HOME`/`USERPROFILE` (via homedir)\n * roots it; SCOPEBOND_HOME does not move the agent's own config. */\nexport function userHarnessFile(harness: Harness): string {\n if (harness === \"cursor\") return join(homedir(), \".cursor\", \"hooks.json\");\n if (harness === \"codex\") return join(homedir(), \".codex\", \"hooks.json\");\n return join(homedir(), \".claude\", \"settings.json\");\n}\n\n/** The file in the user home that pins trusted project policies: absolute project\n * config dir → SHA-256 of its policy.json. It lives under the protected home, so the\n * governed agent cannot write it. */\nexport function trustedProjectsFile(): string {\n return join(userHome(), \"trusted-projects.json\");\n}\n\nfunction policyDigest(dir: string): string | null {\n try { return createHash(\"sha256\").update(readFileSync(join(dir, \"policy.json\"))).digest(\"hex\"); } catch { return null; }\n}\n\nfunction readTrusted(): Record<string, string> {\n try { const p = JSON.parse(readFileSync(trustedProjectsFile(), \"utf8\")); return isRecord(p) ? (p as Record<string, string>) : {}; } catch { return {}; }\n}\n\nconst trustKey = (dir: string): string => resolve(dir).replace(/\\\\/g, \"/\").toLowerCase();\n\n/** Pin the project's current policy as trusted. Editing the policy afterwards un-trusts\n * it until the user trusts it again, so a rewrite by the agent never takes effect. */\nexport function trustProjectPolicy(dir: string): string {\n const digest = policyDigest(dir);\n if (!digest) throw new Error(`no policy at ${join(dir, \"policy.json\")}`);\n const trusted = readTrusted();\n trusted[trustKey(dir)] = digest;\n mkdirSync(userHome(), { recursive: true });\n writeFileSync(trustedProjectsFile(), JSON.stringify(trusted, null, 2) + \"\\n\");\n return digest;\n}\n\n/** Whether the project's policy is exactly the one the user trusted. */\nexport function isTrustedProject(dir: string): boolean {\n const digest = policyDigest(dir);\n return !!digest && readTrusted()[trustKey(dir)] === digest;\n}\n\n/** Resolve the config dir for a hook event, most specific first:\n * explicit override → the payload's project → $CLAUDE_PROJECT_DIR → the user home.\n * A project dir counts only when it has been scaffolded (a policy.json), so a bare\n * working directory never shadows the user-level install — and, once a user-level\n * install exists, only when its policy is trusted (`isTrustedProject`). Otherwise the\n * user's own policy governs: a repository cannot bring a weaker one with it. */\nexport function resolveConfigDir(payloadCwd: string | undefined): string {\n if (process.env.SCOPEBOND_HOOK_DIR) return process.env.SCOPEBOND_HOOK_DIR;\n const candidates: string[] = [];\n if (payloadCwd) candidates.push(join(payloadCwd, \".scopebond\"));\n if (process.env.CLAUDE_PROJECT_DIR) candidates.push(join(process.env.CLAUDE_PROJECT_DIR, \".scopebond\"));\n const home = userHome();\n const homeInstalled = existsSync(join(home, \"policy.json\"));\n for (const dir of candidates) {\n if (existsSync(join(dir, \"policy.json\")) && (!homeInstalled || isTrustedProject(dir))) return dir;\n }\n if (homeInstalled) return home;\n // Nothing scaffolded yet: prefer the payload project, else the user home.\n return candidates[0] ?? home;\n}\n\n/** A project policy present but ignored because it is not trusted (for status/doctor). */\nexport function untrustedProjectPolicy(payloadCwd: string | undefined): string | null {\n if (process.env.SCOPEBOND_HOOK_DIR || !existsSync(join(userHome(), \"policy.json\"))) return null;\n const dirs = [payloadCwd, process.env.CLAUDE_PROJECT_DIR].filter(Boolean).map((d) => join(d as string, \".scopebond\"));\n return dirs.find((d) => existsSync(join(d, \"policy.json\")) && !isTrustedProject(d)) ?? null;\n}\n\nconst isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\nconst isScopebond = (cmd: unknown): boolean =>\n typeof cmd === \"string\" && (\n /@scopebond\\/hook|scopebond-hook(\\s|$)/.test(cmd) ||\n // the user-level absolute form: `<node> <…/cli.js> claude|cursor|codex`\n /cli\\.js[\"']?\\s+(claude|cursor|codex)\\s*$/.test(cmd) ||\n /(^|[\"\\s])scopebond([\"'\\s]).*\\b(claude|cursor|codex)\\b/.test(cmd)\n );\nconst entryMatches = (e: unknown): boolean =>\n isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));\n\n/** Read an agent's JSON config for merging. A missing file is an empty config; a file\n * that exists but does not parse as a JSON object is an error — rewriting it would\n * silently delete the user's other settings, so the installer stops instead. */\nexport function readHarnessConfig(file: string): Record<string, unknown> {\n if (!existsSync(file)) return {};\n const text = readFileSync(file, \"utf8\");\n if (!text.trim()) return {};\n let parsed: unknown;\n try { parsed = JSON.parse(text); } catch (error) {\n throw new Error(`${file} is not valid JSON (${(error as Error).message}); fix or move it, then run this again — it was left unchanged`);\n }\n if (!isRecord(parsed)) throw new Error(`${file} is not a JSON object; fix or move it, then run this again — it was left unchanged`);\n return parsed;\n}\n\n/** Write (idempotently) a harness hook entry pointing at an explicit command string.\n * Shared by the project installer (npx command) and the user installer (absolute path). */\nexport function writeHarnessConfig(file: string, harness: Harness, command: string): string {\n const config = readHarnessConfig(file);\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n if (harness === \"cursor\") {\n config.version = config.version ?? 1;\n for (const event of [\"beforeShellExecution\", \"beforeMCPExecution\", \"beforeReadFile\", \"afterFileEdit\"]) {\n const list = Array.isArray((hooks as Record<string, unknown>)[event]) ? (hooks as Record<string, unknown[]>)[event] : ((hooks as Record<string, unknown[]>)[event] = []);\n const at = list.findIndex(entryMatches);\n const entry = { command: command.replace(/\\bclaude$/, \"cursor\") };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n } else {\n const list = Array.isArray((hooks as Record<string, unknown>).PreToolUse) ? (hooks as Record<string, unknown[]>).PreToolUse : ((hooks as Record<string, unknown[]>).PreToolUse = []);\n const at = list.findIndex(entryMatches);\n // Codex matchers are regular expressions; omitting the matcher means every tool.\n // A literal \"*\" is not a valid regular expression and causes Codex to skip the group.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command }] };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** Remove any Scopebond hook entry from a user-level harness config, leaving the rest\n * intact. Returns true if the file existed. */\nexport function removeHarnessConfig(file: string): boolean {\n if (!existsSync(file)) return false;\n let config: Record<string, unknown>;\n try { config = readHarnessConfig(file); } catch { return true; } // unreadable: leave it untouched\n const hooks = isRecord(config.hooks) ? config.hooks : {};\n for (const [event, value] of Object.entries(hooks)) {\n if (Array.isArray(value)) (hooks as Record<string, unknown[]>)[event] = value.filter((e) => !entryMatches(e));\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return true;\n}\n\n/** Detect a Cursor install by its user config directory. */\nexport function cursorDetected(): boolean {\n return existsSync(join(homedir(), \".cursor\"));\n}\n\n/** Detect a Codex install by its user config directory. */\nexport function codexDetected(): boolean {\n return existsSync(join(homedir(), \".codex\"));\n}\n\n/** The absolute command a user-level harness entry runs: the current Node executable\n * and the absolute path to this CLI, so it does not depend on PATH. */\nexport function absoluteHookCommand(cliPath: string, harness: Harness): string {\n const quote = (s: string) => (/\\s/.test(s) ? `\"${s}\"` : s);\n return `${quote(process.execPath)} ${quote(cliPath)} ${harness}`;\n}\n\nexport function isHarnessConfigured(file: string): boolean {\n if (!existsSync(file)) return false;\n try {\n const p = JSON.parse(readFileSync(file, \"utf8\"));\n if (!isRecord(p) || !isRecord(p.hooks)) return false;\n return Object.values(p.hooks).some((v) => Array.isArray(v) && v.some(entryMatches));\n } catch { return false; }\n}\n\n/** Remove the user-level home (keys, policy, receipts). */\nexport function purgeHome(): void {\n rmSync(userHome(), { recursive: true, force: true });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,8EAA8E;AAC9E,uFAAuF;AACvF,sFAAsF;AACtF,mFAAmF;AACnF,oFAAoF;AACpF,iFAAiF;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnG,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzE,gFAAgF;AAChF,MAAM,UAAU,QAAQ;IACtB,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AACrE,CAAC;AAED;qEACqE;AACrE,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;IAC1E,IAAI,OAAO,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;IACxE,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;AACrD,CAAC;AAED;gEACgE;AAChE,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,MAAc,OAAO,CAAC,GAAG,EAAE;IAC9E,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;IACpE,IAAI,OAAO,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;IAClE,OAAO,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;AAC/C,CAAC;AAED;;;mDAGmD;AACnD,MAAM,UAAU,gBAAgB,CAAC,OAAgB,EAAE,MAAc,OAAO,CAAC,GAAG,EAAE;IAC5E,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACnF,CAAC;AASD,MAAM,UAAU,aAAa,CAAC,OAAgB,EAAE,MAAc,OAAO,CAAC,GAAG,EAAE;IACzE,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACtC,OAAO;QACL,OAAO,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;QACtD,KAAK,EAAE,KAAK,IAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;QACzD,IAAI,EAAE,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;KAC9C,CAAC;AACJ,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,iBAAiB,CAAC,MAAqB;IACrD,MAAM,KAAK,GAAG;QACZ,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;QAC/B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE;QAClC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;KAC1B,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAClB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,2BAA2B,CAAC;IAC7E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK;QAAE,OAAO,8CAA8C,CAAC;IAC9F,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI;QAAE,OAAO,yBAAyB,CAAC;IACxE,OAAO,eAAe,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;AAC7C,CAAC;AAED;iFACiF;AACjF,MAAM,UAAU,wBAAwB,CAAC,OAAe;IACtD,IAAI,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC9E,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC;AAUD,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,GAAG,GAAG,CAAC,IAAc,EAAU,EAAE;QACrC,IAAI,CAAC;YACH,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACpC,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACnE,OAAO,CAAC,CAAC;QACX,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,GAAI,KAAoC,CAAC,MAAM,CAAC;YAC5D,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClD,CAAC;IACH,CAAC,CAAC;IACF,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IACrE,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC7E,OAAO,GAAG,CAAC,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;AACjF,CAAC;AAED;;0BAE0B;AAC1B,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,kFAAkF;QAClF,mFAAmF;QACnF,kFAAkF;QAClF,kEAAkE;QAClE,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,eAAe,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACpH,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,YAAY,EAAE,cAAc,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAChJ,MAAM,KAAK,GAAG,GAAG,GAAG,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7C,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACjD,aAAa,CAAC,OAAO,EAAE,GAAG,QAAQ,GAAG,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,CAAC;QACrG,CAAC;QACD,OAAO,aAAa,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAC;IAAC,CAAC;AAC3B,CAAC;AAED;4DAC4D;AAC5D,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,MAAoC;IACpF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QAAC,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,CAAC,CAAC;IAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3D,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC;IACrB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,MAAM,UAAU,GAAG,CAAC,CAAU,EAAY,EAAE;QAC1C,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,OAAO,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,OAAO,CAAC,GAAG,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC;IAC7B,CAAC,CAAC;IACF,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACnD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,SAAS;QACpC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACnF,OAAO,IAAI,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QACrC,KAAmC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACrD,CAAC;IACD,IAAI,OAAO,GAAG,CAAC;QAAE,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC7E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;sCAEsC;AACtC,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,QAAQ,EAAE,EAAE,uBAAuB,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,CAAC;QAAC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;AAC1H,CAAC;AAED,SAAS,WAAW;IAClB,IAAI,CAAC;QAAC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,mBAAmB,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAA4B,CAAC,CAAC,CAAC,EAAE,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;AAC1J,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;AAEzF;uFACuF;AACvF,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC;IACzE,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;IAChC,SAAS,CAAC,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,aAAa,CAAC,mBAAmB,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9E,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,CAAC,CAAC,MAAM,IAAI,WAAW,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,MAAM,CAAC;AAC7D,CAAC;AAED;;;;;iFAKiF;AACjF,MAAM,UAAU,gBAAgB,CAAC,UAA8B;IAC7D,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC1E,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,IAAI,UAAU;QAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,CAAC;IAChE,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB;QAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAC,CAAC;IACxG,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;IACxB,MAAM,aAAa,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC;IAC5D,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;IACpG,CAAC;IACD,IAAI,aAAa;QAAE,OAAO,IAAI,CAAC;IAC/B,0EAA0E;IAC1E,OAAO,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC/B,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,sBAAsB,CAAC,UAA8B;IACnE,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAChG,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IACtH,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC9F,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAgC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAExH;;;;;;;;;;;;;;;6EAe6E;AAC7E,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AACnE,iFAAiF;AACjF,MAAM,WAAW,GAAG,IAAI,CAAC;AACzB,MAAM,OAAO,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;AAE/E,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,GAAY,EAAW,EAAE;IAC9D,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,WAAW;QAAE,OAAO,KAAK,CAAC;IAC1F,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAChC,sFAAsF;IACtF,IAAI,KAAK,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,kBAAkB,CAAC;QAAE,OAAO,IAAI,CAAC;IACzF,8CAA8C;IAC9C,IAAI,oCAAoC,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAChE,kFAAkF;IAClF,kDAAkD;IAClD,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACtD,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACjD,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;QAC3B,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACvC,OAAO,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AACF,MAAM,WAAW,GAAG,sBAAsB,CAAC;AAE3C;kDACkD;AAClD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAU,EAAW,EAAE,CACzD,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AACpI,MAAM,YAAY,GAAG,mBAAmB,CAAC;AAEzC;;iFAEiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC;IAC5B,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,uBAAwB,KAAe,CAAC,OAAO,gEAAgE,CAAC,CAAC;IAC1I,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,oFAAoF,CAAC,CAAC;IACpI,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;4EAO4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,MAAM,GAAG,GAAG,IAAI,mBAAmB,CAAC;IAC1C,IAAI,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,CAAC;QAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAAC,OAAO,MAAM,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;AAC3E,CAAC;AAED;4FAC4F;AAC5F,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,OAAgB,EAAE,OAAe;IAChF,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACvC,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC1B,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IAC1E,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;QACrC,KAAK,MAAM,KAAK,IAAI,CAAC,sBAAsB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,eAAe,CAAC,EAAE,CAAC;YACtG,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAE,KAAiC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAE,KAAmC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAE,KAAmC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;YACzK,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;YACxC,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,EAAE,CAAC;YAClE,IAAI,EAAE,IAAI,CAAC;gBAAE,IAAI,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC;;gBAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAE,KAAiC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAE,KAAmC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAE,KAAmC,CAAC,UAAU,GAAG,EAAE,CAAC,CAAC;QACrL,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QACxC,iFAAiF;QACjF,sFAAsF;QACtF,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO;YAC/B,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,aAAa,EAAE,qCAAqC,EAAE,CAAC,EAAE;YAC9G,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QAC5D,IAAI,EAAE,IAAI,CAAC;YAAE,IAAI,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC;;YAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvD,CAAC;IACD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC;AACd,CAAC;AAED;gDACgD;AAChD,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QAAC,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC,CAAC,iCAAiC;IAClG,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IACzD,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACnD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAG,KAAmC,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAChH,CAAC;IACD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC;AACd,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,cAAc;IAC5B,OAAO,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED;;;;;;;;iEAQiE;AACjE,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,OAAgB;IACnE,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3F,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,OAAO,EAAE,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACjD,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACrD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IACtF,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAC;IAAC,CAAC;AAC3B,CAAC;AAED;;sEAEsE;AACtE,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,CAAC,KAAc,EAAQ,EAAE;QACvC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAO;QAC7B,IAAI,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAClE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK;gBAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1E,CAAC,CAAC;IACF,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACjD,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAClD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;gBAAE,KAAK,MAAM,CAAC,IAAI,IAAI;oBAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACvG,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;IACtB,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;sFAGsF;AACtF,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,IAAI,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,6EAA6E;IAC7E,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;IACnD,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC5C,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,SAAS,CAAC;YAAE,SAAS;QACrD,OAAO,IAAI,CAAC,CAAC;QACb,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,KAAK,CAAC;IAC3C,CAAC;IACD,kFAAkF;IAClF,OAAO,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AACrE,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,SAAS;IACvB,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACvD,CAAC","sourcesContent":["// SB112 — the user-level installer. One install per developer machine (not per repo):\n// keys and a starter policy live in a user-level home (~/.scopebond, override\n// SCOPEBOND_HOME), and the hook is registered by absolute path in the user-level agent\n// config (~/.claude/settings.json, ~/.cursor/hooks.json, ~/.codex/hooks.json). When a\n// user-level install exists, a project-local `.scopebond` is used only if the user\n// trusted that exact policy (`scopebond trust`, or `init` in the project): a cloned\n// repository, or an agent inside it, must not be able to swap in its own policy.\n\nimport { execFileSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, copyFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, resolve } from \"node:path\";\n\nexport type Harness = \"claude\" | \"cursor\" | \"codex\";\n\n/** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */\nexport function userHome(): string {\n return process.env.SCOPEBOND_HOME ?? join(homedir(), \".scopebond\");\n}\n\n/** The user-level agent config file for a harness. `HOME`/`USERPROFILE` (via homedir)\n * roots it; SCOPEBOND_HOME does not move the agent's own config. */\nexport function userHarnessFile(harness: Harness): string {\n if (harness === \"cursor\") return join(homedir(), \".cursor\", \"hooks.json\");\n if (harness === \"codex\") return join(homedir(), \".codex\", \"hooks.json\");\n return join(homedir(), \".claude\", \"settings.json\");\n}\n\n/** The project-level agent config file for a harness — what `init` writes, so the\n * hook travels with the repository rather than the machine. */\nexport function projectHarnessFile(harness: Harness, cwd: string = process.cwd()): string {\n if (harness === \"cursor\") return join(cwd, \".cursor\", \"hooks.json\");\n if (harness === \"codex\") return join(cwd, \".codex\", \"hooks.json\");\n return join(cwd, \".claude\", \"settings.json\");\n}\n\n/** The personal, per-machine project config for a harness, or null when the harness\n * has none. Claude Code reads `.claude/settings.local.json` beside `settings.json` and\n * merges the hooks of both; it is the place for anything that names a path on this\n * machine. Cursor and Codex have no such file. */\nexport function localHarnessFile(harness: Harness, cwd: string = process.cwd()): string | null {\n return harness === \"claude\" ? join(cwd, \".claude\", \"settings.local.json\") : null;\n}\n\n/** Which config files actually carry a Scopebond hook for this harness. There are three\n * places it can live — the shared project config, the personal project config (Claude\n * Code's `settings.local.json`, where `init` pins the fast command) and the user config\n * `install` writes — and `status`/`doctor` must look at all of them: checking only the\n * user one told everyone who ran `init` that their install had failed. */\nexport interface HarnessScopes { project: string | null; local: string | null; user: string | null }\n\nexport function harnessScopes(harness: Harness, cwd: string = process.cwd()): HarnessScopes {\n const project = projectHarnessFile(harness, cwd);\n const local = localHarnessFile(harness, cwd);\n const user = userHarnessFile(harness);\n return {\n project: isHarnessConfigured(project) ? project : null,\n local: local && isHarnessConfigured(local) ? local : null,\n user: isHarnessConfigured(user) ? user : null,\n };\n}\n\n/** One word for where a harness is wired, for a status line. */\nexport function harnessScopeLabel(scopes: HarnessScopes): string {\n const where = [\n scopes.project ? \"project\" : \"\",\n scopes.local ? \"this machine\" : \"\",\n scopes.user ? \"user\" : \"\",\n ].filter(Boolean);\n if (where.length === 0) return \"\";\n if (where.length === 1 && scopes.project) return \"configured (this project)\";\n if (where.length === 1 && scopes.local) return \"configured (this project, this machine only)\";\n if (where.length === 1 && scopes.user) return \"configured (user-level)\";\n return `configured (${where.join(\" + \")})`;\n}\n\n/** Whether a hook command names a path that exists only on this machine — the pinned\n * `\"<node>\" \"<…/cli.js>\" claude` form — rather than the portable `npx` form. */\nexport function isMachineSpecificCommand(command: string): boolean {\n if (/(^|\\s)npx(\\.cmd)?\\s/.test(command)) return false;\n const first = (command.match(/\"[^\"]+\"|\\S+/) ?? [\"\"])[0].replace(/^\"|\"$/g, \"\");\n return isAbsolute(first) || /^[A-Za-z]:[\\\\/]/.test(first);\n}\n\n/** How git treats a config file: committed (`tracked`), kept out (`ignored`), neither\n * yet (`untracked`, so a `git add .` would commit it), or not in a repository / no git\n * on this machine (`none`). A file git shares must never carry a machine-specific\n * command: on a teammate's machine the path does not exist, the command cannot start,\n * and Claude Code, Cursor and Codex all treat a hook that cannot start as a non-blocking\n * error — the agent runs with no check at all. */\nexport type GitShare = \"tracked\" | \"ignored\" | \"untracked\" | \"none\";\n\nexport function gitShareState(file: string): GitShare {\n const cwd = dirname(file);\n const git = (args: string[]): number => {\n try {\n mkdirSync(cwd, { recursive: true });\n execFileSync(\"git\", args, { cwd, stdio: \"ignore\", timeout: 5000 });\n return 0;\n } catch (error) {\n const status = (error as { status?: number | null }).status;\n return typeof status === \"number\" ? status : -1;\n }\n };\n if (git([\"rev-parse\", \"--is-inside-work-tree\"]) !== 0) return \"none\";\n if (git([\"ls-files\", \"--error-unmatch\", \"--\", file]) === 0) return \"tracked\";\n return git([\"check-ignore\", \"-q\", \"--\", file]) === 0 ? \"ignored\" : \"untracked\";\n}\n\n/** Keep a file out of git for this clone only, through `.git/info/exclude` — never the\n * repository's own `.gitignore`, which is the team's file. Returns true when the file is\n * ignored afterwards. */\nexport function excludeFromGit(file: string): boolean {\n try {\n const cwd = dirname(file);\n mkdirSync(cwd, { recursive: true });\n // The entry is built from git's own view of where this directory sits in the work\n // tree (`--show-prefix`), not from comparing file-system paths: the same directory\n // can be spelled two ways (a Windows 8.3 short name, a symlinked temp dir), and a\n // path computed across the two spellings excludes the wrong file.\n const prefix = execFileSync(\"git\", [\"rev-parse\", \"--show-prefix\"], { cwd, encoding: \"utf8\", timeout: 5000 }).trim();\n const exclude = resolve(cwd, execFileSync(\"git\", [\"rev-parse\", \"--git-path\", \"info/exclude\"], { cwd, encoding: \"utf8\", timeout: 5000 }).trim());\n const entry = \"/\" + prefix + basename(file);\n const existing = existsSync(exclude) ? readFileSync(exclude, \"utf8\") : \"\";\n if (!existing.split(/\\r?\\n/).includes(entry)) {\n mkdirSync(dirname(exclude), { recursive: true });\n writeFileSync(exclude, `${existing}${existing && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\"}${entry}\\n`);\n }\n return gitShareState(file) === \"ignored\";\n } catch { return false; }\n}\n\n/** Remove the Scopebond entries whose command `remove` selects, leaving every other\n * setting alone. Returns the number of entries removed. */\nexport function pruneHarnessEntries(file: string, remove: (command: string) => boolean): number {\n if (!existsSync(file)) return 0;\n let config: Record<string, unknown>;\n try { config = readHarnessConfig(file); } catch { return 0; }\n const hooks = isRecord(config.hooks) ? config.hooks : null;\n if (!hooks) return 0;\n let removed = 0;\n const commandsOf = (e: unknown): string[] => {\n if (!isRecord(e)) return [];\n const own = isScopebond(e.command) ? [String(e.command)] : [];\n const nested = Array.isArray(e.hooks) ? e.hooks.flatMap(commandsOf) : [];\n return [...own, ...nested];\n };\n for (const [event, value] of Object.entries(hooks)) {\n if (!Array.isArray(value)) continue;\n const kept = value.filter((e) => !(entryMatches(e) && commandsOf(e).some(remove)));\n removed += value.length - kept.length;\n (hooks as Record<string, unknown[]>)[event] = kept;\n }\n if (removed > 0) writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return removed;\n}\n\n/** The file in the user home that pins trusted project policies: absolute project\n * config dir → SHA-256 of its policy.json. It lives under the protected home, so the\n * governed agent cannot write it. */\nexport function trustedProjectsFile(): string {\n return join(userHome(), \"trusted-projects.json\");\n}\n\nfunction policyDigest(dir: string): string | null {\n try { return createHash(\"sha256\").update(readFileSync(join(dir, \"policy.json\"))).digest(\"hex\"); } catch { return null; }\n}\n\nfunction readTrusted(): Record<string, string> {\n try { const p = JSON.parse(readFileSync(trustedProjectsFile(), \"utf8\")); return isRecord(p) ? (p as Record<string, string>) : {}; } catch { return {}; }\n}\n\nconst trustKey = (dir: string): string => resolve(dir).replace(/\\\\/g, \"/\").toLowerCase();\n\n/** Pin the project's current policy as trusted. Editing the policy afterwards un-trusts\n * it until the user trusts it again, so a rewrite by the agent never takes effect. */\nexport function trustProjectPolicy(dir: string): string {\n const digest = policyDigest(dir);\n if (!digest) throw new Error(`no policy at ${join(dir, \"policy.json\")}`);\n const trusted = readTrusted();\n trusted[trustKey(dir)] = digest;\n mkdirSync(userHome(), { recursive: true });\n writeFileSync(trustedProjectsFile(), JSON.stringify(trusted, null, 2) + \"\\n\");\n return digest;\n}\n\n/** Whether the project's policy is exactly the one the user trusted. */\nexport function isTrustedProject(dir: string): boolean {\n const digest = policyDigest(dir);\n return !!digest && readTrusted()[trustKey(dir)] === digest;\n}\n\n/** Resolve the config dir for a hook event, most specific first:\n * explicit override → the payload's project → $CLAUDE_PROJECT_DIR → the user home.\n * A project dir counts only when it has been scaffolded (a policy.json), so a bare\n * working directory never shadows the user-level install — and, once a user-level\n * install exists, only when its policy is trusted (`isTrustedProject`). Otherwise the\n * user's own policy governs: a repository cannot bring a weaker one with it. */\nexport function resolveConfigDir(payloadCwd: string | undefined): string {\n if (process.env.SCOPEBOND_HOOK_DIR) return process.env.SCOPEBOND_HOOK_DIR;\n const candidates: string[] = [];\n if (payloadCwd) candidates.push(join(payloadCwd, \".scopebond\"));\n if (process.env.CLAUDE_PROJECT_DIR) candidates.push(join(process.env.CLAUDE_PROJECT_DIR, \".scopebond\"));\n const home = userHome();\n const homeInstalled = existsSync(join(home, \"policy.json\"));\n for (const dir of candidates) {\n if (existsSync(join(dir, \"policy.json\")) && (!homeInstalled || isTrustedProject(dir))) return dir;\n }\n if (homeInstalled) return home;\n // Nothing scaffolded yet: prefer the payload project, else the user home.\n return candidates[0] ?? home;\n}\n\n/** A project policy present but ignored because it is not trusted (for status/doctor). */\nexport function untrustedProjectPolicy(payloadCwd: string | undefined): string | null {\n if (process.env.SCOPEBOND_HOOK_DIR || !existsSync(join(userHome(), \"policy.json\"))) return null;\n const dirs = [payloadCwd, process.env.CLAUDE_PROJECT_DIR].filter(Boolean).map((d) => join(d as string, \".scopebond\"));\n return dirs.find((d) => existsSync(join(d, \"policy.json\")) && !isTrustedProject(d)) ?? null;\n}\n\nconst isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** Whether a harness command string is one of ours. This is the single matcher for\n * every form the installer has ever written, and it is what makes re-running `init`\n * or `install` replace the entry instead of appending a second one:\n * - `npx -y @scopebond/hook@0.6.0 claude` (portable)\n * - `\"<node>\" \"<…/@scopebond/hook/dist/cli.js>\" claude` (pinned, POSIX)\n * - `\"<node>\" \"<…\\@scopebond\\hook\\dist\\cli.js>\" claude` (pinned, Windows)\n * - `scopebond-hook claude` (legacy global binary)\n * The separator class matters: a Windows pinned path spells the scope\n * `@scopebond\\hook`, and matching only `@scopebond/hook` let a second `init`\n * install a duplicate hook — which would double-check every tool call.\n *\n * Written with substring and token tests rather than one regular expression: the\n * previous `(^|[\"\\s])scopebond([\"'\\s]).*\\b(claude|cursor|codex)\\b` backtracked\n * polynomially on a long run of `\\tscopebond\\t` (CodeQL js/polynomial-redos). The\n * input is a command out of a config file rather than a request, so this was not\n * reachable by an attacker — but a linear check is also the clearer one. */\nconst HARNESS_SUBCOMMANDS = new Set([\"claude\", \"cursor\", \"codex\"]);\n/** Long enough for any real command; a bound keeps the work linear and small. */\nconst MAX_COMMAND = 4096;\nconst unquote = (token: string): string => token.replace(/^[\"']+|[\"']+$/g, \"\");\n\nexport const isScopebondHookCommand = (cmd: unknown): boolean => {\n if (typeof cmd !== \"string\" || cmd.length === 0 || cmd.length > MAX_COMMAND) return false;\n const lower = cmd.toLowerCase();\n // The package itself, in either path spelling (`@scopebond/hook`, `@scopebond\\hook`).\n if (lower.includes(\"@scopebond/hook\") || lower.includes(\"@scopebond\\\\hook\")) return true;\n // The legacy global binary, as a whole token.\n if (/(^|[\\s\"'])scopebond-hook([\\s\"']|$)/.test(cmd)) return true;\n // An absolute or bare form whose last argument is one of our harness subcommands:\n // `<node> <…/cli.js> claude`, `scopebond cursor`.\n const tokens = cmd.split(/\\s+/).filter(Boolean);\n const last = unquote(tokens[tokens.length - 1] ?? \"\");\n if (!HARNESS_SUBCOMMANDS.has(last)) return false;\n return tokens.some((token) => {\n const t = unquote(token).toLowerCase();\n return t.includes(\"scopebond\") || /cli\\.(m|c)?js$/.test(t);\n });\n};\nconst isScopebond = isScopebondHookCommand;\n\n/** Whether a harness hook entry (either shape: a bare command, or a group with a\n * nested `hooks` array) is a Scopebond entry. */\nexport const harnessEntryMatches = (e: unknown): boolean =>\n isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));\nconst entryMatches = harnessEntryMatches;\n\n/** Read an agent's JSON config for merging. A missing file is an empty config; a file\n * that exists but does not parse as a JSON object is an error — rewriting it would\n * silently delete the user's other settings, so the installer stops instead. */\nexport function readHarnessConfig(file: string): Record<string, unknown> {\n if (!existsSync(file)) return {};\n const text = readFileSync(file, \"utf8\");\n if (!text.trim()) return {};\n let parsed: unknown;\n try { parsed = JSON.parse(text); } catch (error) {\n throw new Error(`${file} is not valid JSON (${(error as Error).message}); fix or move it, then run this again — it was left unchanged`);\n }\n if (!isRecord(parsed)) throw new Error(`${file} is not a JSON object; fix or move it, then run this again — it was left unchanged`);\n return parsed;\n}\n\n/** Copy an agent config aside before changing it, once. Re-running an installer must not\n * overwrite the backup with the already-modified file, or the original is lost on the\n * second run — so an existing backup is left alone. Returns the backup path when one was\n * written.\n *\n * This exists because these installers rewrite files the user did not create and may not\n * know are involved (`~/.claude/settings.json` holds their theme, plugins and\n * permissions), and the undo was previously \"hope the merge was right\". */\nexport function backupHarnessConfig(file: string): string | null {\n if (!existsSync(file)) return null;\n const backup = `${file}.scopebond-backup`;\n if (existsSync(backup)) return null;\n try { copyFileSync(file, backup); return backup; } catch { return null; }\n}\n\n/** Write (idempotently) a harness hook entry pointing at an explicit command string.\n * Shared by the project installer (npx command) and the user installer (absolute path). */\nexport function writeHarnessConfig(file: string, harness: Harness, command: string): string {\n const config = readHarnessConfig(file);\n backupHarnessConfig(file);\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n if (harness === \"cursor\") {\n config.version = config.version ?? 1;\n for (const event of [\"beforeShellExecution\", \"beforeMCPExecution\", \"beforeReadFile\", \"afterFileEdit\"]) {\n const list = Array.isArray((hooks as Record<string, unknown>)[event]) ? (hooks as Record<string, unknown[]>)[event] : ((hooks as Record<string, unknown[]>)[event] = []);\n const at = list.findIndex(entryMatches);\n const entry = { command: command.replace(/\\bclaude$/, \"cursor\") };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n } else {\n const list = Array.isArray((hooks as Record<string, unknown>).PreToolUse) ? (hooks as Record<string, unknown[]>).PreToolUse : ((hooks as Record<string, unknown[]>).PreToolUse = []);\n const at = list.findIndex(entryMatches);\n // Codex matchers are regular expressions; omitting the matcher means every tool.\n // A literal \"*\" is not a valid regular expression and causes Codex to skip the group.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command }] };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** Remove any Scopebond hook entry from a user-level harness config, leaving the rest\n * intact. Returns true if the file existed. */\nexport function removeHarnessConfig(file: string): boolean {\n if (!existsSync(file)) return false;\n let config: Record<string, unknown>;\n try { config = readHarnessConfig(file); } catch { return true; } // unreadable: leave it untouched\n const hooks = isRecord(config.hooks) ? config.hooks : {};\n for (const [event, value] of Object.entries(hooks)) {\n if (Array.isArray(value)) (hooks as Record<string, unknown[]>)[event] = value.filter((e) => !entryMatches(e));\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return true;\n}\n\n/** Detect a Cursor install by its user config directory. */\nexport function cursorDetected(): boolean {\n return existsSync(join(homedir(), \".cursor\"));\n}\n\n/** Detect a Codex install by its user config directory. */\nexport function codexDetected(): boolean {\n return existsSync(join(homedir(), \".codex\"));\n}\n\n/** The absolute command a harness entry runs: the current Node executable and the\n * absolute path to this CLI, so it does not depend on PATH.\n *\n * Both paths are always quoted on Windows, even without a space in them. A harness\n * runs this string through a shell, and that shell may be bash (Git Bash, WSL, a\n * dev container), where an unquoted Windows path loses every backslash and the\n * hook dies with MODULE_NOT_FOUND. Inside double quotes bash leaves a backslash\n * alone unless it precedes $ ` \" \\ or a newline — none of which occur in a Windows\n * path — and cmd.exe and PowerShell accept quoted paths too. */\nexport function absoluteHookCommand(cliPath: string, harness: Harness): string {\n const quote = (s: string) => (process.platform === \"win32\" || /\\s/.test(s) ? `\"${s}\"` : s);\n return `${quote(process.execPath)} ${quote(cliPath)} ${harness}`;\n}\n\nexport function isHarnessConfigured(file: string): boolean {\n if (!existsSync(file)) return false;\n try {\n const p = JSON.parse(readFileSync(file, \"utf8\"));\n if (!isRecord(p) || !isRecord(p.hooks)) return false;\n return Object.values(p.hooks).some((v) => Array.isArray(v) && v.some(entryMatches));\n } catch { return false; }\n}\n\n/** Every Scopebond command string a harness config currently runs. `doctor` uses it to\n * check that a pinned command still resolves: a hook entry that cannot start is worse\n * than none, because a harness can read the failure as \"no hook\". */\nexport function configuredHookCommands(file: string): string[] {\n if (!existsSync(file)) return [];\n const found: string[] = [];\n const collect = (entry: unknown): void => {\n if (!isRecord(entry)) return;\n if (isScopebond(entry.command)) found.push(String(entry.command));\n if (Array.isArray(entry.hooks)) for (const h of entry.hooks) collect(h);\n };\n try {\n const p = JSON.parse(readFileSync(file, \"utf8\"));\n if (!isRecord(p) || !isRecord(p.hooks)) return [];\n for (const list of Object.values(p.hooks)) if (Array.isArray(list)) for (const e of list) collect(e);\n } catch { return []; }\n return found;\n}\n\n/** Whether a configured hook command can actually start. An `npx` form always can\n * (npm resolves it); a pinned command names two absolute paths — the Node binary and\n * the CLI — and either can go away: a Node version manager switching versions, or the\n * Scopebond home being cleared. Both are exactly what this check exists to catch. */\nexport function hookCommandResolves(command: string): boolean {\n if (/(^|\\s)npx(\\.cmd)?\\s/.test(command)) return true;\n // `\"<node>\" \"<cli.js>\" claude`: check every token that names a file on disk.\n const tokens = command.match(/\"[^\"]+\"|\\S+/g) ?? [];\n let checked = 0;\n for (const raw of tokens) {\n const candidate = raw.replace(/^\"|\"$/g, \"\");\n if (!/\\.(m|c)?js$|\\.exe$/i.test(candidate)) continue;\n checked += 1;\n if (!existsSync(candidate)) return false;\n }\n // A bare `node cli.js` form on POSIX has no .exe to check; the .js still counted.\n return checked > 0 || !/[\\\\/]/.test(command.split(/\\s+/)[0] ?? \"\");\n}\n\n/** Remove the user-level home (keys, policy, receipts). */\nexport function purgeHome(): void {\n rmSync(userHome(), { recursive: true, force: true });\n}\n"]}
|
package/dist/map.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface Mapped {
|
|
|
13
13
|
evaluated: boolean;
|
|
14
14
|
/** The native tool/event name, for diagnostics. */
|
|
15
15
|
source: string;
|
|
16
|
+
/** True when the harness only tells us about this action *after* it happened, so a
|
|
17
|
+
* policy decision on it records and flags the action but did not prevent it. Cursor's
|
|
18
|
+
* `afterFileEdit` is the case that exists today: it has no before-edit counterpart,
|
|
19
|
+
* so a file write there is observed, not stopped. Callers must not describe such a
|
|
20
|
+
* decision as prevention. */
|
|
21
|
+
postHoc?: boolean;
|
|
16
22
|
}
|
|
17
23
|
/** A bare `git push` (no ref) pushes the current branch. The mapper cannot know it,
|
|
18
24
|
* so the runtime resolves it and fills it in here, before evaluation — otherwise the
|
package/dist/map.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../src/map.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AACD,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;;kEAK8D;IAC9D,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../src/map.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AACD,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;;kEAK8D;IAC9D,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf;;;;kCAI8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAkpBD;;;uCAGuC;AACvC,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAM5F;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAyBzE;AAED;;kFAEkF;AAClF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAaxE;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAqBxF"}
|
package/dist/map.js
CHANGED
|
@@ -903,7 +903,10 @@ export function mapCursorEvent(event, payload) {
|
|
|
903
903
|
case "beforeReadFile":
|
|
904
904
|
return pathIntents("file.read", rel(p.path ?? p.file_path, cwd), event);
|
|
905
905
|
case "afterFileEdit":
|
|
906
|
-
|
|
906
|
+
// Cursor reports an edit only once it has been made; there is no before-edit
|
|
907
|
+
// hook to block at. The rule still decides and the decision is still recorded —
|
|
908
|
+
// it just cannot be called prevention.
|
|
909
|
+
return pathIntents("file.write", rel(p.path ?? p.file_path, cwd), event).map((m) => ({ ...m, postHoc: true }));
|
|
907
910
|
case "beforeMCPExecution": {
|
|
908
911
|
const server = String(p.server ?? p.server_name ?? "");
|
|
909
912
|
const tool = String(p.tool ?? p.tool_name ?? "");
|