@vibe-commit/capture 0.1.0 → 0.2.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.
Files changed (59) hide show
  1. package/dist/agent_hooks.d.ts +133 -0
  2. package/dist/agent_hooks.js +228 -0
  3. package/dist/agent_hooks.js.map +1 -0
  4. package/dist/agent_install.d.ts +79 -0
  5. package/dist/agent_install.js +108 -0
  6. package/dist/agent_install.js.map +1 -0
  7. package/dist/agents/claude_code.js +12 -1
  8. package/dist/agents/claude_code.js.map +1 -1
  9. package/dist/agents/codex_cli.js +21 -1
  10. package/dist/agents/codex_cli.js.map +1 -1
  11. package/dist/agents/cursor.js +32 -6
  12. package/dist/agents/cursor.js.map +1 -1
  13. package/dist/agents/registry.d.ts +39 -1
  14. package/dist/agents/registry.js +47 -1
  15. package/dist/agents/registry.js.map +1 -1
  16. package/dist/agents/types.d.ts +76 -0
  17. package/dist/agents/types.js.map +1 -1
  18. package/dist/commands/auth.d.ts +66 -0
  19. package/dist/commands/auth.js +119 -0
  20. package/dist/commands/auth.js.map +1 -0
  21. package/dist/commands/connect.js +83 -1
  22. package/dist/commands/connect.js.map +1 -1
  23. package/dist/commands/status.js +18 -12
  24. package/dist/commands/status.js.map +1 -1
  25. package/dist/copy/index.d.ts +1 -1
  26. package/dist/copy/index.js +1 -1
  27. package/dist/copy/index.js.map +1 -1
  28. package/dist/copy/strings.d.ts +102 -4
  29. package/dist/copy/strings.js +114 -4
  30. package/dist/copy/strings.js.map +1 -1
  31. package/dist/credential.d.ts +46 -0
  32. package/dist/credential.js +59 -1
  33. package/dist/credential.js.map +1 -1
  34. package/dist/hooks/entry.d.ts +12 -1
  35. package/dist/hooks/entry.js +157 -12
  36. package/dist/hooks/entry.js.map +1 -1
  37. package/dist/hooks/fire_lock.d.ts +90 -0
  38. package/dist/hooks/fire_lock.js +192 -0
  39. package/dist/hooks/fire_lock.js.map +1 -0
  40. package/dist/hooks/post_commit.js +8 -2
  41. package/dist/hooks/post_commit.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +53 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/install.d.ts +8 -9
  46. package/dist/install.js +61 -6
  47. package/dist/install.js.map +1 -1
  48. package/dist/paths.d.ts +24 -0
  49. package/dist/paths.js +38 -0
  50. package/dist/paths.js.map +1 -1
  51. package/dist/spool.d.ts +83 -1
  52. package/dist/spool.js +196 -6
  53. package/dist/spool.js.map +1 -1
  54. package/dist/term.d.ts +9 -0
  55. package/dist/term.js +14 -0
  56. package/dist/term.js.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +2 -2
@@ -17,15 +17,19 @@
17
17
  * ⚠ **What is DATA here and what is MEASURED is not the same set**, and the
18
18
  * difference is marked on each member below rather than left to a reader.
19
19
  */
20
- import { cursorTranscriptRoot, isCursorTranscript } from "../paths.js";
20
+ import { cursorHooksPath, cursorTranscriptRoot, isCursorTranscript } from "../paths.js";
21
21
  /**
22
22
  * ⛔ **UNMEASURED, AND ADOPTED AS A FLOOR RATHER THAN GUESSED.**
23
23
  *
24
- * The plan's `D5` — the exact `hook_event_name` strings Cursor sends, and the
25
- * timeouts its `~/.cursor/hooks.json` registers — is **founder-blocked and
26
- * gates `CR-195`**, the unit that writes that file. Nothing installs a Cursor
27
- * hook today, so no Cursor hook has ever fired and these numbers are currently
28
- * unreachable in production.
24
+ * ⚠ **`D5` HAD TWO HALVES AND ONLY ONE OF THEM IS ANSWERED (D205).** It asked
25
+ * for the exact `hook_event_name` strings Cursor sends *and* the timeouts its
26
+ * `~/.cursor/hooks.json` registers. The STRINGS are now measured — see
27
+ * `eventNames` below, read out of the shipped app bundle, which is what
28
+ * discharged `CR-195`'s block. ⛔ **The TIMEOUTS below are still not measured**,
29
+ * and are deliberately left as the floor they always were rather than quietly
30
+ * promoted on the strength of the other half. Nothing installs a Cursor hook
31
+ * today, so no Cursor hook has ever fired and these numbers remain unreachable
32
+ * in production.
29
33
  *
30
34
  * ⚠ **The direction of safety is what picks them.** Too WIDE and the agent
31
35
  * kills the process before this client's watchdog fires — which loses the
@@ -50,6 +54,28 @@ export const CURSOR = {
50
54
  // ⛔ THE GATE D177 §2 REQUIRES TO SHIP IN THIS UNIT — `.jsonl`, plus the
51
55
  // `<workspace>/agent-transcripts/` shape that puts §2's boundary back.
52
56
  admitsFile: isCursorTranscript,
57
+ // ⛔ **THE MEASUREMENT THAT DISCHARGED `D5`'s FIRST HALF — camelCase, and this
58
+ // is the defect `CR-195` exists to fix** (D205). Read out of the SHIPPED app
59
+ // bundle at `/Applications/Cursor.app/Contents/Resources/app`, whose `*.js`
60
+ // carry the event object as literals: seventeen events, of which
61
+ // `stop`/`preCompact`/`sessionEnd` are ours and `subagentStart`/`subagentStop`
62
+ // are the pair `delegatedTranscripts: "announce"` would want next.
63
+ //
64
+ // ⚠ **No live Cursor session was needed**, which is what `D5` had assumed and
65
+ // what kept this unit blocked. The bundle is the same artifact a user runs.
66
+ //
67
+ // ⛔ Against the client's `SessionEnd`, `sessionEnd` matched NOTHING — not an
68
+ // error, a `false` — so a Cursor session took no settle on its final turn and
69
+ // lost it permanently. That is the whole of the bug.
70
+ eventNames: { Stop: "stop", PreCompact: "preCompact", SessionEnd: "sessionEnd" },
71
+ // ⛔ `~/.cursor/hooks.json`, and the `cursor` SHAPE — the two are a pair.
72
+ // Read out of the shipped bundle's validator: a top-level numeric `version`
73
+ // is REQUIRED, entries are a FLAT array of hook scripts each needing a
74
+ // `command`, and an unrecognised event key is a validation ERROR rather than
75
+ // something ignored. The `claude` shape written here would be rejected for
76
+ // having no `command` — silently, registering nothing.
77
+ // ⚠ No env override: none has been measured, per `cursorTranscriptRoot`.
78
+ hookConfig: { path: (home) => cursorHooksPath(home), shape: "cursor" },
53
79
  events: {
54
80
  Stop: {
55
81
  registeredTimeoutMs: CURSOR_REGISTERED_MS,
@@ -1 +1 @@
1
- {"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/agents/cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGvE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,oBAAoB,GAAG,KAAK,CAAC;AACnC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAE/B,MAAM,CAAC,MAAM,MAAM,GAAsB;IACvC,EAAE,EAAE,QAAQ;IACZ,SAAS,EAAE,QAAQ;IACnB,8EAA8E;IAC9E,2EAA2E;IAC3E,6EAA6E;IAC7E,uDAAuD;IACvD,cAAc,EAAE,oBAAoB;IACpC,wEAAwE;IACxE,uEAAuE;IACvE,UAAU,EAAE,kBAAkB;IAC9B,MAAM,EAAE;QACN,IAAI,EAAE;YACJ,mBAAmB,EAAE,oBAAoB;YACzC,cAAc,EAAE,gBAAgB;SACjC;QACD,UAAU,EAAE;YACV,mBAAmB,EAAE,oBAAoB;YACzC,cAAc,EAAE,gBAAgB;SACjC;QACD,UAAU,EAAE;YACV,mBAAmB,EAAE,oBAAoB;YACzC,cAAc,EAAE,gBAAgB;SACjC;KACF;IACD,wEAAwE;IACxE,uDAAuD;IACvD,+EAA+E;IAC/E,+EAA+E;IAC/E,8EAA8E;IAC9E,8EAA8E;IAC9E,+DAA+D;IAC/D,oBAAoB,EAAE,UAAU;IAChC,SAAS,EAAE,QAAQ;IACnB,+EAA+E;IAC/E,6EAA6E;IAC7E,6EAA6E;IAC7E,6EAA6E;IAC7E,+EAA+E;IAC/E,yBAAyB;IACzB,gBAAgB,EAAE,GAAG,EAAE,CAAC,IAAI;IAC5B,8EAA8E;IAC9E,+EAA+E;IAC/E,4EAA4E;IAC5E,6EAA6E;IAC7E,0CAA0C;CAC3C,CAAC"}
1
+ {"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/agents/cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGxF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,oBAAoB,GAAG,KAAK,CAAC;AACnC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAE/B,MAAM,CAAC,MAAM,MAAM,GAAsB;IACvC,EAAE,EAAE,QAAQ;IACZ,SAAS,EAAE,QAAQ;IACnB,8EAA8E;IAC9E,2EAA2E;IAC3E,6EAA6E;IAC7E,uDAAuD;IACvD,cAAc,EAAE,oBAAoB;IACpC,wEAAwE;IACxE,uEAAuE;IACvE,UAAU,EAAE,kBAAkB;IAC9B,8EAA8E;IAC9E,6EAA6E;IAC7E,4EAA4E;IAC5E,iEAAiE;IACjE,+EAA+E;IAC/E,mEAAmE;IACnE,EAAE;IACF,8EAA8E;IAC9E,4EAA4E;IAC5E,EAAE;IACF,6EAA6E;IAC7E,8EAA8E;IAC9E,qDAAqD;IACrD,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE;IAChF,yEAAyE;IACzE,4EAA4E;IAC5E,uEAAuE;IACvE,6EAA6E;IAC7E,2EAA2E;IAC3E,uDAAuD;IACvD,yEAAyE;IACzE,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE;IACtE,MAAM,EAAE;QACN,IAAI,EAAE;YACJ,mBAAmB,EAAE,oBAAoB;YACzC,cAAc,EAAE,gBAAgB;SACjC;QACD,UAAU,EAAE;YACV,mBAAmB,EAAE,oBAAoB;YACzC,cAAc,EAAE,gBAAgB;SACjC;QACD,UAAU,EAAE;YACV,mBAAmB,EAAE,oBAAoB;YACzC,cAAc,EAAE,gBAAgB;SACjC;KACF;IACD,wEAAwE;IACxE,uDAAuD;IACvD,+EAA+E;IAC/E,+EAA+E;IAC/E,8EAA8E;IAC9E,8EAA8E;IAC9E,+DAA+D;IAC/D,oBAAoB,EAAE,UAAU;IAChC,SAAS,EAAE,QAAQ;IACnB,+EAA+E;IAC/E,6EAA6E;IAC7E,6EAA6E;IAC7E,6EAA6E;IAC7E,+EAA+E;IAC/E,yBAAyB;IACzB,gBAAgB,EAAE,GAAG,EAAE,CAAC,IAAI;IAC5B,8EAA8E;IAC9E,+EAA+E;IAC/E,4EAA4E;IAC5E,6EAA6E;IAC7E,0CAA0C;CAC3C,CAAC"}
@@ -26,7 +26,7 @@
26
26
  * SELECTION can pick a suboptimal redaction profile and can never widen the
27
27
  * boundary.
28
28
  */
29
- import { type AgentId, type SelectedAgentId, type TranscriptDialect } from "./types.js";
29
+ import { type AgentId, type HookEventName, type SelectedAgentId, type TranscriptDialect } from "./types.js";
30
30
  /**
31
31
  * Every dialect, in registration order.
32
32
  *
@@ -83,6 +83,44 @@ export declare function resolveAgentId(argv: readonly string[]): string;
83
83
  * the id, not the dialect.
84
84
  */
85
85
  export declare function dialectFor(agentId: string | undefined): TranscriptDialect;
86
+ /**
87
+ * ⛔ **WHICH OF THE THREE EVENTS IS THIS `hook_event_name`? — `CR-195`, D205.**
88
+ *
89
+ * Returns the canonical `HookEventName`, or `null` for a string that names none
90
+ * of them. `null` is a real answer and not a failure: Claude Code, Codex and
91
+ * Cursor between them fire far more events than the three this client registers,
92
+ * and a `SubagentStop` payload arriving here is ordinary rather than wrong.
93
+ *
94
+ * ## ⛔ The dialect's own table first, then a case-insensitive fallback
95
+ *
96
+ * The exact match is the answer whenever the agent spells events the way its
97
+ * dialect says it does. The fallback exists for the case that has already bitten
98
+ * this client once — ⚠ **a spelling nobody anticipated** — and the asymmetry is
99
+ * what justifies it:
100
+ *
101
+ * - **failing to recognise `SessionEnd` costs the final turn, permanently.**
102
+ * That event has no next invocation to pick up what was missed.
103
+ * - **recognising one spuriously costs 150 ms of reserve and one settle.**
104
+ *
105
+ * Those are not the same size, so this resolves in the direction that captures.
106
+ * ⛔ It is D164 §D7's ruling applied to events rather than to agent ids — *"an
107
+ * unknown id falls back; it never stops capture … a misconfigured hook that
108
+ * captures NOTHING is a worse outcome than one that captures conservatively"* —
109
+ * and it is the same shape: a BEHAVIOUR fallback that rewrites nothing on the
110
+ * wire.
111
+ *
112
+ * ⚠ **The fallback can only ever ADD recognition, never redirect it.** It runs
113
+ * only after the exact match has already failed, so a dialect's own spelling
114
+ * always wins; and `HOOK_EVENTS` is unambiguous case-insensitively, so no
115
+ * canonical event can be resolved as a different canonical event.
116
+ * `test/dialect-events.test.ts` pins both directions, including the distinctness
117
+ * that makes the second claim true rather than merely currently true.
118
+ *
119
+ * ⚠ **NOT a widening of the confinement boundary and not adjacent to one.** The
120
+ * value read here decides how long to wait and whether to settle; it never
121
+ * reaches a path that decides what may be READ. That stays `transcriptRoots`.
122
+ */
123
+ export declare function canonicalHookEvent(dialect: TranscriptDialect, wireName: string): HookEventName | null;
86
124
  /**
87
125
  * Every transcript root, deduped — ⛔ **the confinement boundary, computed from
88
126
  * the REGISTRY and never from input.**
@@ -30,7 +30,7 @@ import { isInside, isInsideAny } from "../paths.js";
30
30
  import { CLAUDE_CODE } from "./claude_code.js";
31
31
  import { CODEX_CLI } from "./codex_cli.js";
32
32
  import { CURSOR } from "./cursor.js";
33
- import { UNKNOWN_AGENT_ID, } from "./types.js";
33
+ import { HOOK_EVENTS, UNKNOWN_AGENT_ID, } from "./types.js";
34
34
  /**
35
35
  * Every dialect, in registration order.
36
36
  *
@@ -100,6 +100,52 @@ export function resolveAgentId(argv) {
100
100
  export function dialectFor(agentId) {
101
101
  return DIALECTS.find((dialect) => dialect.id === agentId) ?? CLAUDE_CODE;
102
102
  }
103
+ /**
104
+ * ⛔ **WHICH OF THE THREE EVENTS IS THIS `hook_event_name`? — `CR-195`, D205.**
105
+ *
106
+ * Returns the canonical `HookEventName`, or `null` for a string that names none
107
+ * of them. `null` is a real answer and not a failure: Claude Code, Codex and
108
+ * Cursor between them fire far more events than the three this client registers,
109
+ * and a `SubagentStop` payload arriving here is ordinary rather than wrong.
110
+ *
111
+ * ## ⛔ The dialect's own table first, then a case-insensitive fallback
112
+ *
113
+ * The exact match is the answer whenever the agent spells events the way its
114
+ * dialect says it does. The fallback exists for the case that has already bitten
115
+ * this client once — ⚠ **a spelling nobody anticipated** — and the asymmetry is
116
+ * what justifies it:
117
+ *
118
+ * - **failing to recognise `SessionEnd` costs the final turn, permanently.**
119
+ * That event has no next invocation to pick up what was missed.
120
+ * - **recognising one spuriously costs 150 ms of reserve and one settle.**
121
+ *
122
+ * Those are not the same size, so this resolves in the direction that captures.
123
+ * ⛔ It is D164 §D7's ruling applied to events rather than to agent ids — *"an
124
+ * unknown id falls back; it never stops capture … a misconfigured hook that
125
+ * captures NOTHING is a worse outcome than one that captures conservatively"* —
126
+ * and it is the same shape: a BEHAVIOUR fallback that rewrites nothing on the
127
+ * wire.
128
+ *
129
+ * ⚠ **The fallback can only ever ADD recognition, never redirect it.** It runs
130
+ * only after the exact match has already failed, so a dialect's own spelling
131
+ * always wins; and `HOOK_EVENTS` is unambiguous case-insensitively, so no
132
+ * canonical event can be resolved as a different canonical event.
133
+ * `test/dialect-events.test.ts` pins both directions, including the distinctness
134
+ * that makes the second claim true rather than merely currently true.
135
+ *
136
+ * ⚠ **NOT a widening of the confinement boundary and not adjacent to one.** The
137
+ * value read here decides how long to wait and whether to settle; it never
138
+ * reaches a path that decides what may be READ. That stays `transcriptRoots`.
139
+ */
140
+ export function canonicalHookEvent(dialect, wireName) {
141
+ if (wireName === "")
142
+ return null;
143
+ const exact = HOOK_EVENTS.find((event) => dialect.eventNames[event] === wireName);
144
+ if (exact !== undefined)
145
+ return exact;
146
+ const folded = wireName.toLowerCase();
147
+ return HOOK_EVENTS.find((event) => event.toLowerCase() === folded) ?? null;
148
+ }
103
149
  /**
104
150
  * Every transcript root, deduped — ⛔ **the confinement boundary, computed from
105
151
  * the REGISTRY and never from input.**
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/agents/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EACL,gBAAgB,GAIjB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAiC,CAAC,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AAEvF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAY,aAAa,CAAC;AAEvD,uEAAuE;AACvE,MAAM,UAAU,GAAG,UAAU,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,cAAc,CAAC,IAAuB;IACpD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,SAAS;QAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,EAAE;YAAE,OAAO,KAAK,CAAC;IACjC,CAAC;IACD,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,UAAU,CAAC,OAA2B;IACpD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,WAAW,CAAC;AAC3E,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,GAAsB;IAClE,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,GAAsB,EACtB,cAAsB;IAEtB,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,cAAc,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3E,MAAM,OAAO,GAAG,UAAU,CAAC,kBAAkB,CAAC,IAAI,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;IAC1E,OAAO,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,cAAc,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,GAAsB,EACtB,cAAsB;IAEtB,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAC1C,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,cAAc,CAAC,CAC5D,CAAC;IACF,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC;AACjE,CAAC"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/agents/registry.ts"],"names":[],"mappings":"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"}
@@ -44,6 +44,7 @@
44
44
  * MEASUREMENT un-excluded it — 113 files under Cursor's root, exactly one of
45
45
  * them a transcript.
46
46
  */
47
+ import type { HookDocShape } from "../agent_hooks.js";
47
48
  /**
48
49
  * The canonical agent ids — ⛔ **D164 §1 owns this list; this file CITES it.**
49
50
  *
@@ -159,6 +160,19 @@ export interface EventBudget {
159
160
  /** What this client gives itself. Strictly the smaller of the two. */
160
161
  readonly clientBudgetMs: number;
161
162
  }
163
+ /**
164
+ * One agent's hook-config file, and the document shape it validates.
165
+ *
166
+ * `path` takes `home` and `env` for the reason every other function member on
167
+ * this interface does: a module that reads the real HOME cannot be tested
168
+ * without writing into the developer's own agent configuration — and here that
169
+ * is not a tidiness argument, it is the difference between a test suite and a
170
+ * program that edits `~/.claude/settings.json` when you run `npm test`.
171
+ */
172
+ export interface AgentHookConfig {
173
+ readonly path: (home: string, env: NodeJS.ProcessEnv) => string;
174
+ readonly shape: HookDocShape;
175
+ }
162
176
  /** Where `connect` found the session running right now. */
163
177
  export interface LocatedTranscript {
164
178
  readonly path: string;
@@ -228,6 +242,68 @@ export interface TranscriptDialect {
228
242
  * That is D177 §7's direction test: fail-closed both ways round.
229
243
  */
230
244
  readonly admitsFile: (root: string, path: string) => boolean;
245
+ /**
246
+ * ⛔ **WHAT THIS AGENT CALLS THE THREE EVENTS — `CR-195`, D205.**
247
+ *
248
+ * The canonical name is the KEY and the agent's own spelling is the VALUE, in
249
+ * that direction on purpose: the key set is `HookEventName`, so the compiler
250
+ * makes a dialect answer for all three and `EventBudget`'s totality argument
251
+ * above applies here unchanged.
252
+ *
253
+ * ## ⛔ Why this exists at all, and why it is not a `HookEventName` widening
254
+ *
255
+ * `D5` predicted the failure and `CR-195` measured it. Cursor's events are
256
+ * **camelCase** — `stop`, `preCompact`, `sessionEnd` — against this client's
257
+ * `Stop`, `PreCompact`, `SessionEnd`. `parseHookInput` hands back whatever
258
+ * string it was given, so `input.eventName === "SessionEnd"` against a Cursor
259
+ * payload is **not an error, it is `false`**: no settle on the final turn
260
+ * (lost permanently — that event has no next invocation) and no gap-stamp
261
+ * reserve. Nothing raises and the hook still exits 0.
262
+ *
263
+ * ⚠ **Widening `HookEventName` was the obvious fix and it is the wrong one.**
264
+ * The three map 1:1, so a rename belongs in a per-dialect table; widening the
265
+ * union would make `Readonly<Record<HookEventName, EventBudget>>` non-total
266
+ * over the real event set and force every dialect to declare budgets for
267
+ * events it can never receive.
268
+ *
269
+ * ## ⚠ MEASURED per dialect, and the two measurements differ in kind
270
+ *
271
+ * - **Cursor** — read out of the shipped app bundle's `*.js`, which carry the
272
+ * event object as literals. Seventeen events; three are ours.
273
+ * - **Codex** — read out of the native binary's embedded JSON schema, where
274
+ * each `hook_event_name` is a `const`. PascalCase, so this dialect's table
275
+ * is IDENTITY. ⛔ The binary also carries a snake_case enum of the same
276
+ * events; a differential run against throwaway `CODEX_HOME`s proved that
277
+ * spelling is **not** what `hooks.json` accepts — `SessionEnd` is
278
+ * recognised, `session_end` is silently ignored. See
279
+ * `test/dialect-events.test.ts`, which records the method.
280
+ */
281
+ readonly eventNames: Readonly<Record<HookEventName, string>>;
282
+ /**
283
+ * ⛔ **THE FILE `connect` WRITES TO, AND WHICH DOCUMENT SHAPE IT USES —
284
+ * `CR-195`/U4b, D205.**
285
+ *
286
+ * ⚠ **Hook-config knowledge is not new on this interface**, which is the
287
+ * argument for putting it here rather than in a parallel table keyed by agent
288
+ * id. `events.registeredTimeoutMs` is already defined as *"what the agent's
289
+ * hook config registers"* — this member is what makes that value WRITABLE
290
+ * instead of a mirror of a file maintained by hand in another repository. A
291
+ * second table would be a second source that can disagree with the first,
292
+ * which is the `plan §T1` objection.
293
+ *
294
+ * ⛔ **TWO SHAPES ACROSS THREE AGENTS, and they are not interchangeable.**
295
+ * Claude Code and Codex share the nested form; Cursor requires a top-level
296
+ * numeric `version` and a FLAT entry array, and treats an unrecognised event
297
+ * key as a validation error. Writing the wrong one does not fail loudly — it
298
+ * produces a file that parses, registers nothing, and reports success.
299
+ *
300
+ * ⚠ **`timeoutSec` is NOT a member here, deliberately.** It is derived from
301
+ * `events[event].registeredTimeoutMs`, so the number this client writes into
302
+ * an agent's config and the number it checks its own watchdog against cannot
303
+ * drift apart. For Codex that derivation lands on 3 s for `SessionEnd`, which
304
+ * is exactly the cap Codex silently clamps to — measured, not inferred.
305
+ */
306
+ readonly hookConfig: AgentHookConfig;
231
307
  /** Per-event clocks. See `EventBudget` for the relation that binds them. */
232
308
  readonly events: Readonly<Record<HookEventName, EventBudget>>;
233
309
  readonly delegatedTranscripts: DelegatedTranscripts;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AA0BH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,SAAS,CAAC;AA6D1C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,YAAY,CAAU,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AA4BH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,SAAS,CAAC;AA6D1C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,YAAY,CAAU,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `vibecommit auth` — save this machine's ingest credential. `CR-216/U2`, D206.
3
+ *
4
+ * ## The defect this closes
5
+ *
6
+ * `~/.vibecommit/credentials.json` was a READ PATH WITH NO PRODUCER. `paths.ts`
7
+ * named it, `credential.ts` parsed it, `readSecretFile` policed its mode and
8
+ * `TROUBLESHOOTING.chmodCredentials` told the user to `chmod 600` it — and
9
+ * nothing in the package ever wrote it. The only credential that reached a hook
10
+ * came from `VIBECOMMIT_TOKEN`, the escape hatch `credential.ts:28` documents as
11
+ * being for headless and CI use. So a `git commit` from any ordinary shell ran
12
+ * the post-commit hook with no credential and bound nothing, and because the
13
+ * hook exit contract makes a missing credential SILENT by design — correctly, so
14
+ * capture never derails a developer's turn — the user got transcript capture,
15
+ * zero commit edges, and no error anywhere to explain it.
16
+ *
17
+ * ## Why this is a verb and not a beat inside `connect`
18
+ *
19
+ * The obvious fix was "`connect` already holds a credential, so persist it". It
20
+ * is circular. `connect`'s `credentialBeat` calls the same `loadCredential`
21
+ * every other surface calls, so the credential it holds came from
22
+ * `VIBECOMMIT_TOKEN` or from the file — there is no third source. Persisting it
23
+ * would mean the file could only ever be seeded from the variable it exists to
24
+ * replace, and on the fresh machine that has the defect there is nothing to
25
+ * persist at all. The plaintext has to enter the machine somewhere explicit, and
26
+ * this verb is that place.
27
+ *
28
+ * ## ⛔ Why the credential may not be an argument
29
+ *
30
+ * `vibecommit auth <token>` is the shape everyone reaches for first, and it is
31
+ * the wrong one: `argv` lands in the shell's history file and, on most systems,
32
+ * in `/proc/<pid>/cmdline` for any local process to read while the command runs.
33
+ * `credential.ts` spends a whole module closing four NARROWER leaks — template
34
+ * interpolation, `JSON.stringify`, `console.error(obj)`, an unhandled rejection
35
+ * — so accepting the secret through a wider channel than all of them would make
36
+ * that discipline theatre. It is refused loudly rather than quietly ignored,
37
+ * because a user who typed it needs to be told it is now in their history.
38
+ *
39
+ * ## What this verb is NOT
40
+ *
41
+ * It is not sign-in, and it does not mint. `mintIngestCredential` exists in
42
+ * `vibecommit-mcp` with no route in front of it, so on this side of the wire the
43
+ * plaintext still comes from the web app's `POST /api/ingest-credentials`, shown
44
+ * once, and the human carries it here. Putting a route in front of the minter is
45
+ * the PKCE work `connect.ts` defers; this verb makes the paste path first-class,
46
+ * it does not remove the paste.
47
+ *
48
+ * @provenance vibecommit-mcp src/oauth/ingest_credential.ts — no route mints one, re-verified
49
+ * @provenance vibecommit-web app/api/ingest-credentials/route.ts — the only producer, verified
50
+ */
51
+ import type { CommandContext } from "./context.js";
52
+ export interface AuthDeps {
53
+ /**
54
+ * Read the secret from a terminal WITHOUT ECHOING IT. Injected for the same
55
+ * reason `why` injects its reader: the suite must drive every arm, and a test
56
+ * cannot type into a TTY.
57
+ *
58
+ * ⛔ NOT `ctx.ask`. That is the consent prompt — it echoes, which is right for
59
+ * `y/N` and wrong for a long-lived secret that would then sit in the user's
60
+ * scrollback for anyone who scrolls up or screenshots the window.
61
+ */
62
+ readonly readSecret?: (prompt: string) => Promise<string>;
63
+ /** Read the whole of stdin. The pipe path, and the only path on a non-TTY. */
64
+ readonly readStdin?: () => Promise<string>;
65
+ }
66
+ export declare function auth(ctx: CommandContext, argv: readonly string[], deps?: AuthDeps): Promise<number>;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `vibecommit auth` — save this machine's ingest credential. `CR-216/U2`, D206.
3
+ *
4
+ * ## The defect this closes
5
+ *
6
+ * `~/.vibecommit/credentials.json` was a READ PATH WITH NO PRODUCER. `paths.ts`
7
+ * named it, `credential.ts` parsed it, `readSecretFile` policed its mode and
8
+ * `TROUBLESHOOTING.chmodCredentials` told the user to `chmod 600` it — and
9
+ * nothing in the package ever wrote it. The only credential that reached a hook
10
+ * came from `VIBECOMMIT_TOKEN`, the escape hatch `credential.ts:28` documents as
11
+ * being for headless and CI use. So a `git commit` from any ordinary shell ran
12
+ * the post-commit hook with no credential and bound nothing, and because the
13
+ * hook exit contract makes a missing credential SILENT by design — correctly, so
14
+ * capture never derails a developer's turn — the user got transcript capture,
15
+ * zero commit edges, and no error anywhere to explain it.
16
+ *
17
+ * ## Why this is a verb and not a beat inside `connect`
18
+ *
19
+ * The obvious fix was "`connect` already holds a credential, so persist it". It
20
+ * is circular. `connect`'s `credentialBeat` calls the same `loadCredential`
21
+ * every other surface calls, so the credential it holds came from
22
+ * `VIBECOMMIT_TOKEN` or from the file — there is no third source. Persisting it
23
+ * would mean the file could only ever be seeded from the variable it exists to
24
+ * replace, and on the fresh machine that has the defect there is nothing to
25
+ * persist at all. The plaintext has to enter the machine somewhere explicit, and
26
+ * this verb is that place.
27
+ *
28
+ * ## ⛔ Why the credential may not be an argument
29
+ *
30
+ * `vibecommit auth <token>` is the shape everyone reaches for first, and it is
31
+ * the wrong one: `argv` lands in the shell's history file and, on most systems,
32
+ * in `/proc/<pid>/cmdline` for any local process to read while the command runs.
33
+ * `credential.ts` spends a whole module closing four NARROWER leaks — template
34
+ * interpolation, `JSON.stringify`, `console.error(obj)`, an unhandled rejection
35
+ * — so accepting the secret through a wider channel than all of them would make
36
+ * that discipline theatre. It is refused loudly rather than quietly ignored,
37
+ * because a user who typed it needs to be told it is now in their history.
38
+ *
39
+ * ## What this verb is NOT
40
+ *
41
+ * It is not sign-in, and it does not mint. `mintIngestCredential` exists in
42
+ * `vibecommit-mcp` with no route in front of it, so on this side of the wire the
43
+ * plaintext still comes from the web app's `POST /api/ingest-credentials`, shown
44
+ * once, and the human carries it here. Putting a route in front of the minter is
45
+ * the PKCE work `connect.ts` defers; this verb makes the paste path first-class,
46
+ * it does not remove the paste.
47
+ *
48
+ * @provenance vibecommit-mcp src/oauth/ingest_credential.ts — no route mints one, re-verified
49
+ * @provenance vibecommit-web app/api/ingest-credentials/route.ts — the only producer, verified
50
+ */
51
+ import { AUTH, URLS } from "../copy/index.js";
52
+ import { writeLines } from "./context.js";
53
+ import { EXIT } from "../exit.js";
54
+ import { INGEST_TOKEN_PREFIX, IngestCredential, saveCredential } from "../credential.js";
55
+ import { credentialsPath } from "../paths.js";
56
+ import { renderErrorBlock, tildePath } from "../term.js";
57
+ /**
58
+ * `--stdin` forces the pipe even on a terminal — the CI shape, where a job may
59
+ * well have a TTY attached and must not start prompting because of it.
60
+ */
61
+ const STDIN_FLAG = "--stdin";
62
+ export async function auth(ctx, argv, deps = {}) {
63
+ // ⛔ THE ARGV REFUSAL COMES FIRST, before stdin is read, resumed or opened.
64
+ // Anything else would mean the secret in `argv` had already been accepted by
65
+ // the time we objected to it.
66
+ const positional = argv.filter((arg) => !arg.startsWith("-"));
67
+ if (positional.length > 0) {
68
+ writeLines(ctx.stderr, renderErrorBlock({
69
+ kind: "bad",
70
+ what: AUTH.argvWhat,
71
+ why: [AUTH.argvWhy],
72
+ fixLabel: AUTH.argvFix,
73
+ fixes: [AUTH.argvFixPrompt, AUTH.argvFixStdin],
74
+ }, ctx.colour));
75
+ return EXIT.usage;
76
+ }
77
+ const piped = argv.includes(STDIN_FLAG) || !ctx.stdinIsTty;
78
+ const raw = piped
79
+ ? await (deps.readStdin ?? (() => Promise.resolve("")))()
80
+ : await (deps.readSecret ?? (() => Promise.resolve("")))(AUTH.prompt);
81
+ // One `trim` for both lanes. A paste carries a trailing newline and a pipe may
82
+ // carry `\r\n`; neither is part of the credential, and an untrimmed byte would
83
+ // fail the prefix check further down for a reason the user cannot see.
84
+ const secret = raw.trim();
85
+ if (secret === "") {
86
+ writeLines(ctx.stderr, renderErrorBlock({
87
+ kind: "bad",
88
+ what: AUTH.emptyWhat,
89
+ why: [AUTH.emptyWhy],
90
+ fixLabel: AUTH.emptyFix,
91
+ fixes: [URLS.settings],
92
+ }, ctx.colour));
93
+ return EXIT.failure;
94
+ }
95
+ // The class check happens HERE as well as at the write boundary, and the
96
+ // duplication is deliberate: this one renders a fix for a human who pasted the
97
+ // wrong string, and `saveCredential`'s throws for a caller who should not have
98
+ // asked. Neither is the other's fallback.
99
+ if (!secret.startsWith(INGEST_TOKEN_PREFIX)) {
100
+ writeLines(ctx.stderr, renderErrorBlock({
101
+ kind: "bad",
102
+ what: AUTH.wrongClassWhat,
103
+ why: [AUTH.wrongClassWhy],
104
+ fixLabel: AUTH.wrongClassFix,
105
+ fixes: [URLS.settings],
106
+ }, ctx.colour));
107
+ return EXIT.failure;
108
+ }
109
+ saveCredential(ctx.home, new IngestCredential(secret, "file"));
110
+ writeLines(ctx.stdout, renderErrorBlock({
111
+ kind: "ok",
112
+ what: AUTH.savedWhat,
113
+ why: [AUTH.savedWhy(tildePath(credentialsPath(ctx.home), ctx.home))],
114
+ fixLabel: AUTH.nextLabel,
115
+ fixes: [AUTH.nextCommand],
116
+ }, ctx.colour));
117
+ return EXIT.ok;
118
+ }
119
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"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"}
@@ -22,13 +22,14 @@
22
22
  *
23
23
  * @provenance vibecommit-mcp src/oauth/ingest_credential.ts — no route mints one, verified
24
24
  */
25
- import { CONNECT, COMMANDS, COMMIT_HOOK, ERRORS, HELP, PATH_CLASH, RUNTIME, SIGNIN, URLS, } from "../copy/index.js";
25
+ import { CONNECT, COMMANDS, AGENT_HOOKS, COMMIT_HOOK, ERRORS, HELP, PATH_CLASH, RUNTIME, SIGNIN, URLS, } from "../copy/index.js";
26
26
  import { dialectFor } from "../agents/registry.js";
27
27
  import { grantProject, isAffirmative, isProjectAllowed } from "../consent.js";
28
28
  import { loadCredential } from "../credential.js";
29
29
  import { EXIT } from "../exit.js";
30
30
  import { resolveRepoSlug } from "../git.js";
31
31
  import { confinementRoots, readSpan, DEFAULT_HOOK_BUDGET_MS } from "../hooks/entry.js";
32
+ import { installAgentHooks } from "../agent_install.js";
32
33
  import { classifyInstall, installPostCommitHook } from "../install.js";
33
34
  import { mcpUrl } from "../oauth/discovery.js";
34
35
  import { loadSession } from "../oauth/session.js";
@@ -359,8 +360,89 @@ async function captureBeat(ctx, keys, credential) {
359
360
  // success, which is the only place the PER-CLONE cost can be stated at a
360
361
  // moment the user can act on it.
361
362
  commitHookBeat(ctx, projectKey);
363
+ // ⛔ SESSION CAPTURE IS REGISTERED AND REPORTED HERE (`CR-195`/U4b). This is
364
+ // the beat `/install` has been describing since before it existed — until it,
365
+ // `connect` wrote GIT hooks only and the npm path gave a user no session
366
+ // capture at all. It runs AFTER the commit beat because the two are
367
+ // independent and the commit one is per-clone: reporting the per-clone thing
368
+ // beside the global one, in that order, is what makes the scope difference
369
+ // legible rather than a detail in a sentence.
370
+ agentHookBeat(ctx);
362
371
  return pathClash(ctx);
363
372
  }
373
+ /**
374
+ * Register capture with every agent on this machine and SAY WHAT HAPPENED.
375
+ *
376
+ * ⚠ **Never changes the exit code**, for `commitHookBeat`'s reason: an agent
377
+ * whose config we could not parse does not make the connection fail, and
378
+ * exiting non-zero would tell the user their whole connect failed when one file
379
+ * of three was unreadable.
380
+ *
381
+ * ⛔ **EVERY OUTCOME PRINTS THE PATH.** This is a write OUTSIDE the repository,
382
+ * into a file shared by every project — the one new responsibility `CR-195`
383
+ * names — so "we configured your editor" is the shape this must not take.
384
+ */
385
+ function agentHookBeat(ctx) {
386
+ const outcomes = installAgentHooks(ctx.home, ctx.env, ctx.selfPath);
387
+ for (const outcome of outcomes) {
388
+ // Absent agents say nothing at all. A line per tool the user does not have
389
+ // is the noise DX3's one-line rule exists to avoid, and it would bury the
390
+ // agents that WERE wired under a list of ones that were not.
391
+ if (outcome.install === null)
392
+ continue;
393
+ const agent = AGENT_LABELS[outcome.agentId];
394
+ // ⛔ TRUNCATED, exactly as `commitHookBeat` truncates the repo path. DESIGN.md
395
+ // §13.4 caps every line at 80 columns and `wrap` cannot break a path — it has
396
+ // no spaces — so an untruncated `~/.claude/settings.json` under a long temp
397
+ // or home prefix silently blows the width contract. `test/connect-ending.ts`
398
+ // caught this at 96 columns; the elision keeps the filename, which is the
399
+ // half that identifies the file.
400
+ const path = truncatePath(outcome.path, 56);
401
+ if (outcome.install.kind === "refused" || outcome.install.kind === "failed") {
402
+ const refused = outcome.install.kind === "refused";
403
+ writeLines(ctx.stdout, [
404
+ "",
405
+ ...renderErrorBlock({
406
+ kind: "warn",
407
+ what: refused
408
+ ? AGENT_HOOKS.refusedWhat(agent)
409
+ : AGENT_HOOKS.failedWhat(agent),
410
+ why: [
411
+ refused
412
+ ? AGENT_HOOKS.refusedWhy(path, outcome.install.why)
413
+ : AGENT_HOOKS.failedWhy(path, outcome.install.why),
414
+ ],
415
+ ...(refused
416
+ ? {
417
+ fixLabel: AGENT_HOOKS.refusedFixLabel,
418
+ fixes: [AGENT_HOOKS.refusedFix(path)],
419
+ }
420
+ : {}),
421
+ }, ctx.colour),
422
+ ]);
423
+ continue;
424
+ }
425
+ const line = outcome.install.kind === "installed"
426
+ ? AGENT_HOOKS.installed(agent, path)
427
+ : AGENT_HOOKS.already(agent, path);
428
+ writeLines(ctx.stdout, [
429
+ "",
430
+ ...wrap(line, 2),
431
+ ...wrap(AGENT_HOOKS.kept(agent), 2),
432
+ // ⛔ Codex only, and it is not optional politeness: Codex ships the literal
433
+ // option "Continue without trusting (hooks won't run)", so the file we
434
+ // just wrote does NOT run until the user trusts it. Printing `installed`
435
+ // without this would be a true-sounding claim that is not yet true.
436
+ ...(outcome.agentId === "codex-cli" ? wrap(AGENT_HOOKS.codexTrust, 2) : []),
437
+ ]);
438
+ }
439
+ }
440
+ /** What each agent is called in prose. The ids are wire values, not labels. */
441
+ const AGENT_LABELS = {
442
+ "claude-code": "Claude Code",
443
+ "codex-cli": "Codex CLI",
444
+ cursor: "Cursor",
445
+ };
364
446
  /**
365
447
  * Install the `post-commit` hook for this clone and SAY WHAT HAPPENED.
366
448
  *