speclore 0.1.6 → 0.1.7

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,35 @@ 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
+ isEvalSupported: false
1506
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1507
+ }).promise;
1508
+ const textParts = [];
1509
+ for (let i = 1; i <= doc.numPages; i++) {
1510
+ const page = await doc.getPage(i);
1511
+ const textContent = await page.getTextContent();
1512
+ const pageText = textContent.items.map((item) => item.str).join("");
1513
+ if (pageText) textParts.push(pageText);
1501
1514
  }
1502
- const { readFileSync: readFileSync22 } = await import("fs");
1503
- const buffer = readFileSync22(source);
1504
- const data = await pdfParse(buffer);
1515
+ doc.destroy();
1516
+ const text = textParts.join("\n");
1505
1517
  return [{
1506
1518
  id: source.replace(/\\/g, "/").replace(/\.pdf$/i, ""),
1507
1519
  title: source.split("/").pop()?.replace(/\.pdf$/i, "") ?? "Untitled",
1508
- description: data.text,
1509
- rawContent: data.text,
1520
+ description: text,
1521
+ rawContent: text,
1510
1522
  confidence: 0.75
1511
1523
  }];
1512
1524
  }
@@ -2102,13 +2114,13 @@ var init_image_reader = __esm({
2102
2114
  return /\.(png|jpe?g|webp)$/i.test(source);
2103
2115
  }
2104
2116
  async read(source) {
2105
- const { existsSync: existsSync30, readFileSync: readFileSync22 } = await import("fs");
2106
- if (!existsSync30(source)) {
2117
+ const { existsSync: existsSync31, readFileSync: readFileSync24 } = await import("fs");
2118
+ if (!existsSync31(source)) {
2107
2119
  throw new Error(`Image file not found: ${source}`);
2108
2120
  }
2109
2121
  const ext = source.split(".").pop()?.toLowerCase() ?? "png";
2110
2122
  const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
2111
- const buffer = readFileSync22(source);
2123
+ const buffer = readFileSync24(source);
2112
2124
  let text;
2113
2125
  try {
2114
2126
  const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
@@ -2137,7 +2149,7 @@ var init_image_reader = __esm({
2137
2149
  });
2138
2150
 
2139
2151
  // src/plugins/builtin/cursor-writer.ts
2140
- 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";
2141
2153
  import { join as join14 } from "path";
2142
2154
  var CursorWriter;
2143
2155
  var init_cursor_writer = __esm({
@@ -2153,7 +2165,7 @@ var init_cursor_writer = __esm({
2153
2165
  write(constraints) {
2154
2166
  this.projectRoot = constraints.projectRoot;
2155
2167
  const rulesDir = join14(constraints.projectRoot, ".cursor", "rules");
2156
- mkdirSync6(rulesDir, { recursive: true });
2168
+ mkdirSync7(rulesDir, { recursive: true });
2157
2169
  const frontmatter = [
2158
2170
  "---",
2159
2171
  "description: SpecLore coding constraints \u2014 auto-generated",
@@ -2232,7 +2244,7 @@ var init_cursor_writer = __esm({
2232
2244
  });
2233
2245
 
2234
2246
  // src/plugins/builtin/claude-writer.ts
2235
- 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";
2236
2248
  import { join as join15 } from "path";
2237
2249
  var ClaudeWriter;
2238
2250
  var init_claude_writer = __esm({
@@ -2248,7 +2260,7 @@ var init_claude_writer = __esm({
2248
2260
  write(constraints) {
2249
2261
  this.projectRoot = constraints.projectRoot;
2250
2262
  const rulesDir = join15(constraints.projectRoot, ".claude", "rules");
2251
- mkdirSync7(rulesDir, { recursive: true });
2263
+ mkdirSync8(rulesDir, { recursive: true });
2252
2264
  const content = this.buildMarkdown(constraints);
2253
2265
  writeFileSync8(join15(rulesDir, "speclore.md"), content, "utf-8");
2254
2266
  return Promise.resolve();
@@ -2314,7 +2326,7 @@ var init_claude_writer = __esm({
2314
2326
  });
2315
2327
 
2316
2328
  // src/plugins/builtin/qoder-writer.ts
2317
- 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";
2318
2330
  import { join as join16 } from "path";
2319
2331
  var QoderWriter;
2320
2332
  var init_qoder_writer = __esm({
@@ -2330,7 +2342,7 @@ var init_qoder_writer = __esm({
2330
2342
  write(constraints) {
2331
2343
  this.projectRoot = constraints.projectRoot;
2332
2344
  const rulesDir = join16(constraints.projectRoot, ".qoder", "rules");
2333
- mkdirSync8(rulesDir, { recursive: true });
2345
+ mkdirSync9(rulesDir, { recursive: true });
2334
2346
  const content = this.buildMarkdown(constraints);
2335
2347
  writeFileSync9(join16(rulesDir, "speclore.md"), content, "utf-8");
2336
2348
  return Promise.resolve();
@@ -2763,11 +2775,12 @@ var init_markdown_reader = __esm({
2763
2775
 
2764
2776
  // src/core/requirement-reader/docx-reader.ts
2765
2777
  import { basename as basename3, extname as extname3 } from "path";
2778
+ import { readFileSync as readFileSync10 } from "fs";
2766
2779
  import mammoth from "mammoth";
2767
- async function readDocxFile(filePath) {
2768
- const result = await mammoth.extractRawText({ path: filePath });
2780
+ async function parseDocxBuffer(buffer, idHint = "document") {
2781
+ const result = await mammoth.extractRawText({ buffer });
2769
2782
  const content = result.value;
2770
- 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, "-");
2771
2784
  const lines = content.split("\n").filter((l) => l.trim());
2772
2785
  const title = lines[0]?.trim() ?? id;
2773
2786
  return {
@@ -2778,6 +2791,11 @@ async function readDocxFile(filePath) {
2778
2791
  confidence: 0.9
2779
2792
  };
2780
2793
  }
2794
+ async function readDocxFile(filePath) {
2795
+ const buffer = readFileSync10(filePath);
2796
+ const name = basename3(filePath, extname3(filePath));
2797
+ return parseDocxBuffer(buffer, name);
2798
+ }
2781
2799
  var init_docx_reader2 = __esm({
2782
2800
  "src/core/requirement-reader/docx-reader.ts"() {
2783
2801
  "use strict";
@@ -2786,17 +2804,16 @@ var init_docx_reader2 = __esm({
2786
2804
 
2787
2805
  // src/core/requirement-reader/xlsx-reader.ts
2788
2806
  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, "-");
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, "-");
2793
2810
  const firstSheetName = workbook.SheetNames[0];
2794
2811
  if (!firstSheetName) {
2795
- return Promise.reject(new Error(`No sheets found in ${filePath}`));
2812
+ return Promise.reject(new Error("No sheets found in workbook"));
2796
2813
  }
2797
2814
  const sheet = workbook.Sheets[firstSheetName];
2798
2815
  if (!sheet) {
2799
- return Promise.reject(new Error(`Sheet "${firstSheetName}" not found in ${filePath}`));
2816
+ return Promise.reject(new Error(`Sheet "${firstSheetName}" not found`));
2800
2817
  }
2801
2818
  const title = firstSheetName;
2802
2819
  const rows = utils2.sheet_to_json(sheet, { header: 1, defval: "" });
@@ -2832,6 +2849,11 @@ function readXlsxFile(filePath) {
2832
2849
  confidence: 0.85
2833
2850
  });
2834
2851
  }
2852
+ function readXlsxFile(filePath) {
2853
+ const workbook = readFile2(filePath);
2854
+ const idHint = basename4(filePath, extname4(filePath));
2855
+ return parseWorkbook2(workbook, idHint);
2856
+ }
2835
2857
  var init_xlsx_reader2 = __esm({
2836
2858
  "src/core/requirement-reader/xlsx-reader.ts"() {
2837
2859
  "use strict";
@@ -2842,21 +2864,42 @@ var init_xlsx_reader2 = __esm({
2842
2864
  // src/core/requirement-reader/pdf-reader.ts
2843
2865
  import { basename as basename5, extname as extname5 } from "path";
2844
2866
  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());
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
+ isEvalSupported: false
2874
+ }).promise;
2875
+ const textParts = [];
2876
+ for (let i = 1; i <= doc.numPages; i++) {
2877
+ const page = await doc.getPage(i);
2878
+ const textContent = await page.getTextContent();
2879
+ const pageText = textContent.items.map((item) => item.str).join("");
2880
+ if (pageText) textParts.push(pageText);
2881
+ }
2882
+ doc.destroy();
2883
+ return textParts.join("\n");
2884
+ }
2885
+ async function parsePdfBuffer(dataBuffer, idHint = "document") {
2886
+ const text = await extractPdfText(dataBuffer);
2887
+ const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2888
+ const lines = text.split("\n").filter((l) => l.trim());
2851
2889
  const title = lines[0]?.trim() ?? id;
2852
2890
  return {
2853
2891
  id,
2854
2892
  title,
2855
- description: data.text,
2856
- rawContent: data.text,
2893
+ description: text,
2894
+ rawContent: text,
2857
2895
  confidence: 0.8
2858
2896
  };
2859
2897
  }
2898
+ async function readPdfFile(filePath) {
2899
+ const dataBuffer = await readFile3(filePath);
2900
+ const name = basename5(filePath, extname5(filePath));
2901
+ return parsePdfBuffer(dataBuffer, name);
2902
+ }
2860
2903
  var init_pdf_reader2 = __esm({
2861
2904
  "src/core/requirement-reader/pdf-reader.ts"() {
2862
2905
  "use strict";
@@ -2865,14 +2908,14 @@ var init_pdf_reader2 = __esm({
2865
2908
 
2866
2909
  // src/core/requirement-reader/image-reader.ts
2867
2910
  import { basename as basename6, extname as extname6 } from "path";
2868
- import { readFileSync as readFileSync10 } from "fs";
2869
- async function readImageFile(filePath) {
2911
+ import { readFileSync as readFileSync11 } from "fs";
2912
+ async function readImageFile(filePath, providerOverride) {
2870
2913
  const id = basename6(filePath, extname6(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2871
2914
  logger.info(`Reading image via AI Vision: ${filePath}`);
2872
2915
  const ext = extname6(filePath).toLowerCase();
2873
2916
  const mimeType = MIME_MAP[ext] ?? "image/png";
2874
- const buffer = readFileSync10(filePath);
2875
- const provider = await createProvider();
2917
+ const buffer = readFileSync11(filePath);
2918
+ const provider = providerOverride ?? await createProvider();
2876
2919
  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
2920
  if (!provider.generateWithImage) {
2878
2921
  throw new Error(`AI provider '${provider.name}' does not support image/vision input. Use a vision-capable model.`);
@@ -3317,11 +3360,11 @@ var init_prompt_builder = __esm({
3317
3360
  });
3318
3361
 
3319
3362
  // src/core/feature-generator/generator.ts
3320
- import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync9, existsSync as existsSync15 } from "fs";
3363
+ import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync15 } from "fs";
3321
3364
  import { join as join17, dirname as dirname2 } from "path";
3322
3365
  import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
3323
3366
  import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
3324
- async function generateFeature(requirement, context, config, projectRoot2) {
3367
+ async function generateFeature(requirement, context, config, projectRoot2, providerOverride) {
3325
3368
  logger.info(`Generating feature for: ${requirement.title}`);
3326
3369
  const registry = getRegistry();
3327
3370
  await registry.invokeLifecycle("beforeSpec", requirement);
@@ -3329,7 +3372,7 @@ async function generateFeature(requirement, context, config, projectRoot2) {
3329
3372
  const aiConfig = config.ai;
3330
3373
  const fallbackConfigs = aiConfig?.fallbackProviders ?? [];
3331
3374
  const providerConfigs = aiConfig ? [aiConfig, ...fallbackConfigs] : [];
3332
- const provider = providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig);
3375
+ const provider = providerOverride ?? (providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig));
3333
3376
  if (!provider.isAvailable()) {
3334
3377
  throw new Error("AI provider not available. Set API key in environment or config.yaml.");
3335
3378
  }
@@ -3386,7 +3429,7 @@ Please fix these issues and regenerate the complete Feature file.`;
3386
3429
  const moduleDir = inferModule(requirement, context);
3387
3430
  const filePath = join17(outputDir, moduleDir, `${requirement.id}.feature`);
3388
3431
  if (!existsSync15(dirname2(filePath))) {
3389
- mkdirSync9(dirname2(filePath), { recursive: true });
3432
+ mkdirSync10(dirname2(filePath), { recursive: true });
3390
3433
  }
3391
3434
  writeFileSync10(filePath, featureContent, "utf-8");
3392
3435
  logger.info(`Feature written: ${toPosixPath(filePath)}`);
@@ -3552,9 +3595,9 @@ function detectAITools2(projectRoot2) {
3552
3595
  tools.push("cursor");
3553
3596
  logger.debug("Detected Cursor (.cursor/)");
3554
3597
  }
3555
- if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md")) || existsSync16(join19(projectRoot2, ".mcp.json"))) {
3598
+ if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))) {
3556
3599
  tools.push("claude");
3557
- logger.debug("Detected Claude Code (.claude/ or CLAUDE.md or .mcp.json)");
3600
+ logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
3558
3601
  }
3559
3602
  if (existsSync16(join19(projectRoot2, ".qoder"))) {
3560
3603
  tools.push("qoder");
@@ -3571,7 +3614,7 @@ var init_ai_tool_detector = __esm({
3571
3614
  });
3572
3615
 
3573
3616
  // src/core/constraint-coder/constraint-writer.ts
3574
- import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync10, existsSync as existsSync17 } from "fs";
3617
+ import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync17 } from "fs";
3575
3618
  import { join as join20 } from "path";
3576
3619
  function writeConstraints(projectRoot2, tools, content) {
3577
3620
  const writtenFiles = [];
@@ -3645,7 +3688,7 @@ function buildConstraintText(content) {
3645
3688
  }
3646
3689
  function writeCursorRule2(projectRoot2, text) {
3647
3690
  const dir = join20(projectRoot2, ".cursor", "rules");
3648
- if (!existsSync17(dir)) mkdirSync10(dir, { recursive: true });
3691
+ if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
3649
3692
  const filePath = join20(dir, "speclore.mdc");
3650
3693
  const frontmatter = [
3651
3694
  "---",
@@ -3660,14 +3703,14 @@ function writeCursorRule2(projectRoot2, text) {
3660
3703
  }
3661
3704
  function writeClaudeRule2(projectRoot2, text) {
3662
3705
  const dir = join20(projectRoot2, ".claude", "rules");
3663
- if (!existsSync17(dir)) mkdirSync10(dir, { recursive: true });
3706
+ if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
3664
3707
  const filePath = join20(dir, "speclore.md");
3665
3708
  writeFileSync11(filePath, text, "utf-8");
3666
3709
  return filePath;
3667
3710
  }
3668
3711
  function writeQoderRule2(projectRoot2, text) {
3669
3712
  const dir = join20(projectRoot2, ".qoder", "rules");
3670
- if (!existsSync17(dir)) mkdirSync10(dir, { recursive: true });
3713
+ if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
3671
3714
  const filePath = join20(dir, "speclore.md");
3672
3715
  writeFileSync11(filePath, text, "utf-8");
3673
3716
  return filePath;
@@ -3747,14 +3790,14 @@ var init_constraint_coder = __esm({
3747
3790
  init_logger();
3748
3791
  init_ai_tool_detector();
3749
3792
  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`;
3793
+ MAPPING_INSTRUCTIONS = `Generate a mapping file for each test file at .speclore/mappings/{module}/{feature-name}.json.
3794
+ Format: { "feature": "specs/...", "scenarios": { "Scenario name": { "testFile": "...", "testMethod": "..." } } }
3795
+ Keep mapping files in sync whenever tests are modified.`;
3753
3796
  }
3754
3797
  });
3755
3798
 
3756
3799
  // src/core/test-scaffolder/framework-detector.ts
3757
- import { readFileSync as readFileSync11, existsSync as existsSync18 } from "fs";
3800
+ import { readFileSync as readFileSync12, existsSync as existsSync18 } from "fs";
3758
3801
  import { join as join21 } from "path";
3759
3802
  function detectTestFramework(projectRoot2) {
3760
3803
  const pkgPath = join21(projectRoot2, "package.json");
@@ -3762,7 +3805,7 @@ function detectTestFramework(projectRoot2) {
3762
3805
  return "vitest";
3763
3806
  }
3764
3807
  try {
3765
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
3808
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3766
3809
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3767
3810
  if ("vitest" in deps) return "vitest";
3768
3811
  if ("jest" in deps || "@jest/core" in deps) return "jest";
@@ -3778,7 +3821,7 @@ var init_framework_detector = __esm({
3778
3821
  });
3779
3822
 
3780
3823
  // src/core/test-scaffolder/index.ts
3781
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
3824
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync12 } from "fs";
3782
3825
  import { join as join22, relative as relative3, dirname as dirname3, basename as basename8 } from "path";
3783
3826
  function generateTestScaffolding(projectRoot2, features, config) {
3784
3827
  const framework = detectTestFramework(projectRoot2);
@@ -3801,7 +3844,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
3801
3844
  const content = generateTestFileContent(feature.featureName, scenarios, framework);
3802
3845
  const dir = dirname3(absTestPath);
3803
3846
  if (!existsSync19(dir)) {
3804
- mkdirSync11(dir, { recursive: true });
3847
+ mkdirSync12(dir, { recursive: true });
3805
3848
  }
3806
3849
  writeFileSync12(absTestPath, content, "utf-8");
3807
3850
  results.push({ testFile: testFilePath, framework, scenarios: scenarios.length });
@@ -3874,7 +3917,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
3874
3917
  return lines.join("\n");
3875
3918
  }
3876
3919
  function appendMissingScenarios(testFilePath, scenarios, _framework) {
3877
- const content = readFileSync12(testFilePath, "utf-8");
3920
+ const content = readFileSync13(testFilePath, "utf-8");
3878
3921
  const missing = [];
3879
3922
  for (const scenario of scenarios) {
3880
3923
  if (!content.includes(`Scenario: ${scenario.name}`)) {
@@ -3915,7 +3958,7 @@ var init_test_scaffolder = __esm({
3915
3958
  });
3916
3959
 
3917
3960
  // src/core/verifier/mapping-resolver.ts
3918
- import { readFileSync as readFileSync14, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
3961
+ import { readFileSync as readFileSync15, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
3919
3962
  import { join as join24 } from "path";
3920
3963
  function resolveMappings(projectRoot2, features, testOutput) {
3921
3964
  const results = [];
@@ -3945,7 +3988,7 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
3945
3988
  const files = findMappingFiles(mappingsDir);
3946
3989
  for (const file of files) {
3947
3990
  try {
3948
- const content = readFileSync14(file, "utf-8");
3991
+ const content = readFileSync15(file, "utf-8");
3949
3992
  const mapping = JSON.parse(content);
3950
3993
  if (mapping.scenarios && scenario.name in mapping.scenarios) {
3951
3994
  const entry = mapping.scenarios[scenario.name];
@@ -3975,7 +4018,7 @@ function resolveFromTag(projectRoot2, _feature, scenario) {
3975
4018
  const files = findTestFiles(testsDir);
3976
4019
  for (const file of files) {
3977
4020
  try {
3978
- const content = readFileSync14(file, "utf-8");
4021
+ const content = readFileSync15(file, "utf-8");
3979
4022
  const tagRegex = /@speclore-scenario:\s*(.+)/g;
3980
4023
  let match;
3981
4024
  while ((match = tagRegex.exec(content)) !== null) {
@@ -4155,13 +4198,13 @@ var report_generator_exports = {};
4155
4198
  __export(report_generator_exports, {
4156
4199
  generateReport: () => generateReport
4157
4200
  });
4158
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync12, existsSync as existsSync22, readFileSync as readFileSync15 } from "fs";
4201
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync22, readFileSync as readFileSync16 } from "fs";
4159
4202
  import { join as join25, dirname as dirname4 } from "path";
4160
4203
  import { fileURLToPath as fileURLToPath2 } from "url";
4161
4204
  function generateReport(report, projectRoot2, config) {
4162
4205
  const reportsDir = join25(projectRoot2, ".speclore", "reports");
4163
4206
  if (!existsSync22(reportsDir)) {
4164
- mkdirSync12(reportsDir, { recursive: true });
4207
+ mkdirSync13(reportsDir, { recursive: true });
4165
4208
  }
4166
4209
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
4167
4210
  const writtenFiles = [];
@@ -4218,7 +4261,7 @@ function renderHtmlReport(report) {
4218
4261
  const templatePath = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "cli", "templates", "report.html");
4219
4262
  let template;
4220
4263
  try {
4221
- template = readFileSync15(templatePath, "utf-8");
4264
+ template = readFileSync16(templatePath, "utf-8");
4222
4265
  } catch {
4223
4266
  template = getInlineTemplate();
4224
4267
  }
@@ -4297,7 +4340,7 @@ var init_verifier = __esm({
4297
4340
  });
4298
4341
 
4299
4342
  // src/core/analyzer/rdg-builder.ts
4300
- import { readFileSync as readFileSync16, existsSync as existsSync23, readdirSync as readdirSync3 } from "fs";
4343
+ import { readFileSync as readFileSync17, existsSync as existsSync23, readdirSync as readdirSync3 } from "fs";
4301
4344
  import { join as join26 } from "path";
4302
4345
  var init_rdg_builder = __esm({
4303
4346
  "src/core/analyzer/rdg-builder.ts"() {
@@ -4327,6 +4370,9 @@ import { execFileSync as execFileSync3 } from "child_process";
4327
4370
  import { globSync as globSync5 } from "glob";
4328
4371
  function analyzeImpact(projectRoot2, context, config) {
4329
4372
  const changedFiles = getChangedFiles(projectRoot2);
4373
+ return analyzeImpactWithChanges(changedFiles, context, config, projectRoot2);
4374
+ }
4375
+ function analyzeImpactWithChanges(changedFiles, context, config, projectRoot2) {
4330
4376
  const affectedModules = determineAffectedModules(changedFiles, context);
4331
4377
  const affectedFeatures = determineAffectedFeatures(affectedModules, projectRoot2, config.spec.outputDir);
4332
4378
  logger.info(
@@ -4402,11 +4448,11 @@ var init_analyzer = __esm({
4402
4448
  });
4403
4449
 
4404
4450
  // 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";
4451
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync, existsSync as existsSync28, statSync as statSync3 } from "fs";
4452
+ import { join as join31 } from "path";
4407
4453
  function acquireLock(specLoreDir) {
4408
- const lockPath = join30(specLoreDir, LOCK_FILENAME);
4409
- if (existsSync27(lockPath)) {
4454
+ const lockPath = join31(specLoreDir, LOCK_FILENAME);
4455
+ if (existsSync28(lockPath)) {
4410
4456
  const existing = readLockInfo(lockPath);
4411
4457
  if (existing && !isLockExpired(existing)) {
4412
4458
  logger.debug(`Lock already held by PID ${existing.pid}, not expired`);
@@ -4420,7 +4466,7 @@ function acquireLock(specLoreDir) {
4420
4466
  timestamp: Date.now()
4421
4467
  };
4422
4468
  try {
4423
- writeFileSync15(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
4469
+ writeFileSync16(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
4424
4470
  } catch {
4425
4471
  logger.debug("Lock write failed \u2014 another process may have acquired it");
4426
4472
  return false;
@@ -4429,8 +4475,8 @@ function acquireLock(specLoreDir) {
4429
4475
  return true;
4430
4476
  }
4431
4477
  function releaseLock(specLoreDir) {
4432
- const lockPath = join30(specLoreDir, LOCK_FILENAME);
4433
- if (!existsSync27(lockPath)) {
4478
+ const lockPath = join31(specLoreDir, LOCK_FILENAME);
4479
+ if (!existsSync28(lockPath)) {
4434
4480
  return;
4435
4481
  }
4436
4482
  const existing = readLockInfo(lockPath);
@@ -4441,7 +4487,7 @@ function releaseLock(specLoreDir) {
4441
4487
  }
4442
4488
  function readLockInfo(lockPath) {
4443
4489
  try {
4444
- const content = readFileSync19(lockPath, "utf-8");
4490
+ const content = readFileSync21(lockPath, "utf-8");
4445
4491
  const parsed = JSON.parse(content);
4446
4492
  if (typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
4447
4493
  return parsed;
@@ -4511,8 +4557,8 @@ var init_schemas = __esm({
4511
4557
  });
4512
4558
 
4513
4559
  // 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";
4560
+ import { join as join32 } from "path";
4561
+ import { readFileSync as readFileSync22, existsSync as existsSync29, mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
4516
4562
  import { globSync as globSync7 } from "glob";
4517
4563
  import { toJSONSchema } from "zod";
4518
4564
  async function executeSpecTool(args, projectRoot2) {
@@ -4523,7 +4569,7 @@ async function executeSpecTool(args, projectRoot2) {
4523
4569
  const config = loadConfig(projectRoot2);
4524
4570
  logger.info(`Reading requirement from: ${args.source.slice(0, 80)}...`);
4525
4571
  const req = await readRequirement(args.source);
4526
- const specLoreDir = join31(projectRoot2, ".speclore");
4572
+ const specLoreDir = join32(projectRoot2, ".speclore");
4527
4573
  const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
4528
4574
  const feature = await generateFeature(req, context, config, projectRoot2);
4529
4575
  const createdFiles = [feature.path];
@@ -4555,7 +4601,7 @@ async function executeCodeTool(args, projectRoot2) {
4555
4601
  }
4556
4602
  ensureProjectReady(projectRoot2);
4557
4603
  const config = loadConfig(projectRoot2);
4558
- const specLoreDir = join31(projectRoot2, ".speclore");
4604
+ const specLoreDir = join32(projectRoot2, ".speclore");
4559
4605
  const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
4560
4606
  const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
4561
4607
  if (featureFiles.length === 0) {
@@ -4608,7 +4654,7 @@ async function executeCodeTool(args, projectRoot2) {
4608
4654
  async function executeVerifyTool(args, projectRoot2) {
4609
4655
  ensureProjectReady(projectRoot2);
4610
4656
  const config = loadConfig(projectRoot2);
4611
- const specLoreDir = join31(projectRoot2, ".speclore");
4657
+ const specLoreDir = join32(projectRoot2, ".speclore");
4612
4658
  const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
4613
4659
  const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
4614
4660
  const stateManager = new StateManager(projectRoot2);
@@ -4670,22 +4716,22 @@ async function executeVerifyTool(args, projectRoot2) {
4670
4716
  };
4671
4717
  }
4672
4718
  function ensureProjectReady(projectRoot2) {
4673
- const specLoreDir = join31(projectRoot2, ".speclore");
4674
- const configPath = join31(specLoreDir, "config.yaml");
4719
+ const specLoreDir = join32(projectRoot2, ".speclore");
4720
+ const configPath = join32(specLoreDir, "config.yaml");
4675
4721
  let configCreated = false;
4676
- if (!existsSync28(specLoreDir)) {
4677
- mkdirSync13(specLoreDir, { recursive: true });
4722
+ if (!existsSync29(specLoreDir)) {
4723
+ mkdirSync14(specLoreDir, { recursive: true });
4678
4724
  }
4679
- if (!existsSync28(configPath)) {
4725
+ if (!existsSync29(configPath)) {
4680
4726
  const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
4681
- writeFileSync16(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4727
+ writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4682
4728
  configCreated = true;
4683
4729
  logger.info(`Auto-created default config: ${configPath}`);
4684
4730
  }
4685
4731
  const stateManager = new StateManager(projectRoot2);
4686
4732
  stateManager.ensureInitialized();
4687
4733
  const config = loadConfig(projectRoot2);
4688
- const specsDir = join31(projectRoot2, config.spec.outputDir);
4734
+ const specsDir = join32(projectRoot2, config.spec.outputDir);
4689
4735
  const migrated = stateManager.migrateFeatures(specsDir);
4690
4736
  if (migrated > 0) {
4691
4737
  logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
@@ -4722,14 +4768,14 @@ function resolveNextStep(currentState, _summary) {
4722
4768
  }
4723
4769
  }
4724
4770
  function resolveFeatureFiles(patterns, projectRoot2, config) {
4725
- const specsDir = join31(projectRoot2, config.spec.outputDir);
4771
+ const specsDir = join32(projectRoot2, config.spec.outputDir);
4726
4772
  const searchPatterns = patterns && patterns.length > 0 ? patterns : [`${specsDir}/**/*.feature`];
4727
4773
  const files = [];
4728
4774
  for (const pattern of searchPatterns) {
4729
4775
  const matches = globSync7(pattern, { cwd: projectRoot2, absolute: true });
4730
4776
  for (const filePath of matches) {
4731
- if (existsSync28(filePath)) {
4732
- const content = readFileSync20(filePath, "utf-8");
4777
+ if (existsSync29(filePath)) {
4778
+ const content = readFileSync22(filePath, "utf-8");
4733
4779
  files.push(parseFeatureFile(filePath, content));
4734
4780
  }
4735
4781
  }
@@ -4837,8 +4883,8 @@ var init_tools = __esm({
4837
4883
  });
4838
4884
 
4839
4885
  // 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";
4886
+ import { readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync15, writeFileSync as writeFileSync18 } from "fs";
4887
+ import { join as join33 } from "path";
4842
4888
  import { globSync as globSync8 } from "glob";
4843
4889
  function executeStatusTool(args, projectRoot2) {
4844
4890
  const { configCreated } = ensureProjectReadyForStatus(projectRoot2);
@@ -4854,9 +4900,9 @@ function executeStatusTool(args, projectRoot2) {
4854
4900
  continue;
4855
4901
  }
4856
4902
  let scenarioCount = 0;
4857
- if (existsSync29(path)) {
4903
+ if (existsSync30(path)) {
4858
4904
  try {
4859
- const content = readFileSync21(path, "utf-8");
4905
+ const content = readFileSync23(path, "utf-8");
4860
4906
  const matches = content.match(/Scenario(?: Outline)?:/g);
4861
4907
  scenarioCount = matches?.length ?? 0;
4862
4908
  } catch {
@@ -4871,7 +4917,7 @@ function executeStatusTool(args, projectRoot2) {
4871
4917
  lastVerify: entry.lastVerify ? { passed: entry.lastVerify.passed, failed: entry.lastVerify.failed, timestamp: entry.lastVerify.timestamp } : void 0
4872
4918
  });
4873
4919
  }
4874
- const specsDir = join32(projectRoot2, config.spec.outputDir);
4920
+ const specsDir = join33(projectRoot2, config.spec.outputDir);
4875
4921
  const allFeatureFiles = globSync8(`${specsDir}/**/*.feature`, { cwd: projectRoot2, absolute: true });
4876
4922
  const trackedPaths = new Set(featureEntries.map((f) => f.path));
4877
4923
  for (const filePath of allFeatureFiles) {
@@ -4879,7 +4925,7 @@ function executeStatusTool(args, projectRoot2) {
4879
4925
  if (args.feature && !filePath.includes(args.feature)) continue;
4880
4926
  let scenarioCount = 0;
4881
4927
  try {
4882
- const content = readFileSync21(filePath, "utf-8");
4928
+ const content = readFileSync23(filePath, "utf-8");
4883
4929
  const matches = content.match(/Scenario(?: Outline)?:/g);
4884
4930
  scenarioCount = matches?.length ?? 0;
4885
4931
  } catch {
@@ -4936,22 +4982,22 @@ function buildRecommendedActions(features, summary, testCommand) {
4936
4982
  return actions;
4937
4983
  }
4938
4984
  function ensureProjectReadyForStatus(projectRoot2) {
4939
- const specLoreDir = join32(projectRoot2, ".speclore");
4940
- const configPath = join32(specLoreDir, "config.yaml");
4985
+ const specLoreDir = join33(projectRoot2, ".speclore");
4986
+ const configPath = join33(specLoreDir, "config.yaml");
4941
4987
  let configCreated = false;
4942
- if (!existsSync29(specLoreDir)) {
4943
- mkdirSync14(specLoreDir, { recursive: true });
4988
+ if (!existsSync30(specLoreDir)) {
4989
+ mkdirSync15(specLoreDir, { recursive: true });
4944
4990
  }
4945
- if (!existsSync29(configPath)) {
4991
+ if (!existsSync30(configPath)) {
4946
4992
  const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
4947
- writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4993
+ writeFileSync18(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4948
4994
  configCreated = true;
4949
4995
  logger.info(`Auto-created default config: ${configPath}`);
4950
4996
  }
4951
4997
  const stateManager = new StateManager(projectRoot2);
4952
4998
  stateManager.ensureInitialized();
4953
4999
  const config = loadConfig(projectRoot2);
4954
- const specsDir = join32(projectRoot2, config.spec.outputDir);
5000
+ const specsDir = join33(projectRoot2, config.spec.outputDir);
4955
5001
  const migrated = stateManager.migrateFeatures(specsDir);
4956
5002
  if (migrated > 0) {
4957
5003
  logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
@@ -4975,7 +5021,7 @@ __export(server_exports, {
4975
5021
  });
4976
5022
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4977
5023
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4978
- import { mkdirSync as mkdirSync15 } from "fs";
5024
+ import { mkdirSync as mkdirSync16 } from "fs";
4979
5025
  function getProjectRoot() {
4980
5026
  return projectRoot;
4981
5027
  }
@@ -4983,7 +5029,7 @@ async function startMcpServer() {
4983
5029
  projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
4984
5030
  const specLoreDir = `${projectRoot}/.speclore`;
4985
5031
  try {
4986
- mkdirSync15(specLoreDir, { recursive: true });
5032
+ mkdirSync16(specLoreDir, { recursive: true });
4987
5033
  const locked = acquireLock(specLoreDir);
4988
5034
  if (!locked) {
4989
5035
  logger.warn("Could not acquire lock \u2014 another SpecLore instance may be running.");
@@ -5096,11 +5142,11 @@ import { Command } from "commander";
5096
5142
  init_logger();
5097
5143
  init_config2();
5098
5144
  init_detector();
5099
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
5145
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
5100
5146
  import { join as join7 } from "path";
5101
5147
 
5102
5148
  // src/setup/config-writer.ts
5103
- import { writeFileSync as writeFileSync2, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
5149
+ import { writeFileSync as writeFileSync2, existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
5104
5150
  import { join as join5 } from "path";
5105
5151
 
5106
5152
  // src/infra/install-detector.ts
@@ -5151,13 +5197,42 @@ function detectCloneInstall(projectRoot2) {
5151
5197
 
5152
5198
  // src/setup/config-writer.ts
5153
5199
  init_logger();
5154
- function writeMcpConfig(projectRoot2, _globalMode) {
5200
+ function writeMcpConfig(projectRoot2, tools, _globalMode) {
5201
+ const installInfo = detectInstallMethod(projectRoot2);
5202
+ const serverCommand = getServerCommand(installInfo);
5203
+ const detectedToolNames = tools.map((t) => t.tool);
5204
+ if (detectedToolNames.includes("cursor")) {
5205
+ writeCursorMcp(projectRoot2, serverCommand);
5206
+ }
5207
+ if (detectedToolNames.includes("claude")) {
5208
+ writeClaudeMcp(projectRoot2, serverCommand);
5209
+ }
5210
+ if (detectedToolNames.includes("qoder")) {
5211
+ writeQoderMcp(projectRoot2, serverCommand);
5212
+ }
5213
+ if (detectedToolNames.length > 0) {
5214
+ logger.info(`MCP configuration written for: ${detectedToolNames.join(", ")}`);
5215
+ }
5216
+ }
5217
+ function writeMcpForClient(projectRoot2, client) {
5155
5218
  const installInfo = detectInstallMethod(projectRoot2);
5156
5219
  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.");
5220
+ switch (client) {
5221
+ case "cursor":
5222
+ mkdirSync2(join5(projectRoot2, ".cursor"), { recursive: true });
5223
+ writeCursorMcp(projectRoot2, serverCommand);
5224
+ break;
5225
+ case "claude":
5226
+ if (!existsSync4(join5(projectRoot2, ".claude"))) {
5227
+ mkdirSync2(join5(projectRoot2, ".claude"), { recursive: true });
5228
+ }
5229
+ writeClaudeMcp(projectRoot2, serverCommand);
5230
+ break;
5231
+ case "qoder":
5232
+ mkdirSync2(join5(projectRoot2, ".qoder"), { recursive: true });
5233
+ writeQoderMcp(projectRoot2, serverCommand);
5234
+ break;
5235
+ }
5161
5236
  }
5162
5237
  function getServerCommand(installInfo) {
5163
5238
  if (installInfo.mode === "npm") {
@@ -5181,6 +5256,9 @@ function writeCursorMcp(projectRoot2, serverCmd) {
5181
5256
  logger.info(` Cursor: ${mcpPath}`);
5182
5257
  }
5183
5258
  function writeClaudeMcp(projectRoot2, serverCmd) {
5259
+ const hasClaudeDir = existsSync4(join5(projectRoot2, ".claude"));
5260
+ const hasClaudeMd = existsSync4(join5(projectRoot2, "CLAUDE.md"));
5261
+ if (!hasClaudeDir && !hasClaudeMd) return;
5184
5262
  const mcpPath = join5(projectRoot2, ".mcp.json");
5185
5263
  const existing = readExistingJson(mcpPath);
5186
5264
  if (!existing.mcpServers) existing.mcpServers = {};
@@ -5219,7 +5297,7 @@ function readExistingJson(filePath) {
5219
5297
 
5220
5298
  // src/setup/rule-writer.ts
5221
5299
  init_logger();
5222
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
5300
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
5223
5301
  import { join as join6 } from "path";
5224
5302
  function writeRuleFiles(projectRoot2, tools) {
5225
5303
  for (const tool of tools) {
@@ -5238,7 +5316,7 @@ function writeRuleFiles(projectRoot2, tools) {
5238
5316
  }
5239
5317
  function writeCursorRule(projectRoot2) {
5240
5318
  const rulesDir = join6(projectRoot2, ".cursor", "rules");
5241
- mkdirSync2(rulesDir, { recursive: true });
5319
+ mkdirSync3(rulesDir, { recursive: true });
5242
5320
  const content = `---
5243
5321
  description: SpecLore \u2014 AI coding constraints (setup placeholder)
5244
5322
  globs:
@@ -5260,7 +5338,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
5260
5338
  }
5261
5339
  function writeClaudeRule(projectRoot2) {
5262
5340
  const rulesDir = join6(projectRoot2, ".claude", "rules");
5263
- mkdirSync2(rulesDir, { recursive: true });
5341
+ mkdirSync3(rulesDir, { recursive: true });
5264
5342
  const content = `# SpecLore
5265
5343
 
5266
5344
  SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
@@ -5276,7 +5354,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
5276
5354
  }
5277
5355
  function writeQoderRule(projectRoot2) {
5278
5356
  const rulesDir = join6(projectRoot2, ".qoder", "rules");
5279
- mkdirSync2(rulesDir, { recursive: true });
5357
+ mkdirSync3(rulesDir, { recursive: true });
5280
5358
  const content = `# SpecLore
5281
5359
 
5282
5360
  SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
@@ -5300,13 +5378,20 @@ function registerSetupCommand(program) {
5300
5378
  logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5301
5379
  const tools = detectAITools(projectRoot2);
5302
5380
  if (tools.length === 0) {
5303
- logger.warn("No supported AI tools detected. Continuing anyway...");
5381
+ logger.warn("No supported AI tools detected.");
5382
+ logger.info("");
5383
+ logger.info(" To enable MCP integration, open your AI client in this project first,");
5384
+ logger.info(" then re-run setup. Or use manual configuration:");
5385
+ logger.info(" speclore mcp add cursor \u2014 configure for Cursor");
5386
+ logger.info(" speclore mcp add claude \u2014 configure for Claude Code");
5387
+ logger.info(" speclore mcp add qoder \u2014 configure for Qoder");
5388
+ logger.info("");
5304
5389
  } else {
5305
5390
  logger.info(`Detected AI tools: ${tools.map((t) => t.tool).join(", ")}`);
5306
5391
  }
5307
5392
  const specLoreDir = opts.global ? join7(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".speclore") : join7(projectRoot2, ".speclore");
5308
5393
  if (!existsSync5(specLoreDir)) {
5309
- mkdirSync3(specLoreDir, { recursive: true });
5394
+ mkdirSync4(specLoreDir, { recursive: true });
5310
5395
  }
5311
5396
  const configPath = join7(specLoreDir, "config.yaml");
5312
5397
  if (!existsSync5(configPath)) {
@@ -5314,12 +5399,23 @@ function registerSetupCommand(program) {
5314
5399
  writeFileSync4(configPath, generateDefaultConfigYaml(projectName), "utf-8");
5315
5400
  logger.info(`Created config: ${configPath}`);
5316
5401
  }
5317
- writeMcpConfig(projectRoot2, opts.global ?? false);
5402
+ writeMcpConfig(projectRoot2, tools, opts.global ?? false);
5318
5403
  writeRuleFiles(projectRoot2, tools);
5319
5404
  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");
5405
+ logger.info("Setup complete! Next steps:");
5406
+ logger.info("");
5407
+ logger.info(" Option A \u2014 CLI workflow (terminal users):");
5408
+ logger.info(' speclore spec "your requirement" \u2192 generate .feature');
5409
+ logger.info(" speclore code \u2192 generate constraints + tests");
5410
+ logger.info(" speclore verify \u2192 run acceptance");
5411
+ logger.info("");
5412
+ logger.info(" Option B \u2014 AI client workflow (recommended):");
5413
+ logger.info(" Open Cursor / Qoder / Claude Code and start chatting.");
5414
+ logger.info(" MCP is already configured \u2014 AI handles the full pipeline.");
5415
+ logger.info("");
5416
+ logger.info(" Optional \u2014 Pre-scan project context:");
5417
+ logger.info(" speclore init \u2192 scan modules/entities/APIs for better AI context");
5418
+ logger.info(" (Not required \u2014 context is auto-built on first spec/code call)");
5323
5419
  });
5324
5420
  }
5325
5421
 
@@ -5329,7 +5425,7 @@ init_logger();
5329
5425
  init_context_engine();
5330
5426
  import { join as join11 } from "path";
5331
5427
  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) => {
5428
+ 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
5429
  if (opts.verbose) logger.setLevel("debug");
5334
5430
  const projectRoot2 = process.cwd();
5335
5431
  logger.info("SpecLore Init");
@@ -5393,7 +5489,7 @@ init_test_scaffolder();
5393
5489
  init_context_engine();
5394
5490
  init_state_manager();
5395
5491
  import { join as join23 } from "path";
5396
- import { readFileSync as readFileSync13, existsSync as existsSync20 } from "fs";
5492
+ import { readFileSync as readFileSync14, existsSync as existsSync20 } from "fs";
5397
5493
  import { globSync as globSync4 } from "glob";
5398
5494
  function registerCodeCommand(program) {
5399
5495
  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 +5545,7 @@ function resolveFeatures(patterns, projectRoot2, config) {
5449
5545
  const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
5450
5546
  for (const filePath of matches) {
5451
5547
  if (existsSync20(filePath)) {
5452
- const content = readFileSync13(filePath, "utf-8");
5548
+ const content = readFileSync14(filePath, "utf-8");
5453
5549
  const featureMatch = content.match(/Feature:\s*(.+)/);
5454
5550
  files.push({
5455
5551
  path: filePath,
@@ -5473,7 +5569,7 @@ init_context_engine();
5473
5569
  init_analyzer();
5474
5570
  init_state_manager();
5475
5571
  import { join as join27 } from "path";
5476
- import { readFileSync as readFileSync17, existsSync as existsSync24 } from "fs";
5572
+ import { readFileSync as readFileSync18, existsSync as existsSync24 } from "fs";
5477
5573
  import { globSync as globSync6 } from "glob";
5478
5574
  function registerVerifyCommand(program) {
5479
5575
  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 +5656,7 @@ function resolveFeatures2(patterns, projectRoot2, config) {
5560
5656
  const matches = globSync6(pattern, { cwd: projectRoot2, absolute: true });
5561
5657
  for (const filePath of matches) {
5562
5658
  if (existsSync24(filePath)) {
5563
- const content = readFileSync17(filePath, "utf-8");
5659
+ const content = readFileSync18(filePath, "utf-8");
5564
5660
  const featureMatch = content.match(/Feature:\s*(.+)/);
5565
5661
  files.push({
5566
5662
  path: filePath,
@@ -5581,7 +5677,7 @@ init_logger();
5581
5677
 
5582
5678
  // src/setup/cleanup.ts
5583
5679
  init_logger();
5584
- import { rmSync as rmSync4, existsSync as existsSync25, readFileSync as readFileSync18, writeFileSync as writeFileSync14 } from "fs";
5680
+ import { rmSync as rmSync4, existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
5585
5681
  import { join as join28 } from "path";
5586
5682
  function runTeardown(projectRoot2, globalMode) {
5587
5683
  if (globalMode) {
@@ -5610,7 +5706,7 @@ function runTeardown(projectRoot2, globalMode) {
5610
5706
  function removeMcpEntry(mcpPath, serverName) {
5611
5707
  if (!existsSync25(mcpPath)) return;
5612
5708
  try {
5613
- const config = JSON.parse(readFileSync18(mcpPath, "utf-8"));
5709
+ const config = JSON.parse(readFileSync19(mcpPath, "utf-8"));
5614
5710
  if (config.mcpServers?.[serverName]) {
5615
5711
  delete config.mcpServers[serverName];
5616
5712
  writeFileSync14(mcpPath, JSON.stringify(config, null, 2), "utf-8");
@@ -5706,6 +5802,114 @@ function registerMigrateCommand(program) {
5706
5802
  });
5707
5803
  }
5708
5804
 
5805
+ // src/cli/commands/mcp-config.ts
5806
+ init_logger();
5807
+ import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync15, rmSync as rmSync5 } from "fs";
5808
+ import { join as join30 } from "path";
5809
+ var VALID_CLIENTS = ["cursor", "claude", "qoder"];
5810
+ function getMcpPath(projectRoot2, client) {
5811
+ switch (client) {
5812
+ case "cursor":
5813
+ return join30(projectRoot2, ".cursor", "mcp.json");
5814
+ case "claude":
5815
+ return join30(projectRoot2, ".mcp.json");
5816
+ case "qoder":
5817
+ return join30(projectRoot2, ".qoder", "mcp.json");
5818
+ }
5819
+ }
5820
+ function clientLabel(client) {
5821
+ switch (client) {
5822
+ case "cursor":
5823
+ return "Cursor";
5824
+ case "claude":
5825
+ return "Claude Code";
5826
+ case "qoder":
5827
+ return "Qoder";
5828
+ }
5829
+ }
5830
+ function registerMcpConfigCommands(program) {
5831
+ const mcpCmd = program.commands.find((c) => c.name() === "mcp");
5832
+ if (!mcpCmd) return;
5833
+ mcpCmd.command("add <client>").description("Manually write SpecLore MCP config for a specific AI client (cursor | claude | qoder)").action((clientArg) => {
5834
+ const projectRoot2 = process.cwd();
5835
+ const client = validateClient(clientArg);
5836
+ if (!client) return;
5837
+ logger.info(`Configuring MCP for ${clientLabel(client)}...`);
5838
+ writeMcpForClient(projectRoot2, client);
5839
+ logger.info(`\u2713 ${clientLabel(client)} MCP configured at ${getMcpPath(projectRoot2, client)}`);
5840
+ });
5841
+ mcpCmd.command("remove <client>").description("Remove SpecLore MCP config from a specific AI client (cursor | claude | qoder)").action((clientArg) => {
5842
+ const projectRoot2 = process.cwd();
5843
+ const client = validateClient(clientArg);
5844
+ if (!client) return;
5845
+ const mcpPath = getMcpPath(projectRoot2, client);
5846
+ if (!existsSync27(mcpPath)) {
5847
+ logger.info(`${clientLabel(client)}: no MCP config file found at ${mcpPath}`);
5848
+ return;
5849
+ }
5850
+ const removed = removeMcpServerEntry(mcpPath, "speclore");
5851
+ if (removed) {
5852
+ logger.info(`\u2713 Removed speclore from ${clientLabel(client)}: ${mcpPath}`);
5853
+ } else {
5854
+ logger.info(`${clientLabel(client)}: speclore entry not found in ${mcpPath}`);
5855
+ }
5856
+ });
5857
+ mcpCmd.command("list").description("Show current SpecLore MCP configuration status for all clients").action(() => {
5858
+ const projectRoot2 = process.cwd();
5859
+ logger.info("SpecLore MCP Configuration Status");
5860
+ 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");
5861
+ let anyFound = false;
5862
+ for (const client of VALID_CLIENTS) {
5863
+ const mcpPath = getMcpPath(projectRoot2, client);
5864
+ if (!existsSync27(mcpPath)) {
5865
+ logger.info(` \u25CB ${clientLabel(client)}: no config file (${mcpPath})`);
5866
+ continue;
5867
+ }
5868
+ try {
5869
+ const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
5870
+ if (config.mcpServers?.["speclore"]) {
5871
+ logger.info(` \u2713 ${clientLabel(client)}: configured \u2192 ${mcpPath}`);
5872
+ anyFound = true;
5873
+ } else {
5874
+ logger.info(` \u25CB ${clientLabel(client)}: config file exists but speclore not registered \u2192 ${mcpPath}`);
5875
+ }
5876
+ } catch {
5877
+ logger.info(` \u2717 ${clientLabel(client)}: failed to parse ${mcpPath}`);
5878
+ }
5879
+ }
5880
+ if (!anyFound) {
5881
+ logger.info("");
5882
+ logger.info(" No SpecLore MCP configurations found.");
5883
+ logger.info(" Run `speclore setup` or `speclore mcp add <client>` to configure.");
5884
+ }
5885
+ });
5886
+ }
5887
+ function validateClient(arg) {
5888
+ const normalised = arg.toLowerCase().trim();
5889
+ if (!VALID_CLIENTS.includes(normalised)) {
5890
+ logger.error(`Unknown client "${arg}". Supported: ${VALID_CLIENTS.join(", ")}`);
5891
+ return null;
5892
+ }
5893
+ return normalised;
5894
+ }
5895
+ function removeMcpServerEntry(mcpPath, serverName) {
5896
+ if (!existsSync27(mcpPath)) return false;
5897
+ try {
5898
+ const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
5899
+ if (!config.mcpServers?.[serverName]) return false;
5900
+ delete config.mcpServers[serverName];
5901
+ const remainingServers = Object.keys(config.mcpServers ?? {});
5902
+ if (remainingServers.length === 0) {
5903
+ rmSync5(mcpPath);
5904
+ } else {
5905
+ writeFileSync15(mcpPath, JSON.stringify(config, null, 2), "utf-8");
5906
+ }
5907
+ return true;
5908
+ } catch {
5909
+ return false;
5910
+ }
5911
+ }
5912
+
5709
5913
  // src/cli/index.ts
5710
5914
  init_version();
5711
5915
  function createProgram() {
@@ -5725,6 +5929,7 @@ function createProgram() {
5725
5929
  const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
5726
5930
  await startMcpServer2();
5727
5931
  });
5932
+ registerMcpConfigCommands(program);
5728
5933
  program.argument("[text...]", "Quick requirement text \u2192 .feature").action(async (text) => {
5729
5934
  if (text.length > 0) {
5730
5935
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));