@abdulmunimjemal/codescope 0.2.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 +25 -9
- package/dist/cli.js +329 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +64 -5
- package/dist/index.js +295 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,12 +36,12 @@ incumbent and shares codescope's architecture. In a **measured head-to-head**
|
|
|
36
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
37
|
- **3–5× smaller index** on disk (2.5 MB vs 8.2 MB; 22.9 MB vs 112.8 MB).
|
|
38
38
|
- **Fewer tokens per answer** for definition lookups on every repo tested; callers is ≈parity.
|
|
39
|
-
- Feature parity
|
|
39
|
+
- **Feature parity:** `callers`, `callees`, `impact`, `context`, **`affected`** (test-impact), and **`install`** (agent auto-wiring) — plus **20 languages**.
|
|
40
40
|
|
|
41
|
-
codegraph still leads on **
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
codegraph still leads on **maturity & adoption** (35k★, a real user base) and
|
|
42
|
+
indexes a few extra node kinds (constants, properties, routes). Pick codescope
|
|
43
|
+
when footprint, index speed, and token cost matter; pick codegraph for the most
|
|
44
|
+
battle-tested option.
|
|
45
45
|
|
|
46
46
|
## Install
|
|
47
47
|
|
|
@@ -55,11 +55,20 @@ collides with an existing package; the installed command is still `codescope`.)
|
|
|
55
55
|
|
|
56
56
|
## Quick start
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
watching for changes, and serves the graph over stdio:
|
|
58
|
+
The one-liner — wire codescope into your agents automatically, from your repo:
|
|
60
59
|
|
|
61
60
|
```bash
|
|
62
|
-
codescope
|
|
61
|
+
npx @abdulmunimjemal/codescope install # adds codescope to Claude Code + Cursor
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That writes the MCP server config (non-destructively) so your agent launches
|
|
65
|
+
codescope on the repo. Restart the agent and you're done. `--agent claude|cursor`
|
|
66
|
+
targets one; `--global` writes to your home dir instead of the project.
|
|
67
|
+
|
|
68
|
+
Prefer to wire it by hand? The server command is:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
codescope mcp /path/to/your/repo # index, watch, and serve over stdio
|
|
63
72
|
```
|
|
64
73
|
|
|
65
74
|
**Claude Code** (`.mcp.json` or `claude mcp add`):
|
|
@@ -82,6 +91,10 @@ codescope index . # build the graph, print stats
|
|
|
82
91
|
codescope search useState # fuzzy symbol search
|
|
83
92
|
codescope get GraphStore # jump to a definition
|
|
84
93
|
codescope callers parseSource # who calls this
|
|
94
|
+
codescope callees indexAll # what it calls
|
|
95
|
+
codescope impact GraphStore # blast radius before a change
|
|
96
|
+
codescope context "auth flow" # ranked relevance map for a task
|
|
97
|
+
codescope affected src/store.ts # which tests are affected by a change
|
|
85
98
|
codescope neighborhood handleRequest --depth 3
|
|
86
99
|
codescope watch . # keep the graph fresh, log updates
|
|
87
100
|
```
|
|
@@ -127,7 +140,10 @@ The index lives in `.codescope/graph.db` (add `.codescope/` to your
|
|
|
127
140
|
|
|
128
141
|
## Languages
|
|
129
142
|
|
|
130
|
-
TypeScript, JavaScript, TSX/JSX, Python, Go, Rust, Java, Ruby, C,
|
|
143
|
+
20 languages: TypeScript, JavaScript, TSX/JSX, Python, Go, Rust, Java, Ruby, C,
|
|
144
|
+
C++, C#, PHP, Scala, Solidity, Zig, Kotlin, Objective-C, Lua, Bash, and OCaml.
|
|
145
|
+
Definition extraction (functions, classes, methods, …) works for all; call and
|
|
146
|
+
import edges are available for the languages whose grammars expose them.
|
|
131
147
|
|
|
132
148
|
## Programmatic API
|
|
133
149
|
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,62 @@
|
|
|
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
|
|
9
61
|
function shortSig(sig) {
|
|
10
62
|
if (!sig) return "";
|
|
@@ -60,6 +112,14 @@ function formatContext(b) {
|
|
|
60
112
|
}
|
|
61
113
|
return lines.join("\n");
|
|
62
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
|
+
}
|
|
63
123
|
function formatStats(s) {
|
|
64
124
|
const kinds = Object.entries(s.byKind).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
65
125
|
const langs = Object.entries(s.byLang).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
@@ -290,6 +350,118 @@ var php = {
|
|
|
290
350
|
importRules: [{ type: "namespace_use_declaration", childTypes: ["namespace_use_clause"] }],
|
|
291
351
|
exportTypes: /* @__PURE__ */ new Set()
|
|
292
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
|
+
};
|
|
293
465
|
var LANGUAGES = {
|
|
294
466
|
typescript,
|
|
295
467
|
tsx,
|
|
@@ -302,7 +474,15 @@ var LANGUAGES = {
|
|
|
302
474
|
c,
|
|
303
475
|
cpp,
|
|
304
476
|
csharp,
|
|
305
|
-
php
|
|
477
|
+
php,
|
|
478
|
+
scala,
|
|
479
|
+
solidity,
|
|
480
|
+
zig,
|
|
481
|
+
kotlin,
|
|
482
|
+
objc,
|
|
483
|
+
lua,
|
|
484
|
+
bash,
|
|
485
|
+
ocaml
|
|
306
486
|
};
|
|
307
487
|
var EXT_TO_LANG = {
|
|
308
488
|
".ts": "typescript",
|
|
@@ -327,7 +507,19 @@ var EXT_TO_LANG = {
|
|
|
327
507
|
".hpp": "cpp",
|
|
328
508
|
".hh": "cpp",
|
|
329
509
|
".cs": "csharp",
|
|
330
|
-
".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"
|
|
331
523
|
};
|
|
332
524
|
var SUPPORTED_EXTENSIONS = Object.keys(EXT_TO_LANG);
|
|
333
525
|
function languageForPath(path) {
|
|
@@ -413,7 +605,7 @@ function classify(node, lang) {
|
|
|
413
605
|
return null;
|
|
414
606
|
}
|
|
415
607
|
function buildSymbol(node, rule, container, containerKind, lang) {
|
|
416
|
-
const name = symbolName(node, rule
|
|
608
|
+
const name = symbolName(node, rule);
|
|
417
609
|
if (!name) return null;
|
|
418
610
|
let kind = rule.kind;
|
|
419
611
|
if (kind === "function" && lang.nestedFunctionsAreMethods && containerKind === "class") {
|
|
@@ -433,9 +625,20 @@ function buildSymbol(node, rule, container, containerKind, lang) {
|
|
|
433
625
|
endByte: node.endIndex
|
|
434
626
|
};
|
|
435
627
|
}
|
|
436
|
-
function symbolName(node,
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
}
|
|
439
642
|
}
|
|
440
643
|
function cDeclaratorName(node) {
|
|
441
644
|
let decl = node.childForFieldName("declarator");
|
|
@@ -651,13 +854,65 @@ function errorMessage(err) {
|
|
|
651
854
|
return err instanceof Error ? err.message : String(err);
|
|
652
855
|
}
|
|
653
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
|
+
|
|
654
909
|
// src/mcp.ts
|
|
655
910
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
656
911
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
657
912
|
import { z } from "zod";
|
|
658
913
|
|
|
659
914
|
// src/version.ts
|
|
660
|
-
var VERSION = "0.
|
|
915
|
+
var VERSION = "0.3.0";
|
|
661
916
|
|
|
662
917
|
// src/mcp.ts
|
|
663
918
|
var KIND = z.enum(["function", "method", "class", "interface", "type", "enum", "variable"]);
|
|
@@ -777,6 +1032,18 @@ function createServer(store) {
|
|
|
777
1032
|
},
|
|
778
1033
|
async ({ name, depth, limit }) => textResult(formatNeighborhood(store.neighborhood(name, { depth, limit })))
|
|
779
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
|
+
);
|
|
780
1047
|
server.registerTool(
|
|
781
1048
|
"stats",
|
|
782
1049
|
{
|
|
@@ -1097,6 +1364,18 @@ var GraphStore = class {
|
|
|
1097
1364
|
).all(name, limit);
|
|
1098
1365
|
return rows.map(toRefRow);
|
|
1099
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
|
+
}
|
|
1100
1379
|
/** The symbols defined in a file, in source order. */
|
|
1101
1380
|
fileOutline(path) {
|
|
1102
1381
|
return this.db.prepare(
|
|
@@ -1285,6 +1564,7 @@ Usage:
|
|
|
1285
1564
|
codescope <command> [path] [options]
|
|
1286
1565
|
|
|
1287
1566
|
Commands:
|
|
1567
|
+
install [path] Wire codescope into your agents (Claude Code, Cursor) automatically.
|
|
1288
1568
|
mcp [path] Index, watch for changes, and serve the graph over MCP (stdio).
|
|
1289
1569
|
This is what you wire into Claude Code / Cursor / Codex.
|
|
1290
1570
|
index [path] Build (or refresh) the on-disk graph and print stats.
|
|
@@ -1296,9 +1576,12 @@ Commands:
|
|
|
1296
1576
|
callees <name> [path] List what a function/method calls.
|
|
1297
1577
|
impact <name> [path] Transitive callers (blast radius) of a symbol.
|
|
1298
1578
|
context <query> [path] Ranked relevance map for a task (matches + neighbours).
|
|
1579
|
+
affected <files...> Test files affected by a set of changed files.
|
|
1299
1580
|
neighborhood <name> Show the call neighbourhood around a symbol.
|
|
1300
1581
|
|
|
1301
1582
|
Options:
|
|
1583
|
+
--agent <name> install target: claude | cursor | all (default all).
|
|
1584
|
+
--global install: write to your home dir instead of the project.
|
|
1302
1585
|
--path <dir> Repository root (default: current directory or the positional path).
|
|
1303
1586
|
--db <file> SQLite graph location (default: <root>/.codescope/graph.db).
|
|
1304
1587
|
--memory Use an in-memory graph (not persisted).
|
|
@@ -1315,13 +1598,19 @@ Examples:
|
|
|
1315
1598
|
codescope mcp . # add this command to your agent's MCP config
|
|
1316
1599
|
`;
|
|
1317
1600
|
function parseArgs(argv) {
|
|
1318
|
-
const flags = { positional: [], memory: false };
|
|
1601
|
+
const flags = { positional: [], memory: false, global: false };
|
|
1319
1602
|
for (let i = 0; i < argv.length; i++) {
|
|
1320
1603
|
const arg = argv[i];
|
|
1321
1604
|
switch (arg) {
|
|
1322
1605
|
case "--memory":
|
|
1323
1606
|
flags.memory = true;
|
|
1324
1607
|
break;
|
|
1608
|
+
case "--global":
|
|
1609
|
+
flags.global = true;
|
|
1610
|
+
break;
|
|
1611
|
+
case "--agent":
|
|
1612
|
+
flags.agent = argv[++i];
|
|
1613
|
+
break;
|
|
1325
1614
|
case "--path":
|
|
1326
1615
|
flags.path = argv[++i];
|
|
1327
1616
|
break;
|
|
@@ -1349,7 +1638,7 @@ function rootDir(flags, positionalRootIndex = 0) {
|
|
|
1349
1638
|
function openStore(root, flags) {
|
|
1350
1639
|
if (flags.memory) return new GraphStore(":memory:");
|
|
1351
1640
|
const dir = resolve2(root, ".codescope");
|
|
1352
|
-
|
|
1641
|
+
mkdirSync2(dir, { recursive: true });
|
|
1353
1642
|
return new GraphStore(flags.db ?? resolve2(dir, "graph.db"));
|
|
1354
1643
|
}
|
|
1355
1644
|
async function ensureIndexed(indexer, store) {
|
|
@@ -1421,6 +1710,32 @@ async function cmdQuery(command, root, flags) {
|
|
|
1421
1710
|
`);
|
|
1422
1711
|
store.close();
|
|
1423
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
|
+
}
|
|
1424
1739
|
async function cmdWatch(root, flags) {
|
|
1425
1740
|
const store = openStore(root, flags);
|
|
1426
1741
|
const indexer = new Indexer(store, root);
|
|
@@ -1487,6 +1802,10 @@ async function main() {
|
|
|
1487
1802
|
case "context":
|
|
1488
1803
|
case "neighborhood":
|
|
1489
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);
|
|
1490
1809
|
case "watch":
|
|
1491
1810
|
return cmdWatch(rootDir(flags), flags);
|
|
1492
1811
|
case "mcp":
|