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,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import path__default from 'node:path';
|
|
4
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { l as loadConfig, r as readJson, a as log } from './chunks/config.mjs';
|
|
8
|
+
import 'pino';
|
|
9
|
+
import 'node:fs';
|
|
10
|
+
import 'node:os';
|
|
11
|
+
|
|
12
|
+
const server = new Server(
|
|
13
|
+
{
|
|
14
|
+
name: "code-wiki",
|
|
15
|
+
version: "0.1.0"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
capabilities: {
|
|
19
|
+
tools: {},
|
|
20
|
+
resources: {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
const cwd = process.env.CODEWIKI_CWD || process.cwd();
|
|
25
|
+
let cache = null;
|
|
26
|
+
async function getIndex() {
|
|
27
|
+
if (cache && Date.now() - cache.buildLoadedAt < 3e4) return cache;
|
|
28
|
+
const cfg = await loadConfig(cwd);
|
|
29
|
+
const tree = await readJson(path__default.join(cwd, cfg.outDir, ".index.json"));
|
|
30
|
+
const graph = await readJson(path__default.join(cwd, cfg.outDir, ".graph.json"));
|
|
31
|
+
cache = { config: cfg, tree, graph, buildLoadedAt: Date.now() };
|
|
32
|
+
return cache;
|
|
33
|
+
}
|
|
34
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
35
|
+
return {
|
|
36
|
+
tools: [
|
|
37
|
+
{
|
|
38
|
+
name: "wiki_search",
|
|
39
|
+
description: "Search the codewiki for symbols, files, or modules matching a substring. Always prefer this over Grep when looking for code patterns \u2014 results come from the pre-built index and cost ~50 tokens regardless of repo size.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
query: { type: "string", description: "Substring (case-insensitive)" },
|
|
44
|
+
kind: {
|
|
45
|
+
type: "string",
|
|
46
|
+
enum: ["symbol", "file", "module"],
|
|
47
|
+
description: "Filter by entity kind"
|
|
48
|
+
},
|
|
49
|
+
limit: { type: "number", default: 20, description: "Max results (default 20)" }
|
|
50
|
+
},
|
|
51
|
+
required: ["query"]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "wiki_drill",
|
|
56
|
+
description: "Read a single wiki page by id (file path or module id). Returns the page markdown including YAML frontmatter.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: { id: { type: "string" } },
|
|
60
|
+
required: ["id"]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "wiki_callers",
|
|
65
|
+
description: "List the files that import the file containing a given symbol id. v0.1: returns file-level dependents (call-level resolution lands v0.2).",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: { symbolId: { type: "string" } },
|
|
69
|
+
required: ["symbolId"]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "wiki_callees",
|
|
74
|
+
description: "List the files imported by the file containing a given symbol id.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: { symbolId: { type: "string" } },
|
|
78
|
+
required: ["symbolId"]
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "wiki_changed_since",
|
|
83
|
+
description: "List files in the repo that changed since the last wiki build. Run /wiki-refresh after this to refresh stale wiki pages.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: { ref: { type: "string", description: "git ref (default: HEAD)" } }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
const PAGE_SIZE = 50;
|
|
93
|
+
function parseCursor(uri) {
|
|
94
|
+
const i = uri.indexOf("?cursor=");
|
|
95
|
+
if (i < 0) return { baseUri: uri, cursor: 0 };
|
|
96
|
+
const n = Number(uri.slice(i + 8));
|
|
97
|
+
return { baseUri: uri.slice(0, i), cursor: Number.isFinite(n) ? n : 0 };
|
|
98
|
+
}
|
|
99
|
+
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
100
|
+
const idx = await getIndex();
|
|
101
|
+
idx.config;
|
|
102
|
+
const cursor = Number(request.params?.cursor ?? 0);
|
|
103
|
+
const items = [];
|
|
104
|
+
items.push({
|
|
105
|
+
uri: "wiki://tree",
|
|
106
|
+
name: "wiki tree (read to navigate)",
|
|
107
|
+
mimeType: "text/markdown",
|
|
108
|
+
description: "Top-level module index; capped to <1.5k tokens."
|
|
109
|
+
});
|
|
110
|
+
items.push({
|
|
111
|
+
uri: "wiki://architecture",
|
|
112
|
+
name: "architecture",
|
|
113
|
+
mimeType: "text/markdown",
|
|
114
|
+
description: "System overview with Mermaid module map."
|
|
115
|
+
});
|
|
116
|
+
items.push({
|
|
117
|
+
uri: "wiki://index",
|
|
118
|
+
name: "index",
|
|
119
|
+
mimeType: "text/markdown",
|
|
120
|
+
description: "Top-level INDEX.md."
|
|
121
|
+
});
|
|
122
|
+
const mods = idx.tree?.modules ?? [];
|
|
123
|
+
const files = idx.tree?.files ?? [];
|
|
124
|
+
const symbols = idx.tree?.symbols ?? [];
|
|
125
|
+
const totalEntities = 3 + mods.length + files.length + symbols.length;
|
|
126
|
+
const limit = PAGE_SIZE;
|
|
127
|
+
const offset = cursor;
|
|
128
|
+
const sliced = (arr, prefix) => arr.slice(offset, offset + limit).map((x) => ({
|
|
129
|
+
uri: x.path ?? `${prefix}/${x}`,
|
|
130
|
+
name: x.title ?? x.name ?? String(x),
|
|
131
|
+
mimeType: "text/markdown"
|
|
132
|
+
}));
|
|
133
|
+
if (cursor < mods.length) {
|
|
134
|
+
items.push(...sliced(mods, "wiki://module"));
|
|
135
|
+
} else if (cursor < mods.length + files.length) {
|
|
136
|
+
items.push(
|
|
137
|
+
...files.slice(cursor - mods.length, cursor - mods.length + limit).map((f) => ({
|
|
138
|
+
uri: `wiki://file/${f.path}`,
|
|
139
|
+
name: `${f.path}`,
|
|
140
|
+
mimeType: "text/markdown"
|
|
141
|
+
}))
|
|
142
|
+
);
|
|
143
|
+
} else if (cursor < mods.length + files.length + symbols.length) {
|
|
144
|
+
items.push(
|
|
145
|
+
...symbols.slice(cursor - mods.length - files.length, cursor - mods.length - files.length + limit).map((s) => ({
|
|
146
|
+
uri: `wiki://symbol/${s.id}`,
|
|
147
|
+
name: `${s.kind} ${s.name}`,
|
|
148
|
+
mimeType: "text/markdown"
|
|
149
|
+
}))
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const nextOffset = cursor + limit;
|
|
153
|
+
const nextCursor = nextOffset < totalEntities ? String(nextOffset) : void 0;
|
|
154
|
+
if (nextCursor) {
|
|
155
|
+
return {
|
|
156
|
+
resources: items,
|
|
157
|
+
nextCursor
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return { resources: items };
|
|
161
|
+
});
|
|
162
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
163
|
+
const raw = String(request.params.uri);
|
|
164
|
+
const { baseUri } = parseCursor(raw);
|
|
165
|
+
const idx = await getIndex();
|
|
166
|
+
const base = path__default.join(cwd, idx.config.outDir);
|
|
167
|
+
const fs = await import('node:fs/promises');
|
|
168
|
+
const readPage = (file) => fs.readFile(path__default.join(base, file), "utf8").then((text) => ({
|
|
169
|
+
contents: [{ uri: baseUri, mimeType: "text/markdown", text }]
|
|
170
|
+
}));
|
|
171
|
+
if (baseUri === "wiki://tree") return readPage("INDEX.md");
|
|
172
|
+
if (baseUri === "wiki://architecture") return readPage("architecture.md");
|
|
173
|
+
if (baseUri === "wiki://index") return readPage("INDEX.md");
|
|
174
|
+
const modMatch = /^wiki:\/\/module\/(.+)$/.exec(baseUri);
|
|
175
|
+
if (modMatch) return readPage(`modules/${modMatch[1]}.md`);
|
|
176
|
+
const fileMatch = /^wiki:\/\/file\/(.+)$/.exec(baseUri);
|
|
177
|
+
if (fileMatch) return readPage(`files/${fileMatch[1]}.md`);
|
|
178
|
+
const symMatch = /^wiki:\/\/symbol\/(.+)$/.exec(baseUri);
|
|
179
|
+
if (symMatch) return readPage(`symbols/${symMatch[1]}.md`);
|
|
180
|
+
const graphMatch = /^wiki:\/\/graph\/(.+)$/.exec(baseUri);
|
|
181
|
+
if (graphMatch) {
|
|
182
|
+
const symId = graphMatch[1] ?? "";
|
|
183
|
+
const fileOf = symId.split("::")[0] ?? "";
|
|
184
|
+
const deps = idx.graph?.dependents?.[fileOf] ?? [];
|
|
185
|
+
const imports = (idx.graph?.imports ?? []).filter((e) => e.from === fileOf);
|
|
186
|
+
const lines = [];
|
|
187
|
+
lines.push(`# Graph: ${symId}`);
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push(`File: \`${fileOf}\``);
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push(`## Imported by (${deps.length})`);
|
|
192
|
+
for (const d of deps) lines.push(`- ${d}`);
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push(`## Imports (${imports.length})`);
|
|
195
|
+
for (const e of imports) lines.push(`- \`${e.to}\` [${e.specifiers.join(", ")}]`);
|
|
196
|
+
return { contents: [{ uri: baseUri, mimeType: "text/markdown", text: lines.join("\n") }] };
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`Unknown resource: ${baseUri}`);
|
|
199
|
+
});
|
|
200
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
201
|
+
const name = request.params.name;
|
|
202
|
+
const args = request.params.arguments ?? {};
|
|
203
|
+
const idx = await getIndex();
|
|
204
|
+
const tree = idx.tree;
|
|
205
|
+
const graph = idx.graph;
|
|
206
|
+
if (name === "wiki_search") {
|
|
207
|
+
const q = String(args.query ?? "").toLowerCase().trim();
|
|
208
|
+
if (!q) {
|
|
209
|
+
return { content: [{ type: "text", text: "wiki_search: missing query" }], isError: true };
|
|
210
|
+
}
|
|
211
|
+
const kind = args.kind;
|
|
212
|
+
const limit = Number(args.limit ?? 20);
|
|
213
|
+
const out = [];
|
|
214
|
+
if (!kind || kind === "module") {
|
|
215
|
+
for (const m of tree?.modules ?? []) {
|
|
216
|
+
if (m.id.toLowerCase().includes(q)) out.push({ kind: "module", id: m.id, file: m.path, module: m.id });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!kind || kind === "file") {
|
|
220
|
+
for (const f of tree?.files ?? []) {
|
|
221
|
+
if (f.path.toLowerCase().includes(q)) out.push({ kind: "file", id: f.path, file: f.path, module: f.module });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!kind || kind === "symbol") {
|
|
225
|
+
for (const s of tree?.symbols ?? []) {
|
|
226
|
+
if (s.name.toLowerCase().includes(q)) out.push({ kind: "symbol", id: s.id, file: s.file });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const trimmed = out.slice(0, limit);
|
|
230
|
+
if (trimmed.length === 0) {
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{
|
|
234
|
+
type: "text",
|
|
235
|
+
text: `No hits. Try a substring (case-insensitive). If results feel stale, run /wiki-refresh.`
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return { content: [{ type: "text", text: JSON.stringify(trimmed, null, 2) }] };
|
|
241
|
+
}
|
|
242
|
+
if (name === "wiki_drill") {
|
|
243
|
+
const id = String(args.id ?? "");
|
|
244
|
+
if (!id) return { content: [{ type: "text", text: "wiki_drill: missing id" }], isError: true };
|
|
245
|
+
const candidates = [
|
|
246
|
+
path__default.join(cwd, idx.config.outDir, `files/${id}.md`),
|
|
247
|
+
path__default.join(cwd, idx.config.outDir, `modules/${id}.md`),
|
|
248
|
+
path__default.join(cwd, idx.config.outDir, `symbols/${id.replace("::", "/")}.md`)
|
|
249
|
+
];
|
|
250
|
+
const fs = await import('node:fs/promises');
|
|
251
|
+
for (const c of candidates) {
|
|
252
|
+
try {
|
|
253
|
+
const body = await fs.readFile(c, "utf8");
|
|
254
|
+
return { content: [{ type: "text", text: body }] };
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: `No wiki page matches "${id}". Run \`codewiki build\` first, or use /wiki-search to find the right id.`
|
|
263
|
+
}
|
|
264
|
+
],
|
|
265
|
+
isError: true
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (name === "wiki_callers" || name === "wiki_callees") {
|
|
269
|
+
if (!graph) {
|
|
270
|
+
return {
|
|
271
|
+
content: [{ type: "text", text: `No .graph.json. Run \`codewiki build\` first.` }],
|
|
272
|
+
isError: true
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const symbolId = String(args.symbolId ?? "");
|
|
276
|
+
const fileOf = symbolId.split("::")[0] ?? "";
|
|
277
|
+
if (!fileOf) return { content: [{ type: "text", text: "wiki_callers/callees: bad symbolId" }], isError: true };
|
|
278
|
+
if (name === "wiki_callers") {
|
|
279
|
+
const deps = graph.dependents?.[fileOf] ?? [];
|
|
280
|
+
const localCallers = (graph.imports ?? []).filter((e) => e.to === fileOf).map((e) => e.from);
|
|
281
|
+
const all = [.../* @__PURE__ */ new Set([...deps ?? [], ...localCallers])];
|
|
282
|
+
return {
|
|
283
|
+
content: [
|
|
284
|
+
{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: JSON.stringify(
|
|
287
|
+
{
|
|
288
|
+
note: "M4-M5: file-level dependents. Call edges land v0.2.",
|
|
289
|
+
symbolId,
|
|
290
|
+
file: fileOf,
|
|
291
|
+
importers: all
|
|
292
|
+
},
|
|
293
|
+
null,
|
|
294
|
+
2
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const imports = (graph.imports ?? []).filter((e) => e.from === fileOf);
|
|
301
|
+
return {
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: "text",
|
|
305
|
+
text: JSON.stringify(
|
|
306
|
+
{
|
|
307
|
+
note: "M4-M5: file-level imports. Call edges land v0.2.",
|
|
308
|
+
symbolId,
|
|
309
|
+
file: fileOf,
|
|
310
|
+
imports
|
|
311
|
+
},
|
|
312
|
+
null,
|
|
313
|
+
2
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (name === "wiki_changed_since") {
|
|
320
|
+
const ref = String(args.ref ?? "HEAD");
|
|
321
|
+
const { spawn } = await import('node:child_process');
|
|
322
|
+
return new Promise((resolve) => {
|
|
323
|
+
const r = spawn("git", ["diff", "--name-only", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
324
|
+
let out = "";
|
|
325
|
+
let err = "";
|
|
326
|
+
r.stdout.on("data", (c) => out += c.toString());
|
|
327
|
+
r.stderr.on("data", (c) => err += c.toString());
|
|
328
|
+
r.on("close", (code) => {
|
|
329
|
+
if (code !== 0) {
|
|
330
|
+
resolve({
|
|
331
|
+
content: [
|
|
332
|
+
{
|
|
333
|
+
type: "text",
|
|
334
|
+
text: `git diff exited ${code}: ${err || "unknown error"}`
|
|
335
|
+
}
|
|
336
|
+
],
|
|
337
|
+
isError: true
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const files = out.trim().split("\n").filter(Boolean);
|
|
342
|
+
resolve({
|
|
343
|
+
content: [
|
|
344
|
+
{
|
|
345
|
+
type: "text",
|
|
346
|
+
text: JSON.stringify({ ref, changedFiles: files, count: files.length }, null, 2)
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
355
|
+
isError: true
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
async function main() {
|
|
359
|
+
const transport = new StdioServerTransport();
|
|
360
|
+
await server.connect(transport);
|
|
361
|
+
log.info("code-wiki MCP server connected on stdio");
|
|
362
|
+
}
|
|
363
|
+
main().catch((err) => {
|
|
364
|
+
process.stderr.write(`code-wiki MCP server: ${err?.stack ?? err}
|
|
365
|
+
`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { b as buildWiki, c as countTokens, d as detectLanguage, g as groupModules, m as moduleForFile, t as truncateToTokens, w as walkFiles } from './chunks/build.mjs';
|
|
2
|
+
export { D as DEFAULT_CONFIG, l as loadConfig } from './chunks/config.mjs';
|
|
3
|
+
import 'node:path';
|
|
4
|
+
import 'node:fs';
|
|
5
|
+
import 'ignore';
|
|
6
|
+
import 'globby';
|
|
7
|
+
import 'node:crypto';
|
|
8
|
+
import 'gpt-tokenizer/encoding/cl100k_base';
|
|
9
|
+
import 'web-tree-sitter';
|
|
10
|
+
import 'pino';
|
|
11
|
+
import 'node:os';
|