@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/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
- /** Symbols that call a given name (both bare `foo()` and `x.foo()`). */
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, r.kind, r.start_row, r.start_col
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
- ORDER BY f.path, r.start_row LIMIT ?`
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
- const sig = s.signature ? ` \xB7 ${s.signature}` : "";
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 ? `${r.fromSymbol}` : "(top level)";
995
- return `${where} \u2192 ${r.name} [${r.kind}] \u2014 ${r.file}:${r.startRow + 1}`;
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.1.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
  {