speclore 0.1.8 → 0.1.10

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.
@@ -148,7 +148,7 @@ var init_cost_tracker = __esm({
148
148
  totalTokens: promptTokens + completionTokens,
149
149
  estimatedCostUsd: cost
150
150
  });
151
- logger.debug(`AI usage: ${model} \u2014 ${promptTokens}+${completionTokens} tokens, ~$${cost.toFixed(4)}`);
151
+ logger.debug(`AI usage: ${model} - ${promptTokens}+${completionTokens} tokens, ~$${cost.toFixed(4)}`);
152
152
  return cost;
153
153
  }
154
154
  /**
@@ -818,7 +818,7 @@ function acquireLock(specLoreDir) {
818
818
  try {
819
819
  writeFileSync(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
820
820
  } catch {
821
- logger.debug("Lock write failed \u2014 another process may have acquired it");
821
+ logger.debug("Lock write failed - another process may have acquired it");
822
822
  return false;
823
823
  }
824
824
  logger.debug(`Lock acquired by PID ${process.pid}`);
@@ -878,8 +878,8 @@ init_logger();
878
878
 
879
879
  // src/core/requirement-reader/index.ts
880
880
  init_logger();
881
- import { existsSync as existsSync6 } from "fs";
882
- import { extname as extname6 } from "path";
881
+ import { existsSync as existsSync7 } from "fs";
882
+ import { extname as extname7 } from "path";
883
883
 
884
884
  // src/plugins/registry.ts
885
885
  init_logger();
@@ -1026,8 +1026,8 @@ var PdfReader = class {
1026
1026
  } catch {
1027
1027
  throw new Error("pdfjs-dist package is required for PDF support. Install: npm i pdfjs-dist");
1028
1028
  }
1029
- const { readFileSync: readFileSync18 } = await import("fs");
1030
- const buffer = readFileSync18(source);
1029
+ const { readFileSync: readFileSync19 } = await import("fs");
1030
+ const buffer = readFileSync19(source);
1031
1031
  const uint8 = new Uint8Array(buffer);
1032
1032
  const doc = await pdfjsLib.getDocument({
1033
1033
  data: uint8,
@@ -1060,13 +1060,13 @@ var ImageReader = class {
1060
1060
  return /\.(png|jpe?g|webp)$/i.test(source);
1061
1061
  }
1062
1062
  async read(source) {
1063
- const { existsSync: existsSync22, readFileSync: readFileSync18 } = await import("fs");
1064
- if (!existsSync22(source)) {
1063
+ const { existsSync: existsSync23, readFileSync: readFileSync19 } = await import("fs");
1064
+ if (!existsSync23(source)) {
1065
1065
  throw new Error(`Image file not found: ${source}`);
1066
1066
  }
1067
1067
  const ext = source.split(".").pop()?.toLowerCase() ?? "png";
1068
1068
  const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
1069
- const buffer = readFileSync18(source);
1069
+ const buffer = readFileSync19(source);
1070
1070
  let text;
1071
1071
  try {
1072
1072
  const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
@@ -1258,26 +1258,45 @@ var ClaudeWriter = class {
1258
1258
  };
1259
1259
 
1260
1260
  // src/plugins/builtin/qoder-writer.ts
1261
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync5, rmSync as rmSync3 } from "fs";
1262
- import { join as join4 } from "path";
1261
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync6, rmSync as rmSync3 } from "fs";
1262
+ import { join as join5 } from "path";
1263
+
1264
+ // src/infra/path-utils.ts
1265
+ import { posix, sep, relative, resolve, isAbsolute, join as join4 } from "path";
1266
+ import { existsSync as existsSync5 } from "fs";
1267
+ import { glob, globSync } from "glob";
1268
+ function toPosixPath(p) {
1269
+ return p.split(/[/\\]/).join(posix.sep);
1270
+ }
1271
+ function resolveQoderDir(projectRoot2) {
1272
+ if (existsSync5(join4(projectRoot2, ".qoder-cn"))) return ".qoder-cn";
1273
+ return ".qoder";
1274
+ }
1275
+ function hasQoderDir(projectRoot2) {
1276
+ return existsSync5(join4(projectRoot2, ".qoder")) || existsSync5(join4(projectRoot2, ".qoder-cn"));
1277
+ }
1278
+
1279
+ // src/plugins/builtin/qoder-writer.ts
1263
1280
  var QoderWriter = class {
1264
1281
  toolName = "qoder";
1265
1282
  configFile = ".qoder/rules/speclore.md";
1266
1283
  projectRoot = "";
1267
1284
  detect(projectRoot2) {
1268
- return existsSync5(join4(projectRoot2, ".qoder"));
1285
+ return existsSync6(join5(projectRoot2, ".qoder")) || existsSync6(join5(projectRoot2, ".qoder-cn"));
1269
1286
  }
1270
1287
  write(constraints) {
1271
1288
  this.projectRoot = constraints.projectRoot;
1272
- const rulesDir = join4(constraints.projectRoot, ".qoder", "rules");
1289
+ const qoderDir = resolveQoderDir(constraints.projectRoot);
1290
+ const rulesDir = join5(constraints.projectRoot, qoderDir, "rules");
1273
1291
  mkdirSync3(rulesDir, { recursive: true });
1274
1292
  const content = this.buildMarkdown(constraints);
1275
- writeFileSync4(join4(rulesDir, "speclore.md"), content, "utf-8");
1293
+ writeFileSync4(join5(rulesDir, "speclore.md"), content, "utf-8");
1276
1294
  return Promise.resolve();
1277
1295
  }
1278
1296
  remove() {
1279
- const filePath = join4(this.projectRoot, ".qoder", "rules", "speclore.md");
1280
- if (existsSync5(filePath)) rmSync3(filePath);
1297
+ const qoderDir = resolveQoderDir(this.projectRoot);
1298
+ const filePath = join5(this.projectRoot, qoderDir, "rules", "speclore.md");
1299
+ if (existsSync6(filePath)) rmSync3(filePath);
1281
1300
  return Promise.resolve();
1282
1301
  }
1283
1302
  buildMarkdown(c) {
@@ -1806,9 +1825,30 @@ async function readImageFile(filePath, providerOverride) {
1806
1825
  };
1807
1826
  }
1808
1827
 
1828
+ // src/core/requirement-reader/text-reader.ts
1829
+ import { readFileSync as readFileSync6 } from "fs";
1830
+ import { basename as basename6, extname as extname6 } from "path";
1831
+ function readTextFile(filePath) {
1832
+ const content = readFileSync6(filePath, "utf-8");
1833
+ const id = deriveId2(filePath);
1834
+ const lines = content.split("\n").filter((line) => line.trim().length > 0);
1835
+ const title = lines[0]?.slice(0, 100) ?? basename6(filePath, extname6(filePath));
1836
+ return Promise.resolve({
1837
+ id,
1838
+ title,
1839
+ description: content,
1840
+ rawContent: content,
1841
+ confidence: 1
1842
+ });
1843
+ }
1844
+ function deriveId2(filePath) {
1845
+ const name = basename6(filePath, extname6(filePath));
1846
+ return name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
1847
+ }
1848
+
1809
1849
  // src/core/requirement-reader/url-reader.ts
1810
1850
  init_logger();
1811
- import { basename as basename6 } from "path";
1851
+ import { basename as basename7 } from "path";
1812
1852
  var FETCH_TIMEOUT_MS = 1e4;
1813
1853
  var MAX_CONTENT_LENGTH = 1e5;
1814
1854
  async function readUrl(url) {
@@ -1848,7 +1888,7 @@ async function readUrl(url) {
1848
1888
  content = content.slice(0, MAX_CONTENT_LENGTH) + "\n... (truncated)";
1849
1889
  }
1850
1890
  const urlPath = new URL(url).pathname;
1851
- const id = basename6(urlPath).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-") || "url-content";
1891
+ const id = basename7(urlPath).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-") || "url-content";
1852
1892
  return {
1853
1893
  id,
1854
1894
  title,
@@ -1896,30 +1936,43 @@ var UrlAuthError = class extends Error {
1896
1936
  };
1897
1937
 
1898
1938
  // src/core/requirement-reader/index.ts
1939
+ function sanitizeSource(source) {
1940
+ return source.replace(/[\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF\u00AD]/g, "");
1941
+ }
1899
1942
  async function readRequirement(source) {
1900
- const sourceType = classifySource(source);
1901
- logger.info(`Reading requirement from ${sourceType}: ${truncate(source, 80)}`);
1943
+ const cleaned = sanitizeSource(source);
1944
+ if (cleaned !== source) {
1945
+ logger.debug("Stripped invisible Unicode characters from source input.");
1946
+ }
1947
+ const sourceType = classifySource(cleaned);
1948
+ logger.info(`Reading requirement from ${sourceType}: ${truncate(cleaned, 120)}`);
1902
1949
  switch (sourceType) {
1903
1950
  case "file":
1904
- return readFromFile(source);
1951
+ return readFromFile(cleaned);
1905
1952
  case "url":
1906
- return readFromUrl(source);
1953
+ return readFromUrl(cleaned);
1907
1954
  case "text":
1908
- return readFromText(source);
1955
+ return readFromText(cleaned);
1909
1956
  }
1910
1957
  }
1911
1958
  function classifySource(source) {
1912
1959
  if (/^https?:\/\//i.test(source)) {
1913
1960
  return "url";
1914
1961
  }
1915
- const ext = extname6(source).toLowerCase();
1916
- const supportedExts = [".md", ".docx", ".xlsx", ".xls", ".pdf", ".png", ".jpg", ".jpeg", ".webp"];
1917
- if (supportedExts.includes(ext) && existsSync6(source)) {
1962
+ const ext = extname7(source).toLowerCase();
1963
+ const supportedExts = [".md", ".txt", ".docx", ".xlsx", ".xls", ".pdf", ".png", ".jpg", ".jpeg", ".webp"];
1964
+ if (supportedExts.includes(ext) && existsSync7(source)) {
1918
1965
  return "file";
1919
1966
  }
1920
- if (existsSync6(source) && (source.includes("/") || source.includes("\\"))) {
1967
+ if (existsSync7(source) && (source.includes("/") || source.includes("\\"))) {
1921
1968
  return "file";
1922
1969
  }
1970
+ if (supportedExts.includes(ext) && (source.includes("/") || source.includes("\\"))) {
1971
+ throw new Error(
1972
+ `File not found: ${source}
1973
+ The path looks like a file but does not exist. Check the path and try again. If copying from Windows, make sure no invisible characters are included.`
1974
+ );
1975
+ }
1923
1976
  return "text";
1924
1977
  }
1925
1978
  async function readFromFile(filePath) {
@@ -1933,10 +1986,12 @@ async function readFromFile(filePath) {
1933
1986
  }
1934
1987
  return results[0];
1935
1988
  }
1936
- const ext = extname6(filePath).toLowerCase();
1989
+ const ext = extname7(filePath).toLowerCase();
1937
1990
  switch (ext) {
1938
1991
  case ".md":
1939
1992
  return readMarkdownFile(filePath);
1993
+ case ".txt":
1994
+ return readTextFile(filePath);
1940
1995
  case ".docx":
1941
1996
  return readDocxFile(filePath);
1942
1997
  case ".xlsx":
@@ -1972,7 +2027,7 @@ function truncate(str, maxLen) {
1972
2027
 
1973
2028
  // src/core/feature-generator/generator.ts
1974
2029
  init_provider();
1975
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
2030
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync8 } from "fs";
1976
2031
  import { join as join6, dirname } from "path";
1977
2032
  import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
1978
2033
  import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
@@ -2184,15 +2239,6 @@ function buildPrompt(requirement, context, config) {
2184
2239
 
2185
2240
  // src/core/feature-generator/generator.ts
2186
2241
  init_logger();
2187
-
2188
- // src/infra/path-utils.ts
2189
- import { posix, sep, relative, resolve, isAbsolute, join as join5 } from "path";
2190
- import { glob, globSync } from "glob";
2191
- function toPosixPath(p) {
2192
- return p.split(/[/\\]/).join(posix.sep);
2193
- }
2194
-
2195
- // src/core/feature-generator/generator.ts
2196
2242
  var MAX_VALIDATION_RETRIES = 2;
2197
2243
  async function generateFeature(requirement, context, config, projectRoot2, providerOverride) {
2198
2244
  logger.info(`Generating feature for: ${requirement.title}`);
@@ -2258,7 +2304,7 @@ Please fix these issues and regenerate the complete Feature file.`;
2258
2304
  const outputDir = join6(projectRoot2, config.spec.outputDir);
2259
2305
  const moduleDir = inferModule(requirement, context);
2260
2306
  const filePath = join6(outputDir, moduleDir, `${requirement.id}.feature`);
2261
- if (!existsSync7(dirname(filePath))) {
2307
+ if (!existsSync8(dirname(filePath))) {
2262
2308
  mkdirSync4(dirname(filePath), { recursive: true });
2263
2309
  }
2264
2310
  writeFileSync5(filePath, featureContent, "utf-8");
@@ -2389,21 +2435,21 @@ function inferModule(requirement, context) {
2389
2435
 
2390
2436
  // src/core/constraint-coder/ai-tool-detector.ts
2391
2437
  init_logger();
2392
- import { existsSync as existsSync8 } from "fs";
2438
+ import { existsSync as existsSync9 } from "fs";
2393
2439
  import { join as join7 } from "path";
2394
2440
  function detectAITools(projectRoot2) {
2395
2441
  const tools = [];
2396
- if (existsSync8(join7(projectRoot2, ".cursor"))) {
2442
+ if (existsSync9(join7(projectRoot2, ".cursor"))) {
2397
2443
  tools.push("cursor");
2398
2444
  logger.debug("Detected Cursor (.cursor/)");
2399
2445
  }
2400
- if (existsSync8(join7(projectRoot2, ".claude")) || existsSync8(join7(projectRoot2, "CLAUDE.md"))) {
2446
+ if (existsSync9(join7(projectRoot2, ".claude")) || existsSync9(join7(projectRoot2, "CLAUDE.md"))) {
2401
2447
  tools.push("claude");
2402
2448
  logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
2403
2449
  }
2404
- if (existsSync8(join7(projectRoot2, ".qoder"))) {
2450
+ if (hasQoderDir(projectRoot2)) {
2405
2451
  tools.push("qoder");
2406
- logger.debug("Detected Qoder (.qoder/)");
2452
+ logger.debug("Detected Qoder (.qoder/ or .qoder-cn/)");
2407
2453
  }
2408
2454
  logger.debug(`Detected AI tools: ${tools.length > 0 ? tools.join(", ") : "none"}`);
2409
2455
  return tools;
@@ -2411,7 +2457,7 @@ function detectAITools(projectRoot2) {
2411
2457
 
2412
2458
  // src/core/constraint-coder/constraint-writer.ts
2413
2459
  init_logger();
2414
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9 } from "fs";
2460
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
2415
2461
  import { join as join8 } from "path";
2416
2462
  function writeConstraints(projectRoot2, tools, content) {
2417
2463
  const writtenFiles = [];
@@ -2485,7 +2531,7 @@ function buildConstraintText(content) {
2485
2531
  }
2486
2532
  function writeCursorRule(projectRoot2, text) {
2487
2533
  const dir = join8(projectRoot2, ".cursor", "rules");
2488
- if (!existsSync9(dir)) mkdirSync5(dir, { recursive: true });
2534
+ if (!existsSync10(dir)) mkdirSync5(dir, { recursive: true });
2489
2535
  const filePath = join8(dir, "speclore.mdc");
2490
2536
  const frontmatter = [
2491
2537
  "---",
@@ -2500,14 +2546,15 @@ function writeCursorRule(projectRoot2, text) {
2500
2546
  }
2501
2547
  function writeClaudeRule(projectRoot2, text) {
2502
2548
  const dir = join8(projectRoot2, ".claude", "rules");
2503
- if (!existsSync9(dir)) mkdirSync5(dir, { recursive: true });
2549
+ if (!existsSync10(dir)) mkdirSync5(dir, { recursive: true });
2504
2550
  const filePath = join8(dir, "speclore.md");
2505
2551
  writeFileSync6(filePath, text, "utf-8");
2506
2552
  return filePath;
2507
2553
  }
2508
2554
  function writeQoderRule(projectRoot2, text) {
2509
- const dir = join8(projectRoot2, ".qoder", "rules");
2510
- if (!existsSync9(dir)) mkdirSync5(dir, { recursive: true });
2555
+ const qoderDir = resolveQoderDir(projectRoot2);
2556
+ const dir = join8(projectRoot2, qoderDir, "rules");
2557
+ if (!existsSync10(dir)) mkdirSync5(dir, { recursive: true });
2511
2558
  const filePath = join8(dir, "speclore.md");
2512
2559
  writeFileSync6(filePath, text, "utf-8");
2513
2560
  return filePath;
@@ -2518,7 +2565,7 @@ init_logger();
2518
2565
  function readModuleConfig(config) {
2519
2566
  const boundaries = [];
2520
2567
  for (const [name, mod] of Object.entries(config.project.modules)) {
2521
- logger.debug(`Reading module config: ${name} \u2192 ${mod.path}`);
2568
+ logger.debug(`Reading module config: ${name} -> ${mod.path}`);
2522
2569
  boundaries.push({
2523
2570
  name,
2524
2571
  responsibility: mod.responsibility,
@@ -2579,7 +2626,7 @@ async function generateConstraints(projectRoot2, features, _context, config) {
2579
2626
  try {
2580
2627
  await writer.write(content);
2581
2628
  writtenFiles.push(writer.configFile);
2582
- logger.info(`Constraint written via plugin: ${writer.toolName} \u2192 ${writer.configFile}`);
2629
+ logger.info(`Constraint written via plugin: ${writer.toolName} -> ${writer.configFile}`);
2583
2630
  } catch (err) {
2584
2631
  logger.warn(`Writer plugin '${tool}' failed: ${err instanceof Error ? err.message : String(err)}`);
2585
2632
  }
@@ -2596,7 +2643,7 @@ import { execFileSync } from "child_process";
2596
2643
 
2597
2644
  // src/core/verifier/mapping-resolver.ts
2598
2645
  init_logger();
2599
- import { readFileSync as readFileSync6, existsSync as existsSync10, readdirSync } from "fs";
2646
+ import { readFileSync as readFileSync7, existsSync as existsSync11, readdirSync } from "fs";
2600
2647
  import { join as join9 } from "path";
2601
2648
  function resolveMappings(projectRoot2, features, testOutput) {
2602
2649
  const results = [];
@@ -2621,17 +2668,17 @@ function resolveMapping(projectRoot2, feature, scenario, _testOutput) {
2621
2668
  }
2622
2669
  function resolveFromMappingFile(projectRoot2, _feature, scenario) {
2623
2670
  const mappingsDir = join9(projectRoot2, ".speclore", "mappings");
2624
- if (!existsSync10(mappingsDir)) return null;
2671
+ if (!existsSync11(mappingsDir)) return null;
2625
2672
  try {
2626
2673
  const files = findMappingFiles(mappingsDir);
2627
2674
  for (const file of files) {
2628
2675
  try {
2629
- const content = readFileSync6(file, "utf-8");
2676
+ const content = readFileSync7(file, "utf-8");
2630
2677
  const mapping = JSON.parse(content);
2631
2678
  if (mapping.scenarios && scenario.name in mapping.scenarios) {
2632
2679
  const entry = mapping.scenarios[scenario.name];
2633
2680
  if (entry) {
2634
- logger.debug(`Mapping file hit: ${scenario.name} \u2192 ${entry.testMethod}`);
2681
+ logger.debug(`Mapping file hit: ${scenario.name} -> ${entry.testMethod}`);
2635
2682
  return {
2636
2683
  name: scenario.name,
2637
2684
  status: "passed",
@@ -2651,18 +2698,18 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
2651
2698
  }
2652
2699
  function resolveFromTag(projectRoot2, _feature, scenario) {
2653
2700
  const testsDir = join9(projectRoot2, "tests");
2654
- if (!existsSync10(testsDir)) return null;
2701
+ if (!existsSync11(testsDir)) return null;
2655
2702
  try {
2656
2703
  const files = findTestFiles(testsDir);
2657
2704
  for (const file of files) {
2658
2705
  try {
2659
- const content = readFileSync6(file, "utf-8");
2706
+ const content = readFileSync7(file, "utf-8");
2660
2707
  const tagRegex = /@speclore-scenario:\s*(.+)/g;
2661
2708
  let match;
2662
2709
  while ((match = tagRegex.exec(content)) !== null) {
2663
2710
  if (match[1].trim() === scenario.name) {
2664
2711
  const methodMatch = findTestMethodNearby(content, match.index);
2665
- logger.debug(`Tag hit: ${scenario.name} \u2192 ${methodMatch}`);
2712
+ logger.debug(`Tag hit: ${scenario.name} -> ${methodMatch}`);
2666
2713
  return {
2667
2714
  name: scenario.name,
2668
2715
  status: "passed",
@@ -2819,20 +2866,20 @@ function executeTestCommand(projectRoot2, config) {
2819
2866
 
2820
2867
  // src/core/verifier/report-generator.ts
2821
2868
  init_logger();
2822
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
2869
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
2823
2870
  import { join as join10, dirname as dirname2 } from "path";
2824
2871
  import { fileURLToPath } from "url";
2825
2872
 
2826
2873
  // src/core/context-engine/context-writer.ts
2827
2874
  init_logger();
2828
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync13, statSync as statSync3, mkdirSync as mkdirSync7 } from "fs";
2875
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync14, statSync as statSync3, mkdirSync as mkdirSync7 } from "fs";
2829
2876
  import { join as join13 } from "path";
2830
2877
  import { execFileSync as execFileSync2 } from "child_process";
2831
2878
 
2832
2879
  // src/core/context-engine/graph-builder.ts
2833
2880
  init_logger();
2834
- import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync8 } from "fs";
2835
- import { join as join11, basename as basename7, extname as extname7, relative as relative2 } from "path";
2881
+ import { existsSync as existsSync13, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync9 } from "fs";
2882
+ import { join as join11, basename as basename8, extname as extname8, relative as relative2 } from "path";
2836
2883
  function detectProjectInfo(projectRoot2) {
2837
2884
  const info = {
2838
2885
  language: "unknown",
@@ -2841,11 +2888,11 @@ function detectProjectInfo(projectRoot2) {
2841
2888
  testFramework: "unknown",
2842
2889
  directoryStructure: ""
2843
2890
  };
2844
- if (existsSync12(join11(projectRoot2, "package.json"))) {
2891
+ if (existsSync13(join11(projectRoot2, "package.json"))) {
2845
2892
  info.language = "typescript";
2846
2893
  info.buildTool = "npm";
2847
2894
  try {
2848
- const pkg = JSON.parse(readFileSync8(join11(projectRoot2, "package.json"), "utf-8"));
2895
+ const pkg = JSON.parse(readFileSync9(join11(projectRoot2, "package.json"), "utf-8"));
2849
2896
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2850
2897
  if ("next" in deps) info.framework = "next.js";
2851
2898
  else if ("@nestjs/core" in deps) info.framework = "nestjs";
@@ -2857,33 +2904,33 @@ function detectProjectInfo(projectRoot2) {
2857
2904
  } catch {
2858
2905
  logger.debug("Failed to parse package.json, using default project info");
2859
2906
  }
2860
- } else if (existsSync12(join11(projectRoot2, "pom.xml"))) {
2907
+ } else if (existsSync13(join11(projectRoot2, "pom.xml"))) {
2861
2908
  info.language = "java";
2862
2909
  info.buildTool = "maven";
2863
2910
  info.testFramework = "junit";
2864
2911
  try {
2865
- const pom = readFileSync8(join11(projectRoot2, "pom.xml"), "utf-8");
2912
+ const pom = readFileSync9(join11(projectRoot2, "pom.xml"), "utf-8");
2866
2913
  if (pom.includes("spring-boot")) info.framework = "spring-boot";
2867
2914
  } catch {
2868
2915
  }
2869
- } else if (existsSync12(join11(projectRoot2, "build.gradle")) || existsSync12(join11(projectRoot2, "build.gradle.kts"))) {
2916
+ } else if (existsSync13(join11(projectRoot2, "build.gradle")) || existsSync13(join11(projectRoot2, "build.gradle.kts"))) {
2870
2917
  info.language = "java";
2871
2918
  info.buildTool = "gradle";
2872
2919
  info.testFramework = "junit";
2873
- } else if (existsSync12(join11(projectRoot2, "go.mod"))) {
2920
+ } else if (existsSync13(join11(projectRoot2, "go.mod"))) {
2874
2921
  info.language = "go";
2875
2922
  info.buildTool = "go";
2876
2923
  info.testFramework = "go-test";
2877
- } else if (existsSync12(join11(projectRoot2, "Cargo.toml"))) {
2924
+ } else if (existsSync13(join11(projectRoot2, "Cargo.toml"))) {
2878
2925
  info.language = "rust";
2879
2926
  info.buildTool = "cargo";
2880
2927
  info.testFramework = "cargo-test";
2881
- } else if (existsSync12(join11(projectRoot2, "requirements.txt")) || existsSync12(join11(projectRoot2, "pyproject.toml"))) {
2928
+ } else if (existsSync13(join11(projectRoot2, "requirements.txt")) || existsSync13(join11(projectRoot2, "pyproject.toml"))) {
2882
2929
  info.language = "python";
2883
2930
  info.buildTool = "pip";
2884
2931
  info.testFramework = "pytest";
2885
2932
  try {
2886
- const reqs = existsSync12(join11(projectRoot2, "requirements.txt")) ? readFileSync8(join11(projectRoot2, "requirements.txt"), "utf-8") : readFileSync8(join11(projectRoot2, "pyproject.toml"), "utf-8");
2933
+ const reqs = existsSync13(join11(projectRoot2, "requirements.txt")) ? readFileSync9(join11(projectRoot2, "requirements.txt"), "utf-8") : readFileSync9(join11(projectRoot2, "pyproject.toml"), "utf-8");
2887
2934
  if (reqs.includes("django")) info.framework = "django";
2888
2935
  else if (reqs.includes("flask")) info.framework = "flask";
2889
2936
  else if (reqs.includes("fastapi")) info.framework = "fastapi";
@@ -2931,7 +2978,7 @@ function buildDependencyGraph(projectRoot2, modules) {
2931
2978
  }
2932
2979
  for (const mod of modules) {
2933
2980
  const modPath = join11(projectRoot2, mod.path);
2934
- if (!existsSync12(modPath)) continue;
2981
+ if (!existsSync13(modPath)) continue;
2935
2982
  try {
2936
2983
  const imports = scanImports(modPath);
2937
2984
  for (const otherMod of modules) {
@@ -2967,10 +3014,10 @@ function scanImports(dir) {
2967
3014
  walk(fullPath);
2968
3015
  continue;
2969
3016
  }
2970
- const ext = extname7(entry);
3017
+ const ext = extname8(entry);
2971
3018
  if (![".ts", ".tsx", ".js", ".jsx", ".java", ".py"].includes(ext)) continue;
2972
3019
  try {
2973
- const content = readFileSync8(fullPath, "utf-8");
3020
+ const content = readFileSync9(fullPath, "utf-8");
2974
3021
  const importRegex = /(?:import\s+.*?from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
2975
3022
  let match;
2976
3023
  while ((match = importRegex.exec(content)) !== null) {
@@ -3003,11 +3050,11 @@ function readDirRecursive(dir) {
3003
3050
  var ANALYZABLE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".java", ".py"]);
3004
3051
  function analyzeFileForEntities(filePath, modName, modPath, modRelativePath) {
3005
3052
  const entities = [];
3006
- const ext = extname7(filePath);
3007
- const name = basename7(filePath, ext);
3053
+ const ext = extname8(filePath);
3054
+ const name = basename8(filePath, ext);
3008
3055
  if (!ANALYZABLE_EXTENSIONS.has(ext)) return entities;
3009
3056
  try {
3010
- const content = readFileSync8(filePath, "utf-8");
3057
+ const content = readFileSync9(filePath, "utf-8");
3011
3058
  const relFile = toPosixPath(join11(modRelativePath, relative2(modPath, filePath)));
3012
3059
  if (/(?:Entity|Model|Domain|Schema)$/i.test(name)) {
3013
3060
  entities.push({ name, module: modName, file: relFile });
@@ -3056,11 +3103,11 @@ function analyzeFileForEntities(filePath, modName, modPath, modRelativePath) {
3056
3103
  }
3057
3104
  function analyzeFileForApis(filePath, modName, modPath, modRelativePath) {
3058
3105
  const apis = [];
3059
- const ext = extname7(filePath);
3060
- const name = basename7(filePath, ext);
3106
+ const ext = extname8(filePath);
3107
+ const name = basename8(filePath, ext);
3061
3108
  if (!ANALYZABLE_EXTENSIONS.has(ext)) return apis;
3062
3109
  try {
3063
- const content = readFileSync8(filePath, "utf-8");
3110
+ const content = readFileSync9(filePath, "utf-8");
3064
3111
  const relFile = toPosixPath(join11(modRelativePath, relative2(modPath, filePath)));
3065
3112
  if (/Controller|Resource|Handler|Route$/i.test(name)) {
3066
3113
  apis.push({
@@ -3134,7 +3181,7 @@ function extractEntities(projectRoot2, modules) {
3134
3181
  const seen = /* @__PURE__ */ new Set();
3135
3182
  for (const mod of modules) {
3136
3183
  const modPath = join11(projectRoot2, mod.path);
3137
- if (!existsSync12(modPath)) continue;
3184
+ if (!existsSync13(modPath)) continue;
3138
3185
  try {
3139
3186
  const files = readDirRecursive(modPath);
3140
3187
  for (const file of files) {
@@ -3157,7 +3204,7 @@ function extractApis(projectRoot2, modules) {
3157
3204
  const seen = /* @__PURE__ */ new Set();
3158
3205
  for (const mod of modules) {
3159
3206
  const modPath = join11(projectRoot2, mod.path);
3160
- if (!existsSync12(modPath)) continue;
3207
+ if (!existsSync13(modPath)) continue;
3161
3208
  try {
3162
3209
  const files = readDirRecursive(modPath);
3163
3210
  for (const file of files) {
@@ -3177,7 +3224,7 @@ function extractApis(projectRoot2, modules) {
3177
3224
  }
3178
3225
 
3179
3226
  // src/version.ts
3180
- import { readFileSync as readFileSync9 } from "fs";
3227
+ import { readFileSync as readFileSync10 } from "fs";
3181
3228
  import { fileURLToPath as fileURLToPath2 } from "url";
3182
3229
  import { join as join12, dirname as dirname3 } from "path";
3183
3230
  function readVersion() {
@@ -3185,7 +3232,7 @@ function readVersion() {
3185
3232
  for (let depth = 0; depth < 4; depth++) {
3186
3233
  try {
3187
3234
  const pkgPath = join12(currentDir, "package.json");
3188
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
3235
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
3189
3236
  if (typeof pkg.version === "string") return pkg.version;
3190
3237
  } catch {
3191
3238
  }
@@ -3241,7 +3288,7 @@ function buildContext(projectRoot2, config) {
3241
3288
  }
3242
3289
  function loadContext(specLoreDir) {
3243
3290
  const contextPath = join13(specLoreDir, CONTEXT_FILENAME);
3244
- if (!existsSync13(contextPath)) {
3291
+ if (!existsSync14(contextPath)) {
3245
3292
  logger.debug("No cached context.json found");
3246
3293
  return null;
3247
3294
  }
@@ -3256,7 +3303,7 @@ function loadContext(specLoreDir) {
3256
3303
  return null;
3257
3304
  }
3258
3305
  try {
3259
- const content = readFileSync10(contextPath, "utf-8");
3306
+ const content = readFileSync11(contextPath, "utf-8");
3260
3307
  const context = JSON.parse(content);
3261
3308
  logger.debug("Loaded cached context.json");
3262
3309
  return context;
@@ -3274,8 +3321,8 @@ function hasGitHeadChanged(specLoreDir) {
3274
3321
  stdio: ["pipe", "pipe", "pipe"]
3275
3322
  }).trim();
3276
3323
  const headFile = join13(specLoreDir, ".git-head");
3277
- if (!existsSync13(headFile)) return true;
3278
- const savedHead = readFileSync10(headFile, "utf-8").trim();
3324
+ if (!existsSync14(headFile)) return true;
3325
+ const savedHead = readFileSync11(headFile, "utf-8").trim();
3279
3326
  if (savedHead !== head) {
3280
3327
  writeFileSync8(headFile, head, "utf-8");
3281
3328
  return true;
@@ -3294,7 +3341,7 @@ function truncateTo(text, maxLines) {
3294
3341
 
3295
3342
  // src/core/analyzer/rdg-builder.ts
3296
3343
  init_logger();
3297
- import { readFileSync as readFileSync11, existsSync as existsSync14, readdirSync as readdirSync3 } from "fs";
3344
+ import { readFileSync as readFileSync12, existsSync as existsSync15, readdirSync as readdirSync3 } from "fs";
3298
3345
  import { join as join14 } from "path";
3299
3346
 
3300
3347
  // src/core/analyzer/cdg-builder.ts
@@ -3369,7 +3416,7 @@ function determineAffectedFeatures(affectedModules, projectRoot2, outputDir) {
3369
3416
  }
3370
3417
 
3371
3418
  // src/core/state-manager/index.ts
3372
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync15, mkdirSync as mkdirSync8 } from "fs";
3419
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync16, mkdirSync as mkdirSync8 } from "fs";
3373
3420
  import { join as join15 } from "path";
3374
3421
  import yaml from "js-yaml";
3375
3422
  import { globSync as globSync3 } from "glob";
@@ -3397,11 +3444,11 @@ var StateManager = class {
3397
3444
  }
3398
3445
  /** Load state from disk, or return default if not exists */
3399
3446
  load() {
3400
- if (!existsSync15(this.statePath)) {
3447
+ if (!existsSync16(this.statePath)) {
3401
3448
  return createDefaultState();
3402
3449
  }
3403
3450
  try {
3404
- const content = readFileSync12(this.statePath, "utf-8");
3451
+ const content = readFileSync13(this.statePath, "utf-8");
3405
3452
  const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
3406
3453
  if (parsed && typeof parsed === "object") {
3407
3454
  return parsed;
@@ -3413,14 +3460,14 @@ var StateManager = class {
3413
3460
  /** Persist state to disk */
3414
3461
  save(state) {
3415
3462
  const dir = join15(this.projectRoot, ".speclore");
3416
- if (!existsSync15(dir)) {
3463
+ if (!existsSync16(dir)) {
3417
3464
  mkdirSync8(dir, { recursive: true });
3418
3465
  }
3419
3466
  writeFileSync9(this.statePath, yaml.dump(state, { lineWidth: 120 }), "utf-8");
3420
3467
  }
3421
3468
  /** Ensure state file exists with default values */
3422
3469
  ensureInitialized() {
3423
- if (!existsSync15(this.statePath)) {
3470
+ if (!existsSync16(this.statePath)) {
3424
3471
  const state2 = createDefaultState();
3425
3472
  this.save(state2);
3426
3473
  return state2;
@@ -3563,19 +3610,19 @@ var StateManager = class {
3563
3610
  };
3564
3611
 
3565
3612
  // src/core/test-scaffolder/index.ts
3566
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
3567
- import { join as join17, relative as relative3, dirname as dirname4, basename as basename8 } from "path";
3613
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync10, existsSync as existsSync18, mkdirSync as mkdirSync9 } from "fs";
3614
+ import { join as join17, relative as relative3, dirname as dirname4, basename as basename9 } from "path";
3568
3615
 
3569
3616
  // src/core/test-scaffolder/framework-detector.ts
3570
- import { readFileSync as readFileSync13, existsSync as existsSync16 } from "fs";
3617
+ import { readFileSync as readFileSync14, existsSync as existsSync17 } from "fs";
3571
3618
  import { join as join16 } from "path";
3572
3619
  function detectTestFramework(projectRoot2) {
3573
3620
  const pkgPath = join16(projectRoot2, "package.json");
3574
- if (!existsSync16(pkgPath)) {
3621
+ if (!existsSync17(pkgPath)) {
3575
3622
  return "vitest";
3576
3623
  }
3577
3624
  try {
3578
- const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
3625
+ const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
3579
3626
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3580
3627
  if ("vitest" in deps) return "vitest";
3581
3628
  if ("jest" in deps || "@jest/core" in deps) return "jest";
@@ -3598,7 +3645,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
3598
3645
  }
3599
3646
  const absTestPath = join17(projectRoot2, testFilePath);
3600
3647
  const scenarios = feature.scenarios;
3601
- if (existsSync17(absTestPath)) {
3648
+ if (existsSync18(absTestPath)) {
3602
3649
  const appended = appendMissingScenarios(absTestPath, scenarios, framework);
3603
3650
  if (appended > 0) {
3604
3651
  results.push({ testFile: testFilePath, framework, scenarios: appended });
@@ -3607,7 +3654,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
3607
3654
  } else {
3608
3655
  const content = generateTestFileContent(feature.featureName, scenarios, framework);
3609
3656
  const dir = dirname4(absTestPath);
3610
- if (!existsSync17(dir)) {
3657
+ if (!existsSync18(dir)) {
3611
3658
  mkdirSync9(dir, { recursive: true });
3612
3659
  }
3613
3660
  writeFileSync10(absTestPath, content, "utf-8");
@@ -3626,7 +3673,7 @@ function resolveTestFilePath(projectRoot2, featurePath, config) {
3626
3673
  return resolveTestFromPattern(match, pattern);
3627
3674
  }
3628
3675
  }
3629
- const featureBase = basename8(featurePath, ".feature");
3676
+ const featureBase = basename9(featurePath, ".feature");
3630
3677
  const featureDir = dirname4(relative3(projectRoot2, featurePath));
3631
3678
  return join17(featureDir, `${featureBase}.test.ts`).replace(/\\/g, "/");
3632
3679
  }
@@ -3681,7 +3728,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
3681
3728
  return lines.join("\n");
3682
3729
  }
3683
3730
  function appendMissingScenarios(testFilePath, scenarios, _framework) {
3684
- const content = readFileSync14(testFilePath, "utf-8");
3731
+ const content = readFileSync15(testFilePath, "utf-8");
3685
3732
  const missing = [];
3686
3733
  for (const scenario of scenarios) {
3687
3734
  if (!content.includes(`Scenario: ${scenario.name}`)) {
@@ -3715,7 +3762,7 @@ function appendMissingScenarios(testFilePath, scenarios, _framework) {
3715
3762
  }
3716
3763
 
3717
3764
  // src/infra/config.ts
3718
- import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
3765
+ import { readFileSync as readFileSync16, existsSync as existsSync19 } from "fs";
3719
3766
  import { join as join18 } from "path";
3720
3767
  import { homedir } from "os";
3721
3768
  import yaml2 from "js-yaml";
@@ -3774,11 +3821,11 @@ function loadProjectConfig(projectRoot2) {
3774
3821
  return readYamlIfExists(projectPath);
3775
3822
  }
3776
3823
  function readYamlIfExists(filePath) {
3777
- if (!existsSync18(filePath)) {
3824
+ if (!existsSync19(filePath)) {
3778
3825
  return null;
3779
3826
  }
3780
3827
  try {
3781
- const content = readFileSync15(filePath, "utf-8");
3828
+ const content = readFileSync16(filePath, "utf-8");
3782
3829
  const parsed = yaml2.load(content, { schema: yaml2.JSON_SCHEMA });
3783
3830
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3784
3831
  return parsed;
@@ -3902,7 +3949,7 @@ verify:
3902
3949
  // src/mcp/tools.ts
3903
3950
  init_logger();
3904
3951
  import { join as join19 } from "path";
3905
- import { readFileSync as readFileSync16, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
3952
+ import { readFileSync as readFileSync17, existsSync as existsSync20, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
3906
3953
  import { globSync as globSync4 } from "glob";
3907
3954
  import { toJSONSchema } from "zod";
3908
3955
 
@@ -3925,10 +3972,23 @@ var statusInputSchema = z.object({
3925
3972
  });
3926
3973
 
3927
3974
  // src/mcp/tools.ts
3928
- var SPEC_TOOL_DESC = "Convert a requirement (text, file path, or URL) into structured BDD .feature files with scenario-level acceptance criteria.";
3929
- var CODE_TOOL_DESC = "Generate AI coding constraint files for Cursor / Claude Code / Qoder based on .feature specs and project context. Requires .feature files to exist.";
3930
- var VERIFY_TOOL_DESC = "Run tests and map results back to .feature scenarios, producing an acceptance report. Requires test scaffolding generated by speclore.code.";
3931
- var STATUS_TOOL_DESC = "Get current project workflow status, feature states, and recommended next steps. Call this first to understand where the project stands.";
3975
+ var SPEC_TOOL_DESC = [
3976
+ "Convert a requirement (text, file path, or URL) into structured BDD .feature files with scenario-level acceptance criteria.",
3977
+ "IMPORTANT: After calling this tool, ALWAYS show the user: (1) the created file path, (2) the list of generated scenarios, (3) the recommended next step."
3978
+ ].join(" ");
3979
+ var CODE_TOOL_DESC = [
3980
+ "Generate AI coding constraint files for Cursor / Claude Code / Qoder based on .feature specs and project context. Requires .feature files to exist.",
3981
+ "IMPORTANT: After calling this tool, ALWAYS show the user: (1) which constraint files were written, (2) test scaffold files generated, (3) the recommended next step."
3982
+ ].join(" ");
3983
+ var VERIFY_TOOL_DESC = [
3984
+ "Run tests and map results back to .feature scenarios, producing an acceptance report. Requires test scaffolding generated by speclore.code.",
3985
+ "IMPORTANT: After calling this tool, ALWAYS show the user: (1) pass/fail summary, (2) any failed scenario details, (3) the recommended next step."
3986
+ ].join(" ");
3987
+ var STATUS_TOOL_DESC = [
3988
+ "Get current project workflow status, feature states, and recommended next steps.",
3989
+ "BEST PRACTICE: Call this tool FIRST when starting a new conversation about the project to understand where it stands.",
3990
+ "ALWAYS show the user the full status including: project state, feature list, and recommended actions."
3991
+ ].join(" ");
3932
3992
  var specJsonSchema = toJSONSchema(specInputSchema, { target: "draft-07" });
3933
3993
  var codeJsonSchema = toJSONSchema(codeInputSchema, { target: "draft-07" });
3934
3994
  var verifyJsonSchema = toJSONSchema(verifyInputSchema, { target: "draft-07" });
@@ -3936,10 +3996,11 @@ async function executeSpecTool(args, projectRoot2) {
3936
3996
  if (!args.source || args.source.length > 5e4) {
3937
3997
  throw new Error("source must be between 1 and 50000 characters");
3938
3998
  }
3999
+ const sanitizedSource = sanitizeSource(args.source);
3939
4000
  ensureProjectReady(projectRoot2);
3940
4001
  const config = loadConfig(projectRoot2);
3941
- logger.info(`Reading requirement from: ${args.source.slice(0, 80)}...`);
3942
- const req = await readRequirement(args.source);
4002
+ logger.info(`Reading requirement from: ${sanitizedSource.slice(0, 120)}`);
4003
+ const req = await readRequirement(sanitizedSource);
3943
4004
  const specLoreDir = join19(projectRoot2, ".speclore");
3944
4005
  const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
3945
4006
  const feature = await generateFeature(req, context, config, projectRoot2);
@@ -4090,10 +4151,10 @@ function ensureProjectReady(projectRoot2) {
4090
4151
  const specLoreDir = join19(projectRoot2, ".speclore");
4091
4152
  const configPath = join19(specLoreDir, "config.yaml");
4092
4153
  let configCreated = false;
4093
- if (!existsSync19(specLoreDir)) {
4154
+ if (!existsSync20(specLoreDir)) {
4094
4155
  mkdirSync10(specLoreDir, { recursive: true });
4095
4156
  }
4096
- if (!existsSync19(configPath)) {
4157
+ if (!existsSync20(configPath)) {
4097
4158
  const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
4098
4159
  writeFileSync11(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4099
4160
  configCreated = true;
@@ -4145,8 +4206,8 @@ function resolveFeatureFiles(patterns, projectRoot2, config) {
4145
4206
  for (const pattern of searchPatterns) {
4146
4207
  const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
4147
4208
  for (const filePath of matches) {
4148
- if (existsSync19(filePath)) {
4149
- const content = readFileSync16(filePath, "utf-8");
4209
+ if (existsSync20(filePath)) {
4210
+ const content = readFileSync17(filePath, "utf-8");
4150
4211
  files.push(parseFeatureFile(filePath, content));
4151
4212
  }
4152
4213
  }
@@ -4230,29 +4291,30 @@ function buildCodingGuidance(_features, context, config) {
4230
4291
  }
4231
4292
 
4232
4293
  // src/setup/detector.ts
4233
- import { existsSync as existsSync20 } from "fs";
4294
+ import { existsSync as existsSync21 } from "fs";
4234
4295
  import { join as join20 } from "path";
4235
4296
  function detectAITools2(projectRoot2) {
4236
4297
  const tools = [];
4237
4298
  const cursorFiles = [".cursor/mcp.json", ".cursor/rules"];
4238
- const cursorConfigs = cursorFiles.filter((f) => existsSync20(join20(projectRoot2, f)));
4299
+ const cursorConfigs = cursorFiles.filter((f) => existsSync21(join20(projectRoot2, f)));
4239
4300
  tools.push({
4240
4301
  tool: "cursor",
4241
- detected: existsSync20(join20(projectRoot2, ".cursor")),
4302
+ detected: existsSync21(join20(projectRoot2, ".cursor")),
4242
4303
  configFiles: cursorConfigs
4243
4304
  });
4244
4305
  const claudeFiles = [".claude/", "CLAUDE.md"];
4245
- const claudeConfigs = claudeFiles.filter((f) => existsSync20(join20(projectRoot2, f)));
4306
+ const claudeConfigs = claudeFiles.filter((f) => existsSync21(join20(projectRoot2, f)));
4246
4307
  tools.push({
4247
4308
  tool: "claude",
4248
4309
  detected: claudeConfigs.length > 0,
4249
4310
  configFiles: claudeConfigs
4250
4311
  });
4251
- const qoderFiles = [".qoder/mcp.json", ".qoder/rules"];
4252
- const qoderConfigs = qoderFiles.filter((f) => existsSync20(join20(projectRoot2, f)));
4312
+ const qoderDir = hasQoderDir(projectRoot2) ? existsSync21(join20(projectRoot2, ".qoder-cn")) ? ".qoder-cn" : ".qoder" : ".qoder";
4313
+ const qoderFiles = [`${qoderDir}/mcp.json`, `${qoderDir}/rules`];
4314
+ const qoderConfigs = qoderFiles.filter((f) => existsSync21(join20(projectRoot2, f)));
4253
4315
  tools.push({
4254
4316
  tool: "qoder",
4255
- detected: existsSync20(join20(projectRoot2, ".qoder")),
4317
+ detected: hasQoderDir(projectRoot2),
4256
4318
  configFiles: qoderConfigs
4257
4319
  });
4258
4320
  return tools.filter((t) => t.detected);
@@ -4260,7 +4322,7 @@ function detectAITools2(projectRoot2) {
4260
4322
 
4261
4323
  // src/mcp/status.ts
4262
4324
  init_logger();
4263
- import { readFileSync as readFileSync17, existsSync as existsSync21, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
4325
+ import { readFileSync as readFileSync18, existsSync as existsSync22, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
4264
4326
  import { join as join21 } from "path";
4265
4327
  import { globSync as globSync5 } from "glob";
4266
4328
  function executeStatusTool(args, projectRoot2) {
@@ -4277,9 +4339,9 @@ function executeStatusTool(args, projectRoot2) {
4277
4339
  continue;
4278
4340
  }
4279
4341
  let scenarioCount = 0;
4280
- if (existsSync21(path)) {
4342
+ if (existsSync22(path)) {
4281
4343
  try {
4282
- const content = readFileSync17(path, "utf-8");
4344
+ const content = readFileSync18(path, "utf-8");
4283
4345
  const matches = content.match(/Scenario(?: Outline)?:/g);
4284
4346
  scenarioCount = matches?.length ?? 0;
4285
4347
  } catch {
@@ -4302,7 +4364,7 @@ function executeStatusTool(args, projectRoot2) {
4302
4364
  if (args.feature && !filePath.includes(args.feature)) continue;
4303
4365
  let scenarioCount = 0;
4304
4366
  try {
4305
- const content = readFileSync17(filePath, "utf-8");
4367
+ const content = readFileSync18(filePath, "utf-8");
4306
4368
  const matches = content.match(/Scenario(?: Outline)?:/g);
4307
4369
  scenarioCount = matches?.length ?? 0;
4308
4370
  } catch {
@@ -4362,10 +4424,10 @@ function ensureProjectReadyForStatus(projectRoot2) {
4362
4424
  const specLoreDir = join21(projectRoot2, ".speclore");
4363
4425
  const configPath = join21(specLoreDir, "config.yaml");
4364
4426
  let configCreated = false;
4365
- if (!existsSync21(specLoreDir)) {
4427
+ if (!existsSync22(specLoreDir)) {
4366
4428
  mkdirSync11(specLoreDir, { recursive: true });
4367
4429
  }
4368
- if (!existsSync21(configPath)) {
4430
+ if (!existsSync22(configPath)) {
4369
4431
  const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
4370
4432
  writeFileSync12(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4371
4433
  configCreated = true;
@@ -4402,7 +4464,11 @@ server.registerTool(
4402
4464
  { source: args.source, module: args.module },
4403
4465
  getProjectRoot()
4404
4466
  );
4405
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
4467
+ const summary = buildSpecSummary(result);
4468
+ return { content: [{ type: "text", text: `${summary}
4469
+
4470
+ ---
4471
+ ${JSON.stringify(result, null, 2)}` }] };
4406
4472
  }
4407
4473
  );
4408
4474
  server.registerTool(
@@ -4419,7 +4485,11 @@ server.registerTool(
4419
4485
  { features: args.features, tools: args.tools },
4420
4486
  getProjectRoot()
4421
4487
  );
4422
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
4488
+ const summary = buildCodeSummary(result);
4489
+ return { content: [{ type: "text", text: `${summary}
4490
+
4491
+ ---
4492
+ ${JSON.stringify(result, null, 2)}` }] };
4423
4493
  }
4424
4494
  );
4425
4495
  server.registerTool(
@@ -4433,7 +4503,11 @@ server.registerTool(
4433
4503
  { features: args.features, impact: args.impact },
4434
4504
  getProjectRoot()
4435
4505
  );
4436
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
4506
+ const summary = buildVerifySummary(result);
4507
+ return { content: [{ type: "text", text: `${summary}
4508
+
4509
+ ---
4510
+ ${JSON.stringify(result, null, 2)}` }] };
4437
4511
  }
4438
4512
  );
4439
4513
  server.registerTool(
@@ -4447,7 +4521,11 @@ server.registerTool(
4447
4521
  { feature: args.feature },
4448
4522
  getProjectRoot()
4449
4523
  );
4450
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
4524
+ const summary = buildStatusSummary(result);
4525
+ return { content: [{ type: "text", text: `${summary}
4526
+
4527
+ ---
4528
+ ${JSON.stringify(result, null, 2)}` }] };
4451
4529
  }
4452
4530
  );
4453
4531
  var projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
@@ -4461,10 +4539,10 @@ async function startMcpServer() {
4461
4539
  mkdirSync12(specLoreDir, { recursive: true });
4462
4540
  const locked = acquireLock(specLoreDir);
4463
4541
  if (!locked) {
4464
- logger.warn("Could not acquire lock \u2014 another SpecLore instance may be running.");
4542
+ logger.warn("Could not acquire lock - another SpecLore instance may be running.");
4465
4543
  }
4466
4544
  } catch {
4467
- logger.warn("Could not acquire lock \u2014 .speclore/ directory may not exist.");
4545
+ logger.warn("Could not acquire lock - .speclore/ directory may not exist.");
4468
4546
  }
4469
4547
  const transport = new StdioServerTransport();
4470
4548
  await server.connect(transport);
@@ -4484,6 +4562,58 @@ async function startMcpServer() {
4484
4562
  process.exit(0);
4485
4563
  });
4486
4564
  }
4565
+ function buildSpecSummary(result) {
4566
+ const lines = [];
4567
+ lines.push("[SpecLore] Feature file generated successfully.");
4568
+ lines.push(` File: ${result.createdFiles.join(", ")}`);
4569
+ lines.push(` Scenarios: ${result.scenarios.length}`);
4570
+ for (const sc of result.scenarios) {
4571
+ lines.push(` - ${sc.name}`);
4572
+ }
4573
+ lines.push(` Next step: ${result.nextSteps}`);
4574
+ return lines.join("\n");
4575
+ }
4576
+ function buildCodeSummary(result) {
4577
+ const lines = [];
4578
+ lines.push("[SpecLore] Constraints and test scaffolding generated.");
4579
+ lines.push(` Constraint files: ${result.writtenFiles.length > 0 ? result.writtenFiles.join(", ") : "none"}`);
4580
+ lines.push(` Test scaffold files: ${result.scaffoldFiles.length > 0 ? result.scaffoldFiles.map((s) => s.testFile).join(", ") : "none"}`);
4581
+ lines.push(` Next step: ${result.workflow.nextStep}`);
4582
+ return lines.join("\n");
4583
+ }
4584
+ function buildVerifySummary(result) {
4585
+ const lines = [];
4586
+ const icon = result.failed === 0 ? "[PASS]" : "[FAIL]";
4587
+ lines.push(`[SpecLore] Verification ${icon} ${result.summary}`);
4588
+ if (result.failed > 0) {
4589
+ lines.push(` Failed: ${result.failed} scenario(s)`);
4590
+ for (const d of result.failedDetails) {
4591
+ lines.push(` - ${d.feature} > ${d.scenario}: ${d.error}`);
4592
+ }
4593
+ }
4594
+ if (result.unmapped > 0) {
4595
+ lines.push(` Unmapped: ${result.unmapped} scenario(s) without test mapping`);
4596
+ }
4597
+ lines.push(` Next step: ${result.workflow.nextStep}`);
4598
+ return lines.join("\n");
4599
+ }
4600
+ function buildStatusSummary(result) {
4601
+ const lines = [];
4602
+ lines.push(`[SpecLore] Project status: ${result.project.initialized ? "initialized" : "not initialized"}`);
4603
+ lines.push(` AI tools detected: ${result.project.aiToolsDetected.length > 0 ? result.project.aiToolsDetected.join(", ") : "none"}`);
4604
+ lines.push(` Features: ${result.summary.total} total`);
4605
+ if (result.summary.specified > 0) lines.push(` - ${result.summary.specified} specified`);
4606
+ if (result.summary.constrained > 0) lines.push(` - ${result.summary.constrained} constrained`);
4607
+ if (result.summary.coding > 0) lines.push(` - ${result.summary.coding} coding`);
4608
+ if (result.summary.verified > 0) lines.push(` - ${result.summary.verified} verified`);
4609
+ if (result.recommendedActions.length > 0) {
4610
+ lines.push(" Recommended actions:");
4611
+ for (const action of result.recommendedActions) {
4612
+ lines.push(` -> ${action}`);
4613
+ }
4614
+ }
4615
+ return lines.join("\n");
4616
+ }
4487
4617
 
4488
4618
  // src/bin/mcp.ts
4489
4619
  startMcpServer().catch((err) => {