@scopebond/hook 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -22
- package/dist/cli.js +124 -31
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +2 -1
- package/dist/cloud.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +2 -2
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +19 -11
- package/dist/init.js.map +1 -1
- package/dist/install.d.ts +21 -2
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +101 -24
- package/dist/install.js.map +1 -1
- package/dist/map.d.ts +10 -3
- package/dist/map.d.ts.map +1 -1
- package/dist/map.js +799 -54
- package/dist/map.js.map +1 -1
- package/dist/minimize.d.ts +6 -3
- package/dist/minimize.d.ts.map +1 -1
- package/dist/minimize.js +24 -4
- package/dist/minimize.js.map +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +75 -9
- package/dist/runtime.js.map +1 -1
- package/dist/shell.d.ts +53 -4
- package/dist/shell.d.ts.map +1 -1
- package/dist/shell.js +737 -81
- package/dist/shell.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +6 -5
package/dist/install.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// SB112 — the user-level installer. One install per developer machine (not per repo):
|
|
2
2
|
// keys and a starter policy live in a user-level home (~/.scopebond, override
|
|
3
3
|
// SCOPEBOND_HOME), and the hook is registered by absolute path in the user-level agent
|
|
4
|
-
// config (~/.claude/settings.json, ~/.cursor/hooks.json).
|
|
5
|
-
//
|
|
4
|
+
// config (~/.claude/settings.json, ~/.cursor/hooks.json, ~/.codex/hooks.json). When a
|
|
5
|
+
// user-level install exists, a project-local `.scopebond` is used only if the user
|
|
6
|
+
// trusted that exact policy (`scopebond trust`, or `init` in the project): a cloned
|
|
7
|
+
// repository, or an agent inside it, must not be able to swap in its own policy.
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
6
9
|
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
7
10
|
import { homedir } from "node:os";
|
|
8
|
-
import { dirname, join } from "node:path";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
12
|
/** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */
|
|
10
13
|
export function userHome() {
|
|
11
14
|
return process.env.SCOPEBOND_HOME ?? join(homedir(), ".scopebond");
|
|
@@ -13,12 +16,59 @@ export function userHome() {
|
|
|
13
16
|
/** The user-level agent config file for a harness. `HOME`/`USERPROFILE` (via homedir)
|
|
14
17
|
* roots it; SCOPEBOND_HOME does not move the agent's own config. */
|
|
15
18
|
export function userHarnessFile(harness) {
|
|
16
|
-
|
|
19
|
+
if (harness === "cursor")
|
|
20
|
+
return join(homedir(), ".cursor", "hooks.json");
|
|
21
|
+
if (harness === "codex")
|
|
22
|
+
return join(homedir(), ".codex", "hooks.json");
|
|
23
|
+
return join(homedir(), ".claude", "settings.json");
|
|
24
|
+
}
|
|
25
|
+
/** The file in the user home that pins trusted project policies: absolute project
|
|
26
|
+
* config dir → SHA-256 of its policy.json. It lives under the protected home, so the
|
|
27
|
+
* governed agent cannot write it. */
|
|
28
|
+
export function trustedProjectsFile() {
|
|
29
|
+
return join(userHome(), "trusted-projects.json");
|
|
30
|
+
}
|
|
31
|
+
function policyDigest(dir) {
|
|
32
|
+
try {
|
|
33
|
+
return createHash("sha256").update(readFileSync(join(dir, "policy.json"))).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readTrusted() {
|
|
40
|
+
try {
|
|
41
|
+
const p = JSON.parse(readFileSync(trustedProjectsFile(), "utf8"));
|
|
42
|
+
return isRecord(p) ? p : {};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const trustKey = (dir) => resolve(dir).replace(/\\/g, "/").toLowerCase();
|
|
49
|
+
/** Pin the project's current policy as trusted. Editing the policy afterwards un-trusts
|
|
50
|
+
* it until the user trusts it again, so a rewrite by the agent never takes effect. */
|
|
51
|
+
export function trustProjectPolicy(dir) {
|
|
52
|
+
const digest = policyDigest(dir);
|
|
53
|
+
if (!digest)
|
|
54
|
+
throw new Error(`no policy at ${join(dir, "policy.json")}`);
|
|
55
|
+
const trusted = readTrusted();
|
|
56
|
+
trusted[trustKey(dir)] = digest;
|
|
57
|
+
mkdirSync(userHome(), { recursive: true });
|
|
58
|
+
writeFileSync(trustedProjectsFile(), JSON.stringify(trusted, null, 2) + "\n");
|
|
59
|
+
return digest;
|
|
60
|
+
}
|
|
61
|
+
/** Whether the project's policy is exactly the one the user trusted. */
|
|
62
|
+
export function isTrustedProject(dir) {
|
|
63
|
+
const digest = policyDigest(dir);
|
|
64
|
+
return !!digest && readTrusted()[trustKey(dir)] === digest;
|
|
17
65
|
}
|
|
18
66
|
/** Resolve the config dir for a hook event, most specific first:
|
|
19
67
|
* explicit override → the payload's project → $CLAUDE_PROJECT_DIR → the user home.
|
|
20
68
|
* A project dir counts only when it has been scaffolded (a policy.json), so a bare
|
|
21
|
-
* working directory never shadows the user-level install
|
|
69
|
+
* working directory never shadows the user-level install — and, once a user-level
|
|
70
|
+
* install exists, only when its policy is trusted (`isTrustedProject`). Otherwise the
|
|
71
|
+
* user's own policy governs: a repository cannot bring a weaker one with it. */
|
|
22
72
|
export function resolveConfigDir(payloadCwd) {
|
|
23
73
|
if (process.env.SCOPEBOND_HOOK_DIR)
|
|
24
74
|
return process.env.SCOPEBOND_HOOK_DIR;
|
|
@@ -27,35 +77,55 @@ export function resolveConfigDir(payloadCwd) {
|
|
|
27
77
|
candidates.push(join(payloadCwd, ".scopebond"));
|
|
28
78
|
if (process.env.CLAUDE_PROJECT_DIR)
|
|
29
79
|
candidates.push(join(process.env.CLAUDE_PROJECT_DIR, ".scopebond"));
|
|
80
|
+
const home = userHome();
|
|
81
|
+
const homeInstalled = existsSync(join(home, "policy.json"));
|
|
30
82
|
for (const dir of candidates) {
|
|
31
|
-
if (existsSync(join(dir, "policy.json")))
|
|
83
|
+
if (existsSync(join(dir, "policy.json")) && (!homeInstalled || isTrustedProject(dir)))
|
|
32
84
|
return dir;
|
|
33
85
|
}
|
|
34
|
-
|
|
35
|
-
if (existsSync(join(home, "policy.json")))
|
|
86
|
+
if (homeInstalled)
|
|
36
87
|
return home;
|
|
37
88
|
// Nothing scaffolded yet: prefer the payload project, else the user home.
|
|
38
89
|
return candidates[0] ?? home;
|
|
39
90
|
}
|
|
91
|
+
/** A project policy present but ignored because it is not trusted (for status/doctor). */
|
|
92
|
+
export function untrustedProjectPolicy(payloadCwd) {
|
|
93
|
+
if (process.env.SCOPEBOND_HOOK_DIR || !existsSync(join(userHome(), "policy.json")))
|
|
94
|
+
return null;
|
|
95
|
+
const dirs = [payloadCwd, process.env.CLAUDE_PROJECT_DIR].filter(Boolean).map((d) => join(d, ".scopebond"));
|
|
96
|
+
return dirs.find((d) => existsSync(join(d, "policy.json")) && !isTrustedProject(d)) ?? null;
|
|
97
|
+
}
|
|
40
98
|
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
41
99
|
const isScopebond = (cmd) => typeof cmd === "string" && (/@scopebond\/hook|scopebond-hook(\s|$)/.test(cmd) ||
|
|
42
|
-
// the user-level absolute form: `<node> <…/cli.js> claude|cursor`
|
|
43
|
-
/cli\.js["']?\s+(claude|cursor)\s*$/.test(cmd) ||
|
|
44
|
-
/(^|["\s])scopebond(["'\s]).*\b(claude|cursor)\b/.test(cmd));
|
|
100
|
+
// the user-level absolute form: `<node> <…/cli.js> claude|cursor|codex`
|
|
101
|
+
/cli\.js["']?\s+(claude|cursor|codex)\s*$/.test(cmd) ||
|
|
102
|
+
/(^|["\s])scopebond(["'\s]).*\b(claude|cursor|codex)\b/.test(cmd));
|
|
45
103
|
const entryMatches = (e) => isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));
|
|
104
|
+
/** Read an agent's JSON config for merging. A missing file is an empty config; a file
|
|
105
|
+
* that exists but does not parse as a JSON object is an error — rewriting it would
|
|
106
|
+
* silently delete the user's other settings, so the installer stops instead. */
|
|
107
|
+
export function readHarnessConfig(file) {
|
|
108
|
+
if (!existsSync(file))
|
|
109
|
+
return {};
|
|
110
|
+
const text = readFileSync(file, "utf8");
|
|
111
|
+
if (!text.trim())
|
|
112
|
+
return {};
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(text);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw new Error(`${file} is not valid JSON (${error.message}); fix or move it, then run this again — it was left unchanged`);
|
|
119
|
+
}
|
|
120
|
+
if (!isRecord(parsed))
|
|
121
|
+
throw new Error(`${file} is not a JSON object; fix or move it, then run this again — it was left unchanged`);
|
|
122
|
+
return parsed;
|
|
123
|
+
}
|
|
46
124
|
/** Write (idempotently) a harness hook entry pointing at an explicit command string.
|
|
47
125
|
* Shared by the project installer (npx command) and the user installer (absolute path). */
|
|
48
126
|
export function writeHarnessConfig(file, harness, command) {
|
|
127
|
+
const config = readHarnessConfig(file);
|
|
49
128
|
mkdirSync(dirname(file), { recursive: true });
|
|
50
|
-
let config = {};
|
|
51
|
-
if (existsSync(file)) {
|
|
52
|
-
try {
|
|
53
|
-
const p = JSON.parse(readFileSync(file, "utf8"));
|
|
54
|
-
if (isRecord(p))
|
|
55
|
-
config = p;
|
|
56
|
-
}
|
|
57
|
-
catch { /* start fresh on unreadable */ }
|
|
58
|
-
}
|
|
59
129
|
const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});
|
|
60
130
|
if (harness === "cursor") {
|
|
61
131
|
config.version = config.version ?? 1;
|
|
@@ -72,7 +142,11 @@ export function writeHarnessConfig(file, harness, command) {
|
|
|
72
142
|
else {
|
|
73
143
|
const list = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : (hooks.PreToolUse = []);
|
|
74
144
|
const at = list.findIndex(entryMatches);
|
|
75
|
-
|
|
145
|
+
// Codex matchers are regular expressions; omitting the matcher means every tool.
|
|
146
|
+
// A literal "*" is not a valid regular expression and causes Codex to skip the group.
|
|
147
|
+
const entry = harness === "codex"
|
|
148
|
+
? { hooks: [{ type: "command", command, timeout: 30, statusMessage: "Checking this action with Scopebond" }] }
|
|
149
|
+
: { matcher: "*", hooks: [{ type: "command", command }] };
|
|
76
150
|
if (at >= 0)
|
|
77
151
|
list[at] = entry;
|
|
78
152
|
else
|
|
@@ -88,12 +162,11 @@ export function removeHarnessConfig(file) {
|
|
|
88
162
|
return false;
|
|
89
163
|
let config;
|
|
90
164
|
try {
|
|
91
|
-
|
|
92
|
-
config = isRecord(p) ? p : {};
|
|
165
|
+
config = readHarnessConfig(file);
|
|
93
166
|
}
|
|
94
167
|
catch {
|
|
95
168
|
return true;
|
|
96
|
-
}
|
|
169
|
+
} // unreadable: leave it untouched
|
|
97
170
|
const hooks = isRecord(config.hooks) ? config.hooks : {};
|
|
98
171
|
for (const [event, value] of Object.entries(hooks)) {
|
|
99
172
|
if (Array.isArray(value))
|
|
@@ -106,6 +179,10 @@ export function removeHarnessConfig(file) {
|
|
|
106
179
|
export function cursorDetected() {
|
|
107
180
|
return existsSync(join(homedir(), ".cursor"));
|
|
108
181
|
}
|
|
182
|
+
/** Detect a Codex install by its user config directory. */
|
|
183
|
+
export function codexDetected() {
|
|
184
|
+
return existsSync(join(homedir(), ".codex"));
|
|
185
|
+
}
|
|
109
186
|
/** The absolute command a user-level harness entry runs: the current Node executable
|
|
110
187
|
* and the absolute path to this CLI, so it does not depend on PATH. */
|
|
111
188
|
export function absoluteHookCommand(cliPath, harness) {
|
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). A project-local `.scopebond`\n// still wins when present, so per-project policies keep working.\n\nimport { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nexport type Harness = \"claude\" | \"cursor\";\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 return harness === \"cursor\" ? join(homedir(), \".cursor\", \"hooks.json\") : join(homedir(), \".claude\", \"settings.json\");\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. */\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 for (const dir of candidates) {\n if (existsSync(join(dir, \"policy.json\"))) return dir;\n }\n const home = userHome();\n if (existsSync(join(home, \"policy.json\"))) return home;\n // Nothing scaffolded yet: prefer the payload project, else the user home.\n return candidates[0] ?? home;\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`\n /cli\\.js[\"']?\\s+(claude|cursor)\\s*$/.test(cmd) ||\n /(^|[\"\\s])scopebond([\"'\\s]).*\\b(claude|cursor)\\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/** 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 mkdirSync(dirname(file), { recursive: true });\n let config: Record<string, unknown> = {};\n if (existsSync(file)) { try { const p = JSON.parse(readFileSync(file, \"utf8\")); if (isRecord(p)) config = p; } catch { /* start fresh on unreadable */ } }\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 const entry = { 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 { const p = JSON.parse(readFileSync(file, \"utf8\")); config = isRecord(p) ? p : {}; } catch { return true; }\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/** 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":"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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"]}
|
package/dist/map.d.ts
CHANGED
|
@@ -4,9 +4,12 @@ export interface NormalizedIntent {
|
|
|
4
4
|
}
|
|
5
5
|
export interface Mapped {
|
|
6
6
|
intent: NormalizedIntent;
|
|
7
|
-
/** false when no taxonomy type applies (an unknown tool
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
/** false when no taxonomy type applies (an unknown tool) or when the command's
|
|
8
|
+
* effect cannot be judged from its text (`patch`, `git apply`, `tar x`): emitted to
|
|
9
|
+
* the observation path (not_evaluated) in normal mode and denied by the closed
|
|
10
|
+
* allowlist in strict mode — never granted. A shell command that could not be
|
|
11
|
+
* parsed, or whose program is only known at run time, is evaluated with an empty
|
|
12
|
+
* program, which the starter policy denies in every mode. */
|
|
10
13
|
evaluated: boolean;
|
|
11
14
|
/** The native tool/event name, for diagnostics. */
|
|
12
15
|
source: string;
|
|
@@ -18,6 +21,10 @@ export interface Mapped {
|
|
|
18
21
|
export declare function fillPushBranch(mapped: Mapped[], branch: string | null | undefined): Mapped[];
|
|
19
22
|
/** Map a Claude Code PreToolUse payload to the normalized actions it represents. */
|
|
20
23
|
export declare function mapClaudeToolUse(input: Record<string, unknown>): Mapped[];
|
|
24
|
+
/** Map an OpenAI Codex PreToolUse payload. Codex reports shell and unified-exec
|
|
25
|
+
* calls as `Bash`, file patches as `apply_patch`, and MCP calls by their native
|
|
26
|
+
* `mcp__server__tool` name. Other local tools remain visible but unevaluated. */
|
|
27
|
+
export declare function mapCodexToolUse(input: Record<string, unknown>): Mapped[];
|
|
21
28
|
/** Map a Cursor hook event to the normalized actions it represents. */
|
|
22
29
|
export declare function mapCursorEvent(event: string, payload: Record<string, unknown>): Mapped[];
|
|
23
30
|
//# sourceMappingURL=map.d.ts.map
|
package/dist/map.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../src/map.ts"],"names":[],"mappings":"
|
|
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;CAChB;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,CAkBxF"}
|