@scopebond/hook 0.2.1 → 0.4.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/dist/init.d.ts CHANGED
@@ -9,6 +9,6 @@ export declare function scaffold(dir: string, opts?: {
9
9
  * lets `connect` finish setup without the user hand-editing JSON.
10
10
  * `cwd` defaults to the current directory (the project root the user runs it in). */
11
11
  export declare function installHarness(harness: "claude" | "cursor", cwd?: string): string;
12
- /** The harness configuration snippet to install the hook. */
12
+ /** The harness configuration snippet to install the hook (version-pinned npx). */
13
13
  export declare function harnessSnippet(harness: "claude" | "cursor"): string;
14
14
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAUA,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,CAAA;CAAE,CAgB9G;AAED;;;sFAGsF;AACtF,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAsBhG;AAED,6DAA6D;AAC7D,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAiBnE"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAWA,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,CAAA;CAAE,CAgB9G;AAED;;;sFAGsF;AACtF,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CA6BhG;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAkBnE"}
package/dist/init.js CHANGED
@@ -6,6 +6,7 @@ import { dirname, join } from "node:path";
6
6
  import { loadOrCreateAttester } from "@scopebond/gateway/node";
7
7
  import { createSigner } from "@scopebond/sdk";
8
8
  import { starterPolicy } from "./runtime.js";
9
+ import { hookCommand } from "./version.js";
9
10
  export function scaffold(dir, opts = {}) {
10
11
  mkdirSync(dir, { recursive: true });
11
12
  const keyPath = join(dir, "agent.key");
@@ -42,40 +43,52 @@ export function installHarness(harness, cwd = process.cwd()) {
42
43
  catch { /* start fresh on unreadable */ }
43
44
  }
44
45
  const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});
45
- const has = (list, cmd) => Array.isArray(list) && list.some((e) => isRecord(e) && (e.command === cmd
46
- || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && h.command === cmd))));
46
+ const cursorCmd = hookCommand("cursor");
47
+ const claudeCmd = hookCommand("claude");
48
+ // Match any existing Scopebond hook entry (pinned or a legacy bare `scopebond-hook`)
49
+ // so re-running never appends a duplicate and an old bare command is replaced.
50
+ const isScopebond = (cmd) => typeof cmd === "string" && /(^|\s)(npx\s+.*)?@scopebond\/hook|(^|\s)scopebond-hook(\s|$)/.test(cmd);
51
+ const entryMatches = (e) => isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));
47
52
  if (harness === "cursor") {
48
53
  config.version = config.version ?? 1;
49
54
  for (const event of ["beforeShellExecution", "beforeMCPExecution", "beforeReadFile", "afterFileEdit"]) {
50
55
  const list = Array.isArray(hooks[event]) ? hooks[event] : (hooks[event] = []);
51
- if (!has(list, "scopebond-hook cursor"))
52
- list.push({ command: "scopebond-hook cursor" });
56
+ const existing = list.findIndex(entryMatches);
57
+ if (existing >= 0)
58
+ list[existing] = { command: cursorCmd };
59
+ else
60
+ list.push({ command: cursorCmd });
53
61
  }
54
62
  }
55
63
  else {
56
64
  const list = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : (hooks.PreToolUse = []);
57
- if (!has(list, "scopebond-hook claude"))
58
- list.push({ matcher: "*", hooks: [{ type: "command", command: "scopebond-hook claude" }] });
65
+ const existing = list.findIndex(entryMatches);
66
+ const entry = { matcher: "*", hooks: [{ type: "command", command: claudeCmd }] };
67
+ if (existing >= 0)
68
+ list[existing] = entry;
69
+ else
70
+ list.push(entry);
59
71
  }
60
72
  writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
61
73
  return file;
62
74
  }
63
- /** The harness configuration snippet to install the hook. */
75
+ /** The harness configuration snippet to install the hook (version-pinned npx). */
64
76
  export function harnessSnippet(harness) {
65
77
  if (harness === "cursor") {
78
+ const cmd = hookCommand("cursor");
66
79
  return JSON.stringify({
67
80
  version: 1,
68
81
  hooks: {
69
- beforeShellExecution: [{ command: "scopebond-hook cursor" }],
70
- beforeMCPExecution: [{ command: "scopebond-hook cursor" }],
71
- beforeReadFile: [{ command: "scopebond-hook cursor" }],
72
- afterFileEdit: [{ command: "scopebond-hook cursor" }],
82
+ beforeShellExecution: [{ command: cmd }],
83
+ beforeMCPExecution: [{ command: cmd }],
84
+ beforeReadFile: [{ command: cmd }],
85
+ afterFileEdit: [{ command: cmd }],
73
86
  },
74
87
  }, null, 2);
75
88
  }
76
89
  return JSON.stringify({
77
90
  hooks: {
78
- PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: "scopebond-hook claude" }] }],
91
+ PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: hookCommand("claude") }] }],
79
92
  },
80
93
  }, null, 2);
81
94
  }
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\";\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\", 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\") : join(cwd, \".claude\", \"settings.json\");\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 const has = (list: unknown, cmd: string): boolean =>\n Array.isArray(list) && list.some((e) => isRecord(e) && (e.command === cmd\n || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && h.command === cmd))));\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 if (!has(list, \"scopebond-hook cursor\")) list.push({ command: \"scopebond-hook cursor\" });\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 if (!has(list, \"scopebond-hook claude\")) list.push({ matcher: \"*\", hooks: [{ type: \"command\", command: \"scopebond-hook claude\" }] });\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** The harness configuration snippet to install the hook. */\nexport function harnessSnippet(harness: \"claude\" | \"cursor\"): string {\n if (harness === \"cursor\") {\n return JSON.stringify({\n version: 1,\n hooks: {\n beforeShellExecution: [{ command: \"scopebond-hook cursor\" }],\n beforeMCPExecution: [{ command: \"scopebond-hook cursor\" }],\n beforeReadFile: [{ command: \"scopebond-hook cursor\" }],\n afterFileEdit: [{ command: \"scopebond-hook cursor\" }],\n },\n }, null, 2);\n }\n return JSON.stringify({\n hooks: {\n PreToolUse: [{ matcher: \"*\", hooks: [{ type: \"command\", command: \"scopebond-hook 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 { starterPolicy } from \"./runtime.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\", 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\") : join(cwd, \".claude\", \"settings.json\");\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 const cursorCmd = hookCommand(\"cursor\");\n const claudeCmd = hookCommand(\"claude\");\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 const entry = { 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\"): 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 return JSON.stringify({\n hooks: {\n PreToolUse: [{ matcher: \"*\", hooks: [{ type: \"command\", command: hookCommand(\"claude\") }] }],\n },\n }, null, 2);\n}\n"]}
@@ -0,0 +1,26 @@
1
+ export type Harness = "claude" | "cursor";
2
+ /** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */
3
+ export declare function userHome(): string;
4
+ /** The user-level agent config file for a harness. `HOME`/`USERPROFILE` (via homedir)
5
+ * roots it; SCOPEBOND_HOME does not move the agent's own config. */
6
+ export declare function userHarnessFile(harness: Harness): string;
7
+ /** Resolve the config dir for a hook event, most specific first:
8
+ * explicit override → the payload's project → $CLAUDE_PROJECT_DIR → the user home.
9
+ * A project dir counts only when it has been scaffolded (a policy.json), so a bare
10
+ * working directory never shadows the user-level install. */
11
+ export declare function resolveConfigDir(payloadCwd: string | undefined): string;
12
+ /** Write (idempotently) a harness hook entry pointing at an explicit command string.
13
+ * Shared by the project installer (npx command) and the user installer (absolute path). */
14
+ export declare function writeHarnessConfig(file: string, harness: Harness, command: string): string;
15
+ /** Remove any Scopebond hook entry from a user-level harness config, leaving the rest
16
+ * intact. Returns true if the file existed. */
17
+ export declare function removeHarnessConfig(file: string): boolean;
18
+ /** Detect a Cursor install by its user config directory. */
19
+ export declare function cursorDetected(): boolean;
20
+ /** The absolute command a user-level harness entry runs: the current Node executable
21
+ * and the absolute path to this CLI, so it does not depend on PATH. */
22
+ export declare function absoluteHookCommand(cliPath: string, harness: Harness): string;
23
+ export declare function isHarnessConfigured(file: string): boolean;
24
+ /** Remove the user-level home (keys, policy, receipts). */
25
+ export declare function purgeHome(): void;
26
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1C,gFAAgF;AAChF,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;qEACqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAExD;AAED;;;8DAG8D;AAC9D,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYvE;AAaD;4FAC4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAqB1F;AAED;gDACgD;AAChD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzD;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED;wEACwE;AACxE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAG7E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOzD;AAED,2DAA2D;AAC3D,wBAAgB,SAAS,IAAI,IAAI,CAEhC"}
@@ -0,0 +1,132 @@
1
+ // SB112 — the user-level installer. One install per developer machine (not per repo):
2
+ // keys and a starter policy live in a user-level home (~/.scopebond, override
3
+ // SCOPEBOND_HOME), and the hook is registered by absolute path in the user-level agent
4
+ // config (~/.claude/settings.json, ~/.cursor/hooks.json). A project-local `.scopebond`
5
+ // still wins when present, so per-project policies keep working.
6
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+ /** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */
10
+ export function userHome() {
11
+ return process.env.SCOPEBOND_HOME ?? join(homedir(), ".scopebond");
12
+ }
13
+ /** The user-level agent config file for a harness. `HOME`/`USERPROFILE` (via homedir)
14
+ * roots it; SCOPEBOND_HOME does not move the agent's own config. */
15
+ export function userHarnessFile(harness) {
16
+ return harness === "cursor" ? join(homedir(), ".cursor", "hooks.json") : join(homedir(), ".claude", "settings.json");
17
+ }
18
+ /** Resolve the config dir for a hook event, most specific first:
19
+ * explicit override → the payload's project → $CLAUDE_PROJECT_DIR → the user home.
20
+ * 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. */
22
+ export function resolveConfigDir(payloadCwd) {
23
+ if (process.env.SCOPEBOND_HOOK_DIR)
24
+ return process.env.SCOPEBOND_HOOK_DIR;
25
+ const candidates = [];
26
+ if (payloadCwd)
27
+ candidates.push(join(payloadCwd, ".scopebond"));
28
+ if (process.env.CLAUDE_PROJECT_DIR)
29
+ candidates.push(join(process.env.CLAUDE_PROJECT_DIR, ".scopebond"));
30
+ for (const dir of candidates) {
31
+ if (existsSync(join(dir, "policy.json")))
32
+ return dir;
33
+ }
34
+ const home = userHome();
35
+ if (existsSync(join(home, "policy.json")))
36
+ return home;
37
+ // Nothing scaffolded yet: prefer the payload project, else the user home.
38
+ return candidates[0] ?? home;
39
+ }
40
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
41
+ 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));
45
+ const entryMatches = (e) => isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));
46
+ /** Write (idempotently) a harness hook entry pointing at an explicit command string.
47
+ * Shared by the project installer (npx command) and the user installer (absolute path). */
48
+ export function writeHarnessConfig(file, harness, command) {
49
+ 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
+ const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});
60
+ if (harness === "cursor") {
61
+ config.version = config.version ?? 1;
62
+ for (const event of ["beforeShellExecution", "beforeMCPExecution", "beforeReadFile", "afterFileEdit"]) {
63
+ const list = Array.isArray(hooks[event]) ? hooks[event] : (hooks[event] = []);
64
+ const at = list.findIndex(entryMatches);
65
+ const entry = { command: command.replace(/\bclaude$/, "cursor") };
66
+ if (at >= 0)
67
+ list[at] = entry;
68
+ else
69
+ list.push(entry);
70
+ }
71
+ }
72
+ else {
73
+ const list = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : (hooks.PreToolUse = []);
74
+ const at = list.findIndex(entryMatches);
75
+ const entry = { matcher: "*", hooks: [{ type: "command", command }] };
76
+ if (at >= 0)
77
+ list[at] = entry;
78
+ else
79
+ list.push(entry);
80
+ }
81
+ writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
82
+ return file;
83
+ }
84
+ /** Remove any Scopebond hook entry from a user-level harness config, leaving the rest
85
+ * intact. Returns true if the file existed. */
86
+ export function removeHarnessConfig(file) {
87
+ if (!existsSync(file))
88
+ return false;
89
+ let config;
90
+ try {
91
+ const p = JSON.parse(readFileSync(file, "utf8"));
92
+ config = isRecord(p) ? p : {};
93
+ }
94
+ catch {
95
+ return true;
96
+ }
97
+ const hooks = isRecord(config.hooks) ? config.hooks : {};
98
+ for (const [event, value] of Object.entries(hooks)) {
99
+ if (Array.isArray(value))
100
+ hooks[event] = value.filter((e) => !entryMatches(e));
101
+ }
102
+ writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
103
+ return true;
104
+ }
105
+ /** Detect a Cursor install by its user config directory. */
106
+ export function cursorDetected() {
107
+ return existsSync(join(homedir(), ".cursor"));
108
+ }
109
+ /** The absolute command a user-level harness entry runs: the current Node executable
110
+ * and the absolute path to this CLI, so it does not depend on PATH. */
111
+ export function absoluteHookCommand(cliPath, harness) {
112
+ const quote = (s) => (/\s/.test(s) ? `"${s}"` : s);
113
+ return `${quote(process.execPath)} ${quote(cliPath)} ${harness}`;
114
+ }
115
+ export function isHarnessConfigured(file) {
116
+ if (!existsSync(file))
117
+ return false;
118
+ try {
119
+ const p = JSON.parse(readFileSync(file, "utf8"));
120
+ if (!isRecord(p) || !isRecord(p.hooks))
121
+ return false;
122
+ return Object.values(p.hooks).some((v) => Array.isArray(v) && v.some(entryMatches));
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ /** Remove the user-level home (keys, policy, receipts). */
129
+ export function purgeHome() {
130
+ rmSync(userHome(), { recursive: true, force: true });
131
+ }
132
+ //# sourceMappingURL=install.js.map
@@ -0,0 +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"]}
package/dist/map.d.ts CHANGED
@@ -4,14 +4,20 @@ export interface NormalizedIntent {
4
4
  }
5
5
  export interface Mapped {
6
6
  intent: NormalizedIntent;
7
- /** false when no taxonomy type applies: emitted as tool.<name> and routed to the
8
- * observation path (not_evaluated), never granted. */
7
+ /** false when no taxonomy type applies (an unknown tool, or a shell command that
8
+ * could not be parsed): emitted to the observation path (not_evaluated) in normal
9
+ * mode and denied by the closed allowlist in strict mode — never granted. */
9
10
  evaluated: boolean;
10
11
  /** The native tool/event name, for diagnostics. */
11
12
  source: string;
12
13
  }
13
- /** Map a Claude Code PreToolUse payload to a normalized taxonomy action. */
14
- export declare function mapClaudeToolUse(input: Record<string, unknown>): Mapped;
15
- /** Map a Cursor hook event to a normalized taxonomy action. */
16
- export declare function mapCursorEvent(event: string, payload: Record<string, unknown>): Mapped;
14
+ /** A bare `git push` (no ref) pushes the current branch. The mapper cannot know it,
15
+ * so the runtime resolves it and fills it in here, before evaluation — otherwise the
16
+ * starter policy's ref bound would fail-closed on every plain push. Only git.push
17
+ * intents with no ref are touched. */
18
+ export declare function fillPushBranch(mapped: Mapped[], branch: string | null | undefined): Mapped[];
19
+ /** Map a Claude Code PreToolUse payload to the normalized actions it represents. */
20
+ export declare function mapClaudeToolUse(input: Record<string, unknown>): Mapped[];
21
+ /** Map a Cursor hook event to the normalized actions it represents. */
22
+ export declare function mapCursorEvent(event: string, payload: Record<string, unknown>): Mapped[];
17
23
  //# 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":"AAMA,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;2DACuD;IACvD,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;CAChB;AAqDD,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAkBvE;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAkBtF"}
1
+ {"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../src/map.ts"],"names":[],"mappings":"AAYA,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;;kFAE8E;IAC9E,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;CAChB;AAuGD;;;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,CAuBzE;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAkBxF"}
package/dist/map.js CHANGED
@@ -1,46 +1,96 @@
1
- // The mapper: a pure function from a coding agent's native tool call to a
2
- // normalized Scopebond action (Action Taxonomy v1). This is the connector's core
3
- // — deterministic and side-effect free, so it can be conformance-tested directly.
4
- import { digest, redactCommand } from "./minimize.js";
1
+ // The mapper: a pure function from a coding agent's native tool call to the
2
+ // normalized Scopebond actions (Action Taxonomy v1) it represents. Deterministic
3
+ // and side-effect free, so it can be conformance-tested directly.
4
+ //
5
+ // A shell tool call can carry several commands (`a && b`, `$(c)`, `bash -c '…'`);
6
+ // the mapper decomposes it and returns one intent per simple command, so the
7
+ // runtime evaluates every one and denies if any is out of policy. Non-shell tools
8
+ // map to a single intent (a one-element array).
9
+ import { digest, redactCommand, scrubParam, scrubSecrets } from "./minimize.js";
10
+ import { decomposeShell, parseGitPush } from "./shell.js";
11
+ const basename = (t) => t.replace(/^.*[\\/]/, "");
12
+ // Normalize Windows backslash separators to "/" so a path guard written with "/"
13
+ // (the hook's own .scopebond/**, .claude/settings*, *.key rules) cannot be bypassed
14
+ // on Windows by a backslash path. The taxonomy path space is "/"-separated.
15
+ const normPath = (s) => s.replace(/\\/g, "/");
5
16
  const rel = (value, cwd) => {
6
- const s = String(value ?? "");
7
- if (cwd && s.startsWith(cwd))
8
- return s.slice(cwd.length).replace(/^[\\/]+/, "");
17
+ const s = normPath(String(value ?? ""));
18
+ const c = cwd ? normPath(cwd) : undefined;
19
+ if (c && s.startsWith(c))
20
+ return s.slice(c.length).replace(/^\/+/, "");
9
21
  return s;
10
22
  };
11
- /** The invoked program's basename, after stripping sudo and `env VAR=val` prefixes. */
12
- function programOf(command) {
13
- const stripped = command.trim().replace(/^sudo\s+/, "").replace(/^(?:env\s+\S+=\S+\s+)+/, "");
14
- const first = stripped.split(/\s+/)[0] ?? "";
15
- return first.replace(/^.*[\\/]/, "");
16
- }
17
- // Best-effort `git push [--force|-f] [remote] [ref]` parse. Ambiguous pushes omit
18
- // the ref, which a ref bound then denies (fail closed).
19
- function parseGitPush(command) {
20
- const t = command.trim().replace(/^sudo\s+/, "");
21
- if (!/^git\s+push(\s|$)/.test(t))
22
- return null;
23
- const force = /(?:^|\s)(?:--force\b|--force-with-lease\b|-f\b)/.test(t);
24
- const rest = t.replace(/^git\s+push\b/, "").trim();
25
- const positional = rest.split(/\s+/).filter((a) => a && !a.startsWith("-"));
26
- const params = { force };
27
- if (positional[0])
28
- params.remote = positional[0];
29
- if (positional[1])
30
- params.ref = positional[1].replace(/^[^:]*:/, ""); // src:dst → dst
31
- return params;
23
+ // Shell programs that READ a file given as an operand. A read of the signing keys
24
+ // or a secret file through the shell (`cat .scopebond/attester.key`) must reach the
25
+ // same file.read guard as the Read tool, or the cooperative protection is a fiction.
26
+ const READERS = new Set([
27
+ "cat", "tac", "less", "more", "head", "tail", "nl", "od", "xxd", "hexdump",
28
+ "strings", "base64", "bat", "type", "get-content", "gc",
29
+ ]);
30
+ /** Derive the additional file.read / file.write intents a simple shell command
31
+ * implies: operands of a reader program, and the targets of `>`/`>>` redirections.
32
+ * These flow through the same protect-read / protect-write clauses as the native
33
+ * file tools. A false positive (a non-protected operand) is a harmless extra
34
+ * receipt that the starter policy allows. */
35
+ function fileOpsFromShell(sc, cwd) {
36
+ const ops = [];
37
+ if (READERS.has(sc.program.toLowerCase())) {
38
+ for (const t of sc.argv) {
39
+ if (t.startsWith("-") || /^\d+$/.test(t))
40
+ continue; // skip flags and flag values (head -n 5)
41
+ ops.push({ intent: { action_type: "file.read", params: { path: rel(scrubParam(t), cwd) } }, evaluated: true, source: "shell" });
42
+ }
43
+ }
44
+ for (let i = 0; i < sc.argv.length; i++) {
45
+ // `> file`, `>>file`, `2>file`, `&>file` — the token after the redirection op is a write target.
46
+ const m = /^(?:\d|&)?(>>?)(.*)$/.exec(sc.argv[i]);
47
+ if (!m)
48
+ continue;
49
+ const target = m[2] || sc.argv[i + 1];
50
+ if (target && !target.startsWith("-") && !/^(?:\d|&)?>>?/.test(target))
51
+ ops.push({ intent: { action_type: "file.write", params: { path: rel(scrubParam(target), cwd) } }, evaluated: true, source: "shell" });
52
+ }
53
+ return ops;
32
54
  }
33
- function mapShell(command, cwd) {
34
- const push = parseGitPush(command);
35
- if (push)
36
- return { intent: { action_type: "git.push", params: push }, evaluated: true, source: "shell" };
37
- return {
55
+ /** Map one parsed simple command to the intents it implies: the git.push or
56
+ * shell.exec itself, plus any file reads/writes it performs. An opaque
57
+ * (unparseable) command becomes a single un-evaluated shell.exec so it fails closed. */
58
+ function mapSimpleCommand(sc, cwd) {
59
+ if (sc.opaque) {
60
+ return [{
61
+ intent: { action_type: "shell.exec", params: { command: redactCommand(sc.raw), program: "", ...(cwd ? { cwd } : {}) } },
62
+ evaluated: false, source: "shell",
63
+ }];
64
+ }
65
+ const push = parseGitPush(sc);
66
+ if (push) {
67
+ const params = { force: push.force };
68
+ if (push.remote !== undefined)
69
+ params.remote = scrubParam(push.remote);
70
+ if (push.ref !== undefined)
71
+ params.ref = scrubParam(push.ref);
72
+ return [{ intent: { action_type: "git.push", params }, evaluated: true, source: "shell" }];
73
+ }
74
+ const exec = {
38
75
  intent: {
39
76
  action_type: "shell.exec",
40
- params: { command: redactCommand(command), program: programOf(command), ...(cwd ? { cwd } : {}) },
77
+ // Scrub before storing: the raw command through the blob-aware scrubber, and
78
+ // the whole first token BEFORE taking its basename, so a bare-secret command
79
+ // containing "/" cannot leak a path-fragment as the program.
80
+ params: { command: redactCommand(sc.raw), program: basename(scrubSecrets(sc.programRaw)), ...(cwd ? { cwd } : {}) },
41
81
  },
42
82
  evaluated: true, source: "shell",
43
83
  };
84
+ return [exec, ...fileOpsFromShell(sc, cwd)];
85
+ }
86
+ /** Decompose a shell command into the intents it will run. An empty command yields
87
+ * a single un-evaluated placeholder (nothing to grant). */
88
+ function mapShell(command, cwd) {
89
+ const commands = decomposeShell(command);
90
+ if (commands.length === 0) {
91
+ return [{ intent: { action_type: "shell.exec", params: { command: redactCommand(command), program: "" } }, evaluated: false, source: "shell" }];
92
+ }
93
+ return commands.flatMap((sc) => mapSimpleCommand(sc, cwd));
44
94
  }
45
95
  function parseMcpName(name) {
46
96
  if (!name.startsWith("mcp__"))
@@ -51,37 +101,54 @@ function parseMcpName(name) {
51
101
  return { server: parts[1], tool: parts.slice(2).join("__") };
52
102
  }
53
103
  function splitUrl(url) {
104
+ // The query string is dropped; a token in the path or an unparseable URL is scrubbed.
54
105
  try {
55
106
  const u = new URL(url);
56
- return { host: u.host, path: u.pathname };
107
+ return { host: u.host, path: scrubParam(u.pathname) };
57
108
  }
58
109
  catch {
59
- return { host: url, path: "" };
110
+ return { host: scrubParam(url), path: "" };
60
111
  }
61
112
  }
62
- /** Map a Claude Code PreToolUse payload to a normalized taxonomy action. */
113
+ const one = (intent, evaluated, source) => [{ intent, evaluated, source }];
114
+ /** A bare `git push` (no ref) pushes the current branch. The mapper cannot know it,
115
+ * so the runtime resolves it and fills it in here, before evaluation — otherwise the
116
+ * starter policy's ref bound would fail-closed on every plain push. Only git.push
117
+ * intents with no ref are touched. */
118
+ export function fillPushBranch(mapped, branch) {
119
+ if (!branch)
120
+ return mapped;
121
+ return mapped.map((m) => m.intent.action_type === "git.push" && m.intent.params.ref === undefined
122
+ ? { ...m, intent: { ...m.intent, params: { ...m.intent.params, ref: scrubParam(branch) } } }
123
+ : m);
124
+ }
125
+ /** Map a Claude Code PreToolUse payload to the normalized actions it represents. */
63
126
  export function mapClaudeToolUse(input) {
64
127
  const name = String(input?.tool_name ?? "");
65
128
  const ti = (input?.tool_input ?? {});
66
129
  const cwd = input?.cwd ? String(input.cwd) : undefined;
67
- if (name === "Bash")
130
+ // Every shell-executing tool decomposes the same way. Claude Code exposes Bash;
131
+ // some hosts/agents expose PowerShell or a generic Shell tool — mapping only Bash
132
+ // let a PowerShell command (e.g. `Remove-Item -Recurse -Force .`) fall through to
133
+ // an un-evaluated tool.<name> and be allowed.
134
+ if (name === "Bash" || name === "PowerShell" || name === "Shell")
68
135
  return mapShell(String(ti.command ?? ""), cwd);
69
136
  if (name === "Write" || name === "Edit" || name === "MultiEdit")
70
- return { intent: { action_type: "file.write", params: { path: rel(ti.file_path, cwd) } }, evaluated: true, source: name };
137
+ return one({ action_type: "file.write", params: { path: rel(ti.file_path, cwd) } }, true, name);
71
138
  if (name === "NotebookEdit")
72
- return { intent: { action_type: "file.write", params: { path: rel(ti.notebook_path ?? ti.file_path, cwd) } }, evaluated: true, source: name };
139
+ return one({ action_type: "file.write", params: { path: rel(ti.notebook_path ?? ti.file_path, cwd) } }, true, name);
73
140
  if (name === "Read")
74
- return { intent: { action_type: "file.read", params: { path: rel(ti.file_path, cwd) } }, evaluated: true, source: name };
141
+ return one({ action_type: "file.read", params: { path: rel(ti.file_path, cwd) } }, true, name);
75
142
  if (name === "WebFetch") {
76
143
  const { host, path } = splitUrl(String(ti.url ?? ""));
77
- return { intent: { action_type: "net.fetch", params: { host, path, method: "GET" } }, evaluated: true, source: name };
144
+ return one({ action_type: "net.fetch", params: { host, path, method: "GET" } }, true, name);
78
145
  }
79
146
  const mcp = parseMcpName(name);
80
147
  if (mcp)
81
- return { intent: { action_type: "mcp.tool.call", params: { server: mcp.server, tool: mcp.tool, args_digest: digest(ti) } }, evaluated: true, source: name };
82
- return { intent: { action_type: `tool.${name.toLowerCase()}`, params: {} }, evaluated: false, source: name };
148
+ return one({ action_type: "mcp.tool.call", params: { server: mcp.server, tool: mcp.tool, args_digest: digest(ti) } }, true, name);
149
+ return one({ action_type: `tool.${name.toLowerCase()}`, params: {} }, false, name);
83
150
  }
84
- /** Map a Cursor hook event to a normalized taxonomy action. */
151
+ /** Map a Cursor hook event to the normalized actions it represents. */
85
152
  export function mapCursorEvent(event, payload) {
86
153
  const p = payload ?? {};
87
154
  const cwd = p.cwd ? String(p.cwd) : undefined;
@@ -89,16 +156,16 @@ export function mapCursorEvent(event, payload) {
89
156
  case "beforeShellExecution":
90
157
  return mapShell(String(p.command ?? ""), cwd);
91
158
  case "beforeReadFile":
92
- return { intent: { action_type: "file.read", params: { path: rel(p.path ?? p.file_path, cwd) } }, evaluated: true, source: event };
159
+ return one({ action_type: "file.read", params: { path: rel(p.path ?? p.file_path, cwd) } }, true, event);
93
160
  case "afterFileEdit":
94
- return { intent: { action_type: "file.write", params: { path: rel(p.path ?? p.file_path, cwd) } }, evaluated: true, source: event };
161
+ return one({ action_type: "file.write", params: { path: rel(p.path ?? p.file_path, cwd) } }, true, event);
95
162
  case "beforeMCPExecution": {
96
163
  const server = String(p.server ?? p.server_name ?? "");
97
164
  const tool = String(p.tool ?? p.tool_name ?? "");
98
- return { intent: { action_type: "mcp.tool.call", params: { server, tool, args_digest: digest(p.args ?? p.arguments ?? {}) } }, evaluated: true, source: event };
165
+ return one({ action_type: "mcp.tool.call", params: { server, tool, args_digest: digest(p.args ?? p.arguments ?? {}) } }, true, event);
99
166
  }
100
167
  default:
101
- return { intent: { action_type: `tool.${event.toLowerCase()}`, params: {} }, evaluated: false, source: event };
168
+ return one({ action_type: `tool.${event.toLowerCase()}`, params: {} }, false, event);
102
169
  }
103
170
  }
104
171
  //# sourceMappingURL=map.js.map