@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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registering our hook in an AGENT's own config — `CR-195`, D205.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ **THIS IS NOT `install.ts`'s CHAINING, AND THE DIFFERENCE IS THE CONTAINER,
|
|
5
|
+
* NOT THE RULE.** The rule is the same one D154 settled for git hooks — *never
|
|
6
|
+
* clobber; whatever was there keeps working*. The MECHANISM cannot be the same:
|
|
7
|
+
*
|
|
8
|
+
* - `.git/hooks/post-commit` is a **single-slot executable file**. One path,
|
|
9
|
+
* one script, no way for two owners to coexist — so `writeHook` renames the
|
|
10
|
+
* incumbent aside and invokes it explicitly. That is the only move available.
|
|
11
|
+
* - `~/.claude/settings.json` is a **multi-slot document**, and `hooks.Stop`
|
|
12
|
+
* is an ARRAY that already expresses coexistence natively.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ **So renaming here would be catastrophic rather than conservative.** That
|
|
15
|
+
* file holds the user's model, permissions, enabled plugins, marketplaces and
|
|
16
|
+
* theme; moving it aside to write our own would delete all of it, and on a
|
|
17
|
+
* machine with an existing `Stop` hook it would fire on the very first
|
|
18
|
+
* `connect`. The faithful translation of *never clobber* into this container is
|
|
19
|
+
* **append into the array and leave every other byte alone**.
|
|
20
|
+
*
|
|
21
|
+
* ## The three rules, and what each one is defending against
|
|
22
|
+
*
|
|
23
|
+
* 1. **APPEND, never replace.** A foreign entry is never removed, rewritten or
|
|
24
|
+
* reordered. Ours goes last.
|
|
25
|
+
* 2. **IDEMPOTENT via a marker.** JSON has nowhere to put a comment, so ours
|
|
26
|
+
* carries `_vibecommit`. Without it, `connect` run twice appends twice and
|
|
27
|
+
* the hook fires twice per event — two processes racing on one session
|
|
28
|
+
* state file. ⚠ The marker field is TOLERATED by all three readers, and
|
|
29
|
+
* that is measured rather than assumed: Codex parses and clamps with the
|
|
30
|
+
* field present on the inner and outer object alike, and Cursor's validator
|
|
31
|
+
* checks only known members (`HWu`/`jWu`/`$Wu` in its bundle).
|
|
32
|
+
* 3. ⛔ **REFUSE on unparseable, never overwrite.** A file we could not read is
|
|
33
|
+
* a file whose contents we cannot preserve. Overwriting it would be the
|
|
34
|
+
* destructive outcome wearing a success message — the exact defect class
|
|
35
|
+
* this whole unit exists to delete. Fail-closed, the same direction as
|
|
36
|
+
* `install.ts`'s `core.hooksPath` refusal.
|
|
37
|
+
*
|
|
38
|
+
* ## ⛔ TWO DOCUMENT SHAPES, MEASURED — they are NOT interchangeable
|
|
39
|
+
*
|
|
40
|
+
* The brief recorded all three agents as "identical to Claude Code". That is
|
|
41
|
+
* true of the STDIN payload and false of the CONFIG document:
|
|
42
|
+
*
|
|
43
|
+
* | | `claude` shape | `cursor` shape |
|
|
44
|
+
* |---|---|---|
|
|
45
|
+
* | used by | Claude Code, Codex CLI | Cursor |
|
|
46
|
+
* | top-level `version` | absent | ⛔ REQUIRED positive integer |
|
|
47
|
+
* | entry | `[{ hooks: [{ type, command, timeout }] }]` | ⛔ flat `[{ command, timeout }]` |
|
|
48
|
+
* | unknown event key | ignored | ⛔ validation ERROR |
|
|
49
|
+
*
|
|
50
|
+
* Cursor's is read out of its shipped bundle (`parseAndValidateHooksConfig` →
|
|
51
|
+
* `M6s` → `zWu` → `HWu`), which rejects a document with no numeric `version`
|
|
52
|
+
* outright. Codex's is MEASURED: a probe wrote the `claude` shape to a throwaway
|
|
53
|
+
* `CODEX_HOME` and Codex parsed it, recognised the event and clamped the
|
|
54
|
+
* timeout — which it could only do having read the inner entry.
|
|
55
|
+
*
|
|
56
|
+
* ⚠ **Writing the wrong shape does not error.** It produces a file that parses,
|
|
57
|
+
* registers nothing, and reports success — the same matches-nothing failure as
|
|
58
|
+
* an unrecognised event name, relocated to the document level.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* What marks an entry as ours.
|
|
62
|
+
*
|
|
63
|
+
* ⚠ **A FIELD, not a substring of the command.** Matching on the command text
|
|
64
|
+
* would claim a user's own hook that happens to mention this product, and would
|
|
65
|
+
* lose track of ours the moment the binary path changed — which is exactly when
|
|
66
|
+
* idempotence matters, because that is a re-`connect`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const AGENT_HOOK_MARKER = "vibecommit-capture-hook v1";
|
|
69
|
+
/** Which document shape an agent's hook config uses. See the module docblock. */
|
|
70
|
+
export type HookDocShape = "claude" | "cursor";
|
|
71
|
+
/** One event we want registered, already in the agent's own vocabulary. */
|
|
72
|
+
export interface HookRegistration {
|
|
73
|
+
/** The agent's OWN spelling — `SessionEnd` or `sessionEnd`, never canonical. */
|
|
74
|
+
readonly event: string;
|
|
75
|
+
readonly command: string;
|
|
76
|
+
/** ⛔ SECONDS. Every one of the three configs is in seconds; our budgets are ms. */
|
|
77
|
+
readonly timeoutSec: number;
|
|
78
|
+
}
|
|
79
|
+
export type MergeOutcome = {
|
|
80
|
+
readonly kind: "merged";
|
|
81
|
+
readonly text: string;
|
|
82
|
+
/** Events where ours was newly appended. */
|
|
83
|
+
readonly added: readonly string[];
|
|
84
|
+
/** Events where ours was already present and was refreshed. */
|
|
85
|
+
readonly updated: readonly string[];
|
|
86
|
+
/** Foreign entries left untouched beside ours, across all events. */
|
|
87
|
+
readonly foreign: number;
|
|
88
|
+
}
|
|
89
|
+
/** ⛔ The document exists and could not be read. NOTHING is written. */
|
|
90
|
+
| {
|
|
91
|
+
readonly kind: "refused";
|
|
92
|
+
readonly why: string;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* ⛔ **THE WHOLE MERGE, AS A PURE FUNCTION ON TEXT.**
|
|
96
|
+
*
|
|
97
|
+
* `raw` is the file's current contents, or `null` when it does not exist. No fs,
|
|
98
|
+
* no `home`, no clock — so the semantics that matter can be driven by a test
|
|
99
|
+
* without a temp directory, and the writer below is left with nothing but I/O.
|
|
100
|
+
* That split is deliberate: every rule in the module docblock is a property of
|
|
101
|
+
* this function, and none of them needs a real `~/.claude` to check.
|
|
102
|
+
*/
|
|
103
|
+
export declare function mergeHookDocument(raw: string | null, shape: HookDocShape, registrations: readonly HookRegistration[]): MergeOutcome;
|
|
104
|
+
export type AgentHookInstall = {
|
|
105
|
+
readonly kind: "installed";
|
|
106
|
+
readonly path: string;
|
|
107
|
+
readonly events: readonly string[];
|
|
108
|
+
} | {
|
|
109
|
+
readonly kind: "already";
|
|
110
|
+
readonly path: string;
|
|
111
|
+
readonly events: readonly string[];
|
|
112
|
+
} | {
|
|
113
|
+
readonly kind: "refused";
|
|
114
|
+
readonly path: string;
|
|
115
|
+
readonly why: string;
|
|
116
|
+
} | {
|
|
117
|
+
readonly kind: "failed";
|
|
118
|
+
readonly path: string;
|
|
119
|
+
readonly why: string;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Merge our registrations into `path` and write it back.
|
|
123
|
+
*
|
|
124
|
+
* ⚠ **Written through a temp file in the same directory, then renamed.** A
|
|
125
|
+
* partial write here is the user's settings truncated, and `rename(2)` within a
|
|
126
|
+
* directory is atomic — so a crash mid-write leaves the original intact rather
|
|
127
|
+
* than a half-document that the next run would then REFUSE to touch, locking
|
|
128
|
+
* the user out of their own config.
|
|
129
|
+
*
|
|
130
|
+
* ⚠ The file's existing mode is preserved when there is one. A settings file
|
|
131
|
+
* the user tightened to `0600` must not be widened by us having rewritten it.
|
|
132
|
+
*/
|
|
133
|
+
export declare function writeAgentHooks(path: string, shape: HookDocShape, registrations: readonly HookRegistration[]): AgentHookInstall;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registering our hook in an AGENT's own config — `CR-195`, D205.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ **THIS IS NOT `install.ts`'s CHAINING, AND THE DIFFERENCE IS THE CONTAINER,
|
|
5
|
+
* NOT THE RULE.** The rule is the same one D154 settled for git hooks — *never
|
|
6
|
+
* clobber; whatever was there keeps working*. The MECHANISM cannot be the same:
|
|
7
|
+
*
|
|
8
|
+
* - `.git/hooks/post-commit` is a **single-slot executable file**. One path,
|
|
9
|
+
* one script, no way for two owners to coexist — so `writeHook` renames the
|
|
10
|
+
* incumbent aside and invokes it explicitly. That is the only move available.
|
|
11
|
+
* - `~/.claude/settings.json` is a **multi-slot document**, and `hooks.Stop`
|
|
12
|
+
* is an ARRAY that already expresses coexistence natively.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ **So renaming here would be catastrophic rather than conservative.** That
|
|
15
|
+
* file holds the user's model, permissions, enabled plugins, marketplaces and
|
|
16
|
+
* theme; moving it aside to write our own would delete all of it, and on a
|
|
17
|
+
* machine with an existing `Stop` hook it would fire on the very first
|
|
18
|
+
* `connect`. The faithful translation of *never clobber* into this container is
|
|
19
|
+
* **append into the array and leave every other byte alone**.
|
|
20
|
+
*
|
|
21
|
+
* ## The three rules, and what each one is defending against
|
|
22
|
+
*
|
|
23
|
+
* 1. **APPEND, never replace.** A foreign entry is never removed, rewritten or
|
|
24
|
+
* reordered. Ours goes last.
|
|
25
|
+
* 2. **IDEMPOTENT via a marker.** JSON has nowhere to put a comment, so ours
|
|
26
|
+
* carries `_vibecommit`. Without it, `connect` run twice appends twice and
|
|
27
|
+
* the hook fires twice per event — two processes racing on one session
|
|
28
|
+
* state file. ⚠ The marker field is TOLERATED by all three readers, and
|
|
29
|
+
* that is measured rather than assumed: Codex parses and clamps with the
|
|
30
|
+
* field present on the inner and outer object alike, and Cursor's validator
|
|
31
|
+
* checks only known members (`HWu`/`jWu`/`$Wu` in its bundle).
|
|
32
|
+
* 3. ⛔ **REFUSE on unparseable, never overwrite.** A file we could not read is
|
|
33
|
+
* a file whose contents we cannot preserve. Overwriting it would be the
|
|
34
|
+
* destructive outcome wearing a success message — the exact defect class
|
|
35
|
+
* this whole unit exists to delete. Fail-closed, the same direction as
|
|
36
|
+
* `install.ts`'s `core.hooksPath` refusal.
|
|
37
|
+
*
|
|
38
|
+
* ## ⛔ TWO DOCUMENT SHAPES, MEASURED — they are NOT interchangeable
|
|
39
|
+
*
|
|
40
|
+
* The brief recorded all three agents as "identical to Claude Code". That is
|
|
41
|
+
* true of the STDIN payload and false of the CONFIG document:
|
|
42
|
+
*
|
|
43
|
+
* | | `claude` shape | `cursor` shape |
|
|
44
|
+
* |---|---|---|
|
|
45
|
+
* | used by | Claude Code, Codex CLI | Cursor |
|
|
46
|
+
* | top-level `version` | absent | ⛔ REQUIRED positive integer |
|
|
47
|
+
* | entry | `[{ hooks: [{ type, command, timeout }] }]` | ⛔ flat `[{ command, timeout }]` |
|
|
48
|
+
* | unknown event key | ignored | ⛔ validation ERROR |
|
|
49
|
+
*
|
|
50
|
+
* Cursor's is read out of its shipped bundle (`parseAndValidateHooksConfig` →
|
|
51
|
+
* `M6s` → `zWu` → `HWu`), which rejects a document with no numeric `version`
|
|
52
|
+
* outright. Codex's is MEASURED: a probe wrote the `claude` shape to a throwaway
|
|
53
|
+
* `CODEX_HOME` and Codex parsed it, recognised the event and clamped the
|
|
54
|
+
* timeout — which it could only do having read the inner entry.
|
|
55
|
+
*
|
|
56
|
+
* ⚠ **Writing the wrong shape does not error.** It produces a file that parses,
|
|
57
|
+
* registers nothing, and reports success — the same matches-nothing failure as
|
|
58
|
+
* an unrecognised event name, relocated to the document level.
|
|
59
|
+
*/
|
|
60
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
61
|
+
import { dirname, join } from "node:path";
|
|
62
|
+
/**
|
|
63
|
+
* What marks an entry as ours.
|
|
64
|
+
*
|
|
65
|
+
* ⚠ **A FIELD, not a substring of the command.** Matching on the command text
|
|
66
|
+
* would claim a user's own hook that happens to mention this product, and would
|
|
67
|
+
* lose track of ours the moment the binary path changed — which is exactly when
|
|
68
|
+
* idempotence matters, because that is a re-`connect`.
|
|
69
|
+
*/
|
|
70
|
+
export const AGENT_HOOK_MARKER = "vibecommit-capture-hook v1";
|
|
71
|
+
/** The field carrying it. Leading underscore: a hint that it is not the agent's. */
|
|
72
|
+
const MARKER_KEY = "_vibecommit";
|
|
73
|
+
/** The `version` we mint when CREATING a `cursor` document. */
|
|
74
|
+
const CURSOR_CONFIG_VERSION = 1;
|
|
75
|
+
/**
|
|
76
|
+
* ⛔ **THE WHOLE MERGE, AS A PURE FUNCTION ON TEXT.**
|
|
77
|
+
*
|
|
78
|
+
* `raw` is the file's current contents, or `null` when it does not exist. No fs,
|
|
79
|
+
* no `home`, no clock — so the semantics that matter can be driven by a test
|
|
80
|
+
* without a temp directory, and the writer below is left with nothing but I/O.
|
|
81
|
+
* That split is deliberate: every rule in the module docblock is a property of
|
|
82
|
+
* this function, and none of them needs a real `~/.claude` to check.
|
|
83
|
+
*/
|
|
84
|
+
export function mergeHookDocument(raw, shape, registrations) {
|
|
85
|
+
let doc;
|
|
86
|
+
if (raw === null || raw.trim() === "") {
|
|
87
|
+
doc = {};
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(raw);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// ⛔ RULE 3. We hold the user's settings in our hands here; a parse we do
|
|
96
|
+
// not understand is not permission to replace them.
|
|
97
|
+
return {
|
|
98
|
+
kind: "refused",
|
|
99
|
+
why: error instanceof Error ? error.message : "not valid JSON",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
103
|
+
return { kind: "refused", why: "the document is not a JSON object" };
|
|
104
|
+
}
|
|
105
|
+
doc = { ...parsed };
|
|
106
|
+
}
|
|
107
|
+
// ⛔ MINTED ONLY ON CREATE. An existing `version` is the user's (or another
|
|
108
|
+
// tool's) and is left exactly as found — bumping someone else's schema version
|
|
109
|
+
// is the same class of act as renaming their settings file, in a smaller hat.
|
|
110
|
+
if (shape === "cursor" && doc.version === undefined) {
|
|
111
|
+
doc.version = CURSOR_CONFIG_VERSION;
|
|
112
|
+
}
|
|
113
|
+
const existingHooks = doc.hooks;
|
|
114
|
+
if (existingHooks !== undefined) {
|
|
115
|
+
if (existingHooks === null || typeof existingHooks !== "object" || Array.isArray(existingHooks)) {
|
|
116
|
+
// Present and not an object: we cannot merge into it without destroying it.
|
|
117
|
+
return { kind: "refused", why: "`hooks` is present but is not an object" };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const hooks = {
|
|
121
|
+
...(existingHooks ?? {}),
|
|
122
|
+
};
|
|
123
|
+
const added = [];
|
|
124
|
+
const updated = [];
|
|
125
|
+
let foreign = 0;
|
|
126
|
+
for (const registration of registrations) {
|
|
127
|
+
const current = hooks[registration.event];
|
|
128
|
+
if (current !== undefined && !Array.isArray(current)) {
|
|
129
|
+
return {
|
|
130
|
+
kind: "refused",
|
|
131
|
+
why: `\`hooks.${registration.event}\` is present but is not an array`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const entries = [...(current ?? [])];
|
|
135
|
+
const ours = entryFor(shape, registration);
|
|
136
|
+
const at = entries.findIndex(isOurs);
|
|
137
|
+
if (at >= 0) {
|
|
138
|
+
// ⛔ REFRESH IN PLACE, and the position is kept. The binary path moves on
|
|
139
|
+
// every reinstall, so this arm is the ordinary one on a re-`connect` — and
|
|
140
|
+
// appending instead would grow the array without bound.
|
|
141
|
+
entries[at] = ours;
|
|
142
|
+
updated.push(registration.event);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
entries.push(ours);
|
|
146
|
+
added.push(registration.event);
|
|
147
|
+
}
|
|
148
|
+
foreign += entries.filter((entry) => !isOurs(entry)).length;
|
|
149
|
+
hooks[registration.event] = entries;
|
|
150
|
+
}
|
|
151
|
+
doc.hooks = hooks;
|
|
152
|
+
return { kind: "merged", text: `${JSON.stringify(doc, null, 2)}\n`, added, updated, foreign };
|
|
153
|
+
}
|
|
154
|
+
/** Ours? ⛔ By the marker alone — never by the command, which changes. */
|
|
155
|
+
function isOurs(entry) {
|
|
156
|
+
return (entry !== null &&
|
|
157
|
+
typeof entry === "object" &&
|
|
158
|
+
!Array.isArray(entry) &&
|
|
159
|
+
entry[MARKER_KEY] === AGENT_HOOK_MARKER);
|
|
160
|
+
}
|
|
161
|
+
/** Our entry, in the shape that agent's reader actually validates. */
|
|
162
|
+
function entryFor(shape, registration) {
|
|
163
|
+
if (shape === "cursor") {
|
|
164
|
+
// ⛔ FLAT. `zWu` validates `hooks[event]` as an array of hook SCRIPTS, and
|
|
165
|
+
// `HWu` requires a `command` on each — so a nested `{ hooks: [...] }` object
|
|
166
|
+
// is rejected outright for having none.
|
|
167
|
+
return {
|
|
168
|
+
[MARKER_KEY]: AGENT_HOOK_MARKER,
|
|
169
|
+
command: registration.command,
|
|
170
|
+
timeout: registration.timeoutSec,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// ⛔ NESTED, and `matcher` is deliberately omitted rather than set to `"*"` —
|
|
174
|
+
// these three events carry no tool name to match on, and the plugin's own
|
|
175
|
+
// `hooks.json` omits it too.
|
|
176
|
+
return {
|
|
177
|
+
[MARKER_KEY]: AGENT_HOOK_MARKER,
|
|
178
|
+
hooks: [
|
|
179
|
+
{ type: "command", command: registration.command, timeout: registration.timeoutSec },
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Merge our registrations into `path` and write it back.
|
|
185
|
+
*
|
|
186
|
+
* ⚠ **Written through a temp file in the same directory, then renamed.** A
|
|
187
|
+
* partial write here is the user's settings truncated, and `rename(2)` within a
|
|
188
|
+
* directory is atomic — so a crash mid-write leaves the original intact rather
|
|
189
|
+
* than a half-document that the next run would then REFUSE to touch, locking
|
|
190
|
+
* the user out of their own config.
|
|
191
|
+
*
|
|
192
|
+
* ⚠ The file's existing mode is preserved when there is one. A settings file
|
|
193
|
+
* the user tightened to `0600` must not be widened by us having rewritten it.
|
|
194
|
+
*/
|
|
195
|
+
export function writeAgentHooks(path, shape, registrations) {
|
|
196
|
+
let raw = null;
|
|
197
|
+
let mode;
|
|
198
|
+
try {
|
|
199
|
+
raw = readFileSync(path, "utf8");
|
|
200
|
+
mode = statSync(path).mode & 0o777;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Absent is the ordinary case on a first install; anything else surfaces on
|
|
204
|
+
// the write below rather than being guessed at here.
|
|
205
|
+
raw = null;
|
|
206
|
+
}
|
|
207
|
+
const merged = mergeHookDocument(raw, shape, registrations);
|
|
208
|
+
if (merged.kind === "refused")
|
|
209
|
+
return { kind: "refused", path, why: merged.why };
|
|
210
|
+
const temp = join(dirname(path), `.${AGENT_HOOK_MARKER.replace(/\W+/g, "-")}.tmp`);
|
|
211
|
+
try {
|
|
212
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
213
|
+
writeFileSync(temp, merged.text, { mode: mode ?? 0o600 });
|
|
214
|
+
if (mode !== undefined)
|
|
215
|
+
chmodSync(temp, mode);
|
|
216
|
+
renameSync(temp, path);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return { kind: "failed", path, why: error instanceof Error ? error.message : "write failed" };
|
|
220
|
+
}
|
|
221
|
+
const events = registrations.map((registration) => registration.event);
|
|
222
|
+
// "already" is not "nothing happened" — the command was still refreshed. It is
|
|
223
|
+
// the answer to *did this connect ADD anything*, which is what a user reads.
|
|
224
|
+
return merged.added.length === 0
|
|
225
|
+
? { kind: "already", path, events }
|
|
226
|
+
: { kind: "installed", path, events };
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=agent_hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent_hooks.js","sourceRoot":"","sources":["../src/agent_hooks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registering capture with every agent on this machine — `CR-195`/U4b, D205.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ **THIS IS THE FILE `/install` HAS BEEN DESCRIBING SINCE BEFORE IT EXISTED.**
|
|
5
|
+
* `app/install/page.tsx` told users that `vibecommit connect` *"wires Claude Code
|
|
6
|
+
* hooks on Stop, PreCompact and SessionEnd"*. It did not: `connect` wrote GIT
|
|
7
|
+
* hooks only, and nothing in `src/` ever wrote `settings.json`. The three events
|
|
8
|
+
* were registered in exactly one place — `claude-plugin/hooks/hooks.json` — so
|
|
9
|
+
* the npm path gave a user no session capture at all, silently, while the page
|
|
10
|
+
* said otherwise.
|
|
11
|
+
*
|
|
12
|
+
* ## What is written, and what decides it
|
|
13
|
+
*
|
|
14
|
+
* Everything below is DERIVED from the registry rather than restated here:
|
|
15
|
+
*
|
|
16
|
+
* - **which file** — `dialect.hookConfig.path(home, env)`, honouring
|
|
17
|
+
* `CLAUDE_CONFIG_DIR` and `CODEX_HOME` because those products honour them;
|
|
18
|
+
* - **which document shape** — `dialect.hookConfig.shape`, two shapes across
|
|
19
|
+
* three agents (Cursor's is its own);
|
|
20
|
+
* - **which event names** — `dialect.eventNames`, camelCase for Cursor;
|
|
21
|
+
* - **which timeouts** — `dialect.events[event].registeredTimeoutMs`, so the
|
|
22
|
+
* number written into an agent's config and the number this client checks
|
|
23
|
+
* its own watchdog against cannot drift apart.
|
|
24
|
+
*
|
|
25
|
+
* ⚠ That last one matters more than it looks. `EventBudget` requires
|
|
26
|
+
* `clientBudgetMs < registeredTimeoutMs`, and until this file existed the
|
|
27
|
+
* "registered" half was a MIRROR of a JSON file hand-maintained in another
|
|
28
|
+
* repository — `test/agent-registry.test.ts` says so, and says a drift in it is
|
|
29
|
+
* invisible from here. For the three files this writes, the mirror becomes the
|
|
30
|
+
* source.
|
|
31
|
+
*
|
|
32
|
+
* ## ⛔ Only agents that are actually here
|
|
33
|
+
*
|
|
34
|
+
* A config is written only when the directory that would contain it already
|
|
35
|
+
* exists. Creating `~/.cursor/hooks.json` on a machine with no Cursor is not
|
|
36
|
+
* harmless: it is a file the user never asked for, in a tool they do not run,
|
|
37
|
+
* that a future Cursor install would silently inherit. ⚠ The directory — never
|
|
38
|
+
* the config file — is the evidence, because the file's absence is the ordinary
|
|
39
|
+
* first-install case and is exactly what this function is for.
|
|
40
|
+
*/
|
|
41
|
+
import { type AgentHookInstall, type HookRegistration } from "./agent_hooks.js";
|
|
42
|
+
import { type AgentId, type TranscriptDialect } from "./agents/types.js";
|
|
43
|
+
/** What happened for one agent. `null` install means the agent is not here. */
|
|
44
|
+
export interface AgentHookOutcome {
|
|
45
|
+
readonly agentId: AgentId;
|
|
46
|
+
readonly path: string;
|
|
47
|
+
readonly install: AgentHookInstall | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The command an agent's config invokes.
|
|
51
|
+
*
|
|
52
|
+
* ⛔ **`hook` LEADS, AND THE FLAG NEVER DOES.** `invocationMode` returns `"hook"`
|
|
53
|
+
* only when `argv[0] === "hook"`, so a registered command whose first argument
|
|
54
|
+
* is `--agent=` runs the binary INTERACTIVELY — inside a hook, printing to a
|
|
55
|
+
* stream the agent reads back into the user's turn. `registry.ts` names this as
|
|
56
|
+
* the trap the wave was most likely to ship, and `test/agent-registry.test.ts`
|
|
57
|
+
* holds both arms.
|
|
58
|
+
*
|
|
59
|
+
* ⚠ **The flag is passed explicitly for every agent, Claude Code included**,
|
|
60
|
+
* even though `DEFAULT_AGENT_ID` makes it redundant there. A config that names
|
|
61
|
+
* its own agent is self-describing, and — since Cursor reads Claude Code's
|
|
62
|
+
* settings and maps them onto its own events — it is the only thing that tells
|
|
63
|
+
* the two invocations apart when both fire.
|
|
64
|
+
*/
|
|
65
|
+
export declare function hookCommand(binPath: string, agentId: AgentId): string;
|
|
66
|
+
/** Every event this client registers, in one agent's own vocabulary. */
|
|
67
|
+
export declare function registrationsFor(dialect: TranscriptDialect, binPath: string): readonly HookRegistration[];
|
|
68
|
+
/** Is this agent on the machine at all? The DIRECTORY, never the config file. */
|
|
69
|
+
export declare function agentPresent(dialect: TranscriptDialect, home: string, env: NodeJS.ProcessEnv): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Register capture with every agent present on this machine.
|
|
72
|
+
*
|
|
73
|
+
* ⚠ **Never throws and never partially aborts.** Each agent is independent: a
|
|
74
|
+
* refusal to overwrite one unparseable `settings.json` must not stop the other
|
|
75
|
+
* two being wired, and the caller reports all three. That is the same shape
|
|
76
|
+
* `installCaptureHooks` uses for its two git hooks and the same reason —
|
|
77
|
+
* a per-target verdict is the only honest answer when the targets are unrelated.
|
|
78
|
+
*/
|
|
79
|
+
export declare function installAgentHooks(home: string, env: NodeJS.ProcessEnv, binPath: string): readonly AgentHookOutcome[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registering capture with every agent on this machine — `CR-195`/U4b, D205.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ **THIS IS THE FILE `/install` HAS BEEN DESCRIBING SINCE BEFORE IT EXISTED.**
|
|
5
|
+
* `app/install/page.tsx` told users that `vibecommit connect` *"wires Claude Code
|
|
6
|
+
* hooks on Stop, PreCompact and SessionEnd"*. It did not: `connect` wrote GIT
|
|
7
|
+
* hooks only, and nothing in `src/` ever wrote `settings.json`. The three events
|
|
8
|
+
* were registered in exactly one place — `claude-plugin/hooks/hooks.json` — so
|
|
9
|
+
* the npm path gave a user no session capture at all, silently, while the page
|
|
10
|
+
* said otherwise.
|
|
11
|
+
*
|
|
12
|
+
* ## What is written, and what decides it
|
|
13
|
+
*
|
|
14
|
+
* Everything below is DERIVED from the registry rather than restated here:
|
|
15
|
+
*
|
|
16
|
+
* - **which file** — `dialect.hookConfig.path(home, env)`, honouring
|
|
17
|
+
* `CLAUDE_CONFIG_DIR` and `CODEX_HOME` because those products honour them;
|
|
18
|
+
* - **which document shape** — `dialect.hookConfig.shape`, two shapes across
|
|
19
|
+
* three agents (Cursor's is its own);
|
|
20
|
+
* - **which event names** — `dialect.eventNames`, camelCase for Cursor;
|
|
21
|
+
* - **which timeouts** — `dialect.events[event].registeredTimeoutMs`, so the
|
|
22
|
+
* number written into an agent's config and the number this client checks
|
|
23
|
+
* its own watchdog against cannot drift apart.
|
|
24
|
+
*
|
|
25
|
+
* ⚠ That last one matters more than it looks. `EventBudget` requires
|
|
26
|
+
* `clientBudgetMs < registeredTimeoutMs`, and until this file existed the
|
|
27
|
+
* "registered" half was a MIRROR of a JSON file hand-maintained in another
|
|
28
|
+
* repository — `test/agent-registry.test.ts` says so, and says a drift in it is
|
|
29
|
+
* invisible from here. For the three files this writes, the mirror becomes the
|
|
30
|
+
* source.
|
|
31
|
+
*
|
|
32
|
+
* ## ⛔ Only agents that are actually here
|
|
33
|
+
*
|
|
34
|
+
* A config is written only when the directory that would contain it already
|
|
35
|
+
* exists. Creating `~/.cursor/hooks.json` on a machine with no Cursor is not
|
|
36
|
+
* harmless: it is a file the user never asked for, in a tool they do not run,
|
|
37
|
+
* that a future Cursor install would silently inherit. ⚠ The directory — never
|
|
38
|
+
* the config file — is the evidence, because the file's absence is the ordinary
|
|
39
|
+
* first-install case and is exactly what this function is for.
|
|
40
|
+
*/
|
|
41
|
+
import { existsSync } from "node:fs";
|
|
42
|
+
import { dirname } from "node:path";
|
|
43
|
+
import { writeAgentHooks } from "./agent_hooks.js";
|
|
44
|
+
import { DIALECTS } from "./agents/registry.js";
|
|
45
|
+
import { HOOK_EVENTS } from "./agents/types.js";
|
|
46
|
+
/**
|
|
47
|
+
* The command an agent's config invokes.
|
|
48
|
+
*
|
|
49
|
+
* ⛔ **`hook` LEADS, AND THE FLAG NEVER DOES.** `invocationMode` returns `"hook"`
|
|
50
|
+
* only when `argv[0] === "hook"`, so a registered command whose first argument
|
|
51
|
+
* is `--agent=` runs the binary INTERACTIVELY — inside a hook, printing to a
|
|
52
|
+
* stream the agent reads back into the user's turn. `registry.ts` names this as
|
|
53
|
+
* the trap the wave was most likely to ship, and `test/agent-registry.test.ts`
|
|
54
|
+
* holds both arms.
|
|
55
|
+
*
|
|
56
|
+
* ⚠ **The flag is passed explicitly for every agent, Claude Code included**,
|
|
57
|
+
* even though `DEFAULT_AGENT_ID` makes it redundant there. A config that names
|
|
58
|
+
* its own agent is self-describing, and — since Cursor reads Claude Code's
|
|
59
|
+
* settings and maps them onto its own events — it is the only thing that tells
|
|
60
|
+
* the two invocations apart when both fire.
|
|
61
|
+
*/
|
|
62
|
+
export function hookCommand(binPath, agentId) {
|
|
63
|
+
return `${quote(binPath)} hook --agent=${agentId}`;
|
|
64
|
+
}
|
|
65
|
+
/** Double-quote for the shells these configs are read by. */
|
|
66
|
+
function quote(value) {
|
|
67
|
+
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
68
|
+
}
|
|
69
|
+
/** Every event this client registers, in one agent's own vocabulary. */
|
|
70
|
+
export function registrationsFor(dialect, binPath) {
|
|
71
|
+
return HOOK_EVENTS.map((event) => ({
|
|
72
|
+
event: dialect.eventNames[event],
|
|
73
|
+
command: hookCommand(binPath, dialect.id),
|
|
74
|
+
// ⛔ SECONDS, from the registry's milliseconds. Every one of the three
|
|
75
|
+
// configs takes seconds; `EventBudget` is in milliseconds because the
|
|
76
|
+
// client's own watchdog is. For Codex this lands `SessionEnd` on 3 s, which
|
|
77
|
+
// is exactly the cap Codex clamps to whatever we ask for — measured, so the
|
|
78
|
+
// number we write is the number that takes effect.
|
|
79
|
+
timeoutSec: Math.max(1, Math.round(dialect.events[event].registeredTimeoutMs / 1000)),
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
/** Is this agent on the machine at all? The DIRECTORY, never the config file. */
|
|
83
|
+
export function agentPresent(dialect, home, env) {
|
|
84
|
+
return existsSync(dirname(dialect.hookConfig.path(home, env)));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Register capture with every agent present on this machine.
|
|
88
|
+
*
|
|
89
|
+
* ⚠ **Never throws and never partially aborts.** Each agent is independent: a
|
|
90
|
+
* refusal to overwrite one unparseable `settings.json` must not stop the other
|
|
91
|
+
* two being wired, and the caller reports all three. That is the same shape
|
|
92
|
+
* `installCaptureHooks` uses for its two git hooks and the same reason —
|
|
93
|
+
* a per-target verdict is the only honest answer when the targets are unrelated.
|
|
94
|
+
*/
|
|
95
|
+
export function installAgentHooks(home, env, binPath) {
|
|
96
|
+
return DIALECTS.map((dialect) => {
|
|
97
|
+
const path = dialect.hookConfig.path(home, env);
|
|
98
|
+
if (!agentPresent(dialect, home, env)) {
|
|
99
|
+
return { agentId: dialect.id, path, install: null };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
agentId: dialect.id,
|
|
103
|
+
path,
|
|
104
|
+
install: writeAgentHooks(path, dialect.hookConfig.shape, registrationsFor(dialect, binPath)),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=agent_install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent_install.js","sourceRoot":"","sources":["../src/agent_install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAgD,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAwC,MAAM,mBAAmB,CAAC;AAStF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAAgB;IAC3D,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,iBAAiB,OAAO,EAAE,CAAC;AACrD,CAAC;AAED,6DAA6D;AAC7D,SAAS,KAAK,CAAC,KAAa;IAC1B,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC;AACpD,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,gBAAgB,CAC9B,OAA0B,EAC1B,OAAe;IAEf,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACjC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC;QAChC,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;QACzC,sEAAsE;QACtE,sEAAsE;QACtE,4EAA4E;QAC5E,4EAA4E;QAC5E,mDAAmD;QACnD,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;KACtF,CAAC,CAAC,CAAC;AACN,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,YAAY,CAAC,OAA0B,EAAE,IAAY,EAAE,GAAsB;IAC3F,OAAO,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,GAAsB,EACtB,OAAe;IAEf,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;YACtC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACtD,CAAC;QACD,OAAO;YACL,OAAO,EAAE,OAAO,CAAC,EAAE;YACnB,IAAI;YACJ,OAAO,EAAE,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;SAC7F,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { readdirSync, statSync } from "node:fs";
|
|
11
11
|
import { basename, join } from "node:path";
|
|
12
|
-
import { isInside, isTranscriptFile, transcriptRoot } from "../paths.js";
|
|
12
|
+
import { claudeSettingsPath, isInside, isTranscriptFile, transcriptRoot } from "../paths.js";
|
|
13
13
|
/**
|
|
14
14
|
* The client-side backstop, per event.
|
|
15
15
|
*
|
|
@@ -138,6 +138,17 @@ export const CLAUDE_CODE = {
|
|
|
138
138
|
// `subagents/` directory, so every file here is already a transcript; there is
|
|
139
139
|
// no interior shape to require, unlike Cursor's.
|
|
140
140
|
admitsFile: isTranscriptFile,
|
|
141
|
+
// ⛔ IDENTITY, and it is the reference the other two are measured AGAINST —
|
|
142
|
+
// `HOOK_EVENTS` is spelled the way Claude Code spells it, because this is the
|
|
143
|
+
// agent the client was built for. Declared explicitly rather than defaulted so
|
|
144
|
+
// that a dialect added later must ANSWER the question; a default would let a
|
|
145
|
+
// camelCase agent inherit Claude's spellings and match nothing (`CR-195`).
|
|
146
|
+
eventNames: { Stop: "Stop", PreCompact: "PreCompact", SessionEnd: "SessionEnd" },
|
|
147
|
+
// ⛔ `~/.claude/settings.json`, honouring `CLAUDE_CONFIG_DIR`. ⚠ This is the
|
|
148
|
+
// file the PLUGIN path does not need — the plugin ships its own `hooks.json`
|
|
149
|
+
// — and the npm path had no writer at all, which is the defect `/install`
|
|
150
|
+
// has been advertising past since it first claimed `connect` wires hooks.
|
|
151
|
+
hookConfig: { path: claudeSettingsPath, shape: "claude" },
|
|
141
152
|
events: {
|
|
142
153
|
Stop: {
|
|
143
154
|
registeredTimeoutMs: CLAUDE_REGISTERED_STOP_MS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude_code.js","sourceRoot":"","sources":["../../src/agents/claude_code.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"claude_code.js","sourceRoot":"","sources":["../../src/agents/claude_code.ts"],"names":[],"mappings":"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"}
|
package/dist/agents/codex_cli.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* measured by this task.** They are recorded as the brief stated them so a later
|
|
12
12
|
* reader can check them against the product rather than against this file.
|
|
13
13
|
*/
|
|
14
|
-
import { codexTranscriptRoot, isTranscriptFile } from "../paths.js";
|
|
14
|
+
import { codexHooksPath, codexTranscriptRoot, isTranscriptFile } from "../paths.js";
|
|
15
15
|
/**
|
|
16
16
|
* `SessionEnd` is the tight one — 3 s registered, 2.2 s client.
|
|
17
17
|
*
|
|
@@ -33,6 +33,26 @@ export const CODEX_CLI = {
|
|
|
33
33
|
// shape to require here either. ⚠ CARRIED FROM THE BRIEF like the numbers
|
|
34
34
|
// above: this task did not re-walk a Codex tree to confirm it.
|
|
35
35
|
admitsFile: isTranscriptFile,
|
|
36
|
+
// ⛔ **IDENTITY — MEASURED, and it is the one member on this dialect that is
|
|
37
|
+
// no longer carried from the brief** (`CR-195`, D205). Read out of the native
|
|
38
|
+
// binary's embedded JSON schema, where every `hook_event_name` is a `const`:
|
|
39
|
+
// `PermissionRequest PostCompact PostToolUse PreCompact PreToolUse SessionEnd
|
|
40
|
+
// SessionStart Stop SubagentStart SubagentStop UserPromptSubmit`. All three of
|
|
41
|
+
// ours are present and spelled exactly as Claude Code spells them.
|
|
42
|
+
//
|
|
43
|
+
// ⚠ **The binary ALSO carries a snake_case enum** (`session_end`,
|
|
44
|
+
// `pre_compact`, …) beside a TOML-serialisation error string, and taking it
|
|
45
|
+
// for the config vocabulary was the near-miss of this unit. A differential run
|
|
46
|
+
// against three throwaway `CODEX_HOME`s settled it: `SessionEnd` is recognised
|
|
47
|
+
// and says so by clamping, while `session_end` and a bogus key produce NO
|
|
48
|
+
// diagnostic — a file that parses, registers nothing, and reports success.
|
|
49
|
+
eventNames: { Stop: "Stop", PreCompact: "PreCompact", SessionEnd: "SessionEnd" },
|
|
50
|
+
// ⛔ `~/.codex/hooks.json`, honouring `CODEX_HOME` — MEASURED, since every
|
|
51
|
+
// probe in this unit drove a real `codex` through a throwaway `CODEX_HOME`
|
|
52
|
+
// and it read the `hooks.json` there. The NESTED shape, also measured: a
|
|
53
|
+
// document in this form was parsed, its event recognised and its timeout
|
|
54
|
+
// clamped, which Codex can only do having read the inner entry.
|
|
55
|
+
hookConfig: { path: codexHooksPath, shape: "claude" },
|
|
36
56
|
events: {
|
|
37
57
|
Stop: {
|
|
38
58
|
registeredTimeoutMs: CODEX_TURN_REGISTERED_MS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex_cli.js","sourceRoot":"","sources":["../../src/agents/codex_cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"codex_cli.js","sourceRoot":"","sources":["../../src/agents/codex_cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpF;;;;;;GAMG;AACH,MAAM,+BAA+B,GAAG,KAAK,CAAC;AAC9C,MAAM,2BAA2B,GAAG,KAAK,CAAC;AAE1C,2EAA2E;AAC3E,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,oBAAoB,GAAG,KAAK,CAAC;AAEnC,MAAM,CAAC,MAAM,SAAS,GAAsB;IAC1C,EAAE,EAAE,WAAW;IACf,SAAS,EAAE,QAAQ;IACnB,cAAc,EAAE,mBAAmB;IACnC,yEAAyE;IACzE,4EAA4E;IAC5E,0EAA0E;IAC1E,+DAA+D;IAC/D,UAAU,EAAE,gBAAgB;IAC5B,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,8EAA8E;IAC9E,+EAA+E;IAC/E,mEAAmE;IACnE,EAAE;IACF,kEAAkE;IAClE,4EAA4E;IAC5E,+EAA+E;IAC/E,+EAA+E;IAC/E,0EAA0E;IAC1E,2EAA2E;IAC3E,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE;IAChF,0EAA0E;IAC1E,2EAA2E;IAC3E,yEAAyE;IACzE,yEAAyE;IACzE,gEAAgE;IAChE,UAAU,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,QAAQ,EAAE;IACrD,MAAM,EAAE;QACN,IAAI,EAAE;YACJ,mBAAmB,EAAE,wBAAwB;YAC7C,cAAc,EAAE,oBAAoB;SACrC;QACD,UAAU,EAAE;YACV,mBAAmB,EAAE,wBAAwB;YAC7C,cAAc,EAAE,oBAAoB;SACrC;QACD,UAAU,EAAE;YACV,mBAAmB,EAAE,+BAA+B;YACpD,cAAc,EAAE,2BAA2B;SAC5C;KACF;IACD,+EAA+E;IAC/E,6EAA6E;IAC7E,oEAAoE;IACpE,oBAAoB,EAAE,UAAU;IAChC,SAAS,EAAE,WAAW;IACtB,8EAA8E;IAC9E,0EAA0E;IAC1E,8EAA8E;IAC9E,6EAA6E;IAC7E,+EAA+E;IAC/E,8BAA8B;IAC9B,gBAAgB,EAAE,GAAG,EAAE,CAAC,IAAI;IAC5B,4EAA4E;IAC5E,wEAAwE;IACxE,6EAA6E;IAC7E,oDAAoD;CACrD,CAAC"}
|