vibe-code-explainer 0.1.10 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +181 -78
  2. package/dist/{chunk-IIUJ6UAO.js → chunk-2PUO5G3C.js} +75 -2
  3. package/dist/chunk-2PUO5G3C.js.map +1 -0
  4. package/dist/chunk-5NCRRHU7.js +89 -0
  5. package/dist/chunk-5NCRRHU7.js.map +1 -0
  6. package/dist/{chunk-OXXWT37Z.js → chunk-SWGQLRTO.js} +24 -11
  7. package/dist/chunk-SWGQLRTO.js.map +1 -0
  8. package/dist/{chunk-QTQXXXT4.js → chunk-YS2XIZIA.js} +29 -12
  9. package/dist/chunk-YS2XIZIA.js.map +1 -0
  10. package/dist/cli/index.js +4 -4
  11. package/dist/{config-NF5WYSJB.js → config-5PDPXG7Z.js} +128 -8
  12. package/dist/config-5PDPXG7Z.js.map +1 -0
  13. package/dist/hooks/post-tool.js +9 -7
  14. package/dist/hooks/post-tool.js.map +1 -1
  15. package/dist/{init-5ZJML72X.js → init-KUVD2YGA.js} +110 -31
  16. package/dist/init-KUVD2YGA.js.map +1 -0
  17. package/dist/{ollama-Z5EWJ4H6.js → ollama-34TOVCUY.js} +3 -2
  18. package/dist/schema-TBXFNCIG.js +17 -0
  19. package/dist/uninstall-CNGJWJYQ.js +101 -0
  20. package/dist/uninstall-CNGJWJYQ.js.map +1 -0
  21. package/package.json +1 -1
  22. package/dist/chunk-IIUJ6UAO.js.map +0 -1
  23. package/dist/chunk-OXXWT37Z.js.map +0 -1
  24. package/dist/chunk-PGDNR7HQ.js +0 -50
  25. package/dist/chunk-PGDNR7HQ.js.map +0 -1
  26. package/dist/chunk-QTQXXXT4.js.map +0 -1
  27. package/dist/config-NF5WYSJB.js.map +0 -1
  28. package/dist/init-5ZJML72X.js.map +0 -1
  29. package/dist/schema-SJTKT73Y.js +0 -11
  30. package/dist/uninstall-BXMUKVRD.js +0 -63
  31. package/dist/uninstall-BXMUKVRD.js.map +0 -1
  32. /package/dist/{ollama-Z5EWJ4H6.js.map → ollama-34TOVCUY.js.map} +0 -0
  33. /package/dist/{schema-SJTKT73Y.js.map → schema-TBXFNCIG.js.map} +0 -0
package/README.md CHANGED
@@ -78,56 +78,88 @@ with no changes on your side.
78
78
 
79
79
  ### 2. Run the installer
80
80
 
81
- In your terminal, navigate to the project where you want Claude Code
82
- explanations:
81
+ In your terminal:
83
82
 
84
83
  ```bash
85
- cd path/to/your/project
84
+ cd path/to/your/project # or stay in your home directory for a global install
86
85
  npx vibe-code-explainer init
87
86
  ```
88
87
 
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
- ```
88
+ The installer is interactive and walks you through six steps:
89
+
90
+ #### 1. Install scope: project or global
91
+
92
+ - **This project only** — hooks live in `.claude/settings.local.json` and the
93
+ config in `./code-explainer.config.json`. Only this project's Claude Code
94
+ sessions get explanations. Best if you only want it in one place or if
95
+ different projects need different settings.
96
+ - **Globally (every project)** — hooks live in `~/.claude/settings.json`
97
+ (your user-level Claude Code config) and the config in
98
+ `~/.code-explainer.config.json`. **Every** Claude Code session on your
99
+ machine gets explanations automatically, no per-project setup needed.
100
+ The package is installed once via `npm install -g`.
101
+
102
+ A project config always takes precedence over the global config if both
103
+ exist, so you can set a global default and override per-project.
104
+
105
+ #### 2. Engine
106
+
107
+ - **Local LLM (Ollama)** free, private, works offline. Your code never
108
+ leaves your machine.
109
+ - **Claude Code (native)** uses `claude -p` under the hood for the
110
+ highest-quality explanations and unrelated-change detection. Costs API
111
+ tokens per explanation.
112
+
113
+ #### 3. Detail level
114
+
115
+ - **Standard**1–2 sentence explanation per change (recommended)
116
+ - **Minimal** — one short sentence per change
117
+ - **Verbose** — detailed bullet-point breakdown of every change
118
+
119
+ #### 4. Language
120
+
121
+ Pick the language the explanations are written in. Supported:
124
122
 
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)
123
+ English, Portuguese, Spanish, French, German, Italian, Chinese, Japanese,
124
+ Korean.
125
+
126
+ Only the `summary` and `riskReason` fields are translated. JSON keys and the
127
+ risk labels (`none` / `low` / `medium` / `high`) stay in English.
128
+
129
+ #### 5. Model (Ollama only)
130
+
131
+ - If you have an NVIDIA GPU, the installer auto-detects your VRAM and picks
132
+ the right model.
133
+ - Otherwise, you get a chooser with VRAM hints:
134
+
135
+ | Model | Recommended for | Notes |
136
+ |-------|-----------------|-------|
137
+ | `qwen3.5:4b` | ≤ 8 GB VRAM | newest (Mar 2026), fastest, ~3.4 GB |
138
+ | `qwen2.5-coder:7b` | ≤ 8 GB VRAM | code-specialized, ~4.7 GB |
139
+ | `qwen3.5:9b` | 8–12 GB VRAM | newest, general-purpose, ~6.6 GB |
140
+ | `qwen2.5-coder:14b` | 12–16 GB VRAM | code-specialized, ~9 GB |
141
+ | `qwen3.5:27b` | 16–24 GB VRAM | newest, ~17 GB |
142
+ | `qwen2.5-coder:32b` | ≥ 24 GB VRAM | best code quality, ~19 GB |
143
+
144
+ Pick whichever matches your hardware. The newer `qwen3.5` family (released
145
+ March 2026) is general-purpose with strong coding ability. The
146
+ `qwen2.5-coder` family is code-specialized and remains a solid choice.
147
+
148
+ #### 6. Pull the model + warmup (Ollama only)
149
+
150
+ The installer runs `ollama pull <model>` and shows the real download progress
151
+ bar. Then it sends a trivial warmup diff so the first real explanation is
152
+ fast (otherwise the model has to load into VRAM on first use, which can take
153
+ 10–15 seconds).
154
+
155
+ Skip the warmup with:
156
+
157
+ ```bash
158
+ npx vibe-code-explainer init --skip-warmup
159
+ ```
129
160
 
130
- Next time you run `claude` in this directory, you'll see explanations.
161
+ When the installer finishes, you're ready to go. Next time you run `claude`
162
+ in a directory covered by this install, you'll see explanations.
131
163
 
132
164
  ---
133
165
 
@@ -182,21 +214,46 @@ Every explanation includes a risk rating:
182
214
 
183
215
  ## Configuration
184
216
 
185
- Change any setting at any time by running:
217
+ Change any setting at any time by running `config`. Which command depends on
218
+ how you installed it:
219
+
220
+ **If you installed globally** (the package is on your PATH):
221
+
222
+ ```bash
223
+ vibe-code-explainer config
224
+ ```
225
+
226
+ **If you installed per-project** (or want to be safe):
186
227
 
187
228
  ```bash
188
229
  npx vibe-code-explainer config
189
230
  ```
190
231
 
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.
232
+ Both commands do the same thing. Use whichever works in your shell. If you
233
+ see a stale version run via `npx`, force the latest with:
234
+
235
+ ```bash
236
+ npx vibe-code-explainer@latest config
237
+ ```
238
+
239
+ The menu auto-detects whether to edit the project config (`./code-explainer.config.json`)
240
+ or the global config (`~/.code-explainer.config.json`): project config wins
241
+ if both exist.
242
+
243
+ It opens an interactive menu showing your current settings. Pick what you
244
+ want to change, one at a time. Every change is saved immediately. When you
245
+ change the model, the tool checks whether Ollama already has it pulled and
246
+ offers to download it on the spot if not.
193
247
 
194
248
  ```
249
+ code-explainer config (project)
250
+
195
251
  Current settings:
196
252
  Engine: Local LLM (Ollama)
197
- Model: qwen2.5-coder:7b
253
+ Model: qwen3.5:4b
198
254
  Ollama URL: http://localhost:11434
199
- Detail level: Standard
255
+ Detail level: standard
256
+ Language: English
200
257
  Hooks: Edit ✓ Write ✓ Bash ✓
201
258
  Excluded: *.lock, dist/**, node_modules/**
202
259
  Skip if slow: 8s
@@ -206,27 +263,35 @@ Current settings:
206
263
  Model
207
264
  Ollama URL
208
265
  Detail level
266
+ Language
209
267
  Enable/disable hooks
210
268
  File exclusions
211
269
  Latency timeout
212
270
  Back (save and exit)
213
271
  ```
214
272
 
273
+ If you installed globally, `config` edits `~/.code-explainer.config.json`.
274
+ If you installed per-project, it edits `./code-explainer.config.json`.
275
+ If both exist, the project config takes precedence at runtime.
276
+
215
277
  ### Configurable options
216
278
 
217
279
  - **Engine** — swap between Ollama (local) and Claude Code (native). Switching
218
280
  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.
281
+ - **Model** — pick a different Ollama model. The VRAM hints are visible in
282
+ the chooser. You can also set any model name Ollama supports by editing the
283
+ JSON file directly (e.g., `deepseek-coder-v2:16b`).
284
+ - **Ollama URL** — defaults to `http://localhost:11434`. Change this if you
285
+ run Ollama in a Docker container on a different port, or on a separate
286
+ machine. Non-loopback URLs trigger a security warning because your code
287
+ would be sent over the network.
226
288
  - **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.
289
+ [Install → Detail level](#3-detail-level) for what each produces.
290
+ - **Language** — English, Portuguese, Spanish, French, German, Italian,
291
+ Chinese, Japanese, Korean. Applies to the `summary` and `riskReason`
292
+ fields; JSON keys and risk labels stay in English.
293
+ - **Hooks** — turn on or off individually. If Bash explanations feel noisy,
294
+ disable just that hook and keep Edit + Write.
230
295
  - **File exclusions** — glob patterns for files you never want explained.
231
296
  Defaults cover lockfiles, build output, and dependencies. Add patterns like
232
297
  `*.generated.*` if your project has codegen.
@@ -236,15 +301,21 @@ Current settings:
236
301
 
237
302
  ### Editing the config file directly
238
303
 
239
- If you prefer editing JSON manually, the config lives at
240
- `code-explainer.config.json` in your project root. Structure:
304
+ If you prefer editing JSON manually:
305
+
306
+ - **Project install:** edit `code-explainer.config.json` in your project root.
307
+ - **Global install:** edit `~/.code-explainer.config.json` in your home
308
+ directory.
309
+
310
+ Full config schema:
241
311
 
242
312
  ```json
243
313
  {
244
314
  "engine": "ollama",
245
- "ollamaModel": "qwen2.5-coder:7b",
315
+ "ollamaModel": "qwen3.5:4b",
246
316
  "ollamaUrl": "http://localhost:11434",
247
317
  "detailLevel": "standard",
318
+ "language": "en",
248
319
  "hooks": {
249
320
  "edit": true,
250
321
  "write": true,
@@ -264,16 +335,33 @@ If you prefer editing JSON manually, the config lives at
264
335
  }
265
336
  ```
266
337
 
338
+ Field types:
339
+
340
+ | Field | Values |
341
+ |-------|--------|
342
+ | `engine` | `"ollama"` or `"claude"` |
343
+ | `ollamaModel` | Any model tag available on your Ollama install |
344
+ | `ollamaUrl` | Any valid URL (warns on non-loopback) |
345
+ | `detailLevel` | `"minimal"` / `"standard"` / `"verbose"` |
346
+ | `language` | `"en"` / `"pt"` / `"es"` / `"fr"` / `"de"` / `"it"` / `"zh"` / `"ja"` / `"ko"` |
347
+ | `hooks.edit` / `hooks.write` / `hooks.bash` | `true` or `false` |
348
+ | `exclude` | Array of glob patterns |
349
+ | `skipIfSlowMs` | Number in milliseconds; `0` means never skip |
350
+
267
351
  Changes take effect on the next Claude Code tool call — no restart needed.
268
352
 
269
353
  ---
270
354
 
271
355
  ## Session tools
272
356
 
357
+ All commands below accept both forms: `vibe-code-explainer <cmd>` (if
358
+ globally installed, on PATH) or `npx vibe-code-explainer <cmd>` (if per-project).
359
+
273
360
  ### Summary of what Claude has done
274
361
 
275
362
  ```bash
276
- npx vibe-code-explainer summary
363
+ vibe-code-explainer summary
364
+ # or: npx vibe-code-explainer summary
277
365
  ```
278
366
 
279
367
  Prints a report of every explained change in your current Claude Code session:
@@ -282,7 +370,8 @@ total changes, files touched, risk breakdown, and flagged unrelated files.
282
370
  ### Clear the session
283
371
 
284
372
  ```bash
285
- npx vibe-code-explainer session end
373
+ vibe-code-explainer session end
374
+ # or: npx vibe-code-explainer session end
286
375
  ```
287
376
 
288
377
  Clears the session state (tracked files and cache). Run this when you start a
@@ -291,7 +380,8 @@ new task so the drift-detection summary doesn't include old edits.
291
380
  ### Warm up the model
292
381
 
293
382
  ```bash
294
- npx vibe-code-explainer warmup
383
+ vibe-code-explainer warmup
384
+ # or: npx vibe-code-explainer warmup
295
385
  ```
296
386
 
297
387
  Sends a trivial diff to Ollama to pre-load the model. Useful if you closed and
@@ -303,17 +393,29 @@ slow (10–15 seconds) as the model gets loaded into VRAM.
303
393
  ## Uninstall
304
394
 
305
395
  ```bash
306
- npx vibe-code-explainer uninstall
396
+ vibe-code-explainer uninstall
397
+ # or: npx vibe-code-explainer uninstall
307
398
  ```
308
399
 
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.
400
+ The uninstaller auto-detects whether you have a project install, a global
401
+ install, or both. If both exist, it asks which to remove.
402
+
403
+ **Project uninstall:** removes the PostToolUse hook entries from
404
+ `.claude/settings.local.json` and deletes `code-explainer.config.json`. Other
405
+ entries (hooks from other tools, other settings) are preserved.
312
406
 
313
- Ollama and any pulled models stay installed. If you want to remove them:
407
+ **Global uninstall:** removes the PostToolUse hooks from
408
+ `~/.claude/settings.json` and deletes `~/.code-explainer.config.json`. The
409
+ globally-installed npm package stays on disk — remove it with:
314
410
 
315
411
  ```bash
316
- ollama rm qwen2.5-coder:7b
412
+ npm uninstall -g vibe-code-explainer
413
+ ```
414
+
415
+ Ollama and any pulled models stay installed either way. To remove them:
416
+
417
+ ```bash
418
+ ollama rm qwen3.5:4b
317
419
  # or whichever model you pulled
318
420
  ```
319
421
 
@@ -333,9 +435,10 @@ Most common causes, in order:
333
435
  ollama serve
334
436
  ```
335
437
  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.
438
+ 3. **The engine is misconfigured.** Run `vibe-code-explainer config` (or
439
+ `npx vibe-code-explainer config`) and check the engine and URL. Try
440
+ switching to the Claude Code engine as a test — if that works, Ollama is
441
+ the issue.
339
442
  4. **The file is excluded.** Check your `exclude` patterns in the config.
340
443
 
341
444
  ### The explanation took too long and got skipped
@@ -343,8 +446,8 @@ Most common causes, in order:
343
446
  Default timeout is 8 seconds. Cold starts with Ollama (first explanation
344
447
  after starting the service) can be 10–15 seconds. Two fixes:
345
448
 
346
- - Run `npx vibe-code-explainer warmup` right after starting Ollama.
347
- - Raise the timeout to 15 seconds via `npx vibe-code-explainer config →
449
+ - Run `vibe-code-explainer warmup` right after starting Ollama.
450
+ - Raise the timeout to 15 seconds via `vibe-code-explainer config →
348
451
  Latency timeout`.
349
452
 
350
453
  ### Explanations are low-quality
@@ -354,7 +457,7 @@ business logic, security-relevant changes). If explanations feel wrong on
354
457
  tricky diffs, switch to the Claude Code engine:
355
458
 
356
459
  ```bash
357
- npx vibe-code-explainer config
460
+ vibe-code-explainer config
358
461
  # → Engine → Claude Code (native)
359
462
  ```
360
463
 
@@ -366,7 +469,7 @@ cost of API tokens per explanation.
366
469
  Add a glob pattern via the config menu:
367
470
 
368
471
  ```
369
- npx vibe-code-explainer config → File exclusions → Add a pattern
472
+ vibe-code-explainer config → File exclusions → Add a pattern
370
473
  ```
371
474
 
372
475
  For example, `*.md` to skip all markdown files, or `src/generated/**` to skip
@@ -375,7 +478,7 @@ a whole directory.
375
478
  ### I want to turn off Bash explanations but keep Edit/Write
376
479
 
377
480
  ```
378
- npx vibe-code-explainer config → Enable/disable hooks
481
+ vibe-code-explainer config → Enable/disable hooks
379
482
  ```
380
483
 
381
484
  Uncheck Bash, keep Edit and Write checked.
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/config/merge.ts
4
4
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
5
+ import { homedir } from "os";
5
6
  import { dirname, join } from "path";
6
7
  var HOOK_MARKER = "code-explainer";
7
8
  function buildHookCommand(hookScriptPath) {
@@ -97,9 +98,81 @@ function removeHooksFromSettings(projectRoot, { useLocal = true } = {}) {
97
98
  }
98
99
  return { removed: removedAny, path: lastPath };
99
100
  }
101
+ function mergeHooksIntoUserSettings(hookScriptPath) {
102
+ const userClaudeDir = join(homedir(), ".claude");
103
+ const settingsPath = join(userClaudeDir, "settings.json");
104
+ let settings = {};
105
+ let created = false;
106
+ if (existsSync(settingsPath)) {
107
+ const raw = readFileSync(settingsPath, "utf-8");
108
+ try {
109
+ settings = JSON.parse(raw);
110
+ } catch (err) {
111
+ const msg = err instanceof Error ? err.message : String(err);
112
+ throw new Error(
113
+ `[code-explainer] Cannot merge hooks into ${settingsPath}. The file is not valid JSON. Fix: repair the JSON manually or delete the file to regenerate. Original error: ${msg}`
114
+ );
115
+ }
116
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
117
+ throw new Error(
118
+ `[code-explainer] Cannot merge hooks into ${settingsPath}. The file does not contain a JSON object at the top level.`
119
+ );
120
+ }
121
+ } else {
122
+ created = true;
123
+ if (!existsSync(userClaudeDir)) {
124
+ mkdirSync(userClaudeDir, { recursive: true });
125
+ }
126
+ }
127
+ if (!settings.hooks) settings.hooks = {};
128
+ const ourEntries = {
129
+ PostToolUse: [
130
+ {
131
+ matcher: "Edit|Write|MultiEdit",
132
+ hooks: [{ type: "command", command: `node "${hookScriptPath}"` }]
133
+ },
134
+ {
135
+ matcher: "Bash",
136
+ hooks: [{ type: "command", command: `node "${hookScriptPath}"` }]
137
+ }
138
+ ]
139
+ };
140
+ const existingPostTool = settings.hooks.PostToolUse ?? [];
141
+ const cleaned = existingPostTool.map((entry) => ({
142
+ ...entry,
143
+ hooks: entry.hooks.filter((h) => !isCodeExplainerHook(h.command))
144
+ })).filter((entry) => entry.hooks.length > 0);
145
+ settings.hooks.PostToolUse = [...cleaned, ...ourEntries.PostToolUse];
146
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
147
+ return { created, path: settingsPath };
148
+ }
149
+ function removeHooksFromUserSettings() {
150
+ const settingsPath = join(homedir(), ".claude", "settings.json");
151
+ if (!existsSync(settingsPath)) return { removed: false, path: null };
152
+ let settings;
153
+ try {
154
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
155
+ } catch {
156
+ return { removed: false, path: null };
157
+ }
158
+ if (!settings.hooks?.PostToolUse) return { removed: false, path: null };
159
+ const before = JSON.stringify(settings.hooks.PostToolUse);
160
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.map((entry) => ({
161
+ ...entry,
162
+ hooks: entry.hooks.filter((h) => !isCodeExplainerHook(h.command))
163
+ })).filter((entry) => entry.hooks.length > 0);
164
+ const after = JSON.stringify(settings.hooks.PostToolUse);
165
+ if (before === after) return { removed: false, path: null };
166
+ if (settings.hooks.PostToolUse.length === 0) delete settings.hooks.PostToolUse;
167
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
168
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
169
+ return { removed: true, path: settingsPath };
170
+ }
100
171
 
101
172
  export {
102
173
  mergeHooksIntoSettings,
103
- removeHooksFromSettings
174
+ removeHooksFromSettings,
175
+ mergeHooksIntoUserSettings,
176
+ removeHooksFromUserSettings
104
177
  };
105
- //# sourceMappingURL=chunk-IIUJ6UAO.js.map
178
+ //# sourceMappingURL=chunk-2PUO5G3C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/merge.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nexport const HOOK_MARKER = \"code-explainer\";\n\ninterface HookMatcherEntry {\n matcher: string;\n hooks: Array<{\n type: \"command\";\n command: string;\n }>;\n}\n\ninterface ClaudeSettings {\n hooks?: Record<string, HookMatcherEntry[]>;\n [key: string]: unknown;\n}\n\nfunction buildHookCommand(hookScriptPath: string): string {\n return `node \"${hookScriptPath}\"`;\n}\n\nfunction buildCodeExplainerEntries(hookScriptPath: string): Record<string, HookMatcherEntry[]> {\n const command = buildHookCommand(hookScriptPath);\n return {\n PostToolUse: [\n {\n matcher: \"Edit|Write|MultiEdit\",\n hooks: [{ type: \"command\", command }],\n },\n {\n matcher: \"Bash\",\n hooks: [{ type: \"command\", command }],\n },\n ],\n };\n}\n\nfunction isCodeExplainerHook(cmd: string): boolean {\n return cmd.includes(HOOK_MARKER) && cmd.includes(\"post-tool\");\n}\n\nexport interface MergeResult {\n created: boolean;\n path: string;\n}\n\n/**\n * Read, parse, merge code-explainer hooks into, and write back the settings file.\n * Creates `.claude/settings.json` if it doesn't exist. Preserves all existing\n * hooks and other top-level keys. Idempotent — re-running does not duplicate.\n *\n * Throws if the existing file is malformed JSON, so the caller can surface\n * the error clearly instead of corrupting user settings.\n */\nexport function mergeHooksIntoSettings(\n projectRoot: string,\n hookScriptPath: string,\n { useLocal = true }: { useLocal?: boolean } = {}\n): MergeResult {\n const claudeDir = join(projectRoot, \".claude\");\n const filename = useLocal ? \"settings.local.json\" : \"settings.json\";\n const settingsPath = join(claudeDir, filename);\n\n let settings: ClaudeSettings = {};\n let created = false;\n\n if (existsSync(settingsPath)) {\n const raw = readFileSync(settingsPath, \"utf-8\");\n try {\n settings = JSON.parse(raw);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[code-explainer] Cannot merge hooks into ${settingsPath}. The file is not valid JSON. Fix: repair the JSON manually (check for trailing commas, unquoted keys) or delete the file to regenerate. Original error: ${msg}`\n );\n }\n if (typeof settings !== \"object\" || settings === null || Array.isArray(settings)) {\n throw new Error(\n `[code-explainer] Cannot merge hooks into ${settingsPath}. The file does not contain a JSON object at the top level. Fix: ensure the file starts with { and ends with }.`\n );\n }\n } else {\n created = true;\n if (!existsSync(claudeDir)) {\n mkdirSync(claudeDir, { recursive: true });\n }\n }\n\n if (!settings.hooks) settings.hooks = {};\n\n const ourEntries = buildCodeExplainerEntries(hookScriptPath);\n const existingPostTool = settings.hooks.PostToolUse ?? [];\n\n // Remove any previous code-explainer entries to keep idempotency.\n const cleaned = existingPostTool\n .map((entry) => ({\n ...entry,\n hooks: entry.hooks.filter((h) => !isCodeExplainerHook(h.command)),\n }))\n .filter((entry) => entry.hooks.length > 0);\n\n settings.hooks.PostToolUse = [...cleaned, ...ourEntries.PostToolUse];\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + \"\\n\");\n\n return { created, path: settingsPath };\n}\n\n/**\n * Remove all code-explainer hook entries from the settings file, preserving\n * other hooks and config. Does nothing if the file or hook entries do not\n * exist. Never throws for missing files.\n */\nexport function removeHooksFromSettings(\n projectRoot: string,\n { useLocal = true }: { useLocal?: boolean } = {}\n): { removed: boolean; path: string | null } {\n const candidates = useLocal\n ? [\".claude/settings.local.json\", \".claude/settings.json\"]\n : [\".claude/settings.json\"];\n\n let removedAny = false;\n let lastPath: string | null = null;\n\n for (const rel of candidates) {\n const path = join(projectRoot, rel);\n if (!existsSync(path)) continue;\n\n let settings: ClaudeSettings;\n try {\n settings = JSON.parse(readFileSync(path, \"utf-8\"));\n } catch {\n // Don't corrupt malformed files during uninstall.\n continue;\n }\n\n if (!settings.hooks?.PostToolUse) continue;\n\n const before = JSON.stringify(settings.hooks.PostToolUse);\n settings.hooks.PostToolUse = settings.hooks.PostToolUse\n .map((entry) => ({\n ...entry,\n hooks: entry.hooks.filter((h) => !isCodeExplainerHook(h.command)),\n }))\n .filter((entry) => entry.hooks.length > 0);\n const after = JSON.stringify(settings.hooks.PostToolUse);\n\n if (before !== after) {\n if (settings.hooks.PostToolUse.length === 0) {\n delete settings.hooks.PostToolUse;\n }\n if (Object.keys(settings.hooks).length === 0) {\n delete settings.hooks;\n }\n writeFileSync(path, JSON.stringify(settings, null, 2) + \"\\n\");\n removedAny = true;\n lastPath = path;\n }\n }\n\n return { removed: removedAny, path: lastPath };\n}\n\nexport { dirname };\n\n/**\n * Merge code-explainer hooks into the user-level ~/.claude/settings.json,\n * so hooks fire in every project. Used by the global install path.\n */\nexport function mergeHooksIntoUserSettings(hookScriptPath: string): MergeResult {\n const userClaudeDir = join(homedir(), \".claude\");\n const settingsPath = join(userClaudeDir, \"settings.json\");\n\n let settings: ClaudeSettings = {};\n let created = false;\n\n if (existsSync(settingsPath)) {\n const raw = readFileSync(settingsPath, \"utf-8\");\n try {\n settings = JSON.parse(raw);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(\n `[code-explainer] Cannot merge hooks into ${settingsPath}. The file is not valid JSON. Fix: repair the JSON manually or delete the file to regenerate. Original error: ${msg}`\n );\n }\n if (typeof settings !== \"object\" || settings === null || Array.isArray(settings)) {\n throw new Error(\n `[code-explainer] Cannot merge hooks into ${settingsPath}. The file does not contain a JSON object at the top level.`\n );\n }\n } else {\n created = true;\n if (!existsSync(userClaudeDir)) {\n mkdirSync(userClaudeDir, { recursive: true });\n }\n }\n\n if (!settings.hooks) settings.hooks = {};\n\n const ourEntries = {\n PostToolUse: [\n {\n matcher: \"Edit|Write|MultiEdit\",\n hooks: [{ type: \"command\" as const, command: `node \"${hookScriptPath}\"` }],\n },\n {\n matcher: \"Bash\",\n hooks: [{ type: \"command\" as const, command: `node \"${hookScriptPath}\"` }],\n },\n ],\n };\n\n const existingPostTool = settings.hooks.PostToolUse ?? [];\n const cleaned = existingPostTool\n .map((entry) => ({\n ...entry,\n hooks: entry.hooks.filter((h) => !isCodeExplainerHook(h.command)),\n }))\n .filter((entry) => entry.hooks.length > 0);\n\n settings.hooks.PostToolUse = [...cleaned, ...ourEntries.PostToolUse];\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + \"\\n\");\n\n return { created, path: settingsPath };\n}\n\n/**\n * Remove code-explainer hook entries from ~/.claude/settings.json.\n * Preserves other hooks and config. Never throws on missing files.\n */\nexport function removeHooksFromUserSettings(): { removed: boolean; path: string | null } {\n const settingsPath = join(homedir(), \".claude\", \"settings.json\");\n if (!existsSync(settingsPath)) return { removed: false, path: null };\n\n let settings: ClaudeSettings;\n try {\n settings = JSON.parse(readFileSync(settingsPath, \"utf-8\"));\n } catch {\n return { removed: false, path: null };\n }\n\n if (!settings.hooks?.PostToolUse) return { removed: false, path: null };\n\n const before = JSON.stringify(settings.hooks.PostToolUse);\n settings.hooks.PostToolUse = settings.hooks.PostToolUse\n .map((entry) => ({\n ...entry,\n hooks: entry.hooks.filter((h) => !isCodeExplainerHook(h.command)),\n }))\n .filter((entry) => entry.hooks.length > 0);\n const after = JSON.stringify(settings.hooks.PostToolUse);\n\n if (before === after) return { removed: false, path: null };\n\n if (settings.hooks.PostToolUse.length === 0) delete settings.hooks.PostToolUse;\n if (Object.keys(settings.hooks).length === 0) delete settings.hooks;\n\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + \"\\n\");\n return { removed: true, path: settingsPath };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config/schema.ts
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ var LANGUAGE_NAMES = {
8
+ en: "English",
9
+ pt: "Portuguese",
10
+ es: "Spanish",
11
+ fr: "French",
12
+ de: "German",
13
+ it: "Italian",
14
+ zh: "Chinese",
15
+ ja: "Japanese",
16
+ ko: "Korean"
17
+ };
18
+ var CONFIG_FILENAME = "code-explainer.config.json";
19
+ function getGlobalConfigPath() {
20
+ return join(homedir(), ".code-explainer.config.json");
21
+ }
22
+ var DEFAULT_CONFIG = {
23
+ engine: "ollama",
24
+ ollamaModel: "qwen3.5:4b",
25
+ ollamaUrl: "http://localhost:11434",
26
+ detailLevel: "standard",
27
+ language: "en",
28
+ hooks: {
29
+ edit: true,
30
+ write: true,
31
+ bash: true
32
+ },
33
+ exclude: ["*.lock", "dist/**", "node_modules/**"],
34
+ skipIfSlowMs: 8e3,
35
+ bashFilter: {
36
+ capturePatterns: [
37
+ "rm",
38
+ "mv",
39
+ "cp",
40
+ "mkdir",
41
+ "npm install",
42
+ "pip install",
43
+ "yarn add",
44
+ "pnpm add",
45
+ "chmod",
46
+ "chown",
47
+ "git checkout",
48
+ "git reset",
49
+ "git revert",
50
+ "sed -i"
51
+ ]
52
+ }
53
+ };
54
+ function mergeConfig(base, overlay) {
55
+ return {
56
+ ...base,
57
+ ...overlay,
58
+ hooks: { ...base.hooks, ...overlay.hooks ?? {} },
59
+ bashFilter: {
60
+ ...base.bashFilter,
61
+ ...overlay.bashFilter ?? {}
62
+ }
63
+ };
64
+ }
65
+ function tryReadJson(path) {
66
+ if (!existsSync(path)) return null;
67
+ try {
68
+ return JSON.parse(readFileSync(path, "utf-8"));
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function loadConfig(configPath) {
74
+ const globalConfig = tryReadJson(getGlobalConfigPath());
75
+ const projectConfig = tryReadJson(configPath);
76
+ let result = DEFAULT_CONFIG;
77
+ if (globalConfig) result = mergeConfig(result, globalConfig);
78
+ if (projectConfig) result = mergeConfig(result, projectConfig);
79
+ return result;
80
+ }
81
+
82
+ export {
83
+ LANGUAGE_NAMES,
84
+ CONFIG_FILENAME,
85
+ getGlobalConfigPath,
86
+ DEFAULT_CONFIG,
87
+ loadConfig
88
+ };
89
+ //# sourceMappingURL=chunk-5NCRRHU7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/schema.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport type Engine = \"ollama\" | \"claude\";\nexport type DetailLevel = \"minimal\" | \"standard\" | \"verbose\";\nexport type RiskLevel = \"none\" | \"low\" | \"medium\" | \"high\";\n\nexport type Language =\n | \"en\"\n | \"pt\"\n | \"es\"\n | \"fr\"\n | \"de\"\n | \"it\"\n | \"zh\"\n | \"ja\"\n | \"ko\";\n\nexport const LANGUAGE_NAMES: Record<Language, string> = {\n en: \"English\",\n pt: \"Portuguese\",\n es: \"Spanish\",\n fr: \"French\",\n de: \"German\",\n it: \"Italian\",\n zh: \"Chinese\",\n ja: \"Japanese\",\n ko: \"Korean\",\n};\n\nexport interface HooksConfig {\n edit: boolean;\n write: boolean;\n bash: boolean;\n}\n\nexport interface BashFilterConfig {\n capturePatterns: string[];\n}\n\nexport interface Config {\n engine: Engine;\n ollamaModel: string;\n ollamaUrl: string;\n detailLevel: DetailLevel;\n language: Language;\n hooks: HooksConfig;\n exclude: string[];\n skipIfSlowMs: number;\n bashFilter: BashFilterConfig;\n}\n\nexport interface ExplanationResult {\n summary: string;\n risk: RiskLevel;\n riskReason: string;\n}\n\nexport interface HookPayload {\n session_id: string;\n transcript_path: string;\n cwd: string;\n permission_mode: string;\n hook_event_name: string;\n tool_name: string;\n tool_input: Record<string, unknown>;\n tool_response: string;\n}\n\nexport const CONFIG_FILENAME = \"code-explainer.config.json\";\n\nexport function getGlobalConfigPath(): string {\n return join(homedir(), \".code-explainer.config.json\");\n}\n\nexport const DEFAULT_CONFIG: Config = {\n engine: \"ollama\",\n ollamaModel: \"qwen3.5:4b\",\n ollamaUrl: \"http://localhost:11434\",\n detailLevel: \"standard\",\n language: \"en\",\n hooks: {\n edit: true,\n write: true,\n bash: true,\n },\n exclude: [\"*.lock\", \"dist/**\", \"node_modules/**\"],\n skipIfSlowMs: 8000,\n bashFilter: {\n capturePatterns: [\n \"rm\",\n \"mv\",\n \"cp\",\n \"mkdir\",\n \"npm install\",\n \"pip install\",\n \"yarn add\",\n \"pnpm add\",\n \"chmod\",\n \"chown\",\n \"git checkout\",\n \"git reset\",\n \"git revert\",\n \"sed -i\",\n ],\n },\n};\n\nfunction mergeConfig(base: Config, overlay: Partial<Config>): Config {\n return {\n ...base,\n ...overlay,\n hooks: { ...base.hooks, ...(overlay.hooks ?? {}) },\n bashFilter: {\n ...base.bashFilter,\n ...(overlay.bashFilter ?? {}),\n },\n };\n}\n\nfunction tryReadJson(path: string): Partial<Config> | null {\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf-8\")) as Partial<Config>;\n } catch {\n return null;\n }\n}\n\n/**\n * Load config with three-level resolution, most specific first:\n * 1. Project config (passed as configPath) — overrides everything\n * 2. Global user config (~/.code-explainer.config.json)\n * 3. Built-in defaults\n *\n * A project config that lacks a field falls through to the global; a global\n * that lacks a field falls through to defaults. This lets a global install\n * set everyone's defaults while still allowing per-project overrides.\n */\nexport function loadConfig(configPath: string): Config {\n const globalConfig = tryReadJson(getGlobalConfigPath());\n const projectConfig = tryReadJson(configPath);\n\n let result = DEFAULT_CONFIG;\n if (globalConfig) result = mergeConfig(result, globalConfig);\n if (projectConfig) result = mergeConfig(result, projectConfig);\n return result;\n}\n"],"mappings":";;;AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,eAAe;AACxB,SAAS,YAAY;AAiBd,IAAM,iBAA2C;AAAA,EACtD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAyCO,IAAM,kBAAkB;AAExB,SAAS,sBAA8B;AAC5C,SAAO,KAAK,QAAQ,GAAG,6BAA6B;AACtD;AAEO,IAAM,iBAAyB;AAAA,EACpC,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,WAAW;AAAA,EACX,aAAa;AAAA,EACb,UAAU;AAAA,EACV,OAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA,EACA,SAAS,CAAC,UAAU,WAAW,iBAAiB;AAAA,EAChD,cAAc;AAAA,EACd,YAAY;AAAA,IACV,iBAAiB;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,YAAY,MAAc,SAAkC;AACnE,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG;AAAA,IACH,OAAO,EAAE,GAAG,KAAK,OAAO,GAAI,QAAQ,SAAS,CAAC,EAAG;AAAA,IACjD,YAAY;AAAA,MACV,GAAG,KAAK;AAAA,MACR,GAAI,QAAQ,cAAc,CAAC;AAAA,IAC7B;AAAA,EACF;AACF;AAEA,SAAS,YAAY,MAAsC;AACzD,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYO,SAAS,WAAW,YAA4B;AACrD,QAAM,eAAe,YAAY,oBAAoB,CAAC;AACtD,QAAM,gBAAgB,YAAY,UAAU;AAE5C,MAAI,SAAS;AACb,MAAI,aAAc,UAAS,YAAY,QAAQ,YAAY;AAC3D,MAAI,cAAe,UAAS,YAAY,QAAQ,aAAa;AAC7D,SAAO;AACT;","names":[]}