@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/tool-tier.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",
@@ -24896,6 +25288,8 @@ var TIER1_TOOLS = [
24896
25288
  editTool,
24897
25289
  codebaseStatsTool,
24898
25290
  codebaseSearchTool,
25291
+ codebaseIncomingCallsTool,
25292
+ codebaseOutgoingCallsTool,
24899
25293
  codebaseIndexTool,
24900
25294
  bashTool,
24901
25295
  grepTool,
@@ -24946,6 +25340,8 @@ var builtinTools = [
24946
25340
  editTool,
24947
25341
  codebaseStatsTool,
24948
25342
  codebaseSearchTool,
25343
+ codebaseIncomingCallsTool,
25344
+ codebaseOutgoingCallsTool,
24949
25345
  codebaseIndexTool,
24950
25346
  deadCodeScanTool,
24951
25347
  replaceTool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/tools",
3
- "version": "0.298.2",
3
+ "version": "0.299.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack built-in tools: read/write/edit, bash/exec, grep/glob, git, fetch, test, lint, and more.",
6
6
  "repository": {
@@ -250,8 +250,8 @@
250
250
  "@typescript/typescript6": "^6.0.2",
251
251
  "turndown": "^7.2.4",
252
252
  "undici": "^8.9.0",
253
- "@wrongstack/core": "0.298.2",
254
- "@wrongstack/kanban": "0.298.2"
253
+ "@wrongstack/core": "0.299.0",
254
+ "@wrongstack/kanban": "0.299.0"
255
255
  },
256
256
  "devDependencies": {
257
257
  "@types/node": "^26.1.2",