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.
- package/dist/chunk-GEAH6PTG.js +37 -0
- package/dist/chunk-GEAH6PTG.js.map +1 -0
- package/dist/{chunk-2PUO5G3C.js → chunk-KK76JK7S.js} +32 -92
- package/dist/chunk-KK76JK7S.js.map +1 -0
- package/dist/chunk-LWASVVBV.js +140 -0
- package/dist/chunk-LWASVVBV.js.map +1 -0
- package/dist/{chunk-ABPTVWQ3.js → chunk-R5H62KGX.js} +86 -85
- package/dist/chunk-R5H62KGX.js.map +1 -0
- package/dist/{chunk-XW3S5GNV.js → chunk-VJN7Y4SI.js} +114 -33
- package/dist/chunk-VJN7Y4SI.js.map +1 -0
- package/dist/cli/index.js +37 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/{config-AHHWBME7.js → config-4DNTCZ6X.js} +127 -8
- package/dist/config-4DNTCZ6X.js.map +1 -0
- package/dist/hooks/post-tool.js +143 -162
- package/dist/hooks/post-tool.js.map +1 -1
- package/dist/{init-XXK6SGF2.js → init-YHRKOKSY.js} +12 -16
- package/dist/init-YHRKOKSY.js.map +1 -0
- package/dist/ollama-43BPUEEC.js +12 -0
- package/dist/{schema-YEJIXFMK.js → schema-MYOWRNBW.js} +8 -4
- package/dist/{tracker-Z5EEYUUZ.js → tracker-Y2G5DW6Y.js} +2 -2
- package/dist/{uninstall-AIH4HVPZ.js → uninstall-YADL7OUB.js} +3 -3
- package/package.json +3 -2
- package/dist/chunk-2PUO5G3C.js.map +0 -1
- package/dist/chunk-ABPTVWQ3.js.map +0 -1
- package/dist/chunk-RK7ZFN4W.js +0 -97
- package/dist/chunk-RK7ZFN4W.js.map +0 -1
- package/dist/chunk-XW3S5GNV.js.map +0 -1
- package/dist/config-AHHWBME7.js.map +0 -1
- package/dist/init-XXK6SGF2.js.map +0 -1
- package/dist/ollama-2WHLTTDD.js +0 -14
- /package/dist/{ollama-2WHLTTDD.js.map → ollama-43BPUEEC.js.map} +0 -0
- /package/dist/{schema-YEJIXFMK.js.map → schema-MYOWRNBW.js.map} +0 -0
- /package/dist/{tracker-Z5EEYUUZ.js.map → tracker-Y2G5DW6Y.js.map} +0 -0
- /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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
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-
|
|
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":";;;AAAA,SAAS,YAAY,cAAc,eAAe,iBAAiB;AACnE,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAEvB,IAAM,cAAc;AAe3B,SAAS,iBAAiB,gBAAgC;AACxD,SAAO,SAAS,cAAc;AAChC;AAEA,SAAS,0BAA0B,gBAA4D;AAC7F,QAAM,UAAU,iBAAiB,cAAc;AAC/C,SAAO;AAAA,IACL,aAAa;AAAA,MACX;AAAA,QACE,SAAS;AAAA,QACT,OAAO,CAAC,EAAE,MAAM,WAAW,QAAQ,CAAC;AAAA,MACtC;AAAA,MACA;AAAA,QACE,SAAS;AAAA,QACT,OAAO,CAAC,EAAE,MAAM,WAAW,QAAQ,CAAC;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,oBAAoB,KAAsB;AACjD,SAAO,IAAI,SAAS,WAAW,KAAK,IAAI,SAAS,WAAW;AAC9D;AAeA,SAAS,iBAAiB,cAAsB,gBAAqC;AACnF,MAAI,WAA2B,CAAC;AAChC,MAAI,UAAU;AAEd,MAAI,WAAW,YAAY,GAAG;AAC5B,UAAM,MAAM,aAAa,cAAc,OAAO;AAC9C,QAAI;AACF,iBAAW,KAAK,MAAM,GAAG;AAAA,IAC3B,SAAS,KAAK;AACZ,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAM,IAAI;AAAA,QACR,4CAA4C,YAAY,4JAA4J,GAAG;AAAA,MACzN;AAAA,IACF;AACA,QAAI,OAAO,aAAa,YAAY,aAAa,QAAQ,MAAM,QAAQ,QAAQ,GAAG;AAChF,YAAM,IAAI;AAAA,QACR,4CAA4C,YAAY;AAAA,MAC1D;AAAA,IACF;AAAA,EACF,OAAO;AACL,cAAU;AACV,UAAM,SAAS,QAAQ,YAAY;AACnC,QAAI,CAAC,WAAW,MAAM,GAAG;AACvB,gBAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AAAA,IACvC;AAAA,EACF;AAEA,MAAI,CAAC,SAAS,MAAO,UAAS,QAAQ,CAAC;AAEvC,QAAM,aAAa,0BAA0B,cAAc;AAC3D,QAAM,mBAAmB,SAAS,MAAM,eAAe,CAAC;AAGxD,QAAM,UAAU,iBACb,IAAI,CAAC,WAAW;AAAA,IACf,GAAG;AAAA,IACH,OAAO,MAAM,MAAM,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,OAAO,CAAC;AAAA,EAClE,EAAE,EACD,OAAO,CAAC,UAAU,MAAM,MAAM,SAAS,CAAC;AAE3C,WAAS,MAAM,cAAc,CAAC,GAAG,SAAS,GAAG,WAAW,WAAW;AAEnE,gBAAc,cAAc,KAAK,UAAU,UAAU,MAAM,CAAC,IAAI,IAAI;AAEpE,SAAO,EAAE,SAAS,MAAM,aAAa;AACvC;AAOA,SAAS,kBAAkB,cAAiE;AAC1F,MAAI,CAAC,WAAW,YAAY,EAAG,QAAO,EAAE,SAAS,OAAO,MAAM,KAAK;AAEnE,MAAI;AACJ,MAAI;AACF,eAAW,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;AAAA,EAC3D,QAAQ;AACN,WAAO,EAAE,SAAS,OAAO,MAAM,KAAK;AAAA,EACtC;AAEA,MAAI,CAAC,SAAS,OAAO,YAAa,QAAO,EAAE,SAAS,OAAO,MAAM,KAAK;AAEtE,QAAM,SAAS,KAAK,UAAU,SAAS,MAAM,WAAW;AACxD,WAAS,MAAM,cAAc,SAAS,MAAM,YACzC,IAAI,CAAC,WAAW;AAAA,IACf,GAAG;AAAA,IACH,OAAO,MAAM,MAAM,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,OAAO,CAAC;AAAA,EAClE,EAAE,EACD,OAAO,CAAC,UAAU,MAAM,MAAM,SAAS,CAAC;AAC3C,QAAM,QAAQ,KAAK,UAAU,SAAS,MAAM,WAAW;AAEvD,MAAI,WAAW,MAAO,QAAO,EAAE,SAAS,OAAO,MAAM,KAAK;AAE1D,MAAI,SAAS,MAAM,YAAY,WAAW,EAAG,QAAO,SAAS,MAAM;AACnE,MAAI,OAAO,KAAK,SAAS,KAAK,EAAE,WAAW,EAAG,QAAO,SAAS;AAE9D,gBAAc,cAAc,KAAK,UAAU,UAAU,MAAM,CAAC,IAAI,IAAI;AACpE,SAAO,EAAE,SAAS,MAAM,MAAM,aAAa;AAC7C;AAKO,SAAS,uBACd,aACA,gBACA,EAAE,WAAW,KAAK,IAA4B,CAAC,GAClC;AACb,QAAM,WAAW,WAAW,wBAAwB;AACpD,QAAM,eAAe,KAAK,aAAa,WAAW,QAAQ;AAC1D,SAAO,iBAAiB,cAAc,cAAc;AACtD;AAMO,SAAS,2BAA2B,gBAAqC;AAC9E,QAAM,eAAe,KAAK,QAAQ,GAAG,WAAW,eAAe;AAC/D,SAAO,iBAAiB,cAAc,cAAc;AACtD;AAMO,SAAS,wBACd,aACA,EAAE,WAAW,KAAK,IAA4B,CAAC,GACJ;AAC3C,QAAM,aAAa,WACf,CAAC,+BAA+B,uBAAuB,IACvD,CAAC,uBAAuB;AAE5B,MAAI,aAAa;AACjB,MAAI,WAA0B;AAE9B,aAAW,OAAO,YAAY;AAC5B,UAAM,IAAI,kBAAkB,KAAK,aAAa,GAAG,CAAC;AAClD,QAAI,EAAE,SAAS;AACb,mBAAa;AACb,iBAAW,EAAE;AAAA,IACf;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,YAAY,MAAM,SAAS;AAC/C;AAKO,SAAS,8BAAyE;AACvF,SAAO,kBAAkB,KAAK,QAAQ,GAAG,WAAW,eAAe,CAAC;AACtE;","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-
|
|
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]*(
|
|
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
|
|
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/
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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(
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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-
|
|
471
|
+
//# sourceMappingURL=chunk-R5H62KGX.js.map
|