vibe-code-explainer 0.3.4 → 0.3.5

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 (35) hide show
  1. package/dist/chunk-GEAH6PTG.js +37 -0
  2. package/dist/chunk-GEAH6PTG.js.map +1 -0
  3. package/dist/chunk-GU4Y5ZWY.js +140 -0
  4. package/dist/chunk-GU4Y5ZWY.js.map +1 -0
  5. package/dist/{chunk-2PUO5G3C.js → chunk-KK76JK7S.js} +32 -92
  6. package/dist/chunk-KK76JK7S.js.map +1 -0
  7. package/dist/{chunk-XW3S5GNV.js → chunk-VJN7Y4SI.js} +114 -33
  8. package/dist/chunk-VJN7Y4SI.js.map +1 -0
  9. package/dist/{chunk-ABPTVWQ3.js → chunk-ZZY3IDL2.js} +86 -85
  10. package/dist/chunk-ZZY3IDL2.js.map +1 -0
  11. package/dist/cli/index.js +37 -9
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/{config-AHHWBME7.js → config-YLMDBCIR.js} +116 -6
  14. package/dist/config-YLMDBCIR.js.map +1 -0
  15. package/dist/hooks/post-tool.js +143 -162
  16. package/dist/hooks/post-tool.js.map +1 -1
  17. package/dist/{init-XXK6SGF2.js → init-UDODKO25.js} +12 -16
  18. package/dist/init-UDODKO25.js.map +1 -0
  19. package/dist/ollama-YSRRK7LL.js +12 -0
  20. package/dist/{schema-YEJIXFMK.js → schema-R3THK35H.js} +8 -4
  21. package/dist/{tracker-Z5EEYUUZ.js → tracker-Y2G5DW6Y.js} +2 -2
  22. package/dist/{uninstall-AIH4HVPZ.js → uninstall-5RVTDKTA.js} +3 -3
  23. package/package.json +3 -2
  24. package/dist/chunk-2PUO5G3C.js.map +0 -1
  25. package/dist/chunk-ABPTVWQ3.js.map +0 -1
  26. package/dist/chunk-RK7ZFN4W.js +0 -97
  27. package/dist/chunk-RK7ZFN4W.js.map +0 -1
  28. package/dist/chunk-XW3S5GNV.js.map +0 -1
  29. package/dist/config-AHHWBME7.js.map +0 -1
  30. package/dist/init-XXK6SGF2.js.map +0 -1
  31. package/dist/ollama-2WHLTTDD.js +0 -14
  32. /package/dist/{ollama-2WHLTTDD.js.map → ollama-YSRRK7LL.js.map} +0 -0
  33. /package/dist/{schema-YEJIXFMK.js.map → schema-R3THK35H.js.map} +0 -0
  34. /package/dist/{tracker-Z5EEYUUZ.js.map → tracker-Y2G5DW6Y.js.map} +0 -0
  35. /package/dist/{uninstall-AIH4HVPZ.js.map → uninstall-5RVTDKTA.js.map} +0 -0
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ flagBool,
4
+ parseFlags
5
+ } from "./chunk-GEAH6PTG.js";
2
6
  import {
3
7
  MODEL_OPTIONS
4
8
  } from "./chunk-SWGQLRTO.js";
@@ -9,7 +13,7 @@ import {
9
13
  LEARNER_LEVEL_NAMES,
10
14
  getGlobalConfigPath,
11
15
  loadConfig
12
- } from "./chunk-RK7ZFN4W.js";
16
+ } from "./chunk-GU4Y5ZWY.js";
13
17
  import "./chunk-7OCVIDC7.js";
14
18
 
15
19
  // src/cli/config.ts
@@ -37,10 +41,10 @@ function normalizeModelName(name) {
37
41
  }
38
42
  function hasModel(installed, wanted) {
39
43
  const wantedNorm = normalizeModelName(wanted);
44
+ const wantedLower = wanted.toLowerCase();
40
45
  return installed.some((n) => {
41
46
  const base = n.toLowerCase();
42
- if (base === wanted.toLowerCase()) return true;
43
- if (base === wanted.toLowerCase()) return true;
47
+ if (base === wantedLower) return true;
44
48
  return normalizeModelName(base).startsWith(wantedNorm);
45
49
  });
46
50
  }
@@ -276,7 +280,110 @@ async function changeTimeout(config) {
276
280
  handleCancel(value);
277
281
  return { ...config, skipIfSlowMs: value };
278
282
  }
279
- async function runConfig() {
283
+ function resolveConfigPath() {
284
+ const projectPath = join(process.cwd(), CONFIG_FILENAME);
285
+ const globalPath = getGlobalConfigPath();
286
+ if (existsSync(projectPath)) return { configPath: projectPath, scope: "project" };
287
+ if (existsSync(globalPath)) return { configPath: globalPath, scope: "global" };
288
+ return null;
289
+ }
290
+ function runConfigShow(args) {
291
+ const { flags } = parseFlags(args);
292
+ const json = flagBool(flags, "json", "j");
293
+ const resolved = resolveConfigPath();
294
+ if (!resolved) {
295
+ process.stderr.write("[code-explainer] No config file found. Run 'vibe-code-explainer init' first.\n");
296
+ process.exit(1);
297
+ }
298
+ const config = loadConfig(resolved.configPath);
299
+ if (json) {
300
+ process.stdout.write(JSON.stringify(config, null, 2) + "\n");
301
+ } else {
302
+ process.stderr.write(renderCurrent(config) + "\n");
303
+ }
304
+ }
305
+ function runConfigGet(args) {
306
+ const { positional } = parseFlags(args);
307
+ const key = positional[0];
308
+ if (!key) {
309
+ process.stderr.write("[code-explainer] Usage: vibe-code-explainer config get <key>\n");
310
+ process.exit(1);
311
+ }
312
+ const resolved = resolveConfigPath();
313
+ if (!resolved) {
314
+ process.stderr.write("[code-explainer] No config file found. Run 'vibe-code-explainer init' first.\n");
315
+ process.exit(1);
316
+ }
317
+ const config = loadConfig(resolved.configPath);
318
+ const parts = key.split(".");
319
+ let cur = config;
320
+ for (const part of parts) {
321
+ if (typeof cur !== "object" || cur === null) {
322
+ process.stderr.write(`[code-explainer] Key '${key}' not found in config.
323
+ `);
324
+ process.exit(1);
325
+ }
326
+ cur = cur[part];
327
+ }
328
+ if (cur === void 0) {
329
+ process.stderr.write(`[code-explainer] Key '${key}' not found in config.
330
+ `);
331
+ process.exit(1);
332
+ }
333
+ if (typeof cur === "object") {
334
+ process.stdout.write(JSON.stringify(cur) + "\n");
335
+ } else {
336
+ process.stdout.write(String(cur) + "\n");
337
+ }
338
+ }
339
+ function runConfigSet(args) {
340
+ const { positional } = parseFlags(args);
341
+ const [key, rawValue] = positional;
342
+ if (!key || rawValue === void 0) {
343
+ process.stderr.write("[code-explainer] Usage: vibe-code-explainer config set <key> <value>\n");
344
+ process.exit(1);
345
+ }
346
+ const resolved = resolveConfigPath();
347
+ if (!resolved) {
348
+ process.stderr.write("[code-explainer] No config file found. Run 'vibe-code-explainer init' first.\n");
349
+ process.exit(1);
350
+ }
351
+ let value = rawValue;
352
+ try {
353
+ value = JSON.parse(rawValue);
354
+ } catch {
355
+ }
356
+ const config = loadConfig(resolved.configPath);
357
+ const parts = key.split(".");
358
+ let cur = config;
359
+ for (let i = 0; i < parts.length - 1; i++) {
360
+ const part = parts[i];
361
+ if (typeof cur[part] !== "object" || cur[part] === null) {
362
+ cur[part] = {};
363
+ }
364
+ cur = cur[part];
365
+ }
366
+ cur[parts[parts.length - 1]] = value;
367
+ writeFileSync(resolved.configPath, JSON.stringify(config, null, 2) + "\n");
368
+ process.stderr.write(`[code-explainer] Set ${key} = ${JSON.stringify(value)} in ${resolved.configPath}
369
+ `);
370
+ }
371
+ async function runConfig(rawArgs = []) {
372
+ const { flags, positional } = parseFlags(rawArgs);
373
+ const subcommand = positional[0];
374
+ const subArgs = positional.slice(1);
375
+ if (subcommand === "show") {
376
+ runConfigShow([...subArgs, ...Object.entries(flags).flatMap(([k, v]) => v === true ? [`--${k}`] : [`--${k}=${v}`])]);
377
+ return;
378
+ }
379
+ if (subcommand === "get") {
380
+ runConfigGet(subArgs);
381
+ return;
382
+ }
383
+ if (subcommand === "set") {
384
+ runConfigSet(subArgs);
385
+ return;
386
+ }
280
387
  const projectPath = join(process.cwd(), CONFIG_FILENAME);
281
388
  const globalPath = getGlobalConfigPath();
282
389
  let configPath;
@@ -298,6 +405,7 @@ Run ${pc.cyan("npx vibe-code-explainer init")} first.`
298
405
  process.exit(1);
299
406
  }
300
407
  intro(pc.bold(`code-explainer config (${scope})`));
408
+ const skipConfirm = flagBool(flags, "yes", "y");
301
409
  let config = loadConfig(configPath);
302
410
  while (true) {
303
411
  note(renderCurrent(config), "Current settings");
@@ -329,9 +437,11 @@ Run ${pc.cyan("npx vibe-code-explainer init")} first.`
329
437
  if (choice === "timeout") config = await changeTimeout(config);
330
438
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
331
439
  }
332
- outro(pc.green("Settings saved."));
440
+ if (!skipConfirm) {
441
+ outro(pc.green("Settings saved."));
442
+ }
333
443
  }
334
444
  export {
335
445
  runConfig
336
446
  };
337
- //# sourceMappingURL=config-AHHWBME7.js.map
447
+ //# sourceMappingURL=config-YLMDBCIR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/config.ts"],"sourcesContent":["import { intro, outro, select, confirm, text, cancel, isCancel, note } from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport { spawn } from \"node:child_process\";\nimport { existsSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport {\n DEFAULT_CONFIG,\n loadConfig,\n LANGUAGE_NAMES,\n LEARNER_LEVEL_NAMES,\n CONFIG_FILENAME,\n getGlobalConfigPath,\n type Config,\n type Engine,\n type DetailLevel,\n type Language,\n type LearnerLevel,\n} from \"../config/schema.js\";\nimport { MODEL_OPTIONS } from \"../detect/vram.js\";\nimport { parseFlags, flagBool } from \"./flags.js\";\n\ninterface OllamaTagResponse {\n models?: Array<{ name?: string; model?: string }>;\n}\n\nasync function listInstalledOllamaModels(url: string): Promise<string[] | null> {\n try {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), 3000);\n const res = await fetch(`${url}/api/tags`, { signal: ctrl.signal });\n clearTimeout(timer);\n if (!res.ok) return null;\n const data = (await res.json()) as OllamaTagResponse;\n if (!data.models) return [];\n return data.models\n .map((m) => m.name ?? m.model ?? \"\")\n .filter((n) => n.length > 0);\n } catch {\n return null;\n }\n}\n\nfunction normalizeModelName(name: string): string {\n // Ollama sometimes returns tags as \"qwen3.5:9b\" and sometimes as\n // \"qwen3.5:9b-q4_K_M\". Compare on the base \"<model>:<tag>\" prefix.\n return name.toLowerCase().split(/[-_]/)[0];\n}\n\nfunction hasModel(installed: string[], wanted: string): boolean {\n const wantedNorm = normalizeModelName(wanted);\n const wantedLower = wanted.toLowerCase();\n return installed.some((n) => {\n const base = n.toLowerCase();\n if (base === wantedLower) return true;\n // Looser match for variant tags (e.g. \"qwen3.5:9b-q4_K_M\" matches \"qwen3.5\")\n return normalizeModelName(base).startsWith(wantedNorm);\n });\n}\n\nasync function pullOllamaModel(model: string): Promise<boolean> {\n note(\n `Pulling ${pc.cyan(model)}\\n${pc.dim(\"This can take a while on the first run (several GB download).\")}`,\n \"Downloading model\"\n );\n return new Promise((resolvePromise) => {\n const child = spawn(\"ollama\", [\"pull\", model], { stdio: \"inherit\" });\n child.on(\"error\", () => {\n process.stderr.write(\n pc.red(\"\\nFailed to run `ollama pull`. Make sure Ollama is installed and running.\\n\")\n );\n resolvePromise(false);\n });\n child.on(\"close\", (code) => {\n if (code === 0) {\n process.stdout.write(pc.green(`\\n\\u2713 Pulled ${model}\\n`));\n resolvePromise(true);\n } else {\n process.stderr.write(pc.red(`\\n\\u2717 ollama pull exited with code ${code}\\n`));\n resolvePromise(false);\n }\n });\n });\n}\n\n\nfunction handleCancel<T>(value: T | symbol): asserts value is T {\n if (isCancel(value)) {\n cancel(\"Exited without saving.\");\n process.exit(0);\n }\n}\n\nfunction renderCurrent(config: Config): string {\n const hooks: string[] = [];\n if (config.hooks.edit) hooks.push(\"Edit\");\n if (config.hooks.write) hooks.push(\"Write\");\n if (config.hooks.bash) hooks.push(\"Bash\");\n\n const excluded = config.exclude.length > 0 ? config.exclude.join(\", \") : \"(none)\";\n const timeoutLabel =\n config.skipIfSlowMs === 0 ? \"Never skip\" : `${Math.round(config.skipIfSlowMs / 1000)}s`;\n\n return [\n `${pc.bold(\"Engine: \")} ${config.engine === \"ollama\" ? \"Local LLM (Ollama)\" : \"Claude Code (native)\"}`,\n `${pc.bold(\"Model: \")} ${config.ollamaModel}`,\n `${pc.bold(\"Ollama URL: \")} ${config.ollamaUrl}`,\n `${pc.bold(\"Detail level: \")} ${config.detailLevel}`,\n `${pc.bold(\"Language: \")} ${LANGUAGE_NAMES[config.language]}`,\n `${pc.bold(\"Learner level:\")} ${LEARNER_LEVEL_NAMES[config.learnerLevel]}`,\n `${pc.bold(\"Hooks: \")} ${hooks.join(\" \\u2713 \") || \"(all disabled)\"}`,\n `${pc.bold(\"Excluded: \")} ${excluded}`,\n `${pc.bold(\"Skip if slow: \")} ${timeoutLabel}`,\n ].join(\"\\n\");\n}\n\ntype MenuChoice =\n | \"engine\"\n | \"model\"\n | \"url\"\n | \"detail\"\n | \"language\"\n | \"level\"\n | \"hooks\"\n | \"exclude\"\n | \"timeout\"\n | \"back\";\n\nasync function changeEngine(config: Config): Promise<Config> {\n const value = await select<Engine>({\n message: \"Explanation engine\",\n options: [\n { label: \"Local LLM (Ollama)\", value: \"ollama\", hint: \"free, private, works offline\" },\n { label: \"Claude Code (native)\", value: \"claude\", hint: \"best quality, uses API tokens\" },\n ],\n initialValue: config.engine,\n });\n handleCancel(value);\n return { ...config, engine: value };\n}\n\nasync function changeModel(config: Config): Promise<Config> {\n const value = await select({\n message: \"Ollama model\",\n options: MODEL_OPTIONS.map((m) => ({\n label: m.label,\n value: m.model,\n hint: m.hint,\n })),\n initialValue: config.ollamaModel,\n });\n handleCancel(value);\n\n if (value === config.ollamaModel) {\n // Nothing actually changed; skip the download check.\n return config;\n }\n\n // Check whether Ollama already has the model pulled. If not, offer to pull it.\n const installed = await listInstalledOllamaModels(config.ollamaUrl);\n if (installed === null) {\n note(\n `Could not reach Ollama at ${pc.cyan(config.ollamaUrl)}. The model will be selected, but you'll need to pull it manually with ${pc.cyan(`ollama pull ${value}`)} before the first explanation.`,\n \"Ollama unreachable\"\n );\n return { ...config, ollamaModel: value };\n }\n\n if (hasModel(installed, value)) {\n note(`${pc.green(\"\\u2713\")} Model ${pc.cyan(value)} is already installed.`, \"Model ready\");\n return { ...config, ollamaModel: value };\n }\n\n const shouldPull = await confirm({\n message: `Model ${value} is not installed locally. Pull it now?`,\n initialValue: true,\n });\n handleCancel(shouldPull);\n\n if (!shouldPull) {\n note(\n `Saved the selection, but you must run ${pc.cyan(`ollama pull ${value}`)} before it works.`,\n \"Model not pulled\"\n );\n return { ...config, ollamaModel: value };\n }\n\n const pullOk = await pullOllamaModel(value);\n if (!pullOk) {\n note(\n `Pull failed. Saving the model selection anyway — run ${pc.cyan(`ollama pull ${value}`)} manually when Ollama is reachable.`,\n \"Pull failed\"\n );\n }\n return { ...config, ollamaModel: value };\n}\n\nasync function changeUrl(config: Config): Promise<Config> {\n const value = await text({\n message: \"Ollama endpoint URL\",\n initialValue: config.ollamaUrl,\n validate(v) {\n try {\n new URL(v);\n return;\n } catch {\n return \"Must be a valid URL (e.g., http://localhost:11434)\";\n }\n },\n });\n handleCancel(value);\n return { ...config, ollamaUrl: value };\n}\n\nasync function changeDetail(config: Config): Promise<Config> {\n const value = await select<DetailLevel>({\n message: \"Detail level\",\n options: [\n { label: \"Standard\", value: \"standard\", hint: \"1-2 sentence explanation per change (recommended)\" },\n { label: \"Minimal\", value: \"minimal\", hint: \"one short sentence per change\" },\n { label: \"Verbose\", value: \"verbose\", hint: \"detailed bullet-point breakdown\" },\n ],\n initialValue: config.detailLevel,\n });\n handleCancel(value);\n return { ...config, detailLevel: value };\n}\n\nasync function changeLanguage(config: Config): Promise<Config> {\n const value = await select<Language>({\n message: \"Language for explanations\",\n options: (Object.keys(LANGUAGE_NAMES) as Language[]).map((code) => ({\n label: LANGUAGE_NAMES[code],\n value: code,\n hint: code === \"en\" ? \"default\" : undefined,\n })),\n initialValue: config.language,\n });\n handleCancel(value);\n return { ...config, language: value };\n}\n\nasync function changeLevel(config: Config): Promise<Config> {\n const value = await select<LearnerLevel>({\n message: \"Programming knowledge level\",\n options: (Object.keys(LEARNER_LEVEL_NAMES) as LearnerLevel[]).map((code) => ({\n label: LEARNER_LEVEL_NAMES[code],\n value: code,\n hint: code === \"intermediate\" ? \"default\" : undefined,\n })),\n initialValue: config.learnerLevel,\n });\n handleCancel(value);\n return { ...config, learnerLevel: value };\n}\n\nasync function changeHooks(config: Config): Promise<Config> {\n const editOn = await confirm({ message: \"Explain file edits?\", initialValue: config.hooks.edit });\n handleCancel(editOn);\n const writeOn = await confirm({ message: \"Explain new files?\", initialValue: config.hooks.write });\n handleCancel(writeOn);\n const bashOn = await confirm({\n message: \"Explain destructive Bash commands (rm, git reset, etc.)?\",\n initialValue: config.hooks.bash,\n });\n handleCancel(bashOn);\n\n return {\n ...config,\n hooks: { edit: editOn, write: writeOn, bash: bashOn },\n };\n}\n\nasync function changeExclude(config: Config): Promise<Config> {\n const action = await select({\n message: `Current exclusions: ${config.exclude.join(\", \") || \"(none)\"}`,\n options: [\n { label: \"Add a pattern\", value: \"add\", hint: \"e.g., *.generated.*\" },\n { label: \"Remove a pattern\", value: \"remove\" },\n { label: \"Reset to defaults\", value: \"reset\", hint: DEFAULT_CONFIG.exclude.join(\", \") },\n { label: \"Back\", value: \"back\" },\n ],\n });\n handleCancel(action);\n\n if (action === \"back\") return config;\n if (action === \"reset\") return { ...config, exclude: [...DEFAULT_CONFIG.exclude] };\n\n if (action === \"add\") {\n const pattern = await text({ message: \"Glob pattern to exclude (e.g., *.generated.*)\" });\n handleCancel(pattern);\n if (!pattern.trim()) return config;\n const exclude = Array.from(new Set([...config.exclude, pattern.trim()]));\n return { ...config, exclude };\n }\n\n if (action === \"remove\") {\n if (config.exclude.length === 0) {\n note(\"No exclusions to remove.\", \"Exclusions\");\n return config;\n }\n const target = await select({\n message: \"Which pattern to remove?\",\n options: config.exclude.map((p) => ({ label: p, value: p })),\n });\n handleCancel(target);\n const exclude = config.exclude.filter((p) => p !== target);\n return { ...config, exclude };\n }\n\n return config;\n}\n\nasync function changeTimeout(config: Config): Promise<Config> {\n const value = await select<number>({\n message: \"Skip explanation if it takes longer than...\",\n options: [\n { label: \"5 seconds\", value: 5000, hint: \"fast, may skip complex changes\" },\n { label: \"8 seconds\", value: 8000, hint: \"balanced (recommended)\" },\n { label: \"15 seconds\", value: 15000, hint: \"patient, rarely skips\" },\n { label: \"Never skip\", value: 0, hint: \"always wait for the explanation\" },\n ],\n initialValue: config.skipIfSlowMs,\n });\n handleCancel(value);\n return { ...config, skipIfSlowMs: value };\n}\n\nfunction resolveConfigPath(): { configPath: string; scope: \"project\" | \"global\" } | null {\n const projectPath = join(process.cwd(), CONFIG_FILENAME);\n const globalPath = getGlobalConfigPath();\n if (existsSync(projectPath)) return { configPath: projectPath, scope: \"project\" };\n if (existsSync(globalPath)) return { configPath: globalPath, scope: \"global\" };\n return null;\n}\n\n/**\n * config show [--json]\n * Print the effective config. With --json, outputs machine-readable JSON so\n * agents can pipe to jq or parse directly.\n */\nfunction runConfigShow(args: string[]): void {\n const { flags } = parseFlags(args);\n const json = flagBool(flags, \"json\", \"j\");\n\n const resolved = resolveConfigPath();\n if (!resolved) {\n process.stderr.write(\"[code-explainer] No config file found. Run 'vibe-code-explainer init' first.\\n\");\n process.exit(1);\n }\n const config = loadConfig(resolved.configPath);\n if (json) {\n process.stdout.write(JSON.stringify(config, null, 2) + \"\\n\");\n } else {\n process.stderr.write(renderCurrent(config) + \"\\n\");\n }\n}\n\n/**\n * config get <key>\n * Print a single config field as a plain string (for scripting).\n * Key may be dot-separated for nested fields (e.g. hooks.bash).\n */\nfunction runConfigGet(args: string[]): void {\n const { positional } = parseFlags(args);\n const key = positional[0];\n if (!key) {\n process.stderr.write(\"[code-explainer] Usage: vibe-code-explainer config get <key>\\n\");\n process.exit(1);\n }\n const resolved = resolveConfigPath();\n if (!resolved) {\n process.stderr.write(\"[code-explainer] No config file found. Run 'vibe-code-explainer init' first.\\n\");\n process.exit(1);\n }\n const config = loadConfig(resolved.configPath) as Record<string, unknown>;\n const parts = key.split(\".\");\n let cur: unknown = config;\n for (const part of parts) {\n if (typeof cur !== \"object\" || cur === null) {\n process.stderr.write(`[code-explainer] Key '${key}' not found in config.\\n`);\n process.exit(1);\n }\n cur = (cur as Record<string, unknown>)[part];\n }\n if (cur === undefined) {\n process.stderr.write(`[code-explainer] Key '${key}' not found in config.\\n`);\n process.exit(1);\n }\n // Output plain scalar or JSON for objects/arrays.\n if (typeof cur === \"object\") {\n process.stdout.write(JSON.stringify(cur) + \"\\n\");\n } else {\n process.stdout.write(String(cur) + \"\\n\");\n }\n}\n\n/**\n * config set <key> <value>\n * Set a single config field. The value is parsed as JSON when it looks like\n * a JSON literal (number, boolean, array, object), otherwise treated as a\n * plain string.\n */\nfunction runConfigSet(args: string[]): void {\n const { positional } = parseFlags(args);\n const [key, rawValue] = positional;\n if (!key || rawValue === undefined) {\n process.stderr.write(\"[code-explainer] Usage: vibe-code-explainer config set <key> <value>\\n\");\n process.exit(1);\n }\n const resolved = resolveConfigPath();\n if (!resolved) {\n process.stderr.write(\"[code-explainer] No config file found. Run 'vibe-code-explainer init' first.\\n\");\n process.exit(1);\n }\n\n let value: unknown = rawValue;\n try {\n value = JSON.parse(rawValue);\n } catch {\n // Use as plain string\n }\n\n // Deep-set the key into the config object.\n const config = loadConfig(resolved.configPath) as Record<string, unknown>;\n const parts = key.split(\".\");\n let cur: Record<string, unknown> = config;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (typeof cur[part] !== \"object\" || cur[part] === null) {\n cur[part] = {};\n }\n cur = cur[part] as Record<string, unknown>;\n }\n cur[parts[parts.length - 1]] = value;\n\n writeFileSync(resolved.configPath, JSON.stringify(config, null, 2) + \"\\n\");\n process.stderr.write(`[code-explainer] Set ${key} = ${JSON.stringify(value)} in ${resolved.configPath}\\n`);\n}\n\nexport async function runConfig(rawArgs: string[] = []): Promise<void> {\n const { flags, positional } = parseFlags(rawArgs);\n const subcommand = positional[0];\n const subArgs = positional.slice(1);\n\n // Non-interactive subcommands for agent-native access.\n if (subcommand === \"show\") { runConfigShow([...subArgs, ...Object.entries(flags).flatMap(([k, v]) => v === true ? [`--${k}`] : [`--${k}=${v}`])]); return; }\n if (subcommand === \"get\") { runConfigGet(subArgs); return; }\n if (subcommand === \"set\") { runConfigSet(subArgs); return; }\n\n // Interactive TUI mode.\n const projectPath = join(process.cwd(), CONFIG_FILENAME);\n const globalPath = getGlobalConfigPath();\n\n let configPath: string;\n let scope: \"project\" | \"global\";\n if (existsSync(projectPath)) {\n configPath = projectPath;\n scope = \"project\";\n } else if (existsSync(globalPath)) {\n configPath = globalPath;\n scope = \"global\";\n } else {\n intro(pc.bold(\"code-explainer config\"));\n cancel(\n `No config file found.\\nSearched: ${pc.cyan(projectPath)}\\n ${pc.cyan(globalPath)}\\nRun ${pc.cyan(\"npx vibe-code-explainer init\")} first.`\n );\n process.exit(1);\n }\n\n intro(pc.bold(`code-explainer config (${scope})`));\n\n // --yes: skip confirmation for non-interactive environments.\n const skipConfirm = flagBool(flags, \"yes\", \"y\");\n\n let config = loadConfig(configPath);\n\n while (true) {\n note(renderCurrent(config), \"Current settings\");\n\n const choice = await select<MenuChoice>({\n message: \"What would you like to change?\",\n options: [\n { label: \"Engine\", value: \"engine\" },\n { label: \"Model\", value: \"model\" },\n { label: \"Ollama URL\", value: \"url\" },\n { label: \"Detail level\", value: \"detail\" },\n { label: \"Language\", value: \"language\" },\n { label: \"Learner level\", value: \"level\" },\n { label: \"Enable/disable hooks\", value: \"hooks\" },\n { label: \"File exclusions\", value: \"exclude\" },\n { label: \"Latency timeout\", value: \"timeout\" },\n { label: \"Back (save and exit)\", value: \"back\" },\n ],\n });\n handleCancel(choice);\n\n if (choice === \"back\") break;\n if (choice === \"engine\") config = await changeEngine(config);\n if (choice === \"model\") config = await changeModel(config);\n if (choice === \"url\") config = await changeUrl(config);\n if (choice === \"detail\") config = await changeDetail(config);\n if (choice === \"language\") config = await changeLanguage(config);\n if (choice === \"level\") config = await changeLevel(config);\n if (choice === \"hooks\") config = await changeHooks(config);\n if (choice === \"exclude\") config = await changeExclude(config);\n if (choice === \"timeout\") config = await changeTimeout(config);\n\n writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\");\n }\n\n if (!skipConfirm) {\n outro(pc.green(\"Settings saved.\"));\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildClaudePrompt,
4
- callOllama
5
- } from "../chunk-ABPTVWQ3.js";
4
+ callOllama,
5
+ parseResponse,
6
+ truncateText
7
+ } from "../chunk-ZZY3IDL2.js";
6
8
  import {
7
9
  DEFAULT_CONFIG,
8
10
  loadConfig
9
- } from "../chunk-RK7ZFN4W.js";
11
+ } from "../chunk-GU4Y5ZWY.js";
10
12
  import {
11
13
  cleanStaleSessionFiles,
12
14
  formatDriftAlert,
@@ -15,10 +17,11 @@ import {
15
17
  formatSkipNotice,
16
18
  getCached,
17
19
  getRecentSummaries,
20
+ isSafeSessionId,
18
21
  readSession,
19
22
  recordEntry,
20
23
  setCached
21
- } from "../chunk-XW3S5GNV.js";
24
+ } from "../chunk-VJN7Y4SI.js";
22
25
  import "../chunk-7OCVIDC7.js";
23
26
 
24
27
  // src/hooks/post-tool.ts
@@ -26,91 +29,6 @@ import { join } from "path";
26
29
 
27
30
  // src/engines/claude.ts
28
31
  import { execFile } from "child_process";
29
- function extractBalancedObject(text, startIdx) {
30
- let depth = 0;
31
- let inString = false;
32
- let escape = false;
33
- for (let i = startIdx; i < text.length; i++) {
34
- const ch = text[i];
35
- if (escape) {
36
- escape = false;
37
- continue;
38
- }
39
- if (ch === "\\") {
40
- escape = true;
41
- continue;
42
- }
43
- if (ch === '"') {
44
- inString = !inString;
45
- continue;
46
- }
47
- if (inString) continue;
48
- if (ch === "{") depth++;
49
- else if (ch === "}") {
50
- depth--;
51
- if (depth === 0) return text.slice(startIdx, i + 1);
52
- }
53
- }
54
- return null;
55
- }
56
- function extractJson(text) {
57
- const trimmed = text.trim();
58
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed;
59
- const fenceOpen = trimmed.match(/```(?:json)?\s*\n?/);
60
- if (fenceOpen) {
61
- const afterOpen = trimmed.slice(fenceOpen.index + fenceOpen[0].length);
62
- const closingIdx = afterOpen.indexOf("```");
63
- const inner = closingIdx !== -1 ? afterOpen.slice(0, closingIdx) : afterOpen;
64
- const innerTrimmed = inner.trim();
65
- if (innerTrimmed.startsWith("{")) {
66
- const lastBrace = innerTrimmed.lastIndexOf("}");
67
- if (lastBrace !== -1) return innerTrimmed.slice(0, lastBrace + 1);
68
- }
69
- }
70
- const firstOpen = trimmed.indexOf("{");
71
- if (firstOpen !== -1) {
72
- const balanced = extractBalancedObject(trimmed, firstOpen);
73
- if (balanced) return balanced;
74
- const lastClose = trimmed.lastIndexOf("}");
75
- if (lastClose > firstOpen) return trimmed.slice(firstOpen, lastClose + 1);
76
- }
77
- return null;
78
- }
79
- function coerceString(v) {
80
- return typeof v === "string" ? v : "";
81
- }
82
- function coerceDeepDive(v) {
83
- if (!Array.isArray(v)) return [];
84
- return v.filter((it) => typeof it === "object" && it !== null).map((it) => ({
85
- term: coerceString(it.term),
86
- explanation: coerceString(it.explanation)
87
- })).filter((it) => it.term.length > 0);
88
- }
89
- function parseResponse(rawText) {
90
- const json = extractJson(rawText);
91
- if (!json) return null;
92
- try {
93
- const parsed = JSON.parse(json);
94
- const risk = coerceString(parsed.risk);
95
- if (!["none", "low", "medium", "high"].includes(risk)) return null;
96
- return {
97
- impact: coerceString(parsed.impact),
98
- howItWorks: coerceString(parsed.howItWorks),
99
- why: coerceString(parsed.why),
100
- deepDive: coerceDeepDive(parsed.deepDive),
101
- isSamePattern: parsed.isSamePattern === true,
102
- samePatternNote: coerceString(parsed.samePatternNote),
103
- risk,
104
- riskReason: coerceString(parsed.riskReason)
105
- };
106
- } catch {
107
- return null;
108
- }
109
- }
110
- function truncateText(text, max) {
111
- if (text.length <= max) return text;
112
- return text.slice(0, max) + "...";
113
- }
114
32
  function runClaude(prompt, timeoutMs) {
115
33
  return new Promise((resolve, reject) => {
116
34
  const child = execFile(
@@ -145,14 +63,23 @@ function runClaude(prompt, timeoutMs) {
145
63
  });
146
64
  }
147
65
  async function callClaude(inputs) {
148
- const prompt = buildClaudePrompt(inputs.config.detailLevel, {
149
- filePath: inputs.filePath,
150
- diff: inputs.diff,
151
- userPrompt: inputs.userPrompt,
152
- language: inputs.config.language,
153
- learnerLevel: inputs.config.learnerLevel,
154
- recentSummaries: inputs.recentSummaries
155
- });
66
+ let prompt;
67
+ try {
68
+ prompt = buildClaudePrompt(inputs.config.detailLevel, {
69
+ filePath: inputs.filePath,
70
+ diff: inputs.diff,
71
+ language: inputs.config.language,
72
+ learnerLevel: inputs.config.learnerLevel,
73
+ recentSummaries: inputs.recentSummaries
74
+ });
75
+ } catch (err) {
76
+ return {
77
+ kind: "error",
78
+ problem: "Failed to build Claude prompt",
79
+ cause: err.message || String(err),
80
+ fix: "Check detailLevel/learnerLevel/language values via 'npx vibe-code-explainer config'"
81
+ };
82
+ }
156
83
  try {
157
84
  const result = await runClaude(prompt, inputs.config.skipIfSlowMs);
158
85
  if (result.code !== 0) {
@@ -321,6 +248,20 @@ function extractNewFileDiff(filePath, cwd) {
321
248
  }
322
249
  return { kind: "empty" };
323
250
  }
251
+ function looksBinary(buf) {
252
+ const sample = buf.length > 8192 ? buf.subarray(0, 8192) : buf;
253
+ if (sample.length === 0) return false;
254
+ if (sample.indexOf(0) !== -1) return true;
255
+ let nonPrint = 0;
256
+ for (let i = 0; i < sample.length; i++) {
257
+ const b = sample[i];
258
+ if (b === 9 || b === 10 || b === 13 || b >= 32 && b <= 126 || b >= 128) {
259
+ continue;
260
+ }
261
+ nonPrint++;
262
+ }
263
+ return nonPrint / sample.length > 0.3;
264
+ }
324
265
  function readFileAsNewDiff(filePath) {
325
266
  if (!existsSync(filePath)) {
326
267
  return { kind: "skip", reason: `file not found: ${filePath}` };
@@ -330,13 +271,17 @@ function readFileAsNewDiff(filePath) {
330
271
  if (stat.size > 2 * 1024 * 1024) {
331
272
  return { kind: "skip", reason: `file too large (${Math.round(stat.size / 1024)}KB)` };
332
273
  }
333
- const raw = readFileSync(filePath, "utf-8");
334
- if (!raw.trim()) {
274
+ const buf = readFileSync(filePath);
275
+ if (buf.length === 0) {
335
276
  return { kind: "empty" };
336
277
  }
337
- if (raw.includes("\0")) {
278
+ if (looksBinary(buf)) {
338
279
  return { kind: "binary", message: `Binary file created: ${filePath}` };
339
280
  }
281
+ const raw = buf.toString("utf-8");
282
+ if (!raw.trim()) {
283
+ return { kind: "empty" };
284
+ }
340
285
  const withMarkers = raw.split("\n").map((l) => `+ ${l}`).join("\n");
341
286
  const diff = `--- /dev/null
342
287
  +++ b/${filePath}
@@ -393,7 +338,32 @@ var MUTATING_COMMANDS = /* @__PURE__ */ new Set([
393
338
  "chown",
394
339
  "ln",
395
340
  "touch",
396
- "dd"
341
+ "dd",
342
+ "tee",
343
+ "install",
344
+ "truncate",
345
+ "shred",
346
+ "rsync",
347
+ "scp",
348
+ "sftp",
349
+ "mount",
350
+ "umount",
351
+ "kill",
352
+ "killall",
353
+ "pkill",
354
+ "crontab",
355
+ "useradd",
356
+ "userdel",
357
+ "usermod",
358
+ "groupadd",
359
+ "groupdel",
360
+ "passwd",
361
+ "chpasswd",
362
+ "visudo",
363
+ "systemctl",
364
+ "service",
365
+ "launchctl"
366
+ // Note: brew is in CONTEXTUAL_COMMANDS (finer-grained control); do not add here.
397
367
  ]);
398
368
  var CONTEXTUAL_COMMANDS = {
399
369
  npm: /\b(install|add|remove|uninstall|update|ci|link|unlink|init|publish)\b/,
@@ -480,9 +450,12 @@ function subCommandShouldCapture(subCmd) {
480
450
  const rest = tokens.slice(idx + 1).join(" ");
481
451
  return contextPattern.test(rest);
482
452
  }
483
- return false;
453
+ return true;
484
454
  }
485
- function shouldCaptureBash(command) {
455
+ function shouldCaptureBash(command, capturePatterns = []) {
456
+ if (capturePatterns.length > 0 && capturePatterns.some((p) => command.includes(p))) {
457
+ return true;
458
+ }
486
459
  const parts = splitCommandChain(command);
487
460
  return parts.some((p) => subCommandShouldCapture(p));
488
461
  }
@@ -536,12 +509,11 @@ function shouldAlertDrift(entries) {
536
509
  const unrelatedFiles = Array.from(
537
510
  new Set(entries.filter((e) => e.unrelated).map((e) => e.file))
538
511
  );
539
- const shouldAlert = unrelatedFiles.length > 0 && unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0 && entries.filter((e) => e.unrelated).length === entries.filter((e) => e.unrelated).length;
540
512
  const lastEntry = entries[entries.length - 1];
541
513
  const lastWasUnrelated = lastEntry?.unrelated ?? false;
542
- const crossedThreshold = lastWasUnrelated && unrelatedFiles.length % DRIFT_ALERT_THRESHOLD === 0;
514
+ const shouldAlert = lastWasUnrelated && unrelatedFiles.length === DRIFT_ALERT_THRESHOLD;
543
515
  return {
544
- shouldAlert: crossedThreshold && shouldAlert,
516
+ shouldAlert,
545
517
  totalFiles: uniqueFiles.length,
546
518
  unrelatedFiles
547
519
  };
@@ -553,11 +525,14 @@ function addOutput(text) {
553
525
  output.push(text);
554
526
  }
555
527
  function safeExit() {
556
- if (output.length > 0) {
557
- const systemMessage = "\n" + output.join("\n");
558
- process.stdout.write(JSON.stringify({ systemMessage }) + "\n");
528
+ if (output.length === 0) {
529
+ process.exit(0);
559
530
  }
560
- process.exit(0);
531
+ const systemMessage = "\n" + output.join("\n");
532
+ const payload = JSON.stringify({ systemMessage }) + "\n";
533
+ process.stdout.write(payload, () => process.exit(0));
534
+ setTimeout(() => process.exit(0), 500);
535
+ throw new Error("unreachable");
561
536
  }
562
537
  async function readStdin() {
563
538
  return new Promise((resolve) => {
@@ -574,10 +549,11 @@ async function readStdin() {
574
549
  function parsePayload(raw) {
575
550
  try {
576
551
  const parsed = JSON.parse(raw);
577
- if (typeof parsed.session_id === "string" && typeof parsed.tool_name === "string") {
578
- return parsed;
579
- }
580
- return null;
552
+ if (typeof parsed !== "object" || parsed === null) return null;
553
+ if (!isSafeSessionId(parsed.session_id)) return null;
554
+ if (typeof parsed.tool_name !== "string") return null;
555
+ if (typeof parsed.tool_input !== "object" || parsed.tool_input === null) return null;
556
+ return parsed;
581
557
  } catch {
582
558
  return null;
583
559
  }
@@ -596,14 +572,48 @@ function isHookEnabled(toolName, config) {
596
572
  if (lower === "bash") return config.hooks.bash;
597
573
  return false;
598
574
  }
599
- async function runEngine(filePath, diff, config, userPrompt, recentSummaries, signal) {
575
+ async function runEngine(filePath, diff, config, recentSummaries, signal) {
600
576
  if (signal.aborted) {
601
577
  return { kind: "skip", reason: "interrupted by user" };
602
578
  }
603
579
  if (config.engine === "ollama") {
604
580
  return callOllama({ filePath, diff, config, recentSummaries });
605
581
  }
606
- return callClaude({ filePath, diff, config, userPrompt, recentSummaries });
582
+ return callClaude({ filePath, diff, config, recentSummaries });
583
+ }
584
+ function buildEditWriteDiff(payload, config, cwd) {
585
+ const lowerTool = payload.tool_name.toLowerCase();
586
+ const input = payload.tool_input;
587
+ const target = input.file_path ?? input.filePath;
588
+ if (!target) {
589
+ safeExit();
590
+ }
591
+ const filePath = target;
592
+ if (isExcluded(filePath, config.exclude)) {
593
+ safeExit();
594
+ }
595
+ let result;
596
+ if (lowerTool === "edit") {
597
+ const oldStr = input.old_string ?? input.oldString ?? "";
598
+ const newStr = input.new_string ?? input.newString ?? "";
599
+ result = oldStr || newStr ? buildDiffFromEdit(filePath, oldStr, newStr) : extractEditDiff(filePath, cwd);
600
+ } else if (lowerTool === "multiedit") {
601
+ result = input.edits && input.edits.length > 0 ? buildDiffFromMultiEdit(filePath, input.edits) : extractEditDiff(filePath, cwd);
602
+ } else {
603
+ result = extractNewFileDiff(filePath, cwd);
604
+ }
605
+ if (result.kind === "empty") {
606
+ safeExit();
607
+ }
608
+ if (result.kind === "skip") {
609
+ addOutput(formatSkipNotice(result.reason));
610
+ safeExit();
611
+ }
612
+ if (result.kind === "binary") {
613
+ addOutput(formatSkipNotice(result.message));
614
+ safeExit();
615
+ }
616
+ return { filePath, diff: result.content };
607
617
  }
608
618
  async function main() {
609
619
  const controller = new AbortController();
@@ -625,48 +635,20 @@ async function main() {
625
635
  let diff;
626
636
  const lowerTool = payload.tool_name.toLowerCase();
627
637
  if (lowerTool === "edit" || lowerTool === "multiedit" || lowerTool === "write") {
628
- const input = payload.tool_input;
629
- const target = input.file_path ?? input.filePath;
638
+ const target = buildEditWriteDiff(payload, config, cwd);
630
639
  if (!target) safeExit();
631
- filePath = target;
632
- if (isExcluded(filePath, config.exclude)) safeExit();
633
- let result2;
634
- if (lowerTool === "edit") {
635
- const oldStr = input.old_string ?? input.oldString ?? "";
636
- const newStr = input.new_string ?? input.newString ?? "";
637
- if (oldStr || newStr) {
638
- result2 = buildDiffFromEdit(filePath, oldStr, newStr);
639
- } else {
640
- result2 = extractEditDiff(filePath, cwd);
641
- }
642
- } else if (lowerTool === "multiedit") {
643
- if (input.edits && input.edits.length > 0) {
644
- result2 = buildDiffFromMultiEdit(filePath, input.edits);
645
- } else {
646
- result2 = extractEditDiff(filePath, cwd);
647
- }
648
- } else {
649
- result2 = extractNewFileDiff(filePath, cwd);
650
- }
651
- if (result2.kind === "empty") safeExit();
652
- if (result2.kind === "skip") {
653
- addOutput(formatSkipNotice(result2.reason));
654
- safeExit();
655
- }
656
- if (result2.kind === "binary") {
657
- addOutput(formatSkipNotice(result2.message));
658
- safeExit();
659
- }
660
- diff = result2.content;
640
+ ({ filePath, diff } = target);
661
641
  } else if (lowerTool === "bash") {
662
642
  const input = payload.tool_input;
663
643
  const command = input.command ?? "";
664
- if (!command || !shouldCaptureBash(command)) safeExit();
644
+ if (!command || !shouldCaptureBash(command, config.bashFilter.capturePatterns)) safeExit();
665
645
  filePath = "<bash command>";
666
646
  diff = command;
667
647
  } else {
668
648
  safeExit();
669
649
  }
650
+ const isBash = filePath === "<bash command>";
651
+ const priorEntries = isBash ? [] : readSession(payload.session_id);
670
652
  const cacheKey = `${filePath}
671
653
  ${diff}`;
672
654
  const cached = getCached(payload.session_id, cacheKey);
@@ -674,15 +656,8 @@ ${diff}`;
674
656
  if (cached) {
675
657
  result = cached;
676
658
  } else {
677
- const recentSummaries = getRecentSummaries(payload.session_id, 3);
678
- const outcome = await runEngine(
679
- filePath,
680
- diff,
681
- config,
682
- void 0,
683
- recentSummaries,
684
- controller.signal
685
- );
659
+ const recentSummaries = getRecentSummaries(payload.session_id, 3, priorEntries);
660
+ const outcome = await runEngine(filePath, diff, config, recentSummaries, controller.signal);
686
661
  if (outcome.kind === "skip") {
687
662
  addOutput(formatSkipNotice(outcome.reason));
688
663
  safeExit();
@@ -695,8 +670,7 @@ ${diff}`;
695
670
  setCached(payload.session_id, cacheKey, result);
696
671
  }
697
672
  let driftReason;
698
- if (filePath !== "<bash command>") {
699
- const priorEntries = readSession(payload.session_id);
673
+ if (!isBash) {
700
674
  const analysis = analyzeDrift(filePath, priorEntries);
701
675
  if (analysis.isUnrelated) {
702
676
  driftReason = analysis.reason;
@@ -718,8 +692,15 @@ ${diff}`;
718
692
  summary: summaryForTracking,
719
693
  unrelated: !!driftReason
720
694
  });
721
- const updated = readSession(payload.session_id);
722
- const driftCheck = shouldAlertDrift(updated);
695
+ const entryJustRecorded = {
696
+ file: filePath,
697
+ timestamp: Date.now(),
698
+ risk: result.risk,
699
+ summary: summaryForTracking,
700
+ unrelated: !!driftReason
701
+ };
702
+ const updatedEntries = [...priorEntries, entryJustRecorded];
703
+ const driftCheck = shouldAlertDrift(updatedEntries);
723
704
  if (driftCheck.shouldAlert) {
724
705
  addOutput(formatDriftAlert(driftCheck.totalFiles, driftCheck.unrelatedFiles, void 0, config.language));
725
706
  }