@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.
- package/README.md +29 -3
- package/dist/accessibility-auditor.d.ts +40 -0
- package/dist/accessibility-auditor.js +411 -0
- package/dist/agent-handoff.d.ts +37 -0
- package/dist/agent-handoff.js +298 -0
- package/dist/api-compatibility-gate.d.ts +38 -0
- package/dist/api-compatibility-gate.js +357 -0
- package/dist/auto-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- package/dist/checkpoint.js +18 -0
- package/dist/code-metrics.d.ts +31 -0
- package/dist/code-metrics.js +338 -0
- package/dist/commit-validator.js +67 -12
- package/dist/cost-tracker.js +58 -16
- package/dist/dead-code-detector.d.ts +34 -0
- package/dist/dead-code-detector.js +354 -0
- package/dist/dep-guard.js +47 -6
- package/dist/dependency-vulnerability-gate.d.ts +35 -0
- package/dist/dependency-vulnerability-gate.js +308 -0
- package/dist/diff-summary.js +97 -10
- package/dist/doc-sync-guard.d.ts +33 -0
- package/dist/doc-sync-guard.js +223 -0
- package/dist/duplicate-code-detector.d.ts +33 -0
- package/dist/duplicate-code-detector.js +384 -0
- package/dist/feature-flag-tracker.d.ts +38 -0
- package/dist/feature-flag-tracker.js +316 -0
- package/dist/file-watcher.js +85 -36
- package/dist/format-on-save.js +76 -10
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +14067 -5054
- package/dist/interface-contract-guard.d.ts +37 -0
- package/dist/interface-contract-guard.js +302 -0
- package/dist/knowledge-graph.d.ts +45 -0
- package/dist/knowledge-graph.js +325 -0
- package/dist/license-audit-gate.d.ts +34 -0
- package/dist/license-audit-gate.js +260 -0
- package/dist/llm-cache.js +5 -0
- package/dist/loop-breaker.d.ts +0 -38
- package/dist/loop-breaker.js +209 -8
- package/dist/migration-planner.d.ts +30 -0
- package/dist/migration-planner.js +349 -0
- package/dist/model-router.js +5 -0
- package/dist/performance-regression-gate.d.ts +33 -0
- package/dist/performance-regression-gate.js +315 -0
- package/dist/plugin-stack-observer.d.ts +35 -0
- package/dist/plugin-stack-observer.js +125 -0
- package/dist/pr-drafter.d.ts +35 -0
- package/dist/pr-drafter.js +334 -0
- package/dist/prompt-firewall.js +5 -0
- package/dist/refactor-suggester.d.ts +38 -0
- package/dist/refactor-suggester.js +382 -0
- package/dist/release-notes-generator.d.ts +27 -0
- package/dist/release-notes-generator.js +209 -0
- package/dist/schema-evolution-guard.d.ts +42 -0
- package/dist/schema-evolution-guard.js +319 -0
- package/dist/security-hotspot-scanner.d.ts +30 -0
- package/dist/security-hotspot-scanner.js +402 -0
- package/dist/semantic-search-indexer.d.ts +38 -0
- package/dist/semantic-search-indexer.js +436 -0
- package/dist/shell-check.js +38 -3
- package/dist/smart-rename.d.ts +26 -0
- package/dist/smart-rename.js +170 -0
- package/dist/spec-linker.js +273 -133
- package/dist/test-coverage-gate.d.ts +37 -0
- package/dist/test-coverage-gate.js +263 -0
- package/dist/test-flake-detector.d.ts +27 -0
- package/dist/test-flake-detector.js +274 -0
- package/dist/test-generator.d.ts +32 -0
- package/dist/test-generator.js +243 -0
- package/dist/test-runner-gate.js +154 -22
- package/dist/todo-listener.d.ts +2 -2
- package/dist/todo-listener.js +5 -5
- package/dist/token-throttle.js +5 -0
- package/dist/type-gate.d.ts +37 -0
- package/dist/type-gate.js +311 -0
- package/package.json +112 -4
- 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 };
|
package/dist/shell-check.js
CHANGED
|
@@ -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(
|
|
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: {
|
|
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 };
|