vibe-code-explainer 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +365 -67
- package/dist/hooks/post-tool.js +4 -31
- package/dist/hooks/post-tool.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,113 +1,304 @@
|
|
|
1
|
-
# code-explainer
|
|
1
|
+
# vibe-code-explainer
|
|
2
2
|
|
|
3
|
-
Real-time diff explanations for vibe coders using Claude Code
|
|
3
|
+
**Real-time diff explanations for vibe coders using Claude Code.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Every time Claude Code edits a file, creates a file, or runs a destructive
|
|
6
|
+
command, vibe-code-explainer prints a plain-English explanation in your terminal
|
|
7
|
+
before Claude continues. Catches unrelated changes, flags risky edits, and makes
|
|
8
|
+
AI-generated code readable for people who can't read diffs.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
No more accepting code blindly.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
(rm, mv, npm install, git reset, etc.), code-explainer:
|
|
13
|
-
|
|
14
|
-
1. Intercepts the change via a PostToolUse hook.
|
|
15
|
-
2. Sends the diff to a local LLM (Ollama) or to Claude Code itself.
|
|
16
|
-
3. Prints a plain-English explanation box in your terminal.
|
|
17
|
-
4. Flags changes as none / low / medium / high risk.
|
|
18
|
-
5. Tracks all changes across your session and alerts when Claude wanders off-task.
|
|
12
|
+
---
|
|
19
13
|
|
|
20
14
|
## Example output
|
|
21
15
|
|
|
16
|
+
**Safe change (low/no risk):**
|
|
17
|
+
|
|
22
18
|
```
|
|
23
19
|
┌─ code-explainer ─────────────────────────────
|
|
24
20
|
│ src/app/page.tsx
|
|
25
21
|
│
|
|
26
|
-
│
|
|
27
|
-
│ that blends two shades of
|
|
28
|
-
│ or data
|
|
22
|
+
│ Changed the page background from a solid dark color to a
|
|
23
|
+
│ gradient that blends two shades of blue. Visual-only change,
|
|
24
|
+
│ no logic or data affected.
|
|
29
25
|
│
|
|
30
26
|
│ Risk: ✅ None
|
|
31
27
|
└──────────────────────────────────────────────
|
|
32
28
|
```
|
|
33
29
|
|
|
30
|
+
**Risky change (medium/high risk):**
|
|
31
|
+
|
|
34
32
|
```
|
|
35
33
|
┌─ code-explainer ⚠️ ──────────────────────────
|
|
36
34
|
│ 🚨 .env
|
|
37
35
|
│
|
|
38
|
-
│ Adds a Stripe payment secret key directly in the environment
|
|
39
|
-
│ This key gives access to your payment system.
|
|
36
|
+
│ Adds a Stripe payment secret key directly in the environment
|
|
37
|
+
│ file. This key gives access to your payment system.
|
|
40
38
|
│
|
|
41
39
|
│ Risk: 🚨 High
|
|
42
|
-
│ A live payment API key is hardcoded in the file
|
|
40
|
+
│ A live payment API key is hardcoded in the file and should
|
|
41
|
+
│ not be committed to version control.
|
|
43
42
|
└──────────────────────────────────────────────
|
|
44
43
|
```
|
|
45
44
|
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## ⚠️ Compatibility: Claude Code CLI only
|
|
48
|
+
|
|
49
|
+
**Hooks currently only fire when you use Claude Code in a terminal (CLI).** The
|
|
50
|
+
VS Code extension and Desktop App ignore PostToolUse hooks as of Claude Code
|
|
51
|
+
v2.1.105 (April 2026). This is a known bug on Anthropic's side, tracked in:
|
|
52
|
+
|
|
53
|
+
- [anthropics/claude-code#21736](https://github.com/anthropics/claude-code/issues/21736)
|
|
54
|
+
- [anthropics/claude-code#27014](https://github.com/anthropics/claude-code/issues/27014)
|
|
55
|
+
- [anthropics/claude-code#42336](https://github.com/anthropics/claude-code/issues/42336)
|
|
56
|
+
|
|
57
|
+
If you are using the Claude Code extension in VS Code and want explanations to
|
|
58
|
+
appear, **open an external terminal** (PowerShell, CMD, Git Bash on Windows;
|
|
59
|
+
Terminal.app on macOS; your favorite terminal on Linux) and run `claude` from
|
|
60
|
+
there. Explanations will appear in that terminal.
|
|
61
|
+
|
|
62
|
+
As soon as Anthropic ships the fix, this tool will work in the extension too
|
|
63
|
+
with no changes on your side.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
46
67
|
## Install
|
|
47
68
|
|
|
69
|
+
### 1. Prerequisites
|
|
70
|
+
|
|
71
|
+
- **Node.js 20 or newer** — already required by Claude Code, so you probably
|
|
72
|
+
have it. Check with `node --version`.
|
|
73
|
+
- **Claude Code** — the CLI. If you don't have it, get it from
|
|
74
|
+
[claude.com/code](https://www.claude.com/code).
|
|
75
|
+
- **Ollama** (optional) — only if you want to use the local LLM engine. The
|
|
76
|
+
installer will offer to install it for you. Get it from
|
|
77
|
+
[ollama.com](https://ollama.com).
|
|
78
|
+
|
|
79
|
+
### 2. Run the installer
|
|
80
|
+
|
|
81
|
+
In your terminal, navigate to the project where you want Claude Code
|
|
82
|
+
explanations:
|
|
83
|
+
|
|
48
84
|
```bash
|
|
85
|
+
cd path/to/your/project
|
|
49
86
|
npx vibe-code-explainer init
|
|
50
87
|
```
|
|
51
88
|
|
|
52
|
-
The interactive
|
|
89
|
+
The installer is interactive and will walk you through:
|
|
90
|
+
|
|
91
|
+
1. **Choose the engine**:
|
|
92
|
+
- **Local LLM (Ollama)** — free, private, works offline. Your code never
|
|
93
|
+
leaves your machine.
|
|
94
|
+
- **Claude Code (native)** — uses `claude -p` under the hood for the
|
|
95
|
+
highest-quality explanations and unrelated-change detection. Costs API
|
|
96
|
+
tokens.
|
|
97
|
+
|
|
98
|
+
2. **Choose the detail level**:
|
|
99
|
+
- **Standard** — 1–2 sentence explanation per change (recommended)
|
|
100
|
+
- **Minimal** — one short sentence per change
|
|
101
|
+
- **Verbose** — detailed bullet-point breakdown of every change
|
|
102
|
+
|
|
103
|
+
3. **Pick a model (Ollama only)**:
|
|
104
|
+
- If you have an NVIDIA GPU, the installer detects your VRAM and
|
|
105
|
+
recommends the right model automatically.
|
|
106
|
+
- Otherwise, a model picker is shown with VRAM hints:
|
|
107
|
+
|
|
108
|
+
| Model | Recommended for |
|
|
109
|
+
|-------|-----------------|
|
|
110
|
+
| `qwen2.5-coder:7b` | ≤ 8 GB VRAM (~4.5 GB quantized, fast) |
|
|
111
|
+
| `qwen2.5-coder:14b` | 12–16 GB VRAM |
|
|
112
|
+
| `qwen3-coder:30b` | ≥ 20 GB VRAM (MoE, fast when it fits) |
|
|
113
|
+
| `qwen2.5-coder:32b` | ≥ 24 GB VRAM (best dense quality) |
|
|
114
|
+
|
|
115
|
+
4. **Pull the model** (Ollama only). You'll see Ollama's own progress bar
|
|
116
|
+
during the download — first run can take several minutes depending on your
|
|
117
|
+
connection.
|
|
118
|
+
|
|
119
|
+
5. **Warmup** — the installer sends a trivial diff to Ollama so the first
|
|
120
|
+
real explanation is fast. Add `--skip-warmup` if you're in a hurry:
|
|
121
|
+
```bash
|
|
122
|
+
npx vibe-code-explainer init --skip-warmup
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
6. **Done.** The installer writes two files to your project:
|
|
126
|
+
- `code-explainer.config.json` — your settings
|
|
127
|
+
- `.claude/settings.local.json` — the PostToolUse hooks (merged with any
|
|
128
|
+
existing hooks, never overwrites)
|
|
129
|
+
|
|
130
|
+
Next time you run `claude` in this directory, you'll see explanations.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Using it
|
|
135
|
+
|
|
136
|
+
Open Claude Code in a terminal, in the project where you installed it:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
cd path/to/your/project
|
|
140
|
+
claude
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Ask Claude to do anything that edits files or runs destructive commands:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
> create a hello.ts with a function that prints "hi"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
An explanation box appears in the Claude Code chat right after each file write,
|
|
150
|
+
edit, or destructive Bash command.
|
|
53
151
|
|
|
54
|
-
|
|
55
|
-
- Ask you to pick an engine (Ollama for free/private, or Claude Code for best quality)
|
|
56
|
-
- Ask you to pick a detail level (minimal / standard / verbose)
|
|
57
|
-
- Pull the recommended model based on your GPU
|
|
58
|
-
- Run a warmup so the first real explanation is fast
|
|
59
|
-
- Wire up the hooks in your project's `.claude/settings.local.json`
|
|
152
|
+
### What gets explained
|
|
60
153
|
|
|
61
|
-
|
|
154
|
+
- **Edits** to existing files (via the `Edit` tool)
|
|
155
|
+
- **New files** (via the `Write` tool)
|
|
156
|
+
- **Multi-edits** (via the `MultiEdit` tool)
|
|
157
|
+
- **Destructive Bash commands** such as `rm`, `mv`, `cp`, `mkdir`, `chmod`,
|
|
158
|
+
`chown`, `git checkout`, `git reset`, `git revert`, `sed -i`,
|
|
159
|
+
`npm install`, `pip install`, `yarn add`, `pnpm add`, and output
|
|
160
|
+
redirections (`>`, `>>`).
|
|
62
161
|
|
|
63
|
-
|
|
162
|
+
### What gets skipped
|
|
64
163
|
|
|
65
|
-
|
|
66
|
-
|
|
164
|
+
- Read-only Bash commands (`ls`, `cat`, `grep`, `git status`, `git log`, etc.)
|
|
165
|
+
- Binary files
|
|
166
|
+
- Empty diffs (no actual change)
|
|
167
|
+
- Files matching your `exclude` patterns (default: `*.lock`, `dist/**`,
|
|
168
|
+
`node_modules/**`)
|
|
67
169
|
|
|
68
|
-
|
|
69
|
-
|------|-------|
|
|
70
|
-
| ≤ 8 GB | `qwen3-coder:30b` (MoE, 3.3B active) |
|
|
71
|
-
| 12-16 GB | `qwen2.5-coder:14b` |
|
|
72
|
-
| ≥ 20 GB | `qwen2.5-coder:32b` |
|
|
73
|
-
| Fallback | `qwen2.5-coder:7b` |
|
|
170
|
+
### Risk levels
|
|
74
171
|
|
|
75
|
-
|
|
172
|
+
Every explanation includes a risk rating:
|
|
76
173
|
|
|
77
|
-
|
|
78
|
-
|
|
174
|
+
| Level | Meaning |
|
|
175
|
+
|-------|---------|
|
|
176
|
+
| ✅ **None** | Visual/text changes, styling, comments, formatting, whitespace |
|
|
177
|
+
| ⚠️ **Low** | Config files, new dependencies, file renames, test changes |
|
|
178
|
+
| ⚠️ **Medium** | Authentication, payment code, API keys, database schema, env vars |
|
|
179
|
+
| 🚨 **High** | Removing security checks, hardcoded secrets, disabling validation |
|
|
180
|
+
|
|
181
|
+
---
|
|
79
182
|
|
|
80
183
|
## Configuration
|
|
81
184
|
|
|
185
|
+
Change any setting at any time by running:
|
|
186
|
+
|
|
82
187
|
```bash
|
|
83
188
|
npx vibe-code-explainer config
|
|
84
189
|
```
|
|
85
190
|
|
|
86
|
-
|
|
191
|
+
This opens an interactive menu showing your current settings. Pick what you
|
|
192
|
+
want to change, one at a time. Every change is saved immediately.
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
Current settings:
|
|
196
|
+
Engine: Local LLM (Ollama)
|
|
197
|
+
Model: qwen2.5-coder:7b
|
|
198
|
+
Ollama URL: http://localhost:11434
|
|
199
|
+
Detail level: Standard
|
|
200
|
+
Hooks: Edit ✓ Write ✓ Bash ✓
|
|
201
|
+
Excluded: *.lock, dist/**, node_modules/**
|
|
202
|
+
Skip if slow: 8s
|
|
203
|
+
|
|
204
|
+
? What would you like to change?
|
|
205
|
+
❯ Engine
|
|
206
|
+
Model
|
|
207
|
+
Ollama URL
|
|
208
|
+
Detail level
|
|
209
|
+
Enable/disable hooks
|
|
210
|
+
File exclusions
|
|
211
|
+
Latency timeout
|
|
212
|
+
Back (save and exit)
|
|
213
|
+
```
|
|
87
214
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- Ollama
|
|
91
|
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
215
|
+
### Configurable options
|
|
216
|
+
|
|
217
|
+
- **Engine** — swap between Ollama (local) and Claude Code (native). Switching
|
|
218
|
+
to Claude Code requires the `claude` CLI to be authenticated.
|
|
219
|
+
- **Model** — pick a different Ollama model. The VRAM hints are visible in the
|
|
220
|
+
chooser. You can also type any model name Ollama supports if you want to use
|
|
221
|
+
one not on the list (e.g., `deepseek-coder-v2:16b`).
|
|
222
|
+
- **Ollama URL** — defaults to `http://localhost:11434`. Change this if you run
|
|
223
|
+
Ollama in a Docker container on a different port, or on a separate machine.
|
|
224
|
+
Non-loopback URLs trigger a security warning because your code would be sent
|
|
225
|
+
over the network.
|
|
226
|
+
- **Detail level** — minimal / standard / verbose. See
|
|
227
|
+
[Install → Detail level](#2-run-the-installer) for what each produces.
|
|
228
|
+
- **Hooks** — turn on or off individually. If you find the Bash explanations
|
|
229
|
+
noisy, disable just that hook and keep Edit + Write.
|
|
230
|
+
- **File exclusions** — glob patterns for files you never want explained.
|
|
231
|
+
Defaults cover lockfiles, build output, and dependencies. Add patterns like
|
|
232
|
+
`*.generated.*` if your project has codegen.
|
|
233
|
+
- **Latency timeout** — maximum time to wait for an explanation before
|
|
234
|
+
skipping it. Options: 5s (aggressive), 8s (balanced, recommended), 15s
|
|
235
|
+
(patient), or never skip.
|
|
236
|
+
|
|
237
|
+
### Editing the config file directly
|
|
238
|
+
|
|
239
|
+
If you prefer editing JSON manually, the config lives at
|
|
240
|
+
`code-explainer.config.json` in your project root. Structure:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"engine": "ollama",
|
|
245
|
+
"ollamaModel": "qwen2.5-coder:7b",
|
|
246
|
+
"ollamaUrl": "http://localhost:11434",
|
|
247
|
+
"detailLevel": "standard",
|
|
248
|
+
"hooks": {
|
|
249
|
+
"edit": true,
|
|
250
|
+
"write": true,
|
|
251
|
+
"bash": true
|
|
252
|
+
},
|
|
253
|
+
"exclude": ["*.lock", "dist/**", "node_modules/**"],
|
|
254
|
+
"skipIfSlowMs": 8000,
|
|
255
|
+
"bashFilter": {
|
|
256
|
+
"capturePatterns": [
|
|
257
|
+
"rm", "mv", "cp", "mkdir",
|
|
258
|
+
"npm install", "pip install", "yarn add", "pnpm add",
|
|
259
|
+
"chmod", "chown",
|
|
260
|
+
"git checkout", "git reset", "git revert",
|
|
261
|
+
"sed -i"
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
95
266
|
|
|
96
|
-
|
|
267
|
+
Changes take effect on the next Claude Code tool call — no restart needed.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Session tools
|
|
272
|
+
|
|
273
|
+
### Summary of what Claude has done
|
|
97
274
|
|
|
98
275
|
```bash
|
|
99
276
|
npx vibe-code-explainer summary
|
|
100
277
|
```
|
|
101
278
|
|
|
102
|
-
|
|
103
|
-
changes, files touched, risk breakdown, and unrelated
|
|
279
|
+
Prints a report of every explained change in your current Claude Code session:
|
|
280
|
+
total changes, files touched, risk breakdown, and flagged unrelated files.
|
|
281
|
+
|
|
282
|
+
### Clear the session
|
|
104
283
|
|
|
105
284
|
```bash
|
|
106
285
|
npx vibe-code-explainer session end
|
|
107
286
|
```
|
|
108
287
|
|
|
109
|
-
Clears the
|
|
110
|
-
|
|
288
|
+
Clears the session state (tracked files and cache). Run this when you start a
|
|
289
|
+
new task so the drift-detection summary doesn't include old edits.
|
|
290
|
+
|
|
291
|
+
### Warm up the model
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
npx vibe-code-explainer warmup
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Sends a trivial diff to Ollama to pre-load the model. Useful if you closed and
|
|
298
|
+
reopened Ollama — the first real explanation afterwards would otherwise be
|
|
299
|
+
slow (10–15 seconds) as the model gets loaded into VRAM.
|
|
300
|
+
|
|
301
|
+
---
|
|
111
302
|
|
|
112
303
|
## Uninstall
|
|
113
304
|
|
|
@@ -115,27 +306,134 @@ don't include old edits.
|
|
|
115
306
|
npx vibe-code-explainer uninstall
|
|
116
307
|
```
|
|
117
308
|
|
|
118
|
-
Removes
|
|
119
|
-
|
|
309
|
+
Removes the PostToolUse hook entries from `.claude/settings.local.json` and
|
|
310
|
+
deletes `code-explainer.config.json`. Other entries in `settings.local.json`
|
|
311
|
+
(hooks from other tools, other settings) are preserved.
|
|
312
|
+
|
|
313
|
+
Ollama and any pulled models stay installed. If you want to remove them:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
ollama rm qwen2.5-coder:7b
|
|
317
|
+
# or whichever model you pulled
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Troubleshooting
|
|
323
|
+
|
|
324
|
+
### The box doesn't appear after an edit
|
|
325
|
+
|
|
326
|
+
Most common causes, in order:
|
|
327
|
+
|
|
328
|
+
1. **You're using the VS Code extension or Desktop App.** Hooks don't fire
|
|
329
|
+
there yet. See [Compatibility](#️-compatibility-claude-code-cli-only)
|
|
330
|
+
above. Use the terminal CLI.
|
|
331
|
+
2. **Ollama is not running.** Start it in a separate terminal:
|
|
332
|
+
```bash
|
|
333
|
+
ollama serve
|
|
334
|
+
```
|
|
335
|
+
Then try another edit.
|
|
336
|
+
3. **The engine is misconfigured.** Run `npx vibe-code-explainer config` and
|
|
337
|
+
check the engine and URL. Try switching to the Claude Code engine as a
|
|
338
|
+
test — if that works, Ollama is the issue.
|
|
339
|
+
4. **The file is excluded.** Check your `exclude` patterns in the config.
|
|
340
|
+
|
|
341
|
+
### The explanation took too long and got skipped
|
|
342
|
+
|
|
343
|
+
Default timeout is 8 seconds. Cold starts with Ollama (first explanation
|
|
344
|
+
after starting the service) can be 10–15 seconds. Two fixes:
|
|
345
|
+
|
|
346
|
+
- Run `npx vibe-code-explainer warmup` right after starting Ollama.
|
|
347
|
+
- Raise the timeout to 15 seconds via `npx vibe-code-explainer config →
|
|
348
|
+
Latency timeout`.
|
|
349
|
+
|
|
350
|
+
### Explanations are low-quality
|
|
351
|
+
|
|
352
|
+
Ollama with a 7B model has real limits for complex code (authentication flows,
|
|
353
|
+
business logic, security-relevant changes). If explanations feel wrong on
|
|
354
|
+
tricky diffs, switch to the Claude Code engine:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npx vibe-code-explainer config
|
|
358
|
+
# → Engine → Claude Code (native)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
This uses `claude -p` under the hood and produces much better output, at the
|
|
362
|
+
cost of API tokens per explanation.
|
|
363
|
+
|
|
364
|
+
### I want to skip explanations for a specific file type
|
|
365
|
+
|
|
366
|
+
Add a glob pattern via the config menu:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
npx vibe-code-explainer config → File exclusions → Add a pattern
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
For example, `*.md` to skip all markdown files, or `src/generated/**` to skip
|
|
373
|
+
a whole directory.
|
|
374
|
+
|
|
375
|
+
### I want to turn off Bash explanations but keep Edit/Write
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
npx vibe-code-explainer config → Enable/disable hooks
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Uncheck Bash, keep Edit and Write checked.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## How it actually works
|
|
386
|
+
|
|
387
|
+
`vibe-code-explainer` is a Claude Code PostToolUse hook. When Claude Code runs
|
|
388
|
+
a tool (Edit, Write, MultiEdit, Bash), the hook script is invoked with the
|
|
389
|
+
tool payload on stdin. The script:
|
|
390
|
+
|
|
391
|
+
1. Reads the payload (file path, old string, new string, or Bash command).
|
|
392
|
+
2. Extracts a unified-style diff directly from the payload's old/new strings.
|
|
393
|
+
3. Checks the diff against a local SHA-256 cache so repeated diffs don't
|
|
394
|
+
re-query the engine.
|
|
395
|
+
4. Checks whether the change is "unrelated" to the session so far based on
|
|
396
|
+
directory ancestry and sensitive-path patterns (`.env`, `auth/`,
|
|
397
|
+
`payment/`, etc.).
|
|
398
|
+
5. Sends the diff to the chosen engine with a prompt that teaches the model
|
|
399
|
+
to distinguish additions, modifications, and removals.
|
|
400
|
+
6. Parses the JSON response (`summary`, `risk`, `riskReason`), formats it
|
|
401
|
+
into a terminal box, and emits it as a `systemMessage` in the Claude Code
|
|
402
|
+
chat.
|
|
403
|
+
7. Records the change in a per-session JSONL file under the OS temp
|
|
404
|
+
directory, keyed to Claude Code's own session ID.
|
|
405
|
+
8. Always exits 0 so Claude Code is never blocked.
|
|
120
406
|
|
|
121
|
-
|
|
407
|
+
All state (config, session tracking, cache) lives either in your project
|
|
408
|
+
directory (config) or in a user-scoped temp directory (session, cache) with
|
|
409
|
+
`0600` file permissions. Nothing is written outside those paths.
|
|
122
410
|
|
|
123
|
-
|
|
124
|
-
- Claude Code installed
|
|
125
|
-
- Ollama installed if you choose the Ollama engine (the installer will offer to install it)
|
|
411
|
+
---
|
|
126
412
|
|
|
127
|
-
##
|
|
413
|
+
## Safety philosophy
|
|
128
414
|
|
|
129
|
-
code-explainer is a **visibility tool**, not a gate. It never blocks
|
|
130
|
-
Code, never reverts changes, never requires confirmation. If the
|
|
131
|
-
engine fails or times out, the hook skips silently with a
|
|
415
|
+
`vibe-code-explainer` is a **visibility tool**, not a gate. It never blocks
|
|
416
|
+
Claude Code, never reverts changes, never requires confirmation. If the
|
|
417
|
+
explanation engine fails or times out, the hook skips silently (with a small
|
|
418
|
+
notice) so your session keeps flowing.
|
|
132
419
|
|
|
133
|
-
For real safety, read the explanations
|
|
420
|
+
For real safety, **read the explanations**, and watch for:
|
|
134
421
|
|
|
135
|
-
- 🚨 High risk
|
|
136
|
-
- ⚠️ Medium risk
|
|
137
|
-
-
|
|
422
|
+
- 🚨 **High risk** — security checks, encryption, hardcoded secrets
|
|
423
|
+
- ⚠️ **Medium risk** — auth, payment, API keys, database, env vars
|
|
424
|
+
- **Session drift alerts** — Claude touched files unrelated to your request
|
|
425
|
+
|
|
426
|
+
When you see any of these, pause, read the diff yourself, and decide if
|
|
427
|
+
you trust the change. That is the entire point of this tool.
|
|
428
|
+
|
|
429
|
+
---
|
|
138
430
|
|
|
139
431
|
## License
|
|
140
432
|
|
|
141
433
|
MIT
|
|
434
|
+
|
|
435
|
+
## Contributing
|
|
436
|
+
|
|
437
|
+
Source code: https://github.com/Beleleoo/CodeExplainer
|
|
438
|
+
|
|
439
|
+
Issues and pull requests welcome.
|
package/dist/hooks/post-tool.js
CHANGED
|
@@ -18,9 +18,7 @@ import {
|
|
|
18
18
|
recordEntry,
|
|
19
19
|
setCached
|
|
20
20
|
} from "../chunk-W67RX53R.js";
|
|
21
|
-
import
|
|
22
|
-
__require
|
|
23
|
-
} from "../chunk-7OCVIDC7.js";
|
|
21
|
+
import "../chunk-7OCVIDC7.js";
|
|
24
22
|
|
|
25
23
|
// src/hooks/post-tool.ts
|
|
26
24
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -496,26 +494,10 @@ var output = [];
|
|
|
496
494
|
function addOutput(text) {
|
|
497
495
|
output.push(text);
|
|
498
496
|
}
|
|
499
|
-
function debugLog(event, data) {
|
|
500
|
-
if (!process.env.CODE_EXPLAINER_DEBUG) return;
|
|
501
|
-
try {
|
|
502
|
-
const fs = __require("fs");
|
|
503
|
-
const os = __require("os");
|
|
504
|
-
const path = __require("path");
|
|
505
|
-
const logPath = path.join(os.tmpdir(), "code-explainer-debug.log");
|
|
506
|
-
const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, ...data }) + "\n";
|
|
507
|
-
fs.appendFileSync(logPath, line);
|
|
508
|
-
} catch {
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
497
|
function safeExit() {
|
|
512
498
|
if (output.length > 0) {
|
|
513
499
|
const systemMessage = "\n" + output.join("\n");
|
|
514
|
-
|
|
515
|
-
debugLog("emit", { bytes: payload.length, preview: systemMessage.slice(0, 200) });
|
|
516
|
-
process.stdout.write(payload + "\n");
|
|
517
|
-
} else {
|
|
518
|
-
debugLog("no-output");
|
|
500
|
+
process.stdout.write(JSON.stringify({ systemMessage }) + "\n");
|
|
519
501
|
}
|
|
520
502
|
process.exit(0);
|
|
521
503
|
}
|
|
@@ -570,19 +552,10 @@ async function main() {
|
|
|
570
552
|
addOutput(formatSkipNotice("interrupted by user"));
|
|
571
553
|
safeExit();
|
|
572
554
|
});
|
|
573
|
-
debugLog("started", { pid: process.pid, argv: process.argv.slice(2) });
|
|
574
555
|
const raw = await readStdin();
|
|
575
|
-
|
|
576
|
-
if (!raw.trim()) {
|
|
577
|
-
debugLog("exit-empty-stdin");
|
|
578
|
-
safeExit();
|
|
579
|
-
}
|
|
556
|
+
if (!raw.trim()) safeExit();
|
|
580
557
|
const payload = parsePayload(raw);
|
|
581
|
-
if (!payload)
|
|
582
|
-
debugLog("exit-bad-payload");
|
|
583
|
-
safeExit();
|
|
584
|
-
}
|
|
585
|
-
debugLog("payload-ok", { tool: payload.tool_name, session: payload.session_id });
|
|
558
|
+
if (!payload) safeExit();
|
|
586
559
|
const cwd = payload.cwd || process.cwd();
|
|
587
560
|
const config = loadConfigSafe(cwd);
|
|
588
561
|
if (!isHookEnabled(payload.tool_name, config)) safeExit();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/post-tool.ts","../../src/engines/claude.ts","../../src/hooks/diff-extractor.ts","../../src/filter/bash-filter.ts","../../src/session/drift.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { loadConfig, DEFAULT_CONFIG } from \"../config/schema.js\";\nimport type { Config, HookPayload, ExplanationResult } from \"../config/schema.js\";\nimport { callOllama, type EngineOutcome } from \"../engines/ollama.js\";\nimport { callClaude } from \"../engines/claude.js\";\nimport {\n extractEditDiff,\n extractNewFileDiff,\n buildDiffFromEdit,\n buildDiffFromMultiEdit,\n isExcluded,\n} from \"./diff-extractor.js\";\nimport { shouldCaptureBash } from \"../filter/bash-filter.js\";\nimport { formatExplanationBox, formatDriftAlert, formatSkipNotice, formatErrorNotice } from \"../format/box.js\";\nimport { recordEntry, readSession, cleanStaleSessionFiles } from \"../session/tracker.js\";\nimport { analyzeDrift, shouldAlertDrift } from \"../session/drift.js\";\nimport { getCached, setCached } from \"../cache/explanation-cache.js\";\n\nconst output: string[] = [];\n\nfunction addOutput(text: string): void {\n output.push(text);\n}\n\nfunction debugLog(event: string, data?: Record<string, unknown>): void {\n if (!process.env.CODE_EXPLAINER_DEBUG) return;\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require(\"node:fs\") as typeof import(\"node:fs\");\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const os = require(\"node:os\") as typeof import(\"node:os\");\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const path = require(\"node:path\") as typeof import(\"node:path\");\n const logPath = path.join(os.tmpdir(), \"code-explainer-debug.log\");\n const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + \"\\n\";\n fs.appendFileSync(logPath, line);\n } catch {\n // ignore\n }\n}\n\n/**\n * Emit the Claude Code hook JSON on stdout so the accumulated output\n * appears as a system message in the user's terminal. Always exit 0 so\n * Claude Code is never blocked.\n */\nfunction safeExit(): never {\n if (output.length > 0) {\n // Leading newline separates the box from Claude Code's \"PostToolUse:X says:\"\n // prefix, which otherwise renders on the same line as the top border.\n const systemMessage = \"\\n\" + output.join(\"\\n\");\n const payload = JSON.stringify({ systemMessage });\n debugLog(\"emit\", { bytes: payload.length, preview: systemMessage.slice(0, 200) });\n process.stdout.write(payload + \"\\n\");\n } else {\n debugLog(\"no-output\");\n }\n process.exit(0);\n}\n\nasync function readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let data = \"\";\n process.stdin.setEncoding(\"utf-8\");\n process.stdin.on(\"data\", (chunk) => {\n data += chunk;\n });\n process.stdin.on(\"end\", () => resolve(data));\n process.stdin.on(\"error\", () => resolve(data));\n // Safety timeout: if stdin has no data in 2s, resolve empty.\n setTimeout(() => resolve(data), 2000);\n });\n}\n\nfunction parsePayload(raw: string): HookPayload | null {\n try {\n const parsed = JSON.parse(raw);\n if (typeof parsed.session_id === \"string\" && typeof parsed.tool_name === \"string\") {\n return parsed as HookPayload;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction loadConfigSafe(cwd: string): Config {\n const path = join(cwd, \"code-explainer.config.json\");\n if (!existsSync(path)) return DEFAULT_CONFIG;\n return loadConfig(path);\n}\n\nfunction isHookEnabled(toolName: string, config: Config): boolean {\n const lower = toolName.toLowerCase();\n if (lower === \"edit\" || lower === \"multiedit\") return config.hooks.edit;\n if (lower === \"write\") return config.hooks.write;\n if (lower === \"bash\") return config.hooks.bash;\n return false;\n}\n\nasync function runEngine(\n filePath: string,\n diff: string,\n config: Config,\n userPrompt: string | undefined,\n signal: AbortSignal\n): Promise<EngineOutcome> {\n if (signal.aborted) {\n return { kind: \"skip\", reason: \"interrupted by user\" };\n }\n if (config.engine === \"ollama\") {\n return callOllama({ filePath, diff, config });\n }\n return callClaude({ filePath, diff, config, userPrompt });\n}\n\nasync function main(): Promise<void> {\n // Interrupt handler — always exit 0 on Ctrl+C.\n const controller = new AbortController();\n process.on(\"SIGINT\", () => {\n controller.abort();\n addOutput(formatSkipNotice(\"interrupted by user\"));\n safeExit();\n });\n\n debugLog(\"started\", { pid: process.pid, argv: process.argv.slice(2) });\n\n const raw = await readStdin();\n debugLog(\"stdin-read\", { bytes: raw.length });\n\n if (!raw.trim()) {\n debugLog(\"exit-empty-stdin\");\n safeExit();\n }\n\n const payload = parsePayload(raw);\n if (!payload) {\n debugLog(\"exit-bad-payload\");\n safeExit();\n }\n\n debugLog(\"payload-ok\", { tool: payload.tool_name, session: payload.session_id });\n\n const cwd = payload.cwd || process.cwd();\n const config = loadConfigSafe(cwd);\n\n if (!isHookEnabled(payload.tool_name, config)) safeExit();\n\n cleanStaleSessionFiles();\n\n // Pass session_id to downstream modules via env (so summary/session-end\n // commands pick the right session without re-parsing the payload).\n process.env.CODE_EXPLAINER_SESSION_ID = payload.session_id;\n\n // Extract the diff based on tool name.\n let filePath: string;\n let diff: string;\n let isNewFile = false;\n\n const lowerTool = payload.tool_name.toLowerCase();\n if (lowerTool === \"edit\" || lowerTool === \"multiedit\" || lowerTool === \"write\") {\n const input = payload.tool_input as {\n file_path?: string;\n filePath?: string;\n old_string?: string;\n new_string?: string;\n oldString?: string;\n newString?: string;\n edits?: Array<{ old_string?: string; new_string?: string; oldString?: string; newString?: string }>;\n };\n const target = input.file_path ?? input.filePath;\n if (!target) safeExit();\n filePath = target as string;\n\n if (isExcluded(filePath, config.exclude)) safeExit();\n\n // Preferred path: use the payload's old/new strings directly. This works\n // for untracked files (very common) and is always more accurate than git\n // diff, which may miss changes on files that were created and edited in\n // the same session without a commit.\n let result;\n if (lowerTool === \"edit\") {\n const oldStr = input.old_string ?? input.oldString ?? \"\";\n const newStr = input.new_string ?? input.newString ?? \"\";\n if (oldStr || newStr) {\n result = buildDiffFromEdit(filePath, oldStr, newStr);\n } else {\n result = extractEditDiff(filePath, cwd);\n }\n } else if (lowerTool === \"multiedit\") {\n if (input.edits && input.edits.length > 0) {\n result = buildDiffFromMultiEdit(filePath, input.edits);\n } else {\n result = extractEditDiff(filePath, cwd);\n }\n } else {\n result = extractNewFileDiff(filePath, cwd);\n }\n\n if (result.kind === \"empty\") safeExit();\n if (result.kind === \"skip\") {\n addOutput(formatSkipNotice(result.reason));\n safeExit();\n }\n if (result.kind === \"binary\") {\n addOutput(formatSkipNotice(result.message));\n safeExit();\n }\n diff = result.content;\n isNewFile = result.kind === \"new-file\";\n } else if (lowerTool === \"bash\") {\n const input = payload.tool_input as { command?: string };\n const command = input.command ?? \"\";\n if (!command || !shouldCaptureBash(command)) safeExit();\n filePath = \"<bash command>\";\n diff = command;\n } else {\n safeExit();\n }\n\n // Cache check.\n const cacheKey = `${filePath}\\n${diff}`;\n const cached = getCached(payload.session_id, cacheKey);\n let result: ExplanationResult | null = null;\n\n if (cached) {\n result = cached;\n } else {\n const outcome = await runEngine(filePath, diff, config, undefined, controller.signal);\n if (outcome.kind === \"skip\") {\n addOutput(formatSkipNotice(outcome.reason));\n safeExit();\n }\n if (outcome.kind === \"error\") {\n addOutput(formatErrorNotice(outcome.problem, outcome.cause, outcome.fix));\n safeExit();\n }\n result = outcome.result;\n setCached(payload.session_id, cacheKey, result);\n }\n\n // Path-heuristic drift analysis (only meaningful for Edit/Write).\n let driftReason: string | undefined;\n if (!isNewFile || filePath !== \"<bash command>\") {\n const priorEntries = readSession(payload.session_id);\n if (filePath !== \"<bash command>\") {\n const analysis = analyzeDrift(filePath, priorEntries);\n if (analysis.isUnrelated) {\n driftReason = analysis.reason;\n }\n }\n }\n\n // Print the explanation box.\n addOutput(formatExplanationBox(filePath, result.summary, result.risk, result.riskReason));\n\n // Record the entry.\n recordEntry(payload.session_id, {\n file: filePath,\n timestamp: Date.now(),\n risk: result.risk,\n summary: result.summary,\n unrelated: !!driftReason,\n });\n\n // Drift alert at threshold.\n const updated = readSession(payload.session_id);\n const driftCheck = shouldAlertDrift(updated);\n if (driftCheck.shouldAlert) {\n addOutput(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles));\n }\n\n safeExit();\n}\n\nmain().catch(() => {\n // Never fail the hook — always exit 0.\n safeExit();\n});\n","import { execFile } from \"node:child_process\";\nimport type { Config, ExplanationResult, RiskLevel } from \"../config/schema.js\";\nimport { buildClaudePrompt } from \"../prompts/templates.js\";\nimport type { EngineOutcome } from \"./ollama.js\";\n\nexport interface ClaudeCallInputs {\n filePath: string;\n diff: string;\n config: Config;\n userPrompt?: string;\n}\n\nfunction extractJson(text: string): string | null {\n const trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n const fenceMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?```/);\n if (fenceMatch) {\n return fenceMatch[1].trim();\n }\n const start = trimmed.indexOf(\"{\");\n const end = trimmed.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return trimmed.slice(start, end + 1);\n }\n return null;\n}\n\nfunction parseResponse(rawText: string): ExplanationResult | null {\n const json = extractJson(rawText);\n if (!json) return null;\n try {\n const parsed = JSON.parse(json);\n if (\n typeof parsed.summary === \"string\" &&\n typeof parsed.risk === \"string\" &&\n typeof parsed.riskReason === \"string\"\n ) {\n const risk = parsed.risk as RiskLevel;\n if (![\"none\", \"low\", \"medium\", \"high\"].includes(risk)) return null;\n return { summary: parsed.summary, risk, riskReason: parsed.riskReason };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction truncateText(text: string, max: number): string {\n if (text.length <= max) return text;\n return text.slice(0, max) + \"...\";\n}\n\ninterface ExecResult {\n stdout: string;\n stderr: string;\n code: number | null;\n}\n\nfunction runClaude(prompt: string, timeoutMs: number): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const child = execFile(\n \"claude\",\n [\"-p\", prompt],\n {\n timeout: timeoutMs,\n maxBuffer: 1024 * 1024 * 2, // 2MB\n windowsHide: true,\n },\n (err, stdout, stderr) => {\n if (err) {\n const e = err as NodeJS.ErrnoException & { killed?: boolean; signal?: string };\n if (e.code === \"ENOENT\") {\n reject(Object.assign(new Error(\"claude CLI not found\"), { code: \"ENOENT\" }));\n return;\n }\n if (e.killed || e.signal === \"SIGTERM\") {\n reject(Object.assign(new Error(\"claude timed out\"), { code: \"TIMEOUT\" }));\n return;\n }\n // Include stderr for context\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: e.code as unknown as number ?? 1 });\n return;\n }\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: 0 });\n }\n );\n child.on(\"error\", (err) => {\n reject(err);\n });\n });\n}\n\nexport async function callClaude(inputs: ClaudeCallInputs): Promise<EngineOutcome> {\n const prompt = buildClaudePrompt(inputs.config.detailLevel, {\n filePath: inputs.filePath,\n diff: inputs.diff,\n userPrompt: inputs.userPrompt,\n });\n\n try {\n const result = await runClaude(prompt, inputs.config.skipIfSlowMs);\n\n if (result.code !== 0) {\n const combined = `${result.stderr}\\n${result.stdout}`.toLowerCase();\n if (/auth|login|unauthorized|not authenticated|api key/i.test(combined)) {\n return {\n kind: \"error\",\n problem: \"Claude Code is not authenticated\",\n cause: \"The 'claude' CLI requires a valid login\",\n fix: \"Run 'claude login' in a terminal, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI returned an error\",\n cause: result.stderr.trim() || `exit code ${result.code}`,\n fix: \"Run 'claude --help' to verify the CLI works, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n\n if (!result.stdout.trim()) {\n return { kind: \"skip\", reason: \"Claude returned an empty response\" };\n }\n\n const parsed = parseResponse(result.stdout);\n if (parsed) {\n return { kind: \"ok\", result: parsed };\n }\n\n // Malformed output: fall back to truncated raw text.\n return {\n kind: \"ok\",\n result: {\n summary: truncateText(result.stdout.trim(), 200),\n risk: \"none\",\n riskReason: \"\",\n },\n };\n } catch (err) {\n const e = err as Error & { code?: string };\n if (e.code === \"ENOENT\") {\n return {\n kind: \"error\",\n problem: \"Claude CLI not found\",\n cause: \"The 'claude' command is not installed or not on PATH\",\n fix: \"Install Claude Code, or switch to Ollama engine via 'npx vibe-code-explainer config'\",\n };\n }\n if (e.code === \"TIMEOUT\") {\n return {\n kind: \"skip\",\n reason: `explanation took too long (>${inputs.config.skipIfSlowMs}ms)`,\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI invocation failed\",\n cause: e.message,\n fix: \"Check that 'claude' works by running 'claude --help' in a terminal\",\n };\n }\n}\n","import { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\n\nexport type DiffResult =\n | { kind: \"diff\"; content: string; lines: number; truncated: boolean }\n | { kind: \"new-file\"; content: string; lines: number; truncated: boolean }\n | { kind: \"binary\"; message: string }\n | { kind: \"empty\" }\n | { kind: \"skip\"; reason: string };\n\nconst MAX_DIFF_LINES = 200;\nconst HEAD_LINES = 150;\nconst TAIL_LINES = 50;\n\nfunction truncateDiff(content: string): { content: string; lines: number; truncated: boolean } {\n const lines = content.split(\"\\n\");\n if (lines.length <= MAX_DIFF_LINES) {\n return { content, lines: lines.length, truncated: false };\n }\n const head = lines.slice(0, HEAD_LINES);\n const tail = lines.slice(-TAIL_LINES);\n const omitted = lines.length - HEAD_LINES - TAIL_LINES;\n const truncated = [\n ...head,\n `[...truncated, ${omitted} more lines not shown]`,\n ...tail,\n ].join(\"\\n\");\n return { content: truncated, lines: lines.length, truncated: true };\n}\n\nfunction runGit(args: string[], cwd: string): string {\n return execFileSync(\"git\", args, { cwd, encoding: \"utf-8\", maxBuffer: 1024 * 1024 * 10 });\n}\n\n/**\n * Build a unified-style diff directly from an Edit tool's old_string/new_string\n * payload. More reliable than `git diff` because it works even on untracked\n * files (the common case: user asks Claude to edit a file that was just\n * created and never committed). Multi-line strings produce proper line-by-line\n * - / + markers so the model can tell additions apart from modifications.\n */\nexport function buildDiffFromEdit(\n filePath: string,\n oldString: string,\n newString: string\n): DiffResult {\n if (!oldString && !newString) return { kind: \"empty\" };\n\n const oldLines = oldString ? oldString.split(\"\\n\") : [];\n const newLines = newString ? newString.split(\"\\n\") : [];\n\n const header = `--- a/${filePath}\\n+++ b/${filePath}\\n@@ Edit @@`;\n const minus = oldLines.map((l) => `-${l}`).join(\"\\n\");\n const plus = newLines.map((l) => `+${l}`).join(\"\\n\");\n\n const parts = [header, minus, plus].filter((s) => s.length > 0);\n const content = parts.join(\"\\n\");\n\n const { content: final, lines, truncated } = truncateDiff(content);\n return { kind: \"diff\", content: final, lines, truncated };\n}\n\n/**\n * Build a combined unified-style diff from a MultiEdit payload's edits array.\n */\nexport function buildDiffFromMultiEdit(\n filePath: string,\n edits: Array<{ old_string?: string; new_string?: string; oldString?: string; newString?: string }>\n): DiffResult {\n if (!edits || edits.length === 0) return { kind: \"empty\" };\n\n const header = `--- a/${filePath}\\n+++ b/${filePath}`;\n const hunks: string[] = [];\n\n for (let i = 0; i < edits.length; i++) {\n const e = edits[i];\n const oldStr = e.old_string ?? e.oldString ?? \"\";\n const newStr = e.new_string ?? e.newString ?? \"\";\n if (!oldStr && !newStr) continue;\n\n const oldLines = oldStr ? oldStr.split(\"\\n\") : [];\n const newLines = newStr ? newStr.split(\"\\n\") : [];\n const minus = oldLines.map((l) => `-${l}`).join(\"\\n\");\n const plus = newLines.map((l) => `+${l}`).join(\"\\n\");\n\n hunks.push(`@@ Edit ${i + 1} of ${edits.length} @@`);\n if (minus) hunks.push(minus);\n if (plus) hunks.push(plus);\n }\n\n if (hunks.length === 0) return { kind: \"empty\" };\n\n const content = [header, ...hunks].join(\"\\n\");\n const { content: final, lines, truncated } = truncateDiff(content);\n return { kind: \"diff\", content: final, lines, truncated };\n}\n\nexport function extractEditDiff(filePath: string, cwd: string): DiffResult {\n // Check if we're in a git repo.\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n return { kind: \"skip\", reason: \"not inside a git repository\" };\n }\n\n // Check for binary.\n try {\n const numstat = runGit([\"diff\", \"--numstat\", \"--\", filePath], cwd).trim();\n if (numstat.startsWith(\"-\\t-\\t\")) {\n return { kind: \"binary\", message: `Binary file modified: ${filePath}` };\n }\n } catch {\n // Non-fatal, fall through to diff.\n }\n\n let diffOutput = \"\";\n try {\n diffOutput = runGit([\"diff\", \"--no-color\", \"--\", filePath], cwd);\n } catch {\n diffOutput = \"\";\n }\n\n if (!diffOutput.trim()) {\n // File may be untracked (newly created via Write/Edit on a fresh file).\n return extractNewFileDiff(filePath, cwd);\n }\n\n const { content, lines, truncated } = truncateDiff(diffOutput);\n return { kind: \"diff\", content, lines, truncated };\n}\n\nexport function extractNewFileDiff(filePath: string, cwd: string): DiffResult {\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n // Not a git repo — fall back to reading the file if possible.\n return readFileAsNewDiff(filePath);\n }\n\n // Check if file is untracked.\n let untracked = \"\";\n try {\n untracked = runGit([\"ls-files\", \"--others\", \"--exclude-standard\", \"--\", filePath], cwd).trim();\n } catch {\n untracked = \"\";\n }\n\n if (untracked) {\n return readFileAsNewDiff(filePath);\n }\n\n // Might be a file with no changes, or tracked without a diff.\n return { kind: \"empty\" };\n}\n\nfunction readFileAsNewDiff(filePath: string): DiffResult {\n if (!existsSync(filePath)) {\n return { kind: \"skip\", reason: `file not found: ${filePath}` };\n }\n\n try {\n const stat = statSync(filePath);\n if (stat.size > 2 * 1024 * 1024) {\n return { kind: \"skip\", reason: `file too large (${Math.round(stat.size / 1024)}KB)` };\n }\n\n const raw = readFileSync(filePath, \"utf-8\");\n if (!raw.trim()) {\n return { kind: \"empty\" };\n }\n\n // Check for binary content (null bytes).\n if (raw.includes(\"\\0\")) {\n return { kind: \"binary\", message: `Binary file created: ${filePath}` };\n }\n\n const withMarkers = raw.split(\"\\n\").map((l) => `+ ${l}`).join(\"\\n\");\n const diff = `--- /dev/null\\n+++ b/${filePath}\\n${withMarkers}`;\n const { content, lines, truncated } = truncateDiff(diff);\n return { kind: \"new-file\", content, lines, truncated };\n } catch {\n return { kind: \"skip\", reason: \"could not read file\" };\n }\n}\n\n/**\n * Minimal glob matcher supporting *, **, and simple extensions.\n * Matches POSIX-style paths (caller normalizes).\n *\n * - `*.ext` matches `file.ext` in any directory\n * - `dir/**` matches anything under `dir/` recursively\n * - `**\\/file.ts` matches `file.ts` anywhere\n */\nexport function matchesGlob(filePath: string, pattern: string): boolean {\n const normalized = filePath.replace(/\\\\/g, \"/\");\n const normalizedPattern = pattern.replace(/\\\\/g, \"/\");\n\n // Build regex from the pattern\n let regexSrc = \"\";\n let i = 0;\n while (i < normalizedPattern.length) {\n const ch = normalizedPattern[i];\n if (ch === \"*\") {\n if (normalizedPattern[i + 1] === \"*\") {\n // ** matches anything (including /)\n regexSrc += \".*\";\n i += 2;\n if (normalizedPattern[i] === \"/\") i++; // consume trailing /\n } else {\n // * matches anything except /\n regexSrc += \"[^/]*\";\n i++;\n }\n } else if (ch === \"?\") {\n regexSrc += \"[^/]\";\n i++;\n } else if (/[.+^${}()|[\\]]/.test(ch)) {\n regexSrc += \"\\\\\" + ch;\n i++;\n } else {\n regexSrc += ch;\n i++;\n }\n }\n\n // If the pattern has no directory component, match the filename anywhere.\n const hasSlash = normalizedPattern.includes(\"/\");\n const anchored = hasSlash\n ? new RegExp(`^${regexSrc}$`)\n : new RegExp(`(^|/)${regexSrc}$`);\n\n return anchored.test(normalized);\n}\n\nexport function isExcluded(filePath: string, patterns: string[]): boolean {\n return patterns.some((p) => matchesGlob(filePath, p));\n}\n","/**\n * Bash command filter — decides whether a Bash command should trigger an\n * explanation. Filters to filesystem-mutating and state-changing commands\n * while skipping read-only operations.\n */\n\n// Commands that modify filesystem or project state.\nconst MUTATING_COMMANDS = new Set([\n \"rm\",\n \"mv\",\n \"cp\",\n \"mkdir\",\n \"rmdir\",\n \"chmod\",\n \"chown\",\n \"ln\",\n \"touch\",\n \"dd\",\n]);\n\n// Commands that need a specific subcommand/flag to be mutating.\nconst CONTEXTUAL_COMMANDS: Record<string, RegExp> = {\n npm: /\\b(install|add|remove|uninstall|update|ci|link|unlink|init|publish)\\b/,\n yarn: /\\b(add|remove|install|upgrade|init|publish|link|unlink)\\b/,\n pnpm: /\\b(add|remove|install|update|link|unlink|publish)\\b/,\n pip: /\\b(install|uninstall)\\b/,\n pip3: /\\b(install|uninstall)\\b/,\n brew: /\\b(install|uninstall|reinstall|upgrade|link|unlink|tap|untap)\\b/,\n apt: /\\b(install|remove|purge|upgrade|update)\\b/,\n \"apt-get\": /\\b(install|remove|purge|upgrade|update)\\b/,\n git: /\\b(checkout|reset|revert|rebase|merge|commit|push|pull|clean|stash|rm|mv|init|clone|cherry-pick|restore|switch)\\b/,\n sed: /(?:^|\\s)-i\\b/,\n curl: /(?:^|\\s)-[a-zA-Z]*o\\b|--output\\b/,\n wget: /.*/,\n tar: /(?:^|\\s)-[a-zA-Z]*x\\b|--extract\\b|(?:^|\\s)-[a-zA-Z]*c\\b|--create\\b/,\n unzip: /.*/,\n docker: /\\b(run|build|push|pull|rm|rmi|exec|start|stop|kill)\\b/,\n make: /.*/,\n cargo: /\\b(build|run|install|add|remove|update|publish)\\b/,\n go: /\\b(build|install|get|mod)\\b/,\n bun: /\\b(install|add|remove|run|build|init|create|link|unlink)\\b/,\n deno: /\\b(install|compile|bundle|run)\\b/,\n};\n\n// Commands that are always read-only and never trigger.\nconst READONLY_COMMANDS = new Set([\n \"ls\",\n \"cat\",\n \"head\",\n \"tail\",\n \"grep\",\n \"find\",\n \"which\",\n \"whereis\",\n \"type\",\n \"echo\",\n \"printf\",\n \"pwd\",\n \"whoami\",\n \"id\",\n \"date\",\n \"uname\",\n \"df\",\n \"du\",\n \"ps\",\n \"top\",\n \"htop\",\n \"stat\",\n \"file\",\n \"wc\",\n \"sort\",\n \"uniq\",\n \"diff\",\n \"man\",\n \"help\",\n \"history\",\n \"tree\",\n \"less\",\n \"more\",\n \"env\",\n \"printenv\",\n \"test\",\n \"true\",\n \"false\",\n]);\n\n/**\n * Split a command string on pipe, semicolon, and logical operators.\n * Returns each sub-command with leading whitespace trimmed.\n */\nexport function splitCommandChain(command: string): string[] {\n // Split on unquoted chain operators. A simple split is good enough for\n // the vibe-coder case; we explicitly do not handle exotic quoting.\n return command\n .split(/(?:\\|\\||&&|[|;])/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Check if a single sub-command (e.g., \"rm file.txt\") should trigger.\n */\nexport function subCommandShouldCapture(subCmd: string): boolean {\n // Detect redirections (> or >>) — always capture.\n if (/(?<!\\d)>>?(?!\\d)/.test(subCmd)) {\n // Bare redirections like `ls > out.txt` still count as mutating.\n return true;\n }\n\n const tokens = subCmd.trim().split(/\\s+/);\n if (tokens.length === 0) return false;\n\n // Skip env-var assignments like `FOO=bar cmd`.\n let idx = 0;\n while (idx < tokens.length && /^[A-Z_][A-Z0-9_]*=/.test(tokens[idx])) {\n idx++;\n }\n const head = tokens[idx];\n if (!head) return false;\n\n // Strip leading path (e.g., /usr/bin/rm -> rm).\n const bin = head.split(/[/\\\\]/).pop() ?? head;\n\n if (READONLY_COMMANDS.has(bin)) return false;\n if (MUTATING_COMMANDS.has(bin)) return true;\n\n const contextPattern = CONTEXTUAL_COMMANDS[bin];\n if (contextPattern) {\n const rest = tokens.slice(idx + 1).join(\" \");\n return contextPattern.test(rest);\n }\n\n return false;\n}\n\n/**\n * Decide whether a full command string should trigger a code-explainer\n * explanation. Returns true if ANY sub-command in the chain is mutating.\n */\nexport function shouldCaptureBash(command: string): boolean {\n const parts = splitCommandChain(command);\n return parts.some((p) => subCommandShouldCapture(p));\n}\n","import type { SessionEntry } from \"./tracker.js\";\n\nconst SENSITIVE_PATTERNS = [\n /(^|\\/)\\.env(\\.|$)/i,\n /(^|\\/)payment/i,\n /(^|\\/)billing/i,\n /(^|\\/)stripe/i,\n /(^|\\/)auth/i,\n /(^|\\/)credential/i,\n /(^|\\/)secret/i,\n /(^|\\/)\\.ssh\\//i,\n];\n\nfunction topLevelDir(path: string): string {\n const norm = path.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n const parts = norm.split(\"/\").filter(Boolean);\n return parts[0] ?? \"\";\n}\n\nexport function matchesSensitivePattern(filePath: string): boolean {\n return SENSITIVE_PATTERNS.some((re) => re.test(filePath));\n}\n\nexport interface DriftAnalysis {\n isUnrelated: boolean;\n reason?: string;\n}\n\n/**\n * Path-heuristic drift detection for the Ollama engine.\n * Flags a new file as unrelated if:\n * 1. It matches a sensitive pattern (env, payment, auth, secrets) AND\n * the session did not start in a similarly-sensitive area.\n * 2. It lives in a different top-level directory than every file\n * edited so far in the session (cross-module drift).\n *\n * Returns `isUnrelated: false` for the first few edits (not enough\n * context to judge).\n */\nexport function analyzeDrift(\n newFilePath: string,\n priorEntries: SessionEntry[]\n): DriftAnalysis {\n // Not enough context yet for the first edit.\n if (priorEntries.length === 0) {\n return { isUnrelated: false };\n }\n\n const priorFiles = Array.from(new Set(priorEntries.map((e) => e.file)));\n const priorTopDirs = new Set(priorFiles.map(topLevelDir));\n const priorHasSensitive = priorFiles.some(matchesSensitivePattern);\n\n // Sensitive-pattern drift: the new file is in a sensitive area but\n // prior session was not working there.\n if (matchesSensitivePattern(newFilePath) && !priorHasSensitive) {\n return {\n isUnrelated: true,\n reason: `touches sensitive area (${newFilePath}) that was not part of earlier edits`,\n };\n }\n\n // Cross-module drift: only flag after at least 2 prior edits established\n // a working area.\n if (priorEntries.length >= 2) {\n const newTop = topLevelDir(newFilePath);\n if (newTop && !priorTopDirs.has(newTop)) {\n return {\n isUnrelated: true,\n reason: `is in a different top-level area (${newTop}) than earlier edits (${Array.from(priorTopDirs).join(\", \")})`,\n };\n }\n }\n\n return { isUnrelated: false };\n}\n\nexport interface DriftThresholdResult {\n shouldAlert: boolean;\n totalFiles: number;\n unrelatedFiles: string[];\n}\n\nconst DRIFT_ALERT_THRESHOLD = 3;\n\n/**\n * Decide whether to surface a drift alert based on accumulated session state.\n * Fires once every time the unrelated count crosses a multiple of the threshold.\n */\nexport function shouldAlertDrift(entries: SessionEntry[]): DriftThresholdResult {\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(\n new Set(entries.filter((e) => e.unrelated).map((e) => e.file))\n );\n\n // Alert exactly when we hit the threshold (not every call afterwards).\n const shouldAlert =\n unrelatedFiles.length > 0 &&\n unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0 &&\n entries.filter((e) => e.unrelated).length ===\n entries.filter((e) => e.unrelated).length;\n\n // Fire specifically on the edit that caused us to cross the threshold.\n const lastEntry = entries[entries.length - 1];\n const lastWasUnrelated = lastEntry?.unrelated ?? false;\n const crossedThreshold =\n lastWasUnrelated && unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0;\n\n return {\n shouldAlert: crossedThreshold && shouldAlert,\n totalFiles: uniqueFiles.length,\n unrelatedFiles,\n };\n}\n"],"mappings":"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","names":["existsSync","existsSync","result"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/post-tool.ts","../../src/engines/claude.ts","../../src/hooks/diff-extractor.ts","../../src/filter/bash-filter.ts","../../src/session/drift.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { loadConfig, DEFAULT_CONFIG } from \"../config/schema.js\";\nimport type { Config, HookPayload, ExplanationResult } from \"../config/schema.js\";\nimport { callOllama, type EngineOutcome } from \"../engines/ollama.js\";\nimport { callClaude } from \"../engines/claude.js\";\nimport {\n extractEditDiff,\n extractNewFileDiff,\n buildDiffFromEdit,\n buildDiffFromMultiEdit,\n isExcluded,\n} from \"./diff-extractor.js\";\nimport { shouldCaptureBash } from \"../filter/bash-filter.js\";\nimport { formatExplanationBox, formatDriftAlert, formatSkipNotice, formatErrorNotice } from \"../format/box.js\";\nimport { recordEntry, readSession, cleanStaleSessionFiles } from \"../session/tracker.js\";\nimport { analyzeDrift, shouldAlertDrift } from \"../session/drift.js\";\nimport { getCached, setCached } from \"../cache/explanation-cache.js\";\n\nconst output: string[] = [];\n\nfunction addOutput(text: string): void {\n output.push(text);\n}\n\n/**\n * Emit the Claude Code hook JSON on stdout so the accumulated output\n * appears as a system message in the user's terminal. Always exit 0 so\n * Claude Code is never blocked.\n */\nfunction safeExit(): never {\n if (output.length > 0) {\n // Leading newline separates the box from Claude Code's \"PostToolUse:X says:\"\n // prefix, which otherwise renders on the same line as the top border.\n const systemMessage = \"\\n\" + output.join(\"\\n\");\n process.stdout.write(JSON.stringify({ systemMessage }) + \"\\n\");\n }\n process.exit(0);\n}\n\nasync function readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let data = \"\";\n process.stdin.setEncoding(\"utf-8\");\n process.stdin.on(\"data\", (chunk) => {\n data += chunk;\n });\n process.stdin.on(\"end\", () => resolve(data));\n process.stdin.on(\"error\", () => resolve(data));\n // Safety timeout: if stdin has no data in 2s, resolve empty.\n setTimeout(() => resolve(data), 2000);\n });\n}\n\nfunction parsePayload(raw: string): HookPayload | null {\n try {\n const parsed = JSON.parse(raw);\n if (typeof parsed.session_id === \"string\" && typeof parsed.tool_name === \"string\") {\n return parsed as HookPayload;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction loadConfigSafe(cwd: string): Config {\n const path = join(cwd, \"code-explainer.config.json\");\n if (!existsSync(path)) return DEFAULT_CONFIG;\n return loadConfig(path);\n}\n\nfunction isHookEnabled(toolName: string, config: Config): boolean {\n const lower = toolName.toLowerCase();\n if (lower === \"edit\" || lower === \"multiedit\") return config.hooks.edit;\n if (lower === \"write\") return config.hooks.write;\n if (lower === \"bash\") return config.hooks.bash;\n return false;\n}\n\nasync function runEngine(\n filePath: string,\n diff: string,\n config: Config,\n userPrompt: string | undefined,\n signal: AbortSignal\n): Promise<EngineOutcome> {\n if (signal.aborted) {\n return { kind: \"skip\", reason: \"interrupted by user\" };\n }\n if (config.engine === \"ollama\") {\n return callOllama({ filePath, diff, config });\n }\n return callClaude({ filePath, diff, config, userPrompt });\n}\n\nasync function main(): Promise<void> {\n // Interrupt handler — always exit 0 on Ctrl+C.\n const controller = new AbortController();\n process.on(\"SIGINT\", () => {\n controller.abort();\n addOutput(formatSkipNotice(\"interrupted by user\"));\n safeExit();\n });\n\n const raw = await readStdin();\n if (!raw.trim()) safeExit();\n\n const payload = parsePayload(raw);\n if (!payload) safeExit();\n\n const cwd = payload.cwd || process.cwd();\n const config = loadConfigSafe(cwd);\n\n if (!isHookEnabled(payload.tool_name, config)) safeExit();\n\n cleanStaleSessionFiles();\n\n // Pass session_id to downstream modules via env (so summary/session-end\n // commands pick the right session without re-parsing the payload).\n process.env.CODE_EXPLAINER_SESSION_ID = payload.session_id;\n\n // Extract the diff based on tool name.\n let filePath: string;\n let diff: string;\n let isNewFile = false;\n\n const lowerTool = payload.tool_name.toLowerCase();\n if (lowerTool === \"edit\" || lowerTool === \"multiedit\" || lowerTool === \"write\") {\n const input = payload.tool_input as {\n file_path?: string;\n filePath?: string;\n old_string?: string;\n new_string?: string;\n oldString?: string;\n newString?: string;\n edits?: Array<{ old_string?: string; new_string?: string; oldString?: string; newString?: string }>;\n };\n const target = input.file_path ?? input.filePath;\n if (!target) safeExit();\n filePath = target as string;\n\n if (isExcluded(filePath, config.exclude)) safeExit();\n\n // Preferred path: use the payload's old/new strings directly. This works\n // for untracked files (very common) and is always more accurate than git\n // diff, which may miss changes on files that were created and edited in\n // the same session without a commit.\n let result;\n if (lowerTool === \"edit\") {\n const oldStr = input.old_string ?? input.oldString ?? \"\";\n const newStr = input.new_string ?? input.newString ?? \"\";\n if (oldStr || newStr) {\n result = buildDiffFromEdit(filePath, oldStr, newStr);\n } else {\n result = extractEditDiff(filePath, cwd);\n }\n } else if (lowerTool === \"multiedit\") {\n if (input.edits && input.edits.length > 0) {\n result = buildDiffFromMultiEdit(filePath, input.edits);\n } else {\n result = extractEditDiff(filePath, cwd);\n }\n } else {\n result = extractNewFileDiff(filePath, cwd);\n }\n\n if (result.kind === \"empty\") safeExit();\n if (result.kind === \"skip\") {\n addOutput(formatSkipNotice(result.reason));\n safeExit();\n }\n if (result.kind === \"binary\") {\n addOutput(formatSkipNotice(result.message));\n safeExit();\n }\n diff = result.content;\n isNewFile = result.kind === \"new-file\";\n } else if (lowerTool === \"bash\") {\n const input = payload.tool_input as { command?: string };\n const command = input.command ?? \"\";\n if (!command || !shouldCaptureBash(command)) safeExit();\n filePath = \"<bash command>\";\n diff = command;\n } else {\n safeExit();\n }\n\n // Cache check.\n const cacheKey = `${filePath}\\n${diff}`;\n const cached = getCached(payload.session_id, cacheKey);\n let result: ExplanationResult | null = null;\n\n if (cached) {\n result = cached;\n } else {\n const outcome = await runEngine(filePath, diff, config, undefined, controller.signal);\n if (outcome.kind === \"skip\") {\n addOutput(formatSkipNotice(outcome.reason));\n safeExit();\n }\n if (outcome.kind === \"error\") {\n addOutput(formatErrorNotice(outcome.problem, outcome.cause, outcome.fix));\n safeExit();\n }\n result = outcome.result;\n setCached(payload.session_id, cacheKey, result);\n }\n\n // Path-heuristic drift analysis (only meaningful for Edit/Write).\n let driftReason: string | undefined;\n if (!isNewFile || filePath !== \"<bash command>\") {\n const priorEntries = readSession(payload.session_id);\n if (filePath !== \"<bash command>\") {\n const analysis = analyzeDrift(filePath, priorEntries);\n if (analysis.isUnrelated) {\n driftReason = analysis.reason;\n }\n }\n }\n\n // Print the explanation box.\n addOutput(formatExplanationBox(filePath, result.summary, result.risk, result.riskReason));\n\n // Record the entry.\n recordEntry(payload.session_id, {\n file: filePath,\n timestamp: Date.now(),\n risk: result.risk,\n summary: result.summary,\n unrelated: !!driftReason,\n });\n\n // Drift alert at threshold.\n const updated = readSession(payload.session_id);\n const driftCheck = shouldAlertDrift(updated);\n if (driftCheck.shouldAlert) {\n addOutput(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles));\n }\n\n safeExit();\n}\n\nmain().catch(() => {\n // Never fail the hook — always exit 0.\n safeExit();\n});\n","import { execFile } from \"node:child_process\";\nimport type { Config, ExplanationResult, RiskLevel } from \"../config/schema.js\";\nimport { buildClaudePrompt } from \"../prompts/templates.js\";\nimport type { EngineOutcome } from \"./ollama.js\";\n\nexport interface ClaudeCallInputs {\n filePath: string;\n diff: string;\n config: Config;\n userPrompt?: string;\n}\n\nfunction extractJson(text: string): string | null {\n const trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n const fenceMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?```/);\n if (fenceMatch) {\n return fenceMatch[1].trim();\n }\n const start = trimmed.indexOf(\"{\");\n const end = trimmed.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1 && end > start) {\n return trimmed.slice(start, end + 1);\n }\n return null;\n}\n\nfunction parseResponse(rawText: string): ExplanationResult | null {\n const json = extractJson(rawText);\n if (!json) return null;\n try {\n const parsed = JSON.parse(json);\n if (\n typeof parsed.summary === \"string\" &&\n typeof parsed.risk === \"string\" &&\n typeof parsed.riskReason === \"string\"\n ) {\n const risk = parsed.risk as RiskLevel;\n if (![\"none\", \"low\", \"medium\", \"high\"].includes(risk)) return null;\n return { summary: parsed.summary, risk, riskReason: parsed.riskReason };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction truncateText(text: string, max: number): string {\n if (text.length <= max) return text;\n return text.slice(0, max) + \"...\";\n}\n\ninterface ExecResult {\n stdout: string;\n stderr: string;\n code: number | null;\n}\n\nfunction runClaude(prompt: string, timeoutMs: number): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const child = execFile(\n \"claude\",\n [\"-p\", prompt],\n {\n timeout: timeoutMs,\n maxBuffer: 1024 * 1024 * 2, // 2MB\n windowsHide: true,\n },\n (err, stdout, stderr) => {\n if (err) {\n const e = err as NodeJS.ErrnoException & { killed?: boolean; signal?: string };\n if (e.code === \"ENOENT\") {\n reject(Object.assign(new Error(\"claude CLI not found\"), { code: \"ENOENT\" }));\n return;\n }\n if (e.killed || e.signal === \"SIGTERM\") {\n reject(Object.assign(new Error(\"claude timed out\"), { code: \"TIMEOUT\" }));\n return;\n }\n // Include stderr for context\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: e.code as unknown as number ?? 1 });\n return;\n }\n resolve({ stdout: stdout.toString(), stderr: stderr.toString(), code: 0 });\n }\n );\n child.on(\"error\", (err) => {\n reject(err);\n });\n });\n}\n\nexport async function callClaude(inputs: ClaudeCallInputs): Promise<EngineOutcome> {\n const prompt = buildClaudePrompt(inputs.config.detailLevel, {\n filePath: inputs.filePath,\n diff: inputs.diff,\n userPrompt: inputs.userPrompt,\n });\n\n try {\n const result = await runClaude(prompt, inputs.config.skipIfSlowMs);\n\n if (result.code !== 0) {\n const combined = `${result.stderr}\\n${result.stdout}`.toLowerCase();\n if (/auth|login|unauthorized|not authenticated|api key/i.test(combined)) {\n return {\n kind: \"error\",\n problem: \"Claude Code is not authenticated\",\n cause: \"The 'claude' CLI requires a valid login\",\n fix: \"Run 'claude login' in a terminal, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI returned an error\",\n cause: result.stderr.trim() || `exit code ${result.code}`,\n fix: \"Run 'claude --help' to verify the CLI works, or switch engines via 'npx vibe-code-explainer config'\",\n };\n }\n\n if (!result.stdout.trim()) {\n return { kind: \"skip\", reason: \"Claude returned an empty response\" };\n }\n\n const parsed = parseResponse(result.stdout);\n if (parsed) {\n return { kind: \"ok\", result: parsed };\n }\n\n // Malformed output: fall back to truncated raw text.\n return {\n kind: \"ok\",\n result: {\n summary: truncateText(result.stdout.trim(), 200),\n risk: \"none\",\n riskReason: \"\",\n },\n };\n } catch (err) {\n const e = err as Error & { code?: string };\n if (e.code === \"ENOENT\") {\n return {\n kind: \"error\",\n problem: \"Claude CLI not found\",\n cause: \"The 'claude' command is not installed or not on PATH\",\n fix: \"Install Claude Code, or switch to Ollama engine via 'npx vibe-code-explainer config'\",\n };\n }\n if (e.code === \"TIMEOUT\") {\n return {\n kind: \"skip\",\n reason: `explanation took too long (>${inputs.config.skipIfSlowMs}ms)`,\n };\n }\n return {\n kind: \"error\",\n problem: \"Claude CLI invocation failed\",\n cause: e.message,\n fix: \"Check that 'claude' works by running 'claude --help' in a terminal\",\n };\n }\n}\n","import { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\n\nexport type DiffResult =\n | { kind: \"diff\"; content: string; lines: number; truncated: boolean }\n | { kind: \"new-file\"; content: string; lines: number; truncated: boolean }\n | { kind: \"binary\"; message: string }\n | { kind: \"empty\" }\n | { kind: \"skip\"; reason: string };\n\nconst MAX_DIFF_LINES = 200;\nconst HEAD_LINES = 150;\nconst TAIL_LINES = 50;\n\nfunction truncateDiff(content: string): { content: string; lines: number; truncated: boolean } {\n const lines = content.split(\"\\n\");\n if (lines.length <= MAX_DIFF_LINES) {\n return { content, lines: lines.length, truncated: false };\n }\n const head = lines.slice(0, HEAD_LINES);\n const tail = lines.slice(-TAIL_LINES);\n const omitted = lines.length - HEAD_LINES - TAIL_LINES;\n const truncated = [\n ...head,\n `[...truncated, ${omitted} more lines not shown]`,\n ...tail,\n ].join(\"\\n\");\n return { content: truncated, lines: lines.length, truncated: true };\n}\n\nfunction runGit(args: string[], cwd: string): string {\n return execFileSync(\"git\", args, { cwd, encoding: \"utf-8\", maxBuffer: 1024 * 1024 * 10 });\n}\n\n/**\n * Build a unified-style diff directly from an Edit tool's old_string/new_string\n * payload. More reliable than `git diff` because it works even on untracked\n * files (the common case: user asks Claude to edit a file that was just\n * created and never committed). Multi-line strings produce proper line-by-line\n * - / + markers so the model can tell additions apart from modifications.\n */\nexport function buildDiffFromEdit(\n filePath: string,\n oldString: string,\n newString: string\n): DiffResult {\n if (!oldString && !newString) return { kind: \"empty\" };\n\n const oldLines = oldString ? oldString.split(\"\\n\") : [];\n const newLines = newString ? newString.split(\"\\n\") : [];\n\n const header = `--- a/${filePath}\\n+++ b/${filePath}\\n@@ Edit @@`;\n const minus = oldLines.map((l) => `-${l}`).join(\"\\n\");\n const plus = newLines.map((l) => `+${l}`).join(\"\\n\");\n\n const parts = [header, minus, plus].filter((s) => s.length > 0);\n const content = parts.join(\"\\n\");\n\n const { content: final, lines, truncated } = truncateDiff(content);\n return { kind: \"diff\", content: final, lines, truncated };\n}\n\n/**\n * Build a combined unified-style diff from a MultiEdit payload's edits array.\n */\nexport function buildDiffFromMultiEdit(\n filePath: string,\n edits: Array<{ old_string?: string; new_string?: string; oldString?: string; newString?: string }>\n): DiffResult {\n if (!edits || edits.length === 0) return { kind: \"empty\" };\n\n const header = `--- a/${filePath}\\n+++ b/${filePath}`;\n const hunks: string[] = [];\n\n for (let i = 0; i < edits.length; i++) {\n const e = edits[i];\n const oldStr = e.old_string ?? e.oldString ?? \"\";\n const newStr = e.new_string ?? e.newString ?? \"\";\n if (!oldStr && !newStr) continue;\n\n const oldLines = oldStr ? oldStr.split(\"\\n\") : [];\n const newLines = newStr ? newStr.split(\"\\n\") : [];\n const minus = oldLines.map((l) => `-${l}`).join(\"\\n\");\n const plus = newLines.map((l) => `+${l}`).join(\"\\n\");\n\n hunks.push(`@@ Edit ${i + 1} of ${edits.length} @@`);\n if (minus) hunks.push(minus);\n if (plus) hunks.push(plus);\n }\n\n if (hunks.length === 0) return { kind: \"empty\" };\n\n const content = [header, ...hunks].join(\"\\n\");\n const { content: final, lines, truncated } = truncateDiff(content);\n return { kind: \"diff\", content: final, lines, truncated };\n}\n\nexport function extractEditDiff(filePath: string, cwd: string): DiffResult {\n // Check if we're in a git repo.\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n return { kind: \"skip\", reason: \"not inside a git repository\" };\n }\n\n // Check for binary.\n try {\n const numstat = runGit([\"diff\", \"--numstat\", \"--\", filePath], cwd).trim();\n if (numstat.startsWith(\"-\\t-\\t\")) {\n return { kind: \"binary\", message: `Binary file modified: ${filePath}` };\n }\n } catch {\n // Non-fatal, fall through to diff.\n }\n\n let diffOutput = \"\";\n try {\n diffOutput = runGit([\"diff\", \"--no-color\", \"--\", filePath], cwd);\n } catch {\n diffOutput = \"\";\n }\n\n if (!diffOutput.trim()) {\n // File may be untracked (newly created via Write/Edit on a fresh file).\n return extractNewFileDiff(filePath, cwd);\n }\n\n const { content, lines, truncated } = truncateDiff(diffOutput);\n return { kind: \"diff\", content, lines, truncated };\n}\n\nexport function extractNewFileDiff(filePath: string, cwd: string): DiffResult {\n try {\n runGit([\"rev-parse\", \"--is-inside-work-tree\"], cwd);\n } catch {\n // Not a git repo — fall back to reading the file if possible.\n return readFileAsNewDiff(filePath);\n }\n\n // Check if file is untracked.\n let untracked = \"\";\n try {\n untracked = runGit([\"ls-files\", \"--others\", \"--exclude-standard\", \"--\", filePath], cwd).trim();\n } catch {\n untracked = \"\";\n }\n\n if (untracked) {\n return readFileAsNewDiff(filePath);\n }\n\n // Might be a file with no changes, or tracked without a diff.\n return { kind: \"empty\" };\n}\n\nfunction readFileAsNewDiff(filePath: string): DiffResult {\n if (!existsSync(filePath)) {\n return { kind: \"skip\", reason: `file not found: ${filePath}` };\n }\n\n try {\n const stat = statSync(filePath);\n if (stat.size > 2 * 1024 * 1024) {\n return { kind: \"skip\", reason: `file too large (${Math.round(stat.size / 1024)}KB)` };\n }\n\n const raw = readFileSync(filePath, \"utf-8\");\n if (!raw.trim()) {\n return { kind: \"empty\" };\n }\n\n // Check for binary content (null bytes).\n if (raw.includes(\"\\0\")) {\n return { kind: \"binary\", message: `Binary file created: ${filePath}` };\n }\n\n const withMarkers = raw.split(\"\\n\").map((l) => `+ ${l}`).join(\"\\n\");\n const diff = `--- /dev/null\\n+++ b/${filePath}\\n${withMarkers}`;\n const { content, lines, truncated } = truncateDiff(diff);\n return { kind: \"new-file\", content, lines, truncated };\n } catch {\n return { kind: \"skip\", reason: \"could not read file\" };\n }\n}\n\n/**\n * Minimal glob matcher supporting *, **, and simple extensions.\n * Matches POSIX-style paths (caller normalizes).\n *\n * - `*.ext` matches `file.ext` in any directory\n * - `dir/**` matches anything under `dir/` recursively\n * - `**\\/file.ts` matches `file.ts` anywhere\n */\nexport function matchesGlob(filePath: string, pattern: string): boolean {\n const normalized = filePath.replace(/\\\\/g, \"/\");\n const normalizedPattern = pattern.replace(/\\\\/g, \"/\");\n\n // Build regex from the pattern\n let regexSrc = \"\";\n let i = 0;\n while (i < normalizedPattern.length) {\n const ch = normalizedPattern[i];\n if (ch === \"*\") {\n if (normalizedPattern[i + 1] === \"*\") {\n // ** matches anything (including /)\n regexSrc += \".*\";\n i += 2;\n if (normalizedPattern[i] === \"/\") i++; // consume trailing /\n } else {\n // * matches anything except /\n regexSrc += \"[^/]*\";\n i++;\n }\n } else if (ch === \"?\") {\n regexSrc += \"[^/]\";\n i++;\n } else if (/[.+^${}()|[\\]]/.test(ch)) {\n regexSrc += \"\\\\\" + ch;\n i++;\n } else {\n regexSrc += ch;\n i++;\n }\n }\n\n // If the pattern has no directory component, match the filename anywhere.\n const hasSlash = normalizedPattern.includes(\"/\");\n const anchored = hasSlash\n ? new RegExp(`^${regexSrc}$`)\n : new RegExp(`(^|/)${regexSrc}$`);\n\n return anchored.test(normalized);\n}\n\nexport function isExcluded(filePath: string, patterns: string[]): boolean {\n return patterns.some((p) => matchesGlob(filePath, p));\n}\n","/**\n * Bash command filter — decides whether a Bash command should trigger an\n * explanation. Filters to filesystem-mutating and state-changing commands\n * while skipping read-only operations.\n */\n\n// Commands that modify filesystem or project state.\nconst MUTATING_COMMANDS = new Set([\n \"rm\",\n \"mv\",\n \"cp\",\n \"mkdir\",\n \"rmdir\",\n \"chmod\",\n \"chown\",\n \"ln\",\n \"touch\",\n \"dd\",\n]);\n\n// Commands that need a specific subcommand/flag to be mutating.\nconst CONTEXTUAL_COMMANDS: Record<string, RegExp> = {\n npm: /\\b(install|add|remove|uninstall|update|ci|link|unlink|init|publish)\\b/,\n yarn: /\\b(add|remove|install|upgrade|init|publish|link|unlink)\\b/,\n pnpm: /\\b(add|remove|install|update|link|unlink|publish)\\b/,\n pip: /\\b(install|uninstall)\\b/,\n pip3: /\\b(install|uninstall)\\b/,\n brew: /\\b(install|uninstall|reinstall|upgrade|link|unlink|tap|untap)\\b/,\n apt: /\\b(install|remove|purge|upgrade|update)\\b/,\n \"apt-get\": /\\b(install|remove|purge|upgrade|update)\\b/,\n git: /\\b(checkout|reset|revert|rebase|merge|commit|push|pull|clean|stash|rm|mv|init|clone|cherry-pick|restore|switch)\\b/,\n sed: /(?:^|\\s)-i\\b/,\n curl: /(?:^|\\s)-[a-zA-Z]*o\\b|--output\\b/,\n wget: /.*/,\n tar: /(?:^|\\s)-[a-zA-Z]*x\\b|--extract\\b|(?:^|\\s)-[a-zA-Z]*c\\b|--create\\b/,\n unzip: /.*/,\n docker: /\\b(run|build|push|pull|rm|rmi|exec|start|stop|kill)\\b/,\n make: /.*/,\n cargo: /\\b(build|run|install|add|remove|update|publish)\\b/,\n go: /\\b(build|install|get|mod)\\b/,\n bun: /\\b(install|add|remove|run|build|init|create|link|unlink)\\b/,\n deno: /\\b(install|compile|bundle|run)\\b/,\n};\n\n// Commands that are always read-only and never trigger.\nconst READONLY_COMMANDS = new Set([\n \"ls\",\n \"cat\",\n \"head\",\n \"tail\",\n \"grep\",\n \"find\",\n \"which\",\n \"whereis\",\n \"type\",\n \"echo\",\n \"printf\",\n \"pwd\",\n \"whoami\",\n \"id\",\n \"date\",\n \"uname\",\n \"df\",\n \"du\",\n \"ps\",\n \"top\",\n \"htop\",\n \"stat\",\n \"file\",\n \"wc\",\n \"sort\",\n \"uniq\",\n \"diff\",\n \"man\",\n \"help\",\n \"history\",\n \"tree\",\n \"less\",\n \"more\",\n \"env\",\n \"printenv\",\n \"test\",\n \"true\",\n \"false\",\n]);\n\n/**\n * Split a command string on pipe, semicolon, and logical operators.\n * Returns each sub-command with leading whitespace trimmed.\n */\nexport function splitCommandChain(command: string): string[] {\n // Split on unquoted chain operators. A simple split is good enough for\n // the vibe-coder case; we explicitly do not handle exotic quoting.\n return command\n .split(/(?:\\|\\||&&|[|;])/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Check if a single sub-command (e.g., \"rm file.txt\") should trigger.\n */\nexport function subCommandShouldCapture(subCmd: string): boolean {\n // Detect redirections (> or >>) — always capture.\n if (/(?<!\\d)>>?(?!\\d)/.test(subCmd)) {\n // Bare redirections like `ls > out.txt` still count as mutating.\n return true;\n }\n\n const tokens = subCmd.trim().split(/\\s+/);\n if (tokens.length === 0) return false;\n\n // Skip env-var assignments like `FOO=bar cmd`.\n let idx = 0;\n while (idx < tokens.length && /^[A-Z_][A-Z0-9_]*=/.test(tokens[idx])) {\n idx++;\n }\n const head = tokens[idx];\n if (!head) return false;\n\n // Strip leading path (e.g., /usr/bin/rm -> rm).\n const bin = head.split(/[/\\\\]/).pop() ?? head;\n\n if (READONLY_COMMANDS.has(bin)) return false;\n if (MUTATING_COMMANDS.has(bin)) return true;\n\n const contextPattern = CONTEXTUAL_COMMANDS[bin];\n if (contextPattern) {\n const rest = tokens.slice(idx + 1).join(\" \");\n return contextPattern.test(rest);\n }\n\n return false;\n}\n\n/**\n * Decide whether a full command string should trigger a code-explainer\n * explanation. Returns true if ANY sub-command in the chain is mutating.\n */\nexport function shouldCaptureBash(command: string): boolean {\n const parts = splitCommandChain(command);\n return parts.some((p) => subCommandShouldCapture(p));\n}\n","import type { SessionEntry } from \"./tracker.js\";\n\nconst SENSITIVE_PATTERNS = [\n /(^|\\/)\\.env(\\.|$)/i,\n /(^|\\/)payment/i,\n /(^|\\/)billing/i,\n /(^|\\/)stripe/i,\n /(^|\\/)auth/i,\n /(^|\\/)credential/i,\n /(^|\\/)secret/i,\n /(^|\\/)\\.ssh\\//i,\n];\n\nfunction topLevelDir(path: string): string {\n const norm = path.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n const parts = norm.split(\"/\").filter(Boolean);\n return parts[0] ?? \"\";\n}\n\nexport function matchesSensitivePattern(filePath: string): boolean {\n return SENSITIVE_PATTERNS.some((re) => re.test(filePath));\n}\n\nexport interface DriftAnalysis {\n isUnrelated: boolean;\n reason?: string;\n}\n\n/**\n * Path-heuristic drift detection for the Ollama engine.\n * Flags a new file as unrelated if:\n * 1. It matches a sensitive pattern (env, payment, auth, secrets) AND\n * the session did not start in a similarly-sensitive area.\n * 2. It lives in a different top-level directory than every file\n * edited so far in the session (cross-module drift).\n *\n * Returns `isUnrelated: false` for the first few edits (not enough\n * context to judge).\n */\nexport function analyzeDrift(\n newFilePath: string,\n priorEntries: SessionEntry[]\n): DriftAnalysis {\n // Not enough context yet for the first edit.\n if (priorEntries.length === 0) {\n return { isUnrelated: false };\n }\n\n const priorFiles = Array.from(new Set(priorEntries.map((e) => e.file)));\n const priorTopDirs = new Set(priorFiles.map(topLevelDir));\n const priorHasSensitive = priorFiles.some(matchesSensitivePattern);\n\n // Sensitive-pattern drift: the new file is in a sensitive area but\n // prior session was not working there.\n if (matchesSensitivePattern(newFilePath) && !priorHasSensitive) {\n return {\n isUnrelated: true,\n reason: `touches sensitive area (${newFilePath}) that was not part of earlier edits`,\n };\n }\n\n // Cross-module drift: only flag after at least 2 prior edits established\n // a working area.\n if (priorEntries.length >= 2) {\n const newTop = topLevelDir(newFilePath);\n if (newTop && !priorTopDirs.has(newTop)) {\n return {\n isUnrelated: true,\n reason: `is in a different top-level area (${newTop}) than earlier edits (${Array.from(priorTopDirs).join(\", \")})`,\n };\n }\n }\n\n return { isUnrelated: false };\n}\n\nexport interface DriftThresholdResult {\n shouldAlert: boolean;\n totalFiles: number;\n unrelatedFiles: string[];\n}\n\nconst DRIFT_ALERT_THRESHOLD = 3;\n\n/**\n * Decide whether to surface a drift alert based on accumulated session state.\n * Fires once every time the unrelated count crosses a multiple of the threshold.\n */\nexport function shouldAlertDrift(entries: SessionEntry[]): DriftThresholdResult {\n const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));\n const unrelatedFiles = Array.from(\n new Set(entries.filter((e) => e.unrelated).map((e) => e.file))\n );\n\n // Alert exactly when we hit the threshold (not every call afterwards).\n const shouldAlert =\n unrelatedFiles.length > 0 &&\n unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0 &&\n entries.filter((e) => e.unrelated).length ===\n entries.filter((e) => e.unrelated).length;\n\n // Fire specifically on the edit that caused us to cross the threshold.\n const lastEntry = entries[entries.length - 1];\n const lastWasUnrelated = lastEntry?.unrelated ?? false;\n const crossedThreshold =\n lastWasUnrelated && unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0;\n\n return {\n shouldAlert: crossedThreshold && shouldAlert,\n totalFiles: uniqueFiles.length,\n unrelatedFiles,\n };\n}\n"],"mappings":"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","names":["existsSync","existsSync","result"]}
|