@vibe-commit/capture 0.1.0 → 0.2.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/dist/agent_hooks.d.ts +133 -0
- package/dist/agent_hooks.js +228 -0
- package/dist/agent_hooks.js.map +1 -0
- package/dist/agent_install.d.ts +79 -0
- package/dist/agent_install.js +108 -0
- package/dist/agent_install.js.map +1 -0
- package/dist/agents/claude_code.js +12 -1
- package/dist/agents/claude_code.js.map +1 -1
- package/dist/agents/codex_cli.js +21 -1
- package/dist/agents/codex_cli.js.map +1 -1
- package/dist/agents/cursor.js +32 -6
- package/dist/agents/cursor.js.map +1 -1
- package/dist/agents/registry.d.ts +39 -1
- package/dist/agents/registry.js +47 -1
- package/dist/agents/registry.js.map +1 -1
- package/dist/agents/types.d.ts +76 -0
- package/dist/agents/types.js.map +1 -1
- package/dist/commands/auth.d.ts +66 -0
- package/dist/commands/auth.js +119 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/connect.js +83 -1
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/status.js +18 -12
- package/dist/commands/status.js.map +1 -1
- package/dist/copy/index.d.ts +1 -1
- package/dist/copy/index.js +1 -1
- package/dist/copy/index.js.map +1 -1
- package/dist/copy/strings.d.ts +102 -4
- package/dist/copy/strings.js +114 -4
- package/dist/copy/strings.js.map +1 -1
- package/dist/credential.d.ts +46 -0
- package/dist/credential.js +59 -1
- package/dist/credential.js.map +1 -1
- package/dist/hooks/entry.d.ts +12 -1
- package/dist/hooks/entry.js +145 -11
- package/dist/hooks/entry.js.map +1 -1
- package/dist/hooks/fire_lock.d.ts +90 -0
- package/dist/hooks/fire_lock.js +192 -0
- package/dist/hooks/fire_lock.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +53 -1
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +8 -9
- package/dist/install.js +61 -6
- package/dist/install.js.map +1 -1
- package/dist/paths.d.ts +24 -0
- package/dist/paths.js +38 -0
- package/dist/paths.js.map +1 -1
- package/dist/term.d.ts +9 -0
- package/dist/term.js +14 -0
- package/dist/term.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/agents/cursor.js
CHANGED
|
@@ -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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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;
|
|
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.**
|
package/dist/agents/registry.js
CHANGED
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/agents/registry.ts"],"names":[],"mappings":"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"}
|
package/dist/agents/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/agents/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;
|
|
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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"}
|
package/dist/commands/connect.js
CHANGED
|
@@ -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
|
*
|