codebase-wiki 0.1.0
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/CHANGELOG.md +34 -0
- package/LICENSE +17 -0
- package/README.md +130 -0
- package/dist/CHANGELOG.md +34 -0
- package/dist/LICENSE +17 -0
- package/dist/README.md +130 -0
- package/dist/chunks/build.mjs +1976 -0
- package/dist/chunks/config.mjs +174 -0
- package/dist/chunks/llm.mjs +214 -0
- package/dist/code-wiki-mcp.mjs +367 -0
- package/dist/code-wiki.mjs +11 -0
- package/dist/codewiki.mjs +2002 -0
- package/dist/grammars/c.wasm +0 -0
- package/dist/grammars/cpp.wasm +0 -0
- package/dist/grammars/go.wasm +0 -0
- package/dist/grammars/java.wasm +0 -0
- package/dist/grammars/javascript.wasm +0 -0
- package/dist/grammars/python.wasm +0 -0
- package/dist/grammars/ruby.wasm +0 -0
- package/dist/grammars/rust.wasm +0 -0
- package/dist/grammars/tsx.wasm +0 -0
- package/dist/grammars/typescript.wasm +0 -0
- package/dist/plugin/.mcp.json +11 -0
- package/dist/plugin/CLAUDE.md +28 -0
- package/dist/plugin/commands/wiki-architecture.md +10 -0
- package/dist/plugin/commands/wiki-build.md +23 -0
- package/dist/plugin/commands/wiki-enrich.md +22 -0
- package/dist/plugin/commands/wiki-graph.md +16 -0
- package/dist/plugin/commands/wiki-help.md +49 -0
- package/dist/plugin/commands/wiki-refresh.md +13 -0
- package/dist/plugin/commands/wiki-search.md +18 -0
- package/dist/plugin/commands/wiki-symbol.md +21 -0
- package/dist/plugin/hooks/hooks.json +74 -0
- package/dist/plugin/hooks/nudge.mjs +197 -0
- package/dist/plugin/hooks/post-read-reminder.mjs +56 -0
- package/dist/plugin/hooks/post-tool-use.mjs +60 -0
- package/dist/plugin/hooks/pre-compact.mjs +25 -0
- package/dist/plugin/hooks/session-start.mjs +38 -0
- package/dist/plugin/hooks/user-prompt-submit.mjs +28 -0
- package/dist/plugin/manifest.json +54 -0
- package/package.json +100 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { promises } from 'node:fs';
|
|
3
|
+
import path__default from 'node:path';
|
|
4
|
+
import 'node:os';
|
|
5
|
+
|
|
6
|
+
const level = process.env.CODEWIKI_LOG ?? (process.env.NODE_ENV === "test" ? "silent" : "info");
|
|
7
|
+
const log = pino({
|
|
8
|
+
level,
|
|
9
|
+
base: { name: "code-wiki" },
|
|
10
|
+
timestamp: pino.stdTimeFunctions.isoTime
|
|
11
|
+
});
|
|
12
|
+
const child = (bindings) => log.child(bindings);
|
|
13
|
+
|
|
14
|
+
function toPosix(p) {
|
|
15
|
+
return p.replace(/\\/g, "/");
|
|
16
|
+
}
|
|
17
|
+
async function exists(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
await promises.access(filePath);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function readText(filePath) {
|
|
26
|
+
return promises.readFile(filePath, "utf8");
|
|
27
|
+
}
|
|
28
|
+
async function readJson(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
const text = await readText(filePath);
|
|
31
|
+
return JSON.parse(text);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function writeText(filePath, content) {
|
|
37
|
+
const dir = path__default.dirname(filePath);
|
|
38
|
+
await promises.mkdir(dir, { recursive: true });
|
|
39
|
+
await promises.writeFile(filePath, content, "utf8");
|
|
40
|
+
}
|
|
41
|
+
async function writeJson(filePath, value) {
|
|
42
|
+
const text = JSON.stringify(value, null, 2) + "\n";
|
|
43
|
+
await writeText(filePath, text);
|
|
44
|
+
}
|
|
45
|
+
async function atomicWriteText(target, content) {
|
|
46
|
+
const dir = path__default.dirname(target);
|
|
47
|
+
await promises.mkdir(dir, { recursive: true });
|
|
48
|
+
const tmp = path__default.join(dir, `.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`);
|
|
49
|
+
await promises.writeFile(tmp, content, "utf8");
|
|
50
|
+
await promises.rename(tmp, target);
|
|
51
|
+
}
|
|
52
|
+
function repoRel(absolute, root) {
|
|
53
|
+
const rel = path__default.relative(root, absolute);
|
|
54
|
+
return toPosix(rel).replace(/^\.\//, "");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_CONFIG = {
|
|
58
|
+
outDir: ".codewiki",
|
|
59
|
+
sourceRoots: ["."],
|
|
60
|
+
ignore: [
|
|
61
|
+
"**/node_modules/**",
|
|
62
|
+
"**/dist/**",
|
|
63
|
+
"**/build/**",
|
|
64
|
+
"**/.next/**",
|
|
65
|
+
"**/.turbo/**",
|
|
66
|
+
"**/coverage/**",
|
|
67
|
+
"**/target/**",
|
|
68
|
+
"**/__pycache__/**",
|
|
69
|
+
"**/.venv/**",
|
|
70
|
+
"**/venv/**",
|
|
71
|
+
"**/*.min.js",
|
|
72
|
+
"**/*.lock",
|
|
73
|
+
"**/package-lock.json",
|
|
74
|
+
"**/yarn.lock",
|
|
75
|
+
"**/pnpm-lock.yaml",
|
|
76
|
+
"**/Cargo.lock",
|
|
77
|
+
"**/*.wasm"
|
|
78
|
+
],
|
|
79
|
+
includeExtensions: [],
|
|
80
|
+
modules: {
|
|
81
|
+
rule: "by-directory",
|
|
82
|
+
alias: {},
|
|
83
|
+
ignore: ["_tests", "_root"]
|
|
84
|
+
},
|
|
85
|
+
nudge: {
|
|
86
|
+
enabled: true,
|
|
87
|
+
ttlSeconds: 300,
|
|
88
|
+
minFileLines: 30,
|
|
89
|
+
blockOnFirstRead: false,
|
|
90
|
+
scope: "all"
|
|
91
|
+
},
|
|
92
|
+
enrichment: {
|
|
93
|
+
enabled: false,
|
|
94
|
+
concurrency: 4,
|
|
95
|
+
fileModel: "claude-haiku-4-5",
|
|
96
|
+
moduleModel: "claude-sonnet-4-5"
|
|
97
|
+
},
|
|
98
|
+
budgets: {
|
|
99
|
+
file: 600,
|
|
100
|
+
symbol: 150,
|
|
101
|
+
module: 800,
|
|
102
|
+
architecture: 1200,
|
|
103
|
+
mermaidNodes: 30
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
function isPlainObject(v) {
|
|
107
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
108
|
+
}
|
|
109
|
+
function mergeConfig(base, override) {
|
|
110
|
+
if (!isPlainObject(override)) return base;
|
|
111
|
+
const merged = JSON.parse(JSON.stringify(base));
|
|
112
|
+
for (const [k, v] of Object.entries(override)) {
|
|
113
|
+
if (k in merged) {
|
|
114
|
+
merged[k] = isPlainObject(v) && isPlainObject(merged[k]) ? mergeConfig(merged[k], v) : v;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return merged;
|
|
118
|
+
}
|
|
119
|
+
async function loadConfig(cwd, configPath) {
|
|
120
|
+
let config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
121
|
+
if (configPath) {
|
|
122
|
+
const override = await readJson(configPath);
|
|
123
|
+
config = mergeConfig(config, override);
|
|
124
|
+
return config;
|
|
125
|
+
}
|
|
126
|
+
const jsonRc = path__default.join(cwd, ".codewikirc.json");
|
|
127
|
+
if (await fileExists(jsonRc)) {
|
|
128
|
+
const override = await readJson(jsonRc);
|
|
129
|
+
config = mergeConfig(config, override);
|
|
130
|
+
return config;
|
|
131
|
+
}
|
|
132
|
+
const rc = path__default.join(cwd, ".codewikirc");
|
|
133
|
+
if (await fileExists(rc)) {
|
|
134
|
+
const text = await promises.readFile(rc, "utf8");
|
|
135
|
+
try {
|
|
136
|
+
const override = JSON.parse(text);
|
|
137
|
+
config = mergeConfig(config, override);
|
|
138
|
+
return config;
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const pkg = path__default.join(cwd, "package.json");
|
|
143
|
+
const pkgJson = await readJson(pkg);
|
|
144
|
+
if (pkgJson && pkgJson["code-wiki"]) {
|
|
145
|
+
config = mergeConfig(config, pkgJson["code-wiki"]);
|
|
146
|
+
}
|
|
147
|
+
return config;
|
|
148
|
+
}
|
|
149
|
+
async function fileExists(p) {
|
|
150
|
+
try {
|
|
151
|
+
const s = await promises.stat(p);
|
|
152
|
+
return s.isFile();
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function configSummary(c) {
|
|
158
|
+
return [
|
|
159
|
+
`outDir=${toPosix(c.outDir)}`,
|
|
160
|
+
`modules=${c.modules.rule}`,
|
|
161
|
+
`languages=${c.includeExtensions.length || "auto"}`,
|
|
162
|
+
`nudge=${c.nudge.enabled ? "on" : "off"}`,
|
|
163
|
+
`enrichment=${c.enrichment.enabled ? "on" : "off"}`
|
|
164
|
+
].join(" ");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const config = {
|
|
168
|
+
__proto__: null,
|
|
169
|
+
DEFAULT_CONFIG: DEFAULT_CONFIG,
|
|
170
|
+
configSummary: configSummary,
|
|
171
|
+
loadConfig: loadConfig
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export { DEFAULT_CONFIG as D, log as a, atomicWriteText as b, repoRel as c, child as d, exists as e, configSummary as f, config as g, loadConfig as l, readJson as r, toPosix as t, writeJson as w };
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { promises } from 'node:fs';
|
|
2
|
+
import path__default from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
import PQueue from 'p-queue';
|
|
6
|
+
import pRetry from 'p-retry';
|
|
7
|
+
import { a as log, r as readJson, w as writeJson } from './config.mjs';
|
|
8
|
+
import { g as groupModules, c as countTokens } from './build.mjs';
|
|
9
|
+
import 'pino';
|
|
10
|
+
import 'node:os';
|
|
11
|
+
import 'ignore';
|
|
12
|
+
import 'globby';
|
|
13
|
+
import 'gpt-tokenizer/encoding/cl100k_base';
|
|
14
|
+
import 'web-tree-sitter';
|
|
15
|
+
|
|
16
|
+
const SYSTEM_PROMPT = "You are a precise code summarizer. Output ONLY a one-line PURPOSE and a 1-3 sentence NOTES section. No bullet lists, no headings, no markdown formatting. Tone: terse, technical, factual.";
|
|
17
|
+
function client(opts) {
|
|
18
|
+
if (opts.client) return opts.client;
|
|
19
|
+
const key = opts.env?.ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY;
|
|
20
|
+
if (!key) return null;
|
|
21
|
+
const sdk = new Anthropic({ apiKey: key });
|
|
22
|
+
return sdk;
|
|
23
|
+
}
|
|
24
|
+
function sha256(s) {
|
|
25
|
+
return createHash("sha256").update(s).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
async function readCache(cwd, hash) {
|
|
28
|
+
const p = path__default.join(cwd, ".codewiki", ".summary-cache", `${hash}.json`);
|
|
29
|
+
const cached = await readJson(p);
|
|
30
|
+
return cached ?? null;
|
|
31
|
+
}
|
|
32
|
+
async function writeCache(cwd, hash, payload) {
|
|
33
|
+
const p = path__default.join(cwd, ".codewiki", ".summary-cache", `${hash}.json`);
|
|
34
|
+
await writeJson(p, payload);
|
|
35
|
+
}
|
|
36
|
+
function isEligible(file) {
|
|
37
|
+
if (!file.language) return false;
|
|
38
|
+
if (file.symbols.length === 0) return false;
|
|
39
|
+
if (file.symbols.every((s) => !s.docstring)) return true;
|
|
40
|
+
const maxCx = file.symbols.reduce((m, s) => Math.max(m, s.complexity), 0);
|
|
41
|
+
if (maxCx > 15) return true;
|
|
42
|
+
if (countTokens(file.oneLineSummary + "\n" + file.symbols.map((s) => s.signature).join("\n")) > 800) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
async function enrichFiles(opts) {
|
|
48
|
+
const { cwd, files, config } = opts;
|
|
49
|
+
const eligible = files.filter(isEligible);
|
|
50
|
+
if (eligible.length === 0) return /* @__PURE__ */ new Map();
|
|
51
|
+
const anthropic = client({});
|
|
52
|
+
if (!anthropic) {
|
|
53
|
+
log.info({ count: eligible.length }, "enrichment skipped: ANTHROPIC_API_KEY not set");
|
|
54
|
+
return /* @__PURE__ */ new Map();
|
|
55
|
+
}
|
|
56
|
+
log.info(
|
|
57
|
+
{ count: eligible.length, model: config.enrichment.fileModel, concurrency: config.enrichment.concurrency },
|
|
58
|
+
"starting file-level enrichment"
|
|
59
|
+
);
|
|
60
|
+
const queue = new PQueue({ concurrency: config.enrichment.concurrency });
|
|
61
|
+
const out = /* @__PURE__ */ new Map();
|
|
62
|
+
const tasks = eligible.map(
|
|
63
|
+
(file) => queue.add(async () => {
|
|
64
|
+
const hash = sha256(file.contentHash + "|enrich-file");
|
|
65
|
+
const cached = await readCache(cwd, hash);
|
|
66
|
+
if (cached) {
|
|
67
|
+
out.set(file.repoPath, cached);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const summary = await pRetry(
|
|
72
|
+
() => callFileSummary(anthropic, file, config).then((s) => ({
|
|
73
|
+
purpose: s.purpose,
|
|
74
|
+
notes: s.notes
|
|
75
|
+
})),
|
|
76
|
+
{ retries: 2, minTimeout: 500, factor: 2 }
|
|
77
|
+
);
|
|
78
|
+
await writeCache(cwd, hash, summary);
|
|
79
|
+
out.set(file.repoPath, summary);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
log.warn({ path: file.repoPath, err: String(err) }, "file enrichment failed");
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
await Promise.all(tasks);
|
|
86
|
+
log.info({ written: out.size }, "file enrichment done");
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
async function callFileSummary(anthropic, file, config) {
|
|
90
|
+
const symbols = file.symbols.slice(0, 30).map((s) => `[${s.kind}] ${s.signature}${s.docstring ? ` \u2014 ${s.docstring}` : ""}`).join("\n");
|
|
91
|
+
const userPrompt = `File: ${file.repoPath} (language=${file.language})
|
|
92
|
+
|
|
93
|
+
Symbols:
|
|
94
|
+
${symbols}
|
|
95
|
+
|
|
96
|
+
Return JSON: {"purpose": "...", "notes": "..."} with at most 30 words of notes. No markdown.`;
|
|
97
|
+
const resp = await anthropic.messages({
|
|
98
|
+
model: config.enrichment.fileModel,
|
|
99
|
+
max_tokens: 400,
|
|
100
|
+
messages: [
|
|
101
|
+
{ role: "user", content: SYSTEM_PROMPT + "\n\n" + userPrompt }
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
const text = resp.content.map((c) => c.text).join("");
|
|
105
|
+
return parseSummaryJson(text);
|
|
106
|
+
}
|
|
107
|
+
function parseSummaryJson(text) {
|
|
108
|
+
try {
|
|
109
|
+
const m = /\{[\s\S]*?\}/.exec(text);
|
|
110
|
+
if (m) {
|
|
111
|
+
const obj = JSON.parse(m[0]);
|
|
112
|
+
if (typeof obj.purpose === "string" && typeof obj.notes === "string") {
|
|
113
|
+
return { purpose: obj.purpose, notes: obj.notes };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
const lines = text.split("\n").filter(Boolean);
|
|
119
|
+
return {
|
|
120
|
+
purpose: (lines[0] ?? "").slice(0, 200),
|
|
121
|
+
notes: lines.slice(1).join(" ").slice(0, 400)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function enrichModules(opts) {
|
|
125
|
+
const { cwd, files, config } = opts;
|
|
126
|
+
const anthropic = client({});
|
|
127
|
+
if (!anthropic) return /* @__PURE__ */ new Map();
|
|
128
|
+
const modules = groupModules(files, config);
|
|
129
|
+
const out = /* @__PURE__ */ new Map();
|
|
130
|
+
const queue = new PQueue({ concurrency: config.enrichment.concurrency });
|
|
131
|
+
const tasks = [];
|
|
132
|
+
for (const [id, m] of modules) {
|
|
133
|
+
tasks.push(
|
|
134
|
+
queue.add(async () => {
|
|
135
|
+
const memberHashes = m.files.map((p) => files.find((f) => f.repoPath === p)?.contentHash ?? "").sort().join("|");
|
|
136
|
+
const hash = sha256(memberHashes + "|enrich-module");
|
|
137
|
+
const cached = await readCache(cwd, hash);
|
|
138
|
+
if (cached) {
|
|
139
|
+
out.set(id, cached);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const summary = await pRetry(
|
|
144
|
+
() => callModuleSummary(anthropic, id, m, files, config).then((s) => ({
|
|
145
|
+
purpose: s.purpose,
|
|
146
|
+
notes: s.notes
|
|
147
|
+
})),
|
|
148
|
+
{ retries: 2, minTimeout: 500, factor: 2 }
|
|
149
|
+
);
|
|
150
|
+
await writeCache(cwd, hash, summary);
|
|
151
|
+
out.set(id, summary);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
log.warn({ module: id, err: String(err) }, "module enrichment failed");
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
await Promise.all(tasks);
|
|
159
|
+
log.info({ written: out.size }, "module enrichment done");
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
async function callModuleSummary(anthropic, moduleId, m, files, config) {
|
|
163
|
+
const fileLines = m.files.slice(0, 12).map((p) => {
|
|
164
|
+
const f = files.find((x) => x.repoPath === p);
|
|
165
|
+
return f ? `${p}: ${f.oneLineSummary}` : p;
|
|
166
|
+
}).join("\n");
|
|
167
|
+
const userPrompt = `Module: ${moduleId}
|
|
168
|
+
|
|
169
|
+
Files:
|
|
170
|
+
${fileLines}
|
|
171
|
+
|
|
172
|
+
Return JSON: {"purpose": "...", "notes": "..."}. <=25 words notes. No markdown.`;
|
|
173
|
+
const resp = await anthropic.messages({
|
|
174
|
+
model: config.enrichment.moduleModel,
|
|
175
|
+
max_tokens: 350,
|
|
176
|
+
messages: [
|
|
177
|
+
{ role: "user", content: SYSTEM_PROMPT + "\n\n" + userPrompt }
|
|
178
|
+
]
|
|
179
|
+
});
|
|
180
|
+
const text = resp.content.map((c) => c.text).join("");
|
|
181
|
+
return parseSummaryJson(text);
|
|
182
|
+
}
|
|
183
|
+
async function enrichAll(opts) {
|
|
184
|
+
const [files, modules] = await Promise.all([
|
|
185
|
+
enrichFiles(opts),
|
|
186
|
+
enrichModules(opts)
|
|
187
|
+
]);
|
|
188
|
+
return { files, modules };
|
|
189
|
+
}
|
|
190
|
+
async function runEnrichmentCli(cwd, opts = {}) {
|
|
191
|
+
const { loadConfig } = await import('./config.mjs').then(function (n) { return n.g; });
|
|
192
|
+
const { walkFiles } = await import('./build.mjs').then(function (n) { return n.a; });
|
|
193
|
+
const cfg = await loadConfig(cwd);
|
|
194
|
+
const walked = await walkFiles({ root: cwd, ignoreGlobs: cfg.ignore });
|
|
195
|
+
const fileIndexes = walked.map((f) => ({
|
|
196
|
+
repoPath: f.repoPath,
|
|
197
|
+
absPath: f.absPath,
|
|
198
|
+
language: f.language,
|
|
199
|
+
size: f.size,
|
|
200
|
+
mtimeMs: f.mtimeMs,
|
|
201
|
+
contentHash: "",
|
|
202
|
+
symbols: [],
|
|
203
|
+
publicSymbols: [],
|
|
204
|
+
oneLineSummary: ""
|
|
205
|
+
}));
|
|
206
|
+
await enrichAll({ cwd, files: fileIndexes, config: cfg, client: opts.client });
|
|
207
|
+
}
|
|
208
|
+
async function ensureCacheDir(cwd) {
|
|
209
|
+
const dir = path__default.join(cwd, ".codewiki", ".summary-cache");
|
|
210
|
+
await promises.mkdir(dir, { recursive: true });
|
|
211
|
+
return dir;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { enrichAll, enrichFiles, enrichModules, ensureCacheDir, parseSummaryJson, runEnrichmentCli };
|