@wrongstack/plugins 0.283.1 → 0.284.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.
@@ -1,4 +1,4 @@
1
- import { statSync, readFileSync, readdirSync } from 'fs';
1
+ import * as fs from 'fs/promises';
2
2
  import { isAbsolute, resolve, relative } from 'path';
3
3
 
4
4
  // src/semantic-search-indexer/index.ts
@@ -11,7 +11,8 @@ var state = {
11
11
  bytesIndexed: 0,
12
12
  truncated: false,
13
13
  queryCount: 0,
14
- reindexCount: 0};
14
+ reindexCount: 0,
15
+ buildPromise: null};
15
16
  var DEFAULTS = {
16
17
  enabled: true,
17
18
  includeExtensions: [
@@ -111,26 +112,16 @@ function shouldIndexFile(filePath, cfg) {
111
112
  }
112
113
  return false;
113
114
  }
114
- function indexFile(absPath, relPath, cfg) {
115
- if (!state.index) return;
116
- if (!shouldIndexFile(relPath, cfg)) return;
117
- let stats;
118
- try {
119
- stats = statSync(absPath);
120
- } catch {
121
- return;
122
- }
123
- if (!stats.isFile() || stats.size > cfg.maxFileBytes) return;
124
- let content;
125
- try {
126
- content = readFileSync(absPath, "utf-8");
127
- } catch {
128
- return;
129
- }
130
- if (content.includes("\0")) return;
115
+ var INDEX_BATCH_SIZE = 32;
116
+ var YIELD_EVERY_FILES = 64;
117
+ function yieldEventLoop() {
118
+ return new Promise((resolve2) => setImmediate(resolve2));
119
+ }
120
+ function addFileToIndex(relPath, content, size, cfg) {
121
+ if (!state.index || content.includes("\0")) return;
131
122
  state.bytesIndexed += content.length;
132
123
  const lines = content.split(/\r?\n/);
133
- state.index.files.set(relPath, { lines, size: stats.size });
124
+ state.index.files.set(relPath, { lines, size });
134
125
  for (let i = 0; i < lines.length; i += 1) {
135
126
  const terms = tokenize(lines[i], cfg.minTokenLength);
136
127
  for (const term of terms) {
@@ -148,10 +139,27 @@ function indexFile(absPath, relPath, cfg) {
148
139
  }
149
140
  }
150
141
  }
151
- function walkDirectory(absPath, cfg, excludes) {
142
+ async function indexFileFromStats(absPath, relPath, stats, cfg) {
143
+ if (!shouldIndexFile(relPath, cfg) || !stats.isFile() || stats.size > cfg.maxFileBytes) return;
144
+ let content;
145
+ try {
146
+ content = await fs.readFile(absPath, "utf-8");
147
+ } catch {
148
+ return;
149
+ }
150
+ addFileToIndex(relPath, content, stats.size, cfg);
151
+ }
152
+ async function flushFileBatch(batch, cfg) {
153
+ if (batch.length === 0) return;
154
+ const current = batch.splice(0, batch.length);
155
+ await Promise.allSettled(
156
+ current.map(({ absPath, relPath, stats }) => indexFileFromStats(absPath, relPath, stats, cfg))
157
+ );
158
+ }
159
+ async function walkDirectory(absPath, cfg, excludes, fileBatch) {
152
160
  let entries;
153
161
  try {
154
- entries = readdirSync(absPath, { withFileTypes: true });
162
+ entries = await fs.readdir(absPath, { withFileTypes: true });
155
163
  } catch {
156
164
  return;
157
165
  }
@@ -166,15 +174,28 @@ function walkDirectory(absPath, cfg, excludes) {
166
174
  if (relChild === "" || relChild === ".") continue;
167
175
  if (excludes.some((re) => re.test(relChild))) continue;
168
176
  if (ent.isDirectory()) {
169
- walkDirectory(absChild, cfg, excludes);
177
+ await walkDirectory(absChild, cfg, excludes, fileBatch);
170
178
  if (state.truncated) return;
171
179
  } else if (ent.isFile()) {
172
- indexFile(absChild, relChild, cfg);
173
180
  state.fileCount += 1;
181
+ if (!shouldIndexFile(relChild, cfg)) continue;
182
+ let stats;
183
+ try {
184
+ stats = await fs.stat(absChild);
185
+ } catch {
186
+ continue;
187
+ }
188
+ fileBatch.push({ absPath: absChild, relPath: relChild, stats });
189
+ if (fileBatch.length >= INDEX_BATCH_SIZE) {
190
+ await flushFileBatch(fileBatch, cfg);
191
+ }
192
+ if (state.fileCount % YIELD_EVERY_FILES === 0) {
193
+ await yieldEventLoop();
194
+ }
174
195
  }
175
196
  }
176
197
  }
177
- function buildIndex(rootPath, cfg) {
198
+ async function buildIndex(rootPath, cfg) {
178
199
  state.index = { terms: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map() };
179
200
  state.cachedPath = rootPath;
180
201
  state.fileCount = 0;
@@ -185,20 +206,50 @@ function buildIndex(rootPath, cfg) {
185
206
  const excludes = compileExcludes(cfg.excludePatterns);
186
207
  let rootStats;
187
208
  try {
188
- rootStats = statSync(rootPath);
209
+ rootStats = await fs.stat(rootPath);
189
210
  } catch {
190
211
  state.termCount = 0;
191
212
  return;
192
213
  }
193
214
  if (rootStats.isFile()) {
194
215
  const relPath = normalizeSlashes(relative(normalizeSlashes(process.cwd()), rootPath));
195
- indexFile(rootPath, relPath === "" ? "." : relPath, cfg);
216
+ await indexFileFromStats(rootPath, relPath === "" ? "." : relPath, rootStats, cfg);
196
217
  state.fileCount = state.index.files.size;
197
218
  } else if (rootStats.isDirectory()) {
198
- walkDirectory(rootPath, cfg, excludes);
219
+ const fileBatch = [];
220
+ await walkDirectory(rootPath, cfg, excludes, fileBatch);
221
+ await flushFileBatch(fileBatch, cfg);
199
222
  }
200
223
  state.termCount = state.index.terms.size;
201
224
  }
225
+ async function ensureIndex(rootPath, cfg) {
226
+ if (state.index && state.cachedPath === rootPath) return;
227
+ state.buildPromise ??= buildIndex(rootPath, cfg).finally(() => {
228
+ state.buildPromise = null;
229
+ });
230
+ await state.buildPromise;
231
+ if (!state.index || state.cachedPath !== rootPath) {
232
+ state.buildPromise = buildIndex(rootPath, cfg).finally(() => {
233
+ state.buildPromise = null;
234
+ });
235
+ await state.buildPromise;
236
+ }
237
+ }
238
+ function compareRankedCandidates(a, b) {
239
+ return b.score - a.score || a.path.localeCompare(b.path);
240
+ }
241
+ function insertTopCandidate(top, candidate, limit) {
242
+ if (limit <= 0) return;
243
+ if (top.length === 0) {
244
+ top.push(candidate);
245
+ return;
246
+ }
247
+ let insertAt = top.findIndex((existing) => compareRankedCandidates(candidate, existing) < 0);
248
+ if (insertAt === -1) insertAt = top.length;
249
+ if (insertAt >= limit) return;
250
+ top.splice(insertAt, 0, candidate);
251
+ if (top.length > limit) top.pop();
252
+ }
202
253
  function runQuery(query, limit, cfg) {
203
254
  if (!state.index) return [];
204
255
  const rawTokens = tokenize(query, cfg.minTokenLength);
@@ -219,12 +270,18 @@ function runQuery(query, limit, cfg) {
219
270
  terms.add(token);
220
271
  }
221
272
  }
222
- const ranked = Array.from(scores.entries()).map(([path, score]) => ({
223
- path,
224
- score,
225
- terms: Array.from(matchedTerms.get(path) ?? [])
226
- })).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
227
- const top = ranked.slice(0, limit);
273
+ const top = [];
274
+ for (const [path, score] of scores) {
275
+ insertTopCandidate(
276
+ top,
277
+ {
278
+ path,
279
+ score,
280
+ terms: Array.from(matchedTerms.get(path) ?? [])
281
+ },
282
+ limit
283
+ );
284
+ }
228
285
  return top.map(({ path, score, terms }) => {
229
286
  const entry = state.index.files.get(path);
230
287
  const matchedLines = [];
@@ -314,6 +371,7 @@ var plugin = {
314
371
  state.truncated = false;
315
372
  state.queryCount = 0;
316
373
  state.reindexCount = 0;
374
+ state.buildPromise = null;
317
375
  const cfg = readConfig(api.config.extensions?.["semantic-search-indexer"]);
318
376
  api.tools.register({
319
377
  name: "semantic_search",
@@ -349,9 +407,7 @@ var plugin = {
349
407
  if (!resolved) {
350
408
  return { ok: false, error: "path outside project root" };
351
409
  }
352
- if (!state.index || state.cachedPath !== resolved) {
353
- buildIndex(resolved, cfg);
354
- }
410
+ await ensureIndex(resolved, cfg);
355
411
  const query = String(input.query ?? "");
356
412
  const limit = typeof input.limit === "number" && input.limit >= 1 ? Math.floor(input.limit) : cfg.defaultLimit;
357
413
  const results = runQuery(query, limit, cfg);
@@ -415,6 +471,7 @@ var plugin = {
415
471
  state.truncated = false;
416
472
  state.queryCount = 0;
417
473
  state.reindexCount = 0;
474
+ state.buildPromise = null;
418
475
  api.log.info("semantic-search-indexer: teardown complete", { final });
419
476
  },
420
477
  async health() {
@@ -1,7 +1,7 @@
1
- import { existsSync, statSync } from 'fs';
2
1
  import * as fsp from 'fs/promises';
3
2
  import 'path';
4
3
  import 'child_process';
4
+ import 'fs';
5
5
  import '@wrongstack/core';
6
6
  import 'os';
7
7
  import 'crypto';
@@ -102,7 +102,10 @@ var prompt_firewall_default = plugin28;
102
102
  // src/secret-scanner/index.ts
103
103
  var BASE_PATTERNS = [
104
104
  // LLM provider keys
105
- { type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
105
+ {
106
+ type: "anthropic_key",
107
+ regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
108
+ },
106
109
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
107
110
  // GitHub
108
111
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
@@ -114,7 +117,10 @@ var BASE_PATTERNS = [
114
117
  // Slack
115
118
  { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
116
119
  // Stripe
117
- { type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
120
+ {
121
+ type: "stripe_key",
122
+ regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
123
+ },
118
124
  // Twilio
119
125
  { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
120
126
  // Telegram
@@ -151,10 +157,7 @@ var BASE_PATTERNS = [
151
157
  var PATTERNS3 = [...BASE_PATTERNS];
152
158
  buildCombinedRegex(PATTERNS3);
153
159
  function buildCombinedRegex(patterns) {
154
- return new RegExp(
155
- patterns.map((p) => `(${p.regex.source})`).join("|"),
156
- "g"
157
- );
160
+ return new RegExp(patterns.map((p) => `(${p.regex.source})`).join("|"), "g");
158
161
  }
159
162
  var plugin29 = {
160
163
  name: "secret-scanner"};
@@ -565,11 +568,10 @@ var plugin62 = {
565
568
  return;
566
569
  }
567
570
  state59.postInvocations += 1;
568
- if (!existsSync(filePath)) return;
569
571
  let content;
570
572
  try {
571
- const stat = statSync(filePath);
572
- if (!stat.isFile()) return;
573
+ const stat4 = await fsp.stat(filePath);
574
+ if (!stat4.isFile()) return;
573
575
  content = await fsp.readFile(filePath, "utf-8");
574
576
  } catch {
575
577
  state59.readErrorCount += 1;
@@ -615,7 +617,11 @@ ${lines}${overflowNote}`
615
617
  \u{1F517} spec-linker (autoFix): wrapped unlinked plugin reference(s) in '${filePath}'.`
616
618
  };
617
619
  };
618
- state59.preHookUnregister = api.registerHook("PreToolUse", "write", preHook);
620
+ state59.preHookUnregister = api.registerHook("PreToolUse", "write", preHook, {
621
+ name: "spec-linker-autofix",
622
+ stage: "mutate",
623
+ failurePolicy: "open"
624
+ });
619
625
  }
620
626
  api.tools.register({
621
627
  name: "spec_linker_status",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/plugins",
3
- "version": "0.283.1",
3
+ "version": "0.284.0",
4
4
  "description": "Official WrongStack plugin collection — 62 focused, single-purpose plugins for code quality, security, observability, planning, and agent coordination",
5
5
  "license": "MIT",
6
6
  "author": "ECOSTACK TECHNOLOGY OÜ",
@@ -275,8 +275,8 @@
275
275
  "vitest": "^4.1.9"
276
276
  },
277
277
  "dependencies": {
278
- "@wrongstack/core": "0.283.1",
279
- "@wrongstack/tools": "0.283.1"
278
+ "@wrongstack/tools": "0.284.0",
279
+ "@wrongstack/core": "0.284.0"
280
280
  },
281
281
  "scripts": {
282
282
  "build": "tsup",