@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/cli.js
CHANGED
|
@@ -1,17 +1,73 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { mkdirSync } from "fs";
|
|
4
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
5
5
|
import { resolve as resolve2 } from "path";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
|
|
8
|
+
// src/affected.ts
|
|
9
|
+
var TEST_PATTERNS = [
|
|
10
|
+
/(^|\/)(tests?|spec|specs|__tests__|e2e|integration)\//i,
|
|
11
|
+
/\.(test|spec)\.[a-z]+$/i,
|
|
12
|
+
// foo.test.ts, foo.spec.js
|
|
13
|
+
/_test\.[a-z]+$/i,
|
|
14
|
+
// foo_test.go, foo_test.py, foo_test.rs
|
|
15
|
+
/(^|\/)test_[^/]+\.(py|rb)$/i,
|
|
16
|
+
// test_foo.py
|
|
17
|
+
/(Test|Tests|Spec)\.[a-z]+$/i,
|
|
18
|
+
// FooTest.java, FooTests.cs, FooSpec.scala
|
|
19
|
+
/_spec\.rb$/i
|
|
20
|
+
// foo_spec.rb
|
|
21
|
+
];
|
|
22
|
+
function isTestFile(path) {
|
|
23
|
+
return TEST_PATTERNS.some((re) => re.test(path));
|
|
24
|
+
}
|
|
25
|
+
function moduleBasename(path) {
|
|
26
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
27
|
+
const dot = base.indexOf(".");
|
|
28
|
+
return dot === -1 ? base : base.slice(0, dot);
|
|
29
|
+
}
|
|
30
|
+
function affected(store, changedPaths, opts = {}) {
|
|
31
|
+
const depth = opts.depth ?? 4;
|
|
32
|
+
const impactedFiles = new Set(changedPaths);
|
|
33
|
+
for (const path of changedPaths) {
|
|
34
|
+
for (const sym of store.fileOutline(path)) {
|
|
35
|
+
for (const caller of store.impact(sym.name, { depth })) {
|
|
36
|
+
impactedFiles.add(caller.file);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let frontier = [...changedPaths];
|
|
41
|
+
for (let d = 0; d < depth && frontier.length > 0; d++) {
|
|
42
|
+
const next = [];
|
|
43
|
+
for (const path of frontier) {
|
|
44
|
+
for (const importer of store.findImporters(moduleBasename(path))) {
|
|
45
|
+
if (!impactedFiles.has(importer)) {
|
|
46
|
+
impactedFiles.add(importer);
|
|
47
|
+
next.push(importer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
frontier = next;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
changed: changedPaths,
|
|
55
|
+
impactedFiles: [...impactedFiles].sort(),
|
|
56
|
+
tests: [...impactedFiles].filter(isTestFile).sort()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
8
60
|
// src/format.ts
|
|
61
|
+
function shortSig(sig) {
|
|
62
|
+
if (!sig) return "";
|
|
63
|
+
const s = sig.length > 88 ? `${sig.slice(0, 87)}\u2026` : sig;
|
|
64
|
+
return ` \xB7 ${s}`;
|
|
65
|
+
}
|
|
9
66
|
function symbolLine(s) {
|
|
10
67
|
const loc = `${s.file}:${s.startRow + 1}`;
|
|
11
68
|
const container = s.container ? `${s.container}.` : "";
|
|
12
69
|
const exp = s.exported ? "export " : "";
|
|
13
|
-
|
|
14
|
-
return `${s.kind} ${exp}${container}${s.name} \u2014 ${loc}${sig}`;
|
|
70
|
+
return `${s.kind} ${exp}${container}${s.name} \u2014 ${loc}${shortSig(s.signature)}`;
|
|
15
71
|
}
|
|
16
72
|
function formatSymbols(rows) {
|
|
17
73
|
if (rows.length === 0) return "No matching symbols.";
|
|
@@ -20,8 +76,8 @@ function formatSymbols(rows) {
|
|
|
20
76
|
function formatRefs(rows) {
|
|
21
77
|
if (rows.length === 0) return "No references.";
|
|
22
78
|
return rows.map((r) => {
|
|
23
|
-
const where = r.fromSymbol
|
|
24
|
-
return `${
|
|
79
|
+
const where = r.fromSymbol ?? "(top level)";
|
|
80
|
+
return `${r.file}:${r.startRow + 1} ${where}`;
|
|
25
81
|
}).join("\n");
|
|
26
82
|
}
|
|
27
83
|
function formatNeighborhood(n) {
|
|
@@ -38,6 +94,32 @@ function formatNeighborhood(n) {
|
|
|
38
94
|
}
|
|
39
95
|
return lines.join("\n");
|
|
40
96
|
}
|
|
97
|
+
function formatImpact(rows) {
|
|
98
|
+
if (rows.length === 0) return "No callers \u2014 changing this is low-risk.";
|
|
99
|
+
return rows.map((r) => `[${r.distance} hop] ${symbolLine(r)}`).join("\n");
|
|
100
|
+
}
|
|
101
|
+
function formatContext(b) {
|
|
102
|
+
const lines = [`context for "${b.query}":`, "", "matches:"];
|
|
103
|
+
if (b.seeds.length === 0) lines.push(" (no symbols matched)");
|
|
104
|
+
else for (const s of b.seeds) lines.push(` ${symbolLine(s)}`);
|
|
105
|
+
if (b.related.length > 0) {
|
|
106
|
+
lines.push("", "related (ranked by call-site centrality):");
|
|
107
|
+
for (const s of b.related) lines.push(` ${symbolLine(s)}`);
|
|
108
|
+
}
|
|
109
|
+
if (b.edges.length > 0) {
|
|
110
|
+
lines.push("", "call edges:");
|
|
111
|
+
for (const e of b.edges) lines.push(` ${e.from} \u2192 ${e.to}`);
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
function formatAffected(r) {
|
|
116
|
+
if (r.tests.length === 0) {
|
|
117
|
+
return `No test files appear affected by ${r.changed.length} changed file(s).`;
|
|
118
|
+
}
|
|
119
|
+
const lines = [`${r.tests.length} test file(s) affected by your changes:`, ""];
|
|
120
|
+
for (const t of r.tests) lines.push(` ${t}`);
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
41
123
|
function formatStats(s) {
|
|
42
124
|
const kinds = Object.entries(s.byKind).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
43
125
|
const langs = Object.entries(s.byLang).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
@@ -268,6 +350,118 @@ var php = {
|
|
|
268
350
|
importRules: [{ type: "namespace_use_declaration", childTypes: ["namespace_use_clause"] }],
|
|
269
351
|
exportTypes: /* @__PURE__ */ new Set()
|
|
270
352
|
};
|
|
353
|
+
var scala = {
|
|
354
|
+
id: "scala",
|
|
355
|
+
wasm: "scala",
|
|
356
|
+
defs: {
|
|
357
|
+
class_definition: { kind: "class" },
|
|
358
|
+
object_definition: { kind: "class" },
|
|
359
|
+
trait_definition: { kind: "interface" },
|
|
360
|
+
function_definition: { kind: "method" },
|
|
361
|
+
type_definition: { kind: "type" }
|
|
362
|
+
},
|
|
363
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
364
|
+
nestedFunctionsAreMethods: false,
|
|
365
|
+
callRules: [
|
|
366
|
+
{ type: "call_expression", fnField: "function", memberTypes: ["field_expression"], memberField: "field" }
|
|
367
|
+
],
|
|
368
|
+
importRules: [{ type: "import_declaration", childTypes: ["stable_identifier", "identifier"] }],
|
|
369
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
370
|
+
};
|
|
371
|
+
var solidity = {
|
|
372
|
+
id: "solidity",
|
|
373
|
+
wasm: "solidity",
|
|
374
|
+
defs: {
|
|
375
|
+
contract_declaration: { kind: "class" },
|
|
376
|
+
interface_declaration: { kind: "interface" },
|
|
377
|
+
library_declaration: { kind: "class" },
|
|
378
|
+
function_definition: { kind: "method" },
|
|
379
|
+
modifier_definition: { kind: "function" },
|
|
380
|
+
struct_declaration: { kind: "class" },
|
|
381
|
+
enum_declaration: { kind: "enum" }
|
|
382
|
+
},
|
|
383
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
384
|
+
nestedFunctionsAreMethods: false,
|
|
385
|
+
callRules: [{ type: "call_expression", fnField: "function" }],
|
|
386
|
+
importRules: [{ type: "import_directive", field: "source" }],
|
|
387
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
388
|
+
};
|
|
389
|
+
var zig = {
|
|
390
|
+
id: "zig",
|
|
391
|
+
wasm: "zig",
|
|
392
|
+
defs: { function_declaration: { kind: "function" } },
|
|
393
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
394
|
+
nestedFunctionsAreMethods: false,
|
|
395
|
+
callRules: [{ type: "call_expression", fnField: "function" }],
|
|
396
|
+
importRules: [],
|
|
397
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
398
|
+
};
|
|
399
|
+
var kotlin = {
|
|
400
|
+
id: "kotlin",
|
|
401
|
+
wasm: "kotlin",
|
|
402
|
+
defs: {
|
|
403
|
+
class_declaration: { kind: "class", name: "first_typed", nameTypes: ["type_identifier"] },
|
|
404
|
+
object_declaration: { kind: "class", name: "first_typed", nameTypes: ["type_identifier"] },
|
|
405
|
+
function_declaration: { kind: "function", name: "first_typed", nameTypes: ["simple_identifier"] }
|
|
406
|
+
},
|
|
407
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
408
|
+
nestedFunctionsAreMethods: true,
|
|
409
|
+
callRules: [],
|
|
410
|
+
importRules: [{ type: "import_header", childTypes: ["identifier"] }],
|
|
411
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
412
|
+
};
|
|
413
|
+
var objc = {
|
|
414
|
+
id: "objc",
|
|
415
|
+
wasm: "objc",
|
|
416
|
+
defs: {
|
|
417
|
+
class_interface: { kind: "class", name: "first_typed", nameTypes: ["identifier"] },
|
|
418
|
+
class_implementation: { kind: "class", name: "first_typed", nameTypes: ["identifier"] },
|
|
419
|
+
method_declaration: { kind: "method", name: "first_typed", nameTypes: ["identifier"] },
|
|
420
|
+
method_definition: { kind: "method", name: "first_typed", nameTypes: ["identifier"] }
|
|
421
|
+
},
|
|
422
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
423
|
+
nestedFunctionsAreMethods: false,
|
|
424
|
+
callRules: [{ type: "call_expression", fnField: "function" }],
|
|
425
|
+
importRules: [{ type: "preproc_include", field: "path" }],
|
|
426
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
427
|
+
};
|
|
428
|
+
var lua = {
|
|
429
|
+
id: "lua",
|
|
430
|
+
wasm: "lua",
|
|
431
|
+
defs: {
|
|
432
|
+
function_definition_statement: { kind: "function" },
|
|
433
|
+
local_function_definition_statement: { kind: "function" }
|
|
434
|
+
},
|
|
435
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
436
|
+
nestedFunctionsAreMethods: false,
|
|
437
|
+
callRules: [],
|
|
438
|
+
importRules: [],
|
|
439
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
440
|
+
};
|
|
441
|
+
var bash = {
|
|
442
|
+
id: "bash",
|
|
443
|
+
wasm: "bash",
|
|
444
|
+
defs: { function_definition: { kind: "function" } },
|
|
445
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
446
|
+
nestedFunctionsAreMethods: false,
|
|
447
|
+
callRules: [],
|
|
448
|
+
importRules: [],
|
|
449
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
450
|
+
};
|
|
451
|
+
var ocaml = {
|
|
452
|
+
id: "ocaml",
|
|
453
|
+
wasm: "ocaml",
|
|
454
|
+
defs: {
|
|
455
|
+
let_binding: { kind: "function", name: "field", nameField: "pattern" },
|
|
456
|
+
module_definition: { kind: "class" },
|
|
457
|
+
type_definition: { kind: "type" }
|
|
458
|
+
},
|
|
459
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
460
|
+
nestedFunctionsAreMethods: false,
|
|
461
|
+
callRules: [],
|
|
462
|
+
importRules: [],
|
|
463
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
464
|
+
};
|
|
271
465
|
var LANGUAGES = {
|
|
272
466
|
typescript,
|
|
273
467
|
tsx,
|
|
@@ -280,7 +474,15 @@ var LANGUAGES = {
|
|
|
280
474
|
c,
|
|
281
475
|
cpp,
|
|
282
476
|
csharp,
|
|
283
|
-
php
|
|
477
|
+
php,
|
|
478
|
+
scala,
|
|
479
|
+
solidity,
|
|
480
|
+
zig,
|
|
481
|
+
kotlin,
|
|
482
|
+
objc,
|
|
483
|
+
lua,
|
|
484
|
+
bash,
|
|
485
|
+
ocaml
|
|
284
486
|
};
|
|
285
487
|
var EXT_TO_LANG = {
|
|
286
488
|
".ts": "typescript",
|
|
@@ -305,7 +507,19 @@ var EXT_TO_LANG = {
|
|
|
305
507
|
".hpp": "cpp",
|
|
306
508
|
".hh": "cpp",
|
|
307
509
|
".cs": "csharp",
|
|
308
|
-
".php": "php"
|
|
510
|
+
".php": "php",
|
|
511
|
+
".scala": "scala",
|
|
512
|
+
".sc": "scala",
|
|
513
|
+
".sol": "solidity",
|
|
514
|
+
".zig": "zig",
|
|
515
|
+
".kt": "kotlin",
|
|
516
|
+
".kts": "kotlin",
|
|
517
|
+
".m": "objc",
|
|
518
|
+
".lua": "lua",
|
|
519
|
+
".sh": "bash",
|
|
520
|
+
".bash": "bash",
|
|
521
|
+
".ml": "ocaml",
|
|
522
|
+
".mli": "ocaml"
|
|
309
523
|
};
|
|
310
524
|
var SUPPORTED_EXTENSIONS = Object.keys(EXT_TO_LANG);
|
|
311
525
|
function languageForPath(path) {
|
|
@@ -391,7 +605,7 @@ function classify(node, lang) {
|
|
|
391
605
|
return null;
|
|
392
606
|
}
|
|
393
607
|
function buildSymbol(node, rule, container, containerKind, lang) {
|
|
394
|
-
const name = symbolName(node, rule
|
|
608
|
+
const name = symbolName(node, rule);
|
|
395
609
|
if (!name) return null;
|
|
396
610
|
let kind = rule.kind;
|
|
397
611
|
if (kind === "function" && lang.nestedFunctionsAreMethods && containerKind === "class") {
|
|
@@ -411,9 +625,20 @@ function buildSymbol(node, rule, container, containerKind, lang) {
|
|
|
411
625
|
endByte: node.endIndex
|
|
412
626
|
};
|
|
413
627
|
}
|
|
414
|
-
function symbolName(node,
|
|
415
|
-
|
|
416
|
-
|
|
628
|
+
function symbolName(node, rule) {
|
|
629
|
+
switch (rule.name ?? "field") {
|
|
630
|
+
case "c_declarator":
|
|
631
|
+
return cDeclaratorName(node);
|
|
632
|
+
case "first_typed": {
|
|
633
|
+
const types = rule.nameTypes ?? [];
|
|
634
|
+
for (const child of node.namedChildren) {
|
|
635
|
+
if (types.includes(child.type)) return child.text;
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
default:
|
|
640
|
+
return node.childForFieldName(rule.nameField ?? "name")?.text ?? null;
|
|
641
|
+
}
|
|
417
642
|
}
|
|
418
643
|
function cDeclaratorName(node) {
|
|
419
644
|
let decl = node.childForFieldName("declarator");
|
|
@@ -629,13 +854,65 @@ function errorMessage(err) {
|
|
|
629
854
|
return err instanceof Error ? err.message : String(err);
|
|
630
855
|
}
|
|
631
856
|
|
|
857
|
+
// src/install.ts
|
|
858
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
859
|
+
import { homedir } from "os";
|
|
860
|
+
import { dirname, join } from "path";
|
|
861
|
+
var PACKAGE = "@abdulmunimjemal/codescope";
|
|
862
|
+
var MCP_SERVER_NAME = "codescope";
|
|
863
|
+
var SUPPORTED_AGENTS = ["claude", "cursor"];
|
|
864
|
+
function serverEntry(serveTarget = ".") {
|
|
865
|
+
return { command: "npx", args: ["-y", PACKAGE, "mcp", serveTarget] };
|
|
866
|
+
}
|
|
867
|
+
function configPath(agent, root, global = false) {
|
|
868
|
+
switch (agent) {
|
|
869
|
+
case "claude":
|
|
870
|
+
return global ? join(homedir(), ".mcp.json") : join(root, ".mcp.json");
|
|
871
|
+
case "cursor":
|
|
872
|
+
return global ? join(homedir(), ".cursor", "mcp.json") : join(root, ".cursor", "mcp.json");
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function readJson(path) {
|
|
876
|
+
if (!existsSync(path)) return {};
|
|
877
|
+
try {
|
|
878
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
879
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
880
|
+
} catch {
|
|
881
|
+
return {};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function installInto(agent, root, opts = {}) {
|
|
885
|
+
const path = configPath(agent, root, opts.global ?? false);
|
|
886
|
+
const config = readJson(path);
|
|
887
|
+
const existing = config.mcpServers;
|
|
888
|
+
const servers = existing && typeof existing === "object" ? existing : {};
|
|
889
|
+
const existed = Object.prototype.hasOwnProperty.call(servers, MCP_SERVER_NAME);
|
|
890
|
+
servers[MCP_SERVER_NAME] = serverEntry(opts.serveTarget);
|
|
891
|
+
config.mcpServers = servers;
|
|
892
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
893
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
894
|
+
`);
|
|
895
|
+
return { agent, path, action: existed ? "updated" : "added" };
|
|
896
|
+
}
|
|
897
|
+
function install(root, opts = {}) {
|
|
898
|
+
const agents = opts.agents ?? [...SUPPORTED_AGENTS];
|
|
899
|
+
return agents.map((agent) => installInto(agent, root, opts));
|
|
900
|
+
}
|
|
901
|
+
function codexSnippet(serveTarget = ".") {
|
|
902
|
+
return [
|
|
903
|
+
"[mcp_servers.codescope]",
|
|
904
|
+
'command = "npx"',
|
|
905
|
+
`args = ["-y", "${PACKAGE}", "mcp", "${serveTarget}"]`
|
|
906
|
+
].join("\n");
|
|
907
|
+
}
|
|
908
|
+
|
|
632
909
|
// src/mcp.ts
|
|
633
910
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
634
911
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
635
912
|
import { z } from "zod";
|
|
636
913
|
|
|
637
914
|
// src/version.ts
|
|
638
|
-
var VERSION = "0.
|
|
915
|
+
var VERSION = "0.3.0";
|
|
639
916
|
|
|
640
917
|
// src/mcp.ts
|
|
641
918
|
var KIND = z.enum(["function", "method", "class", "interface", "type", "enum", "variable"]);
|
|
@@ -681,6 +958,43 @@ function createServer(store) {
|
|
|
681
958
|
},
|
|
682
959
|
async ({ name, limit }) => textResult(formatRefs(store.findCallers(name, { limit })))
|
|
683
960
|
);
|
|
961
|
+
server.registerTool(
|
|
962
|
+
"find_callees",
|
|
963
|
+
{
|
|
964
|
+
title: "Find callees",
|
|
965
|
+
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.",
|
|
966
|
+
inputSchema: {
|
|
967
|
+
name: z.string().describe("the calling function/method name"),
|
|
968
|
+
limit: z.number().int().positive().max(500).optional()
|
|
969
|
+
}
|
|
970
|
+
},
|
|
971
|
+
async ({ name, limit }) => textResult(formatSymbols(store.findCallees(name, { limit })))
|
|
972
|
+
);
|
|
973
|
+
server.registerTool(
|
|
974
|
+
"impact",
|
|
975
|
+
{
|
|
976
|
+
title: "Change impact / blast radius",
|
|
977
|
+
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.",
|
|
978
|
+
inputSchema: {
|
|
979
|
+
name: z.string(),
|
|
980
|
+
depth: z.number().int().min(1).max(6).optional().describe("hops to expand (default 3)"),
|
|
981
|
+
limit: z.number().int().positive().max(300).optional()
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
async ({ name, depth, limit }) => textResult(formatImpact(store.impact(name, { depth, limit })))
|
|
985
|
+
);
|
|
986
|
+
server.registerTool(
|
|
987
|
+
"context",
|
|
988
|
+
{
|
|
989
|
+
title: "Build task context",
|
|
990
|
+
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.",
|
|
991
|
+
inputSchema: {
|
|
992
|
+
query: z.string().describe("a task description or symbol/feature name"),
|
|
993
|
+
maxSymbols: z.number().int().min(5).max(100).optional().describe("cap on symbols (default 30)")
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
async ({ query, maxSymbols }) => textResult(formatContext(store.context(query, { maxSymbols })))
|
|
997
|
+
);
|
|
684
998
|
server.registerTool(
|
|
685
999
|
"find_references",
|
|
686
1000
|
{
|
|
@@ -718,6 +1032,18 @@ function createServer(store) {
|
|
|
718
1032
|
},
|
|
719
1033
|
async ({ name, depth, limit }) => textResult(formatNeighborhood(store.neighborhood(name, { depth, limit })))
|
|
720
1034
|
);
|
|
1035
|
+
server.registerTool(
|
|
1036
|
+
"affected",
|
|
1037
|
+
{
|
|
1038
|
+
title: "Affected tests",
|
|
1039
|
+
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.",
|
|
1040
|
+
inputSchema: {
|
|
1041
|
+
files: z.array(z.string()).describe("repo-relative paths of the changed files"),
|
|
1042
|
+
depth: z.number().int().min(1).max(6).optional()
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
async ({ files, depth }) => textResult(formatAffected(affected(store, files, { depth })))
|
|
1046
|
+
);
|
|
721
1047
|
server.registerTool(
|
|
722
1048
|
"stats",
|
|
723
1049
|
{
|
|
@@ -911,15 +1237,119 @@ var GraphStore = class {
|
|
|
911
1237
|
WHERE s.name = ? ORDER BY s.exported DESC, f.path LIMIT ?`
|
|
912
1238
|
).all(name, clampLimit(opts.limit)).map(toSymbolRow);
|
|
913
1239
|
}
|
|
914
|
-
/**
|
|
1240
|
+
/**
|
|
1241
|
+
* Distinct callers of a name (both bare `foo()` and `x.foo()`). Multiple call
|
|
1242
|
+
* sites from the same caller in the same file collapse to one row — fewer
|
|
1243
|
+
* tokens and a more useful "who depends on this" answer.
|
|
1244
|
+
*/
|
|
915
1245
|
findCallers(name, opts = {}) {
|
|
916
1246
|
return this.db.prepare(
|
|
917
|
-
`SELECT r.id, f.path AS file, r.from_symbol, r.name,
|
|
1247
|
+
`SELECT MIN(r.id) AS id, f.path AS file, r.from_symbol, r.name,
|
|
1248
|
+
MIN(r.kind) AS kind, MIN(r.start_row) AS start_row, MIN(r.start_col) AS start_col
|
|
918
1249
|
FROM refs r JOIN files f ON f.id = r.file_id
|
|
919
1250
|
WHERE r.name = ? AND r.kind IN ('call', 'method')
|
|
920
|
-
|
|
1251
|
+
GROUP BY f.path, r.from_symbol
|
|
1252
|
+
ORDER BY f.path, start_row LIMIT ?`
|
|
921
1253
|
).all(name, clampLimit(opts.limit)).map(toRefRow);
|
|
922
1254
|
}
|
|
1255
|
+
/** The definitions that a symbol calls, resolved kind-aware to project symbols. */
|
|
1256
|
+
findCallees(name, opts = {}) {
|
|
1257
|
+
const limit = clampLimit(opts.limit);
|
|
1258
|
+
const out = [];
|
|
1259
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1260
|
+
for (const callee of this.calleesOf(name)) {
|
|
1261
|
+
const defs = this.resolveCallee(callee.name, callee.kind, 6);
|
|
1262
|
+
if (!defs) continue;
|
|
1263
|
+
for (const d of defs) {
|
|
1264
|
+
const key = `${d.name}@${d.file}:${d.startRow}`;
|
|
1265
|
+
if (!seen.has(key)) {
|
|
1266
|
+
seen.add(key);
|
|
1267
|
+
out.push(d);
|
|
1268
|
+
if (out.length >= limit) return out;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return out;
|
|
1273
|
+
}
|
|
1274
|
+
/** How many call sites reference this name (popularity / centrality signal). */
|
|
1275
|
+
callerCount(name) {
|
|
1276
|
+
return this.db.prepare(
|
|
1277
|
+
"SELECT COUNT(*) AS n FROM refs WHERE name = ? AND kind IN ('call','method')"
|
|
1278
|
+
).get(name)?.n ?? 0;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* The blast radius of changing a symbol: its transitive callers, breadth-first,
|
|
1282
|
+
* annotated with hop distance and ordered nearest-first. Answers "what could
|
|
1283
|
+
* break if I change this?" without reading the codebase.
|
|
1284
|
+
*/
|
|
1285
|
+
impact(name, opts = {}) {
|
|
1286
|
+
const depth = Math.max(1, Math.min(opts.depth ?? 3, 6));
|
|
1287
|
+
const limit = clampLimit(opts.limit, 300);
|
|
1288
|
+
const distance = /* @__PURE__ */ new Map([[name, 0]]);
|
|
1289
|
+
let frontier = [name];
|
|
1290
|
+
for (let d = 0; d < depth && frontier.length > 0 && distance.size < limit; d++) {
|
|
1291
|
+
const next = [];
|
|
1292
|
+
for (const node of frontier) {
|
|
1293
|
+
for (const caller of this.callersOf(node)) {
|
|
1294
|
+
if (!distance.has(caller)) {
|
|
1295
|
+
distance.set(caller, d + 1);
|
|
1296
|
+
next.push(caller);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
frontier = next;
|
|
1301
|
+
}
|
|
1302
|
+
const out = [];
|
|
1303
|
+
for (const [n, dist] of distance) {
|
|
1304
|
+
if (dist === 0) continue;
|
|
1305
|
+
for (const def of this.getSymbol(n, { limit: 3 })) out.push({ ...def, distance: dist });
|
|
1306
|
+
}
|
|
1307
|
+
out.sort((a, b) => a.distance - b.distance || a.file.localeCompare(b.file));
|
|
1308
|
+
return out.slice(0, limit);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* A token-budgeted relevance map for a task: the symbols matching `query` plus
|
|
1312
|
+
* their immediate call neighbourhood, ranked by call-site centrality, capped at
|
|
1313
|
+
* `maxSymbols`. This is the slice of the codebase an agent needs to start a
|
|
1314
|
+
* change — delivered as graph facts, not file dumps.
|
|
1315
|
+
*/
|
|
1316
|
+
context(query, opts = {}) {
|
|
1317
|
+
const maxSymbols = Math.max(5, Math.min(opts.maxSymbols ?? 30, 100));
|
|
1318
|
+
const seeds = this.searchSymbols(query, { limit: Math.min(8, maxSymbols) });
|
|
1319
|
+
const picked = /* @__PURE__ */ new Map();
|
|
1320
|
+
const key = (s) => `${s.name}@${s.file}:${s.startRow}`;
|
|
1321
|
+
for (const s of seeds) picked.set(key(s), s);
|
|
1322
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
1323
|
+
const edges = [];
|
|
1324
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
1325
|
+
for (const seed of seeds) {
|
|
1326
|
+
for (const callee of this.findCallees(seed.name, { limit: 15 })) {
|
|
1327
|
+
addEdge(edges, edgeKeys, seed.name, callee.name);
|
|
1328
|
+
const k = key(callee);
|
|
1329
|
+
if (!picked.has(k) && !candidates.has(k)) {
|
|
1330
|
+
candidates.set(k, { row: callee, score: this.callerCount(callee.name) });
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
for (const caller of this.findCallers(seed.name, { limit: 15 })) {
|
|
1334
|
+
if (!caller.fromSymbol) continue;
|
|
1335
|
+
addEdge(edges, edgeKeys, caller.fromSymbol, seed.name);
|
|
1336
|
+
for (const def of this.getSymbol(caller.fromSymbol, { limit: 1 })) {
|
|
1337
|
+
const k = key(def);
|
|
1338
|
+
if (!picked.has(k) && !candidates.has(k)) {
|
|
1339
|
+
candidates.set(k, { row: def, score: this.callerCount(def.name) });
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
const related = [...candidates.values()].sort((a, b) => b.score - a.score).slice(0, Math.max(0, maxSymbols - picked.size)).map((c2) => c2.row);
|
|
1345
|
+
const keptNames = new Set([...seeds, ...related].map((s) => s.name));
|
|
1346
|
+
return {
|
|
1347
|
+
query,
|
|
1348
|
+
seeds,
|
|
1349
|
+
related,
|
|
1350
|
+
edges: edges.filter((e) => keptNames.has(e.from) && keptNames.has(e.to))
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
923
1353
|
/** All references (calls + imports) to a name. */
|
|
924
1354
|
findReferences(name, opts = {}) {
|
|
925
1355
|
const limit = clampLimit(opts.limit);
|
|
@@ -934,6 +1364,18 @@ var GraphStore = class {
|
|
|
934
1364
|
).all(name, limit);
|
|
935
1365
|
return rows.map(toRefRow);
|
|
936
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Files whose imports reference a module basename (e.g. `store` for
|
|
1369
|
+
* `src/store.ts`). Matches `import … from "./store"`, `"../src/store.js"`,
|
|
1370
|
+
* etc. Used by affected-test analysis to follow import edges, which — unlike
|
|
1371
|
+
* call edges — reliably reach test files (tests import the module under test).
|
|
1372
|
+
*/
|
|
1373
|
+
findImporters(moduleBasename2) {
|
|
1374
|
+
return this.db.prepare(
|
|
1375
|
+
`SELECT DISTINCT f.path FROM refs r JOIN files f ON f.id = r.file_id
|
|
1376
|
+
WHERE r.kind = 'import' AND (r.name = ? OR r.name LIKE ? OR r.name LIKE ?)`
|
|
1377
|
+
).all(moduleBasename2, `%/${moduleBasename2}`, `%/${moduleBasename2}.%`).map((r) => r.path);
|
|
1378
|
+
}
|
|
937
1379
|
/** The symbols defined in a file, in source order. */
|
|
938
1380
|
fileOutline(path) {
|
|
939
1381
|
return this.db.prepare(
|
|
@@ -1122,6 +1564,7 @@ Usage:
|
|
|
1122
1564
|
codescope <command> [path] [options]
|
|
1123
1565
|
|
|
1124
1566
|
Commands:
|
|
1567
|
+
install [path] Wire codescope into your agents (Claude Code, Cursor) automatically.
|
|
1125
1568
|
mcp [path] Index, watch for changes, and serve the graph over MCP (stdio).
|
|
1126
1569
|
This is what you wire into Claude Code / Cursor / Codex.
|
|
1127
1570
|
index [path] Build (or refresh) the on-disk graph and print stats.
|
|
@@ -1130,9 +1573,15 @@ Commands:
|
|
|
1130
1573
|
search <query> [path] Fuzzy-search symbol names.
|
|
1131
1574
|
get <name> [path] Look up a definition by exact name.
|
|
1132
1575
|
callers <name> [path] List callers of a function/method.
|
|
1576
|
+
callees <name> [path] List what a function/method calls.
|
|
1577
|
+
impact <name> [path] Transitive callers (blast radius) of a symbol.
|
|
1578
|
+
context <query> [path] Ranked relevance map for a task (matches + neighbours).
|
|
1579
|
+
affected <files...> Test files affected by a set of changed files.
|
|
1133
1580
|
neighborhood <name> Show the call neighbourhood around a symbol.
|
|
1134
1581
|
|
|
1135
1582
|
Options:
|
|
1583
|
+
--agent <name> install target: claude | cursor | all (default all).
|
|
1584
|
+
--global install: write to your home dir instead of the project.
|
|
1136
1585
|
--path <dir> Repository root (default: current directory or the positional path).
|
|
1137
1586
|
--db <file> SQLite graph location (default: <root>/.codescope/graph.db).
|
|
1138
1587
|
--memory Use an in-memory graph (not persisted).
|
|
@@ -1149,13 +1598,19 @@ Examples:
|
|
|
1149
1598
|
codescope mcp . # add this command to your agent's MCP config
|
|
1150
1599
|
`;
|
|
1151
1600
|
function parseArgs(argv) {
|
|
1152
|
-
const flags = { positional: [], memory: false };
|
|
1601
|
+
const flags = { positional: [], memory: false, global: false };
|
|
1153
1602
|
for (let i = 0; i < argv.length; i++) {
|
|
1154
1603
|
const arg = argv[i];
|
|
1155
1604
|
switch (arg) {
|
|
1156
1605
|
case "--memory":
|
|
1157
1606
|
flags.memory = true;
|
|
1158
1607
|
break;
|
|
1608
|
+
case "--global":
|
|
1609
|
+
flags.global = true;
|
|
1610
|
+
break;
|
|
1611
|
+
case "--agent":
|
|
1612
|
+
flags.agent = argv[++i];
|
|
1613
|
+
break;
|
|
1159
1614
|
case "--path":
|
|
1160
1615
|
flags.path = argv[++i];
|
|
1161
1616
|
break;
|
|
@@ -1183,7 +1638,7 @@ function rootDir(flags, positionalRootIndex = 0) {
|
|
|
1183
1638
|
function openStore(root, flags) {
|
|
1184
1639
|
if (flags.memory) return new GraphStore(":memory:");
|
|
1185
1640
|
const dir = resolve2(root, ".codescope");
|
|
1186
|
-
|
|
1641
|
+
mkdirSync2(dir, { recursive: true });
|
|
1187
1642
|
return new GraphStore(flags.db ?? resolve2(dir, "graph.db"));
|
|
1188
1643
|
}
|
|
1189
1644
|
async function ensureIndexed(indexer, store) {
|
|
@@ -1236,6 +1691,15 @@ async function cmdQuery(command, root, flags) {
|
|
|
1236
1691
|
case "callers":
|
|
1237
1692
|
out = formatRefs(store.findCallers(term, { limit: flags.limit }));
|
|
1238
1693
|
break;
|
|
1694
|
+
case "callees":
|
|
1695
|
+
out = formatSymbols(store.findCallees(term, { limit: flags.limit }));
|
|
1696
|
+
break;
|
|
1697
|
+
case "impact":
|
|
1698
|
+
out = formatImpact(store.impact(term, { depth: flags.depth, limit: flags.limit }));
|
|
1699
|
+
break;
|
|
1700
|
+
case "context":
|
|
1701
|
+
out = formatContext(store.context(term, { maxSymbols: flags.limit }));
|
|
1702
|
+
break;
|
|
1239
1703
|
case "neighborhood":
|
|
1240
1704
|
out = formatNeighborhood(store.neighborhood(term, { depth: flags.depth, limit: flags.limit }));
|
|
1241
1705
|
break;
|
|
@@ -1246,6 +1710,32 @@ async function cmdQuery(command, root, flags) {
|
|
|
1246
1710
|
`);
|
|
1247
1711
|
store.close();
|
|
1248
1712
|
}
|
|
1713
|
+
async function cmdAffected(root, flags) {
|
|
1714
|
+
const files = flags.positional;
|
|
1715
|
+
if (files.length === 0) fail("'affected' needs one or more changed file paths. See --help.");
|
|
1716
|
+
const store = openStore(root, flags);
|
|
1717
|
+
const indexer = new Indexer(store, root);
|
|
1718
|
+
await ensureIndexed(indexer, store);
|
|
1719
|
+
const rels = files.map((f) => indexer.rel(resolve2(root, f)));
|
|
1720
|
+
process.stdout.write(`${formatAffected(affected(store, rels, { depth: flags.depth }))}
|
|
1721
|
+
`);
|
|
1722
|
+
store.close();
|
|
1723
|
+
}
|
|
1724
|
+
function cmdInstall(root, flags) {
|
|
1725
|
+
const agents = !flags.agent || flags.agent === "all" ? [...SUPPORTED_AGENTS] : SUPPORTED_AGENTS.includes(flags.agent) ? [flags.agent] : fail(`unknown --agent '${flags.agent}'. Use: ${SUPPORTED_AGENTS.join(", ")}, or all.`);
|
|
1726
|
+
const results = install(root, { agents, global: flags.global });
|
|
1727
|
+
for (const r of results) {
|
|
1728
|
+
process.stdout.write(`${pc.green("\u2713")} ${r.action} codescope for ${pc.bold(r.agent)} \u2192 ${r.path}
|
|
1729
|
+
`);
|
|
1730
|
+
}
|
|
1731
|
+
process.stdout.write(
|
|
1732
|
+
`
|
|
1733
|
+
${pc.dim("Restart your agent to pick up the change.")}
|
|
1734
|
+
${pc.dim("Codex (TOML config) \u2014 add this to ~/.codex/config.toml:")}
|
|
1735
|
+
${codexSnippet()}
|
|
1736
|
+
`
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1249
1739
|
async function cmdWatch(root, flags) {
|
|
1250
1740
|
const store = openStore(root, flags);
|
|
1251
1741
|
const indexer = new Indexer(store, root);
|
|
@@ -1307,8 +1797,15 @@ async function main() {
|
|
|
1307
1797
|
case "search":
|
|
1308
1798
|
case "get":
|
|
1309
1799
|
case "callers":
|
|
1800
|
+
case "callees":
|
|
1801
|
+
case "impact":
|
|
1802
|
+
case "context":
|
|
1310
1803
|
case "neighborhood":
|
|
1311
1804
|
return cmdQuery(command, resolve2(flags.path ?? flags.positional[1] ?? "."), flags);
|
|
1805
|
+
case "affected":
|
|
1806
|
+
return cmdAffected(resolve2(flags.path ?? "."), flags);
|
|
1807
|
+
case "install":
|
|
1808
|
+
return cmdInstall(rootDir(flags), flags);
|
|
1312
1809
|
case "watch":
|
|
1313
1810
|
return cmdWatch(rootDir(flags), flags);
|
|
1314
1811
|
case "mcp":
|