@manyoars/oars 0.4.1 → 0.5.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 +79 -29
- package/dist/bin.js +3 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +206 -20
- 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 +212 -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 +577 -133
- 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 +172 -12
- package/dist/sessions/pool-wire.js +284 -30
- 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.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 +2 -2
- package/dist/sessions/redact.js +116 -14
- 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 +23 -4
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// What a session could have loaded, recorded when it starts. A transcript holds the files the harness loaded, never the files that existed, and a working folder is often gone by
|
|
2
|
+
// the time anyone asks, so a later read cannot tell a file the harness missed from a file that was never there. The session-start hook writes this snapshot to `candidates.jsonl`.
|
|
3
|
+
//
|
|
4
|
+
// Kinds and counts only: no path, file name, skill name, or file content enters a record. The two levels are how many folders up the repo root and the main checkout sit, which is
|
|
5
|
+
// what lets a loaded file's own level be placed in a position later. Three more instruction-file kinds are counted in the working folder alone, the only place they were measured. The log is append-only and apart from `observations.jsonl`, because a snapshot cannot be captured again from
|
|
6
|
+
// a transcript the way a capture record can, and it never reaches a pool batch.
|
|
7
|
+
import { mkdir, readdir, readFile, stat } from 'node:fs/promises';
|
|
8
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
9
|
+
import { HOOK_HARNESS } from '../telemetry/fields.js';
|
|
10
|
+
import { appendOwnerOnly } from './owner-only.js';
|
|
11
|
+
export const CANDIDATES_FILE = 'candidates.jsonl';
|
|
12
|
+
export const CANDIDATE_RECORD_VERSION = 2;
|
|
13
|
+
/** Where an instruction file sits relative to the session. `mainCheckout` is the span between a linked worktree and the main checkout that holds it, a span the harnesses treat apart. */
|
|
14
|
+
export const INSTRUCTION_POSITIONS = ['working', 'parent', 'above', 'mainCheckout', 'user'];
|
|
15
|
+
/** Claude Code's hook events, the 33 in the installed harness's own list (2.1.319). A name not on this list is counted as `other`, so a settings file cannot put free text in a record. */
|
|
16
|
+
export const HOOK_EVENT_NAMES = [
|
|
17
|
+
'PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'PermissionRequest', 'PermissionDenied', 'Notification', 'UserPromptSubmit', 'SessionStart', 'SessionEnd', 'Stop', 'StopFailure',
|
|
18
|
+
'SubagentStart', 'SubagentStop', 'TaskCreated', 'TaskCompleted', 'TeammateIdle', 'InstructionsLoaded', 'ConfigChange', 'CwdChanged', 'FileChanged', 'WorktreeCreate', 'WorktreeRemove',
|
|
19
|
+
'PreCompact', 'PostCompact', 'Elicitation', 'ElicitationResult', 'Setup',
|
|
20
|
+
'PostToolBatch', 'UserPromptExpansion', 'PreModelSwitch', 'PostModelSwitch', 'DirectoryAdded', 'MessageDisplay',
|
|
21
|
+
];
|
|
22
|
+
/** Instruction files beside `CLAUDE.md`, counted in the working folder only: `CLAUDE.local.md`, `.claude/CLAUDE.md`, and the `.claude/rules/` files that name no `paths` condition. */
|
|
23
|
+
export const WORKING_KINDS = ['local', 'dotClaude', 'rules'];
|
|
24
|
+
/** Where a settings file sits: the user's, the working folder's own `.claude/`, or a folder above it up to the repo root (and the main checkout root of a linked worktree). */
|
|
25
|
+
export const HOOK_SOURCES = ['user', 'working', 'ancestors'];
|
|
26
|
+
const INSTRUCTION_FILE = 'CLAUDE.md';
|
|
27
|
+
const LOCAL_FILE = 'CLAUDE.local.md';
|
|
28
|
+
const SKILL_FILE = 'SKILL.md';
|
|
29
|
+
const EVENT_NAMES = new Set(HOOK_EVENT_NAMES);
|
|
30
|
+
const isEventName = (name) => EVENT_NAMES.has(name);
|
|
31
|
+
const isObject = (value) => typeof value === 'object' && value !== null;
|
|
32
|
+
const isMissing = (error) => {
|
|
33
|
+
const code = isObject(error) ? Reflect.get(error, 'code') : undefined;
|
|
34
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
35
|
+
};
|
|
36
|
+
/** A path's stat, undefined when nothing is there. Any other failure is thrown: a snapshot that could not look is not one that found nothing. */
|
|
37
|
+
const statOf = async (path) => {
|
|
38
|
+
try {
|
|
39
|
+
return await stat(path);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (isMissing(error))
|
|
43
|
+
return undefined;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const isFile = async (path) => (await statOf(path))?.isFile() === true;
|
|
48
|
+
const samePath = (a, b) => relative(a, b) === '';
|
|
49
|
+
/** The folder and each of its parents, nearest first. */
|
|
50
|
+
const chainOf = (dir) => {
|
|
51
|
+
const chain = [dir];
|
|
52
|
+
for (let up = dirname(dir); up !== chain[chain.length - 1]; up = dirname(up))
|
|
53
|
+
chain.push(up);
|
|
54
|
+
return chain;
|
|
55
|
+
};
|
|
56
|
+
/** The main checkout a linked worktree's `.git` pointer names; undefined for any other pointer (a submodule's). */
|
|
57
|
+
const mainCheckoutOf = (pointer, dir) => {
|
|
58
|
+
const target = /^gitdir:\s*(.+?)\s*$/m.exec(pointer)?.[1];
|
|
59
|
+
if (target === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
const gitDir = resolve(dir, target);
|
|
62
|
+
if (basename(dirname(gitDir)) !== 'worktrees')
|
|
63
|
+
return undefined;
|
|
64
|
+
const common = dirname(dirname(gitDir));
|
|
65
|
+
return basename(common) === '.git' ? dirname(common) : undefined;
|
|
66
|
+
};
|
|
67
|
+
const levelsOf = async (chain) => {
|
|
68
|
+
for (const [level, dir] of chain.entries()) {
|
|
69
|
+
const git = await statOf(join(dir, '.git'));
|
|
70
|
+
if (git === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
if (git.isDirectory())
|
|
73
|
+
return { repoLevels: level, mainLevels: null };
|
|
74
|
+
const main = mainCheckoutOf(await readFile(join(dir, '.git'), 'utf8'), dir);
|
|
75
|
+
const mainLevel = main === undefined ? -1 : chain.findIndex((folder) => samePath(folder, main));
|
|
76
|
+
return { repoLevels: level, mainLevels: mainLevel > level ? mainLevel : null };
|
|
77
|
+
}
|
|
78
|
+
return { repoLevels: null, mainLevels: null };
|
|
79
|
+
};
|
|
80
|
+
/** Which position a folder `level` steps above the working folder holds; the derivation places a loaded file's level with this same rule. */
|
|
81
|
+
export const positionOfLevel = (level, { repoLevels, mainLevels }) => {
|
|
82
|
+
if (level === 0)
|
|
83
|
+
return 'working';
|
|
84
|
+
if (repoLevels !== null && level <= repoLevels)
|
|
85
|
+
return 'parent';
|
|
86
|
+
if (mainLevels !== null && level <= mainLevels)
|
|
87
|
+
return 'mainCheckout';
|
|
88
|
+
return 'above';
|
|
89
|
+
};
|
|
90
|
+
const skillCount = async (skillsDir) => {
|
|
91
|
+
let names;
|
|
92
|
+
try {
|
|
93
|
+
names = await readdir(skillsDir);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (isMissing(error))
|
|
97
|
+
return 0;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
const found = await Promise.all(names.map((name) => isFile(join(skillsDir, name, SKILL_FILE))));
|
|
101
|
+
return found.filter(Boolean).length;
|
|
102
|
+
};
|
|
103
|
+
/** A rule file whose front matter carries a `paths` key loads only when a matching file is read; the measurement covered the ones without it. */
|
|
104
|
+
const declaresPaths = (text) => {
|
|
105
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1];
|
|
106
|
+
return front?.split(/\r?\n/).some((line) => /^paths\s*:/.test(line)) === true;
|
|
107
|
+
};
|
|
108
|
+
/** How many `.md` files sit directly in a rules folder and name no `paths` condition. */
|
|
109
|
+
const unconditionalRules = async (rulesDir) => {
|
|
110
|
+
let names;
|
|
111
|
+
try {
|
|
112
|
+
names = await readdir(rulesDir);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (isMissing(error))
|
|
116
|
+
return 0;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
const files = names.filter((name) => name.toLowerCase().endsWith('.md'));
|
|
120
|
+
const unconditional = await Promise.all(files.map(async (name) => ((await isFile(join(rulesDir, name))) ? !declaresPaths(await readFile(join(rulesDir, name), 'utf8')) : false)));
|
|
121
|
+
return unconditional.filter(Boolean).length;
|
|
122
|
+
};
|
|
123
|
+
const addHookCounts = async (file, counts) => {
|
|
124
|
+
let text;
|
|
125
|
+
try {
|
|
126
|
+
text = await readFile(file, 'utf8');
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (isMissing(error))
|
|
130
|
+
return;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
const parsed = JSON.parse(text);
|
|
134
|
+
const hooks = isObject(parsed) ? Reflect.get(parsed, 'hooks') : undefined;
|
|
135
|
+
if (!isObject(hooks))
|
|
136
|
+
return;
|
|
137
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
138
|
+
if (!Array.isArray(groups))
|
|
139
|
+
continue;
|
|
140
|
+
const commands = groups.reduce((sum, group) => {
|
|
141
|
+
const list = isObject(group) ? Reflect.get(group, 'hooks') : undefined;
|
|
142
|
+
return sum + (Array.isArray(list) ? list.length : 0);
|
|
143
|
+
}, 0);
|
|
144
|
+
if (commands === 0)
|
|
145
|
+
continue;
|
|
146
|
+
const name = isEventName(event) ? event : 'other';
|
|
147
|
+
counts[name] = (counts[name] ?? 0) + commands;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const addSettingsHooks = async (dir, counts) => {
|
|
151
|
+
await addHookCounts(join(dir, '.claude', 'settings.json'), counts);
|
|
152
|
+
await addHookCounts(join(dir, '.claude', 'settings.local.json'), counts);
|
|
153
|
+
};
|
|
154
|
+
/** Counts what exists around `cwd`, under the harness's config folder `claudeDir`. It throws on anything but a missing file. */
|
|
155
|
+
export const snapshotCandidates = async ({ cwd, claudeDir }) => {
|
|
156
|
+
const chain = chainOf(cwd);
|
|
157
|
+
const levels = await levelsOf(chain);
|
|
158
|
+
// A session inside the config folder climbs through it: the file there is the user-level one, counted once below.
|
|
159
|
+
const isConfigFolder = (dir) => samePath(dir, claudeDir);
|
|
160
|
+
const isConfigParent = (dir) => samePath(join(dir, '.claude'), claudeDir);
|
|
161
|
+
const instructions = { working: 0, parent: 0, above: 0, mainCheckout: 0, user: (await isFile(join(claudeDir, INSTRUCTION_FILE))) ? 1 : 0 };
|
|
162
|
+
for (const [level, dir] of chain.entries()) {
|
|
163
|
+
if (!isConfigFolder(dir) && (await isFile(join(dir, INSTRUCTION_FILE))))
|
|
164
|
+
instructions[positionOfLevel(level, levels)] += 1;
|
|
165
|
+
}
|
|
166
|
+
// The working folder holds the config folder when the session runs in the home folder: its `.claude/` files are the user's, already counted.
|
|
167
|
+
const workingKinds = { local: (await isFile(join(cwd, LOCAL_FILE))) ? 1 : 0, dotClaude: 0, rules: 0 };
|
|
168
|
+
if (!isConfigParent(cwd)) {
|
|
169
|
+
workingKinds.dotClaude = (await isFile(join(cwd, '.claude', INSTRUCTION_FILE))) ? 1 : 0;
|
|
170
|
+
workingKinds.rules = await unconditionalRules(join(cwd, '.claude', 'rules'));
|
|
171
|
+
}
|
|
172
|
+
// Skills load from every folder up to the repo root, and from the main checkout root when the session runs in a linked worktree.
|
|
173
|
+
const mainRoot = levels.mainLevels === null ? [] : [chain[levels.mainLevels] ?? cwd];
|
|
174
|
+
const projectFolders = [...chain.slice(0, (levels.repoLevels ?? 0) + 1), ...mainRoot].filter((dir) => !isConfigParent(dir));
|
|
175
|
+
const skills = { project: 0, user: await skillCount(join(claudeDir, 'skills')) };
|
|
176
|
+
for (const dir of projectFolders)
|
|
177
|
+
skills.project += await skillCount(join(dir, '.claude', 'skills'));
|
|
178
|
+
// Settings load from the user's folder and the working folder's own `.claude/`; a folder above it is skipped by design but still counted, so the skip can be told from absence.
|
|
179
|
+
const hooks = { user: {}, working: {}, ancestors: {} };
|
|
180
|
+
await addHookCounts(join(claudeDir, 'settings.json'), hooks.user);
|
|
181
|
+
const aboveFolders = chain.slice(1, (levels.repoLevels ?? 0) + 1);
|
|
182
|
+
for (const dir of [...aboveFolders, ...mainRoot]) {
|
|
183
|
+
if (!isConfigParent(dir))
|
|
184
|
+
await addSettingsHooks(dir, hooks.ancestors);
|
|
185
|
+
}
|
|
186
|
+
if (!isConfigParent(cwd))
|
|
187
|
+
await addSettingsHooks(cwd, hooks.working);
|
|
188
|
+
return { instructions, workingKinds, skills, hooks, ...levels };
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Appends this session's snapshot. Fail-open: any error writes nothing and returns, because a hook must never fail or block a session, and a partial count would read as evidence
|
|
192
|
+
* that files were absent. The counts are finished before the store is touched, so nothing partial can reach the file.
|
|
193
|
+
*/
|
|
194
|
+
export const recordCandidates = async (storeDir, { session, cwd, claudeDir, at }) => {
|
|
195
|
+
try {
|
|
196
|
+
const counts = await snapshotCandidates({ cwd, claudeDir });
|
|
197
|
+
const record = { v: CANDIDATE_RECORD_VERSION, session, at, harness: HOOK_HARNESS, ...counts };
|
|
198
|
+
await mkdir(storeDir, { recursive: true });
|
|
199
|
+
await appendOwnerOnly(join(storeDir, CANDIDATES_FILE), `${JSON.stringify(record)}\n`);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Fail-open, as above.
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/** A line of this version with its four count groups: one without them (hand-edited, or written badly) would throw in the join, outside its guard. */
|
|
206
|
+
const isSnapshot = (value) => isObject(value) &&
|
|
207
|
+
Reflect.get(value, 'v') === CANDIDATE_RECORD_VERSION &&
|
|
208
|
+
typeof Reflect.get(value, 'session') === 'string' &&
|
|
209
|
+
['instructions', 'workingKinds', 'skills', 'hooks'].every((group) => isObject(Reflect.get(value, group)));
|
|
210
|
+
const parsedLine = (line) => {
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(line);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
/** The first snapshot of each session (a resumed session starts again and appends another), by session id. A line of another version or a torn line is skipped. */
|
|
219
|
+
export const readCandidates = async (storeDir) => {
|
|
220
|
+
const snapshots = new Map();
|
|
221
|
+
let text;
|
|
222
|
+
try {
|
|
223
|
+
text = await readFile(join(storeDir, CANDIDATES_FILE), 'utf8');
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (isMissing(error))
|
|
227
|
+
return snapshots;
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
for (const line of text.split('\n')) {
|
|
231
|
+
const parsed = parsedLine(line);
|
|
232
|
+
if (isSnapshot(parsed) && !snapshots.has(parsed.session))
|
|
233
|
+
snapshots.set(parsed.session, parsed);
|
|
234
|
+
}
|
|
235
|
+
return snapshots;
|
|
236
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Outcome } from '../cli.js';
|
|
2
|
+
import { type CardPeriod } from './card.js';
|
|
3
|
+
import type { SessionsDeps } from './command.js';
|
|
4
|
+
export declare const ASHORE = "ashore: run oars pool login";
|
|
5
|
+
/**
|
|
6
|
+
* The depositor row. Signed out it points at `oars pool login`; signed in it counts the weeks (Monday-start, UTC) in which this machine's sent record holds a deposit,
|
|
7
|
+
* labeled as this machine's because the record is local. It reads two local files and makes no network call; a sign-in file or pool address that cannot be read is
|
|
8
|
+
* signed out, since a share card must not fail on it.
|
|
9
|
+
*/
|
|
10
|
+
export declare const depositorLine: (storeDir: string, env: NodeJS.ProcessEnv, now: number) => Promise<string>;
|
|
11
|
+
export interface CardOptions {
|
|
12
|
+
period: CardPeriod;
|
|
13
|
+
text: boolean;
|
|
14
|
+
/** A `.png` or `.svg` path: both pictures are written, with this name's extension swapped for each. */
|
|
15
|
+
out?: string | undefined;
|
|
16
|
+
/** Leave the cost row out of the picture, the table, and the JSON. */
|
|
17
|
+
noCost?: boolean | undefined;
|
|
18
|
+
}
|
|
19
|
+
/** The two paths a card is saved to: `--out` names the base, the default is one per period in the working directory. */
|
|
20
|
+
export declare const cardPaths: (period: CardPeriod, out: string | undefined) => {
|
|
21
|
+
svg: string;
|
|
22
|
+
png: string;
|
|
23
|
+
};
|
|
24
|
+
export declare const sessionsCard: (deps: SessionsDeps, options: CardOptions, env: NodeJS.ProcessEnv) => Promise<Outcome>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// `oars card`: reads the local store, prints the card as a table (`--text`) or saves it as an SVG and a PNG, and prints one prefilled x.com link. Nothing is
|
|
2
|
+
// uploaded and no network call is made.
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { CliError, ExitCode } from '../errors.js';
|
|
6
|
+
import { cardOf, cardText, tweetLink } from './card.js';
|
|
7
|
+
import { cardPng, cardSvg } from './card-render.js';
|
|
8
|
+
import { emptyBooks } from './empty-state.js';
|
|
9
|
+
import { poolServer } from './pool-command.js';
|
|
10
|
+
import { outboxFolder } from './pool-outbox.js';
|
|
11
|
+
import { loadSignIns } from './pool-replica.js';
|
|
12
|
+
import { sentLog } from './pool-sent.js';
|
|
13
|
+
import { loadStore } from './store.js';
|
|
14
|
+
import { readSurvival } from './survival.js';
|
|
15
|
+
export const ASHORE = 'ashore: run oars pool login';
|
|
16
|
+
const DAY = 86_400_000;
|
|
17
|
+
const WEEK = 7 * DAY;
|
|
18
|
+
/** 1970-01-01 was a Thursday, so this is the Monday before it: weeks counted from here start on a Monday, UTC. */
|
|
19
|
+
const FIRST_MONDAY = -3 * DAY;
|
|
20
|
+
/**
|
|
21
|
+
* The depositor row. Signed out it points at `oars pool login`; signed in it counts the weeks (Monday-start, UTC) in which this machine's sent record holds a deposit,
|
|
22
|
+
* labeled as this machine's because the record is local. It reads two local files and makes no network call; a sign-in file or pool address that cannot be read is
|
|
23
|
+
* signed out, since a share card must not fail on it.
|
|
24
|
+
*/
|
|
25
|
+
export const depositorLine = async (storeDir, env, now) => {
|
|
26
|
+
try {
|
|
27
|
+
const server = poolServer(env, 'card');
|
|
28
|
+
const signedIn = (await loadSignIns(storeDir, 'card')).some((entry) => entry.server === server && entry.expiresAt > now);
|
|
29
|
+
if (!signedIn)
|
|
30
|
+
return ASHORE;
|
|
31
|
+
const weeks = new Set();
|
|
32
|
+
for (const at of (await sentLog(outboxFolder(storeDir, server))).values()) {
|
|
33
|
+
const time = Date.parse(at);
|
|
34
|
+
if (!Number.isNaN(time))
|
|
35
|
+
weeks.add(Math.floor((time - FIRST_MONDAY) / WEEK));
|
|
36
|
+
}
|
|
37
|
+
return `weeks rowing (this machine): ${String(weeks.size)}`;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof CliError)
|
|
41
|
+
return ASHORE;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
/** The two paths a card is saved to: `--out` names the base, the default is one per period in the working directory. */
|
|
46
|
+
export const cardPaths = (period, out) => {
|
|
47
|
+
if (out !== undefined && !/\.(?:png|svg)$/i.test(out))
|
|
48
|
+
throw new CliError(`--out ${out} must end in .png or .svg.`, ExitCode.USAGE, 'usage');
|
|
49
|
+
const base = resolve(out === undefined ? `oars-card-${period}` : out.replace(/\.(?:png|svg)$/i, ''));
|
|
50
|
+
return { svg: `${base}.svg`, png: `${base}.png` };
|
|
51
|
+
};
|
|
52
|
+
export const sessionsCard = async (deps, options, env) => {
|
|
53
|
+
if (deps.sessions === undefined)
|
|
54
|
+
throw new CliError('The session store is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
55
|
+
const paths = cardPaths(options.period, options.out);
|
|
56
|
+
const store = await loadStore(deps.sessions.storeDir);
|
|
57
|
+
const card = cardOf(store, await readSurvival(deps.sessions.storeDir), options.period, deps.now(), options.noCost !== true);
|
|
58
|
+
const link = tweetLink(card);
|
|
59
|
+
if (card.sessions === 0) {
|
|
60
|
+
const empty = store.sessions.length === 0 && store.prompts.length === 0 ? emptyBooks('prompts') : `No sessions in the last ${String(card.days)} days in the local store.`;
|
|
61
|
+
return { document: { ok: true, card, saved: null, tweet: null }, text: empty };
|
|
62
|
+
}
|
|
63
|
+
const depositor = await depositorLine(deps.sessions.storeDir, env, deps.now());
|
|
64
|
+
if (options.text)
|
|
65
|
+
return { document: { ok: true, card, saved: null, tweet: link }, text: [cardText(card, depositor), '', link].join('\n') };
|
|
66
|
+
const svg = cardSvg(card, depositor);
|
|
67
|
+
await mkdir(dirname(paths.svg), { recursive: true });
|
|
68
|
+
await writeFile(paths.svg, svg);
|
|
69
|
+
await writeFile(paths.png, cardPng(svg));
|
|
70
|
+
return { document: { ok: true, card, saved: paths, tweet: link }, text: [`Saved ${paths.png}`, `Saved ${paths.svg}`, 'Nothing was uploaded: the card was made from your local store.', '', `Post it (add the picture yourself): ${link}`].join('\n') };
|
|
71
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The glyphs from ASCII 32 on, each as five column bytes. */
|
|
2
|
+
export declare const GLYPHS: readonly (readonly number[])[];
|
|
3
|
+
export declare const GLYPH_WIDTH = 5;
|
|
4
|
+
export declare const GLYPH_HEIGHT = 8;
|
|
5
|
+
/** Whether the pixel at (column, row) of a character is lit; any character outside ASCII 32 to 126 draws as `?`. */
|
|
6
|
+
export declare const lit: (char: string, column: number, row: number) => boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// A 5x7 bitmap font for the card's PNG, so the picture needs no renderer dependency, no font file, and no system font. Each glyph is five column bytes
|
|
2
|
+
// (ten hex digits), bit 0 the top row; bit 7 is the one descender row. ASCII 32 to 126 only: a model or harness name the card prints is always inside that range.
|
|
3
|
+
const COLUMNS = [
|
|
4
|
+
// space ! " # $ % & ' ( ) * + , - . /
|
|
5
|
+
'0000000000', '00005f0000', '0007000700', '147f147f14', '242a7f2a12', '2313086462', '3649562050', '0008070300',
|
|
6
|
+
'001c224100', '0041221c00', '2a1c7f1c2a', '08083e0808', '0080703000', '0808080808', '0000606000', '2010080402',
|
|
7
|
+
// 0 to 9
|
|
8
|
+
'3e5149453e', '00427f4000', '7249494946', '2141494d33', '1814127f10', '2745454539', '3c4a494931', '4121110907',
|
|
9
|
+
'3649494936', '464949291e',
|
|
10
|
+
// : ; < = > ? @
|
|
11
|
+
'0000140000', '0040340000', '0008142241', '1414141414', '0041221408', '0201590906', '3e415d594e',
|
|
12
|
+
// A to Z
|
|
13
|
+
'7c1211127c', '7f49494936', '3e41414122', '7f4141413e', '7f49494941', '7f09090901', '3e41415173', '7f0808087f',
|
|
14
|
+
'00417f4100', '2040413f01', '7f08142241', '7f40404040', '7f021c027f', '7f0408107f', '3e4141413e', '7f09090906',
|
|
15
|
+
'3e4151215e', '7f09192946', '2649494932', '03017f0103', '3f4040403f', '1f2040201f', '3f4038403f', '6314081463',
|
|
16
|
+
'0304780403', '6159494d43',
|
|
17
|
+
// [ \ ] ^ _ `
|
|
18
|
+
'007f414141', '0204081020', '004141417f', '0402010204', '4040404040', '0003070800',
|
|
19
|
+
// a to z
|
|
20
|
+
'2054547840', '7f28444438', '3844444428', '384444287f', '3854545418', '00087e0902', '18a4a49c78', '7f08040478',
|
|
21
|
+
'00447d4000', '2040403d00', '7f10284400', '00417f4000', '7c04780478', '7c08040478', '3844444438', 'fc18242418',
|
|
22
|
+
'18242418fc', '7c08040408', '4854545424', '04043f4424', '3c40402078', '1c2040201c', '3c4030403c', '4428102844',
|
|
23
|
+
'4c9090907c', '4464544c44',
|
|
24
|
+
// { | } ~
|
|
25
|
+
'0008364100', '0000770000', '0041360800', '0201020402',
|
|
26
|
+
];
|
|
27
|
+
/** The glyphs from ASCII 32 on, each as five column bytes. */
|
|
28
|
+
export const GLYPHS = COLUMNS.map((hex) => Array.from({ length: 5 }, (_, column) => Number.parseInt(hex.slice(column * 2, column * 2 + 2), 16)));
|
|
29
|
+
export const GLYPH_WIDTH = 5;
|
|
30
|
+
export const GLYPH_HEIGHT = 8;
|
|
31
|
+
/** Whether the pixel at (column, row) of a character is lit; any character outside ASCII 32 to 126 draws as `?`. */
|
|
32
|
+
export const lit = (char, column, row) => {
|
|
33
|
+
const code = char.charCodeAt(0);
|
|
34
|
+
const glyph = GLYPHS[code >= 32 && code <= 126 ? code - 32 : 31] ?? [];
|
|
35
|
+
return (((glyph[column] ?? 0) >> row) & 1) === 1;
|
|
36
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CardLineArm } from '../telemetry/schema.js';
|
|
2
|
+
/** Out of 100 installs, this many never see the line. */
|
|
3
|
+
export declare const HELD_PERCENT = 10;
|
|
4
|
+
/** The ISO week (`2026-W45`) of a local date. The week's Thursday names its year. */
|
|
5
|
+
export declare const isoWeekOf: (now: number) => string;
|
|
6
|
+
/** Friday, Saturday, or Sunday local time: the ISO week's last three days, so a first command on or after Friday 00:00 is in the week it counts. */
|
|
7
|
+
export declare const fridayOrLater: (now: number) => boolean;
|
|
8
|
+
/** The week's text. Singular at one; nothing else (no install, star, or link words). */
|
|
9
|
+
export declare const cardLineText: (sessions: number, strokes: number) => string;
|
|
10
|
+
/** Stable per install: the first four bytes of a SHA-256 of the id, taken mod 100, under the holdback percent. */
|
|
11
|
+
export declare const isHeld: (id: string) => boolean;
|
|
12
|
+
export interface CardLineInput {
|
|
13
|
+
storeDir: string;
|
|
14
|
+
telemetryDir?: string | undefined;
|
|
15
|
+
now: number;
|
|
16
|
+
/** Whether stderr is a terminal: the line is for a person reading it. */
|
|
17
|
+
tty: boolean;
|
|
18
|
+
argv: readonly string[];
|
|
19
|
+
/** The command path and flag names the run observed. */
|
|
20
|
+
path: readonly string[];
|
|
21
|
+
flags: readonly string[];
|
|
22
|
+
stderr: (text: string) => void;
|
|
23
|
+
}
|
|
24
|
+
/** A hook, the MCP server, a recall, `sessions try`, `--json`, or `oars card` itself: the line never goes there. */
|
|
25
|
+
export declare const quiet: (input: Pick<CardLineInput, "argv" | "path" | "flags">) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Decides and, when due, prints. A store that cannot be read writes the week's marker as `not_due`, so a failing store costs one command a week; a week with no
|
|
28
|
+
* session leaves the marker unwritten, so it is checked again on each due command and the line can still come later in the week. Returns the arm for the command event; any error reads `not_due`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const cardLine: (input: CardLineInput) => Promise<CardLineArm>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The weekly line: once per ISO week, on the first command a person runs on or after Friday 00:00 local time, stderr gets one line pointing at `oars card --text`.
|
|
2
|
+
// A stable tenth of installs never see it (the holdback), so the loop's effect can be read later. It is for people at a terminal only: never `--json`, a hook, the MCP
|
|
3
|
+
// server, a recall, `oars sessions try`, or `oars card` itself. It makes no network call and never fails or delays a command: every error is swallowed.
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { isHook } from '../telemetry/observe.js';
|
|
8
|
+
import { cardOf } from './card.js';
|
|
9
|
+
import { mkdirOwnerOnly, writeOwnerOnly } from './owner-only.js';
|
|
10
|
+
import { loadStore } from './store.js';
|
|
11
|
+
import { readSurvival } from './survival.js';
|
|
12
|
+
/** Out of 100 installs, this many never see the line. */
|
|
13
|
+
export const HELD_PERCENT = 10;
|
|
14
|
+
const MARKER = 'card-line.json';
|
|
15
|
+
const ID_FILE = 'card-line-id';
|
|
16
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
17
|
+
const FRIDAY = 5;
|
|
18
|
+
/** The ISO week (`2026-W45`) of a local date. The week's Thursday names its year. */
|
|
19
|
+
export const isoWeekOf = (now) => {
|
|
20
|
+
const date = new Date(now);
|
|
21
|
+
const thursday = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 4 - (date.getDay() || 7), 12);
|
|
22
|
+
const first = new Date(thursday.getFullYear(), 0, 1, 12);
|
|
23
|
+
const week = Math.ceil((Math.round((thursday.getTime() - first.getTime()) / 86_400_000) + 1) / 7);
|
|
24
|
+
return `${String(thursday.getFullYear())}-W${String(week).padStart(2, '0')}`;
|
|
25
|
+
};
|
|
26
|
+
/** Friday, Saturday, or Sunday local time: the ISO week's last three days, so a first command on or after Friday 00:00 is in the week it counts. */
|
|
27
|
+
export const fridayOrLater = (now) => {
|
|
28
|
+
const day = new Date(now).getDay();
|
|
29
|
+
return day === 0 || day >= FRIDAY;
|
|
30
|
+
};
|
|
31
|
+
/** The week's text. Singular at one; nothing else (no install, star, or link words). */
|
|
32
|
+
export const cardLineText = (sessions, strokes) => `Your week: ${String(sessions)} ${sessions === 1 ? 'session' : 'sessions'}, ${String(strokes)} ${strokes === 1 ? 'stroke' : 'strokes'}. \`oars card --text\` for the table.\n`;
|
|
33
|
+
const readTrimmed = async (file) => {
|
|
34
|
+
try {
|
|
35
|
+
return (await readFile(file, 'utf8')).trim();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* This install's id for the holdback, frozen at the first decision so the arm never flips: the id file in the store folder when it exists; otherwise it is seeded from
|
|
43
|
+
* the telemetry install id when that file exists (never created here, so a person who turned telemetry off gets no new file), else a random value, and written.
|
|
44
|
+
*/
|
|
45
|
+
const holdbackId = async (storeDir, telemetryDir) => {
|
|
46
|
+
const own = await readTrimmed(join(storeDir, ID_FILE));
|
|
47
|
+
if (own !== undefined && UUID.test(own))
|
|
48
|
+
return own;
|
|
49
|
+
const shared = telemetryDir === undefined ? undefined : await readTrimmed(join(telemetryDir, 'install-id'));
|
|
50
|
+
const id = shared !== undefined && UUID.test(shared) ? shared : randomUUID();
|
|
51
|
+
await mkdirOwnerOnly(storeDir);
|
|
52
|
+
await writeOwnerOnly(join(storeDir, ID_FILE), `${id}\n`);
|
|
53
|
+
return id;
|
|
54
|
+
};
|
|
55
|
+
/** Stable per install: the first four bytes of a SHA-256 of the id, taken mod 100, under the holdback percent. */
|
|
56
|
+
export const isHeld = (id) => createHash('sha256').update(`card-line:${id}`).digest().readUInt32BE(0) % 100 < HELD_PERCENT;
|
|
57
|
+
/** A hook, the MCP server, a recall, `sessions try`, `--json`, or `oars card` itself: the line never goes there. */
|
|
58
|
+
export const quiet = (input) => {
|
|
59
|
+
const { argv, path, flags } = input;
|
|
60
|
+
return path.length === 0 || path[0] === 'card' || path.includes('hook') || (path[0] === 'sessions' && (path[1] === 'recall' || path[1] === 'try')) || isHook([...argv], { path: [...path], flags: [...flags] }) || flags.includes('json') || flags.includes('mcp') || argv.includes('--json');
|
|
61
|
+
};
|
|
62
|
+
const markedWeek = async (storeDir) => {
|
|
63
|
+
const text = await readTrimmed(join(storeDir, MARKER));
|
|
64
|
+
if (text === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const parsed = JSON.parse(text);
|
|
67
|
+
const week = typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'week') : undefined;
|
|
68
|
+
return typeof week === 'string' ? week : undefined;
|
|
69
|
+
};
|
|
70
|
+
const mark = async (storeDir, week, arm) => {
|
|
71
|
+
await mkdirOwnerOnly(storeDir);
|
|
72
|
+
await writeOwnerOnly(join(storeDir, MARKER), `${JSON.stringify({ week, arm })}\n`);
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Decides and, when due, prints. A store that cannot be read writes the week's marker as `not_due`, so a failing store costs one command a week; a week with no
|
|
76
|
+
* session leaves the marker unwritten, so it is checked again on each due command and the line can still come later in the week. Returns the arm for the command event; any error reads `not_due`.
|
|
77
|
+
*/
|
|
78
|
+
export const cardLine = async (input) => {
|
|
79
|
+
try {
|
|
80
|
+
if (!input.tty || quiet(input) || !fridayOrLater(input.now))
|
|
81
|
+
return 'not_due';
|
|
82
|
+
const week = isoWeekOf(input.now);
|
|
83
|
+
if ((await markedWeek(input.storeDir).catch(() => undefined)) === week)
|
|
84
|
+
return 'not_due';
|
|
85
|
+
let card;
|
|
86
|
+
try {
|
|
87
|
+
card = cardOf(await loadStore(input.storeDir), await readSurvival(input.storeDir), 'week', input.now);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
await mark(input.storeDir, week, 'not_due');
|
|
91
|
+
return 'not_due';
|
|
92
|
+
}
|
|
93
|
+
if (card.sessions === 0)
|
|
94
|
+
return 'not_due';
|
|
95
|
+
const held = isHeld(await holdbackId(input.storeDir, input.telemetryDir));
|
|
96
|
+
const arm = held ? 'held' : 'shown';
|
|
97
|
+
await mark(input.storeDir, week, arm);
|
|
98
|
+
if (!held)
|
|
99
|
+
input.stderr(cardLineText(card.sessions, card.strokes));
|
|
100
|
+
return arm;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return 'not_due';
|
|
104
|
+
}
|
|
105
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Card } from './card.js';
|
|
2
|
+
export declare const PITCH = 36;
|
|
3
|
+
export declare const cardSvg: (card: Card, depositor: string) => string;
|
|
4
|
+
export interface Raster {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
/** 8-bit RGB, row by row. */
|
|
8
|
+
rgb: Buffer;
|
|
9
|
+
}
|
|
10
|
+
/** Pixels for the SVG `cardSvg` writes: rects and monospace text in document order, a text's font size a whole multiple of ten. */
|
|
11
|
+
export declare const rasterize: (svg: string) => Raster;
|
|
12
|
+
/** An 8-bit RGB PNG, unfiltered rows, zlib at level 9 (deterministic for one input). */
|
|
13
|
+
export declare const encodePng: (width: number, height: number, rgb: Buffer) => Buffer;
|
|
14
|
+
/** The PNG for an SVG `cardSvg` wrote. */
|
|
15
|
+
export declare const cardPng: (svg: string) => Buffer;
|