@wrongstack/tools 0.298.3 → 0.299.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.
@@ -2602,6 +2602,136 @@ function mapWriterRefRow(row) {
2602
2602
  }
2603
2603
 
2604
2604
  // src/codebase-index/writer-graph-reader.ts
2605
+ var MAX_SQL_VARS = 900;
2606
+ function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
2607
+ const results = [];
2608
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
2609
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
2610
+ const placeholders = chunk.map(() => "?").join(",");
2611
+ const sql = buildSql(placeholders);
2612
+ results.push(...stmt(sql).all(...chunk, ...extraArgs));
2613
+ }
2614
+ return results;
2615
+ }
2616
+ function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
2617
+ let total = 0;
2618
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
2619
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
2620
+ const placeholders = chunk.map(() => "?").join(",");
2621
+ const sql = buildSql(placeholders);
2622
+ const rows = stmt(sql).all(...chunk, ...extraArgs);
2623
+ total += rows[0]?.n ?? 0;
2624
+ }
2625
+ return total;
2626
+ }
2627
+ function mapCallSiteRow(row) {
2628
+ return {
2629
+ symbol: {
2630
+ id: row.sym_id,
2631
+ name: row.sym_name,
2632
+ kind: row.sym_kind,
2633
+ lang: row.sym_lang,
2634
+ file: row.sym_file,
2635
+ line: row.sym_line,
2636
+ signature: row.sym_signature
2637
+ },
2638
+ callType: row.call_type,
2639
+ line: row.ref_line
2640
+ };
2641
+ }
2642
+ function resolveSymbolIds(stmt, symbolName, file) {
2643
+ const baseSql = file ? `SELECT id FROM symbols WHERE name = ? AND file = ? ORDER BY id` : `SELECT id FROM symbols WHERE name = ? ORDER BY id`;
2644
+ const args = file ? [symbolName, file] : [symbolName];
2645
+ const rows = stmt(baseSql).all(...args);
2646
+ return rows.map((r) => r.id);
2647
+ }
2648
+ function findIncomingCallsByName(stmt, symbolName, file, limit) {
2649
+ const targetIds = resolveSymbolIds(stmt, symbolName, file);
2650
+ if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
2651
+ let matchIds = targetIds;
2652
+ let ambiguous = false;
2653
+ if (file !== void 0) {
2654
+ const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
2655
+ if (allNamedIds.length > targetIds.length) {
2656
+ matchIds = allNamedIds;
2657
+ ambiguous = true;
2658
+ }
2659
+ }
2660
+ const useFallback = !file;
2661
+ const rows = chunkedIdQuery(
2662
+ stmt,
2663
+ matchIds,
2664
+ (ph) => `SELECT
2665
+ s.id AS sym_id,
2666
+ s.name AS sym_name,
2667
+ s.kind AS sym_kind,
2668
+ s.lang AS sym_lang,
2669
+ s.file AS sym_file,
2670
+ s.line AS sym_line,
2671
+ s.signature AS sym_signature,
2672
+ r.call_type,
2673
+ r.line AS ref_line
2674
+ FROM refs r
2675
+ JOIN symbols s ON s.id = r.from_id
2676
+ WHERE r.to_id IN (${ph})
2677
+ ORDER BY r.line, r.id`,
2678
+ []
2679
+ );
2680
+ if (useFallback) {
2681
+ const fallbackRows = stmt(
2682
+ `SELECT
2683
+ s.id AS sym_id,
2684
+ s.name AS sym_name,
2685
+ s.kind AS sym_kind,
2686
+ s.lang AS sym_lang,
2687
+ s.file AS sym_file,
2688
+ s.line AS sym_line,
2689
+ s.signature AS sym_signature,
2690
+ r.call_type,
2691
+ r.line AS ref_line
2692
+ FROM refs r
2693
+ JOIN symbols s ON s.id = r.from_id
2694
+ WHERE r.to_id IS NULL AND r.to_name = ?
2695
+ ORDER BY r.line, r.id`
2696
+ ).all(symbolName);
2697
+ rows.push(...fallbackRows);
2698
+ }
2699
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
2700
+ const allCalls = rows.map(mapCallSiteRow);
2701
+ return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
2702
+ }
2703
+ function findOutgoingCallsByName(stmt, symbolName, file, limit) {
2704
+ const sourceIds = resolveSymbolIds(stmt, symbolName, file);
2705
+ if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
2706
+ const unresolvedCount = chunkedIdScalar(
2707
+ stmt,
2708
+ sourceIds,
2709
+ (ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
2710
+ );
2711
+ const rows = chunkedIdQuery(
2712
+ stmt,
2713
+ sourceIds,
2714
+ (ph) => `SELECT
2715
+ s.id AS sym_id,
2716
+ s.name AS sym_name,
2717
+ s.kind AS sym_kind,
2718
+ s.lang AS sym_lang,
2719
+ s.file AS sym_file,
2720
+ s.line AS sym_line,
2721
+ s.signature AS sym_signature,
2722
+ r.call_type,
2723
+ r.line AS ref_line
2724
+ FROM refs r
2725
+ JOIN symbols s ON s.id = r.to_id
2726
+ WHERE r.from_id IN (${ph})
2727
+ AND r.to_id IS NOT NULL -- INNER JOIN already excludes NULL to_id; this is defensive belt-and-suspenders
2728
+ ORDER BY r.line, r.id`,
2729
+ []
2730
+ );
2731
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
2732
+ const calls = rows.map(mapCallSiteRow).slice(0, limit);
2733
+ return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
2734
+ }
2605
2735
  function findRefsToWithStatement(stmt, symbolId) {
2606
2736
  return stmt(
2607
2737
  "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
@@ -3837,6 +3967,20 @@ var IndexStore = class _IndexStore {
3837
3967
  return false;
3838
3968
  }
3839
3969
  }
3970
+ /**
3971
+ * Find all symbols that reference the named target symbol (incoming callers).
3972
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
3973
+ */
3974
+ findIncomingCallsByName(symbolName, file, limit = 100) {
3975
+ return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
3976
+ }
3977
+ /**
3978
+ * Find all symbols that the named source symbol references (outgoing callees).
3979
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
3980
+ */
3981
+ findOutgoingCallsByName(symbolName, file, limit = 100) {
3982
+ return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
3983
+ }
3840
3984
  /**
3841
3985
  * Find all references TO a given symbol (who calls / uses this symbol?).
3842
3986
  */
@@ -5264,6 +5408,22 @@ function symbolGraphService(args) {
5264
5408
  indexStorePool.release(store);
5265
5409
  }
5266
5410
  }
5411
+ function incomingCallsService(args) {
5412
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5413
+ try {
5414
+ return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
5415
+ } finally {
5416
+ indexStorePool.release(store);
5417
+ }
5418
+ }
5419
+ function outgoingCallsService(args) {
5420
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5421
+ try {
5422
+ return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
5423
+ } finally {
5424
+ indexStorePool.release(store);
5425
+ }
5426
+ }
5267
5427
 
5268
5428
  // src/codebase-index/background-indexer.ts
5269
5429
  init_languages();
@@ -5501,6 +5661,10 @@ async function callInline(op, args, opts) {
5501
5661
  return fileGraphService(args);
5502
5662
  case "symbolGraph":
5503
5663
  return symbolGraphService(args);
5664
+ case "incomingCalls":
5665
+ return incomingCallsService(args);
5666
+ case "outgoingCalls":
5667
+ return outgoingCallsService(args);
5504
5668
  default:
5505
5669
  throw new Error(`unknown index op: ${String(op)}`);
5506
5670
  }
@@ -5694,6 +5858,12 @@ async function fileGraphService2(args) {
5694
5858
  async function symbolGraphService2(args) {
5695
5859
  return callIndexOp("symbolGraph", args, { timeoutMs: DEFAULT_QUERY_TIMEOUT_MS });
5696
5860
  }
5861
+ async function incomingCallsService2(args) {
5862
+ return callIndexOp("incomingCalls", args, { timeoutMs: DEFAULT_QUERY_TIMEOUT_MS });
5863
+ }
5864
+ async function outgoingCallsService2(args) {
5865
+ return callIndexOp("outgoingCalls", args, { timeoutMs: DEFAULT_QUERY_TIMEOUT_MS });
5866
+ }
5697
5867
  function shutdownCodebaseIndexServer(projectRoot, indexDir, reason) {
5698
5868
  return shutdownProjectIndexServer(projectRoot, indexDir, reason);
5699
5869
  }
@@ -5777,6 +5947,214 @@ var codebaseIndexTool = {
5777
5947
  }
5778
5948
  };
5779
5949
 
5950
+ // src/codebase-index/codebase-incoming-calls-tool.ts
5951
+ var codebaseIncomingCallsTool = {
5952
+ name: "codebase-incoming-calls",
5953
+ category: "Project",
5954
+ icon: "index",
5955
+ description: "Find all callers of a function, method, or symbol \u2014 who invokes or references it. Uses the codebase index ref graph for instant, exact results. Always use this instead of grep when checking impact of a change.",
5956
+ usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n- NEVER use grep or manual line reading to check where a function is called.\n- ALWAYS call codebase-incoming-calls({ symbol: "funcName" }) first.\n- Returns exact files, line numbers, caller signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Combine with codebase-outgoing-calls to see what the symbol itself calls.\nIf the index is not built, run codebase-index first.',
5957
+ permission: "auto",
5958
+ mutating: false,
5959
+ capabilities: ["fs.read"],
5960
+ timeoutMs: 35e3,
5961
+ inputSchema: {
5962
+ type: "object",
5963
+ properties: {
5964
+ symbol: {
5965
+ type: "string",
5966
+ description: "The function/method/type name to find callers for"
5967
+ },
5968
+ file: {
5969
+ type: "string",
5970
+ description: "Scope to a specific file when multiple symbols share the same name"
5971
+ },
5972
+ limit: {
5973
+ type: "integer",
5974
+ description: "Maximum call sites to return (default 50, max 200)",
5975
+ minimum: 1,
5976
+ maximum: 200
5977
+ }
5978
+ },
5979
+ required: ["symbol"]
5980
+ },
5981
+ async execute(input, ctx) {
5982
+ const state = getIndexState();
5983
+ if (state.indexing && !state.ready) {
5984
+ return {
5985
+ symbol: input.symbol,
5986
+ calls: [],
5987
+ total: 0,
5988
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
5989
+ };
5990
+ }
5991
+ if (state.lastError) {
5992
+ const circuit = state.circuit;
5993
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s).` : "Try /codebase-reindex.";
5994
+ return {
5995
+ symbol: input.symbol,
5996
+ calls: [],
5997
+ total: 0,
5998
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
5999
+ };
6000
+ }
6001
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
6002
+ const { calls, symbolFound, ambiguous, totalMatches } = await incomingCallsService2(
6003
+ {
6004
+ projectRoot: ctx.projectRoot,
6005
+ indexDir: codebaseIndexDirOverride(ctx),
6006
+ symbol: input.symbol,
6007
+ file: input.file,
6008
+ limit
6009
+ }
6010
+ );
6011
+ if (!symbolFound) {
6012
+ let hasPersistedIndex = state.ready;
6013
+ if (!hasPersistedIndex) {
6014
+ try {
6015
+ const stats = await codebaseIndexStats({
6016
+ projectRoot: ctx.projectRoot,
6017
+ indexDir: codebaseIndexDirOverride(ctx)
6018
+ });
6019
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
6020
+ } catch {
6021
+ }
6022
+ }
6023
+ if (!hasPersistedIndex) {
6024
+ return {
6025
+ symbol: input.symbol,
6026
+ calls: [],
6027
+ total: 0,
6028
+ indexStatus: "No persisted index data found. Run codebase-index to build it."
6029
+ };
6030
+ }
6031
+ return {
6032
+ symbol: input.symbol,
6033
+ calls: [],
6034
+ total: 0,
6035
+ note: `Symbol "${input.symbol}" not found in the index. Use codebase-search to verify the name.`
6036
+ };
6037
+ }
6038
+ const notes = [];
6039
+ if (totalMatches > limit) {
6040
+ notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
6041
+ }
6042
+ if (ambiguous) {
6043
+ notes.push(`Symbol "${input.symbol}" exists in multiple files. Results include callers of all same-named symbols. Use codebase-search to find the exact file and pass it as \`file\`.`);
6044
+ }
6045
+ return {
6046
+ symbol: input.symbol,
6047
+ calls,
6048
+ total: calls.length,
6049
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
6050
+ };
6051
+ }
6052
+ };
6053
+
6054
+ // src/codebase-index/codebase-outgoing-calls-tool.ts
6055
+ var codebaseOutgoingCallsTool = {
6056
+ name: "codebase-outgoing-calls",
6057
+ category: "Project",
6058
+ icon: "index",
6059
+ description: "Find all functions/methods/symbols that a given symbol calls or depends on \u2014 its callees. Uses the codebase index ref graph for instant, exact results. Use this to understand a function's dependencies before modifying it.",
6060
+ usageHint: 'USE THIS TO UNDERSTAND A FUNCTION\'S DEPENDENCIES:\n\n- Call codebase-outgoing-calls({ symbol: "funcName" }) to see everything it calls.\n- Returns exact files, line numbers, callee signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Pair with codebase-incoming-calls for a complete impact picture: incoming = who calls you, outgoing = what you call.\nIf the index is not built, run codebase-index first.',
6061
+ permission: "auto",
6062
+ mutating: false,
6063
+ capabilities: ["fs.read"],
6064
+ timeoutMs: 35e3,
6065
+ inputSchema: {
6066
+ type: "object",
6067
+ properties: {
6068
+ symbol: {
6069
+ type: "string",
6070
+ description: "The function/method/type name to find callees for"
6071
+ },
6072
+ file: {
6073
+ type: "string",
6074
+ description: "Scope to a specific file when multiple symbols share the same name"
6075
+ },
6076
+ limit: {
6077
+ type: "integer",
6078
+ description: "Maximum call sites to return (default 50, max 200)",
6079
+ minimum: 1,
6080
+ maximum: 200
6081
+ }
6082
+ },
6083
+ required: ["symbol"]
6084
+ },
6085
+ async execute(input, ctx) {
6086
+ const state = getIndexState();
6087
+ if (state.indexing && !state.ready) {
6088
+ return {
6089
+ symbol: input.symbol,
6090
+ calls: [],
6091
+ total: 0,
6092
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
6093
+ };
6094
+ }
6095
+ if (state.lastError) {
6096
+ const circuit = state.circuit;
6097
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s).` : "Try /codebase-reindex.";
6098
+ return {
6099
+ symbol: input.symbol,
6100
+ calls: [],
6101
+ total: 0,
6102
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
6103
+ };
6104
+ }
6105
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
6106
+ const { calls, symbolFound, unresolvedCount, totalMatches } = await outgoingCallsService2(
6107
+ {
6108
+ projectRoot: ctx.projectRoot,
6109
+ indexDir: codebaseIndexDirOverride(ctx),
6110
+ symbol: input.symbol,
6111
+ file: input.file,
6112
+ limit
6113
+ }
6114
+ );
6115
+ if (!symbolFound) {
6116
+ let hasPersistedIndex = state.ready;
6117
+ if (!hasPersistedIndex) {
6118
+ try {
6119
+ const stats = await codebaseIndexStats({
6120
+ projectRoot: ctx.projectRoot,
6121
+ indexDir: codebaseIndexDirOverride(ctx)
6122
+ });
6123
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
6124
+ } catch {
6125
+ }
6126
+ }
6127
+ if (!hasPersistedIndex) {
6128
+ return {
6129
+ symbol: input.symbol,
6130
+ calls: [],
6131
+ total: 0,
6132
+ indexStatus: "No persisted index data found. Run codebase-index to build it."
6133
+ };
6134
+ }
6135
+ return {
6136
+ symbol: input.symbol,
6137
+ calls: [],
6138
+ total: 0,
6139
+ note: `Symbol "${input.symbol}" not found in the index. Use codebase-search to verify the name.`
6140
+ };
6141
+ }
6142
+ const notes = [];
6143
+ if (totalMatches > limit) {
6144
+ notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
6145
+ }
6146
+ if (unresolvedCount > 0) {
6147
+ notes.push(`${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`);
6148
+ }
6149
+ return {
6150
+ symbol: input.symbol,
6151
+ calls,
6152
+ total: calls.length,
6153
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
6154
+ };
6155
+ }
6156
+ };
6157
+
5780
6158
  // src/codebase-index/codebase-search-tool.ts
5781
6159
  var codebaseSearchTool = {
5782
6160
  name: "codebase-search",
@@ -6492,9 +6870,11 @@ export {
6492
6870
  buildIndexableText,
6493
6871
  cancelPendingReindexes,
6494
6872
  checkCodebaseIndexServerHealth,
6873
+ codebaseIncomingCallsTool,
6495
6874
  codebaseIndexDirOverride,
6496
6875
  codebaseIndexStats,
6497
6876
  codebaseIndexTool,
6877
+ codebaseOutgoingCallsTool,
6498
6878
  codebaseSearchTool,
6499
6879
  codebaseStatsTool,
6500
6880
  deadCodeScanTool,
@@ -6503,6 +6883,7 @@ export {
6503
6883
  ensureCodebaseIndexServer,
6504
6884
  fileGraphService2 as fileGraphService,
6505
6885
  getIndexState,
6886
+ incomingCallsService2 as incomingCallsService,
6506
6887
  indexCircuitBreaker,
6507
6888
  internalKindToLspKind,
6508
6889
  isIndexReady,
@@ -6511,6 +6892,7 @@ export {
6511
6892
  isIndexing,
6512
6893
  lspKindToInternalKind,
6513
6894
  onIndexStateChange,
6895
+ outgoingCallsService2 as outgoingCallsService,
6514
6896
  packageGraphService2 as packageGraphService,
6515
6897
  resetIndexCircuitBreaker,
6516
6898
  resolveIndexDir,
@@ -2671,6 +2671,136 @@ function mapWriterRefRow(row) {
2671
2671
  }
2672
2672
 
2673
2673
  // src/codebase-index/writer-graph-reader.ts
2674
+ var MAX_SQL_VARS = 900;
2675
+ function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
2676
+ const results = [];
2677
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
2678
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
2679
+ const placeholders = chunk.map(() => "?").join(",");
2680
+ const sql = buildSql(placeholders);
2681
+ results.push(...stmt(sql).all(...chunk, ...extraArgs));
2682
+ }
2683
+ return results;
2684
+ }
2685
+ function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
2686
+ let total = 0;
2687
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
2688
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
2689
+ const placeholders = chunk.map(() => "?").join(",");
2690
+ const sql = buildSql(placeholders);
2691
+ const rows = stmt(sql).all(...chunk, ...extraArgs);
2692
+ total += rows[0]?.n ?? 0;
2693
+ }
2694
+ return total;
2695
+ }
2696
+ function mapCallSiteRow(row) {
2697
+ return {
2698
+ symbol: {
2699
+ id: row.sym_id,
2700
+ name: row.sym_name,
2701
+ kind: row.sym_kind,
2702
+ lang: row.sym_lang,
2703
+ file: row.sym_file,
2704
+ line: row.sym_line,
2705
+ signature: row.sym_signature
2706
+ },
2707
+ callType: row.call_type,
2708
+ line: row.ref_line
2709
+ };
2710
+ }
2711
+ function resolveSymbolIds(stmt, symbolName, file) {
2712
+ const baseSql = file ? `SELECT id FROM symbols WHERE name = ? AND file = ? ORDER BY id` : `SELECT id FROM symbols WHERE name = ? ORDER BY id`;
2713
+ const args = file ? [symbolName, file] : [symbolName];
2714
+ const rows = stmt(baseSql).all(...args);
2715
+ return rows.map((r) => r.id);
2716
+ }
2717
+ function findIncomingCallsByName(stmt, symbolName, file, limit) {
2718
+ const targetIds = resolveSymbolIds(stmt, symbolName, file);
2719
+ if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
2720
+ let matchIds = targetIds;
2721
+ let ambiguous = false;
2722
+ if (file !== void 0) {
2723
+ const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
2724
+ if (allNamedIds.length > targetIds.length) {
2725
+ matchIds = allNamedIds;
2726
+ ambiguous = true;
2727
+ }
2728
+ }
2729
+ const useFallback = !file;
2730
+ const rows = chunkedIdQuery(
2731
+ stmt,
2732
+ matchIds,
2733
+ (ph) => `SELECT
2734
+ s.id AS sym_id,
2735
+ s.name AS sym_name,
2736
+ s.kind AS sym_kind,
2737
+ s.lang AS sym_lang,
2738
+ s.file AS sym_file,
2739
+ s.line AS sym_line,
2740
+ s.signature AS sym_signature,
2741
+ r.call_type,
2742
+ r.line AS ref_line
2743
+ FROM refs r
2744
+ JOIN symbols s ON s.id = r.from_id
2745
+ WHERE r.to_id IN (${ph})
2746
+ ORDER BY r.line, r.id`,
2747
+ []
2748
+ );
2749
+ if (useFallback) {
2750
+ const fallbackRows = stmt(
2751
+ `SELECT
2752
+ s.id AS sym_id,
2753
+ s.name AS sym_name,
2754
+ s.kind AS sym_kind,
2755
+ s.lang AS sym_lang,
2756
+ s.file AS sym_file,
2757
+ s.line AS sym_line,
2758
+ s.signature AS sym_signature,
2759
+ r.call_type,
2760
+ r.line AS ref_line
2761
+ FROM refs r
2762
+ JOIN symbols s ON s.id = r.from_id
2763
+ WHERE r.to_id IS NULL AND r.to_name = ?
2764
+ ORDER BY r.line, r.id`
2765
+ ).all(symbolName);
2766
+ rows.push(...fallbackRows);
2767
+ }
2768
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
2769
+ const allCalls = rows.map(mapCallSiteRow);
2770
+ return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
2771
+ }
2772
+ function findOutgoingCallsByName(stmt, symbolName, file, limit) {
2773
+ const sourceIds = resolveSymbolIds(stmt, symbolName, file);
2774
+ if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
2775
+ const unresolvedCount = chunkedIdScalar(
2776
+ stmt,
2777
+ sourceIds,
2778
+ (ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
2779
+ );
2780
+ const rows = chunkedIdQuery(
2781
+ stmt,
2782
+ sourceIds,
2783
+ (ph) => `SELECT
2784
+ s.id AS sym_id,
2785
+ s.name AS sym_name,
2786
+ s.kind AS sym_kind,
2787
+ s.lang AS sym_lang,
2788
+ s.file AS sym_file,
2789
+ s.line AS sym_line,
2790
+ s.signature AS sym_signature,
2791
+ r.call_type,
2792
+ r.line AS ref_line
2793
+ FROM refs r
2794
+ JOIN symbols s ON s.id = r.to_id
2795
+ WHERE r.from_id IN (${ph})
2796
+ AND r.to_id IS NOT NULL -- INNER JOIN already excludes NULL to_id; this is defensive belt-and-suspenders
2797
+ ORDER BY r.line, r.id`,
2798
+ []
2799
+ );
2800
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
2801
+ const calls = rows.map(mapCallSiteRow).slice(0, limit);
2802
+ return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
2803
+ }
2674
2804
  function findRefsToWithStatement(stmt, symbolId) {
2675
2805
  return stmt(
2676
2806
  "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
@@ -3902,6 +4032,20 @@ var IndexStore = class _IndexStore {
3902
4032
  return false;
3903
4033
  }
3904
4034
  }
4035
+ /**
4036
+ * Find all symbols that reference the named target symbol (incoming callers).
4037
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
4038
+ */
4039
+ findIncomingCallsByName(symbolName, file, limit = 100) {
4040
+ return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
4041
+ }
4042
+ /**
4043
+ * Find all symbols that the named source symbol references (outgoing callees).
4044
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
4045
+ */
4046
+ findOutgoingCallsByName(symbolName, file, limit = 100) {
4047
+ return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
4048
+ }
3905
4049
  /**
3906
4050
  * Find all references TO a given symbol (who calls / uses this symbol?).
3907
4051
  */
@@ -4486,6 +4630,22 @@ function symbolGraphService(args) {
4486
4630
  indexStorePool.release(store);
4487
4631
  }
4488
4632
  }
4633
+ function incomingCallsService(args) {
4634
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4635
+ try {
4636
+ return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
4637
+ } finally {
4638
+ indexStorePool.release(store);
4639
+ }
4640
+ }
4641
+ function outgoingCallsService(args) {
4642
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4643
+ try {
4644
+ return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
4645
+ } finally {
4646
+ indexStorePool.release(store);
4647
+ }
4648
+ }
4489
4649
 
4490
4650
  // src/codebase-index/project-server.ts
4491
4651
  init_languages();
@@ -4639,6 +4799,8 @@ var statsCache = new GenerationLruCache(1);
4639
4799
  var packageGraphCache = new GenerationLruCache(1);
4640
4800
  var fileGraphCache = new GenerationLruCache(32);
4641
4801
  var symbolGraphCache = new GenerationLruCache(64);
4802
+ var incomingCallsCache = new GenerationLruCache(128);
4803
+ var outgoingCallsCache = new GenerationLruCache(128);
4642
4804
  var indexActivity = {
4643
4805
  indexing: false,
4644
4806
  currentFile: 0,
@@ -4658,7 +4820,9 @@ var stopMemoryWatchdog = startSharedHeapWatchdog({
4658
4820
  statsCache: statsCache.size,
4659
4821
  packageGraphCache: packageGraphCache.size,
4660
4822
  fileGraphCache: fileGraphCache.size,
4661
- symbolGraphCache: symbolGraphCache.size
4823
+ symbolGraphCache: symbolGraphCache.size,
4824
+ incomingCallsCache: incomingCallsCache.size,
4825
+ outgoingCallsCache: outgoingCallsCache.size
4662
4826
  })
4663
4827
  });
4664
4828
  var lastProgressBroadcastAt = 0;
@@ -4674,6 +4838,8 @@ function clearQueryCaches() {
4674
4838
  packageGraphCache.clear();
4675
4839
  fileGraphCache.clear();
4676
4840
  symbolGraphCache.clear();
4841
+ incomingCallsCache.clear();
4842
+ outgoingCallsCache.clear();
4677
4843
  }
4678
4844
  function cachedRead(cache, key, load) {
4679
4845
  const generation = indexActivity.generation;
@@ -4881,6 +5047,16 @@ async function dispatchOperation(state, message) {
4881
5047
  message.args.fileFilter,
4882
5048
  () => symbolGraphService(fixedArgs(message.args))
4883
5049
  );
5050
+ case "incomingCalls": {
5051
+ const callArgs = fixedArgs(message.args);
5052
+ const cacheKey = JSON.stringify([callArgs.symbol, callArgs.file ?? "", callArgs.limit ?? 100]);
5053
+ return cachedRead(incomingCallsCache, cacheKey, () => incomingCallsService(callArgs));
5054
+ }
5055
+ case "outgoingCalls": {
5056
+ const callArgs = fixedArgs(message.args);
5057
+ const cacheKey = JSON.stringify([callArgs.symbol, callArgs.file ?? "", callArgs.limit ?? 100]);
5058
+ return cachedRead(outgoingCallsCache, cacheKey, () => outgoingCallsService(callArgs));
5059
+ }
4884
5060
  default:
4885
5061
  throw new Error(`unknown index operation: ${String(op)}`);
4886
5062
  }
@@ -96,6 +96,29 @@ export interface Ref {
96
96
  callType: CallType;
97
97
  line: number;
98
98
  }
99
+ /**
100
+ * A call site — the enriched result of an incoming/outgoing calls query.
101
+ *
102
+ * Unlike the raw `Ref` type, this carries the full caller/callee symbol
103
+ * metadata (name, kind, file, line, signature) so the agent gets a
104
+ * self-contained answer without a second lookup.
105
+ */
106
+ export interface CallSite {
107
+ /** The symbol that makes or receives the call. */
108
+ symbol: {
109
+ id: number;
110
+ name: string;
111
+ kind: SymbolKind;
112
+ lang: SymbolLang;
113
+ file: string;
114
+ line: number;
115
+ signature: string;
116
+ };
117
+ /** Kind of reference: call, type_ref, inherit, implement, import. */
118
+ callType: CallType;
119
+ /** Source line where the reference occurs. */
120
+ line: number;
121
+ }
99
122
  /** A node in the code-map dependency graph. */
100
123
  export interface GraphNode {
101
124
  id: string;