@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/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
- /** Symbols that call a given name (both bare `foo()` and `x.foo()`). */
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, r.kind, r.start_row, r.start_col
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
- ORDER BY f.path, r.start_row LIMIT ?`
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.name ?? "field");
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, strategy) {
726
- if (strategy === "c_declarator") return cDeclaratorName(node);
727
- return node.childForFieldName("name")?.text ?? null;
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
- const sig = s.signature ? ` \xB7 ${s.signature}` : "";
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 ? `${r.fromSymbol}` : "(top level)";
995
- return `${where} \u2192 ${r.name} [${r.kind}] \u2014 ${r.file}:${r.startRow + 1}`;
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.1.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