speclore 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res, err) => function __init() {
4
- if (err) throw err[0];
5
- try {
6
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
- } catch (e) {
8
- throw err = [e], e;
9
- }
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
5
  };
11
6
  var __export = (target, all) => {
12
7
  for (var name in all)
@@ -323,7 +318,7 @@ function detectAITools(projectRoot2) {
323
318
  detected: existsSync2(join2(projectRoot2, ".cursor")),
324
319
  configFiles: cursorConfigs
325
320
  });
326
- const claudeFiles = [".claude/", ".mcp.json", "CLAUDE.md"];
321
+ const claudeFiles = [".claude/", "CLAUDE.md"];
327
322
  const claudeConfigs = claudeFiles.filter((f) => existsSync2(join2(projectRoot2, f)));
328
323
  tools.push({
329
324
  tool: "claude",
@@ -759,7 +754,7 @@ var init_graph_builder = __esm({
759
754
  });
760
755
 
761
756
  // src/core/context-engine/context-writer.ts
762
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, statSync as statSync2, mkdirSync as mkdirSync4 } from "fs";
757
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, statSync as statSync2, mkdirSync as mkdirSync5 } from "fs";
763
758
  import { join as join10 } from "path";
764
759
  import { execFileSync } from "child_process";
765
760
  function buildContext(projectRoot2, config) {
@@ -830,7 +825,7 @@ function loadContext(specLoreDir) {
830
825
  }
831
826
  function writeContextFile(specLoreDir, context) {
832
827
  if (!existsSync7(specLoreDir)) {
833
- mkdirSync4(specLoreDir, { recursive: true });
828
+ mkdirSync5(specLoreDir, { recursive: true });
834
829
  }
835
830
  const contextPath = join10(specLoreDir, CONTEXT_FILENAME);
836
831
  writeFileSync5(contextPath, JSON.stringify(context, null, 2), "utf-8");
@@ -1021,7 +1016,7 @@ var init_cost_tracker = __esm({
1021
1016
  });
1022
1017
 
1023
1018
  // src/core/state-manager/index.ts
1024
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
1019
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
1025
1020
  import { join as join12 } from "path";
1026
1021
  import yaml2 from "js-yaml";
1027
1022
  import { globSync as globSync2 } from "glob";
@@ -1070,7 +1065,7 @@ var init_state_manager = __esm({
1070
1065
  save(state) {
1071
1066
  const dir = join12(this.projectRoot, ".speclore");
1072
1067
  if (!existsSync8(dir)) {
1073
- mkdirSync5(dir, { recursive: true });
1068
+ mkdirSync6(dir, { recursive: true });
1074
1069
  }
1075
1070
  writeFileSync6(this.statePath, yaml2.dump(state, { lineWidth: 120 }), "utf-8");
1076
1071
  }
@@ -1427,7 +1422,43 @@ var init_excel_utils = __esm({
1427
1422
  });
1428
1423
 
1429
1424
  // src/plugins/builtin/xlsx-reader.ts
1430
- import { readFile, utils } from "xlsx";
1425
+ import { readFile, read, utils } from "xlsx";
1426
+ function parseWorkbook(workbook) {
1427
+ const requirements = [];
1428
+ for (const sheetName of workbook.SheetNames) {
1429
+ const sheet = workbook.Sheets[sheetName];
1430
+ if (!sheet) continue;
1431
+ const allRows = utils.sheet_to_json(sheet, { header: 1, defval: "" });
1432
+ if (allRows.length === 0) continue;
1433
+ const headers = allRows[0].map((cell) => formatCellValue(cell));
1434
+ const dataRows = allRows.slice(1);
1435
+ const rows = dataRows.map((row) => {
1436
+ const obj = {};
1437
+ row.forEach((cell, colNumber) => {
1438
+ const key = headers[colNumber] ?? `Col${colNumber + 1}`;
1439
+ obj[key] = formatCellValue(cell);
1440
+ });
1441
+ return obj;
1442
+ });
1443
+ for (let i = 0; i < rows.length; i++) {
1444
+ const row = rows[i];
1445
+ const title = row["Title"] ?? row["title"] ?? row["Name"] ?? row["name"] ?? row["ID"] ?? `Row ${i + 1}`;
1446
+ const description = row["Description"] ?? row["description"] ?? row["Desc"] ?? "";
1447
+ const ac = row["Acceptance Criteria"] ?? row["acceptance"] ?? row["AC"] ?? "";
1448
+ if (description || title) {
1449
+ requirements.push({
1450
+ id: `${sheetName}/${i + 1}`,
1451
+ title: String(title),
1452
+ description: String(description),
1453
+ acceptanceCriteria: ac ? String(ac).split("\n").filter(Boolean) : void 0,
1454
+ rawContent: JSON.stringify(row),
1455
+ confidence: 0.7
1456
+ });
1457
+ }
1458
+ }
1459
+ }
1460
+ return requirements;
1461
+ }
1431
1462
  var XlsxReader;
1432
1463
  var init_xlsx_reader = __esm({
1433
1464
  "src/plugins/builtin/xlsx-reader.ts"() {
@@ -1441,40 +1472,7 @@ var init_xlsx_reader = __esm({
1441
1472
  }
1442
1473
  read(source) {
1443
1474
  const workbook = readFile(source);
1444
- const requirements = [];
1445
- for (const sheetName of workbook.SheetNames) {
1446
- const sheet = workbook.Sheets[sheetName];
1447
- if (!sheet) continue;
1448
- const allRows = utils.sheet_to_json(sheet, { header: 1, defval: "" });
1449
- if (allRows.length === 0) continue;
1450
- const headers = allRows[0].map((cell) => formatCellValue(cell));
1451
- const dataRows = allRows.slice(1);
1452
- const rows = dataRows.map((row) => {
1453
- const obj = {};
1454
- row.forEach((cell, colNumber) => {
1455
- const key = headers[colNumber] ?? `Col${colNumber + 1}`;
1456
- obj[key] = formatCellValue(cell);
1457
- });
1458
- return obj;
1459
- });
1460
- for (let i = 0; i < rows.length; i++) {
1461
- const row = rows[i];
1462
- const title = row["Title"] ?? row["title"] ?? row["Name"] ?? row["name"] ?? row["ID"] ?? `Row ${i + 1}`;
1463
- const description = row["Description"] ?? row["description"] ?? row["Desc"] ?? "";
1464
- const ac = row["Acceptance Criteria"] ?? row["acceptance"] ?? row["AC"] ?? "";
1465
- if (description || title) {
1466
- requirements.push({
1467
- id: `${sheetName}/${i + 1}`,
1468
- title: String(title),
1469
- description: String(description),
1470
- acceptanceCriteria: ac ? String(ac).split("\n").filter(Boolean) : void 0,
1471
- rawContent: JSON.stringify(row),
1472
- confidence: 0.7
1473
- });
1474
- }
1475
- }
1476
- }
1477
- return Promise.resolve(requirements);
1475
+ return Promise.resolve(parseWorkbook(workbook));
1478
1476
  }
1479
1477
  };
1480
1478
  }
@@ -1492,21 +1490,33 @@ var init_pdf_reader = __esm({
1492
1490
  return /\.pdf$/i.test(source);
1493
1491
  }
1494
1492
  async read(source) {
1495
- let pdfParse;
1493
+ let pdfjsLib;
1496
1494
  try {
1497
- const mod = await import("pdf-parse");
1498
- pdfParse = mod.default ?? mod;
1495
+ pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
1499
1496
  } catch {
1500
- throw new Error("pdf-parse package is required for PDF support. Install: npm i pdf-parse");
1497
+ throw new Error("pdfjs-dist package is required for PDF support. Install: npm i pdfjs-dist");
1498
+ }
1499
+ const { readFileSync: readFileSync24 } = await import("fs");
1500
+ const buffer = readFileSync24(source);
1501
+ const uint8 = new Uint8Array(buffer);
1502
+ const doc = await pdfjsLib.getDocument({
1503
+ data: uint8,
1504
+ useWorkerFetch: false
1505
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1506
+ }).promise;
1507
+ const textParts = [];
1508
+ for (let i = 1; i <= doc.numPages; i++) {
1509
+ const page = await doc.getPage(i);
1510
+ const textContent = await page.getTextContent();
1511
+ const pageText = textContent.items.map((item) => item.str).join("");
1512
+ if (pageText) textParts.push(pageText);
1501
1513
  }
1502
- const { readFileSync: readFileSync22 } = await import("fs");
1503
- const buffer = readFileSync22(source);
1504
- const data = await pdfParse(buffer);
1514
+ const text = textParts.join("\n");
1505
1515
  return [{
1506
1516
  id: source.replace(/\\/g, "/").replace(/\.pdf$/i, ""),
1507
1517
  title: source.split("/").pop()?.replace(/\.pdf$/i, "") ?? "Untitled",
1508
- description: data.text,
1509
- rawContent: data.text,
1518
+ description: text,
1519
+ rawContent: text,
1510
1520
  confidence: 0.75
1511
1521
  }];
1512
1522
  }
@@ -2102,13 +2112,13 @@ var init_image_reader = __esm({
2102
2112
  return /\.(png|jpe?g|webp)$/i.test(source);
2103
2113
  }
2104
2114
  async read(source) {
2105
- const { existsSync: existsSync30, readFileSync: readFileSync22 } = await import("fs");
2106
- if (!existsSync30(source)) {
2115
+ const { existsSync: existsSync31, readFileSync: readFileSync24 } = await import("fs");
2116
+ if (!existsSync31(source)) {
2107
2117
  throw new Error(`Image file not found: ${source}`);
2108
2118
  }
2109
2119
  const ext = source.split(".").pop()?.toLowerCase() ?? "png";
2110
2120
  const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
2111
- const buffer = readFileSync22(source);
2121
+ const buffer = readFileSync24(source);
2112
2122
  let text;
2113
2123
  try {
2114
2124
  const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
@@ -2137,7 +2147,7 @@ var init_image_reader = __esm({
2137
2147
  });
2138
2148
 
2139
2149
  // src/plugins/builtin/cursor-writer.ts
2140
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync11, rmSync } from "fs";
2150
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync11, rmSync } from "fs";
2141
2151
  import { join as join14 } from "path";
2142
2152
  var CursorWriter;
2143
2153
  var init_cursor_writer = __esm({
@@ -2153,7 +2163,7 @@ var init_cursor_writer = __esm({
2153
2163
  write(constraints) {
2154
2164
  this.projectRoot = constraints.projectRoot;
2155
2165
  const rulesDir = join14(constraints.projectRoot, ".cursor", "rules");
2156
- mkdirSync6(rulesDir, { recursive: true });
2166
+ mkdirSync7(rulesDir, { recursive: true });
2157
2167
  const frontmatter = [
2158
2168
  "---",
2159
2169
  "description: SpecLore coding constraints \u2014 auto-generated",
@@ -2232,7 +2242,7 @@ var init_cursor_writer = __esm({
2232
2242
  });
2233
2243
 
2234
2244
  // src/plugins/builtin/claude-writer.ts
2235
- import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, existsSync as existsSync12, rmSync as rmSync2 } from "fs";
2245
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync12, rmSync as rmSync2 } from "fs";
2236
2246
  import { join as join15 } from "path";
2237
2247
  var ClaudeWriter;
2238
2248
  var init_claude_writer = __esm({
@@ -2248,7 +2258,7 @@ var init_claude_writer = __esm({
2248
2258
  write(constraints) {
2249
2259
  this.projectRoot = constraints.projectRoot;
2250
2260
  const rulesDir = join15(constraints.projectRoot, ".claude", "rules");
2251
- mkdirSync7(rulesDir, { recursive: true });
2261
+ mkdirSync8(rulesDir, { recursive: true });
2252
2262
  const content = this.buildMarkdown(constraints);
2253
2263
  writeFileSync8(join15(rulesDir, "speclore.md"), content, "utf-8");
2254
2264
  return Promise.resolve();
@@ -2314,7 +2324,7 @@ var init_claude_writer = __esm({
2314
2324
  });
2315
2325
 
2316
2326
  // src/plugins/builtin/qoder-writer.ts
2317
- import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync8, existsSync as existsSync13, rmSync as rmSync3 } from "fs";
2327
+ import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync13, rmSync as rmSync3 } from "fs";
2318
2328
  import { join as join16 } from "path";
2319
2329
  var QoderWriter;
2320
2330
  var init_qoder_writer = __esm({
@@ -2330,7 +2340,7 @@ var init_qoder_writer = __esm({
2330
2340
  write(constraints) {
2331
2341
  this.projectRoot = constraints.projectRoot;
2332
2342
  const rulesDir = join16(constraints.projectRoot, ".qoder", "rules");
2333
- mkdirSync8(rulesDir, { recursive: true });
2343
+ mkdirSync9(rulesDir, { recursive: true });
2334
2344
  const content = this.buildMarkdown(constraints);
2335
2345
  writeFileSync9(join16(rulesDir, "speclore.md"), content, "utf-8");
2336
2346
  return Promise.resolve();
@@ -2763,11 +2773,12 @@ var init_markdown_reader = __esm({
2763
2773
 
2764
2774
  // src/core/requirement-reader/docx-reader.ts
2765
2775
  import { basename as basename3, extname as extname3 } from "path";
2776
+ import { readFileSync as readFileSync10 } from "fs";
2766
2777
  import mammoth from "mammoth";
2767
- async function readDocxFile(filePath) {
2768
- const result = await mammoth.extractRawText({ path: filePath });
2778
+ async function parseDocxBuffer(buffer, idHint = "document") {
2779
+ const result = await mammoth.extractRawText({ buffer });
2769
2780
  const content = result.value;
2770
- const id = basename3(filePath, extname3(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2781
+ const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2771
2782
  const lines = content.split("\n").filter((l) => l.trim());
2772
2783
  const title = lines[0]?.trim() ?? id;
2773
2784
  return {
@@ -2778,6 +2789,11 @@ async function readDocxFile(filePath) {
2778
2789
  confidence: 0.9
2779
2790
  };
2780
2791
  }
2792
+ async function readDocxFile(filePath) {
2793
+ const buffer = readFileSync10(filePath);
2794
+ const name = basename3(filePath, extname3(filePath));
2795
+ return parseDocxBuffer(buffer, name);
2796
+ }
2781
2797
  var init_docx_reader2 = __esm({
2782
2798
  "src/core/requirement-reader/docx-reader.ts"() {
2783
2799
  "use strict";
@@ -2786,17 +2802,16 @@ var init_docx_reader2 = __esm({
2786
2802
 
2787
2803
  // src/core/requirement-reader/xlsx-reader.ts
2788
2804
  import { basename as basename4, extname as extname4 } from "path";
2789
- import { readFile as readFile2, utils as utils2 } from "xlsx";
2790
- function readXlsxFile(filePath) {
2791
- const workbook = readFile2(filePath);
2792
- const id = basename4(filePath, extname4(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2805
+ import { readFile as readFile2, read as read2, utils as utils2 } from "xlsx";
2806
+ function parseWorkbook2(workbook, idHint) {
2807
+ const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2793
2808
  const firstSheetName = workbook.SheetNames[0];
2794
2809
  if (!firstSheetName) {
2795
- return Promise.reject(new Error(`No sheets found in ${filePath}`));
2810
+ return Promise.reject(new Error("No sheets found in workbook"));
2796
2811
  }
2797
2812
  const sheet = workbook.Sheets[firstSheetName];
2798
2813
  if (!sheet) {
2799
- return Promise.reject(new Error(`Sheet "${firstSheetName}" not found in ${filePath}`));
2814
+ return Promise.reject(new Error(`Sheet "${firstSheetName}" not found`));
2800
2815
  }
2801
2816
  const title = firstSheetName;
2802
2817
  const rows = utils2.sheet_to_json(sheet, { header: 1, defval: "" });
@@ -2832,6 +2847,11 @@ function readXlsxFile(filePath) {
2832
2847
  confidence: 0.85
2833
2848
  });
2834
2849
  }
2850
+ function readXlsxFile(filePath) {
2851
+ const workbook = readFile2(filePath);
2852
+ const idHint = basename4(filePath, extname4(filePath));
2853
+ return parseWorkbook2(workbook, idHint);
2854
+ }
2835
2855
  var init_xlsx_reader2 = __esm({
2836
2856
  "src/core/requirement-reader/xlsx-reader.ts"() {
2837
2857
  "use strict";
@@ -2842,21 +2862,40 @@ var init_xlsx_reader2 = __esm({
2842
2862
  // src/core/requirement-reader/pdf-reader.ts
2843
2863
  import { basename as basename5, extname as extname5 } from "path";
2844
2864
  import { readFile as readFile3 } from "fs/promises";
2845
- async function readPdfFile(filePath) {
2846
- const pdfParse = (await import("pdf-parse")).default;
2847
- const dataBuffer = await readFile3(filePath);
2848
- const data = await pdfParse(dataBuffer);
2849
- const id = basename5(filePath, extname5(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2850
- const lines = data.text.split("\n").filter((l) => l.trim());
2865
+ async function extractPdfText(dataBuffer) {
2866
+ const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
2867
+ const uint8 = new Uint8Array(dataBuffer);
2868
+ const doc = await pdfjsLib.getDocument({
2869
+ data: uint8,
2870
+ useWorkerFetch: false
2871
+ }).promise;
2872
+ const textParts = [];
2873
+ for (let i = 1; i <= doc.numPages; i++) {
2874
+ const page = await doc.getPage(i);
2875
+ const textContent = await page.getTextContent();
2876
+ const pageText = textContent.items.map((item) => item.str).join("");
2877
+ if (pageText) textParts.push(pageText);
2878
+ }
2879
+ return textParts.join("\n");
2880
+ }
2881
+ async function parsePdfBuffer(dataBuffer, idHint = "document") {
2882
+ const text = await extractPdfText(dataBuffer);
2883
+ const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2884
+ const lines = text.split("\n").filter((l) => l.trim());
2851
2885
  const title = lines[0]?.trim() ?? id;
2852
2886
  return {
2853
2887
  id,
2854
2888
  title,
2855
- description: data.text,
2856
- rawContent: data.text,
2889
+ description: text,
2890
+ rawContent: text,
2857
2891
  confidence: 0.8
2858
2892
  };
2859
2893
  }
2894
+ async function readPdfFile(filePath) {
2895
+ const dataBuffer = await readFile3(filePath);
2896
+ const name = basename5(filePath, extname5(filePath));
2897
+ return parsePdfBuffer(dataBuffer, name);
2898
+ }
2860
2899
  var init_pdf_reader2 = __esm({
2861
2900
  "src/core/requirement-reader/pdf-reader.ts"() {
2862
2901
  "use strict";
@@ -2865,14 +2904,14 @@ var init_pdf_reader2 = __esm({
2865
2904
 
2866
2905
  // src/core/requirement-reader/image-reader.ts
2867
2906
  import { basename as basename6, extname as extname6 } from "path";
2868
- import { readFileSync as readFileSync10 } from "fs";
2869
- async function readImageFile(filePath) {
2907
+ import { readFileSync as readFileSync11 } from "fs";
2908
+ async function readImageFile(filePath, providerOverride) {
2870
2909
  const id = basename6(filePath, extname6(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2871
2910
  logger.info(`Reading image via AI Vision: ${filePath}`);
2872
2911
  const ext = extname6(filePath).toLowerCase();
2873
2912
  const mimeType = MIME_MAP[ext] ?? "image/png";
2874
- const buffer = readFileSync10(filePath);
2875
- const provider = await createProvider();
2913
+ const buffer = readFileSync11(filePath);
2914
+ const provider = providerOverride ?? await createProvider();
2876
2915
  const prompt = `Please extract all text content from this image. Return the text as-is, preserving structure and formatting. If the image contains a table, convert it to a structured text format.`;
2877
2916
  if (!provider.generateWithImage) {
2878
2917
  throw new Error(`AI provider '${provider.name}' does not support image/vision input. Use a vision-capable model.`);
@@ -3317,11 +3356,11 @@ var init_prompt_builder = __esm({
3317
3356
  });
3318
3357
 
3319
3358
  // src/core/feature-generator/generator.ts
3320
- import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync9, existsSync as existsSync15 } from "fs";
3359
+ import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync15 } from "fs";
3321
3360
  import { join as join17, dirname as dirname2 } from "path";
3322
3361
  import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
3323
3362
  import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
3324
- async function generateFeature(requirement, context, config, projectRoot2) {
3363
+ async function generateFeature(requirement, context, config, projectRoot2, providerOverride) {
3325
3364
  logger.info(`Generating feature for: ${requirement.title}`);
3326
3365
  const registry = getRegistry();
3327
3366
  await registry.invokeLifecycle("beforeSpec", requirement);
@@ -3329,7 +3368,7 @@ async function generateFeature(requirement, context, config, projectRoot2) {
3329
3368
  const aiConfig = config.ai;
3330
3369
  const fallbackConfigs = aiConfig?.fallbackProviders ?? [];
3331
3370
  const providerConfigs = aiConfig ? [aiConfig, ...fallbackConfigs] : [];
3332
- const provider = providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig);
3371
+ const provider = providerOverride ?? (providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig));
3333
3372
  if (!provider.isAvailable()) {
3334
3373
  throw new Error("AI provider not available. Set API key in environment or config.yaml.");
3335
3374
  }
@@ -3386,7 +3425,7 @@ Please fix these issues and regenerate the complete Feature file.`;
3386
3425
  const moduleDir = inferModule(requirement, context);
3387
3426
  const filePath = join17(outputDir, moduleDir, `${requirement.id}.feature`);
3388
3427
  if (!existsSync15(dirname2(filePath))) {
3389
- mkdirSync9(dirname2(filePath), { recursive: true });
3428
+ mkdirSync10(dirname2(filePath), { recursive: true });
3390
3429
  }
3391
3430
  writeFileSync10(filePath, featureContent, "utf-8");
3392
3431
  logger.info(`Feature written: ${toPosixPath(filePath)}`);
@@ -3552,9 +3591,9 @@ function detectAITools2(projectRoot2) {
3552
3591
  tools.push("cursor");
3553
3592
  logger.debug("Detected Cursor (.cursor/)");
3554
3593
  }
3555
- if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md")) || existsSync16(join19(projectRoot2, ".mcp.json"))) {
3594
+ if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))) {
3556
3595
  tools.push("claude");
3557
- logger.debug("Detected Claude Code (.claude/ or CLAUDE.md or .mcp.json)");
3596
+ logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
3558
3597
  }
3559
3598
  if (existsSync16(join19(projectRoot2, ".qoder"))) {
3560
3599
  tools.push("qoder");
@@ -3571,7 +3610,7 @@ var init_ai_tool_detector = __esm({
3571
3610
  });
3572
3611
 
3573
3612
  // src/core/constraint-coder/constraint-writer.ts
3574
- import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync10, existsSync as existsSync17 } from "fs";
3613
+ import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync17 } from "fs";
3575
3614
  import { join as join20 } from "path";
3576
3615
  function writeConstraints(projectRoot2, tools, content) {
3577
3616
  const writtenFiles = [];
@@ -3645,7 +3684,7 @@ function buildConstraintText(content) {
3645
3684
  }
3646
3685
  function writeCursorRule2(projectRoot2, text) {
3647
3686
  const dir = join20(projectRoot2, ".cursor", "rules");
3648
- if (!existsSync17(dir)) mkdirSync10(dir, { recursive: true });
3687
+ if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
3649
3688
  const filePath = join20(dir, "speclore.mdc");
3650
3689
  const frontmatter = [
3651
3690
  "---",
@@ -3660,14 +3699,14 @@ function writeCursorRule2(projectRoot2, text) {
3660
3699
  }
3661
3700
  function writeClaudeRule2(projectRoot2, text) {
3662
3701
  const dir = join20(projectRoot2, ".claude", "rules");
3663
- if (!existsSync17(dir)) mkdirSync10(dir, { recursive: true });
3702
+ if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
3664
3703
  const filePath = join20(dir, "speclore.md");
3665
3704
  writeFileSync11(filePath, text, "utf-8");
3666
3705
  return filePath;
3667
3706
  }
3668
3707
  function writeQoderRule2(projectRoot2, text) {
3669
3708
  const dir = join20(projectRoot2, ".qoder", "rules");
3670
- if (!existsSync17(dir)) mkdirSync10(dir, { recursive: true });
3709
+ if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
3671
3710
  const filePath = join20(dir, "speclore.md");
3672
3711
  writeFileSync11(filePath, text, "utf-8");
3673
3712
  return filePath;
@@ -3747,14 +3786,14 @@ var init_constraint_coder = __esm({
3747
3786
  init_logger();
3748
3787
  init_ai_tool_detector();
3749
3788
  init_constraint_writer();
3750
- MAPPING_INSTRUCTIONS = `\u4E3A\u6BCF\u4E2A\u6D4B\u8BD5\u6587\u4EF6\u751F\u6210\u5BF9\u5E94\u7684\u6620\u5C04\u6587\u4EF6\u5230 .speclore/mappings/{module}/{feature-name}.json\u3002
3751
- \u683C\u5F0F\uFF1A{ "feature": "specs/...", "scenarios": { "Scenario\u540D\u79F0": { "testFile": "...", "testMethod": "..." } } }
3752
- \u6BCF\u6B21\u4FEE\u6539\u6D4B\u8BD5\u65F6\u540C\u6B65\u66F4\u65B0\u6620\u5C04\u6587\u4EF6\u3002`;
3789
+ MAPPING_INSTRUCTIONS = `Generate a mapping file for each test file at .speclore/mappings/{module}/{feature-name}.json.
3790
+ Format: { "feature": "specs/...", "scenarios": { "Scenario name": { "testFile": "...", "testMethod": "..." } } }
3791
+ Keep mapping files in sync whenever tests are modified.`;
3753
3792
  }
3754
3793
  });
3755
3794
 
3756
3795
  // src/core/test-scaffolder/framework-detector.ts
3757
- import { readFileSync as readFileSync11, existsSync as existsSync18 } from "fs";
3796
+ import { readFileSync as readFileSync12, existsSync as existsSync18 } from "fs";
3758
3797
  import { join as join21 } from "path";
3759
3798
  function detectTestFramework(projectRoot2) {
3760
3799
  const pkgPath = join21(projectRoot2, "package.json");
@@ -3762,7 +3801,7 @@ function detectTestFramework(projectRoot2) {
3762
3801
  return "vitest";
3763
3802
  }
3764
3803
  try {
3765
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
3804
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3766
3805
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3767
3806
  if ("vitest" in deps) return "vitest";
3768
3807
  if ("jest" in deps || "@jest/core" in deps) return "jest";
@@ -3778,7 +3817,7 @@ var init_framework_detector = __esm({
3778
3817
  });
3779
3818
 
3780
3819
  // src/core/test-scaffolder/index.ts
3781
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
3820
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync12 } from "fs";
3782
3821
  import { join as join22, relative as relative3, dirname as dirname3, basename as basename8 } from "path";
3783
3822
  function generateTestScaffolding(projectRoot2, features, config) {
3784
3823
  const framework = detectTestFramework(projectRoot2);
@@ -3801,7 +3840,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
3801
3840
  const content = generateTestFileContent(feature.featureName, scenarios, framework);
3802
3841
  const dir = dirname3(absTestPath);
3803
3842
  if (!existsSync19(dir)) {
3804
- mkdirSync11(dir, { recursive: true });
3843
+ mkdirSync12(dir, { recursive: true });
3805
3844
  }
3806
3845
  writeFileSync12(absTestPath, content, "utf-8");
3807
3846
  results.push({ testFile: testFilePath, framework, scenarios: scenarios.length });
@@ -3874,7 +3913,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
3874
3913
  return lines.join("\n");
3875
3914
  }
3876
3915
  function appendMissingScenarios(testFilePath, scenarios, _framework) {
3877
- const content = readFileSync12(testFilePath, "utf-8");
3916
+ const content = readFileSync13(testFilePath, "utf-8");
3878
3917
  const missing = [];
3879
3918
  for (const scenario of scenarios) {
3880
3919
  if (!content.includes(`Scenario: ${scenario.name}`)) {
@@ -3915,7 +3954,7 @@ var init_test_scaffolder = __esm({
3915
3954
  });
3916
3955
 
3917
3956
  // src/core/verifier/mapping-resolver.ts
3918
- import { readFileSync as readFileSync14, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
3957
+ import { readFileSync as readFileSync15, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
3919
3958
  import { join as join24 } from "path";
3920
3959
  function resolveMappings(projectRoot2, features, testOutput) {
3921
3960
  const results = [];
@@ -3945,7 +3984,7 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
3945
3984
  const files = findMappingFiles(mappingsDir);
3946
3985
  for (const file of files) {
3947
3986
  try {
3948
- const content = readFileSync14(file, "utf-8");
3987
+ const content = readFileSync15(file, "utf-8");
3949
3988
  const mapping = JSON.parse(content);
3950
3989
  if (mapping.scenarios && scenario.name in mapping.scenarios) {
3951
3990
  const entry = mapping.scenarios[scenario.name];
@@ -3975,7 +4014,7 @@ function resolveFromTag(projectRoot2, _feature, scenario) {
3975
4014
  const files = findTestFiles(testsDir);
3976
4015
  for (const file of files) {
3977
4016
  try {
3978
- const content = readFileSync14(file, "utf-8");
4017
+ const content = readFileSync15(file, "utf-8");
3979
4018
  const tagRegex = /@speclore-scenario:\s*(.+)/g;
3980
4019
  let match;
3981
4020
  while ((match = tagRegex.exec(content)) !== null) {
@@ -4155,13 +4194,13 @@ var report_generator_exports = {};
4155
4194
  __export(report_generator_exports, {
4156
4195
  generateReport: () => generateReport
4157
4196
  });
4158
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync12, existsSync as existsSync22, readFileSync as readFileSync15 } from "fs";
4197
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync22, readFileSync as readFileSync16 } from "fs";
4159
4198
  import { join as join25, dirname as dirname4 } from "path";
4160
4199
  import { fileURLToPath as fileURLToPath2 } from "url";
4161
4200
  function generateReport(report, projectRoot2, config) {
4162
4201
  const reportsDir = join25(projectRoot2, ".speclore", "reports");
4163
4202
  if (!existsSync22(reportsDir)) {
4164
- mkdirSync12(reportsDir, { recursive: true });
4203
+ mkdirSync13(reportsDir, { recursive: true });
4165
4204
  }
4166
4205
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
4167
4206
  const writtenFiles = [];
@@ -4218,7 +4257,7 @@ function renderHtmlReport(report) {
4218
4257
  const templatePath = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "cli", "templates", "report.html");
4219
4258
  let template;
4220
4259
  try {
4221
- template = readFileSync15(templatePath, "utf-8");
4260
+ template = readFileSync16(templatePath, "utf-8");
4222
4261
  } catch {
4223
4262
  template = getInlineTemplate();
4224
4263
  }
@@ -4297,7 +4336,7 @@ var init_verifier = __esm({
4297
4336
  });
4298
4337
 
4299
4338
  // src/core/analyzer/rdg-builder.ts
4300
- import { readFileSync as readFileSync16, existsSync as existsSync23, readdirSync as readdirSync3 } from "fs";
4339
+ import { readFileSync as readFileSync17, existsSync as existsSync23, readdirSync as readdirSync3 } from "fs";
4301
4340
  import { join as join26 } from "path";
4302
4341
  var init_rdg_builder = __esm({
4303
4342
  "src/core/analyzer/rdg-builder.ts"() {
@@ -4327,6 +4366,9 @@ import { execFileSync as execFileSync3 } from "child_process";
4327
4366
  import { globSync as globSync5 } from "glob";
4328
4367
  function analyzeImpact(projectRoot2, context, config) {
4329
4368
  const changedFiles = getChangedFiles(projectRoot2);
4369
+ return analyzeImpactWithChanges(changedFiles, context, config, projectRoot2);
4370
+ }
4371
+ function analyzeImpactWithChanges(changedFiles, context, config, projectRoot2) {
4330
4372
  const affectedModules = determineAffectedModules(changedFiles, context);
4331
4373
  const affectedFeatures = determineAffectedFeatures(affectedModules, projectRoot2, config.spec.outputDir);
4332
4374
  logger.info(
@@ -4402,11 +4444,11 @@ var init_analyzer = __esm({
4402
4444
  });
4403
4445
 
4404
4446
  // src/infra/file-lock.ts
4405
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync15, unlinkSync, existsSync as existsSync27, statSync as statSync3 } from "fs";
4406
- import { join as join30 } from "path";
4447
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync, existsSync as existsSync28, statSync as statSync3 } from "fs";
4448
+ import { join as join31 } from "path";
4407
4449
  function acquireLock(specLoreDir) {
4408
- const lockPath = join30(specLoreDir, LOCK_FILENAME);
4409
- if (existsSync27(lockPath)) {
4450
+ const lockPath = join31(specLoreDir, LOCK_FILENAME);
4451
+ if (existsSync28(lockPath)) {
4410
4452
  const existing = readLockInfo(lockPath);
4411
4453
  if (existing && !isLockExpired(existing)) {
4412
4454
  logger.debug(`Lock already held by PID ${existing.pid}, not expired`);
@@ -4420,7 +4462,7 @@ function acquireLock(specLoreDir) {
4420
4462
  timestamp: Date.now()
4421
4463
  };
4422
4464
  try {
4423
- writeFileSync15(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
4465
+ writeFileSync16(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
4424
4466
  } catch {
4425
4467
  logger.debug("Lock write failed \u2014 another process may have acquired it");
4426
4468
  return false;
@@ -4429,8 +4471,8 @@ function acquireLock(specLoreDir) {
4429
4471
  return true;
4430
4472
  }
4431
4473
  function releaseLock(specLoreDir) {
4432
- const lockPath = join30(specLoreDir, LOCK_FILENAME);
4433
- if (!existsSync27(lockPath)) {
4474
+ const lockPath = join31(specLoreDir, LOCK_FILENAME);
4475
+ if (!existsSync28(lockPath)) {
4434
4476
  return;
4435
4477
  }
4436
4478
  const existing = readLockInfo(lockPath);
@@ -4441,7 +4483,7 @@ function releaseLock(specLoreDir) {
4441
4483
  }
4442
4484
  function readLockInfo(lockPath) {
4443
4485
  try {
4444
- const content = readFileSync19(lockPath, "utf-8");
4486
+ const content = readFileSync21(lockPath, "utf-8");
4445
4487
  const parsed = JSON.parse(content);
4446
4488
  if (typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
4447
4489
  return parsed;
@@ -4511,8 +4553,8 @@ var init_schemas = __esm({
4511
4553
  });
4512
4554
 
4513
4555
  // src/mcp/tools.ts
4514
- import { join as join31 } from "path";
4515
- import { readFileSync as readFileSync20, existsSync as existsSync28, mkdirSync as mkdirSync13, writeFileSync as writeFileSync16 } from "fs";
4556
+ import { join as join32 } from "path";
4557
+ import { readFileSync as readFileSync22, existsSync as existsSync29, mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
4516
4558
  import { globSync as globSync7 } from "glob";
4517
4559
  import { toJSONSchema } from "zod";
4518
4560
  async function executeSpecTool(args, projectRoot2) {
@@ -4523,7 +4565,7 @@ async function executeSpecTool(args, projectRoot2) {
4523
4565
  const config = loadConfig(projectRoot2);
4524
4566
  logger.info(`Reading requirement from: ${args.source.slice(0, 80)}...`);
4525
4567
  const req = await readRequirement(args.source);
4526
- const specLoreDir = join31(projectRoot2, ".speclore");
4568
+ const specLoreDir = join32(projectRoot2, ".speclore");
4527
4569
  const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
4528
4570
  const feature = await generateFeature(req, context, config, projectRoot2);
4529
4571
  const createdFiles = [feature.path];
@@ -4555,7 +4597,7 @@ async function executeCodeTool(args, projectRoot2) {
4555
4597
  }
4556
4598
  ensureProjectReady(projectRoot2);
4557
4599
  const config = loadConfig(projectRoot2);
4558
- const specLoreDir = join31(projectRoot2, ".speclore");
4600
+ const specLoreDir = join32(projectRoot2, ".speclore");
4559
4601
  const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
4560
4602
  const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
4561
4603
  if (featureFiles.length === 0) {
@@ -4608,7 +4650,7 @@ async function executeCodeTool(args, projectRoot2) {
4608
4650
  async function executeVerifyTool(args, projectRoot2) {
4609
4651
  ensureProjectReady(projectRoot2);
4610
4652
  const config = loadConfig(projectRoot2);
4611
- const specLoreDir = join31(projectRoot2, ".speclore");
4653
+ const specLoreDir = join32(projectRoot2, ".speclore");
4612
4654
  const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
4613
4655
  const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
4614
4656
  const stateManager = new StateManager(projectRoot2);
@@ -4670,22 +4712,22 @@ async function executeVerifyTool(args, projectRoot2) {
4670
4712
  };
4671
4713
  }
4672
4714
  function ensureProjectReady(projectRoot2) {
4673
- const specLoreDir = join31(projectRoot2, ".speclore");
4674
- const configPath = join31(specLoreDir, "config.yaml");
4715
+ const specLoreDir = join32(projectRoot2, ".speclore");
4716
+ const configPath = join32(specLoreDir, "config.yaml");
4675
4717
  let configCreated = false;
4676
- if (!existsSync28(specLoreDir)) {
4677
- mkdirSync13(specLoreDir, { recursive: true });
4718
+ if (!existsSync29(specLoreDir)) {
4719
+ mkdirSync14(specLoreDir, { recursive: true });
4678
4720
  }
4679
- if (!existsSync28(configPath)) {
4721
+ if (!existsSync29(configPath)) {
4680
4722
  const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
4681
- writeFileSync16(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4723
+ writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4682
4724
  configCreated = true;
4683
4725
  logger.info(`Auto-created default config: ${configPath}`);
4684
4726
  }
4685
4727
  const stateManager = new StateManager(projectRoot2);
4686
4728
  stateManager.ensureInitialized();
4687
4729
  const config = loadConfig(projectRoot2);
4688
- const specsDir = join31(projectRoot2, config.spec.outputDir);
4730
+ const specsDir = join32(projectRoot2, config.spec.outputDir);
4689
4731
  const migrated = stateManager.migrateFeatures(specsDir);
4690
4732
  if (migrated > 0) {
4691
4733
  logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
@@ -4722,14 +4764,14 @@ function resolveNextStep(currentState, _summary) {
4722
4764
  }
4723
4765
  }
4724
4766
  function resolveFeatureFiles(patterns, projectRoot2, config) {
4725
- const specsDir = join31(projectRoot2, config.spec.outputDir);
4767
+ const specsDir = join32(projectRoot2, config.spec.outputDir);
4726
4768
  const searchPatterns = patterns && patterns.length > 0 ? patterns : [`${specsDir}/**/*.feature`];
4727
4769
  const files = [];
4728
4770
  for (const pattern of searchPatterns) {
4729
4771
  const matches = globSync7(pattern, { cwd: projectRoot2, absolute: true });
4730
4772
  for (const filePath of matches) {
4731
- if (existsSync28(filePath)) {
4732
- const content = readFileSync20(filePath, "utf-8");
4773
+ if (existsSync29(filePath)) {
4774
+ const content = readFileSync22(filePath, "utf-8");
4733
4775
  files.push(parseFeatureFile(filePath, content));
4734
4776
  }
4735
4777
  }
@@ -4837,8 +4879,8 @@ var init_tools = __esm({
4837
4879
  });
4838
4880
 
4839
4881
  // src/mcp/status.ts
4840
- import { readFileSync as readFileSync21, existsSync as existsSync29, mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
4841
- import { join as join32 } from "path";
4882
+ import { readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync15, writeFileSync as writeFileSync18 } from "fs";
4883
+ import { join as join33 } from "path";
4842
4884
  import { globSync as globSync8 } from "glob";
4843
4885
  function executeStatusTool(args, projectRoot2) {
4844
4886
  const { configCreated } = ensureProjectReadyForStatus(projectRoot2);
@@ -4854,9 +4896,9 @@ function executeStatusTool(args, projectRoot2) {
4854
4896
  continue;
4855
4897
  }
4856
4898
  let scenarioCount = 0;
4857
- if (existsSync29(path)) {
4899
+ if (existsSync30(path)) {
4858
4900
  try {
4859
- const content = readFileSync21(path, "utf-8");
4901
+ const content = readFileSync23(path, "utf-8");
4860
4902
  const matches = content.match(/Scenario(?: Outline)?:/g);
4861
4903
  scenarioCount = matches?.length ?? 0;
4862
4904
  } catch {
@@ -4871,7 +4913,7 @@ function executeStatusTool(args, projectRoot2) {
4871
4913
  lastVerify: entry.lastVerify ? { passed: entry.lastVerify.passed, failed: entry.lastVerify.failed, timestamp: entry.lastVerify.timestamp } : void 0
4872
4914
  });
4873
4915
  }
4874
- const specsDir = join32(projectRoot2, config.spec.outputDir);
4916
+ const specsDir = join33(projectRoot2, config.spec.outputDir);
4875
4917
  const allFeatureFiles = globSync8(`${specsDir}/**/*.feature`, { cwd: projectRoot2, absolute: true });
4876
4918
  const trackedPaths = new Set(featureEntries.map((f) => f.path));
4877
4919
  for (const filePath of allFeatureFiles) {
@@ -4879,7 +4921,7 @@ function executeStatusTool(args, projectRoot2) {
4879
4921
  if (args.feature && !filePath.includes(args.feature)) continue;
4880
4922
  let scenarioCount = 0;
4881
4923
  try {
4882
- const content = readFileSync21(filePath, "utf-8");
4924
+ const content = readFileSync23(filePath, "utf-8");
4883
4925
  const matches = content.match(/Scenario(?: Outline)?:/g);
4884
4926
  scenarioCount = matches?.length ?? 0;
4885
4927
  } catch {
@@ -4936,22 +4978,22 @@ function buildRecommendedActions(features, summary, testCommand) {
4936
4978
  return actions;
4937
4979
  }
4938
4980
  function ensureProjectReadyForStatus(projectRoot2) {
4939
- const specLoreDir = join32(projectRoot2, ".speclore");
4940
- const configPath = join32(specLoreDir, "config.yaml");
4981
+ const specLoreDir = join33(projectRoot2, ".speclore");
4982
+ const configPath = join33(specLoreDir, "config.yaml");
4941
4983
  let configCreated = false;
4942
- if (!existsSync29(specLoreDir)) {
4943
- mkdirSync14(specLoreDir, { recursive: true });
4984
+ if (!existsSync30(specLoreDir)) {
4985
+ mkdirSync15(specLoreDir, { recursive: true });
4944
4986
  }
4945
- if (!existsSync29(configPath)) {
4987
+ if (!existsSync30(configPath)) {
4946
4988
  const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
4947
- writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4989
+ writeFileSync18(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4948
4990
  configCreated = true;
4949
4991
  logger.info(`Auto-created default config: ${configPath}`);
4950
4992
  }
4951
4993
  const stateManager = new StateManager(projectRoot2);
4952
4994
  stateManager.ensureInitialized();
4953
4995
  const config = loadConfig(projectRoot2);
4954
- const specsDir = join32(projectRoot2, config.spec.outputDir);
4996
+ const specsDir = join33(projectRoot2, config.spec.outputDir);
4955
4997
  const migrated = stateManager.migrateFeatures(specsDir);
4956
4998
  if (migrated > 0) {
4957
4999
  logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
@@ -4975,7 +5017,7 @@ __export(server_exports, {
4975
5017
  });
4976
5018
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4977
5019
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4978
- import { mkdirSync as mkdirSync15 } from "fs";
5020
+ import { mkdirSync as mkdirSync16 } from "fs";
4979
5021
  function getProjectRoot() {
4980
5022
  return projectRoot;
4981
5023
  }
@@ -4983,7 +5025,7 @@ async function startMcpServer() {
4983
5025
  projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
4984
5026
  const specLoreDir = `${projectRoot}/.speclore`;
4985
5027
  try {
4986
- mkdirSync15(specLoreDir, { recursive: true });
5028
+ mkdirSync16(specLoreDir, { recursive: true });
4987
5029
  const locked = acquireLock(specLoreDir);
4988
5030
  if (!locked) {
4989
5031
  logger.warn("Could not acquire lock \u2014 another SpecLore instance may be running.");
@@ -5096,11 +5138,11 @@ import { Command } from "commander";
5096
5138
  init_logger();
5097
5139
  init_config2();
5098
5140
  init_detector();
5099
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
5141
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
5100
5142
  import { join as join7 } from "path";
5101
5143
 
5102
5144
  // src/setup/config-writer.ts
5103
- import { writeFileSync as writeFileSync2, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
5145
+ import { writeFileSync as writeFileSync2, existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
5104
5146
  import { join as join5 } from "path";
5105
5147
 
5106
5148
  // src/infra/install-detector.ts
@@ -5151,13 +5193,42 @@ function detectCloneInstall(projectRoot2) {
5151
5193
 
5152
5194
  // src/setup/config-writer.ts
5153
5195
  init_logger();
5154
- function writeMcpConfig(projectRoot2, _globalMode) {
5196
+ function writeMcpConfig(projectRoot2, tools, _globalMode) {
5197
+ const installInfo = detectInstallMethod(projectRoot2);
5198
+ const serverCommand = getServerCommand(installInfo);
5199
+ const detectedToolNames = tools.map((t) => t.tool);
5200
+ if (detectedToolNames.includes("cursor")) {
5201
+ writeCursorMcp(projectRoot2, serverCommand);
5202
+ }
5203
+ if (detectedToolNames.includes("claude")) {
5204
+ writeClaudeMcp(projectRoot2, serverCommand);
5205
+ }
5206
+ if (detectedToolNames.includes("qoder")) {
5207
+ writeQoderMcp(projectRoot2, serverCommand);
5208
+ }
5209
+ if (detectedToolNames.length > 0) {
5210
+ logger.info(`MCP configuration written for: ${detectedToolNames.join(", ")}`);
5211
+ }
5212
+ }
5213
+ function writeMcpForClient(projectRoot2, client) {
5155
5214
  const installInfo = detectInstallMethod(projectRoot2);
5156
5215
  const serverCommand = getServerCommand(installInfo);
5157
- writeCursorMcp(projectRoot2, serverCommand);
5158
- writeClaudeMcp(projectRoot2, serverCommand);
5159
- writeQoderMcp(projectRoot2, serverCommand);
5160
- logger.info("MCP configuration written for all detected AI clients.");
5216
+ switch (client) {
5217
+ case "cursor":
5218
+ mkdirSync2(join5(projectRoot2, ".cursor"), { recursive: true });
5219
+ writeCursorMcp(projectRoot2, serverCommand);
5220
+ break;
5221
+ case "claude":
5222
+ if (!existsSync4(join5(projectRoot2, ".claude"))) {
5223
+ mkdirSync2(join5(projectRoot2, ".claude"), { recursive: true });
5224
+ }
5225
+ writeClaudeMcp(projectRoot2, serverCommand);
5226
+ break;
5227
+ case "qoder":
5228
+ mkdirSync2(join5(projectRoot2, ".qoder"), { recursive: true });
5229
+ writeQoderMcp(projectRoot2, serverCommand);
5230
+ break;
5231
+ }
5161
5232
  }
5162
5233
  function getServerCommand(installInfo) {
5163
5234
  if (installInfo.mode === "npm") {
@@ -5181,6 +5252,9 @@ function writeCursorMcp(projectRoot2, serverCmd) {
5181
5252
  logger.info(` Cursor: ${mcpPath}`);
5182
5253
  }
5183
5254
  function writeClaudeMcp(projectRoot2, serverCmd) {
5255
+ const hasClaudeDir = existsSync4(join5(projectRoot2, ".claude"));
5256
+ const hasClaudeMd = existsSync4(join5(projectRoot2, "CLAUDE.md"));
5257
+ if (!hasClaudeDir && !hasClaudeMd) return;
5184
5258
  const mcpPath = join5(projectRoot2, ".mcp.json");
5185
5259
  const existing = readExistingJson(mcpPath);
5186
5260
  if (!existing.mcpServers) existing.mcpServers = {};
@@ -5219,7 +5293,7 @@ function readExistingJson(filePath) {
5219
5293
 
5220
5294
  // src/setup/rule-writer.ts
5221
5295
  init_logger();
5222
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
5296
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
5223
5297
  import { join as join6 } from "path";
5224
5298
  function writeRuleFiles(projectRoot2, tools) {
5225
5299
  for (const tool of tools) {
@@ -5238,7 +5312,7 @@ function writeRuleFiles(projectRoot2, tools) {
5238
5312
  }
5239
5313
  function writeCursorRule(projectRoot2) {
5240
5314
  const rulesDir = join6(projectRoot2, ".cursor", "rules");
5241
- mkdirSync2(rulesDir, { recursive: true });
5315
+ mkdirSync3(rulesDir, { recursive: true });
5242
5316
  const content = `---
5243
5317
  description: SpecLore \u2014 AI coding constraints (setup placeholder)
5244
5318
  globs:
@@ -5260,7 +5334,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
5260
5334
  }
5261
5335
  function writeClaudeRule(projectRoot2) {
5262
5336
  const rulesDir = join6(projectRoot2, ".claude", "rules");
5263
- mkdirSync2(rulesDir, { recursive: true });
5337
+ mkdirSync3(rulesDir, { recursive: true });
5264
5338
  const content = `# SpecLore
5265
5339
 
5266
5340
  SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
@@ -5276,7 +5350,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
5276
5350
  }
5277
5351
  function writeQoderRule(projectRoot2) {
5278
5352
  const rulesDir = join6(projectRoot2, ".qoder", "rules");
5279
- mkdirSync2(rulesDir, { recursive: true });
5353
+ mkdirSync3(rulesDir, { recursive: true });
5280
5354
  const content = `# SpecLore
5281
5355
 
5282
5356
  SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
@@ -5300,13 +5374,20 @@ function registerSetupCommand(program) {
5300
5374
  logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5301
5375
  const tools = detectAITools(projectRoot2);
5302
5376
  if (tools.length === 0) {
5303
- logger.warn("No supported AI tools detected. Continuing anyway...");
5377
+ logger.warn("No supported AI tools detected.");
5378
+ logger.info("");
5379
+ logger.info(" To enable MCP integration, open your AI client in this project first,");
5380
+ logger.info(" then re-run setup. Or use manual configuration:");
5381
+ logger.info(" speclore mcp add cursor \u2014 configure for Cursor");
5382
+ logger.info(" speclore mcp add claude \u2014 configure for Claude Code");
5383
+ logger.info(" speclore mcp add qoder \u2014 configure for Qoder");
5384
+ logger.info("");
5304
5385
  } else {
5305
5386
  logger.info(`Detected AI tools: ${tools.map((t) => t.tool).join(", ")}`);
5306
5387
  }
5307
5388
  const specLoreDir = opts.global ? join7(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".speclore") : join7(projectRoot2, ".speclore");
5308
5389
  if (!existsSync5(specLoreDir)) {
5309
- mkdirSync3(specLoreDir, { recursive: true });
5390
+ mkdirSync4(specLoreDir, { recursive: true });
5310
5391
  }
5311
5392
  const configPath = join7(specLoreDir, "config.yaml");
5312
5393
  if (!existsSync5(configPath)) {
@@ -5314,12 +5395,23 @@ function registerSetupCommand(program) {
5314
5395
  writeFileSync4(configPath, generateDefaultConfigYaml(projectName), "utf-8");
5315
5396
  logger.info(`Created config: ${configPath}`);
5316
5397
  }
5317
- writeMcpConfig(projectRoot2, opts.global ?? false);
5398
+ writeMcpConfig(projectRoot2, tools, opts.global ?? false);
5318
5399
  writeRuleFiles(projectRoot2, tools);
5319
5400
  logger.info("");
5320
- logger.info("Setup complete! You can now:");
5321
- logger.info(" \u2022 Run `speclore init` to initialize project context");
5322
- logger.info(" \u2022 Or just start talking to your AI client about requirements");
5401
+ logger.info("Setup complete! Next steps:");
5402
+ logger.info("");
5403
+ logger.info(" Option A \u2014 CLI workflow (terminal users):");
5404
+ logger.info(' speclore spec "your requirement" \u2192 generate .feature');
5405
+ logger.info(" speclore code \u2192 generate constraints + tests");
5406
+ logger.info(" speclore verify \u2192 run acceptance");
5407
+ logger.info("");
5408
+ logger.info(" Option B \u2014 AI client workflow (recommended):");
5409
+ logger.info(" Open Cursor / Qoder / Claude Code and start chatting.");
5410
+ logger.info(" MCP is already configured \u2014 AI handles the full pipeline.");
5411
+ logger.info("");
5412
+ logger.info(" Optional \u2014 Pre-scan project context:");
5413
+ logger.info(" speclore init \u2192 scan modules/entities/APIs for better AI context");
5414
+ logger.info(" (Not required \u2014 context is auto-built on first spec/code call)");
5323
5415
  });
5324
5416
  }
5325
5417
 
@@ -5329,7 +5421,7 @@ init_logger();
5329
5421
  init_context_engine();
5330
5422
  import { join as join11 } from "path";
5331
5423
  function registerInitCommand(program) {
5332
- program.command("init").description("Initialize project: scan structure, detect modules, generate context.json").option("-v, --verbose", "Enable debug logging").action((opts) => {
5424
+ program.command("init").description("Scan project structure and generate context.json (optional \u2014 auto-runs on first spec/code call)").option("-v, --verbose", "Enable debug logging").action((opts) => {
5333
5425
  if (opts.verbose) logger.setLevel("debug");
5334
5426
  const projectRoot2 = process.cwd();
5335
5427
  logger.info("SpecLore Init");
@@ -5393,7 +5485,7 @@ init_test_scaffolder();
5393
5485
  init_context_engine();
5394
5486
  init_state_manager();
5395
5487
  import { join as join23 } from "path";
5396
- import { readFileSync as readFileSync13, existsSync as existsSync20 } from "fs";
5488
+ import { readFileSync as readFileSync14, existsSync as existsSync20 } from "fs";
5397
5489
  import { globSync as globSync4 } from "glob";
5398
5490
  function registerCodeCommand(program) {
5399
5491
  program.command("code").description("Generate AI coding constraint files from .feature specs").argument("[features...]", "Feature file paths or glob patterns (all if omitted)").option("-v, --verbose", "Enable debug logging").action(async (features, opts) => {
@@ -5449,7 +5541,7 @@ function resolveFeatures(patterns, projectRoot2, config) {
5449
5541
  const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
5450
5542
  for (const filePath of matches) {
5451
5543
  if (existsSync20(filePath)) {
5452
- const content = readFileSync13(filePath, "utf-8");
5544
+ const content = readFileSync14(filePath, "utf-8");
5453
5545
  const featureMatch = content.match(/Feature:\s*(.+)/);
5454
5546
  files.push({
5455
5547
  path: filePath,
@@ -5473,7 +5565,7 @@ init_context_engine();
5473
5565
  init_analyzer();
5474
5566
  init_state_manager();
5475
5567
  import { join as join27 } from "path";
5476
- import { readFileSync as readFileSync17, existsSync as existsSync24 } from "fs";
5568
+ import { readFileSync as readFileSync18, existsSync as existsSync24 } from "fs";
5477
5569
  import { globSync as globSync6 } from "glob";
5478
5570
  function registerVerifyCommand(program) {
5479
5571
  program.command("verify").description("Run tests and map results to .feature scenarios").argument("[features...]", "Feature file paths or glob patterns (all if omitted)").option("--impact", "Enable change impact analysis").option("--watch", "Watch for .feature file changes and re-run").option("--timeout <minutes>", "Watch timeout in minutes (default: 30)", "30").option("-v, --verbose", "Enable debug logging").action(async (features, opts) => {
@@ -5560,7 +5652,7 @@ function resolveFeatures2(patterns, projectRoot2, config) {
5560
5652
  const matches = globSync6(pattern, { cwd: projectRoot2, absolute: true });
5561
5653
  for (const filePath of matches) {
5562
5654
  if (existsSync24(filePath)) {
5563
- const content = readFileSync17(filePath, "utf-8");
5655
+ const content = readFileSync18(filePath, "utf-8");
5564
5656
  const featureMatch = content.match(/Feature:\s*(.+)/);
5565
5657
  files.push({
5566
5658
  path: filePath,
@@ -5581,7 +5673,7 @@ init_logger();
5581
5673
 
5582
5674
  // src/setup/cleanup.ts
5583
5675
  init_logger();
5584
- import { rmSync as rmSync4, existsSync as existsSync25, readFileSync as readFileSync18, writeFileSync as writeFileSync14 } from "fs";
5676
+ import { rmSync as rmSync4, existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
5585
5677
  import { join as join28 } from "path";
5586
5678
  function runTeardown(projectRoot2, globalMode) {
5587
5679
  if (globalMode) {
@@ -5610,7 +5702,7 @@ function runTeardown(projectRoot2, globalMode) {
5610
5702
  function removeMcpEntry(mcpPath, serverName) {
5611
5703
  if (!existsSync25(mcpPath)) return;
5612
5704
  try {
5613
- const config = JSON.parse(readFileSync18(mcpPath, "utf-8"));
5705
+ const config = JSON.parse(readFileSync19(mcpPath, "utf-8"));
5614
5706
  if (config.mcpServers?.[serverName]) {
5615
5707
  delete config.mcpServers[serverName];
5616
5708
  writeFileSync14(mcpPath, JSON.stringify(config, null, 2), "utf-8");
@@ -5706,6 +5798,114 @@ function registerMigrateCommand(program) {
5706
5798
  });
5707
5799
  }
5708
5800
 
5801
+ // src/cli/commands/mcp-config.ts
5802
+ init_logger();
5803
+ import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync15, rmSync as rmSync5 } from "fs";
5804
+ import { join as join30 } from "path";
5805
+ var VALID_CLIENTS = ["cursor", "claude", "qoder"];
5806
+ function getMcpPath(projectRoot2, client) {
5807
+ switch (client) {
5808
+ case "cursor":
5809
+ return join30(projectRoot2, ".cursor", "mcp.json");
5810
+ case "claude":
5811
+ return join30(projectRoot2, ".mcp.json");
5812
+ case "qoder":
5813
+ return join30(projectRoot2, ".qoder", "mcp.json");
5814
+ }
5815
+ }
5816
+ function clientLabel(client) {
5817
+ switch (client) {
5818
+ case "cursor":
5819
+ return "Cursor";
5820
+ case "claude":
5821
+ return "Claude Code";
5822
+ case "qoder":
5823
+ return "Qoder";
5824
+ }
5825
+ }
5826
+ function registerMcpConfigCommands(program) {
5827
+ const mcpCmd = program.commands.find((c) => c.name() === "mcp");
5828
+ if (!mcpCmd) return;
5829
+ mcpCmd.command("add <client>").description("Manually write SpecLore MCP config for a specific AI client (cursor | claude | qoder)").action((clientArg) => {
5830
+ const projectRoot2 = process.cwd();
5831
+ const client = validateClient(clientArg);
5832
+ if (!client) return;
5833
+ logger.info(`Configuring MCP for ${clientLabel(client)}...`);
5834
+ writeMcpForClient(projectRoot2, client);
5835
+ logger.info(`\u2713 ${clientLabel(client)} MCP configured at ${getMcpPath(projectRoot2, client)}`);
5836
+ });
5837
+ mcpCmd.command("remove <client>").description("Remove SpecLore MCP config from a specific AI client (cursor | claude | qoder)").action((clientArg) => {
5838
+ const projectRoot2 = process.cwd();
5839
+ const client = validateClient(clientArg);
5840
+ if (!client) return;
5841
+ const mcpPath = getMcpPath(projectRoot2, client);
5842
+ if (!existsSync27(mcpPath)) {
5843
+ logger.info(`${clientLabel(client)}: no MCP config file found at ${mcpPath}`);
5844
+ return;
5845
+ }
5846
+ const removed = removeMcpServerEntry(mcpPath, "speclore");
5847
+ if (removed) {
5848
+ logger.info(`\u2713 Removed speclore from ${clientLabel(client)}: ${mcpPath}`);
5849
+ } else {
5850
+ logger.info(`${clientLabel(client)}: speclore entry not found in ${mcpPath}`);
5851
+ }
5852
+ });
5853
+ mcpCmd.command("list").description("Show current SpecLore MCP configuration status for all clients").action(() => {
5854
+ const projectRoot2 = process.cwd();
5855
+ logger.info("SpecLore MCP Configuration Status");
5856
+ logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5857
+ let anyFound = false;
5858
+ for (const client of VALID_CLIENTS) {
5859
+ const mcpPath = getMcpPath(projectRoot2, client);
5860
+ if (!existsSync27(mcpPath)) {
5861
+ logger.info(` \u25CB ${clientLabel(client)}: no config file (${mcpPath})`);
5862
+ continue;
5863
+ }
5864
+ try {
5865
+ const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
5866
+ if (config.mcpServers?.["speclore"]) {
5867
+ logger.info(` \u2713 ${clientLabel(client)}: configured \u2192 ${mcpPath}`);
5868
+ anyFound = true;
5869
+ } else {
5870
+ logger.info(` \u25CB ${clientLabel(client)}: config file exists but speclore not registered \u2192 ${mcpPath}`);
5871
+ }
5872
+ } catch {
5873
+ logger.info(` \u2717 ${clientLabel(client)}: failed to parse ${mcpPath}`);
5874
+ }
5875
+ }
5876
+ if (!anyFound) {
5877
+ logger.info("");
5878
+ logger.info(" No SpecLore MCP configurations found.");
5879
+ logger.info(" Run `speclore setup` or `speclore mcp add <client>` to configure.");
5880
+ }
5881
+ });
5882
+ }
5883
+ function validateClient(arg) {
5884
+ const normalised = arg.toLowerCase().trim();
5885
+ if (!VALID_CLIENTS.includes(normalised)) {
5886
+ logger.error(`Unknown client "${arg}". Supported: ${VALID_CLIENTS.join(", ")}`);
5887
+ return null;
5888
+ }
5889
+ return normalised;
5890
+ }
5891
+ function removeMcpServerEntry(mcpPath, serverName) {
5892
+ if (!existsSync27(mcpPath)) return false;
5893
+ try {
5894
+ const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
5895
+ if (!config.mcpServers?.[serverName]) return false;
5896
+ delete config.mcpServers[serverName];
5897
+ const remainingServers = Object.keys(config.mcpServers ?? {});
5898
+ if (remainingServers.length === 0) {
5899
+ rmSync5(mcpPath);
5900
+ } else {
5901
+ writeFileSync15(mcpPath, JSON.stringify(config, null, 2), "utf-8");
5902
+ }
5903
+ return true;
5904
+ } catch {
5905
+ return false;
5906
+ }
5907
+ }
5908
+
5709
5909
  // src/cli/index.ts
5710
5910
  init_version();
5711
5911
  function createProgram() {
@@ -5725,6 +5925,7 @@ function createProgram() {
5725
5925
  const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
5726
5926
  await startMcpServer2();
5727
5927
  });
5928
+ registerMcpConfigCommands(program);
5728
5929
  program.argument("[text...]", "Quick requirement text \u2192 .feature").action(async (text) => {
5729
5930
  if (text.length > 0) {
5730
5931
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));