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