@manyoars/oars 0.3.0 → 0.4.1
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 +44 -29
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +102 -32
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/gate/hook-main.js +13 -1
- package/dist/gate/hook-process.d.ts +11 -1
- package/dist/gate/hook-process.js +50 -2
- package/dist/gate/hook-record.d.ts +3 -0
- package/dist/gate/hook-record.js +8 -0
- package/dist/gate/hook.d.ts +2 -0
- package/dist/gate/hook.js +3 -0
- package/dist/gate/replay.js +2 -5
- package/dist/gate/report.js +1 -1
- package/dist/gate/transcripts.js +1 -1
- package/dist/hook-bin.js +27 -2
- package/dist/issuer.d.ts +7 -9
- package/dist/issuer.js +10 -32
- package/dist/main.js +3 -0
- package/dist/sessions/actions.js +78 -0
- package/dist/sessions/allow.d.ts +10 -1
- package/dist/sessions/allow.js +29 -9
- package/dist/sessions/baseline.d.ts +1 -1
- package/dist/sessions/baseline.js +6 -4
- package/dist/sessions/call-shape.d.ts +19 -0
- package/dist/sessions/call-shape.js +85 -0
- package/dist/sessions/cell-increments.d.ts +5 -1
- package/dist/sessions/cell-increments.js +84 -50
- package/dist/sessions/cells.d.ts +33 -3
- package/dist/sessions/cells.js +89 -9
- package/dist/sessions/codex.js +93 -27
- package/dist/sessions/command.d.ts +11 -1
- package/dist/sessions/command.js +64 -18
- package/dist/sessions/coverage.d.ts +19 -0
- package/dist/sessions/coverage.js +56 -0
- package/dist/sessions/cursor.js +2 -2
- package/dist/sessions/derive.js +72 -6
- package/dist/sessions/exclusion.d.ts +6 -0
- package/dist/sessions/exclusion.js +8 -0
- package/dist/sessions/extract.d.ts +4 -2
- package/dist/sessions/extract.js +58 -26
- package/dist/sessions/hook-calls.js +1 -1
- package/dist/sessions/hook-index.d.ts +14 -1
- package/dist/sessions/hook-index.js +10 -3
- package/dist/sessions/hooks.d.ts +5 -1
- package/dist/sessions/hooks.js +133 -22
- package/dist/sessions/identity.d.ts +8 -3
- package/dist/sessions/identity.js +45 -15
- package/dist/sessions/install.d.ts +18 -2
- package/dist/sessions/install.js +53 -8
- package/dist/sessions/mcp.d.ts +3 -2
- package/dist/sessions/mcp.js +8 -4
- package/dist/sessions/motif.d.ts +2 -1
- package/dist/sessions/motif.js +2 -2
- package/dist/sessions/opencode.d.ts +33 -0
- package/dist/sessions/opencode.js +319 -0
- package/dist/sessions/otel.d.ts +60 -0
- package/dist/sessions/otel.js +494 -0
- package/dist/sessions/package-books.d.ts +48 -0
- package/dist/sessions/package-books.js +120 -0
- package/dist/sessions/package-resolve.d.ts +2 -0
- package/dist/sessions/package-resolve.js +3 -1
- package/dist/sessions/pi.d.ts +36 -0
- package/dist/sessions/pi.js +353 -0
- package/dist/sessions/pool-books.d.ts +21 -0
- package/dist/sessions/pool-books.js +75 -0
- package/dist/sessions/pool-choices.d.ts +5 -0
- package/dist/sessions/pool-choices.js +11 -0
- package/dist/sessions/pool-command.d.ts +76 -5
- package/dist/sessions/pool-command.js +263 -136
- package/dist/sessions/pool-nightly-deps.d.ts +3 -0
- package/dist/sessions/pool-nightly-deps.js +11 -0
- package/dist/sessions/pool-nightly.d.ts +10 -0
- package/dist/sessions/pool-nightly.js +85 -0
- package/dist/sessions/pool-outbox.d.ts +15 -2
- package/dist/sessions/pool-outbox.js +79 -26
- package/dist/sessions/pool-print.d.ts +1 -1
- package/dist/sessions/pool-raw.d.ts +27 -0
- package/dist/sessions/pool-raw.js +68 -0
- package/dist/sessions/pool-replica.d.ts +35 -0
- package/dist/sessions/pool-replica.js +91 -0
- package/dist/sessions/pool-send-lock.d.ts +20 -0
- package/dist/sessions/pool-send-lock.js +87 -0
- package/dist/sessions/pool-sent.d.ts +4 -0
- package/dist/sessions/pool-sent.js +26 -0
- package/dist/sessions/pool-totals.d.ts +10 -0
- package/dist/sessions/pool-totals.js +21 -0
- package/dist/sessions/pool-wire.d.ts +240 -30
- package/dist/sessions/pool-wire.js +695 -215
- package/dist/sessions/public-vocabulary.d.ts +2 -3
- package/dist/sessions/public-vocabulary.js +2 -9
- package/dist/sessions/query.d.ts +16 -4
- package/dist/sessions/query.js +32 -10
- package/dist/sessions/read-cost.js +14 -3
- package/dist/sessions/read-recovery-edge.d.ts +2 -0
- package/dist/sessions/read-recovery-edge.js +26 -0
- package/dist/sessions/read.d.ts +8 -1
- package/dist/sessions/read.js +6 -3
- package/dist/sessions/recall-bundle.d.ts +12 -0
- package/dist/sessions/recall-bundle.js +33 -0
- package/dist/sessions/recall-frames.d.ts +18 -0
- package/dist/sessions/recall-frames.js +44 -0
- package/dist/sessions/recall-next.d.ts +145 -0
- package/dist/sessions/recall-next.js +241 -0
- package/dist/sessions/recall-object.d.ts +2 -0
- package/dist/sessions/recall-object.js +1 -0
- package/dist/sessions/recall-push.d.ts +2 -2
- package/dist/sessions/recall-push.js +3 -51
- package/dist/sessions/recall-reads.d.ts +7 -0
- package/dist/sessions/recall-reads.js +7 -0
- package/dist/sessions/recovery-calls.d.ts +21 -0
- package/dist/sessions/recovery-calls.js +25 -0
- package/dist/sessions/redact.d.ts +11 -0
- package/dist/sessions/redact.js +59 -0
- package/dist/sessions/schema.d.ts +59 -9
- package/dist/sessions/schema.js +22 -3
- package/dist/sessions/session-shape.d.ts +6 -3
- package/dist/sessions/session-shape.js +19 -6
- package/dist/sessions/sources.js +75 -1
- package/dist/sessions/store.d.ts +13 -1
- package/dist/sessions/store.js +36 -16
- package/dist/sessions/survival.d.ts +2 -0
- package/dist/sessions/survival.js +2 -2
- package/dist/sessions/transcript-tail.d.ts +14 -0
- package/dist/sessions/transcript-tail.js +64 -0
- package/dist/sessions/vscode-copilot.js +24 -7
- package/dist/telemetry/command.d.ts +8 -0
- package/dist/telemetry/command.js +85 -0
- package/dist/telemetry/consent.d.ts +17 -0
- package/dist/telemetry/consent.js +63 -0
- package/dist/telemetry/crash.d.ts +8 -0
- package/dist/telemetry/crash.js +52 -0
- package/dist/telemetry/emit.d.ts +31 -0
- package/dist/telemetry/emit.js +42 -0
- package/dist/telemetry/fields.d.ts +16 -0
- package/dist/telemetry/fields.js +26 -0
- package/dist/telemetry/health.d.ts +21 -0
- package/dist/telemetry/health.js +108 -0
- package/dist/telemetry/hook-emit.d.ts +4 -0
- package/dist/telemetry/hook-emit.js +18 -0
- package/dist/telemetry/measures.d.ts +32 -0
- package/dist/telemetry/measures.js +128 -0
- package/dist/telemetry/observe.d.ts +13 -0
- package/dist/telemetry/observe.js +51 -0
- package/dist/telemetry/read-events.d.ts +9 -0
- package/dist/telemetry/read-events.js +17 -0
- package/dist/telemetry/record.d.ts +17 -0
- package/dist/telemetry/record.js +26 -0
- package/dist/telemetry/report.d.ts +77 -0
- package/dist/telemetry/report.js +188 -0
- package/dist/telemetry/schema.d.ts +40 -0
- package/dist/telemetry/schema.js +187 -0
- package/dist/telemetry/send-main.d.ts +1 -0
- package/dist/telemetry/send-main.js +11 -0
- package/dist/telemetry/send.d.ts +36 -0
- package/dist/telemetry/send.js +172 -0
- package/dist/telemetry/spool.d.ts +17 -0
- package/dist/telemetry/spool.js +70 -0
- package/dist/telemetry/wire.d.ts +25 -0
- package/dist/telemetry/wire.js +76 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @manyoars/oars
|
|
2
2
|
|
|
3
|
-
The `oars` command line for Many Oars: sign in, manage API keys, and keep a local memory of your own
|
|
3
|
+
The `oars` command line for Many Oars: sign in, manage API keys, and keep a local memory of your own coding-agent sessions (`oars sessions`).
|
|
4
4
|
|
|
5
|
-
Accounts are not open yet
|
|
5
|
+
Accounts are not open yet: `oars login` uses the Many Oars issuer by default, and until Many Oars sign-in at `login.manyoars.com` is deployed it exits 2 with an error that names `login.manyoars.com`.
|
|
6
6
|
|
|
7
7
|
Node 24 or later. ESM only.
|
|
8
8
|
|
|
@@ -34,7 +34,7 @@ These flags work on every command:
|
|
|
34
34
|
Signs in with your browser (RFC 8628 device flow) and stores the login.
|
|
35
35
|
|
|
36
36
|
- `--no-browser` prints the link and code without opening a browser.
|
|
37
|
-
- `--issuer-url <url>` is the Keycloak realm URL. The default is `OARS_ISSUER_URL`. With neither, `login` uses the built-in Many Oars issuer
|
|
37
|
+
- `--issuer-url <url>` is the Keycloak realm URL. The default is `OARS_ISSUER_URL`. With neither, `login` uses the built-in Many Oars issuer, `https://login.manyoars.com/realms/aor`; nothing has to be switched on first. When the built-in issuer does not answer discovery (it may not be deployed yet), with no `--issuer-url` or `OARS_ISSUER_URL`, the command prints "Cannot reach Many Oars sign-in at login.manyoars.com (it may not be deployed yet). Pass --issuer-url or set OARS_ISSUER_URL to use another issuer." and exits 2.
|
|
38
38
|
|
|
39
39
|
### logout
|
|
40
40
|
|
|
@@ -44,24 +44,37 @@ Revokes the stored login and deletes it.
|
|
|
44
44
|
|
|
45
45
|
`oars keys` manages the API keys of your account.
|
|
46
46
|
|
|
47
|
-
- `oars keys create --name <name>` creates a key and prints its secret once; the platform keeps no copy. The default authorities are `AGENT_OPERATE` and `AGENT_READ`; `--authority <name>` (repeatable) sets others, and `--expires-in-days <n>` sets an expiry.
|
|
47
|
+
- `oars keys create --name <name>` creates a key and prints its secret once; the platform keeps no copy. The default authorities are `AGENT_OPERATE` and `AGENT_READ`; `--authority <name>` (repeatable) sets others, and `--expires-in-days <n>` sets an expiry. `--expires-in-days` takes 1 to 365; without it the platform sets the expiry, and `oars keys list` shows it. Replace a key before it expires.
|
|
48
48
|
- `--workload <id>` limits the key to one workload you created. Such a key may carry only `AGENT_OPERATE` and `AGENT_READ` (more is a 400, `API_KEY_WORKLOAD_SCOPE_EXCEEDS_CAP`), and it works on that workload only.
|
|
49
49
|
- A retry with the same `--idempotency-key` returns no secret: the command exits 5 and names the key, and you revoke it and create another.
|
|
50
50
|
- Creating or revoking a key needs a signed-in person. With `OARS_API_KEY` set, any API key gets 403 `HUMAN_SESSION_REQUIRED` (exit 3) and logging in again does not help: unset it and use the `oars login` session.
|
|
51
51
|
- `oars keys list` lists the keys with their prefix, scope, and expiry. A secret never comes back.
|
|
52
52
|
- `oars keys revoke <keyId>` ends a key at once. There is no `rotate`: revoke the key and create another.
|
|
53
53
|
|
|
54
|
+
### telemetry
|
|
55
|
+
|
|
56
|
+
oars records how it is used, on this machine, and says so: on the first run that is not a hook it prints a notice on stderr, once. Each command leaves an event in a local spool under `<config>/oars/telemetry/`: the command path, the names of the flags given (never their values), the duration, the exit code, a closed error class, the oars version, the OS, and the time. So does each fire of a hook (the bare `oars gate hook` included, written after its answer is printed: which hook, how long, hit, miss, held, or error, the bytes it printed, and the harness name and version and the model name when the hook saw them), each recall (how it was asked and which read families it served, as counts), each deposit pass or sent batch (sessions counted, batches, bytes, and how many sessions each gate kept out, never which), and each fetch (an ETag hit, a cap hit). Five more kinds of event are recorded, each only counts, sizes, and closed words: once a night, a `store-health` snapshot of the session store (counts and sizes, and the lists of harness versions and model names it holds) and a `run-cost` event (time and peak memory); a `crash` event when oars fails in a way it did not plan for (the error's class and a hash of its stack, never its message); and, for each `oars pool deposit` pass, one `wire` event per harness (which fields its batches held and how many bytes each took) and, with `--send`, a `wire-send` event (the HTTP status, the pool's refusal code, and the size). Nothing else is recorded: no session content, prompt text, file path, URL, or argument. Every field of every event, with an example, is in [docs/telemetry.md](https://github.com/manyoars/oars/blob/main/docs/telemetry.md). oars sends the events in batches to the pool server in `OARS_POOL_URL` if you set one, else to the Many Oars pool (`https://pool.manyoars.com`), which is live. A per-install random id (not derived from your machine or account) is in each event.
|
|
57
|
+
|
|
58
|
+
- `oars telemetry status` says whether it is on, which switch decided that, whether the notice was shown, and how large the spool is.
|
|
59
|
+
- `oars telemetry off` stops recording and sending; `oars telemetry on` resumes it and prints the notice again.
|
|
60
|
+
- `oars telemetry show` prints the spool exactly as stored, oldest first.
|
|
61
|
+
- `oars telemetry report` prints your own numbers from the spool (hook latency percentiles, hit rates, and store health over the last 7 and 30 days) with no server and no account, and the fleet's percentile beside each when `OARS_POOL_URL` names a server that serves it.
|
|
62
|
+
- `OARS_TELEMETRY=0` or `DO_NOT_TRACK=1` turns it off for the process, and wins over `oars telemetry on`.
|
|
63
|
+
|
|
64
|
+
Every event type, with an example, is in [docs/telemetry.md](https://github.com/manyoars/oars/blob/main/docs/telemetry.md).
|
|
65
|
+
|
|
54
66
|
### sessions
|
|
55
67
|
|
|
56
|
-
`oars sessions` keeps a local, deterministic memory of your own
|
|
68
|
+
`oars sessions` keeps a local, deterministic memory of your own coding-agent sessions. The session commands make no model call and no network call, except `gold arms`, which sends redacted text through your own Claude subscription. oars also records telemetry events (a command's path, its flag names, how long it took, its exit code, and its error class, with a random install id, the oars version, the OS, and the time; never session content, prompts, paths, URLs, or argument values), which oars sends in batches to the pool server in `OARS_POOL_URL`, else to the Many Oars pool (live) ([what telemetry records and how to turn it off](https://github.com/manyoars/oars/blob/main/docs/telemetry.md)).
|
|
57
69
|
|
|
58
|
-
What it captures:
|
|
70
|
+
What it captures: by default, the sessions of every Claude Code project folder and the sessions of Cursor, Codex, Copilot CLI, Gemini CLI, Grok Build, OpenCode, Pi, and VS Code Copilot Chat, plus OpenTelemetry GenAI agent traces (OTLP JSON files) from the folder `OARS_OTEL_TRACES` names (else `<config>/oars/otel-traces`), when there is one. `--allow` narrows that to the folders and stores you name (`--allow cursor`, `codex`, `copilot`, `gemini`, `grok`, `opencode`, `pi`, `vscode-copilot`, `otel` name the stores), and `--deny` keeps one out. Where it stores them: `<config>/oars/sessions/` on your machine (`<config>` is `%APPDATA%` on Windows, `$XDG_CONFIG_HOME` or `~/.config` elsewhere). The `raw/` folder in it is on by default and holds raw transcripts (a hard link or copy of each deposited one). Secret masking applies to the log, not to `raw/`: those files are unredacted and keep whatever the transcript held, secrets included. They stay on your machine: apart from `gold arms`, no session command uploads them, telemetry carries nothing from them, and after a yes to the raw prompt (`oars pool raw`) and covenant acceptance, the nightly task uploads a redacted copy of each (never the file itself), except from a folder set to skeleton only or off. To remove them, see [Remove everything](#remove-everything).
|
|
59
71
|
|
|
60
|
-
Apart from that, oars itself sends nothing; the hooks and the MCP tool add recall text to the running session, and your harness sends that to its model like any other context.
|
|
72
|
+
Apart from that and telemetry (on by default, off with `oars telemetry off`, `OARS_TELEMETRY=0`, or `DO_NOT_TRACK=1`; it sends to the pool server in `OARS_POOL_URL`, else to the Many Oars pool at `pool.manyoars.com`, which is live), oars itself sends nothing, except what you opted into with a pool server: after you accept its covenant, the nightly task (registered on Windows only) sends closed-vocabulary session skeletons, and, only after a separate yes to the raw prompt, a locally redacted (best-effort) copy of each full transcript; `oars pool outbox` prints what is queued when you run it, but the nightly task builds new batches and sends them in the same run without printing them first, and a folder can be switched to skeleton only or off; the hooks and the MCP tool add recall text to the running session, and your harness sends that to its model like any other context.
|
|
61
73
|
|
|
62
74
|
```bash
|
|
63
|
-
# Read the
|
|
64
|
-
oars sessions deposit --
|
|
75
|
+
# Read every folder; or only the ones you name (a path, or its name under ~/.claude/projects); --deny keeps one out.
|
|
76
|
+
oars sessions deposit --redact-domain corp.example
|
|
77
|
+
oars sessions deposit --allow ~/work/app --allow ~/work/tools --deny ~/work/tools/secret
|
|
65
78
|
|
|
66
79
|
# Look up an error as printed, a file, or a prompt class.
|
|
67
80
|
oars sessions recall --error "unexpected EOF while looking for matching \`''"
|
|
@@ -83,7 +96,7 @@ oars sessions read --family lead-lag # order 3: a labeled experiment beh
|
|
|
83
96
|
oars sessions read --family fit --slice model=claude-opus-5-5 --json
|
|
84
97
|
oars sessions books --by class --since 30 # tokens, wall time, turns, errors, verify-after-edit rate, and the outcome signals
|
|
85
98
|
oars sessions books --by pull # the same per package, MCP server, plugin, or skill a prompt pulled
|
|
86
|
-
oars sessions books --by harness # the same, grouped by Claude Code, Cursor, Codex, Copilot, Gemini, Grok Build, and VS Code Copilot Chat (deposit the others with --allow cursor, --allow codex, --allow copilot, --allow gemini, --allow grok,
|
|
99
|
+
oars sessions books --by harness # the same, grouped by Claude Code, Cursor, Codex, Copilot, Gemini, Grok Build, OpenCode, Pi, and VS Code Copilot Chat (deposit the others with --allow cursor, --allow codex, --allow copilot, --allow gemini, --allow grok, --allow opencode, --allow pi, --allow vscode-copilot, or --allow otel)
|
|
87
100
|
oars sessions measure --last 50 # recurring errors per 100 sessions, cache-write per session, recall calls per session
|
|
88
101
|
oars sessions measure --last 50 --repo <root> # the same, for one repository's sessions
|
|
89
102
|
oars sessions verify # consistency invariants on the stored skeletons and capture records
|
|
@@ -99,7 +112,7 @@ before a line is written (redaction runs on the full text, then the text is cut)
|
|
|
99
112
|
text, never raw content. It keeps these text fields and no others: the first 300 characters of each prompt, the first informative line of a
|
|
100
113
|
tool error (cut to 1,000 characters in the error-line record, 160 characters in a derived example), TLDR lines (400) and Confidence lines (120) from assistant text, WebSearch queries, fetched URLs, file
|
|
101
114
|
paths, and git branch names (the last four have no length limit), all redacted first; plus the project folder slug (not redacted, so it
|
|
102
|
-
carries your username and directory names; the log stays local), skill names, effort values, model names, harness versions, tool names (including MCP server names), a shell call's program and subcommand, and tool-input key names. A tool call's file target is a salted hash, never the path. Apart from `gold arms`,
|
|
115
|
+
carries your username and directory names; the log stays local), skill names, effort values, model names, harness versions, tool names (including MCP server names), a shell call's program and subcommand, and tool-input key names. A tool call's file target is a salted hash, never the path. Apart from `gold arms`, no `oars sessions` command uploads anything, and telemetry carries none of these fields; `oars pool` and the nightly task send skeletons that carry some of these names after redaction (skill, effort, model, harness version, tool, and MCP server names, a shell call's program, subcommand, and flag names, tool-input key names, a pulled package's name (and its version only when a public list holds it), and a web host (sent as it is after redaction; the pool hashes it on arrival), each after redaction; see `docs/pool.md`, "Wire field register").
|
|
103
116
|
Subagent transcripts (`<project>/<session>/subagents/`) are deposited with their parent session and subagent type; `books --by class`
|
|
104
117
|
shows their tokens as the `delegated` column, and their errors are in the signatures, `measure`, and recall under the session that
|
|
105
118
|
spawned them, so per-session counts still count main sessions. Every deposited transcript is also
|
|
@@ -109,25 +122,26 @@ field, and a Merkle root over the raw lines) that `verify` checks; the skeleton
|
|
|
109
122
|
per hundred sessions and, on a second line, the distinct recurring signatures in the window.
|
|
110
123
|
#### Many folders: `--allow` prefixes, globs, and `--allow-file`
|
|
111
124
|
|
|
112
|
-
An `--allow` entry is a folder (or its slug), a folder prefix with a trailing slash (`~/work/` reads that folder and every project under it), or a glob (`*` any run of characters, `?` one character, matched against the project-folder name Claude Code derives from the path). `--allow-file <list>` reads entries from a file, one per line (blank lines and `#` comments skipped), so hundreds of folders need no batching and no long command line. A leading `~` is expanded to your home folder, in a list file or quoted entry too. Both repeat and combine. Entries a deposit saves are what the SessionEnd hook
|
|
125
|
+
An `--allow` entry is a folder (or its slug), a folder prefix with a trailing slash (`~/work/` reads that folder and every project under it), or a glob (`*` any run of characters, `?` one character, matched against the project-folder name Claude Code derives from the path). `--allow-file <list>` reads entries from a file, one per line (blank lines and `#` comments skipped), so hundreds of folders need no batching and no long command line. A leading `~` is expanded to your home folder, in a list file or quoted entry too. Both repeat and combine. With neither, a deposit reads every folder. `--deny <entry>` (or `!<entry>` in a list file) keeps the folders it names out, in any of those forms; it is saved, and it wins over every `--allow` until that exact entry is named with `--allow` again. Naming it again lifts the denial and nothing else when the saved list names no folder (every folder stays in scope); when the saved list names folders, the entry joins them. Entries a deposit saves are what the SessionEnd hook, the nightly task, `oars pool deposit`, and `oars gate replay` reuse; with none saved, they read every folder. An `--allow-file` that names nothing is refused.
|
|
113
126
|
|
|
114
127
|
#### Recall in a running session: hooks
|
|
115
128
|
|
|
116
|
-
`oars sessions hook install` writes
|
|
129
|
+
`oars sessions hook install` writes five entries into one Claude Code settings file. Each is a deterministic, local, fail-open call into the store (no model, no network); an error inside a hook exits 0 with no output, so a hook never blocks or fails a session.
|
|
117
130
|
|
|
118
|
-
| Event | Does |
|
|
131
|
+
| Event | Does | Timeout written at install |
|
|
119
132
|
|---|---|---|
|
|
120
|
-
| `SessionStart` | injects `oars sessions recall --context` for the session's cwd as additional context: the top 5 errors by sessions within the latest 300 sessions of the repository (the `measure --last 300 --repo` window) and what usually followed | under 500 ms |
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
133
|
+
| `SessionStart` | injects `oars sessions recall --context` for the session's cwd as additional context: the top 5 errors by sessions within the latest 300 sessions of the repository (the `measure --last 300 --repo` window) and what usually followed. Then, when any position qualifies, a table of the positions where earlier sessions split on the next step (a leading step at 20 to 80 percent of at least 20 observations; names and counts only, at most 600 bytes), from the kept pool bundle for the session's harness and version first and this store's pool second (the local half is derived over that harness and version bucket only, or over the harness's versions when the transcript states none). The table is one `recalls.jsonl` row and falls under the 10 percent holdback | under 500 ms |
|
|
134
|
+
| `UserPromptSubmit` | the same table again, only when it differs from the one this session was last sent (shown or held), so an unchanged table costs a prompt nothing (the check reads the last 64 KB of the recall log, shared by every session, so in a long or parallel session the last table row can fall out of the window and an unchanged table is sent again) | under 500 ms |
|
|
135
|
+
| `PreToolUse` (no matcher: every tool) | the gate hook's role: it fires after the call is decided, so it is a warning and does not claim to move the next step. For a call whose tool and input key names match a shape that fails at least half its calls (of at least 20 calls in at least 3 sessions), and at least twice as often as its action does, injects its counts as additional context, once per session and shape (the check reads the last 64 KB of the recall log, about 44 rows, so a very long session can be told again, and the arm redraws when the nightly index moves), and records the recall. 10 percent of those sessions and shapes are held back: a held arm records what it would have shown and injects nothing. Starts node per call | under 500 ms |
|
|
136
|
+
| `PostToolUseFailure` (matcher: every tool the action table maps, and `mcp__.*`) | the recovery edge: for a failed call it reads what followed this failure (same action and error class) in the kept pool bundle, else in this store, and adds "after a failed X (code), the next step was Y xN". It stays silent when the top next step is the failed action again (a retry), when a local edge has fewer than 5 followed steps, and when nothing followed. A call the cells do not cover keeps the older line, "seen in N sessions; what followed: X", under the same holdback, silent on a strict retry lead. Every served or held line is a `recalls.jsonl` row; 10 percent of sessions are held. For Bash and PowerShell a shell pre-filter in the registered command reads the event first and starts node only when it carries `Exit code` | pre-filter: shell builtins only, no node start; whole path under 1 s |
|
|
137
|
+
| `SessionEnd` | deposits that one transcript (idempotent by size and mtime), only from a folder the saved allowlist holds (every folder until a deposit names some, less any `--deny`). Once per session: `Stop` fires after every turn and would append a new revision of the growing transcript each time. A session the harness ends without `SessionEnd` is picked up by the nightly deposit | under 5 s |
|
|
124
138
|
|
|
125
|
-
Claude Code fires `PostToolUse` on success only; a failed Bash call arrives as `PostToolUseFailure`, so that is the event registered. The pre-filter is a `read`, a `case`, and a `printf | node` in the shell the harness runs hooks in (Git Bash on Windows
|
|
139
|
+
Claude Code fires `PostToolUse` on success only; a failed Bash call arrives as `PostToolUseFailure`, so that is the event registered. The pre-filter is a `read`, a `case`, and a `printf | node` in the shell the harness runs hooks in (Git Bash on Windows), so it needs no `findstr` and no extra dependency. Writers of the store (`SessionEnd`, the nightly run, `deposit`) take `store.lock`, so parallel sessions cannot lose each other's `state.json` entries; a lock older than ten minutes is broken.
|
|
126
140
|
|
|
127
141
|
```bash
|
|
128
|
-
#
|
|
142
|
+
# The default is the user settings file; --scope project or --file names another.
|
|
143
|
+
oars sessions hook install # ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR)
|
|
129
144
|
oars sessions hook install --scope file --file ./.claude/settings.local.json
|
|
130
|
-
oars sessions hook install --scope user # ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR)
|
|
131
145
|
oars sessions hook install --scope project # ./.claude/settings.json
|
|
132
146
|
oars sessions hook install --file my-settings.json --keep-retention
|
|
133
147
|
oars sessions hook status # user, project, and the nightly task
|
|
@@ -138,7 +152,7 @@ Install merges: existing hooks (a Stop hook that rotates sessions, say) stay, ev
|
|
|
138
152
|
|
|
139
153
|
#### Nightly schedule (Windows)
|
|
140
154
|
|
|
141
|
-
`oars sessions hook install
|
|
155
|
+
On Windows, `oars sessions hook install` registers the Task Scheduler task `OarsSessionsNightly` (daily, 03:00) unless it is already there; `--schedule nightly` registers it by itself, with no settings file. The task runs the full deposit over the saved `--allow` and `--redact-domain` entries and refreshes the recurrence index the hooks read. `--schedule off` removes it and records the choice in the store (`<config>/oars/sessions/schedule.json`), so a later plain install leaves it off until `--schedule nightly` registers it again and clears the record; `hook status` shows whether it is registered. A machine that refuses the task still gets the hooks, and the install says how to retry. Alone, `--schedule` writes no settings file.
|
|
142
156
|
|
|
143
157
|
#### Reading `measure` before and after
|
|
144
158
|
|
|
@@ -146,30 +160,31 @@ Install merges: existing hooks (a Stop hook that rotates sessions, say) stay, ev
|
|
|
146
160
|
|
|
147
161
|
#### Remove everything
|
|
148
162
|
|
|
149
|
-
`oars sessions hook uninstall --scope user` (or the scope you installed); `oars sessions hook install --schedule off`; delete the store folder `<config>/oars/sessions/` (observations, skeletons, state, the hook index, `hook-calls.jsonl`, and `raw/`, which holds the unredacted transcripts). `gold sample` also writes its prompt file and sidecar to `~/.claude/session-commons/review/` (or `$CLAUDE_CONFIG_DIR/session-commons/review/`, or your `--out-dir`); delete those too.
|
|
163
|
+
`oars sessions hook uninstall --scope user` (or the scope you installed); `oars sessions hook install --schedule off`; delete the store folder `<config>/oars/sessions/` (observations, skeletons, state, the hook index, `hook-calls.jsonl`, and `raw/`, which holds the unredacted transcripts). `gold sample` also writes its prompt file and sidecar to `~/.claude/session-commons/review/` (or `$CLAUDE_CONFIG_DIR/session-commons/review/`, or your `--out-dir`); delete those too. oars also writes `<config>/oars/telemetry/` (the telemetry spool and install id; delete it too) and, after `oars login`, `<config>/oars/credentials.json` (`oars logout` deletes it). `cleanupPeriodDays` stays at the value install set unless you change it.
|
|
150
164
|
|
|
151
165
|
### Gate: what your agents picked
|
|
152
166
|
|
|
153
167
|
```
|
|
154
|
-
oars gate replay
|
|
155
|
-
oars gate replay --registry-state
|
|
168
|
+
oars gate replay # find installs, fetch-and-runs, MCP servers, and skills in logs on disk, and ask npm or pip what the registry holds
|
|
169
|
+
oars gate replay --no-registry-state # the same, without asking the registry
|
|
156
170
|
oars gate report # counts over crossings.jsonl, by who recorded each, and what replay cannot see
|
|
157
171
|
oars gate install # add the hook to Claude Code at user scope, so crossings are recorded as they happen
|
|
158
172
|
oars gate uninstall # remove only that hook entry
|
|
159
173
|
oars gate policy # print the policy in force (warn, block, or off per class)
|
|
160
174
|
```
|
|
161
175
|
|
|
162
|
-
Replay is local and read-only on your transcripts; it writes `crossings.jsonl` and the novelty index `gate-index.txt` into the sessions store and nothing else (
|
|
176
|
+
Replay is local and read-only on your transcripts; it writes `crossings.jsonl` and the novelty index `gate-index.txt` into the sessions store and nothing else (npm and pip also keep their own caches, unless `--no-registry-state`). The hook (`oars gate hook`, run by Claude Code, not by you) appends to `crossings.jsonl` as each crossing happens, plus `gate-index.txt`, `gate-registry.jsonl`, and the empty `gate-ask-*.lock` files in the same store (a registry ask the hook had to cut is finished by a helper process that ends on its own timeouts, and the package manager keeps its own cache outside the store), and always exits 0. With no policy file it adds a note about a name first published in the last 30 days or an npm name the registry does not hold; `gate-policy.json` changes that per class, and a block needs `block` written by you. A block is a labeled deny, and a crossing goes nowhere unless you opt in (no opt-in exists yet). `report` reads only `crossings.jsonl`. Both record picks and neither judges them; a note or label states a fact about the registry and nothing else. See `docs/gate.md` in the repository.
|
|
163
177
|
|
|
164
178
|
### pool
|
|
165
179
|
|
|
166
|
-
`oars pool`
|
|
180
|
+
`oars pool` uses the Many Oars pool, `https://pool.manyoars.com`, which is live (deposits there are closed until its covenant is published), or a pool server you name with `--server <url>` or `OARS_POOL_URL`. Every other command works without it.
|
|
167
181
|
|
|
168
182
|
- `oars pool login --provider github|google` signs in by the provider's device flow, run here, and prints every request before it sends it. `OARS_GITHUB_CLIENT_ID` (or `OARS_GOOGLE_CLIENT_ID` and `OARS_GOOGLE_CLIENT_SECRET`) must be set; an unset one stops the command before anything is sent and names the variable.
|
|
169
|
-
- `oars pool deposit` writes a batch of
|
|
183
|
+
- `oars pool deposit` writes a batch of session skeletons (a closed list of fields, each value redacted first; a host is hashed by the pool on arrival) to an outbox for each session that passes your folder allowlist and the exclusion check, and stops: it sends nothing itself. `--send <hash>` sends one batch that `outbox` printed and that is unchanged on disk, once this account has accepted the pool covenant (`--accept-covenant <version>`, or an earlier acceptance); before that it reads the covenant, sends no batch, and exits 3 with `covenant_not_accepted`. A hash that was never shown, or a batch that changed, is refused before any request. The nightly task is the other sender: after the pool says your account accepted its covenant, it sends new batches, and raw copies only if you said yes to the raw prompt (`oars pool raw --consent yes`). With no sign-in, no acceptance, or an unreachable server it sends nothing.
|
|
170
184
|
- `deposit`, `outbox`, and `fetch` need `oars pool login` first; without it they exit 3 and name the server.
|
|
185
|
+
- `oars pool coverage [--org-size <band>] [--repo-visibility public|private|unknown]` sets the two coverage bands only you can state, read locally and never from the network; an unset band goes on the wire as `unknown`.
|
|
171
186
|
- `oars pool outbox` prints each batch exactly as it would be sent, with its SHA-256, and `--explain` adds each field's rule.
|
|
172
|
-
- `oars pool fetch --harness-version <version>` fetches that version's signed bundle, verifies it against the key pinned at login, and keeps it locally.
|
|
187
|
+
- `oars pool fetch --harness-version <version>` fetches that version's signed bundle, verifies it against the key pinned at login, and keeps it locally. It prints the public and disclosure floors the bundle states (and any read family on a floor of its own) and holds no floor of its own: the pool sets them, so a change is a deploy and never a new release of oars.
|
|
173
188
|
|
|
174
189
|
## The `--json` contract
|
|
175
190
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { CredentialStore } from './credentials.js';
|
|
|
2
2
|
import { ExitCode } from './errors.js';
|
|
3
3
|
import type { InstallRuntime } from './sessions/install.js';
|
|
4
4
|
import type { SessionsRuntime } from './sessions/store.js';
|
|
5
|
+
import type { TelemetryRuntime } from './telemetry/record.js';
|
|
5
6
|
/** Everything the CLI takes from its process, so the tests can stand in for each. */
|
|
6
7
|
export interface RunDeps {
|
|
7
8
|
env: NodeJS.ProcessEnv;
|
|
@@ -22,11 +23,13 @@ export interface RunDeps {
|
|
|
22
23
|
sessions?: SessionsRuntime;
|
|
23
24
|
/** Where `oars sessions hook` finds the hook script, the settings files, and schtasks; absent in a run that has none. */
|
|
24
25
|
install?: InstallRuntime;
|
|
26
|
+
/** Where telemetry keeps its id, settings, and spool; absent in a run that has none, which records nothing. */
|
|
27
|
+
telemetry?: TelemetryRuntime | undefined;
|
|
25
28
|
}
|
|
26
29
|
/** What a command produced: the `--json` document, and the line a human sees instead. */
|
|
27
30
|
export interface Outcome {
|
|
28
31
|
document: object;
|
|
29
32
|
text: string;
|
|
30
33
|
}
|
|
31
|
-
/** Runs one invocation (`argv` without the node and script entries) and returns the exit code. */
|
|
34
|
+
/** Runs one invocation (`argv` without the node and script entries) and returns the exit code. Telemetry records it afterward and cannot change it. */
|
|
32
35
|
export declare const run: (argv: string[], deps: RunDeps) => Promise<ExitCode>;
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,11 @@ import { gateInstall, gatePolicy, gateReplay, gateReport, gateUninstall } from '
|
|
|
12
12
|
import { DEADLINE_MS, gateHook } from './gate/hook.js';
|
|
13
13
|
import { managerReader } from './gate/registry.js';
|
|
14
14
|
import { exclusionPaths, sessionsExclusions } from './sessions/exclusion.js';
|
|
15
|
-
import { poolDeposit, poolFetch, poolLogin, poolOutbox } from './sessions/pool-command.js';
|
|
15
|
+
import { poolCoverage, poolDeposit, poolFetch, poolLogin, poolOutbox, poolRaw } from './sessions/pool-command.js';
|
|
16
|
+
import { telemetryOff, telemetryOn, telemetryReport, telemetryShow, telemetryStatus } from './telemetry/command.js';
|
|
17
|
+
import { recordCrash } from './telemetry/crash.js';
|
|
18
|
+
import { emitOf } from './telemetry/emit.js';
|
|
19
|
+
import { finishTelemetry, observeCommand } from './telemetry/observe.js';
|
|
16
20
|
const flag = (options, name) => options[name] === true;
|
|
17
21
|
const text = (options, name) => {
|
|
18
22
|
const value = options[name];
|
|
@@ -55,21 +59,24 @@ const printFailure = (deps, json, error) => {
|
|
|
55
59
|
const { title, detail, actionUrl } = error.problem;
|
|
56
60
|
deps.io.stderr(`${[title ?? error.message, detail, actionUrl].filter((line) => line !== undefined && line !== '').join('\n')}\n`);
|
|
57
61
|
};
|
|
58
|
-
const sessionDeps = (deps) => ({ sessions: deps.sessions, install: deps.install, now: deps.now });
|
|
59
|
-
const buildProgram = (deps) => {
|
|
62
|
+
const sessionDeps = (deps) => ({ sessions: deps.sessions, install: deps.install, now: deps.now, telemetry: emitOf(deps) });
|
|
63
|
+
const buildProgram = (deps, observed) => {
|
|
60
64
|
const program = new Command('oars')
|
|
61
|
-
.description('Many Oars: sign in, manage API keys, and keep a local, searchable record of your own
|
|
65
|
+
.description('Many Oars: sign in, manage API keys, and keep a local, searchable record of your own coding-agent sessions')
|
|
62
66
|
.version(deps.version)
|
|
63
67
|
.exitOverride()
|
|
64
68
|
.configureOutput({ writeOut: deps.io.stdout, writeErr: deps.io.stderr })
|
|
65
69
|
.option('--json', 'print one JSON document on stdout instead of text')
|
|
66
70
|
.option('--api-url <url>', 'the platform API (default: OARS_API_URL, else https://api.manyoars.com)')
|
|
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")
|
|
71
|
+
.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")
|
|
72
|
+
.hook('preAction', (_program, action) => {
|
|
73
|
+
observeCommand(observed, action);
|
|
74
|
+
});
|
|
68
75
|
program
|
|
69
76
|
.command('login')
|
|
70
77
|
.description('sign in with your browser (RFC 8628 device flow)')
|
|
71
78
|
.option('--no-browser', 'print the link and code without opening a browser')
|
|
72
|
-
.option('--issuer-url <url>', 'the Keycloak realm URL (default: OARS_ISSUER_URL, else Many Oars
|
|
79
|
+
.option('--issuer-url <url>', 'the Keycloak realm URL (default: OARS_ISSUER_URL, else the Many Oars issuer)')
|
|
73
80
|
.action(async (_options, command) => {
|
|
74
81
|
const options = command.optsWithGlobals();
|
|
75
82
|
const outcome = await login(deps, {
|
|
@@ -122,16 +129,17 @@ const buildProgram = (deps) => {
|
|
|
122
129
|
const options = command.optsWithGlobals();
|
|
123
130
|
emit(deps, flag(options, 'json'), await keysRevoke(deps, { apiUrl: resolveApiUrl(deps, text(options, 'apiUrl')), keyId }));
|
|
124
131
|
});
|
|
125
|
-
const sessions = program.command('sessions').description("deposit and recall your own
|
|
132
|
+
const sessions = program.command('sessions').description("deposit and recall your own coding-agent session logs, locally (these commands make no model call and no network call, except `gold arms`, which asks your own Claude subscription; telemetry is separate: oars telemetry)");
|
|
126
133
|
sessions
|
|
127
134
|
.command('deposit')
|
|
128
|
-
.description('read session logs
|
|
129
|
-
.option('--allow <folder>', 'a project folder to read: its path, or its name under ~/.claude/projects (repeatable); `cursor`, `codex`, `copilot`, `gemini`, `grok`,
|
|
130
|
-
.option('--allow-file <list>', 'a file of --allow entries, one per line (# starts a comment); repeatable, combines with --allow', collect)
|
|
135
|
+
.description('read session logs into the local observation log, from every folder unless --allow names some or --deny keeps some out; idempotent. Subagent transcripts under each session (<project>/<session>/subagents/) are deposited with their parent, and each raw transcript is hard-linked or copied into the oars archive beside the store')
|
|
136
|
+
.option('--allow <folder>', 'a project folder to read: its path, or its name under ~/.claude/projects (repeatable); `cursor`, `codex`, `copilot`, `gemini`, `grok`, `opencode`, `pi`, `vscode-copilot`, and `otel` name the Cursor, Codex, Copilot CLI, Gemini CLI, Grok Build, OpenCode, Pi, VS Code Copilot Chat, and OpenTelemetry GenAI trace stores. A trailing slash names everything under a folder; * and ? are wildcards', collect)
|
|
137
|
+
.option('--allow-file <list>', 'a file of --allow entries, one per line (# starts a comment, and !<entry> keeps a folder out); repeatable, combines with --allow', collect)
|
|
138
|
+
.option('--deny <folder>', 'a folder never to read, in any form --allow takes (repeatable); saved, and it wins over every --allow until that exact entry is named with --allow', collect)
|
|
131
139
|
.option('--redact-domain <domain>', 'an organization domain to mask in everything written (repeatable)', collect)
|
|
132
140
|
.action(async (_options, command) => {
|
|
133
141
|
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') }));
|
|
142
|
+
emit(deps, flag(options, 'json'), await sessionsDeposit(sessionDeps(deps), { allow: list(options, 'allow'), allowFiles: list(options, 'allowFile'), deny: list(options, 'deny'), redactDomains: list(options, 'redactDomain') }));
|
|
135
143
|
});
|
|
136
144
|
sessions
|
|
137
145
|
.command('recall')
|
|
@@ -212,10 +220,10 @@ const buildProgram = (deps) => {
|
|
|
212
220
|
});
|
|
213
221
|
sessions
|
|
214
222
|
.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
|
|
217
|
-
if (value !== 'class' && value !== 'model' && value !== 'harness' && value !== 'pull')
|
|
218
|
-
throw new InvalidArgumentError('it must be class, model, harness, or
|
|
223
|
+
.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; with --by package, the public-package counts a field report prints, exclusions applied')
|
|
224
|
+
.option('--by <key>', 'class, model, harness, pull, or package (default class)', (value) => {
|
|
225
|
+
if (value !== 'class' && value !== 'model' && value !== 'harness' && value !== 'pull' && value !== 'package')
|
|
226
|
+
throw new InvalidArgumentError('it must be class, model, harness, pull, or package.');
|
|
219
227
|
return value;
|
|
220
228
|
})
|
|
221
229
|
.option('--since <days>', 'only the last N days', positiveInteger)
|
|
@@ -223,8 +231,9 @@ const buildProgram = (deps) => {
|
|
|
223
231
|
.action(async (_options, command) => {
|
|
224
232
|
const options = command.optsWithGlobals();
|
|
225
233
|
const picked = text(options, 'by');
|
|
226
|
-
const by = picked === 'model' || picked === 'harness' || picked === 'pull' ? picked : 'class';
|
|
227
|
-
|
|
234
|
+
const by = picked === 'model' || picked === 'harness' || picked === 'pull' || picked === 'package' ? picked : 'class';
|
|
235
|
+
const termsFile = deps.sessions === undefined ? undefined : exclusionPaths(deps.sessions.projectsRoot, deps.env).terms;
|
|
236
|
+
emit(deps, flag(options, 'json'), await sessionsBooks(sessionDeps(deps), { by, sinceDays: count(options, 'since'), repo: text(options, 'repo'), termsFile }));
|
|
228
237
|
});
|
|
229
238
|
sessions
|
|
230
239
|
.command('measure')
|
|
@@ -313,11 +322,11 @@ const buildProgram = (deps) => {
|
|
|
313
322
|
};
|
|
314
323
|
hook
|
|
315
324
|
.command('install')
|
|
316
|
-
.description('merge the SessionStart, PostToolUseFailure, and SessionEnd hooks into a settings file (existing hooks are kept)
|
|
317
|
-
.option('--scope <scope>', 'user (~/.claude/settings.json), project (./.claude/settings.json), or file (needs --file;
|
|
325
|
+
.description('merge the SessionStart, UserPromptSubmit, PreToolUse, PostToolUseFailure, and SessionEnd hooks into a settings file (existing hooks are kept), raise cleanupPeriodDays to at least 365, and on Windows register the nightly task (which sends telemetry while telemetry is on, and pool batches only after you accept a pool covenant)')
|
|
326
|
+
.option('--scope <scope>', 'user (~/.claude/settings.json; the default), project (./.claude/settings.json), or file (needs --file; implied by --file)', scopeOption)
|
|
318
327
|
.option('--file <path>', 'the settings file for --scope file')
|
|
319
328
|
.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) => {
|
|
329
|
+
.option('--schedule <mode>', 'nightly: register a Windows task that runs the full deposit and the recurrence refresh daily (a plain install on Windows does this too); off: remove it', (value) => {
|
|
321
330
|
if (value !== 'nightly' && value !== 'off')
|
|
322
331
|
throw new InvalidArgumentError('it must be nightly or off.');
|
|
323
332
|
return value;
|
|
@@ -335,7 +344,7 @@ const buildProgram = (deps) => {
|
|
|
335
344
|
hook
|
|
336
345
|
.command('uninstall')
|
|
337
346
|
.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;
|
|
347
|
+
.option('--scope <scope>', 'user (the default), project, or file (needs --file; implied by --file)', scopeOption)
|
|
339
348
|
.option('--file <path>', 'the settings file for --scope file')
|
|
340
349
|
.action(async (_options, command) => {
|
|
341
350
|
const options = command.optsWithGlobals();
|
|
@@ -352,9 +361,9 @@ const buildProgram = (deps) => {
|
|
|
352
361
|
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
362
|
gate
|
|
354
363
|
.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
|
|
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
|
|
364
|
+
.description('find the crossings in your session logs and write them to crossings.jsonl in the sessions store; reads nothing outside the allowlist, and asks the package manager what the registry holds unless --no-registry-state')
|
|
365
|
+
.option('--allow <folder>', 'a project folder to read, as for `oars sessions deposit` (repeatable); default: the allowlist a deposit saved, else every folder', collect)
|
|
366
|
+
.option('--no-registry-state', 'do not ask the package manager what the registry holds for each package named (npm view, pip index versions; asked by default, as the live hook does, and the manager keeps its own cache)')
|
|
358
367
|
.action(async (_options, command) => {
|
|
359
368
|
const options = command.optsWithGlobals();
|
|
360
369
|
emit(deps, flag(options, 'json'), await gateReplay(sessionDeps(deps), { allow: list(options, 'allow'), ...(flag(options, 'registryState') ? { registry: managerReader() } : {}) }));
|
|
@@ -398,9 +407,9 @@ const buildProgram = (deps) => {
|
|
|
398
407
|
.action(async (_options, command) => {
|
|
399
408
|
emit(deps, flag(command.optsWithGlobals(), 'json'), await gateReport(sessionDeps(deps)));
|
|
400
409
|
});
|
|
401
|
-
const pool = program.command('pool').description('sign in to a pool server, deposit closed
|
|
410
|
+
const pool = program.command('pool').description('sign in to a pool server, deposit session skeletons (a closed list of fields, each value redacted first) and, after a yes to the raw prompt, redacted raw copies, and fetch signed reads; local capture, recall, and reads need none of it');
|
|
402
411
|
const serverOption = '--server <url>';
|
|
403
|
-
const serverHelp = 'the pool server, https or plain http to localhost (
|
|
412
|
+
const serverHelp = 'the pool server, https or plain http to localhost (default: OARS_POOL_URL, else https://pool.manyoars.com)';
|
|
404
413
|
pool
|
|
405
414
|
.command('login')
|
|
406
415
|
.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')
|
|
@@ -412,14 +421,33 @@ const buildProgram = (deps) => {
|
|
|
412
421
|
});
|
|
413
422
|
pool
|
|
414
423
|
.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>
|
|
416
|
-
.option('--accept-covenant <version>', 'accept this version of the pool covenant before sending (with --send
|
|
424
|
+
.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> sends one batch that oars pool outbox printed and that is unchanged, once this account has accepted the pool covenant')
|
|
425
|
+
.option('--accept-covenant <version>', 'accept this version of the pool covenant before sending (with --send)')
|
|
417
426
|
.option('--send <hash>', 'the SHA-256 of a batch that oars pool outbox printed')
|
|
418
427
|
.option(serverOption, serverHelp)
|
|
419
428
|
.action(async (_options, command) => {
|
|
420
429
|
const options = command.optsWithGlobals();
|
|
421
430
|
emit(deps, flag(options, 'json'), await poolDeposit(deps, { server: text(options, 'server'), acceptCovenant: text(options, 'acceptCovenant'), send: text(options, 'send') }));
|
|
422
431
|
});
|
|
432
|
+
pool
|
|
433
|
+
.command('raw')
|
|
434
|
+
.description('the raw prompt: whether a redacted copy of each full transcript may leave besides its skeleton (--consent yes|no), and what one folder may send (--folder <folder> --mode all|skeleton|off); with no option, shows the prompt and your answer')
|
|
435
|
+
.option('--consent <answer>', 'yes or no; a no, or no answer, sends skeletons only')
|
|
436
|
+
.option('--folder <entry>', 'a folder, folder prefix, or glob, as --allow takes it')
|
|
437
|
+
.option('--mode <mode>', 'all, skeleton, or off for that folder: on by default after you accept; skeleton sends no raw; off sends nothing from that folder to the pool from now on')
|
|
438
|
+
.action(async (_options, command) => {
|
|
439
|
+
const options = command.optsWithGlobals();
|
|
440
|
+
emit(deps, flag(options, 'json'), await poolRaw(deps, { consent: text(options, 'consent'), folder: text(options, 'folder'), mode: text(options, 'mode') }));
|
|
441
|
+
});
|
|
442
|
+
pool
|
|
443
|
+
.command('coverage')
|
|
444
|
+
.description('state whom your deposits cover, kept on this machine and never looked up: --org-size solo, 2-10, 11-100, 101-1000, over-1000, or unknown; --repo-visibility public, private, or unknown. With no flag it prints what is set')
|
|
445
|
+
.option('--org-size <band>', 'how many people your organization has')
|
|
446
|
+
.option('--repo-visibility <visibility>', 'whether your repositories are public or private')
|
|
447
|
+
.action(async (_options, command) => {
|
|
448
|
+
const options = command.optsWithGlobals();
|
|
449
|
+
emit(deps, flag(options, 'json'), await poolCoverage(deps, { orgSize: text(options, 'orgSize'), repoVisibility: text(options, 'repoVisibility') }));
|
|
450
|
+
});
|
|
423
451
|
pool
|
|
424
452
|
.command('outbox')
|
|
425
453
|
.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')
|
|
@@ -433,12 +461,44 @@ const buildProgram = (deps) => {
|
|
|
433
461
|
.command('fetch')
|
|
434
462
|
.description('fetch the signed bundle for one harness version, verify it against the key pinned at login, and keep it locally')
|
|
435
463
|
.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')
|
|
464
|
+
.requiredOption('--harness-version <version>', 'the public harness version whose bundle to fetch, or unknown for a session that recorded none (Cursor, Gemini, Grok Build, Pi, or a VS Code Copilot Chat session whose requests name no agent version; there is no bundle for otel)')
|
|
437
465
|
.option(serverOption, serverHelp)
|
|
438
466
|
.action(async (_options, command) => {
|
|
439
467
|
const options = command.optsWithGlobals();
|
|
440
468
|
emit(deps, flag(options, 'json'), await poolFetch(deps, { server: text(options, 'server'), harness: text(options, 'harness'), harnessVersion: text(options, 'harnessVersion') ?? '' }));
|
|
441
469
|
});
|
|
470
|
+
const telemetry = program.command('telemetry').description('what oars records about its own use, on this machine (docs/telemetry.md): status, on, off, show, and report');
|
|
471
|
+
const telemetryJson = (command) => flag(command.optsWithGlobals(), 'json');
|
|
472
|
+
telemetry
|
|
473
|
+
.command('status')
|
|
474
|
+
.description('whether telemetry is on, which switch decided it, whether the notice was shown, and the size of the spool')
|
|
475
|
+
.action((_options, command) => {
|
|
476
|
+
emit(deps, telemetryJson(command), telemetryStatus(deps));
|
|
477
|
+
});
|
|
478
|
+
telemetry
|
|
479
|
+
.command('on')
|
|
480
|
+
.description('turn telemetry on, and print what it records (OARS_TELEMETRY=0 and DO_NOT_TRACK=1 still win)')
|
|
481
|
+
.action((_options, command) => {
|
|
482
|
+
emit(deps, telemetryJson(command), telemetryOn(deps));
|
|
483
|
+
});
|
|
484
|
+
telemetry
|
|
485
|
+
.command('off')
|
|
486
|
+
.description('turn telemetry off: nothing more is recorded or sent')
|
|
487
|
+
.action((_options, command) => {
|
|
488
|
+
emit(deps, telemetryJson(command), telemetryOff(deps));
|
|
489
|
+
});
|
|
490
|
+
telemetry
|
|
491
|
+
.command('show')
|
|
492
|
+
.description('print the spool exactly as stored: every event telemetry has kept, oldest first')
|
|
493
|
+
.action((_options, command) => {
|
|
494
|
+
emit(deps, telemetryJson(command), telemetryShow(deps));
|
|
495
|
+
});
|
|
496
|
+
telemetry
|
|
497
|
+
.command('report')
|
|
498
|
+
.description('your own hook latency, hit rates, and store health over the last 7 and 30 days, from the spool, beside the fleet\'s when OARS_POOL_URL names a pool server that answers')
|
|
499
|
+
.action(async (_options, command) => {
|
|
500
|
+
emit(deps, telemetryJson(command), await telemetryReport(deps));
|
|
501
|
+
});
|
|
442
502
|
return program;
|
|
443
503
|
};
|
|
444
504
|
const asCliError = (error) => {
|
|
@@ -448,16 +508,18 @@ const asCliError = (error) => {
|
|
|
448
508
|
return new CliError(error.message.replace(/^error: /, ''), ExitCode.USAGE, 'usage');
|
|
449
509
|
return new CliError(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`, ExitCode.UNEXPECTED, 'unexpected');
|
|
450
510
|
};
|
|
451
|
-
|
|
452
|
-
export const run = async (argv, deps) => {
|
|
511
|
+
const execute = async (argv, deps, observed) => {
|
|
453
512
|
try {
|
|
454
|
-
await buildProgram(deps).parseAsync(argv, { from: 'user' });
|
|
513
|
+
await buildProgram(deps, observed).parseAsync(argv, { from: 'user' });
|
|
455
514
|
return ExitCode.OK;
|
|
456
515
|
}
|
|
457
516
|
catch (error) {
|
|
458
517
|
// --help and --version end the parse through exitOverride with exit code 0; commander has already printed them.
|
|
459
518
|
if (error instanceof CommanderError && error.exitCode === 0)
|
|
460
519
|
return ExitCode.OK;
|
|
520
|
+
// A failure the program did not mean (not a CliError, not commander's own): its class and a hash of its stack are recorded, then it is handled as before.
|
|
521
|
+
if (!(error instanceof CliError) && !(error instanceof CommanderError))
|
|
522
|
+
recordCrash(deps, error);
|
|
461
523
|
const failure = asCliError(error);
|
|
462
524
|
// Commander wrote its own usage message to stderr; a CliError is ours to print.
|
|
463
525
|
if (!(error instanceof CommanderError) || argv.includes('--json'))
|
|
@@ -465,3 +527,11 @@ export const run = async (argv, deps) => {
|
|
|
465
527
|
return failure.exitCode;
|
|
466
528
|
}
|
|
467
529
|
};
|
|
530
|
+
/** Runs one invocation (`argv` without the node and script entries) and returns the exit code. Telemetry records it afterward and cannot change it. */
|
|
531
|
+
export const run = async (argv, deps) => {
|
|
532
|
+
const observed = { path: [], flags: [] };
|
|
533
|
+
const startedAt = deps.now();
|
|
534
|
+
const exit = await execute(argv, deps, observed);
|
|
535
|
+
finishTelemetry(deps, argv, observed, startedAt, exit);
|
|
536
|
+
return exit;
|
|
537
|
+
};
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Outcome, RunDeps } from '../cli.js';
|
|
2
2
|
export interface LoginOptions {
|
|
3
3
|
apiUrl: string;
|
|
4
|
-
/** `--issuer-url`; `OARS_ISSUER_URL` when absent, then the built-in production issuer
|
|
4
|
+
/** `--issuer-url`; `OARS_ISSUER_URL` when absent, then the built-in production issuer (issuer.ts). */
|
|
5
5
|
issuerUrl?: string;
|
|
6
6
|
/** False for `--no-browser`. */
|
|
7
7
|
browser: boolean;
|
package/dist/commands/login.js
CHANGED
|
@@ -5,7 +5,7 @@ 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
|
-
const issuerUrl =
|
|
8
|
+
const issuerUrl = resolveIssuerUrl(deps, options.issuerUrl);
|
|
9
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;
|
package/dist/gate/hook-main.js
CHANGED
|
@@ -7,8 +7,10 @@ import { performance } from 'node:perf_hooks';
|
|
|
7
7
|
import { defaultConfigDir } from '../credentials.js';
|
|
8
8
|
import { DEADLINE_MS } from './hook.js';
|
|
9
9
|
import { isLocked, startFill, startHelper } from './hook-fill.js';
|
|
10
|
+
import { fireRecorder } from './hook-record.js';
|
|
10
11
|
import { runHookProcess } from './hook-process.js';
|
|
11
12
|
import { managerReader } from './registry.js';
|
|
13
|
+
import { hookEmitter } from '../telemetry/hook-emit.js';
|
|
12
14
|
/** The process itself ends this long after `DEADLINE_MS`. */
|
|
13
15
|
const GRACE_MS = 100;
|
|
14
16
|
const readStdin = async () => {
|
|
@@ -32,4 +34,14 @@ const depsFor = (stage) => {
|
|
|
32
34
|
stage,
|
|
33
35
|
};
|
|
34
36
|
};
|
|
35
|
-
|
|
37
|
+
// One event per fire, recorded after the answer is written (`runHookProcess`), so it is never in front of the tool call. The version is read from `dist/gate/../../package.json`.
|
|
38
|
+
const emit = hookEmitter(process.env, process.platform, homedir(), new URL('../../package.json', import.meta.url), Date.now);
|
|
39
|
+
await runHookProcess({
|
|
40
|
+
stdin: readStdin,
|
|
41
|
+
deps: depsFor,
|
|
42
|
+
exit: (code) => process.exit(code),
|
|
43
|
+
write: (text) => new Promise((done) => process.stdout.write(text, () => { done(); })),
|
|
44
|
+
deadlineMs: DEADLINE_MS + GRACE_MS - performance.now(),
|
|
45
|
+
clock: () => performance.now(),
|
|
46
|
+
record: fireRecorder(emit),
|
|
47
|
+
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type GateHookDeps } from './hook.js';
|
|
2
|
+
/** What telemetry is told of one fire of `oars gate hook`: how long the handler took, how it ended, and the bytes it printed. */
|
|
3
|
+
export interface HookFire {
|
|
4
|
+
latencyMs: number;
|
|
5
|
+
outcome: 'hit' | 'miss' | 'held' | 'error';
|
|
6
|
+
bytes: number;
|
|
7
|
+
}
|
|
2
8
|
export interface HookProcess {
|
|
3
9
|
/** Reads the whole of stdin. Called inside the guard, so a stream that cannot be set up is a failed read, not a crash. */
|
|
4
10
|
stdin: () => Promise<string>;
|
|
@@ -9,6 +15,10 @@ export interface HookProcess {
|
|
|
9
15
|
write?: (text: string) => Promise<void>;
|
|
10
16
|
/** Milliseconds from now until the process ends itself. */
|
|
11
17
|
deadlineMs: number;
|
|
18
|
+
/** Told once how the fire went, with the transcript path the harness named (to read the frame from, never to record), after its output is written so recording is never in front of the answer. A throw here is swallowed: the exit code and stdout do not depend on it. */
|
|
19
|
+
record?: (fire: HookFire, transcript: string | undefined) => void | Promise<void>;
|
|
20
|
+
/** Milliseconds, only compared with itself: the latency in a fire is the difference across the handler. Absent: `Date.now`. */
|
|
21
|
+
clock?: () => number;
|
|
12
22
|
}
|
|
13
23
|
/** Resolves once `exit(0)` has been called, or never when the deadline timer is what ended the process. */
|
|
14
|
-
export declare const runHookProcess: ({ stdin, deps, exit, write, deadlineMs }: HookProcess) => Promise<void>;
|
|
24
|
+
export declare const runHookProcess: ({ stdin, deps, exit, write, deadlineMs, record, clock }: HookProcess) => Promise<void>;
|