@manyoars/oars 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -28
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +6 -3
- package/dist/cli.js +367 -88
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.js +2 -2
- package/dist/errors.d.ts +13 -13
- package/dist/errors.js +28 -11
- package/dist/gate/checks.d.ts +68 -0
- package/dist/gate/checks.js +584 -0
- package/dist/gate/command.d.ts +14 -0
- package/dist/gate/command.js +61 -0
- package/dist/gate/hook-fill-main.d.ts +1 -0
- package/dist/gate/hook-fill-main.js +16 -0
- package/dist/gate/hook-fill.d.ts +38 -0
- package/dist/gate/hook-fill.js +94 -0
- package/dist/gate/hook-main.d.ts +1 -0
- package/dist/gate/hook-main.js +35 -0
- package/dist/gate/hook-process.d.ts +14 -0
- package/dist/gate/hook-process.js +64 -0
- package/dist/gate/hook.d.ts +46 -0
- package/dist/gate/hook.js +256 -0
- package/dist/gate/install.d.ts +25 -0
- package/dist/gate/install.js +126 -0
- package/dist/gate/local-bin.d.ts +8 -0
- package/dist/gate/local-bin.js +36 -0
- package/dist/gate/notes.d.ts +19 -0
- package/dist/gate/notes.js +27 -0
- package/dist/gate/novelty.d.ts +35 -0
- package/dist/gate/novelty.js +99 -0
- package/dist/gate/policy.d.ts +25 -0
- package/dist/gate/policy.js +73 -0
- package/dist/gate/record.d.ts +120 -0
- package/dist/gate/record.js +125 -0
- package/dist/gate/registry-state.d.ts +10 -0
- package/dist/gate/registry-state.js +30 -0
- package/dist/gate/registry.d.ts +56 -0
- package/dist/gate/registry.js +94 -0
- package/dist/gate/replay.d.ts +20 -0
- package/dist/gate/replay.js +257 -0
- package/dist/gate/report.d.ts +50 -0
- package/dist/gate/report.js +89 -0
- package/dist/gate/scope.d.ts +7 -0
- package/dist/gate/scope.js +27 -0
- package/dist/gate/transcripts.d.ts +68 -0
- package/dist/gate/transcripts.js +232 -0
- package/dist/hook-bin.d.ts +1 -0
- package/dist/hook-bin.js +25 -0
- package/dist/issuer.d.ts +7 -1
- package/dist/issuer.js +18 -1
- package/dist/main.d.ts +1 -0
- package/dist/main.js +64 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +351 -0
- package/dist/sessions/allow.d.ts +14 -0
- package/dist/sessions/allow.js +51 -0
- package/dist/sessions/archive.d.ts +11 -0
- package/dist/sessions/archive.js +38 -0
- package/dist/sessions/audit.d.ts +42 -0
- package/dist/sessions/audit.js +104 -0
- package/dist/sessions/baseline.d.ts +62 -0
- package/dist/sessions/baseline.js +188 -0
- package/dist/sessions/capture-runtime.d.ts +10 -0
- package/dist/sessions/capture-runtime.js +12 -0
- package/dist/sessions/cell-increments.d.ts +43 -0
- package/dist/sessions/cell-increments.js +209 -0
- package/dist/sessions/cells.d.ts +79 -0
- package/dist/sessions/cells.js +181 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +679 -0
- package/dist/sessions/command.d.ts +117 -0
- package/dist/sessions/command.js +373 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/cursor.d.ts +24 -0
- package/dist/sessions/cursor.js +324 -0
- package/dist/sessions/derive.d.ts +57 -0
- package/dist/sessions/derive.js +320 -0
- package/dist/sessions/exclusion.d.ts +81 -0
- package/dist/sessions/exclusion.js +353 -0
- package/dist/sessions/extract.d.ts +179 -0
- package/dist/sessions/extract.js +1774 -0
- package/dist/sessions/fold.d.ts +17 -0
- package/dist/sessions/fold.js +71 -0
- package/dist/sessions/frame.d.ts +24 -0
- package/dist/sessions/frame.js +95 -0
- package/dist/sessions/gemini.d.ts +37 -0
- package/dist/sessions/gemini.js +613 -0
- package/dist/sessions/gold-arms.d.ts +67 -0
- package/dist/sessions/gold-arms.js +172 -0
- package/dist/sessions/gold.d.ts +197 -0
- package/dist/sessions/gold.js +463 -0
- package/dist/sessions/graph.d.ts +12 -0
- package/dist/sessions/graph.js +34 -0
- package/dist/sessions/grok.d.ts +36 -0
- package/dist/sessions/grok.js +421 -0
- package/dist/sessions/hook-calls.d.ts +3 -0
- package/dist/sessions/hook-calls.js +47 -0
- package/dist/sessions/hook-index.d.ts +53 -0
- package/dist/sessions/hook-index.js +157 -0
- package/dist/sessions/hooks.d.ts +13 -0
- package/dist/sessions/hooks.js +170 -0
- package/dist/sessions/identity.d.ts +22 -0
- package/dist/sessions/identity.js +101 -0
- package/dist/sessions/install.d.ts +54 -0
- package/dist/sessions/install.js +201 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +6 -0
- package/dist/sessions/mcp.js +153 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +43 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/package-resolve.d.ts +30 -0
- package/dist/sessions/package-resolve.js +183 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pool-command.d.ts +44 -0
- package/dist/sessions/pool-command.js +542 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-outbox.d.ts +63 -0
- package/dist/sessions/pool-outbox.js +260 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-wire.d.ts +225 -0
- package/dist/sessions/pool-wire.js +620 -0
- package/dist/sessions/pool.d.ts +32 -0
- package/dist/sessions/pool.js +164 -0
- package/dist/sessions/projection.d.ts +75 -0
- package/dist/sessions/projection.js +165 -0
- package/dist/sessions/public-vocabulary.d.ts +73 -0
- package/dist/sessions/public-vocabulary.js +302 -0
- package/dist/sessions/query.d.ts +111 -0
- package/dist/sessions/query.js +246 -0
- package/dist/sessions/read-adoption.d.ts +2 -0
- package/dist/sessions/read-adoption.js +27 -0
- package/dist/sessions/read-call-shape.d.ts +2 -0
- package/dist/sessions/read-call-shape.js +25 -0
- package/dist/sessions/read-change.d.ts +2 -0
- package/dist/sessions/read-change.js +30 -0
- package/dist/sessions/read-cost.d.ts +4 -0
- package/dist/sessions/read-cost.js +37 -0
- package/dist/sessions/read-day-move.d.ts +3 -0
- package/dist/sessions/read-day-move.js +65 -0
- package/dist/sessions/read-fit.d.ts +7 -0
- package/dist/sessions/read-fit.js +41 -0
- package/dist/sessions/read-interaction.d.ts +5 -0
- package/dist/sessions/read-interaction.js +62 -0
- package/dist/sessions/read-lead-lag.d.ts +15 -0
- package/dist/sessions/read-lead-lag.js +81 -0
- package/dist/sessions/read-live.d.ts +6 -0
- package/dist/sessions/read-live.js +38 -0
- package/dist/sessions/read-method.d.ts +2 -0
- package/dist/sessions/read-method.js +29 -0
- package/dist/sessions/read-next-step.d.ts +2 -0
- package/dist/sessions/read-next-step.js +35 -0
- package/dist/sessions/read-order.d.ts +54 -0
- package/dist/sessions/read-order.js +62 -0
- package/dist/sessions/read-readable.d.ts +8 -0
- package/dist/sessions/read-readable.js +24 -0
- package/dist/sessions/read-recovery.d.ts +20 -0
- package/dist/sessions/read-recovery.js +97 -0
- package/dist/sessions/read-rows.d.ts +13 -0
- package/dist/sessions/read-rows.js +47 -0
- package/dist/sessions/read-substitution.d.ts +5 -0
- package/dist/sessions/read-substitution.js +51 -0
- package/dist/sessions/read-support.d.ts +48 -0
- package/dist/sessions/read-support.js +73 -0
- package/dist/sessions/read-tool-day.d.ts +2 -0
- package/dist/sessions/read-tool-day.js +26 -0
- package/dist/sessions/read-version-boundary.d.ts +31 -0
- package/dist/sessions/read-version-boundary.js +95 -0
- package/dist/sessions/read.d.ts +79 -0
- package/dist/sessions/read.js +84 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-object.d.ts +137 -0
- package/dist/sessions/recall-object.js +77 -0
- package/dist/sessions/recall-pool.d.ts +19 -0
- package/dist/sessions/recall-pool.js +96 -0
- package/dist/sessions/recall-push.d.ts +26 -0
- package/dist/sessions/recall-push.js +95 -0
- package/dist/sessions/recall-reads.d.ts +117 -0
- package/dist/sessions/recall-reads.js +410 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/redact.d.ts +6 -0
- package/dist/sessions/redact.js +54 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +644 -0
- package/dist/sessions/schema.js +148 -0
- package/dist/sessions/session-shape.d.ts +73 -0
- package/dist/sessions/session-shape.js +342 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +159 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +297 -0
- package/dist/sessions/store.js +1478 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +25 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/verify.d.ts +53 -0
- package/dist/sessions/verify.js +286 -0
- package/dist/sessions/vocab-load.d.ts +26 -0
- package/dist/sessions/vocab-load.js +61 -0
- package/dist/sessions/vscode-copilot.d.ts +38 -0
- package/dist/sessions/vscode-copilot.js +477 -0
- package/package.json +4 -5
- package/vocab/crates.json +1 -0
- package/vocab/go.json +1 -0
- package/vocab/harness.json +1 -0
- package/vocab/homebrew.json +1 -0
- package/vocab/manifest.json +1 -0
- package/vocab/maven.json +1 -0
- package/vocab/models.json +1 -0
- package/vocab/npm.json +1 -0
- package/vocab/programs.json +1 -0
- package/vocab/pypi.json +1 -0
- package/vocab/rubygems.json +1 -0
- package/dist/commands/deploy-input.d.ts +0 -38
- package/dist/commands/deploy-input.js +0 -150
- package/dist/commands/deploy.d.ts +0 -22
- package/dist/commands/deploy.js +0 -124
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -18
- package/dist/commands/templates.d.ts +0 -11
- package/dist/commands/templates.js +0 -32
- package/dist/commands/test-charge.d.ts +0 -11
- package/dist/commands/test-charge.js +0 -34
- package/dist/journal.d.ts +0 -26
- package/dist/journal.js +0 -58
- package/parity/deploy-plans/paid-api.json +0 -34
- package/parity/deploy-plans/per-task-agent.json +0 -34
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// The hook index: a small file written at the end of every deposit so a hook never parses the whole observation log.
|
|
2
|
+
// SessionStart reads `projects` (the recurring signatures per project folder); PostToolUse reads `signatures` (sessions and
|
|
3
|
+
// what followed, by exact signature). Both are pure functions of the store.
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { resolveReads } from './recall-pool.js';
|
|
7
|
+
import { aboveBaseRate, fitReads, toolShapeId } from './recall-reads.js';
|
|
8
|
+
import { expandHome } from './allow.js';
|
|
9
|
+
import { slugOf } from './store.js';
|
|
10
|
+
const INDEX_FILE = 'hook-index.json';
|
|
11
|
+
/** A signature is "recurring" once it has been seen in this many sessions. */
|
|
12
|
+
export const RECURRING_SESSIONS = 2;
|
|
13
|
+
const PER_PROJECT = 12;
|
|
14
|
+
const FOLLOWED = 3;
|
|
15
|
+
/** SessionStart ranks by sessions within this many latest sessions of its repository: the `measure --last 300 --repo` window the G1 reading uses. */
|
|
16
|
+
export const RECENT_SESSIONS = 300;
|
|
17
|
+
const topFollowed = (following) => Object.entries(following)
|
|
18
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
19
|
+
.slice(0, FOLLOWED);
|
|
20
|
+
const tally = () => ({ sessions: new Set(), following: {}, harness: new Set() });
|
|
21
|
+
// The same order `measure` uses, so the window here is the window `measure --last 300 --repo` counts.
|
|
22
|
+
const byStart = (a, b) => (a.tsFirst ?? '').localeCompare(b.tsFirst ?? '') || a.session.localeCompare(b.session);
|
|
23
|
+
const WORKTREE_SLUG = '--claude-worktrees-';
|
|
24
|
+
/**
|
|
25
|
+
* Groups project folder slugs into repositories without knowing any root: a worktree slug folds into its repository's, and a slug that
|
|
26
|
+
* extends another stored slug (a subfolder) joins the shortest one, the same extension rule `belongs` applies at recall time.
|
|
27
|
+
*/
|
|
28
|
+
const repositoryOf = (projects) => {
|
|
29
|
+
const baseOf = (project) => project.split(WORKTREE_SLUG)[0] ?? project;
|
|
30
|
+
const bases = [...new Set(projects.map(baseOf))].sort((a, b) => a.length - b.length);
|
|
31
|
+
return (project) => {
|
|
32
|
+
const base = baseOf(project);
|
|
33
|
+
return bases.find((candidate) => base === candidate || base.startsWith(`${candidate}-`)) ?? base;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export const buildIndex = (store, redactDomains, at, reads = []) => {
|
|
37
|
+
const projectOf = new Map(store.sessions.map((session) => [session.session, session.project]));
|
|
38
|
+
// The latest 300 of each repository, not of the whole store: another repository's deposits must not move this one's ranking.
|
|
39
|
+
const family = repositoryOf([...new Set(store.sessions.map((session) => session.project))]);
|
|
40
|
+
const byRepository = new Map();
|
|
41
|
+
for (const session of store.sessions) {
|
|
42
|
+
const key = family(session.project);
|
|
43
|
+
const group = byRepository.get(key) ?? [];
|
|
44
|
+
byRepository.set(key, group);
|
|
45
|
+
group.push(session);
|
|
46
|
+
}
|
|
47
|
+
const recentNames = new Set([...byRepository.values()].flatMap((group) => group.sort(byStart).slice(-RECENT_SESSIONS).map((session) => session.session)));
|
|
48
|
+
const overall = new Map();
|
|
49
|
+
const byProject = new Map();
|
|
50
|
+
for (const error of store.errors) {
|
|
51
|
+
const project = projectOf.get(error.session);
|
|
52
|
+
const targets = [overall];
|
|
53
|
+
if (project !== undefined) {
|
|
54
|
+
const own = byProject.get(project) ?? new Map();
|
|
55
|
+
byProject.set(project, own);
|
|
56
|
+
targets.push(own);
|
|
57
|
+
}
|
|
58
|
+
for (const target of targets) {
|
|
59
|
+
const entry = target.get(error.sig) ?? tally();
|
|
60
|
+
target.set(error.sig, entry);
|
|
61
|
+
entry.sessions.add(error.session);
|
|
62
|
+
if (error.harnessVersion !== null)
|
|
63
|
+
entry.harness.add(error.harnessVersion);
|
|
64
|
+
for (const tool of error.next)
|
|
65
|
+
entry.following[tool] = (entry.following[tool] ?? 0) + 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const signatures = {};
|
|
69
|
+
for (const [sig, entry] of overall) {
|
|
70
|
+
if (entry.sessions.size >= RECURRING_SESSIONS)
|
|
71
|
+
signatures[sig] = { sessions: entry.sessions.size, followed: topFollowed(entry.following) };
|
|
72
|
+
}
|
|
73
|
+
const projects = {};
|
|
74
|
+
for (const [project, sigs] of byProject) {
|
|
75
|
+
const rows = [...sigs.entries()]
|
|
76
|
+
.filter(([, entry]) => entry.sessions.size >= RECURRING_SESSIONS)
|
|
77
|
+
.map(([sig, entry]) => ({
|
|
78
|
+
sig,
|
|
79
|
+
sessions: entry.sessions.size,
|
|
80
|
+
recent: [...entry.sessions].filter((name) => recentNames.has(name)).length,
|
|
81
|
+
followed: topFollowed(entry.following),
|
|
82
|
+
harness: [...entry.harness].sort(),
|
|
83
|
+
}))
|
|
84
|
+
.filter((row) => row.recent > 0)
|
|
85
|
+
.sort((a, b) => b.recent - a.recent || b.sessions - a.sessions || a.sig.localeCompare(b.sig))
|
|
86
|
+
.slice(0, PER_PROJECT);
|
|
87
|
+
if (rows.length > 0)
|
|
88
|
+
projects[project] = rows;
|
|
89
|
+
}
|
|
90
|
+
return { at, redactDomains: [...redactDomains], projects, signatures, fit: Object.fromEntries(resolveReads(aboveBaseRate(store, fitReads(store)), reads).map((read) => [toolShapeId(read.tool, read.keys), read])) };
|
|
91
|
+
};
|
|
92
|
+
export const indexPath = (storeDir) => join(storeDir, INDEX_FILE);
|
|
93
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
94
|
+
// The file is ours; these checks guard a torn or foreign file, and the inner rows are trusted as written (as the store's are).
|
|
95
|
+
const isIndex = (value) => isRecord(value) && isRecord(value.projects) && isRecord(value.signatures) && Array.isArray(value.redactDomains) && typeof value.at === 'string';
|
|
96
|
+
/** The index as written, or undefined when there is none (or it is torn): a hook then says nothing. */
|
|
97
|
+
export const readIndex = async (storeDir) => {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(await readFile(indexPath(storeDir), 'utf8'));
|
|
100
|
+
return isIndex(parsed) ? parsed : undefined;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// A missing or torn index is "no index".
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/** `<root>/.claude/worktrees/<name>` belongs to `<root>`, so a worktree session sees its repository's history. */
|
|
108
|
+
export const repositoryRoot = (cwd) => {
|
|
109
|
+
const marker = /[\\/]\.claude[\\/]worktrees[\\/]/.exec(cwd);
|
|
110
|
+
return marker === null ? cwd : cwd.slice(0, marker.index);
|
|
111
|
+
};
|
|
112
|
+
/** A project folder belongs to the repository when its slug is the root's slug or extends it (a subfolder or a worktree). */
|
|
113
|
+
const belongs = (project, rootSlug) => project === rootSlug || project.startsWith(`${rootSlug}-`);
|
|
114
|
+
/** True when a project folder slug belongs to the repository `root` sits in (a worktree path counts as its repository). */
|
|
115
|
+
export const inRepository = (project, root) => belongs(project, slugOf(repositoryRoot(resolve(expandHome(root)))));
|
|
116
|
+
const clip = (text, length) => (text.length <= length ? text : `${text.slice(0, length - 1)}…`);
|
|
117
|
+
/** The top recurring signatures for the repository, as a short block for injection; undefined when there is nothing to say. */
|
|
118
|
+
export const contextBlock = (index, options) => {
|
|
119
|
+
const rootSlug = slugOf(repositoryRoot(options.cwd));
|
|
120
|
+
const merged = new Map();
|
|
121
|
+
for (const [project, rows] of Object.entries(index.projects)) {
|
|
122
|
+
if (!belongs(project, rootSlug))
|
|
123
|
+
continue;
|
|
124
|
+
for (const row of rows) {
|
|
125
|
+
if (options.harness !== undefined && !row.harness.includes(options.harness))
|
|
126
|
+
continue;
|
|
127
|
+
const have = merged.get(row.sig);
|
|
128
|
+
if (have === undefined) {
|
|
129
|
+
merged.set(row.sig, { ...row });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
have.sessions += row.sessions;
|
|
133
|
+
have.recent += row.recent;
|
|
134
|
+
const following = {};
|
|
135
|
+
for (const [tool, count] of [...have.followed, ...row.followed])
|
|
136
|
+
following[tool] = (following[tool] ?? 0) + count;
|
|
137
|
+
have.followed = topFollowed(following);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const rows = [...merged.values()].sort((a, b) => b.recent - a.recent || b.sessions - a.sessions || a.sig.localeCompare(b.sig)).slice(0, options.top);
|
|
141
|
+
if (rows.length === 0)
|
|
142
|
+
return undefined;
|
|
143
|
+
const header = `oars recall: errors that recur in this repository's earlier sessions${options.harness === undefined ? '' : ` (Claude Code ${options.harness})`}. Check these before retrying:`;
|
|
144
|
+
const lines = rows.map((row) => {
|
|
145
|
+
const followed = row.followed.map(([tool]) => tool).join(', ');
|
|
146
|
+
return `- ${clip(row.sig, 120)} (${String(row.recent)} sessions in the last ${String(RECENT_SESSIONS)}${followed === '' ? '' : `; next: ${followed}`})`;
|
|
147
|
+
});
|
|
148
|
+
const kept = [];
|
|
149
|
+
let used = header.length;
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
if (used + 1 + line.length > options.maxChars)
|
|
152
|
+
break;
|
|
153
|
+
kept.push(line);
|
|
154
|
+
used += 1 + line.length;
|
|
155
|
+
}
|
|
156
|
+
return kept.length === 0 ? undefined : [header, ...kept].join('\n');
|
|
157
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HookCall } from './schema.js';
|
|
2
|
+
import { type SessionsRuntime } from './store.js';
|
|
3
|
+
export type HookEvent = HookCall['event'];
|
|
4
|
+
export declare const HOOK_EVENTS: readonly HookEvent[];
|
|
5
|
+
/** The runtime a hook process runs with. Only `nightly` reads the non-Claude stores (a saved `--allow cursor` or `--allow codex`); the Stop path stays free of it. */
|
|
6
|
+
export declare const hookRuntime: (event: HookEvent, env: {
|
|
7
|
+
[name: string]: string | undefined;
|
|
8
|
+
}, platform: NodeJS.Platform, home: string, io: Pick<SessionsRuntime, "stdin" | "stdout">) => SessionsRuntime;
|
|
9
|
+
/**
|
|
10
|
+
* Runs one hook. It never fails or blocks the session it runs in: whatever happens, it resolves (the process exits 0), and the
|
|
11
|
+
* hook-call line records the outcome, with the failure's first line when there was one, so the record is evidence, not a claim.
|
|
12
|
+
*/
|
|
13
|
+
export declare const runHook: (event: HookEvent, stdin: string, runtime: SessionsRuntime, now: () => number) => Promise<string | undefined>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// The hook entry points. Each takes the hook's stdin and returns the text for stdout (or undefined for none). They are fail-open:
|
|
2
|
+
// any error inside one is swallowed, so a hook can never fail or block a Claude Code session. Each call is counted in
|
|
3
|
+
// `hook-calls.jsonl` with its own duration, so `measure` can count recall calls per session and the timings can be checked.
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
6
|
+
import { defaultConfigDir } from '../credentials.js';
|
|
7
|
+
import { cursorRoot } from './cursor.js';
|
|
8
|
+
import { SOURCES, sourceLabel } from './sources.js';
|
|
9
|
+
import { signatureOf } from './extract.js';
|
|
10
|
+
import { appendHookCall } from './hook-calls.js';
|
|
11
|
+
import { contextBlock, readIndex } from './hook-index.js';
|
|
12
|
+
import { pushRecall } from './recall-push.js';
|
|
13
|
+
import { toolShapeId } from './recall-reads.js';
|
|
14
|
+
import { makeRedactor } from './redact.js';
|
|
15
|
+
import { errorLine, inputKey, logStep, newContext, runDeposit, stampKey, tryLog } from './steps.js';
|
|
16
|
+
import { deposit, depositSession, readSaved } from './store.js';
|
|
17
|
+
import { recordSurvival } from './survival.js';
|
|
18
|
+
export const HOOK_EVENTS = ['session-start', 'pre-tool', 'post-tool', 'stop', 'nightly'];
|
|
19
|
+
const TOP = 5;
|
|
20
|
+
const MAX_CHARS = 1200;
|
|
21
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
+
const parse = (stdin) => {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(stdin);
|
|
25
|
+
return isRecord(parsed) ? parsed : {};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const str = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
|
|
32
|
+
const additional = (eventName, context) => `${JSON.stringify({ hookSpecificOutput: { hookEventName: eventName, additionalContext: context } })}\n`;
|
|
33
|
+
const sessionStart = async (runtime, input) => {
|
|
34
|
+
const cwd = str(input.cwd);
|
|
35
|
+
const index = cwd === undefined ? undefined : await readIndex(runtime.storeDir);
|
|
36
|
+
const block = index === undefined || cwd === undefined ? undefined : contextBlock(index, { cwd, top: TOP, maxChars: MAX_CHARS });
|
|
37
|
+
return block === undefined ? { hit: false, queried: index !== undefined } : { output: additional('SessionStart', block), hit: true, queried: true };
|
|
38
|
+
};
|
|
39
|
+
const inputKeys = (input) => (isRecord(input.tool_input) ? Object.keys(input.tool_input) : []);
|
|
40
|
+
/**
|
|
41
|
+
* The fit push. The only output this hook writes is `additionalContext`: it holds no permission decision and changes no input, so whatever it does, the call runs as asked.
|
|
42
|
+
* A call with no read says nothing and logs no recall step: that is nearly every call, and nobody asked. A held push is a hit, for what it would have shown, with no output.
|
|
43
|
+
*/
|
|
44
|
+
const preTool = async (runtime, input, now) => {
|
|
45
|
+
const sid = str(input.session_id);
|
|
46
|
+
const tool = str(input.tool_name);
|
|
47
|
+
if (sid === undefined || tool === undefined)
|
|
48
|
+
return { hit: false, queried: false };
|
|
49
|
+
const index = await readIndex(runtime.storeDir);
|
|
50
|
+
if (index === undefined)
|
|
51
|
+
return { hit: false, queried: false };
|
|
52
|
+
const recall = await pushRecall({ storeDir: runtime.storeDir, sid, tool, keys: inputKeys(input), transcript: str(input.transcript_path), platform: process.platform, at: new Date(now()).toISOString(), index });
|
|
53
|
+
if (recall === undefined)
|
|
54
|
+
return { hit: false, queried: false };
|
|
55
|
+
return recall.arm === 'shown' ? { output: additional('PreToolUse', recall.answer.rendered), hit: true, queried: true } : { hit: true, queried: true };
|
|
56
|
+
};
|
|
57
|
+
/** A failed tool arrives as PostToolUseFailure with the text in `error` (Bash and PowerShell start it with `Exit code N`); a PostToolUse response that starts that way is read too. */
|
|
58
|
+
const failureText = (input) => {
|
|
59
|
+
const failed = str(input.error);
|
|
60
|
+
if (failed !== undefined)
|
|
61
|
+
return failed;
|
|
62
|
+
const response = str(input.tool_response);
|
|
63
|
+
return response !== undefined && /^Exit code \d+/.test(response) ? response : undefined;
|
|
64
|
+
};
|
|
65
|
+
const postTool = async (runtime, input) => {
|
|
66
|
+
const tool = str(input.tool_name);
|
|
67
|
+
const text = failureText(input);
|
|
68
|
+
if (tool === undefined || text === undefined)
|
|
69
|
+
return { hit: false, queried: false };
|
|
70
|
+
const index = await readIndex(runtime.storeDir);
|
|
71
|
+
if (index === undefined)
|
|
72
|
+
return { hit: false, queried: false };
|
|
73
|
+
const found = index.signatures[signatureOf(tool, text, makeRedactor(index.redactDomains))];
|
|
74
|
+
if (found === undefined)
|
|
75
|
+
return { hit: false, queried: true };
|
|
76
|
+
const followed = found.followed.map(([next]) => next).join(', ');
|
|
77
|
+
const context = `oars recall: seen in ${String(found.sessions)} sessions; what followed: ${followed === '' ? 'nothing recorded' : followed}`;
|
|
78
|
+
return { output: additional(str(input.hook_event_name) ?? 'PostToolUseFailure', context), hit: true, queried: true };
|
|
79
|
+
};
|
|
80
|
+
const stop = async (runtime, input, now) => {
|
|
81
|
+
const transcript = str(input.transcript_path);
|
|
82
|
+
if (transcript === undefined)
|
|
83
|
+
return { hit: false, queried: false };
|
|
84
|
+
// The SessionEnd hook is the `stop` trigger (spec 4.1); derive and index follow at the nightly run, so only a deposit row is logged.
|
|
85
|
+
const deposited = await runDeposit(newContext(runtime.storeDir, now, 'stop'), false, inputKey('stop', [transcript], []), async (trace) => ({ value: await depositSession(runtime, transcript, now, trace), in: { transcript: 1 } }));
|
|
86
|
+
return { hit: deposited === 'deposited', queried: false };
|
|
87
|
+
};
|
|
88
|
+
const nightly = async (runtime, now) => {
|
|
89
|
+
// A state.json that cannot be read is a failed deposit run like any other: its error row and the skipped derive row (spec 4.4) are logged.
|
|
90
|
+
const saved = await readSaved(runtime.storeDir).catch((failure) => runDeposit(newContext(runtime.storeDir, now, 'nightly'), true, inputKey('nightly', [], []), () => Promise.reject(failure instanceof Error ? failure : new Error(String(failure)))));
|
|
91
|
+
if (saved.allow.length === 0)
|
|
92
|
+
return { hit: false, queried: false };
|
|
93
|
+
const result = await runDeposit(newContext(runtime.storeDir, now, 'nightly'), true, inputKey('nightly', saved.allow, saved.redactDomains), async (trace) => {
|
|
94
|
+
const done = await deposit(runtime, { allow: saved.allow, redactDomains: saved.redactDomains, now }, trace);
|
|
95
|
+
return { value: done, in: { transcript: done.sessions + done.subagents + done.unchanged } };
|
|
96
|
+
});
|
|
97
|
+
const survival = await recordSurvival(runtime, saved.allow, now);
|
|
98
|
+
// A source store that could not be read does not fail the run, so the record carries it or nobody sees it.
|
|
99
|
+
const counts = Object.entries(result.sources);
|
|
100
|
+
const unreadable = counts.flatMap(([, tally]) => tally.unreadableLogged)[0];
|
|
101
|
+
if (unreadable !== undefined)
|
|
102
|
+
return { hit: true, queried: false, error: await errorLine(runtime.storeDir, unreadable) };
|
|
103
|
+
// A skip for an unsafe session id is likewise reported, never silent (the `rejected` doc in sources.ts); an unreadable store, which names its own reason, takes precedence.
|
|
104
|
+
const skipped = counts.find(([, tally]) => tally.rejected > 0);
|
|
105
|
+
if (skipped !== undefined)
|
|
106
|
+
return { hit: true, queried: false, error: `Skipped ${String(skipped[1].rejected)} ${sourceLabel(skipped[0], runtime.sources ?? SOURCES)} sessions whose id is not a safe file name` };
|
|
107
|
+
// A survival read that fails every night is otherwise invisible, for the same reason; it comes last because the run itself did not lose a session.
|
|
108
|
+
return survival === null ? { hit: true, queried: false, error: await errorLine(runtime.storeDir, 'Commit survival not read this run.') } : { hit: true, queried: false };
|
|
109
|
+
};
|
|
110
|
+
/** What a lookup hook looks up, as its recall key (spec 4.2 names none for a lookup): the cwd for session-start, the failure's signature for post-tool, the call's shape for pre-tool; null when it had nothing to look up. */
|
|
111
|
+
const lookupKey = (event, input) => {
|
|
112
|
+
if (event === 'session-start') {
|
|
113
|
+
const cwd = str(input.cwd);
|
|
114
|
+
return cwd === undefined ? null : stampKey(event, cwd);
|
|
115
|
+
}
|
|
116
|
+
const tool = str(input.tool_name);
|
|
117
|
+
if (event === 'pre-tool')
|
|
118
|
+
return tool === undefined ? null : stampKey(event, toolShapeId(tool, inputKeys(input)));
|
|
119
|
+
const text = failureText(input);
|
|
120
|
+
return event === 'post-tool' && tool !== undefined && text !== undefined ? stampKey(event, signatureOf(tool, text, makeRedactor([]))) : null;
|
|
121
|
+
};
|
|
122
|
+
/** The runtime a hook process runs with. Only `nightly` reads the non-Claude stores (a saved `--allow cursor` or `--allow codex`); the Stop path stays free of it. */
|
|
123
|
+
export const hookRuntime = (event, env, platform, home, io) => ({
|
|
124
|
+
storeDir: join(defaultConfigDir(env, platform, home), 'oars', 'sessions'),
|
|
125
|
+
projectsRoot: join(env.CLAUDE_CONFIG_DIR ?? join(home, '.claude'), 'projects'),
|
|
126
|
+
...(event === 'nightly' ? { cursorRoot: cursorRoot(platform, env, home), sourceRoots: Object.fromEntries(SOURCES.map((source) => [source.name, source.root(platform, env, home)])) } : {}),
|
|
127
|
+
...io,
|
|
128
|
+
});
|
|
129
|
+
/**
|
|
130
|
+
* Runs one hook. It never fails or blocks the session it runs in: whatever happens, it resolves (the process exits 0), and the
|
|
131
|
+
* hook-call line records the outcome, with the failure's first line when there was one, so the record is evidence, not a claim.
|
|
132
|
+
*/
|
|
133
|
+
export const runHook = async (event, stdin, runtime, now) => {
|
|
134
|
+
const started = performance.now();
|
|
135
|
+
let session = null;
|
|
136
|
+
let outcome = { hit: false, queried: false };
|
|
137
|
+
let failed = false;
|
|
138
|
+
let failure;
|
|
139
|
+
let key = null;
|
|
140
|
+
try {
|
|
141
|
+
const input = parse(stdin);
|
|
142
|
+
key = lookupKey(event, input);
|
|
143
|
+
session = str(input.session_id) ?? null;
|
|
144
|
+
if (event === 'session-start')
|
|
145
|
+
outcome = await sessionStart(runtime, input);
|
|
146
|
+
else if (event === 'pre-tool')
|
|
147
|
+
outcome = await preTool(runtime, input, now);
|
|
148
|
+
else if (event === 'post-tool')
|
|
149
|
+
outcome = await postTool(runtime, input);
|
|
150
|
+
else if (event === 'stop')
|
|
151
|
+
outcome = await stop(runtime, input, now);
|
|
152
|
+
else
|
|
153
|
+
outcome = await nightly(runtime, now);
|
|
154
|
+
}
|
|
155
|
+
catch (thrown) {
|
|
156
|
+
failure = thrown;
|
|
157
|
+
failed = true;
|
|
158
|
+
}
|
|
159
|
+
const ms = Math.round(performance.now() - started);
|
|
160
|
+
// The failure line is masked once, the same for the hook-calls line and the step row beside it; both writes are guarded, so a counter
|
|
161
|
+
// that cannot be written never costs the session its context.
|
|
162
|
+
const line = failed ? await errorLine(runtime.storeDir, failure).catch(() => 'unknown error') : outcome.error ?? null;
|
|
163
|
+
await tryLog(() => appendHookCall(runtime.storeDir, { at: new Date(now()).toISOString(), event, session, hit: outcome.hit, ms, startMs: Math.round(started), error: line }));
|
|
164
|
+
// A lookup hook that ran a query (or failed trying) is a `recall` step started by its own trigger; the deposit hooks logged their rows as they ran.
|
|
165
|
+
if (key !== null && (failed || outcome.queried)) {
|
|
166
|
+
const report = { in: { query: 1 }, out: failed ? {} : { matches: outcome.hit ? 1 : 0 }, key };
|
|
167
|
+
await tryLog(() => logStep(newContext(runtime.storeDir, now, event), 'recall', failed ? 'error' : 'ok', ms, report, line));
|
|
168
|
+
}
|
|
169
|
+
return outcome.output;
|
|
170
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ActionTable, type CanonicalAction } from './actions.js';
|
|
2
|
+
import type { PullIdentity, ShellIdentity, StepIdentity, ToolPull } from './schema.js';
|
|
3
|
+
/** A word of a shell command, as the tokenizer read it. */
|
|
4
|
+
export interface WordOfCommand {
|
|
5
|
+
text: string;
|
|
6
|
+
quoted: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const NUMBER_FLAG = "-N";
|
|
9
|
+
export declare const isPositional: ({ text, quoted }: WordOfCommand) => boolean;
|
|
10
|
+
/** A pull in public names. A private package's version can still identify it, so it is masked with the name; a stated version stays stated. */
|
|
11
|
+
export declare const pullIdentity: (pull: ToolPull) => PullIdentity;
|
|
12
|
+
/** A shell step whose program is private or unreadable: its flags are its own, so none are kept. */
|
|
13
|
+
export declare const UNREAD_SHELL: ShellIdentity;
|
|
14
|
+
export declare const shellIdentity: (program: string, args: readonly WordOfCommand[], pulls: readonly ToolPull[]) => ShellIdentity;
|
|
15
|
+
/** What a non-shell call's step reads as. `native` comes back masked unless every name it holds is public, since an MCP name spells out its server. */
|
|
16
|
+
export declare const callIdentity: (table: ActionTable, action: CanonicalAction, native: string, shown: string, input: {
|
|
17
|
+
skill: string | undefined;
|
|
18
|
+
url: string | undefined;
|
|
19
|
+
}) => {
|
|
20
|
+
native: string;
|
|
21
|
+
identity: StepIdentity;
|
|
22
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// A step's `identity` (recall spec Section 2.2): what the step acted on, by action, in public names only. Every name passes through
|
|
2
|
+
// `public-vocabulary.ts`, so what may leave the machine is decided in that one file; anything it does not list becomes `<literal>`.
|
|
3
|
+
import { mcpOf } from './actions.js';
|
|
4
|
+
import { BUNDLING_PROGRAMS, isPublicMcpTool, isRegistryVersion, LITERAL, PUBLIC_FLAGS, PUBLIC_HOSTS, PUBLIC_MCP_SERVERS, PUBLIC_PACKAGES, PUBLIC_PROGRAMS, PUBLIC_SKILLS, PUBLIC_SUBCOMMANDS } from './public-vocabulary.js';
|
|
5
|
+
const orLiteral = (name, allowed) => (allowed.has(name) ? name : LITERAL);
|
|
6
|
+
const SHORT_CLUSTER = /^-[a-z]{1,4}$/;
|
|
7
|
+
const SHORT_LETTER = /^-[A-Za-z]$/;
|
|
8
|
+
// A short flag with a value joined after a non-letter, as in `-c1-200`: the first capture is the name and the rest is the value, which is dropped.
|
|
9
|
+
const JOINED_VALUE = /^(-[A-Za-z])[^A-Za-z]/;
|
|
10
|
+
// `head -20`: the digits are the value and the flag has no name of its own, so every such flag reads alike.
|
|
11
|
+
const BARE_NUMBER = /^-\d+$/;
|
|
12
|
+
export const NUMBER_FLAG = '-N';
|
|
13
|
+
/**
|
|
14
|
+
* The names one flag word holds, without its value. On a program that bundles, a short cluster reads as its letters, so `-rf` and `-fr` agree;
|
|
15
|
+
* a value joined to a flag there can still read as letters, one each and never the word. On any other program the cluster is the literal.
|
|
16
|
+
*/
|
|
17
|
+
const flagNames = (text, bundles) => {
|
|
18
|
+
const name = text.split('=')[0] ?? text;
|
|
19
|
+
if (name.startsWith('--') || PUBLIC_FLAGS.has(name))
|
|
20
|
+
return [orLiteral(name, PUBLIC_FLAGS)];
|
|
21
|
+
if (SHORT_LETTER.test(name))
|
|
22
|
+
return [name];
|
|
23
|
+
if (BARE_NUMBER.test(name))
|
|
24
|
+
return [NUMBER_FLAG];
|
|
25
|
+
const joined = JOINED_VALUE.exec(name)?.[1];
|
|
26
|
+
if (joined !== undefined)
|
|
27
|
+
return [joined];
|
|
28
|
+
return bundles && SHORT_CLUSTER.test(name) ? Array.from(name.slice(1), (letter) => `-${letter}`) : [LITERAL];
|
|
29
|
+
};
|
|
30
|
+
const flagsOf = (program, args) => {
|
|
31
|
+
const bundles = BUNDLING_PROGRAMS.has(program);
|
|
32
|
+
const flags = new Set();
|
|
33
|
+
for (const { text, quoted } of args) {
|
|
34
|
+
if (text === '--')
|
|
35
|
+
break;
|
|
36
|
+
if (quoted || !text.startsWith('-') || text.includes('$'))
|
|
37
|
+
continue;
|
|
38
|
+
for (const name of flagNames(text, bundles))
|
|
39
|
+
flags.add(name);
|
|
40
|
+
}
|
|
41
|
+
return [...flags].sort();
|
|
42
|
+
};
|
|
43
|
+
export const isPositional = ({ text, quoted }) => !quoted && text !== '' && !text.startsWith('-') && !/[$\\/]/.test(text);
|
|
44
|
+
/** A subcommand from the program's own table, wherever a global flag's value left it; a positional word outside the table is the literal. */
|
|
45
|
+
const subOf = (program, args) => {
|
|
46
|
+
const table = PUBLIC_SUBCOMMANDS.get(program);
|
|
47
|
+
if (table === undefined)
|
|
48
|
+
return null;
|
|
49
|
+
const positional = args.filter(isPositional);
|
|
50
|
+
const known = positional.find(({ text }) => table.has(text));
|
|
51
|
+
if (known !== undefined)
|
|
52
|
+
return known.text;
|
|
53
|
+
return positional.length === 0 ? null : LITERAL;
|
|
54
|
+
};
|
|
55
|
+
const REGISTRY_OF = {
|
|
56
|
+
npm: 'npm',
|
|
57
|
+
pypi: 'pypi',
|
|
58
|
+
cargo: 'crates',
|
|
59
|
+
go: 'go',
|
|
60
|
+
gem: 'rubygems',
|
|
61
|
+
brew: 'homebrew',
|
|
62
|
+
skill: 'skill',
|
|
63
|
+
plugin: 'plugin',
|
|
64
|
+
mcp: 'mcp',
|
|
65
|
+
};
|
|
66
|
+
/** A pull in public names. A private package's version can still identify it, so it is masked with the name; a stated version stays stated. */
|
|
67
|
+
export const pullIdentity = (pull) => {
|
|
68
|
+
const name = orLiteral(pull.name, PUBLIC_PACKAGES);
|
|
69
|
+
const showsVersion = name !== LITERAL && pull.version !== null && isRegistryVersion(pull.version);
|
|
70
|
+
return { registry: REGISTRY_OF[pull.ecosystem], name, version: pull.version === null ? null : showsVersion ? pull.version : LITERAL };
|
|
71
|
+
};
|
|
72
|
+
/** A shell step whose program is private or unreadable: its flags are its own, so none are kept. */
|
|
73
|
+
export const UNREAD_SHELL = { program: LITERAL, sub: null, flags: [] };
|
|
74
|
+
export const shellIdentity = (program, args, pulls) => {
|
|
75
|
+
if (!PUBLIC_PROGRAMS.has(program))
|
|
76
|
+
return UNREAD_SHELL;
|
|
77
|
+
const identity = { program, sub: subOf(program, args), flags: flagsOf(program, args) };
|
|
78
|
+
return pulls.length === 0 ? identity : { ...identity, pulls: pulls.map(pullIdentity) };
|
|
79
|
+
};
|
|
80
|
+
const hostOf = (url) => {
|
|
81
|
+
if (url === undefined || url === '')
|
|
82
|
+
return { host: null };
|
|
83
|
+
// The parser drops credentials, the path, and the query, and lower-cases the host; the list then decides.
|
|
84
|
+
const parsed = URL.canParse(url) ? new URL(url).hostname : '';
|
|
85
|
+
return { host: orLiteral(parsed, PUBLIC_HOSTS) };
|
|
86
|
+
};
|
|
87
|
+
/** What a non-shell call's step reads as. `native` comes back masked unless every name it holds is public, since an MCP name spells out its server. */
|
|
88
|
+
export const callIdentity = (table, action, native, shown, input) => {
|
|
89
|
+
if (action === 'web.fetch' || action === 'web.search')
|
|
90
|
+
return { native: shown, identity: hostOf(input.url) };
|
|
91
|
+
if (action === 'skill.run')
|
|
92
|
+
return { native: shown, identity: { skill: orLiteral(input.skill ?? '', PUBLIC_SKILLS) } };
|
|
93
|
+
const mcp = action === 'mcp.call' ? mcpOf(table, native) : undefined;
|
|
94
|
+
if (mcp === undefined)
|
|
95
|
+
return { native: shown, identity: null };
|
|
96
|
+
if (!PUBLIC_MCP_SERVERS.has(mcp.server))
|
|
97
|
+
return { native: LITERAL, identity: { server: LITERAL, tool: LITERAL } };
|
|
98
|
+
if (!table.toolDelimited || !isPublicMcpTool(mcp.server, mcp.tool))
|
|
99
|
+
return { native: LITERAL, identity: { server: mcp.server, tool: LITERAL } };
|
|
100
|
+
return { native, identity: { server: mcp.server, tool: mcp.tool } };
|
|
101
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export declare const RETENTION_FLOOR = 365;
|
|
2
|
+
export declare const DEFAULT_RETENTION = 30;
|
|
3
|
+
export declare const TASK_NAME = "OarsSessionsNightly";
|
|
4
|
+
/** What the install needs from the machine, so a test can stand in for each. */
|
|
5
|
+
export interface InstallRuntime {
|
|
6
|
+
nodePath: string;
|
|
7
|
+
/** Absolute path of the built `hook-bin.js`. */
|
|
8
|
+
hookBin: string;
|
|
9
|
+
/** `<claude config dir>/settings.json`. */
|
|
10
|
+
userSettings: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
/** Runs `schtasks` with these arguments. */
|
|
13
|
+
schtasks: (args: string[]) => Promise<{
|
|
14
|
+
code: number;
|
|
15
|
+
output: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export type Scope = 'user' | 'project' | 'file';
|
|
19
|
+
/** A settings file is open-ended JSON the harness owns; only the keys named here are read or written. */
|
|
20
|
+
interface Json {
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The post-tool command reads the event first in the hook shell (Git Bash on Windows, where the harness runs hooks) and starts node only
|
|
25
|
+
* when the failure text carries `Exit code`: a fresh node costs 250 ms to seconds on Windows, the shell builtins a few milliseconds.
|
|
26
|
+
*/
|
|
27
|
+
export declare const commandOf: (runtime: InstallRuntime, arg: string) => string;
|
|
28
|
+
export declare const settingsPath: (runtime: InstallRuntime, scope: Scope, file: string | undefined) => string;
|
|
29
|
+
export declare const readSettings: (path: string) => Promise<Json>;
|
|
30
|
+
/** The retention the file sets: its `cleanupPeriodDays` when a number, else the harness default. */
|
|
31
|
+
export declare const effectiveRetention: (settings: Json) => number;
|
|
32
|
+
export interface InstallResult {
|
|
33
|
+
file: string;
|
|
34
|
+
installed: string[];
|
|
35
|
+
retention: string;
|
|
36
|
+
}
|
|
37
|
+
export declare const installHooks: (runtime: InstallRuntime, path: string, keepRetention: boolean) => Promise<InstallResult>;
|
|
38
|
+
export declare const uninstallHooks: (path: string) => Promise<{
|
|
39
|
+
file: string;
|
|
40
|
+
removed: string[];
|
|
41
|
+
}>;
|
|
42
|
+
export interface FileStatus {
|
|
43
|
+
file: string;
|
|
44
|
+
scope: Scope;
|
|
45
|
+
installed: string[];
|
|
46
|
+
retentionDays: number;
|
|
47
|
+
}
|
|
48
|
+
export declare const hookStatus: (runtime: InstallRuntime, extra: string | undefined) => Promise<FileStatus[]>;
|
|
49
|
+
/** The `schtasks` arguments for the nightly task: the same node and hook-bin the hooks use, at a fixed hour. */
|
|
50
|
+
export declare const scheduleArgs: (runtime: InstallRuntime, hour: number) => string[];
|
|
51
|
+
export declare const SCHEDULE_HOUR = 3;
|
|
52
|
+
export declare const setSchedule: (runtime: InstallRuntime, mode: "nightly" | "off") => Promise<string>;
|
|
53
|
+
export declare const scheduleStatus: (runtime: InstallRuntime) => Promise<string>;
|
|
54
|
+
export {};
|