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.
- package/dist/chunk-GEAH6PTG.js +37 -0
- package/dist/chunk-GEAH6PTG.js.map +1 -0
- package/dist/chunk-GU4Y5ZWY.js +140 -0
- package/dist/chunk-GU4Y5ZWY.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-XW3S5GNV.js → chunk-VJN7Y4SI.js} +114 -33
- package/dist/chunk-VJN7Y4SI.js.map +1 -0
- package/dist/{chunk-ABPTVWQ3.js → chunk-ZZY3IDL2.js} +86 -85
- package/dist/chunk-ZZY3IDL2.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-YLMDBCIR.js} +116 -6
- package/dist/config-YLMDBCIR.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-UDODKO25.js} +12 -16
- package/dist/init-UDODKO25.js.map +1 -0
- package/dist/ollama-YSRRK7LL.js +12 -0
- package/dist/{schema-YEJIXFMK.js → schema-R3THK35H.js} +8 -4
- package/dist/{tracker-Z5EEYUUZ.js → tracker-Y2G5DW6Y.js} +2 -2
- package/dist/{uninstall-AIH4HVPZ.js → uninstall-5RVTDKTA.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-YSRRK7LL.js.map} +0 -0
- /package/dist/{schema-YEJIXFMK.js.map → schema-R3THK35H.js.map} +0 -0
- /package/dist/{tracker-Z5EEYUUZ.js.map → tracker-Y2G5DW6Y.js.map} +0 -0
- /package/dist/{uninstall-AIH4HVPZ.js.map → uninstall-5RVTDKTA.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":[]}
|
|
@@ -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({});
|
|
71
|
+
var BashFilterConfigSchema = z.object({
|
|
72
|
+
capturePatterns: z.array(z.string()).default([])
|
|
73
|
+
}).default({});
|
|
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-GU4Y5ZWY.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({});\n\nconst BashFilterConfigSchema = z.object({\n capturePatterns: z.array(z.string()).default([]),\n}).default({});\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":[]}
|
|
@@ -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":"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","names":[]}
|
|
@@ -4,31 +4,79 @@ import {
|
|
|
4
4
|
} from "./chunk-7OCVIDC7.js";
|
|
5
5
|
|
|
6
6
|
// src/session/tracker.ts
|
|
7
|
-
import { existsSync as
|
|
8
|
-
import {
|
|
9
|
-
import { join as join2 } from "path";
|
|
7
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, unlinkSync as unlinkSync2, readdirSync, statSync } from "fs";
|
|
8
|
+
import { join as join3 } from "path";
|
|
10
9
|
|
|
11
10
|
// src/cache/explanation-cache.ts
|
|
12
11
|
import { createHash } from "crypto";
|
|
13
|
-
import { existsSync, readFileSync, appendFileSync,
|
|
14
|
-
import {
|
|
12
|
+
import { existsSync as existsSync2, readFileSync, appendFileSync, writeFileSync, renameSync, unlinkSync } from "fs";
|
|
13
|
+
import { join as join2 } from "path";
|
|
14
|
+
|
|
15
|
+
// src/session/session-id.ts
|
|
16
|
+
var SAFE_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
17
|
+
function isSafeSessionId(id) {
|
|
18
|
+
return typeof id === "string" && SAFE_ID_PATTERN.test(id);
|
|
19
|
+
}
|
|
20
|
+
function assertSafeSessionId(id) {
|
|
21
|
+
if (!isSafeSessionId(id)) {
|
|
22
|
+
throw new Error(`unsafe session id: ${JSON.stringify(id)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/session/tmpdir.ts
|
|
27
|
+
import { existsSync, mkdirSync } from "fs";
|
|
28
|
+
import { tmpdir, userInfo } from "os";
|
|
15
29
|
import { join } from "path";
|
|
16
30
|
function getUserTmpDir() {
|
|
17
|
-
|
|
31
|
+
let suffix;
|
|
32
|
+
try {
|
|
33
|
+
const info = userInfo();
|
|
34
|
+
suffix = typeof info.username === "string" && info.username ? info.username : "user";
|
|
35
|
+
} catch {
|
|
36
|
+
suffix = "user";
|
|
37
|
+
}
|
|
38
|
+
suffix = suffix.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64) || "user";
|
|
39
|
+
const dir = join(tmpdir(), `code-explainer-${suffix}`);
|
|
18
40
|
if (!existsSync(dir)) {
|
|
19
41
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
20
42
|
}
|
|
21
43
|
return dir;
|
|
22
44
|
}
|
|
45
|
+
|
|
46
|
+
// src/cache/explanation-cache.ts
|
|
47
|
+
var CACHE_ROTATE_THRESHOLD = 500;
|
|
48
|
+
var CACHE_COMPACT_TARGET = 250;
|
|
23
49
|
function getCacheFilePath(sessionId) {
|
|
24
|
-
|
|
50
|
+
assertSafeSessionId(sessionId);
|
|
51
|
+
return join2(getUserTmpDir(), `cache-${sessionId}.jsonl`);
|
|
25
52
|
}
|
|
26
53
|
function hashDiff(diff) {
|
|
27
54
|
return createHash("sha256").update(diff, "utf-8").digest("hex");
|
|
28
55
|
}
|
|
56
|
+
function rotateCacheIfNeeded(path) {
|
|
57
|
+
try {
|
|
58
|
+
const content = readFileSync(path, "utf-8");
|
|
59
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
60
|
+
if (lines.length <= CACHE_ROTATE_THRESHOLD) return;
|
|
61
|
+
const seen = /* @__PURE__ */ new Map();
|
|
62
|
+
for (const line2 of lines) {
|
|
63
|
+
try {
|
|
64
|
+
const entry = JSON.parse(line2);
|
|
65
|
+
seen.set(entry.hash, entry);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const unique = Array.from(seen.values());
|
|
70
|
+
const compacted = unique.slice(-CACHE_COMPACT_TARGET);
|
|
71
|
+
const tmp = path + ".tmp";
|
|
72
|
+
writeFileSync(tmp, compacted.map((e) => JSON.stringify(e)).join("\n") + "\n", { mode: 384 });
|
|
73
|
+
renameSync(tmp, path);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
29
77
|
function getCached(sessionId, diff) {
|
|
30
78
|
const path = getCacheFilePath(sessionId);
|
|
31
|
-
if (!
|
|
79
|
+
if (!existsSync2(path)) return void 0;
|
|
32
80
|
const hash = hashDiff(diff);
|
|
33
81
|
try {
|
|
34
82
|
const content = readFileSync(path, "utf-8");
|
|
@@ -52,12 +100,13 @@ function setCached(sessionId, diff, result) {
|
|
|
52
100
|
const entry = { hash: hashDiff(diff), result };
|
|
53
101
|
try {
|
|
54
102
|
appendFileSync(path, JSON.stringify(entry) + "\n", { mode: 384 });
|
|
103
|
+
rotateCacheIfNeeded(path);
|
|
55
104
|
} catch {
|
|
56
105
|
}
|
|
57
106
|
}
|
|
58
107
|
function clearCache(sessionId) {
|
|
59
108
|
const path = getCacheFilePath(sessionId);
|
|
60
|
-
if (
|
|
109
|
+
if (existsSync2(path)) {
|
|
61
110
|
try {
|
|
62
111
|
unlinkSync(path);
|
|
63
112
|
} catch {
|
|
@@ -487,15 +536,10 @@ function stripAnsi(s) {
|
|
|
487
536
|
|
|
488
537
|
// src/session/tracker.ts
|
|
489
538
|
var TWO_HOURS_MS = 2 * 60 * 60 * 1e3;
|
|
490
|
-
|
|
491
|
-
const dir = join2(tmpdir2(), `code-explainer-${process.getuid?.() ?? "user"}`);
|
|
492
|
-
if (!existsSync2(dir)) {
|
|
493
|
-
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
494
|
-
}
|
|
495
|
-
return dir;
|
|
496
|
-
}
|
|
539
|
+
var CLEANUP_THROTTLE_MS = 60 * 1e3;
|
|
497
540
|
function getSessionFilePath(sessionId) {
|
|
498
|
-
|
|
541
|
+
assertSafeSessionId(sessionId);
|
|
542
|
+
return join3(getUserTmpDir(), `session-${sessionId}.jsonl`);
|
|
499
543
|
}
|
|
500
544
|
function recordEntry(sessionId, entry) {
|
|
501
545
|
const path = getSessionFilePath(sessionId);
|
|
@@ -506,7 +550,7 @@ function recordEntry(sessionId, entry) {
|
|
|
506
550
|
}
|
|
507
551
|
function readSession(sessionId) {
|
|
508
552
|
const path = getSessionFilePath(sessionId);
|
|
509
|
-
if (!
|
|
553
|
+
if (!existsSync3(path)) return [];
|
|
510
554
|
try {
|
|
511
555
|
const content = readFileSync2(path, "utf-8");
|
|
512
556
|
return content.split("\n").filter((l) => l.trim()).map((line2) => {
|
|
@@ -520,19 +564,34 @@ function readSession(sessionId) {
|
|
|
520
564
|
return [];
|
|
521
565
|
}
|
|
522
566
|
}
|
|
523
|
-
function getRecentSummaries(sessionId, n) {
|
|
524
|
-
const
|
|
525
|
-
if (
|
|
526
|
-
return
|
|
567
|
+
function getRecentSummaries(sessionId, n, entries) {
|
|
568
|
+
const all = entries ?? readSession(sessionId);
|
|
569
|
+
if (all.length === 0) return [];
|
|
570
|
+
return all.slice(-n).map((e) => `${e.file}: ${e.summary}`);
|
|
571
|
+
}
|
|
572
|
+
function getCleanupTimestampPath() {
|
|
573
|
+
return join3(getUserTmpDir(), ".last-cleanup");
|
|
527
574
|
}
|
|
528
575
|
function cleanStaleSessionFiles() {
|
|
529
576
|
try {
|
|
530
|
-
const
|
|
577
|
+
const tsPath = getCleanupTimestampPath();
|
|
531
578
|
const now = Date.now();
|
|
579
|
+
if (existsSync3(tsPath)) {
|
|
580
|
+
try {
|
|
581
|
+
const ts = parseInt(readFileSync2(tsPath, "utf-8").trim(), 10);
|
|
582
|
+
if (!isNaN(ts) && now - ts < CLEANUP_THROTTLE_MS) return;
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
writeFileSync2(tsPath, String(now), { mode: 384 });
|
|
588
|
+
} catch {
|
|
589
|
+
}
|
|
590
|
+
const dir = getUserTmpDir();
|
|
532
591
|
const entries = readdirSync(dir);
|
|
533
592
|
for (const name of entries) {
|
|
534
593
|
if (!name.startsWith("session-") && !name.startsWith("cache-")) continue;
|
|
535
|
-
const filePath =
|
|
594
|
+
const filePath = join3(dir, name);
|
|
536
595
|
try {
|
|
537
596
|
const stat = statSync(filePath);
|
|
538
597
|
if (now - stat.mtimeMs > TWO_HOURS_MS) {
|
|
@@ -549,33 +608,56 @@ function getSessionIdFromEnv() {
|
|
|
549
608
|
}
|
|
550
609
|
function findLatestSession() {
|
|
551
610
|
try {
|
|
552
|
-
const dir =
|
|
611
|
+
const dir = getUserTmpDir();
|
|
553
612
|
const entries = readdirSync(dir).filter((n) => n.startsWith("session-") && n.endsWith(".jsonl")).map((n) => ({
|
|
554
613
|
name: n,
|
|
555
614
|
id: n.slice("session-".length, -".jsonl".length),
|
|
556
|
-
mtime: statSync(
|
|
615
|
+
mtime: statSync(join3(dir, n)).mtimeMs
|
|
557
616
|
})).sort((a, b) => b.mtime - a.mtime);
|
|
558
617
|
return entries[0]?.id;
|
|
559
618
|
} catch {
|
|
560
619
|
return void 0;
|
|
561
620
|
}
|
|
562
621
|
}
|
|
563
|
-
async function printSummary() {
|
|
622
|
+
async function printSummary({ json = false } = {}) {
|
|
564
623
|
const sessionId = getSessionIdFromEnv() ?? findLatestSession();
|
|
565
624
|
if (!sessionId) {
|
|
566
|
-
|
|
625
|
+
if (json) {
|
|
626
|
+
process.stdout.write(JSON.stringify({ error: "No active session found" }) + "\n");
|
|
627
|
+
} else {
|
|
628
|
+
process.stderr.write("[code-explainer] No active session found. Session data is created when Claude Code makes changes.\n");
|
|
629
|
+
}
|
|
567
630
|
return;
|
|
568
631
|
}
|
|
569
632
|
const entries = readSession(sessionId);
|
|
570
633
|
if (entries.length === 0) {
|
|
571
|
-
|
|
634
|
+
if (json) {
|
|
635
|
+
process.stdout.write(JSON.stringify({ sessionId, totalChanges: 0, files: [], risks: { none: 0, low: 0, medium: 0, high: 0 } }) + "\n");
|
|
636
|
+
} else {
|
|
637
|
+
process.stderr.write(`[code-explainer] Session '${sessionId}' has no recorded changes yet.
|
|
572
638
|
`);
|
|
639
|
+
}
|
|
573
640
|
return;
|
|
574
641
|
}
|
|
575
642
|
const related = entries.filter((e) => !e.unrelated);
|
|
576
643
|
const unrelated = entries.filter((e) => e.unrelated);
|
|
577
644
|
const uniqueFiles = Array.from(new Set(entries.map((e) => e.file)));
|
|
578
645
|
const unrelatedFiles = Array.from(new Set(unrelated.map((e) => e.file)));
|
|
646
|
+
const risks = { none: 0, low: 0, medium: 0, high: 0 };
|
|
647
|
+
for (const e of entries) risks[e.risk]++;
|
|
648
|
+
if (json) {
|
|
649
|
+
process.stdout.write(JSON.stringify({
|
|
650
|
+
sessionId,
|
|
651
|
+
totalChanges: entries.length,
|
|
652
|
+
filesCount: uniqueFiles.length,
|
|
653
|
+
relatedChanges: related.length,
|
|
654
|
+
unrelatedChanges: unrelated.length,
|
|
655
|
+
unrelatedFiles,
|
|
656
|
+
risks,
|
|
657
|
+
entries: entries.map((e) => ({ file: e.file, risk: e.risk, summary: e.summary, unrelated: !!e.unrelated }))
|
|
658
|
+
}, null, 2) + "\n");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
579
661
|
const alert = formatDriftAlert(uniqueFiles.length, unrelatedFiles);
|
|
580
662
|
printToStderr(alert);
|
|
581
663
|
process.stderr.write(`
|
|
@@ -587,8 +669,6 @@ Total changes: ${entries.length}
|
|
|
587
669
|
`);
|
|
588
670
|
process.stderr.write(`Unrelated/risky: ${unrelated.length}
|
|
589
671
|
`);
|
|
590
|
-
const risks = { none: 0, low: 0, medium: 0, high: 0 };
|
|
591
|
-
for (const e of entries) risks[e.risk]++;
|
|
592
672
|
process.stderr.write(`
|
|
593
673
|
Risk breakdown:
|
|
594
674
|
`);
|
|
@@ -608,7 +688,7 @@ async function endSession() {
|
|
|
608
688
|
return;
|
|
609
689
|
}
|
|
610
690
|
const sessionPath = getSessionFilePath(sessionId);
|
|
611
|
-
if (
|
|
691
|
+
if (existsSync3(sessionPath)) {
|
|
612
692
|
try {
|
|
613
693
|
unlinkSync2(sessionPath);
|
|
614
694
|
} catch {
|
|
@@ -620,6 +700,7 @@ async function endSession() {
|
|
|
620
700
|
}
|
|
621
701
|
|
|
622
702
|
export {
|
|
703
|
+
isSafeSessionId,
|
|
623
704
|
getCached,
|
|
624
705
|
setCached,
|
|
625
706
|
formatExplanationBox,
|
|
@@ -634,4 +715,4 @@ export {
|
|
|
634
715
|
printSummary,
|
|
635
716
|
endSession
|
|
636
717
|
};
|
|
637
|
-
//# sourceMappingURL=chunk-
|
|
718
|
+
//# sourceMappingURL=chunk-VJN7Y4SI.js.map
|