@vibe-commit/capture 0.1.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.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/agents/claude_code.d.ts +11 -0
  4. package/dist/agents/claude_code.js +162 -0
  5. package/dist/agents/claude_code.js.map +1 -0
  6. package/dist/agents/codex_cli.d.ts +15 -0
  7. package/dist/agents/codex_cli.js +67 -0
  8. package/dist/agents/codex_cli.js.map +1 -0
  9. package/dist/agents/cursor.d.ts +21 -0
  10. package/dist/agents/cursor.js +89 -0
  11. package/dist/agents/cursor.js.map +1 -0
  12. package/dist/agents/registry.d.ts +184 -0
  13. package/dist/agents/registry.js +212 -0
  14. package/dist/agents/registry.js.map +1 -0
  15. package/dist/agents/types.d.ts +279 -0
  16. package/dist/agents/types.js +67 -0
  17. package/dist/agents/types.js.map +1 -0
  18. package/dist/commands/connect.d.ts +36 -0
  19. package/dist/commands/connect.js +499 -0
  20. package/dist/commands/connect.js.map +1 -0
  21. package/dist/commands/context.d.ts +45 -0
  22. package/dist/commands/context.js +14 -0
  23. package/dist/commands/context.js.map +1 -0
  24. package/dist/commands/report.d.ts +105 -0
  25. package/dist/commands/report.js +430 -0
  26. package/dist/commands/report.js.map +1 -0
  27. package/dist/commands/status.d.ts +29 -0
  28. package/dist/commands/status.js +236 -0
  29. package/dist/commands/status.js.map +1 -0
  30. package/dist/commands/why.d.ts +349 -0
  31. package/dist/commands/why.js +937 -0
  32. package/dist/commands/why.js.map +1 -0
  33. package/dist/consent.d.ts +59 -0
  34. package/dist/consent.js +111 -0
  35. package/dist/consent.js.map +1 -0
  36. package/dist/copy/claims.d.ts +79 -0
  37. package/dist/copy/claims.js +343 -0
  38. package/dist/copy/claims.js.map +1 -0
  39. package/dist/copy/grades.d.ts +117 -0
  40. package/dist/copy/grades.js +96 -0
  41. package/dist/copy/grades.js.map +1 -0
  42. package/dist/copy/index.d.ts +12 -0
  43. package/dist/copy/index.js +13 -0
  44. package/dist/copy/index.js.map +1 -0
  45. package/dist/copy/strings.d.ts +1328 -0
  46. package/dist/copy/strings.js +1243 -0
  47. package/dist/copy/strings.js.map +1 -0
  48. package/dist/copy/time.d.ts +27 -0
  49. package/dist/copy/time.js +43 -0
  50. package/dist/copy/time.js.map +1 -0
  51. package/dist/credential.d.ts +211 -0
  52. package/dist/credential.js +298 -0
  53. package/dist/credential.js.map +1 -0
  54. package/dist/exit.d.ts +59 -0
  55. package/dist/exit.js +60 -0
  56. package/dist/exit.js.map +1 -0
  57. package/dist/git.d.ts +299 -0
  58. package/dist/git.js +653 -0
  59. package/dist/git.js.map +1 -0
  60. package/dist/hooks/entry.d.ts +277 -0
  61. package/dist/hooks/entry.js +1092 -0
  62. package/dist/hooks/entry.js.map +1 -0
  63. package/dist/hooks/post_commit.d.ts +59 -0
  64. package/dist/hooks/post_commit.js +160 -0
  65. package/dist/hooks/post_commit.js.map +1 -0
  66. package/dist/hooks/post_rewrite.d.ts +59 -0
  67. package/dist/hooks/post_rewrite.js +161 -0
  68. package/dist/hooks/post_rewrite.js.map +1 -0
  69. package/dist/hooks/session_end.d.ts +101 -0
  70. package/dist/hooks/session_end.js +120 -0
  71. package/dist/hooks/session_end.js.map +1 -0
  72. package/dist/index.d.ts +34 -0
  73. package/dist/index.js +360 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/install.d.ts +153 -0
  76. package/dist/install.js +284 -0
  77. package/dist/install.js.map +1 -0
  78. package/dist/json.d.ts +100 -0
  79. package/dist/json.js +94 -0
  80. package/dist/json.js.map +1 -0
  81. package/dist/oauth/discovery.d.ts +96 -0
  82. package/dist/oauth/discovery.js +163 -0
  83. package/dist/oauth/discovery.js.map +1 -0
  84. package/dist/oauth/lock.d.ts +100 -0
  85. package/dist/oauth/lock.js +287 -0
  86. package/dist/oauth/lock.js.map +1 -0
  87. package/dist/oauth/loopback.d.ts +85 -0
  88. package/dist/oauth/loopback.js +124 -0
  89. package/dist/oauth/loopback.js.map +1 -0
  90. package/dist/oauth/pkce.d.ts +58 -0
  91. package/dist/oauth/pkce.js +77 -0
  92. package/dist/oauth/pkce.js.map +1 -0
  93. package/dist/oauth/read.d.ts +120 -0
  94. package/dist/oauth/read.js +144 -0
  95. package/dist/oauth/read.js.map +1 -0
  96. package/dist/oauth/session.d.ts +80 -0
  97. package/dist/oauth/session.js +112 -0
  98. package/dist/oauth/session.js.map +1 -0
  99. package/dist/oauth/signin.d.ts +178 -0
  100. package/dist/oauth/signin.js +225 -0
  101. package/dist/oauth/signin.js.map +1 -0
  102. package/dist/oauth/token.d.ts +139 -0
  103. package/dist/oauth/token.js +183 -0
  104. package/dist/oauth/token.js.map +1 -0
  105. package/dist/paths.d.ts +366 -0
  106. package/dist/paths.js +460 -0
  107. package/dist/paths.js.map +1 -0
  108. package/dist/policy.d.ts +195 -0
  109. package/dist/policy.js +244 -0
  110. package/dist/policy.js.map +1 -0
  111. package/dist/post.d.ts +302 -0
  112. package/dist/post.js +384 -0
  113. package/dist/post.js.map +1 -0
  114. package/dist/project.d.ts +99 -0
  115. package/dist/project.js +192 -0
  116. package/dist/project.js.map +1 -0
  117. package/dist/redact.d.ts +175 -0
  118. package/dist/redact.js +700 -0
  119. package/dist/redact.js.map +1 -0
  120. package/dist/runtime.d.ts +30 -0
  121. package/dist/runtime.js +37 -0
  122. package/dist/runtime.js.map +1 -0
  123. package/dist/spawn_budget.d.ts +101 -0
  124. package/dist/spawn_budget.js +124 -0
  125. package/dist/spawn_budget.js.map +1 -0
  126. package/dist/spool.d.ts +390 -0
  127. package/dist/spool.js +556 -0
  128. package/dist/spool.js.map +1 -0
  129. package/dist/state.d.ts +109 -0
  130. package/dist/state.js +217 -0
  131. package/dist/state.js.map +1 -0
  132. package/dist/system_message.d.ts +101 -0
  133. package/dist/system_message.js +141 -0
  134. package/dist/system_message.js.map +1 -0
  135. package/dist/term.d.ts +112 -0
  136. package/dist/term.js +134 -0
  137. package/dist/term.js.map +1 -0
  138. package/dist/version.d.ts +16 -0
  139. package/dist/version.js +17 -0
  140. package/dist/version.js.map +1 -0
  141. package/package.json +50 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * The hook client — and the enforcement of the hook half of the exit contract.
3
+ *
4
+ * DESIGN.md §13.7, restated because every line below exists to hold it:
5
+ *
6
+ * exit ALWAYS 0. Every path: success, 401, 403, network failure,
7
+ * uncaught exception, unsupported Node, signal.
8
+ * stderr ALWAYS empty.
9
+ * wall clock ALWAYS under the hooks.json timeout for that event.
10
+ * stdout At most one well-formed JSON object carrying `systemMessage`;
11
+ * otherwise empty. If JSON construction fails for ANY reason →
12
+ * empty stdout. The fallback is always silence.
13
+ *
14
+ * ⚠ **That stdout line reads "EMPTY on the happy path" in DESIGN.md §13.7, and
15
+ * as of `CR-023d` it is no longer true.** Style guide §10.1's fourth state
16
+ * (`capture confirmed`) fires on a successful send, once per `(repo, day)` — so
17
+ * a populated stdout no longer implies something went wrong. §13.7 and §10.1
18
+ * disagreed on this before this task; the code now follows §10.1, which D65
19
+ * records as the executed ruling. DESIGN.md is orchestrator-owned (D66 §3), so
20
+ * the amendment there is reported, not made here.
21
+ *
22
+ * **A hook that exits non-zero feeds stderr back into the user's agent and
23
+ * derails the task they were actually doing.** That is the whole reason.
24
+ *
25
+ * ## Read this before trusting a green contract test
26
+ *
27
+ * `silenceStderr()` makes the stderr-empty assertion true STRUCTURALLY. That is
28
+ * the correct engineering choice — it is the only thing that holds when a
29
+ * dependency three levels down decides to warn — but it also means a green
30
+ * "stderr is empty" proves the hook never speaks, and proves **nothing** about
31
+ * whether the credential is handled carefully. The credential's own control is
32
+ * separate and lives in `credential.ts` (opaque wrapper + redaction), with its
33
+ * own red-proof in `test/credential.test.ts`. Do not read one as evidence for
34
+ * the other.
35
+ */
36
+ import { type ProjectKeys } from "../project.js";
37
+ /**
38
+ * Default wall-clock budget. `CR-028` sets the real per-event `timeout` in
39
+ * `claude-plugin/hooks/hooks.json`; this is the client-side backstop, which must
40
+ * be the SMALLER of the two or the backstop never fires.
41
+ *
42
+ * ⚠ **That relation was prose and was checked by nothing until `CR-183`.** It is
43
+ * now asserted for every event of every dialect in `test/agent-registry.test.ts`,
44
+ * which also pins this constant against the `claude-code` dialect's own entry.
45
+ */
46
+ export declare const DEFAULT_HOOK_BUDGET_MS = 5000;
47
+ /**
48
+ * What one send attempt may take, after the settle has taken its share.
49
+ *
50
+ * `consumedMs` is 0 on every event but `SessionEnd`, and at 0 this is exactly
51
+ * the expression that was inline here before `CR-020d`
52
+ * (`Math.floor(budgetMs * SEND_BUDGET_FRACTION)`) for any budget above ~84 ms —
53
+ * so `Stop` and `PreCompact` are unchanged in both timing and code path.
54
+ */
55
+ export declare function sendTimeoutMs(budgetMs: number, consumedMs: number): number;
56
+ /**
57
+ * The budget the SENDS may spend — the whole budget, less the stamp's share.
58
+ *
59
+ * A function, and exported, for the reason `sendTimeoutMs` is: the carve-out is
60
+ * the thing this task changes, and a wall-clock test cannot pin it. Every
61
+ * discriminator that distinguishes reserve from no-reserve through a real
62
+ * process turned out to slide with per-send overhead — a 149 ms window that
63
+ * moves by 25 ms for every 10 ms of connect-and-compress cost — so pinning it
64
+ * there would be tuning a constant to one machine. Here it is arithmetic.
65
+ *
66
+ * ⛔ `budgetMs` itself still governs the SETTLE and the WATCHDOG. Only the sends
67
+ * are carved.
68
+ */
69
+ export declare function sendBudgetMs(budgetMs: number, eventName: string): number;
70
+ export interface HookInput {
71
+ readonly sessionId: string;
72
+ readonly transcriptPath: string;
73
+ readonly cwd: string;
74
+ readonly eventName: string;
75
+ /**
76
+ * ⛔ **THE ANNOUNCED DELEGATED TRANSCRIPT — `CR-196`, D177 §9.**
77
+ *
78
+ * `agent_transcript_path`, which an `announce` dialect hands over on stdin
79
+ * instead of writing a directory to glob. Absent for `discover` dialects and
80
+ * absent on every event but the one that delegates, so `undefined` is the
81
+ * ordinary case rather than an error.
82
+ *
83
+ * ⛔ **INPUT, in the full `/cso` finding-1 sense.** It is a path that reaches
84
+ * a read whose result is uploaded, so it is confined exactly as the main
85
+ * transcript is — `admitsTranscript`, which is the registry-computed boundary
86
+ * plus the file gate. Being handed a path by the agent is not evidence about
87
+ * where the path points.
88
+ */
89
+ readonly agentTranscriptPath?: string;
90
+ }
91
+ /**
92
+ * Parse hook stdin. Claude Code supplies `{ session_id, transcript_path, cwd,
93
+ * hook_event_name }` on stdin — NOT in the environment.
94
+ *
95
+ * ⚠ **And an `announce` dialect adds `agent_transcript_path` (`CR-196`).** It is
96
+ * OPTIONAL in both directions: a `discover` install never sends it, and reading
97
+ * it here rather than in a Cursor-specific parser is deliberate — stdin has one
98
+ * shape and one parser, and a second one would be a second place for the
99
+ * confinement boundary to be forgotten.
100
+ *
101
+ * Returns null on anything malformed. A hook cannot report a parse error (stderr
102
+ * is empty by contract), so the only honest response is to do nothing. ⚠ A
103
+ * malformed `agent_transcript_path` is NOT malformed input: it is dropped and
104
+ * the main transcript still goes, because losing the whole capture over a
105
+ * delegated extra would be the larger loss.
106
+ */
107
+ export declare function parseHookInput(raw: string): HookInput | null;
108
+ /**
109
+ * Replace stderr with a sink for the rest of the process.
110
+ *
111
+ * Node writes uncaught exceptions, unhandled rejections and process warnings to
112
+ * `process.stderr`, so overriding `write` is what makes the contract hold on the
113
+ * paths we do not control. The handlers below stop the non-zero exit; this stops
114
+ * the bytes.
115
+ */
116
+ export declare function silenceStderr(): void;
117
+ export interface HookEnvironment {
118
+ readonly env: NodeJS.ProcessEnv;
119
+ readonly home: string;
120
+ readonly nodeVersion: string;
121
+ /**
122
+ * ⛔ **WHICH AGENT REGISTERED THE HOOK — `--agent=<id>`, resolved in
123
+ * `index.ts`. NOT which agent produced the transcript.**
124
+ *
125
+ * ## The split, and it is load-bearing (`CR-204`, D177 §7)
126
+ *
127
+ * `agentId` answered two different questions until `CR-204`, and one of the
128
+ * two answers was routinely wrong. What each is derived from now:
129
+ *
130
+ * | use | derived from |
131
+ * |---|---|
132
+ * | wire `X-Agent` | ⛔ the CONTAINING ROOT (`agentForTranscript`) |
133
+ * | delegated-transcript discovery | ⛔ the CONTAINING ROOT — sub-agent LAYOUT is a property of the transcript |
134
+ * | the redaction profile | ⛔ the CONTAINING ROOT (`rulesForTranscript`, `CR-185`) |
135
+ * | ⛔ **hook budgets and timeouts** | ⛔ **THIS FIELD** |
136
+ *
137
+ * The budget is the one thing the flag is genuinely the oracle for: the clock
138
+ * belongs to the PROCESS THAT REGISTERED THE HOOK and is waiting on our exit,
139
+ * and it is armed before stdin is read — before any transcript path exists to
140
+ * ask a root about. The other three are properties of the FILE, and no install
141
+ * passes the flag anyway (`DEFAULT_AGENT_ID` is `claude-code`), so deriving
142
+ * them from it meant answering `claude-code` for everyone.
143
+ *
144
+ * ⛔ **HERE, and not on `HookInput`.** `HookInput` is parsed from stdin, which
145
+ * is the attacker-influenced input the confinement boundary exists to
146
+ * constrain; a selector read from there would be the rejected
147
+ * `transcript_path`-as-selector design wearing a different field name. This
148
+ * field arrives through the same injected seam as `home` and `nodeVersion`.
149
+ *
150
+ * The LITERAL, unnarrowed: an id we do not recognise falls back to the default
151
+ * dialect for BEHAVIOUR and is never coerced as a value (plan §D7). ⚠ That is
152
+ * also why it is a `string` and `SelectedAgentId` is not — the flag is not
153
+ * assignable to the value the wire is built from.
154
+ */
155
+ readonly agentId: string;
156
+ readonly readStdin: () => Promise<string>;
157
+ }
158
+ /**
159
+ * Install every contract guard, then run the body.
160
+ *
161
+ * Guards go in FIRST, before any work, because the paths they cover include
162
+ * "the body threw on its first line".
163
+ */
164
+ export declare function runHook(ctx: HookEnvironment): Promise<never>;
165
+ /**
166
+ * The wall clock this invocation gets.
167
+ *
168
+ * ⛔ **THE TIGHTEST BUDGET THE DIALECT REGISTERS, and the reason is the ORDER
169
+ * things happen in `runHook`** — the watchdog is armed BEFORE stdin is read, so
170
+ * the event name is not known yet and a per-event budget cannot be applied here
171
+ * without re-arming the guard. Taking the minimum is the direction that is safe
172
+ * to be wrong in: it can only finish early, never overrun a registered timeout
173
+ * and let the agent kill the process instead.
174
+ *
175
+ * ⚠ **Claude Code is unchanged, arithmetically** — its three events all carry
176
+ * `DEFAULT_HOOK_BUDGET_MS`, so the minimum IS that constant. What this costs is
177
+ * an agent whose events differ: it runs every event at its tightest one, which
178
+ * under-uses the wide ones. Stated rather than smuggled; widening it means
179
+ * re-arming the watchdog after the parse, which is its own change.
180
+ *
181
+ * `VIBECOMMIT_HOOK_TIMEOUT_MS` still wins outright, as it always has.
182
+ */
183
+ export declare function hookBudgetMs(env: NodeJS.ProcessEnv, agentId: string): number;
184
+ /**
185
+ * ⛔ **The redaction confinement ROOT SET — `D184 §3`.**
186
+ *
187
+ * ```
188
+ * isInsideAny([worktree, mainToplevel], candidate)
189
+ * ```
190
+ *
191
+ * ## Why it is a set, and why the common dir is not in it
192
+ *
193
+ * `D184` moved the consent key to the git common dir so that a session inside a
194
+ * `git worktree` is admitted at all. ⛔ **The confinement root MUST NOT follow
195
+ * it there.** `isInside` realpaths both sides and fail-closes anything it cannot
196
+ * place to *outside* — which here means REDACT — so a root at `<main>/.git`
197
+ * would judge every file of an external worktree to be outside the main clone
198
+ * and replace its `content`, `oldString`, `newString` and `structuredPatch` with
199
+ * markers. **The capture still succeeds. The turn counts still look right. The
200
+ * content is gone**, and a test asserting *"worktree sessions capture now"*
201
+ * passes on an empty payload.
202
+ *
203
+ * Two roots, because both are genuinely the user's own tree: the worktree the
204
+ * session is running in, and the main clone it was cut from — a builder in a
205
+ * worktree reads the main clone's files constantly.
206
+ *
207
+ * ## ⛔ The `paths.ts:487-489` prohibition, answered — `D184 §5`
208
+ *
209
+ * > *"`roots` comes from the REGISTRY. It must never be derived from input."*
210
+ *
211
+ * **That prohibition is about PROVENANCE, and both members have the right one.**
212
+ * The named hazard is *anything on hook stdin* — attacker-supplied. Neither of
213
+ * these is: `keys` can only be produced by `resolveProjectKeys`, which is git's
214
+ * own answer about the directory the session runs in; `mainToplevel` is git's
215
+ * answer about the repository **the consent gate already approved**, and it is
216
+ * content-validated by `mainWorkTree` above. The worktree root has exactly the
217
+ * same provenance and has been used this way since `CR-024d`.
218
+ *
219
+ * ⚠ **Provenance is the whole argument, so this code preserves it:** nothing
220
+ * here is read from stdin, the derivation is validated by content, and a failure
221
+ * yields no second root rather than a fallback. An empty set would refuse
222
+ * everything (`paths.ts:491-492`), which is the correct direction — but this
223
+ * function cannot return one, because `keys.worktree` is always a member.
224
+ *
225
+ * ⚠ The dedup is cosmetic. `isInsideAny` over a repeated root is a union with
226
+ * itself; it is dropped so that a main-clone session reads as the single root it
227
+ * has always had, and so a cell asserting "no second root" is asserting
228
+ * something.
229
+ */
230
+ export declare function confinementRoots(keys: ProjectKeys): readonly string[];
231
+ /**
232
+ * Read `[from, to)` of the transcript, REDACT it, and compress it.
233
+ *
234
+ * The path is already confined to the Claude Code transcript root by the caller
235
+ * (`/cso` finding 1) — this function must never be handed one that is not.
236
+ * Returns null on any failure, because a hook cannot report a read error.
237
+ *
238
+ * ## The one chokepoint
239
+ *
240
+ * This is the single place transcript bytes become a request body, which is why
241
+ * `CR-024d`'s redaction goes HERE and nowhere else — the same discipline
242
+ * `post.ts` applies to `expose()`: one call site, greppable, reviewable. Any
243
+ * future path that reads transcript bytes for upload must come through here or
244
+ * it silently bypasses the confidentiality boundary.
245
+ *
246
+ * `redactSpan` is length-preserving BY CONTRACT (see `redact.ts`), which the
247
+ * server's offset accounting depends on — so the compressed body still covers
248
+ * exactly `[from, to)`.
249
+ *
250
+ * ## ⛔ The SCRUBBER is chosen HERE, from the path, and never from `ctx.agentId`
251
+ *
252
+ * `CR-185`, D164 §5. This function knows the file it is about to read, so it is
253
+ * the one place that can answer *which registry root contains it* — and a
254
+ * caller therefore cannot pick the wrong profile for a file it hands over. The
255
+ * flag is not consulted: both no-flag install paths resolve to `claude-code`,
256
+ * and running Claude's key set over a Codex record redacts NOTHING.
257
+ *
258
+ * ⚠ **`home` and `env` are here for that lookup and nothing else.** They are
259
+ * passed rather than read from the process for the reason `paths.ts` states: a
260
+ * module that reads the real HOME cannot be tested without touching the
261
+ * developer's own transcripts.
262
+ *
263
+ * ## ⛔ `projectRoots` is a SET, and the consent key is not in it
264
+ *
265
+ * `D184 §3`. It was one string — the consented toplevel — while the consent key
266
+ * and the confinement boundary were the same value. They are not any more:
267
+ * `confinementRoots` above builds `[worktree, mainToplevel]` and the git common
268
+ * dir the consent gate approved is deliberately absent from it. A single-element
269
+ * set is the ordinary main-clone case and behaves exactly as the old parameter
270
+ * did.
271
+ *
272
+ * EXPORTED for `connect`'s test capture (`CR-025`), which reads a transcript on
273
+ * a path the hook never takes. It reuses this rather than opening one itself,
274
+ * because a second reader would bypass both the confinement and the redaction —
275
+ * exactly what the chokepoint note above warns about.
276
+ */
277
+ export declare function readSpan(path: string, from: number, to: number, projectRoots: readonly string[], home: string, env: NodeJS.ProcessEnv): Uint8Array | null;