@scopebond/hook 0.3.0 → 0.5.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 +54 -18
- package/dist/cli.js +191 -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 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- 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 +17 -2
- package/dist/init.js.map +1 -1
- package/dist/install.d.ts +28 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +144 -0
- package/dist/install.js.map +1 -0
- package/dist/map.d.ts +4 -0
- package/dist/map.d.ts.map +1 -1
- package/dist/map.js +101 -15
- package/dist/map.js.map +1 -1
- package/dist/runtime.js +6 -6
- package/dist/runtime.js.map +1 -1
- package/dist/version.d.ts +6 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -1
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -1,26 +1,60 @@
|
|
|
1
1
|
# @scopebond/hook
|
|
2
2
|
|
|
3
|
-
The Scopebond connector for **Claude Code** and **
|
|
3
|
+
The Scopebond connector for **Claude Code**, **Cursor**, and **OpenAI Codex**. It checks every tool
|
|
4
4
|
call a coding agent makes against your policy *before it runs*, blocks the ones
|
|
5
5
|
outside policy, and records a signed receipt — locally, on the machine, and
|
|
6
6
|
optionally mirrored to your Scopebond Cloud workspace for monitoring.
|
|
7
7
|
|
|
8
|
-
- **
|
|
9
|
-
|
|
10
|
-
- **
|
|
8
|
+
- **What it does:** checks supported agent actions before they run and blocks an
|
|
9
|
+
action when it breaks your policy.
|
|
10
|
+
- **Where it works:** actions the coding agent routes through its tool system
|
|
11
11
|
(shell, file, MCP, web). A process started outside the harness is not covered.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Install once for your machine
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
|
|
16
|
+
npm i -g @scopebond/hook
|
|
17
|
+
scopebond install # detects Cursor and Codex
|
|
18
|
+
scopebond install --codex # set up Codex only
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
`install` sets Scopebond up **once per developer machine**, not per repository: it
|
|
22
|
+
scaffolds a user-level home (`~/.scopebond`, override `SCOPEBOND_HOME`) with a
|
|
23
|
+
signing key, a countersigning key and a starter policy, and registers the hook by
|
|
24
|
+
absolute path in your user-level `~/.claude/settings.json`, `~/.cursor/hooks.json`,
|
|
25
|
+
or `~/.codex/hooks.json`. After Codex setup, open Codex, run `/hooks`, review
|
|
26
|
+
Scopebond, and choose **Trust** once. Every project you open is then governed, and a project-local
|
|
27
|
+
`.scopebond/` still takes precedence when you want a per-repo policy.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
scopebond status # home, which agents are configured, cloud, receipts
|
|
31
|
+
scopebond doctor # node version, config location, cloud reachability
|
|
32
|
+
scopebond log # recent decisions scopebond verify # offline
|
|
33
|
+
scopebond uninstall # remove the hook (add --purge to delete keys too)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Install as a Claude Code plugin
|
|
37
|
+
|
|
38
|
+
Scopebond is also a Claude Code plugin (this repo is a plugin marketplace):
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
/plugin marketplace add avouro-com/scopebond
|
|
42
|
+
/plugin install scopebond
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Per-repository enroll (alternative)
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
npx @scopebond/hook init # Claude Code
|
|
49
|
+
npx @scopebond/hook init --cursor # Cursor
|
|
50
|
+
npx @scopebond/hook init --codex # Codex, then approve it once with /hooks
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`init` scaffolds `.scopebond/` in the current project (a machine signing key, a
|
|
54
|
+
countersigning key, a starter policy — "protect main and production paths" — and a
|
|
55
|
+
`.gitignore` so none of it is committed) and configures `.claude/settings.json`,
|
|
56
|
+
`.cursor/hooks.json`, or `.codex/hooks.json`. Then run one safe command in the agent and see the receipt in
|
|
57
|
+
`.scopebond/receipts.db`.
|
|
24
58
|
|
|
25
59
|
## Connect it to your workspace (optional)
|
|
26
60
|
|
|
@@ -32,11 +66,13 @@ as `scopebond-enrollment.json`, then:
|
|
|
32
66
|
npx @scopebond/hook connect https://<your-workspace> scopebond-enrollment.json
|
|
33
67
|
```
|
|
34
68
|
|
|
69
|
+
In Windows PowerShell, use `npx.cmd` instead of `npx` if script execution policy blocks `npx.ps1`; no execution-policy change is needed. Enrollment registers both the gateway attester and the hook's separate agent signing key with proof of possession, so Cloud can verify authenticated receipts.
|
|
70
|
+
|
|
35
71
|
`connect` does the whole setup in one command: it scaffolds `.scopebond/` if needed,
|
|
36
72
|
enrolls this machine's countersigning key, stores a scoped machine credential in
|
|
37
|
-
`.scopebond/cloud.json` (a secret — never commit it), **and configures
|
|
38
|
-
for you** (it merges the hook into
|
|
39
|
-
already there — pass `--no-install` to skip, or `--
|
|
73
|
+
`.scopebond/cloud.json` (a secret — never commit it), **and configures your coding
|
|
74
|
+
agent for you** (it merges the hook into the agent's settings, preserving anything
|
|
75
|
+
already there — pass `--no-install` to skip, `--cursor` for Cursor, or `--codex` for Codex). The
|
|
40
76
|
enrollment argument can be a file, an inline blob, or JSON on stdin, so the portal
|
|
41
77
|
can hand you a single copy-paste command with nothing to save.
|
|
42
78
|
|
|
@@ -51,8 +87,8 @@ anything still queued — run it on a session-end hook (and set
|
|
|
51
87
|
Each tool call is mapped to a normalized [Action Taxonomy](https://github.com/avouro-com/scopebond)
|
|
52
88
|
action (`shell.exec`, `git.push`, `file.write`, `mcp.tool.call`, …), signed by the
|
|
53
89
|
machine key and decided against your policy by an in-process check-only gateway.
|
|
54
|
-
An allowed action is
|
|
55
|
-
|
|
90
|
+
An allowed action is recorded and left to the coding agent's normal permission
|
|
91
|
+
prompt; the hook never auto-approves it. A denied action is blocked. Unknown tools are
|
|
56
92
|
recorded as *not evaluated* and grant nothing. Anything unexpected fails closed
|
|
57
93
|
(deny) with a repair message.
|
|
58
94
|
|
|
@@ -68,10 +104,10 @@ unmapped tools too — fail-closed coverage for anything the taxonomy does not m
|
|
|
68
104
|
The mapper and runtime are exported for testing and embedding:
|
|
69
105
|
|
|
70
106
|
```js
|
|
71
|
-
import { mapClaudeToolUse, createHookRuntime } from "@scopebond/hook";
|
|
107
|
+
import { mapClaudeToolUse, mapCodexToolUse, createHookRuntime } from "@scopebond/hook";
|
|
72
108
|
```
|
|
73
109
|
|
|
74
|
-
`mapClaudeToolUse`
|
|
110
|
+
`mapClaudeToolUse`, `mapCursorEvent`, and `mapCodexToolUse` are pure functions (native payload →
|
|
75
111
|
normalized action). `createHookRuntime` builds the check-only gateway from a
|
|
76
112
|
policy, a machine key and a local receipt log; pass `cloud: { connection }` (from
|
|
77
113
|
`connectCloud`) to mirror receipts to a workspace.
|
package/dist/cli.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// scopebond-hook claude evaluate a Claude Code PreToolUse call (stdin JSON)
|
|
6
6
|
// scopebond-hook cursor evaluate a Cursor hook event (stdin JSON)
|
|
7
|
-
// scopebond-hook
|
|
8
|
-
// scopebond-hook
|
|
7
|
+
// scopebond-hook codex evaluate a Codex PreToolUse call (stdin JSON)
|
|
8
|
+
// scopebond-hook init [--cursor|--codex] [--no-install]
|
|
9
|
+
// scopebond-hook connect <url> <bundle.json> [--cursor|--codex]
|
|
9
10
|
// enroll with a Cloud workspace and start exporting
|
|
10
11
|
// scopebond-hook log [-n N] show the most recent local receipts
|
|
11
12
|
// scopebond-hook verify verify every local receipt offline against the attester key
|
|
@@ -20,10 +21,13 @@ import { tmpdir } from "node:os";
|
|
|
20
21
|
import { execFileSync } from "node:child_process";
|
|
21
22
|
import { verifyReceipt } from "@scopebond/gateway";
|
|
22
23
|
import { openReceiptStore, loadOrCreateAttester } from "@scopebond/gateway/node";
|
|
23
|
-
import { mapClaudeToolUse, mapCursorEvent, fillPushBranch } from "./map.js";
|
|
24
|
+
import { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from "./map.js";
|
|
24
25
|
import { createHookRuntime } from "./runtime.js";
|
|
25
26
|
import { scaffold, harnessSnippet, installHarness } from "./init.js";
|
|
27
|
+
import { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, } from "./install.js";
|
|
26
28
|
import { connectCloud, loadConnection } from "./cloud.js";
|
|
29
|
+
import { cliCommand, hookVersion } from "./version.js";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
27
31
|
/** The current git branch in `cwd` (best-effort). A bare `git push` pushes it, so
|
|
28
32
|
* the runtime fills it in before evaluating; on failure the ref stays absent and
|
|
29
33
|
* the starter policy fails closed. */
|
|
@@ -90,30 +94,48 @@ function denyClaude(reason) {
|
|
|
90
94
|
process.stderr.write(`Scopebond: ${reason}\n`);
|
|
91
95
|
process.exit(2);
|
|
92
96
|
}
|
|
93
|
-
|
|
97
|
+
/** Codex accepts the structured deny response on a successful hook exit. Keeping
|
|
98
|
+
* exit 0 lets its UI show a completed policy check instead of a failed hook while
|
|
99
|
+
* still preventing the tool call. */
|
|
100
|
+
function denyCodex(reason) {
|
|
101
|
+
process.stdout.write(JSON.stringify({
|
|
102
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
|
|
103
|
+
}) + "\n");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
const harnessName = (harness) => harness === "claude" ? "Claude Code" : harness === "cursor" ? "Cursor" : "Codex";
|
|
107
|
+
const harnessFileName = (harness) => harness === "claude" ? ".claude/settings.json" : harness === "cursor" ? ".cursor/hooks.json" : ".codex/hooks.json";
|
|
108
|
+
const selectedHarness = (args) => args.includes("--codex") ? "codex" : args.includes("--cursor") ? "cursor" : "claude";
|
|
109
|
+
const codexTrustStep = "Open Codex, run `/hooks`, review Scopebond, and choose Trust. Then start a new task.";
|
|
110
|
+
async function runPreToolUse(mapper, deny = denyClaude) {
|
|
94
111
|
let input;
|
|
95
112
|
try {
|
|
96
113
|
input = JSON.parse(readStdin());
|
|
97
114
|
}
|
|
98
115
|
catch {
|
|
99
|
-
|
|
116
|
+
deny("hook received invalid JSON on stdin");
|
|
100
117
|
}
|
|
101
118
|
try {
|
|
102
119
|
const cwd = input?.cwd ? String(input.cwd) : process.cwd();
|
|
103
|
-
const runtime = createHookRuntime(runtimePaths(
|
|
104
|
-
const decision = await runtime.evaluate(fillPushBranch(
|
|
120
|
+
const runtime = createHookRuntime(runtimePaths(resolveConfigDir(cwd)));
|
|
121
|
+
const decision = await runtime.evaluate(fillPushBranch(mapper(input), currentBranch(cwd)));
|
|
105
122
|
await runtime.flush();
|
|
106
123
|
if (decision.decision === "deny")
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// would suppress the user's normal review. Scopebond blocks; it does not approve.
|
|
124
|
+
deny(decision.reason);
|
|
125
|
+
// Stay silent on allow/not_evaluated so the coding agent's normal permission
|
|
126
|
+
// flow remains in charge. Scopebond blocks; it never silently approves.
|
|
111
127
|
process.exit(0);
|
|
112
128
|
}
|
|
113
129
|
catch (error) {
|
|
114
|
-
|
|
130
|
+
deny(`Scopebond hook failed closed: ${error.message}. Repair: run \`${cliCommand("init")}\`.`);
|
|
115
131
|
}
|
|
116
132
|
}
|
|
133
|
+
async function runClaude() {
|
|
134
|
+
await runPreToolUse(mapClaudeToolUse);
|
|
135
|
+
}
|
|
136
|
+
async function runCodex() {
|
|
137
|
+
await runPreToolUse(mapCodexToolUse, denyCodex);
|
|
138
|
+
}
|
|
117
139
|
async function runCursor() {
|
|
118
140
|
let input = {};
|
|
119
141
|
try {
|
|
@@ -125,7 +147,7 @@ async function runCursor() {
|
|
|
125
147
|
let message = "Scopebond hook failed closed";
|
|
126
148
|
try {
|
|
127
149
|
const cwd = input?.cwd ? String(input.cwd) : process.cwd();
|
|
128
|
-
const runtime = createHookRuntime(runtimePaths(
|
|
150
|
+
const runtime = createHookRuntime(runtimePaths(resolveConfigDir(cwd)));
|
|
129
151
|
const decision = await runtime.evaluate(fillPushBranch(mapCursorEvent(event, input), currentBranch(cwd)));
|
|
130
152
|
await runtime.flush();
|
|
131
153
|
// Only an out-of-policy action is denied outright; an allowed or unevaluated
|
|
@@ -135,13 +157,13 @@ async function runCursor() {
|
|
|
135
157
|
}
|
|
136
158
|
catch (error) {
|
|
137
159
|
permission = "deny";
|
|
138
|
-
message = `Scopebond hook failed closed: ${error.message}. Repair: run
|
|
160
|
+
message = `Scopebond hook failed closed: ${error.message}. Repair: run \`${cliCommand("init")}\`.`;
|
|
139
161
|
}
|
|
140
162
|
process.stdout.write(JSON.stringify({ permission, agentMessage: message }) + "\n");
|
|
141
163
|
process.exit(0);
|
|
142
164
|
}
|
|
143
165
|
function runInit(args) {
|
|
144
|
-
const harness = args
|
|
166
|
+
const harness = selectedHarness(args);
|
|
145
167
|
const dir = configDir();
|
|
146
168
|
const { agentKid, policyPath } = scaffold(dir, { force: args.includes("--force") });
|
|
147
169
|
console.log(`Scopebond hook enrolled in ${dir}`);
|
|
@@ -152,16 +174,20 @@ function runInit(args) {
|
|
|
152
174
|
// hand-editing step; --no-install prints the snippet instead.
|
|
153
175
|
if (!args.includes("--no-install")) {
|
|
154
176
|
const file = installHarness(harness);
|
|
155
|
-
console.log(`✓ ${harness
|
|
177
|
+
console.log(`✓ ${harnessName(harness)} configured in ${file}`);
|
|
178
|
+
if (harness === "codex")
|
|
179
|
+
console.log(`\nOne last step: ${codexTrustStep}`);
|
|
156
180
|
}
|
|
157
181
|
else {
|
|
158
|
-
console.log(`Add this to your ${harness
|
|
182
|
+
console.log(`Add this to your ${harnessFileName(harness)}:`);
|
|
159
183
|
console.log(harnessSnippet(harness));
|
|
160
184
|
}
|
|
161
185
|
console.log("");
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
console.log(
|
|
186
|
+
// Print the runnable `npx` form: after `npx @scopebond/hook init` there is no
|
|
187
|
+
// `scopebond-hook` binary on PATH, so a bare `scopebond-hook log` would fail.
|
|
188
|
+
console.log(`Next: run one command in the agent, then \`${cliCommand("log")}\` to see the receipt`);
|
|
189
|
+
console.log(`and \`${cliCommand("verify")}\` to check it offline. Try \`${cliCommand('test "rm -rf /"')}\`.`);
|
|
190
|
+
console.log(`To send receipts to a workspace: ${cliCommand("connect <workspace-url> <enrollment>")}`);
|
|
165
191
|
}
|
|
166
192
|
function decisionOf(payload) {
|
|
167
193
|
const rr = String(payload.realtime_result ?? "");
|
|
@@ -183,7 +209,7 @@ function describeIntent(payload) {
|
|
|
183
209
|
return `${String(intent.action_type ?? "?")}${bits ? ` ${bits}` : ""}`;
|
|
184
210
|
}
|
|
185
211
|
async function runLog(args) {
|
|
186
|
-
const dir =
|
|
212
|
+
const dir = resolveConfigDir(process.cwd());
|
|
187
213
|
const dbPath = join(dir, "receipts.db");
|
|
188
214
|
if (!existsSync(dbPath)) {
|
|
189
215
|
console.log("no receipts yet — run a command in the agent first.");
|
|
@@ -202,10 +228,10 @@ async function runLog(args) {
|
|
|
202
228
|
const p = r.payload;
|
|
203
229
|
console.log(`${String(p.timestamp ?? "")} ${decisionOf(p).padEnd(13)} ${describeIntent(p)}`);
|
|
204
230
|
}
|
|
205
|
-
console.log(`\n${recent.length} of ${all.length} receipt(s). Verify them:
|
|
231
|
+
console.log(`\n${recent.length} of ${all.length} receipt(s). Verify them: ${cliCommand("verify")}`);
|
|
206
232
|
}
|
|
207
233
|
async function runVerify() {
|
|
208
|
-
const dir =
|
|
234
|
+
const dir = resolveConfigDir(process.cwd());
|
|
209
235
|
const dbPath = join(dir, "receipts.db");
|
|
210
236
|
const attesterPath = join(dir, "attester.key");
|
|
211
237
|
if (!existsSync(dbPath) || !existsSync(attesterPath)) {
|
|
@@ -240,7 +266,7 @@ async function runTest(args) {
|
|
|
240
266
|
console.error('usage: scopebond-hook test "<shell command>"');
|
|
241
267
|
process.exit(1);
|
|
242
268
|
}
|
|
243
|
-
const dir =
|
|
269
|
+
const dir = resolveConfigDir(process.cwd());
|
|
244
270
|
if (!existsSync(join(dir, "policy.json"))) {
|
|
245
271
|
console.error("no policy yet — run `scopebond-hook init` first.");
|
|
246
272
|
process.exit(1);
|
|
@@ -272,9 +298,9 @@ async function runConnect(args) {
|
|
|
272
298
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
273
299
|
const url = positional[0];
|
|
274
300
|
const bundleArg = positional[1];
|
|
275
|
-
const harness = args
|
|
301
|
+
const harness = selectedHarness(args);
|
|
276
302
|
if (!url) {
|
|
277
|
-
console.error("usage: scopebond-hook connect <workspace-url> <enrollment> [--cursor] [--no-install]");
|
|
303
|
+
console.error("usage: scopebond-hook connect <workspace-url> <enrollment> [--claude|--cursor|--codex] [--no-install]");
|
|
278
304
|
process.exit(1);
|
|
279
305
|
}
|
|
280
306
|
const dir = configDir();
|
|
@@ -298,10 +324,12 @@ async function runConnect(args) {
|
|
|
298
324
|
// caller opts out. This removes the "paste this snippet" step.
|
|
299
325
|
if (!args.includes("--no-install")) {
|
|
300
326
|
const file = installHarness(harness);
|
|
301
|
-
console.log(`✓ ${harness
|
|
327
|
+
console.log(`✓ ${harnessName(harness)} configured in ${file}`);
|
|
328
|
+
if (harness === "codex")
|
|
329
|
+
console.log(`\nOne last step: ${codexTrustStep}`);
|
|
302
330
|
}
|
|
303
331
|
else {
|
|
304
|
-
console.log(`Add this to your ${harness
|
|
332
|
+
console.log(`Add this to your ${harnessFileName(harness)}:`);
|
|
305
333
|
console.log(harnessSnippet(harness));
|
|
306
334
|
}
|
|
307
335
|
console.log("");
|
|
@@ -313,7 +341,7 @@ async function runConnect(args) {
|
|
|
313
341
|
}
|
|
314
342
|
}
|
|
315
343
|
async function runFlush() {
|
|
316
|
-
const dir =
|
|
344
|
+
const dir = resolveConfigDir(process.cwd());
|
|
317
345
|
if (!loadConnection(dir)) {
|
|
318
346
|
console.error("not connected to a workspace; run `scopebond-hook connect` first");
|
|
319
347
|
process.exit(1);
|
|
@@ -321,8 +349,122 @@ async function runFlush() {
|
|
|
321
349
|
const runtime = createHookRuntime(runtimePaths(dir));
|
|
322
350
|
await runtime.exporter?.flush();
|
|
323
351
|
const status = runtime.exporter?.status();
|
|
352
|
+
runtime.exporter?.stop();
|
|
324
353
|
console.log(`flushed; ${status?.pending ?? 0} receipt(s) still pending${status?.lastError ? ` (last error: ${status.lastError})` : ""}`);
|
|
325
|
-
|
|
354
|
+
// Let pending HTTP handles close normally (forced exit can abort on Windows).
|
|
355
|
+
process.exitCode = status && status.pending > 0 ? 1 : 0;
|
|
356
|
+
}
|
|
357
|
+
/** The absolute path to this CLI file, for registering the hook by absolute path. */
|
|
358
|
+
function cliPath() {
|
|
359
|
+
return fileURLToPath(import.meta.url);
|
|
360
|
+
}
|
|
361
|
+
/** `install` — the once-per-machine, user-level install (SB112). Scaffolds the
|
|
362
|
+
* user home and registers the hook by absolute path in the user-level agent config,
|
|
363
|
+
* so every project a developer opens is governed without a per-repo `init`. */
|
|
364
|
+
function runInstall(args) {
|
|
365
|
+
const dir = userHome();
|
|
366
|
+
const { agentKid, policyPath } = scaffold(dir, { force: args.includes("--force") });
|
|
367
|
+
console.log(`Scopebond installed for this user in ${dir}`);
|
|
368
|
+
console.log(` machine key ${agentKid}`);
|
|
369
|
+
console.log(` policy ${policyPath} (starter — edit the limits)`);
|
|
370
|
+
console.log("");
|
|
371
|
+
const harnesses = args.includes("--codex") ? ["codex"]
|
|
372
|
+
: args.includes("--cursor") ? ["cursor"]
|
|
373
|
+
: args.includes("--claude") ? ["claude"]
|
|
374
|
+
: ["claude", ...(cursorDetected() ? ["cursor"] : []), ...(codexDetected() ? ["codex"] : [])];
|
|
375
|
+
if (!args.includes("--no-install")) {
|
|
376
|
+
for (const h of harnesses) {
|
|
377
|
+
const file = writeHarnessConfig(userHarnessFile(h), h, absoluteHookCommand(cliPath(), h));
|
|
378
|
+
console.log(`✓ ${harnessName(h)} configured in ${file}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
console.log("Add this to your user-level agent config:");
|
|
383
|
+
console.log(harnessSnippet(harnesses[0]));
|
|
384
|
+
}
|
|
385
|
+
if (harnesses.includes("codex"))
|
|
386
|
+
console.log(`\nOne last step for Codex: ${codexTrustStep}`);
|
|
387
|
+
console.log("");
|
|
388
|
+
console.log("A project-local .scopebond still takes precedence when present.");
|
|
389
|
+
console.log(`Check it: ${cliCommand("doctor")} · see decisions: ${cliCommand("log")}`);
|
|
390
|
+
console.log(`To send receipts to a workspace: ${cliCommand("connect <workspace-url> <enrollment>")}`);
|
|
391
|
+
}
|
|
392
|
+
function runStatus() {
|
|
393
|
+
const home = userHome();
|
|
394
|
+
const installed = existsSync(join(home, "policy.json"));
|
|
395
|
+
const claude = isHarnessConfigured(userHarnessFile("claude"));
|
|
396
|
+
const cursor = isHarnessConfigured(userHarnessFile("cursor"));
|
|
397
|
+
const codex = isHarnessConfigured(userHarnessFile("codex"));
|
|
398
|
+
const connected = !!loadConnection(resolveConfigDir(process.cwd()));
|
|
399
|
+
const dbPath = join(resolveConfigDir(process.cwd()), "receipts.db");
|
|
400
|
+
console.log(`Scopebond hook ${hookVersion()}`);
|
|
401
|
+
console.log(` user home ${home} ${installed ? "(installed)" : "(not installed — run `scopebond install`)"}`);
|
|
402
|
+
console.log(` active config ${resolveConfigDir(process.cwd())}`);
|
|
403
|
+
console.log(` Claude Code ${claude ? "configured" : "not configured"}`);
|
|
404
|
+
console.log(` Cursor ${cursor ? "configured" : cursorDetected() ? "detected, not configured" : "not detected"}`);
|
|
405
|
+
console.log(` Codex ${codex ? "configured (approve once with /hooks)" : codexDetected() ? "detected, not configured" : "not detected"}`);
|
|
406
|
+
console.log(` cloud workspace ${connected ? "connected" : "not connected (local only)"}`);
|
|
407
|
+
console.log(` local receipts ${existsSync(dbPath) ? dbPath : "none yet"}`);
|
|
408
|
+
}
|
|
409
|
+
async function runDoctor() {
|
|
410
|
+
const problems = [];
|
|
411
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
412
|
+
const nodeOk = major > 22 || (major === 22 && minor >= 13);
|
|
413
|
+
console.log(`Scopebond doctor`);
|
|
414
|
+
console.log(` node ${process.versions.node} ${nodeOk ? "ok" : "TOO OLD (need >=22.13)"}`);
|
|
415
|
+
if (!nodeOk)
|
|
416
|
+
problems.push("node >=22.13 is required (the Cloud outbox uses node:sqlite)");
|
|
417
|
+
const cli = cliPath();
|
|
418
|
+
console.log(` cli ${cli} ${existsSync(cli) ? "ok" : "MISSING"}`);
|
|
419
|
+
const active = resolveConfigDir(process.cwd());
|
|
420
|
+
const hasPolicy = existsSync(join(active, "policy.json"));
|
|
421
|
+
console.log(` active config ${active} ${hasPolicy ? "ok" : "no policy (run `scopebond install` or `init`)"}`);
|
|
422
|
+
if (!hasPolicy)
|
|
423
|
+
problems.push("no policy found in the active config dir");
|
|
424
|
+
const codex = isHarnessConfigured(userHarnessFile("codex"));
|
|
425
|
+
console.log(` Codex hook ${codex ? "configured" : codexDetected() ? "not configured — run `scopebond install --codex`" : "not detected"}`);
|
|
426
|
+
if (codex)
|
|
427
|
+
console.log(` Codex approval run /hooks in Codex and approve Scopebond once`);
|
|
428
|
+
const connection = loadConnection(active);
|
|
429
|
+
if (!connection) {
|
|
430
|
+
console.log(` cloud not connected (local only) — receipts stay on this machine`);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
let reachable = "unknown";
|
|
434
|
+
try {
|
|
435
|
+
const res = await fetch(new URL("/healthz", connection.url).toString(), { method: "GET" });
|
|
436
|
+
reachable = res.ok ? "reachable" : `unhealthy (${res.status})`;
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
reachable = `unreachable (${error.message})`;
|
|
440
|
+
}
|
|
441
|
+
console.log(` cloud ${connection.url} — ${reachable}`);
|
|
442
|
+
}
|
|
443
|
+
console.log(problems.length ? `\n${problems.length} problem(s): ${problems.join("; ")}` : `\nAll good.`);
|
|
444
|
+
process.exitCode = problems.length ? 1 : 0;
|
|
445
|
+
}
|
|
446
|
+
function runUninstall(args) {
|
|
447
|
+
let removed = 0;
|
|
448
|
+
for (const h of ["claude", "cursor", "codex"]) {
|
|
449
|
+
if (removeHarnessConfig(userHarnessFile(h))) {
|
|
450
|
+
console.log(`✓ removed the Scopebond hook from ${userHarnessFile(h)}`);
|
|
451
|
+
removed++;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (removed === 0)
|
|
455
|
+
console.log("no user-level harness config found.");
|
|
456
|
+
if (args.includes("--purge")) {
|
|
457
|
+
purgeHome();
|
|
458
|
+
console.log(`✓ purged ${userHome()} (keys, policy, receipts)`);
|
|
459
|
+
}
|
|
460
|
+
else
|
|
461
|
+
console.log(`Kept ${userHome()} (keys, policy, receipts). Use --purge to remove it too.`);
|
|
462
|
+
}
|
|
463
|
+
function runLogin() {
|
|
464
|
+
console.log("Device-code login is not available yet.");
|
|
465
|
+
console.log(`For now, connect with a one-time enrollment from your workspace:`);
|
|
466
|
+
console.log(` ${cliCommand("connect <workspace-url> <enrollment>")}`);
|
|
467
|
+
process.exit(0);
|
|
326
468
|
}
|
|
327
469
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
328
470
|
if (cmd === "claude") {
|
|
@@ -331,9 +473,15 @@ if (cmd === "claude") {
|
|
|
331
473
|
else if (cmd === "cursor") {
|
|
332
474
|
await runCursor();
|
|
333
475
|
}
|
|
476
|
+
else if (cmd === "codex") {
|
|
477
|
+
await runCodex();
|
|
478
|
+
}
|
|
334
479
|
else if (cmd === "init") {
|
|
335
480
|
runInit(rest);
|
|
336
481
|
}
|
|
482
|
+
else if (cmd === "install") {
|
|
483
|
+
runInstall(rest);
|
|
484
|
+
}
|
|
337
485
|
else if (cmd === "connect") {
|
|
338
486
|
await runConnect(rest);
|
|
339
487
|
}
|
|
@@ -349,8 +497,20 @@ else if (cmd === "test") {
|
|
|
349
497
|
else if (cmd === "flush") {
|
|
350
498
|
await runFlush();
|
|
351
499
|
}
|
|
500
|
+
else if (cmd === "status") {
|
|
501
|
+
runStatus();
|
|
502
|
+
}
|
|
503
|
+
else if (cmd === "doctor") {
|
|
504
|
+
await runDoctor();
|
|
505
|
+
}
|
|
506
|
+
else if (cmd === "uninstall") {
|
|
507
|
+
runUninstall(rest);
|
|
508
|
+
}
|
|
509
|
+
else if (cmd === "login") {
|
|
510
|
+
runLogin();
|
|
511
|
+
}
|
|
352
512
|
else {
|
|
353
|
-
console.error("usage: scopebond
|
|
513
|
+
console.error("usage: scopebond <claude|cursor|codex|init|install|connect|log|verify|test|flush|status|doctor|uninstall|login> [--cursor] [--claude] [--codex] [--force] [--strict] [--no-install] [--purge]");
|
|
354
514
|
process.exit(1);
|
|
355
515
|
}
|
|
356
516
|
//# sourceMappingURL=cli.js.map
|