@scopebond/hook 0.9.0 → 0.11.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 +42 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +124 -11
- 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 +71 -8
- package/dist/cloud.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/managed.d.ts +71 -0
- package/dist/managed.d.ts.map +1 -0
- package/dist/managed.js +230 -0
- package/dist/managed.js.map +1 -0
- 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 +45 -0
- package/dist/policy-sync.d.ts.map +1 -0
- package/dist/policy-sync.js +173 -0
- package/dist/policy-sync.js.map +1 -0
- 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 +5 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +5 -0
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -216,6 +216,48 @@ locally and retried if the workspace is unreachable. `npx @scopebond/hook flush`
|
|
|
216
216
|
delivers anything still queued — run it on a session-end hook (and set
|
|
217
217
|
`SCOPEBOND_HOOK_FLUSH_MS=0`) if you want zero per-call latency.
|
|
218
218
|
|
|
219
|
+
**Reconnecting.** Run `login` again from any folder: unless that folder has its own project
|
|
220
|
+
setup, it repairs the connection the hook actually uses (usually the user-level one) instead
|
|
221
|
+
of creating a second one; pass `--project` to set up the folder you are in. If the workspace
|
|
222
|
+
no longer accepts this computer's countersigning key (the computer was replaced or
|
|
223
|
+
disconnected there), `login` replaces the key and keeps the old one in
|
|
224
|
+
`.scopebond/retired-keys/`.
|
|
225
|
+
|
|
226
|
+
**Records signed by an earlier key.** The new connection cannot deliver records the earlier
|
|
227
|
+
key signed, so they leave the delivery queue but stay in the local log.
|
|
228
|
+
`npx @scopebond/hook recover` asks the workspace to accept them, waits while an owner or
|
|
229
|
+
admin approves it there, then sends them; the workspace checks each signature against the
|
|
230
|
+
key it kept and labels the records as recovered.
|
|
231
|
+
|
|
232
|
+
### Rules set by your workspace
|
|
233
|
+
|
|
234
|
+
A connected computer keeps its own rules (`.scopebond/rules.json`) until someone who manages
|
|
235
|
+
the workspace changes a rule for it there. From then on the workspace decides, rule by rule,
|
|
236
|
+
whether a matching action is **blocked** or only **recorded**, and can add entries to this
|
|
237
|
+
computer's lists (protected branches, programs, allowed sites). The workspace never sends
|
|
238
|
+
patterns: the hook compiles its choices with the same compiler as `rules apply`.
|
|
239
|
+
|
|
240
|
+
- **When it applies.** At most once every five minutes, a tool call also checks for changes,
|
|
241
|
+
alongside sending its activity record and capped at about one and a half seconds
|
|
242
|
+
(`SCOPEBOND_POLICY_SYNC_MS`); every other call only reads two small files. A change therefore
|
|
243
|
+
applies within a few minutes of the agent's next action, and a workspace that is slow or
|
|
244
|
+
unreachable never holds up the agent for longer than the cap. The new rules govern from the
|
|
245
|
+
next action. `npx @scopebond/hook policy sync` checks right now.
|
|
246
|
+
- **What is checked.** A rules document must be complete, issued for this computer, match its
|
|
247
|
+
digest and be newer than the one in force; the resulting policy must load. Anything else is
|
|
248
|
+
refused, the rules already in force stay, and the refusal is reported to the workspace.
|
|
249
|
+
- **What is confirmed.** After loading, the hook tells the workspace exactly which version it
|
|
250
|
+
loaded, so the workspace shows *Applied* only for computers that confirmed it.
|
|
251
|
+
- **What the workspace cannot change.** Protection of Scopebond's own settings and of the
|
|
252
|
+
agents' hook settings, the machine key policy, fail-closed handling of anything unreadable,
|
|
253
|
+
and this computer's own opt-ins (`allowed_roots`, `protect_remote_database`).
|
|
254
|
+
- **Going back.** While the workspace sets the rules, `rules` edits and `policy load` are
|
|
255
|
+
refused here. If the connection is revoked, or the workspace stops setting rules for this
|
|
256
|
+
computer, the hook recompiles `policy.json` from `rules.json`: a computer is never left
|
|
257
|
+
without rules. `status` shows which rules are in force and when they were last checked.
|
|
258
|
+
|
|
259
|
+
Set `SCOPEBOND_POLICY_SYNC=off` to stop the five-minute check (the rules in force stay).
|
|
260
|
+
|
|
219
261
|
### Session, health and action observations (opt-in)
|
|
220
262
|
|
|
221
263
|
Beyond receipts, the hook can send a second kind of signed record, an *observation*,
|
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":";AAiqBA,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAS7F"}
|
package/dist/cli.js
CHANGED
|
@@ -35,13 +35,16 @@ import { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } fro
|
|
|
35
35
|
import { createHookRuntime } from "./runtime.js";
|
|
36
36
|
import { useDigestKey, loadOrCreateDigestKey } from "./minimize.js";
|
|
37
37
|
import { scaffold, harnessSnippet, placeHook } from "./init.js";
|
|
38
|
-
import { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, projectHarnessFile, localHarnessFile, gitShareState, isMachineSpecificCommand, trustProjectPolicy, untrustedProjectPolicy, wireLifecycleHooks, unwireLifecycleHooks, } from "./install.js";
|
|
38
|
+
import { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, projectHarnessFile, localHarnessFile, gitShareState, isMachineSpecificCommand, trustProjectPolicy, untrustedProjectPolicy, isTrustedProject, wireLifecycleHooks, unwireLifecycleHooks, } from "./install.js";
|
|
39
39
|
import { openObservations, describeObservations, observationStatus, stopReasonFromClaude, exitFromClaudeFailure, HEARTBEAT_INTERVAL_MS, OBSERVATIONS_SCOPE, } from "./obs-emitter.js";
|
|
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";
|
|
44
|
-
import {
|
|
43
|
+
import { connectCloud, ingestUrl, loadConnection } from "./cloud.js";
|
|
44
|
+
import { recoverEarlierReceipts } from "./recover.js";
|
|
45
|
+
import { loadPolicyExport, policyBuilds } from "./policy-load.js";
|
|
46
|
+
import { isManaged, readMeta, MANAGED_DOC_FILE } from "./managed.js";
|
|
47
|
+
import { syncIfDue, syncPolicy } from "./policy-sync.js";
|
|
45
48
|
import { loadBudgetExport } from "./budget-load.js";
|
|
46
49
|
import { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor } from "./rules.js";
|
|
47
50
|
import { createSigner } from "@scopebond/sdk";
|
|
@@ -215,7 +218,8 @@ async function runPreToolUse(mapper, deny = denyClaude, raw, harness = "claude")
|
|
|
215
218
|
useDigestKey(loadOrCreateDigestKey(dir));
|
|
216
219
|
const decision = await runtime.evaluate([...fillPushBranch(mapper(input), currentBranch(cwd)), ...databaseGuard(dir, cwd, input)], { groupKey: callId(input) });
|
|
217
220
|
const observer = recordObservations(dir, cwd, input, decision, harness);
|
|
218
|
-
|
|
221
|
+
// Workspace rules: at most every five minutes, a capped check alongside the record delivery. It never fails the call.
|
|
222
|
+
await Promise.all([runtime.flush(), observer?.flush() ?? Promise.resolve(), syncIfDue(dir, () => syncOptionsFor(dir))]);
|
|
219
223
|
observer?.close();
|
|
220
224
|
// Close before deciding: the receipt is already committed, and leaving the handle
|
|
221
225
|
// open is what made the write-ahead log grow without bound.
|
|
@@ -326,7 +330,7 @@ async function runCursor() {
|
|
|
326
330
|
const mapped = fillPushBranch(mapCursorEvent(event, input), currentBranch(cwd));
|
|
327
331
|
const decision = await runtime.evaluate(mapped, { groupKey: callId(input) });
|
|
328
332
|
const observer = recordObservations(dir, cwd, input, decision, "cursor");
|
|
329
|
-
await Promise.all([runtime.flush(), observer?.flush() ?? Promise.resolve()]);
|
|
333
|
+
await Promise.all([runtime.flush(), observer?.flush() ?? Promise.resolve(), syncIfDue(dir, () => syncOptionsFor(dir))]);
|
|
330
334
|
observer?.close();
|
|
331
335
|
// An `afterFileEdit` violation is real and recorded, but the edit has already
|
|
332
336
|
// landed. Say so rather than letting "blocked" imply it was stopped.
|
|
@@ -677,6 +681,11 @@ function runRules(args) {
|
|
|
677
681
|
printHelp("rules", true);
|
|
678
682
|
process.exit(1);
|
|
679
683
|
}
|
|
684
|
+
if (isManaged(dir)) {
|
|
685
|
+
console.error("The rules on this computer are set by your Scopebond workspace, so they cannot be changed here.");
|
|
686
|
+
console.error("Change them in the workspace (Rules), or disconnect this computer to manage its rules locally again.");
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
680
689
|
// Changing what governs the agent is the same class of action as `init`.
|
|
681
690
|
requireInteractive("rules", args);
|
|
682
691
|
const policyPath = join(dir, "policy.json");
|
|
@@ -863,11 +872,11 @@ async function runConnect(args) {
|
|
|
863
872
|
const bundleArg = positional[1];
|
|
864
873
|
const harness = selectedHarness(args);
|
|
865
874
|
if (!url) {
|
|
866
|
-
console.error(`usage: ${cliCommand("connect <workspace-url> <enrollment> [--claude|--cursor|--codex] [--no-install]")}`);
|
|
875
|
+
console.error(`usage: ${cliCommand("connect <workspace-url> <enrollment> [--claude|--cursor|--codex] [--no-install] [--project]")}`);
|
|
867
876
|
console.error(enrollmentHelp);
|
|
868
877
|
process.exit(1);
|
|
869
878
|
}
|
|
870
|
-
const dir =
|
|
879
|
+
const dir = connectDir(args);
|
|
871
880
|
// One command sets everything up: scaffold the key, attester and starter policy if
|
|
872
881
|
// they do not exist, then enroll and persist the scoped machine credential. The
|
|
873
882
|
// enrollment can be a file, an inline base64 blob (what the portal hands out) or
|
|
@@ -889,6 +898,10 @@ async function finishConnect(dir, url, bundle, harness, args) {
|
|
|
889
898
|
try {
|
|
890
899
|
const c = await connectCloud(dir, url, bundle);
|
|
891
900
|
console.log(`✓ Connected to ${c.url}`);
|
|
901
|
+
if (c.rotatedFrom)
|
|
902
|
+
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")}`);
|
|
903
|
+
if (c.setAside)
|
|
904
|
+
console.log(` ${c.setAside.toLocaleString()} queued record(s) signed by an earlier key cannot go through this connection. To deliver them, run: ${cliCommand("recover")}`);
|
|
892
905
|
// With a user-level install present, the hook ignores a project policy until it is
|
|
893
906
|
// trusted, and would fall back to the user home, which holds no cloud.json: the agent
|
|
894
907
|
// stays governed, but nothing reaches the workspace. Connecting this project is the
|
|
@@ -951,6 +964,43 @@ const enrollmentHelp = [
|
|
|
951
964
|
"Each enrollment is single-use and expires soon after it is created; if this one was",
|
|
952
965
|
"used or has expired, create a new one there.",
|
|
953
966
|
].join("\n");
|
|
967
|
+
/** Where `connect` and `login` write. A project someone set up here with `init` (it has a
|
|
968
|
+
* policy) is connected, as before. Otherwise the configuration the hook itself uses from
|
|
969
|
+
* here — usually the user-level install — so reconnecting from any folder repairs the
|
|
970
|
+
* connection that is actually failing instead of creating a second, project-level one
|
|
971
|
+
* beside it. `--project` asks for a new per-project setup explicitly. */
|
|
972
|
+
function connectDir(args) {
|
|
973
|
+
const project = configDir();
|
|
974
|
+
return args.includes("--project") || existsSync(join(project, "policy.json")) ? project : resolveConfigDir(process.cwd());
|
|
975
|
+
}
|
|
976
|
+
/** `recover [--no-wait]`: deliver receipts that an earlier, since-revoked key of this computer
|
|
977
|
+
* signed and the workspace never received, after an owner or admin approves it there. */
|
|
978
|
+
async function runRecover(args) {
|
|
979
|
+
const dir = resolveConfigDir(process.cwd());
|
|
980
|
+
const connection = loadConnection(dir);
|
|
981
|
+
if (!connection) {
|
|
982
|
+
console.error(`not connected to a workspace; run \`${cliCommand("login <workspace-url>")}\` first`);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
console.log(`Recovering from ${dir}`);
|
|
986
|
+
const result = await recoverEarlierReceipts(dir, connection, {
|
|
987
|
+
log: (line) => console.log(line),
|
|
988
|
+
fetch,
|
|
989
|
+
now: Date.now,
|
|
990
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
991
|
+
}, { wait: !args.includes("--no-wait"), waitMs: 15 * 60_000, pollMs: 5_000 });
|
|
992
|
+
if (result.groups) {
|
|
993
|
+
const parts = [
|
|
994
|
+
`recovered ${result.accepted.toLocaleString()}`,
|
|
995
|
+
`already present ${result.duplicates.toLocaleString()}`,
|
|
996
|
+
`refused ${result.rejected.toLocaleString()}`,
|
|
997
|
+
...(result.pending ? [`waiting for approval ${result.pending.toLocaleString()}`] : []),
|
|
998
|
+
...(result.skipped ? [`skipped ${result.skipped.toLocaleString()}`] : []),
|
|
999
|
+
];
|
|
1000
|
+
console.log(`\nDone: ${parts.join(", ")}.`);
|
|
1001
|
+
}
|
|
1002
|
+
process.exitCode = result.failed || result.pending ? 1 : 0;
|
|
1003
|
+
}
|
|
954
1004
|
async function runFlush() {
|
|
955
1005
|
const dir = resolveConfigDir(process.cwd());
|
|
956
1006
|
if (!loadConnection(dir)) {
|
|
@@ -970,12 +1020,21 @@ async function runFlush() {
|
|
|
970
1020
|
* `--yes`, make it the active policy here; then acknowledge it (or its refusal) to the workspace. */
|
|
971
1021
|
async function runPolicy(args) {
|
|
972
1022
|
const [sub, file] = args;
|
|
1023
|
+
if (sub === "sync") {
|
|
1024
|
+
await runPolicySync(args.includes("--background"));
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
973
1027
|
if (sub !== "load" || !file) {
|
|
974
|
-
console.error(`usage: ${cliCommand("policy load <export.json> [--yes]")}`);
|
|
1028
|
+
console.error(`usage: ${cliCommand("policy sync")} | ${cliCommand("policy load <export.json> [--yes]")}`);
|
|
975
1029
|
process.exitCode = 2;
|
|
976
1030
|
return;
|
|
977
1031
|
}
|
|
978
1032
|
const dir = resolveConfigDir(process.cwd());
|
|
1033
|
+
if (isManaged(dir)) {
|
|
1034
|
+
console.error("The rules on this computer are set by your Scopebond workspace; a policy file cannot replace them here.");
|
|
1035
|
+
process.exitCode = 1;
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
979
1038
|
const apply = args.includes("--yes");
|
|
980
1039
|
const connection = loadConnection(dir);
|
|
981
1040
|
const outcome = loadPolicyExport(dir, file, { apply, environmentId: connection?.environment_id });
|
|
@@ -1002,6 +1061,41 @@ async function runPolicy(args) {
|
|
|
1002
1061
|
return;
|
|
1003
1062
|
await sendPolicyAck(dir, ack);
|
|
1004
1063
|
}
|
|
1064
|
+
/** What a rules check needs for this config directory. A project policy governs only once trusted, so it is re-pinned after a
|
|
1065
|
+
* write, exactly when `rules apply` would. */
|
|
1066
|
+
function syncOptionsFor(dir) {
|
|
1067
|
+
const home = userHome();
|
|
1068
|
+
const repin = dir !== home && existsSync(join(home, "policy.json")) && isTrustedProject(dir);
|
|
1069
|
+
const agentKid = createSigner({ privateKeyPem: readFileSync(join(dir, "agent.key"), "utf8") }).kid;
|
|
1070
|
+
return { agentKid, hookVersion: hookVersion(), policyBuilds, afterPolicyWrite: repin ? (d) => { trustProjectPolicy(d); } : undefined };
|
|
1071
|
+
}
|
|
1072
|
+
/** `policy sync`: bring this computer's rules in line with its workspace now (the hook also checks every five minutes). */
|
|
1073
|
+
async function runPolicySync(background) {
|
|
1074
|
+
const dir = resolveConfigDir(process.cwd());
|
|
1075
|
+
let outcome;
|
|
1076
|
+
try {
|
|
1077
|
+
outcome = await syncPolicy(dir, syncOptionsFor(dir));
|
|
1078
|
+
}
|
|
1079
|
+
catch (error) {
|
|
1080
|
+
outcome = { state: "unavailable", message: error.message };
|
|
1081
|
+
}
|
|
1082
|
+
if (background)
|
|
1083
|
+
return;
|
|
1084
|
+
const lines = {
|
|
1085
|
+
not_connected: "This computer is not connected to a Scopebond workspace; it uses its own rules.",
|
|
1086
|
+
own_rules: "Your workspace does not set rules for this computer; it uses its own rules.",
|
|
1087
|
+
unchanged: "Up to date with your workspace.",
|
|
1088
|
+
applied: "Updated to your workspace's latest rules.",
|
|
1089
|
+
refused: "Could not apply your workspace's rules; the rules already in force stay.",
|
|
1090
|
+
disconnected: "The workspace connection is no longer valid; this computer now uses its own rules.",
|
|
1091
|
+
unavailable: "Could not reach your workspace; the rules already in force stay.",
|
|
1092
|
+
};
|
|
1093
|
+
console.log(lines[outcome.state]);
|
|
1094
|
+
if (outcome.state === "refused" || outcome.state === "unavailable") {
|
|
1095
|
+
console.log(` ${outcome.message}`);
|
|
1096
|
+
process.exitCode = 1;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1005
1099
|
/** Queue a `policy_ack` (loaded or rejected) through the observation outbox and try to deliver it now. */
|
|
1006
1100
|
async function sendPolicyAck(dir, ack) {
|
|
1007
1101
|
const observed = openObservations(dir, { adapterVersion: hookVersion(), spawnHeartbeat: false });
|
|
@@ -1128,7 +1222,7 @@ async function runObservations(args) {
|
|
|
1128
1222
|
const emitter = opened.emitter;
|
|
1129
1223
|
try {
|
|
1130
1224
|
if (sub === "flush") {
|
|
1131
|
-
const outcome = await uploadPending(emitter.store, { url: emitter.connection
|
|
1225
|
+
const outcome = await uploadPending(emitter.store, { url: ingestUrl(emitter.connection), credential: emitter.connection.credential, timeoutMs: 10_000 });
|
|
1132
1226
|
const left = emitter.store.pendingSummary().count;
|
|
1133
1227
|
console.log(`${outcome.result}: ${outcome.acknowledged} acknowledged, ${outcome.deferred} deferred, ${outcome.rejected} refused; ${left} still pending${outcome.detail ? ` (${outcome.detail})` : ""}`);
|
|
1134
1228
|
process.exitCode = left > 0 ? 1 : 0;
|
|
@@ -1299,6 +1393,16 @@ function runStatus() {
|
|
|
1299
1393
|
console.log(` Cursor ${harnessScopeLabel(cursor) || (cursorDetected() ? "detected, not configured" : "not detected")}`);
|
|
1300
1394
|
console.log(` Codex ${codex.project || codex.user ? `${harnessScopeLabel(codex)} — approve once with /hooks` : codexDetected() ? "detected, not configured" : "not detected"}`);
|
|
1301
1395
|
console.log(` cloud workspace ${connected ? "connected" : "not connected (local only)"}`);
|
|
1396
|
+
{
|
|
1397
|
+
const activeDir = resolveConfigDir(process.cwd());
|
|
1398
|
+
const meta = readMeta(activeDir);
|
|
1399
|
+
const managed = existsSync(join(activeDir, MANAGED_DOC_FILE));
|
|
1400
|
+
const checked = meta.checked_at ? `, last checked ${meta.checked_at}` : "";
|
|
1401
|
+
console.log(` rules ${managed ? `set by your workspace (version ${meta.revision})${checked}` : `this computer's own (${rulesPath(activeDir)})${connected ? checked : ""}`}`);
|
|
1402
|
+
console.log(` always on: protection of Scopebond's own settings and the agents' hook settings`);
|
|
1403
|
+
if (meta.last_error)
|
|
1404
|
+
console.log(` last problem: ${meta.last_error}`);
|
|
1405
|
+
}
|
|
1302
1406
|
const observationLines = describeObservations(resolveConfigDir(process.cwd()));
|
|
1303
1407
|
console.log(` observations ${observationLines[0]}`);
|
|
1304
1408
|
for (const line of observationLines.slice(1))
|
|
@@ -1515,7 +1619,7 @@ async function runLogin(args) {
|
|
|
1515
1619
|
origin = parsed.origin;
|
|
1516
1620
|
}
|
|
1517
1621
|
catch {
|
|
1518
|
-
console.error(`usage: ${cliCommand("login <workspace-url> [--claude|--cursor|--codex] [--no-install]")}`);
|
|
1622
|
+
console.error(`usage: ${cliCommand("login <workspace-url> [--claude|--cursor|--codex] [--no-install] [--project]")}`);
|
|
1519
1623
|
console.error("The workspace URL is the address of your Scopebond workspace, for example https://cloud.scopebond.com.");
|
|
1520
1624
|
process.exit(1);
|
|
1521
1625
|
}
|
|
@@ -1545,7 +1649,7 @@ async function runLogin(args) {
|
|
|
1545
1649
|
const deadline = Date.now() + Math.max(60, Number(start.json.expires_in ?? 600)) * 1000;
|
|
1546
1650
|
console.log(`To connect this computer, open:\n\n ${verify}\n\nand check that it shows the code ${userCode}\n`);
|
|
1547
1651
|
console.log("Waiting for approval (the code expires in 10 minutes; Ctrl+C to stop)…");
|
|
1548
|
-
const dir =
|
|
1652
|
+
const dir = connectDir(args);
|
|
1549
1653
|
scaffold(dir, {});
|
|
1550
1654
|
while (Date.now() < deadline) {
|
|
1551
1655
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
@@ -1688,6 +1792,12 @@ const COMMANDS = [
|
|
|
1688
1792
|
{ name: "connect", args: "<workspace-url> <enrollment> [--claude|--cursor|--codex]",
|
|
1689
1793
|
summary: "send receipts to a Scopebond Cloud workspace as well as keeping them locally" },
|
|
1690
1794
|
{ name: "flush", summary: "deliver any receipts still queued for the workspace now" },
|
|
1795
|
+
{ name: "recover", args: "[--no-wait]", summary: "deliver records an earlier, revoked key signed, once the workspace approves",
|
|
1796
|
+
detail: [
|
|
1797
|
+
"When this computer was replaced or disconnected while records were still queued, the",
|
|
1798
|
+
"workspace refuses them from the new connection. This asks the workspace to accept them,",
|
|
1799
|
+
"waits while an owner or admin approves it on Activity, then sends them, labelled Recovered.",
|
|
1800
|
+
] },
|
|
1691
1801
|
{ name: "trust", args: "[--yes]", summary: "let this project's .scopebond policy govern here (pinned by hash)" },
|
|
1692
1802
|
{ name: "uninstall", args: "[--purge] [--yes]", summary: "remove the hook from your agent config; --purge also deletes the home" },
|
|
1693
1803
|
{ name: "claude", summary: "(internal) decide one Claude Code PreToolUse call, JSON on stdin" },
|
|
@@ -1778,6 +1888,9 @@ else if (cmd === "test") {
|
|
|
1778
1888
|
else if (cmd === "flush") {
|
|
1779
1889
|
await runFlush();
|
|
1780
1890
|
}
|
|
1891
|
+
else if (cmd === "recover") {
|
|
1892
|
+
await runRecover(rest);
|
|
1893
|
+
}
|
|
1781
1894
|
else if (cmd === "status") {
|
|
1782
1895
|
runStatus();
|
|
1783
1896
|
}
|