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