vibe-code-explainer 0.3.4 → 0.3.6

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-2PUO5G3C.js → chunk-KK76JK7S.js} +32 -92
  4. package/dist/chunk-KK76JK7S.js.map +1 -0
  5. package/dist/chunk-LWASVVBV.js +140 -0
  6. package/dist/chunk-LWASVVBV.js.map +1 -0
  7. package/dist/{chunk-ABPTVWQ3.js → chunk-R5H62KGX.js} +86 -85
  8. package/dist/chunk-R5H62KGX.js.map +1 -0
  9. package/dist/{chunk-XW3S5GNV.js → chunk-VJN7Y4SI.js} +114 -33
  10. package/dist/chunk-VJN7Y4SI.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-4DNTCZ6X.js} +127 -8
  14. package/dist/config-4DNTCZ6X.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-YHRKOKSY.js} +12 -16
  18. package/dist/init-YHRKOKSY.js.map +1 -0
  19. package/dist/ollama-43BPUEEC.js +12 -0
  20. package/dist/{schema-YEJIXFMK.js → schema-MYOWRNBW.js} +8 -4
  21. package/dist/{tracker-Z5EEYUUZ.js → tracker-Y2G5DW6Y.js} +2 -2
  22. package/dist/{uninstall-AIH4HVPZ.js → uninstall-YADL7OUB.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-43BPUEEC.js.map} +0 -0
  33. /package/dist/{schema-YEJIXFMK.js.map → schema-MYOWRNBW.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-YADL7OUB.js.map} +0 -0
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/flags.ts
4
+ function parseFlags(args) {
5
+ const flags = {};
6
+ const positional = [];
7
+ let i = 0;
8
+ while (i < args.length) {
9
+ const arg = args[i];
10
+ if (arg.startsWith("--")) {
11
+ const eqIdx = arg.indexOf("=");
12
+ if (eqIdx !== -1) {
13
+ const name = arg.slice(2, eqIdx);
14
+ const value = arg.slice(eqIdx + 1);
15
+ flags[name] = value;
16
+ } else {
17
+ flags[arg.slice(2)] = true;
18
+ }
19
+ } else if (arg.startsWith("-") && arg.length === 2) {
20
+ const name = arg.slice(1);
21
+ flags[name] = true;
22
+ } else {
23
+ positional.push(arg);
24
+ }
25
+ i++;
26
+ }
27
+ return { flags, positional };
28
+ }
29
+ function flagBool(flags, ...names) {
30
+ return names.some((n) => flags[n] === true || flags[n] !== void 0);
31
+ }
32
+
33
+ export {
34
+ parseFlags,
35
+ flagBool
36
+ };
37
+ //# sourceMappingURL=chunk-GEAH6PTG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/flags.ts"],"sourcesContent":["/**\n * Hand-rolled CLI flag parser — no library dependency.\n *\n * Supports:\n * --flag boolean flag (value: true)\n * --flag=value string flag\n * --flag value string flag (next positional arg is consumed as value)\n *\n * Returns:\n * flags — Record of flag name → string | true\n * positional — remaining args that are not flags or flag values\n */\nexport interface ParsedFlags {\n flags: Record<string, string | true>;\n positional: string[];\n}\n\nexport function parseFlags(args: string[]): ParsedFlags {\n const flags: Record<string, string | true> = {};\n const positional: string[] = [];\n\n let i = 0;\n while (i < args.length) {\n const arg = args[i];\n\n if (arg.startsWith(\"--\")) {\n const eqIdx = arg.indexOf(\"=\");\n if (eqIdx !== -1) {\n // --flag=value (only this form captures a string value)\n const name = arg.slice(2, eqIdx);\n const value = arg.slice(eqIdx + 1);\n flags[name] = value;\n } else {\n // --flag (always boolean; no peek-consume to avoid positional ambiguity)\n flags[arg.slice(2)] = true;\n }\n } else if (arg.startsWith(\"-\") && arg.length === 2) {\n // Short flags: -j, -y\n const name = arg.slice(1);\n flags[name] = true;\n } else {\n positional.push(arg);\n }\n\n i++;\n }\n\n return { flags, positional };\n}\n\nexport function flagBool(flags: Record<string, string | true>, ...names: string[]): boolean {\n return names.some((n) => flags[n] === true || flags[n] !== undefined);\n}\n\nexport function flagString(\n flags: Record<string, string | true>,\n name: string\n): string | undefined {\n const v = flags[name];\n return typeof v === \"string\" ? v : undefined;\n}\n"],"mappings":";;;AAiBO,SAAS,WAAW,MAA6B;AACtD,QAAM,QAAuC,CAAC;AAC9C,QAAM,aAAuB,CAAC;AAE9B,MAAI,IAAI;AACR,SAAO,IAAI,KAAK,QAAQ;AACtB,UAAM,MAAM,KAAK,CAAC;AAElB,QAAI,IAAI,WAAW,IAAI,GAAG;AACxB,YAAM,QAAQ,IAAI,QAAQ,GAAG;AAC7B,UAAI,UAAU,IAAI;AAEhB,cAAM,OAAO,IAAI,MAAM,GAAG,KAAK;AAC/B,cAAM,QAAQ,IAAI,MAAM,QAAQ,CAAC;AACjC,cAAM,IAAI,IAAI;AAAA,MAChB,OAAO;AAEL,cAAM,IAAI,MAAM,CAAC,CAAC,IAAI;AAAA,MACxB;AAAA,IACF,WAAW,IAAI,WAAW,GAAG,KAAK,IAAI,WAAW,GAAG;AAElD,YAAM,OAAO,IAAI,MAAM,CAAC;AACxB,YAAM,IAAI,IAAI;AAAA,IAChB,OAAO;AACL,iBAAW,KAAK,GAAG;AAAA,IACrB;AAEA;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,WAAW;AAC7B;AAEO,SAAS,SAAS,UAAyC,OAA0B;AAC1F,SAAO,MAAM,KAAK,CAAC,MAAM,MAAM,CAAC,MAAM,QAAQ,MAAM,CAAC,MAAM,MAAS;AACtE;","names":[]}
@@ -26,10 +26,7 @@ function buildCodeExplainerEntries(hookScriptPath) {
26
26
  function isCodeExplainerHook(cmd) {
27
27
  return cmd.includes(HOOK_MARKER) && cmd.includes("post-tool");
28
28
  }
29
- function mergeHooksIntoSettings(projectRoot, hookScriptPath, { useLocal = true } = {}) {
30
- const claudeDir = join(projectRoot, ".claude");
31
- const filename = useLocal ? "settings.local.json" : "settings.json";
32
- const settingsPath = join(claudeDir, filename);
29
+ function mergeHooksAtPath(settingsPath, hookScriptPath) {
33
30
  let settings = {};
34
31
  let created = false;
35
32
  if (existsSync(settingsPath)) {
@@ -49,8 +46,9 @@ function mergeHooksIntoSettings(projectRoot, hookScriptPath, { useLocal = true }
49
46
  }
50
47
  } else {
51
48
  created = true;
52
- if (!existsSync(claudeDir)) {
53
- mkdirSync(claudeDir, { recursive: true });
49
+ const parent = dirname(settingsPath);
50
+ if (!existsSync(parent)) {
51
+ mkdirSync(parent, { recursive: true });
54
52
  }
55
53
  }
56
54
  if (!settings.hooks) settings.hooks = {};
@@ -64,90 +62,7 @@ function mergeHooksIntoSettings(projectRoot, hookScriptPath, { useLocal = true }
64
62
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
65
63
  return { created, path: settingsPath };
66
64
  }
67
- function removeHooksFromSettings(projectRoot, { useLocal = true } = {}) {
68
- const candidates = useLocal ? [".claude/settings.local.json", ".claude/settings.json"] : [".claude/settings.json"];
69
- let removedAny = false;
70
- let lastPath = null;
71
- for (const rel of candidates) {
72
- const path = join(projectRoot, rel);
73
- if (!existsSync(path)) continue;
74
- let settings;
75
- try {
76
- settings = JSON.parse(readFileSync(path, "utf-8"));
77
- } catch {
78
- continue;
79
- }
80
- if (!settings.hooks?.PostToolUse) continue;
81
- const before = JSON.stringify(settings.hooks.PostToolUse);
82
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.map((entry) => ({
83
- ...entry,
84
- hooks: entry.hooks.filter((h) => !isCodeExplainerHook(h.command))
85
- })).filter((entry) => entry.hooks.length > 0);
86
- const after = JSON.stringify(settings.hooks.PostToolUse);
87
- if (before !== after) {
88
- if (settings.hooks.PostToolUse.length === 0) {
89
- delete settings.hooks.PostToolUse;
90
- }
91
- if (Object.keys(settings.hooks).length === 0) {
92
- delete settings.hooks;
93
- }
94
- writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
95
- removedAny = true;
96
- lastPath = path;
97
- }
98
- }
99
- return { removed: removedAny, path: lastPath };
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");
65
+ function removeHooksAtPath(settingsPath) {
151
66
  if (!existsSync(settingsPath)) return { removed: false, path: null };
152
67
  let settings;
153
68
  try {
@@ -168,11 +83,36 @@ function removeHooksFromUserSettings() {
168
83
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
169
84
  return { removed: true, path: settingsPath };
170
85
  }
86
+ function mergeHooksIntoSettings(projectRoot, hookScriptPath, { useLocal = true } = {}) {
87
+ const filename = useLocal ? "settings.local.json" : "settings.json";
88
+ const settingsPath = join(projectRoot, ".claude", filename);
89
+ return mergeHooksAtPath(settingsPath, hookScriptPath);
90
+ }
91
+ function mergeHooksIntoUserSettings(hookScriptPath) {
92
+ const settingsPath = join(homedir(), ".claude", "settings.json");
93
+ return mergeHooksAtPath(settingsPath, hookScriptPath);
94
+ }
95
+ function removeHooksFromSettings(projectRoot, { useLocal = true } = {}) {
96
+ const candidates = useLocal ? [".claude/settings.local.json", ".claude/settings.json"] : [".claude/settings.json"];
97
+ let removedAny = false;
98
+ let lastPath = null;
99
+ for (const rel of candidates) {
100
+ const r = removeHooksAtPath(join(projectRoot, rel));
101
+ if (r.removed) {
102
+ removedAny = true;
103
+ lastPath = r.path;
104
+ }
105
+ }
106
+ return { removed: removedAny, path: lastPath };
107
+ }
108
+ function removeHooksFromUserSettings() {
109
+ return removeHooksAtPath(join(homedir(), ".claude", "settings.json"));
110
+ }
171
111
 
172
112
  export {
173
113
  mergeHooksIntoSettings,
174
- removeHooksFromSettings,
175
114
  mergeHooksIntoUserSettings,
115
+ removeHooksFromSettings,
176
116
  removeHooksFromUserSettings
177
117
  };
178
- //# sourceMappingURL=chunk-2PUO5G3C.js.map
118
+ //# sourceMappingURL=chunk-KK76JK7S.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 * Core merge: read/parse/merge-and-write at a specific settings path.\n * Creates the parent dir if needed. Preserves all existing hooks and other\n * top-level keys. Idempotent — removes previous code-explainer entries\n * before adding the new ones so re-running does not duplicate.\n *\n * Throws if the existing file is malformed JSON or not a top-level object.\n */\nfunction mergeHooksAtPath(settingsPath: string, hookScriptPath: string): MergeResult {\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 const parent = dirname(settingsPath);\n if (!existsSync(parent)) {\n mkdirSync(parent, { 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 * Core remove: read/filter/write at a specific settings path.\n * Silently skips missing or malformed files (we don't want to corrupt\n * unrelated user config during an uninstall).\n */\nfunction removeHooksAtPath(settingsPath: string): { removed: boolean; path: string | null } {\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\n/**\n * Merge code-explainer hooks into a project's .claude/settings[.local].json.\n */\nexport function mergeHooksIntoSettings(\n projectRoot: string,\n hookScriptPath: string,\n { useLocal = true }: { useLocal?: boolean } = {}\n): MergeResult {\n const filename = useLocal ? \"settings.local.json\" : \"settings.json\";\n const settingsPath = join(projectRoot, \".claude\", filename);\n return mergeHooksAtPath(settingsPath, hookScriptPath);\n}\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 settingsPath = join(homedir(), \".claude\", \"settings.json\");\n return mergeHooksAtPath(settingsPath, hookScriptPath);\n}\n\n/**\n * Remove all code-explainer hook entries from a project's settings files,\n * preserving other hooks and config.\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 r = removeHooksAtPath(join(projectRoot, rel));\n if (r.removed) {\n removedAny = true;\n lastPath = r.path;\n }\n }\n\n return { removed: removedAny, path: lastPath };\n}\n\n/**\n * Remove code-explainer hook entries from ~/.claude/settings.json.\n */\nexport function removeHooksFromUserSettings(): { removed: boolean; path: string | null } {\n return removeHooksAtPath(join(homedir(), \".claude\", \"settings.json\"));\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,140 @@
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
+ import { z } from "zod";
8
+ var LANGUAGE_NAMES = {
9
+ en: "English",
10
+ pt: "Portuguese",
11
+ es: "Spanish",
12
+ fr: "French",
13
+ de: "German",
14
+ it: "Italian",
15
+ zh: "Chinese",
16
+ ja: "Japanese",
17
+ ko: "Korean"
18
+ };
19
+ var LEARNER_LEVEL_NAMES = {
20
+ none: "Never programmed",
21
+ beginner: "Just starting out",
22
+ intermediate: "Read code with difficulty",
23
+ regular: "Code regularly"
24
+ };
25
+ var CONFIG_FILENAME = "code-explainer.config.json";
26
+ function getGlobalConfigPath() {
27
+ return join(homedir(), ".code-explainer.config.json");
28
+ }
29
+ var DEFAULT_CONFIG = {
30
+ engine: "ollama",
31
+ ollamaModel: "qwen3.5:4b",
32
+ ollamaUrl: "http://localhost:11434",
33
+ detailLevel: "standard",
34
+ language: "en",
35
+ learnerLevel: "intermediate",
36
+ hooks: {
37
+ edit: true,
38
+ write: true,
39
+ bash: true
40
+ },
41
+ exclude: ["*.lock", "dist/**", "node_modules/**"],
42
+ skipIfSlowMs: 3e4,
43
+ bashFilter: {
44
+ capturePatterns: [
45
+ "rm",
46
+ "mv",
47
+ "cp",
48
+ "mkdir",
49
+ "npm install",
50
+ "pip install",
51
+ "yarn add",
52
+ "pnpm add",
53
+ "chmod",
54
+ "chown",
55
+ "git checkout",
56
+ "git reset",
57
+ "git revert",
58
+ "sed -i"
59
+ ]
60
+ }
61
+ };
62
+ var EngineSchema = z.enum(["ollama", "claude"]);
63
+ var DetailLevelSchema = z.enum(["minimal", "standard", "verbose"]);
64
+ var LanguageSchema = z.enum(["en", "pt", "es", "fr", "de", "it", "zh", "ja", "ko"]);
65
+ var LearnerLevelSchema = z.enum(["none", "beginner", "intermediate", "regular"]);
66
+ var HooksConfigSchema = z.object({
67
+ edit: z.boolean().default(true),
68
+ write: z.boolean().default(true),
69
+ bash: z.boolean().default(true)
70
+ }).default({ edit: true, write: true, bash: true });
71
+ var BashFilterConfigSchema = z.object({
72
+ capturePatterns: z.array(z.string()).default([])
73
+ }).default({ capturePatterns: [] });
74
+ var ConfigSchema = z.object({
75
+ engine: EngineSchema.default("ollama"),
76
+ ollamaModel: z.string().min(1).default("qwen3.5:4b"),
77
+ ollamaUrl: z.string().url().default("http://localhost:11434"),
78
+ detailLevel: DetailLevelSchema.default("standard"),
79
+ language: LanguageSchema.default("en"),
80
+ learnerLevel: LearnerLevelSchema.default("intermediate"),
81
+ hooks: HooksConfigSchema,
82
+ exclude: z.array(z.string()).default(["*.lock", "dist/**", "node_modules/**"]),
83
+ skipIfSlowMs: z.coerce.number().int().min(0).default(3e4),
84
+ bashFilter: BashFilterConfigSchema
85
+ });
86
+ function validateConfig(raw) {
87
+ const result = ConfigSchema.safeParse(raw);
88
+ if (result.success) return result.data;
89
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".") || "<root>"}: ${i.message}`).join("\n");
90
+ throw new Error(`[code-explainer] Invalid config:
91
+ ${issues}`);
92
+ }
93
+ function mergeConfig(base, overlay) {
94
+ return {
95
+ ...base,
96
+ ...overlay,
97
+ hooks: { ...base.hooks, ...overlay.hooks ?? {} },
98
+ bashFilter: {
99
+ ...base.bashFilter,
100
+ ...overlay.bashFilter ?? {}
101
+ }
102
+ };
103
+ }
104
+ function tryReadJson(path) {
105
+ if (!existsSync(path)) return null;
106
+ let raw;
107
+ try {
108
+ raw = JSON.parse(readFileSync(path, "utf-8"));
109
+ } catch (err) {
110
+ const msg = err instanceof Error ? err.message : String(err);
111
+ throw new Error(`[code-explainer] Config file ${path} is not valid JSON: ${msg}`);
112
+ }
113
+ const partial = ConfigSchema.partial().safeParse(raw);
114
+ if (!partial.success) {
115
+ const issues = partial.error.issues.map((i) => ` ${i.path.join(".") || "<root>"}: ${i.message}`).join("\n");
116
+ throw new Error(`[code-explainer] Invalid config in ${path}:
117
+ ${issues}`);
118
+ }
119
+ return partial.data;
120
+ }
121
+ function loadConfig(configPath) {
122
+ const globalConfig = tryReadJson(getGlobalConfigPath());
123
+ const projectConfig = tryReadJson(configPath);
124
+ let result = DEFAULT_CONFIG;
125
+ if (globalConfig) result = mergeConfig(result, globalConfig);
126
+ if (projectConfig) result = mergeConfig(result, projectConfig);
127
+ return result;
128
+ }
129
+
130
+ export {
131
+ LANGUAGE_NAMES,
132
+ LEARNER_LEVEL_NAMES,
133
+ CONFIG_FILENAME,
134
+ getGlobalConfigPath,
135
+ DEFAULT_CONFIG,
136
+ ConfigSchema,
137
+ validateConfig,
138
+ loadConfig
139
+ };
140
+ //# sourceMappingURL=chunk-LWASVVBV.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\";\nimport { z } from \"zod\";\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 type LearnerLevel = \"none\" | \"beginner\" | \"intermediate\" | \"regular\";\n\nexport const LEARNER_LEVEL_NAMES: Record<LearnerLevel, string> = {\n none: \"Never programmed\",\n beginner: \"Just starting out\",\n intermediate: \"Read code with difficulty\",\n regular: \"Code regularly\",\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 learnerLevel: LearnerLevel;\n hooks: HooksConfig;\n exclude: string[];\n skipIfSlowMs: number;\n bashFilter: BashFilterConfig;\n}\n\nexport interface DeepDiveItem {\n term: string;\n explanation: string;\n}\n\nexport interface ExplanationResult {\n impact: string;\n howItWorks: string;\n why: string;\n deepDive: DeepDiveItem[];\n isSamePattern: boolean;\n samePatternNote: 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 // Claude Code sends this as an object for Edit/Write/MultiEdit and a string\n // for Bash; type it as unknown so consumers validate before use.\n tool_response: unknown;\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 learnerLevel: \"intermediate\",\n hooks: {\n edit: true,\n write: true,\n bash: true,\n },\n exclude: [\"*.lock\", \"dist/**\", \"node_modules/**\"],\n skipIfSlowMs: 30000,\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\n// ---------------------------------------------------------------------------\n// Zod schema — used to validate and parse config files at load time.\n// Using z.coerce where sensible so that older config files with slightly\n// different types (e.g., skipIfSlowMs stored as a string) still work.\n// ---------------------------------------------------------------------------\n\nconst EngineSchema = z.enum([\"ollama\", \"claude\"]);\nconst DetailLevelSchema = z.enum([\"minimal\", \"standard\", \"verbose\"]);\nconst LanguageSchema = z.enum([\"en\", \"pt\", \"es\", \"fr\", \"de\", \"it\", \"zh\", \"ja\", \"ko\"]);\nconst LearnerLevelSchema = z.enum([\"none\", \"beginner\", \"intermediate\", \"regular\"]);\n\nconst HooksConfigSchema = z.object({\n edit: z.boolean().default(true),\n write: z.boolean().default(true),\n bash: z.boolean().default(true),\n}).default({ edit: true, write: true, bash: true });\n\nconst BashFilterConfigSchema = z.object({\n capturePatterns: z.array(z.string()).default([]),\n}).default({ capturePatterns: [] });\n\nexport const ConfigSchema = z.object({\n engine: EngineSchema.default(\"ollama\"),\n ollamaModel: z.string().min(1).default(\"qwen3.5:4b\"),\n ollamaUrl: z.string().url().default(\"http://localhost:11434\"),\n detailLevel: DetailLevelSchema.default(\"standard\"),\n language: LanguageSchema.default(\"en\"),\n learnerLevel: LearnerLevelSchema.default(\"intermediate\"),\n hooks: HooksConfigSchema,\n exclude: z.array(z.string()).default([\"*.lock\", \"dist/**\", \"node_modules/**\"]),\n skipIfSlowMs: z.coerce.number().int().min(0).default(30000),\n bashFilter: BashFilterConfigSchema,\n});\n\nexport type ConfigInput = z.input<typeof ConfigSchema>;\n\n/**\n * Validate a raw JSON object against the config schema.\n * Returns a typed Config on success, or throws a ZodError-derived Error with\n * a human-readable message listing all invalid fields.\n */\nexport function validateConfig(raw: unknown): Config {\n const result = ConfigSchema.safeParse(raw);\n if (result.success) return result.data;\n\n const issues = result.error.issues\n .map((i) => ` ${i.path.join(\".\") || \"<root>\"}: ${i.message}`)\n .join(\"\\n\");\n throw new Error(`[code-explainer] Invalid config:\\n${issues}`);\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\n/**\n * Read and parse a config file. Returns the partial config or null if the\n * file is missing. Throws if the JSON is malformed or fails schema validation\n * (so callers surface useful errors rather than silently using defaults).\n */\nfunction tryReadJson(path: string): Partial<Config> | null {\n if (!existsSync(path)) return null;\n let raw: unknown;\n try {\n raw = JSON.parse(readFileSync(path, \"utf-8\"));\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`[code-explainer] Config file ${path} is not valid JSON: ${msg}`);\n }\n // Partial validation: only validate keys that are present. Unknown keys are\n // ignored (forward-compat). We re-use ConfigSchema with .partial() so that\n // missing keys fall through to the DEFAULT_CONFIG merger rather than errors.\n const partial = ConfigSchema.partial().safeParse(raw);\n if (!partial.success) {\n const issues = partial.error.issues\n .map((i) => ` ${i.path.join(\".\") || \"<root>\"}: ${i.message}`)\n .join(\"\\n\");\n throw new Error(`[code-explainer] Invalid config in ${path}:\\n${issues}`);\n }\n return partial.data as Partial<Config>;\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":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  LANGUAGE_NAMES
4
- } from "./chunk-RK7ZFN4W.js";
4
+ } from "./chunk-LWASVVBV.js";
5
5
 
6
6
  // src/prompts/templates.ts
7
7
  var FILE_LANGUAGE_MAP = {
@@ -43,9 +43,10 @@ function detectLanguage(filePath) {
43
43
  const ext = filePath.slice(dotIdx).toLowerCase();
44
44
  return FILE_LANGUAGE_MAP[ext] ?? "Unknown";
45
45
  }
46
- var INJECTION_PATTERN = /^[+\-\s]*(?:\/\/+|\/\*+|#+|--|;+|\*+)?\s*(RULES?|SYSTEM|INSTRUCTION|OUTPUT|PROMPT|ASSISTANT|USER)\s*:/i;
46
+ var INJECTION_PATTERN = /^[+\-\s]*(?:\/\/+|\/\*+|#+|--|;+|\*+|`+|'+|"+|<!--+|@+|%%*)?\s*(RULES?|SYSTEM|INSTRUCTION|OUTPUT|PROMPT|ASSISTANT|USER|CONTEXT|IGNORE\s+PREVIOUS|DISREGARD|FORGET)\s*:/i;
47
47
  function sanitizeDiff(diff, maxChars = 4e3) {
48
- const lines = diff.split("\n");
48
+ const normalized = diff.normalize("NFKC");
49
+ const lines = normalized.split("\n");
49
50
  const kept = [];
50
51
  let linesStripped = 0;
51
52
  for (const line of lines) {
@@ -182,13 +183,7 @@ function buildClaudePrompt(detail, inputs) {
182
183
  const fileLang = detectLanguage(inputs.filePath);
183
184
  const language = inputs.language ?? "en";
184
185
  const learnerLevel = inputs.learnerLevel ?? "intermediate";
185
- const userPrompt = inputs.userPrompt;
186
186
  const recent = recentSummariesContext(inputs.recentSummaries ?? []);
187
- const userContextBlock = userPrompt ? `
188
- The user originally asked the assistant to do this:
189
- "${userPrompt}"
190
-
191
- UNRELATED-CHANGE CHECK: If the change you are about to explain is NOT related to that request, set "risk" to at least "medium" and explain in "riskReason" that this change was not part of the original ask. Mention the specific mismatch.` : "";
192
187
  return `You are code-explainer, a tool that helps non-developers understand and decide on code changes proposed by an AI coding assistant.
193
188
 
194
189
  Your goal: give the reader enough context to feel confident accepting or questioning the change, AND help them recognize this kind of change in the future.
@@ -199,7 +194,6 @@ When teaching, focus on:
199
194
  - why: why this approach was used (idioms, patterns, common practice)
200
195
 
201
196
  A unified diff has "-" lines (removed) and "+" lines (added). Together they show a CHANGE. Only "+" lines = addition. Only "-" lines = removal.
202
- ${userContextBlock}
203
197
 
204
198
  ${recent}
205
199
 
@@ -227,15 +221,35 @@ ${levelInstruction(learnerLevel)}
227
221
  ${languageInstruction(language)}`;
228
222
  }
229
223
 
230
- // src/engines/ollama.ts
231
- function isLoopback(url) {
232
- try {
233
- const u = new URL(url);
234
- const host = u.hostname;
235
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
236
- } catch {
237
- return false;
224
+ // src/engines/parse.ts
225
+ function extractBalancedObject(text, startIdx) {
226
+ let depth = 0;
227
+ let inString = false;
228
+ let escape = false;
229
+ for (let i = startIdx; i < text.length; i++) {
230
+ const ch = text[i];
231
+ if (escape) {
232
+ escape = false;
233
+ continue;
234
+ }
235
+ if (ch === "\\") {
236
+ escape = true;
237
+ continue;
238
+ }
239
+ if (ch === '"') {
240
+ inString = !inString;
241
+ continue;
242
+ }
243
+ if (inString) continue;
244
+ if (ch === "{") depth++;
245
+ else if (ch === "}") {
246
+ depth--;
247
+ if (depth === 0) {
248
+ return text.slice(startIdx, i + 1);
249
+ }
250
+ }
238
251
  }
252
+ return null;
239
253
  }
240
254
  function extractJson(text) {
241
255
  const trimmed = text.trim();
@@ -266,54 +280,27 @@ function extractJson(text) {
266
280
  }
267
281
  return null;
268
282
  }
269
- function extractBalancedObject(text, startIdx) {
270
- let depth = 0;
271
- let inString = false;
272
- let escape = false;
273
- for (let i = startIdx; i < text.length; i++) {
274
- const ch = text[i];
275
- if (escape) {
276
- escape = false;
277
- continue;
278
- }
279
- if (ch === "\\") {
280
- escape = true;
281
- continue;
282
- }
283
- if (ch === '"') {
284
- inString = !inString;
285
- continue;
286
- }
287
- if (inString) continue;
288
- if (ch === "{") depth++;
289
- else if (ch === "}") {
290
- depth--;
291
- if (depth === 0) {
292
- return text.slice(startIdx, i + 1);
293
- }
294
- }
295
- }
296
- return null;
297
- }
298
283
  function coerceString(v) {
299
284
  return typeof v === "string" ? v : "";
300
285
  }
301
286
  function coerceDeepDive(v) {
302
287
  if (!Array.isArray(v)) return [];
303
- return v.filter((it) => typeof it === "object" && it !== null).map((it) => ({
288
+ return v.filter(
289
+ (it) => typeof it === "object" && it !== null
290
+ ).map((it) => ({
304
291
  term: coerceString(it.term),
305
292
  explanation: coerceString(it.explanation)
306
293
  })).filter((it) => it.term.length > 0);
307
294
  }
295
+ function coerceRisk(v) {
296
+ const s = coerceString(v);
297
+ return s === "low" || s === "medium" || s === "high" ? s : "none";
298
+ }
308
299
  function parseResponse(rawText) {
309
300
  const json = extractJson(rawText);
310
301
  if (!json) return null;
311
302
  try {
312
303
  const parsed = JSON.parse(json);
313
- const risk = coerceString(parsed.risk);
314
- if (!["none", "low", "medium", "high"].includes(risk)) {
315
- return null;
316
- }
317
304
  return {
318
305
  impact: coerceString(parsed.impact),
319
306
  howItWorks: coerceString(parsed.howItWorks),
@@ -321,7 +308,7 @@ function parseResponse(rawText) {
321
308
  deepDive: coerceDeepDive(parsed.deepDive),
322
309
  isSamePattern: parsed.isSamePattern === true,
323
310
  samePatternNote: coerceString(parsed.samePatternNote),
324
- risk,
311
+ risk: coerceRisk(parsed.risk),
325
312
  riskReason: coerceString(parsed.riskReason)
326
313
  };
327
314
  } catch {
@@ -332,6 +319,17 @@ function truncateText(text, max) {
332
319
  if (text.length <= max) return text;
333
320
  return text.slice(0, max) + "...";
334
321
  }
322
+
323
+ // src/engines/ollama.ts
324
+ function isLoopback(url) {
325
+ try {
326
+ const u = new URL(url);
327
+ const host = u.hostname;
328
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
329
+ } catch {
330
+ return false;
331
+ }
332
+ }
335
333
  async function callOllama(inputs) {
336
334
  const { config } = inputs;
337
335
  if (!isLoopback(config.ollamaUrl)) {
@@ -342,16 +340,27 @@ async function callOllama(inputs) {
342
340
  fix: "Change ollamaUrl to http://localhost:11434 via 'npx vibe-code-explainer config'"
343
341
  };
344
342
  }
345
- const systemPrompt = buildOllamaSystemPrompt(
346
- config.detailLevel,
347
- config.language,
348
- config.learnerLevel
349
- );
350
- const userPrompt = buildOllamaUserPrompt({
351
- filePath: inputs.filePath,
352
- diff: inputs.diff,
353
- recentSummaries: inputs.recentSummaries
354
- });
343
+ let systemPrompt;
344
+ let userPrompt;
345
+ try {
346
+ systemPrompt = buildOllamaSystemPrompt(
347
+ config.detailLevel,
348
+ config.language,
349
+ config.learnerLevel
350
+ );
351
+ userPrompt = buildOllamaUserPrompt({
352
+ filePath: inputs.filePath,
353
+ diff: inputs.diff,
354
+ recentSummaries: inputs.recentSummaries
355
+ });
356
+ } catch (err) {
357
+ return {
358
+ kind: "error",
359
+ problem: "Failed to build Ollama prompt",
360
+ cause: err.message || String(err),
361
+ fix: "Check detailLevel/learnerLevel/language values via 'npx vibe-code-explainer config'"
362
+ };
363
+ }
355
364
  const controller = new AbortController();
356
365
  const timeout = config.skipIfSlowMs > 0 ? setTimeout(() => controller.abort(), config.skipIfSlowMs) : null;
357
366
  try {
@@ -366,9 +375,9 @@ async function callOllama(inputs) {
366
375
  }),
367
376
  signal: controller.signal
368
377
  });
369
- if (timeout !== null) clearTimeout(timeout);
370
378
  if (!response.ok) {
371
379
  const text = await response.text().catch(() => "");
380
+ if (timeout !== null) clearTimeout(timeout);
372
381
  if (response.status === 404 || /model.*not found/i.test(text)) {
373
382
  return {
374
383
  kind: "error",
@@ -385,6 +394,7 @@ async function callOllama(inputs) {
385
394
  };
386
395
  }
387
396
  const data = await response.json();
397
+ if (timeout !== null) clearTimeout(timeout);
388
398
  const rawText = data.response ?? "";
389
399
  if (!rawText.trim()) {
390
400
  return { kind: "skip", reason: "Ollama returned an empty response" };
@@ -433,38 +443,29 @@ async function callOllama(inputs) {
433
443
  };
434
444
  }
435
445
  }
436
- async function runWarmup() {
437
- const { loadConfig, DEFAULT_CONFIG } = await import("./schema-YEJIXFMK.js");
438
- const config = (() => {
439
- try {
440
- return loadConfig("code-explainer.config.json");
441
- } catch {
442
- return DEFAULT_CONFIG;
443
- }
444
- })();
445
- process.stderr.write(`[code-explainer] Warming up ${config.ollamaModel}...
446
- `);
446
+ async function runWarmup(config) {
447
447
  const outcome = await callOllama({
448
448
  filePath: "warmup.txt",
449
449
  diff: "+ hello world",
450
450
  config: { ...config, skipIfSlowMs: 6e4 }
451
451
  });
452
- if (outcome.kind === "ok") {
453
- process.stderr.write("[code-explainer] Warmup complete. First real explanation will be fast.\n");
454
- } else if (outcome.kind === "error") {
455
- process.stderr.write(`[code-explainer] Warmup failed. ${outcome.problem}. ${outcome.cause}. Fix: ${outcome.fix}.
456
- `);
457
- process.exit(1);
458
- } else {
459
- process.stderr.write(`[code-explainer] Warmup skipped: ${outcome.reason}
460
- `);
452
+ if (outcome.kind === "ok") return { kind: "ok" };
453
+ if (outcome.kind === "error") {
454
+ return {
455
+ kind: "error",
456
+ problem: outcome.problem,
457
+ cause: outcome.cause,
458
+ fix: outcome.fix
459
+ };
461
460
  }
461
+ return { kind: "skip", reason: outcome.reason };
462
462
  }
463
463
 
464
464
  export {
465
465
  buildClaudePrompt,
466
466
  parseResponse,
467
+ truncateText,
467
468
  callOllama,
468
469
  runWarmup
469
470
  };
470
- //# sourceMappingURL=chunk-ABPTVWQ3.js.map
471
+ //# sourceMappingURL=chunk-R5H62KGX.js.map