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