@wrongstack/plugins 0.281.1 → 0.282.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.
Files changed (78) hide show
  1. package/README.md +29 -3
  2. package/dist/accessibility-auditor.d.ts +40 -0
  3. package/dist/accessibility-auditor.js +411 -0
  4. package/dist/agent-handoff.d.ts +37 -0
  5. package/dist/agent-handoff.js +298 -0
  6. package/dist/api-compatibility-gate.d.ts +38 -0
  7. package/dist/api-compatibility-gate.js +357 -0
  8. package/dist/auto-i18n-extractor.d.ts +36 -0
  9. package/dist/auto-i18n-extractor.js +335 -0
  10. package/dist/checkpoint.js +18 -0
  11. package/dist/code-metrics.d.ts +31 -0
  12. package/dist/code-metrics.js +338 -0
  13. package/dist/commit-validator.js +67 -12
  14. package/dist/cost-tracker.js +58 -16
  15. package/dist/dead-code-detector.d.ts +34 -0
  16. package/dist/dead-code-detector.js +354 -0
  17. package/dist/dep-guard.js +47 -6
  18. package/dist/dependency-vulnerability-gate.d.ts +35 -0
  19. package/dist/dependency-vulnerability-gate.js +308 -0
  20. package/dist/diff-summary.js +97 -10
  21. package/dist/doc-sync-guard.d.ts +33 -0
  22. package/dist/doc-sync-guard.js +223 -0
  23. package/dist/duplicate-code-detector.d.ts +33 -0
  24. package/dist/duplicate-code-detector.js +384 -0
  25. package/dist/feature-flag-tracker.d.ts +38 -0
  26. package/dist/feature-flag-tracker.js +316 -0
  27. package/dist/file-watcher.js +85 -36
  28. package/dist/format-on-save.js +76 -10
  29. package/dist/import-organizer.js +73 -14
  30. package/dist/index.d.ts +27 -0
  31. package/dist/index.js +14067 -5054
  32. package/dist/interface-contract-guard.d.ts +37 -0
  33. package/dist/interface-contract-guard.js +302 -0
  34. package/dist/knowledge-graph.d.ts +45 -0
  35. package/dist/knowledge-graph.js +325 -0
  36. package/dist/license-audit-gate.d.ts +34 -0
  37. package/dist/license-audit-gate.js +260 -0
  38. package/dist/llm-cache.js +5 -0
  39. package/dist/loop-breaker.d.ts +0 -38
  40. package/dist/loop-breaker.js +209 -8
  41. package/dist/migration-planner.d.ts +30 -0
  42. package/dist/migration-planner.js +349 -0
  43. package/dist/model-router.js +5 -0
  44. package/dist/performance-regression-gate.d.ts +33 -0
  45. package/dist/performance-regression-gate.js +315 -0
  46. package/dist/plugin-stack-observer.d.ts +35 -0
  47. package/dist/plugin-stack-observer.js +125 -0
  48. package/dist/pr-drafter.d.ts +35 -0
  49. package/dist/pr-drafter.js +334 -0
  50. package/dist/prompt-firewall.js +5 -0
  51. package/dist/refactor-suggester.d.ts +38 -0
  52. package/dist/refactor-suggester.js +382 -0
  53. package/dist/release-notes-generator.d.ts +27 -0
  54. package/dist/release-notes-generator.js +209 -0
  55. package/dist/schema-evolution-guard.d.ts +42 -0
  56. package/dist/schema-evolution-guard.js +319 -0
  57. package/dist/security-hotspot-scanner.d.ts +30 -0
  58. package/dist/security-hotspot-scanner.js +402 -0
  59. package/dist/semantic-search-indexer.d.ts +38 -0
  60. package/dist/semantic-search-indexer.js +436 -0
  61. package/dist/shell-check.js +38 -3
  62. package/dist/smart-rename.d.ts +26 -0
  63. package/dist/smart-rename.js +170 -0
  64. package/dist/spec-linker.js +273 -133
  65. package/dist/test-coverage-gate.d.ts +37 -0
  66. package/dist/test-coverage-gate.js +263 -0
  67. package/dist/test-flake-detector.d.ts +27 -0
  68. package/dist/test-flake-detector.js +274 -0
  69. package/dist/test-generator.d.ts +32 -0
  70. package/dist/test-generator.js +243 -0
  71. package/dist/test-runner-gate.js +154 -22
  72. package/dist/todo-listener.d.ts +2 -2
  73. package/dist/todo-listener.js +5 -5
  74. package/dist/token-throttle.js +5 -0
  75. package/dist/type-gate.d.ts +37 -0
  76. package/dist/type-gate.js +311 -0
  77. package/package.json +112 -4
  78. package/LICENSE +0 -21
@@ -0,0 +1,436 @@
1
+ import { statSync, readFileSync, readdirSync } from 'fs';
2
+ import { isAbsolute, resolve, relative } from 'path';
3
+
4
+ // src/semantic-search-indexer/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ index: null,
8
+ cachedPath: null,
9
+ fileCount: 0,
10
+ termCount: 0,
11
+ bytesIndexed: 0,
12
+ truncated: false,
13
+ queryCount: 0,
14
+ reindexCount: 0};
15
+ var DEFAULTS = {
16
+ enabled: true,
17
+ includeExtensions: [
18
+ ".ts",
19
+ ".tsx",
20
+ ".js",
21
+ ".jsx",
22
+ ".mjs",
23
+ ".cjs",
24
+ ".py",
25
+ ".java",
26
+ ".go",
27
+ ".rs",
28
+ ".rb",
29
+ ".php",
30
+ ".cpp",
31
+ ".c",
32
+ ".h",
33
+ ".cs",
34
+ ".swift",
35
+ ".kt",
36
+ ".scala",
37
+ ".sh",
38
+ ".md",
39
+ ".json",
40
+ ".yaml",
41
+ ".yml",
42
+ ".css",
43
+ ".scss",
44
+ ".html"
45
+ ],
46
+ excludePatterns: ["node_modules", "\\.git", "\\.wrongstack", "\\.temp_files", "coverage", "dist"],
47
+ maxFileBytes: 1e6,
48
+ defaultLimit: 10,
49
+ minTokenLength: 2,
50
+ maxMatchesPerFile: 10,
51
+ maxFiles: 5e3
52
+ };
53
+ function readConfig(raw) {
54
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS, includeExtensions: [...DEFAULTS.includeExtensions], excludePatterns: [...DEFAULTS.excludePatterns] };
55
+ const r = raw;
56
+ const includeExtensions = Array.isArray(r["includeExtensions"]) ? r["includeExtensions"].filter((x) => typeof x === "string") : [...DEFAULTS.includeExtensions];
57
+ const excludePatterns = Array.isArray(r["excludePatterns"]) ? r["excludePatterns"].filter((x) => typeof x === "string") : [...DEFAULTS.excludePatterns];
58
+ const clamp = (v, min, max, fallback) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max ? v : fallback;
59
+ return {
60
+ enabled: r["enabled"] !== false,
61
+ includeExtensions,
62
+ excludePatterns,
63
+ maxFileBytes: clamp(r["maxFileBytes"], 1024, 5e7, DEFAULTS.maxFileBytes),
64
+ defaultLimit: clamp(r["defaultLimit"], 1, 1e3, DEFAULTS.defaultLimit),
65
+ minTokenLength: clamp(r["minTokenLength"], 1, 10, DEFAULTS.minTokenLength),
66
+ maxMatchesPerFile: clamp(r["maxMatchesPerFile"], 1, 100, DEFAULTS.maxMatchesPerFile),
67
+ maxFiles: clamp(r["maxFiles"], 1, 5e4, DEFAULTS.maxFiles)
68
+ };
69
+ }
70
+ function normalizeSlashes(p) {
71
+ return p.replace(/\\/g, "/");
72
+ }
73
+ function withinProject(p) {
74
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
75
+ const root = normalizeSlashes(process.cwd());
76
+ const resolved = normalizeSlashes(isAbsolute(p) ? resolve(p) : resolve(root, p));
77
+ const rel = normalizeSlashes(relative(root, resolved));
78
+ if (rel === "" || rel === ".") return true;
79
+ if (rel.startsWith("..")) return false;
80
+ if (isAbsolute(rel)) return false;
81
+ return true;
82
+ }
83
+ function resolveProjectPath(p) {
84
+ const raw = typeof p === "string" && p.length > 0 ? p : ".";
85
+ if (!withinProject(raw)) return null;
86
+ const root = normalizeSlashes(process.cwd());
87
+ return normalizeSlashes(isAbsolute(raw) ? resolve(raw) : resolve(root, raw));
88
+ }
89
+ function tokenize(text, minLength) {
90
+ const tokens = [];
91
+ for (const match of text.toLowerCase().match(/[a-z0-9]+/g) ?? []) {
92
+ if (match.length >= minLength) tokens.push(match);
93
+ }
94
+ return tokens;
95
+ }
96
+ function compileExcludes(patterns) {
97
+ const out = [];
98
+ for (const p of patterns) {
99
+ try {
100
+ out.push(new RegExp(p));
101
+ } catch {
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ function shouldIndexFile(filePath, cfg) {
107
+ if (cfg.includeExtensions.length === 0) return true;
108
+ const lower = filePath.toLowerCase();
109
+ for (const ext of cfg.includeExtensions) {
110
+ if (lower.endsWith(ext.toLowerCase())) return true;
111
+ }
112
+ return false;
113
+ }
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;
131
+ state.bytesIndexed += content.length;
132
+ const lines = content.split(/\r?\n/);
133
+ state.index.files.set(relPath, { lines, size: stats.size });
134
+ for (let i = 0; i < lines.length; i += 1) {
135
+ const terms = tokenize(lines[i], cfg.minTokenLength);
136
+ for (const term of terms) {
137
+ let postings = state.index.terms.get(term);
138
+ if (!postings) {
139
+ postings = /* @__PURE__ */ new Map();
140
+ state.index.terms.set(term, postings);
141
+ }
142
+ let posting = postings.get(relPath);
143
+ if (!posting) {
144
+ posting = { tf: 0 };
145
+ postings.set(relPath, posting);
146
+ }
147
+ posting.tf += 1;
148
+ }
149
+ }
150
+ }
151
+ function walkDirectory(absPath, cfg, excludes) {
152
+ let entries;
153
+ try {
154
+ entries = readdirSync(absPath, { withFileTypes: true });
155
+ } catch {
156
+ return;
157
+ }
158
+ const root = normalizeSlashes(process.cwd());
159
+ for (const ent of entries) {
160
+ if (state.fileCount >= cfg.maxFiles) {
161
+ state.truncated = true;
162
+ return;
163
+ }
164
+ const absChild = normalizeSlashes(resolve(absPath, ent.name));
165
+ const relChild = normalizeSlashes(relative(root, absChild));
166
+ if (relChild === "" || relChild === ".") continue;
167
+ if (excludes.some((re) => re.test(relChild))) continue;
168
+ if (ent.isDirectory()) {
169
+ walkDirectory(absChild, cfg, excludes);
170
+ if (state.truncated) return;
171
+ } else if (ent.isFile()) {
172
+ indexFile(absChild, relChild, cfg);
173
+ state.fileCount += 1;
174
+ }
175
+ }
176
+ }
177
+ function buildIndex(rootPath, cfg) {
178
+ state.index = { terms: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map() };
179
+ state.cachedPath = rootPath;
180
+ state.fileCount = 0;
181
+ state.termCount = 0;
182
+ state.bytesIndexed = 0;
183
+ state.truncated = false;
184
+ state.reindexCount += 1;
185
+ const excludes = compileExcludes(cfg.excludePatterns);
186
+ let rootStats;
187
+ try {
188
+ rootStats = statSync(rootPath);
189
+ } catch {
190
+ state.termCount = 0;
191
+ return;
192
+ }
193
+ if (rootStats.isFile()) {
194
+ const relPath = normalizeSlashes(relative(normalizeSlashes(process.cwd()), rootPath));
195
+ indexFile(rootPath, relPath === "" ? "." : relPath, cfg);
196
+ state.fileCount = state.index.files.size;
197
+ } else if (rootStats.isDirectory()) {
198
+ walkDirectory(rootPath, cfg, excludes);
199
+ }
200
+ state.termCount = state.index.terms.size;
201
+ }
202
+ function runQuery(query, limit, cfg) {
203
+ if (!state.index) return [];
204
+ const rawTokens = tokenize(query, cfg.minTokenLength);
205
+ const uniqueTokens = [...new Set(rawTokens)];
206
+ if (uniqueTokens.length === 0) return [];
207
+ const scores = /* @__PURE__ */ new Map();
208
+ const matchedTerms = /* @__PURE__ */ new Map();
209
+ for (const token of uniqueTokens) {
210
+ const postings = state.index.terms.get(token);
211
+ if (!postings) continue;
212
+ for (const [filePath, posting] of postings) {
213
+ scores.set(filePath, (scores.get(filePath) ?? 0) + posting.tf);
214
+ let terms = matchedTerms.get(filePath);
215
+ if (!terms) {
216
+ terms = /* @__PURE__ */ new Set();
217
+ matchedTerms.set(filePath, terms);
218
+ }
219
+ terms.add(token);
220
+ }
221
+ }
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);
228
+ return top.map(({ path, score, terms }) => {
229
+ const entry = state.index.files.get(path);
230
+ const matchedLines = [];
231
+ if (entry) {
232
+ const querySet = new Set(uniqueTokens);
233
+ for (let i = 0; i < entry.lines.length; i += 1) {
234
+ const line = entry.lines[i];
235
+ const lower = line.toLowerCase();
236
+ for (const token of querySet) {
237
+ if (lower.includes(token)) {
238
+ matchedLines.push({ lineNumber: i + 1, text: line.trim() });
239
+ break;
240
+ }
241
+ }
242
+ if (matchedLines.length >= cfg.maxMatchesPerFile) break;
243
+ }
244
+ }
245
+ return { path, score, matchedTerms: terms, matchedLines };
246
+ });
247
+ }
248
+ var plugin = {
249
+ name: "semantic-search-indexer",
250
+ version: "0.1.0",
251
+ description: "Builds an in-memory keyword index over project source files and answers ranked search queries",
252
+ apiVersion: API_VERSION,
253
+ capabilities: { tools: true },
254
+ defaultConfig: { ...DEFAULTS, includeExtensions: [...DEFAULTS.includeExtensions], excludePatterns: [...DEFAULTS.excludePatterns] },
255
+ configSchema: {
256
+ type: "object",
257
+ properties: {
258
+ enabled: { type: "boolean", default: true, description: "Master switch." },
259
+ includeExtensions: {
260
+ type: "array",
261
+ items: { type: "string" },
262
+ default: DEFAULTS.includeExtensions,
263
+ description: "Only index files with these extensions. Empty = all files."
264
+ },
265
+ excludePatterns: {
266
+ type: "array",
267
+ items: { type: "string" },
268
+ default: DEFAULTS.excludePatterns,
269
+ description: "Regex patterns; matching relative paths are skipped."
270
+ },
271
+ maxFileBytes: {
272
+ type: "number",
273
+ minimum: 1024,
274
+ maximum: 5e7,
275
+ default: 1e6,
276
+ description: "Files larger than this are skipped."
277
+ },
278
+ defaultLimit: {
279
+ type: "number",
280
+ minimum: 1,
281
+ maximum: 1e3,
282
+ default: 10,
283
+ description: "Default number of results per query."
284
+ },
285
+ minTokenLength: {
286
+ type: "number",
287
+ minimum: 1,
288
+ maximum: 10,
289
+ default: 2,
290
+ description: "Minimum length of an indexed/query token."
291
+ },
292
+ maxMatchesPerFile: {
293
+ type: "number",
294
+ minimum: 1,
295
+ maximum: 100,
296
+ default: 10,
297
+ description: "Maximum matching lines returned per result."
298
+ },
299
+ maxFiles: {
300
+ type: "number",
301
+ minimum: 1,
302
+ maximum: 5e4,
303
+ default: 5e3,
304
+ description: "Maximum files to index in one build."
305
+ }
306
+ }
307
+ },
308
+ setup(api) {
309
+ state.index = null;
310
+ state.cachedPath = null;
311
+ state.fileCount = 0;
312
+ state.termCount = 0;
313
+ state.bytesIndexed = 0;
314
+ state.truncated = false;
315
+ state.queryCount = 0;
316
+ state.reindexCount = 0;
317
+ const cfg = readConfig(api.config.extensions?.["semantic-search-indexer"]);
318
+ api.tools.register({
319
+ name: "semantic_search",
320
+ description: "Search project source files with a lightweight keyword index. Returns ranked file paths, TF scores, and matched lines.",
321
+ inputSchema: {
322
+ type: "object",
323
+ properties: {
324
+ query: {
325
+ type: "string",
326
+ description: "Space-separated keywords to search for."
327
+ },
328
+ limit: {
329
+ type: "number",
330
+ description: "Maximum number of results (defaults to configured defaultLimit)."
331
+ },
332
+ path: {
333
+ type: "string",
334
+ description: "Directory or file to search under (defaults to project root)."
335
+ }
336
+ },
337
+ required: ["query"]
338
+ },
339
+ permission: "auto",
340
+ category: "Search",
341
+ mutating: false,
342
+ riskTier: "safe",
343
+ icon: "search",
344
+ async execute(input) {
345
+ if (!cfg.enabled) {
346
+ return { ok: false, error: "semantic-search-indexer is disabled" };
347
+ }
348
+ const resolved = resolveProjectPath(input.path);
349
+ if (!resolved) {
350
+ return { ok: false, error: "path outside project root" };
351
+ }
352
+ if (!state.index || state.cachedPath !== resolved) {
353
+ buildIndex(resolved, cfg);
354
+ }
355
+ const query = String(input.query ?? "");
356
+ const limit = typeof input.limit === "number" && input.limit >= 1 ? Math.floor(input.limit) : cfg.defaultLimit;
357
+ const results = runQuery(query, limit, cfg);
358
+ const queryTokens = [...new Set(tokenize(query, cfg.minTokenLength))];
359
+ state.queryCount += 1;
360
+ api.metrics.counter("queries");
361
+ return {
362
+ ok: true,
363
+ query,
364
+ queryTokens,
365
+ indexedPath: state.cachedPath,
366
+ totalResults: results.length,
367
+ limit,
368
+ results
369
+ };
370
+ }
371
+ });
372
+ api.tools.register({
373
+ name: "semantic_index_status",
374
+ description: "Reports semantic-search-indexer state: indexed path, file/term counts, and counters.",
375
+ inputSchema: { type: "object", properties: {} },
376
+ permission: "auto",
377
+ category: "Diagnostics",
378
+ mutating: false,
379
+ riskTier: "safe",
380
+ icon: "index",
381
+ async execute() {
382
+ return {
383
+ ok: true,
384
+ enabled: cfg.enabled,
385
+ indexedPath: state.cachedPath,
386
+ fileCount: state.fileCount,
387
+ termCount: state.termCount,
388
+ bytesIndexed: state.bytesIndexed,
389
+ truncated: state.truncated,
390
+ counters: {
391
+ queries: state.queryCount,
392
+ reindexes: state.reindexCount
393
+ }
394
+ };
395
+ }
396
+ });
397
+ api.log.info("semantic-search-indexer plugin loaded", {
398
+ version: "0.1.0",
399
+ defaultLimit: cfg.defaultLimit,
400
+ maxFiles: cfg.maxFiles
401
+ });
402
+ },
403
+ teardown(api) {
404
+ const final = {
405
+ fileCount: state.fileCount,
406
+ termCount: state.termCount,
407
+ queries: state.queryCount,
408
+ reindexes: state.reindexCount
409
+ };
410
+ state.index = null;
411
+ state.cachedPath = null;
412
+ state.fileCount = 0;
413
+ state.termCount = 0;
414
+ state.bytesIndexed = 0;
415
+ state.truncated = false;
416
+ state.queryCount = 0;
417
+ state.reindexCount = 0;
418
+ api.log.info("semantic-search-indexer: teardown complete", { final });
419
+ },
420
+ async health() {
421
+ return {
422
+ ok: true,
423
+ message: `semantic-search-indexer: ${state.fileCount} file(s), ${state.termCount} term(s), ${state.queryCount} query(ies)`,
424
+ counters: {
425
+ fileCount: state.fileCount,
426
+ termCount: state.termCount,
427
+ bytesIndexed: state.bytesIndexed,
428
+ queries: state.queryCount,
429
+ reindexes: state.reindexCount
430
+ }
431
+ };
432
+ }
433
+ };
434
+ var semantic_search_indexer_default = plugin;
435
+
436
+ export { semantic_search_indexer_default as default };
@@ -1,9 +1,19 @@
1
1
  import { execSync, execFileSync } from 'child_process';
2
2
  import { readdirSync, existsSync } from 'fs';
3
- import { join } from 'path';
3
+ import { join, isAbsolute, resolve, relative } from 'path';
4
4
 
5
5
  // src/shell-check/index.ts
6
6
  var API_VERSION = "^0.1.10";
7
+ function withinProject(p) {
8
+ const root = process.cwd();
9
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
10
+ const rel = relative(root, resolved);
11
+ if (rel === "" || rel === ".") return true;
12
+ if (rel.startsWith("..")) return false;
13
+ if (isAbsolute(rel)) return false;
14
+ return true;
15
+ }
16
+ var MAX_PATH_LEN = 4096;
7
17
  var state = {
8
18
  /** Per-session invocation count. */
9
19
  invocationCount: 0,
@@ -17,7 +27,9 @@ function runShellCheck(files, severity, cwd) {
17
27
  try {
18
28
  execSync("shellcheck --version", { encoding: "utf-8", stdio: "ignore", windowsHide: true });
19
29
  } catch {
20
- throw new Error("shellcheck is not installed. Install via: apt install shellcheck / brew install shellcheck");
30
+ throw new Error(
31
+ "shellcheck is not installed. Install via: apt install shellcheck / brew install shellcheck"
32
+ );
21
33
  }
22
34
  }
23
35
  const levelMap = {
@@ -94,7 +106,11 @@ var plugin = {
94
106
  type: "object",
95
107
  properties: {
96
108
  severity: { type: "string", enum: ["error", "warning", "info", "style"], default: "warning" },
97
- severityThreshold: { type: "string", enum: ["error", "warning", "info", "style"], default: "warning" },
109
+ severityThreshold: {
110
+ type: "string",
111
+ enum: ["error", "warning", "info", "style"],
112
+ default: "warning"
113
+ },
98
114
  ignoredCodes: { type: "array", items: { type: "string" }, default: [] },
99
115
  autoScanOnBash: { type: "boolean", default: false }
100
116
  }
@@ -147,6 +163,25 @@ var plugin = {
147
163
  const pattern = inp.pattern ?? "";
148
164
  const severity = inp.severity ?? "warning";
149
165
  state.invocationCount += 1;
166
+ const pathIsSafe = (p) => typeof p === "string" && p.length > 0 && p.length <= MAX_PATH_LEN && withinProject(p);
167
+ if (!pathIsSafe(directory)) {
168
+ return {
169
+ ok: false,
170
+ error: `directory path is outside the project root: ${directory}`,
171
+ issues: [],
172
+ filesScanned: 0,
173
+ rejectedOutsideProject: true
174
+ };
175
+ }
176
+ if (files?.some((f) => !pathIsSafe(f))) {
177
+ return {
178
+ ok: false,
179
+ error: "one or more file paths are outside the project root",
180
+ issues: [],
181
+ filesScanned: 0,
182
+ rejectedOutsideProject: true
183
+ };
184
+ }
150
185
  let checkFiles;
151
186
  let scannedDirectories = false;
152
187
  if (files && files.length > 0) {
@@ -0,0 +1,26 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * smart-rename plugin — whole-word identifier rename inside a single source
5
+ * file using regex replacement.
6
+ *
7
+ * Tool registered:
8
+ * - smart_rename : Replace whole-word occurrences of oldName with newName.
9
+ *
10
+ * No hooks are registered.
11
+ *
12
+ * Config (`config.extensions['smart-rename']`):
13
+ *
14
+ * ```jsonc
15
+ * {
16
+ * "enabled": true,
17
+ * "extensions": [".ts", ".tsx", ".js", ".jsx"]
18
+ * }
19
+ * ```
20
+ *
21
+ * @public
22
+ */
23
+
24
+ declare const plugin: Plugin;
25
+
26
+ export { plugin as default };
@@ -0,0 +1,170 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { extname, resolve, isAbsolute, relative } from 'path';
3
+
4
+ // src/smart-rename/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ renameCount: 0,
8
+ replacementCount: 0,
9
+ errorCount: 0
10
+ };
11
+ var DEFAULTS = {
12
+ enabled: true,
13
+ extensions: [".ts", ".tsx", ".js", ".jsx"]
14
+ };
15
+ function readConfig(raw) {
16
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
17
+ const r = raw;
18
+ return {
19
+ enabled: r["enabled"] !== false,
20
+ extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS.extensions
21
+ };
22
+ }
23
+ function withinProject(p) {
24
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
25
+ const root = process.cwd();
26
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
27
+ const rel = relative(root, resolved);
28
+ if (rel === "" || rel === ".") return true;
29
+ if (rel.startsWith("..")) return false;
30
+ if (isAbsolute(rel)) return false;
31
+ return true;
32
+ }
33
+ function toPosix(p) {
34
+ return p.replace(/\\/g, "/");
35
+ }
36
+ function relativePath(p) {
37
+ return toPosix(relative(process.cwd(), p));
38
+ }
39
+ function escapeRegex(s) {
40
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41
+ }
42
+ function renameInContent(content, oldName, newName) {
43
+ const re = new RegExp(`\\b${escapeRegex(oldName)}\\b`, "g");
44
+ let replacements = 0;
45
+ const preview = content.replace(re, () => {
46
+ replacements++;
47
+ return newName;
48
+ });
49
+ return { preview, replacements };
50
+ }
51
+ var plugin = {
52
+ name: "smart-rename",
53
+ version: "0.1.0",
54
+ description: "Whole-word identifier rename inside a single source file",
55
+ apiVersion: API_VERSION,
56
+ capabilities: { tools: true },
57
+ defaultConfig: { ...DEFAULTS },
58
+ configSchema: {
59
+ type: "object",
60
+ properties: {
61
+ enabled: { type: "boolean", default: true, description: "Master switch." },
62
+ extensions: {
63
+ type: "array",
64
+ items: { type: "string" },
65
+ default: [".ts", ".tsx", ".js", ".jsx"],
66
+ description: "File extensions allowed for renaming."
67
+ }
68
+ }
69
+ },
70
+ setup(api) {
71
+ state.renameCount = 0;
72
+ state.replacementCount = 0;
73
+ state.errorCount = 0;
74
+ const cfg = readConfig(api.config.extensions?.["smart-rename"]);
75
+ api.tools.register({
76
+ name: "smart_rename",
77
+ description: "Replace whole-word occurrences of an identifier in a source file. Returns a preview by default; set apply:true to write the result back to disk.",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {
81
+ path: { type: "string", description: "Source file path (relative to project root)." },
82
+ oldName: { type: "string", description: "Identifier to replace." },
83
+ newName: { type: "string", description: "New identifier." },
84
+ apply: { type: "boolean", default: false, description: "When true, write the preview back to disk." }
85
+ },
86
+ required: ["path", "oldName", "newName"]
87
+ },
88
+ permission: "auto",
89
+ category: "Development",
90
+ mutating: true,
91
+ async execute(input) {
92
+ if (!cfg.enabled) return { ok: false, error: "smart-rename is disabled" };
93
+ const rawPath = input.path;
94
+ const oldName = input.oldName;
95
+ const newName = input.newName;
96
+ if (!rawPath || typeof rawPath !== "string") {
97
+ return { ok: false, error: "path is required" };
98
+ }
99
+ if (!oldName || typeof oldName !== "string" || oldName.length === 0) {
100
+ return { ok: false, error: "oldName is required" };
101
+ }
102
+ if (!newName || typeof newName !== "string" || newName.length === 0) {
103
+ return { ok: false, error: "newName is required" };
104
+ }
105
+ if (!withinProject(rawPath)) {
106
+ return { ok: false, error: "path is outside the project root" };
107
+ }
108
+ const ext = extname(rawPath).toLowerCase();
109
+ if (!cfg.extensions.includes(ext)) {
110
+ return { ok: false, error: `extension ${ext} is not allowed for rename` };
111
+ }
112
+ const resolved = resolve(process.cwd(), rawPath);
113
+ let content;
114
+ try {
115
+ content = readFileSync(resolved, "utf-8");
116
+ } catch (err) {
117
+ state.errorCount += 1;
118
+ return { ok: false, error: String(err) };
119
+ }
120
+ const { preview, replacements } = renameInContent(content, oldName, newName);
121
+ state.renameCount += 1;
122
+ state.replacementCount += replacements;
123
+ if (input.apply) {
124
+ try {
125
+ writeFileSync(resolved, preview, "utf-8");
126
+ } catch (err) {
127
+ state.errorCount += 1;
128
+ return { ok: false, error: String(err) };
129
+ }
130
+ }
131
+ return {
132
+ ok: true,
133
+ path: relativePath(resolved),
134
+ replacements,
135
+ preview,
136
+ applied: Boolean(input.apply)
137
+ };
138
+ }
139
+ });
140
+ api.log.info("smart-rename plugin loaded", {
141
+ version: "0.1.0",
142
+ extensions: cfg.extensions
143
+ });
144
+ },
145
+ teardown(api) {
146
+ const final = {
147
+ renames: state.renameCount,
148
+ replacements: state.replacementCount,
149
+ errors: state.errorCount
150
+ };
151
+ state.renameCount = 0;
152
+ state.replacementCount = 0;
153
+ state.errorCount = 0;
154
+ api.log.info("smart-rename: teardown complete", { final });
155
+ },
156
+ async health() {
157
+ return {
158
+ ok: state.errorCount === 0,
159
+ message: state.errorCount ? `smart-rename: ${state.errorCount} error(s)` : `smart-rename: ${state.renameCount} rename(s), ${state.replacementCount} replacement(s)`,
160
+ counters: {
161
+ renames: state.renameCount,
162
+ replacements: state.replacementCount,
163
+ errors: state.errorCount
164
+ }
165
+ };
166
+ }
167
+ };
168
+ var smart_rename_default = plugin;
169
+
170
+ export { smart_rename_default as default };