@abdulmunimjemal/codescope 0.1.0 → 0.3.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 +38 -14
- package/dist/cli.js +514 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +111 -6
- package/dist/index.js +467 -14
- 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);
|
|
@@ -203,6 +307,18 @@ var GraphStore = class {
|
|
|
203
307
|
).all(name, limit);
|
|
204
308
|
return rows.map(toRefRow);
|
|
205
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Files whose imports reference a module basename (e.g. `store` for
|
|
312
|
+
* `src/store.ts`). Matches `import … from "./store"`, `"../src/store.js"`,
|
|
313
|
+
* etc. Used by affected-test analysis to follow import edges, which — unlike
|
|
314
|
+
* call edges — reliably reach test files (tests import the module under test).
|
|
315
|
+
*/
|
|
316
|
+
findImporters(moduleBasename2) {
|
|
317
|
+
return this.db.prepare(
|
|
318
|
+
`SELECT DISTINCT f.path FROM refs r JOIN files f ON f.id = r.file_id
|
|
319
|
+
WHERE r.kind = 'import' AND (r.name = ? OR r.name LIKE ? OR r.name LIKE ?)`
|
|
320
|
+
).all(moduleBasename2, `%/${moduleBasename2}`, `%/${moduleBasename2}.%`).map((r) => r.path);
|
|
321
|
+
}
|
|
206
322
|
/** The symbols defined in a file, in source order. */
|
|
207
323
|
fileOutline(path) {
|
|
208
324
|
return this.db.prepare(
|
|
@@ -579,6 +695,118 @@ var php = {
|
|
|
579
695
|
importRules: [{ type: "namespace_use_declaration", childTypes: ["namespace_use_clause"] }],
|
|
580
696
|
exportTypes: /* @__PURE__ */ new Set()
|
|
581
697
|
};
|
|
698
|
+
var scala = {
|
|
699
|
+
id: "scala",
|
|
700
|
+
wasm: "scala",
|
|
701
|
+
defs: {
|
|
702
|
+
class_definition: { kind: "class" },
|
|
703
|
+
object_definition: { kind: "class" },
|
|
704
|
+
trait_definition: { kind: "interface" },
|
|
705
|
+
function_definition: { kind: "method" },
|
|
706
|
+
type_definition: { kind: "type" }
|
|
707
|
+
},
|
|
708
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
709
|
+
nestedFunctionsAreMethods: false,
|
|
710
|
+
callRules: [
|
|
711
|
+
{ type: "call_expression", fnField: "function", memberTypes: ["field_expression"], memberField: "field" }
|
|
712
|
+
],
|
|
713
|
+
importRules: [{ type: "import_declaration", childTypes: ["stable_identifier", "identifier"] }],
|
|
714
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
715
|
+
};
|
|
716
|
+
var solidity = {
|
|
717
|
+
id: "solidity",
|
|
718
|
+
wasm: "solidity",
|
|
719
|
+
defs: {
|
|
720
|
+
contract_declaration: { kind: "class" },
|
|
721
|
+
interface_declaration: { kind: "interface" },
|
|
722
|
+
library_declaration: { kind: "class" },
|
|
723
|
+
function_definition: { kind: "method" },
|
|
724
|
+
modifier_definition: { kind: "function" },
|
|
725
|
+
struct_declaration: { kind: "class" },
|
|
726
|
+
enum_declaration: { kind: "enum" }
|
|
727
|
+
},
|
|
728
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
729
|
+
nestedFunctionsAreMethods: false,
|
|
730
|
+
callRules: [{ type: "call_expression", fnField: "function" }],
|
|
731
|
+
importRules: [{ type: "import_directive", field: "source" }],
|
|
732
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
733
|
+
};
|
|
734
|
+
var zig = {
|
|
735
|
+
id: "zig",
|
|
736
|
+
wasm: "zig",
|
|
737
|
+
defs: { function_declaration: { kind: "function" } },
|
|
738
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
739
|
+
nestedFunctionsAreMethods: false,
|
|
740
|
+
callRules: [{ type: "call_expression", fnField: "function" }],
|
|
741
|
+
importRules: [],
|
|
742
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
743
|
+
};
|
|
744
|
+
var kotlin = {
|
|
745
|
+
id: "kotlin",
|
|
746
|
+
wasm: "kotlin",
|
|
747
|
+
defs: {
|
|
748
|
+
class_declaration: { kind: "class", name: "first_typed", nameTypes: ["type_identifier"] },
|
|
749
|
+
object_declaration: { kind: "class", name: "first_typed", nameTypes: ["type_identifier"] },
|
|
750
|
+
function_declaration: { kind: "function", name: "first_typed", nameTypes: ["simple_identifier"] }
|
|
751
|
+
},
|
|
752
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
753
|
+
nestedFunctionsAreMethods: true,
|
|
754
|
+
callRules: [],
|
|
755
|
+
importRules: [{ type: "import_header", childTypes: ["identifier"] }],
|
|
756
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
757
|
+
};
|
|
758
|
+
var objc = {
|
|
759
|
+
id: "objc",
|
|
760
|
+
wasm: "objc",
|
|
761
|
+
defs: {
|
|
762
|
+
class_interface: { kind: "class", name: "first_typed", nameTypes: ["identifier"] },
|
|
763
|
+
class_implementation: { kind: "class", name: "first_typed", nameTypes: ["identifier"] },
|
|
764
|
+
method_declaration: { kind: "method", name: "first_typed", nameTypes: ["identifier"] },
|
|
765
|
+
method_definition: { kind: "method", name: "first_typed", nameTypes: ["identifier"] }
|
|
766
|
+
},
|
|
767
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
768
|
+
nestedFunctionsAreMethods: false,
|
|
769
|
+
callRules: [{ type: "call_expression", fnField: "function" }],
|
|
770
|
+
importRules: [{ type: "preproc_include", field: "path" }],
|
|
771
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
772
|
+
};
|
|
773
|
+
var lua = {
|
|
774
|
+
id: "lua",
|
|
775
|
+
wasm: "lua",
|
|
776
|
+
defs: {
|
|
777
|
+
function_definition_statement: { kind: "function" },
|
|
778
|
+
local_function_definition_statement: { kind: "function" }
|
|
779
|
+
},
|
|
780
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
781
|
+
nestedFunctionsAreMethods: false,
|
|
782
|
+
callRules: [],
|
|
783
|
+
importRules: [],
|
|
784
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
785
|
+
};
|
|
786
|
+
var bash = {
|
|
787
|
+
id: "bash",
|
|
788
|
+
wasm: "bash",
|
|
789
|
+
defs: { function_definition: { kind: "function" } },
|
|
790
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
791
|
+
nestedFunctionsAreMethods: false,
|
|
792
|
+
callRules: [],
|
|
793
|
+
importRules: [],
|
|
794
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
795
|
+
};
|
|
796
|
+
var ocaml = {
|
|
797
|
+
id: "ocaml",
|
|
798
|
+
wasm: "ocaml",
|
|
799
|
+
defs: {
|
|
800
|
+
let_binding: { kind: "function", name: "field", nameField: "pattern" },
|
|
801
|
+
module_definition: { kind: "class" },
|
|
802
|
+
type_definition: { kind: "type" }
|
|
803
|
+
},
|
|
804
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
805
|
+
nestedFunctionsAreMethods: false,
|
|
806
|
+
callRules: [],
|
|
807
|
+
importRules: [],
|
|
808
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
809
|
+
};
|
|
582
810
|
var LANGUAGES = {
|
|
583
811
|
typescript,
|
|
584
812
|
tsx,
|
|
@@ -591,7 +819,15 @@ var LANGUAGES = {
|
|
|
591
819
|
c,
|
|
592
820
|
cpp,
|
|
593
821
|
csharp,
|
|
594
|
-
php
|
|
822
|
+
php,
|
|
823
|
+
scala,
|
|
824
|
+
solidity,
|
|
825
|
+
zig,
|
|
826
|
+
kotlin,
|
|
827
|
+
objc,
|
|
828
|
+
lua,
|
|
829
|
+
bash,
|
|
830
|
+
ocaml
|
|
595
831
|
};
|
|
596
832
|
var EXT_TO_LANG = {
|
|
597
833
|
".ts": "typescript",
|
|
@@ -616,7 +852,19 @@ var EXT_TO_LANG = {
|
|
|
616
852
|
".hpp": "cpp",
|
|
617
853
|
".hh": "cpp",
|
|
618
854
|
".cs": "csharp",
|
|
619
|
-
".php": "php"
|
|
855
|
+
".php": "php",
|
|
856
|
+
".scala": "scala",
|
|
857
|
+
".sc": "scala",
|
|
858
|
+
".sol": "solidity",
|
|
859
|
+
".zig": "zig",
|
|
860
|
+
".kt": "kotlin",
|
|
861
|
+
".kts": "kotlin",
|
|
862
|
+
".m": "objc",
|
|
863
|
+
".lua": "lua",
|
|
864
|
+
".sh": "bash",
|
|
865
|
+
".bash": "bash",
|
|
866
|
+
".ml": "ocaml",
|
|
867
|
+
".mli": "ocaml"
|
|
620
868
|
};
|
|
621
869
|
var SUPPORTED_EXTENSIONS = Object.keys(EXT_TO_LANG);
|
|
622
870
|
function languageForPath(path) {
|
|
@@ -702,7 +950,7 @@ function classify(node, lang) {
|
|
|
702
950
|
return null;
|
|
703
951
|
}
|
|
704
952
|
function buildSymbol(node, rule, container, containerKind, lang) {
|
|
705
|
-
const name = symbolName(node, rule
|
|
953
|
+
const name = symbolName(node, rule);
|
|
706
954
|
if (!name) return null;
|
|
707
955
|
let kind = rule.kind;
|
|
708
956
|
if (kind === "function" && lang.nestedFunctionsAreMethods && containerKind === "class") {
|
|
@@ -722,9 +970,20 @@ function buildSymbol(node, rule, container, containerKind, lang) {
|
|
|
722
970
|
endByte: node.endIndex
|
|
723
971
|
};
|
|
724
972
|
}
|
|
725
|
-
function symbolName(node,
|
|
726
|
-
|
|
727
|
-
|
|
973
|
+
function symbolName(node, rule) {
|
|
974
|
+
switch (rule.name ?? "field") {
|
|
975
|
+
case "c_declarator":
|
|
976
|
+
return cDeclaratorName(node);
|
|
977
|
+
case "first_typed": {
|
|
978
|
+
const types = rule.nameTypes ?? [];
|
|
979
|
+
for (const child of node.namedChildren) {
|
|
980
|
+
if (types.includes(child.type)) return child.text;
|
|
981
|
+
}
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
default:
|
|
985
|
+
return node.childForFieldName(rule.nameField ?? "name")?.text ?? null;
|
|
986
|
+
}
|
|
728
987
|
}
|
|
729
988
|
function cDeclaratorName(node) {
|
|
730
989
|
let decl = node.childForFieldName("declarator");
|
|
@@ -969,20 +1228,79 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
969
1228
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
970
1229
|
import { z } from "zod";
|
|
971
1230
|
|
|
1231
|
+
// src/affected.ts
|
|
1232
|
+
var TEST_PATTERNS = [
|
|
1233
|
+
/(^|\/)(tests?|spec|specs|__tests__|e2e|integration)\//i,
|
|
1234
|
+
/\.(test|spec)\.[a-z]+$/i,
|
|
1235
|
+
// foo.test.ts, foo.spec.js
|
|
1236
|
+
/_test\.[a-z]+$/i,
|
|
1237
|
+
// foo_test.go, foo_test.py, foo_test.rs
|
|
1238
|
+
/(^|\/)test_[^/]+\.(py|rb)$/i,
|
|
1239
|
+
// test_foo.py
|
|
1240
|
+
/(Test|Tests|Spec)\.[a-z]+$/i,
|
|
1241
|
+
// FooTest.java, FooTests.cs, FooSpec.scala
|
|
1242
|
+
/_spec\.rb$/i
|
|
1243
|
+
// foo_spec.rb
|
|
1244
|
+
];
|
|
1245
|
+
function isTestFile(path) {
|
|
1246
|
+
return TEST_PATTERNS.some((re) => re.test(path));
|
|
1247
|
+
}
|
|
1248
|
+
function moduleBasename(path) {
|
|
1249
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
1250
|
+
const dot = base.indexOf(".");
|
|
1251
|
+
return dot === -1 ? base : base.slice(0, dot);
|
|
1252
|
+
}
|
|
1253
|
+
function affected(store, changedPaths, opts = {}) {
|
|
1254
|
+
const depth = opts.depth ?? 4;
|
|
1255
|
+
const impactedFiles = new Set(changedPaths);
|
|
1256
|
+
for (const path of changedPaths) {
|
|
1257
|
+
for (const sym of store.fileOutline(path)) {
|
|
1258
|
+
for (const caller of store.impact(sym.name, { depth })) {
|
|
1259
|
+
impactedFiles.add(caller.file);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
let frontier = [...changedPaths];
|
|
1264
|
+
for (let d = 0; d < depth && frontier.length > 0; d++) {
|
|
1265
|
+
const next = [];
|
|
1266
|
+
for (const path of frontier) {
|
|
1267
|
+
for (const importer of store.findImporters(moduleBasename(path))) {
|
|
1268
|
+
if (!impactedFiles.has(importer)) {
|
|
1269
|
+
impactedFiles.add(importer);
|
|
1270
|
+
next.push(importer);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
frontier = next;
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
changed: changedPaths,
|
|
1278
|
+
impactedFiles: [...impactedFiles].sort(),
|
|
1279
|
+
tests: [...impactedFiles].filter(isTestFile).sort()
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
972
1283
|
// src/format.ts
|
|
973
1284
|
var format_exports = {};
|
|
974
1285
|
__export(format_exports, {
|
|
1286
|
+
formatAffected: () => formatAffected,
|
|
1287
|
+
formatContext: () => formatContext,
|
|
1288
|
+
formatImpact: () => formatImpact,
|
|
975
1289
|
formatNeighborhood: () => formatNeighborhood,
|
|
976
1290
|
formatRefs: () => formatRefs,
|
|
977
1291
|
formatStats: () => formatStats,
|
|
978
1292
|
formatSymbols: () => formatSymbols
|
|
979
1293
|
});
|
|
1294
|
+
function shortSig(sig) {
|
|
1295
|
+
if (!sig) return "";
|
|
1296
|
+
const s = sig.length > 88 ? `${sig.slice(0, 87)}\u2026` : sig;
|
|
1297
|
+
return ` \xB7 ${s}`;
|
|
1298
|
+
}
|
|
980
1299
|
function symbolLine(s) {
|
|
981
1300
|
const loc = `${s.file}:${s.startRow + 1}`;
|
|
982
1301
|
const container = s.container ? `${s.container}.` : "";
|
|
983
1302
|
const exp = s.exported ? "export " : "";
|
|
984
|
-
|
|
985
|
-
return `${s.kind} ${exp}${container}${s.name} \u2014 ${loc}${sig}`;
|
|
1303
|
+
return `${s.kind} ${exp}${container}${s.name} \u2014 ${loc}${shortSig(s.signature)}`;
|
|
986
1304
|
}
|
|
987
1305
|
function formatSymbols(rows) {
|
|
988
1306
|
if (rows.length === 0) return "No matching symbols.";
|
|
@@ -991,8 +1309,8 @@ function formatSymbols(rows) {
|
|
|
991
1309
|
function formatRefs(rows) {
|
|
992
1310
|
if (rows.length === 0) return "No references.";
|
|
993
1311
|
return rows.map((r) => {
|
|
994
|
-
const where = r.fromSymbol
|
|
995
|
-
return `${
|
|
1312
|
+
const where = r.fromSymbol ?? "(top level)";
|
|
1313
|
+
return `${r.file}:${r.startRow + 1} ${where}`;
|
|
996
1314
|
}).join("\n");
|
|
997
1315
|
}
|
|
998
1316
|
function formatNeighborhood(n) {
|
|
@@ -1009,6 +1327,32 @@ function formatNeighborhood(n) {
|
|
|
1009
1327
|
}
|
|
1010
1328
|
return lines.join("\n");
|
|
1011
1329
|
}
|
|
1330
|
+
function formatImpact(rows) {
|
|
1331
|
+
if (rows.length === 0) return "No callers \u2014 changing this is low-risk.";
|
|
1332
|
+
return rows.map((r) => `[${r.distance} hop] ${symbolLine(r)}`).join("\n");
|
|
1333
|
+
}
|
|
1334
|
+
function formatContext(b) {
|
|
1335
|
+
const lines = [`context for "${b.query}":`, "", "matches:"];
|
|
1336
|
+
if (b.seeds.length === 0) lines.push(" (no symbols matched)");
|
|
1337
|
+
else for (const s of b.seeds) lines.push(` ${symbolLine(s)}`);
|
|
1338
|
+
if (b.related.length > 0) {
|
|
1339
|
+
lines.push("", "related (ranked by call-site centrality):");
|
|
1340
|
+
for (const s of b.related) lines.push(` ${symbolLine(s)}`);
|
|
1341
|
+
}
|
|
1342
|
+
if (b.edges.length > 0) {
|
|
1343
|
+
lines.push("", "call edges:");
|
|
1344
|
+
for (const e of b.edges) lines.push(` ${e.from} \u2192 ${e.to}`);
|
|
1345
|
+
}
|
|
1346
|
+
return lines.join("\n");
|
|
1347
|
+
}
|
|
1348
|
+
function formatAffected(r) {
|
|
1349
|
+
if (r.tests.length === 0) {
|
|
1350
|
+
return `No test files appear affected by ${r.changed.length} changed file(s).`;
|
|
1351
|
+
}
|
|
1352
|
+
const lines = [`${r.tests.length} test file(s) affected by your changes:`, ""];
|
|
1353
|
+
for (const t of r.tests) lines.push(` ${t}`);
|
|
1354
|
+
return lines.join("\n");
|
|
1355
|
+
}
|
|
1012
1356
|
function formatStats(s) {
|
|
1013
1357
|
const kinds = Object.entries(s.byKind).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
1014
1358
|
const langs = Object.entries(s.byLang).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
@@ -1021,7 +1365,7 @@ function formatStats(s) {
|
|
|
1021
1365
|
}
|
|
1022
1366
|
|
|
1023
1367
|
// src/version.ts
|
|
1024
|
-
var VERSION = "0.
|
|
1368
|
+
var VERSION = "0.3.0";
|
|
1025
1369
|
|
|
1026
1370
|
// src/mcp.ts
|
|
1027
1371
|
var KIND = z.enum(["function", "method", "class", "interface", "type", "enum", "variable"]);
|
|
@@ -1067,6 +1411,43 @@ function createServer(store) {
|
|
|
1067
1411
|
},
|
|
1068
1412
|
async ({ name, limit }) => textResult(formatRefs(store.findCallers(name, { limit })))
|
|
1069
1413
|
);
|
|
1414
|
+
server.registerTool(
|
|
1415
|
+
"find_callees",
|
|
1416
|
+
{
|
|
1417
|
+
title: "Find callees",
|
|
1418
|
+
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.",
|
|
1419
|
+
inputSchema: {
|
|
1420
|
+
name: z.string().describe("the calling function/method name"),
|
|
1421
|
+
limit: z.number().int().positive().max(500).optional()
|
|
1422
|
+
}
|
|
1423
|
+
},
|
|
1424
|
+
async ({ name, limit }) => textResult(formatSymbols(store.findCallees(name, { limit })))
|
|
1425
|
+
);
|
|
1426
|
+
server.registerTool(
|
|
1427
|
+
"impact",
|
|
1428
|
+
{
|
|
1429
|
+
title: "Change impact / blast radius",
|
|
1430
|
+
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.",
|
|
1431
|
+
inputSchema: {
|
|
1432
|
+
name: z.string(),
|
|
1433
|
+
depth: z.number().int().min(1).max(6).optional().describe("hops to expand (default 3)"),
|
|
1434
|
+
limit: z.number().int().positive().max(300).optional()
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
async ({ name, depth, limit }) => textResult(formatImpact(store.impact(name, { depth, limit })))
|
|
1438
|
+
);
|
|
1439
|
+
server.registerTool(
|
|
1440
|
+
"context",
|
|
1441
|
+
{
|
|
1442
|
+
title: "Build task context",
|
|
1443
|
+
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.",
|
|
1444
|
+
inputSchema: {
|
|
1445
|
+
query: z.string().describe("a task description or symbol/feature name"),
|
|
1446
|
+
maxSymbols: z.number().int().min(5).max(100).optional().describe("cap on symbols (default 30)")
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
async ({ query, maxSymbols }) => textResult(formatContext(store.context(query, { maxSymbols })))
|
|
1450
|
+
);
|
|
1070
1451
|
server.registerTool(
|
|
1071
1452
|
"find_references",
|
|
1072
1453
|
{
|
|
@@ -1104,6 +1485,18 @@ function createServer(store) {
|
|
|
1104
1485
|
},
|
|
1105
1486
|
async ({ name, depth, limit }) => textResult(formatNeighborhood(store.neighborhood(name, { depth, limit })))
|
|
1106
1487
|
);
|
|
1488
|
+
server.registerTool(
|
|
1489
|
+
"affected",
|
|
1490
|
+
{
|
|
1491
|
+
title: "Affected tests",
|
|
1492
|
+
description: "Given a list of changed files, return the test files likely affected \u2014 the symbols those files define, walked through their transitive callers, filtered to tests. Use before running a suite to know what's worth re-running.",
|
|
1493
|
+
inputSchema: {
|
|
1494
|
+
files: z.array(z.string()).describe("repo-relative paths of the changed files"),
|
|
1495
|
+
depth: z.number().int().min(1).max(6).optional()
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
1498
|
+
async ({ files, depth }) => textResult(formatAffected(affected(store, files, { depth })))
|
|
1499
|
+
);
|
|
1107
1500
|
server.registerTool(
|
|
1108
1501
|
"stats",
|
|
1109
1502
|
{
|
|
@@ -1120,17 +1513,77 @@ async function runStdioServer(store) {
|
|
|
1120
1513
|
const transport = new StdioServerTransport();
|
|
1121
1514
|
await server.connect(transport);
|
|
1122
1515
|
}
|
|
1516
|
+
|
|
1517
|
+
// src/install.ts
|
|
1518
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1519
|
+
import { homedir } from "os";
|
|
1520
|
+
import { dirname, join } from "path";
|
|
1521
|
+
var PACKAGE = "@abdulmunimjemal/codescope";
|
|
1522
|
+
var MCP_SERVER_NAME = "codescope";
|
|
1523
|
+
var SUPPORTED_AGENTS = ["claude", "cursor"];
|
|
1524
|
+
function serverEntry(serveTarget = ".") {
|
|
1525
|
+
return { command: "npx", args: ["-y", PACKAGE, "mcp", serveTarget] };
|
|
1526
|
+
}
|
|
1527
|
+
function configPath(agent, root, global = false) {
|
|
1528
|
+
switch (agent) {
|
|
1529
|
+
case "claude":
|
|
1530
|
+
return global ? join(homedir(), ".mcp.json") : join(root, ".mcp.json");
|
|
1531
|
+
case "cursor":
|
|
1532
|
+
return global ? join(homedir(), ".cursor", "mcp.json") : join(root, ".cursor", "mcp.json");
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
function readJson(path) {
|
|
1536
|
+
if (!existsSync(path)) return {};
|
|
1537
|
+
try {
|
|
1538
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1539
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1540
|
+
} catch {
|
|
1541
|
+
return {};
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
function installInto(agent, root, opts = {}) {
|
|
1545
|
+
const path = configPath(agent, root, opts.global ?? false);
|
|
1546
|
+
const config = readJson(path);
|
|
1547
|
+
const existing = config.mcpServers;
|
|
1548
|
+
const servers = existing && typeof existing === "object" ? existing : {};
|
|
1549
|
+
const existed = Object.prototype.hasOwnProperty.call(servers, MCP_SERVER_NAME);
|
|
1550
|
+
servers[MCP_SERVER_NAME] = serverEntry(opts.serveTarget);
|
|
1551
|
+
config.mcpServers = servers;
|
|
1552
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1553
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
1554
|
+
`);
|
|
1555
|
+
return { agent, path, action: existed ? "updated" : "added" };
|
|
1556
|
+
}
|
|
1557
|
+
function install(root, opts = {}) {
|
|
1558
|
+
const agents = opts.agents ?? [...SUPPORTED_AGENTS];
|
|
1559
|
+
return agents.map((agent) => installInto(agent, root, opts));
|
|
1560
|
+
}
|
|
1561
|
+
function codexSnippet(serveTarget = ".") {
|
|
1562
|
+
return [
|
|
1563
|
+
"[mcp_servers.codescope]",
|
|
1564
|
+
'command = "npx"',
|
|
1565
|
+
`args = ["-y", "${PACKAGE}", "mcp", "${serveTarget}"]`
|
|
1566
|
+
].join("\n");
|
|
1567
|
+
}
|
|
1123
1568
|
export {
|
|
1124
1569
|
GraphStore,
|
|
1125
1570
|
Indexer,
|
|
1126
1571
|
LANGUAGES,
|
|
1572
|
+
SUPPORTED_AGENTS,
|
|
1127
1573
|
SUPPORTED_EXTENSIONS,
|
|
1128
1574
|
VERSION,
|
|
1575
|
+
affected,
|
|
1576
|
+
codexSnippet,
|
|
1577
|
+
configPath,
|
|
1129
1578
|
createServer,
|
|
1130
1579
|
format_exports as format,
|
|
1580
|
+
install,
|
|
1581
|
+
installInto,
|
|
1582
|
+
isTestFile,
|
|
1131
1583
|
languageForPath,
|
|
1132
1584
|
parseSource,
|
|
1133
1585
|
runStdioServer,
|
|
1586
|
+
serverEntry,
|
|
1134
1587
|
watch
|
|
1135
1588
|
};
|
|
1136
1589
|
//# sourceMappingURL=index.js.map
|