@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/explain.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The sentence a person — and the coding agent they are supervising — sees when
|
|
2
|
+
// Scopebond blocks an action.
|
|
3
|
+
//
|
|
4
|
+
// The engine's own reason is precise and unreadable ("param ref fails pattern"):
|
|
5
|
+
// it names the mechanism, not the rule. The policy clause that decided already
|
|
6
|
+
// carries a written `description`, and the verdict names that clause, so both are
|
|
7
|
+
// available here. The composed message says what was blocked, which rule blocked
|
|
8
|
+
// it, why that rule exists, and where to change it — and it keeps the engine's
|
|
9
|
+
// detail so a surprising decision stays debuggable.
|
|
10
|
+
//
|
|
11
|
+
// This string is also fed back to the agent (Claude Code's
|
|
12
|
+
// `permissionDecisionReason`, Cursor's `agentMessage`), so a clear one lets the
|
|
13
|
+
// agent pick a different approach instead of retrying the same blocked call.
|
|
14
|
+
/** How long a clause description may run before it is cut. Long enough for the
|
|
15
|
+
* starter policy's first clause, short enough not to bury a terminal. */
|
|
16
|
+
const MAX_WHY = 300;
|
|
17
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
18
|
+
/** One line naming the action: `git.push origin main`, `shell.exec rm`,
|
|
19
|
+
* `file.read .env`. Shared with `log` so a decision reads the same in both. */
|
|
20
|
+
export function describeAction(intent) {
|
|
21
|
+
const type = str(intent?.action_type);
|
|
22
|
+
const p = (intent?.params ?? {});
|
|
23
|
+
const bits = type === "shell.exec" ? str(p.program)
|
|
24
|
+
: type === "git.push" ? `${str(p.remote)} ${str(p.ref)}`.trim()
|
|
25
|
+
: type === "file.write" || type === "file.read" ? str(p.path)
|
|
26
|
+
: type === "mcp.tool.call" ? `${str(p.server)}/${str(p.tool)}`
|
|
27
|
+
: type === "net.fetch" ? str(p.host) : "";
|
|
28
|
+
return `${type || "?"}${bits ? ` ${bits}` : ""}`;
|
|
29
|
+
}
|
|
30
|
+
/** Trim a description to one readable fragment, cut on a word boundary. */
|
|
31
|
+
function shorten(text, max = MAX_WHY) {
|
|
32
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
33
|
+
if (clean.length <= max)
|
|
34
|
+
return clean;
|
|
35
|
+
const cut = clean.slice(0, max);
|
|
36
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
37
|
+
return `${(lastSpace > max * 0.6 ? cut.slice(0, lastSpace) : cut).replace(/[,;:—-]$/, "")}…`;
|
|
38
|
+
}
|
|
39
|
+
/** Find the clause that decided, by the id the verdict reported. */
|
|
40
|
+
export function findClause(policy, clauseId) {
|
|
41
|
+
if (!clauseId || !Array.isArray(policy?.clauses))
|
|
42
|
+
return null;
|
|
43
|
+
for (const clause of policy.clauses) {
|
|
44
|
+
if (clause && typeof clause === "object" && str(clause.id) === clauseId) {
|
|
45
|
+
return clause;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/** Compose the block message. Degrades in order: with a clause and a description
|
|
51
|
+
* it explains the rule; with a clause but no description it still names it; with
|
|
52
|
+
* no clause at all (an undetermined verdict, a kill switch) it reports the
|
|
53
|
+
* engine's reason unchanged rather than inventing a rule that did not decide. */
|
|
54
|
+
export function explainDeny(input) {
|
|
55
|
+
const detail = input.detail.trim() || "denied";
|
|
56
|
+
const action = describeAction(input.intent);
|
|
57
|
+
const clause = findClause(input.policy, input.clauseId);
|
|
58
|
+
const clauseId = str(clause?.id) || str(input.clauseId);
|
|
59
|
+
// "blocked" is a claim about prevention, so it is only used where the action really
|
|
60
|
+
// was stopped in flight.
|
|
61
|
+
const verb = input.postHoc ? "recorded an out-of-policy" : "blocked";
|
|
62
|
+
if (!clauseId) {
|
|
63
|
+
return action ? `Scopebond ${verb} ${action}: ${detail}` : `Scopebond ${verb} action: ${detail}`;
|
|
64
|
+
}
|
|
65
|
+
const mode = str(clause?.mode) || "enforce";
|
|
66
|
+
const why = shorten(str(clause?.description));
|
|
67
|
+
const lines = [
|
|
68
|
+
`Scopebond ${verb} ${action || "action"} — rule "${clauseId}" (${mode}).`,
|
|
69
|
+
...(why ? [`Why: ${why}`] : []),
|
|
70
|
+
...(detail && detail !== "denied" ? [`Detail: ${detail}`] : []),
|
|
71
|
+
`Change the rule: ${input.remedy ?? `edit clause "${clauseId}" in ${input.policyPath || "your Scopebond policy"}`}`,
|
|
72
|
+
];
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=explain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain.js","sourceRoot":"","sources":["../src/explain.ts"],"names":[],"mappings":"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","sourcesContent":["// The sentence a person — and the coding agent they are supervising — sees when\n// Scopebond blocks an action.\n//\n// The engine's own reason is precise and unreadable (\"param ref fails pattern\"):\n// it names the mechanism, not the rule. The policy clause that decided already\n// carries a written `description`, and the verdict names that clause, so both are\n// available here. The composed message says what was blocked, which rule blocked\n// it, why that rule exists, and where to change it — and it keeps the engine's\n// detail so a surprising decision stays debuggable.\n//\n// This string is also fed back to the agent (Claude Code's\n// `permissionDecisionReason`, Cursor's `agentMessage`), so a clear one lets the\n// agent pick a different approach instead of retrying the same blocked call.\n\n/** A policy clause, as much of it as the message needs. */\nexport interface ExplainClause {\n id?: unknown;\n mode?: unknown;\n description?: unknown;\n}\n\nexport interface ExplainPolicy {\n clauses?: unknown;\n}\n\n/** An intent, as much of it as the message needs. */\nexport interface ExplainIntent {\n action_type?: unknown;\n params?: unknown;\n}\n\n/** How long a clause description may run before it is cut. Long enough for the\n * starter policy's first clause, short enough not to bury a terminal. */\nconst MAX_WHY = 300;\n\nconst str = (v: unknown): string => (typeof v === \"string\" ? v : \"\");\n\n/** One line naming the action: `git.push origin main`, `shell.exec rm`,\n * `file.read .env`. Shared with `log` so a decision reads the same in both. */\nexport function describeAction(intent: ExplainIntent | undefined): string {\n const type = str(intent?.action_type);\n const p = (intent?.params ?? {}) as Record<string, unknown>;\n const bits = type === \"shell.exec\" ? str(p.program)\n : type === \"git.push\" ? `${str(p.remote)} ${str(p.ref)}`.trim()\n : type === \"file.write\" || type === \"file.read\" ? str(p.path)\n : type === \"mcp.tool.call\" ? `${str(p.server)}/${str(p.tool)}`\n : type === \"net.fetch\" ? str(p.host) : \"\";\n return `${type || \"?\"}${bits ? ` ${bits}` : \"\"}`;\n}\n\n/** Trim a description to one readable fragment, cut on a word boundary. */\nfunction shorten(text: string, max = MAX_WHY): string {\n const clean = text.replace(/\\s+/g, \" \").trim();\n if (clean.length <= max) return clean;\n const cut = clean.slice(0, max);\n const lastSpace = cut.lastIndexOf(\" \");\n return `${(lastSpace > max * 0.6 ? cut.slice(0, lastSpace) : cut).replace(/[,;:—-]$/, \"\")}…`;\n}\n\n/** Find the clause that decided, by the id the verdict reported. */\nexport function findClause(policy: ExplainPolicy | undefined, clauseId: string | null | undefined): ExplainClause | null {\n if (!clauseId || !Array.isArray(policy?.clauses)) return null;\n for (const clause of policy.clauses) {\n if (clause && typeof clause === \"object\" && str((clause as ExplainClause).id) === clauseId) {\n return clause as ExplainClause;\n }\n }\n return null;\n}\n\nexport interface ExplainDenyInput {\n /** The loaded policy, for the deciding clause's own words. */\n policy?: ExplainPolicy;\n /** The clause id the verdict reported, when it reported one. */\n clauseId?: string | null;\n /** The engine's reason — kept verbatim as the technical detail. */\n detail: string;\n /** The action that was blocked. */\n intent?: ExplainIntent;\n /** Where the rule lives, so the next step is a real path. */\n policyPath?: string;\n /** How to change it, when there is something better to say than \"edit this file\" — the\n * compiled policy is generated, so pointing a reader at it invites them to hand-edit\n * something that will be overwritten. */\n remedy?: string;\n /** The harness reported this action only after it happened, so the decision records\n * and flags it but did not prevent it. The wording must not claim otherwise. */\n postHoc?: boolean;\n}\n\n/** Compose the block message. Degrades in order: with a clause and a description\n * it explains the rule; with a clause but no description it still names it; with\n * no clause at all (an undetermined verdict, a kill switch) it reports the\n * engine's reason unchanged rather than inventing a rule that did not decide. */\nexport function explainDeny(input: ExplainDenyInput): string {\n const detail = input.detail.trim() || \"denied\";\n const action = describeAction(input.intent);\n const clause = findClause(input.policy, input.clauseId);\n const clauseId = str(clause?.id) || str(input.clauseId);\n // \"blocked\" is a claim about prevention, so it is only used where the action really\n // was stopped in flight.\n const verb = input.postHoc ? \"recorded an out-of-policy\" : \"blocked\";\n if (!clauseId) {\n return action ? `Scopebond ${verb} ${action}: ${detail}` : `Scopebond ${verb} action: ${detail}`;\n }\n const mode = str(clause?.mode) || \"enforce\";\n const why = shorten(str(clause?.description));\n const lines = [\n `Scopebond ${verb} ${action || \"action\"} — rule \"${clauseId}\" (${mode}).`,\n ...(why ? [`Why: ${why}`] : []),\n ...(detail && detail !== \"denied\" ? [`Detail: ${detail}`] : []),\n `Change the rule: ${input.remedy ?? `edit clause \"${clauseId}\" in ${input.policyPath || \"your Scopebond policy\"}`}`,\n ];\n return lines.join(\"\\n\");\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,17 @@ export { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } fro
|
|
|
2
2
|
export type { Mapped, NormalizedIntent } from "./map.js";
|
|
3
3
|
export { createHookRuntime, starterPolicy, upgradeStarterPolicy } from "./runtime.js";
|
|
4
4
|
export type { RuntimeConfig, Decision } from "./runtime.js";
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export
|
|
5
|
+
export { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor, RULES_FILE, } from "./rules.js";
|
|
6
|
+
export type { RuleSet, PathRule } from "./rules.js";
|
|
7
|
+
export { scaffold, harnessSnippet, installHarness, placeHook } from "./init.js";
|
|
8
|
+
export type { HookPlacement } from "./init.js";
|
|
9
|
+
export { userHome, userHarnessFile, projectHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, isScopebondHookCommand, harnessEntryMatches, localHarnessFile, isMachineSpecificCommand, gitShareState, excludeFromGit, pruneHarnessEntries, } from "./install.js";
|
|
10
|
+
export type { Harness, HarnessScopes, GitShare } from "./install.js";
|
|
11
|
+
export { explainDeny, describeAction, findClause } from "./explain.js";
|
|
12
|
+
export type { ExplainDenyInput, ExplainIntent, ExplainPolicy, ExplainClause } from "./explain.js";
|
|
13
|
+
export { ensureDurableRuntime, isEphemeralPath, nodeModulesRootOf, runtimeRoot, runtimeDirFor, pinnedCliPath, } from "./runtime-install.js";
|
|
14
|
+
export type { PinResult } from "./runtime-install.js";
|
|
8
15
|
export { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from "./cloud.js";
|
|
9
16
|
export type { HookConnection } from "./cloud.js";
|
|
10
|
-
export { scrubSecrets, scrubParam, redactCommand, digest, sha256 } from "./minimize.js";
|
|
17
|
+
export { scrubSecrets, scrubParam, redactCommand, digest, sha256, keyedDigest, useDigestKey, loadOrCreateDigestKey } from "./minimize.js";
|
|
11
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC7F,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACtF,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC7F,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACtF,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,GAC/F,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAChF,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EACL,QAAQ,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EACxG,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,SAAS,EAClF,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,EACpG,aAAa,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,EAC7E,sBAAsB,EAAE,mBAAmB,EAC3C,gBAAgB,EAAE,wBAAwB,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,GAC/F,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACvE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EACL,oBAAoB,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,GACpG,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACxG,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
// binary is the thin harness adapter over them.
|
|
4
4
|
export { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from "./map.js";
|
|
5
5
|
export { createHookRuntime, starterPolicy, upgradeStarterPolicy } from "./runtime.js";
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
6
|
+
export { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor, RULES_FILE, } from "./rules.js";
|
|
7
|
+
export { scaffold, harnessSnippet, installHarness, placeHook } from "./init.js";
|
|
8
|
+
export { userHome, userHarnessFile, projectHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, isScopebondHookCommand, harnessEntryMatches, localHarnessFile, isMachineSpecificCommand, gitShareState, excludeFromGit, pruneHarnessEntries, } from "./install.js";
|
|
9
|
+
export { explainDeny, describeAction, findClause } from "./explain.js";
|
|
10
|
+
export { ensureDurableRuntime, isEphemeralPath, nodeModulesRootOf, runtimeRoot, runtimeDirFor, pinnedCliPath, } from "./runtime-install.js";
|
|
8
11
|
export { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from "./cloud.js";
|
|
9
|
-
export { scrubSecrets, scrubParam, redactCommand, digest, sha256 } from "./minimize.js";
|
|
12
|
+
export { scrubSecrets, scrubParam, redactCommand, digest, sha256, keyedDigest, useDigestKey, loadOrCreateDigestKey } from "./minimize.js";
|
|
10
13
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,4EAA4E;AAC5E,gDAAgD;AAEhD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,4EAA4E;AAC5E,gDAAgD;AAEhD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEtF,OAAO,EACL,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,GAC/F,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEhF,OAAO,EACL,QAAQ,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EACxG,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,SAAS,EAClF,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,EACpG,aAAa,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,EAC7E,sBAAsB,EAAE,mBAAmB,EAC3C,gBAAgB,EAAE,wBAAwB,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,GAC/F,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,EACL,oBAAoB,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,GACpG,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAExG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC","sourcesContent":["// @scopebond/hook — the Claude Code, Cursor + Codex connector. The library surface is\n// the deterministic mapper and the check-only runtime; the `scopebond-hook`\n// binary is the thin harness adapter over them.\n\nexport { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from \"./map.js\";\nexport type { Mapped, NormalizedIntent } from \"./map.js\";\nexport { createHookRuntime, starterPolicy, upgradeStarterPolicy } from \"./runtime.js\";\nexport type { RuntimeConfig, Decision } from \"./runtime.js\";\nexport {\n compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor, RULES_FILE,\n} from \"./rules.js\";\nexport type { RuleSet, PathRule } from \"./rules.js\";\nexport { scaffold, harnessSnippet, installHarness, placeHook } from \"./init.js\";\nexport type { HookPlacement } from \"./init.js\";\nexport {\n userHome, userHarnessFile, projectHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig,\n cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome,\n readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile,\n harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves,\n isScopebondHookCommand, harnessEntryMatches,\n localHarnessFile, isMachineSpecificCommand, gitShareState, excludeFromGit, pruneHarnessEntries,\n} from \"./install.js\";\nexport type { Harness, HarnessScopes, GitShare } from \"./install.js\";\nexport { explainDeny, describeAction, findClause } from \"./explain.js\";\nexport type { ExplainDenyInput, ExplainIntent, ExplainPolicy, ExplainClause } from \"./explain.js\";\nexport {\n ensureDurableRuntime, isEphemeralPath, nodeModulesRootOf, runtimeRoot, runtimeDirFor, pinnedCliPath,\n} from \"./runtime-install.js\";\nexport type { PinResult } from \"./runtime-install.js\";\nexport { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from \"./cloud.js\";\nexport type { HookConnection } from \"./cloud.js\";\nexport { scrubSecrets, scrubParam, redactCommand, digest, sha256, keyedDigest, useDigestKey, loadOrCreateDigestKey } from \"./minimize.js\";\n"]}
|
package/dist/init.d.ts
CHANGED
|
@@ -3,12 +3,49 @@ export declare function scaffold(dir: string, opts?: {
|
|
|
3
3
|
}): {
|
|
4
4
|
agentKid: string;
|
|
5
5
|
policyPath: string;
|
|
6
|
+
rulesPath: string;
|
|
6
7
|
};
|
|
8
|
+
/** Where `init` put the hook, and why — for the lines it prints. */
|
|
9
|
+
export interface HookPlacement {
|
|
10
|
+
/** The config file written. */
|
|
11
|
+
file: string;
|
|
12
|
+
/** The command written into it. */
|
|
13
|
+
command: string;
|
|
14
|
+
/** `shared`: the project config a team commits (portable command only).
|
|
15
|
+
* `personal`: a file only this clone uses (the pinned, machine-specific command). */
|
|
16
|
+
scope: "shared" | "personal";
|
|
17
|
+
/** One line for the user when the placement differs from what they might expect. */
|
|
18
|
+
note?: string;
|
|
19
|
+
/** Machine-specific entries moved out of a shared config file (a repair). */
|
|
20
|
+
repaired: number;
|
|
21
|
+
}
|
|
22
|
+
/** Place the hook for a project so that nothing machine-specific reaches a file git
|
|
23
|
+
* shares. A pinned command (`"<node>" "<…/cli.js>" claude`) is fast but names paths that
|
|
24
|
+
* exist only here; in a committed `.claude/settings.json` it cannot start on a
|
|
25
|
+
* teammate's machine, and an agent treats a hook that cannot start as a non-blocking
|
|
26
|
+
* error — so the agent there would run unchecked, while the file says it is governed.
|
|
27
|
+
*
|
|
28
|
+
* - `shared` (or no pinned command): the portable `npx` form in the project config.
|
|
29
|
+
* - Claude Code, pinned: `.claude/settings.local.json`, kept out of git for this clone;
|
|
30
|
+
* any machine-specific entry an older `init` wrote into `settings.json` is removed.
|
|
31
|
+
* - Cursor / Codex, pinned: they have no personal file, so the project file is used
|
|
32
|
+
* only if git does not already track it (then excluded for this clone); a tracked
|
|
33
|
+
* file gets the portable form. */
|
|
34
|
+
export declare function placeHook(harness: "claude" | "cursor" | "codex", cwd: string, pinned: string | undefined, opts?: {
|
|
35
|
+
shared?: boolean;
|
|
36
|
+
}): HookPlacement;
|
|
7
37
|
/** Install the hook into the agent's own config file, merging with anything already
|
|
8
38
|
* there (idempotent — safe to run twice). Returns the file it wrote. This is what
|
|
9
39
|
* lets `connect` finish setup without the user hand-editing JSON.
|
|
10
|
-
* `cwd` defaults to the current directory (the project root the user runs it in).
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
40
|
+
* `cwd` defaults to the current directory (the project root the user runs it in).
|
|
41
|
+
* Writes exactly where it is told; `placeHook` decides where a command may go. */
|
|
42
|
+
export declare function installHarness(harness: "claude" | "cursor" | "codex", cwd?: string,
|
|
43
|
+
/** The command to run per tool call. Defaults to the portable `npx` form. */
|
|
44
|
+
command?: string,
|
|
45
|
+
/** The file to write; defaults to the shared project config. */
|
|
46
|
+
target?: string): string;
|
|
47
|
+
/** The harness configuration snippet to install the hook by hand. Defaults to the
|
|
48
|
+
* version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so
|
|
49
|
+
* the printed snippet matches what `init` would have written. */
|
|
50
|
+
export declare function harnessSnippet(harness: "claude" | "cursor" | "codex", command?: string): string;
|
|
14
51
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAgBA,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAyBjI;AAED,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB;0FACsF;IACtF,KAAK,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC7B,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;qCAWqC;AACrC,wBAAgB,SAAS,CACvB,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EACtC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9B,aAAa,CAiDf;AAED;;;;mFAImF;AACnF,wBAAgB,cAAc,CAC5B,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EACtC,GAAG,GAAE,MAAsB;AAC3B,6EAA6E;AAC7E,OAAO,CAAC,EAAE,MAAM;AAChB,gEAAgE;AAChE,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAmCR;AAED;;kEAEkE;AAClE,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA6B/F"}
|
package/dist/init.js
CHANGED
|
@@ -5,9 +5,10 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { loadOrCreateAttester } from "@scopebond/gateway/node";
|
|
7
7
|
import { createSigner } from "@scopebond/sdk";
|
|
8
|
-
import {
|
|
9
|
-
import { readHarnessConfig } from "./install.js";
|
|
8
|
+
import { compile, defaultRules, loadRules, saveRules, rulesPath } from "./rules.js";
|
|
9
|
+
import { readHarnessConfig, projectHarnessFile, localHarnessFile, harnessEntryMatches, backupHarnessConfig, gitShareState, excludeFromGit, pruneHarnessEntries, configuredHookCommands, isMachineSpecificCommand, } from "./install.js";
|
|
10
10
|
import { hookCommand } from "./version.js";
|
|
11
|
+
import { loadOrCreateDigestKey } from "./minimize.js";
|
|
11
12
|
export function scaffold(dir, opts = {}) {
|
|
12
13
|
mkdirSync(dir, { recursive: true });
|
|
13
14
|
const keyPath = join(dir, "agent.key");
|
|
@@ -20,31 +21,110 @@ export function scaffold(dir, opts = {}) {
|
|
|
20
21
|
// Machine signing key (agent) + gateway countersigning key (attester). Reused if present.
|
|
21
22
|
loadOrCreateAttester({ file: keyPath });
|
|
22
23
|
loadOrCreateAttester({ file: attesterPath });
|
|
24
|
+
// The per-machine key for keyed command and argument digests (see minimize.ts).
|
|
25
|
+
loadOrCreateDigestKey(dir);
|
|
23
26
|
const agent = createSigner({ privateKeyPem: readFileSync(keyPath, "utf8") });
|
|
27
|
+
// The editable rule set beside the compiled policy, so "edit the limits" means editing a
|
|
28
|
+
// readable list rather than a 700-character lookahead. `compile(defaultRules())` produces
|
|
29
|
+
// the starter policy's exact patterns — `rules.test.mjs` pins that — so writing either
|
|
30
|
+
// form enforces the same thing.
|
|
31
|
+
const rulesFile = rulesPath(dir);
|
|
32
|
+
if (!existsSync(rulesFile) || opts.force)
|
|
33
|
+
saveRules(dir, defaultRules());
|
|
24
34
|
if (!existsSync(policyPath) || opts.force) {
|
|
25
|
-
|
|
35
|
+
const rules = loadRules(dir) ?? defaultRules();
|
|
36
|
+
writeFileSync(policyPath, JSON.stringify(compile(rules, agent.kid), null, 2) + "\n");
|
|
26
37
|
}
|
|
27
|
-
return { agentKid: agent.kid, policyPath };
|
|
38
|
+
return { agentKid: agent.kid, policyPath, rulesPath: rulesFile };
|
|
39
|
+
}
|
|
40
|
+
/** Place the hook for a project so that nothing machine-specific reaches a file git
|
|
41
|
+
* shares. A pinned command (`"<node>" "<…/cli.js>" claude`) is fast but names paths that
|
|
42
|
+
* exist only here; in a committed `.claude/settings.json` it cannot start on a
|
|
43
|
+
* teammate's machine, and an agent treats a hook that cannot start as a non-blocking
|
|
44
|
+
* error — so the agent there would run unchecked, while the file says it is governed.
|
|
45
|
+
*
|
|
46
|
+
* - `shared` (or no pinned command): the portable `npx` form in the project config.
|
|
47
|
+
* - Claude Code, pinned: `.claude/settings.local.json`, kept out of git for this clone;
|
|
48
|
+
* any machine-specific entry an older `init` wrote into `settings.json` is removed.
|
|
49
|
+
* - Cursor / Codex, pinned: they have no personal file, so the project file is used
|
|
50
|
+
* only if git does not already track it (then excluded for this clone); a tracked
|
|
51
|
+
* file gets the portable form. */
|
|
52
|
+
export function placeHook(harness, cwd, pinned, opts = {}) {
|
|
53
|
+
const shared = projectHarnessFile(harness, cwd);
|
|
54
|
+
const portable = hookCommand(harness);
|
|
55
|
+
const local = localHarnessFile(harness, cwd);
|
|
56
|
+
if (opts.shared || !pinned) {
|
|
57
|
+
const file = installHarness(harness, cwd, portable);
|
|
58
|
+
// One hook per project: a personal copy beside a shared one would check every action twice.
|
|
59
|
+
if (local)
|
|
60
|
+
pruneHarnessEntries(local, () => true);
|
|
61
|
+
return { file, command: portable, scope: "shared", repaired: 0 };
|
|
62
|
+
}
|
|
63
|
+
if (local) {
|
|
64
|
+
const state = gitShareState(local);
|
|
65
|
+
if (state === "tracked") {
|
|
66
|
+
const file = installHarness(harness, cwd, portable);
|
|
67
|
+
return { file, command: portable, scope: "shared", repaired: 0,
|
|
68
|
+
note: `${local} is committed to git, so the portable command was used (it starts on any machine)` };
|
|
69
|
+
}
|
|
70
|
+
// An existing portable hook in the shared file is the team's; leave it rather than
|
|
71
|
+
// running two hooks, and say how to get the fast one.
|
|
72
|
+
if (configuredHookCommands(shared).some((c) => !isMachineSpecificCommand(c))) {
|
|
73
|
+
return { file: shared, command: portable, scope: "shared", repaired: 0,
|
|
74
|
+
note: `kept the shared hook already in ${shared}; remove it there to pin a faster one on this machine` };
|
|
75
|
+
}
|
|
76
|
+
// A personal file git would still add is not personal: if it cannot be excluded,
|
|
77
|
+
// fall back to the portable command rather than leave a pinned path to be committed.
|
|
78
|
+
if (state === "untracked" && !excludeFromGit(local)) {
|
|
79
|
+
const file = installHarness(harness, cwd, portable);
|
|
80
|
+
pruneHarnessEntries(local, () => true);
|
|
81
|
+
pruneHarnessEntries(shared, isMachineSpecificCommand);
|
|
82
|
+
return { file, command: portable, scope: "shared", repaired: 0,
|
|
83
|
+
note: `${local} could not be kept out of git, so the portable command was used` };
|
|
84
|
+
}
|
|
85
|
+
const file = installHarness(harness, cwd, pinned, local);
|
|
86
|
+
const repaired = pruneHarnessEntries(shared, isMachineSpecificCommand);
|
|
87
|
+
return { file, command: pinned, scope: "personal", repaired };
|
|
88
|
+
}
|
|
89
|
+
const state = gitShareState(shared);
|
|
90
|
+
if (state === "tracked") {
|
|
91
|
+
const file = installHarness(harness, cwd, portable);
|
|
92
|
+
return { file, command: portable, scope: "shared", repaired: 0,
|
|
93
|
+
note: `${shared} is committed to git, so the portable command was used (it starts on any machine)` };
|
|
94
|
+
}
|
|
95
|
+
if (state === "untracked" && !excludeFromGit(shared)) {
|
|
96
|
+
const file = installHarness(harness, cwd, portable);
|
|
97
|
+
return { file, command: portable, scope: "shared", repaired: 0,
|
|
98
|
+
note: `${shared} could not be kept out of git, so the portable command was used` };
|
|
99
|
+
}
|
|
100
|
+
const file = installHarness(harness, cwd, pinned);
|
|
101
|
+
return { file, command: pinned, scope: "personal", repaired: 0 };
|
|
28
102
|
}
|
|
29
103
|
/** Install the hook into the agent's own config file, merging with anything already
|
|
30
104
|
* there (idempotent — safe to run twice). Returns the file it wrote. This is what
|
|
31
105
|
* lets `connect` finish setup without the user hand-editing JSON.
|
|
32
|
-
* `cwd` defaults to the current directory (the project root the user runs it in).
|
|
33
|
-
|
|
106
|
+
* `cwd` defaults to the current directory (the project root the user runs it in).
|
|
107
|
+
* Writes exactly where it is told; `placeHook` decides where a command may go. */
|
|
108
|
+
export function installHarness(harness, cwd = process.cwd(),
|
|
109
|
+
/** The command to run per tool call. Defaults to the portable `npx` form. */
|
|
110
|
+
command,
|
|
111
|
+
/** The file to write; defaults to the shared project config. */
|
|
112
|
+
target) {
|
|
34
113
|
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
35
|
-
const file =
|
|
36
|
-
: harness === "codex" ? join(cwd, ".codex", "hooks.json")
|
|
37
|
-
: join(cwd, ".claude", "settings.json");
|
|
114
|
+
const file = target ?? projectHarnessFile(harness, cwd);
|
|
38
115
|
const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object
|
|
116
|
+
backupHarnessConfig(file); // keep the user's original beside it before the first change
|
|
39
117
|
mkdirSync(dirname(file), { recursive: true });
|
|
40
118
|
const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
119
|
+
const forHarness = (h) => command ? command.replace(/\b(claude|cursor|codex)$/, h) : hookCommand(h);
|
|
120
|
+
const cursorCmd = forHarness("cursor");
|
|
121
|
+
const claudeCmd = forHarness("claude");
|
|
122
|
+
const codexCmd = forHarness("codex");
|
|
123
|
+
// Match any existing Scopebond hook entry — npx, pinned absolute (either path
|
|
124
|
+
// separator) or a legacy bare `scopebond-hook` — so re-running replaces the entry
|
|
125
|
+
// instead of appending a second one. One shared matcher with `install`: a private
|
|
126
|
+
// copy here missed the pinned Windows form and duplicated the hook.
|
|
127
|
+
const entryMatches = harnessEntryMatches;
|
|
48
128
|
if (harness === "cursor") {
|
|
49
129
|
config.version = config.version ?? 1;
|
|
50
130
|
for (const event of ["beforeShellExecution", "beforeMCPExecution", "beforeReadFile", "afterFileEdit"]) {
|
|
@@ -71,10 +151,13 @@ export function installHarness(harness, cwd = process.cwd()) {
|
|
|
71
151
|
writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
|
|
72
152
|
return file;
|
|
73
153
|
}
|
|
74
|
-
/** The harness configuration snippet to install the hook
|
|
75
|
-
|
|
154
|
+
/** The harness configuration snippet to install the hook by hand. Defaults to the
|
|
155
|
+
* version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so
|
|
156
|
+
* the printed snippet matches what `init` would have written. */
|
|
157
|
+
export function harnessSnippet(harness, command) {
|
|
158
|
+
const cmdFor = (h) => command ? command.replace(/\b(claude|cursor|codex)$/, h) : hookCommand(h);
|
|
76
159
|
if (harness === "cursor") {
|
|
77
|
-
const cmd =
|
|
160
|
+
const cmd = cmdFor("cursor");
|
|
78
161
|
return JSON.stringify({
|
|
79
162
|
version: 1,
|
|
80
163
|
hooks: {
|
|
@@ -89,14 +172,14 @@ export function harnessSnippet(harness) {
|
|
|
89
172
|
return JSON.stringify({
|
|
90
173
|
hooks: {
|
|
91
174
|
PreToolUse: [{
|
|
92
|
-
hooks: [{ type: "command", command:
|
|
175
|
+
hooks: [{ type: "command", command: cmdFor("codex"), timeout: 30, statusMessage: "Checking this action with Scopebond" }],
|
|
93
176
|
}],
|
|
94
177
|
},
|
|
95
178
|
}, null, 2);
|
|
96
179
|
}
|
|
97
180
|
return JSON.stringify({
|
|
98
181
|
hooks: {
|
|
99
|
-
PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command:
|
|
182
|
+
PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: cmdFor("claude") }] }],
|
|
100
183
|
},
|
|
101
184
|
}, null, 2);
|
|
102
185
|
}
|
package/dist/init.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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","sourcesContent":["// `scopebond-hook init` — scaffold the local enrollment: a machine signing key, a\n// gateway countersigning key and a starter policy. Keys and receipts stay on the\n// machine; no credentials are handled.\n\nimport { mkdirSync, writeFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { createSigner } from \"@scopebond/sdk\";\nimport { starterPolicy } from \"./runtime.js\";\nimport { readHarnessConfig } from \"./install.js\";\nimport { hookCommand } from \"./version.js\";\n\nexport function scaffold(dir: string, opts: { force?: boolean } = {}): { agentKid: string; policyPath: string } {\n mkdirSync(dir, { recursive: true });\n const keyPath = join(dir, \"agent.key\");\n const attesterPath = join(dir, \"attester.key\");\n const policyPath = join(dir, \"policy.json\");\n // Never let the signing keys, the Cloud credential or the local log be committed.\n const gitignorePath = join(dir, \".gitignore\");\n if (!existsSync(gitignorePath)) writeFileSync(gitignorePath, \"*\\n\");\n // Machine signing key (agent) + gateway countersigning key (attester). Reused if present.\n loadOrCreateAttester({ file: keyPath });\n loadOrCreateAttester({ file: attesterPath });\n const agent = createSigner({ privateKeyPem: readFileSync(keyPath, \"utf8\") });\n if (!existsSync(policyPath) || opts.force) {\n writeFileSync(policyPath, JSON.stringify(starterPolicy(agent.kid), null, 2) + \"\\n\");\n }\n return { agentKid: agent.kid, policyPath };\n}\n\n/** Install the hook into the agent's own config file, merging with anything already\n * there (idempotent — safe to run twice). Returns the file it wrote. This is what\n * lets `connect` finish setup without the user hand-editing JSON.\n * `cwd` defaults to the current directory (the project root the user runs it in). */\nexport function installHarness(harness: \"claude\" | \"cursor\" | \"codex\", cwd: string = process.cwd()): string {\n const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n const file = harness === \"cursor\" ? join(cwd, \".cursor\", \"hooks.json\")\n : harness === \"codex\" ? join(cwd, \".codex\", \"hooks.json\")\n : join(cwd, \".claude\", \"settings.json\");\n const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n const cursorCmd = hookCommand(\"cursor\");\n const claudeCmd = hookCommand(\"claude\");\n const codexCmd = hookCommand(\"codex\");\n // Match any existing Scopebond hook entry (pinned or a legacy bare `scopebond-hook`)\n // so re-running never appends a duplicate and an old bare command is replaced.\n const isScopebond = (cmd: unknown): boolean => typeof cmd === \"string\" && /(^|\\s)(npx\\s+.*)?@scopebond\\/hook|(^|\\s)scopebond-hook(\\s|$)/.test(cmd);\n const entryMatches = (e: unknown): boolean =>\n isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));\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 existing = list.findIndex(entryMatches);\n if (existing >= 0) list[existing] = { command: cursorCmd }; else list.push({ command: cursorCmd });\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 existing = list.findIndex(entryMatches);\n // Codex matchers are regular expressions. No matcher means all supported tools.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command: codexCmd, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command: claudeCmd }] };\n if (existing >= 0) list[existing] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** The harness configuration snippet to install the hook (version-pinned npx). */\nexport function harnessSnippet(harness: \"claude\" | \"cursor\" | \"codex\"): string {\n if (harness === \"cursor\") {\n const cmd = hookCommand(\"cursor\");\n return JSON.stringify({\n version: 1,\n hooks: {\n beforeShellExecution: [{ command: cmd }],\n beforeMCPExecution: [{ command: cmd }],\n beforeReadFile: [{ command: cmd }],\n afterFileEdit: [{ command: cmd }],\n },\n }, null, 2);\n }\n if (harness === \"codex\") {\n return JSON.stringify({\n hooks: {\n PreToolUse: [{\n hooks: [{ type: \"command\", command: hookCommand(\"codex\"), timeout: 30, statusMessage: \"Checking this action with Scopebond\" }],\n }],\n },\n }, null, 2);\n }\n return JSON.stringify({\n hooks: {\n PreToolUse: [{ matcher: \"*\", hooks: [{ type: \"command\", command: hookCommand(\"claude\") }] }],\n },\n }, null, 2);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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","sourcesContent":["// `scopebond-hook init` — scaffold the local enrollment: a machine signing key, a\n// gateway countersigning key and a starter policy. Keys and receipts stay on the\n// machine; no credentials are handled.\n\nimport { mkdirSync, writeFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { createSigner } from \"@scopebond/sdk\";\nimport { compile, defaultRules, loadRules, saveRules, rulesPath } from \"./rules.js\";\nimport {\n readHarnessConfig, projectHarnessFile, localHarnessFile, harnessEntryMatches, backupHarnessConfig,\n gitShareState, excludeFromGit, pruneHarnessEntries, configuredHookCommands, isMachineSpecificCommand,\n} from \"./install.js\";\nimport { hookCommand } from \"./version.js\";\nimport { loadOrCreateDigestKey } from \"./minimize.js\";\n\nexport function scaffold(dir: string, opts: { force?: boolean } = {}): { agentKid: string; policyPath: string; rulesPath: string } {\n mkdirSync(dir, { recursive: true });\n const keyPath = join(dir, \"agent.key\");\n const attesterPath = join(dir, \"attester.key\");\n const policyPath = join(dir, \"policy.json\");\n // Never let the signing keys, the Cloud credential or the local log be committed.\n const gitignorePath = join(dir, \".gitignore\");\n if (!existsSync(gitignorePath)) writeFileSync(gitignorePath, \"*\\n\");\n // Machine signing key (agent) + gateway countersigning key (attester). Reused if present.\n loadOrCreateAttester({ file: keyPath });\n loadOrCreateAttester({ file: attesterPath });\n // The per-machine key for keyed command and argument digests (see minimize.ts).\n loadOrCreateDigestKey(dir);\n const agent = createSigner({ privateKeyPem: readFileSync(keyPath, \"utf8\") });\n // The editable rule set beside the compiled policy, so \"edit the limits\" means editing a\n // readable list rather than a 700-character lookahead. `compile(defaultRules())` produces\n // the starter policy's exact patterns — `rules.test.mjs` pins that — so writing either\n // form enforces the same thing.\n const rulesFile = rulesPath(dir);\n if (!existsSync(rulesFile) || opts.force) saveRules(dir, defaultRules());\n if (!existsSync(policyPath) || opts.force) {\n const rules = loadRules(dir) ?? defaultRules();\n writeFileSync(policyPath, JSON.stringify(compile(rules, agent.kid), null, 2) + \"\\n\");\n }\n return { agentKid: agent.kid, policyPath, rulesPath: rulesFile };\n}\n\n/** Where `init` put the hook, and why — for the lines it prints. */\nexport interface HookPlacement {\n /** The config file written. */\n file: string;\n /** The command written into it. */\n command: string;\n /** `shared`: the project config a team commits (portable command only).\n * `personal`: a file only this clone uses (the pinned, machine-specific command). */\n scope: \"shared\" | \"personal\";\n /** One line for the user when the placement differs from what they might expect. */\n note?: string;\n /** Machine-specific entries moved out of a shared config file (a repair). */\n repaired: number;\n}\n\n/** Place the hook for a project so that nothing machine-specific reaches a file git\n * shares. A pinned command (`\"<node>\" \"<…/cli.js>\" claude`) is fast but names paths that\n * exist only here; in a committed `.claude/settings.json` it cannot start on a\n * teammate's machine, and an agent treats a hook that cannot start as a non-blocking\n * error — so the agent there would run unchecked, while the file says it is governed.\n *\n * - `shared` (or no pinned command): the portable `npx` form in the project config.\n * - Claude Code, pinned: `.claude/settings.local.json`, kept out of git for this clone;\n * any machine-specific entry an older `init` wrote into `settings.json` is removed.\n * - Cursor / Codex, pinned: they have no personal file, so the project file is used\n * only if git does not already track it (then excluded for this clone); a tracked\n * file gets the portable form. */\nexport function placeHook(\n harness: \"claude\" | \"cursor\" | \"codex\",\n cwd: string,\n pinned: string | undefined,\n opts: { shared?: boolean } = {},\n): HookPlacement {\n const shared = projectHarnessFile(harness, cwd);\n const portable = hookCommand(harness);\n const local = localHarnessFile(harness, cwd);\n if (opts.shared || !pinned) {\n const file = installHarness(harness, cwd, portable);\n // One hook per project: a personal copy beside a shared one would check every action twice.\n if (local) pruneHarnessEntries(local, () => true);\n return { file, command: portable, scope: \"shared\", repaired: 0 };\n }\n if (local) {\n const state = gitShareState(local);\n if (state === \"tracked\") {\n const file = installHarness(harness, cwd, portable);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${local} is committed to git, so the portable command was used (it starts on any machine)` };\n }\n // An existing portable hook in the shared file is the team's; leave it rather than\n // running two hooks, and say how to get the fast one.\n if (configuredHookCommands(shared).some((c) => !isMachineSpecificCommand(c))) {\n return { file: shared, command: portable, scope: \"shared\", repaired: 0,\n note: `kept the shared hook already in ${shared}; remove it there to pin a faster one on this machine` };\n }\n // A personal file git would still add is not personal: if it cannot be excluded,\n // fall back to the portable command rather than leave a pinned path to be committed.\n if (state === \"untracked\" && !excludeFromGit(local)) {\n const file = installHarness(harness, cwd, portable);\n pruneHarnessEntries(local, () => true);\n pruneHarnessEntries(shared, isMachineSpecificCommand);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${local} could not be kept out of git, so the portable command was used` };\n }\n const file = installHarness(harness, cwd, pinned, local);\n const repaired = pruneHarnessEntries(shared, isMachineSpecificCommand);\n return { file, command: pinned, scope: \"personal\", repaired };\n }\n const state = gitShareState(shared);\n if (state === \"tracked\") {\n const file = installHarness(harness, cwd, portable);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${shared} is committed to git, so the portable command was used (it starts on any machine)` };\n }\n if (state === \"untracked\" && !excludeFromGit(shared)) {\n const file = installHarness(harness, cwd, portable);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${shared} could not be kept out of git, so the portable command was used` };\n }\n const file = installHarness(harness, cwd, pinned);\n return { file, command: pinned, scope: \"personal\", repaired: 0 };\n}\n\n/** Install the hook into the agent's own config file, merging with anything already\n * there (idempotent — safe to run twice). Returns the file it wrote. This is what\n * lets `connect` finish setup without the user hand-editing JSON.\n * `cwd` defaults to the current directory (the project root the user runs it in).\n * Writes exactly where it is told; `placeHook` decides where a command may go. */\nexport function installHarness(\n harness: \"claude\" | \"cursor\" | \"codex\",\n cwd: string = process.cwd(),\n /** The command to run per tool call. Defaults to the portable `npx` form. */\n command?: string,\n /** The file to write; defaults to the shared project config. */\n target?: string,\n): string {\n const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n const file = target ?? projectHarnessFile(harness, cwd);\n const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object\n backupHarnessConfig(file); // keep the user's original beside it before the first change\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n const forHarness = (h: \"claude\" | \"cursor\" | \"codex\"): string =>\n command ? command.replace(/\\b(claude|cursor|codex)$/, h) : hookCommand(h);\n const cursorCmd = forHarness(\"cursor\");\n const claudeCmd = forHarness(\"claude\");\n const codexCmd = forHarness(\"codex\");\n // Match any existing Scopebond hook entry — npx, pinned absolute (either path\n // separator) or a legacy bare `scopebond-hook` — so re-running replaces the entry\n // instead of appending a second one. One shared matcher with `install`: a private\n // copy here missed the pinned Windows form and duplicated the hook.\n const entryMatches = harnessEntryMatches;\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 existing = list.findIndex(entryMatches);\n if (existing >= 0) list[existing] = { command: cursorCmd }; else list.push({ command: cursorCmd });\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 existing = list.findIndex(entryMatches);\n // Codex matchers are regular expressions. No matcher means all supported tools.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command: codexCmd, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command: claudeCmd }] };\n if (existing >= 0) list[existing] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** The harness configuration snippet to install the hook by hand. Defaults to the\n * version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so\n * the printed snippet matches what `init` would have written. */\nexport function harnessSnippet(harness: \"claude\" | \"cursor\" | \"codex\", command?: string): string {\n const cmdFor = (h: \"claude\" | \"cursor\" | \"codex\"): string =>\n command ? command.replace(/\\b(claude|cursor|codex)$/, h) : hookCommand(h);\n if (harness === \"cursor\") {\n const cmd = cmdFor(\"cursor\");\n return JSON.stringify({\n version: 1,\n hooks: {\n beforeShellExecution: [{ command: cmd }],\n beforeMCPExecution: [{ command: cmd }],\n beforeReadFile: [{ command: cmd }],\n afterFileEdit: [{ command: cmd }],\n },\n }, null, 2);\n }\n if (harness === \"codex\") {\n return JSON.stringify({\n hooks: {\n PreToolUse: [{\n hooks: [{ type: \"command\", command: cmdFor(\"codex\"), timeout: 30, statusMessage: \"Checking this action with Scopebond\" }],\n }],\n },\n }, null, 2);\n }\n return JSON.stringify({\n hooks: {\n PreToolUse: [{ matcher: \"*\", hooks: [{ type: \"command\", command: cmdFor(\"claude\") }] }],\n },\n }, null, 2);\n}\n"]}
|
package/dist/install.d.ts
CHANGED
|
@@ -4,6 +4,45 @@ export declare function userHome(): string;
|
|
|
4
4
|
/** The user-level agent config file for a harness. `HOME`/`USERPROFILE` (via homedir)
|
|
5
5
|
* roots it; SCOPEBOND_HOME does not move the agent's own config. */
|
|
6
6
|
export declare function userHarnessFile(harness: Harness): string;
|
|
7
|
+
/** The project-level agent config file for a harness — what `init` writes, so the
|
|
8
|
+
* hook travels with the repository rather than the machine. */
|
|
9
|
+
export declare function projectHarnessFile(harness: Harness, cwd?: string): string;
|
|
10
|
+
/** The personal, per-machine project config for a harness, or null when the harness
|
|
11
|
+
* has none. Claude Code reads `.claude/settings.local.json` beside `settings.json` and
|
|
12
|
+
* merges the hooks of both; it is the place for anything that names a path on this
|
|
13
|
+
* machine. Cursor and Codex have no such file. */
|
|
14
|
+
export declare function localHarnessFile(harness: Harness, cwd?: string): string | null;
|
|
15
|
+
/** Which config files actually carry a Scopebond hook for this harness. There are three
|
|
16
|
+
* places it can live — the shared project config, the personal project config (Claude
|
|
17
|
+
* Code's `settings.local.json`, where `init` pins the fast command) and the user config
|
|
18
|
+
* `install` writes — and `status`/`doctor` must look at all of them: checking only the
|
|
19
|
+
* user one told everyone who ran `init` that their install had failed. */
|
|
20
|
+
export interface HarnessScopes {
|
|
21
|
+
project: string | null;
|
|
22
|
+
local: string | null;
|
|
23
|
+
user: string | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function harnessScopes(harness: Harness, cwd?: string): HarnessScopes;
|
|
26
|
+
/** One word for where a harness is wired, for a status line. */
|
|
27
|
+
export declare function harnessScopeLabel(scopes: HarnessScopes): string;
|
|
28
|
+
/** Whether a hook command names a path that exists only on this machine — the pinned
|
|
29
|
+
* `"<node>" "<…/cli.js>" claude` form — rather than the portable `npx` form. */
|
|
30
|
+
export declare function isMachineSpecificCommand(command: string): boolean;
|
|
31
|
+
/** How git treats a config file: committed (`tracked`), kept out (`ignored`), neither
|
|
32
|
+
* yet (`untracked`, so a `git add .` would commit it), or not in a repository / no git
|
|
33
|
+
* on this machine (`none`). A file git shares must never carry a machine-specific
|
|
34
|
+
* command: on a teammate's machine the path does not exist, the command cannot start,
|
|
35
|
+
* and Claude Code, Cursor and Codex all treat a hook that cannot start as a non-blocking
|
|
36
|
+
* error — the agent runs with no check at all. */
|
|
37
|
+
export type GitShare = "tracked" | "ignored" | "untracked" | "none";
|
|
38
|
+
export declare function gitShareState(file: string): GitShare;
|
|
39
|
+
/** Keep a file out of git for this clone only, through `.git/info/exclude` — never the
|
|
40
|
+
* repository's own `.gitignore`, which is the team's file. Returns true when the file is
|
|
41
|
+
* ignored afterwards. */
|
|
42
|
+
export declare function excludeFromGit(file: string): boolean;
|
|
43
|
+
/** Remove the Scopebond entries whose command `remove` selects, leaving every other
|
|
44
|
+
* setting alone. Returns the number of entries removed. */
|
|
45
|
+
export declare function pruneHarnessEntries(file: string, remove: (command: string) => boolean): number;
|
|
7
46
|
/** The file in the user home that pins trusted project policies: absolute project
|
|
8
47
|
* config dir → SHA-256 of its policy.json. It lives under the protected home, so the
|
|
9
48
|
* governed agent cannot write it. */
|
|
@@ -22,10 +61,23 @@ export declare function isTrustedProject(dir: string): boolean;
|
|
|
22
61
|
export declare function resolveConfigDir(payloadCwd: string | undefined): string;
|
|
23
62
|
/** A project policy present but ignored because it is not trusted (for status/doctor). */
|
|
24
63
|
export declare function untrustedProjectPolicy(payloadCwd: string | undefined): string | null;
|
|
64
|
+
export declare const isScopebondHookCommand: (cmd: unknown) => boolean;
|
|
65
|
+
/** Whether a harness hook entry (either shape: a bare command, or a group with a
|
|
66
|
+
* nested `hooks` array) is a Scopebond entry. */
|
|
67
|
+
export declare const harnessEntryMatches: (e: unknown) => boolean;
|
|
25
68
|
/** Read an agent's JSON config for merging. A missing file is an empty config; a file
|
|
26
69
|
* that exists but does not parse as a JSON object is an error — rewriting it would
|
|
27
70
|
* silently delete the user's other settings, so the installer stops instead. */
|
|
28
71
|
export declare function readHarnessConfig(file: string): Record<string, unknown>;
|
|
72
|
+
/** Copy an agent config aside before changing it, once. Re-running an installer must not
|
|
73
|
+
* overwrite the backup with the already-modified file, or the original is lost on the
|
|
74
|
+
* second run — so an existing backup is left alone. Returns the backup path when one was
|
|
75
|
+
* written.
|
|
76
|
+
*
|
|
77
|
+
* This exists because these installers rewrite files the user did not create and may not
|
|
78
|
+
* know are involved (`~/.claude/settings.json` holds their theme, plugins and
|
|
79
|
+
* permissions), and the undo was previously "hope the merge was right". */
|
|
80
|
+
export declare function backupHarnessConfig(file: string): string | null;
|
|
29
81
|
/** Write (idempotently) a harness hook entry pointing at an explicit command string.
|
|
30
82
|
* Shared by the project installer (npx command) and the user installer (absolute path). */
|
|
31
83
|
export declare function writeHarnessConfig(file: string, harness: Harness, command: string): string;
|
|
@@ -36,10 +88,26 @@ export declare function removeHarnessConfig(file: string): boolean;
|
|
|
36
88
|
export declare function cursorDetected(): boolean;
|
|
37
89
|
/** Detect a Codex install by its user config directory. */
|
|
38
90
|
export declare function codexDetected(): boolean;
|
|
39
|
-
/** The absolute command a
|
|
40
|
-
*
|
|
91
|
+
/** The absolute command a harness entry runs: the current Node executable and the
|
|
92
|
+
* absolute path to this CLI, so it does not depend on PATH.
|
|
93
|
+
*
|
|
94
|
+
* Both paths are always quoted on Windows, even without a space in them. A harness
|
|
95
|
+
* runs this string through a shell, and that shell may be bash (Git Bash, WSL, a
|
|
96
|
+
* dev container), where an unquoted Windows path loses every backslash and the
|
|
97
|
+
* hook dies with MODULE_NOT_FOUND. Inside double quotes bash leaves a backslash
|
|
98
|
+
* alone unless it precedes $ ` " \ or a newline — none of which occur in a Windows
|
|
99
|
+
* path — and cmd.exe and PowerShell accept quoted paths too. */
|
|
41
100
|
export declare function absoluteHookCommand(cliPath: string, harness: Harness): string;
|
|
42
101
|
export declare function isHarnessConfigured(file: string): boolean;
|
|
102
|
+
/** Every Scopebond command string a harness config currently runs. `doctor` uses it to
|
|
103
|
+
* check that a pinned command still resolves: a hook entry that cannot start is worse
|
|
104
|
+
* than none, because a harness can read the failure as "no hook". */
|
|
105
|
+
export declare function configuredHookCommands(file: string): string[];
|
|
106
|
+
/** Whether a configured hook command can actually start. An `npx` form always can
|
|
107
|
+
* (npm resolves it); a pinned command names two absolute paths — the Node binary and
|
|
108
|
+
* the CLI — and either can go away: a Node version manager switching versions, or the
|
|
109
|
+
* Scopebond home being cleared. Both are exactly what this check exists to catch. */
|
|
110
|
+
export declare function hookCommandResolves(command: string): boolean;
|
|
43
111
|
/** Remove the user-level home (keys, policy, receipts). */
|
|
44
112
|
export declare function purgeHome(): void;
|
|
45
113
|
//# sourceMappingURL=install.d.ts.map
|
package/dist/install.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpD,gFAAgF;AAChF,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;qEACqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAIxD;AAED;gEACgE;AAChE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAIxF;AAED;;;mDAGmD;AACnD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAE7F;AAED;;;;2EAI2E;AAC3E,MAAM,WAAW,aAAa;IAAG,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AAEpG,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,aAAa,CAS1F;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAW/D;AAED;iFACiF;AACjF,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAIjE;AAED;;;;;mDAKmD;AACnD,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;AAEpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAepD;AAED;;0BAE0B;AAC1B,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAkBpD;AAED;4DAC4D;AAC5D,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,GAAG,MAAM,CAqB9F;AAED;;sCAEsC;AACtC,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAYD;uFACuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQtD;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED;;;;;iFAKiF;AACjF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAavE;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIpF;AAyBD,eAAO,MAAM,sBAAsB,GAAI,KAAK,OAAO,KAAG,OAgBrD,CAAC;AAGF;kDACkD;AAClD,eAAO,MAAM,mBAAmB,GAAI,GAAG,OAAO,KAAG,OACkF,CAAC;AAGpI;;iFAEiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvE;AAED;;;;;;;4EAO4E;AAC5E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/D;AAED;4FAC4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyB1F;AAED;gDACgD;AAChD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzD;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;;;;;;iEAQiE;AACjE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAG7E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOzD;AAED;;sEAEsE;AACtE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAc7D;AAED;;;sFAGsF;AACtF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAa5D;AAED,2DAA2D;AAC3D,wBAAgB,SAAS,IAAI,IAAI,CAEhC"}
|