@contentrain/mcp 1.0.7 → 1.1.1

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 (74) hide show
  1. package/README.md +13 -2
  2. package/dist/{apply-manager-BDtHM8z-.mjs → apply-manager-Cf5OWOJ7.mjs} +7 -7
  3. package/dist/{apply-manager-BDtHM8z-.mjs.map → apply-manager-Cf5OWOJ7.mjs.map} +1 -1
  4. package/dist/{branch-lifecycle-DB6sgeJi.mjs → branch-lifecycle-wGJg25LT.mjs} +31 -12
  5. package/dist/branch-lifecycle-wGJg25LT.mjs.map +1 -0
  6. package/dist/{config-CPlhDwsv.mjs → config-B5kc2oeV.mjs} +2 -2
  7. package/dist/{config-CPlhDwsv.mjs.map → config-B5kc2oeV.mjs.map} +1 -1
  8. package/dist/{context-CUFulSi2.mjs → context-DIDt_TxX.mjs} +4 -4
  9. package/dist/{context-CUFulSi2.mjs.map → context-DIDt_TxX.mjs.map} +1 -1
  10. package/dist/core/apply-manager.mjs +10 -5
  11. package/dist/core/config.mjs +3 -1
  12. package/dist/core/content-manager.d.mts +1 -9
  13. package/dist/core/content-manager.d.mts.map +1 -1
  14. package/dist/core/content-manager.mjs +4 -1
  15. package/dist/core/context.mjs +6 -2
  16. package/dist/core/graph-builder.mjs +4 -1
  17. package/dist/core/meta-manager.mjs +3 -1
  18. package/dist/core/model-manager.mjs +4 -1
  19. package/dist/core/scan-config.mjs +3 -1
  20. package/dist/core/scanner.mjs +4 -1
  21. package/dist/core/validator.d.mts.map +1 -1
  22. package/dist/core/validator.mjs +6 -2
  23. package/dist/{detect-C8XmJ4S6.mjs → detect-DinOQAmK.mjs} +2 -2
  24. package/dist/{detect-C8XmJ4S6.mjs.map → detect-DinOQAmK.mjs.map} +1 -1
  25. package/dist/{fs-BbuGnVcG.mjs → fs-BnIQ4dKj.mjs} +2 -2
  26. package/dist/{fs-BbuGnVcG.mjs.map → fs-BnIQ4dKj.mjs.map} +1 -1
  27. package/dist/git/branch-lifecycle.d.mts.map +1 -1
  28. package/dist/git/branch-lifecycle.mjs +4 -1
  29. package/dist/git/transaction.d.mts +7 -3
  30. package/dist/git/transaction.d.mts.map +1 -1
  31. package/dist/git/transaction.mjs +9 -4
  32. package/dist/{graph-builder-fPrDDwnu.mjs → graph-builder-tM7zj-SX.mjs} +3 -3
  33. package/dist/{graph-builder-fPrDDwnu.mjs.map → graph-builder-tM7zj-SX.mjs.map} +1 -1
  34. package/dist/{id-BdD4HvMY.mjs → id-DV_T9Ic8.mjs} +3 -5
  35. package/dist/id-DV_T9Ic8.mjs.map +1 -0
  36. package/dist/index.mjs +16 -9
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{meta-manager-CECrfsKm.mjs → meta-manager--0IwAPCl.mjs} +2 -2
  39. package/dist/{meta-manager-CECrfsKm.mjs.map → meta-manager--0IwAPCl.mjs.map} +1 -1
  40. package/dist/{model-manager-kl78AAMg.mjs → model-manager-DW_fOGRM.mjs} +16 -186
  41. package/dist/model-manager-DW_fOGRM.mjs.map +1 -0
  42. package/dist/{scan-config-AJGxCdNd.mjs → scan-config-QgnJ1-52.mjs} +2 -2
  43. package/dist/{scan-config-AJGxCdNd.mjs.map → scan-config-QgnJ1-52.mjs.map} +1 -1
  44. package/dist/{scanner-BdTiv8sw.mjs → scanner-DPrbU-oK.mjs} +3 -3
  45. package/dist/{scanner-BdTiv8sw.mjs.map → scanner-DPrbU-oK.mjs.map} +1 -1
  46. package/dist/serializer-CnHTtmc1.mjs +2 -0
  47. package/dist/{server-QgivXkem.mjs → server-BXguCuJb.mjs} +62 -47
  48. package/dist/server-BXguCuJb.mjs.map +1 -0
  49. package/dist/server.mjs +16 -9
  50. package/dist/templates/index.mjs +1 -1
  51. package/dist/{templates-3HWi-PTu.mjs → templates-bRPzYq4M.mjs} +1 -1
  52. package/dist/{templates-3HWi-PTu.mjs.map → templates-bRPzYq4M.mjs.map} +1 -1
  53. package/dist/transaction-B0V0_SE2.mjs +288 -0
  54. package/dist/transaction-B0V0_SE2.mjs.map +1 -0
  55. package/dist/util/detect.mjs +3 -1
  56. package/dist/util/fs.mjs +2 -1
  57. package/dist/util/id.d.mts +2 -1
  58. package/dist/util/id.d.mts.map +1 -1
  59. package/dist/util/id.mjs +1 -1
  60. package/dist/util/serializer.d.mts +2 -14
  61. package/dist/util/serializer.mjs +2 -2
  62. package/dist/{validator-CyckZAVr.mjs → validator-SQYUV2JP.mjs} +23 -197
  63. package/dist/validator-SQYUV2JP.mjs.map +1 -0
  64. package/package.json +3 -3
  65. package/dist/branch-lifecycle-DB6sgeJi.mjs.map +0 -1
  66. package/dist/id-BdD4HvMY.mjs.map +0 -1
  67. package/dist/model-manager-kl78AAMg.mjs.map +0 -1
  68. package/dist/serializer-Dueih-i2.mjs +0 -33
  69. package/dist/serializer-Dueih-i2.mjs.map +0 -1
  70. package/dist/server-QgivXkem.mjs.map +0 -1
  71. package/dist/transaction-_AiXqGuD.mjs +0 -143
  72. package/dist/transaction-_AiXqGuD.mjs.map +0 -1
  73. package/dist/util/serializer.d.mts.map +0 -1
  74. package/dist/validator-CyckZAVr.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"scanner-BdTiv8sw.mjs","names":["FILE_EXT_RE"],"sources":["../src/core/ast-scanner/pre-filter.ts","../src/core/ast-scanner/index.ts","../src/core/scanner.ts"],"sourcesContent":["import type { ExtractedString, PreFilterRule } from './types.js'\n\n// ─── Pre-filter Result ───\n\nexport interface PreFilterResult {\n /** Strings that passed the pre-filter (candidates for agent) */\n candidates: ExtractedString[]\n /** Total number of strings removed by the pre-filter */\n filtered: number\n /** Breakdown: reason string -> count of strings filtered for that reason */\n filterReasons: Record<string, number>\n}\n\n// ─── Value-based patterns (deterministic, no intelligence) ───\n\nconst PURE_NUMBER_RE = /^\\d+(\\.\\d+)?$/\nconst HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i\nconst 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\n\n// ─── HTML / component prop technical values ───\n\nconst HTML_PROP_VALUES = new Set([\n // CSS / variant keywords\n 'class', 'variant', 'secondary', 'outline', 'ghost', 'destructive', 'default', 'primary',\n // Link / target\n '_blank', 'noopener', 'noreferrer',\n // Input / button types\n 'button', 'submit', 'reset', 'text', 'numeric', 'password', 'email', 'checkbox', 'radio',\n // Layout / display\n 'hidden', 'none', 'auto', 'inherit', 'initial',\n // Size tokens\n 'sm', 'md', 'lg', 'xl', 'xs', '2xl', '3xl',\n // Icon variants\n 'icon', 'icon-sm', 'icon-lg',\n])\n\n// ─── Slot / event technical names (single lowercase word) ───\n\nconst SLOT_EVENT_NAMES = new Set([\n 'header', 'footer', 'default', 'trigger', 'content',\n 'sidebar', 'overlay', 'body', 'actions',\n 'click', 'change', 'input', 'focus', 'blur', 'submit',\n 'mounted', 'unmounted', 'updated',\n])\n\n// ─── Tailwind / CSS utility detection ───\n\nconst 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-)/\n\n/**\n * Returns true if the string looks like a CSS class list (Tailwind or similar).\n * Requires 2+ space-separated segments with majority matching utility patterns.\n */\nfunction isCssClassList(value: string): boolean {\n const segments = value.trim().split(/\\s+/)\n if (segments.length < 2) return false\n let matched = 0\n for (const seg of segments) {\n if (TAILWIND_SEGMENT_RE.test(seg)) matched++\n }\n return matched / segments.length >= 0.5\n}\n\n/**\n * Returns true if the string is a single CSS utility token (e.g. \"bg-blue-500\").\n */\nfunction isSingleCssUtility(value: string): boolean {\n const trimmed = value.trim()\n // Must be a single token, no spaces\n if (trimmed.includes(' ')) return false\n return TAILWIND_SEGMENT_RE.test(trimmed)\n}\n\n// ─── Pre-filter Rules ───\n\nconst PRE_FILTER_RULES: PreFilterRule[] = [\n // Context-based rules (AST-determined, 100% accurate)\n { context: 'import_path', reason: 'import_path' },\n { context: 'type_annotation', reason: 'type_annotation' },\n { context: 'css_class', reason: 'css_class' },\n { context: 'css_utility_call', reason: 'css_utility_call' },\n { context: 'console_call', reason: 'console_call' },\n { context: 'test_assertion', reason: 'test_assertion' },\n { context: 'switch_case', reason: 'switch_case' },\n\n // Value-based rules (structural, not heuristic)\n { condition: (v) => v.length <= 1, reason: 'single_char' },\n { condition: (v) => PURE_NUMBER_RE.test(v), reason: 'pure_number' },\n { condition: (v) => v.startsWith('--'), reason: 'cli_flag' },\n { condition: (v) => HEX_COLOR_RE.test(v), reason: 'hex_color' },\n { condition: (v) => FILE_EXT_RE.test(v), reason: 'file_extension' },\n\n // HTML / component prop technical values (exact match, case-insensitive)\n { condition: (v) => HTML_PROP_VALUES.has(v.toLowerCase()), reason: 'html_prop_value' },\n\n // CSS class lists (Tailwind-style multi-segment strings)\n { condition: (v) => isCssClassList(v), reason: 'css_class_list' },\n\n // Single CSS utility token (e.g. \"bg-blue-500\", \"rounded-lg\")\n { condition: (v) => isSingleCssUtility(v), reason: 'css_utility_token' },\n\n // Slot / event technical names (single lowercase word only)\n { condition: (v) => {\n const lower = v.toLowerCase()\n return v === lower && !v.includes(' ') && SLOT_EVENT_NAMES.has(lower)\n }, reason: 'slot_event_name' },\n]\n\n// ─── Public API ───\n\n/**\n * Structural pre-filter: removes strings that are 100% NOT content.\n *\n * Conservative — when in doubt, INCLUDE the string.\n * Returns candidates (passed), count of filtered, and reason breakdown.\n */\nexport function applyPreFilter(strings: ExtractedString[]): PreFilterResult {\n const candidates: ExtractedString[] = []\n const filterReasons: Record<string, number> = {}\n let filtered = 0\n\n for (const str of strings) {\n const matchedRule = findMatchingRule(str)\n\n if (matchedRule) {\n filtered++\n filterReasons[matchedRule.reason] = (filterReasons[matchedRule.reason] ?? 0) + 1\n } else {\n candidates.push(str)\n }\n }\n\n return { candidates, filtered, filterReasons }\n}\n\n/**\n * Find the first pre-filter rule that matches this string.\n * Returns the rule if matched, undefined if the string should pass through.\n */\nfunction findMatchingRule(str: ExtractedString): PreFilterRule | undefined {\n for (const rule of PRE_FILTER_RULES) {\n if (rule.context !== undefined && str.context === rule.context) {\n return rule\n }\n if (rule.condition !== undefined && rule.condition(str.value)) {\n return rule\n }\n }\n return undefined\n}\n","import type { ExtractedString } from './types.js'\nimport { applyPreFilter } from './pre-filter.js'\nimport { parseTsx } from './tsx-parser.js'\n\n// ─── Extension sets ───\n\nconst TSX_EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs'])\nconst VUE_EXTENSIONS = new Set(['.vue'])\nconst SVELTE_EXTENSIONS = new Set(['.svelte'])\nconst ASTRO_EXTENSIONS = new Set(['.astro'])\n\n// ─── Lazy-loaded parsers ───\n\n/**\n * Lazily import vue-parser. Returns undefined if not available\n * (@vue/compiler-sfc is an optional dependency).\n */\nasync function loadVueParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]> | ExtractedString[]) | undefined> {\n try {\n const mod = await import('./vue-parser.js')\n return mod.parseVue\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import svelte-parser. Returns undefined if not available\n * (svelte is an optional dependency).\n */\nasync function loadSvelteParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./svelte-parser.js')\n return mod.parseSvelte\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import astro-parser. Returns undefined if not available\n * (@astrojs/compiler is an optional dependency).\n */\nasync function loadAstroParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./astro-parser.js')\n return mod.parseAstro\n } catch {\n return undefined\n }\n}\n\n// ─── Regex fallback for unknown extensions ───\n\nconst SURROUNDING_MAX = 120\n\n/**\n * Minimal regex-based extractor for file types without AST parsers.\n * Extracts quoted strings and tag text — conservative, low accuracy.\n * Used as fallback when dedicated parsers are unavailable.\n */\nfunction extractWithRegex(content: string, _filePath: string): ExtractedString[] {\n const lines = content.split('\\n')\n const results: ExtractedString[] = []\n\n // Simple quoted string extraction\n const stringRe = /(['\"`])(?:(?!\\1|\\\\).|\\\\.)*?\\1/g\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n const trimmed = line.trim()\n\n // Skip comments, imports\n if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) continue\n if (/^\\s*(import|export)\\s/.test(line)) continue\n\n let match: RegExpExecArray | null\n stringRe.lastIndex = 0\n\n while ((match = stringRe.exec(line)) !== null) {\n // Remove surrounding quotes\n const raw = match[0]\n const value = raw.slice(1, -1)\n if (value.length === 0) continue\n\n const surrounding = buildSurrounding(lines, i)\n\n results.push({\n value,\n line: i + 1,\n column: match.index + 1,\n context: 'other',\n scope: 'script',\n parent: '',\n surrounding,\n })\n }\n\n // Tag text extraction: >text<\n const tagTextRe = />([^<>{]+)</g\n let tagMatch: RegExpExecArray | null\n tagTextRe.lastIndex = 0\n\n while ((tagMatch = tagTextRe.exec(line)) !== null) {\n const text = tagMatch[1]!.trim()\n if (text.length === 0) continue\n if (/^[\\s\\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue\n\n results.push({\n value: text,\n line: i + 1,\n column: tagMatch.index + 1,\n context: 'template_text',\n scope: 'template',\n parent: '',\n surrounding: buildSurrounding(lines, i),\n })\n }\n }\n\n return results\n}\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\n// ─── Public API ───\n\n/**\n * Extract strings from a source file with structural context.\n *\n * Routes to the correct parser based on file extension:\n * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based)\n * - .vue -> vue-parser (lazy-loaded, AST-based)\n * - .svelte -> svelte-parser (lazy-loaded, AST-based)\n * - .astro -> astro-parser (lazy-loaded, AST-based)\n * - unknown -> empty array\n *\n * Falls back to regex extraction when the dedicated parser's\n * optional dependency is not installed.\n *\n * Applies structural pre-filter to remove 100% non-content strings.\n * Returns only candidates that should be sent to the agent.\n */\nexport async function extractStrings(\n filePath: string,\n content: string,\n ext: string,\n): Promise<ExtractedString[]> {\n const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`\n let rawStrings: ExtractedString[]\n\n if (TSX_EXTENSIONS.has(normalizedExt)) {\n rawStrings = parseTsx(content, filePath)\n } else if (VUE_EXTENSIONS.has(normalizedExt)) {\n const parseVue = await loadVueParser()\n if (parseVue) {\n rawStrings = await parseVue(content, filePath)\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (SVELTE_EXTENSIONS.has(normalizedExt)) {\n const parseSvelte = await loadSvelteParser()\n if (parseSvelte) {\n try {\n rawStrings = await parseSvelte(content, filePath)\n } catch {\n // svelte/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (ASTRO_EXTENSIONS.has(normalizedExt)) {\n const parseAstro = await loadAstroParser()\n if (parseAstro) {\n try {\n rawStrings = await parseAstro(content, filePath)\n } catch {\n // @astrojs/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n return []\n }\n\n // Apply structural pre-filter\n const { candidates } = applyPreFilter(rawStrings)\n return candidates\n}\n\n// Re-export types for convenience\nexport type { ExtractedString, StructuralContext, PreFilterRule } from './types.js'\nexport { applyPreFilter } from './pre-filter.js'\nexport type { PreFilterResult } from './pre-filter.js'\n","import type {\n StringContext,\n ScanCandidate,\n DuplicateGroup,\n ScanCandidatesResult,\n ScanSummaryResult,\n} from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText } from '../util/fs.js'\nimport {\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\nimport { extractStrings } from './ast-scanner/index.js'\nimport type { StructuralContext } from './ast-scanner/types.js'\n\n// ─── Options ───\n\nexport interface ScanOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n limit?: number\n offset?: number\n min_length?: number\n max_length?: number\n}\n\n// ─── Constants ───\n\nconst DEFAULT_LIMIT = 50\nconst DEFAULT_OFFSET = 0\nconst DEFAULT_MIN_LENGTH = 2\nconst DEFAULT_MAX_LENGTH = 500\nconst SUMMARY_SAMPLE_SIZE = 10\n\n// ─── Context mapping: StructuralContext → StringContext ───\n\nconst CONTEXT_MAP: Record<StructuralContext, StringContext> = {\n 'template_text': 'template_text',\n 'template_attribute': 'template_attribute',\n 'jsx_text': 'jsx_text',\n 'jsx_attribute': 'jsx_attribute',\n 'variable_assignment': 'variable_assignment',\n 'object_property': 'object_value',\n 'function_argument': 'function_argument',\n 'array_element': 'other',\n 'enum_value': 'other',\n 'template_literal': 'other',\n 'switch_case': 'other',\n 'other': 'other',\n // Pre-filtered contexts should not reach here, but map them just in case\n 'import_path': 'other',\n 'type_annotation': 'other',\n 'css_class': 'other',\n 'css_utility_call': 'other',\n 'console_call': 'other',\n 'test_assertion': 'other',\n}\n\n// ─── Legacy Pre-filter (kept exported for backward compatibility) ───\n\nconst IMPORT_PATH_RE = /^(\\.\\/|\\.\\.\\/|@\\/|@[a-z]|[a-z][a-z0-9-]*\\/)/\nconst 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:)/\nconst URL_ROUTE_RE = /^(https?:\\/\\/|\\/api\\/|\\/v[0-9]|#\\/|mailto:|tel:)/\nconst ROUTE_LIKE_RE = /^\\/[a-z0-9/:_-]*$/i\nconst COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\\(|hsla?\\(|transparent|inherit|currentColor)$/i\nconst CAMEL_CASE_RE = /^[a-z][a-zA-Z0-9]*$/\nconst SCREAMING_SNAKE_RE = /^[A-Z][A-Z0-9_]+$/\nconst KEBAB_TECH_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/\nconst 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\nconst REGEX_LIKE_RE = /^\\^|^\\(.*\\)$|\\$$|\\\\d|\\\\w|\\\\s|\\.\\*|\\.\\+/\nconst CONSOLE_RE = /console\\.(log|warn|error|debug|info|trace)\\s*\\(/\n\n// Common content words that should NOT be filtered when they appear in kebab-case\nconst CONTENT_WORDS = new Set([\n 'sign', 'up', 'log', 'in', 'out', 'get', 'started', 'learn', 'more',\n 'read', 'click', 'here', 'go', 'back', 'next', 'prev', 'previous',\n 'see', 'all', 'view', 'show', 'hide', 'open', 'close', 'add', 'new',\n 'save', 'edit', 'delete', 'remove', 'submit', 'cancel', 'confirm',\n 'welcome', 'hello', 'thank', 'you', 'good', 'bye', 'hi', 'hey',\n 'no', 'yes', 'ok', 'sorry', 'please', 'help', 'about', 'contact',\n 'home', 'page', 'not', 'found', 'error', 'success', 'loading',\n])\n\n/**\n * Determines if a string is non-content (technical) and should be filtered out.\n * Pure function — no side effects, no semantic decisions.\n * Conservative: if in doubt, returns false (keeps the string).\n *\n * @deprecated Kept for backward compatibility. Scanner v2 uses structural pre-filter instead.\n */\nexport function isNonContent(value: string, lineContext: string, minLength: number = DEFAULT_MIN_LENGTH, maxLength: number = DEFAULT_MAX_LENGTH): boolean {\n // Length checks\n if (value.length < minLength) return true\n if (value.length > maxLength) return true\n\n // Import/require paths (no spaces, looks like module path)\n if (IMPORT_PATH_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — single class prefix\n if (CSS_CLASS_PREFIX_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — multiple space-separated tokens that all look like utility classes\n if (value.includes(' ') && looksLikeCssClasses(value)) return true\n\n // URL/route patterns\n if (URL_ROUTE_RE.test(value)) return true\n if (value.startsWith('/') && ROUTE_LIKE_RE.test(value)) return true\n\n // Color codes\n if (COLOR_RE.test(value)) return true\n\n // Technical identifiers (includes kebab-case with content-word awareness)\n if (isTechnicalIdentifier(value)) return true\n\n // File paths with extensions\n if (FILE_EXT_RE.test(value)) return true\n\n // RegExp-like patterns\n if (REGEX_LIKE_RE.test(value)) return true\n\n // Console/log context\n if (CONSOLE_RE.test(lineContext)) return true\n\n return false\n}\n\nfunction isTechnicalIdentifier(value: string): boolean {\n // No spaces allowed in identifiers\n if (value.includes(' ')) return false\n\n // camelCase: starts lowercase, has at least one uppercase letter, no spaces\n if (CAMEL_CASE_RE.test(value) && /[A-Z]/.test(value)) return true\n\n // SCREAMING_SNAKE_CASE — only filter if it contains underscores (true SCREAMING_SNAKE like API_KEY, MAX_COUNT).\n // Short uppercase words without underscores (OK, FAQ, GPS) are likely real UI labels.\n if (SCREAMING_SNAKE_RE.test(value) && value.includes('_')) return true\n\n // kebab-case: filter only if NO content words are present\n if (KEBAB_TECH_RE.test(value)) {\n const parts = value.split('-')\n const hasContentWord = parts.some(p => CONTENT_WORDS.has(p))\n if (!hasContentWord) return true\n }\n\n // Bare npm-like package names: all lowercase, no dashes with content words,\n // no spaces, < 40 chars. Single lowercase words are kept (could be content).\n if (/^[a-z][a-z0-9-]*$/.test(value) && value.length < 40 && !value.includes(' ')) {\n // Only filter if it has a slash (scoped package) or looks like a known package pattern\n // Single words or content-word kebab phrases pass through\n if (value.includes('/')) return true\n }\n\n return false\n}\n\nfunction looksLikeCssClasses(value: string): boolean {\n const tokens = value.split(/\\s+/)\n if (tokens.length < 2) return false\n\n // If ALL tokens look like CSS utilities, filter\n const cssLikeCount = tokens.filter(t => isCssClassToken(t)).length\n return cssLikeCount === tokens.length\n}\n\nfunction isCssClassToken(token: string): boolean {\n // Tailwind-like: prefix- or modifier: patterns\n if (CSS_CLASS_PREFIX_RE.test(token)) return true\n // Common standalone classes\n 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\n // Arbitrary Tailwind with brackets: w-[100px]\n if (/^[a-z]+-\\[.+\\]$/.test(token)) return true\n // Responsive/state prefixes: sm:text-lg, hover:bg-blue-500\n if (/^(sm|md|lg|xl|2xl|hover|focus|active|disabled|first|last|odd|even|dark|group-hover|peer-checked):/.test(token)) return true\n return false\n}\n\n// ─── Secondary filter for v2 candidates ───\n\n/**\n * Apply legacy isNonContent filter as a secondary pass on v2 candidates.\n * This catches heuristic patterns (URLs, regex, camelCase identifiers, etc.)\n * that the structural pre-filter does not cover.\n */\nfunction applySecondaryFilter(\n value: string,\n _surrounding: string,\n minLength: number,\n maxLength: number,\n): boolean {\n // Pass empty lineContext — v2 pre-filter handles context-based detection (console, test, etc.)\n // Using surrounding would cause false positives because it spans +-1 lines\n return isNonContent(value, '', minLength, maxLength)\n}\n\n// ─── Main: scanCandidates ───\n\nexport async function scanCandidates(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanCandidatesResult> {\n const limit = options?.limit ?? DEFAULT_LIMIT\n const offset = options?.offset ?? DEFAULT_OFFSET\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n const allCandidates: ScanCandidate[] = []\n const dupeMap = new Map<string, Array<{ file: string; line: number }>>()\n let rawStringsFound = 0\n\n const filePromises = files.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return { relPath, extractions: [] }\n\n const ext = extname(filePath)\n const extractions = await extractStrings(filePath, content, ext)\n return { relPath, extractions }\n })\n\n const fileResults = await Promise.all(filePromises)\n\n for (const { relPath, extractions } of fileResults) {\n rawStringsFound += extractions.length\n\n for (const extraction of extractions) {\n // Apply secondary (legacy) filter for heuristic patterns\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n\n const mappedContext = CONTEXT_MAP[extraction.context]\n\n const candidate: ScanCandidate = {\n file: relPath,\n line: extraction.line,\n column: extraction.column,\n value: extraction.value,\n context: mappedContext,\n surrounding: extraction.surrounding,\n }\n allCandidates.push(candidate)\n\n // Track duplicates\n const key = extraction.value\n if (!dupeMap.has(key)) {\n dupeMap.set(key, [])\n }\n dupeMap.get(key)!.push({ file: relPath, line: extraction.line })\n }\n }\n\n // Build duplicate groups (only count >= 2), sorted by count descending\n const duplicates: DuplicateGroup[] = [...dupeMap.entries()]\n .filter(([, occurrences]) => occurrences.length >= 2)\n .map(([value, occurrences]) => ({ value, count: occurrences.length, occurrences }))\n .toSorted((a, b) => b.count - a.count)\n\n // Pagination\n const afterFiltering = allCandidates.length\n const paginated = allCandidates.slice(offset, offset + limit)\n const hasMore = afterFiltering > offset + limit\n\n return {\n candidates: paginated,\n duplicates,\n stats: {\n files_scanned: files.length,\n raw_strings_found: rawStringsFound,\n after_filtering: afterFiltering,\n candidates_returned: paginated.length,\n has_more: hasMore,\n },\n }\n}\n\n// ─── Main: scanSummary ───\n\nexport async function scanSummary(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanSummaryResult> {\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // Group files by directory\n const dirFiles = new Map<string, string[]>()\n const fileTypes: Record<string, number> = {}\n\n for (const relPath of files) {\n const parts = relPath.split('/')\n const dir = parts.slice(0, -1).join('/') || '.'\n const ext = extname(relPath)\n\n if (!dirFiles.has(dir)) dirFiles.set(dir, [])\n dirFiles.get(dir)!.push(relPath)\n\n fileTypes[ext] = (fileTypes[ext] ?? 0) + 1\n }\n\n // Sample files per directory and count candidates\n const byDirectory: Record<string, { files: number; candidates: number }> = {}\n const freqMap = new Map<string, number>()\n let totalCandidatesEstimate = 0\n\n for (const [dir, dirFileList] of dirFiles) {\n const totalInDir = dirFileList.length\n const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE)\n let sampleCandidates = 0\n\n const samplePromises = sampleFiles.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return []\n\n const ext = extname(filePath)\n return extractStrings(filePath, content, ext)\n })\n\n const sampleResults = await Promise.all(samplePromises)\n\n for (const extractions of sampleResults) {\n for (const extraction of extractions) {\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n sampleCandidates++\n\n // Track frequencies for top_repeated\n const prev = freqMap.get(extraction.value) ?? 0\n freqMap.set(extraction.value, prev + 1)\n }\n }\n\n // Estimate for full directory\n const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0\n const estimatedCandidates = Math.round(avgPerFile * totalInDir)\n\n byDirectory[dir] = {\n files: totalInDir,\n candidates: estimatedCandidates,\n }\n\n totalCandidatesEstimate += estimatedCandidates\n }\n\n // Top repeated strings\n const topRepeated = [...freqMap.entries()]\n .filter(([, count]) => count >= 2)\n .toSorted((a, b) => b[1] - a[1])\n .slice(0, 20)\n .map(([value, count]) => ({ value, count }))\n\n return {\n total_files: files.length,\n total_candidates_estimate: totalCandidatesEstimate,\n by_directory: byDirectory,\n top_repeated: topRepeated,\n sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`,\n file_types: fileTypes,\n }\n}\n"],"mappings":";;;;;AAeA,MAAM,iBAAiB;AACvB,MAAM,eAAe;AACrB,MAAMA,gBAAc;AAIpB,MAAM,mBAAmB,IAAI,IAAI;CAE/B;CAAS;CAAW;CAAa;CAAW;CAAS;CAAe;CAAW;CAE/E;CAAU;CAAY;CAEtB;CAAU;CAAU;CAAS;CAAQ;CAAW;CAAY;CAAS;CAAY;CAEjF;CAAU;CAAQ;CAAQ;CAAW;CAErC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAO;CAErC;CAAQ;CAAW;CACpB,CAAC;AAIF,MAAM,mBAAmB,IAAI,IAAI;CAC/B;CAAU;CAAU;CAAW;CAAW;CAC1C;CAAW;CAAW;CAAQ;CAC9B;CAAS;CAAU;CAAS;CAAS;CAAQ;CAC7C;CAAW;CAAa;CACzB,CAAC;AAIF,MAAM,sBAAsB;;;;;AAM5B,SAAS,eAAe,OAAwB;CAC9C,MAAM,WAAW,MAAM,MAAM,CAAC,MAAM,MAAM;AAC1C,KAAI,SAAS,SAAS,EAAG,QAAO;CAChC,IAAI,UAAU;AACd,MAAK,MAAM,OAAO,SAChB,KAAI,oBAAoB,KAAK,IAAI,CAAE;AAErC,QAAO,UAAU,SAAS,UAAU;;;;;AAMtC,SAAS,mBAAmB,OAAwB;CAClD,MAAM,UAAU,MAAM,MAAM;AAE5B,KAAI,QAAQ,SAAS,IAAI,CAAE,QAAO;AAClC,QAAO,oBAAoB,KAAK,QAAQ;;AAK1C,MAAM,mBAAoC;CAExC;EAAE,SAAS;EAAe,QAAQ;EAAe;CACjD;EAAE,SAAS;EAAmB,QAAQ;EAAmB;CACzD;EAAE,SAAS;EAAa,QAAQ;EAAa;CAC7C;EAAE,SAAS;EAAoB,QAAQ;EAAoB;CAC3D;EAAE,SAAS;EAAgB,QAAQ;EAAgB;CACnD;EAAE,SAAS;EAAkB,QAAQ;EAAkB;CACvD;EAAE,SAAS;EAAe,QAAQ;EAAe;CAGjD;EAAE,YAAY,MAAM,EAAE,UAAU;EAAG,QAAQ;EAAe;CAC1D;EAAE,YAAY,MAAM,eAAe,KAAK,EAAE;EAAE,QAAQ;EAAe;CACnE;EAAE,YAAY,MAAM,EAAE,WAAW,KAAK;EAAE,QAAQ;EAAY;CAC5D;EAAE,YAAY,MAAM,aAAa,KAAK,EAAE;EAAE,QAAQ;EAAa;CAC/D;EAAE,YAAY,MAAMA,cAAY,KAAK,EAAE;EAAE,QAAQ;EAAkB;CAGnE;EAAE,YAAY,MAAM,iBAAiB,IAAI,EAAE,aAAa,CAAC;EAAE,QAAQ;EAAmB;CAGtF;EAAE,YAAY,MAAM,eAAe,EAAE;EAAE,QAAQ;EAAkB;CAGjE;EAAE,YAAY,MAAM,mBAAmB,EAAE;EAAE,QAAQ;EAAqB;CAGxE;EAAE,YAAY,MAAM;GAClB,MAAM,QAAQ,EAAE,aAAa;AAC7B,UAAO,MAAM,SAAS,CAAC,EAAE,SAAS,IAAI,IAAI,iBAAiB,IAAI,MAAM;;EACpE,QAAQ;EAAmB;CAC/B;;;;;;;AAUD,SAAgB,eAAe,SAA6C;CAC1E,MAAM,aAAgC,EAAE;CACxC,MAAM,gBAAwC,EAAE;CAChD,IAAI,WAAW;AAEf,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,cAAc,iBAAiB,IAAI;AAEzC,MAAI,aAAa;AACf;AACA,iBAAc,YAAY,WAAW,cAAc,YAAY,WAAW,KAAK;QAE/E,YAAW,KAAK,IAAI;;AAIxB,QAAO;EAAE;EAAY;EAAU;EAAe;;;;;;AAOhD,SAAS,iBAAiB,KAAiD;AACzE,MAAK,MAAM,QAAQ,kBAAkB;AACnC,MAAI,KAAK,YAAY,KAAA,KAAa,IAAI,YAAY,KAAK,QACrD,QAAO;AAET,MAAI,KAAK,cAAc,KAAA,KAAa,KAAK,UAAU,IAAI,MAAM,CAC3D,QAAO;;;;;AC3Ib,MAAM,iBAAiB,IAAI,IAAI;CAAC;CAAQ;CAAQ;CAAO;CAAO;CAAO,CAAC;AACtE,MAAM,iBAAiB,IAAI,IAAI,CAAC,OAAO,CAAC;AACxC,MAAM,oBAAoB,IAAI,IAAI,CAAC,UAAU,CAAC;AAC9C,MAAM,mBAAmB,IAAI,IAAI,CAAC,SAAS,CAAC;;;;;AAQ5C,eAAe,gBAA8H;AAC3I,KAAI;AAEF,UADY,MAAM,OAAO,8BACd;SACL;AACN;;;;;;;AAQJ,eAAe,mBAA6G;AAC1H,KAAI;AAEF,UADY,MAAM,OAAO,iCACd;SACL;AACN;;;;;;;AAQJ,eAAe,kBAA4G;AACzH,KAAI;AAEF,UADY,MAAM,OAAO,gCACd;SACL;AACN;;;AAMJ,MAAM,kBAAkB;;;;;;AAOxB,SAAS,iBAAiB,SAAiB,WAAsC;CAC/E,MAAM,QAAQ,QAAQ,MAAM,KAAK;CACjC,MAAM,UAA6B,EAAE;CAGrC,MAAM,WAAW;AAEjB,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,OAAO,MAAM;EACnB,MAAM,UAAU,KAAK,MAAM;AAG3B,MAAI,QAAQ,WAAW,KAAK,IAAI,QAAQ,WAAW,KAAK,IAAI,QAAQ,WAAW,IAAI,CAAE;AACrF,MAAI,wBAAwB,KAAK,KAAK,CAAE;EAExC,IAAI;AACJ,WAAS,YAAY;AAErB,UAAQ,QAAQ,SAAS,KAAK,KAAK,MAAM,MAAM;GAG7C,MAAM,QADM,MAAM,GACA,MAAM,GAAG,GAAG;AAC9B,OAAI,MAAM,WAAW,EAAG;GAExB,MAAM,cAAc,iBAAiB,OAAO,EAAE;AAE9C,WAAQ,KAAK;IACX;IACA,MAAM,IAAI;IACV,QAAQ,MAAM,QAAQ;IACtB,SAAS;IACT,OAAO;IACP,QAAQ;IACR;IACD,CAAC;;EAIJ,MAAM,YAAY;EAClB,IAAI;AACJ,YAAU,YAAY;AAEtB,UAAQ,WAAW,UAAU,KAAK,KAAK,MAAM,MAAM;GACjD,MAAM,OAAO,SAAS,GAAI,MAAM;AAChC,OAAI,KAAK,WAAW,EAAG;AACvB,OAAI,YAAY,KAAK,KAAK,IAAI,CAAC,WAAW,KAAK,KAAK,CAAE;AAEtD,WAAQ,KAAK;IACX,OAAO;IACP,MAAM,IAAI;IACV,QAAQ,SAAS,QAAQ;IACzB,SAAS;IACT,OAAO;IACP,QAAQ;IACR,aAAa,iBAAiB,OAAO,EAAE;IACxC,CAAC;;;AAIN,QAAO;;AAGT,SAAS,iBAAiB,OAAiB,SAAyB;CAClE,MAAM,QAAQ,KAAK,IAAI,GAAG,UAAU,EAAE;CACtC,MAAM,MAAM,KAAK,IAAI,MAAM,SAAS,GAAG,UAAU,EAAE;CAEnD,MAAM,QAAkB,EAAE;AAC1B,MAAK,IAAI,IAAI,OAAO,KAAK,KAAK,KAAK;EACjC,MAAM,OAAO,MAAM;AACnB,MAAI,SAAS,KAAA,EACX,OAAM,KAAK,KAAK;;CAIpB,MAAM,SAAS,MAAM,KAAK,KAAK;AAC/B,KAAI,OAAO,SAAS,gBAClB,QAAO,OAAO,MAAM,GAAG,gBAAgB;AAEzC,QAAO;;;;;;;;;;;;;;;;;;AAqBT,eAAsB,eACpB,UACA,SACA,KAC4B;CAC5B,MAAM,gBAAgB,IAAI,WAAW,IAAI,GAAG,MAAM,IAAI;CACtD,IAAI;AAEJ,KAAI,eAAe,IAAI,cAAc,CACnC,cAAa,SAAS,SAAS,SAAS;UAC/B,eAAe,IAAI,cAAc,EAAE;EAC5C,MAAM,WAAW,MAAM,eAAe;AACtC,MAAI,SACF,cAAa,MAAM,SAAS,SAAS,SAAS;MAE9C,cAAa,iBAAiB,SAAS,SAAS;YAEzC,kBAAkB,IAAI,cAAc,EAAE;EAC/C,MAAM,cAAc,MAAM,kBAAkB;AAC5C,MAAI,YACF,KAAI;AACF,gBAAa,MAAM,YAAY,SAAS,SAAS;UAC3C;AAEN,gBAAa,iBAAiB,SAAS,SAAS;;MAGlD,cAAa,iBAAiB,SAAS,SAAS;YAEzC,iBAAiB,IAAI,cAAc,EAAE;EAC9C,MAAM,aAAa,MAAM,iBAAiB;AAC1C,MAAI,WACF,KAAI;AACF,gBAAa,MAAM,WAAW,SAAS,SAAS;UAC1C;AAEN,gBAAa,iBAAiB,SAAS,SAAS;;MAGlD,cAAa,iBAAiB,SAAS,SAAS;OAGlD,QAAO,EAAE;CAIX,MAAM,EAAE,eAAe,eAAe,WAAW;AACjD,QAAO;;;;ACjLT,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAI5B,MAAM,cAAwD;CAC5D,iBAAiB;CACjB,sBAAsB;CACtB,YAAY;CACZ,iBAAiB;CACjB,uBAAuB;CACvB,mBAAmB;CACnB,qBAAqB;CACrB,iBAAiB;CACjB,cAAc;CACd,oBAAoB;CACpB,eAAe;CACf,SAAS;CAET,eAAe;CACf,mBAAmB;CACnB,aAAa;CACb,oBAAoB;CACpB,gBAAgB;CAChB,kBAAkB;CACnB;AAID,MAAM,iBAAiB;AACvB,MAAM,sBAAsB;AAC5B,MAAM,eAAe;AACrB,MAAM,gBAAgB;AACtB,MAAM,WAAW;AACjB,MAAM,gBAAgB;AACtB,MAAM,qBAAqB;AAC3B,MAAM,gBAAgB;AACtB,MAAM,cAAc;AACpB,MAAM,gBAAgB;AACtB,MAAM,aAAa;AAGnB,MAAM,gBAAgB,IAAI,IAAI;CAC5B;CAAQ;CAAM;CAAO;CAAM;CAAO;CAAO;CAAW;CAAS;CAC7D;CAAQ;CAAS;CAAQ;CAAM;CAAQ;CAAQ;CAAQ;CACvD;CAAO;CAAO;CAAQ;CAAQ;CAAQ;CAAQ;CAAS;CAAO;CAC9D;CAAQ;CAAQ;CAAU;CAAU;CAAU;CAAU;CACxD;CAAW;CAAS;CAAS;CAAO;CAAQ;CAAO;CAAM;CACzD;CAAM;CAAO;CAAM;CAAS;CAAU;CAAQ;CAAS;CACvD;CAAQ;CAAQ;CAAO;CAAS;CAAS;CAAW;CACrD,CAAC;;;;;;;;AASF,SAAgB,aAAa,OAAe,aAAqB,YAAoB,oBAAoB,YAAoB,oBAA6B;AAExJ,KAAI,MAAM,SAAS,UAAW,QAAO;AACrC,KAAI,MAAM,SAAS,UAAW,QAAO;AAGrC,KAAI,eAAe,KAAK,MAAM,IAAI,CAAC,MAAM,SAAS,IAAI,CAAE,QAAO;AAG/D,KAAI,oBAAoB,KAAK,MAAM,IAAI,CAAC,MAAM,SAAS,IAAI,CAAE,QAAO;AAGpE,KAAI,MAAM,SAAS,IAAI,IAAI,oBAAoB,MAAM,CAAE,QAAO;AAG9D,KAAI,aAAa,KAAK,MAAM,CAAE,QAAO;AACrC,KAAI,MAAM,WAAW,IAAI,IAAI,cAAc,KAAK,MAAM,CAAE,QAAO;AAG/D,KAAI,SAAS,KAAK,MAAM,CAAE,QAAO;AAGjC,KAAI,sBAAsB,MAAM,CAAE,QAAO;AAGzC,KAAI,YAAY,KAAK,MAAM,CAAE,QAAO;AAGpC,KAAI,cAAc,KAAK,MAAM,CAAE,QAAO;AAGtC,KAAI,WAAW,KAAK,YAAY,CAAE,QAAO;AAEzC,QAAO;;AAGT,SAAS,sBAAsB,OAAwB;AAErD,KAAI,MAAM,SAAS,IAAI,CAAE,QAAO;AAGhC,KAAI,cAAc,KAAK,MAAM,IAAI,QAAQ,KAAK,MAAM,CAAE,QAAO;AAI7D,KAAI,mBAAmB,KAAK,MAAM,IAAI,MAAM,SAAS,IAAI,CAAE,QAAO;AAGlE,KAAI,cAAc,KAAK,MAAM;MAGvB,CAFU,MAAM,MAAM,IAAI,CACD,MAAK,MAAK,cAAc,IAAI,EAAE,CAAC,CACvC,QAAO;;AAK9B,KAAI,oBAAoB,KAAK,MAAM,IAAI,MAAM,SAAS,MAAM,CAAC,MAAM,SAAS,IAAI;MAG1E,MAAM,SAAS,IAAI,CAAE,QAAO;;AAGlC,QAAO;;AAGT,SAAS,oBAAoB,OAAwB;CACnD,MAAM,SAAS,MAAM,MAAM,MAAM;AACjC,KAAI,OAAO,SAAS,EAAG,QAAO;AAI9B,QADqB,OAAO,QAAO,MAAK,gBAAgB,EAAE,CAAC,CAAC,WACpC,OAAO;;AAGjC,SAAS,gBAAgB,OAAwB;AAE/C,KAAI,oBAAoB,KAAK,MAAM,CAAE,QAAO;AAE5C,KAAI,+PAA+P,KAAK,MAAM,CAAE,QAAO;AAEvR,KAAI,kBAAkB,KAAK,MAAM,CAAE,QAAO;AAE1C,KAAI,oGAAoG,KAAK,MAAM,CAAE,QAAO;AAC5H,QAAO;;;;;;;AAUT,SAAS,qBACP,OACA,cACA,WACA,WACS;AAGT,QAAO,aAAa,OAAO,IAAI,WAAW,UAAU;;AAKtD,eAAsB,eACpB,aACA,SAC+B;CAC/B,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,YAAY,SAAS,cAAc;CACzC,MAAM,YAAY,SAAS,cAAc;CAGzC,MAAM,QAAQ,MAAM,cAAc,aAAa;EAC7C,OAFe,SAAS,SAAS,MAAM,qBAAqB,YAAY;EAGxE,SAAS,SAAS;EAClB,SAAS,SAAS;EACnB,CAAC;CAEF,MAAM,gBAAiC,EAAE;CACzC,MAAM,0BAAU,IAAI,KAAoD;CACxE,IAAI,kBAAkB;CAEtB,MAAM,eAAe,MAAM,IAAI,OAAO,YAAY;EAChD,MAAM,WAAW,KAAK,aAAa,QAAQ;EAC3C,MAAM,UAAU,MAAM,SAAS,SAAS;AACxC,MAAI,CAAC,QAAS,QAAO;GAAE;GAAS,aAAa,EAAE;GAAE;AAIjD,SAAO;GAAE;GAAS,aADE,MAAM,eAAe,UAAU,SADvC,QAAQ,SAAS,CACmC;GACjC;GAC/B;CAEF,MAAM,cAAc,MAAM,QAAQ,IAAI,aAAa;AAEnD,MAAK,MAAM,EAAE,SAAS,iBAAiB,aAAa;AAClD,qBAAmB,YAAY;AAE/B,OAAK,MAAM,cAAc,aAAa;AAEpC,OAAI,qBAAqB,WAAW,OAAO,WAAW,aAAa,WAAW,UAAU,CAAE;GAE1F,MAAM,gBAAgB,YAAY,WAAW;GAE7C,MAAM,YAA2B;IAC/B,MAAM;IACN,MAAM,WAAW;IACjB,QAAQ,WAAW;IACnB,OAAO,WAAW;IAClB,SAAS;IACT,aAAa,WAAW;IACzB;AACD,iBAAc,KAAK,UAAU;GAG7B,MAAM,MAAM,WAAW;AACvB,OAAI,CAAC,QAAQ,IAAI,IAAI,CACnB,SAAQ,IAAI,KAAK,EAAE,CAAC;AAEtB,WAAQ,IAAI,IAAI,CAAE,KAAK;IAAE,MAAM;IAAS,MAAM,WAAW;IAAM,CAAC;;;CAKpE,MAAM,aAA+B,CAAC,GAAG,QAAQ,SAAS,CAAC,CACxD,QAAQ,GAAG,iBAAiB,YAAY,UAAU,EAAE,CACpD,KAAK,CAAC,OAAO,kBAAkB;EAAE;EAAO,OAAO,YAAY;EAAQ;EAAa,EAAE,CAClF,UAAU,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;CAGxC,MAAM,iBAAiB,cAAc;CACrC,MAAM,YAAY,cAAc,MAAM,QAAQ,SAAS,MAAM;CAC7D,MAAM,UAAU,iBAAiB,SAAS;AAE1C,QAAO;EACL,YAAY;EACZ;EACA,OAAO;GACL,eAAe,MAAM;GACrB,mBAAmB;GACnB,iBAAiB;GACjB,qBAAqB,UAAU;GAC/B,UAAU;GACX;EACF;;AAKH,eAAsB,YACpB,aACA,SAC4B;CAC5B,MAAM,YAAY,SAAS,cAAc;CACzC,MAAM,YAAY,SAAS,cAAc;CAGzC,MAAM,QAAQ,MAAM,cAAc,aAAa;EAC7C,OAFe,SAAS,SAAS,MAAM,qBAAqB,YAAY;EAGxE,SAAS,SAAS;EAClB,SAAS,SAAS;EACnB,CAAC;CAGF,MAAM,2BAAW,IAAI,KAAuB;CAC5C,MAAM,YAAoC,EAAE;AAE5C,MAAK,MAAM,WAAW,OAAO;EAE3B,MAAM,MADQ,QAAQ,MAAM,IAAI,CACd,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,IAAI;EAC5C,MAAM,MAAM,QAAQ,QAAQ;AAE5B,MAAI,CAAC,SAAS,IAAI,IAAI,CAAE,UAAS,IAAI,KAAK,EAAE,CAAC;AAC7C,WAAS,IAAI,IAAI,CAAE,KAAK,QAAQ;AAEhC,YAAU,QAAQ,UAAU,QAAQ,KAAK;;CAI3C,MAAM,cAAqE,EAAE;CAC7E,MAAM,0BAAU,IAAI,KAAqB;CACzC,IAAI,0BAA0B;AAE9B,MAAK,MAAM,CAAC,KAAK,gBAAgB,UAAU;EACzC,MAAM,aAAa,YAAY;EAC/B,MAAM,cAAc,YAAY,MAAM,GAAG,oBAAoB;EAC7D,IAAI,mBAAmB;EAEvB,MAAM,iBAAiB,YAAY,IAAI,OAAO,YAAY;GACxD,MAAM,WAAW,KAAK,aAAa,QAAQ;GAC3C,MAAM,UAAU,MAAM,SAAS,SAAS;AACxC,OAAI,CAAC,QAAS,QAAO,EAAE;AAGvB,UAAO,eAAe,UAAU,SADpB,QAAQ,SAAS,CACgB;IAC7C;EAEF,MAAM,gBAAgB,MAAM,QAAQ,IAAI,eAAe;AAEvD,OAAK,MAAM,eAAe,cACxB,MAAK,MAAM,cAAc,aAAa;AACpC,OAAI,qBAAqB,WAAW,OAAO,WAAW,aAAa,WAAW,UAAU,CAAE;AAC1F;GAGA,MAAM,OAAO,QAAQ,IAAI,WAAW,MAAM,IAAI;AAC9C,WAAQ,IAAI,WAAW,OAAO,OAAO,EAAE;;EAK3C,MAAM,aAAa,YAAY,SAAS,IAAI,mBAAmB,YAAY,SAAS;EACpF,MAAM,sBAAsB,KAAK,MAAM,aAAa,WAAW;AAE/D,cAAY,OAAO;GACjB,OAAO;GACP,YAAY;GACb;AAED,6BAA2B;;CAI7B,MAAM,cAAc,CAAC,GAAG,QAAQ,SAAS,CAAC,CACvC,QAAQ,GAAG,WAAW,SAAS,EAAE,CACjC,UAAU,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG,CAC/B,MAAM,GAAG,GAAG,CACZ,KAAK,CAAC,OAAO,YAAY;EAAE;EAAO;EAAO,EAAE;AAE9C,QAAO;EACL,aAAa,MAAM;EACnB,2BAA2B;EAC3B,cAAc;EACd,cAAc;EACd,eAAe,kBAAkB,oBAAoB;EACrD,YAAY;EACb"}
1
+ {"version":3,"file":"scanner-DPrbU-oK.mjs","names":["FILE_EXT_RE"],"sources":["../src/core/ast-scanner/pre-filter.ts","../src/core/ast-scanner/index.ts","../src/core/scanner.ts"],"sourcesContent":["import type { ExtractedString, PreFilterRule } from './types.js'\n\n// ─── Pre-filter Result ───\n\nexport interface PreFilterResult {\n /** Strings that passed the pre-filter (candidates for agent) */\n candidates: ExtractedString[]\n /** Total number of strings removed by the pre-filter */\n filtered: number\n /** Breakdown: reason string -> count of strings filtered for that reason */\n filterReasons: Record<string, number>\n}\n\n// ─── Value-based patterns (deterministic, no intelligence) ───\n\nconst PURE_NUMBER_RE = /^\\d+(\\.\\d+)?$/\nconst HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i\nconst 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\n\n// ─── HTML / component prop technical values ───\n\nconst HTML_PROP_VALUES = new Set([\n // CSS / variant keywords\n 'class', 'variant', 'secondary', 'outline', 'ghost', 'destructive', 'default', 'primary',\n // Link / target\n '_blank', 'noopener', 'noreferrer',\n // Input / button types\n 'button', 'submit', 'reset', 'text', 'numeric', 'password', 'email', 'checkbox', 'radio',\n // Layout / display\n 'hidden', 'none', 'auto', 'inherit', 'initial',\n // Size tokens\n 'sm', 'md', 'lg', 'xl', 'xs', '2xl', '3xl',\n // Icon variants\n 'icon', 'icon-sm', 'icon-lg',\n])\n\n// ─── Slot / event technical names (single lowercase word) ───\n\nconst SLOT_EVENT_NAMES = new Set([\n 'header', 'footer', 'default', 'trigger', 'content',\n 'sidebar', 'overlay', 'body', 'actions',\n 'click', 'change', 'input', 'focus', 'blur', 'submit',\n 'mounted', 'unmounted', 'updated',\n])\n\n// ─── Tailwind / CSS utility detection ───\n\nconst 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-)/\n\n/**\n * Returns true if the string looks like a CSS class list (Tailwind or similar).\n * Requires 2+ space-separated segments with majority matching utility patterns.\n */\nfunction isCssClassList(value: string): boolean {\n const segments = value.trim().split(/\\s+/)\n if (segments.length < 2) return false\n let matched = 0\n for (const seg of segments) {\n if (TAILWIND_SEGMENT_RE.test(seg)) matched++\n }\n return matched / segments.length >= 0.5\n}\n\n/**\n * Returns true if the string is a single CSS utility token (e.g. \"bg-blue-500\").\n */\nfunction isSingleCssUtility(value: string): boolean {\n const trimmed = value.trim()\n // Must be a single token, no spaces\n if (trimmed.includes(' ')) return false\n return TAILWIND_SEGMENT_RE.test(trimmed)\n}\n\n// ─── Pre-filter Rules ───\n\nconst PRE_FILTER_RULES: PreFilterRule[] = [\n // Context-based rules (AST-determined, 100% accurate)\n { context: 'import_path', reason: 'import_path' },\n { context: 'type_annotation', reason: 'type_annotation' },\n { context: 'css_class', reason: 'css_class' },\n { context: 'css_utility_call', reason: 'css_utility_call' },\n { context: 'console_call', reason: 'console_call' },\n { context: 'test_assertion', reason: 'test_assertion' },\n { context: 'switch_case', reason: 'switch_case' },\n\n // Value-based rules (structural, not heuristic)\n { condition: (v) => v.length <= 1, reason: 'single_char' },\n { condition: (v) => PURE_NUMBER_RE.test(v), reason: 'pure_number' },\n { condition: (v) => v.startsWith('--'), reason: 'cli_flag' },\n { condition: (v) => HEX_COLOR_RE.test(v), reason: 'hex_color' },\n { condition: (v) => FILE_EXT_RE.test(v), reason: 'file_extension' },\n\n // HTML / component prop technical values (exact match, case-insensitive)\n { condition: (v) => HTML_PROP_VALUES.has(v.toLowerCase()), reason: 'html_prop_value' },\n\n // CSS class lists (Tailwind-style multi-segment strings)\n { condition: (v) => isCssClassList(v), reason: 'css_class_list' },\n\n // Single CSS utility token (e.g. \"bg-blue-500\", \"rounded-lg\")\n { condition: (v) => isSingleCssUtility(v), reason: 'css_utility_token' },\n\n // Slot / event technical names (single lowercase word only)\n { condition: (v) => {\n const lower = v.toLowerCase()\n return v === lower && !v.includes(' ') && SLOT_EVENT_NAMES.has(lower)\n }, reason: 'slot_event_name' },\n]\n\n// ─── Public API ───\n\n/**\n * Structural pre-filter: removes strings that are 100% NOT content.\n *\n * Conservative — when in doubt, INCLUDE the string.\n * Returns candidates (passed), count of filtered, and reason breakdown.\n */\nexport function applyPreFilter(strings: ExtractedString[]): PreFilterResult {\n const candidates: ExtractedString[] = []\n const filterReasons: Record<string, number> = {}\n let filtered = 0\n\n for (const str of strings) {\n const matchedRule = findMatchingRule(str)\n\n if (matchedRule) {\n filtered++\n filterReasons[matchedRule.reason] = (filterReasons[matchedRule.reason] ?? 0) + 1\n } else {\n candidates.push(str)\n }\n }\n\n return { candidates, filtered, filterReasons }\n}\n\n/**\n * Find the first pre-filter rule that matches this string.\n * Returns the rule if matched, undefined if the string should pass through.\n */\nfunction findMatchingRule(str: ExtractedString): PreFilterRule | undefined {\n for (const rule of PRE_FILTER_RULES) {\n if (rule.context !== undefined && str.context === rule.context) {\n return rule\n }\n if (rule.condition !== undefined && rule.condition(str.value)) {\n return rule\n }\n }\n return undefined\n}\n","import type { ExtractedString } from './types.js'\nimport { applyPreFilter } from './pre-filter.js'\nimport { parseTsx } from './tsx-parser.js'\n\n// ─── Extension sets ───\n\nconst TSX_EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs'])\nconst VUE_EXTENSIONS = new Set(['.vue'])\nconst SVELTE_EXTENSIONS = new Set(['.svelte'])\nconst ASTRO_EXTENSIONS = new Set(['.astro'])\n\n// ─── Lazy-loaded parsers ───\n\n/**\n * Lazily import vue-parser. Returns undefined if not available\n * (@vue/compiler-sfc is an optional dependency).\n */\nasync function loadVueParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]> | ExtractedString[]) | undefined> {\n try {\n const mod = await import('./vue-parser.js')\n return mod.parseVue\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import svelte-parser. Returns undefined if not available\n * (svelte is an optional dependency).\n */\nasync function loadSvelteParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./svelte-parser.js')\n return mod.parseSvelte\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import astro-parser. Returns undefined if not available\n * (@astrojs/compiler is an optional dependency).\n */\nasync function loadAstroParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./astro-parser.js')\n return mod.parseAstro\n } catch {\n return undefined\n }\n}\n\n// ─── Regex fallback for unknown extensions ───\n\nconst SURROUNDING_MAX = 120\n\n/**\n * Minimal regex-based extractor for file types without AST parsers.\n * Extracts quoted strings and tag text — conservative, low accuracy.\n * Used as fallback when dedicated parsers are unavailable.\n */\nfunction extractWithRegex(content: string, _filePath: string): ExtractedString[] {\n const lines = content.split('\\n')\n const results: ExtractedString[] = []\n\n // Simple quoted string extraction\n const stringRe = /(['\"`])(?:(?!\\1|\\\\).|\\\\.)*?\\1/g\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n const trimmed = line.trim()\n\n // Skip comments, imports\n if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) continue\n if (/^\\s*(import|export)\\s/.test(line)) continue\n\n let match: RegExpExecArray | null\n stringRe.lastIndex = 0\n\n while ((match = stringRe.exec(line)) !== null) {\n // Remove surrounding quotes\n const raw = match[0]\n const value = raw.slice(1, -1)\n if (value.length === 0) continue\n\n const surrounding = buildSurrounding(lines, i)\n\n results.push({\n value,\n line: i + 1,\n column: match.index + 1,\n context: 'other',\n scope: 'script',\n parent: '',\n surrounding,\n })\n }\n\n // Tag text extraction: >text<\n const tagTextRe = />([^<>{]+)</g\n let tagMatch: RegExpExecArray | null\n tagTextRe.lastIndex = 0\n\n while ((tagMatch = tagTextRe.exec(line)) !== null) {\n const text = tagMatch[1]!.trim()\n if (text.length === 0) continue\n if (/^[\\s\\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue\n\n results.push({\n value: text,\n line: i + 1,\n column: tagMatch.index + 1,\n context: 'template_text',\n scope: 'template',\n parent: '',\n surrounding: buildSurrounding(lines, i),\n })\n }\n }\n\n return results\n}\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\n// ─── Public API ───\n\n/**\n * Extract strings from a source file with structural context.\n *\n * Routes to the correct parser based on file extension:\n * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based)\n * - .vue -> vue-parser (lazy-loaded, AST-based)\n * - .svelte -> svelte-parser (lazy-loaded, AST-based)\n * - .astro -> astro-parser (lazy-loaded, AST-based)\n * - unknown -> empty array\n *\n * Falls back to regex extraction when the dedicated parser's\n * optional dependency is not installed.\n *\n * Applies structural pre-filter to remove 100% non-content strings.\n * Returns only candidates that should be sent to the agent.\n */\nexport async function extractStrings(\n filePath: string,\n content: string,\n ext: string,\n): Promise<ExtractedString[]> {\n const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`\n let rawStrings: ExtractedString[]\n\n if (TSX_EXTENSIONS.has(normalizedExt)) {\n rawStrings = parseTsx(content, filePath)\n } else if (VUE_EXTENSIONS.has(normalizedExt)) {\n const parseVue = await loadVueParser()\n if (parseVue) {\n rawStrings = await parseVue(content, filePath)\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (SVELTE_EXTENSIONS.has(normalizedExt)) {\n const parseSvelte = await loadSvelteParser()\n if (parseSvelte) {\n try {\n rawStrings = await parseSvelte(content, filePath)\n } catch {\n // svelte/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (ASTRO_EXTENSIONS.has(normalizedExt)) {\n const parseAstro = await loadAstroParser()\n if (parseAstro) {\n try {\n rawStrings = await parseAstro(content, filePath)\n } catch {\n // @astrojs/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n return []\n }\n\n // Apply structural pre-filter\n const { candidates } = applyPreFilter(rawStrings)\n return candidates\n}\n\n// Re-export types for convenience\nexport type { ExtractedString, StructuralContext, PreFilterRule } from './types.js'\nexport { applyPreFilter } from './pre-filter.js'\nexport type { PreFilterResult } from './pre-filter.js'\n","import type {\n StringContext,\n ScanCandidate,\n DuplicateGroup,\n ScanCandidatesResult,\n ScanSummaryResult,\n} from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText } from '../util/fs.js'\nimport {\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\nimport { extractStrings } from './ast-scanner/index.js'\nimport type { StructuralContext } from './ast-scanner/types.js'\n\n// ─── Options ───\n\nexport interface ScanOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n limit?: number\n offset?: number\n min_length?: number\n max_length?: number\n}\n\n// ─── Constants ───\n\nconst DEFAULT_LIMIT = 50\nconst DEFAULT_OFFSET = 0\nconst DEFAULT_MIN_LENGTH = 2\nconst DEFAULT_MAX_LENGTH = 500\nconst SUMMARY_SAMPLE_SIZE = 10\n\n// ─── Context mapping: StructuralContext → StringContext ───\n\nconst CONTEXT_MAP: Record<StructuralContext, StringContext> = {\n 'template_text': 'template_text',\n 'template_attribute': 'template_attribute',\n 'jsx_text': 'jsx_text',\n 'jsx_attribute': 'jsx_attribute',\n 'variable_assignment': 'variable_assignment',\n 'object_property': 'object_value',\n 'function_argument': 'function_argument',\n 'array_element': 'other',\n 'enum_value': 'other',\n 'template_literal': 'other',\n 'switch_case': 'other',\n 'other': 'other',\n // Pre-filtered contexts should not reach here, but map them just in case\n 'import_path': 'other',\n 'type_annotation': 'other',\n 'css_class': 'other',\n 'css_utility_call': 'other',\n 'console_call': 'other',\n 'test_assertion': 'other',\n}\n\n// ─── Legacy Pre-filter (kept exported for backward compatibility) ───\n\nconst IMPORT_PATH_RE = /^(\\.\\/|\\.\\.\\/|@\\/|@[a-z]|[a-z][a-z0-9-]*\\/)/\nconst 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:)/\nconst URL_ROUTE_RE = /^(https?:\\/\\/|\\/api\\/|\\/v[0-9]|#\\/|mailto:|tel:)/\nconst ROUTE_LIKE_RE = /^\\/[a-z0-9/:_-]*$/i\nconst COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\\(|hsla?\\(|transparent|inherit|currentColor)$/i\nconst CAMEL_CASE_RE = /^[a-z][a-zA-Z0-9]*$/\nconst SCREAMING_SNAKE_RE = /^[A-Z][A-Z0-9_]+$/\nconst KEBAB_TECH_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/\nconst 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\nconst REGEX_LIKE_RE = /^\\^|^\\(.*\\)$|\\$$|\\\\d|\\\\w|\\\\s|\\.\\*|\\.\\+/\nconst CONSOLE_RE = /console\\.(log|warn|error|debug|info|trace)\\s*\\(/\n\n// Common content words that should NOT be filtered when they appear in kebab-case\nconst CONTENT_WORDS = new Set([\n 'sign', 'up', 'log', 'in', 'out', 'get', 'started', 'learn', 'more',\n 'read', 'click', 'here', 'go', 'back', 'next', 'prev', 'previous',\n 'see', 'all', 'view', 'show', 'hide', 'open', 'close', 'add', 'new',\n 'save', 'edit', 'delete', 'remove', 'submit', 'cancel', 'confirm',\n 'welcome', 'hello', 'thank', 'you', 'good', 'bye', 'hi', 'hey',\n 'no', 'yes', 'ok', 'sorry', 'please', 'help', 'about', 'contact',\n 'home', 'page', 'not', 'found', 'error', 'success', 'loading',\n])\n\n/**\n * Determines if a string is non-content (technical) and should be filtered out.\n * Pure function — no side effects, no semantic decisions.\n * Conservative: if in doubt, returns false (keeps the string).\n *\n * @deprecated Kept for backward compatibility. Scanner v2 uses structural pre-filter instead.\n */\nexport function isNonContent(value: string, lineContext: string, minLength: number = DEFAULT_MIN_LENGTH, maxLength: number = DEFAULT_MAX_LENGTH): boolean {\n // Length checks\n if (value.length < minLength) return true\n if (value.length > maxLength) return true\n\n // Import/require paths (no spaces, looks like module path)\n if (IMPORT_PATH_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — single class prefix\n if (CSS_CLASS_PREFIX_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — multiple space-separated tokens that all look like utility classes\n if (value.includes(' ') && looksLikeCssClasses(value)) return true\n\n // URL/route patterns\n if (URL_ROUTE_RE.test(value)) return true\n if (value.startsWith('/') && ROUTE_LIKE_RE.test(value)) return true\n\n // Color codes\n if (COLOR_RE.test(value)) return true\n\n // Technical identifiers (includes kebab-case with content-word awareness)\n if (isTechnicalIdentifier(value)) return true\n\n // File paths with extensions\n if (FILE_EXT_RE.test(value)) return true\n\n // RegExp-like patterns\n if (REGEX_LIKE_RE.test(value)) return true\n\n // Console/log context\n if (CONSOLE_RE.test(lineContext)) return true\n\n return false\n}\n\nfunction isTechnicalIdentifier(value: string): boolean {\n // No spaces allowed in identifiers\n if (value.includes(' ')) return false\n\n // camelCase: starts lowercase, has at least one uppercase letter, no spaces\n if (CAMEL_CASE_RE.test(value) && /[A-Z]/.test(value)) return true\n\n // SCREAMING_SNAKE_CASE — only filter if it contains underscores (true SCREAMING_SNAKE like API_KEY, MAX_COUNT).\n // Short uppercase words without underscores (OK, FAQ, GPS) are likely real UI labels.\n if (SCREAMING_SNAKE_RE.test(value) && value.includes('_')) return true\n\n // kebab-case: filter only if NO content words are present\n if (KEBAB_TECH_RE.test(value)) {\n const parts = value.split('-')\n const hasContentWord = parts.some(p => CONTENT_WORDS.has(p))\n if (!hasContentWord) return true\n }\n\n // Bare npm-like package names: all lowercase, no dashes with content words,\n // no spaces, < 40 chars. Single lowercase words are kept (could be content).\n if (/^[a-z][a-z0-9-]*$/.test(value) && value.length < 40 && !value.includes(' ')) {\n // Only filter if it has a slash (scoped package) or looks like a known package pattern\n // Single words or content-word kebab phrases pass through\n if (value.includes('/')) return true\n }\n\n return false\n}\n\nfunction looksLikeCssClasses(value: string): boolean {\n const tokens = value.split(/\\s+/)\n if (tokens.length < 2) return false\n\n // If ALL tokens look like CSS utilities, filter\n const cssLikeCount = tokens.filter(t => isCssClassToken(t)).length\n return cssLikeCount === tokens.length\n}\n\nfunction isCssClassToken(token: string): boolean {\n // Tailwind-like: prefix- or modifier: patterns\n if (CSS_CLASS_PREFIX_RE.test(token)) return true\n // Common standalone classes\n 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\n // Arbitrary Tailwind with brackets: w-[100px]\n if (/^[a-z]+-\\[.+\\]$/.test(token)) return true\n // Responsive/state prefixes: sm:text-lg, hover:bg-blue-500\n if (/^(sm|md|lg|xl|2xl|hover|focus|active|disabled|first|last|odd|even|dark|group-hover|peer-checked):/.test(token)) return true\n return false\n}\n\n// ─── Secondary filter for v2 candidates ───\n\n/**\n * Apply legacy isNonContent filter as a secondary pass on v2 candidates.\n * This catches heuristic patterns (URLs, regex, camelCase identifiers, etc.)\n * that the structural pre-filter does not cover.\n */\nfunction applySecondaryFilter(\n value: string,\n _surrounding: string,\n minLength: number,\n maxLength: number,\n): boolean {\n // Pass empty lineContext — v2 pre-filter handles context-based detection (console, test, etc.)\n // Using surrounding would cause false positives because it spans +-1 lines\n return isNonContent(value, '', minLength, maxLength)\n}\n\n// ─── Main: scanCandidates ───\n\nexport async function scanCandidates(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanCandidatesResult> {\n const limit = options?.limit ?? DEFAULT_LIMIT\n const offset = options?.offset ?? DEFAULT_OFFSET\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n const allCandidates: ScanCandidate[] = []\n const dupeMap = new Map<string, Array<{ file: string; line: number }>>()\n let rawStringsFound = 0\n\n const filePromises = files.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return { relPath, extractions: [] }\n\n const ext = extname(filePath)\n const extractions = await extractStrings(filePath, content, ext)\n return { relPath, extractions }\n })\n\n const fileResults = await Promise.all(filePromises)\n\n for (const { relPath, extractions } of fileResults) {\n rawStringsFound += extractions.length\n\n for (const extraction of extractions) {\n // Apply secondary (legacy) filter for heuristic patterns\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n\n const mappedContext = CONTEXT_MAP[extraction.context]\n\n const candidate: ScanCandidate = {\n file: relPath,\n line: extraction.line,\n column: extraction.column,\n value: extraction.value,\n context: mappedContext,\n surrounding: extraction.surrounding,\n }\n allCandidates.push(candidate)\n\n // Track duplicates\n const key = extraction.value\n if (!dupeMap.has(key)) {\n dupeMap.set(key, [])\n }\n dupeMap.get(key)!.push({ file: relPath, line: extraction.line })\n }\n }\n\n // Build duplicate groups (only count >= 2), sorted by count descending\n const duplicates: DuplicateGroup[] = [...dupeMap.entries()]\n .filter(([, occurrences]) => occurrences.length >= 2)\n .map(([value, occurrences]) => ({ value, count: occurrences.length, occurrences }))\n .toSorted((a, b) => b.count - a.count)\n\n // Pagination\n const afterFiltering = allCandidates.length\n const paginated = allCandidates.slice(offset, offset + limit)\n const hasMore = afterFiltering > offset + limit\n\n return {\n candidates: paginated,\n duplicates,\n stats: {\n files_scanned: files.length,\n raw_strings_found: rawStringsFound,\n after_filtering: afterFiltering,\n candidates_returned: paginated.length,\n has_more: hasMore,\n },\n }\n}\n\n// ─── Main: scanSummary ───\n\nexport async function scanSummary(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanSummaryResult> {\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // Group files by directory\n const dirFiles = new Map<string, string[]>()\n const fileTypes: Record<string, number> = {}\n\n for (const relPath of files) {\n const parts = relPath.split('/')\n const dir = parts.slice(0, -1).join('/') || '.'\n const ext = extname(relPath)\n\n if (!dirFiles.has(dir)) dirFiles.set(dir, [])\n dirFiles.get(dir)!.push(relPath)\n\n fileTypes[ext] = (fileTypes[ext] ?? 0) + 1\n }\n\n // Sample files per directory and count candidates\n const byDirectory: Record<string, { files: number; candidates: number }> = {}\n const freqMap = new Map<string, number>()\n let totalCandidatesEstimate = 0\n\n for (const [dir, dirFileList] of dirFiles) {\n const totalInDir = dirFileList.length\n const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE)\n let sampleCandidates = 0\n\n const samplePromises = sampleFiles.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return []\n\n const ext = extname(filePath)\n return extractStrings(filePath, content, ext)\n })\n\n const sampleResults = await Promise.all(samplePromises)\n\n for (const extractions of sampleResults) {\n for (const extraction of extractions) {\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n sampleCandidates++\n\n // Track frequencies for top_repeated\n const prev = freqMap.get(extraction.value) ?? 0\n freqMap.set(extraction.value, prev + 1)\n }\n }\n\n // Estimate for full directory\n const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0\n const estimatedCandidates = Math.round(avgPerFile * totalInDir)\n\n byDirectory[dir] = {\n files: totalInDir,\n candidates: estimatedCandidates,\n }\n\n totalCandidatesEstimate += estimatedCandidates\n }\n\n // Top repeated strings\n const topRepeated = [...freqMap.entries()]\n .filter(([, count]) => count >= 2)\n .toSorted((a, b) => b[1] - a[1])\n .slice(0, 20)\n .map(([value, count]) => ({ value, count }))\n\n return {\n total_files: files.length,\n total_candidates_estimate: totalCandidatesEstimate,\n by_directory: byDirectory,\n top_repeated: topRepeated,\n sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`,\n file_types: fileTypes,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { canonicalStringify, sortKeys } from "@contentrain/types";
2
+ export { sortKeys as n, canonicalStringify as t };
@@ -1,20 +1,21 @@
1
- import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-BbuGnVcG.mjs";
2
- import { n as readVocabulary, t as readConfig } from "./config-CPlhDwsv.mjs";
3
- import { n as readMeta, r as writeMeta } from "./meta-manager-CECrfsKm.mjs";
4
- import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, i as deleteModel, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-kl78AAMg.mjs";
5
- import { t as readContext } from "./context-CUFulSi2.mjs";
6
- import { t as detectStack } from "./detect-C8XmJ4S6.mjs";
7
- import { n as cleanupMergedBranches, t as checkBranchHealth } from "./branch-lifecycle-DB6sgeJi.mjs";
8
- import { n as listTemplates, t as getTemplate } from "./templates-3HWi-PTu.mjs";
9
- import { n as createTransaction, t as buildBranchName } from "./transaction-_AiXqGuD.mjs";
10
- import { t as validateProject } from "./validator-CyckZAVr.mjs";
11
- import { t as buildGraph } from "./graph-builder-fPrDDwnu.mjs";
12
- import { n as scanCandidates, r as scanSummary } from "./scanner-BdTiv8sw.mjs";
13
- import { n as applyExtract, r as applyReuse } from "./apply-manager-BDtHM8z-.mjs";
1
+ import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-BnIQ4dKj.mjs";
2
+ import { n as readVocabulary, t as readConfig } from "./config-B5kc2oeV.mjs";
3
+ import { n as readMeta, r as writeMeta } from "./meta-manager--0IwAPCl.mjs";
4
+ import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, i as deleteModel, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-DW_fOGRM.mjs";
5
+ import { t as readContext } from "./context-DIDt_TxX.mjs";
6
+ import { t as detectStack } from "./detect-DinOQAmK.mjs";
7
+ import { n as cleanupMergedBranches, t as checkBranchHealth } from "./branch-lifecycle-wGJg25LT.mjs";
8
+ import { n as listTemplates, t as getTemplate } from "./templates-bRPzYq4M.mjs";
9
+ import { n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-B0V0_SE2.mjs";
10
+ import { t as validateProject } from "./validator-SQYUV2JP.mjs";
11
+ import { t as buildGraph } from "./graph-builder-tM7zj-SX.mjs";
12
+ import { n as scanCandidates, r as scanSummary } from "./scanner-DPrbU-oK.mjs";
13
+ import { n as applyExtract, r as applyReuse } from "./apply-manager-Cf5OWOJ7.mjs";
14
14
  import { join } from "node:path";
15
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
16
  import { z } from "zod";
17
17
  import { appendFile, readFile, writeFile } from "node:fs/promises";
18
+ import { CONTENTRAIN_BRANCH } from "@contentrain/types";
18
19
  import { simpleGit } from "simple-git";
19
20
  //#region src/tools/context.ts
20
21
  function registerContextTools(server, projectRoot) {
@@ -403,12 +404,13 @@ function registerSetupTools(server, projectRoot) {
403
404
  });
404
405
  await updateGitignore(wt);
405
406
  });
406
- await tx.commit("[contentrain] init: project setup");
407
- const gitResult = await tx.complete({
407
+ await tx.commit("[contentrain] init: project setup", {
408
408
  tool: "contentrain_init",
409
409
  model: "",
410
410
  locale: supportedLocales[0] ?? "en"
411
411
  });
412
+ const gitResult = await tx.complete();
413
+ await ensureContentBranch(projectRoot);
412
414
  return { content: [{
413
415
  type: "text",
414
416
  text: JSON.stringify({
@@ -432,7 +434,8 @@ function registerSetupTools(server, projectRoot) {
432
434
  git: {
433
435
  branch,
434
436
  action: gitResult.action,
435
- commit: gitResult.commit
437
+ commit: gitResult.commit,
438
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
436
439
  },
437
440
  next_steps: ["Create models with contentrain_model_save or contentrain_scaffold", "Use contentrain_scan to find hardcoded strings"]
438
441
  }, null, 2)
@@ -503,12 +506,12 @@ function registerSetupTools(server, projectRoot) {
503
506
  if (with_sample_content && tmpl.sample_content) contentCreated = await writeSampleContent(wt, tmpl, effectiveLocales, config);
504
507
  if (tmpl.vocabulary) vocabAdded = await mergeVocabulary(wt, tmpl.vocabulary, effectiveLocales);
505
508
  });
506
- await tx.commit(`[contentrain] scaffold: ${templateId} (${defaultLocale})`);
507
- const gitResult = await tx.complete({
509
+ await tx.commit(`[contentrain] scaffold: ${templateId} (${defaultLocale})`, {
508
510
  tool: "contentrain_scaffold",
509
511
  model: tmpl.models.map((m) => m.id).join(", "),
510
512
  locale: defaultLocale
511
513
  });
514
+ const gitResult = await tx.complete();
512
515
  return { content: [{
513
516
  type: "text",
514
517
  text: JSON.stringify({
@@ -520,7 +523,8 @@ function registerSetupTools(server, projectRoot) {
520
523
  git: {
521
524
  branch,
522
525
  action: gitResult.action,
523
- commit: gitResult.commit
526
+ commit: gitResult.commit,
527
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
524
528
  },
525
529
  context_updated: true,
526
530
  next_steps: [
@@ -689,11 +693,11 @@ function registerModelTools(server, projectRoot) {
689
693
  await tx.write(async (wt) => {
690
694
  await writeModel(wt, model);
691
695
  });
692
- await tx.commit(`[contentrain] ${action}: ${input.id}`);
693
- const gitResult = await tx.complete({
696
+ await tx.commit(`[contentrain] ${action}: ${input.id}`, {
694
697
  tool: "contentrain_model_save",
695
698
  model: input.id
696
699
  });
700
+ const gitResult = await tx.complete();
697
701
  const defaultLocale = config.locales.default;
698
702
  const contentDir = resolveContentDir(projectRoot, model);
699
703
  const contentPath = model.content_path ?? `.contentrain/content/${input.domain}/${input.id}`;
@@ -717,7 +721,8 @@ function registerModelTools(server, projectRoot) {
717
721
  git: {
718
722
  branch,
719
723
  action: gitResult.action,
720
- commit: gitResult.commit
724
+ commit: gitResult.commit,
725
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
721
726
  },
722
727
  context_updated: true,
723
728
  content_path: contentPath + "/",
@@ -786,11 +791,11 @@ function registerModelTools(server, projectRoot) {
786
791
  await tx.write(async (wt) => {
787
792
  filesRemoved = await deleteModel(wt, modelId);
788
793
  });
789
- await tx.commit(`[contentrain] delete: ${modelId}`);
790
- const gitResult = await tx.complete({
794
+ await tx.commit(`[contentrain] delete: ${modelId}`, {
791
795
  tool: "contentrain_model_delete",
792
796
  model: modelId
793
797
  });
798
+ const gitResult = await tx.complete();
794
799
  return { content: [{
795
800
  type: "text",
796
801
  text: JSON.stringify({
@@ -800,7 +805,8 @@ function registerModelTools(server, projectRoot) {
800
805
  git: {
801
806
  branch,
802
807
  action: gitResult.action,
803
- commit: gitResult.commit
808
+ commit: gitResult.commit,
809
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
804
810
  },
805
811
  files_removed: filesRemoved,
806
812
  context_updated: true
@@ -912,13 +918,13 @@ function registerContentTools(server, projectRoot) {
912
918
  results = await writeContent(wt, model, input.entries, config);
913
919
  entryIds = results.map((r) => r.id ?? r.slug ?? r.locale).filter(Boolean);
914
920
  });
915
- await tx.commit(`[contentrain] content: ${input.model}`);
916
- const gitResult = await tx.complete({
921
+ await tx.commit(`[contentrain] content: ${input.model}`, {
917
922
  tool: "contentrain_content_save",
918
923
  model: input.model,
919
924
  locale: input.entries[0]?.locale,
920
925
  entries: entryIds
921
926
  });
927
+ const gitResult = await tx.complete();
922
928
  const validationResult = await validateProject(projectRoot, { model: input.model });
923
929
  return { content: [{
924
930
  type: "text",
@@ -929,7 +935,8 @@ function registerContentTools(server, projectRoot) {
929
935
  git: {
930
936
  branch,
931
937
  action: gitResult.action,
932
- commit: gitResult.commit
938
+ commit: gitResult.commit,
939
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
933
940
  },
934
941
  validation: {
935
942
  valid: validationResult.valid,
@@ -999,12 +1006,12 @@ function registerContentTools(server, projectRoot) {
999
1006
  keys: input.keys
1000
1007
  });
1001
1008
  });
1002
- await tx.commit(`[contentrain] delete content: ${input.model}`);
1003
- const gitResult = await tx.complete({
1009
+ await tx.commit(`[contentrain] delete content: ${input.model}`, {
1004
1010
  tool: "contentrain_content_delete",
1005
1011
  model: input.model,
1006
1012
  locale: input.locale
1007
1013
  });
1014
+ const gitResult = await tx.complete();
1008
1015
  return { content: [{
1009
1016
  type: "text",
1010
1017
  text: JSON.stringify({
@@ -1015,7 +1022,8 @@ function registerContentTools(server, projectRoot) {
1015
1022
  git: {
1016
1023
  branch,
1017
1024
  action: gitResult.action,
1018
- commit: gitResult.commit
1025
+ commit: gitResult.commit,
1026
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
1019
1027
  },
1020
1028
  context_updated: true
1021
1029
  }, null, 2)
@@ -1119,11 +1127,11 @@ function registerWorkflowTools(server, projectRoot) {
1119
1127
  });
1120
1128
  });
1121
1129
  if (result.fixed > 0) {
1122
- await tx.commit(`[contentrain] validate: auto-fix ${result.fixed} issue(s)`);
1123
- const gitResult = await tx.complete({
1130
+ await tx.commit(`[contentrain] validate: auto-fix ${result.fixed} issue(s)`, {
1124
1131
  tool: "contentrain_validate",
1125
1132
  model: input.model ?? "*"
1126
1133
  });
1134
+ const gitResult = await tx.complete();
1127
1135
  const nextSteps = [];
1128
1136
  if (result.summary.errors > 0) nextSteps.push("Fix remaining errors manually");
1129
1137
  if (result.summary.warnings > 0) nextSteps.push("Review warnings");
@@ -1137,7 +1145,8 @@ function registerWorkflowTools(server, projectRoot) {
1137
1145
  git: {
1138
1146
  branch,
1139
1147
  action: gitResult.action,
1140
- commit: gitResult.commit
1148
+ commit: gitResult.commit,
1149
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
1141
1150
  },
1142
1151
  context_updated: true,
1143
1152
  next_steps: nextSteps
@@ -1234,12 +1243,12 @@ function registerWorkflowTools(server, projectRoot) {
1234
1243
  isError: true
1235
1244
  };
1236
1245
  branchesToPush = branchesToPush.filter((b) => unmergedSet.has(b));
1237
- } else branchesToPush = branchSummary.all.filter((b) => b.startsWith("contentrain/") && unmergedSet.has(b));
1246
+ } else branchesToPush = branchSummary.all.filter((b) => b.startsWith("cr/") && unmergedSet.has(b));
1238
1247
  if (branchesToPush.length === 0) return {
1239
1248
  content: [{
1240
1249
  type: "text",
1241
1250
  text: JSON.stringify({
1242
- error: "No unmerged contentrain/* branches found to push.",
1251
+ error: "No unmerged cr/* branches found to push.",
1243
1252
  next_steps: ["Make changes first with contentrain_content_save or contentrain_model_save"]
1244
1253
  })
1245
1254
  }],
@@ -1256,6 +1265,9 @@ function registerWorkflowTools(server, projectRoot) {
1256
1265
  error: error instanceof Error ? error.message : String(error)
1257
1266
  });
1258
1267
  }
1268
+ try {
1269
+ await git.push(remoteName, CONTENTRAIN_BRANCH);
1270
+ } catch {}
1259
1271
  const cleanup = await cleanupMergedBranches(projectRoot);
1260
1272
  const nextSteps = [];
1261
1273
  if (pushed.length > 0) nextSteps.push("Create PRs on your git platform for review");
@@ -1649,12 +1661,12 @@ function registerBulkTools(server, projectRoot) {
1649
1661
  copiedCount = 1;
1650
1662
  }
1651
1663
  });
1652
- await tx.commit(`[contentrain] bulk: copy ${input.source_locale} → ${input.target_locale} for ${input.model}`);
1653
- const gitResult = await tx.complete({
1664
+ await tx.commit(`[contentrain] bulk: copy ${input.source_locale} → ${input.target_locale} for ${input.model}`, {
1654
1665
  tool: "contentrain_bulk",
1655
1666
  model: input.model,
1656
1667
  locale: input.target_locale
1657
1668
  });
1669
+ const gitResult = await tx.complete();
1658
1670
  return { content: [{
1659
1671
  type: "text",
1660
1672
  text: JSON.stringify({
@@ -1665,7 +1677,8 @@ function registerBulkTools(server, projectRoot) {
1665
1677
  git: {
1666
1678
  branch,
1667
1679
  action: gitResult.action,
1668
- commit: gitResult.commit
1680
+ commit: gitResult.commit,
1681
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
1669
1682
  },
1670
1683
  context_updated: true
1671
1684
  }, null, 2)
@@ -1731,12 +1744,12 @@ function registerBulkTools(server, projectRoot) {
1731
1744
  await Promise.all(updateOps);
1732
1745
  }
1733
1746
  });
1734
- await tx.commit(`[contentrain] bulk: update status → ${input.status} for ${input.model}`);
1735
- const gitResult = await tx.complete({
1747
+ await tx.commit(`[contentrain] bulk: update status → ${input.status} for ${input.model}`, {
1736
1748
  tool: "contentrain_bulk",
1737
1749
  model: input.model,
1738
1750
  entries: input.entry_ids
1739
1751
  });
1752
+ const gitResult = await tx.complete();
1740
1753
  return { content: [{
1741
1754
  type: "text",
1742
1755
  text: JSON.stringify({
@@ -1748,7 +1761,8 @@ function registerBulkTools(server, projectRoot) {
1748
1761
  git: {
1749
1762
  branch,
1750
1763
  action: gitResult.action,
1751
- commit: gitResult.commit
1764
+ commit: gitResult.commit,
1765
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
1752
1766
  },
1753
1767
  context_updated: true
1754
1768
  }, null, 2)
@@ -1798,12 +1812,12 @@ function registerBulkTools(server, projectRoot) {
1798
1812
  allRemoved.push(...removed);
1799
1813
  }
1800
1814
  });
1801
- await tx.commit(`[contentrain] bulk: delete ${input.entry_ids.length} entries from ${input.model}`);
1802
- const gitResult = await tx.complete({
1815
+ await tx.commit(`[contentrain] bulk: delete ${input.entry_ids.length} entries from ${input.model}`, {
1803
1816
  tool: "contentrain_bulk",
1804
1817
  model: input.model,
1805
1818
  entries: input.entry_ids
1806
1819
  });
1820
+ const gitResult = await tx.complete();
1807
1821
  return { content: [{
1808
1822
  type: "text",
1809
1823
  text: JSON.stringify({
@@ -1815,7 +1829,8 @@ function registerBulkTools(server, projectRoot) {
1815
1829
  git: {
1816
1830
  branch,
1817
1831
  action: gitResult.action,
1818
- commit: gitResult.commit
1832
+ commit: gitResult.commit,
1833
+ ...gitResult.sync ? { sync: gitResult.sync } : {}
1819
1834
  },
1820
1835
  context_updated: true
1821
1836
  }, null, 2)
@@ -1845,7 +1860,7 @@ function registerBulkTools(server, projectRoot) {
1845
1860
  }
1846
1861
  //#endregion
1847
1862
  //#region package.json
1848
- var version = "1.0.7";
1863
+ var version = "1.1.1";
1849
1864
  //#endregion
1850
1865
  //#region src/server.ts
1851
1866
  function createServer(projectRoot) {
@@ -1865,4 +1880,4 @@ function createServer(projectRoot) {
1865
1880
  //#endregion
1866
1881
  export { createServer as t };
1867
1882
 
1868
- //# sourceMappingURL=server-QgivXkem.mjs.map
1883
+ //# sourceMappingURL=server-BXguCuJb.mjs.map