vibe-code-explainer 0.1.8 → 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 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
- code-explainer hooks into Claude Code's file modifications and prints a plain-English
6
- explanation of every change in your terminal, before Claude continues. No more
7
- accepting code blindly. No more scrolling past diffs you can't read.
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
- ## What it does
10
+ No more accepting code blindly.
10
11
 
11
- Every time Claude edits a file, creates a file, or runs a destructive command
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
- Changes the page background from a solid dark color to a gradient
27
- │ that blends two shades of dark blue. Visual-only change, no logic
28
- │ or data is affected.
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 file.
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 setup will:
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
- - Detect if Ollama is installed (and offer to install it if not)
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
- ## Engines
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
- **Ollama (Local LLM)**
162
+ ### What gets skipped
64
163
 
65
- Free, offline, private. Your code never leaves your machine. Auto-selects a
66
- Qwen code model based on available VRAM, or lets you pick.
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
- | VRAM | Model |
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
- **Claude Code (native)**
172
+ Every explanation includes a risk rating:
76
173
 
77
- Uses `claude -p` for the highest-quality explanations. Can detect changes that
78
- are unrelated to what you asked for. Uses API tokens.
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
- Opens an interactive menu showing current settings. Change any of:
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
- - Engine (Ollama or Claude Code)
89
- - Model (with VRAM recommendations)
90
- - Ollama URL (for Docker or remote Ollama)
91
- - Detail level
92
- - Which hooks are active (Edit, Write, Bash)
93
- - File exclusion patterns (skip explanations for `*.lock`, `dist/**`, etc.)
94
- - Latency timeout (5s, 8s, 15s, or never skip)
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
- ## Session commands
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
- Shows a report of everything Claude changed in the current session total
103
- changes, files touched, risk breakdown, and unrelated-file alerts.
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 current session state. Use before starting a new project so summaries
110
- don't include old edits.
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 hooks from `.claude/settings.local.json` and deletes the config file.
119
- Leaves Ollama and pulled models alone.
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
- ## Requirements
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
- - Node.js 20+
124
- - Claude Code installed
125
- - Ollama installed if you choose the Ollama engine (the installer will offer to install it)
411
+ ---
126
412
 
127
- ## How safety works
413
+ ## Safety philosophy
128
414
 
129
- code-explainer is a **visibility tool**, not a gate. It never blocks Claude
130
- Code, never reverts changes, never requires confirmation. If the explanation
131
- engine fails or times out, the hook skips silently with a notice to stderr.
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, and watch for:
420
+ For real safety, **read the explanations**, and watch for:
134
421
 
135
- - 🚨 High risk something is touching security, encryption, or secrets
136
- - ⚠️ Medium risk auth, payment, API keys, database, env vars
137
- - SESSION DRIFT Claude has touched files unrelated to your request
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.
@@ -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,25 +494,10 @@ var output = [];
496
494
  function addOutput(text) {
497
495
  output.push(text);
498
496
  }
499
- function debugLog(event, data) {
500
- try {
501
- const fs = __require("fs");
502
- const os = __require("os");
503
- const path = __require("path");
504
- const logPath = path.join(os.tmpdir(), "code-explainer-debug.log");
505
- const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, ...data }) + "\n";
506
- fs.appendFileSync(logPath, line);
507
- } catch {
508
- }
509
- }
510
497
  function safeExit() {
511
498
  if (output.length > 0) {
512
499
  const systemMessage = "\n" + output.join("\n");
513
- const payload = JSON.stringify({ systemMessage });
514
- debugLog("emit", { bytes: payload.length, preview: systemMessage.slice(0, 200) });
515
- process.stdout.write(payload + "\n");
516
- } else {
517
- debugLog("no-output");
500
+ process.stdout.write(JSON.stringify({ systemMessage }) + "\n");
518
501
  }
519
502
  process.exit(0);
520
503
  }
@@ -569,19 +552,10 @@ async function main() {
569
552
  addOutput(formatSkipNotice("interrupted by user"));
570
553
  safeExit();
571
554
  });
572
- debugLog("started", { pid: process.pid, argv: process.argv.slice(2) });
573
555
  const raw = await readStdin();
574
- debugLog("stdin-read", { bytes: raw.length });
575
- if (!raw.trim()) {
576
- debugLog("exit-empty-stdin");
577
- safeExit();
578
- }
556
+ if (!raw.trim()) safeExit();
579
557
  const payload = parsePayload(raw);
580
- if (!payload) {
581
- debugLog("exit-bad-payload");
582
- safeExit();
583
- }
584
- debugLog("payload-ok", { tool: payload.tool_name, session: payload.session_id });
558
+ if (!payload) safeExit();
585
559
  const cwd = payload.cwd || process.cwd();
586
560
  const config = loadConfigSafe(cwd);
587
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 // NOTE: temporarily unconditional in 0.1.8 to diagnose VS Code hook visibility\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-code-explainer",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Real-time diff explanations for vibe coders using Claude Code",
5
5
  "type": "module",
6
6
  "bin": {