@wrongstack/tools 0.73.1 → 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.
@@ -11,6 +11,8 @@ interface CodebaseIndexOutput {
11
11
  langStats: Record<string, number>;
12
12
  durationMs: number;
13
13
  errors: string[];
14
+ /** Advisory note when the indexer was skipped (e.g. another index in progress). */
15
+ note?: string;
14
16
  }
15
17
 
16
18
  /** Language a symbol belongs to. */
@@ -122,6 +124,8 @@ interface CodebaseSearchOutput {
122
124
  results: SearchResult[];
123
125
  total: number;
124
126
  query: string;
127
+ /** Non-empty when the index blocked the search (not ready, indexing, failed). */
128
+ indexStatus?: string;
125
129
  }
126
130
 
127
131
  /**
@@ -142,6 +146,8 @@ interface CodebaseStatsOutput {
142
146
  sizeBytes: number;
143
147
  indexPath: string;
144
148
  version: number;
149
+ /** Non-empty when the index is not ready or is still building. */
150
+ indexStatus?: string;
145
151
  }
146
152
 
147
153
  /**
@@ -159,17 +165,43 @@ interface CodebaseStatsOutput {
159
165
  * 2. **Debounce** — rapid successive edits to the same file (editor autosave,
160
166
  * multi-edit) coalesce into a single reindex, keyed per `(indexDir, file)`.
161
167
  *
168
+ * 3. **State tracking** — exposes whether the initial index has completed (`ready`)
169
+ * and whether a build is in progress (`indexing`), so downstream tools
170
+ * (codebase-search, codebase-stats) can gate on it and UIs can show progress.
171
+ *
162
172
  * `runIndexer` only reads `opts` (and ignores its `_ctx` parameter), so callers
163
173
  * outside the agent loop pass a minimal stub cast to the expected shape — no
164
174
  * live agent `Context` is required.
165
175
  */
166
176
 
177
+ /** True once the first full-project index has completed (success or failure). */
178
+ declare function isIndexReady(): boolean;
179
+ /** True while an index build is actively running. */
180
+ declare function isIndexing(): boolean;
181
+ /** Current indexing progress: { currentFile, totalFiles, ready, indexing }. */
182
+ declare function getIndexState(): {
183
+ ready: boolean;
184
+ indexing: boolean;
185
+ currentFile: number;
186
+ totalFiles: number;
187
+ lastError: string | null;
188
+ };
189
+ /**
190
+ * Optional callback fired on every lifecycle transition (started, progress,
191
+ * completed, failed). Plug into the event bus or a TUI dispatcher to surface
192
+ * the indexing state in real time.
193
+ */
194
+ type IndexStateListener = (state: ReturnType<typeof getIndexState>) => void;
195
+ declare function onIndexStateChange(listener: IndexStateListener): () => void;
167
196
  /** True when the file's extension maps to a language the indexer can parse. */
168
197
  declare function isIndexableFile(filePath: string): boolean;
169
198
  /**
170
199
  * Run a full-project scan and await it. Used at session start and by the manual
171
200
  * `/codebase-reindex` command. Incremental by default (unchanged files skipped
172
201
  * via mtime, so repeat runs are cheap); pass `force` to clear and rebuild.
202
+ *
203
+ * Sets the global `_ready` flag on completion so downstream tools know the
204
+ * index is usable.
173
205
  */
174
206
  declare function runStartupIndex(opts: {
175
207
  projectRoot: string;
@@ -192,4 +224,4 @@ declare function enqueueReindex(opts: {
192
224
  /** Cancel all pending debounced reindexes. For teardown / tests. */
193
225
  declare function cancelPendingReindexes(): void;
194
226
 
195
- export { type FileMeta as F, type IndexResult as I, type Ref as R, SCHEMA_VERSION as S, type FileSymbols as a, type IndexStats as b, type SearchResult as c, type Symbol as d, type SymbolKind as e, type SymbolLang as f, cancelPendingReindexes as g, codebaseIndexTool as h, codebaseSearchTool as i, codebaseStatsTool as j, enqueueReindex as k, isIndexableFile as l, runStartupIndex as r };
227
+ export { type FileMeta as F, type IndexResult as I, type Ref as R, SCHEMA_VERSION as S, type FileSymbols as a, type IndexStats as b, type SearchResult as c, type Symbol as d, type SymbolKind as e, type SymbolLang as f, cancelPendingReindexes as g, codebaseIndexTool as h, codebaseSearchTool as i, codebaseStatsTool as j, enqueueReindex as k, getIndexState as l, isIndexReady as m, isIndexableFile as n, isIndexing as o, onIndexStateChange as p, runStartupIndex as r };
package/dist/builtin.js CHANGED
@@ -2676,7 +2676,41 @@ async function loadGitignoreMatcher(projectRoot) {
2676
2676
  return compileGitignore(lines);
2677
2677
  }
2678
2678
 
2679
+ // src/codebase-index/background-indexer.ts
2680
+ var _ready = false;
2681
+ var _indexing = false;
2682
+ var _currentFile = 0;
2683
+ var _totalFiles = 0;
2684
+ var _lastError = null;
2685
+ function setIndexReady() {
2686
+ _ready = true;
2687
+ }
2688
+ function getIndexState() {
2689
+ return {
2690
+ ready: _ready,
2691
+ indexing: _indexing,
2692
+ currentFile: _currentFile,
2693
+ totalFiles: _totalFiles,
2694
+ lastError: _lastError
2695
+ };
2696
+ }
2697
+ var _listeners = [];
2698
+ function emitState() {
2699
+ const state = getIndexState();
2700
+ for (const l of _listeners) l(state);
2701
+ }
2702
+ function _setIndexProgress(current, total) {
2703
+ _currentFile = current;
2704
+ _totalFiles = total;
2705
+ emitState();
2706
+ }
2707
+ Promise.resolve();
2708
+
2679
2709
  // src/codebase-index/indexer.ts
2710
+ var YIELD_EVERY_N = 50;
2711
+ function yieldEventLoop() {
2712
+ return new Promise((resolve7) => setImmediate(resolve7));
2713
+ }
2680
2714
  var DEFAULT_IGNORE = [
2681
2715
  "node_modules",
2682
2716
  ".git",
@@ -2780,7 +2814,12 @@ async function runIndexer(_ctx, opts) {
2780
2814
  if (!force) {
2781
2815
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
2782
2816
  }
2783
- for (const file of files) {
2817
+ for (let fi = 0; fi < files.length; fi++) {
2818
+ const file = files[fi];
2819
+ _setIndexProgress(fi + 1, files.length);
2820
+ if (fi > 0 && fi % YIELD_EVERY_N === 0) {
2821
+ await yieldEventLoop();
2822
+ }
2784
2823
  let stat10;
2785
2824
  try {
2786
2825
  stat10 = await fs12.stat(file);
@@ -2900,6 +2939,7 @@ var codebaseIndexTool = {
2900
2939
  langs: input.langs,
2901
2940
  indexDir: codebaseIndexDirOverride(ctx)
2902
2941
  });
2942
+ setIndexReady();
2903
2943
  return result;
2904
2944
  }
2905
2945
  };
@@ -3035,6 +3075,31 @@ var codebaseSearchTool = {
3035
3075
  required: ["query"]
3036
3076
  },
3037
3077
  async execute(input, ctx) {
3078
+ const state = getIndexState();
3079
+ if (!state.ready) {
3080
+ return {
3081
+ results: [],
3082
+ total: 0,
3083
+ query: input.query,
3084
+ 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."
3085
+ };
3086
+ }
3087
+ if (state.indexing) {
3088
+ return {
3089
+ results: [],
3090
+ total: 0,
3091
+ query: input.query,
3092
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
3093
+ };
3094
+ }
3095
+ if (state.lastError) {
3096
+ return {
3097
+ results: [],
3098
+ total: 0,
3099
+ query: input.query,
3100
+ indexStatus: `Index build failed: ${state.lastError}. Try /codebase-reindex.`
3101
+ };
3102
+ }
3038
3103
  const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
3039
3104
  try {
3040
3105
  const limit = Math.min(input.limit ?? 20, 100);
@@ -3092,6 +3157,32 @@ var codebaseStatsTool = {
3092
3157
  additionalProperties: false
3093
3158
  },
3094
3159
  async execute(_input, ctx) {
3160
+ const idxState = getIndexState();
3161
+ if (!idxState.ready) {
3162
+ return {
3163
+ totalSymbols: 0,
3164
+ totalFiles: 0,
3165
+ byLang: {},
3166
+ byKind: {},
3167
+ lastIndexed: null,
3168
+ sizeBytes: 0,
3169
+ indexPath: "",
3170
+ version: SCHEMA_VERSION,
3171
+ indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
3172
+ };
3173
+ }
3174
+ if (idxState.indexing) {
3175
+ const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
3176
+ try {
3177
+ const stats = store2.getStats();
3178
+ return {
3179
+ ...stats,
3180
+ indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
3181
+ };
3182
+ } finally {
3183
+ store2.close();
3184
+ }
3185
+ }
3095
3186
  const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
3096
3187
  try {
3097
3188
  const stats = store.getStats();
@@ -6589,9 +6680,10 @@ var todoTool = {
6589
6680
  name: "todo",
6590
6681
  category: "Session",
6591
6682
  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).",
6592
- 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.",
6683
+ 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.",
6593
6684
  permission: "auto",
6594
6685
  mutating: false,
6686
+ // mutates only conversation state (ctx.todos), not external state — no confirmation needed
6595
6687
  timeoutMs: 1e3,
6596
6688
  inputSchema: {
6597
6689
  type: "object",