@manyoars/oars 0.2.0 → 0.3.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 +132 -28
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +6 -3
- package/dist/cli.js +367 -88
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.js +2 -2
- package/dist/errors.d.ts +13 -13
- package/dist/errors.js +28 -11
- package/dist/gate/checks.d.ts +68 -0
- package/dist/gate/checks.js +584 -0
- package/dist/gate/command.d.ts +14 -0
- package/dist/gate/command.js +61 -0
- package/dist/gate/hook-fill-main.d.ts +1 -0
- package/dist/gate/hook-fill-main.js +16 -0
- package/dist/gate/hook-fill.d.ts +38 -0
- package/dist/gate/hook-fill.js +94 -0
- package/dist/gate/hook-main.d.ts +1 -0
- package/dist/gate/hook-main.js +35 -0
- package/dist/gate/hook-process.d.ts +14 -0
- package/dist/gate/hook-process.js +64 -0
- package/dist/gate/hook.d.ts +46 -0
- package/dist/gate/hook.js +256 -0
- package/dist/gate/install.d.ts +25 -0
- package/dist/gate/install.js +126 -0
- package/dist/gate/local-bin.d.ts +8 -0
- package/dist/gate/local-bin.js +36 -0
- package/dist/gate/notes.d.ts +19 -0
- package/dist/gate/notes.js +27 -0
- package/dist/gate/novelty.d.ts +35 -0
- package/dist/gate/novelty.js +99 -0
- package/dist/gate/policy.d.ts +25 -0
- package/dist/gate/policy.js +73 -0
- package/dist/gate/record.d.ts +120 -0
- package/dist/gate/record.js +125 -0
- package/dist/gate/registry-state.d.ts +10 -0
- package/dist/gate/registry-state.js +30 -0
- package/dist/gate/registry.d.ts +56 -0
- package/dist/gate/registry.js +94 -0
- package/dist/gate/replay.d.ts +20 -0
- package/dist/gate/replay.js +257 -0
- package/dist/gate/report.d.ts +50 -0
- package/dist/gate/report.js +89 -0
- package/dist/gate/scope.d.ts +7 -0
- package/dist/gate/scope.js +27 -0
- package/dist/gate/transcripts.d.ts +68 -0
- package/dist/gate/transcripts.js +232 -0
- package/dist/hook-bin.d.ts +1 -0
- package/dist/hook-bin.js +25 -0
- package/dist/issuer.d.ts +7 -1
- package/dist/issuer.js +18 -1
- package/dist/main.d.ts +1 -0
- package/dist/main.js +64 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +351 -0
- package/dist/sessions/allow.d.ts +14 -0
- package/dist/sessions/allow.js +51 -0
- package/dist/sessions/archive.d.ts +11 -0
- package/dist/sessions/archive.js +38 -0
- package/dist/sessions/audit.d.ts +42 -0
- package/dist/sessions/audit.js +104 -0
- package/dist/sessions/baseline.d.ts +62 -0
- package/dist/sessions/baseline.js +188 -0
- package/dist/sessions/capture-runtime.d.ts +10 -0
- package/dist/sessions/capture-runtime.js +12 -0
- package/dist/sessions/cell-increments.d.ts +43 -0
- package/dist/sessions/cell-increments.js +209 -0
- package/dist/sessions/cells.d.ts +79 -0
- package/dist/sessions/cells.js +181 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +679 -0
- package/dist/sessions/command.d.ts +117 -0
- package/dist/sessions/command.js +373 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/cursor.d.ts +24 -0
- package/dist/sessions/cursor.js +324 -0
- package/dist/sessions/derive.d.ts +57 -0
- package/dist/sessions/derive.js +320 -0
- package/dist/sessions/exclusion.d.ts +81 -0
- package/dist/sessions/exclusion.js +353 -0
- package/dist/sessions/extract.d.ts +179 -0
- package/dist/sessions/extract.js +1774 -0
- package/dist/sessions/fold.d.ts +17 -0
- package/dist/sessions/fold.js +71 -0
- package/dist/sessions/frame.d.ts +24 -0
- package/dist/sessions/frame.js +95 -0
- package/dist/sessions/gemini.d.ts +37 -0
- package/dist/sessions/gemini.js +613 -0
- package/dist/sessions/gold-arms.d.ts +67 -0
- package/dist/sessions/gold-arms.js +172 -0
- package/dist/sessions/gold.d.ts +197 -0
- package/dist/sessions/gold.js +463 -0
- package/dist/sessions/graph.d.ts +12 -0
- package/dist/sessions/graph.js +34 -0
- package/dist/sessions/grok.d.ts +36 -0
- package/dist/sessions/grok.js +421 -0
- package/dist/sessions/hook-calls.d.ts +3 -0
- package/dist/sessions/hook-calls.js +47 -0
- package/dist/sessions/hook-index.d.ts +53 -0
- package/dist/sessions/hook-index.js +157 -0
- package/dist/sessions/hooks.d.ts +13 -0
- package/dist/sessions/hooks.js +170 -0
- package/dist/sessions/identity.d.ts +22 -0
- package/dist/sessions/identity.js +101 -0
- package/dist/sessions/install.d.ts +54 -0
- package/dist/sessions/install.js +201 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +6 -0
- package/dist/sessions/mcp.js +153 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +43 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/package-resolve.d.ts +30 -0
- package/dist/sessions/package-resolve.js +183 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pool-command.d.ts +44 -0
- package/dist/sessions/pool-command.js +542 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-outbox.d.ts +63 -0
- package/dist/sessions/pool-outbox.js +260 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-wire.d.ts +225 -0
- package/dist/sessions/pool-wire.js +620 -0
- package/dist/sessions/pool.d.ts +32 -0
- package/dist/sessions/pool.js +164 -0
- package/dist/sessions/projection.d.ts +75 -0
- package/dist/sessions/projection.js +165 -0
- package/dist/sessions/public-vocabulary.d.ts +73 -0
- package/dist/sessions/public-vocabulary.js +302 -0
- package/dist/sessions/query.d.ts +111 -0
- package/dist/sessions/query.js +246 -0
- package/dist/sessions/read-adoption.d.ts +2 -0
- package/dist/sessions/read-adoption.js +27 -0
- package/dist/sessions/read-call-shape.d.ts +2 -0
- package/dist/sessions/read-call-shape.js +25 -0
- package/dist/sessions/read-change.d.ts +2 -0
- package/dist/sessions/read-change.js +30 -0
- package/dist/sessions/read-cost.d.ts +4 -0
- package/dist/sessions/read-cost.js +37 -0
- package/dist/sessions/read-day-move.d.ts +3 -0
- package/dist/sessions/read-day-move.js +65 -0
- package/dist/sessions/read-fit.d.ts +7 -0
- package/dist/sessions/read-fit.js +41 -0
- package/dist/sessions/read-interaction.d.ts +5 -0
- package/dist/sessions/read-interaction.js +62 -0
- package/dist/sessions/read-lead-lag.d.ts +15 -0
- package/dist/sessions/read-lead-lag.js +81 -0
- package/dist/sessions/read-live.d.ts +6 -0
- package/dist/sessions/read-live.js +38 -0
- package/dist/sessions/read-method.d.ts +2 -0
- package/dist/sessions/read-method.js +29 -0
- package/dist/sessions/read-next-step.d.ts +2 -0
- package/dist/sessions/read-next-step.js +35 -0
- package/dist/sessions/read-order.d.ts +54 -0
- package/dist/sessions/read-order.js +62 -0
- package/dist/sessions/read-readable.d.ts +8 -0
- package/dist/sessions/read-readable.js +24 -0
- package/dist/sessions/read-recovery.d.ts +20 -0
- package/dist/sessions/read-recovery.js +97 -0
- package/dist/sessions/read-rows.d.ts +13 -0
- package/dist/sessions/read-rows.js +47 -0
- package/dist/sessions/read-substitution.d.ts +5 -0
- package/dist/sessions/read-substitution.js +51 -0
- package/dist/sessions/read-support.d.ts +48 -0
- package/dist/sessions/read-support.js +73 -0
- package/dist/sessions/read-tool-day.d.ts +2 -0
- package/dist/sessions/read-tool-day.js +26 -0
- package/dist/sessions/read-version-boundary.d.ts +31 -0
- package/dist/sessions/read-version-boundary.js +95 -0
- package/dist/sessions/read.d.ts +79 -0
- package/dist/sessions/read.js +84 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-object.d.ts +137 -0
- package/dist/sessions/recall-object.js +77 -0
- package/dist/sessions/recall-pool.d.ts +19 -0
- package/dist/sessions/recall-pool.js +96 -0
- package/dist/sessions/recall-push.d.ts +26 -0
- package/dist/sessions/recall-push.js +95 -0
- package/dist/sessions/recall-reads.d.ts +117 -0
- package/dist/sessions/recall-reads.js +410 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/redact.d.ts +6 -0
- package/dist/sessions/redact.js +54 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +644 -0
- package/dist/sessions/schema.js +148 -0
- package/dist/sessions/session-shape.d.ts +73 -0
- package/dist/sessions/session-shape.js +342 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +159 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +297 -0
- package/dist/sessions/store.js +1478 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +25 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/verify.d.ts +53 -0
- package/dist/sessions/verify.js +286 -0
- package/dist/sessions/vocab-load.d.ts +26 -0
- package/dist/sessions/vocab-load.js +61 -0
- package/dist/sessions/vscode-copilot.d.ts +38 -0
- package/dist/sessions/vscode-copilot.js +477 -0
- package/package.json +4 -5
- package/vocab/crates.json +1 -0
- package/vocab/go.json +1 -0
- package/vocab/harness.json +1 -0
- package/vocab/homebrew.json +1 -0
- package/vocab/manifest.json +1 -0
- package/vocab/maven.json +1 -0
- package/vocab/models.json +1 -0
- package/vocab/npm.json +1 -0
- package/vocab/programs.json +1 -0
- package/vocab/pypi.json +1 -0
- package/vocab/rubygems.json +1 -0
- package/dist/commands/deploy-input.d.ts +0 -38
- package/dist/commands/deploy-input.js +0 -150
- package/dist/commands/deploy.d.ts +0 -22
- package/dist/commands/deploy.js +0 -124
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -18
- package/dist/commands/templates.d.ts +0 -11
- package/dist/commands/templates.js +0 -32
- package/dist/commands/test-charge.d.ts +0 -11
- package/dist/commands/test-charge.js +0 -34
- package/dist/journal.d.ts +0 -26
- package/dist/journal.js +0 -58
- package/parity/deploy-plans/paid-api.json +0 -34
- package/parity/deploy-plans/per-task-agent.json +0 -34
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { Command, CommanderError, InvalidArgumentError } from 'commander';
|
|
3
3
|
import { resolveApiUrl } from './api.js';
|
|
4
|
-
import {
|
|
4
|
+
import { keysCreate, keysList, keysRevoke, SCOPED_AUTHORITIES } from './commands/keys.js';
|
|
5
5
|
import { login } from './commands/login.js';
|
|
6
6
|
import { logout } from './commands/logout.js';
|
|
7
|
-
import { status } from './commands/status.js';
|
|
8
|
-
import { templates } from './commands/templates.js';
|
|
9
|
-
import { testCharge } from './commands/test-charge.js';
|
|
10
7
|
import { CliError, ExitCode } from './errors.js';
|
|
8
|
+
import { closedQueryOf, formsRefusal, RECALL_FAMILIES } from './sessions/recall-reads.js';
|
|
9
|
+
import { FAMILIES } from './sessions/read.js';
|
|
10
|
+
import { sessionsBooks, sessionsContext, sessionsDeposit, sessionsGoldArms, sessionsGoldSample, sessionsGoldScore, sessionsHookInstall, sessionsHookStatus, sessionsHookUninstall, sessionsMcp, sessionsLineage, sessionsMeasure, sessionsRead, sessionsRecall, sessionsRecallCounts, sessionsVerify } from './sessions/command.js';
|
|
11
|
+
import { gateInstall, gatePolicy, gateReplay, gateReport, gateUninstall } from './gate/command.js';
|
|
12
|
+
import { DEADLINE_MS, gateHook } from './gate/hook.js';
|
|
13
|
+
import { managerReader } from './gate/registry.js';
|
|
14
|
+
import { exclusionPaths, sessionsExclusions } from './sessions/exclusion.js';
|
|
15
|
+
import { poolDeposit, poolFetch, poolLogin, poolOutbox } from './sessions/pool-command.js';
|
|
11
16
|
const flag = (options, name) => options[name] === true;
|
|
12
17
|
const text = (options, name) => {
|
|
13
18
|
const value = options[name];
|
|
@@ -28,45 +33,38 @@ const positiveInteger = (value) => {
|
|
|
28
33
|
throw new InvalidArgumentError('it must be a positive whole number.');
|
|
29
34
|
return Number(value);
|
|
30
35
|
};
|
|
36
|
+
/** An ISO time, normalized to the form session timestamps are stored in so the two compare as text. */
|
|
37
|
+
const isoTime = (value) => {
|
|
38
|
+
const parsed = Date.parse(value);
|
|
39
|
+
if (Number.isNaN(parsed))
|
|
40
|
+
throw new InvalidArgumentError('it must be an ISO time such as 2026-11-01T16:00:00Z.');
|
|
41
|
+
return new Date(parsed).toISOString();
|
|
42
|
+
};
|
|
31
43
|
const emit = (deps, json, outcome) => {
|
|
32
44
|
deps.io.stdout(`${json ? JSON.stringify(outcome.document) : outcome.text}\n`);
|
|
33
45
|
};
|
|
34
|
-
/** The last line of an exit 6: the one thing a person must do, by link when the platform sent one, then to rerun. Stripe onboarding is the step most runs wait on. */
|
|
35
|
-
const needsYouLine = (code, actionUrl) => {
|
|
36
|
-
const stripe = code === 'ACCOUNT_NOT_READY';
|
|
37
|
-
if (actionUrl !== undefined && actionUrl !== '') {
|
|
38
|
-
return `Needs you: ${stripe ? 'finish Stripe onboarding at' : 'open'} ${actionUrl}, then rerun this command.`;
|
|
39
|
-
}
|
|
40
|
-
return stripe
|
|
41
|
-
? 'Needs you: finish Stripe onboarding (Account, then Payout account, in the Many Oars web app), then rerun this command.'
|
|
42
|
-
: 'Needs you: a step only a person can do. Do it in the Many Oars web app, then rerun this command.';
|
|
43
|
-
};
|
|
44
46
|
/**
|
|
45
|
-
* `--json` prints `{ok: false, step, problem}` on stdout, plus
|
|
46
|
-
*
|
|
47
|
+
* `--json` prints `{ok: false, step, problem}` on stdout, plus the error's details; otherwise the problem's title, detail,
|
|
48
|
+
* and URL go to stderr.
|
|
47
49
|
*/
|
|
48
50
|
const printFailure = (deps, json, error) => {
|
|
49
|
-
const { partial } = error;
|
|
50
51
|
if (json) {
|
|
51
|
-
deps.io.stdout(`${JSON.stringify({ ok: false, step: error.step, problem: error.problem, ...
|
|
52
|
+
deps.io.stdout(`${JSON.stringify({ ok: false, step: error.step, problem: error.problem, ...error.details })}\n`);
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
const { title, detail, actionUrl } = error.problem;
|
|
55
|
-
|
|
56
|
-
const needsYou = error.exitCode === ExitCode.HUMAN ? [needsYouLine(error.problem.code, actionUrl)] : [];
|
|
57
|
-
deps.io.stderr(`${[title ?? error.message, detail, ...(needsYou.length === 0 ? [actionUrl] : []), ...resume, ...needsYou].filter((line) => line !== undefined && line !== '').join('\n')}\n`);
|
|
58
|
-
if (partial?.sdkKey !== undefined)
|
|
59
|
-
deps.io.stdout(`SDK key (shown once; save it now): ${partial.sdkKey}\n`);
|
|
56
|
+
deps.io.stderr(`${[title ?? error.message, detail, actionUrl].filter((line) => line !== undefined && line !== '').join('\n')}\n`);
|
|
60
57
|
};
|
|
61
|
-
const
|
|
58
|
+
const sessionDeps = (deps) => ({ sessions: deps.sessions, install: deps.install, now: deps.now });
|
|
59
|
+
const buildProgram = (deps) => {
|
|
62
60
|
const program = new Command('oars')
|
|
63
|
-
.description('
|
|
61
|
+
.description('Many Oars: sign in, manage API keys, and keep a local, searchable record of your own Claude Code sessions')
|
|
64
62
|
.version(deps.version)
|
|
65
63
|
.exitOverride()
|
|
66
64
|
.configureOutput({ writeOut: deps.io.stdout, writeErr: deps.io.stderr })
|
|
67
65
|
.option('--json', 'print one JSON document on stdout instead of text')
|
|
68
66
|
.option('--api-url <url>', 'the platform API (default: OARS_API_URL, else https://api.manyoars.com)')
|
|
69
|
-
.option('--idempotency-key <key>', "the base of
|
|
67
|
+
.option('--idempotency-key <key>', "the base of the idempotency key (default: a fresh UUID); rerun a failed command with the same key to replay it");
|
|
70
68
|
program
|
|
71
69
|
.command('login')
|
|
72
70
|
.description('sign in with your browser (RFC 8628 device flow)')
|
|
@@ -88,77 +86,358 @@ const buildProgram = (deps, argv) => {
|
|
|
88
86
|
const options = command.optsWithGlobals();
|
|
89
87
|
emit(deps, flag(options, 'json'), await logout(deps));
|
|
90
88
|
});
|
|
91
|
-
program
|
|
92
|
-
|
|
93
|
-
.
|
|
89
|
+
const keys = program.command('keys').description('create, list, and revoke your API keys; a key made with --workload works on that one workload only');
|
|
90
|
+
keys
|
|
91
|
+
.command('create')
|
|
92
|
+
.description('create an API key and print its secret once; --workload limits it to one workload you created')
|
|
93
|
+
.requiredOption('--name <name>', 'a name to recognize the key by')
|
|
94
|
+
.option('--workload <id>', 'limit the key to this workload (it may then carry only AGENT_OPERATE and AGENT_READ)')
|
|
95
|
+
.option('--authority <name>', 'an authority for the key (repeatable; default AGENT_OPERATE and AGENT_READ)', collect)
|
|
96
|
+
.option('--expires-in-days <n>', 'days until the key expires', positiveInteger)
|
|
94
97
|
.action(async (_options, command) => {
|
|
95
98
|
const options = command.optsWithGlobals();
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
program
|
|
99
|
-
.command('deploy')
|
|
100
|
-
.description('register a workload, create its prices and deploy it; stops at the first failed step (exit 9)')
|
|
101
|
-
.option('--template <id>', 'a starter template from the catalog (see: oars templates)')
|
|
102
|
-
.option('--image <ref>', 'an image pinned by digest (name@sha256:...)')
|
|
103
|
-
.option('--manifest <file>', 'a JSON workload manifest')
|
|
104
|
-
.option('--name <name>', 'the workload name (--image)')
|
|
105
|
-
.option('--port <n>', 'the service port (--image; default 8080)', positiveInteger)
|
|
106
|
-
.option('--public', 'expose the service publicly (--image)')
|
|
107
|
-
.option('--cpu-millicores <n>', 'CPU in millicores (--image; default 250)', positiveInteger)
|
|
108
|
-
.option('--memory-mib <n>', 'memory in MiB (--image; default 256)', positiveInteger)
|
|
109
|
-
.option('--always-on', 'keep the public service running instead of idling')
|
|
110
|
-
.option('--price <ref>', 'a starter price to create instead of all of them (--template; repeatable)', collect)
|
|
111
|
-
.option('--no-prices', "create none of the template's starter prices (--template)")
|
|
112
|
-
.option('--price-file <file>', 'a JSON array of prices to create (--image, --manifest)')
|
|
113
|
-
.option('--secret <NAME=value>', 'a secret for the workload (repeatable); prefer --secret-from-env', collect)
|
|
114
|
-
.option('--secret-from-env <NAME>', 'a secret read from the environment variable NAME (repeatable)', collect)
|
|
115
|
-
.option('--no-start', 'register and price the workload without deploying it')
|
|
116
|
-
.action(async (_options, command) => {
|
|
117
|
-
const options = command.optsWithGlobals();
|
|
118
|
-
const outcome = await deploy(deps, {
|
|
99
|
+
const authorities = list(options, 'authority');
|
|
100
|
+
const outcome = await keysCreate(deps, {
|
|
119
101
|
apiUrl: resolveApiUrl(deps, text(options, 'apiUrl')),
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
idempotencyKey: text(options, 'idempotencyKey'),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
manifestFile: text(options, 'manifest'),
|
|
126
|
-
name: text(options, 'name'),
|
|
127
|
-
port: count(options, 'port'),
|
|
128
|
-
public: flag(options, 'public') ? true : undefined,
|
|
129
|
-
cpuMillicores: count(options, 'cpuMillicores'),
|
|
130
|
-
memoryMib: count(options, 'memoryMib'),
|
|
131
|
-
alwaysOn: flag(options, 'alwaysOn') ? true : undefined,
|
|
132
|
-
prices: list(options, 'price'),
|
|
133
|
-
noPrices: options.prices === false,
|
|
134
|
-
priceFile: text(options, 'priceFile'),
|
|
135
|
-
secrets: list(options, 'secret'),
|
|
136
|
-
secretsFromEnv: list(options, 'secretFromEnv'),
|
|
102
|
+
name: text(options, 'name') ?? '',
|
|
103
|
+
authorities: authorities.length === 0 ? SCOPED_AUTHORITIES : authorities,
|
|
104
|
+
idempotencyKey: text(options, 'idempotencyKey') ?? randomUUID(),
|
|
105
|
+
...(text(options, 'workload') === undefined ? {} : { workloadId: text(options, 'workload') }),
|
|
106
|
+
...(count(options, 'expiresInDays') === undefined ? {} : { expiresInDays: count(options, 'expiresInDays') }),
|
|
137
107
|
});
|
|
138
108
|
emit(deps, flag(options, 'json'), outcome);
|
|
139
109
|
});
|
|
140
|
-
|
|
110
|
+
keys
|
|
111
|
+
.command('list')
|
|
112
|
+
.description('list your API keys (the secrets are never shown again)')
|
|
113
|
+
.action(async (_options, command) => {
|
|
114
|
+
const options = command.optsWithGlobals();
|
|
115
|
+
emit(deps, flag(options, 'json'), await keysList(deps, { apiUrl: resolveApiUrl(deps, text(options, 'apiUrl')) }));
|
|
116
|
+
});
|
|
117
|
+
keys
|
|
118
|
+
.command('revoke')
|
|
119
|
+
.description('revoke an API key at once')
|
|
120
|
+
.argument('<keyId>', 'the key id that `keys list` prints')
|
|
121
|
+
.action(async (keyId, _options, command) => {
|
|
122
|
+
const options = command.optsWithGlobals();
|
|
123
|
+
emit(deps, flag(options, 'json'), await keysRevoke(deps, { apiUrl: resolveApiUrl(deps, text(options, 'apiUrl')), keyId }));
|
|
124
|
+
});
|
|
125
|
+
const sessions = program.command('sessions').description("deposit and recall your own Claude Code session logs, locally (no model calls, no network, except `gold arms`, which asks your own Claude subscription)");
|
|
126
|
+
sessions
|
|
127
|
+
.command('deposit')
|
|
128
|
+
.description('read session logs from the folders you name into the local observation log; idempotent. Subagent transcripts under each session (<project>/<session>/subagents/) are deposited with their parent, and each raw transcript is hard-linked into the oars archive beside the store')
|
|
129
|
+
.option('--allow <folder>', 'a project folder to read: its path, or its name under ~/.claude/projects (repeatable); `cursor`, `codex`, `copilot`, `gemini`, `grok`, and `vscode-copilot` name the Cursor, Codex, Copilot CLI, Gemini CLI, Grok Build, and VS Code Copilot Chat stores. A trailing slash names everything under a folder; * and ? are wildcards', collect)
|
|
130
|
+
.option('--allow-file <list>', 'a file of --allow entries, one per line (# starts a comment); repeatable, combines with --allow', collect)
|
|
131
|
+
.option('--redact-domain <domain>', 'an organization domain to mask in everything written (repeatable)', collect)
|
|
132
|
+
.action(async (_options, command) => {
|
|
133
|
+
const options = command.optsWithGlobals();
|
|
134
|
+
emit(deps, flag(options, 'json'), await sessionsDeposit(sessionDeps(deps), { allow: list(options, 'allow'), allowFiles: list(options, 'allowFile'), redactDomains: list(options, 'redactDomain') }));
|
|
135
|
+
});
|
|
136
|
+
sessions
|
|
137
|
+
.command('recall')
|
|
138
|
+
.description('look a tool error, a file, or a prompt class up in the store (--mcp serves the same query to a running agent). Note: an error pasted with a leading <tool_use_error> line does not match a stored signature, because the extractor keeps that markup line as the signature (reference-script behavior); paste the line below it')
|
|
139
|
+
.option('--error <text>', 'an error as printed; matched by its normalized signature')
|
|
140
|
+
.option('--path <file>', 'a file path or name that earlier sessions touched')
|
|
141
|
+
.option('--class <name>', 'a prompt class (edit-verify, edit-only, shell-only, research, orchestration, explore-only, mixed, no-tools)')
|
|
142
|
+
.option('--since <days>', 'only the last N days', positiveInteger)
|
|
143
|
+
.option('--pattern <id>', 'what happened from here: a read ID for a step so far (repeatable)', collect)
|
|
144
|
+
.option('--entity <name>', 'what is known about a public name: a package, a tool, an MCP server, an action, or a model')
|
|
145
|
+
.option('--family <name>', `one read family (${RECALL_FAMILIES.join(', ')}); only recovery and fit are narrowed by --class and --since`)
|
|
146
|
+
.option('--text <text>', 'look for text in the local record')
|
|
147
|
+
.option('--session <id>', 'the session asking, so what it did next can be read for this recall')
|
|
148
|
+
.option('--counts', 'print counts of recalls by mode, arm, and answer, with what followed, instead of answering (see --day)')
|
|
149
|
+
.option('--day <yyyy-mm-dd>', 'with --counts: only this UTC day')
|
|
150
|
+
.option('--mcp', 'serve one recall tool over MCP on stdio instead of answering once')
|
|
151
|
+
.option('--context', "print the repository's top recurring errors as a short block for injection into a session (see --cwd, --harness, --top, --max-chars)")
|
|
152
|
+
.option('--cwd <dir>', "with --context: the session's working directory (default: this one)")
|
|
153
|
+
.option('--harness <version>', 'with --context: only errors seen under this Claude Code version')
|
|
154
|
+
.option('--top <n>', 'with --context: how many signatures (default 5)', positiveInteger)
|
|
155
|
+
.option('--max-chars <n>', "with --context: the block's length limit (default 1200)", positiveInteger)
|
|
156
|
+
.action(async (_options, command) => {
|
|
157
|
+
const options = command.optsWithGlobals();
|
|
158
|
+
if (flag(options, 'counts')) {
|
|
159
|
+
emit(deps, flag(options, 'json'), await sessionsRecallCounts(sessionDeps(deps), text(options, 'day')));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (flag(options, 'context')) {
|
|
163
|
+
emit(deps, flag(options, 'json'), await sessionsContext(sessionDeps(deps), {
|
|
164
|
+
cwd: text(options, 'cwd') ?? process.cwd(),
|
|
165
|
+
...(text(options, 'harness') === undefined ? {} : { harness: text(options, 'harness') }),
|
|
166
|
+
top: count(options, 'top') ?? 5,
|
|
167
|
+
maxChars: count(options, 'maxChars') ?? 1200,
|
|
168
|
+
}));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (flag(options, 'mcp')) {
|
|
172
|
+
await sessionsMcp(sessionDeps(deps));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const session = text(options, 'session') ?? null;
|
|
176
|
+
const conflict = formsRefusal([
|
|
177
|
+
...(list(options, 'pattern').length > 0 ? ['prefix'] : []),
|
|
178
|
+
...(text(options, 'entity') === undefined ? [] : ['entity']),
|
|
179
|
+
...(text(options, 'family') === undefined ? [] : ['family']),
|
|
180
|
+
...(text(options, 'text') === undefined ? [] : ['text']),
|
|
181
|
+
...(text(options, 'error') === undefined && text(options, 'path') === undefined ? [] : ['error or path']),
|
|
182
|
+
]);
|
|
183
|
+
if (conflict !== undefined)
|
|
184
|
+
throw new CliError(conflict, ExitCode.USAGE, 'usage');
|
|
185
|
+
const closed = closedQueryOf({ pattern: list(options, 'pattern'), entity: text(options, 'entity'), family: text(options, 'family'), text: text(options, 'text'), cls: text(options, 'class'), sinceDays: count(options, 'since') });
|
|
186
|
+
if (closed !== undefined) {
|
|
187
|
+
emit(deps, flag(options, 'json'), await sessionsRecall(sessionDeps(deps), closed, session));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const query = {
|
|
191
|
+
...(text(options, 'error') === undefined ? {} : { error: text(options, 'error') }),
|
|
192
|
+
...(text(options, 'path') === undefined ? {} : { path: text(options, 'path') }),
|
|
193
|
+
...(text(options, 'class') === undefined ? {} : { cls: text(options, 'class') }),
|
|
194
|
+
...(count(options, 'since') === undefined ? {} : { sinceDays: count(options, 'since') }),
|
|
195
|
+
};
|
|
196
|
+
emit(deps, flag(options, 'json'), await sessionsRecall(sessionDeps(deps), query, session));
|
|
197
|
+
});
|
|
198
|
+
sessions
|
|
199
|
+
.command('lineage <id>')
|
|
200
|
+
.description('walk a recall or read back through its evidence, one line per hop with its frame range and day; refuses evidence newer than the recall')
|
|
201
|
+
.action(async (id, _options, command) => {
|
|
202
|
+
emit(deps, flag(command.optsWithGlobals(), 'json'), await sessionsLineage(sessionDeps(deps), id));
|
|
203
|
+
});
|
|
204
|
+
sessions
|
|
205
|
+
.command('read')
|
|
206
|
+
.description('derived reads of your own sessions at every order, each row stating its order and clusters: rates at order 1, relations between them at order 2, one labeled experiment at order 3, and what this store cannot read')
|
|
207
|
+
.option('--family <name>', `only this family: ${FAMILIES.join(', ')}`)
|
|
208
|
+
.option('--slice <axis=value>', 'only reads holding this value on this axis, such as model=claude-opus-5-5 (repeatable; all must match)', collect)
|
|
209
|
+
.action(async (_options, command) => {
|
|
210
|
+
const options = command.optsWithGlobals();
|
|
211
|
+
emit(deps, flag(options, 'json'), await sessionsRead(sessionDeps(deps), { family: text(options, 'family'), slices: list(options, 'slice') }));
|
|
212
|
+
});
|
|
213
|
+
sessions
|
|
214
|
+
.command('books')
|
|
215
|
+
.description('tokens, wall time, turns, errors, verify-after-edit rate, and the outcome signals (verify passed, stops, redos, commits, and commit survival) per prompt class, model, harness, or pull')
|
|
216
|
+
.option('--by <key>', 'class, model, harness, or pull (default class)', (value) => {
|
|
217
|
+
if (value !== 'class' && value !== 'model' && value !== 'harness' && value !== 'pull')
|
|
218
|
+
throw new InvalidArgumentError('it must be class, model, harness, or pull.');
|
|
219
|
+
return value;
|
|
220
|
+
})
|
|
221
|
+
.option('--since <days>', 'only the last N days', positiveInteger)
|
|
222
|
+
.option('--repo <root>', "count only this repository's sessions (worktrees and subfolders included); without it, the whole store")
|
|
223
|
+
.action(async (_options, command) => {
|
|
224
|
+
const options = command.optsWithGlobals();
|
|
225
|
+
const picked = text(options, 'by');
|
|
226
|
+
const by = picked === 'model' || picked === 'harness' || picked === 'pull' ? picked : 'class';
|
|
227
|
+
emit(deps, flag(options, 'json'), await sessionsBooks(sessionDeps(deps), { by, sinceDays: count(options, 'since'), repo: text(options, 'repo') }));
|
|
228
|
+
});
|
|
229
|
+
sessions
|
|
230
|
+
.command('measure')
|
|
231
|
+
.description('the G1 signals for the last N sessions and the N before, and the pipeline step counters over the whole log')
|
|
232
|
+
.option('--last <n>', 'sessions per window (default 50)', positiveInteger)
|
|
233
|
+
.option('--since <iso>', 'pin the current window to the first N sessions started at or after this ISO time (the prior window is the N before it); without it the window is the last N and moves with every deposit', isoTime)
|
|
234
|
+
.option('--repo <root>', "count only this repository's sessions (worktrees and subfolders included); without it, the whole store")
|
|
235
|
+
.action(async (_options, command) => {
|
|
236
|
+
const options = command.optsWithGlobals();
|
|
237
|
+
emit(deps, flag(options, 'json'), await sessionsMeasure(sessionDeps(deps), { last: count(options, 'last') ?? 50, since: text(options, 'since'), repo: text(options, 'repo') }));
|
|
238
|
+
});
|
|
239
|
+
sessions
|
|
240
|
+
.command('verify')
|
|
241
|
+
.description('run the consistency invariants on the stored skeletons and capture records, report every session on its own, and audit each against its raw file (Merkle root, counts)')
|
|
242
|
+
.option('--session <id>', 'one session instead of all')
|
|
243
|
+
.option('--strict', 'exit non-zero (4) when any invariant has exceptions, a step is stuck, or the raw audit finds a root mismatch or counts that do not bind; by default FAIL lines still exit 0')
|
|
244
|
+
.action(async (_options, command) => {
|
|
245
|
+
const options = command.optsWithGlobals();
|
|
246
|
+
const { failed, report, ...outcome } = await sessionsVerify(sessionDeps(deps), { session: text(options, 'session') });
|
|
247
|
+
const json = flag(options, 'json');
|
|
248
|
+
if (flag(options, 'strict') && failed.length > 0) {
|
|
249
|
+
// One document under --json: the failure carries the invariants, so the success document is not printed first.
|
|
250
|
+
if (!json)
|
|
251
|
+
emit(deps, json, outcome);
|
|
252
|
+
throw new CliError(`${String(failed.length)} ${failed.length === 1 ? 'check' : 'checks'} failed: ${failed.join('; ')}`, ExitCode.INVALID, 'verify', undefined, json ? report : undefined);
|
|
253
|
+
}
|
|
254
|
+
emit(deps, json, outcome);
|
|
255
|
+
});
|
|
256
|
+
sessions
|
|
257
|
+
.command('exclusions')
|
|
258
|
+
.description('scan the local store and transcripts under the exclusion rule, write the excluded sessions to a local file, and print counts only (the term file is named by OARS_EXCLUSION_TERMS)')
|
|
259
|
+
.option('--out <file>', 'where to write the excluded sessions (default: session-commons/exclusion-list.jsonl beside the claude projects folder)')
|
|
260
|
+
.action(async (_options, command) => {
|
|
261
|
+
const options = command.optsWithGlobals();
|
|
262
|
+
if (deps.sessions === undefined)
|
|
263
|
+
throw new CliError('The session store is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
264
|
+
emit(deps, flag(options, 'json'), await sessionsExclusions(deps.sessions, exclusionPaths(deps.sessions.projectsRoot, deps.env, text(options, 'out'))));
|
|
265
|
+
});
|
|
266
|
+
const gold = sessions.command('gold').description('a blind, human-judged sample of prompts, and every signal scored against those judgments');
|
|
267
|
+
gold
|
|
268
|
+
.command('sample')
|
|
269
|
+
.description('write about 200 prompts in batches to judge blind, across prompt classes and models, with the signals in a hidden sidecar; it never overwrites a file')
|
|
270
|
+
.option('--size <n>', 'prompts to draw (default 200)', positiveInteger)
|
|
271
|
+
.option('--batch <n>', 'prompts per batch (default 50)', positiveInteger)
|
|
272
|
+
.option('--seed <text>', "the draw's seed (default today's date)")
|
|
273
|
+
.option('--exclude-term <text>', 'also leave out any session that mentions this, in any case, beyond the local term file (OARS_EXCLUSION_TERMS) that always applies (repeatable)', collect)
|
|
274
|
+
.option('--out-dir <folder>', 'where to write (default <claude config>/session-commons/review)')
|
|
275
|
+
.action(async (_options, command) => {
|
|
276
|
+
const options = command.optsWithGlobals();
|
|
277
|
+
if (deps.sessions === undefined)
|
|
278
|
+
throw new CliError('The session store is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
279
|
+
emit(deps, flag(options, 'json'), await sessionsGoldSample(sessionDeps(deps), {
|
|
280
|
+
termsFile: exclusionPaths(deps.sessions.projectsRoot, deps.env).terms,
|
|
281
|
+
size: count(options, 'size') ?? 200,
|
|
282
|
+
batch: count(options, 'batch') ?? 50,
|
|
283
|
+
seed: text(options, 'seed'),
|
|
284
|
+
excludeTerms: list(options, 'excludeTerm'),
|
|
285
|
+
outDir: text(options, 'outDir'),
|
|
286
|
+
}));
|
|
287
|
+
});
|
|
288
|
+
gold
|
|
289
|
+
.command('score')
|
|
290
|
+
.description('per signal and per model, the AUROC with its 95% interval and the base rate, from whatever is judged so far')
|
|
291
|
+
.argument('<file>', 'the gold file')
|
|
292
|
+
.action(async (file, _options, command) => {
|
|
293
|
+
const options = command.optsWithGlobals();
|
|
294
|
+
emit(deps, flag(options, 'json'), await sessionsGoldScore({ file }));
|
|
295
|
+
});
|
|
296
|
+
gold
|
|
297
|
+
.command('arms')
|
|
298
|
+
.description('fill the hidden sidecar: the closing claim, its Confidence line, and fresh single-turn readers on your Claude subscription, with fixed, hashed wording. It resumes, and never touches the gold file')
|
|
299
|
+
.argument('<file>', 'the gold file')
|
|
300
|
+
.action(async (file, _options, command) => {
|
|
301
|
+
const options = command.optsWithGlobals();
|
|
302
|
+
emit(deps, flag(options, 'json'), await sessionsGoldArms(sessionDeps(deps), { file }));
|
|
303
|
+
});
|
|
304
|
+
const hook = sessions.command('hook').description('install the recall hooks into a Claude Code settings file, and the nightly deposit task');
|
|
305
|
+
const scopeOption = (value) => {
|
|
306
|
+
if (value !== 'user' && value !== 'project' && value !== 'file')
|
|
307
|
+
throw new InvalidArgumentError('it must be user, project, or file.');
|
|
308
|
+
return value;
|
|
309
|
+
};
|
|
310
|
+
const scopeOf = (options) => {
|
|
311
|
+
const value = text(options, 'scope');
|
|
312
|
+
return value === 'user' || value === 'project' || value === 'file' ? value : undefined;
|
|
313
|
+
};
|
|
314
|
+
hook
|
|
315
|
+
.command('install')
|
|
316
|
+
.description('merge the SessionStart, PostToolUseFailure, and SessionEnd hooks into a settings file (existing hooks are kept) and raise cleanupPeriodDays to at least 365')
|
|
317
|
+
.option('--scope <scope>', 'user (~/.claude/settings.json), project (./.claude/settings.json), or file (needs --file; the default)', scopeOption)
|
|
318
|
+
.option('--file <path>', 'the settings file for --scope file')
|
|
319
|
+
.option('--keep-retention', 'leave cleanupPeriodDays as it is (the harness deletes raw transcripts older than it)')
|
|
320
|
+
.option('--schedule <mode>', 'nightly: register a Windows task that runs the full deposit and the recurrence refresh daily; off: remove it', (value) => {
|
|
321
|
+
if (value !== 'nightly' && value !== 'off')
|
|
322
|
+
throw new InvalidArgumentError('it must be nightly or off.');
|
|
323
|
+
return value;
|
|
324
|
+
})
|
|
325
|
+
.action(async (_options, command) => {
|
|
326
|
+
const options = command.optsWithGlobals();
|
|
327
|
+
const schedule = text(options, 'schedule');
|
|
328
|
+
emit(deps, flag(options, 'json'), await sessionsHookInstall(sessionDeps(deps), {
|
|
329
|
+
scope: scopeOf(options),
|
|
330
|
+
file: text(options, 'file'),
|
|
331
|
+
keepRetention: flag(options, 'keepRetention'),
|
|
332
|
+
schedule: schedule === 'nightly' || schedule === 'off' ? schedule : undefined,
|
|
333
|
+
}));
|
|
334
|
+
});
|
|
335
|
+
hook
|
|
336
|
+
.command('uninstall')
|
|
337
|
+
.description('remove only the hooks `install` wrote from a settings file (cleanupPeriodDays stays; `install --schedule off` removes the task)')
|
|
338
|
+
.option('--scope <scope>', 'user, project, or file (needs --file; the default)', scopeOption)
|
|
339
|
+
.option('--file <path>', 'the settings file for --scope file')
|
|
340
|
+
.action(async (_options, command) => {
|
|
341
|
+
const options = command.optsWithGlobals();
|
|
342
|
+
emit(deps, flag(options, 'json'), await sessionsHookUninstall(sessionDeps(deps), { scope: scopeOf(options), file: text(options, 'file') }));
|
|
343
|
+
});
|
|
344
|
+
hook
|
|
141
345
|
.command('status')
|
|
142
|
-
.description('
|
|
143
|
-
.
|
|
144
|
-
.action(async (
|
|
346
|
+
.description('what is installed in the user and project settings (and --file), and whether the nightly task is registered')
|
|
347
|
+
.option('--file <path>', 'one more settings file to report')
|
|
348
|
+
.action(async (_options, command) => {
|
|
145
349
|
const options = command.optsWithGlobals();
|
|
146
|
-
emit(deps, flag(options, 'json'), await
|
|
350
|
+
emit(deps, flag(options, 'json'), await sessionsHookStatus(sessionDeps(deps), { file: text(options, 'file') }));
|
|
147
351
|
});
|
|
148
|
-
program
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
.
|
|
152
|
-
.option('--
|
|
153
|
-
.
|
|
352
|
+
const gate = program.command('gate').description('record the install, fetch-and-run, MCP, and skill picks your agents make, live through a hook or from logs already on disk, without judging them');
|
|
353
|
+
gate
|
|
354
|
+
.command('replay')
|
|
355
|
+
.description('find the crossings in your session logs and write them to crossings.jsonl in the sessions store; reads nothing outside the folders you name, and no network unless --registry-state')
|
|
356
|
+
.option('--allow <folder>', 'a project folder to read, as for `oars sessions deposit` (repeatable); default: the allowlist a deposit saved', collect)
|
|
357
|
+
.option('--registry-state', 'ask the package manager what the registry holds for each package named (npm view, pip index versions); off by default, and the manager keeps its own cache')
|
|
358
|
+
.action(async (_options, command) => {
|
|
154
359
|
const options = command.optsWithGlobals();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
360
|
+
emit(deps, flag(options, 'json'), await gateReplay(sessionDeps(deps), { allow: list(options, 'allow'), ...(flag(options, 'registryState') ? { registry: managerReader() } : {}) }));
|
|
361
|
+
});
|
|
362
|
+
gate
|
|
363
|
+
.command('hook')
|
|
364
|
+
.description('the PreToolUse hook: reads the tool call on stdin, records any crossing in crossings.jsonl, and prints nothing unless the gate policy says to: a note as context, or a deny labeled with the fact for a name the registry does not hold; it never allows or changes a call (bin.js answers this command itself, before the rest of the CLI loads)')
|
|
365
|
+
.action(async () => {
|
|
366
|
+
const runtime = sessionDeps(deps).sessions;
|
|
367
|
+
// No store means no file: an empty store folder would put crossings.jsonl in the working folder.
|
|
368
|
+
if (runtime === undefined)
|
|
369
|
+
return;
|
|
370
|
+
let stdin = '';
|
|
371
|
+
for await (const chunk of runtime.stdin)
|
|
372
|
+
stdin += String(chunk);
|
|
373
|
+
const output = await gateHook(stdin, { storeDir: runtime.storeDir, now: deps.now, clock: () => performance.now(), deadlineMs: DEADLINE_MS });
|
|
374
|
+
if (output !== '')
|
|
375
|
+
deps.io.stdout(output);
|
|
376
|
+
});
|
|
377
|
+
gate
|
|
378
|
+
.command('install')
|
|
379
|
+
.description('wire the gate hook into Claude Code at user scope (PreToolUse for Bash, PowerShell, Skill, and MCP tools); rerunning changes nothing, and every other setting and hook is kept')
|
|
380
|
+
.action(async (_options, command) => {
|
|
381
|
+
emit(deps, flag(command.optsWithGlobals(), 'json'), await gateInstall(sessionDeps(deps)));
|
|
382
|
+
});
|
|
383
|
+
gate
|
|
384
|
+
.command('uninstall')
|
|
385
|
+
.description('remove only the gate hook `install` wrote from the user settings; crossings.jsonl stays')
|
|
386
|
+
.action(async (_options, command) => {
|
|
387
|
+
emit(deps, flag(command.optsWithGlobals(), 'json'), await gateUninstall(sessionDeps(deps)));
|
|
388
|
+
});
|
|
389
|
+
gate
|
|
390
|
+
.command('policy')
|
|
391
|
+
.description('print the gate policy in force: per class warn, block, or off, the day count for a young name, and the holdback share; read from gate-policy.json in the sessions store, whose absence or damage means the defaults')
|
|
392
|
+
.action(async (_options, command) => {
|
|
393
|
+
emit(deps, flag(command.optsWithGlobals(), 'json'), await gatePolicy(sessionDeps(deps)));
|
|
394
|
+
});
|
|
395
|
+
gate
|
|
396
|
+
.command('report')
|
|
397
|
+
.description('count the crossings in crossings.jsonl, by who recorded them, and say what replay cannot see')
|
|
398
|
+
.action(async (_options, command) => {
|
|
399
|
+
emit(deps, flag(command.optsWithGlobals(), 'json'), await gateReport(sessionDeps(deps)));
|
|
400
|
+
});
|
|
401
|
+
const pool = program.command('pool').description('sign in to a pool server, deposit closed-vocabulary session skeletons, and fetch signed reads; local capture, recall, and reads need none of it');
|
|
402
|
+
const serverOption = '--server <url>';
|
|
403
|
+
const serverHelp = 'the pool server, https or plain http to localhost (or OARS_POOL_URL; there is no default)';
|
|
404
|
+
pool
|
|
405
|
+
.command('login')
|
|
406
|
+
.description('sign in by GitHub or Google device flow, run here against the provider; prints each request before sending it, and the pool keeps a pseudonym; no harness token is read')
|
|
407
|
+
.requiredOption('--provider <name>', 'github or google')
|
|
408
|
+
.option(serverOption, serverHelp)
|
|
409
|
+
.action(async (_options, command) => {
|
|
410
|
+
const options = command.optsWithGlobals();
|
|
411
|
+
emit(deps, flag(options, 'json'), await poolLogin(deps, { server: text(options, 'server'), provider: text(options, 'provider') }));
|
|
412
|
+
});
|
|
413
|
+
pool
|
|
414
|
+
.command('deposit')
|
|
415
|
+
.description('write a batch to the outbox for each session that passes the folder allowlist and the exclusion check, and stop: nothing is sent, and only counts print. --send <hash> runs the checks that a batch was printed and is unchanged, then stops: sending is disabled in this release')
|
|
416
|
+
.option('--accept-covenant <version>', 'accept this version of the pool covenant before sending (with --send, which is disabled in this release)')
|
|
417
|
+
.option('--send <hash>', 'the SHA-256 of a batch that oars pool outbox printed')
|
|
418
|
+
.option(serverOption, serverHelp)
|
|
419
|
+
.action(async (_options, command) => {
|
|
420
|
+
const options = command.optsWithGlobals();
|
|
421
|
+
emit(deps, flag(options, 'json'), await poolDeposit(deps, { server: text(options, 'server'), acceptCovenant: text(options, 'acceptCovenant'), send: text(options, 'send') }));
|
|
422
|
+
});
|
|
423
|
+
pool
|
|
424
|
+
.command('outbox')
|
|
425
|
+
.description('print every batch exactly as it would be sent, request line and headers included, with the credential as its size; prints each batch SHA-256 and records that it was shown')
|
|
426
|
+
.option('--explain', 'print each field of a batch with the rule that admits its values')
|
|
427
|
+
.option(serverOption, serverHelp)
|
|
428
|
+
.action(async (_options, command) => {
|
|
429
|
+
const options = command.optsWithGlobals();
|
|
430
|
+
emit(deps, flag(options, 'json'), await poolOutbox(deps, { server: text(options, 'server'), explain: flag(options, 'explain') }));
|
|
431
|
+
});
|
|
432
|
+
pool
|
|
433
|
+
.command('fetch')
|
|
434
|
+
.description('fetch the signed bundle for one harness version, verify it against the key pinned at login, and keep it locally')
|
|
435
|
+
.option('--harness <name>', 'the harness whose bundle to fetch (default: the one your local store holds the most sessions of, claude-code when none leads)')
|
|
436
|
+
.requiredOption('--harness-version <version>', 'the public harness version whose bundle to fetch')
|
|
437
|
+
.option(serverOption, serverHelp)
|
|
438
|
+
.action(async (_options, command) => {
|
|
439
|
+
const options = command.optsWithGlobals();
|
|
440
|
+
emit(deps, flag(options, 'json'), await poolFetch(deps, { server: text(options, 'server'), harness: text(options, 'harness'), harnessVersion: text(options, 'harnessVersion') ?? '' }));
|
|
162
441
|
});
|
|
163
442
|
return program;
|
|
164
443
|
};
|
|
@@ -172,7 +451,7 @@ const asCliError = (error) => {
|
|
|
172
451
|
/** Runs one invocation (`argv` without the node and script entries) and returns the exit code. */
|
|
173
452
|
export const run = async (argv, deps) => {
|
|
174
453
|
try {
|
|
175
|
-
await buildProgram(deps
|
|
454
|
+
await buildProgram(deps).parseAsync(argv, { from: 'user' });
|
|
176
455
|
return ExitCode.OK;
|
|
177
456
|
}
|
|
178
457
|
catch (error) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Outcome, RunDeps } from '../cli.js';
|
|
2
|
+
/** One step's `Idempotency-Key`: the lowercase SHA-256 hex of `<base>:<step>`, the 64-hex form every platform interceptor accepts. */
|
|
3
|
+
export declare const stepKey: (base: string, step: string) => string;
|
|
4
|
+
/** The only authorities a key scoped to one workload may carry; the platform answers 400 API_KEY_WORKLOAD_SCOPE_EXCEEDS_CAP for more. */
|
|
5
|
+
export declare const SCOPED_AUTHORITIES: string[];
|
|
6
|
+
export interface KeysCreateOptions {
|
|
7
|
+
apiUrl: string;
|
|
8
|
+
name: string;
|
|
9
|
+
/** Scopes the key to this one workload; absent for an ordinary key. */
|
|
10
|
+
workloadId?: string;
|
|
11
|
+
authorities: string[];
|
|
12
|
+
expiresInDays?: number;
|
|
13
|
+
/** The base of the step key, so a retry of this command replays the same answer (and, by design, no secret). */
|
|
14
|
+
idempotencyKey: string;
|
|
15
|
+
}
|
|
16
|
+
export interface KeysListOptions {
|
|
17
|
+
apiUrl: string;
|
|
18
|
+
}
|
|
19
|
+
export interface KeysRevokeOptions {
|
|
20
|
+
apiUrl: string;
|
|
21
|
+
keyId: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `POST /me/api-keys`: with `--workload` the key works on that one workload only. The secret prints once, here; the platform never
|
|
25
|
+
* stores it, and a replay of the same request returns no secret.
|
|
26
|
+
*/
|
|
27
|
+
export declare const keysCreate: (deps: RunDeps, options: KeysCreateOptions) => Promise<Outcome>;
|
|
28
|
+
/** `GET /me/api-keys`: the secrets never come back, only the prefix. */
|
|
29
|
+
export declare const keysList: (deps: RunDeps, options: KeysListOptions) => Promise<Outcome>;
|
|
30
|
+
/** `DELETE /me/api-keys/{keyId}`: a bodyless 204; the key stops working at once. */
|
|
31
|
+
export declare const keysRevoke: (deps: RunDeps, options: KeysRevokeOptions) => Promise<Outcome>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { apiClient, unwrap } from '../api.js';
|
|
3
|
+
import { CliError, ExitCode } from '../errors.js';
|
|
4
|
+
import { resolveAuthToken } from '../session.js';
|
|
5
|
+
/** One step's `Idempotency-Key`: the lowercase SHA-256 hex of `<base>:<step>`, the 64-hex form every platform interceptor accepts. */
|
|
6
|
+
export const stepKey = (base, step) => createHash('sha256').update(`${base}:${step}`).digest('hex');
|
|
7
|
+
const CREATE_STEP = 'keys-create';
|
|
8
|
+
const LIST_STEP = 'keys-list';
|
|
9
|
+
const REVOKE_STEP = 'keys-revoke';
|
|
10
|
+
// A developer's keys fit one request nearly always; the loop below only covers one that outgrows it.
|
|
11
|
+
const PAGE_SIZE = 100;
|
|
12
|
+
/** The only authorities a key scoped to one workload may carry; the platform answers 400 API_KEY_WORKLOAD_SCOPE_EXCEEDS_CAP for more. */
|
|
13
|
+
export const SCOPED_AUTHORITIES = ['AGENT_OPERATE', 'AGENT_READ'];
|
|
14
|
+
const describeKey = (key) => [
|
|
15
|
+
key.keyId,
|
|
16
|
+
key.name,
|
|
17
|
+
key.status,
|
|
18
|
+
`${key.prefix}...`,
|
|
19
|
+
key.workloadId === undefined ? 'all workloads' : `workload ${key.workloadId}`,
|
|
20
|
+
key.authorities.join(','),
|
|
21
|
+
key.expiresAt === undefined ? 'no expiry' : `expires ${key.expiresAt}`,
|
|
22
|
+
].join(' ');
|
|
23
|
+
/**
|
|
24
|
+
* `POST /me/api-keys`: with `--workload` the key works on that one workload only. The secret prints once, here; the platform never
|
|
25
|
+
* stores it, and a replay of the same request returns no secret.
|
|
26
|
+
*/
|
|
27
|
+
export const keysCreate = async (deps, options) => {
|
|
28
|
+
const client = apiClient(deps, { apiUrl: options.apiUrl, step: CREATE_STEP, token: await resolveAuthToken(deps, options.apiUrl) });
|
|
29
|
+
const issued = unwrap(CREATE_STEP, await client.POST('/me/api-keys', {
|
|
30
|
+
params: { header: { 'Idempotency-Key': stepKey(options.idempotencyKey, CREATE_STEP) } },
|
|
31
|
+
body: {
|
|
32
|
+
name: options.name,
|
|
33
|
+
authorities: options.authorities,
|
|
34
|
+
...(options.workloadId === undefined ? {} : { workloadId: options.workloadId }),
|
|
35
|
+
...(options.expiresInDays === undefined ? {} : { expiresInDays: options.expiresInDays }),
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
if (issued.secret === undefined) {
|
|
39
|
+
throw new CliError(`Key ${issued.key.keyId} was already created and its secret was shown once; the platform keeps no copy. Revoke it and create another.`, ExitCode.CONFLICT, CREATE_STEP);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
document: { ok: true, key: issued.key, secret: issued.secret },
|
|
43
|
+
text: [`Created key: ${describeKey(issued.key)}.`, `Secret (shown once; save it now): ${issued.secret}`].join('\n'),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/** `GET /me/api-keys`: the secrets never come back, only the prefix. */
|
|
47
|
+
export const keysList = async (deps, options) => {
|
|
48
|
+
const client = apiClient(deps, { apiUrl: options.apiUrl, step: LIST_STEP, token: await resolveAuthToken(deps, options.apiUrl) });
|
|
49
|
+
const keys = [];
|
|
50
|
+
for (let page = 0;; page += 1) {
|
|
51
|
+
const result = unwrap(LIST_STEP, await client.GET('/me/api-keys', { params: { query: { page, size: PAGE_SIZE } } }));
|
|
52
|
+
keys.push(...result.content);
|
|
53
|
+
if (result.last)
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
return { document: { ok: true, keys }, text: keys.length === 0 ? 'You have no API keys.' : keys.map(describeKey).join('\n') };
|
|
57
|
+
};
|
|
58
|
+
/** `DELETE /me/api-keys/{keyId}`: a bodyless 204; the key stops working at once. */
|
|
59
|
+
export const keysRevoke = async (deps, options) => {
|
|
60
|
+
const client = apiClient(deps, { apiUrl: options.apiUrl, step: REVOKE_STEP, token: await resolveAuthToken(deps, options.apiUrl) });
|
|
61
|
+
const { response, error } = await client.DELETE('/me/api-keys/{keyId}', { params: { path: { keyId: options.keyId } } });
|
|
62
|
+
if (!response.ok)
|
|
63
|
+
unwrap(REVOKE_STEP, { error, response });
|
|
64
|
+
return { document: { ok: true, keyId: options.keyId, revoked: true }, text: `Revoked key ${options.keyId}.` };
|
|
65
|
+
};
|
package/dist/commands/login.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { CliError, ExitCode } from '../errors.js';
|
|
2
|
-
import { resolveIssuerUrl } from '../issuer.js';
|
|
2
|
+
import { discoverIssuer, resolveIssuerUrl } from '../issuer.js';
|
|
3
3
|
import { awaitDeviceTokens, discover, startDeviceAuthorization } from '../oidc.js';
|
|
4
4
|
import { exchangeKeycloakToken } from '../session.js';
|
|
5
5
|
const STEP = 'login';
|
|
6
6
|
/** RFC 8628 device flow on Keycloak, then the exchange; stores the credentials. */
|
|
7
7
|
export const login = async (deps, options) => {
|
|
8
8
|
const issuerUrl = await resolveIssuerUrl(deps, options.issuerUrl);
|
|
9
|
-
const config = await discover(issuerUrl, { ...deps, step: STEP });
|
|
9
|
+
const config = await discoverIssuer(deps, options.issuerUrl, () => discover(issuerUrl, { ...deps, step: STEP }));
|
|
10
10
|
const device = await startDeviceAuthorization(config, STEP);
|
|
11
11
|
const url = device.verification_uri_complete ?? device.verification_uri;
|
|
12
12
|
deps.io.stderr(`To sign in, open ${url}\nand confirm the code ${device.user_code}\n`);
|