@manyoars/oars 0.2.0 → 0.4.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 +147 -28
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +10 -4
- package/dist/cli.js +440 -91
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +3 -3
- 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 +47 -0
- package/dist/gate/hook-process.d.ts +24 -0
- package/dist/gate/hook-process.js +112 -0
- package/dist/gate/hook-record.d.ts +3 -0
- package/dist/gate/hook-record.js +8 -0
- package/dist/gate/hook.d.ts +48 -0
- package/dist/gate/hook.js +259 -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 +254 -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 +50 -0
- package/dist/issuer.d.ts +10 -6
- package/dist/issuer.js +19 -24
- package/dist/main.d.ts +1 -0
- package/dist/main.js +67 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +429 -0
- package/dist/sessions/allow.d.ts +23 -0
- package/dist/sessions/allow.js +71 -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 +190 -0
- package/dist/sessions/call-shape.d.ts +19 -0
- package/dist/sessions/call-shape.js +85 -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 +47 -0
- package/dist/sessions/cell-increments.js +243 -0
- package/dist/sessions/cells.d.ts +109 -0
- package/dist/sessions/cells.js +261 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +745 -0
- package/dist/sessions/command.d.ts +127 -0
- package/dist/sessions/command.js +419 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/coverage.d.ts +19 -0
- package/dist/sessions/coverage.js +56 -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 +386 -0
- package/dist/sessions/exclusion.d.ts +87 -0
- package/dist/sessions/exclusion.js +361 -0
- package/dist/sessions/extract.d.ts +181 -0
- package/dist/sessions/extract.js +1806 -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 +66 -0
- package/dist/sessions/hook-index.js +164 -0
- package/dist/sessions/hooks.d.ts +17 -0
- package/dist/sessions/hooks.js +281 -0
- package/dist/sessions/identity.d.ts +27 -0
- package/dist/sessions/identity.js +131 -0
- package/dist/sessions/install.d.ts +70 -0
- package/dist/sessions/install.js +246 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +7 -0
- package/dist/sessions/mcp.js +157 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +44 -0
- package/dist/sessions/motif.js +59 -0
- 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 +32 -0
- package/dist/sessions/package-resolve.js +185 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- 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 +115 -0
- package/dist/sessions/pool-command.js +669 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- 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 +76 -0
- package/dist/sessions/pool-outbox.js +313 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- 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 +435 -0
- package/dist/sessions/pool-wire.js +1100 -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 +72 -0
- package/dist/sessions/public-vocabulary.js +295 -0
- package/dist/sessions/query.d.ts +123 -0
- package/dist/sessions/query.js +268 -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 +48 -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-edge.d.ts +2 -0
- package/dist/sessions/read-recovery-edge.js +26 -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 +86 -0
- package/dist/sessions/read.js +87 -0
- package/dist/sessions/recall-bundle.d.ts +12 -0
- package/dist/sessions/recall-bundle.js +33 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -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 +139 -0
- package/dist/sessions/recall-object.js +78 -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 +47 -0
- package/dist/sessions/recall-reads.d.ts +124 -0
- package/dist/sessions/recall-reads.js +417 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/recovery-calls.d.ts +21 -0
- package/dist/sessions/recovery-calls.js +25 -0
- package/dist/sessions/redact.d.ts +17 -0
- package/dist/sessions/redact.js +113 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +694 -0
- package/dist/sessions/schema.js +167 -0
- package/dist/sessions/session-shape.d.ts +76 -0
- package/dist/sessions/session-shape.js +355 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +233 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +309 -0
- package/dist/sessions/store.js +1498 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +27 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/transcript-tail.d.ts +14 -0
- package/dist/sessions/transcript-tail.js +64 -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 +494 -0
- 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 +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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// The sender: the only code that sends telemetry. It runs from the nightly task, and at most once a day from an interactive command as a detached process; never on a hook path, and
|
|
2
|
+
// nothing waits for it. Before any byte leaves it calls `telemetrySendAllowed` (telemetry on, notice shown) and sends to the pool server in `OARS_POOL_URL`, else the Many Oars pool, the same default `oars pool`
|
|
3
|
+
// reads and with the same rules (https, or http to this machine).
|
|
4
|
+
//
|
|
5
|
+
// A batch is up to 250 events. Only a 2xx clears what was sent; anything else (an error status, a redirect, a timeout, a refused connection) keeps the spool for the next try. That makes
|
|
6
|
+
// delivery at least once: a batch the server kept whose answer was lost is sent again.
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { defaultConfigDir } from '../credentials.js';
|
|
12
|
+
import { telemetrySendAllowed } from './consent.js';
|
|
13
|
+
import { installId, readSpool, spoolPaths } from './spool.js';
|
|
14
|
+
/** The pool route takes at most this many events (Analytics Engine's per-request write limit). */
|
|
15
|
+
export const BATCH_EVENTS = 250;
|
|
16
|
+
/** One request's whole life, connect to last byte; a server that is slower than this is treated as down. */
|
|
17
|
+
export const SEND_TIMEOUT_MS = 10_000;
|
|
18
|
+
const DAY_MS = 86_400_000;
|
|
19
|
+
const MARK = 'last-send';
|
|
20
|
+
/** The pool server, validated the way `oars pool` does it, or undefined. The pool command's code is loaded only after sending is allowed, so a run with telemetry off or the notice unshown does not pay for it. */
|
|
21
|
+
const serverOf = async (env) => {
|
|
22
|
+
try {
|
|
23
|
+
const { poolServer } = await import('../sessions/pool-command.js');
|
|
24
|
+
return poolServer(env, 'telemetry');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** The route answers 400 to an event whose `type` is not a string of 1 to 64 characters, and a failed batch blocks every later send, so such an object is junk here (the pool's `telemetry.ts` holds the rule). */
|
|
31
|
+
const eventOf = (line) => {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(line);
|
|
34
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
35
|
+
return undefined;
|
|
36
|
+
const type = Reflect.get(parsed, 'type');
|
|
37
|
+
return typeof type === 'string' && type.length >= 1 && type.length <= 64 ? parsed : undefined;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** True only for a 2xx. A redirect is not followed: a pool never redirects a POST, and a 3xx from anything else is not the pool. */
|
|
44
|
+
const post = async (url, install, events, deps) => {
|
|
45
|
+
try {
|
|
46
|
+
const response = await (deps.fetch ?? fetch)(url, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'content-type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({ install, events }),
|
|
50
|
+
redirect: 'manual',
|
|
51
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? SEND_TIMEOUT_MS),
|
|
52
|
+
});
|
|
53
|
+
await response.body?.cancel();
|
|
54
|
+
return response.ok;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/** Takes the sent lines out of the spool, by content and oldest first, so an event written after the read stays and a rotation in between does not matter. A file that cannot be rewritten keeps its lines, and they are sent again. */
|
|
61
|
+
const dropLines = (dir, gone) => {
|
|
62
|
+
const counts = new Map();
|
|
63
|
+
for (const line of gone)
|
|
64
|
+
counts.set(line, (counts.get(line) ?? 0) + 1);
|
|
65
|
+
const { previous, current } = spoolPaths(dir);
|
|
66
|
+
for (const path of [previous, current]) {
|
|
67
|
+
try {
|
|
68
|
+
const lines = readFileSync(path, 'utf8')
|
|
69
|
+
.split('\n')
|
|
70
|
+
.filter((line) => line !== '');
|
|
71
|
+
const kept = lines.filter((line) => {
|
|
72
|
+
const left = counts.get(line) ?? 0;
|
|
73
|
+
if (left === 0)
|
|
74
|
+
return true;
|
|
75
|
+
counts.set(line, left - 1);
|
|
76
|
+
return false;
|
|
77
|
+
});
|
|
78
|
+
if (kept.length === lines.length)
|
|
79
|
+
continue;
|
|
80
|
+
const scratch = `${path}.tmp`;
|
|
81
|
+
writeFileSync(scratch, kept.map((line) => `${line}\n`).join(''));
|
|
82
|
+
renameSync(scratch, path);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// No such file, or one another process holds: its lines stay, and a later send takes them.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
export const writeMark = (dir, now) => {
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
writeFileSync(join(dir, MARK), String(now));
|
|
92
|
+
};
|
|
93
|
+
/** Whether a day has passed since the last send was started. A missing, unreadable, or future mark counts as due. */
|
|
94
|
+
export const dueForSend = (dir, now) => {
|
|
95
|
+
let last;
|
|
96
|
+
try {
|
|
97
|
+
last = Number(readFileSync(join(dir, MARK), 'utf8').trim());
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return !Number.isFinite(last) || last > now || now - last >= DAY_MS;
|
|
103
|
+
};
|
|
104
|
+
/** Sends what the spool holds. Never throws. */
|
|
105
|
+
export const sendSpool = async (deps) => {
|
|
106
|
+
const tally = { sent: 0, requests: 0 };
|
|
107
|
+
const finish = (outcome) => ({ outcome, ...tally });
|
|
108
|
+
if (!telemetrySendAllowed(deps.env, deps.dir))
|
|
109
|
+
return finish('not-allowed');
|
|
110
|
+
const server = await serverOf(deps.env);
|
|
111
|
+
if (server === undefined)
|
|
112
|
+
return finish('no-server');
|
|
113
|
+
try {
|
|
114
|
+
const events = [];
|
|
115
|
+
const junk = [];
|
|
116
|
+
for (const line of readSpool(deps.dir)) {
|
|
117
|
+
const event = eventOf(line);
|
|
118
|
+
if (event === undefined)
|
|
119
|
+
junk.push(line);
|
|
120
|
+
else
|
|
121
|
+
events.push({ line, event });
|
|
122
|
+
}
|
|
123
|
+
if (events.length === 0)
|
|
124
|
+
return finish('empty');
|
|
125
|
+
const install = installId(deps.dir);
|
|
126
|
+
writeMark(deps.dir, deps.now());
|
|
127
|
+
const sent = [];
|
|
128
|
+
let failed = false;
|
|
129
|
+
for (let at = 0; at < events.length && !failed; at += BATCH_EVENTS) {
|
|
130
|
+
const batch = events.slice(at, at + BATCH_EVENTS);
|
|
131
|
+
tally.requests += 1;
|
|
132
|
+
if (await post(`${server}/v1/telemetry`, install, batch.map(({ event }) => event), deps)) {
|
|
133
|
+
tally.sent += batch.length;
|
|
134
|
+
sent.push(...batch.map(({ line }) => line));
|
|
135
|
+
}
|
|
136
|
+
else
|
|
137
|
+
failed = true;
|
|
138
|
+
}
|
|
139
|
+
// A line that is not an event can never be sent, so it goes with the first batch the server takes.
|
|
140
|
+
if (sent.length > 0)
|
|
141
|
+
dropLines(deps.dir, [...sent, ...junk]);
|
|
142
|
+
return finish(failed ? 'failed' : 'sent');
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return finish('failed');
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
/** The nightly task's send, in the telemetry folder under the config folder. */
|
|
149
|
+
export const sendNightly = (env, platform, home, now) => sendSpool({ env, dir: join(defaultConfigDir(env, platform, home), 'oars', 'telemetry'), now });
|
|
150
|
+
/**
|
|
151
|
+
* Called after an interactive command: starts one detached send, at most once a day, when sending is allowed. The mark is written first, so two commands at
|
|
152
|
+
* once start one. Nothing here throws or waits, and the run has no way to start a send unless `main.ts` gave it one.
|
|
153
|
+
*/
|
|
154
|
+
export const startDailySend = (runtime, env, now) => {
|
|
155
|
+
try {
|
|
156
|
+
if (runtime.spawnSend === undefined)
|
|
157
|
+
return;
|
|
158
|
+
if (!telemetrySendAllowed(env, runtime.dir) || !dueForSend(runtime.dir, now))
|
|
159
|
+
return;
|
|
160
|
+
writeMark(runtime.dir, now);
|
|
161
|
+
runtime.spawnSend(runtime.dir);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Telemetry is never allowed to change what a command does.
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
/** Starts `send-main.js` detached with no stdio and never waits for it, as the gate's helper is started. */
|
|
168
|
+
export const spawnDetachedSend = (dir, start = spawn) => {
|
|
169
|
+
const child = start(process.execPath, [fileURLToPath(new URL('./send-main.js', import.meta.url)), dir], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
170
|
+
child.on('error', () => undefined);
|
|
171
|
+
child.unref();
|
|
172
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** One mebibyte per file, so the folder holds at most two. */
|
|
2
|
+
export declare const SPOOL_CAP_BYTES = 1048576;
|
|
3
|
+
export declare const spoolPaths: (dir: string) => {
|
|
4
|
+
current: string;
|
|
5
|
+
previous: string;
|
|
6
|
+
idFile: string;
|
|
7
|
+
settings: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* This install's id: a random version 4 UUID made on first use and kept in the folder. It comes from the system's random source, never from the
|
|
11
|
+
* machine, the user, or a path, so deleting the file starts a new, unrelated id.
|
|
12
|
+
*/
|
|
13
|
+
export declare const installId: (dir: string) => string;
|
|
14
|
+
/** Appends one line, rotating first when it would pass `capBytes`; a line larger than the cap on its own is not written. */
|
|
15
|
+
export declare const appendLine: (dir: string, line: string, capBytes?: number) => void;
|
|
16
|
+
/** Every spooled line, oldest first. */
|
|
17
|
+
export declare const readSpool: (dir: string) => string[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The telemetry folder: a per-install random id, and an append-only spool of one JSON event per line. Nothing here sends anything. The spool is
|
|
2
|
+
// capped: when a line would pass the cap, the current file becomes the old file (replacing the one before) and a new one starts, so the folder
|
|
3
|
+
// never holds more than twice the cap.
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { appendFileSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
/** One mebibyte per file, so the folder holds at most two. */
|
|
8
|
+
export const SPOOL_CAP_BYTES = 1_048_576;
|
|
9
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
10
|
+
export const spoolPaths = (dir) => ({
|
|
11
|
+
current: join(dir, 'events.jsonl'),
|
|
12
|
+
previous: join(dir, 'events.1.jsonl'),
|
|
13
|
+
idFile: join(dir, 'install-id'),
|
|
14
|
+
settings: join(dir, 'settings.json'),
|
|
15
|
+
});
|
|
16
|
+
const readIfPresent = (path) => {
|
|
17
|
+
try {
|
|
18
|
+
return readFileSync(path, 'utf8');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* This install's id: a random version 4 UUID made on first use and kept in the folder. It comes from the system's random source, never from the
|
|
26
|
+
* machine, the user, or a path, so deleting the file starts a new, unrelated id.
|
|
27
|
+
*/
|
|
28
|
+
export const installId = (dir) => {
|
|
29
|
+
const { idFile } = spoolPaths(dir);
|
|
30
|
+
const existing = readIfPresent(idFile);
|
|
31
|
+
const kept = existing?.trim();
|
|
32
|
+
if (kept !== undefined && UUID.test(kept))
|
|
33
|
+
return kept;
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
const fresh = randomUUID();
|
|
36
|
+
try {
|
|
37
|
+
// Exclusive when no file stood there, so two first runs at once cannot each keep their own id; a file that is not an id is replaced.
|
|
38
|
+
writeFileSync(idFile, `${fresh}\n`, { flag: existing === undefined ? 'wx' : 'w' });
|
|
39
|
+
return fresh;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const winner = readIfPresent(idFile)?.trim();
|
|
43
|
+
if (winner !== undefined && UUID.test(winner))
|
|
44
|
+
return winner;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/** Appends one line, rotating first when it would pass `capBytes`; a line larger than the cap on its own is not written. */
|
|
49
|
+
export const appendLine = (dir, line, capBytes = SPOOL_CAP_BYTES) => {
|
|
50
|
+
const { current, previous } = spoolPaths(dir);
|
|
51
|
+
const bytes = Buffer.byteLength(line) + 1;
|
|
52
|
+
if (bytes > capBytes)
|
|
53
|
+
return;
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
let size = 0;
|
|
56
|
+
try {
|
|
57
|
+
size = statSync(current).size;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// No spool yet.
|
|
61
|
+
}
|
|
62
|
+
if (size + bytes > capBytes)
|
|
63
|
+
renameSync(current, previous);
|
|
64
|
+
appendFileSync(current, `${line}\n`);
|
|
65
|
+
};
|
|
66
|
+
/** Every spooled line, oldest first. */
|
|
67
|
+
export const readSpool = (dir) => {
|
|
68
|
+
const { current, previous } = spoolPaths(dir);
|
|
69
|
+
return [previous, current].flatMap((path) => (readIfPresent(path) ?? '').split('\n').filter((line) => line !== ''));
|
|
70
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Emitter } from './emit.js';
|
|
2
|
+
/** One field across the batches of a pass: in how many it appeared, and the bytes of its values (a field that holds others counts them too). */
|
|
3
|
+
interface FieldTally {
|
|
4
|
+
present: number;
|
|
5
|
+
bytes: number;
|
|
6
|
+
}
|
|
7
|
+
interface HarnessTally {
|
|
8
|
+
batches: number;
|
|
9
|
+
bytes: number;
|
|
10
|
+
fields: Map<string, FieldTally>;
|
|
11
|
+
}
|
|
12
|
+
/** The tally of one deposit pass, by the harness that wrote each batch. */
|
|
13
|
+
export type WireTally = Map<string, HarnessTally>;
|
|
14
|
+
export declare const newTally: () => WireTally;
|
|
15
|
+
/** Adds one batch (the parsed skeleton, before it is written) to the tally. It never throws: a batch it cannot read (a cycle) adds nothing, and the deposit goes on. */
|
|
16
|
+
export declare const tallyBatch: (tally: WireTally, batch: unknown) => void;
|
|
17
|
+
/** One `wire` event for each harness the pass built a batch for: the names, and beside them the batches that held each and the bytes it took. */
|
|
18
|
+
export declare const recordWire: (emitter: Emitter, tally: WireTally) => void;
|
|
19
|
+
/** One send of one batch. `code` is the pool's own word for a refusal; the schema drops anything that is not a code. */
|
|
20
|
+
export declare const recordSend: (emitter: Emitter, send: {
|
|
21
|
+
status: number;
|
|
22
|
+
code?: string | undefined;
|
|
23
|
+
bytes: number;
|
|
24
|
+
}) => void;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Wire policing: what each deposit batch carried and what a send came to, so a field that is narrow (rarely present) or costly (many bytes nobody reads)
|
|
2
|
+
// shows up. Field names are only those of the register (`FIELD_ROWS`, the same list `oars pool outbox --explain` answers from); a value is never read
|
|
3
|
+
// past its size, and a name the register does not hold is not counted.
|
|
4
|
+
import { FIELD_ROWS } from '../sessions/pool-outbox.js';
|
|
5
|
+
import { HARNESSES, OTHER } from '../sessions/pool-wire.js';
|
|
6
|
+
import { emitEvent } from './emit.js';
|
|
7
|
+
export const newTally = () => new Map();
|
|
8
|
+
const REGISTER = new Set(Object.keys(FIELD_ROWS));
|
|
9
|
+
/** JSON.stringify answers `undefined` for a value it leaves out, which its signature does not say. */
|
|
10
|
+
const asText = (value) => JSON.stringify(value);
|
|
11
|
+
/** The bytes of a value as JSON; a value JSON leaves out (`undefined`) is not on the wire and has none. */
|
|
12
|
+
const sizeOf = (value) => {
|
|
13
|
+
const text = asText(value);
|
|
14
|
+
return text === undefined ? 0 : Buffer.byteLength(text);
|
|
15
|
+
};
|
|
16
|
+
const harnessOf = (batch) => {
|
|
17
|
+
const transcripts = typeof batch === 'object' && batch !== null ? Reflect.get(batch, 'transcripts') : undefined;
|
|
18
|
+
const first = Array.isArray(transcripts) ? transcripts[0] : undefined;
|
|
19
|
+
const harness = typeof first === 'object' && first !== null ? Reflect.get(first, 'harness') : undefined;
|
|
20
|
+
return typeof harness === 'string' && HARNESSES.some((known) => known === harness) ? harness : OTHER;
|
|
21
|
+
};
|
|
22
|
+
const walk = (value, seen) => {
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
for (const entry of value)
|
|
25
|
+
walk(entry, seen);
|
|
26
|
+
}
|
|
27
|
+
else if (typeof value === 'object' && value !== null) {
|
|
28
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
29
|
+
if (REGISTER.has(name) && entry !== undefined)
|
|
30
|
+
seen.set(name, (seen.get(name) ?? 0) + sizeOf(entry));
|
|
31
|
+
walk(entry, seen);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const add = (tally, batch) => {
|
|
36
|
+
const harness = harnessOf(batch);
|
|
37
|
+
const entry = tally.get(harness) ?? { batches: 0, bytes: 0, fields: new Map() };
|
|
38
|
+
entry.batches += 1;
|
|
39
|
+
entry.bytes += sizeOf(batch);
|
|
40
|
+
const seen = new Map();
|
|
41
|
+
walk(batch, seen);
|
|
42
|
+
for (const [name, bytes] of seen) {
|
|
43
|
+
const field = entry.fields.get(name) ?? { present: 0, bytes: 0 };
|
|
44
|
+
field.present += 1;
|
|
45
|
+
field.bytes += bytes;
|
|
46
|
+
entry.fields.set(name, field);
|
|
47
|
+
}
|
|
48
|
+
tally.set(harness, entry);
|
|
49
|
+
};
|
|
50
|
+
/** Adds one batch (the parsed skeleton, before it is written) to the tally. It never throws: a batch it cannot read (a cycle) adds nothing, and the deposit goes on. */
|
|
51
|
+
export const tallyBatch = (tally, batch) => {
|
|
52
|
+
try {
|
|
53
|
+
add(tally, batch);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Telemetry is never allowed to change what a command does.
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/** One `wire` event for each harness the pass built a batch for: the names, and beside them the batches that held each and the bytes it took. */
|
|
60
|
+
export const recordWire = (emitter, tally) => {
|
|
61
|
+
for (const [harness, entry] of [...tally].sort(([a], [b]) => a.localeCompare(b))) {
|
|
62
|
+
const fields = [...entry.fields].sort(([a], [b]) => a.localeCompare(b));
|
|
63
|
+
emitEvent(emitter, 'wire', {
|
|
64
|
+
harness,
|
|
65
|
+
batches: entry.batches,
|
|
66
|
+
bytes: entry.bytes,
|
|
67
|
+
fields: fields.map(([name]) => name),
|
|
68
|
+
present: fields.map(([, field]) => field.present),
|
|
69
|
+
fieldBytes: fields.map(([, field]) => field.bytes),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/** One send of one batch. `code` is the pool's own word for a refusal; the schema drops anything that is not a code. */
|
|
74
|
+
export const recordSend = (emitter, send) => {
|
|
75
|
+
emitEvent(emitter, 'wire-send', { sent: send.status === 201 ? 1 : 0, status: send.status, bytes: send.bytes, ...(send.code === undefined ? {} : { code: send.code }) });
|
|
76
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manyoars/oars",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The oars CLI:
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "The oars CLI: sign in, manage API keys, and keep a local memory of your own coding-agent sessions on your machine. The session commands upload nothing except what `gold arms` sends through your own Claude subscription; telemetry (on by default) and the nightly pool send after you accept a pool covenant are described in the README",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
-
"
|
|
20
|
+
"vocab"
|
|
21
21
|
],
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@playwright/test": "^1.64.1",
|
|
39
39
|
"msw": "^3.0.3",
|
|
40
|
-
"typescript": "~5.9.3"
|
|
41
|
-
"yaml": "^2.9.1"
|
|
40
|
+
"typescript": "~5.9.3"
|
|
42
41
|
}
|
|
43
42
|
}
|