@contentrain/mcp 0.3.1 → 1.0.3

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 (115) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +154 -238
  3. package/dist/apply-manager-BAeqrAHX.mjs +751 -0
  4. package/dist/apply-manager-BAeqrAHX.mjs.map +1 -0
  5. package/dist/astro-parser-RofKmKQi.mjs +166 -0
  6. package/dist/astro-parser-RofKmKQi.mjs.map +1 -0
  7. package/dist/branch-lifecycle-DB6sgeJi.mjs +97 -0
  8. package/dist/branch-lifecycle-DB6sgeJi.mjs.map +1 -0
  9. package/dist/chunk-BEJ448es.mjs +35 -0
  10. package/dist/compiler-rIC4ETHv.mjs +44509 -0
  11. package/dist/compiler-rIC4ETHv.mjs.map +1 -0
  12. package/dist/compiler-sfc.cjs-BbD04rn_.mjs +50279 -0
  13. package/dist/compiler-sfc.cjs-BbD04rn_.mjs.map +1 -0
  14. package/dist/config-CPlhDwsv.mjs +27 -0
  15. package/dist/config-CPlhDwsv.mjs.map +1 -0
  16. package/dist/context-CRBCTYPB.mjs +46 -0
  17. package/dist/context-CRBCTYPB.mjs.map +1 -0
  18. package/dist/core/apply-manager.d.mts +144 -0
  19. package/dist/core/apply-manager.d.mts.map +1 -0
  20. package/dist/core/apply-manager.mjs +6 -0
  21. package/dist/core/config.d.mts +8 -0
  22. package/dist/core/config.d.mts.map +1 -0
  23. package/dist/core/config.mjs +2 -0
  24. package/dist/core/content-manager.d.mts +53 -0
  25. package/dist/core/content-manager.d.mts.map +1 -0
  26. package/dist/core/content-manager.mjs +2 -0
  27. package/dist/core/context.d.mts +13 -0
  28. package/dist/core/context.d.mts.map +1 -0
  29. package/dist/core/context.mjs +3 -0
  30. package/dist/core/graph-builder.d.mts +12 -0
  31. package/dist/core/graph-builder.d.mts.map +1 -0
  32. package/dist/core/graph-builder.mjs +2 -0
  33. package/dist/core/meta-manager.d.mts +14 -0
  34. package/dist/core/meta-manager.d.mts.map +1 -0
  35. package/dist/core/meta-manager.mjs +2 -0
  36. package/dist/core/model-manager.d.mts +44 -0
  37. package/dist/core/model-manager.d.mts.map +1 -0
  38. package/dist/core/model-manager.mjs +2 -0
  39. package/dist/core/scan-config.d.mts +24 -0
  40. package/dist/core/scan-config.d.mts.map +1 -0
  41. package/dist/core/scan-config.mjs +2 -0
  42. package/dist/core/scanner.d.mts +25 -0
  43. package/dist/core/scanner.d.mts.map +1 -0
  44. package/dist/core/scanner.mjs +3 -0
  45. package/dist/core/validator.d.mts +23 -0
  46. package/dist/core/validator.d.mts.map +1 -0
  47. package/dist/core/validator.mjs +3 -0
  48. package/dist/detect-C8XmJ4S6.mjs +362 -0
  49. package/dist/detect-C8XmJ4S6.mjs.map +1 -0
  50. package/dist/fs-BbuGnVcG.mjs +52 -0
  51. package/dist/fs-BbuGnVcG.mjs.map +1 -0
  52. package/dist/git/branch-lifecycle.d.mts +28 -0
  53. package/dist/git/branch-lifecycle.d.mts.map +1 -0
  54. package/dist/git/branch-lifecycle.mjs +2 -0
  55. package/dist/git/transaction.d.mts +25 -0
  56. package/dist/git/transaction.d.mts.map +1 -0
  57. package/dist/git/transaction.mjs +4 -0
  58. package/dist/graph-builder-fPrDDwnu.mjs +162 -0
  59. package/dist/graph-builder-fPrDDwnu.mjs.map +1 -0
  60. package/dist/id-BdD4HvMY.mjs +12 -0
  61. package/dist/id-BdD4HvMY.mjs.map +1 -0
  62. package/dist/index.d.mts +1 -359
  63. package/dist/index.mjs +15 -2161
  64. package/dist/index.mjs.map +1 -1
  65. package/dist/meta-manager-CECrfsKm.mjs +53 -0
  66. package/dist/meta-manager-CECrfsKm.mjs.map +1 -0
  67. package/dist/model-manager-sfPdrVIJ.mjs +1016 -0
  68. package/dist/model-manager-sfPdrVIJ.mjs.map +1 -0
  69. package/dist/node-Cbq5eR5i.mjs +300 -0
  70. package/dist/node-Cbq5eR5i.mjs.map +1 -0
  71. package/dist/scan-config-AJGxCdNd.mjs +159 -0
  72. package/dist/scan-config-AJGxCdNd.mjs.map +1 -0
  73. package/dist/scanner-BdTiv8sw.mjs +619 -0
  74. package/dist/scanner-BdTiv8sw.mjs.map +1 -0
  75. package/dist/serializer-Dueih-i2.mjs +33 -0
  76. package/dist/serializer-Dueih-i2.mjs.map +1 -0
  77. package/dist/server-CC12z_xq.mjs +1868 -0
  78. package/dist/server-CC12z_xq.mjs.map +1 -0
  79. package/dist/server.d.mts +7 -0
  80. package/dist/server.d.mts.map +1 -0
  81. package/dist/server.mjs +11 -0
  82. package/dist/source-map-generator-DpO-ejrt.mjs +893 -0
  83. package/dist/source-map-generator-DpO-ejrt.mjs.map +1 -0
  84. package/dist/svelte-parser-COTNG92w.mjs +225 -0
  85. package/dist/svelte-parser-COTNG92w.mjs.map +1 -0
  86. package/dist/templates/index.d.mts +8 -0
  87. package/dist/templates/index.d.mts.map +1 -0
  88. package/dist/templates/index.mjs +2 -0
  89. package/dist/templates-3HWi-PTu.mjs +824 -0
  90. package/dist/templates-3HWi-PTu.mjs.map +1 -0
  91. package/dist/transaction-DXkMulzr.mjs +128 -0
  92. package/dist/transaction-DXkMulzr.mjs.map +1 -0
  93. package/dist/tsx-parser-Bf8Firrq.mjs +2 -0
  94. package/dist/tsx-parser-CYVyBWjn.mjs +234 -0
  95. package/dist/tsx-parser-CYVyBWjn.mjs.map +1 -0
  96. package/dist/util/detect.d.mts +20 -0
  97. package/dist/util/detect.d.mts.map +1 -0
  98. package/dist/util/detect.mjs +2 -0
  99. package/dist/util/fs.d.mts +12 -0
  100. package/dist/util/fs.d.mts.map +1 -0
  101. package/dist/util/fs.mjs +2 -0
  102. package/dist/util/id.d.mts +6 -0
  103. package/dist/util/id.d.mts.map +1 -0
  104. package/dist/util/id.mjs +2 -0
  105. package/dist/util/serializer.d.mts +14 -0
  106. package/dist/util/serializer.d.mts.map +1 -0
  107. package/dist/util/serializer.mjs +2 -0
  108. package/dist/validator-C3lZqO3U.mjs +755 -0
  109. package/dist/validator-C3lZqO3U.mjs.map +1 -0
  110. package/dist/vue-parser-CjS4YAoQ.mjs +225 -0
  111. package/dist/vue-parser-CjS4YAoQ.mjs.map +1 -0
  112. package/package.json +113 -43
  113. package/dist/fs-D83-Ii_y.mjs +0 -75
  114. package/dist/fs-D83-Ii_y.mjs.map +0 -1
  115. package/dist/index.d.mts.map +0 -1
@@ -0,0 +1,619 @@
1
+ import { o as readText } from "./fs-BbuGnVcG.mjs";
2
+ import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-AJGxCdNd.mjs";
3
+ import { t as parseTsx } from "./tsx-parser-CYVyBWjn.mjs";
4
+ import { extname, join } from "node:path";
5
+ //#region src/core/ast-scanner/pre-filter.ts
6
+ const PURE_NUMBER_RE = /^\d+(\.\d+)?$/;
7
+ const HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i;
8
+ const FILE_EXT_RE$1 = /\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i;
9
+ const HTML_PROP_VALUES = new Set([
10
+ "class",
11
+ "variant",
12
+ "secondary",
13
+ "outline",
14
+ "ghost",
15
+ "destructive",
16
+ "default",
17
+ "primary",
18
+ "_blank",
19
+ "noopener",
20
+ "noreferrer",
21
+ "button",
22
+ "submit",
23
+ "reset",
24
+ "text",
25
+ "numeric",
26
+ "password",
27
+ "email",
28
+ "checkbox",
29
+ "radio",
30
+ "hidden",
31
+ "none",
32
+ "auto",
33
+ "inherit",
34
+ "initial",
35
+ "sm",
36
+ "md",
37
+ "lg",
38
+ "xl",
39
+ "xs",
40
+ "2xl",
41
+ "3xl",
42
+ "icon",
43
+ "icon-sm",
44
+ "icon-lg"
45
+ ]);
46
+ const SLOT_EVENT_NAMES = new Set([
47
+ "header",
48
+ "footer",
49
+ "default",
50
+ "trigger",
51
+ "content",
52
+ "sidebar",
53
+ "overlay",
54
+ "body",
55
+ "actions",
56
+ "click",
57
+ "change",
58
+ "input",
59
+ "focus",
60
+ "blur",
61
+ "submit",
62
+ "mounted",
63
+ "unmounted",
64
+ "updated"
65
+ ]);
66
+ const TAILWIND_SEGMENT_RE = /^(?:bg-|text-|border-|flex|grid|p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|rounded|shadow|w-|h-|min-|max-|gap-|space-|items-|justify-|self-|overflow-|z-|opacity-|transition|duration-|ease-|animate-|font-|leading-|tracking-|decoration-|underline|line-through|uppercase|lowercase|capitalize|truncate|whitespace-|break-|sr-only|not-sr-only|hover:|focus:|active:|disabled:|dark:|sm:|md:|lg:|xl:|2xl:|group-|peer-|ring-|outline-|divide-|table-|col-|row-|aspect-|object-|inset-|top-|right-|bottom-|left-|translate-|rotate-|scale-|skew-|origin-|cursor-|select-|resize-|fill-|stroke-|block|inline|absolute|relative|fixed|sticky|static|float-|clear-|isolate|visible|invisible|grow|shrink|basis-|order-|place-)/;
67
+ /**
68
+ * Returns true if the string looks like a CSS class list (Tailwind or similar).
69
+ * Requires 2+ space-separated segments with majority matching utility patterns.
70
+ */
71
+ function isCssClassList(value) {
72
+ const segments = value.trim().split(/\s+/);
73
+ if (segments.length < 2) return false;
74
+ let matched = 0;
75
+ for (const seg of segments) if (TAILWIND_SEGMENT_RE.test(seg)) matched++;
76
+ return matched / segments.length >= .5;
77
+ }
78
+ /**
79
+ * Returns true if the string is a single CSS utility token (e.g. "bg-blue-500").
80
+ */
81
+ function isSingleCssUtility(value) {
82
+ const trimmed = value.trim();
83
+ if (trimmed.includes(" ")) return false;
84
+ return TAILWIND_SEGMENT_RE.test(trimmed);
85
+ }
86
+ const PRE_FILTER_RULES = [
87
+ {
88
+ context: "import_path",
89
+ reason: "import_path"
90
+ },
91
+ {
92
+ context: "type_annotation",
93
+ reason: "type_annotation"
94
+ },
95
+ {
96
+ context: "css_class",
97
+ reason: "css_class"
98
+ },
99
+ {
100
+ context: "css_utility_call",
101
+ reason: "css_utility_call"
102
+ },
103
+ {
104
+ context: "console_call",
105
+ reason: "console_call"
106
+ },
107
+ {
108
+ context: "test_assertion",
109
+ reason: "test_assertion"
110
+ },
111
+ {
112
+ context: "switch_case",
113
+ reason: "switch_case"
114
+ },
115
+ {
116
+ condition: (v) => v.length <= 1,
117
+ reason: "single_char"
118
+ },
119
+ {
120
+ condition: (v) => PURE_NUMBER_RE.test(v),
121
+ reason: "pure_number"
122
+ },
123
+ {
124
+ condition: (v) => v.startsWith("--"),
125
+ reason: "cli_flag"
126
+ },
127
+ {
128
+ condition: (v) => HEX_COLOR_RE.test(v),
129
+ reason: "hex_color"
130
+ },
131
+ {
132
+ condition: (v) => FILE_EXT_RE$1.test(v),
133
+ reason: "file_extension"
134
+ },
135
+ {
136
+ condition: (v) => HTML_PROP_VALUES.has(v.toLowerCase()),
137
+ reason: "html_prop_value"
138
+ },
139
+ {
140
+ condition: (v) => isCssClassList(v),
141
+ reason: "css_class_list"
142
+ },
143
+ {
144
+ condition: (v) => isSingleCssUtility(v),
145
+ reason: "css_utility_token"
146
+ },
147
+ {
148
+ condition: (v) => {
149
+ const lower = v.toLowerCase();
150
+ return v === lower && !v.includes(" ") && SLOT_EVENT_NAMES.has(lower);
151
+ },
152
+ reason: "slot_event_name"
153
+ }
154
+ ];
155
+ /**
156
+ * Structural pre-filter: removes strings that are 100% NOT content.
157
+ *
158
+ * Conservative — when in doubt, INCLUDE the string.
159
+ * Returns candidates (passed), count of filtered, and reason breakdown.
160
+ */
161
+ function applyPreFilter(strings) {
162
+ const candidates = [];
163
+ const filterReasons = {};
164
+ let filtered = 0;
165
+ for (const str of strings) {
166
+ const matchedRule = findMatchingRule(str);
167
+ if (matchedRule) {
168
+ filtered++;
169
+ filterReasons[matchedRule.reason] = (filterReasons[matchedRule.reason] ?? 0) + 1;
170
+ } else candidates.push(str);
171
+ }
172
+ return {
173
+ candidates,
174
+ filtered,
175
+ filterReasons
176
+ };
177
+ }
178
+ /**
179
+ * Find the first pre-filter rule that matches this string.
180
+ * Returns the rule if matched, undefined if the string should pass through.
181
+ */
182
+ function findMatchingRule(str) {
183
+ for (const rule of PRE_FILTER_RULES) {
184
+ if (rule.context !== void 0 && str.context === rule.context) return rule;
185
+ if (rule.condition !== void 0 && rule.condition(str.value)) return rule;
186
+ }
187
+ }
188
+ //#endregion
189
+ //#region src/core/ast-scanner/index.ts
190
+ const TSX_EXTENSIONS = new Set([
191
+ ".tsx",
192
+ ".jsx",
193
+ ".ts",
194
+ ".js",
195
+ ".mjs"
196
+ ]);
197
+ const VUE_EXTENSIONS = new Set([".vue"]);
198
+ const SVELTE_EXTENSIONS = new Set([".svelte"]);
199
+ const ASTRO_EXTENSIONS = new Set([".astro"]);
200
+ /**
201
+ * Lazily import vue-parser. Returns undefined if not available
202
+ * (@vue/compiler-sfc is an optional dependency).
203
+ */
204
+ async function loadVueParser() {
205
+ try {
206
+ return (await import("./vue-parser-CjS4YAoQ.mjs")).parseVue;
207
+ } catch {
208
+ return;
209
+ }
210
+ }
211
+ /**
212
+ * Lazily import svelte-parser. Returns undefined if not available
213
+ * (svelte is an optional dependency).
214
+ */
215
+ async function loadSvelteParser() {
216
+ try {
217
+ return (await import("./svelte-parser-COTNG92w.mjs")).parseSvelte;
218
+ } catch {
219
+ return;
220
+ }
221
+ }
222
+ /**
223
+ * Lazily import astro-parser. Returns undefined if not available
224
+ * (@astrojs/compiler is an optional dependency).
225
+ */
226
+ async function loadAstroParser() {
227
+ try {
228
+ return (await import("./astro-parser-RofKmKQi.mjs")).parseAstro;
229
+ } catch {
230
+ return;
231
+ }
232
+ }
233
+ const SURROUNDING_MAX = 120;
234
+ /**
235
+ * Minimal regex-based extractor for file types without AST parsers.
236
+ * Extracts quoted strings and tag text — conservative, low accuracy.
237
+ * Used as fallback when dedicated parsers are unavailable.
238
+ */
239
+ function extractWithRegex(content, _filePath) {
240
+ const lines = content.split("\n");
241
+ const results = [];
242
+ const stringRe = /(['"`])(?:(?!\1|\\).|\\.)*?\1/g;
243
+ for (let i = 0; i < lines.length; i++) {
244
+ const line = lines[i];
245
+ const trimmed = line.trim();
246
+ if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) continue;
247
+ if (/^\s*(import|export)\s/.test(line)) continue;
248
+ let match;
249
+ stringRe.lastIndex = 0;
250
+ while ((match = stringRe.exec(line)) !== null) {
251
+ const value = match[0].slice(1, -1);
252
+ if (value.length === 0) continue;
253
+ const surrounding = buildSurrounding(lines, i);
254
+ results.push({
255
+ value,
256
+ line: i + 1,
257
+ column: match.index + 1,
258
+ context: "other",
259
+ scope: "script",
260
+ parent: "",
261
+ surrounding
262
+ });
263
+ }
264
+ const tagTextRe = />([^<>{]+)</g;
265
+ let tagMatch;
266
+ tagTextRe.lastIndex = 0;
267
+ while ((tagMatch = tagTextRe.exec(line)) !== null) {
268
+ const text = tagMatch[1].trim();
269
+ if (text.length === 0) continue;
270
+ if (/^[\s\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue;
271
+ results.push({
272
+ value: text,
273
+ line: i + 1,
274
+ column: tagMatch.index + 1,
275
+ context: "template_text",
276
+ scope: "template",
277
+ parent: "",
278
+ surrounding: buildSurrounding(lines, i)
279
+ });
280
+ }
281
+ }
282
+ return results;
283
+ }
284
+ function buildSurrounding(lines, lineIdx) {
285
+ const start = Math.max(0, lineIdx - 1);
286
+ const end = Math.min(lines.length - 1, lineIdx + 1);
287
+ const parts = [];
288
+ for (let i = start; i <= end; i++) {
289
+ const line = lines[i];
290
+ if (line !== void 0) parts.push(line);
291
+ }
292
+ const joined = parts.join("\n");
293
+ if (joined.length > SURROUNDING_MAX) return joined.slice(0, SURROUNDING_MAX);
294
+ return joined;
295
+ }
296
+ /**
297
+ * Extract strings from a source file with structural context.
298
+ *
299
+ * Routes to the correct parser based on file extension:
300
+ * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based)
301
+ * - .vue -> vue-parser (lazy-loaded, AST-based)
302
+ * - .svelte -> svelte-parser (lazy-loaded, AST-based)
303
+ * - .astro -> astro-parser (lazy-loaded, AST-based)
304
+ * - unknown -> empty array
305
+ *
306
+ * Falls back to regex extraction when the dedicated parser's
307
+ * optional dependency is not installed.
308
+ *
309
+ * Applies structural pre-filter to remove 100% non-content strings.
310
+ * Returns only candidates that should be sent to the agent.
311
+ */
312
+ async function extractStrings(filePath, content, ext) {
313
+ const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`;
314
+ let rawStrings;
315
+ if (TSX_EXTENSIONS.has(normalizedExt)) rawStrings = parseTsx(content, filePath);
316
+ else if (VUE_EXTENSIONS.has(normalizedExt)) {
317
+ const parseVue = await loadVueParser();
318
+ if (parseVue) rawStrings = await parseVue(content, filePath);
319
+ else rawStrings = extractWithRegex(content, filePath);
320
+ } else if (SVELTE_EXTENSIONS.has(normalizedExt)) {
321
+ const parseSvelte = await loadSvelteParser();
322
+ if (parseSvelte) try {
323
+ rawStrings = await parseSvelte(content, filePath);
324
+ } catch {
325
+ rawStrings = extractWithRegex(content, filePath);
326
+ }
327
+ else rawStrings = extractWithRegex(content, filePath);
328
+ } else if (ASTRO_EXTENSIONS.has(normalizedExt)) {
329
+ const parseAstro = await loadAstroParser();
330
+ if (parseAstro) try {
331
+ rawStrings = await parseAstro(content, filePath);
332
+ } catch {
333
+ rawStrings = extractWithRegex(content, filePath);
334
+ }
335
+ else rawStrings = extractWithRegex(content, filePath);
336
+ } else return [];
337
+ const { candidates } = applyPreFilter(rawStrings);
338
+ return candidates;
339
+ }
340
+ //#endregion
341
+ //#region src/core/scanner.ts
342
+ const DEFAULT_LIMIT = 50;
343
+ const DEFAULT_OFFSET = 0;
344
+ const DEFAULT_MIN_LENGTH = 2;
345
+ const DEFAULT_MAX_LENGTH = 500;
346
+ const SUMMARY_SAMPLE_SIZE = 10;
347
+ const CONTEXT_MAP = {
348
+ "template_text": "template_text",
349
+ "template_attribute": "template_attribute",
350
+ "jsx_text": "jsx_text",
351
+ "jsx_attribute": "jsx_attribute",
352
+ "variable_assignment": "variable_assignment",
353
+ "object_property": "object_value",
354
+ "function_argument": "function_argument",
355
+ "array_element": "other",
356
+ "enum_value": "other",
357
+ "template_literal": "other",
358
+ "switch_case": "other",
359
+ "other": "other",
360
+ "import_path": "other",
361
+ "type_annotation": "other",
362
+ "css_class": "other",
363
+ "css_utility_call": "other",
364
+ "console_call": "other",
365
+ "test_assertion": "other"
366
+ };
367
+ const IMPORT_PATH_RE = /^(\.\/|\.\.\/|@\/|@[a-z]|[a-z][a-z0-9-]*\/)/;
368
+ const CSS_CLASS_PREFIX_RE = /^(flex|grid|block|inline|hidden|absolute|relative|fixed|sticky|p-|m-|w-|h-|bg-|text-|border-|rounded|shadow|gap-|space-|font-|leading-|tracking-|opacity-|z-|overflow-|cursor-|transition|transform|animate-|hover:|focus:|active:|dark:|sm:|md:|lg:|xl:)/;
369
+ const URL_ROUTE_RE = /^(https?:\/\/|\/api\/|\/v[0-9]|#\/|mailto:|tel:)/;
370
+ const ROUTE_LIKE_RE = /^\/[a-z0-9/:_-]*$/i;
371
+ const COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\(|transparent|inherit|currentColor)$/i;
372
+ const CAMEL_CASE_RE = /^[a-z][a-zA-Z0-9]*$/;
373
+ const SCREAMING_SNAKE_RE = /^[A-Z][A-Z0-9_]+$/;
374
+ const KEBAB_TECH_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
375
+ const FILE_EXT_RE = /\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i;
376
+ const REGEX_LIKE_RE = /^\^|^\(.*\)$|\$$|\\d|\\w|\\s|\.\*|\.\+/;
377
+ const CONSOLE_RE = /console\.(log|warn|error|debug|info|trace)\s*\(/;
378
+ const CONTENT_WORDS = new Set([
379
+ "sign",
380
+ "up",
381
+ "log",
382
+ "in",
383
+ "out",
384
+ "get",
385
+ "started",
386
+ "learn",
387
+ "more",
388
+ "read",
389
+ "click",
390
+ "here",
391
+ "go",
392
+ "back",
393
+ "next",
394
+ "prev",
395
+ "previous",
396
+ "see",
397
+ "all",
398
+ "view",
399
+ "show",
400
+ "hide",
401
+ "open",
402
+ "close",
403
+ "add",
404
+ "new",
405
+ "save",
406
+ "edit",
407
+ "delete",
408
+ "remove",
409
+ "submit",
410
+ "cancel",
411
+ "confirm",
412
+ "welcome",
413
+ "hello",
414
+ "thank",
415
+ "you",
416
+ "good",
417
+ "bye",
418
+ "hi",
419
+ "hey",
420
+ "no",
421
+ "yes",
422
+ "ok",
423
+ "sorry",
424
+ "please",
425
+ "help",
426
+ "about",
427
+ "contact",
428
+ "home",
429
+ "page",
430
+ "not",
431
+ "found",
432
+ "error",
433
+ "success",
434
+ "loading"
435
+ ]);
436
+ /**
437
+ * Determines if a string is non-content (technical) and should be filtered out.
438
+ * Pure function — no side effects, no semantic decisions.
439
+ * Conservative: if in doubt, returns false (keeps the string).
440
+ *
441
+ * @deprecated Kept for backward compatibility. Scanner v2 uses structural pre-filter instead.
442
+ */
443
+ function isNonContent(value, lineContext, minLength = DEFAULT_MIN_LENGTH, maxLength = DEFAULT_MAX_LENGTH) {
444
+ if (value.length < minLength) return true;
445
+ if (value.length > maxLength) return true;
446
+ if (IMPORT_PATH_RE.test(value) && !value.includes(" ")) return true;
447
+ if (CSS_CLASS_PREFIX_RE.test(value) && !value.includes(" ")) return true;
448
+ if (value.includes(" ") && looksLikeCssClasses(value)) return true;
449
+ if (URL_ROUTE_RE.test(value)) return true;
450
+ if (value.startsWith("/") && ROUTE_LIKE_RE.test(value)) return true;
451
+ if (COLOR_RE.test(value)) return true;
452
+ if (isTechnicalIdentifier(value)) return true;
453
+ if (FILE_EXT_RE.test(value)) return true;
454
+ if (REGEX_LIKE_RE.test(value)) return true;
455
+ if (CONSOLE_RE.test(lineContext)) return true;
456
+ return false;
457
+ }
458
+ function isTechnicalIdentifier(value) {
459
+ if (value.includes(" ")) return false;
460
+ if (CAMEL_CASE_RE.test(value) && /[A-Z]/.test(value)) return true;
461
+ if (SCREAMING_SNAKE_RE.test(value) && value.includes("_")) return true;
462
+ if (KEBAB_TECH_RE.test(value)) {
463
+ if (!value.split("-").some((p) => CONTENT_WORDS.has(p))) return true;
464
+ }
465
+ if (/^[a-z][a-z0-9-]*$/.test(value) && value.length < 40 && !value.includes(" ")) {
466
+ if (value.includes("/")) return true;
467
+ }
468
+ return false;
469
+ }
470
+ function looksLikeCssClasses(value) {
471
+ const tokens = value.split(/\s+/);
472
+ if (tokens.length < 2) return false;
473
+ return tokens.filter((t) => isCssClassToken(t)).length === tokens.length;
474
+ }
475
+ function isCssClassToken(token) {
476
+ if (CSS_CLASS_PREFIX_RE.test(token)) return true;
477
+ if (/^(flex|grid|block|inline|hidden|container|relative|absolute|fixed|sticky|static|isolate|overflow-hidden|overflow-auto|overflow-scroll|underline|italic|uppercase|lowercase|capitalize|truncate|break-words|break-all|whitespace-nowrap|sr-only|not-sr-only)$/.test(token)) return true;
478
+ if (/^[a-z]+-\[.+\]$/.test(token)) return true;
479
+ if (/^(sm|md|lg|xl|2xl|hover|focus|active|disabled|first|last|odd|even|dark|group-hover|peer-checked):/.test(token)) return true;
480
+ return false;
481
+ }
482
+ /**
483
+ * Apply legacy isNonContent filter as a secondary pass on v2 candidates.
484
+ * This catches heuristic patterns (URLs, regex, camelCase identifiers, etc.)
485
+ * that the structural pre-filter does not cover.
486
+ */
487
+ function applySecondaryFilter(value, _surrounding, minLength, maxLength) {
488
+ return isNonContent(value, "", minLength, maxLength);
489
+ }
490
+ async function scanCandidates(projectRoot, options) {
491
+ const limit = options?.limit ?? DEFAULT_LIMIT;
492
+ const offset = options?.offset ?? DEFAULT_OFFSET;
493
+ const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH;
494
+ const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH;
495
+ const files = await discoverFiles(projectRoot, {
496
+ paths: options?.paths ?? await autoDetectSourceDirs(projectRoot),
497
+ include: options?.include,
498
+ exclude: options?.exclude
499
+ });
500
+ const allCandidates = [];
501
+ const dupeMap = /* @__PURE__ */ new Map();
502
+ let rawStringsFound = 0;
503
+ const filePromises = files.map(async (relPath) => {
504
+ const filePath = join(projectRoot, relPath);
505
+ const content = await readText(filePath);
506
+ if (!content) return {
507
+ relPath,
508
+ extractions: []
509
+ };
510
+ return {
511
+ relPath,
512
+ extractions: await extractStrings(filePath, content, extname(filePath))
513
+ };
514
+ });
515
+ const fileResults = await Promise.all(filePromises);
516
+ for (const { relPath, extractions } of fileResults) {
517
+ rawStringsFound += extractions.length;
518
+ for (const extraction of extractions) {
519
+ if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue;
520
+ const mappedContext = CONTEXT_MAP[extraction.context];
521
+ const candidate = {
522
+ file: relPath,
523
+ line: extraction.line,
524
+ column: extraction.column,
525
+ value: extraction.value,
526
+ context: mappedContext,
527
+ surrounding: extraction.surrounding
528
+ };
529
+ allCandidates.push(candidate);
530
+ const key = extraction.value;
531
+ if (!dupeMap.has(key)) dupeMap.set(key, []);
532
+ dupeMap.get(key).push({
533
+ file: relPath,
534
+ line: extraction.line
535
+ });
536
+ }
537
+ }
538
+ const duplicates = [...dupeMap.entries()].filter(([, occurrences]) => occurrences.length >= 2).map(([value, occurrences]) => ({
539
+ value,
540
+ count: occurrences.length,
541
+ occurrences
542
+ })).toSorted((a, b) => b.count - a.count);
543
+ const afterFiltering = allCandidates.length;
544
+ const paginated = allCandidates.slice(offset, offset + limit);
545
+ const hasMore = afterFiltering > offset + limit;
546
+ return {
547
+ candidates: paginated,
548
+ duplicates,
549
+ stats: {
550
+ files_scanned: files.length,
551
+ raw_strings_found: rawStringsFound,
552
+ after_filtering: afterFiltering,
553
+ candidates_returned: paginated.length,
554
+ has_more: hasMore
555
+ }
556
+ };
557
+ }
558
+ async function scanSummary(projectRoot, options) {
559
+ const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH;
560
+ const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH;
561
+ const files = await discoverFiles(projectRoot, {
562
+ paths: options?.paths ?? await autoDetectSourceDirs(projectRoot),
563
+ include: options?.include,
564
+ exclude: options?.exclude
565
+ });
566
+ const dirFiles = /* @__PURE__ */ new Map();
567
+ const fileTypes = {};
568
+ for (const relPath of files) {
569
+ const dir = relPath.split("/").slice(0, -1).join("/") || ".";
570
+ const ext = extname(relPath);
571
+ if (!dirFiles.has(dir)) dirFiles.set(dir, []);
572
+ dirFiles.get(dir).push(relPath);
573
+ fileTypes[ext] = (fileTypes[ext] ?? 0) + 1;
574
+ }
575
+ const byDirectory = {};
576
+ const freqMap = /* @__PURE__ */ new Map();
577
+ let totalCandidatesEstimate = 0;
578
+ for (const [dir, dirFileList] of dirFiles) {
579
+ const totalInDir = dirFileList.length;
580
+ const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE);
581
+ let sampleCandidates = 0;
582
+ const samplePromises = sampleFiles.map(async (relPath) => {
583
+ const filePath = join(projectRoot, relPath);
584
+ const content = await readText(filePath);
585
+ if (!content) return [];
586
+ return extractStrings(filePath, content, extname(filePath));
587
+ });
588
+ const sampleResults = await Promise.all(samplePromises);
589
+ for (const extractions of sampleResults) for (const extraction of extractions) {
590
+ if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue;
591
+ sampleCandidates++;
592
+ const prev = freqMap.get(extraction.value) ?? 0;
593
+ freqMap.set(extraction.value, prev + 1);
594
+ }
595
+ const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0;
596
+ const estimatedCandidates = Math.round(avgPerFile * totalInDir);
597
+ byDirectory[dir] = {
598
+ files: totalInDir,
599
+ candidates: estimatedCandidates
600
+ };
601
+ totalCandidatesEstimate += estimatedCandidates;
602
+ }
603
+ const topRepeated = [...freqMap.entries()].filter(([, count]) => count >= 2).toSorted((a, b) => b[1] - a[1]).slice(0, 20).map(([value, count]) => ({
604
+ value,
605
+ count
606
+ }));
607
+ return {
608
+ total_files: files.length,
609
+ total_candidates_estimate: totalCandidatesEstimate,
610
+ by_directory: byDirectory,
611
+ top_repeated: topRepeated,
612
+ sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`,
613
+ file_types: fileTypes
614
+ };
615
+ }
616
+ //#endregion
617
+ export { scanCandidates as n, scanSummary as r, isNonContent as t };
618
+
619
+ //# sourceMappingURL=scanner-BdTiv8sw.mjs.map