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.
package/dist/cli/index.js CHANGED
@@ -308,30 +308,51 @@ var init_config2 = __esm({
308
308
  }
309
309
  });
310
310
 
311
- // src/setup/detector.ts
311
+ // src/infra/path-utils.ts
312
+ import { posix, sep, relative, resolve, isAbsolute, join as join2 } from "path";
312
313
  import { existsSync as existsSync2 } from "fs";
313
- import { join as join2 } from "path";
314
+ import { glob, globSync } from "glob";
315
+ function toPosixPath(p) {
316
+ return p.split(/[/\\]/).join(posix.sep);
317
+ }
318
+ function resolveQoderDir(projectRoot2) {
319
+ if (existsSync2(join2(projectRoot2, ".qoder-cn"))) return ".qoder-cn";
320
+ return ".qoder";
321
+ }
322
+ function hasQoderDir(projectRoot2) {
323
+ return existsSync2(join2(projectRoot2, ".qoder")) || existsSync2(join2(projectRoot2, ".qoder-cn"));
324
+ }
325
+ var init_path_utils = __esm({
326
+ "src/infra/path-utils.ts"() {
327
+ "use strict";
328
+ }
329
+ });
330
+
331
+ // src/setup/detector.ts
332
+ import { existsSync as existsSync3 } from "fs";
333
+ import { join as join3 } from "path";
314
334
  function detectAITools(projectRoot2) {
315
335
  const tools = [];
316
336
  const cursorFiles = [".cursor/mcp.json", ".cursor/rules"];
317
- const cursorConfigs = cursorFiles.filter((f) => existsSync2(join2(projectRoot2, f)));
337
+ const cursorConfigs = cursorFiles.filter((f) => existsSync3(join3(projectRoot2, f)));
318
338
  tools.push({
319
339
  tool: "cursor",
320
- detected: existsSync2(join2(projectRoot2, ".cursor")),
340
+ detected: existsSync3(join3(projectRoot2, ".cursor")),
321
341
  configFiles: cursorConfigs
322
342
  });
323
343
  const claudeFiles = [".claude/", "CLAUDE.md"];
324
- const claudeConfigs = claudeFiles.filter((f) => existsSync2(join2(projectRoot2, f)));
344
+ const claudeConfigs = claudeFiles.filter((f) => existsSync3(join3(projectRoot2, f)));
325
345
  tools.push({
326
346
  tool: "claude",
327
347
  detected: claudeConfigs.length > 0,
328
348
  configFiles: claudeConfigs
329
349
  });
330
- const qoderFiles = [".qoder/mcp.json", ".qoder/rules"];
331
- const qoderConfigs = qoderFiles.filter((f) => existsSync2(join2(projectRoot2, f)));
350
+ const qoderDir = hasQoderDir(projectRoot2) ? existsSync3(join3(projectRoot2, ".qoder-cn")) ? ".qoder-cn" : ".qoder" : ".qoder";
351
+ const qoderFiles = [`${qoderDir}/mcp.json`, `${qoderDir}/rules`];
352
+ const qoderConfigs = qoderFiles.filter((f) => existsSync3(join3(projectRoot2, f)));
332
353
  tools.push({
333
354
  tool: "qoder",
334
- detected: existsSync2(join2(projectRoot2, ".qoder")),
355
+ detected: hasQoderDir(projectRoot2),
335
356
  configFiles: qoderConfigs
336
357
  });
337
358
  return tools.filter((t) => t.detected);
@@ -339,23 +360,24 @@ function detectAITools(projectRoot2) {
339
360
  var init_detector = __esm({
340
361
  "src/setup/detector.ts"() {
341
362
  "use strict";
363
+ init_path_utils();
342
364
  }
343
365
  });
344
366
 
345
367
  // src/version.ts
346
368
  import { readFileSync as readFileSync2 } from "fs";
347
369
  import { fileURLToPath } from "url";
348
- import { join as join3, dirname } from "path";
370
+ import { join as join4, dirname } from "path";
349
371
  function readVersion() {
350
372
  let currentDir = dirname(fileURLToPath(import.meta.url));
351
373
  for (let depth = 0; depth < 4; depth++) {
352
374
  try {
353
- const pkgPath = join3(currentDir, "package.json");
375
+ const pkgPath = join4(currentDir, "package.json");
354
376
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
355
377
  if (typeof pkg.version === "string") return pkg.version;
356
378
  } catch {
357
379
  }
358
- currentDir = join3(currentDir, "..");
380
+ currentDir = join4(currentDir, "..");
359
381
  }
360
382
  return "0.0.0";
361
383
  }
@@ -371,7 +393,7 @@ var init_version = __esm({
371
393
  function readModuleConfig(config) {
372
394
  const boundaries = [];
373
395
  for (const [name, mod] of Object.entries(config.project.modules)) {
374
- logger.debug(`Reading module config: ${name} \u2192 ${mod.path}`);
396
+ logger.debug(`Reading module config: ${name} -> ${mod.path}`);
375
397
  boundaries.push({
376
398
  name,
377
399
  responsibility: mod.responsibility,
@@ -389,20 +411,8 @@ var init_config_reader = __esm({
389
411
  }
390
412
  });
391
413
 
392
- // src/infra/path-utils.ts
393
- import { posix, sep, relative, resolve, isAbsolute, join as join8 } from "path";
394
- import { glob, globSync } from "glob";
395
- function toPosixPath(p) {
396
- return p.split(/[/\\]/).join(posix.sep);
397
- }
398
- var init_path_utils = __esm({
399
- "src/infra/path-utils.ts"() {
400
- "use strict";
401
- }
402
- });
403
-
404
414
  // src/core/context-engine/graph-builder.ts
405
- import { existsSync as existsSync6, readdirSync, statSync, readFileSync as readFileSync5 } from "fs";
415
+ import { existsSync as existsSync7, readdirSync, statSync, readFileSync as readFileSync5 } from "fs";
406
416
  import { join as join9, basename, extname, relative as relative2 } from "path";
407
417
  function detectProjectInfo(projectRoot2) {
408
418
  const info = {
@@ -412,7 +422,7 @@ function detectProjectInfo(projectRoot2) {
412
422
  testFramework: "unknown",
413
423
  directoryStructure: ""
414
424
  };
415
- if (existsSync6(join9(projectRoot2, "package.json"))) {
425
+ if (existsSync7(join9(projectRoot2, "package.json"))) {
416
426
  info.language = "typescript";
417
427
  info.buildTool = "npm";
418
428
  try {
@@ -428,7 +438,7 @@ function detectProjectInfo(projectRoot2) {
428
438
  } catch {
429
439
  logger.debug("Failed to parse package.json, using default project info");
430
440
  }
431
- } else if (existsSync6(join9(projectRoot2, "pom.xml"))) {
441
+ } else if (existsSync7(join9(projectRoot2, "pom.xml"))) {
432
442
  info.language = "java";
433
443
  info.buildTool = "maven";
434
444
  info.testFramework = "junit";
@@ -437,24 +447,24 @@ function detectProjectInfo(projectRoot2) {
437
447
  if (pom.includes("spring-boot")) info.framework = "spring-boot";
438
448
  } catch {
439
449
  }
440
- } else if (existsSync6(join9(projectRoot2, "build.gradle")) || existsSync6(join9(projectRoot2, "build.gradle.kts"))) {
450
+ } else if (existsSync7(join9(projectRoot2, "build.gradle")) || existsSync7(join9(projectRoot2, "build.gradle.kts"))) {
441
451
  info.language = "java";
442
452
  info.buildTool = "gradle";
443
453
  info.testFramework = "junit";
444
- } else if (existsSync6(join9(projectRoot2, "go.mod"))) {
454
+ } else if (existsSync7(join9(projectRoot2, "go.mod"))) {
445
455
  info.language = "go";
446
456
  info.buildTool = "go";
447
457
  info.testFramework = "go-test";
448
- } else if (existsSync6(join9(projectRoot2, "Cargo.toml"))) {
458
+ } else if (existsSync7(join9(projectRoot2, "Cargo.toml"))) {
449
459
  info.language = "rust";
450
460
  info.buildTool = "cargo";
451
461
  info.testFramework = "cargo-test";
452
- } else if (existsSync6(join9(projectRoot2, "requirements.txt")) || existsSync6(join9(projectRoot2, "pyproject.toml"))) {
462
+ } else if (existsSync7(join9(projectRoot2, "requirements.txt")) || existsSync7(join9(projectRoot2, "pyproject.toml"))) {
453
463
  info.language = "python";
454
464
  info.buildTool = "pip";
455
465
  info.testFramework = "pytest";
456
466
  try {
457
- const reqs = existsSync6(join9(projectRoot2, "requirements.txt")) ? readFileSync5(join9(projectRoot2, "requirements.txt"), "utf-8") : readFileSync5(join9(projectRoot2, "pyproject.toml"), "utf-8");
467
+ const reqs = existsSync7(join9(projectRoot2, "requirements.txt")) ? readFileSync5(join9(projectRoot2, "requirements.txt"), "utf-8") : readFileSync5(join9(projectRoot2, "pyproject.toml"), "utf-8");
458
468
  if (reqs.includes("django")) info.framework = "django";
459
469
  else if (reqs.includes("flask")) info.framework = "flask";
460
470
  else if (reqs.includes("fastapi")) info.framework = "fastapi";
@@ -502,7 +512,7 @@ function buildDependencyGraph(projectRoot2, modules) {
502
512
  }
503
513
  for (const mod of modules) {
504
514
  const modPath = join9(projectRoot2, mod.path);
505
- if (!existsSync6(modPath)) continue;
515
+ if (!existsSync7(modPath)) continue;
506
516
  try {
507
517
  const imports = scanImports(modPath);
508
518
  for (const otherMod of modules) {
@@ -704,7 +714,7 @@ function extractEntities(projectRoot2, modules) {
704
714
  const seen = /* @__PURE__ */ new Set();
705
715
  for (const mod of modules) {
706
716
  const modPath = join9(projectRoot2, mod.path);
707
- if (!existsSync6(modPath)) continue;
717
+ if (!existsSync7(modPath)) continue;
708
718
  try {
709
719
  const files = readDirRecursive(modPath);
710
720
  for (const file of files) {
@@ -727,7 +737,7 @@ function extractApis(projectRoot2, modules) {
727
737
  const seen = /* @__PURE__ */ new Set();
728
738
  for (const mod of modules) {
729
739
  const modPath = join9(projectRoot2, mod.path);
730
- if (!existsSync6(modPath)) continue;
740
+ if (!existsSync7(modPath)) continue;
731
741
  try {
732
742
  const files = readDirRecursive(modPath);
733
743
  for (const file of files) {
@@ -756,7 +766,7 @@ var init_graph_builder = __esm({
756
766
  });
757
767
 
758
768
  // src/core/context-engine/context-writer.ts
759
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, statSync as statSync2, mkdirSync as mkdirSync5 } from "fs";
769
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync8, statSync as statSync2, mkdirSync as mkdirSync5 } from "fs";
760
770
  import { join as join10 } from "path";
761
771
  import { execFileSync } from "child_process";
762
772
  function buildContext(projectRoot2, config) {
@@ -801,7 +811,7 @@ function buildContext(projectRoot2, config) {
801
811
  }
802
812
  function loadContext(specLoreDir) {
803
813
  const contextPath = join10(specLoreDir, CONTEXT_FILENAME);
804
- if (!existsSync7(contextPath)) {
814
+ if (!existsSync8(contextPath)) {
805
815
  logger.debug("No cached context.json found");
806
816
  return null;
807
817
  }
@@ -826,7 +836,7 @@ function loadContext(specLoreDir) {
826
836
  }
827
837
  }
828
838
  function writeContextFile(specLoreDir, context) {
829
- if (!existsSync7(specLoreDir)) {
839
+ if (!existsSync8(specLoreDir)) {
830
840
  mkdirSync5(specLoreDir, { recursive: true });
831
841
  }
832
842
  const contextPath = join10(specLoreDir, CONTEXT_FILENAME);
@@ -849,7 +859,7 @@ function hasGitHeadChanged(specLoreDir) {
849
859
  stdio: ["pipe", "pipe", "pipe"]
850
860
  }).trim();
851
861
  const headFile = join10(specLoreDir, ".git-head");
852
- if (!existsSync7(headFile)) return true;
862
+ if (!existsSync8(headFile)) return true;
853
863
  const savedHead = readFileSync6(headFile, "utf-8").trim();
854
864
  if (savedHead !== head) {
855
865
  writeFileSync5(headFile, head, "utf-8");
@@ -951,7 +961,7 @@ var init_cost_tracker = __esm({
951
961
  totalTokens: promptTokens + completionTokens,
952
962
  estimatedCostUsd: cost
953
963
  });
954
- logger.debug(`AI usage: ${model} \u2014 ${promptTokens}+${completionTokens} tokens, ~$${cost.toFixed(4)}`);
964
+ logger.debug(`AI usage: ${model} - ${promptTokens}+${completionTokens} tokens, ~$${cost.toFixed(4)}`);
955
965
  return cost;
956
966
  }
957
967
  /**
@@ -1018,7 +1028,7 @@ var init_cost_tracker = __esm({
1018
1028
  });
1019
1029
 
1020
1030
  // src/core/state-manager/index.ts
1021
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
1031
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
1022
1032
  import { join as join12 } from "path";
1023
1033
  import yaml2 from "js-yaml";
1024
1034
  import { globSync as globSync2 } from "glob";
@@ -1050,7 +1060,7 @@ var init_state_manager = __esm({
1050
1060
  }
1051
1061
  /** Load state from disk, or return default if not exists */
1052
1062
  load() {
1053
- if (!existsSync8(this.statePath)) {
1063
+ if (!existsSync9(this.statePath)) {
1054
1064
  return createDefaultState();
1055
1065
  }
1056
1066
  try {
@@ -1066,14 +1076,14 @@ var init_state_manager = __esm({
1066
1076
  /** Persist state to disk */
1067
1077
  save(state) {
1068
1078
  const dir = join12(this.projectRoot, ".speclore");
1069
- if (!existsSync8(dir)) {
1079
+ if (!existsSync9(dir)) {
1070
1080
  mkdirSync6(dir, { recursive: true });
1071
1081
  }
1072
1082
  writeFileSync6(this.statePath, yaml2.dump(state, { lineWidth: 120 }), "utf-8");
1073
1083
  }
1074
1084
  /** Ensure state file exists with default values */
1075
1085
  ensureInitialized() {
1076
- if (!existsSync8(this.statePath)) {
1086
+ if (!existsSync9(this.statePath)) {
1077
1087
  const state2 = createDefaultState();
1078
1088
  this.save(state2);
1079
1089
  return state2;
@@ -1222,7 +1232,7 @@ var status_exports = {};
1222
1232
  __export(status_exports, {
1223
1233
  registerStatusCommand: () => registerStatusCommand
1224
1234
  });
1225
- import { existsSync as existsSync9 } from "fs";
1235
+ import { existsSync as existsSync10 } from "fs";
1226
1236
  import { join as join13 } from "path";
1227
1237
  import { globSync as globSync3 } from "glob";
1228
1238
  function registerStatusCommand(program) {
@@ -1235,7 +1245,7 @@ function registerStatusCommand(program) {
1235
1245
  console.log("");
1236
1246
  const config = loadConfig(projectRoot2);
1237
1247
  const configPath = join13(projectRoot2, ".speclore", "config.yaml");
1238
- if (existsSync9(configPath)) {
1248
+ if (existsSync10(configPath)) {
1239
1249
  console.log(`\u2713 Config: ${configPath}`);
1240
1250
  console.log(` Project: ${config.project.name || "(unnamed)"}`);
1241
1251
  console.log(` Profile: ${config.project.profile}`);
@@ -1244,13 +1254,13 @@ function registerStatusCommand(program) {
1244
1254
  console.log("\u2717 Config not found. Run `speclore setup` first.");
1245
1255
  }
1246
1256
  const contextPath = join13(projectRoot2, ".speclore", "context.json");
1247
- if (existsSync9(contextPath)) {
1257
+ if (existsSync10(contextPath)) {
1248
1258
  console.log(`\u2713 Context: ${contextPath}`);
1249
1259
  } else {
1250
1260
  console.log("\u2717 Context not found. Run `speclore init` first.");
1251
1261
  }
1252
1262
  const specsDir = join13(projectRoot2, config.spec.outputDir);
1253
- if (existsSync9(specsDir)) {
1263
+ if (existsSync10(specsDir)) {
1254
1264
  const features = globSync3("**/*.feature", { cwd: specsDir });
1255
1265
  console.log(`\u2713 Features: ${features.length} file(s) in ${config.spec.outputDir}/`);
1256
1266
  } else {
@@ -1331,7 +1341,7 @@ var init_status = __esm({
1331
1341
  });
1332
1342
 
1333
1343
  // src/plugins/builtin/md-reader.ts
1334
- import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
1344
+ import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
1335
1345
  var MarkdownReader;
1336
1346
  var init_md_reader = __esm({
1337
1347
  "src/plugins/builtin/md-reader.ts"() {
@@ -1343,7 +1353,7 @@ var init_md_reader = __esm({
1343
1353
  return /\.md$/i.test(source) || /\.markdown$/i.test(source);
1344
1354
  }
1345
1355
  read(source) {
1346
- if (!existsSync10(source)) {
1356
+ if (!existsSync11(source)) {
1347
1357
  throw new Error(`File not found: ${source}`);
1348
1358
  }
1349
1359
  const content = readFileSync8(source, "utf-8");
@@ -1498,8 +1508,8 @@ var init_pdf_reader = __esm({
1498
1508
  } catch {
1499
1509
  throw new Error("pdfjs-dist package is required for PDF support. Install: npm i pdfjs-dist");
1500
1510
  }
1501
- const { readFileSync: readFileSync24 } = await import("fs");
1502
- const buffer = readFileSync24(source);
1511
+ const { readFileSync: readFileSync25 } = await import("fs");
1512
+ const buffer = readFileSync25(source);
1503
1513
  const uint8 = new Uint8Array(buffer);
1504
1514
  const doc = await pdfjsLib.getDocument({
1505
1515
  data: uint8,
@@ -2114,13 +2124,13 @@ var init_image_reader = __esm({
2114
2124
  return /\.(png|jpe?g|webp)$/i.test(source);
2115
2125
  }
2116
2126
  async read(source) {
2117
- const { existsSync: existsSync31, readFileSync: readFileSync24 } = await import("fs");
2118
- if (!existsSync31(source)) {
2127
+ const { existsSync: existsSync32, readFileSync: readFileSync25 } = await import("fs");
2128
+ if (!existsSync32(source)) {
2119
2129
  throw new Error(`Image file not found: ${source}`);
2120
2130
  }
2121
2131
  const ext = source.split(".").pop()?.toLowerCase() ?? "png";
2122
2132
  const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
2123
- const buffer = readFileSync24(source);
2133
+ const buffer = readFileSync25(source);
2124
2134
  let text;
2125
2135
  try {
2126
2136
  const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
@@ -2149,7 +2159,7 @@ var init_image_reader = __esm({
2149
2159
  });
2150
2160
 
2151
2161
  // src/plugins/builtin/cursor-writer.ts
2152
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync11, rmSync } from "fs";
2162
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync12, rmSync } from "fs";
2153
2163
  import { join as join14 } from "path";
2154
2164
  var CursorWriter;
2155
2165
  var init_cursor_writer = __esm({
@@ -2160,7 +2170,7 @@ var init_cursor_writer = __esm({
2160
2170
  configFile = ".cursor/rules/speclore.mdc";
2161
2171
  projectRoot = "";
2162
2172
  detect(projectRoot2) {
2163
- return existsSync11(join14(projectRoot2, ".cursor"));
2173
+ return existsSync12(join14(projectRoot2, ".cursor"));
2164
2174
  }
2165
2175
  write(constraints) {
2166
2176
  this.projectRoot = constraints.projectRoot;
@@ -2180,7 +2190,7 @@ var init_cursor_writer = __esm({
2180
2190
  }
2181
2191
  remove() {
2182
2192
  const filePath = join14(this.projectRoot, ".cursor", "rules", "speclore.mdc");
2183
- if (existsSync11(filePath)) rmSync(filePath);
2193
+ if (existsSync12(filePath)) rmSync(filePath);
2184
2194
  return Promise.resolve();
2185
2195
  }
2186
2196
  buildMarkdown(c) {
@@ -2244,7 +2254,7 @@ var init_cursor_writer = __esm({
2244
2254
  });
2245
2255
 
2246
2256
  // src/plugins/builtin/claude-writer.ts
2247
- import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync12, rmSync as rmSync2 } from "fs";
2257
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync13, rmSync as rmSync2 } from "fs";
2248
2258
  import { join as join15 } from "path";
2249
2259
  var ClaudeWriter;
2250
2260
  var init_claude_writer = __esm({
@@ -2255,7 +2265,7 @@ var init_claude_writer = __esm({
2255
2265
  configFile = ".claude/rules/speclore.md";
2256
2266
  projectRoot = "";
2257
2267
  detect(projectRoot2) {
2258
- return existsSync12(join15(projectRoot2, ".claude")) || existsSync12(join15(projectRoot2, ".mcp.json"));
2268
+ return existsSync13(join15(projectRoot2, ".claude")) || existsSync13(join15(projectRoot2, ".mcp.json"));
2259
2269
  }
2260
2270
  write(constraints) {
2261
2271
  this.projectRoot = constraints.projectRoot;
@@ -2267,7 +2277,7 @@ var init_claude_writer = __esm({
2267
2277
  }
2268
2278
  remove() {
2269
2279
  const filePath = join15(this.projectRoot, ".claude", "rules", "speclore.md");
2270
- if (existsSync12(filePath)) rmSync2(filePath);
2280
+ if (existsSync13(filePath)) rmSync2(filePath);
2271
2281
  return Promise.resolve();
2272
2282
  }
2273
2283
  buildMarkdown(c) {
@@ -2326,30 +2336,33 @@ var init_claude_writer = __esm({
2326
2336
  });
2327
2337
 
2328
2338
  // src/plugins/builtin/qoder-writer.ts
2329
- import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync13, rmSync as rmSync3 } from "fs";
2339
+ import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync14, rmSync as rmSync3 } from "fs";
2330
2340
  import { join as join16 } from "path";
2331
2341
  var QoderWriter;
2332
2342
  var init_qoder_writer = __esm({
2333
2343
  "src/plugins/builtin/qoder-writer.ts"() {
2334
2344
  "use strict";
2345
+ init_path_utils();
2335
2346
  QoderWriter = class {
2336
2347
  toolName = "qoder";
2337
2348
  configFile = ".qoder/rules/speclore.md";
2338
2349
  projectRoot = "";
2339
2350
  detect(projectRoot2) {
2340
- return existsSync13(join16(projectRoot2, ".qoder"));
2351
+ return existsSync14(join16(projectRoot2, ".qoder")) || existsSync14(join16(projectRoot2, ".qoder-cn"));
2341
2352
  }
2342
2353
  write(constraints) {
2343
2354
  this.projectRoot = constraints.projectRoot;
2344
- const rulesDir = join16(constraints.projectRoot, ".qoder", "rules");
2355
+ const qoderDir = resolveQoderDir(constraints.projectRoot);
2356
+ const rulesDir = join16(constraints.projectRoot, qoderDir, "rules");
2345
2357
  mkdirSync9(rulesDir, { recursive: true });
2346
2358
  const content = this.buildMarkdown(constraints);
2347
2359
  writeFileSync9(join16(rulesDir, "speclore.md"), content, "utf-8");
2348
2360
  return Promise.resolve();
2349
2361
  }
2350
2362
  remove() {
2351
- const filePath = join16(this.projectRoot, ".qoder", "rules", "speclore.md");
2352
- if (existsSync13(filePath)) rmSync3(filePath);
2363
+ const qoderDir = resolveQoderDir(this.projectRoot);
2364
+ const filePath = join16(this.projectRoot, qoderDir, "rules", "speclore.md");
2365
+ if (existsSync14(filePath)) rmSync3(filePath);
2353
2366
  return Promise.resolve();
2354
2367
  }
2355
2368
  buildMarkdown(c) {
@@ -2943,8 +2956,34 @@ var init_image_reader2 = __esm({
2943
2956
  }
2944
2957
  });
2945
2958
 
2959
+ // src/core/requirement-reader/text-reader.ts
2960
+ import { readFileSync as readFileSync12 } from "fs";
2961
+ import { basename as basename7, extname as extname7 } from "path";
2962
+ function readTextFile(filePath) {
2963
+ const content = readFileSync12(filePath, "utf-8");
2964
+ const id = deriveId2(filePath);
2965
+ const lines = content.split("\n").filter((line) => line.trim().length > 0);
2966
+ const title = lines[0]?.slice(0, 100) ?? basename7(filePath, extname7(filePath));
2967
+ return Promise.resolve({
2968
+ id,
2969
+ title,
2970
+ description: content,
2971
+ rawContent: content,
2972
+ confidence: 1
2973
+ });
2974
+ }
2975
+ function deriveId2(filePath) {
2976
+ const name = basename7(filePath, extname7(filePath));
2977
+ return name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
2978
+ }
2979
+ var init_text_reader = __esm({
2980
+ "src/core/requirement-reader/text-reader.ts"() {
2981
+ "use strict";
2982
+ }
2983
+ });
2984
+
2946
2985
  // src/core/requirement-reader/url-reader.ts
2947
- import { basename as basename7 } from "path";
2986
+ import { basename as basename8 } from "path";
2948
2987
  async function readUrl(url) {
2949
2988
  validateUrl(url);
2950
2989
  logger.info(`Fetching URL: ${url}`);
@@ -2982,7 +3021,7 @@ async function readUrl(url) {
2982
3021
  content = content.slice(0, MAX_CONTENT_LENGTH) + "\n... (truncated)";
2983
3022
  }
2984
3023
  const urlPath = new URL(url).pathname;
2985
- const id = basename7(urlPath).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-") || "url-content";
3024
+ const id = basename8(urlPath).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-") || "url-content";
2986
3025
  return {
2987
3026
  id,
2988
3027
  title,
@@ -3041,34 +3080,48 @@ var init_url_reader = __esm({
3041
3080
  // src/core/requirement-reader/index.ts
3042
3081
  var requirement_reader_exports = {};
3043
3082
  __export(requirement_reader_exports, {
3044
- readRequirement: () => readRequirement
3083
+ readRequirement: () => readRequirement,
3084
+ sanitizeSource: () => sanitizeSource
3045
3085
  });
3046
- import { existsSync as existsSync14 } from "fs";
3047
- import { extname as extname7 } from "path";
3086
+ import { existsSync as existsSync15 } from "fs";
3087
+ import { extname as extname8 } from "path";
3088
+ function sanitizeSource(source) {
3089
+ return source.replace(/[\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF\u00AD]/g, "");
3090
+ }
3048
3091
  async function readRequirement(source) {
3049
- const sourceType = classifySource(source);
3050
- logger.info(`Reading requirement from ${sourceType}: ${truncate(source, 80)}`);
3092
+ const cleaned = sanitizeSource(source);
3093
+ if (cleaned !== source) {
3094
+ logger.debug("Stripped invisible Unicode characters from source input.");
3095
+ }
3096
+ const sourceType = classifySource(cleaned);
3097
+ logger.info(`Reading requirement from ${sourceType}: ${truncate(cleaned, 120)}`);
3051
3098
  switch (sourceType) {
3052
3099
  case "file":
3053
- return readFromFile(source);
3100
+ return readFromFile(cleaned);
3054
3101
  case "url":
3055
- return readFromUrl(source);
3102
+ return readFromUrl(cleaned);
3056
3103
  case "text":
3057
- return readFromText(source);
3104
+ return readFromText(cleaned);
3058
3105
  }
3059
3106
  }
3060
3107
  function classifySource(source) {
3061
3108
  if (/^https?:\/\//i.test(source)) {
3062
3109
  return "url";
3063
3110
  }
3064
- const ext = extname7(source).toLowerCase();
3065
- const supportedExts = [".md", ".docx", ".xlsx", ".xls", ".pdf", ".png", ".jpg", ".jpeg", ".webp"];
3066
- if (supportedExts.includes(ext) && existsSync14(source)) {
3111
+ const ext = extname8(source).toLowerCase();
3112
+ const supportedExts = [".md", ".txt", ".docx", ".xlsx", ".xls", ".pdf", ".png", ".jpg", ".jpeg", ".webp"];
3113
+ if (supportedExts.includes(ext) && existsSync15(source)) {
3067
3114
  return "file";
3068
3115
  }
3069
- if (existsSync14(source) && (source.includes("/") || source.includes("\\"))) {
3116
+ if (existsSync15(source) && (source.includes("/") || source.includes("\\"))) {
3070
3117
  return "file";
3071
3118
  }
3119
+ if (supportedExts.includes(ext) && (source.includes("/") || source.includes("\\"))) {
3120
+ throw new Error(
3121
+ `File not found: ${source}
3122
+ 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.`
3123
+ );
3124
+ }
3072
3125
  return "text";
3073
3126
  }
3074
3127
  async function readFromFile(filePath) {
@@ -3082,10 +3135,12 @@ async function readFromFile(filePath) {
3082
3135
  }
3083
3136
  return results[0];
3084
3137
  }
3085
- const ext = extname7(filePath).toLowerCase();
3138
+ const ext = extname8(filePath).toLowerCase();
3086
3139
  switch (ext) {
3087
3140
  case ".md":
3088
3141
  return readMarkdownFile(filePath);
3142
+ case ".txt":
3143
+ return readTextFile(filePath);
3089
3144
  case ".docx":
3090
3145
  return readDocxFile(filePath);
3091
3146
  case ".xlsx":
@@ -3128,6 +3183,7 @@ var init_requirement_reader = __esm({
3128
3183
  init_xlsx_reader2();
3129
3184
  init_pdf_reader2();
3130
3185
  init_image_reader2();
3186
+ init_text_reader();
3131
3187
  init_url_reader();
3132
3188
  }
3133
3189
  });
@@ -3358,7 +3414,7 @@ var init_prompt_builder = __esm({
3358
3414
  });
3359
3415
 
3360
3416
  // src/core/feature-generator/generator.ts
3361
- import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync15 } from "fs";
3417
+ import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync16 } from "fs";
3362
3418
  import { join as join17, dirname as dirname2 } from "path";
3363
3419
  import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
3364
3420
  import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
@@ -3426,7 +3482,7 @@ Please fix these issues and regenerate the complete Feature file.`;
3426
3482
  const outputDir = join17(projectRoot2, config.spec.outputDir);
3427
3483
  const moduleDir = inferModule(requirement, context);
3428
3484
  const filePath = join17(outputDir, moduleDir, `${requirement.id}.feature`);
3429
- if (!existsSync15(dirname2(filePath))) {
3485
+ if (!existsSync16(dirname2(filePath))) {
3430
3486
  mkdirSync10(dirname2(filePath), { recursive: true });
3431
3487
  }
3432
3488
  writeFileSync10(filePath, featureContent, "utf-8");
@@ -3585,21 +3641,21 @@ var init_feature_generator = __esm({
3585
3641
  });
3586
3642
 
3587
3643
  // src/core/constraint-coder/ai-tool-detector.ts
3588
- import { existsSync as existsSync16 } from "fs";
3644
+ import { existsSync as existsSync17 } from "fs";
3589
3645
  import { join as join19 } from "path";
3590
3646
  function detectAITools2(projectRoot2) {
3591
3647
  const tools = [];
3592
- if (existsSync16(join19(projectRoot2, ".cursor"))) {
3648
+ if (existsSync17(join19(projectRoot2, ".cursor"))) {
3593
3649
  tools.push("cursor");
3594
3650
  logger.debug("Detected Cursor (.cursor/)");
3595
3651
  }
3596
- if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))) {
3652
+ if (existsSync17(join19(projectRoot2, ".claude")) || existsSync17(join19(projectRoot2, "CLAUDE.md"))) {
3597
3653
  tools.push("claude");
3598
3654
  logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
3599
3655
  }
3600
- if (existsSync16(join19(projectRoot2, ".qoder"))) {
3656
+ if (hasQoderDir(projectRoot2)) {
3601
3657
  tools.push("qoder");
3602
- logger.debug("Detected Qoder (.qoder/)");
3658
+ logger.debug("Detected Qoder (.qoder/ or .qoder-cn/)");
3603
3659
  }
3604
3660
  logger.debug(`Detected AI tools: ${tools.length > 0 ? tools.join(", ") : "none"}`);
3605
3661
  return tools;
@@ -3608,11 +3664,12 @@ var init_ai_tool_detector = __esm({
3608
3664
  "src/core/constraint-coder/ai-tool-detector.ts"() {
3609
3665
  "use strict";
3610
3666
  init_logger();
3667
+ init_path_utils();
3611
3668
  }
3612
3669
  });
3613
3670
 
3614
3671
  // src/core/constraint-coder/constraint-writer.ts
3615
- import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync17 } from "fs";
3672
+ import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync18 } from "fs";
3616
3673
  import { join as join20 } from "path";
3617
3674
  function writeConstraints(projectRoot2, tools, content) {
3618
3675
  const writtenFiles = [];
@@ -3686,7 +3743,7 @@ function buildConstraintText(content) {
3686
3743
  }
3687
3744
  function writeCursorRule2(projectRoot2, text) {
3688
3745
  const dir = join20(projectRoot2, ".cursor", "rules");
3689
- if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
3746
+ if (!existsSync18(dir)) mkdirSync11(dir, { recursive: true });
3690
3747
  const filePath = join20(dir, "speclore.mdc");
3691
3748
  const frontmatter = [
3692
3749
  "---",
@@ -3701,14 +3758,15 @@ function writeCursorRule2(projectRoot2, text) {
3701
3758
  }
3702
3759
  function writeClaudeRule2(projectRoot2, text) {
3703
3760
  const dir = join20(projectRoot2, ".claude", "rules");
3704
- if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
3761
+ if (!existsSync18(dir)) mkdirSync11(dir, { recursive: true });
3705
3762
  const filePath = join20(dir, "speclore.md");
3706
3763
  writeFileSync11(filePath, text, "utf-8");
3707
3764
  return filePath;
3708
3765
  }
3709
3766
  function writeQoderRule2(projectRoot2, text) {
3710
- const dir = join20(projectRoot2, ".qoder", "rules");
3711
- if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
3767
+ const qoderDir = resolveQoderDir(projectRoot2);
3768
+ const dir = join20(projectRoot2, qoderDir, "rules");
3769
+ if (!existsSync18(dir)) mkdirSync11(dir, { recursive: true });
3712
3770
  const filePath = join20(dir, "speclore.md");
3713
3771
  writeFileSync11(filePath, text, "utf-8");
3714
3772
  return filePath;
@@ -3766,7 +3824,7 @@ async function generateConstraints(projectRoot2, features, _context, config) {
3766
3824
  try {
3767
3825
  await writer.write(content);
3768
3826
  writtenFiles.push(writer.configFile);
3769
- logger.info(`Constraint written via plugin: ${writer.toolName} \u2192 ${writer.configFile}`);
3827
+ logger.info(`Constraint written via plugin: ${writer.toolName} -> ${writer.configFile}`);
3770
3828
  } catch (err) {
3771
3829
  logger.warn(`Writer plugin '${tool}' failed: ${err instanceof Error ? err.message : String(err)}`);
3772
3830
  }
@@ -3795,15 +3853,15 @@ Keep mapping files in sync whenever tests are modified.`;
3795
3853
  });
3796
3854
 
3797
3855
  // src/core/test-scaffolder/framework-detector.ts
3798
- import { readFileSync as readFileSync12, existsSync as existsSync18 } from "fs";
3856
+ import { readFileSync as readFileSync13, existsSync as existsSync19 } from "fs";
3799
3857
  import { join as join21 } from "path";
3800
3858
  function detectTestFramework(projectRoot2) {
3801
3859
  const pkgPath = join21(projectRoot2, "package.json");
3802
- if (!existsSync18(pkgPath)) {
3860
+ if (!existsSync19(pkgPath)) {
3803
3861
  return "vitest";
3804
3862
  }
3805
3863
  try {
3806
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3864
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
3807
3865
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3808
3866
  if ("vitest" in deps) return "vitest";
3809
3867
  if ("jest" in deps || "@jest/core" in deps) return "jest";
@@ -3819,8 +3877,8 @@ var init_framework_detector = __esm({
3819
3877
  });
3820
3878
 
3821
3879
  // src/core/test-scaffolder/index.ts
3822
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync12 } from "fs";
3823
- import { join as join22, relative as relative3, dirname as dirname3, basename as basename8 } from "path";
3880
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync12, existsSync as existsSync20, mkdirSync as mkdirSync12 } from "fs";
3881
+ import { join as join22, relative as relative3, dirname as dirname3, basename as basename9 } from "path";
3824
3882
  function generateTestScaffolding(projectRoot2, features, config) {
3825
3883
  const framework = detectTestFramework(projectRoot2);
3826
3884
  const results = [];
@@ -3832,7 +3890,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
3832
3890
  }
3833
3891
  const absTestPath = join22(projectRoot2, testFilePath);
3834
3892
  const scenarios = feature.scenarios;
3835
- if (existsSync19(absTestPath)) {
3893
+ if (existsSync20(absTestPath)) {
3836
3894
  const appended = appendMissingScenarios(absTestPath, scenarios, framework);
3837
3895
  if (appended > 0) {
3838
3896
  results.push({ testFile: testFilePath, framework, scenarios: appended });
@@ -3841,7 +3899,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
3841
3899
  } else {
3842
3900
  const content = generateTestFileContent(feature.featureName, scenarios, framework);
3843
3901
  const dir = dirname3(absTestPath);
3844
- if (!existsSync19(dir)) {
3902
+ if (!existsSync20(dir)) {
3845
3903
  mkdirSync12(dir, { recursive: true });
3846
3904
  }
3847
3905
  writeFileSync12(absTestPath, content, "utf-8");
@@ -3860,7 +3918,7 @@ function resolveTestFilePath(projectRoot2, featurePath, config) {
3860
3918
  return resolveTestFromPattern(match, pattern);
3861
3919
  }
3862
3920
  }
3863
- const featureBase = basename8(featurePath, ".feature");
3921
+ const featureBase = basename9(featurePath, ".feature");
3864
3922
  const featureDir = dirname3(relative3(projectRoot2, featurePath));
3865
3923
  return join22(featureDir, `${featureBase}.test.ts`).replace(/\\/g, "/");
3866
3924
  }
@@ -3915,7 +3973,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
3915
3973
  return lines.join("\n");
3916
3974
  }
3917
3975
  function appendMissingScenarios(testFilePath, scenarios, _framework) {
3918
- const content = readFileSync13(testFilePath, "utf-8");
3976
+ const content = readFileSync14(testFilePath, "utf-8");
3919
3977
  const missing = [];
3920
3978
  for (const scenario of scenarios) {
3921
3979
  if (!content.includes(`Scenario: ${scenario.name}`)) {
@@ -3956,7 +4014,7 @@ var init_test_scaffolder = __esm({
3956
4014
  });
3957
4015
 
3958
4016
  // src/core/verifier/mapping-resolver.ts
3959
- import { readFileSync as readFileSync15, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
4017
+ import { readFileSync as readFileSync16, existsSync as existsSync22, readdirSync as readdirSync2 } from "fs";
3960
4018
  import { join as join24 } from "path";
3961
4019
  function resolveMappings(projectRoot2, features, testOutput) {
3962
4020
  const results = [];
@@ -3981,17 +4039,17 @@ function resolveMapping(projectRoot2, feature, scenario, _testOutput) {
3981
4039
  }
3982
4040
  function resolveFromMappingFile(projectRoot2, _feature, scenario) {
3983
4041
  const mappingsDir = join24(projectRoot2, ".speclore", "mappings");
3984
- if (!existsSync21(mappingsDir)) return null;
4042
+ if (!existsSync22(mappingsDir)) return null;
3985
4043
  try {
3986
4044
  const files = findMappingFiles(mappingsDir);
3987
4045
  for (const file of files) {
3988
4046
  try {
3989
- const content = readFileSync15(file, "utf-8");
4047
+ const content = readFileSync16(file, "utf-8");
3990
4048
  const mapping = JSON.parse(content);
3991
4049
  if (mapping.scenarios && scenario.name in mapping.scenarios) {
3992
4050
  const entry = mapping.scenarios[scenario.name];
3993
4051
  if (entry) {
3994
- logger.debug(`Mapping file hit: ${scenario.name} \u2192 ${entry.testMethod}`);
4052
+ logger.debug(`Mapping file hit: ${scenario.name} -> ${entry.testMethod}`);
3995
4053
  return {
3996
4054
  name: scenario.name,
3997
4055
  status: "passed",
@@ -4011,18 +4069,18 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
4011
4069
  }
4012
4070
  function resolveFromTag(projectRoot2, _feature, scenario) {
4013
4071
  const testsDir = join24(projectRoot2, "tests");
4014
- if (!existsSync21(testsDir)) return null;
4072
+ if (!existsSync22(testsDir)) return null;
4015
4073
  try {
4016
4074
  const files = findTestFiles(testsDir);
4017
4075
  for (const file of files) {
4018
4076
  try {
4019
- const content = readFileSync15(file, "utf-8");
4077
+ const content = readFileSync16(file, "utf-8");
4020
4078
  const tagRegex = /@speclore-scenario:\s*(.+)/g;
4021
4079
  let match;
4022
4080
  while ((match = tagRegex.exec(content)) !== null) {
4023
4081
  if (match[1].trim() === scenario.name) {
4024
4082
  const methodMatch = findTestMethodNearby(content, match.index);
4025
- logger.debug(`Tag hit: ${scenario.name} \u2192 ${methodMatch}`);
4083
+ logger.debug(`Tag hit: ${scenario.name} -> ${methodMatch}`);
4026
4084
  return {
4027
4085
  name: scenario.name,
4028
4086
  status: "passed",
@@ -4196,12 +4254,12 @@ var report_generator_exports = {};
4196
4254
  __export(report_generator_exports, {
4197
4255
  generateReport: () => generateReport
4198
4256
  });
4199
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync22, readFileSync as readFileSync16 } from "fs";
4257
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync23, readFileSync as readFileSync17 } from "fs";
4200
4258
  import { join as join25, dirname as dirname4 } from "path";
4201
4259
  import { fileURLToPath as fileURLToPath2 } from "url";
4202
4260
  function generateReport(report, projectRoot2, config) {
4203
4261
  const reportsDir = join25(projectRoot2, ".speclore", "reports");
4204
- if (!existsSync22(reportsDir)) {
4262
+ if (!existsSync23(reportsDir)) {
4205
4263
  mkdirSync13(reportsDir, { recursive: true });
4206
4264
  }
4207
4265
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
@@ -4259,7 +4317,7 @@ function renderHtmlReport(report) {
4259
4317
  const templatePath = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "cli", "templates", "report.html");
4260
4318
  let template;
4261
4319
  try {
4262
- template = readFileSync16(templatePath, "utf-8");
4320
+ template = readFileSync17(templatePath, "utf-8");
4263
4321
  } catch {
4264
4322
  template = getInlineTemplate();
4265
4323
  }
@@ -4338,7 +4396,7 @@ var init_verifier = __esm({
4338
4396
  });
4339
4397
 
4340
4398
  // src/core/analyzer/rdg-builder.ts
4341
- import { readFileSync as readFileSync17, existsSync as existsSync23, readdirSync as readdirSync3 } from "fs";
4399
+ import { readFileSync as readFileSync18, existsSync as existsSync24, readdirSync as readdirSync3 } from "fs";
4342
4400
  import { join as join26 } from "path";
4343
4401
  var init_rdg_builder = __esm({
4344
4402
  "src/core/analyzer/rdg-builder.ts"() {
@@ -4446,11 +4504,11 @@ var init_analyzer = __esm({
4446
4504
  });
4447
4505
 
4448
4506
  // src/infra/file-lock.ts
4449
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync, existsSync as existsSync28, statSync as statSync3 } from "fs";
4507
+ import { readFileSync as readFileSync22, writeFileSync as writeFileSync16, unlinkSync, existsSync as existsSync29, statSync as statSync3 } from "fs";
4450
4508
  import { join as join31 } from "path";
4451
4509
  function acquireLock(specLoreDir) {
4452
4510
  const lockPath = join31(specLoreDir, LOCK_FILENAME);
4453
- if (existsSync28(lockPath)) {
4511
+ if (existsSync29(lockPath)) {
4454
4512
  const existing = readLockInfo(lockPath);
4455
4513
  if (existing && !isLockExpired(existing)) {
4456
4514
  logger.debug(`Lock already held by PID ${existing.pid}, not expired`);
@@ -4466,7 +4524,7 @@ function acquireLock(specLoreDir) {
4466
4524
  try {
4467
4525
  writeFileSync16(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
4468
4526
  } catch {
4469
- logger.debug("Lock write failed \u2014 another process may have acquired it");
4527
+ logger.debug("Lock write failed - another process may have acquired it");
4470
4528
  return false;
4471
4529
  }
4472
4530
  logger.debug(`Lock acquired by PID ${process.pid}`);
@@ -4474,7 +4532,7 @@ function acquireLock(specLoreDir) {
4474
4532
  }
4475
4533
  function releaseLock(specLoreDir) {
4476
4534
  const lockPath = join31(specLoreDir, LOCK_FILENAME);
4477
- if (!existsSync28(lockPath)) {
4535
+ if (!existsSync29(lockPath)) {
4478
4536
  return;
4479
4537
  }
4480
4538
  const existing = readLockInfo(lockPath);
@@ -4485,7 +4543,7 @@ function releaseLock(specLoreDir) {
4485
4543
  }
4486
4544
  function readLockInfo(lockPath) {
4487
4545
  try {
4488
- const content = readFileSync21(lockPath, "utf-8");
4546
+ const content = readFileSync22(lockPath, "utf-8");
4489
4547
  const parsed = JSON.parse(content);
4490
4548
  if (typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
4491
4549
  return parsed;
@@ -4556,17 +4614,18 @@ var init_schemas = __esm({
4556
4614
 
4557
4615
  // src/mcp/tools.ts
4558
4616
  import { join as join32 } from "path";
4559
- import { readFileSync as readFileSync22, existsSync as existsSync29, mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
4617
+ import { readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
4560
4618
  import { globSync as globSync7 } from "glob";
4561
4619
  import { toJSONSchema } from "zod";
4562
4620
  async function executeSpecTool(args, projectRoot2) {
4563
4621
  if (!args.source || args.source.length > 5e4) {
4564
4622
  throw new Error("source must be between 1 and 50000 characters");
4565
4623
  }
4624
+ const sanitizedSource = sanitizeSource(args.source);
4566
4625
  ensureProjectReady(projectRoot2);
4567
4626
  const config = loadConfig(projectRoot2);
4568
- logger.info(`Reading requirement from: ${args.source.slice(0, 80)}...`);
4569
- const req = await readRequirement(args.source);
4627
+ logger.info(`Reading requirement from: ${sanitizedSource.slice(0, 120)}`);
4628
+ const req = await readRequirement(sanitizedSource);
4570
4629
  const specLoreDir = join32(projectRoot2, ".speclore");
4571
4630
  const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
4572
4631
  const feature = await generateFeature(req, context, config, projectRoot2);
@@ -4717,10 +4776,10 @@ function ensureProjectReady(projectRoot2) {
4717
4776
  const specLoreDir = join32(projectRoot2, ".speclore");
4718
4777
  const configPath = join32(specLoreDir, "config.yaml");
4719
4778
  let configCreated = false;
4720
- if (!existsSync29(specLoreDir)) {
4779
+ if (!existsSync30(specLoreDir)) {
4721
4780
  mkdirSync14(specLoreDir, { recursive: true });
4722
4781
  }
4723
- if (!existsSync29(configPath)) {
4782
+ if (!existsSync30(configPath)) {
4724
4783
  const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
4725
4784
  writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4726
4785
  configCreated = true;
@@ -4772,8 +4831,8 @@ function resolveFeatureFiles(patterns, projectRoot2, config) {
4772
4831
  for (const pattern of searchPatterns) {
4773
4832
  const matches = globSync7(pattern, { cwd: projectRoot2, absolute: true });
4774
4833
  for (const filePath of matches) {
4775
- if (existsSync29(filePath)) {
4776
- const content = readFileSync22(filePath, "utf-8");
4834
+ if (existsSync30(filePath)) {
4835
+ const content = readFileSync23(filePath, "utf-8");
4777
4836
  files.push(parseFeatureFile(filePath, content));
4778
4837
  }
4779
4838
  }
@@ -4870,10 +4929,23 @@ var init_tools = __esm({
4870
4929
  init_config2();
4871
4930
  init_logger();
4872
4931
  init_schemas();
4873
- SPEC_TOOL_DESC = "Convert a requirement (text, file path, or URL) into structured BDD .feature files with scenario-level acceptance criteria.";
4874
- 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.";
4875
- VERIFY_TOOL_DESC = "Run tests and map results back to .feature scenarios, producing an acceptance report. Requires test scaffolding generated by speclore.code.";
4876
- STATUS_TOOL_DESC = "Get current project workflow status, feature states, and recommended next steps. Call this first to understand where the project stands.";
4932
+ SPEC_TOOL_DESC = [
4933
+ "Convert a requirement (text, file path, or URL) into structured BDD .feature files with scenario-level acceptance criteria.",
4934
+ "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."
4935
+ ].join(" ");
4936
+ CODE_TOOL_DESC = [
4937
+ "Generate AI coding constraint files for Cursor / Claude Code / Qoder based on .feature specs and project context. Requires .feature files to exist.",
4938
+ "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."
4939
+ ].join(" ");
4940
+ VERIFY_TOOL_DESC = [
4941
+ "Run tests and map results back to .feature scenarios, producing an acceptance report. Requires test scaffolding generated by speclore.code.",
4942
+ "IMPORTANT: After calling this tool, ALWAYS show the user: (1) pass/fail summary, (2) any failed scenario details, (3) the recommended next step."
4943
+ ].join(" ");
4944
+ STATUS_TOOL_DESC = [
4945
+ "Get current project workflow status, feature states, and recommended next steps.",
4946
+ "BEST PRACTICE: Call this tool FIRST when starting a new conversation about the project to understand where it stands.",
4947
+ "ALWAYS show the user the full status including: project state, feature list, and recommended actions."
4948
+ ].join(" ");
4877
4949
  specJsonSchema = toJSONSchema(specInputSchema, { target: "draft-07" });
4878
4950
  codeJsonSchema = toJSONSchema(codeInputSchema, { target: "draft-07" });
4879
4951
  verifyJsonSchema = toJSONSchema(verifyInputSchema, { target: "draft-07" });
@@ -4881,7 +4953,7 @@ var init_tools = __esm({
4881
4953
  });
4882
4954
 
4883
4955
  // src/mcp/status.ts
4884
- import { readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync15, writeFileSync as writeFileSync18 } from "fs";
4956
+ import { readFileSync as readFileSync24, existsSync as existsSync31, mkdirSync as mkdirSync15, writeFileSync as writeFileSync18 } from "fs";
4885
4957
  import { join as join33 } from "path";
4886
4958
  import { globSync as globSync8 } from "glob";
4887
4959
  function executeStatusTool(args, projectRoot2) {
@@ -4898,9 +4970,9 @@ function executeStatusTool(args, projectRoot2) {
4898
4970
  continue;
4899
4971
  }
4900
4972
  let scenarioCount = 0;
4901
- if (existsSync30(path)) {
4973
+ if (existsSync31(path)) {
4902
4974
  try {
4903
- const content = readFileSync23(path, "utf-8");
4975
+ const content = readFileSync24(path, "utf-8");
4904
4976
  const matches = content.match(/Scenario(?: Outline)?:/g);
4905
4977
  scenarioCount = matches?.length ?? 0;
4906
4978
  } catch {
@@ -4923,7 +4995,7 @@ function executeStatusTool(args, projectRoot2) {
4923
4995
  if (args.feature && !filePath.includes(args.feature)) continue;
4924
4996
  let scenarioCount = 0;
4925
4997
  try {
4926
- const content = readFileSync23(filePath, "utf-8");
4998
+ const content = readFileSync24(filePath, "utf-8");
4927
4999
  const matches = content.match(/Scenario(?: Outline)?:/g);
4928
5000
  scenarioCount = matches?.length ?? 0;
4929
5001
  } catch {
@@ -4983,10 +5055,10 @@ function ensureProjectReadyForStatus(projectRoot2) {
4983
5055
  const specLoreDir = join33(projectRoot2, ".speclore");
4984
5056
  const configPath = join33(specLoreDir, "config.yaml");
4985
5057
  let configCreated = false;
4986
- if (!existsSync30(specLoreDir)) {
5058
+ if (!existsSync31(specLoreDir)) {
4987
5059
  mkdirSync15(specLoreDir, { recursive: true });
4988
5060
  }
4989
- if (!existsSync30(configPath)) {
5061
+ if (!existsSync31(configPath)) {
4990
5062
  const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
4991
5063
  writeFileSync18(configPath, generateDefaultConfigYaml(projectName), "utf-8");
4992
5064
  configCreated = true;
@@ -5030,10 +5102,10 @@ async function startMcpServer() {
5030
5102
  mkdirSync16(specLoreDir, { recursive: true });
5031
5103
  const locked = acquireLock(specLoreDir);
5032
5104
  if (!locked) {
5033
- logger.warn("Could not acquire lock \u2014 another SpecLore instance may be running.");
5105
+ logger.warn("Could not acquire lock - another SpecLore instance may be running.");
5034
5106
  }
5035
5107
  } catch {
5036
- logger.warn("Could not acquire lock \u2014 .speclore/ directory may not exist.");
5108
+ logger.warn("Could not acquire lock - .speclore/ directory may not exist.");
5037
5109
  }
5038
5110
  const transport = new StdioServerTransport();
5039
5111
  await server.connect(transport);
@@ -5053,6 +5125,58 @@ async function startMcpServer() {
5053
5125
  process.exit(0);
5054
5126
  });
5055
5127
  }
5128
+ function buildSpecSummary(result) {
5129
+ const lines = [];
5130
+ lines.push("[SpecLore] Feature file generated successfully.");
5131
+ lines.push(` File: ${result.createdFiles.join(", ")}`);
5132
+ lines.push(` Scenarios: ${result.scenarios.length}`);
5133
+ for (const sc of result.scenarios) {
5134
+ lines.push(` - ${sc.name}`);
5135
+ }
5136
+ lines.push(` Next step: ${result.nextSteps}`);
5137
+ return lines.join("\n");
5138
+ }
5139
+ function buildCodeSummary(result) {
5140
+ const lines = [];
5141
+ lines.push("[SpecLore] Constraints and test scaffolding generated.");
5142
+ lines.push(` Constraint files: ${result.writtenFiles.length > 0 ? result.writtenFiles.join(", ") : "none"}`);
5143
+ lines.push(` Test scaffold files: ${result.scaffoldFiles.length > 0 ? result.scaffoldFiles.map((s) => s.testFile).join(", ") : "none"}`);
5144
+ lines.push(` Next step: ${result.workflow.nextStep}`);
5145
+ return lines.join("\n");
5146
+ }
5147
+ function buildVerifySummary(result) {
5148
+ const lines = [];
5149
+ const icon = result.failed === 0 ? "[PASS]" : "[FAIL]";
5150
+ lines.push(`[SpecLore] Verification ${icon} ${result.summary}`);
5151
+ if (result.failed > 0) {
5152
+ lines.push(` Failed: ${result.failed} scenario(s)`);
5153
+ for (const d of result.failedDetails) {
5154
+ lines.push(` - ${d.feature} > ${d.scenario}: ${d.error}`);
5155
+ }
5156
+ }
5157
+ if (result.unmapped > 0) {
5158
+ lines.push(` Unmapped: ${result.unmapped} scenario(s) without test mapping`);
5159
+ }
5160
+ lines.push(` Next step: ${result.workflow.nextStep}`);
5161
+ return lines.join("\n");
5162
+ }
5163
+ function buildStatusSummary(result) {
5164
+ const lines = [];
5165
+ lines.push(`[SpecLore] Project status: ${result.project.initialized ? "initialized" : "not initialized"}`);
5166
+ lines.push(` AI tools detected: ${result.project.aiToolsDetected.length > 0 ? result.project.aiToolsDetected.join(", ") : "none"}`);
5167
+ lines.push(` Features: ${result.summary.total} total`);
5168
+ if (result.summary.specified > 0) lines.push(` - ${result.summary.specified} specified`);
5169
+ if (result.summary.constrained > 0) lines.push(` - ${result.summary.constrained} constrained`);
5170
+ if (result.summary.coding > 0) lines.push(` - ${result.summary.coding} coding`);
5171
+ if (result.summary.verified > 0) lines.push(` - ${result.summary.verified} verified`);
5172
+ if (result.recommendedActions.length > 0) {
5173
+ lines.push(" Recommended actions:");
5174
+ for (const action of result.recommendedActions) {
5175
+ lines.push(` -> ${action}`);
5176
+ }
5177
+ }
5178
+ return lines.join("\n");
5179
+ }
5056
5180
  var server, projectRoot;
5057
5181
  var init_server = __esm({
5058
5182
  "src/mcp/server.ts"() {
@@ -5081,7 +5205,11 @@ var init_server = __esm({
5081
5205
  { source: args.source, module: args.module },
5082
5206
  getProjectRoot()
5083
5207
  );
5084
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
5208
+ const summary = buildSpecSummary(result);
5209
+ return { content: [{ type: "text", text: `${summary}
5210
+
5211
+ ---
5212
+ ${JSON.stringify(result, null, 2)}` }] };
5085
5213
  }
5086
5214
  );
5087
5215
  server.registerTool(
@@ -5098,7 +5226,11 @@ var init_server = __esm({
5098
5226
  { features: args.features, tools: args.tools },
5099
5227
  getProjectRoot()
5100
5228
  );
5101
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
5229
+ const summary = buildCodeSummary(result);
5230
+ return { content: [{ type: "text", text: `${summary}
5231
+
5232
+ ---
5233
+ ${JSON.stringify(result, null, 2)}` }] };
5102
5234
  }
5103
5235
  );
5104
5236
  server.registerTool(
@@ -5112,7 +5244,11 @@ var init_server = __esm({
5112
5244
  { features: args.features, impact: args.impact },
5113
5245
  getProjectRoot()
5114
5246
  );
5115
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
5247
+ const summary = buildVerifySummary(result);
5248
+ return { content: [{ type: "text", text: `${summary}
5249
+
5250
+ ---
5251
+ ${JSON.stringify(result, null, 2)}` }] };
5116
5252
  }
5117
5253
  );
5118
5254
  server.registerTool(
@@ -5126,7 +5262,11 @@ var init_server = __esm({
5126
5262
  { feature: args.feature },
5127
5263
  getProjectRoot()
5128
5264
  );
5129
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
5265
+ const summary = buildStatusSummary(result);
5266
+ return { content: [{ type: "text", text: `${summary}
5267
+
5268
+ ---
5269
+ ${JSON.stringify(result, null, 2)}` }] };
5130
5270
  }
5131
5271
  );
5132
5272
  projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
@@ -5140,18 +5280,18 @@ import { Command } from "commander";
5140
5280
  init_logger();
5141
5281
  init_config2();
5142
5282
  init_detector();
5143
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
5144
- import { join as join7 } from "path";
5283
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
5284
+ import { join as join8 } from "path";
5145
5285
 
5146
5286
  // src/setup/config-writer.ts
5147
- import { writeFileSync as writeFileSync2, existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
5148
- import { join as join5 } from "path";
5287
+ import { writeFileSync as writeFileSync2, existsSync as existsSync5, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
5288
+ import { join as join6 } from "path";
5149
5289
 
5150
5290
  // src/infra/install-detector.ts
5151
5291
  init_logger();
5152
5292
  init_version();
5153
- import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync, mkdirSync } from "fs";
5154
- import { join as join4 } from "path";
5293
+ import { readFileSync as readFileSync3, existsSync as existsSync4, writeFileSync, mkdirSync } from "fs";
5294
+ import { join as join5 } from "path";
5155
5295
  import { createRequire } from "module";
5156
5296
  var INSTALL_PATH_FILE = ".install-path";
5157
5297
  function detectInstallMethod(projectRoot2) {
@@ -5179,13 +5319,13 @@ function detectNpmInstall() {
5179
5319
  }
5180
5320
  }
5181
5321
  function detectCloneInstall(projectRoot2) {
5182
- const installPathFile = join4(projectRoot2, ".speclore", INSTALL_PATH_FILE);
5183
- if (!existsSync3(installPathFile)) {
5322
+ const installPathFile = join5(projectRoot2, ".speclore", INSTALL_PATH_FILE);
5323
+ if (!existsSync4(installPathFile)) {
5184
5324
  return null;
5185
5325
  }
5186
5326
  try {
5187
5327
  const localPath = readFileSync3(installPathFile, "utf-8").trim();
5188
- if (existsSync3(localPath)) {
5328
+ if (existsSync4(localPath)) {
5189
5329
  return { mode: "clone", version: VERSION, localPath };
5190
5330
  }
5191
5331
  } catch {
@@ -5195,6 +5335,7 @@ function detectCloneInstall(projectRoot2) {
5195
5335
 
5196
5336
  // src/setup/config-writer.ts
5197
5337
  init_logger();
5338
+ init_path_utils();
5198
5339
  function writeMcpConfig(projectRoot2, tools, _globalMode) {
5199
5340
  const installInfo = detectInstallMethod(projectRoot2);
5200
5341
  const serverCommand = getServerCommand(installInfo);
@@ -5217,19 +5358,21 @@ function writeMcpForClient(projectRoot2, client) {
5217
5358
  const serverCommand = getServerCommand(installInfo);
5218
5359
  switch (client) {
5219
5360
  case "cursor":
5220
- mkdirSync2(join5(projectRoot2, ".cursor"), { recursive: true });
5361
+ mkdirSync2(join6(projectRoot2, ".cursor"), { recursive: true });
5221
5362
  writeCursorMcp(projectRoot2, serverCommand);
5222
5363
  break;
5223
5364
  case "claude":
5224
- if (!existsSync4(join5(projectRoot2, ".claude"))) {
5225
- mkdirSync2(join5(projectRoot2, ".claude"), { recursive: true });
5365
+ if (!existsSync5(join6(projectRoot2, ".claude"))) {
5366
+ mkdirSync2(join6(projectRoot2, ".claude"), { recursive: true });
5226
5367
  }
5227
5368
  writeClaudeMcp(projectRoot2, serverCommand);
5228
5369
  break;
5229
- case "qoder":
5230
- mkdirSync2(join5(projectRoot2, ".qoder"), { recursive: true });
5370
+ case "qoder": {
5371
+ const qoderDir = resolveQoderDir(projectRoot2);
5372
+ mkdirSync2(join6(projectRoot2, qoderDir), { recursive: true });
5231
5373
  writeQoderMcp(projectRoot2, serverCommand);
5232
5374
  break;
5375
+ }
5233
5376
  }
5234
5377
  }
5235
5378
  function getServerCommand(installInfo) {
@@ -5237,12 +5380,12 @@ function getServerCommand(installInfo) {
5237
5380
  return { command: "npx", args: ["speclore", "mcp"] };
5238
5381
  }
5239
5382
  const localPath = installInfo.localPath ?? process.cwd();
5240
- return { command: "node", args: [join5(localPath, "dist", "mcp", "server.js")] };
5383
+ return { command: "node", args: [join6(localPath, "dist", "mcp", "server.js")] };
5241
5384
  }
5242
5385
  function writeCursorMcp(projectRoot2, serverCmd) {
5243
- const mcpDir = join5(projectRoot2, ".cursor");
5244
- if (!existsSync4(mcpDir)) return;
5245
- const mcpPath = join5(mcpDir, "mcp.json");
5386
+ const mcpDir = join6(projectRoot2, ".cursor");
5387
+ if (!existsSync5(mcpDir)) return;
5388
+ const mcpPath = join6(mcpDir, "mcp.json");
5246
5389
  const existing = readExistingJson(mcpPath);
5247
5390
  if (!existing.mcpServers) existing.mcpServers = {};
5248
5391
  existing.mcpServers.speclore = {
@@ -5254,10 +5397,10 @@ function writeCursorMcp(projectRoot2, serverCmd) {
5254
5397
  logger.info(` Cursor: ${mcpPath}`);
5255
5398
  }
5256
5399
  function writeClaudeMcp(projectRoot2, serverCmd) {
5257
- const hasClaudeDir = existsSync4(join5(projectRoot2, ".claude"));
5258
- const hasClaudeMd = existsSync4(join5(projectRoot2, "CLAUDE.md"));
5400
+ const hasClaudeDir = existsSync5(join6(projectRoot2, ".claude"));
5401
+ const hasClaudeMd = existsSync5(join6(projectRoot2, "CLAUDE.md"));
5259
5402
  if (!hasClaudeDir && !hasClaudeMd) return;
5260
- const mcpPath = join5(projectRoot2, ".mcp.json");
5403
+ const mcpPath = join6(projectRoot2, ".mcp.json");
5261
5404
  const existing = readExistingJson(mcpPath);
5262
5405
  if (!existing.mcpServers) existing.mcpServers = {};
5263
5406
  existing.mcpServers.speclore = {
@@ -5269,9 +5412,10 @@ function writeClaudeMcp(projectRoot2, serverCmd) {
5269
5412
  logger.info(` Claude Code: ${mcpPath}`);
5270
5413
  }
5271
5414
  function writeQoderMcp(projectRoot2, serverCmd) {
5272
- const mcpDir = join5(projectRoot2, ".qoder");
5273
- if (!existsSync4(mcpDir)) return;
5274
- const mcpPath = join5(mcpDir, "mcp.json");
5415
+ const qoderDir = resolveQoderDir(projectRoot2);
5416
+ const mcpDir = join6(projectRoot2, qoderDir);
5417
+ if (!existsSync5(mcpDir)) return;
5418
+ const mcpPath = join6(mcpDir, "mcp.json");
5275
5419
  const existing = readExistingJson(mcpPath);
5276
5420
  if (!existing.mcpServers) existing.mcpServers = {};
5277
5421
  existing.mcpServers.speclore = {
@@ -5283,7 +5427,7 @@ function writeQoderMcp(projectRoot2, serverCmd) {
5283
5427
  logger.info(` Qoder: ${mcpPath}`);
5284
5428
  }
5285
5429
  function readExistingJson(filePath) {
5286
- if (existsSync4(filePath)) {
5430
+ if (existsSync5(filePath)) {
5287
5431
  try {
5288
5432
  return JSON.parse(readFileSync4(filePath, "utf-8"));
5289
5433
  } catch {
@@ -5294,9 +5438,10 @@ function readExistingJson(filePath) {
5294
5438
  }
5295
5439
 
5296
5440
  // src/setup/rule-writer.ts
5441
+ init_path_utils();
5297
5442
  init_logger();
5298
5443
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
5299
- import { join as join6 } from "path";
5444
+ import { join as join7 } from "path";
5300
5445
  function writeRuleFiles(projectRoot2, tools) {
5301
5446
  for (const tool of tools) {
5302
5447
  switch (tool.tool) {
@@ -5313,7 +5458,7 @@ function writeRuleFiles(projectRoot2, tools) {
5313
5458
  }
5314
5459
  }
5315
5460
  function writeCursorRule(projectRoot2) {
5316
- const rulesDir = join6(projectRoot2, ".cursor", "rules");
5461
+ const rulesDir = join7(projectRoot2, ".cursor", "rules");
5317
5462
  mkdirSync3(rulesDir, { recursive: true });
5318
5463
  const content = `---
5319
5464
  description: SpecLore \u2014 AI coding constraints (setup placeholder)
@@ -5331,11 +5476,11 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
5331
5476
 
5332
5477
  > This is a placeholder. Run \`speclore code\` to generate full constraints with module boundaries.
5333
5478
  `;
5334
- writeFileSync3(join6(rulesDir, "speclore.mdc"), content, "utf-8");
5479
+ writeFileSync3(join7(rulesDir, "speclore.mdc"), content, "utf-8");
5335
5480
  logger.info(` Cursor rule: .cursor/rules/speclore.mdc`);
5336
5481
  }
5337
5482
  function writeClaudeRule(projectRoot2) {
5338
- const rulesDir = join6(projectRoot2, ".claude", "rules");
5483
+ const rulesDir = join7(projectRoot2, ".claude", "rules");
5339
5484
  mkdirSync3(rulesDir, { recursive: true });
5340
5485
  const content = `# SpecLore
5341
5486
 
@@ -5347,11 +5492,12 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
5347
5492
 
5348
5493
  > This is a placeholder. Run \`speclore code\` to generate full constraints with module boundaries.
5349
5494
  `;
5350
- writeFileSync3(join6(rulesDir, "speclore.md"), content, "utf-8");
5495
+ writeFileSync3(join7(rulesDir, "speclore.md"), content, "utf-8");
5351
5496
  logger.info(` Claude Code rule: .claude/rules/speclore.md`);
5352
5497
  }
5353
5498
  function writeQoderRule(projectRoot2) {
5354
- const rulesDir = join6(projectRoot2, ".qoder", "rules");
5499
+ const qoderDir = resolveQoderDir(projectRoot2);
5500
+ const rulesDir = join7(projectRoot2, qoderDir, "rules");
5355
5501
  mkdirSync3(rulesDir, { recursive: true });
5356
5502
  const content = `# SpecLore
5357
5503
 
@@ -5363,8 +5509,8 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
5363
5509
 
5364
5510
  > This is a placeholder. Run \`speclore code\` to generate full constraints with module boundaries.
5365
5511
  `;
5366
- writeFileSync3(join6(rulesDir, "speclore.md"), content, "utf-8");
5367
- logger.info(` Qoder rule: .qoder/rules/speclore.md`);
5512
+ writeFileSync3(join7(rulesDir, "speclore.md"), content, "utf-8");
5513
+ logger.info(` Qoder rule: ${qoderDir}/rules/speclore.md`);
5368
5514
  }
5369
5515
 
5370
5516
  // src/cli/commands/setup.ts
@@ -5373,26 +5519,26 @@ function registerSetupCommand(program) {
5373
5519
  if (opts.verbose) logger.setLevel("debug");
5374
5520
  const projectRoot2 = process.cwd();
5375
5521
  logger.info("SpecLore Setup");
5376
- logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5522
+ logger.info("---------------");
5377
5523
  const tools = detectAITools(projectRoot2);
5378
5524
  if (tools.length === 0) {
5379
5525
  logger.warn("No supported AI tools detected.");
5380
5526
  logger.info("");
5381
5527
  logger.info(" To enable MCP integration, open your AI client in this project first,");
5382
5528
  logger.info(" then re-run setup. Or use manual configuration:");
5383
- logger.info(" speclore mcp add cursor \u2014 configure for Cursor");
5384
- logger.info(" speclore mcp add claude \u2014 configure for Claude Code");
5385
- logger.info(" speclore mcp add qoder \u2014 configure for Qoder");
5529
+ logger.info(" speclore mcp add cursor - configure for Cursor");
5530
+ logger.info(" speclore mcp add claude - configure for Claude Code");
5531
+ logger.info(" speclore mcp add qoder - configure for Qoder");
5386
5532
  logger.info("");
5387
5533
  } else {
5388
5534
  logger.info(`Detected AI tools: ${tools.map((t) => t.tool).join(", ")}`);
5389
5535
  }
5390
- const specLoreDir = opts.global ? join7(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".speclore") : join7(projectRoot2, ".speclore");
5391
- if (!existsSync5(specLoreDir)) {
5536
+ const specLoreDir = opts.global ? join8(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".speclore") : join8(projectRoot2, ".speclore");
5537
+ if (!existsSync6(specLoreDir)) {
5392
5538
  mkdirSync4(specLoreDir, { recursive: true });
5393
5539
  }
5394
- const configPath = join7(specLoreDir, "config.yaml");
5395
- if (!existsSync5(configPath)) {
5540
+ const configPath = join8(specLoreDir, "config.yaml");
5541
+ if (!existsSync6(configPath)) {
5396
5542
  const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
5397
5543
  writeFileSync4(configPath, generateDefaultConfigYaml(projectName), "utf-8");
5398
5544
  logger.info(`Created config: ${configPath}`);
@@ -5402,18 +5548,18 @@ function registerSetupCommand(program) {
5402
5548
  logger.info("");
5403
5549
  logger.info("Setup complete! Next steps:");
5404
5550
  logger.info("");
5405
- logger.info(" Option A \u2014 CLI workflow (terminal users):");
5406
- logger.info(' speclore spec "your requirement" \u2192 generate .feature');
5407
- logger.info(" speclore code \u2192 generate constraints + tests");
5408
- logger.info(" speclore verify \u2192 run acceptance");
5551
+ logger.info(" Option A - CLI workflow (terminal users):");
5552
+ logger.info(' speclore spec "your requirement" -> generate .feature');
5553
+ logger.info(" speclore code -> generate constraints + tests");
5554
+ logger.info(" speclore verify -> run acceptance");
5409
5555
  logger.info("");
5410
- logger.info(" Option B \u2014 AI client workflow (recommended):");
5556
+ logger.info(" Option B - AI client workflow (recommended):");
5411
5557
  logger.info(" Open Cursor / Qoder / Claude Code and start chatting.");
5412
- logger.info(" MCP is already configured \u2014 AI handles the full pipeline.");
5558
+ logger.info(" MCP is already configured - AI handles the full pipeline.");
5413
5559
  logger.info("");
5414
- logger.info(" Optional \u2014 Pre-scan project context:");
5415
- logger.info(" speclore init \u2192 scan modules/entities/APIs for better AI context");
5416
- logger.info(" (Not required \u2014 context is auto-built on first spec/code call)");
5560
+ logger.info(" Optional - Pre-scan project context:");
5561
+ logger.info(" speclore init -> scan modules/entities/APIs for better AI context");
5562
+ logger.info(" (Not required - context is auto-built on first spec/code call)");
5417
5563
  });
5418
5564
  }
5419
5565
 
@@ -5427,7 +5573,7 @@ function registerInitCommand(program) {
5427
5573
  if (opts.verbose) logger.setLevel("debug");
5428
5574
  const projectRoot2 = process.cwd();
5429
5575
  logger.info("SpecLore Init");
5430
- logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5576
+ logger.info("--------------");
5431
5577
  const config = loadConfig(projectRoot2);
5432
5578
  logger.info(`Project: ${config.project.name || "(unnamed)"}`);
5433
5579
  const context = buildContext(projectRoot2, config);
@@ -5458,11 +5604,12 @@ function registerSpecCommand(program) {
5458
5604
  program.command("spec").description("Convert a requirement source into BDD .feature file(s)").argument("<source>", "Requirement source: file path, URL, or text").option("-m, --module <name>", "Target module name").option("-v, --verbose", "Enable debug logging").action(async (source, opts) => {
5459
5605
  if (opts.verbose) logger.setLevel("debug");
5460
5606
  const projectRoot2 = process.cwd();
5461
- logger.info(`Reading requirement: ${source.slice(0, 80)}...`);
5607
+ const cleanedSource = sanitizeSource(source);
5608
+ logger.info(`Reading requirement: ${cleanedSource.slice(0, 120)}`);
5462
5609
  const config = loadConfig(projectRoot2);
5463
5610
  const specLoreDir = join18(projectRoot2, ".speclore");
5464
5611
  const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
5465
- const requirements = await readRequirement(source);
5612
+ const requirements = await readRequirement(cleanedSource);
5466
5613
  const features = await generateFeature(requirements, context, config, projectRoot2);
5467
5614
  logger.info(`Created: ${features.path} (${features.scenarios.length} scenarios)`);
5468
5615
  if (features.needsReview.length > 0) {
@@ -5487,7 +5634,7 @@ init_test_scaffolder();
5487
5634
  init_context_engine();
5488
5635
  init_state_manager();
5489
5636
  import { join as join23 } from "path";
5490
- import { readFileSync as readFileSync14, existsSync as existsSync20 } from "fs";
5637
+ import { readFileSync as readFileSync15, existsSync as existsSync21 } from "fs";
5491
5638
  import { globSync as globSync4 } from "glob";
5492
5639
  function registerCodeCommand(program) {
5493
5640
  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) => {
@@ -5542,8 +5689,8 @@ function resolveFeatures(patterns, projectRoot2, config) {
5542
5689
  for (const pattern of searchPatterns) {
5543
5690
  const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
5544
5691
  for (const filePath of matches) {
5545
- if (existsSync20(filePath)) {
5546
- const content = readFileSync14(filePath, "utf-8");
5692
+ if (existsSync21(filePath)) {
5693
+ const content = readFileSync15(filePath, "utf-8");
5547
5694
  const featureMatch = content.match(/Feature:\s*(.+)/);
5548
5695
  files.push({
5549
5696
  path: filePath,
@@ -5567,7 +5714,7 @@ init_context_engine();
5567
5714
  init_analyzer();
5568
5715
  init_state_manager();
5569
5716
  import { join as join27 } from "path";
5570
- import { readFileSync as readFileSync18, existsSync as existsSync24 } from "fs";
5717
+ import { readFileSync as readFileSync19, existsSync as existsSync25 } from "fs";
5571
5718
  import { globSync as globSync6 } from "glob";
5572
5719
  function registerVerifyCommand(program) {
5573
5720
  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) => {
@@ -5653,8 +5800,8 @@ function resolveFeatures2(patterns, projectRoot2, config) {
5653
5800
  for (const pattern of searchPatterns) {
5654
5801
  const matches = globSync6(pattern, { cwd: projectRoot2, absolute: true });
5655
5802
  for (const filePath of matches) {
5656
- if (existsSync24(filePath)) {
5657
- const content = readFileSync18(filePath, "utf-8");
5803
+ if (existsSync25(filePath)) {
5804
+ const content = readFileSync19(filePath, "utf-8");
5658
5805
  const featureMatch = content.match(/Feature:\s*(.+)/);
5659
5806
  files.push({
5660
5807
  path: filePath,
@@ -5675,36 +5822,38 @@ init_logger();
5675
5822
 
5676
5823
  // src/setup/cleanup.ts
5677
5824
  init_logger();
5678
- import { rmSync as rmSync4, existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
5825
+ init_path_utils();
5826
+ import { rmSync as rmSync4, existsSync as existsSync26, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
5679
5827
  import { join as join28 } from "path";
5680
5828
  function runTeardown(projectRoot2, globalMode) {
5681
5829
  if (globalMode) {
5682
5830
  const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
5683
5831
  const globalDir = join28(homeDir, ".speclore");
5684
- if (existsSync25(globalDir)) {
5832
+ if (existsSync26(globalDir)) {
5685
5833
  rmSync4(globalDir, { recursive: true, force: true });
5686
5834
  logger.info(`Removed: ${globalDir}`);
5687
5835
  }
5688
5836
  } else {
5689
5837
  const specLoreDir = join28(projectRoot2, ".speclore");
5690
- if (existsSync25(specLoreDir)) {
5838
+ if (existsSync26(specLoreDir)) {
5691
5839
  rmSync4(specLoreDir, { recursive: true, force: true });
5692
5840
  logger.info(`Removed: ${specLoreDir}`);
5693
5841
  }
5694
5842
  removeMcpEntry(join28(projectRoot2, ".cursor", "mcp.json"), "speclore");
5695
5843
  removeMcpEntry(join28(projectRoot2, ".mcp.json"), "speclore");
5696
- removeMcpEntry(join28(projectRoot2, ".qoder", "mcp.json"), "speclore");
5844
+ const qoderDir = resolveQoderDir(projectRoot2);
5845
+ removeMcpEntry(join28(projectRoot2, qoderDir, "mcp.json"), "speclore");
5697
5846
  removeIfExists(join28(projectRoot2, ".cursor", "rules", "speclore.mdc"));
5698
5847
  removeIfExists(join28(projectRoot2, ".claude", "rules", "speclore.md"));
5699
- removeIfExists(join28(projectRoot2, ".qoder", "rules", "speclore.md"));
5848
+ removeIfExists(join28(projectRoot2, qoderDir, "rules", "speclore.md"));
5700
5849
  logger.info("Project teardown complete.");
5701
5850
  }
5702
5851
  return Promise.resolve();
5703
5852
  }
5704
5853
  function removeMcpEntry(mcpPath, serverName) {
5705
- if (!existsSync25(mcpPath)) return;
5854
+ if (!existsSync26(mcpPath)) return;
5706
5855
  try {
5707
- const config = JSON.parse(readFileSync19(mcpPath, "utf-8"));
5856
+ const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
5708
5857
  if (config.mcpServers?.[serverName]) {
5709
5858
  delete config.mcpServers[serverName];
5710
5859
  writeFileSync14(mcpPath, JSON.stringify(config, null, 2), "utf-8");
@@ -5714,7 +5863,7 @@ function removeMcpEntry(mcpPath, serverName) {
5714
5863
  }
5715
5864
  }
5716
5865
  function removeIfExists(filePath) {
5717
- if (existsSync25(filePath)) {
5866
+ if (existsSync26(filePath)) {
5718
5867
  rmSync4(filePath);
5719
5868
  logger.info(` Removed: ${filePath}`);
5720
5869
  }
@@ -5748,7 +5897,7 @@ init_config2();
5748
5897
  init_logger();
5749
5898
  init_state_manager();
5750
5899
  import { join as join29 } from "path";
5751
- import { existsSync as existsSync26 } from "fs";
5900
+ import { existsSync as existsSync27 } from "fs";
5752
5901
  function registerMigrateCommand(program) {
5753
5902
  program.command("migrate").description("Register existing .feature files into workflow state (for upgrading projects)").option("-v, --verbose", "Enable debug logging").action((opts) => {
5754
5903
  if (opts.verbose) logger.setLevel("debug");
@@ -5758,7 +5907,7 @@ function registerMigrateCommand(program) {
5758
5907
  console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
5759
5908
  console.log("");
5760
5909
  const configPath = join29(projectRoot2, ".speclore", "config.yaml");
5761
- if (!existsSync26(configPath)) {
5910
+ if (!existsSync27(configPath)) {
5762
5911
  console.log("\u2717 No .speclore/config.yaml found. Run `speclore setup` first.");
5763
5912
  console.log("");
5764
5913
  return;
@@ -5769,7 +5918,7 @@ function registerMigrateCommand(program) {
5769
5918
  stateManager.ensureInitialized();
5770
5919
  console.log("\u2713 State file ready");
5771
5920
  const specsDir = join29(projectRoot2, config.spec.outputDir);
5772
- if (!existsSync26(specsDir)) {
5921
+ if (!existsSync27(specsDir)) {
5773
5922
  console.log(`\u25CB No ${config.spec.outputDir}/ directory found \u2014 nothing to migrate.`);
5774
5923
  console.log("");
5775
5924
  return;
@@ -5802,8 +5951,9 @@ function registerMigrateCommand(program) {
5802
5951
 
5803
5952
  // src/cli/commands/mcp-config.ts
5804
5953
  init_logger();
5805
- import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync15, rmSync as rmSync5 } from "fs";
5954
+ import { existsSync as existsSync28, readFileSync as readFileSync21, writeFileSync as writeFileSync15, rmSync as rmSync5 } from "fs";
5806
5955
  import { join as join30 } from "path";
5956
+ init_path_utils();
5807
5957
  var VALID_CLIENTS = ["cursor", "claude", "qoder"];
5808
5958
  function getMcpPath(projectRoot2, client) {
5809
5959
  switch (client) {
@@ -5811,8 +5961,10 @@ function getMcpPath(projectRoot2, client) {
5811
5961
  return join30(projectRoot2, ".cursor", "mcp.json");
5812
5962
  case "claude":
5813
5963
  return join30(projectRoot2, ".mcp.json");
5814
- case "qoder":
5815
- return join30(projectRoot2, ".qoder", "mcp.json");
5964
+ case "qoder": {
5965
+ const qoderDir = resolveQoderDir(projectRoot2);
5966
+ return join30(projectRoot2, qoderDir, "mcp.json");
5967
+ }
5816
5968
  }
5817
5969
  }
5818
5970
  function clientLabel(client) {
@@ -5834,20 +5986,20 @@ function registerMcpConfigCommands(program) {
5834
5986
  if (!client) return;
5835
5987
  logger.info(`Configuring MCP for ${clientLabel(client)}...`);
5836
5988
  writeMcpForClient(projectRoot2, client);
5837
- logger.info(`\u2713 ${clientLabel(client)} MCP configured at ${getMcpPath(projectRoot2, client)}`);
5989
+ logger.info(`[OK] ${clientLabel(client)} MCP configured at ${getMcpPath(projectRoot2, client)}`);
5838
5990
  });
5839
5991
  mcpCmd.command("remove <client>").description("Remove SpecLore MCP config from a specific AI client (cursor | claude | qoder)").action((clientArg) => {
5840
5992
  const projectRoot2 = process.cwd();
5841
5993
  const client = validateClient(clientArg);
5842
5994
  if (!client) return;
5843
5995
  const mcpPath = getMcpPath(projectRoot2, client);
5844
- if (!existsSync27(mcpPath)) {
5996
+ if (!existsSync28(mcpPath)) {
5845
5997
  logger.info(`${clientLabel(client)}: no MCP config file found at ${mcpPath}`);
5846
5998
  return;
5847
5999
  }
5848
6000
  const removed = removeMcpServerEntry(mcpPath, "speclore");
5849
6001
  if (removed) {
5850
- logger.info(`\u2713 Removed speclore from ${clientLabel(client)}: ${mcpPath}`);
6002
+ logger.info(`[OK] Removed speclore from ${clientLabel(client)}: ${mcpPath}`);
5851
6003
  } else {
5852
6004
  logger.info(`${clientLabel(client)}: speclore entry not found in ${mcpPath}`);
5853
6005
  }
@@ -5855,24 +6007,24 @@ function registerMcpConfigCommands(program) {
5855
6007
  mcpCmd.command("list").description("Show current SpecLore MCP configuration status for all clients").action(() => {
5856
6008
  const projectRoot2 = process.cwd();
5857
6009
  logger.info("SpecLore MCP Configuration Status");
5858
- logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
6010
+ logger.info("------------------------------");
5859
6011
  let anyFound = false;
5860
6012
  for (const client of VALID_CLIENTS) {
5861
6013
  const mcpPath = getMcpPath(projectRoot2, client);
5862
- if (!existsSync27(mcpPath)) {
5863
- logger.info(` \u25CB ${clientLabel(client)}: no config file (${mcpPath})`);
6014
+ if (!existsSync28(mcpPath)) {
6015
+ logger.info(` [-] ${clientLabel(client)}: no config file (${mcpPath})`);
5864
6016
  continue;
5865
6017
  }
5866
6018
  try {
5867
- const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
6019
+ const config = JSON.parse(readFileSync21(mcpPath, "utf-8"));
5868
6020
  if (config.mcpServers?.["speclore"]) {
5869
- logger.info(` \u2713 ${clientLabel(client)}: configured \u2192 ${mcpPath}`);
6021
+ logger.info(` [OK] ${clientLabel(client)}: configured -> ${mcpPath}`);
5870
6022
  anyFound = true;
5871
6023
  } else {
5872
- logger.info(` \u25CB ${clientLabel(client)}: config file exists but speclore not registered \u2192 ${mcpPath}`);
6024
+ logger.info(` [-] ${clientLabel(client)}: config file exists but speclore not registered -> ${mcpPath}`);
5873
6025
  }
5874
6026
  } catch {
5875
- logger.info(` \u2717 ${clientLabel(client)}: failed to parse ${mcpPath}`);
6027
+ logger.info(` [!] ${clientLabel(client)}: failed to parse ${mcpPath}`);
5876
6028
  }
5877
6029
  }
5878
6030
  if (!anyFound) {
@@ -5891,9 +6043,9 @@ function validateClient(arg) {
5891
6043
  return normalised;
5892
6044
  }
5893
6045
  function removeMcpServerEntry(mcpPath, serverName) {
5894
- if (!existsSync27(mcpPath)) return false;
6046
+ if (!existsSync28(mcpPath)) return false;
5895
6047
  try {
5896
- const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
6048
+ const config = JSON.parse(readFileSync21(mcpPath, "utf-8"));
5897
6049
  if (!config.mcpServers?.[serverName]) return false;
5898
6050
  delete config.mcpServers[serverName];
5899
6051
  const remainingServers = Object.keys(config.mcpServers ?? {});