@iderouter/index-mcp 0.2.0-beta.2 → 0.2.0-beta.4
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/README.md +1 -0
- package/package.json +1 -1
- package/src/index.js +235 -5
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ client skill catalog automatically.
|
|
|
75
75
|
## Tools
|
|
76
76
|
|
|
77
77
|
- `index_codebase`: index a local directory.
|
|
78
|
+
- `summarize_codebase`: produce a fast project overview from the local index while background fine indexing continues.
|
|
78
79
|
- `search_code`: semantic search against the local index.
|
|
79
80
|
- `ask_codebase`: answer a repository question from indexed code and cite matching snippets.
|
|
80
81
|
- `clear_index`: delete an index.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -314,6 +314,7 @@ const searchResultCache = new Map();
|
|
|
314
314
|
const SEARCH_RESULT_CACHE_TTL_MS = Number(process.env.IDEROUTER_SEARCH_RESULT_CACHE_TTL_MS || 120000);
|
|
315
315
|
const queryAnalysisCache = new Map();
|
|
316
316
|
const QUERY_ANALYSIS_CACHE_TTL_MS = Number(process.env.IDEROUTER_QUERY_ANALYSIS_CACHE_TTL_MS || 300000);
|
|
317
|
+
const contextProbeCache = new Map();
|
|
317
318
|
|
|
318
319
|
function log(message) {
|
|
319
320
|
process.stderr.write(`[${SERVER_NAME}] ${message}\n`);
|
|
@@ -341,8 +342,9 @@ Use this skill when a repository has the \`iderouter-index\` MCP configured.
|
|
|
341
342
|
|
|
342
343
|
1. Run \`index_codebase(path=...)\` when the repository is new or stale.
|
|
343
344
|
2. Use \`get_indexing_status(path=...)\` to wait for coarse or priority-semantic readiness.
|
|
344
|
-
3. Use \`
|
|
345
|
-
4. Use \`
|
|
345
|
+
3. Use \`summarize_codebase(path=...)\` for a fast project overview while background fine indexing continues.
|
|
346
|
+
4. Use \`ask_codebase(path=..., question=...)\` for repository questions.
|
|
347
|
+
5. Use \`search_code(path=..., query=...)\` when you need ranked snippets or exact evidence.
|
|
346
348
|
|
|
347
349
|
## Notes
|
|
348
350
|
|
|
@@ -352,6 +354,89 @@ Use this skill when a repository has the \`iderouter-index\` MCP configured.
|
|
|
352
354
|
`;
|
|
353
355
|
}
|
|
354
356
|
|
|
357
|
+
function userCodexConfigPath() {
|
|
358
|
+
const codexHome = String(process.env.CODEX_HOME || "").trim();
|
|
359
|
+
if (codexHome) {
|
|
360
|
+
return path.join(codexHome, "config.toml");
|
|
361
|
+
}
|
|
362
|
+
return path.join(os.homedir(), ".codex", "config.toml");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function contextProbeCacheKey(codebasePath) {
|
|
366
|
+
return `context:${codebasePath}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function summarizeContextText(text, maxLines = 4) {
|
|
370
|
+
return String(text || "")
|
|
371
|
+
.split("\n")
|
|
372
|
+
.map((line) => line.trim())
|
|
373
|
+
.filter(Boolean)
|
|
374
|
+
.filter((line) => !/^#/.test(line))
|
|
375
|
+
.map((line) => line.replace(/\*\*/g, "").replace(/`/g, ""))
|
|
376
|
+
.slice(0, maxLines)
|
|
377
|
+
.join(" ");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function readOptionalFileProbe(filePath, kind) {
|
|
381
|
+
try {
|
|
382
|
+
const stat = await fs.stat(filePath);
|
|
383
|
+
if (!stat.isFile()) return null;
|
|
384
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
385
|
+
return {
|
|
386
|
+
kind,
|
|
387
|
+
filePath,
|
|
388
|
+
mtimeMs: Math.trunc(stat.mtimeMs),
|
|
389
|
+
size: stat.size,
|
|
390
|
+
excerpt: summarizeContextText(content),
|
|
391
|
+
};
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function probeAgentContext(codebasePath) {
|
|
398
|
+
const cacheKey = contextProbeCacheKey(codebasePath);
|
|
399
|
+
const repoAgentsPath = path.join(codebasePath, "AGENTS.md");
|
|
400
|
+
const repoClaudePath = path.join(codebasePath, "CLAUDE.md");
|
|
401
|
+
const codexConfigPath = userCodexConfigPath();
|
|
402
|
+
const [agentsProbe, claudeProbe, codexProbe] = await Promise.all([
|
|
403
|
+
readOptionalFileProbe(repoAgentsPath, "repo_agents"),
|
|
404
|
+
readOptionalFileProbe(repoClaudePath, "repo_claude"),
|
|
405
|
+
readOptionalFileProbe(codexConfigPath, "codex_config"),
|
|
406
|
+
]);
|
|
407
|
+
const fingerprint = [
|
|
408
|
+
agentsProbe ? `${agentsProbe.filePath}:${agentsProbe.mtimeMs}:${agentsProbe.size}` : "",
|
|
409
|
+
claudeProbe ? `${claudeProbe.filePath}:${claudeProbe.mtimeMs}:${claudeProbe.size}` : "",
|
|
410
|
+
codexProbe ? `${codexProbe.filePath}:${codexProbe.mtimeMs}:${codexProbe.size}` : "",
|
|
411
|
+
].join("|");
|
|
412
|
+
const cached = contextProbeCache.get(cacheKey);
|
|
413
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
414
|
+
return cached.value;
|
|
415
|
+
}
|
|
416
|
+
const value = {
|
|
417
|
+
found: [agentsProbe, claudeProbe, codexProbe].filter(Boolean),
|
|
418
|
+
hasRepoAgents: Boolean(agentsProbe),
|
|
419
|
+
hasRepoClaude: Boolean(claudeProbe),
|
|
420
|
+
hasCodexConfig: Boolean(codexProbe),
|
|
421
|
+
summary: [agentsProbe, claudeProbe, codexProbe]
|
|
422
|
+
.filter(Boolean)
|
|
423
|
+
.map((item) => `${item.kind}:${item.excerpt}`)
|
|
424
|
+
.join(" | "),
|
|
425
|
+
};
|
|
426
|
+
contextProbeCache.set(cacheKey, { fingerprint, value });
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function contextProbeNotice(contextProbe) {
|
|
431
|
+
if (!contextProbe) return "";
|
|
432
|
+
const found = [];
|
|
433
|
+
if (contextProbe.hasRepoAgents) found.push("AGENTS.md");
|
|
434
|
+
if (contextProbe.hasRepoClaude) found.push("CLAUDE.md");
|
|
435
|
+
if (contextProbe.hasCodexConfig) found.push("Codex config");
|
|
436
|
+
if (found.length === 0) return "";
|
|
437
|
+
return ` Agent context detected: ${found.join(", ")}.`;
|
|
438
|
+
}
|
|
439
|
+
|
|
355
440
|
async function initCodexSkill() {
|
|
356
441
|
const skillDir = codexSkillDir();
|
|
357
442
|
await fs.mkdir(skillDir, { recursive: true });
|
|
@@ -6713,14 +6798,15 @@ async function searchCode(args) {
|
|
|
6713
6798
|
async function collectSearchState(codebasePath, args, query, limit) {
|
|
6714
6799
|
const loadedIndex = await loadExistingIndex(codebasePath);
|
|
6715
6800
|
let job = await loadLatestJobForPath(codebasePath);
|
|
6801
|
+
const contextProbe = await probeAgentContext(codebasePath);
|
|
6716
6802
|
if (!loadedIndex) {
|
|
6717
6803
|
if (isActiveJobStatus(job?.status)) {
|
|
6718
|
-
return `No local index file is ready yet for ${codebasePath}. Index job ${job.id} is ${job.status}: ${job.progress}% (${job.embeddedCount}/${job.totalChunks} changed chunks), step=${job.currentStep}
|
|
6804
|
+
return `No local index file is ready yet for ${codebasePath}. Index job ${job.id} is ${job.status}: ${job.progress}% (${job.embeddedCount}/${job.totalChunks} changed chunks), step=${job.currentStep}.${contextProbeNotice(contextProbe)} ${AGENT_INDEX_NOTICE}`;
|
|
6719
6805
|
}
|
|
6720
6806
|
if (job?.status === "failed") {
|
|
6721
|
-
return `No local index file is available for ${codebasePath}. Last index job ${job.id} failed at ${job.progress}%: ${cleanDiagnosticMessage(job.error)}
|
|
6807
|
+
return `No local index file is available for ${codebasePath}. Last index job ${job.id} failed at ${job.progress}%: ${cleanDiagnosticMessage(job.error)}.${contextProbeNotice(contextProbe)} ${AGENT_INDEX_NOTICE}`;
|
|
6722
6808
|
}
|
|
6723
|
-
return `No local index found for ${codebasePath}. Run index_codebase first
|
|
6809
|
+
return `No local index found for ${codebasePath}. Run index_codebase first, then use summarize_codebase(path=...) for a fast project overview.${contextProbeNotice(contextProbe)} ${COMPANION_PROMPT_NOTICE} ${AGENT_INDEX_NOTICE}`;
|
|
6724
6810
|
}
|
|
6725
6811
|
const usingPartialIndex = loadedIndex.complete === false;
|
|
6726
6812
|
let resumed = null;
|
|
@@ -6863,6 +6949,117 @@ function summarizeAnswerFromResults(question, results, limit) {
|
|
|
6863
6949
|
].join("\n");
|
|
6864
6950
|
}
|
|
6865
6951
|
|
|
6952
|
+
function summarizeRepoDomains(results) {
|
|
6953
|
+
const domains = new Set();
|
|
6954
|
+
for (const result of results || []) {
|
|
6955
|
+
const relativePath = String(result.relativePath || "").toLowerCase();
|
|
6956
|
+
if (relativePath.startsWith("controller/")) domains.add("controller");
|
|
6957
|
+
else if (relativePath.startsWith("service/")) domains.add("service");
|
|
6958
|
+
else if (relativePath.startsWith("model/")) domains.add("model");
|
|
6959
|
+
else if (relativePath.startsWith("router/")) domains.add("router");
|
|
6960
|
+
else if (relativePath.startsWith("relay/")) domains.add("relay");
|
|
6961
|
+
else if (relativePath.startsWith("pkg/")) domains.add("pkg");
|
|
6962
|
+
else if (relativePath.startsWith("setting/")) domains.add("setting");
|
|
6963
|
+
else if (relativePath.startsWith("dto/")) domains.add("dto");
|
|
6964
|
+
else if (relativePath.startsWith("web/") || relativePath.startsWith("iderouter_frontend/")) domains.add("frontend");
|
|
6965
|
+
else if (relativePath.startsWith("common/")) domains.add("common");
|
|
6966
|
+
}
|
|
6967
|
+
return [...domains];
|
|
6968
|
+
}
|
|
6969
|
+
|
|
6970
|
+
function summarizeRepoTechStack(results) {
|
|
6971
|
+
const files = (results || []).map((result) => String(result.relativePath || "").toLowerCase());
|
|
6972
|
+
const stack = [];
|
|
6973
|
+
if (files.some((file) => file.endsWith(".go"))) stack.push("Go backend");
|
|
6974
|
+
if (files.some((file) => file.includes("web/") || file.includes("iderouter_frontend/") || file.endsWith(".tsx") || file.endsWith(".ts"))) {
|
|
6975
|
+
stack.push("TypeScript/React frontend");
|
|
6976
|
+
}
|
|
6977
|
+
if (files.some((file) => file.endsWith(".sql"))) stack.push("SQL schema or migrations");
|
|
6978
|
+
if (files.some((file) => file.endsWith(".yml") || file.endsWith(".yaml"))) stack.push("YAML workflow/config");
|
|
6979
|
+
return stack;
|
|
6980
|
+
}
|
|
6981
|
+
|
|
6982
|
+
function summarizeRepoTechStackFromContext(contextProbe, fallbackStack) {
|
|
6983
|
+
const summary = String(contextProbe?.summary || "").toLowerCase();
|
|
6984
|
+
const merged = new Set(Array.isArray(fallbackStack) ? fallbackStack : []);
|
|
6985
|
+
if (summary.includes("go ")) merged.add("Go backend");
|
|
6986
|
+
if (summary.includes("react") || summary.includes("typescript")) merged.add("TypeScript/React frontend");
|
|
6987
|
+
if (summary.includes("tailwind")) merged.add("Tailwind CSS");
|
|
6988
|
+
if (summary.includes("gin")) merged.add("Gin web framework");
|
|
6989
|
+
if (summary.includes("gorm")) merged.add("GORM");
|
|
6990
|
+
if (summary.includes("redis")) merged.add("Redis");
|
|
6991
|
+
if (summary.includes("sqlite") || summary.includes("mysql") || summary.includes("postgresql")) {
|
|
6992
|
+
merged.add("Multi-database support");
|
|
6993
|
+
}
|
|
6994
|
+
return [...merged];
|
|
6995
|
+
}
|
|
6996
|
+
|
|
6997
|
+
function summarizeEntrypoints(results, limit = 5) {
|
|
6998
|
+
const preferredRoleOrder = ["controller", "router", "service", "relay", "model", "pkg", "frontend", "core"];
|
|
6999
|
+
const bestByRole = new Map();
|
|
7000
|
+
for (const result of results || []) {
|
|
7001
|
+
const role = pathRole(result.relativePath);
|
|
7002
|
+
if (!preferredRoleOrder.includes(role)) continue;
|
|
7003
|
+
const existing = bestByRole.get(role);
|
|
7004
|
+
if (!existing || Number(result.score || 0) > Number(existing.score || 0)) {
|
|
7005
|
+
bestByRole.set(role, result);
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
const ordered = [];
|
|
7009
|
+
for (const role of preferredRoleOrder) {
|
|
7010
|
+
const item = bestByRole.get(role);
|
|
7011
|
+
if (item) ordered.push(item);
|
|
7012
|
+
if (ordered.length >= limit) break;
|
|
7013
|
+
}
|
|
7014
|
+
if (ordered.length >= limit) return ordered.slice(0, limit);
|
|
7015
|
+
const seen = new Set(ordered.map((item) => `${item.relativePath}:${item.startLine}:${item.endLine}`));
|
|
7016
|
+
for (const result of results || []) {
|
|
7017
|
+
const key = `${result.relativePath}:${result.startLine}:${result.endLine}`;
|
|
7018
|
+
if (seen.has(key)) continue;
|
|
7019
|
+
if (["docs", "tests", "mcp", "other"].includes(pathRole(result.relativePath))) continue;
|
|
7020
|
+
ordered.push(result);
|
|
7021
|
+
seen.add(key);
|
|
7022
|
+
if (ordered.length >= limit) break;
|
|
7023
|
+
}
|
|
7024
|
+
return ordered;
|
|
7025
|
+
}
|
|
7026
|
+
|
|
7027
|
+
function repoSummaryLines(codebasePath, state, contextProbe) {
|
|
7028
|
+
const results = Array.isArray(state?.results) ? state.results : [];
|
|
7029
|
+
const topResults = results.slice(0, 8);
|
|
7030
|
+
const domains = summarizeRepoDomains(topResults);
|
|
7031
|
+
const stack = summarizeRepoTechStackFromContext(contextProbe, summarizeRepoTechStack(topResults));
|
|
7032
|
+
const entrypoints = summarizeEntrypoints(topResults, 5);
|
|
7033
|
+
const lines = [
|
|
7034
|
+
`Fast repository summary for ${codebasePath}:`,
|
|
7035
|
+
"",
|
|
7036
|
+
`Status: ${state?.usingPartialIndex ? "searchable partial index" : "ready index"}${state?.refresh?.skippedDueToActiveJob ? " while background indexing continues" : ""}.`,
|
|
7037
|
+
];
|
|
7038
|
+
if (stack.length > 0) {
|
|
7039
|
+
lines.push(`Likely stack: ${stack.join(", ")}.`);
|
|
7040
|
+
}
|
|
7041
|
+
if (domains.length > 0) {
|
|
7042
|
+
lines.push(`Indexed layers present: ${domains.join(", ")}.`);
|
|
7043
|
+
}
|
|
7044
|
+
if (contextProbe?.found?.length) {
|
|
7045
|
+
lines.push("");
|
|
7046
|
+
lines.push("Agent Context:");
|
|
7047
|
+
for (const item of contextProbe.found.slice(0, 3)) {
|
|
7048
|
+
lines.push(`- ${path.basename(item.filePath)}: ${item.excerpt || "context file detected"}`);
|
|
7049
|
+
}
|
|
7050
|
+
}
|
|
7051
|
+
if (entrypoints.length > 0) {
|
|
7052
|
+
lines.push("");
|
|
7053
|
+
lines.push("Key entrypoints:");
|
|
7054
|
+
for (const result of entrypoints) {
|
|
7055
|
+
lines.push(`- ${result.relativePath}:${result.startLine}-${result.endLine}`);
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
lines.push("");
|
|
7059
|
+
lines.push("Use get_indexing_status(path=...) to check background fine indexing progress.");
|
|
7060
|
+
return lines.join("\n");
|
|
7061
|
+
}
|
|
7062
|
+
|
|
6866
7063
|
async function askCodebase(args) {
|
|
6867
7064
|
const codebasePath = normalizePath(args.path);
|
|
6868
7065
|
const question = String(args.question || "").trim();
|
|
@@ -6904,6 +7101,25 @@ async function askCodebase(args) {
|
|
|
6904
7101
|
return summarizeAnswerFromResults(question, state.results, limit);
|
|
6905
7102
|
}
|
|
6906
7103
|
|
|
7104
|
+
async function summarizeCodebase(args) {
|
|
7105
|
+
const codebasePath = normalizePath(args.path);
|
|
7106
|
+
const summaryQuery = "repository overview architecture core modules backend frontend key entrypoints";
|
|
7107
|
+
const contextProbe = await probeAgentContext(codebasePath);
|
|
7108
|
+
const state = await collectSearchState(
|
|
7109
|
+
codebasePath,
|
|
7110
|
+
{ ...args, query: summaryQuery, __skipSearchCache: true },
|
|
7111
|
+
summaryQuery,
|
|
7112
|
+
Math.min(Number(args.limit || 8), 12),
|
|
7113
|
+
);
|
|
7114
|
+
if (typeof state === "string") {
|
|
7115
|
+
return `${state}${contextProbeNotice(contextProbe)}`;
|
|
7116
|
+
}
|
|
7117
|
+
if (!Array.isArray(state.results) || state.results.length === 0) {
|
|
7118
|
+
return `I could not build a repository summary for ${codebasePath} from the current index.${contextProbeNotice(contextProbe)}`;
|
|
7119
|
+
}
|
|
7120
|
+
return `${repoSummaryLines(codebasePath, state, contextProbe)}${contextProbeNotice(contextProbe)}`;
|
|
7121
|
+
}
|
|
7122
|
+
|
|
6907
7123
|
function latestJobForPath(codebasePath) {
|
|
6908
7124
|
let latest = null;
|
|
6909
7125
|
for (const job of indexJobs.values()) {
|
|
@@ -7766,6 +7982,18 @@ const tools = [
|
|
|
7766
7982
|
required: ["path", "query"],
|
|
7767
7983
|
},
|
|
7768
7984
|
},
|
|
7985
|
+
{
|
|
7986
|
+
name: "summarize_codebase",
|
|
7987
|
+
description: "Produce a fast repository overview from the local index while background fine indexing may still be running.",
|
|
7988
|
+
inputSchema: {
|
|
7989
|
+
type: "object",
|
|
7990
|
+
properties: {
|
|
7991
|
+
path: { type: "string" },
|
|
7992
|
+
limit: { type: "number" },
|
|
7993
|
+
},
|
|
7994
|
+
required: ["path"],
|
|
7995
|
+
},
|
|
7996
|
+
},
|
|
7769
7997
|
{
|
|
7770
7998
|
name: "ask_codebase",
|
|
7771
7999
|
description: "Answer a repository question using the indexed local code context and cite the strongest matching snippets.",
|
|
@@ -7806,6 +8034,8 @@ async function callTool(name, args) {
|
|
|
7806
8034
|
return args?.wait ? indexCodebase(args || {}) : startIndexJob(args || {});
|
|
7807
8035
|
case "search_code":
|
|
7808
8036
|
return searchCode(args || {});
|
|
8037
|
+
case "summarize_codebase":
|
|
8038
|
+
return summarizeCodebase(args || {});
|
|
7809
8039
|
case "ask_codebase":
|
|
7810
8040
|
return askCodebase(args || {});
|
|
7811
8041
|
case "clear_index":
|