@abdulmunimjemal/codescope 0.1.0 → 0.2.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/README.md +18 -10
- package/dist/cli.js +186 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +50 -4
- package/dist/index.js +173 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -180,15 +180,119 @@ var GraphStore = class {
|
|
|
180
180
|
WHERE s.name = ? ORDER BY s.exported DESC, f.path LIMIT ?`
|
|
181
181
|
).all(name, clampLimit(opts.limit)).map(toSymbolRow);
|
|
182
182
|
}
|
|
183
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Distinct callers of a name (both bare `foo()` and `x.foo()`). Multiple call
|
|
185
|
+
* sites from the same caller in the same file collapse to one row — fewer
|
|
186
|
+
* tokens and a more useful "who depends on this" answer.
|
|
187
|
+
*/
|
|
184
188
|
findCallers(name, opts = {}) {
|
|
185
189
|
return this.db.prepare(
|
|
186
|
-
`SELECT r.id, f.path AS file, r.from_symbol, r.name,
|
|
190
|
+
`SELECT MIN(r.id) AS id, f.path AS file, r.from_symbol, r.name,
|
|
191
|
+
MIN(r.kind) AS kind, MIN(r.start_row) AS start_row, MIN(r.start_col) AS start_col
|
|
187
192
|
FROM refs r JOIN files f ON f.id = r.file_id
|
|
188
193
|
WHERE r.name = ? AND r.kind IN ('call', 'method')
|
|
189
|
-
|
|
194
|
+
GROUP BY f.path, r.from_symbol
|
|
195
|
+
ORDER BY f.path, start_row LIMIT ?`
|
|
190
196
|
).all(name, clampLimit(opts.limit)).map(toRefRow);
|
|
191
197
|
}
|
|
198
|
+
/** The definitions that a symbol calls, resolved kind-aware to project symbols. */
|
|
199
|
+
findCallees(name, opts = {}) {
|
|
200
|
+
const limit = clampLimit(opts.limit);
|
|
201
|
+
const out = [];
|
|
202
|
+
const seen = /* @__PURE__ */ new Set();
|
|
203
|
+
for (const callee of this.calleesOf(name)) {
|
|
204
|
+
const defs = this.resolveCallee(callee.name, callee.kind, 6);
|
|
205
|
+
if (!defs) continue;
|
|
206
|
+
for (const d of defs) {
|
|
207
|
+
const key = `${d.name}@${d.file}:${d.startRow}`;
|
|
208
|
+
if (!seen.has(key)) {
|
|
209
|
+
seen.add(key);
|
|
210
|
+
out.push(d);
|
|
211
|
+
if (out.length >= limit) return out;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
/** How many call sites reference this name (popularity / centrality signal). */
|
|
218
|
+
callerCount(name) {
|
|
219
|
+
return this.db.prepare(
|
|
220
|
+
"SELECT COUNT(*) AS n FROM refs WHERE name = ? AND kind IN ('call','method')"
|
|
221
|
+
).get(name)?.n ?? 0;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The blast radius of changing a symbol: its transitive callers, breadth-first,
|
|
225
|
+
* annotated with hop distance and ordered nearest-first. Answers "what could
|
|
226
|
+
* break if I change this?" without reading the codebase.
|
|
227
|
+
*/
|
|
228
|
+
impact(name, opts = {}) {
|
|
229
|
+
const depth = Math.max(1, Math.min(opts.depth ?? 3, 6));
|
|
230
|
+
const limit = clampLimit(opts.limit, 300);
|
|
231
|
+
const distance = /* @__PURE__ */ new Map([[name, 0]]);
|
|
232
|
+
let frontier = [name];
|
|
233
|
+
for (let d = 0; d < depth && frontier.length > 0 && distance.size < limit; d++) {
|
|
234
|
+
const next = [];
|
|
235
|
+
for (const node of frontier) {
|
|
236
|
+
for (const caller of this.callersOf(node)) {
|
|
237
|
+
if (!distance.has(caller)) {
|
|
238
|
+
distance.set(caller, d + 1);
|
|
239
|
+
next.push(caller);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
frontier = next;
|
|
244
|
+
}
|
|
245
|
+
const out = [];
|
|
246
|
+
for (const [n, dist] of distance) {
|
|
247
|
+
if (dist === 0) continue;
|
|
248
|
+
for (const def of this.getSymbol(n, { limit: 3 })) out.push({ ...def, distance: dist });
|
|
249
|
+
}
|
|
250
|
+
out.sort((a, b) => a.distance - b.distance || a.file.localeCompare(b.file));
|
|
251
|
+
return out.slice(0, limit);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* A token-budgeted relevance map for a task: the symbols matching `query` plus
|
|
255
|
+
* their immediate call neighbourhood, ranked by call-site centrality, capped at
|
|
256
|
+
* `maxSymbols`. This is the slice of the codebase an agent needs to start a
|
|
257
|
+
* change — delivered as graph facts, not file dumps.
|
|
258
|
+
*/
|
|
259
|
+
context(query, opts = {}) {
|
|
260
|
+
const maxSymbols = Math.max(5, Math.min(opts.maxSymbols ?? 30, 100));
|
|
261
|
+
const seeds = this.searchSymbols(query, { limit: Math.min(8, maxSymbols) });
|
|
262
|
+
const picked = /* @__PURE__ */ new Map();
|
|
263
|
+
const key = (s) => `${s.name}@${s.file}:${s.startRow}`;
|
|
264
|
+
for (const s of seeds) picked.set(key(s), s);
|
|
265
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
266
|
+
const edges = [];
|
|
267
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
268
|
+
for (const seed of seeds) {
|
|
269
|
+
for (const callee of this.findCallees(seed.name, { limit: 15 })) {
|
|
270
|
+
addEdge(edges, edgeKeys, seed.name, callee.name);
|
|
271
|
+
const k = key(callee);
|
|
272
|
+
if (!picked.has(k) && !candidates.has(k)) {
|
|
273
|
+
candidates.set(k, { row: callee, score: this.callerCount(callee.name) });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const caller of this.findCallers(seed.name, { limit: 15 })) {
|
|
277
|
+
if (!caller.fromSymbol) continue;
|
|
278
|
+
addEdge(edges, edgeKeys, caller.fromSymbol, seed.name);
|
|
279
|
+
for (const def of this.getSymbol(caller.fromSymbol, { limit: 1 })) {
|
|
280
|
+
const k = key(def);
|
|
281
|
+
if (!picked.has(k) && !candidates.has(k)) {
|
|
282
|
+
candidates.set(k, { row: def, score: this.callerCount(def.name) });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const related = [...candidates.values()].sort((a, b) => b.score - a.score).slice(0, Math.max(0, maxSymbols - picked.size)).map((c2) => c2.row);
|
|
288
|
+
const keptNames = new Set([...seeds, ...related].map((s) => s.name));
|
|
289
|
+
return {
|
|
290
|
+
query,
|
|
291
|
+
seeds,
|
|
292
|
+
related,
|
|
293
|
+
edges: edges.filter((e) => keptNames.has(e.from) && keptNames.has(e.to))
|
|
294
|
+
};
|
|
295
|
+
}
|
|
192
296
|
/** All references (calls + imports) to a name. */
|
|
193
297
|
findReferences(name, opts = {}) {
|
|
194
298
|
const limit = clampLimit(opts.limit);
|
|
@@ -972,17 +1076,23 @@ import { z } from "zod";
|
|
|
972
1076
|
// src/format.ts
|
|
973
1077
|
var format_exports = {};
|
|
974
1078
|
__export(format_exports, {
|
|
1079
|
+
formatContext: () => formatContext,
|
|
1080
|
+
formatImpact: () => formatImpact,
|
|
975
1081
|
formatNeighborhood: () => formatNeighborhood,
|
|
976
1082
|
formatRefs: () => formatRefs,
|
|
977
1083
|
formatStats: () => formatStats,
|
|
978
1084
|
formatSymbols: () => formatSymbols
|
|
979
1085
|
});
|
|
1086
|
+
function shortSig(sig) {
|
|
1087
|
+
if (!sig) return "";
|
|
1088
|
+
const s = sig.length > 88 ? `${sig.slice(0, 87)}\u2026` : sig;
|
|
1089
|
+
return ` \xB7 ${s}`;
|
|
1090
|
+
}
|
|
980
1091
|
function symbolLine(s) {
|
|
981
1092
|
const loc = `${s.file}:${s.startRow + 1}`;
|
|
982
1093
|
const container = s.container ? `${s.container}.` : "";
|
|
983
1094
|
const exp = s.exported ? "export " : "";
|
|
984
|
-
|
|
985
|
-
return `${s.kind} ${exp}${container}${s.name} \u2014 ${loc}${sig}`;
|
|
1095
|
+
return `${s.kind} ${exp}${container}${s.name} \u2014 ${loc}${shortSig(s.signature)}`;
|
|
986
1096
|
}
|
|
987
1097
|
function formatSymbols(rows) {
|
|
988
1098
|
if (rows.length === 0) return "No matching symbols.";
|
|
@@ -991,8 +1101,8 @@ function formatSymbols(rows) {
|
|
|
991
1101
|
function formatRefs(rows) {
|
|
992
1102
|
if (rows.length === 0) return "No references.";
|
|
993
1103
|
return rows.map((r) => {
|
|
994
|
-
const where = r.fromSymbol
|
|
995
|
-
return `${
|
|
1104
|
+
const where = r.fromSymbol ?? "(top level)";
|
|
1105
|
+
return `${r.file}:${r.startRow + 1} ${where}`;
|
|
996
1106
|
}).join("\n");
|
|
997
1107
|
}
|
|
998
1108
|
function formatNeighborhood(n) {
|
|
@@ -1009,6 +1119,24 @@ function formatNeighborhood(n) {
|
|
|
1009
1119
|
}
|
|
1010
1120
|
return lines.join("\n");
|
|
1011
1121
|
}
|
|
1122
|
+
function formatImpact(rows) {
|
|
1123
|
+
if (rows.length === 0) return "No callers \u2014 changing this is low-risk.";
|
|
1124
|
+
return rows.map((r) => `[${r.distance} hop] ${symbolLine(r)}`).join("\n");
|
|
1125
|
+
}
|
|
1126
|
+
function formatContext(b) {
|
|
1127
|
+
const lines = [`context for "${b.query}":`, "", "matches:"];
|
|
1128
|
+
if (b.seeds.length === 0) lines.push(" (no symbols matched)");
|
|
1129
|
+
else for (const s of b.seeds) lines.push(` ${symbolLine(s)}`);
|
|
1130
|
+
if (b.related.length > 0) {
|
|
1131
|
+
lines.push("", "related (ranked by call-site centrality):");
|
|
1132
|
+
for (const s of b.related) lines.push(` ${symbolLine(s)}`);
|
|
1133
|
+
}
|
|
1134
|
+
if (b.edges.length > 0) {
|
|
1135
|
+
lines.push("", "call edges:");
|
|
1136
|
+
for (const e of b.edges) lines.push(` ${e.from} \u2192 ${e.to}`);
|
|
1137
|
+
}
|
|
1138
|
+
return lines.join("\n");
|
|
1139
|
+
}
|
|
1012
1140
|
function formatStats(s) {
|
|
1013
1141
|
const kinds = Object.entries(s.byKind).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
1014
1142
|
const langs = Object.entries(s.byLang).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
@@ -1021,7 +1149,7 @@ function formatStats(s) {
|
|
|
1021
1149
|
}
|
|
1022
1150
|
|
|
1023
1151
|
// src/version.ts
|
|
1024
|
-
var VERSION = "0.
|
|
1152
|
+
var VERSION = "0.2.0";
|
|
1025
1153
|
|
|
1026
1154
|
// src/mcp.ts
|
|
1027
1155
|
var KIND = z.enum(["function", "method", "class", "interface", "type", "enum", "variable"]);
|
|
@@ -1067,6 +1195,43 @@ function createServer(store) {
|
|
|
1067
1195
|
},
|
|
1068
1196
|
async ({ name, limit }) => textResult(formatRefs(store.findCallers(name, { limit })))
|
|
1069
1197
|
);
|
|
1198
|
+
server.registerTool(
|
|
1199
|
+
"find_callees",
|
|
1200
|
+
{
|
|
1201
|
+
title: "Find callees",
|
|
1202
|
+
description: "List the functions/methods that a given symbol calls (resolved to their definitions). The outgoing side of the call graph \u2014 what this symbol depends on.",
|
|
1203
|
+
inputSchema: {
|
|
1204
|
+
name: z.string().describe("the calling function/method name"),
|
|
1205
|
+
limit: z.number().int().positive().max(500).optional()
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
async ({ name, limit }) => textResult(formatSymbols(store.findCallees(name, { limit })))
|
|
1209
|
+
);
|
|
1210
|
+
server.registerTool(
|
|
1211
|
+
"impact",
|
|
1212
|
+
{
|
|
1213
|
+
title: "Change impact / blast radius",
|
|
1214
|
+
description: "Show the transitive callers of a symbol \u2014 everything that could be affected if you change it \u2014 annotated with hop distance. Use before editing a widely-used function.",
|
|
1215
|
+
inputSchema: {
|
|
1216
|
+
name: z.string(),
|
|
1217
|
+
depth: z.number().int().min(1).max(6).optional().describe("hops to expand (default 3)"),
|
|
1218
|
+
limit: z.number().int().positive().max(300).optional()
|
|
1219
|
+
}
|
|
1220
|
+
},
|
|
1221
|
+
async ({ name, depth, limit }) => textResult(formatImpact(store.impact(name, { depth, limit })))
|
|
1222
|
+
);
|
|
1223
|
+
server.registerTool(
|
|
1224
|
+
"context",
|
|
1225
|
+
{
|
|
1226
|
+
title: "Build task context",
|
|
1227
|
+
description: "Given a task or feature query, return a compact, ranked relevance map: the matching symbols plus their immediate call neighbourhood, ordered by how widely each is called. The fastest way to orient an agent before a change \u2014 graph facts instead of reading files.",
|
|
1228
|
+
inputSchema: {
|
|
1229
|
+
query: z.string().describe("a task description or symbol/feature name"),
|
|
1230
|
+
maxSymbols: z.number().int().min(5).max(100).optional().describe("cap on symbols (default 30)")
|
|
1231
|
+
}
|
|
1232
|
+
},
|
|
1233
|
+
async ({ query, maxSymbols }) => textResult(formatContext(store.context(query, { maxSymbols })))
|
|
1234
|
+
);
|
|
1070
1235
|
server.registerTool(
|
|
1071
1236
|
"find_references",
|
|
1072
1237
|
{
|