@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/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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
the
|
|
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
|
-
|
|
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
|
|
24
|
-
return `${
|
|
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.
|
|
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
|
-
/**
|
|
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,
|
|
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
|
-
|
|
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":
|