@wrongstack/tools 0.298.2 → 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.
package/dist/pack.js CHANGED
@@ -7082,6 +7082,10 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
7082
7082
  const start = Date.now();
7083
7083
  const pidStr = String(process.pid);
7084
7084
  const hostStr = os2.hostname();
7085
+ try {
7086
+ await fs6.mkdir(path9.dirname(lockfilePath), { recursive: true });
7087
+ } catch {
7088
+ }
7085
7089
  while (Date.now() - start < timeoutMs) {
7086
7090
  try {
7087
7091
  await fs6.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
@@ -7747,6 +7751,16 @@ function parseKillCommand(command) {
7747
7751
  if (pgrepMatch) {
7748
7752
  return null;
7749
7753
  }
7754
+ const posixScriptMatch = normalized.match(SCRIPT_KILL_RE_POSIX);
7755
+ if (posixScriptMatch) {
7756
+ return {
7757
+ name: "kill-script",
7758
+ signal: "FORCE",
7759
+ isGroupKill: false,
7760
+ isAllKill: false,
7761
+ originalCommand: command
7762
+ };
7763
+ }
7750
7764
  return null;
7751
7765
  }
7752
7766
  async function getProtectedEntries() {
@@ -10538,6 +10552,136 @@ function mapWriterRefRow(row) {
10538
10552
  }
10539
10553
 
10540
10554
  // src/codebase-index/writer-graph-reader.ts
10555
+ var MAX_SQL_VARS = 900;
10556
+ function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
10557
+ const results = [];
10558
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
10559
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
10560
+ const placeholders = chunk.map(() => "?").join(",");
10561
+ const sql = buildSql(placeholders);
10562
+ results.push(...stmt(sql).all(...chunk, ...extraArgs));
10563
+ }
10564
+ return results;
10565
+ }
10566
+ function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
10567
+ let total = 0;
10568
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
10569
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
10570
+ const placeholders = chunk.map(() => "?").join(",");
10571
+ const sql = buildSql(placeholders);
10572
+ const rows = stmt(sql).all(...chunk, ...extraArgs);
10573
+ total += rows[0]?.n ?? 0;
10574
+ }
10575
+ return total;
10576
+ }
10577
+ function mapCallSiteRow(row) {
10578
+ return {
10579
+ symbol: {
10580
+ id: row.sym_id,
10581
+ name: row.sym_name,
10582
+ kind: row.sym_kind,
10583
+ lang: row.sym_lang,
10584
+ file: row.sym_file,
10585
+ line: row.sym_line,
10586
+ signature: row.sym_signature
10587
+ },
10588
+ callType: row.call_type,
10589
+ line: row.ref_line
10590
+ };
10591
+ }
10592
+ function resolveSymbolIds(stmt, symbolName, file) {
10593
+ const baseSql = file ? `SELECT id FROM symbols WHERE name = ? AND file = ? ORDER BY id` : `SELECT id FROM symbols WHERE name = ? ORDER BY id`;
10594
+ const args = file ? [symbolName, file] : [symbolName];
10595
+ const rows = stmt(baseSql).all(...args);
10596
+ return rows.map((r) => r.id);
10597
+ }
10598
+ function findIncomingCallsByName(stmt, symbolName, file, limit) {
10599
+ const targetIds = resolveSymbolIds(stmt, symbolName, file);
10600
+ if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
10601
+ let matchIds = targetIds;
10602
+ let ambiguous = false;
10603
+ if (file !== void 0) {
10604
+ const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
10605
+ if (allNamedIds.length > targetIds.length) {
10606
+ matchIds = allNamedIds;
10607
+ ambiguous = true;
10608
+ }
10609
+ }
10610
+ const useFallback = !file;
10611
+ const rows = chunkedIdQuery(
10612
+ stmt,
10613
+ matchIds,
10614
+ (ph) => `SELECT
10615
+ s.id AS sym_id,
10616
+ s.name AS sym_name,
10617
+ s.kind AS sym_kind,
10618
+ s.lang AS sym_lang,
10619
+ s.file AS sym_file,
10620
+ s.line AS sym_line,
10621
+ s.signature AS sym_signature,
10622
+ r.call_type,
10623
+ r.line AS ref_line
10624
+ FROM refs r
10625
+ JOIN symbols s ON s.id = r.from_id
10626
+ WHERE r.to_id IN (${ph})
10627
+ ORDER BY r.line, r.id`,
10628
+ []
10629
+ );
10630
+ if (useFallback) {
10631
+ const fallbackRows = stmt(
10632
+ `SELECT
10633
+ s.id AS sym_id,
10634
+ s.name AS sym_name,
10635
+ s.kind AS sym_kind,
10636
+ s.lang AS sym_lang,
10637
+ s.file AS sym_file,
10638
+ s.line AS sym_line,
10639
+ s.signature AS sym_signature,
10640
+ r.call_type,
10641
+ r.line AS ref_line
10642
+ FROM refs r
10643
+ JOIN symbols s ON s.id = r.from_id
10644
+ WHERE r.to_id IS NULL AND r.to_name = ?
10645
+ ORDER BY r.line, r.id`
10646
+ ).all(symbolName);
10647
+ rows.push(...fallbackRows);
10648
+ }
10649
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
10650
+ const allCalls = rows.map(mapCallSiteRow);
10651
+ return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
10652
+ }
10653
+ function findOutgoingCallsByName(stmt, symbolName, file, limit) {
10654
+ const sourceIds = resolveSymbolIds(stmt, symbolName, file);
10655
+ if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
10656
+ const unresolvedCount = chunkedIdScalar(
10657
+ stmt,
10658
+ sourceIds,
10659
+ (ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
10660
+ );
10661
+ const rows = chunkedIdQuery(
10662
+ stmt,
10663
+ sourceIds,
10664
+ (ph) => `SELECT
10665
+ s.id AS sym_id,
10666
+ s.name AS sym_name,
10667
+ s.kind AS sym_kind,
10668
+ s.lang AS sym_lang,
10669
+ s.file AS sym_file,
10670
+ s.line AS sym_line,
10671
+ s.signature AS sym_signature,
10672
+ r.call_type,
10673
+ r.line AS ref_line
10674
+ FROM refs r
10675
+ JOIN symbols s ON s.id = r.to_id
10676
+ WHERE r.from_id IN (${ph})
10677
+ AND r.to_id IS NOT NULL -- INNER JOIN already excludes NULL to_id; this is defensive belt-and-suspenders
10678
+ ORDER BY r.line, r.id`,
10679
+ []
10680
+ );
10681
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
10682
+ const calls = rows.map(mapCallSiteRow).slice(0, limit);
10683
+ return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
10684
+ }
10541
10685
  function findRefsToWithStatement(stmt, symbolId) {
10542
10686
  return stmt(
10543
10687
  "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 = ?)"
@@ -11773,6 +11917,20 @@ var IndexStore = class _IndexStore {
11773
11917
  return false;
11774
11918
  }
11775
11919
  }
11920
+ /**
11921
+ * Find all symbols that reference the named target symbol (incoming callers).
11922
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
11923
+ */
11924
+ findIncomingCallsByName(symbolName, file, limit = 100) {
11925
+ return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
11926
+ }
11927
+ /**
11928
+ * Find all symbols that the named source symbol references (outgoing callees).
11929
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
11930
+ */
11931
+ findOutgoingCallsByName(symbolName, file, limit = 100) {
11932
+ return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
11933
+ }
11776
11934
  /**
11777
11935
  * Find all references TO a given symbol (who calls / uses this symbol?).
11778
11936
  */
@@ -13155,6 +13313,22 @@ function symbolGraphService(args) {
13155
13313
  indexStorePool.release(store);
13156
13314
  }
13157
13315
  }
13316
+ function incomingCallsService(args) {
13317
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
13318
+ try {
13319
+ return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
13320
+ } finally {
13321
+ indexStorePool.release(store);
13322
+ }
13323
+ }
13324
+ function outgoingCallsService(args) {
13325
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
13326
+ try {
13327
+ return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
13328
+ } finally {
13329
+ indexStorePool.release(store);
13330
+ }
13331
+ }
13158
13332
 
13159
13333
  // src/codebase-index/background-indexer.ts
13160
13334
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
@@ -13366,6 +13540,10 @@ async function callInline(op, args, opts) {
13366
13540
  return fileGraphService(args);
13367
13541
  case "symbolGraph":
13368
13542
  return symbolGraphService(args);
13543
+ case "incomingCalls":
13544
+ return incomingCallsService(args);
13545
+ case "outgoingCalls":
13546
+ return outgoingCallsService(args);
13369
13547
  default:
13370
13548
  throw new Error(`unknown index op: ${String(op)}`);
13371
13549
  }
@@ -13461,6 +13639,12 @@ async function codebaseIndexStats(args, opts = {}) {
13461
13639
  signal: opts.signal
13462
13640
  });
13463
13641
  }
13642
+ async function incomingCallsService2(args) {
13643
+ return callIndexOp("incomingCalls", args, { timeoutMs: DEFAULT_QUERY_TIMEOUT_MS });
13644
+ }
13645
+ async function outgoingCallsService2(args) {
13646
+ return callIndexOp("outgoingCalls", args, { timeoutMs: DEFAULT_QUERY_TIMEOUT_MS });
13647
+ }
13464
13648
 
13465
13649
  // src/codebase-index/codebase-index-tool.ts
13466
13650
  var codebaseIndexTool = {
@@ -13525,6 +13709,214 @@ var codebaseIndexTool = {
13525
13709
  }
13526
13710
  };
13527
13711
 
13712
+ // src/codebase-index/codebase-incoming-calls-tool.ts
13713
+ var codebaseIncomingCallsTool = {
13714
+ name: "codebase-incoming-calls",
13715
+ category: "Project",
13716
+ icon: "index",
13717
+ 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.",
13718
+ 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.',
13719
+ permission: "auto",
13720
+ mutating: false,
13721
+ capabilities: ["fs.read"],
13722
+ timeoutMs: 35e3,
13723
+ inputSchema: {
13724
+ type: "object",
13725
+ properties: {
13726
+ symbol: {
13727
+ type: "string",
13728
+ description: "The function/method/type name to find callers for"
13729
+ },
13730
+ file: {
13731
+ type: "string",
13732
+ description: "Scope to a specific file when multiple symbols share the same name"
13733
+ },
13734
+ limit: {
13735
+ type: "integer",
13736
+ description: "Maximum call sites to return (default 50, max 200)",
13737
+ minimum: 1,
13738
+ maximum: 200
13739
+ }
13740
+ },
13741
+ required: ["symbol"]
13742
+ },
13743
+ async execute(input, ctx) {
13744
+ const state = getIndexState();
13745
+ if (state.indexing && !state.ready) {
13746
+ return {
13747
+ symbol: input.symbol,
13748
+ calls: [],
13749
+ total: 0,
13750
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
13751
+ };
13752
+ }
13753
+ if (state.lastError) {
13754
+ const circuit = state.circuit;
13755
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s).` : "Try /codebase-reindex.";
13756
+ return {
13757
+ symbol: input.symbol,
13758
+ calls: [],
13759
+ total: 0,
13760
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
13761
+ };
13762
+ }
13763
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
13764
+ const { calls, symbolFound, ambiguous, totalMatches } = await incomingCallsService2(
13765
+ {
13766
+ projectRoot: ctx.projectRoot,
13767
+ indexDir: codebaseIndexDirOverride(ctx),
13768
+ symbol: input.symbol,
13769
+ file: input.file,
13770
+ limit
13771
+ }
13772
+ );
13773
+ if (!symbolFound) {
13774
+ let hasPersistedIndex = state.ready;
13775
+ if (!hasPersistedIndex) {
13776
+ try {
13777
+ const stats = await codebaseIndexStats({
13778
+ projectRoot: ctx.projectRoot,
13779
+ indexDir: codebaseIndexDirOverride(ctx)
13780
+ });
13781
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
13782
+ } catch {
13783
+ }
13784
+ }
13785
+ if (!hasPersistedIndex) {
13786
+ return {
13787
+ symbol: input.symbol,
13788
+ calls: [],
13789
+ total: 0,
13790
+ indexStatus: "No persisted index data found. Run codebase-index to build it."
13791
+ };
13792
+ }
13793
+ return {
13794
+ symbol: input.symbol,
13795
+ calls: [],
13796
+ total: 0,
13797
+ note: `Symbol "${input.symbol}" not found in the index. Use codebase-search to verify the name.`
13798
+ };
13799
+ }
13800
+ const notes = [];
13801
+ if (totalMatches > limit) {
13802
+ notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
13803
+ }
13804
+ if (ambiguous) {
13805
+ 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\`.`);
13806
+ }
13807
+ return {
13808
+ symbol: input.symbol,
13809
+ calls,
13810
+ total: calls.length,
13811
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
13812
+ };
13813
+ }
13814
+ };
13815
+
13816
+ // src/codebase-index/codebase-outgoing-calls-tool.ts
13817
+ var codebaseOutgoingCallsTool = {
13818
+ name: "codebase-outgoing-calls",
13819
+ category: "Project",
13820
+ icon: "index",
13821
+ 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.",
13822
+ 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.',
13823
+ permission: "auto",
13824
+ mutating: false,
13825
+ capabilities: ["fs.read"],
13826
+ timeoutMs: 35e3,
13827
+ inputSchema: {
13828
+ type: "object",
13829
+ properties: {
13830
+ symbol: {
13831
+ type: "string",
13832
+ description: "The function/method/type name to find callees for"
13833
+ },
13834
+ file: {
13835
+ type: "string",
13836
+ description: "Scope to a specific file when multiple symbols share the same name"
13837
+ },
13838
+ limit: {
13839
+ type: "integer",
13840
+ description: "Maximum call sites to return (default 50, max 200)",
13841
+ minimum: 1,
13842
+ maximum: 200
13843
+ }
13844
+ },
13845
+ required: ["symbol"]
13846
+ },
13847
+ async execute(input, ctx) {
13848
+ const state = getIndexState();
13849
+ if (state.indexing && !state.ready) {
13850
+ return {
13851
+ symbol: input.symbol,
13852
+ calls: [],
13853
+ total: 0,
13854
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
13855
+ };
13856
+ }
13857
+ if (state.lastError) {
13858
+ const circuit = state.circuit;
13859
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s).` : "Try /codebase-reindex.";
13860
+ return {
13861
+ symbol: input.symbol,
13862
+ calls: [],
13863
+ total: 0,
13864
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
13865
+ };
13866
+ }
13867
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
13868
+ const { calls, symbolFound, unresolvedCount, totalMatches } = await outgoingCallsService2(
13869
+ {
13870
+ projectRoot: ctx.projectRoot,
13871
+ indexDir: codebaseIndexDirOverride(ctx),
13872
+ symbol: input.symbol,
13873
+ file: input.file,
13874
+ limit
13875
+ }
13876
+ );
13877
+ if (!symbolFound) {
13878
+ let hasPersistedIndex = state.ready;
13879
+ if (!hasPersistedIndex) {
13880
+ try {
13881
+ const stats = await codebaseIndexStats({
13882
+ projectRoot: ctx.projectRoot,
13883
+ indexDir: codebaseIndexDirOverride(ctx)
13884
+ });
13885
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
13886
+ } catch {
13887
+ }
13888
+ }
13889
+ if (!hasPersistedIndex) {
13890
+ return {
13891
+ symbol: input.symbol,
13892
+ calls: [],
13893
+ total: 0,
13894
+ indexStatus: "No persisted index data found. Run codebase-index to build it."
13895
+ };
13896
+ }
13897
+ return {
13898
+ symbol: input.symbol,
13899
+ calls: [],
13900
+ total: 0,
13901
+ note: `Symbol "${input.symbol}" not found in the index. Use codebase-search to verify the name.`
13902
+ };
13903
+ }
13904
+ const notes = [];
13905
+ if (totalMatches > limit) {
13906
+ notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
13907
+ }
13908
+ if (unresolvedCount > 0) {
13909
+ notes.push(`${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`);
13910
+ }
13911
+ return {
13912
+ symbol: input.symbol,
13913
+ calls,
13914
+ total: calls.length,
13915
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
13916
+ };
13917
+ }
13918
+ };
13919
+
13528
13920
  // src/codebase-index/codebase-search-tool.ts
13529
13921
  var codebaseSearchTool = {
13530
13922
  name: "codebase-search",
@@ -24898,6 +25290,8 @@ var builtinTools = [
24898
25290
  editTool,
24899
25291
  codebaseStatsTool,
24900
25292
  codebaseSearchTool,
25293
+ codebaseIncomingCallsTool,
25294
+ codebaseOutgoingCallsTool,
24901
25295
  codebaseIndexTool,
24902
25296
  deadCodeScanTool,
24903
25297
  replaceTool,
package/dist/ps-slash.js CHANGED
@@ -626,6 +626,10 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
626
626
  const start = Date.now();
627
627
  const pidStr = String(process.pid);
628
628
  const hostStr = os2.hostname();
629
+ try {
630
+ await fs.mkdir(path.dirname(lockfilePath), { recursive: true });
631
+ } catch {
632
+ }
629
633
  while (Date.now() - start < timeoutMs) {
630
634
  try {
631
635
  await fs.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
package/dist/read.js CHANGED
@@ -2736,6 +2736,136 @@ function mapWriterRefRow(row) {
2736
2736
  }
2737
2737
 
2738
2738
  // src/codebase-index/writer-graph-reader.ts
2739
+ var MAX_SQL_VARS = 900;
2740
+ function chunkedIdQuery(stmt, ids, buildSql, extraArgs = []) {
2741
+ const results = [];
2742
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
2743
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
2744
+ const placeholders = chunk.map(() => "?").join(",");
2745
+ const sql = buildSql(placeholders);
2746
+ results.push(...stmt(sql).all(...chunk, ...extraArgs));
2747
+ }
2748
+ return results;
2749
+ }
2750
+ function chunkedIdScalar(stmt, ids, buildSql, extraArgs = []) {
2751
+ let total = 0;
2752
+ for (let start = 0; start < ids.length; start += MAX_SQL_VARS) {
2753
+ const chunk = ids.slice(start, start + MAX_SQL_VARS);
2754
+ const placeholders = chunk.map(() => "?").join(",");
2755
+ const sql = buildSql(placeholders);
2756
+ const rows = stmt(sql).all(...chunk, ...extraArgs);
2757
+ total += rows[0]?.n ?? 0;
2758
+ }
2759
+ return total;
2760
+ }
2761
+ function mapCallSiteRow(row) {
2762
+ return {
2763
+ symbol: {
2764
+ id: row.sym_id,
2765
+ name: row.sym_name,
2766
+ kind: row.sym_kind,
2767
+ lang: row.sym_lang,
2768
+ file: row.sym_file,
2769
+ line: row.sym_line,
2770
+ signature: row.sym_signature
2771
+ },
2772
+ callType: row.call_type,
2773
+ line: row.ref_line
2774
+ };
2775
+ }
2776
+ function resolveSymbolIds(stmt, symbolName, file) {
2777
+ const baseSql = file ? `SELECT id FROM symbols WHERE name = ? AND file = ? ORDER BY id` : `SELECT id FROM symbols WHERE name = ? ORDER BY id`;
2778
+ const args = file ? [symbolName, file] : [symbolName];
2779
+ const rows = stmt(baseSql).all(...args);
2780
+ return rows.map((r) => r.id);
2781
+ }
2782
+ function findIncomingCallsByName(stmt, symbolName, file, limit) {
2783
+ const targetIds = resolveSymbolIds(stmt, symbolName, file);
2784
+ if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
2785
+ let matchIds = targetIds;
2786
+ let ambiguous = false;
2787
+ if (file !== void 0) {
2788
+ const allNamedIds = resolveSymbolIds(stmt, symbolName, void 0);
2789
+ if (allNamedIds.length > targetIds.length) {
2790
+ matchIds = allNamedIds;
2791
+ ambiguous = true;
2792
+ }
2793
+ }
2794
+ const useFallback = !file;
2795
+ const rows = chunkedIdQuery(
2796
+ stmt,
2797
+ matchIds,
2798
+ (ph) => `SELECT
2799
+ s.id AS sym_id,
2800
+ s.name AS sym_name,
2801
+ s.kind AS sym_kind,
2802
+ s.lang AS sym_lang,
2803
+ s.file AS sym_file,
2804
+ s.line AS sym_line,
2805
+ s.signature AS sym_signature,
2806
+ r.call_type,
2807
+ r.line AS ref_line
2808
+ FROM refs r
2809
+ JOIN symbols s ON s.id = r.from_id
2810
+ WHERE r.to_id IN (${ph})
2811
+ ORDER BY r.line, r.id`,
2812
+ []
2813
+ );
2814
+ if (useFallback) {
2815
+ const fallbackRows = stmt(
2816
+ `SELECT
2817
+ s.id AS sym_id,
2818
+ s.name AS sym_name,
2819
+ s.kind AS sym_kind,
2820
+ s.lang AS sym_lang,
2821
+ s.file AS sym_file,
2822
+ s.line AS sym_line,
2823
+ s.signature AS sym_signature,
2824
+ r.call_type,
2825
+ r.line AS ref_line
2826
+ FROM refs r
2827
+ JOIN symbols s ON s.id = r.from_id
2828
+ WHERE r.to_id IS NULL AND r.to_name = ?
2829
+ ORDER BY r.line, r.id`
2830
+ ).all(symbolName);
2831
+ rows.push(...fallbackRows);
2832
+ }
2833
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
2834
+ const allCalls = rows.map(mapCallSiteRow);
2835
+ return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
2836
+ }
2837
+ function findOutgoingCallsByName(stmt, symbolName, file, limit) {
2838
+ const sourceIds = resolveSymbolIds(stmt, symbolName, file);
2839
+ if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
2840
+ const unresolvedCount = chunkedIdScalar(
2841
+ stmt,
2842
+ sourceIds,
2843
+ (ph) => `SELECT COUNT(*) AS n FROM refs WHERE from_id IN (${ph}) AND to_id IS NULL`
2844
+ );
2845
+ const rows = chunkedIdQuery(
2846
+ stmt,
2847
+ sourceIds,
2848
+ (ph) => `SELECT
2849
+ s.id AS sym_id,
2850
+ s.name AS sym_name,
2851
+ s.kind AS sym_kind,
2852
+ s.lang AS sym_lang,
2853
+ s.file AS sym_file,
2854
+ s.line AS sym_line,
2855
+ s.signature AS sym_signature,
2856
+ r.call_type,
2857
+ r.line AS ref_line
2858
+ FROM refs r
2859
+ JOIN symbols s ON s.id = r.to_id
2860
+ WHERE r.from_id IN (${ph})
2861
+ AND r.to_id IS NOT NULL -- INNER JOIN already excludes NULL to_id; this is defensive belt-and-suspenders
2862
+ ORDER BY r.line, r.id`,
2863
+ []
2864
+ );
2865
+ rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
2866
+ const calls = rows.map(mapCallSiteRow).slice(0, limit);
2867
+ return { calls, symbolFound: true, unresolvedCount, totalMatches: rows.length };
2868
+ }
2739
2869
  function findRefsToWithStatement(stmt, symbolId) {
2740
2870
  return stmt(
2741
2871
  "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 = ?)"
@@ -3971,6 +4101,20 @@ var IndexStore = class _IndexStore {
3971
4101
  return false;
3972
4102
  }
3973
4103
  }
4104
+ /**
4105
+ * Find all symbols that reference the named target symbol (incoming callers).
4106
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
4107
+ */
4108
+ findIncomingCallsByName(symbolName, file, limit = 100) {
4109
+ return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
4110
+ }
4111
+ /**
4112
+ * Find all symbols that the named source symbol references (outgoing callees).
4113
+ * Accepts a name instead of an id so the agent doesn't need a prior lookup.
4114
+ */
4115
+ findOutgoingCallsByName(symbolName, file, limit = 100) {
4116
+ return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
4117
+ }
3974
4118
  /**
3975
4119
  * Find all references TO a given symbol (who calls / uses this symbol?).
3976
4120
  */
@@ -4555,6 +4699,22 @@ function symbolGraphService(args) {
4555
4699
  indexStorePool.release(store);
4556
4700
  }
4557
4701
  }
4702
+ function incomingCallsService(args) {
4703
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4704
+ try {
4705
+ return store.findIncomingCallsByName(args.symbol, args.file, args.limit ?? 100);
4706
+ } finally {
4707
+ indexStorePool.release(store);
4708
+ }
4709
+ }
4710
+ function outgoingCallsService(args) {
4711
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4712
+ try {
4713
+ return store.findOutgoingCallsByName(args.symbol, args.file, args.limit ?? 100);
4714
+ } finally {
4715
+ indexStorePool.release(store);
4716
+ }
4717
+ }
4558
4718
 
4559
4719
  // src/codebase-index/project-server-client.ts
4560
4720
  import { spawn as spawn4 } from "node:child_process";
@@ -5456,6 +5616,10 @@ async function callInline(op, args, opts) {
5456
5616
  return fileGraphService(args);
5457
5617
  case "symbolGraph":
5458
5618
  return symbolGraphService(args);
5619
+ case "incomingCalls":
5620
+ return incomingCallsService(args);
5621
+ case "outgoingCalls":
5622
+ return outgoingCallsService(args);
5459
5623
  default:
5460
5624
  throw new Error(`unknown index op: ${String(op)}`);
5461
5625
  }