@scopebond/hook 0.10.0 → 0.12.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 +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +116 -15
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts +24 -2
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +77 -9
- package/dist/cloud.js.map +1 -1
- package/dist/delivery-report.d.ts +16 -0
- package/dist/delivery-report.d.ts.map +1 -0
- package/dist/delivery-report.js +51 -0
- package/dist/delivery-report.js.map +1 -0
- package/dist/delivery-state.d.ts +30 -0
- package/dist/delivery-state.d.ts.map +1 -0
- package/dist/delivery-state.js +86 -0
- package/dist/delivery-state.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +8 -2
- package/dist/install.js.map +1 -1
- package/dist/managed.d.ts +3 -1
- package/dist/managed.d.ts.map +1 -1
- package/dist/managed.js +50 -4
- package/dist/managed.js.map +1 -1
- package/dist/obs-emitter.d.ts.map +1 -1
- package/dist/obs-emitter.js +2 -2
- package/dist/obs-emitter.js.map +1 -1
- package/dist/policy-sync.d.ts.map +1 -1
- package/dist/policy-sync.js +6 -2
- package/dist/policy-sync.js.map +1 -1
- package/dist/proof.d.ts +1 -1
- package/dist/proof.d.ts.map +1 -1
- package/dist/proof.js +2 -1
- package/dist/proof.js.map +1 -1
- package/dist/recover.d.ts +33 -0
- package/dist/recover.d.ts.map +1 -0
- package/dist/recover.js +201 -0
- package/dist/recover.js.map +1 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +12 -2
- package/dist/runtime.js.map +1 -1
- package/dist/version.d.ts +4 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +5 -5
- package/dist/version.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -200,7 +200,20 @@ portal's **Connect** step (it gives you a one-use enrollment bundle), save it as
|
|
|
200
200
|
npx @scopebond/hook connect https://<your-workspace> scopebond-enrollment.json
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
In Windows PowerShell, use `npx.cmd` instead of `npx` if script execution policy blocks `npx.ps1`; no execution-policy change is needed.
|
|
203
|
+
In Windows PowerShell, use `npx.cmd` instead of `npx` if script execution policy blocks `npx.ps1`; no execution-policy change is needed. Every command the hook prints for you to run uses `npx.cmd` on Windows.
|
|
204
|
+
|
|
205
|
+
### Is it delivering?
|
|
206
|
+
|
|
207
|
+
`status` shows when this computer last delivered records, how many are waiting to send and the
|
|
208
|
+
last problem. Records are never dropped from the queue while they wait: a long outage only makes
|
|
209
|
+
the queue longer. If the workspace refuses this computer's connection (it was revoked, replaced or
|
|
210
|
+
removed), `status` says **NOT DELIVERING** with the time it stopped and the one command that fixes
|
|
211
|
+
it, which is signing in again. `doctor` also checks that the workspace still accepts the connection,
|
|
212
|
+
not only that it is reachable, and fails when it does not.
|
|
213
|
+
|
|
214
|
+
Signing in puts the hook in your user-level agent settings (`~/.claude/settings.json`, `~/.cursor/hooks.json` or `~/.codex/hooks.json`), so every project on this computer is checked; `--project` connects one project instead. Signing in again never rewrites a settings file that already holds the right hook entry.
|
|
215
|
+
|
|
216
|
+
Enrollment registers both the gateway attester and the hook's separate agent signing key with proof of possession, so Cloud can verify authenticated receipts.
|
|
204
217
|
|
|
205
218
|
`connect` does the whole setup in one command: it scaffolds `.scopebond/` if needed,
|
|
206
219
|
enrolls this machine's countersigning key, stores a scoped machine credential in
|
|
@@ -216,6 +229,19 @@ locally and retried if the workspace is unreachable. `npx @scopebond/hook flush`
|
|
|
216
229
|
delivers anything still queued — run it on a session-end hook (and set
|
|
217
230
|
`SCOPEBOND_HOOK_FLUSH_MS=0`) if you want zero per-call latency.
|
|
218
231
|
|
|
232
|
+
**Reconnecting.** Run `login` again from any folder: unless that folder has its own project
|
|
233
|
+
setup, it repairs the connection the hook actually uses (usually the user-level one) instead
|
|
234
|
+
of creating a second one; pass `--project` to set up the folder you are in. If the workspace
|
|
235
|
+
no longer accepts this computer's countersigning key (the computer was replaced or
|
|
236
|
+
disconnected there), `login` replaces the key and keeps the old one in
|
|
237
|
+
`.scopebond/retired-keys/`.
|
|
238
|
+
|
|
239
|
+
**Records signed by an earlier key.** The new connection cannot deliver records the earlier
|
|
240
|
+
key signed, so they leave the delivery queue but stay in the local log.
|
|
241
|
+
`npx @scopebond/hook recover` asks the workspace to accept them, waits while an owner or
|
|
242
|
+
admin approves it there, then sends them; the workspace checks each signature against the
|
|
243
|
+
key it kept and labels the records as recovered.
|
|
244
|
+
|
|
219
245
|
### Rules set by your workspace
|
|
220
246
|
|
|
221
247
|
A connected computer keeps its own rules (`.scopebond/rules.json`) until someone who manages
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAmqBA,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAS7F"}
|
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,8 @@ import { openObservations, describeObservations, observationStatus, stopReasonFr
|
|
|
40
40
|
import { OBSERVATION_DB, ObservationStore } from "./obs-store.js";
|
|
41
41
|
import { loadOrCreateBindingKey } from "./observation.js";
|
|
42
42
|
import { uploadPending } from "./obs-upload.js";
|
|
43
|
-
import { connectCloud, loadConnection } from "./cloud.js";
|
|
43
|
+
import { connectCloud, ingestUrl, loadConnection } from "./cloud.js";
|
|
44
|
+
import { recoverEarlierReceipts } from "./recover.js";
|
|
44
45
|
import { loadPolicyExport, policyBuilds } from "./policy-load.js";
|
|
45
46
|
import { isManaged, readMeta, MANAGED_DOC_FILE } from "./managed.js";
|
|
46
47
|
import { syncIfDue, syncPolicy } from "./policy-sync.js";
|
|
@@ -51,6 +52,8 @@ import { runDispatchCommand } from "./dispatch-cli.js";
|
|
|
51
52
|
import { describeAction } from "./explain.js";
|
|
52
53
|
import { ensureDurableRuntime, pinnedCliPath, isEphemeralPath } from "./runtime-install.js";
|
|
53
54
|
import { cliCommand, hookCommand, hookVersion } from "./version.js";
|
|
55
|
+
import { recordRulesCredential } from "./delivery-state.js";
|
|
56
|
+
import { describeDelivery } from "./delivery-report.js";
|
|
54
57
|
import { computeManifest, renderManifest } from "./capabilities.js";
|
|
55
58
|
import { runProofFixtures, loadProofs, saveProofs, proofPassed, deliverProofReceipts } from "./proof.js";
|
|
56
59
|
import { fileURLToPath } from "node:url";
|
|
@@ -871,11 +874,11 @@ async function runConnect(args) {
|
|
|
871
874
|
const bundleArg = positional[1];
|
|
872
875
|
const harness = selectedHarness(args);
|
|
873
876
|
if (!url) {
|
|
874
|
-
console.error(`usage: ${cliCommand("connect <workspace-url> <enrollment> [--claude|--cursor|--codex] [--no-install]")}`);
|
|
877
|
+
console.error(`usage: ${cliCommand("connect <workspace-url> <enrollment> [--claude|--cursor|--codex] [--no-install] [--project]")}`);
|
|
875
878
|
console.error(enrollmentHelp);
|
|
876
879
|
process.exit(1);
|
|
877
880
|
}
|
|
878
|
-
const dir =
|
|
881
|
+
const dir = connectDir(args);
|
|
879
882
|
// One command sets everything up: scaffold the key, attester and starter policy if
|
|
880
883
|
// they do not exist, then enroll and persist the scoped machine credential. The
|
|
881
884
|
// enrollment can be a file, an inline base64 blob (what the portal hands out) or
|
|
@@ -897,6 +900,10 @@ async function finishConnect(dir, url, bundle, harness, args) {
|
|
|
897
900
|
try {
|
|
898
901
|
const c = await connectCloud(dir, url, bundle);
|
|
899
902
|
console.log(`✓ Connected to ${c.url}`);
|
|
903
|
+
if (c.rotatedFrom)
|
|
904
|
+
console.log(`✓ This computer's earlier key (${c.rotatedFrom}) was no longer accepted, so it was replaced; the old key is kept in ${join(dir, "retired-keys")}`);
|
|
905
|
+
if (c.setAside)
|
|
906
|
+
console.log(` ${c.setAside.toLocaleString()} queued record(s) signed by an earlier key cannot go through this connection. To deliver them, run: ${cliCommand("recover")}`);
|
|
900
907
|
// With a user-level install present, the hook ignores a project policy until it is
|
|
901
908
|
// trusted, and would fall back to the user home, which holds no cloud.json: the agent
|
|
902
909
|
// stays governed, but nothing reaches the workspace. Connecting this project is the
|
|
@@ -915,7 +922,14 @@ async function finishConnect(dir, url, bundle, harness, args) {
|
|
|
915
922
|
console.log(`✓ ${harnessName(harness)} already configured in ${existing}`);
|
|
916
923
|
}
|
|
917
924
|
else if (!args.includes("--no-install")) {
|
|
918
|
-
|
|
925
|
+
// A connection for this computer (the user home, the default) covers every project,
|
|
926
|
+
// so the hook goes into the user-level agent settings. Placing it in the current
|
|
927
|
+
// folder's project settings — what this did before — left every other project
|
|
928
|
+
// unchecked, and a sign-in run from a scratch folder governed only that folder.
|
|
929
|
+
// `--project` (a per-project connection) keeps the project placement.
|
|
930
|
+
const file = dir === userHome()
|
|
931
|
+
? writeHarnessConfig(userHarnessFile(harness), harness, durableHookCommand(harness))
|
|
932
|
+
: placeHook(harness, process.cwd(), undefined).file;
|
|
919
933
|
console.log(`✓ ${harnessName(harness)} configured in ${file}`);
|
|
920
934
|
if (harness === "codex")
|
|
921
935
|
console.log(`\nOne last step: ${codexTrustStep}`);
|
|
@@ -959,6 +973,43 @@ const enrollmentHelp = [
|
|
|
959
973
|
"Each enrollment is single-use and expires soon after it is created; if this one was",
|
|
960
974
|
"used or has expired, create a new one there.",
|
|
961
975
|
].join("\n");
|
|
976
|
+
/** Where `connect` and `login` write. A project someone set up here with `init` (it has a
|
|
977
|
+
* policy) is connected, as before. Otherwise the configuration the hook itself uses from
|
|
978
|
+
* here — usually the user-level install — so reconnecting from any folder repairs the
|
|
979
|
+
* connection that is actually failing instead of creating a second, project-level one
|
|
980
|
+
* beside it. `--project` asks for a new per-project setup explicitly. */
|
|
981
|
+
function connectDir(args) {
|
|
982
|
+
const project = configDir();
|
|
983
|
+
return args.includes("--project") || existsSync(join(project, "policy.json")) ? project : resolveConfigDir(process.cwd());
|
|
984
|
+
}
|
|
985
|
+
/** `recover [--no-wait]`: deliver receipts that an earlier, since-revoked key of this computer
|
|
986
|
+
* signed and the workspace never received, after an owner or admin approves it there. */
|
|
987
|
+
async function runRecover(args) {
|
|
988
|
+
const dir = resolveConfigDir(process.cwd());
|
|
989
|
+
const connection = loadConnection(dir);
|
|
990
|
+
if (!connection) {
|
|
991
|
+
console.error(`not connected to a workspace; run \`${cliCommand("login <workspace-url>")}\` first`);
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
console.log(`Recovering from ${dir}`);
|
|
995
|
+
const result = await recoverEarlierReceipts(dir, connection, {
|
|
996
|
+
log: (line) => console.log(line),
|
|
997
|
+
fetch,
|
|
998
|
+
now: Date.now,
|
|
999
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
1000
|
+
}, { wait: !args.includes("--no-wait"), waitMs: 15 * 60_000, pollMs: 5_000 });
|
|
1001
|
+
if (result.groups) {
|
|
1002
|
+
const parts = [
|
|
1003
|
+
`recovered ${result.accepted.toLocaleString()}`,
|
|
1004
|
+
`already present ${result.duplicates.toLocaleString()}`,
|
|
1005
|
+
`refused ${result.rejected.toLocaleString()}`,
|
|
1006
|
+
...(result.pending ? [`waiting for approval ${result.pending.toLocaleString()}`] : []),
|
|
1007
|
+
...(result.skipped ? [`skipped ${result.skipped.toLocaleString()}`] : []),
|
|
1008
|
+
];
|
|
1009
|
+
console.log(`\nDone: ${parts.join(", ")}.`);
|
|
1010
|
+
}
|
|
1011
|
+
process.exitCode = result.failed || result.pending ? 1 : 0;
|
|
1012
|
+
}
|
|
962
1013
|
async function runFlush() {
|
|
963
1014
|
const dir = resolveConfigDir(process.cwd());
|
|
964
1015
|
if (!loadConnection(dir)) {
|
|
@@ -1180,7 +1231,7 @@ async function runObservations(args) {
|
|
|
1180
1231
|
const emitter = opened.emitter;
|
|
1181
1232
|
try {
|
|
1182
1233
|
if (sub === "flush") {
|
|
1183
|
-
const outcome = await uploadPending(emitter.store, { url: emitter.connection
|
|
1234
|
+
const outcome = await uploadPending(emitter.store, { url: ingestUrl(emitter.connection), credential: emitter.connection.credential, timeoutMs: 10_000 });
|
|
1184
1235
|
const left = emitter.store.pendingSummary().count;
|
|
1185
1236
|
console.log(`${outcome.result}: ${outcome.acknowledged} acknowledged, ${outcome.deferred} deferred, ${outcome.rejected} refused; ${left} still pending${outcome.detail ? ` (${outcome.detail})` : ""}`);
|
|
1186
1237
|
process.exitCode = left > 0 ? 1 : 0;
|
|
@@ -1237,16 +1288,17 @@ function cliPath() {
|
|
|
1237
1288
|
/** `install` — the once-per-machine, user-level install (SB112). Scaffolds the
|
|
1238
1289
|
* user home and registers the hook by absolute path in the user-level agent config,
|
|
1239
1290
|
* so every project a developer opens is governed without a per-repo `init`. */
|
|
1291
|
+
/** The command a user-level agent setting runs. Run through `npx`, this CLI lives in npm's
|
|
1292
|
+
* throwaway cache; registering that path would leave a hook that stops starting whenever
|
|
1293
|
+
* npm clears it — and a hook that cannot start lets every action through. Pin the durable
|
|
1294
|
+
* copy, as `init` does, and fall back to the portable `npx` command when none can be made. */
|
|
1295
|
+
function durableHookCommand(h) {
|
|
1296
|
+
const pin = ensureDurableRuntime(cliPath(), hookVersion());
|
|
1297
|
+
return pin.cli ? absoluteHookCommand(pin.cli, h) : hookCommand(h);
|
|
1298
|
+
}
|
|
1240
1299
|
function runInstall(args) {
|
|
1241
1300
|
const dir = userHome();
|
|
1242
|
-
|
|
1243
|
-
// would leave a hook that stops starting whenever npm clears it — and a hook that
|
|
1244
|
-
// cannot start lets every action through. Pin the durable copy, as `init` does, and
|
|
1245
|
-
// fall back to the portable `npx` command when none can be made.
|
|
1246
|
-
const commandFor = (h) => {
|
|
1247
|
-
const pin = ensureDurableRuntime(cliPath(), hookVersion());
|
|
1248
|
-
return pin.cli ? absoluteHookCommand(pin.cli, h) : hookCommand(h);
|
|
1249
|
-
};
|
|
1301
|
+
const commandFor = durableHookCommand;
|
|
1250
1302
|
const harnessesFor = () => args.includes("--codex") ? ["codex"]
|
|
1251
1303
|
: args.includes("--cursor") ? ["cursor"]
|
|
1252
1304
|
: args.includes("--claude") ? ["claude"]
|
|
@@ -1351,6 +1403,13 @@ function runStatus() {
|
|
|
1351
1403
|
console.log(` Cursor ${harnessScopeLabel(cursor) || (cursorDetected() ? "detected, not configured" : "not detected")}`);
|
|
1352
1404
|
console.log(` Codex ${codex.project || codex.user ? `${harnessScopeLabel(codex)} — approve once with /hooks` : codexDetected() ? "detected, not configured" : "not detected"}`);
|
|
1353
1405
|
console.log(` cloud workspace ${connected ? "connected" : "not connected (local only)"}`);
|
|
1406
|
+
{
|
|
1407
|
+
const activeDir = resolveConfigDir(process.cwd());
|
|
1408
|
+
const connection = loadConnection(activeDir);
|
|
1409
|
+
if (connection)
|
|
1410
|
+
for (const line of describeDelivery(activeDir, connection).lines)
|
|
1411
|
+
console.log(` ${line}`);
|
|
1412
|
+
}
|
|
1354
1413
|
{
|
|
1355
1414
|
const activeDir = resolveConfigDir(process.cwd());
|
|
1356
1415
|
const meta = readMeta(activeDir);
|
|
@@ -1515,6 +1574,33 @@ async function runDoctor() {
|
|
|
1515
1574
|
reachable = `unreachable (${error.message})`;
|
|
1516
1575
|
}
|
|
1517
1576
|
console.log(` cloud ${connection.url} — ${reachable}`);
|
|
1577
|
+
// SB273: an authenticated check. Reaching the workspace says nothing about whether it
|
|
1578
|
+
// still accepts this computer; the rules endpoint answers 401 when it does not.
|
|
1579
|
+
let accepted = "unknown";
|
|
1580
|
+
try {
|
|
1581
|
+
const res = await fetch(new URL("/v1/policy", connection.url).toString(), {
|
|
1582
|
+
headers: { authorization: `Bearer ${connection.credential}`, "x-scopebond-hook-version": hookVersion() },
|
|
1583
|
+
redirect: "error", signal: AbortSignal.timeout(10_000),
|
|
1584
|
+
});
|
|
1585
|
+
if (res.status === 401) {
|
|
1586
|
+
accepted = "REFUSED (401)";
|
|
1587
|
+
recordRulesCredential(active, false, Date.now());
|
|
1588
|
+
}
|
|
1589
|
+
else if (res.ok || res.status === 204 || res.status === 304) {
|
|
1590
|
+
accepted = "accepted";
|
|
1591
|
+
recordRulesCredential(active, true, Date.now());
|
|
1592
|
+
}
|
|
1593
|
+
else
|
|
1594
|
+
accepted = `unknown (HTTP ${res.status})`;
|
|
1595
|
+
}
|
|
1596
|
+
catch (error) {
|
|
1597
|
+
accepted = `not checked (${error.message})`;
|
|
1598
|
+
}
|
|
1599
|
+
console.log(` connection ${accepted}`);
|
|
1600
|
+
const delivery = describeDelivery(active, connection);
|
|
1601
|
+
for (const line of delivery.lines)
|
|
1602
|
+
console.log(` ${line}`);
|
|
1603
|
+
problems.push(...delivery.problems);
|
|
1518
1604
|
}
|
|
1519
1605
|
console.log(problems.length ? `\n${problems.length} problem(s): ${problems.join("; ")}` : `\nAll good.`);
|
|
1520
1606
|
process.exitCode = problems.length ? 1 : 0;
|
|
@@ -1577,7 +1663,7 @@ async function runLogin(args) {
|
|
|
1577
1663
|
origin = parsed.origin;
|
|
1578
1664
|
}
|
|
1579
1665
|
catch {
|
|
1580
|
-
console.error(`usage: ${cliCommand("login <workspace-url> [--claude|--cursor|--codex] [--no-install]")}`);
|
|
1666
|
+
console.error(`usage: ${cliCommand("login <workspace-url> [--claude|--cursor|--codex] [--no-install] [--project]")}`);
|
|
1581
1667
|
console.error("The workspace URL is the address of your Scopebond workspace, for example https://cloud.scopebond.com.");
|
|
1582
1668
|
process.exit(1);
|
|
1583
1669
|
}
|
|
@@ -1588,6 +1674,12 @@ async function runLogin(args) {
|
|
|
1588
1674
|
});
|
|
1589
1675
|
return { status: response.status, json: await response.json().catch(() => ({})) };
|
|
1590
1676
|
};
|
|
1677
|
+
// SB276: run from inside an agent session (its own terminal or tool call), a sign-in can
|
|
1678
|
+
// land in the agent's working folder rather than this person's, and the person may never
|
|
1679
|
+
// see the code to approve. Say so plainly; the hook settings are left alone either way.
|
|
1680
|
+
if (process.env.CLAUDECODE || process.env.CODEX_SANDBOX || process.env.CURSOR_AGENT) {
|
|
1681
|
+
console.error("Note: this looks like a coding agent's own terminal. Signing in works best from your own terminal window, outside the agent.");
|
|
1682
|
+
}
|
|
1591
1683
|
let start;
|
|
1592
1684
|
try {
|
|
1593
1685
|
start = await post("/v1/device/code", { client_name: hostname(), harness });
|
|
@@ -1607,7 +1699,7 @@ async function runLogin(args) {
|
|
|
1607
1699
|
const deadline = Date.now() + Math.max(60, Number(start.json.expires_in ?? 600)) * 1000;
|
|
1608
1700
|
console.log(`To connect this computer, open:\n\n ${verify}\n\nand check that it shows the code ${userCode}\n`);
|
|
1609
1701
|
console.log("Waiting for approval (the code expires in 10 minutes; Ctrl+C to stop)…");
|
|
1610
|
-
const dir =
|
|
1702
|
+
const dir = connectDir(args);
|
|
1611
1703
|
scaffold(dir, {});
|
|
1612
1704
|
while (Date.now() < deadline) {
|
|
1613
1705
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
@@ -1750,6 +1842,12 @@ const COMMANDS = [
|
|
|
1750
1842
|
{ name: "connect", args: "<workspace-url> <enrollment> [--claude|--cursor|--codex]",
|
|
1751
1843
|
summary: "send receipts to a Scopebond Cloud workspace as well as keeping them locally" },
|
|
1752
1844
|
{ name: "flush", summary: "deliver any receipts still queued for the workspace now" },
|
|
1845
|
+
{ name: "recover", args: "[--no-wait]", summary: "deliver records an earlier, revoked key signed, once the workspace approves",
|
|
1846
|
+
detail: [
|
|
1847
|
+
"When this computer was replaced or disconnected while records were still queued, the",
|
|
1848
|
+
"workspace refuses them from the new connection. This asks the workspace to accept them,",
|
|
1849
|
+
"waits while an owner or admin approves it on Activity, then sends them, labelled Recovered.",
|
|
1850
|
+
] },
|
|
1753
1851
|
{ name: "trust", args: "[--yes]", summary: "let this project's .scopebond policy govern here (pinned by hash)" },
|
|
1754
1852
|
{ name: "uninstall", args: "[--purge] [--yes]", summary: "remove the hook from your agent config; --purge also deletes the home" },
|
|
1755
1853
|
{ name: "claude", summary: "(internal) decide one Claude Code PreToolUse call, JSON on stdin" },
|
|
@@ -1840,6 +1938,9 @@ else if (cmd === "test") {
|
|
|
1840
1938
|
else if (cmd === "flush") {
|
|
1841
1939
|
await runFlush();
|
|
1842
1940
|
}
|
|
1941
|
+
else if (cmd === "recover") {
|
|
1942
|
+
await runRecover(rest);
|
|
1943
|
+
}
|
|
1843
1944
|
else if (cmd === "status") {
|
|
1844
1945
|
runStatus();
|
|
1845
1946
|
}
|