@shahmilsaari/memory-core 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -356,11 +356,13 @@ Runs in the background and checks each file the moment you save it. You see viol
356
356
  Options:
357
357
  ```bash
358
358
  npx @shahmilsaari/memory-core watch --path src/ # watch a specific folder only
359
+ npx @shahmilsaari/memory-core watch --scan-on-start # snapshot-scan tracked files once, then keep watching
359
360
  npx @shahmilsaari/memory-core watch --verbose # show extra details
360
361
  npx @shahmilsaari/memory-core watch --debug # show prompt, diff, and raw model output
361
362
  ```
362
363
 
363
- Only checks source files — ignores `node_modules`, `dist`, config files, JSON, etc.
364
+ Only checks source files — ignores `node_modules`, `dist`, config files, JSON, etc.
365
+ `--scan-on-start` is useful after a big refactor because it refreshes current live stats automatically before normal save-based watch mode begins.
364
366
 
365
367
  ---
366
368
 
@@ -369,7 +371,7 @@ Only checks source files — ignores `node_modules`, `dist`, config files, JSON,
369
371
  ```bash
370
372
  npx @shahmilsaari/memory-core dashboard
371
373
  npx @shahmilsaari/memory-core dashboard --port 5178
372
- npx @shahmilsaari/memory-core dashboard --path src/
374
+ npx @shahmilsaari/memory-core dashboard --path /absolute/path/to/project
373
375
  npx @shahmilsaari/memory-core dashboard --no-watch
374
376
  ```
375
377
 
@@ -384,6 +386,7 @@ Starts a local Svelte dashboard at `http://localhost:5178` by default. The dashb
384
386
  - live reload for `.env`, `.memory-core.env`, `.memory-core.json`, and `.memory-core-stats.json`
385
387
 
386
388
  The WebSocket endpoint is local: `ws://localhost:5178/ws`. Runtime config changes are reloaded and pushed to the browser without restarting the dashboard after the dashboard process is running.
389
+ When `--path` is provided, it must point to the project root (the directory containing `.memory-core.json`).
387
390
 
388
391
  ---
389
392
 
@@ -394,9 +397,13 @@ npx @shahmilsaari/memory-core check --staged # check staged files
394
397
  npx @shahmilsaari/memory-core check --staged --verbose # with extra detail
395
398
  npx @shahmilsaari/memory-core check --staged --debug # show prompt, diff, and raw model output
396
399
  npx @shahmilsaari/memory-core check --ci # CI mode using memories.json
400
+ npx @shahmilsaari/memory-core check --all # scan all tracked source files (not just staged changes)
401
+ npx @shahmilsaari/memory-core check --all --path src/ # scan only tracked files under src/
397
402
  ```
398
403
 
399
- `--staged` is the same path used by the pre-commit hook. `--ci` reads `memories.json` and uses a deterministic CI-friendly diff check, so pull requests can enforce rules without a local database or Ollama setup.
404
+ `--staged` is the same path used by the pre-commit hook. `--ci` reads `memories.json` and uses a deterministic CI-friendly diff check, so pull requests can enforce rules without a local database or Ollama setup.
405
+ `--all` runs a full tracked-file snapshot check and exits non-zero if violations are found.
406
+ `--all` and `--ci` are mutually exclusive in the same command.
400
407
 
401
408
  ---
402
409
 
@@ -585,9 +592,13 @@ Generates `.github/workflows/memory-core.yml`. Adds a PR check that runs `npx @s
585
592
 
586
593
  ```bash
587
594
  npx @shahmilsaari/memory-core stats
595
+ npx @shahmilsaari/memory-core stats --reset
588
596
  ```
589
597
 
590
- Shows which rules fire most often and which files have the most violations. Useful for spotting systemic issues in the codebase.
598
+ Shows which rules fire most often and which files have the most violations.
599
+ - If live watch state exists, `stats` shows current live counters.
600
+ - Otherwise it shows historical counters recorded over time.
601
+ - Use `--reset` to clear counters and recent violation history.
591
602
 
592
603
  ---
593
604
 
@@ -966,8 +966,8 @@ var seeds = [
966
966
  // src/watcher.ts
967
967
  import { watch } from "chokidar";
968
968
  import { spawnSync } from "child_process";
969
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
970
- import { join as join7, relative as relative2 } from "path";
969
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
970
+ import { join as join7, relative as relative2, resolve as resolve4, sep } from "path";
971
971
  import chalk from "chalk";
972
972
 
973
973
  // src/generator.ts
@@ -1195,6 +1195,14 @@ var ResilientGraphRepository = class {
1195
1195
  var ChokidarWatchService = class {
1196
1196
  async start(options = {}) {
1197
1197
  await startWatch({
1198
+ path: options.path,
1199
+ verbose: options.verbose,
1200
+ debug: options.debug,
1201
+ scanOnStart: options.scanOnStart
1202
+ });
1203
+ }
1204
+ async scan(options = {}) {
1205
+ return scanFiles({
1198
1206
  path: options.path,
1199
1207
  verbose: options.verbose,
1200
1208
  debug: options.debug
@@ -1896,7 +1904,7 @@ var OUTPUT_FILES = [
1896
1904
  var AGENT_NAMES = [...new Set(OUTPUT_FILES.map((f) => f.agent))];
1897
1905
  Handlebars.registerHelper(
1898
1906
  "join",
1899
- (arr, sep) => Array.isArray(arr) ? arr.join(sep) : ""
1907
+ (arr, sep2) => Array.isArray(arr) ? arr.join(sep2) : ""
1900
1908
  );
1901
1909
  Handlebars.registerHelper(
1902
1910
  "bullet",
@@ -2284,25 +2292,62 @@ var SOURCE_EXTENSIONS3 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|
2284
2292
  var reasonMap = new Map(
2285
2293
  seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
2286
2294
  );
2287
- function recordViolations(violations) {
2288
- const statsPath = join7(process.cwd(), ".memory-core-stats.json");
2289
- let stats = { rules: {}, files: {} };
2290
- if (existsSync6(statsPath)) {
2291
- try {
2292
- stats = JSON.parse(readFileSync5(statsPath, "utf-8"));
2293
- } catch {
2294
- stats = { rules: {}, files: {} };
2295
+ function readStatsFile(statsPath) {
2296
+ if (!existsSync6(statsPath)) return { rules: {}, files: {} };
2297
+ try {
2298
+ return JSON.parse(readFileSync5(statsPath, "utf-8"));
2299
+ } catch {
2300
+ return { rules: {}, files: {} };
2301
+ }
2302
+ }
2303
+ function rebuildLiveCounters(byFile) {
2304
+ const rules = {};
2305
+ const files = {};
2306
+ for (const [file, violations] of Object.entries(byFile)) {
2307
+ if (!Array.isArray(violations) || violations.length === 0) continue;
2308
+ files[file] = violations.length;
2309
+ for (const violation of violations) {
2310
+ rules[violation.rule] = (rules[violation.rule] ?? 0) + 1;
2295
2311
  }
2296
2312
  }
2313
+ return { rules, files };
2314
+ }
2315
+ function resetLiveStats(cwd) {
2316
+ const statsPath = join7(cwd, ".memory-core-stats.json");
2317
+ const stats = readStatsFile(statsPath);
2318
+ stats.rules ??= {};
2319
+ stats.files ??= {};
2320
+ stats.live = {
2321
+ rules: {},
2322
+ files: {},
2323
+ byFile: {}
2324
+ };
2325
+ writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
2326
+ }
2327
+ function recordWatchResult(cwd, file, violations) {
2328
+ const statsPath = join7(cwd, ".memory-core-stats.json");
2329
+ const stats = readStatsFile(statsPath);
2297
2330
  stats.rules ??= {};
2298
2331
  stats.files ??= {};
2332
+ stats.live ??= { rules: {}, files: {}, byFile: {} };
2333
+ stats.live.byFile ??= {};
2334
+ if (violations.length === 0) {
2335
+ delete stats.live.byFile[file];
2336
+ } else {
2337
+ stats.live.byFile[file] = violations;
2338
+ }
2339
+ const live = rebuildLiveCounters(stats.live.byFile);
2340
+ stats.live.rules = live.rules;
2341
+ stats.live.files = live.files;
2299
2342
  for (const violation of violations) {
2300
2343
  stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
2301
2344
  if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
2302
2345
  }
2303
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2304
- const recent = violations.map((violation) => ({ ...violation, timestamp, source: "watch" }));
2305
- stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
2346
+ if (violations.length > 0) {
2347
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2348
+ const recent = violations.map((violation) => ({ ...violation, timestamp, source: "watch" }));
2349
+ stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
2350
+ }
2306
2351
  writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
2307
2352
  }
2308
2353
  function loadConfig(cwd) {
@@ -2333,7 +2378,7 @@ function getProfileRules(config2) {
2333
2378
  }
2334
2379
  return { rules, avoids };
2335
2380
  }
2336
- async function loadRelevantRules(config2, rel, diff, fallbackRules) {
2381
+ async function loadRelevantRules(cwd, config2, rel, diff, fallbackRules) {
2337
2382
  try {
2338
2383
  const query = buildContextQuery([
2339
2384
  rel,
@@ -2344,7 +2389,7 @@ async function loadRelevantRules(config2, rel, diff, fallbackRules) {
2344
2389
  ]);
2345
2390
  const memories = await retrieveContextualMemories({
2346
2391
  query,
2347
- cwd: process.cwd(),
2392
+ cwd,
2348
2393
  config: config2,
2349
2394
  limit: 15
2350
2395
  });
@@ -2363,10 +2408,11 @@ ${violation.file}`.toLowerCase();
2363
2408
  return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
2364
2409
  });
2365
2410
  }
2366
- async function verifyViolations(diff, violations, allowPatterns, debug) {
2411
+ async function verifyViolations(inputText, violations, allowPatterns, debug, mode = "diff") {
2367
2412
  if (violations.length === 0) return violations;
2413
+ const sourceLabel = mode === "snapshot" ? "file content" : "diff";
2368
2414
  const systemPrompt = `You are verifying candidate architecture violations.
2369
- Only keep violations that are directly supported by the diff.
2415
+ Only keep violations that are directly supported by the ${sourceLabel}.
2370
2416
  Reject speculative or weak matches.
2371
2417
  Treat these allowlisted patterns as intentional and valid:
2372
2418
  ${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${pattern}`).join("\n") : "(none)"}
@@ -2374,8 +2420,8 @@ ${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${
2374
2420
  Return strict JSON:
2375
2421
  {"violations":[{"rule":"...","file":"...","line":1,"issue":"...","suggestion":"...","reason":"..."}]}
2376
2422
  Do not include any text outside the JSON.`;
2377
- const userPrompt = `Diff:
2378
- ${diff.slice(0, 6e3)}
2423
+ const userPrompt = `${mode === "snapshot" ? "File content" : "Diff"}:
2424
+ ${inputText.slice(0, 6e3)}
2379
2425
 
2380
2426
  Candidate violations:
2381
2427
  ${JSON.stringify(violations, null, 2)}`;
@@ -2407,26 +2453,128 @@ async function loadIgnorePatterns() {
2407
2453
  return [];
2408
2454
  }
2409
2455
  }
2410
- async function checkFile(filePath, cwd, config2, verbose, debug) {
2411
- const rel = relative2(cwd, filePath);
2412
- let diff;
2413
- const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd });
2414
- if (headResult.stdout?.trim()) {
2415
- diff = headResult.stdout;
2456
+ function normalizeForGit(pathLike) {
2457
+ return pathLike.split(sep).join("/");
2458
+ }
2459
+ function listSourceFilesFromFilesystem(dir) {
2460
+ if (!existsSync6(dir)) return [];
2461
+ const files = [];
2462
+ const stack = [dir];
2463
+ while (stack.length > 0) {
2464
+ const current = stack.pop();
2465
+ let entries = [];
2466
+ try {
2467
+ entries = readdirSync3(current);
2468
+ } catch {
2469
+ continue;
2470
+ }
2471
+ for (const entry of entries) {
2472
+ const absolute = join7(current, entry);
2473
+ let isDirectory = false;
2474
+ let isFile = false;
2475
+ try {
2476
+ const stats = statSync2(absolute);
2477
+ isDirectory = stats.isDirectory();
2478
+ isFile = stats.isFile();
2479
+ } catch {
2480
+ continue;
2481
+ }
2482
+ if (isDirectory) {
2483
+ if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build" || entry === "coverage") {
2484
+ continue;
2485
+ }
2486
+ stack.push(absolute);
2487
+ continue;
2488
+ }
2489
+ if (isFile && SOURCE_EXTENSIONS3.test(absolute)) files.push(absolute);
2490
+ }
2491
+ }
2492
+ return files;
2493
+ }
2494
+ function listTrackedSourceFiles(projectRoot, watchPath) {
2495
+ const relPrefix = normalizeForGit(relative2(projectRoot, watchPath));
2496
+ const inRoot = relPrefix === "" || relPrefix === ".";
2497
+ const prefixWithSlash = inRoot ? "" : `${relPrefix}/`;
2498
+ const listed = spawnSync("git", ["ls-files"], { encoding: "utf-8", cwd: projectRoot });
2499
+ if (listed.status !== 0) {
2500
+ return listSourceFilesFromFilesystem(watchPath).sort();
2501
+ }
2502
+ const files = (listed.stdout ?? "").split("\n").filter((file) => file.length > 0).filter((file) => SOURCE_EXTENSIONS3.test(file)).filter((file) => inRoot || file.startsWith(prefixWithSlash)).map((file) => join7(projectRoot, file)).filter((file) => existsSync6(file));
2503
+ return [...new Set(files)].sort();
2504
+ }
2505
+ async function runSnapshotScan(projectRoot, watchPath, config2, verbose, debug, onEvent) {
2506
+ const files = listTrackedSourceFiles(projectRoot, watchPath);
2507
+ if (files.length === 0) {
2508
+ console.log(chalk.yellow("\n No tracked source files found for scan.\n"));
2509
+ return {
2510
+ filesChecked: 0,
2511
+ filesWithViolations: 0,
2512
+ violations: 0
2513
+ };
2514
+ }
2515
+ console.log(chalk.dim(`
2516
+ scanning ${files.length} tracked source files...
2517
+ `));
2518
+ const summary = {
2519
+ filesChecked: 0,
2520
+ filesWithViolations: 0,
2521
+ violations: 0
2522
+ };
2523
+ for (const filePath of files) {
2524
+ const rel = normalizeForGit(relative2(projectRoot, filePath));
2525
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2526
+ onEvent?.({ type: "saved", timestamp, file: rel });
2527
+ const result = await checkFile(filePath, projectRoot, config2, verbose, debug, "snapshot");
2528
+ if (result.type !== "checked") {
2529
+ if (result.type === "skipped") {
2530
+ onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
2531
+ } else {
2532
+ onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message: result.message });
2533
+ }
2534
+ continue;
2535
+ }
2536
+ summary.filesChecked += 1;
2537
+ if (result.violations.length === 0) {
2538
+ onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
2539
+ continue;
2540
+ }
2541
+ summary.filesWithViolations += 1;
2542
+ summary.violations += result.violations.length;
2543
+ onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations: result.violations });
2544
+ }
2545
+ return summary;
2546
+ }
2547
+ async function checkFile(filePath, projectRoot, config2, verbose, debug, mode = "diff") {
2548
+ const rel = relative2(projectRoot, filePath).split(sep).join("/");
2549
+ if (rel.startsWith("..")) return { type: "skipped", reason: "File is outside project root" };
2550
+ let inputText;
2551
+ if (mode === "snapshot") {
2552
+ if (!existsSync6(filePath)) return { type: "skipped", reason: "File no longer exists" };
2553
+ inputText = readFileSync5(filePath, "utf-8");
2554
+ if (!inputText.trim()) return { type: "skipped", reason: "File is empty" };
2416
2555
  } else {
2417
- const noIndexResult = spawnSync("git", ["diff", "--no-index", "/dev/null", rel], { encoding: "utf-8", cwd });
2418
- diff = noIndexResult.stdout ?? "";
2556
+ const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd: projectRoot });
2557
+ if (headResult.stdout?.trim()) {
2558
+ inputText = headResult.stdout;
2559
+ } else {
2560
+ const noIndexResult = spawnSync("git", ["diff", "--no-index", "/dev/null", rel], {
2561
+ encoding: "utf-8",
2562
+ cwd: projectRoot
2563
+ });
2564
+ inputText = noIndexResult.stdout ?? "";
2565
+ }
2566
+ if (!inputText.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
2419
2567
  }
2420
- if (!diff.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
2421
2568
  const { rules: fallbackRules, avoids } = getProfileRules(config2);
2422
- const rules = await loadRelevantRules(config2, rel, diff, fallbackRules);
2569
+ const rules = await loadRelevantRules(projectRoot, config2, rel, inputText, fallbackRules);
2423
2570
  if (rules.length === 0) return { type: "skipped", reason: "No applicable architecture rules" };
2424
- const MAX_DIFF = 6e3;
2425
- const truncated = diff.length > MAX_DIFF;
2426
- const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
2571
+ const MAX_INPUT = 6e3;
2572
+ const truncated = inputText.length > MAX_INPUT;
2573
+ const inputToSend = truncated ? inputText.slice(0, MAX_INPUT) + "\n\n[input truncated]" : inputText;
2427
2574
  if (verbose || debug) {
2575
+ const label = mode === "snapshot" ? "snapshot" : `${inputText.length} chars`;
2428
2576
  console.log(chalk.dim(`
2429
- [watch] checking ${rel} (${diff.length} chars)\u2026`));
2577
+ [watch] checking ${rel} (${label})\u2026`));
2430
2578
  }
2431
2579
  const rulesWithReasons = rules.map((r, i) => {
2432
2580
  const why = reasonMap.get(r);
@@ -2435,7 +2583,7 @@ async function checkFile(filePath, cwd, config2, verbose, debug) {
2435
2583
  }).join("\n");
2436
2584
  const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config2), ...await loadIgnorePatterns()])];
2437
2585
  const astViolations = findAstDeterministicViolationsForFile(rel, {
2438
- cwd,
2586
+ cwd: projectRoot,
2439
2587
  config: config2,
2440
2588
  rules,
2441
2589
  reasonLookup: reasonMap
@@ -2443,8 +2591,9 @@ async function checkFile(filePath, cwd, config2, verbose, debug) {
2443
2591
  ...violation,
2444
2592
  severity: "error"
2445
2593
  }));
2594
+ const analysisTarget = mode === "snapshot" ? "file content" : "file diff";
2446
2595
  const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
2447
- Analyze the file diff and identify ONLY clear, definite rule violations.
2596
+ Analyze the ${analysisTarget} and identify ONLY clear, definite rule violations.
2448
2597
  Use the WHY for each rule to understand intent and judge edge cases.
2449
2598
 
2450
2599
  Rules to enforce:
@@ -2464,16 +2613,19 @@ No text outside the JSON.`;
2464
2613
  console.log(chalk.dim(" \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"));
2465
2614
  console.log(systemPrompt);
2466
2615
  console.log(chalk.dim(" \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"));
2467
- console.log(chalk.gray(` [debug] diff length: ${diff.length} chars`));
2468
- console.log(chalk.dim(diffToSend));
2616
+ console.log(chalk.gray(` [debug] input length: ${inputText.length} chars`));
2617
+ console.log(chalk.dim(inputToSend));
2469
2618
  console.log(chalk.dim(" \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"));
2470
2619
  }
2471
2620
  try {
2621
+ const reviewPrompt = mode === "snapshot" ? `Review this file ${rel}:
2622
+
2623
+ ${inputToSend}` : `Review this diff for ${rel}:
2624
+
2625
+ ${inputToSend}`;
2472
2626
  const raw = await callChatModel([
2473
2627
  { role: "system", content: systemPrompt },
2474
- { role: "user", content: `Review this diff for ${rel}:
2475
-
2476
- ${diffToSend}` }
2628
+ { role: "user", content: reviewPrompt }
2477
2629
  ]);
2478
2630
  if (debug) {
2479
2631
  console.log(chalk.gray(" [debug] raw response:"));
@@ -2493,7 +2645,7 @@ ${diffToSend}` }
2493
2645
  } catch {
2494
2646
  violations = [];
2495
2647
  }
2496
- violations = await verifyViolations(diff, violations, allowPatterns, debug);
2648
+ violations = await verifyViolations(inputText, violations, allowPatterns, debug, mode);
2497
2649
  violations = [...astViolations, ...violations];
2498
2650
  violations = applyAllowPatterns(violations, allowPatterns);
2499
2651
  violations = violations.map((violation) => ({
@@ -2501,6 +2653,7 @@ ${diffToSend}` }
2501
2653
  code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
2502
2654
  }));
2503
2655
  if (violations.length === 0) {
2656
+ recordWatchResult(projectRoot, rel, []);
2504
2657
  console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no violations"));
2505
2658
  return { type: "checked", violations: [] };
2506
2659
  }
@@ -2522,7 +2675,7 @@ ${diffToSend}` }
2522
2675
  if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
2523
2676
  console.log();
2524
2677
  });
2525
- recordViolations(violations);
2678
+ recordWatchResult(projectRoot, rel, violations);
2526
2679
  console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
2527
2680
  console.log();
2528
2681
  return { type: "checked", violations };
@@ -2536,6 +2689,7 @@ ${diffToSend}` }
2536
2689
  code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
2537
2690
  }));
2538
2691
  if (violations.length === 0) {
2692
+ recordWatchResult(projectRoot, rel, []);
2539
2693
  console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no deterministic violations"));
2540
2694
  return { type: "checked", violations: [] };
2541
2695
  }
@@ -2557,15 +2711,56 @@ ${diffToSend}` }
2557
2711
  if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
2558
2712
  console.log();
2559
2713
  });
2560
- recordViolations(violations);
2714
+ recordWatchResult(projectRoot, rel, violations);
2561
2715
  console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
2562
2716
  console.log();
2563
2717
  return { type: "checked", violations };
2564
2718
  }
2565
2719
  }
2720
+ async function scanFiles(options = {}) {
2721
+ const projectRoot = resolve4(process.cwd());
2722
+ const watchPath = resolve4(projectRoot, options.path ?? ".");
2723
+ const config2 = loadConfig(projectRoot);
2724
+ if (!config2) {
2725
+ throw new Error("No .memory-core.json found. Run: memory-core init");
2726
+ }
2727
+ const { rules } = getProfileRules(config2);
2728
+ if (rules.length === 0) {
2729
+ console.log(chalk.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to scan.\n"));
2730
+ return {
2731
+ filesChecked: 0,
2732
+ filesWithViolations: 0,
2733
+ violations: 0
2734
+ };
2735
+ }
2736
+ resetLiveStats(projectRoot);
2737
+ console.log(chalk.cyan("\n archmind scan \u2014 checking tracked source files\n"));
2738
+ console.log(chalk.dim(` project: ${projectRoot}`));
2739
+ console.log(chalk.dim(` path: ${watchPath}`));
2740
+ console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
2741
+ console.log(chalk.dim(` rules: ${rules.length}
2742
+ `));
2743
+ const summary = await runSnapshotScan(
2744
+ projectRoot,
2745
+ watchPath,
2746
+ config2,
2747
+ options.verbose ?? false,
2748
+ options.debug ?? false,
2749
+ options.onEvent
2750
+ );
2751
+ const cleanFiles = summary.filesChecked - summary.filesWithViolations;
2752
+ console.log(chalk.bold("\n scan summary\n"));
2753
+ console.log(chalk.dim(` files checked: ${summary.filesChecked}`));
2754
+ console.log(chalk.dim(` files clean: ${cleanFiles}`));
2755
+ console.log(chalk.dim(` files with violations: ${summary.filesWithViolations}`));
2756
+ console.log(chalk.dim(` total violations: ${summary.violations}
2757
+ `));
2758
+ return summary;
2759
+ }
2566
2760
  async function startWatch(options = {}) {
2567
- const cwd = process.cwd();
2568
- const config2 = loadConfig(cwd);
2761
+ const projectRoot = resolve4(process.cwd());
2762
+ const watchPath = resolve4(projectRoot, options.path ?? ".");
2763
+ const config2 = loadConfig(projectRoot);
2569
2764
  const exitOnSetupFailure = options.exitOnSetupFailure ?? true;
2570
2765
  if (!config2) {
2571
2766
  const message = "No .memory-core.json found. Run: memory-core init";
@@ -2586,8 +2781,9 @@ async function startWatch(options = {}) {
2586
2781
  if (exitOnSetupFailure) process.exit(0);
2587
2782
  return;
2588
2783
  }
2589
- const watchPath = options.path ?? cwd;
2784
+ resetLiveStats(projectRoot);
2590
2785
  console.log(chalk.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
2786
+ console.log(chalk.dim(` project: ${projectRoot}`));
2591
2787
  console.log(chalk.dim(` watching: ${watchPath}`));
2592
2788
  console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
2593
2789
  console.log(chalk.dim(` rules: ${rules.length}`));
@@ -2599,6 +2795,18 @@ async function startWatch(options = {}) {
2599
2795
  model: getChatProviderLabel(),
2600
2796
  rules: rules.length
2601
2797
  });
2798
+ if (options.scanOnStart) {
2799
+ console.log(chalk.dim(" running initial snapshot scan before watch events..."));
2800
+ await runSnapshotScan(
2801
+ projectRoot,
2802
+ watchPath,
2803
+ config2,
2804
+ options.verbose ?? false,
2805
+ options.debug ?? false,
2806
+ options.onEvent
2807
+ );
2808
+ console.log(chalk.dim(" initial scan complete.\n"));
2809
+ }
2602
2810
  const pending = /* @__PURE__ */ new Map();
2603
2811
  const watcher = watch(watchPath, {
2604
2812
  ignored: [
@@ -2620,13 +2828,26 @@ async function startWatch(options = {}) {
2620
2828
  if (pending.has(filePath)) clearTimeout(pending.get(filePath));
2621
2829
  const timer = setTimeout(async () => {
2622
2830
  pending.delete(filePath);
2623
- const rel = relative2(cwd, filePath);
2831
+ const rel = normalizeForGit(relative2(projectRoot, filePath));
2832
+ if (rel.startsWith("..")) return;
2624
2833
  const timestamp = /* @__PURE__ */ new Date();
2625
2834
  console.log(chalk.dim(`
2626
2835
  [${timestamp.toLocaleTimeString()}] saved: ${rel}`));
2627
2836
  options.onEvent?.({ type: "saved", timestamp: timestamp.toISOString(), file: rel });
2628
- const result = await checkFile(filePath, cwd, config2, options.verbose ?? false, options.debug ?? false);
2837
+ const result = await checkFile(
2838
+ filePath,
2839
+ projectRoot,
2840
+ config2,
2841
+ options.verbose ?? false,
2842
+ options.debug ?? false,
2843
+ "diff"
2844
+ );
2629
2845
  if (result.type === "skipped") {
2846
+ if (result.reason === "No changes compared with HEAD") {
2847
+ recordWatchResult(projectRoot, rel, []);
2848
+ options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
2849
+ return;
2850
+ }
2630
2851
  options.onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
2631
2852
  return;
2632
2853
  }
@@ -2645,6 +2866,12 @@ async function startWatch(options = {}) {
2645
2866
  };
2646
2867
  watcher.on("add", handle);
2647
2868
  watcher.on("change", handle);
2869
+ watcher.on("unlink", (filePath) => {
2870
+ const rel = normalizeForGit(relative2(projectRoot, filePath));
2871
+ if (rel.startsWith("..")) return;
2872
+ recordWatchResult(projectRoot, rel, []);
2873
+ options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
2874
+ });
2648
2875
  watcher.on("error", (err) => {
2649
2876
  const message = err instanceof Error ? err.message : String(err);
2650
2877
  console.error(chalk.red(` watcher error: ${message}`));
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  retrieveMemorySelection,
21
21
  runMigrations,
22
22
  seeds
23
- } from "./chunk-PDQXIKL7.js";
23
+ } from "./chunk-WJG77BPO.js";
24
24
 
25
25
  // src/cli.ts
26
26
  import { Command } from "commander";
@@ -122,10 +122,18 @@ var reasonMap = new Map(
122
122
  );
123
123
  var HOOK_PATH = join2(".git", "hooks", "pre-commit");
124
124
  var HOOK_MARKER = "# archmind-memory-core";
125
- function buildHookScript(advisory) {
125
+ function buildHookBody(advisory) {
126
126
  const suffix = advisory ? " || true" : "";
127
- return `#!/bin/sh
128
- ${HOOK_MARKER}${advisory ? " advisory" : ""}
127
+ return `${HOOK_MARKER}${advisory ? " advisory" : ""}
128
+ if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
129
+ exit 0
130
+ fi
131
+ if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
132
+ exit 0
133
+ fi
134
+ if [ -n "\${SKIP_HOOKS:-}" ]; then
135
+ exit 0
136
+ fi
129
137
  if command -v memory-core >/dev/null 2>&1; then
130
138
  memory-core check --staged${suffix}
131
139
  elif [ -f "./node_modules/.bin/memory-core" ]; then
@@ -137,6 +145,26 @@ else
137
145
  fi
138
146
  `;
139
147
  }
148
+ function buildHookScript(advisory) {
149
+ return `#!/bin/sh
150
+
151
+ ${buildHookBody(advisory)}`;
152
+ }
153
+ function normalizeHookPreamble(content) {
154
+ const lines = content.split("\n");
155
+ const normalized = [];
156
+ let shebangSeen = false;
157
+ for (const line of lines) {
158
+ if (/^\s*#!\/bin\/sh\s*$/.test(line)) {
159
+ if (shebangSeen) continue;
160
+ shebangSeen = true;
161
+ normalized.push("#!/bin/sh");
162
+ continue;
163
+ }
164
+ normalized.push(line);
165
+ }
166
+ return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
167
+ }
140
168
  function recordViolations(violations, source = "hook") {
141
169
  const statsPath = join2(process.cwd(), ".memory-core-stats.json");
142
170
  let stats = { rules: {}, files: {} };
@@ -403,18 +431,26 @@ function installHook(advisory = true) {
403
431
  process.exit(1);
404
432
  }
405
433
  const script = buildHookScript(advisory);
434
+ const body = buildHookBody(advisory).trimEnd();
406
435
  if (existsSync2(HOOK_PATH)) {
407
436
  const existing = readFileSync2(HOOK_PATH, "utf-8");
408
437
  if (existing.includes(HOOK_MARKER)) {
409
438
  const markerIndex = existing.indexOf(HOOK_MARKER);
410
- const before = markerIndex > 1 ? existing.slice(0, markerIndex).trimEnd() + "\n\n" : "";
411
- writeFileSync2(HOOK_PATH, before + script);
439
+ const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
440
+ const normalizedBefore = normalizeHookPreamble(beforeRaw);
441
+ const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
442
+ const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
443
+ ${preamble}`;
444
+ writeFileSync2(HOOK_PATH, `${preambleWithShebang}
445
+
446
+ ${body}
447
+ `);
412
448
  chmodSync(HOOK_PATH, 493);
413
449
  const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
414
450
  console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
415
451
  return;
416
452
  }
417
- writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + script);
453
+ writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
418
454
  } else {
419
455
  writeFileSync2(HOOK_PATH, script);
420
456
  }
@@ -435,7 +471,7 @@ function uninstallHook() {
435
471
  return;
436
472
  }
437
473
  const markerIndex = content.indexOf(HOOK_MARKER);
438
- const before = markerIndex > 1 ? content.slice(0, markerIndex).trimEnd() : "";
474
+ const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
439
475
  if (before && before !== "#!/bin/sh") {
440
476
  writeFileSync2(HOOK_PATH, `${before}
441
477
  `);
@@ -575,6 +611,7 @@ ${diffToSend}` }
575
611
  });
576
612
  console.log(chalk.dim(" Fix the violations above, then commit again."));
577
613
  console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
614
+ console.log(chalk.dim(" Env bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
578
615
  console.log(chalk.dim(' To save as memory: memory-core remember "<lesson>"'));
579
616
  console.log();
580
617
  recordViolations(violations);
@@ -2036,8 +2073,23 @@ program.command("uninstall").description("Remove memory-core from the current pr
2036
2073
  console.log(chalk2.gray(" \u2713 cleaned .gitignore memory-core block"));
2037
2074
  }
2038
2075
  });
2039
- program.command("stats").description("Show violation counters recorded by check and watch").action(() => {
2076
+ program.command("stats").description("Show violation counters recorded by check and watch").option("--reset", "Reset violation counters and recent history").action((opts) => {
2040
2077
  const statsPath = join3(process.cwd(), ".memory-core-stats.json");
2078
+ if (opts.reset) {
2079
+ const emptyStats = {
2080
+ rules: {},
2081
+ files: {},
2082
+ live: {
2083
+ rules: {},
2084
+ files: {},
2085
+ byFile: {}
2086
+ },
2087
+ recentViolations: []
2088
+ };
2089
+ writeFileSync3(statsPath, JSON.stringify(emptyStats, null, 2) + "\n", "utf-8");
2090
+ console.log(chalk2.green("\n Violation stats reset.\n"));
2091
+ return;
2092
+ }
2041
2093
  if (!existsSync3(statsPath)) {
2042
2094
  console.log(chalk2.yellow("\n No violation stats recorded yet.\n"));
2043
2095
  return;
@@ -2051,12 +2103,31 @@ program.command("stats").description("Show violation counters recorded by check
2051
2103
  console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${count}`);
2052
2104
  });
2053
2105
  };
2054
- printTop("Top rules", stats.rules);
2055
- printTop("Top files", stats.files);
2056
- console.log();
2106
+ const liveRules = stats.live?.rules ?? {};
2107
+ const liveFiles = stats.live?.files ?? {};
2108
+ const hasLiveState = !!stats.live;
2109
+ const hasLiveViolations = Object.keys(liveRules).length > 0 || Object.keys(liveFiles).length > 0;
2110
+ printTop(
2111
+ hasLiveState ? "Top rules (current watch state)" : "Top rules",
2112
+ hasLiveState ? liveRules : stats.rules
2113
+ );
2114
+ printTop(
2115
+ hasLiveState ? "Top files (current watch state)" : "Top files",
2116
+ hasLiveState ? liveFiles : stats.files
2117
+ );
2118
+ if (!hasLiveState) {
2119
+ console.log(chalk2.dim("\n Note: these counters are historical events, not live current code state."));
2120
+ console.log(chalk2.dim(" Start watch for live counters, or reset with: memory-core stats --reset\n"));
2121
+ } else {
2122
+ if (hasLiveViolations) {
2123
+ console.log(chalk2.dim("\n Live counters auto-refresh while watch is running.\n"));
2124
+ } else {
2125
+ console.log(chalk2.dim("\n Current live state has no violations.\n"));
2126
+ }
2127
+ }
2057
2128
  });
2058
2129
  program.command("dashboard").description("Start the live Svelte dashboard with WebSocket watch events").option("-p, --port <port>", "Dashboard port", "5178").option("--path <dir>", "Directory to watch (default: current directory)").option("--no-watch", "Serve the dashboard without starting file watch").action(async (opts) => {
2059
- const { startDashboard } = await import("./dashboard-server-AUX4BQP6.js");
2130
+ const { startDashboard } = await import("./dashboard-server-OEDFMSFB.js");
2060
2131
  await startDashboard({
2061
2132
  port: parseInt(opts.port, 10),
2062
2133
  path: opts.path,
@@ -2460,16 +2531,30 @@ hook.command("install").description("Install pre-commit hook (advisory mode by d
2460
2531
  hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
2461
2532
  uninstallHook();
2462
2533
  });
2463
- program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
2534
+ program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--all", "Check all tracked source files, including already-committed files").option("--path <dir>", "Directory to check for --all mode (default: current directory)").option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
2535
+ if (opts.ci && opts.all) {
2536
+ console.error(chalk2.red("\n Choose one mode: --ci or --all.\n"));
2537
+ process.exit(1);
2538
+ }
2464
2539
  if (opts.ci) {
2465
2540
  await checkCi({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
2466
2541
  return;
2467
2542
  }
2543
+ if (opts.all) {
2544
+ const summary = await phase1.providers.watchService.scan({
2545
+ path: opts.path,
2546
+ verbose: opts.verbose,
2547
+ debug: opts.debug
2548
+ });
2549
+ if (summary.violations > 0) process.exit(1);
2550
+ return;
2551
+ }
2468
2552
  await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
2469
2553
  });
2470
- program.command("watch").description("Watch source files and check violations in real-time on every save").option("--path <dir>", "Directory to watch (default: current directory)").option("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
2554
+ program.command("watch").description("Watch source files and check violations in real-time on every save").option("--path <dir>", "Directory to watch (default: current directory)").option("--scan-on-start", "Run an initial full snapshot scan before watching file changes").option("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
2471
2555
  await phase1.providers.watchService.start({
2472
2556
  path: opts.path,
2557
+ scanOnStart: opts.scanOnStart,
2473
2558
  verbose: opts.verbose,
2474
2559
  debug: opts.debug
2475
2560
  });
@@ -12,13 +12,13 @@ import {
12
12
  saveMemory,
13
13
  startWatch,
14
14
  updateMemory
15
- } from "./chunk-PDQXIKL7.js";
15
+ } from "./chunk-WJG77BPO.js";
16
16
 
17
17
  // src/dashboard-server.ts
18
18
  import { createHash } from "crypto";
19
19
  import { createReadStream, existsSync, readFileSync, watch } from "fs";
20
20
  import { createServer } from "http";
21
- import { extname, join, normalize, relative } from "path";
21
+ import { extname, join, normalize, relative, resolve } from "path";
22
22
  import { fileURLToPath } from "url";
23
23
  import chalk from "chalk";
24
24
  var clients = /* @__PURE__ */ new Set();
@@ -46,6 +46,7 @@ var snapshotBroadcastTimer;
46
46
  var snapshotBroadcastInFlight = false;
47
47
  var snapshotBroadcastQueued = false;
48
48
  var snapshotBroadcastForceRefresh = false;
49
+ var projectRoot = process.cwd();
49
50
  function readJsonFile(path, fallback) {
50
51
  if (!existsSync(path)) return fallback;
51
52
  try {
@@ -55,7 +56,7 @@ function readJsonFile(path, fallback) {
55
56
  }
56
57
  }
57
58
  function readProjectConfig() {
58
- return readJsonFile(join(process.cwd(), ".memory-core.json"), null);
59
+ return readJsonFile(join(projectRoot, ".memory-core.json"), null);
59
60
  }
60
61
  function parseEnvFile(raw) {
61
62
  const values = {};
@@ -71,8 +72,8 @@ function parseEnvFile(raw) {
71
72
  return values;
72
73
  }
73
74
  function getRuntimeEnvPath() {
74
- const memoryEnv = join(process.cwd(), ".memory-core.env");
75
- return existsSync(memoryEnv) ? memoryEnv : join(process.cwd(), ".env");
75
+ const memoryEnv = join(projectRoot, ".memory-core.env");
76
+ return existsSync(memoryEnv) ? memoryEnv : join(projectRoot, ".env");
76
77
  }
77
78
  function reloadRuntimeEnv() {
78
79
  const envPath = getRuntimeEnvPath();
@@ -91,18 +92,20 @@ function invalidateSnapshotBase() {
91
92
  snapshotBaseCache = void 0;
92
93
  }
93
94
  function readStats() {
94
- return readJsonFile(join(process.cwd(), ".memory-core-stats.json"), { rules: {}, files: {} });
95
+ return readJsonFile(join(projectRoot, ".memory-core-stats.json"), { rules: {}, files: {} });
95
96
  }
96
97
  function topEntries(values = {}, limit = 8) {
97
98
  return Object.entries(values).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([name, count]) => ({ name, count }));
98
99
  }
99
100
  function buildStatsPayload() {
100
101
  const stats = readStats();
102
+ const rules = stats.live?.rules ?? stats.rules ?? {};
103
+ const files = stats.live?.files ?? stats.files ?? {};
101
104
  return {
102
- rules: stats.rules ?? {},
103
- files: stats.files ?? {},
104
- topRules: topEntries(stats.rules),
105
- topFiles: topEntries(stats.files),
105
+ rules,
106
+ files,
107
+ topRules: topEntries(rules),
108
+ topFiles: topEntries(files),
106
109
  recentViolations: stats.recentViolations ?? []
107
110
  };
108
111
  }
@@ -206,12 +209,12 @@ async function getModelStatus(forceRefresh = false) {
206
209
  return status;
207
210
  }
208
211
  async function getRuntimeStatus(config) {
209
- const detected = detectProject(process.cwd());
212
+ const detected = detectProject(projectRoot);
210
213
  const declaredArchitectures = [
211
214
  config?.backendArchitecture,
212
215
  config?.frontendFramework
213
216
  ].filter((value) => typeof value === "string" && value.length > 0);
214
- const activeArchitectures = inferProjectArchitectures(process.cwd(), config);
217
+ const activeArchitectures = inferProjectArchitectures(projectRoot, config);
215
218
  const database = parseDatabaseUrl(Config.databaseUrl);
216
219
  let postgres = {
217
220
  ...database,
@@ -242,7 +245,7 @@ async function getRuntimeStatus(config) {
242
245
  const model = await modelPromise;
243
246
  return {
244
247
  project: {
245
- name: config?.projectName ?? process.cwd().split("/").pop() ?? "project",
248
+ name: config?.projectName ?? projectRoot.split("/").pop() ?? "project",
246
249
  type: config?.projectType ?? "unknown",
247
250
  language: config?.language ?? detected.language,
248
251
  initialized: config !== null,
@@ -376,7 +379,7 @@ async function handleApi(req, res, url) {
376
379
  return;
377
380
  }
378
381
  const config = readProjectConfig();
379
- const activeArchitectures = inferProjectArchitectures(process.cwd(), config);
382
+ const activeArchitectures = inferProjectArchitectures(projectRoot, config);
380
383
  await saveMemory({
381
384
  type: typeof body.type === "string" ? body.type : "rule",
382
385
  scope: typeof body.scope === "string" ? body.scope : "project",
@@ -635,14 +638,14 @@ function startConfigWatch() {
635
638
  }, 150);
636
639
  };
637
640
  for (const file of watchedFiles) {
638
- const filePath = join(process.cwd(), file);
641
+ const filePath = join(projectRoot, file);
639
642
  if (!existsSync(filePath)) continue;
640
643
  watchedPaths.add(filePath);
641
644
  watchers.push(watch(filePath, () => reload(filePath)));
642
645
  }
643
- watchers.push(watch(process.cwd(), (_eventType, filename) => {
646
+ watchers.push(watch(projectRoot, (_eventType, filename) => {
644
647
  if (typeof filename !== "string" || !watchedFiles.includes(filename)) return;
645
- const filePath = join(process.cwd(), filename);
648
+ const filePath = join(projectRoot, filename);
646
649
  if (!existsSync(filePath)) return;
647
650
  if (!watchedPaths.has(filePath)) {
648
651
  watchedPaths.add(filePath);
@@ -656,6 +659,7 @@ function startConfigWatch() {
656
659
  };
657
660
  }
658
661
  async function startDashboard(options = {}) {
662
+ projectRoot = resolve(options.path ?? process.cwd());
659
663
  reloadRuntimeEnv();
660
664
  const port = options.port ?? 5178;
661
665
  const stopConfigWatch = startConfigWatch();
@@ -683,8 +687,8 @@ async function startDashboard(options = {}) {
683
687
  }
684
688
  void closePool();
685
689
  });
686
- await new Promise((resolve) => {
687
- server.listen(port, resolve);
690
+ await new Promise((resolve2) => {
691
+ server.listen(port, resolve2);
688
692
  });
689
693
  console.log(chalk.green(`
690
694
  Dashboard: http://localhost:${port}
@@ -692,7 +696,7 @@ async function startDashboard(options = {}) {
692
696
  if (options.watch ?? true) {
693
697
  watcherStatus.enabled = true;
694
698
  void startWatch({
695
- path: options.path,
699
+ path: projectRoot,
696
700
  onEvent: handleWatchEvent,
697
701
  exitOnSetupFailure: false
698
702
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shahmilsaari/memory-core",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Universal AI memory core — generate AI context files from architecture profiles with RAG support",
5
5
  "homepage": "https://memory-core.shahmilsaari.my/",
6
6
  "type": "module",