@wrongstack/tools 0.68.0 → 0.77.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.d.ts CHANGED
@@ -33,7 +33,7 @@ export { forgetTool, rememberTool } from './memory.js';
33
33
  export { createModeTool } from './mode.js';
34
34
  export { KillOpts, RegistryStats, TrackedProcess, _resetProcessRegistry, getProcessRegistry } from './process-registry.js';
35
35
  export { CircuitBreaker, CircuitBreakerConfig, CircuitBreakerSnapshot } from './circuit-breaker.js';
36
- export { g as codebaseIndexTool, h as codebaseSearchTool, i as codebaseStatsTool } from './codebase-stats-tool-C8ApERbn.js';
36
+ export { g as cancelPendingReindexes, h as codebaseIndexTool, i as codebaseSearchTool, j as codebaseStatsTool, k as enqueueReindex, l as getIndexState, m as isIndexReady, n as isIndexableFile, o as isIndexing, p as onIndexStateChange, r as runStartupIndex } from './background-indexer-C70RD7LU.js';
37
37
  export { builtinTools } from './builtin.js';
38
38
  export { builtinToolsPack } from './pack.js';
39
39
  import 'node:child_process';
package/dist/index.js CHANGED
@@ -2463,9 +2463,10 @@ var todoTool = {
2463
2463
  name: "todo",
2464
2464
  category: "Session",
2465
2465
  description: "Manage the session-level todo list. This is the primary mechanism for tracking multi-step work. The list is fully replaced on every call (not appended).",
2466
- usageHint: "BEST PRACTICE for complex tasks:\n- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\n- Only **one** item should be `in_progress` at any time.\n- Update the list frequently as work progresses (mark items done, add new ones, change status).\n- The system and user can see this list, so keep it honest and up-to-date.\nThis tool is extremely valuable for maintaining focus and giving the user visibility into your plan.",
2466
+ usageHint: "BEST PRACTICE for complex tasks:\n- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\n- Only **one** item should be `in_progress` at any time.\n- Update the list frequently as work progresses (mark items done, add new ones, change status).\n- **Re-order items** to reflect current priorities \u2014 the full list is replaced each call, so item order is entirely under your control.\n- When all items are completed the board auto-clears \u2014 you do NOT need to send an empty list.\n- The system and user can see this list, so keep it honest and up-to-date.\nThis tool is extremely valuable for maintaining focus and giving the user visibility into your plan.",
2467
2467
  permission: "auto",
2468
2468
  mutating: false,
2469
+ // mutates only conversation state (ctx.todos), not external state — no confirmation needed
2469
2470
  timeoutMs: 1e3,
2470
2471
  inputSchema: {
2471
2472
  type: "object",
@@ -3112,8 +3113,8 @@ var jsonTool = {
3112
3113
  };
3113
3114
  }
3114
3115
  };
3115
- function query(data, path18) {
3116
- const parts = path18.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
3116
+ function query(data, path19) {
3117
+ const parts = path19.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
3117
3118
  let current = data;
3118
3119
  for (const part of parts) {
3119
3120
  if (current === null || current === void 0) return void 0;
@@ -4404,7 +4405,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
4404
4405
  }
4405
4406
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
4406
4407
  var MAX_TAIL_LINES = 1e5;
4407
- async function fileLogs(path18, lines, filterRe, stream) {
4408
+ async function fileLogs(path19, lines, filterRe, stream) {
4408
4409
  const { createInterface } = await import('node:readline');
4409
4410
  const { createReadStream } = await import('node:fs');
4410
4411
  const entries = [];
@@ -4413,7 +4414,7 @@ async function fileLogs(path18, lines, filterRe, stream) {
4413
4414
  let writeIdx = 0;
4414
4415
  let totalLines = 0;
4415
4416
  const rl = createInterface({
4416
- input: createReadStream(path18),
4417
+ input: createReadStream(path19),
4417
4418
  crlfDelay: Number.POSITIVE_INFINITY
4418
4419
  });
4419
4420
  for await (const line of rl) {
@@ -4434,7 +4435,7 @@ async function fileLogs(path18, lines, filterRe, stream) {
4434
4435
  if (parsed) entries.push(parsed);
4435
4436
  }
4436
4437
  return {
4437
- source: path18,
4438
+ source: path19,
4438
4439
  entries,
4439
4440
  total: entries.length,
4440
4441
  truncated: totalLines > effLines,
@@ -4874,10 +4875,14 @@ var toolSearchTool = {
4874
4875
  permission: t.permission,
4875
4876
  mutating: t.mutating
4876
4877
  }));
4878
+ const totalAvailable = tools.length;
4879
+ const hint = results.length === 0 && query2 ? `No tools matched "${input.query}". Use tool-help (without arguments) to see all ${totalAvailable} available tools.` : void 0;
4877
4880
  return {
4878
4881
  tools: results,
4879
4882
  total: filtered.length,
4880
- truncated: filtered.length > limit
4883
+ truncated: filtered.length > limit,
4884
+ ...hint ? { hint } : {},
4885
+ _available: totalAvailable
4881
4886
  };
4882
4887
  }
4883
4888
  };
@@ -5565,6 +5570,17 @@ var IndexStore = class {
5565
5570
  ({ id, text }) => ({ id, text })
5566
5571
  );
5567
5572
  }
5573
+ /**
5574
+ * Largest symbol id currently in the table (0 when empty). New ids must be
5575
+ * allocated from this, NOT from `COUNT(*)`: incremental reindexes delete a
5576
+ * changed file's rows, so the row count drops below the max id and a
5577
+ * count-based id would collide with a surviving row (UNIQUE constraint on
5578
+ * `symbols.id`). Ids may have gaps — that is fine.
5579
+ */
5580
+ getMaxSymbolId() {
5581
+ const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
5582
+ return rows[0]?.m ?? 0;
5583
+ }
5568
5584
  // ─── Stats ───────────────────────────────────────────────────────────────────
5569
5585
  getStats() {
5570
5586
  const sizeBytes = this.sizeBytes();
@@ -6883,8 +6899,181 @@ function makeSymbol2(opts) {
6883
6899
  text: `${opts.name} ${opts.signature}`.trim()
6884
6900
  };
6885
6901
  }
6902
+ function globBody(glob) {
6903
+ return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
6904
+ }
6905
+ function compileGitignore(lines) {
6906
+ const rules = [];
6907
+ for (const raw of lines) {
6908
+ let line = raw.replace(/\r$/, "");
6909
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
6910
+ line = line.trim();
6911
+ let negated = false;
6912
+ if (line.startsWith("!")) {
6913
+ negated = true;
6914
+ line = line.slice(1);
6915
+ }
6916
+ let dirOnly = false;
6917
+ if (line.endsWith("/")) {
6918
+ dirOnly = true;
6919
+ line = line.slice(0, -1);
6920
+ }
6921
+ if (!line) continue;
6922
+ const anchored = line.startsWith("/") || line.includes("/");
6923
+ if (line.startsWith("/")) line = line.slice(1);
6924
+ const body = globBody(line);
6925
+ const prefix = anchored ? "^" : "(?:^|.*/)";
6926
+ rules.push({
6927
+ eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
6928
+ under: new RegExp(`${prefix}${body}/.*$`),
6929
+ negated,
6930
+ dirOnly
6931
+ });
6932
+ }
6933
+ return (relPath, isDir) => {
6934
+ const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
6935
+ let ignored = false;
6936
+ for (const r of rules) {
6937
+ const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
6938
+ if (re.test(p)) ignored = !r.negated;
6939
+ }
6940
+ return ignored;
6941
+ };
6942
+ }
6943
+ async function loadGitignoreMatcher(projectRoot) {
6944
+ let lines = [];
6945
+ try {
6946
+ const raw = await fs4.readFile(path.join(projectRoot, ".gitignore"), "utf8");
6947
+ lines = raw.split("\n");
6948
+ } catch {
6949
+ }
6950
+ return compileGitignore(lines);
6951
+ }
6952
+
6953
+ // src/codebase-index/background-indexer.ts
6954
+ var _ready = false;
6955
+ var _indexing = false;
6956
+ var _currentFile = 0;
6957
+ var _totalFiles = 0;
6958
+ var _lastError = null;
6959
+ function isIndexReady() {
6960
+ return _ready;
6961
+ }
6962
+ function setIndexReady() {
6963
+ _ready = true;
6964
+ }
6965
+ function isIndexing() {
6966
+ return _indexing;
6967
+ }
6968
+ function getIndexState() {
6969
+ return {
6970
+ ready: _ready,
6971
+ indexing: _indexing,
6972
+ currentFile: _currentFile,
6973
+ totalFiles: _totalFiles,
6974
+ lastError: _lastError
6975
+ };
6976
+ }
6977
+ var _listeners = [];
6978
+ function onIndexStateChange(listener) {
6979
+ _listeners.push(listener);
6980
+ return () => {
6981
+ _listeners = _listeners.filter((l) => l !== listener);
6982
+ };
6983
+ }
6984
+ function emitState() {
6985
+ const state = getIndexState();
6986
+ for (const l of _listeners) l(state);
6987
+ }
6988
+ function _setIndexProgress(current, total) {
6989
+ _currentFile = current;
6990
+ _totalFiles = total;
6991
+ emitState();
6992
+ }
6993
+ function stubCtx(projectRoot) {
6994
+ return {
6995
+ projectRoot,
6996
+ cwd: projectRoot,
6997
+ messages: [],
6998
+ todos: [],
6999
+ readFiles: /* @__PURE__ */ new Set(),
7000
+ fileMtimes: /* @__PURE__ */ new Map()
7001
+ };
7002
+ }
7003
+ var chain = Promise.resolve();
7004
+ function withMutex(job) {
7005
+ const run = chain.then(job, job);
7006
+ chain = run.then(
7007
+ () => void 0,
7008
+ () => void 0
7009
+ );
7010
+ return run;
7011
+ }
7012
+ var DEFAULT_DEBOUNCE_MS = 400;
7013
+ var debounceTimers = /* @__PURE__ */ new Map();
7014
+ function debounceKey(indexDir, file) {
7015
+ return `${indexDir ?? ""}|${file}`;
7016
+ }
7017
+ function isIndexableFile(filePath) {
7018
+ return detectLang(filePath) !== null;
7019
+ }
7020
+ async function runStartupIndex(opts) {
7021
+ _indexing = true;
7022
+ _currentFile = 0;
7023
+ _totalFiles = 0;
7024
+ _lastError = null;
7025
+ emitState();
7026
+ try {
7027
+ const result = await withMutex(
7028
+ () => runIndexer(stubCtx(opts.projectRoot), {
7029
+ projectRoot: opts.projectRoot,
7030
+ indexDir: opts.indexDir,
7031
+ force: opts.force
7032
+ })
7033
+ );
7034
+ _ready = true;
7035
+ return result;
7036
+ } catch (err) {
7037
+ _lastError = err instanceof Error ? err.message : String(err);
7038
+ _ready = true;
7039
+ throw err;
7040
+ } finally {
7041
+ _indexing = false;
7042
+ emitState();
7043
+ }
7044
+ }
7045
+ function enqueueReindex(opts) {
7046
+ const files = opts.files.filter(isIndexableFile);
7047
+ if (files.length === 0) return;
7048
+ const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
7049
+ for (const file of files) {
7050
+ const key = debounceKey(opts.indexDir, file);
7051
+ const existing = debounceTimers.get(key);
7052
+ if (existing) clearTimeout(existing);
7053
+ const timer = setTimeout(() => {
7054
+ debounceTimers.delete(key);
7055
+ void withMutex(
7056
+ () => runIndexer(stubCtx(opts.projectRoot), {
7057
+ projectRoot: opts.projectRoot,
7058
+ files: [file],
7059
+ indexDir: opts.indexDir
7060
+ })
7061
+ ).catch((err) => opts.onError?.(err));
7062
+ }, ms);
7063
+ timer.unref?.();
7064
+ debounceTimers.set(key, timer);
7065
+ }
7066
+ }
7067
+ function cancelPendingReindexes() {
7068
+ for (const t of debounceTimers.values()) clearTimeout(t);
7069
+ debounceTimers.clear();
7070
+ }
6886
7071
 
6887
7072
  // src/codebase-index/indexer.ts
7073
+ var YIELD_EVERY_N = 50;
7074
+ function yieldEventLoop() {
7075
+ return new Promise((resolve7) => setImmediate(resolve7));
7076
+ }
6888
7077
  var DEFAULT_IGNORE5 = [
6889
7078
  "node_modules",
6890
7079
  ".git",
@@ -6896,7 +7085,7 @@ var DEFAULT_IGNORE5 = [
6896
7085
  "__snapshots__",
6897
7086
  ".nyc_output"
6898
7087
  ];
6899
- async function findSourceFiles(projectRoot, ignore) {
7088
+ async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
6900
7089
  const results = [];
6901
7090
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE5, ...ignore]);
6902
7091
  const globs = [
@@ -6921,10 +7110,12 @@ async function findSourceFiles(projectRoot, ignore) {
6921
7110
  for (const e of entries) {
6922
7111
  if (ignoreSet.has(e.name)) continue;
6923
7112
  const full = path.join(dir, e.name);
7113
+ const rel = path.relative(projectRoot, full).replace(/\\/g, "/");
6924
7114
  if (e.isDirectory()) {
7115
+ if (isGitIgnored(rel, true)) continue;
6925
7116
  await walk(full);
6926
7117
  } else if (e.isFile()) {
6927
- const rel = path.relative(projectRoot, full).replace(/\\/g, "/");
7118
+ if (isGitIgnored(rel, false)) continue;
6928
7119
  const ext = path.extname(e.name);
6929
7120
  for (const { ext: extName, pat } of globs) {
6930
7121
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
@@ -6967,11 +7158,12 @@ async function runIndexer(_ctx, opts) {
6967
7158
  const langStats = {};
6968
7159
  let filesIndexed = 0;
6969
7160
  let symbolsIndexed = 0;
7161
+ const isGitIgnored = await loadGitignoreMatcher(projectRoot);
6970
7162
  let files;
6971
7163
  if (opts.files && opts.files.length > 0) {
6972
- files = opts.files.map((f) => path.resolve(projectRoot, f));
7164
+ files = opts.files.map((f) => path.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path.relative(projectRoot, f).replace(/\\/g, "/"), false));
6973
7165
  } else {
6974
- files = await findSourceFiles(projectRoot, ignore);
7166
+ files = await findSourceFiles(projectRoot, ignore, isGitIgnored);
6975
7167
  }
6976
7168
  if (langs && langs.length > 0) {
6977
7169
  const langSet = new Set(langs);
@@ -6985,7 +7177,12 @@ async function runIndexer(_ctx, opts) {
6985
7177
  if (!force) {
6986
7178
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
6987
7179
  }
6988
- for (const file of files) {
7180
+ for (let fi = 0; fi < files.length; fi++) {
7181
+ const file = files[fi];
7182
+ _setIndexProgress(fi + 1, files.length);
7183
+ if (fi > 0 && fi % YIELD_EVERY_N === 0) {
7184
+ await yieldEventLoop();
7185
+ }
6989
7186
  let stat10;
6990
7187
  try {
6991
7188
  stat10 = await fs4.stat(file);
@@ -7003,8 +7200,8 @@ async function runIndexer(_ctx, opts) {
7003
7200
  filesIndexed++;
7004
7201
  continue;
7005
7202
  }
7006
- store.deleteSymbolsForFile(file);
7007
7203
  store.deleteRefsForFile(file);
7204
+ store.deleteSymbolsForFile(file);
7008
7205
  let content;
7009
7206
  try {
7010
7207
  content = await fs4.readFile(file, "utf8");
@@ -7030,7 +7227,7 @@ async function runIndexer(_ctx, opts) {
7030
7227
  filesIndexed++;
7031
7228
  continue;
7032
7229
  }
7033
- const nextId = store.getStats().totalSymbols + 1;
7230
+ const nextId = store.getMaxSymbolId() + 1;
7034
7231
  const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
7035
7232
  store.insertSymbols(symbolsWithIds, nextId);
7036
7233
  const count = symbolsWithIds.length;
@@ -7099,12 +7296,23 @@ var codebaseIndexTool = {
7099
7296
  }
7100
7297
  },
7101
7298
  async execute(input, ctx) {
7299
+ if (isIndexing()) {
7300
+ return {
7301
+ filesIndexed: 0,
7302
+ symbolsIndexed: 0,
7303
+ langStats: {},
7304
+ durationMs: 0,
7305
+ errors: [],
7306
+ note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
7307
+ };
7308
+ }
7102
7309
  const result = await runIndexer(ctx, {
7103
7310
  projectRoot: ctx.projectRoot,
7104
7311
  force: input.force ?? false,
7105
7312
  langs: input.langs,
7106
7313
  indexDir: codebaseIndexDirOverride(ctx)
7107
7314
  });
7315
+ setIndexReady();
7108
7316
  return result;
7109
7317
  }
7110
7318
  };
@@ -7240,6 +7448,31 @@ var codebaseSearchTool = {
7240
7448
  required: ["query"]
7241
7449
  },
7242
7450
  async execute(input, ctx) {
7451
+ const state = getIndexState();
7452
+ if (!state.ready) {
7453
+ return {
7454
+ results: [],
7455
+ total: 0,
7456
+ query: input.query,
7457
+ indexStatus: state.indexing ? `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.` : "Index not yet built. The codebase is being indexed at startup \u2014 search will be available shortly."
7458
+ };
7459
+ }
7460
+ if (state.indexing) {
7461
+ return {
7462
+ results: [],
7463
+ total: 0,
7464
+ query: input.query,
7465
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
7466
+ };
7467
+ }
7468
+ if (state.lastError) {
7469
+ return {
7470
+ results: [],
7471
+ total: 0,
7472
+ query: input.query,
7473
+ indexStatus: `Index build failed: ${state.lastError}. Try /codebase-reindex.`
7474
+ };
7475
+ }
7243
7476
  const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
7244
7477
  try {
7245
7478
  const limit = Math.min(input.limit ?? 20, 100);
@@ -7297,6 +7530,32 @@ var codebaseStatsTool = {
7297
7530
  additionalProperties: false
7298
7531
  },
7299
7532
  async execute(_input, ctx) {
7533
+ const idxState = getIndexState();
7534
+ if (!idxState.ready) {
7535
+ return {
7536
+ totalSymbols: 0,
7537
+ totalFiles: 0,
7538
+ byLang: {},
7539
+ byKind: {},
7540
+ lastIndexed: null,
7541
+ sizeBytes: 0,
7542
+ indexPath: "",
7543
+ version: SCHEMA_VERSION,
7544
+ indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
7545
+ };
7546
+ }
7547
+ if (idxState.indexing) {
7548
+ const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
7549
+ try {
7550
+ const stats = store2.getStats();
7551
+ return {
7552
+ ...stats,
7553
+ indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
7554
+ };
7555
+ } finally {
7556
+ store2.close();
7557
+ }
7558
+ }
7300
7559
  const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
7301
7560
  try {
7302
7561
  const stats = store.getStats();
@@ -7361,6 +7620,6 @@ var builtinToolsPack = {
7361
7620
  tools: builtinTools
7362
7621
  };
7363
7622
 
7364
- export { CircuitBreaker, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, execTool, fetchTool, forgetTool, formatTool, getProcessRegistry, gitTool, globTool, grepTool, installTool, jsonTool, lintTool, logsTool, outdatedTool, patchTool, planTool, readTool, rememberTool, replaceTool, scaffoldTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
7623
+ export { CircuitBreaker, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, rememberTool, replaceTool, runStartupIndex, scaffoldTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
7365
7624
  //# sourceMappingURL=index.js.map
7366
7625
  //# sourceMappingURL=index.js.map