@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 CHANGED
@@ -28,15 +28,20 @@ a changed file in **~0.5 ms** — roughly **3,000× cheaper than a full re-index
28
28
 
29
29
  ## How it compares to codegraph
30
30
 
31
- If you know [codegraph](https://github.com/colbymchenry/codegraph) (~35k★): it's
32
- the mature, feature-rich option 20+ languages, impact/test-affected analysis, a
33
- task-context builder, agent auto-install and it already does incremental
34
- indexing and file-watching. **codescope doesn't try to out-feature it.** In a
35
- measured head-to-head ([BENCHMARKS.md](./BENCHMARKS.md)) codescope's edge is being
36
- **leaner**: ~3× smaller index DB and faster pure indexing on the same repo, a
37
- ~1k-LOC auditable codebase, and zero-config `npx codescope mcp .`. Pick codescope
38
- if you want something small, fast, and easy to read; pick codegraph if you want
39
- the deepest feature set and widest language coverage.
31
+ [codegraph](https://github.com/colbymchenry/codegraph) (~35k★) is the mature
32
+ incumbent and shares codescope's architecture. In a **measured head-to-head**
33
+ ([BENCHMARKS.md](./BENCHMARKS.md), both tools run on the same repos), codescope
34
+ **wins the efficiency axes**:
35
+
36
+ - **~4× faster indexing** (696 ms vs 2,855 ms on a 262-file repo; 5.2 s vs 20 s on 3,500 files).
37
+ - **3–5× smaller index** on disk (2.5 MB vs 8.2 MB; 22.9 MB vs 112.8 MB).
38
+ - **Fewer tokens per answer** for definition lookups on every repo tested; callers is ≈parity.
39
+ - Feature parity on the core graph queries: `callers`, `callees`, `impact`, `context`.
40
+
41
+ codegraph still leads on **language breadth** (20+ vs 12), **extra tooling**
42
+ (`affected` test-impact, agent auto-install), and **maturity/adoption**. Pick
43
+ codescope when footprint, index speed, and token cost matter most; pick codegraph
44
+ when you need the widest language coverage and the extra tooling.
40
45
 
41
46
  ## Install
42
47
 
@@ -87,7 +92,10 @@ codescope watch . # keep the graph fresh, log updates
87
92
  |------|-----------------|
88
93
  | `search_symbols(query, kind?, limit?)` | fuzzy substring search over definitions — use instead of grep/glob |
89
94
  | `get_symbol(name, limit?)` | jump to a definition by exact name (kind, `file:line`, signature) |
90
- | `find_callers(name, limit?)` | who calls this function/method |
95
+ | `find_callers(name, limit?)` | who calls this function/method (distinct callers) |
96
+ | `find_callees(name, limit?)` | what this symbol calls — its outgoing dependencies |
97
+ | `impact(name, depth?, limit?)` | transitive callers (blast radius) before you change something |
98
+ | `context(query, maxSymbols?)` | a ranked relevance map for a task — matches + neighbours, the fastest way to orient |
91
99
  | `find_references(name, kind?, limit?)` | all calls + imports of a name |
92
100
  | `file_outline(path)` | every symbol in a file, in order — a compact alternative to reading it |
93
101
  | `neighborhood(name, depth?, limit?)` | the call neighbourhood (callers + callees) around a symbol, as a subgraph |
package/dist/cli.js CHANGED
@@ -6,12 +6,16 @@ import { resolve as resolve2 } from "path";
6
6
  import pc from "picocolors";
7
7
 
8
8
  // src/format.ts
9
+ function shortSig(sig) {
10
+ if (!sig) return "";
11
+ const s = sig.length > 88 ? `${sig.slice(0, 87)}\u2026` : sig;
12
+ return ` \xB7 ${s}`;
13
+ }
9
14
  function symbolLine(s) {
10
15
  const loc = `${s.file}:${s.startRow + 1}`;
11
16
  const container = s.container ? `${s.container}.` : "";
12
17
  const exp = s.exported ? "export " : "";
13
- const sig = s.signature ? ` \xB7 ${s.signature}` : "";
14
- return `${s.kind} ${exp}${container}${s.name} \u2014 ${loc}${sig}`;
18
+ return `${s.kind} ${exp}${container}${s.name} \u2014 ${loc}${shortSig(s.signature)}`;
15
19
  }
16
20
  function formatSymbols(rows) {
17
21
  if (rows.length === 0) return "No matching symbols.";
@@ -20,8 +24,8 @@ function formatSymbols(rows) {
20
24
  function formatRefs(rows) {
21
25
  if (rows.length === 0) return "No references.";
22
26
  return rows.map((r) => {
23
- const where = r.fromSymbol ? `${r.fromSymbol}` : "(top level)";
24
- return `${where} \u2192 ${r.name} [${r.kind}] \u2014 ${r.file}:${r.startRow + 1}`;
27
+ const where = r.fromSymbol ?? "(top level)";
28
+ return `${r.file}:${r.startRow + 1} ${where}`;
25
29
  }).join("\n");
26
30
  }
27
31
  function formatNeighborhood(n) {
@@ -38,6 +42,24 @@ function formatNeighborhood(n) {
38
42
  }
39
43
  return lines.join("\n");
40
44
  }
45
+ function formatImpact(rows) {
46
+ if (rows.length === 0) return "No callers \u2014 changing this is low-risk.";
47
+ return rows.map((r) => `[${r.distance} hop] ${symbolLine(r)}`).join("\n");
48
+ }
49
+ function formatContext(b) {
50
+ const lines = [`context for "${b.query}":`, "", "matches:"];
51
+ if (b.seeds.length === 0) lines.push(" (no symbols matched)");
52
+ else for (const s of b.seeds) lines.push(` ${symbolLine(s)}`);
53
+ if (b.related.length > 0) {
54
+ lines.push("", "related (ranked by call-site centrality):");
55
+ for (const s of b.related) lines.push(` ${symbolLine(s)}`);
56
+ }
57
+ if (b.edges.length > 0) {
58
+ lines.push("", "call edges:");
59
+ for (const e of b.edges) lines.push(` ${e.from} \u2192 ${e.to}`);
60
+ }
61
+ return lines.join("\n");
62
+ }
41
63
  function formatStats(s) {
42
64
  const kinds = Object.entries(s.byKind).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
43
65
  const langs = Object.entries(s.byLang).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
@@ -635,7 +657,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
635
657
  import { z } from "zod";
636
658
 
637
659
  // src/version.ts
638
- var VERSION = "0.1.0";
660
+ var VERSION = "0.2.0";
639
661
 
640
662
  // src/mcp.ts
641
663
  var KIND = z.enum(["function", "method", "class", "interface", "type", "enum", "variable"]);
@@ -681,6 +703,43 @@ function createServer(store) {
681
703
  },
682
704
  async ({ name, limit }) => textResult(formatRefs(store.findCallers(name, { limit })))
683
705
  );
706
+ server.registerTool(
707
+ "find_callees",
708
+ {
709
+ title: "Find callees",
710
+ 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.",
711
+ inputSchema: {
712
+ name: z.string().describe("the calling function/method name"),
713
+ limit: z.number().int().positive().max(500).optional()
714
+ }
715
+ },
716
+ async ({ name, limit }) => textResult(formatSymbols(store.findCallees(name, { limit })))
717
+ );
718
+ server.registerTool(
719
+ "impact",
720
+ {
721
+ title: "Change impact / blast radius",
722
+ 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.",
723
+ inputSchema: {
724
+ name: z.string(),
725
+ depth: z.number().int().min(1).max(6).optional().describe("hops to expand (default 3)"),
726
+ limit: z.number().int().positive().max(300).optional()
727
+ }
728
+ },
729
+ async ({ name, depth, limit }) => textResult(formatImpact(store.impact(name, { depth, limit })))
730
+ );
731
+ server.registerTool(
732
+ "context",
733
+ {
734
+ title: "Build task context",
735
+ 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.",
736
+ inputSchema: {
737
+ query: z.string().describe("a task description or symbol/feature name"),
738
+ maxSymbols: z.number().int().min(5).max(100).optional().describe("cap on symbols (default 30)")
739
+ }
740
+ },
741
+ async ({ query, maxSymbols }) => textResult(formatContext(store.context(query, { maxSymbols })))
742
+ );
684
743
  server.registerTool(
685
744
  "find_references",
686
745
  {
@@ -911,15 +970,119 @@ var GraphStore = class {
911
970
  WHERE s.name = ? ORDER BY s.exported DESC, f.path LIMIT ?`
912
971
  ).all(name, clampLimit(opts.limit)).map(toSymbolRow);
913
972
  }
914
- /** Symbols that call a given name (both bare `foo()` and `x.foo()`). */
973
+ /**
974
+ * Distinct callers of a name (both bare `foo()` and `x.foo()`). Multiple call
975
+ * sites from the same caller in the same file collapse to one row — fewer
976
+ * tokens and a more useful "who depends on this" answer.
977
+ */
915
978
  findCallers(name, opts = {}) {
916
979
  return this.db.prepare(
917
- `SELECT r.id, f.path AS file, r.from_symbol, r.name, r.kind, r.start_row, r.start_col
980
+ `SELECT MIN(r.id) AS id, f.path AS file, r.from_symbol, r.name,
981
+ MIN(r.kind) AS kind, MIN(r.start_row) AS start_row, MIN(r.start_col) AS start_col
918
982
  FROM refs r JOIN files f ON f.id = r.file_id
919
983
  WHERE r.name = ? AND r.kind IN ('call', 'method')
920
- ORDER BY f.path, r.start_row LIMIT ?`
984
+ GROUP BY f.path, r.from_symbol
985
+ ORDER BY f.path, start_row LIMIT ?`
921
986
  ).all(name, clampLimit(opts.limit)).map(toRefRow);
922
987
  }
988
+ /** The definitions that a symbol calls, resolved kind-aware to project symbols. */
989
+ findCallees(name, opts = {}) {
990
+ const limit = clampLimit(opts.limit);
991
+ const out = [];
992
+ const seen = /* @__PURE__ */ new Set();
993
+ for (const callee of this.calleesOf(name)) {
994
+ const defs = this.resolveCallee(callee.name, callee.kind, 6);
995
+ if (!defs) continue;
996
+ for (const d of defs) {
997
+ const key = `${d.name}@${d.file}:${d.startRow}`;
998
+ if (!seen.has(key)) {
999
+ seen.add(key);
1000
+ out.push(d);
1001
+ if (out.length >= limit) return out;
1002
+ }
1003
+ }
1004
+ }
1005
+ return out;
1006
+ }
1007
+ /** How many call sites reference this name (popularity / centrality signal). */
1008
+ callerCount(name) {
1009
+ return this.db.prepare(
1010
+ "SELECT COUNT(*) AS n FROM refs WHERE name = ? AND kind IN ('call','method')"
1011
+ ).get(name)?.n ?? 0;
1012
+ }
1013
+ /**
1014
+ * The blast radius of changing a symbol: its transitive callers, breadth-first,
1015
+ * annotated with hop distance and ordered nearest-first. Answers "what could
1016
+ * break if I change this?" without reading the codebase.
1017
+ */
1018
+ impact(name, opts = {}) {
1019
+ const depth = Math.max(1, Math.min(opts.depth ?? 3, 6));
1020
+ const limit = clampLimit(opts.limit, 300);
1021
+ const distance = /* @__PURE__ */ new Map([[name, 0]]);
1022
+ let frontier = [name];
1023
+ for (let d = 0; d < depth && frontier.length > 0 && distance.size < limit; d++) {
1024
+ const next = [];
1025
+ for (const node of frontier) {
1026
+ for (const caller of this.callersOf(node)) {
1027
+ if (!distance.has(caller)) {
1028
+ distance.set(caller, d + 1);
1029
+ next.push(caller);
1030
+ }
1031
+ }
1032
+ }
1033
+ frontier = next;
1034
+ }
1035
+ const out = [];
1036
+ for (const [n, dist] of distance) {
1037
+ if (dist === 0) continue;
1038
+ for (const def of this.getSymbol(n, { limit: 3 })) out.push({ ...def, distance: dist });
1039
+ }
1040
+ out.sort((a, b) => a.distance - b.distance || a.file.localeCompare(b.file));
1041
+ return out.slice(0, limit);
1042
+ }
1043
+ /**
1044
+ * A token-budgeted relevance map for a task: the symbols matching `query` plus
1045
+ * their immediate call neighbourhood, ranked by call-site centrality, capped at
1046
+ * `maxSymbols`. This is the slice of the codebase an agent needs to start a
1047
+ * change — delivered as graph facts, not file dumps.
1048
+ */
1049
+ context(query, opts = {}) {
1050
+ const maxSymbols = Math.max(5, Math.min(opts.maxSymbols ?? 30, 100));
1051
+ const seeds = this.searchSymbols(query, { limit: Math.min(8, maxSymbols) });
1052
+ const picked = /* @__PURE__ */ new Map();
1053
+ const key = (s) => `${s.name}@${s.file}:${s.startRow}`;
1054
+ for (const s of seeds) picked.set(key(s), s);
1055
+ const candidates = /* @__PURE__ */ new Map();
1056
+ const edges = [];
1057
+ const edgeKeys = /* @__PURE__ */ new Set();
1058
+ for (const seed of seeds) {
1059
+ for (const callee of this.findCallees(seed.name, { limit: 15 })) {
1060
+ addEdge(edges, edgeKeys, seed.name, callee.name);
1061
+ const k = key(callee);
1062
+ if (!picked.has(k) && !candidates.has(k)) {
1063
+ candidates.set(k, { row: callee, score: this.callerCount(callee.name) });
1064
+ }
1065
+ }
1066
+ for (const caller of this.findCallers(seed.name, { limit: 15 })) {
1067
+ if (!caller.fromSymbol) continue;
1068
+ addEdge(edges, edgeKeys, caller.fromSymbol, seed.name);
1069
+ for (const def of this.getSymbol(caller.fromSymbol, { limit: 1 })) {
1070
+ const k = key(def);
1071
+ if (!picked.has(k) && !candidates.has(k)) {
1072
+ candidates.set(k, { row: def, score: this.callerCount(def.name) });
1073
+ }
1074
+ }
1075
+ }
1076
+ }
1077
+ const related = [...candidates.values()].sort((a, b) => b.score - a.score).slice(0, Math.max(0, maxSymbols - picked.size)).map((c2) => c2.row);
1078
+ const keptNames = new Set([...seeds, ...related].map((s) => s.name));
1079
+ return {
1080
+ query,
1081
+ seeds,
1082
+ related,
1083
+ edges: edges.filter((e) => keptNames.has(e.from) && keptNames.has(e.to))
1084
+ };
1085
+ }
923
1086
  /** All references (calls + imports) to a name. */
924
1087
  findReferences(name, opts = {}) {
925
1088
  const limit = clampLimit(opts.limit);
@@ -1130,6 +1293,9 @@ Commands:
1130
1293
  search <query> [path] Fuzzy-search symbol names.
1131
1294
  get <name> [path] Look up a definition by exact name.
1132
1295
  callers <name> [path] List callers of a function/method.
1296
+ callees <name> [path] List what a function/method calls.
1297
+ impact <name> [path] Transitive callers (blast radius) of a symbol.
1298
+ context <query> [path] Ranked relevance map for a task (matches + neighbours).
1133
1299
  neighborhood <name> Show the call neighbourhood around a symbol.
1134
1300
 
1135
1301
  Options:
@@ -1236,6 +1402,15 @@ async function cmdQuery(command, root, flags) {
1236
1402
  case "callers":
1237
1403
  out = formatRefs(store.findCallers(term, { limit: flags.limit }));
1238
1404
  break;
1405
+ case "callees":
1406
+ out = formatSymbols(store.findCallees(term, { limit: flags.limit }));
1407
+ break;
1408
+ case "impact":
1409
+ out = formatImpact(store.impact(term, { depth: flags.depth, limit: flags.limit }));
1410
+ break;
1411
+ case "context":
1412
+ out = formatContext(store.context(term, { maxSymbols: flags.limit }));
1413
+ break;
1239
1414
  case "neighborhood":
1240
1415
  out = formatNeighborhood(store.neighborhood(term, { depth: flags.depth, limit: flags.limit }));
1241
1416
  break;
@@ -1307,6 +1482,9 @@ async function main() {
1307
1482
  case "search":
1308
1483
  case "get":
1309
1484
  case "callers":
1485
+ case "callees":
1486
+ case "impact":
1487
+ case "context":
1310
1488
  case "neighborhood":
1311
1489
  return cmdQuery(command, resolve2(flags.path ?? flags.positional[1] ?? "."), flags);
1312
1490
  case "watch":