@manyoars/oars 0.4.1 → 0.5.2
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 +79 -29
- package/dist/bin.js +3 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +207 -21
- package/dist/club.d.ts +19 -0
- package/dist/club.js +132 -0
- package/dist/gate/hook-fill-main.js +1 -1
- package/dist/gate/hook-fill.d.ts +1 -1
- package/dist/gate/hook-fill.js +1 -1
- package/dist/gate/hook-main.js +7 -5
- package/dist/gate/hook.d.ts +12 -1
- package/dist/gate/hook.js +38 -7
- package/dist/hook-bin.js +23 -4
- package/dist/main.js +5 -0
- package/dist/node-floor.d.ts +10 -0
- package/dist/node-floor.js +23 -0
- package/dist/oars-hook.d.ts +2 -0
- package/dist/oars-hook.js +7 -0
- package/dist/sessions/allow.d.ts +1 -0
- package/dist/sessions/allow.js +1 -1
- package/dist/sessions/call-shape.d.ts +111 -0
- package/dist/sessions/call-shape.js +464 -0
- package/dist/sessions/candidates.d.ts +65 -0
- package/dist/sessions/candidates.js +236 -0
- package/dist/sessions/card-command.d.ts +24 -0
- package/dist/sessions/card-command.js +71 -0
- package/dist/sessions/card-font.d.ts +6 -0
- package/dist/sessions/card-font.js +36 -0
- package/dist/sessions/card-line.d.ts +30 -0
- package/dist/sessions/card-line.js +105 -0
- package/dist/sessions/card-render.d.ts +15 -0
- package/dist/sessions/card-render.js +146 -0
- package/dist/sessions/card.d.ts +59 -0
- package/dist/sessions/card.js +107 -0
- package/dist/sessions/cell-increments.d.ts +14 -3
- package/dist/sessions/cell-increments.js +79 -12
- package/dist/sessions/cells.d.ts +42 -4
- package/dist/sessions/cells.js +72 -18
- package/dist/sessions/codex-hook-bin.d.ts +1 -0
- package/dist/sessions/codex-hook-bin.js +24 -0
- package/dist/sessions/codex-hook-install.d.ts +21 -0
- package/dist/sessions/codex-hook-install.js +47 -0
- package/dist/sessions/codex-hook-view.d.ts +20 -0
- package/dist/sessions/codex-hook-view.js +20 -0
- package/dist/sessions/codex-hook.d.ts +77 -0
- package/dist/sessions/codex-hook.js +255 -0
- package/dist/sessions/codex.d.ts +9 -0
- package/dist/sessions/codex.js +104 -18
- package/dist/sessions/command.d.ts +17 -2
- package/dist/sessions/command.js +145 -33
- package/dist/sessions/copilot.d.ts +3 -0
- package/dist/sessions/copilot.js +12 -4
- package/dist/sessions/derive.js +2 -1
- package/dist/sessions/empty-state.d.ts +17 -0
- package/dist/sessions/empty-state.js +38 -0
- package/dist/sessions/exclusion.js +48 -3
- package/dist/sessions/extract.d.ts +16 -3
- package/dist/sessions/extract.js +281 -24
- package/dist/sessions/gemini.d.ts +2 -0
- package/dist/sessions/gemini.js +29 -2
- package/dist/sessions/harness-files.d.ts +27 -0
- package/dist/sessions/harness-files.js +109 -0
- package/dist/sessions/head-only.d.ts +7 -0
- package/dist/sessions/head-only.js +21 -0
- package/dist/sessions/hook-calls.js +4 -3
- package/dist/sessions/hooks.d.ts +1 -1
- package/dist/sessions/hooks.js +6 -2
- package/dist/sessions/identity.d.ts +6 -0
- package/dist/sessions/injected.d.ts +13 -0
- package/dist/sessions/injected.js +260 -0
- package/dist/sessions/install.d.ts +31 -20
- package/dist/sessions/install.js +80 -82
- package/dist/sessions/load-status.d.ts +41 -0
- package/dist/sessions/load-status.js +156 -0
- package/dist/sessions/machine-child-deps.d.ts +5 -0
- package/dist/sessions/machine-child-deps.js +83 -0
- package/dist/sessions/machine-child.d.ts +43 -0
- package/dist/sessions/machine-child.js +120 -0
- package/dist/sessions/machine-credential.d.ts +10 -0
- package/dist/sessions/machine-credential.js +20 -0
- package/dist/sessions/machine-enroll.d.ts +47 -0
- package/dist/sessions/machine-enroll.js +230 -0
- package/dist/sessions/machine-hold.d.ts +49 -0
- package/dist/sessions/machine-hold.js +88 -0
- package/dist/sessions/machine-ids.d.ts +6 -0
- package/dist/sessions/machine-ids.js +9 -0
- package/dist/sessions/machine-notice.d.ts +21 -0
- package/dist/sessions/machine-notice.js +89 -0
- package/dist/sessions/machine-pull.d.ts +40 -0
- package/dist/sessions/machine-pull.js +220 -0
- package/dist/sessions/machine-repo.d.ts +7 -0
- package/dist/sessions/machine-repo.js +30 -0
- package/dist/sessions/machine-scan-cache.d.ts +12 -0
- package/dist/sessions/machine-scan-cache.js +47 -0
- package/dist/sessions/machine-send.d.ts +12 -0
- package/dist/sessions/machine-send.js +156 -0
- package/dist/sessions/machine-stop.d.ts +22 -0
- package/dist/sessions/machine-stop.js +53 -0
- package/dist/sessions/machine.d.ts +41 -0
- package/dist/sessions/machine.js +89 -0
- package/dist/sessions/opencode.d.ts +2 -0
- package/dist/sessions/opencode.js +6 -0
- package/dist/sessions/otel.d.ts +21 -0
- package/dist/sessions/otel.js +109 -38
- package/dist/sessions/owner-only.d.ts +7 -0
- package/dist/sessions/owner-only.js +34 -0
- package/dist/sessions/pi.d.ts +13 -1
- package/dist/sessions/pi.js +27 -13
- package/dist/sessions/pool-books.d.ts +1 -1
- package/dist/sessions/pool-books.js +1 -1
- package/dist/sessions/pool-choices.d.ts +4 -2
- package/dist/sessions/pool-choices.js +4 -2
- package/dist/sessions/pool-command.d.ts +46 -6
- package/dist/sessions/pool-command.js +613 -136
- package/dist/sessions/pool-delta.d.ts +7 -0
- package/dist/sessions/pool-delta.js +28 -5
- package/dist/sessions/pool-due.d.ts +43 -0
- package/dist/sessions/pool-due.js +113 -0
- package/dist/sessions/pool-nightly.d.ts +9 -1
- package/dist/sessions/pool-nightly.js +65 -21
- package/dist/sessions/pool-outbox.d.ts +51 -5
- package/dist/sessions/pool-outbox.js +217 -24
- package/dist/sessions/pool-print.d.ts +1 -1
- package/dist/sessions/pool-raw.js +4 -3
- package/dist/sessions/pool-replica.d.ts +4 -4
- package/dist/sessions/pool-replica.js +26 -7
- package/dist/sessions/pool-send-lock.js +1 -1
- package/dist/sessions/pool-sent.d.ts +2 -0
- package/dist/sessions/pool-sent.js +21 -4
- package/dist/sessions/pool-wire.d.ts +176 -15
- package/dist/sessions/pool-wire.js +297 -33
- package/dist/sessions/pool.js +4 -2
- package/dist/sessions/public-vocabulary.d.ts +5 -0
- package/dist/sessions/public-vocabulary.js +16 -0
- package/dist/sessions/read-call-shape.js +12 -6
- package/dist/sessions/read-fit.d.ts +5 -0
- package/dist/sessions/read-fit.js +23 -2
- package/dist/sessions/read-interaction.js +1 -1
- package/dist/sessions/read-lead-lag.js +1 -1
- package/dist/sessions/read-readable.js +1 -1
- package/dist/sessions/read.d.ts +1 -1
- package/dist/sessions/recall-consequence.d.ts +2 -2
- package/dist/sessions/recall-consequence.js +14 -9
- package/dist/sessions/recall-object.d.ts +9 -2
- package/dist/sessions/receipt.d.ts +8 -0
- package/dist/sessions/receipt.js +78 -0
- package/dist/sessions/recovery-calls.d.ts +3 -2
- package/dist/sessions/recovery-calls.js +9 -3
- package/dist/sessions/redact.d.ts +11 -2
- package/dist/sessions/redact.js +201 -23
- package/dist/sessions/repo-root.d.ts +7 -0
- package/dist/sessions/repo-root.js +17 -13
- package/dist/sessions/reversion.d.ts +8 -0
- package/dist/sessions/reversion.js +114 -0
- package/dist/sessions/schema.d.ts +78 -4
- package/dist/sessions/schema.js +31 -1
- package/dist/sessions/session-shape.d.ts +2 -1
- package/dist/sessions/session-shape.js +15 -0
- package/dist/sessions/sources.d.ts +9 -0
- package/dist/sessions/sources.js +16 -5
- package/dist/sessions/steps.js +3 -2
- package/dist/sessions/store.d.ts +41 -2
- package/dist/sessions/store.js +68 -16
- package/dist/sessions/try-demo.d.ts +8 -0
- package/dist/sessions/try-demo.js +90 -0
- package/dist/sessions/try.d.ts +13 -0
- package/dist/sessions/try.js +50 -0
- package/dist/sessions/vscode-copilot.d.ts +4 -0
- package/dist/sessions/vscode-copilot.js +25 -18
- package/dist/telemetry/command.js +12 -6
- package/dist/telemetry/consent.d.ts +9 -3
- package/dist/telemetry/consent.js +25 -9
- package/dist/telemetry/fields.d.ts +11 -0
- package/dist/telemetry/fields.js +56 -0
- package/dist/telemetry/measures.d.ts +13 -0
- package/dist/telemetry/measures.js +30 -0
- package/dist/telemetry/observe.d.ts +15 -2
- package/dist/telemetry/observe.js +34 -7
- package/dist/telemetry/region.d.ts +6 -0
- package/dist/telemetry/region.js +28 -0
- package/dist/telemetry/report.js +1 -1
- package/dist/telemetry/schema.d.ts +3 -0
- package/dist/telemetry/schema.js +11 -1
- package/dist/telemetry/send.d.ts +5 -2
- package/dist/telemetry/send.js +15 -7
- package/dist/telemetry/words.d.ts +6 -0
- package/dist/telemetry/words.js +6 -0
- package/package.json +24 -5
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The bundled demo behind `oars sessions try`. EVERYTHING HERE IS MADE UP: three invented Claude Code sessions in an invented project
|
|
2
|
+
// (`/work/demo`), written so the same two errors recur. No line comes from a real session, a real person, or a real repository, and no
|
|
3
|
+
// number `try` prints from it describes anyone's work. The sessions are lines of the same shape Claude Code writes, so the demo runs the
|
|
4
|
+
// real deposit, recall, books, and hook code and not a canned answer.
|
|
5
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { slugOf } from './allow.js';
|
|
8
|
+
/** The invented working directory the demo sessions claim; no such folder is read. Resolved, because a folder's name is its path's slug on this platform. */
|
|
9
|
+
export const DEMO_CWD = resolve('/work/demo');
|
|
10
|
+
/** The folder Claude Code would name for `DEMO_CWD` under `~/.claude/projects`. */
|
|
11
|
+
export const DEMO_FOLDER = slugOf(DEMO_CWD);
|
|
12
|
+
/** An error the three demo sessions all hit, as a stranger would paste it. */
|
|
13
|
+
export const DEMO_ERROR = 'unexpected EOF while looking for matching quote';
|
|
14
|
+
const SHELL_ERROR = `Exit code 2\nbash: -c: line 1: ${DEMO_ERROR}\n`;
|
|
15
|
+
const WRITE_ERROR = 'File has not been read yet. Read it first before writing to it.';
|
|
16
|
+
const shell = (command, result, isError = false) => ({ tool: 'Bash', input: { command }, result, isError });
|
|
17
|
+
const read = (file) => ({ tool: 'Read', input: { file_path: file }, result: 'file contents', isError: false });
|
|
18
|
+
const edit = (file, result, isError = false) => ({ tool: 'Write', input: { file_path: file, content: 'echo ok' }, result, isError });
|
|
19
|
+
const SESSIONS = [
|
|
20
|
+
{
|
|
21
|
+
id: 'demo-0001',
|
|
22
|
+
day: '2026-10-06',
|
|
23
|
+
prompt: 'fix the failing build in /work/demo/src/main.ts',
|
|
24
|
+
turns: [read('/work/demo/src/main.ts'), shell("echo 'it's broken", SHELL_ERROR, true), edit('/work/demo/fix.sh', WRITE_ERROR, true), read('/work/demo/fix.sh'), edit('/work/demo/fix.sh', 'ok'), shell('npm test', 'all passed')],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'demo-0002',
|
|
28
|
+
day: '2026-10-13',
|
|
29
|
+
prompt: 'add a test for the parser in /work/demo/src/parse.ts',
|
|
30
|
+
turns: [read('/work/demo/src/parse.ts'), shell("git commit -m 'add parser test", SHELL_ERROR, true), shell('git commit -m "add parser test"', 'committed'), shell('npm test', 'all passed')],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'demo-0003',
|
|
34
|
+
day: '2026-10-20',
|
|
35
|
+
prompt: 'why is the deploy script slow',
|
|
36
|
+
turns: [read('/work/demo/deploy.sh'), edit('/work/demo/deploy.sh', WRITE_ERROR, true), read('/work/demo/deploy.sh'), shell("echo 'timing", SHELL_ERROR, true)],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
const stamp = (day, second) => `${day}T10:${String(Math.floor(second / 60)).padStart(2, '0')}:${String(second % 60).padStart(2, '0')}.000Z`;
|
|
40
|
+
/** One session as the JSON lines Claude Code writes: a prompt, then per turn an assistant tool call and the user line that carries its result. */
|
|
41
|
+
const linesOf = (session) => {
|
|
42
|
+
let at = 0;
|
|
43
|
+
let parent = null;
|
|
44
|
+
const next = () => {
|
|
45
|
+
at += 3;
|
|
46
|
+
const uuid = `${session.id}-${String(at).padStart(4, '0')}`;
|
|
47
|
+
const line = { uuid, time: stamp(session.day, at), parentUuid: parent };
|
|
48
|
+
parent = uuid;
|
|
49
|
+
return line;
|
|
50
|
+
};
|
|
51
|
+
const common = { isSidechain: false, version: '2.1.300' };
|
|
52
|
+
const first = next();
|
|
53
|
+
const lines = [{ type: 'user', ...common, uuid: first.uuid, parentUuid: null, timestamp: first.time, cwd: DEMO_CWD, gitBranch: 'main', message: { role: 'user', content: session.prompt } }];
|
|
54
|
+
session.turns.forEach((turn, index) => {
|
|
55
|
+
const call = next();
|
|
56
|
+
const result = next();
|
|
57
|
+
const id = `t${String(index + 1)}`;
|
|
58
|
+
lines.push({
|
|
59
|
+
type: 'assistant',
|
|
60
|
+
...common,
|
|
61
|
+
uuid: call.uuid,
|
|
62
|
+
parentUuid: call.parentUuid,
|
|
63
|
+
timestamp: call.time,
|
|
64
|
+
requestedModel: 'claude-opus-5-5',
|
|
65
|
+
requestId: `req_${session.id}_${String(index + 1)}`,
|
|
66
|
+
apiBlockIndex: 0,
|
|
67
|
+
message: {
|
|
68
|
+
role: 'assistant',
|
|
69
|
+
model: 'claude-opus-5-5',
|
|
70
|
+
content: [{ type: 'tool_use', id, name: turn.tool, input: turn.input }],
|
|
71
|
+
usage: { input_tokens: 10, cache_creation_input_tokens: 100, cache_read_input_tokens: 9000, output_tokens: 80 },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
lines.push({
|
|
75
|
+
type: 'user',
|
|
76
|
+
...common,
|
|
77
|
+
uuid: result.uuid,
|
|
78
|
+
parentUuid: result.parentUuid,
|
|
79
|
+
timestamp: result.time,
|
|
80
|
+
message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, is_error: turn.isError, content: turn.result }] },
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
return lines.map((line) => JSON.stringify(line));
|
|
84
|
+
};
|
|
85
|
+
/** Writes the demo sessions under `<projectsRoot>/<DEMO_FOLDER>/`, the layout Claude Code uses. */
|
|
86
|
+
export const writeDemoSessions = async (projectsRoot) => {
|
|
87
|
+
const folder = join(projectsRoot, DEMO_FOLDER);
|
|
88
|
+
await mkdir(folder, { recursive: true });
|
|
89
|
+
await Promise.all(SESSIONS.map((session) => writeFile(join(folder, `${session.id}.jsonl`), `${linesOf(session).join('\n')}\n`)));
|
|
90
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Outcome } from '../cli.js';
|
|
2
|
+
import { type SessionsDeps } from './command.js';
|
|
3
|
+
/** Printed once at the top and under every heading, so no line of the output can be mistaken for the reader's own history. */
|
|
4
|
+
export declare const SYNTHETIC_LABEL = "SYNTHETIC DATA: made-up sessions bundled with oars, not your history";
|
|
5
|
+
/** A prefix the throwaway folder carries, so a leftover one is recognizable in the temp folder. */
|
|
6
|
+
export declare const TRY_PREFIX = "oars-try-";
|
|
7
|
+
/**
|
|
8
|
+
* Builds the demo store under the OS temp folder, runs the three commands over it with the same code `oars sessions` runs, and removes the
|
|
9
|
+
* folder on the way out, success or failure (a thrown step included). A SIGINT or SIGTERM during the run skips `finally` and leaves the
|
|
10
|
+
* folder in the temp folder for the OS to clear; the run is about a second long. The runtime is its own: it never takes the caller's store, projects folder, or other sources,
|
|
11
|
+
* and no command here reaches the network or a model.
|
|
12
|
+
*/
|
|
13
|
+
export declare const sessionsTry: (deps: Pick<SessionsDeps, "now">) => Promise<Outcome>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// `oars sessions try`: recall, books, and the hook line over a throwaway store of made-up sessions, for someone with no history of their own.
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { PassThrough } from 'node:stream';
|
|
6
|
+
import { sessionsBooks, sessionsContext, sessionsDeposit, sessionsRecall } from './command.js';
|
|
7
|
+
import { DEMO_CWD, DEMO_ERROR, writeDemoSessions } from './try-demo.js';
|
|
8
|
+
/** Printed once at the top and under every heading, so no line of the output can be mistaken for the reader's own history. */
|
|
9
|
+
export const SYNTHETIC_LABEL = 'SYNTHETIC DATA: made-up sessions bundled with oars, not your history';
|
|
10
|
+
/** A prefix the throwaway folder carries, so a leftover one is recognizable in the temp folder. */
|
|
11
|
+
export const TRY_PREFIX = 'oars-try-';
|
|
12
|
+
const heading = (title, command) => ['', `== ${title} (${SYNTHETIC_LABEL}) ==`, `$ ${command}`];
|
|
13
|
+
/**
|
|
14
|
+
* Builds the demo store under the OS temp folder, runs the three commands over it with the same code `oars sessions` runs, and removes the
|
|
15
|
+
* folder on the way out, success or failure (a thrown step included). A SIGINT or SIGTERM during the run skips `finally` and leaves the
|
|
16
|
+
* folder in the temp folder for the OS to clear; the run is about a second long. The runtime is its own: it never takes the caller's store, projects folder, or other sources,
|
|
17
|
+
* and no command here reaches the network or a model.
|
|
18
|
+
*/
|
|
19
|
+
export const sessionsTry = async (deps) => {
|
|
20
|
+
const root = await mkdtemp(join(tmpdir(), TRY_PREFIX));
|
|
21
|
+
try {
|
|
22
|
+
const projectsRoot = join(root, 'projects');
|
|
23
|
+
await writeDemoSessions(projectsRoot);
|
|
24
|
+
const runtime = { storeDir: join(root, 'store'), projectsRoot, sources: [], stdin: new PassThrough(), stdout: { write: () => true } };
|
|
25
|
+
const own = { sessions: runtime, install: undefined, now: deps.now };
|
|
26
|
+
const deposited = await sessionsDeposit(own, { allow: [], allowFiles: [], redactDomains: [] });
|
|
27
|
+
const recalled = await sessionsRecall(own, { error: DEMO_ERROR });
|
|
28
|
+
const booked = await sessionsBooks(own, { by: 'class', sinceDays: undefined });
|
|
29
|
+
const hooked = await sessionsContext(own, { cwd: DEMO_CWD, top: 5, maxChars: 1200 });
|
|
30
|
+
const text = [
|
|
31
|
+
`${SYNTHETIC_LABEL}.`,
|
|
32
|
+
'oars read three invented Claude Code sessions into a throwaway store, ran the commands below over it, and deletes that store when it exits.',
|
|
33
|
+
'Your own store and session folders were not read or written, and nothing left this machine.',
|
|
34
|
+
...heading('Deposit', 'oars sessions deposit'),
|
|
35
|
+
deposited.text,
|
|
36
|
+
...heading('Recall', `oars sessions recall --error "${DEMO_ERROR}"`),
|
|
37
|
+
recalled.text,
|
|
38
|
+
...heading('Books', 'oars sessions books --by class'),
|
|
39
|
+
booked.text,
|
|
40
|
+
...heading('Hook line', `oars sessions recall --context --cwd ${DEMO_CWD}`),
|
|
41
|
+
hooked.text,
|
|
42
|
+
'',
|
|
43
|
+
'To use oars on your own sessions: oars sessions deposit',
|
|
44
|
+
].join('\n');
|
|
45
|
+
return { document: { ok: true, synthetic: true, label: SYNTHETIC_LABEL, deposit: deposited.document, recall: recalled.document, books: booked.document, hook: hooked.document }, text };
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await rm(root, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -20,6 +20,8 @@ export interface VscodeStore {
|
|
|
20
20
|
sessions: VscodeSession[];
|
|
21
21
|
/** The session as transcript text for `captureSession`; throws when no file of it can be read or holds no parseable entry. */
|
|
22
22
|
transcript: (session: VscodeSession) => Promise<string>;
|
|
23
|
+
/** True when the session's chat data holds an empty request list (`chatHeadOnly`); `sessions verify` counts such a file as empty, not unreadable. */
|
|
24
|
+
headOnly: (session: VscodeSession) => Promise<boolean>;
|
|
23
25
|
/** Files skipped because their name is not a safe file name (it names a skeleton), counted so the skip is never silent. */
|
|
24
26
|
rejected: number;
|
|
25
27
|
/** Folders and files that could not be inspected, reported once each and never fatal; the reason names no path. */
|
|
@@ -34,5 +36,7 @@ export declare const chatBadLines: (text: string) => string[];
|
|
|
34
36
|
export declare const chatData: (text: string, jsonl: boolean) => Obj;
|
|
35
37
|
/** One chat session as the lines `captureSession` reads. */
|
|
36
38
|
export declare const chatLines: (data: Obj, fallbackId: string, folder: string | null) => Obj[];
|
|
39
|
+
/** A session object whose request list is empty: the chat was opened and nothing was asked. Anything else with no turn (a renamed list, say) is not head-only. */
|
|
40
|
+
export declare const chatHeadOnly: (data: Obj) => boolean;
|
|
37
41
|
export declare const openVscodeCopilotStore: (root: string) => Promise<VscodeStore | undefined>;
|
|
38
42
|
export {};
|
|
@@ -378,6 +378,26 @@ export const chatLines = (data, fallbackId, folder) => {
|
|
|
378
378
|
}
|
|
379
379
|
return lines;
|
|
380
380
|
};
|
|
381
|
+
/** The first of the session's files that reads, handed to `use`; the failure of the last one when none does. */
|
|
382
|
+
const firstReadable = async (session, use) => {
|
|
383
|
+
let failure = new Error('no file');
|
|
384
|
+
for (const file of session.files) {
|
|
385
|
+
try {
|
|
386
|
+
const text = await readFile(file, 'utf8');
|
|
387
|
+
const jsonl = file.endsWith('.jsonl');
|
|
388
|
+
// VS Code throws on an empty log ("Empty log file", objectMutationLog.ts:393) and falls back to the `.json`; the last file alone may read as an empty session.
|
|
389
|
+
if (jsonl && text.trim() === '' && file !== session.files[session.files.length - 1])
|
|
390
|
+
throw new Error('empty log file');
|
|
391
|
+
return use(chatData(text, jsonl), text, jsonl);
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
failure = error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
throw failure;
|
|
398
|
+
};
|
|
399
|
+
/** A session object whose request list is empty: the chat was opened and nothing was asked. Anything else with no turn (a renamed list, say) is not head-only. */
|
|
400
|
+
export const chatHeadOnly = (data) => Array.isArray(data.requests) && data.requests.length === 0;
|
|
381
401
|
export const openVscodeCopilotStore = async (root) => {
|
|
382
402
|
const unreadable = [];
|
|
383
403
|
const found = new Map();
|
|
@@ -472,23 +492,10 @@ export const openVscodeCopilotStore = async (root) => {
|
|
|
472
492
|
sessions,
|
|
473
493
|
rejected,
|
|
474
494
|
unreadable,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const jsonl = file.endsWith('.jsonl');
|
|
481
|
-
// VS Code throws on an empty log ("Empty log file", objectMutationLog.ts:393) and falls back to the `.json`; the last file alone may read as an empty session.
|
|
482
|
-
if (jsonl && text.trim() === '' && file !== session.files[session.files.length - 1])
|
|
483
|
-
throw new Error('empty log file');
|
|
484
|
-
const lines = [...chatLines(chatData(text, jsonl), session.id, session.folder).map((line) => JSON.stringify(line)), ...(jsonl ? chatBadLines(text) : [])];
|
|
485
|
-
return lines.join('\n') + (lines.length === 0 ? '' : '\n');
|
|
486
|
-
}
|
|
487
|
-
catch (error) {
|
|
488
|
-
failure = error;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
throw failure;
|
|
492
|
-
},
|
|
495
|
+
headOnly: async (session) => await firstReadable(session, (data) => chatHeadOnly(data)),
|
|
496
|
+
transcript: async (session) => await firstReadable(session, (data, text, jsonl) => {
|
|
497
|
+
const lines = [...chatLines(data, session.id, session.folder).map((line) => JSON.stringify(line)), ...(jsonl ? chatBadLines(text) : [])];
|
|
498
|
+
return lines.join('\n') + (lines.length === 0 ? '' : '\n');
|
|
499
|
+
}),
|
|
493
500
|
};
|
|
494
501
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// `oars telemetry status|on|off|show`.
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import { CliError, ExitCode } from '../errors.js';
|
|
4
|
-
import { consentOf, markNoticeShown, NOTICE, setEnabled } from './consent.js';
|
|
4
|
+
import { consentOf, everShownNotice, markNoticeShown, NOTICE, setEnabled } from './consent.js';
|
|
5
|
+
import { recordFirstRun } from './observe.js';
|
|
5
6
|
import { readFleet, reportOf, textOf } from './report.js';
|
|
6
7
|
import { readSpool, spoolPaths } from './spool.js';
|
|
7
8
|
const STEP = 'telemetry';
|
|
@@ -15,6 +16,7 @@ const REASONS = {
|
|
|
15
16
|
setting: 'by `oars telemetry on` or `off`',
|
|
16
17
|
OARS_TELEMETRY: 'by OARS_TELEMETRY',
|
|
17
18
|
DO_NOT_TRACK: 'by DO_NOT_TRACK',
|
|
19
|
+
region: "because this machine's time zone is in the EU, the EEA, or the UK",
|
|
18
20
|
};
|
|
19
21
|
const sizeOf = (path) => {
|
|
20
22
|
try {
|
|
@@ -34,7 +36,7 @@ export const telemetryStatus = (deps) => {
|
|
|
34
36
|
describe(consent),
|
|
35
37
|
`The notice has ${consent.noticeShown ? '' : 'not '}been shown.`,
|
|
36
38
|
`${String(events)} events (${String(bytes)} bytes) in ${dir}.`,
|
|
37
|
-
'Events are sent to the pool server in OARS_POOL_URL, else to the Many Oars pool (pool.manyoars.com, which is live), and only while telemetry is on and the notice has been shown.',
|
|
39
|
+
'Events are sent to the pool server in OARS_POOL_URL, else to the Many Oars pool (pool.manyoars.com, which is live), and only while telemetry is on and the notice has been shown, and never from a machine whose time zone is in the EU, the EEA, or the UK.',
|
|
38
40
|
];
|
|
39
41
|
return { document: { ok: true, step: STEP, enabled: consent.enabled, reason: consent.reason, noticeShown: consent.noticeShown, events, bytes, dir }, text: lines.join('\n') };
|
|
40
42
|
};
|
|
@@ -47,12 +49,16 @@ const save = (dir, enabled) => {
|
|
|
47
49
|
}
|
|
48
50
|
};
|
|
49
51
|
export const telemetryOn = (deps) => {
|
|
50
|
-
const
|
|
52
|
+
const runtime = runtimeOf(deps);
|
|
53
|
+
const { dir } = runtime;
|
|
54
|
+
const first = !everShownNotice(dir);
|
|
51
55
|
save(dir, true);
|
|
52
|
-
// Running `on` is the notice: it is printed here and recorded as shown.
|
|
53
|
-
markNoticeShown(dir, deps.now);
|
|
56
|
+
// Running `on` is the notice: it is printed here and recorded as shown, so the run's own check finds it shown and sends the `first-run` event from here.
|
|
57
|
+
const marked = markNoticeShown(dir, deps.now);
|
|
54
58
|
const consent = consentOf(deps.env, dir);
|
|
55
|
-
|
|
59
|
+
if (first && marked && consent.enabled)
|
|
60
|
+
recordFirstRun(deps, runtime);
|
|
61
|
+
const override = consent.enabled ? '' : consent.reason === 'region' ? `\nIt stays off: this machine's time zone is in the EU, the EEA, or the UK.` : `\nIt stays off in this shell: ${consent.reason} is set.`;
|
|
56
62
|
return { document: { ok: true, step: STEP, enabled: consent.enabled, reason: consent.reason, notice: NOTICE }, text: `Telemetry is on.\n${NOTICE}${override}` };
|
|
57
63
|
};
|
|
58
64
|
export const telemetryOff = (deps) => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
/** The version of the notice below. `markNoticeShown` stores it and each batch is stamped with the stored version; raise it whenever the notice changes in meaning. */
|
|
2
|
+
export declare const NOTICE_VERSION = 2;
|
|
1
3
|
/** Printed once on stderr, on the first run that is not a hook; also what `oars telemetry on` prints. */
|
|
2
4
|
export declare const NOTICE: string;
|
|
3
|
-
export type Reason = 'default' | 'setting' | 'OARS_TELEMETRY' | 'DO_NOT_TRACK';
|
|
5
|
+
export type Reason = 'default' | 'setting' | 'OARS_TELEMETRY' | 'DO_NOT_TRACK' | 'region';
|
|
4
6
|
export interface Consent {
|
|
5
7
|
enabled: boolean;
|
|
6
8
|
/** Which switch decided `enabled`. */
|
|
@@ -8,10 +10,14 @@ export interface Consent {
|
|
|
8
10
|
/** The notice was printed (or `on` was run) on this install. */
|
|
9
11
|
noticeShown: boolean;
|
|
10
12
|
}
|
|
11
|
-
export declare const consentOf: (env: NodeJS.ProcessEnv, dir: string) => Consent;
|
|
13
|
+
export declare const consentOf: (env: NodeJS.ProcessEnv, dir: string, zone?: string | undefined) => Consent;
|
|
12
14
|
/** Saves `on` or `off`. Throws when the folder cannot be written: the command that asked reports it. */
|
|
13
15
|
export declare const setEnabled: (dir: string, enabled: boolean) => void;
|
|
14
16
|
/** Records that the notice was shown; `false` when it could not be recorded (nothing is recorded or sent from a folder that cannot be written). */
|
|
15
17
|
export declare const markNoticeShown: (dir: string, now: () => number) => boolean;
|
|
18
|
+
/** Whether this install has ever been shown any notice (an older version included). */
|
|
19
|
+
export declare const everShownNotice: (dir: string) => boolean;
|
|
20
|
+
/** The notice version this install was shown, or `undefined` when none (or an unreadable file). */
|
|
21
|
+
export declare const shownNoticeVersion: (dir: string) => number | undefined;
|
|
16
22
|
/** Whether the sender (t875) may send: telemetry on, and the notice shown. Off, or notice not yet shown, means nothing sends. */
|
|
17
|
-
export declare const telemetrySendAllowed: (env: NodeJS.ProcessEnv, dir: string) => boolean;
|
|
23
|
+
export declare const telemetrySendAllowed: (env: NodeJS.ProcessEnv, dir: string, zone?: string | undefined) => boolean;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Whether telemetry records and may send, and the one-time notice. The off switches are `oars telemetry off` (a saved setting), `OARS_TELEMETRY=0`, and
|
|
2
|
-
// `DO_NOT_TRACK=1
|
|
2
|
+
// `DO_NOT_TRACK=1`, and a machine whose time zone is in the EU, the EEA, or the UK (`region.ts`), which no switch overrides; the environment wins over a saved `on`. `telemetrySendAllowed` is the check the sender must call before any byte leaves.
|
|
3
3
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { isClosedZone, machineZone } from './region.js';
|
|
4
5
|
import { spoolPaths } from './spool.js';
|
|
6
|
+
/** The version of the notice below. `markNoticeShown` stores it and each batch is stamped with the stored version; raise it whenever the notice changes in meaning. */
|
|
7
|
+
export const NOTICE_VERSION = 2;
|
|
5
8
|
/** Printed once on stderr, on the first run that is not a hook; also what `oars telemetry on` prints. */
|
|
6
9
|
export const NOTICE = [
|
|
7
|
-
'oars records how it is used, on this machine: one event per command with its path, flag names (never their values), how long it took, the exit code, and the class of any error; one per hook fire, recall, deposit, and fetch, as counts and closed words (which hook, how long, hit or miss, how many bytes, the harness name and version and the model name, how many sessions a deposit counted and how many each gate kept out, never which); each with a random install id, the oars version, the OS, and the time.
|
|
8
|
-
'It never records session content, prompts, file paths, URLs, arguments, or anything you typed. oars sends the events in batches to the pool server in OARS_POOL_URL, else to the Many Oars pool (pool.manyoars.com, which is live)
|
|
10
|
+
'oars records how it is used, on this machine: one event per command with its path, flag names (never their values), how long it took, the exit code, and the class of any error; one per hook fire, recall, deposit, and fetch, as counts and closed words (which hook, how long, hit or miss, how many bytes, the harness name and version and the model name, how many sessions a deposit counted and how many each gate kept out, never which); each with a random install id, the oars version, the OS, and the time. Each time the background sender runs, at most about once an hour, it also records counts and sizes of the session store, the lists of harness versions and model names it holds, and what the run cost. Events are sent in a batch at most once a day. A crash is recorded as the error\'s class and a hash of its stack (never its message). Each pool deposit records, per batch, which fields it held and how many bytes each took, and each send (a wire-send event) its HTTP status, the pool\'s refusal code, and its size. It also records one event on the first run with whether it ran in CI and how this copy was installed, as closed words; whether each command ran in CI, as a closed word; and the covenant version each time the covenant screen is shown at a terminal.',
|
|
11
|
+
'It never records session content, prompts, file paths, URLs, arguments, or anything you typed. oars sends the events in batches to the pool server in OARS_POOL_URL, else to the Many Oars pool (pool.manyoars.com, which is live). Many Oars Co. may license and sell Usage Data. oars sends no telemetry from a machine whose time zone is in the EU, the EEA, or the UK.',
|
|
9
12
|
'See exactly what is kept: oars telemetry show. Turn it off: oars telemetry off, or set OARS_TELEMETRY=0 or DO_NOT_TRACK=1. More: docs/telemetry.md.',
|
|
10
13
|
].join('\n');
|
|
11
14
|
const readSettings = (dir) => {
|
|
@@ -15,7 +18,12 @@ const readSettings = (dir) => {
|
|
|
15
18
|
return {};
|
|
16
19
|
const enabled = Reflect.get(parsed, 'enabled');
|
|
17
20
|
const shown = Reflect.get(parsed, 'noticeShownAt');
|
|
18
|
-
|
|
21
|
+
const version = Reflect.get(parsed, 'noticeVersion');
|
|
22
|
+
return {
|
|
23
|
+
...(typeof enabled === 'boolean' ? { enabled } : {}),
|
|
24
|
+
...(typeof shown === 'string' ? { noticeShownAt: shown } : {}),
|
|
25
|
+
...(typeof version === 'number' ? { noticeVersion: version } : {}),
|
|
26
|
+
};
|
|
19
27
|
}
|
|
20
28
|
catch {
|
|
21
29
|
return {};
|
|
@@ -31,9 +39,13 @@ const writeSettings = (dir, settings) => {
|
|
|
31
39
|
};
|
|
32
40
|
const OFF_WORDS = new Set(['0', 'false', 'off', 'no']);
|
|
33
41
|
const DO_NOT_TRACK_WORDS = new Set(['1', 'true']);
|
|
34
|
-
export const consentOf = (env, dir) => {
|
|
42
|
+
export const consentOf = (env, dir, zone = machineZone()) => {
|
|
35
43
|
const settings = readSettings(dir);
|
|
36
|
-
|
|
44
|
+
// An install that saw an older notice (no stored version, or another one) has not seen this one: it sees it again, and sends nothing until then.
|
|
45
|
+
const noticeShown = settings.noticeShownAt !== undefined && settings.noticeVersion === NOTICE_VERSION;
|
|
46
|
+
// First, so that no setting or `oars telemetry on` turns it back on in a closed region.
|
|
47
|
+
if (isClosedZone(zone))
|
|
48
|
+
return { enabled: false, reason: 'region', noticeShown };
|
|
37
49
|
if (OFF_WORDS.has((env.OARS_TELEMETRY ?? '').toLowerCase()))
|
|
38
50
|
return { enabled: false, reason: 'OARS_TELEMETRY', noticeShown };
|
|
39
51
|
if (DO_NOT_TRACK_WORDS.has((env.DO_NOT_TRACK ?? '').toLowerCase()))
|
|
@@ -49,15 +61,19 @@ export const setEnabled = (dir, enabled) => {
|
|
|
49
61
|
/** Records that the notice was shown; `false` when it could not be recorded (nothing is recorded or sent from a folder that cannot be written). */
|
|
50
62
|
export const markNoticeShown = (dir, now) => {
|
|
51
63
|
try {
|
|
52
|
-
writeSettings(dir, { ...readSettings(dir), noticeShownAt: new Date(now()).toISOString() });
|
|
64
|
+
writeSettings(dir, { ...readSettings(dir), noticeShownAt: new Date(now()).toISOString(), noticeVersion: NOTICE_VERSION });
|
|
53
65
|
return true;
|
|
54
66
|
}
|
|
55
67
|
catch {
|
|
56
68
|
return false;
|
|
57
69
|
}
|
|
58
70
|
};
|
|
71
|
+
/** Whether this install has ever been shown any notice (an older version included). */
|
|
72
|
+
export const everShownNotice = (dir) => readSettings(dir).noticeShownAt !== undefined;
|
|
73
|
+
/** The notice version this install was shown, or `undefined` when none (or an unreadable file). */
|
|
74
|
+
export const shownNoticeVersion = (dir) => readSettings(dir).noticeVersion;
|
|
59
75
|
/** Whether the sender (t875) may send: telemetry on, and the notice shown. Off, or notice not yet shown, means nothing sends. */
|
|
60
|
-
export const telemetrySendAllowed = (env, dir) => {
|
|
61
|
-
const consent = consentOf(env, dir);
|
|
76
|
+
export const telemetrySendAllowed = (env, dir, zone = machineZone()) => {
|
|
77
|
+
const consent = consentOf(env, dir, zone);
|
|
62
78
|
return consent.enabled && consent.noticeShown;
|
|
63
79
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Recall } from '../sessions/recall-object.js';
|
|
2
|
+
import type { CiWord, SurfaceWord } from './words.js';
|
|
2
3
|
/** The harness every hook oars installs runs under (Claude Code); one spelling for the session hooks and the gate hook, so the two emitters cannot drift. */
|
|
3
4
|
export declare const HOOK_HARNESS = "claude-code";
|
|
4
5
|
/** The harness a hook ran under, as the closed list names it, else `other`. */
|
|
@@ -14,3 +15,13 @@ export declare const frameWords: (harness: string | undefined, harnessVersion: s
|
|
|
14
15
|
};
|
|
15
16
|
/** A recall as counts: how it was asked, which families it served, how many reads and evidence rows, the size of the answer, and its arm. Never the query text, a name, or the rendered answer. */
|
|
16
17
|
export declare const recallFields: (recall: Recall, families: readonly string[], legacy?: boolean) => object;
|
|
18
|
+
/** Whether this run is in CI, as a closed word read from the environment's variable names. Only the name decides the word: the value is tested and dropped, never returned. */
|
|
19
|
+
export declare const ciWord: (env: NodeJS.ProcessEnv) => CiWord;
|
|
20
|
+
/**
|
|
21
|
+
* How this copy arrived, as a closed word, from the folder oars runs in and `npm_config_user_agent` and from nothing else. A harness store is read first (a plugin can carry its own
|
|
22
|
+
* `node_modules`), then a package runner, then a global install; a folder that fits none reads `unknown`, never a guess. The agent only confirms a runner: a package manager that started the
|
|
23
|
+
* process says its name there, and a folder called `dlx` with no such agent is not a runner.
|
|
24
|
+
*/
|
|
25
|
+
export declare const surfaceWord: (installPath: string, userAgent: string | undefined) => SurfaceWord;
|
|
26
|
+
/** The folder this module runs from: inside the package wherever it was installed (a symlinked bin resolves to its real folder). */
|
|
27
|
+
export declare const installFolder: () => string;
|
package/dist/telemetry/fields.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HARNESSES, OTHER } from '../sessions/pool-wire.js';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
2
3
|
import { modelWord, versionWord } from './schema.js';
|
|
3
4
|
/** The harness every hook oars installs runs under (Claude Code); one spelling for the session hooks and the gate hook, so the two emitters cannot drift. */
|
|
4
5
|
export const HOOK_HARNESS = 'claude-code';
|
|
@@ -24,3 +25,58 @@ export const recallFields = (recall, families, legacy = false) => ({
|
|
|
24
25
|
tokens: recall.answer.tokens,
|
|
25
26
|
arm: recall.arm,
|
|
26
27
|
});
|
|
28
|
+
/** A provider's own variable and the word it is read as. Order is the precedence: most providers also set `CI`, which on its own reads `other`. */
|
|
29
|
+
const CI_PROVIDERS = [
|
|
30
|
+
['GITHUB_ACTIONS', 'github-actions'],
|
|
31
|
+
['GITLAB_CI', 'gitlab-ci'],
|
|
32
|
+
['CIRCLECI', 'circleci'],
|
|
33
|
+
['TRAVIS', 'travis-ci'],
|
|
34
|
+
['JENKINS_URL', 'jenkins'],
|
|
35
|
+
['BUILDKITE', 'buildkite'],
|
|
36
|
+
['TF_BUILD', 'azure-pipelines'],
|
|
37
|
+
['BITBUCKET_BUILD_NUMBER', 'bitbucket'],
|
|
38
|
+
['TEAMCITY_VERSION', 'teamcity'],
|
|
39
|
+
['APPVEYOR', 'appveyor'],
|
|
40
|
+
['CODEBUILD_BUILD_ID', 'codebuild'],
|
|
41
|
+
];
|
|
42
|
+
/** A variable counts when it holds anything but the words a shell uses for no (an empty value, `0`, `false`, `no`, `off`). */
|
|
43
|
+
const NOT_SET = new Set(['', '0', 'false', 'no', 'off']);
|
|
44
|
+
const isSet = (value) => value !== undefined && !NOT_SET.has(value.toLowerCase());
|
|
45
|
+
/** Whether this run is in CI, as a closed word read from the environment's variable names. Only the name decides the word: the value is tested and dropped, never returned. */
|
|
46
|
+
export const ciWord = (env) => {
|
|
47
|
+
for (const [variable, word] of CI_PROVIDERS)
|
|
48
|
+
if (isSet(env[variable]))
|
|
49
|
+
return word;
|
|
50
|
+
return isSet(env.CI) ? 'other' : 'none';
|
|
51
|
+
};
|
|
52
|
+
/** The harness stores a plugin or extension lands in, matched on the folder names the harness uses under its home. */
|
|
53
|
+
const HARNESS_STORES = [
|
|
54
|
+
['/.claude/plugins/', 'claude-plugin'],
|
|
55
|
+
['/.codex/plugins/', 'codex-plugin'],
|
|
56
|
+
['/.gemini/extensions/', 'gemini-extension'],
|
|
57
|
+
];
|
|
58
|
+
/** Where a package manager keeps a global install: npm and Node's own prefix (`lib/node_modules`, Windows `npm/node_modules`), pnpm, yarn, and bun. */
|
|
59
|
+
const GLOBAL_FOLDERS = ['/lib/node_modules/', '/npm/node_modules/', '/pnpm/global/', '/yarn/global/node_modules/', '/.bun/install/global/'];
|
|
60
|
+
/** npm's own folder for a package run once (`npx`), which no other tool uses. */
|
|
61
|
+
const NPX_FOLDER = '/_npx/';
|
|
62
|
+
/** The folders pnpm (`dlx`) and bun (`bunx-<id>-<package>`) run a package from: names common enough that only a package manager's own agent makes them count. */
|
|
63
|
+
const RUNNER_FOLDERS = /\/(dlx|bunx-[^/]*)\//;
|
|
64
|
+
const PACKAGE_MANAGER_AGENT = /^(npm|pnpm|yarn|bun)\//;
|
|
65
|
+
/**
|
|
66
|
+
* How this copy arrived, as a closed word, from the folder oars runs in and `npm_config_user_agent` and from nothing else. A harness store is read first (a plugin can carry its own
|
|
67
|
+
* `node_modules`), then a package runner, then a global install; a folder that fits none reads `unknown`, never a guess. The agent only confirms a runner: a package manager that started the
|
|
68
|
+
* process says its name there, and a folder called `dlx` with no such agent is not a runner.
|
|
69
|
+
*/
|
|
70
|
+
export const surfaceWord = (installPath, userAgent) => {
|
|
71
|
+
const path = `/${installPath.split(String.fromCharCode(92)).join('/').toLowerCase()}`;
|
|
72
|
+
const store = HARNESS_STORES.find(([folder]) => path.includes(folder));
|
|
73
|
+
if (store !== undefined)
|
|
74
|
+
return store[1];
|
|
75
|
+
if (path.includes(NPX_FOLDER))
|
|
76
|
+
return 'npx';
|
|
77
|
+
if (RUNNER_FOLDERS.test(path) && userAgent !== undefined && PACKAGE_MANAGER_AGENT.test(userAgent))
|
|
78
|
+
return 'npx';
|
|
79
|
+
return GLOBAL_FOLDERS.some((folder) => path.includes(folder)) ? 'npm-global' : 'unknown';
|
|
80
|
+
};
|
|
81
|
+
/** The folder this module runs from: inside the package wherever it was installed (a symlinked bin resolves to its real folder). */
|
|
82
|
+
export const installFolder = () => fileURLToPath(import.meta.url);
|
|
@@ -15,6 +15,19 @@ export declare const WINDOWS: readonly number[];
|
|
|
15
15
|
export declare const HOOK_NAMES: readonly string[];
|
|
16
16
|
export declare const OTHER = "other";
|
|
17
17
|
export declare const UNRECORDED = "unrecorded";
|
|
18
|
+
/**
|
|
19
|
+
* The cell of a `first-run` event: one yes (the first run was in CI) or no under the oars version, in a metric named for the listing surface (`first-run.npx`), so the pool's rollup holds each install's
|
|
20
|
+
* first-run day, its surface, and whether it ran in CI. `measuresOf` leaves it out on purpose: a rate over one event is no measure of the program, so the user's own report has no use for it, and the pool's fleet read skips it for the same reason. A word off
|
|
21
|
+
* the closed lists reads as `unknown` or not-CI, and the install is still counted.
|
|
22
|
+
*/
|
|
23
|
+
export declare const firstRunMeasures: (event: unknown) => Measure[];
|
|
24
|
+
/**
|
|
25
|
+
* The cell of a `covenant-shown` event: one yes under the oars version, in a metric named for the covenant version the CLI told the operator to read (`covenant-shown.2026-11-10`), so the pool's rollup counts shows by version and
|
|
26
|
+
* day. Raw batches are swept at 90 days and a cell added later cannot count older shows, so the cell is kept from the first event. `measuresOf` leaves it out and the fleet read skips it (`isRecordOnlyMetric`): a count of shows is
|
|
27
|
+
* for the operator's query, not a measure of the program. A version that is not a word reads `other`, and a missing one `unrecorded`; the show is still counted.
|
|
28
|
+
*/
|
|
29
|
+
export declare const covenantShownMeasures: (event: unknown) => Measure[];
|
|
30
|
+
export declare const isRecordOnlyMetric: (metric: string) => boolean;
|
|
18
31
|
/** What one event measures; an event of a type nothing measures, or that is not an object, measures nothing. */
|
|
19
32
|
export declare const measuresOf: (event: unknown) => Measure[];
|
|
20
33
|
/** Every cell is named by these three together. */
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// A measure is one observation in a cell: a latency in milliseconds, or a yes/no that rates are made from. The cell is (metric, dimension, value): `hook.pre-tool.latency` at harness
|
|
3
3
|
// `claude-code`, version `2.1.5`. The reader is tolerant on purpose: the pool reads batches kept whole from any client, so a value that does not fit is skipped, never trusted.
|
|
4
4
|
import { isHarness } from '../sessions/pool-wire.js';
|
|
5
|
+
import { CI_WORDS, SURFACE_WORDS } from './words.js';
|
|
5
6
|
/** The windows the report shows and the pool serves, in days. */
|
|
6
7
|
export const WINDOWS = [7, 30];
|
|
7
8
|
const DAY_MS = 86_400_000;
|
|
@@ -73,6 +74,35 @@ const storeMeasures = (event, data) => {
|
|
|
73
74
|
found.push(rate('store.verifyFailure', 'cli', version, failed > 0));
|
|
74
75
|
return found;
|
|
75
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* The cell of a `first-run` event: one yes (the first run was in CI) or no under the oars version, in a metric named for the listing surface (`first-run.npx`), so the pool's rollup holds each install's
|
|
79
|
+
* first-run day, its surface, and whether it ran in CI. `measuresOf` leaves it out on purpose: a rate over one event is no measure of the program, so the user's own report has no use for it, and the pool's fleet read skips it for the same reason. A word off
|
|
80
|
+
* the closed lists reads as `unknown` or not-CI, and the install is still counted.
|
|
81
|
+
*/
|
|
82
|
+
export const firstRunMeasures = (event) => {
|
|
83
|
+
if (fieldOf(event, 'type') !== 'first-run')
|
|
84
|
+
return [];
|
|
85
|
+
const data = fieldOf(event, 'data');
|
|
86
|
+
const surface = fieldOf(data, 'surface');
|
|
87
|
+
const ci = fieldOf(data, 'ci');
|
|
88
|
+
const known = typeof surface === 'string' && SURFACE_WORDS.some((word) => word === surface) ? surface : 'unknown';
|
|
89
|
+
const inCi = typeof ci === 'string' && ci !== 'none' && CI_WORDS.some((word) => word === ci);
|
|
90
|
+
return [rate(`first-run.${known}`, 'cli', valueOf(fieldOf(event, 'cli')), inCi)];
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The cell of a `covenant-shown` event: one yes under the oars version, in a metric named for the covenant version the CLI told the operator to read (`covenant-shown.2026-11-10`), so the pool's rollup counts shows by version and
|
|
94
|
+
* day. Raw batches are swept at 90 days and a cell added later cannot count older shows, so the cell is kept from the first event. `measuresOf` leaves it out and the fleet read skips it (`isRecordOnlyMetric`): a count of shows is
|
|
95
|
+
* for the operator's query, not a measure of the program. A version that is not a word reads `other`, and a missing one `unrecorded`; the show is still counted.
|
|
96
|
+
*/
|
|
97
|
+
export const covenantShownMeasures = (event) => {
|
|
98
|
+
if (fieldOf(event, 'type') !== 'covenant-shown')
|
|
99
|
+
return [];
|
|
100
|
+
const version = valueOf(fieldOf(fieldOf(event, 'data'), 'version'));
|
|
101
|
+
return [rate(`covenant-shown.${version}`, 'cli', valueOf(fieldOf(event, 'cli')), true)];
|
|
102
|
+
};
|
|
103
|
+
/** The metrics the rollup records for the operator's queries and the fleet read never serves: a cell of one event or one count per install measures nothing across installs. */
|
|
104
|
+
const RECORD_ONLY_PREFIXES = ['first-run.', 'covenant-shown.'];
|
|
105
|
+
export const isRecordOnlyMetric = (metric) => RECORD_ONLY_PREFIXES.some((prefix) => metric.startsWith(prefix));
|
|
76
106
|
/** What one event measures; an event of a type nothing measures, or that is not an object, measures nothing. */
|
|
77
107
|
export const measuresOf = (event) => {
|
|
78
108
|
const type = fieldOf(event, 'type');
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { RunDeps } from '../cli.js';
|
|
3
3
|
import type { ExitCode } from '../errors.js';
|
|
4
|
+
import type { CardLineArm } from './schema.js';
|
|
5
|
+
import { type TelemetryRuntime } from './record.js';
|
|
4
6
|
export interface Observed {
|
|
5
7
|
/** The command path, such as `['pool', 'fetch']`; empty when no command ran (`--help`, `--version`, a usage error). */
|
|
6
8
|
path: string[];
|
|
7
9
|
/** The long names (as commander keys them) of the options given on the command line. */
|
|
8
10
|
flags: string[];
|
|
11
|
+
/** What the weekly card line did on this command (`sessions/card-line.ts`); absent when it was never asked. */
|
|
12
|
+
cardLine?: CardLineArm;
|
|
9
13
|
}
|
|
10
14
|
/** Called by commander just before an action runs. */
|
|
11
15
|
export declare const observeCommand: (observed: Observed, action: Command) => void;
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
export declare const isHook: (argv: string[], observed: Observed) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The `first-run` event, from the one place that shows the notice. `installPath` is where this copy runs from; only a test names it, and it is read here because it is needed on a first run only.
|
|
19
|
+
* Never throws: `oars telemetry on`, which shows the notice itself, calls it outside `finishTelemetry`'s guard.
|
|
20
|
+
*/
|
|
21
|
+
export declare const recordFirstRun: (deps: RunDeps, runtime: TelemetryRuntime, installPath?: string) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Prints the notice on the first run that is not a hook, then records the event. Never throws and never changes what the command did. `installPath` is where this copy runs from; only a test names it,
|
|
24
|
+
* and it is read inside the guard because it is needed on a first run only.
|
|
25
|
+
*/
|
|
26
|
+
export declare const finishTelemetry: (deps: RunDeps, argv: string[], observed: Observed, startedAt: number, exit: ExitCode, installPath?: string) => void;
|