memorix 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/dist/index.js CHANGED
@@ -3,6 +3,12 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
6
12
  var __esm = (fn, res) => function __init() {
7
13
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
14
  };
@@ -470,7 +476,8 @@ var init_types = __esm({
470
476
  "discovery": "\u{1F7E3}",
471
477
  "why-it-exists": "\u{1F7E0}",
472
478
  "decision": "\u{1F7E4}",
473
- "trade-off": "\u2696\uFE0F"
479
+ "trade-off": "\u2696\uFE0F",
480
+ "reasoning": "\u{1F9E0}"
474
481
  };
475
482
  TOPIC_KEY_FAMILIES = {
476
483
  "architecture": ["architecture", "design", "adr", "structure", "pattern"],
@@ -483,6 +490,146 @@ var init_types = __esm({
483
490
  }
484
491
  });
485
492
 
493
+ // src/config/yaml-loader.ts
494
+ var yaml_loader_exports = {};
495
+ __export(yaml_loader_exports, {
496
+ initProjectRoot: () => initProjectRoot,
497
+ loadYamlConfig: () => loadYamlConfig,
498
+ resetYamlConfigCache: () => resetYamlConfigCache
499
+ });
500
+ import { existsSync, readFileSync } from "fs";
501
+ import { join } from "path";
502
+ import { homedir } from "os";
503
+ function initProjectRoot(root) {
504
+ globalProjectRoot = root;
505
+ cachedYamlConfig = null;
506
+ cachedProjectRoot = null;
507
+ }
508
+ function loadYamlConfig(projectRoot) {
509
+ const resolvedRoot = projectRoot ?? globalProjectRoot ?? null;
510
+ if (cachedYamlConfig !== null && cachedProjectRoot === resolvedRoot) {
511
+ return cachedYamlConfig;
512
+ }
513
+ const userYaml = join(homedir(), ".memorix", "memorix.yml");
514
+ const projectYaml = resolvedRoot ? join(resolvedRoot, "memorix.yml") : null;
515
+ let userConfig = {};
516
+ let projectConfig = {};
517
+ if (existsSync(userYaml)) {
518
+ try {
519
+ userConfig = parseYaml(readFileSync(userYaml, "utf-8"));
520
+ } catch (err) {
521
+ console.error(`[memorix] Warning: Failed to parse ${userYaml}: ${err}`);
522
+ }
523
+ }
524
+ if (projectYaml && existsSync(projectYaml)) {
525
+ try {
526
+ projectConfig = parseYaml(readFileSync(projectYaml, "utf-8"));
527
+ } catch (err) {
528
+ console.error(`[memorix] Warning: Failed to parse ${projectYaml}: ${err}`);
529
+ }
530
+ }
531
+ cachedYamlConfig = {
532
+ ...userConfig,
533
+ ...projectConfig,
534
+ // Deep merge for nested objects where both exist
535
+ llm: { ...userConfig.llm, ...projectConfig.llm },
536
+ embedding: { ...userConfig.embedding, ...projectConfig.embedding },
537
+ git: { ...userConfig.git, ...projectConfig.git },
538
+ behavior: { ...userConfig.behavior, ...projectConfig.behavior },
539
+ server: { ...userConfig.server, ...projectConfig.server },
540
+ team: { ...userConfig.team, ...projectConfig.team }
541
+ };
542
+ cachedProjectRoot = resolvedRoot;
543
+ return cachedYamlConfig;
544
+ }
545
+ function resetYamlConfigCache() {
546
+ cachedYamlConfig = null;
547
+ cachedProjectRoot = null;
548
+ }
549
+ function parseYaml(content) {
550
+ try {
551
+ const yaml = __require("js-yaml");
552
+ return yaml.load(content) ?? {};
553
+ } catch {
554
+ try {
555
+ const matter9 = __require("gray-matter");
556
+ const parsed = matter9(`---
557
+ ${content}
558
+ ---`);
559
+ return parsed.data ?? {};
560
+ } catch {
561
+ console.error("[memorix] YAML parse failed \u2014 check memorix.yml syntax");
562
+ return {};
563
+ }
564
+ }
565
+ }
566
+ var cachedYamlConfig, cachedProjectRoot, globalProjectRoot;
567
+ var init_yaml_loader = __esm({
568
+ "src/config/yaml-loader.ts"() {
569
+ "use strict";
570
+ init_esm_shims();
571
+ cachedYamlConfig = null;
572
+ cachedProjectRoot = null;
573
+ globalProjectRoot = null;
574
+ }
575
+ });
576
+
577
+ // src/config/dotenv-loader.ts
578
+ var dotenv_loader_exports = {};
579
+ __export(dotenv_loader_exports, {
580
+ getLoadedEnvFiles: () => getLoadedEnvFiles,
581
+ loadDotenv: () => loadDotenv,
582
+ resetDotenv: () => resetDotenv
583
+ });
584
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
585
+ import { join as join2 } from "path";
586
+ import { homedir as homedir2 } from "os";
587
+ import { parse } from "dotenv";
588
+ function loadEnvFile(filePath) {
589
+ if (!existsSync2(filePath)) return;
590
+ const parsed = parse(readFileSync2(filePath, "utf-8"));
591
+ for (const [key, value] of Object.entries(parsed)) {
592
+ if (!(key in process.env)) {
593
+ process.env[key] = value;
594
+ injectedKeys.add(key);
595
+ }
596
+ }
597
+ loadedEnvFiles.push(filePath);
598
+ }
599
+ function loadDotenv(projectRoot) {
600
+ if (dotenvLoaded && dotenvProjectRoot === (projectRoot ?? null)) return;
601
+ loadedEnvFiles.length = 0;
602
+ if (projectRoot) {
603
+ loadEnvFile(join2(projectRoot, ".env"));
604
+ }
605
+ loadEnvFile(join2(homedir2(), ".memorix", ".env"));
606
+ dotenvLoaded = true;
607
+ dotenvProjectRoot = projectRoot ?? null;
608
+ }
609
+ function resetDotenv() {
610
+ for (const key of injectedKeys) {
611
+ delete process.env[key];
612
+ }
613
+ injectedKeys.clear();
614
+ dotenvLoaded = false;
615
+ dotenvProjectRoot = null;
616
+ loadedEnvFiles.length = 0;
617
+ }
618
+ function getLoadedEnvFiles() {
619
+ return loadedEnvFiles;
620
+ }
621
+ var dotenvLoaded, dotenvProjectRoot, loadedEnvFiles, injectedKeys;
622
+ var init_dotenv_loader = __esm({
623
+ "src/config/dotenv-loader.ts"() {
624
+ "use strict";
625
+ init_esm_shims();
626
+ dotenvLoaded = false;
627
+ dotenvProjectRoot = null;
628
+ loadedEnvFiles = [];
629
+ injectedKeys = /* @__PURE__ */ new Set();
630
+ }
631
+ });
632
+
486
633
  // src/config.ts
487
634
  var config_exports = {};
488
635
  __export(config_exports, {
@@ -491,22 +638,29 @@ __export(config_exports, {
491
638
  getEmbeddingDimensions: () => getEmbeddingDimensions,
492
639
  getEmbeddingMode: () => getEmbeddingMode,
493
640
  getEmbeddingModel: () => getEmbeddingModel,
641
+ getGitConfig: () => getGitConfig,
494
642
  getLLMApiKey: () => getLLMApiKey,
495
643
  getLLMBaseUrl: () => getLLMBaseUrl,
496
644
  getLLMModel: () => getLLMModel,
497
645
  getLLMProvider: () => getLLMProvider,
646
+ getLoadedEnvFiles: () => getLoadedEnvFiles,
647
+ getServerConfig: () => getServerConfig,
648
+ getTeamConfig: () => getTeamConfig,
649
+ loadDotenv: () => loadDotenv,
498
650
  loadFileConfig: () => loadFileConfig,
499
- resetConfigCache: () => resetConfigCache
651
+ loadYamlConfig: () => loadYamlConfig,
652
+ resetConfigCache: () => resetConfigCache,
653
+ resetDotenv: () => resetDotenv
500
654
  });
501
- import { existsSync, readFileSync } from "fs";
502
- import { join } from "path";
503
- import { homedir } from "os";
655
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
656
+ import { join as join3 } from "path";
657
+ import { homedir as homedir3 } from "os";
504
658
  function loadFileConfig() {
505
659
  if (cachedConfig !== null) return cachedConfig;
506
- const configPath = join(homedir(), ".memorix", "config.json");
660
+ const configPath = join3(homedir3(), ".memorix", "config.json");
507
661
  try {
508
- if (existsSync(configPath)) {
509
- cachedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
662
+ if (existsSync3(configPath)) {
663
+ cachedConfig = JSON.parse(readFileSync3(configPath, "utf-8"));
510
664
  return cachedConfig;
511
665
  }
512
666
  } catch {
@@ -518,10 +672,14 @@ function resetConfigCache() {
518
672
  cachedConfig = null;
519
673
  }
520
674
  function getLLMApiKey() {
521
- return process.env.MEMORIX_LLM_API_KEY || loadFileConfig().llm?.apiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENROUTER_API_KEY || void 0;
675
+ return process.env.MEMORIX_LLM_API_KEY || // LLM-specific (优先级最高)
676
+ process.env.MEMORIX_API_KEY || // Unified API key (fallback)
677
+ loadYamlConfig().llm?.apiKey || loadFileConfig().llm?.apiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENROUTER_API_KEY || void 0;
522
678
  }
523
679
  function getLLMProvider() {
524
680
  if (process.env.MEMORIX_LLM_PROVIDER) return process.env.MEMORIX_LLM_PROVIDER;
681
+ const yml = loadYamlConfig();
682
+ if (yml.llm?.provider) return yml.llm.provider;
525
683
  const cfg = loadFileConfig();
526
684
  if (cfg.llm?.provider) return cfg.llm.provider;
527
685
  if (process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) return "anthropic";
@@ -529,14 +687,17 @@ function getLLMProvider() {
529
687
  return "openai";
530
688
  }
531
689
  function getLLMModel(providerDefault) {
532
- return process.env.MEMORIX_LLM_MODEL || loadFileConfig().llm?.model || providerDefault;
690
+ return process.env.MEMORIX_LLM_MODEL || loadYamlConfig().llm?.model || loadFileConfig().llm?.model || providerDefault;
533
691
  }
534
692
  function getLLMBaseUrl(providerDefault) {
535
- return process.env.MEMORIX_LLM_BASE_URL || loadFileConfig().llm?.baseUrl || providerDefault;
693
+ return process.env.MEMORIX_LLM_BASE_URL || loadYamlConfig().llm?.baseUrl || loadFileConfig().llm?.baseUrl || providerDefault;
536
694
  }
537
695
  function getEmbeddingMode() {
538
696
  const env = process.env.MEMORIX_EMBEDDING?.toLowerCase()?.trim();
539
697
  if (env === "fastembed" || env === "transformers" || env === "api" || env === "auto") return env;
698
+ const yml = loadYamlConfig();
699
+ const ymlEmb = yml.embedding?.provider;
700
+ if (ymlEmb === "fastembed" || ymlEmb === "transformers" || ymlEmb === "api" || ymlEmb === "auto") return ymlEmb;
540
701
  const cfg = loadFileConfig();
541
702
  if (cfg.embedding === "fastembed" || cfg.embedding === "transformers" || cfg.embedding === "api" || cfg.embedding === "auto") {
542
703
  return cfg.embedding;
@@ -544,26 +705,42 @@ function getEmbeddingMode() {
544
705
  return "off";
545
706
  }
546
707
  function getEmbeddingApiKey() {
547
- return process.env.MEMORIX_EMBEDDING_API_KEY || loadFileConfig().embeddingApi?.apiKey || process.env.MEMORIX_LLM_API_KEY || loadFileConfig().llm?.apiKey || process.env.OPENAI_API_KEY || void 0;
708
+ return process.env.MEMORIX_EMBEDDING_API_KEY || // Embedding-specific (优先级最高)
709
+ process.env.MEMORIX_API_KEY || // Unified API key (fallback)
710
+ process.env.MEMORIX_LLM_API_KEY || loadYamlConfig().embedding?.apiKey || loadFileConfig().embeddingApi?.apiKey || loadYamlConfig().llm?.apiKey || loadFileConfig().llm?.apiKey || process.env.OPENAI_API_KEY || void 0;
548
711
  }
549
712
  function getEmbeddingBaseUrl() {
550
- return process.env.MEMORIX_EMBEDDING_BASE_URL || loadFileConfig().embeddingApi?.baseUrl || process.env.MEMORIX_LLM_BASE_URL || loadFileConfig().llm?.baseUrl || "https://api.openai.com/v1";
713
+ return process.env.MEMORIX_EMBEDDING_BASE_URL || loadYamlConfig().embedding?.baseUrl || loadFileConfig().embeddingApi?.baseUrl || process.env.MEMORIX_LLM_BASE_URL || loadYamlConfig().llm?.baseUrl || loadFileConfig().llm?.baseUrl || "https://api.openai.com/v1";
551
714
  }
552
715
  function getEmbeddingModel() {
553
- return process.env.MEMORIX_EMBEDDING_MODEL || loadFileConfig().embeddingApi?.model || "text-embedding-3-small";
716
+ return process.env.MEMORIX_EMBEDDING_MODEL || loadYamlConfig().embedding?.model || loadFileConfig().embeddingApi?.model || "text-embedding-3-small";
554
717
  }
555
718
  function getEmbeddingDimensions() {
556
719
  const envDim = process.env.MEMORIX_EMBEDDING_DIMENSIONS;
557
720
  if (envDim) return parseInt(envDim, 10);
721
+ const ymlDim = loadYamlConfig().embedding?.dimensions;
722
+ if (ymlDim) return ymlDim;
558
723
  const cfgDim = loadFileConfig().embeddingApi?.dimensions;
559
724
  if (cfgDim) return cfgDim;
560
725
  return null;
561
726
  }
727
+ function getGitConfig() {
728
+ return loadYamlConfig().git ?? {};
729
+ }
730
+ function getServerConfig() {
731
+ return loadYamlConfig().server ?? {};
732
+ }
733
+ function getTeamConfig() {
734
+ return loadYamlConfig().team ?? {};
735
+ }
562
736
  var cachedConfig;
563
737
  var init_config = __esm({
564
738
  "src/config.ts"() {
565
739
  "use strict";
566
740
  init_esm_shims();
741
+ init_yaml_loader();
742
+ init_dotenv_loader();
743
+ init_yaml_loader();
567
744
  cachedConfig = null;
568
745
  }
569
746
  });
@@ -575,8 +752,8 @@ __export(fastembed_provider_exports, {
575
752
  });
576
753
  import { createHash } from "crypto";
577
754
  import { readFile, writeFile, mkdir } from "fs/promises";
578
- import { join as join2 } from "path";
579
- import { homedir as homedir2 } from "os";
755
+ import { join as join4 } from "path";
756
+ import { homedir as homedir4 } from "os";
580
757
  function textHash(text) {
581
758
  return createHash("sha256").update(text).digest("hex").slice(0, 16);
582
759
  }
@@ -604,8 +781,8 @@ var init_fastembed_provider = __esm({
604
781
  "src/embedding/fastembed-provider.ts"() {
605
782
  "use strict";
606
783
  init_esm_shims();
607
- CACHE_DIR = process.env.MEMORIX_DATA_DIR || join2(homedir2(), ".memorix", "data");
608
- CACHE_FILE = join2(CACHE_DIR, ".embedding-cache.json");
784
+ CACHE_DIR = process.env.MEMORIX_DATA_DIR || join4(homedir4(), ".memorix", "data");
785
+ CACHE_FILE = join4(CACHE_DIR, ".embedding-cache.json");
609
786
  cache = /* @__PURE__ */ new Map();
610
787
  MAX_CACHE_SIZE = 5e3;
611
788
  diskCacheDirty = false;
@@ -631,8 +808,8 @@ var init_fastembed_provider = __esm({
631
808
  }
632
809
  async embed(text) {
633
810
  const hash = textHash(text);
634
- const cached = cache.get(hash);
635
- if (cached) return cached;
811
+ const cached2 = cache.get(hash);
812
+ if (cached2) return cached2;
636
813
  const raw = await this.model.queryEmbed(text);
637
814
  const result = Array.from(raw);
638
815
  if (result.length !== this.dimensions) {
@@ -647,9 +824,9 @@ var init_fastembed_provider = __esm({
647
824
  const uncachedTexts = [];
648
825
  for (let i = 0; i < texts.length; i++) {
649
826
  const hash = textHash(texts[i]);
650
- const cached = cache.get(hash);
651
- if (cached) {
652
- results[i] = cached;
827
+ const cached2 = cache.get(hash);
828
+ if (cached2) {
829
+ results[i] = cached2;
653
830
  } else {
654
831
  uncachedIndices.push(i);
655
832
  uncachedTexts.push(texts[i]);
@@ -718,8 +895,8 @@ var init_transformers_provider = __esm({
718
895
  return new _TransformersProvider(extractor);
719
896
  }
720
897
  async embed(text) {
721
- const cached = cache2.get(text);
722
- if (cached) return cached;
898
+ const cached2 = cache2.get(text);
899
+ if (cached2) return cached2;
723
900
  const output = await this.extractor(text, {
724
901
  pooling: "mean",
725
902
  normalize: true
@@ -736,9 +913,9 @@ var init_transformers_provider = __esm({
736
913
  const uncachedIndices = [];
737
914
  const uncachedTexts = [];
738
915
  for (let i = 0; i < texts.length; i++) {
739
- const cached = cache2.get(texts[i]);
740
- if (cached) {
741
- results[i] = cached;
916
+ const cached2 = cache2.get(texts[i]);
917
+ if (cached2) {
918
+ results[i] = cached2;
742
919
  } else {
743
920
  uncachedIndices.push(i);
744
921
  uncachedTexts.push(texts[i]);
@@ -777,8 +954,8 @@ __export(api_provider_exports, {
777
954
  });
778
955
  import { createHash as createHash2 } from "crypto";
779
956
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
780
- import { join as join3 } from "path";
781
- import { homedir as homedir3 } from "os";
957
+ import { join as join5 } from "path";
958
+ import { homedir as homedir5 } from "os";
782
959
  function normalizeText(text) {
783
960
  return text.replace(/\s+/g, " ").trim().slice(0, MAX_INPUT_CHARS);
784
961
  }
@@ -861,8 +1038,8 @@ var init_api_provider = __esm({
861
1038
  "src/embedding/api-provider.ts"() {
862
1039
  "use strict";
863
1040
  init_esm_shims();
864
- CACHE_DIR2 = process.env.MEMORIX_DATA_DIR || join3(homedir3(), ".memorix", "data");
865
- CACHE_FILE2 = join3(CACHE_DIR2, ".embedding-api-cache.json");
1041
+ CACHE_DIR2 = process.env.MEMORIX_DATA_DIR || join5(homedir5(), ".memorix", "data");
1042
+ CACHE_FILE2 = join5(CACHE_DIR2, ".embedding-api-cache.json");
866
1043
  cache3 = /* @__PURE__ */ new Map();
867
1044
  MAX_CACHE_SIZE3 = 1e4;
868
1045
  diskCacheDirty2 = false;
@@ -914,7 +1091,8 @@ var init_api_provider = __esm({
914
1091
  model = cfg.getEmbeddingModel();
915
1092
  requestedDimensions = cfg.getEmbeddingDimensions();
916
1093
  } catch {
917
- apiKey = process.env.MEMORIX_EMBEDDING_API_KEY || process.env.MEMORIX_LLM_API_KEY || process.env.OPENAI_API_KEY;
1094
+ apiKey = process.env.MEMORIX_EMBEDDING_API_KEY || process.env.MEMORIX_API_KEY || // Unified API key
1095
+ process.env.MEMORIX_LLM_API_KEY || process.env.OPENAI_API_KEY;
918
1096
  baseUrl = process.env.MEMORIX_EMBEDDING_BASE_URL || process.env.MEMORIX_LLM_BASE_URL || "https://api.openai.com/v1";
919
1097
  model = process.env.MEMORIX_EMBEDDING_MODEL || "text-embedding-3-small";
920
1098
  const dimStr = process.env.MEMORIX_EMBEDDING_DIMENSIONS;
@@ -952,8 +1130,8 @@ var init_api_provider = __esm({
952
1130
  async embed(text) {
953
1131
  const normalized = normalizeText(text);
954
1132
  const hash = textHash2(normalized);
955
- const cached = cache3.get(hash);
956
- if (cached) return cached;
1133
+ const cached2 = cache3.get(hash);
1134
+ if (cached2) return cached2;
957
1135
  const body = {
958
1136
  model: this.config.model,
959
1137
  input: normalized
@@ -982,9 +1160,9 @@ var init_api_provider = __esm({
982
1160
  const uncachedTexts = [];
983
1161
  for (let i = 0; i < normalizedTexts.length; i++) {
984
1162
  const hash = textHash2(normalizedTexts[i]);
985
- const cached = cache3.get(hash);
986
- if (cached) {
987
- results[i] = cached;
1163
+ const cached2 = cache3.get(hash);
1164
+ if (cached2) {
1165
+ results[i] = cached2;
988
1166
  } else {
989
1167
  uncachedIndices.push(i);
990
1168
  uncachedTexts.push(normalizedTexts[i]);
@@ -1248,6 +1426,7 @@ function detectQueryIntent(query) {
1248
1426
  confidence,
1249
1427
  typeBoosts: INTENT_TYPE_BOOSTS[bestIntent],
1250
1428
  fieldBoosts: INTENT_FIELD_BOOSTS[bestIntent],
1429
+ sourceBoosts: INTENT_SOURCE_BOOSTS[bestIntent],
1251
1430
  preferChronological: bestIntent === "when"
1252
1431
  };
1253
1432
  }
@@ -1257,7 +1436,7 @@ function applyIntentBoost(score, type, intentResult) {
1257
1436
  const effectiveBoost = 1 + (boost - 1) * intentResult.confidence;
1258
1437
  return score * effectiveBoost;
1259
1438
  }
1260
- var INTENT_PATTERNS, INTENT_TYPE_BOOSTS, INTENT_FIELD_BOOSTS;
1439
+ var INTENT_PATTERNS, INTENT_TYPE_BOOSTS, INTENT_SOURCE_BOOSTS, INTENT_FIELD_BOOSTS;
1261
1440
  var init_intent_detector = __esm({
1262
1441
  "src/search/intent-detector.ts"() {
1263
1442
  "use strict";
@@ -1390,6 +1569,35 @@ var init_intent_detector = __esm({
1390
1569
  // No special boosting
1391
1570
  }
1392
1571
  };
1572
+ INTENT_SOURCE_BOOSTS = {
1573
+ what_changed: {
1574
+ git: 2,
1575
+ // "what changed" → prefer commit-derived ground truth
1576
+ agent: 0.8
1577
+ },
1578
+ when: {
1579
+ git: 1.8,
1580
+ // Temporal queries → git has precise timestamps
1581
+ agent: 1
1582
+ },
1583
+ why: {
1584
+ agent: 2,
1585
+ // "why" → prefer reasoning/decision memories
1586
+ git: 0.7
1587
+ // commits rarely explain WHY
1588
+ },
1589
+ how: {
1590
+ agent: 1.5,
1591
+ // "how" → prefer explanations
1592
+ git: 1
1593
+ },
1594
+ problem: {
1595
+ git: 1.5,
1596
+ // Bug fixes often in commit history
1597
+ agent: 1.5
1598
+ // But also in problem-solution memories
1599
+ }
1600
+ };
1393
1601
  INTENT_FIELD_BOOSTS = {
1394
1602
  why: {
1395
1603
  title: 2,
@@ -1436,7 +1644,7 @@ function normalizePath(p) {
1436
1644
  return normalized;
1437
1645
  }
1438
1646
  function idPriority(id) {
1439
- if (id.startsWith("placeholder/")) return 0;
1647
+ if (id.startsWith("untracked/")) return 0;
1440
1648
  if (id.startsWith("local/")) return 1;
1441
1649
  return 2;
1442
1650
  }
@@ -1626,6 +1834,14 @@ var init_aliases = __esm({
1626
1834
  });
1627
1835
 
1628
1836
  // src/llm/provider.ts
1837
+ var provider_exports2 = {};
1838
+ __export(provider_exports2, {
1839
+ callLLM: () => callLLM,
1840
+ getLLMConfig: () => getLLMConfig,
1841
+ initLLM: () => initLLM,
1842
+ isLLMEnabled: () => isLLMEnabled,
1843
+ setLLMConfig: () => setLLMConfig
1844
+ });
1629
1845
  function initLLM() {
1630
1846
  const { getLLMApiKey: getLLMApiKey2, getLLMProvider: getLLMProvider2, getLLMModel: getLLMModel2, getLLMBaseUrl: getLLMBaseUrl2 } = (init_config(), __toCommonJS(config_exports));
1631
1847
  const apiKey = getLLMApiKey2();
@@ -1649,6 +1865,9 @@ function isLLMEnabled() {
1649
1865
  function getLLMConfig() {
1650
1866
  return currentConfig;
1651
1867
  }
1868
+ function setLLMConfig(config) {
1869
+ currentConfig = config;
1870
+ }
1652
1871
  async function callLLM(systemPrompt, userMessage) {
1653
1872
  if (!currentConfig) {
1654
1873
  throw new Error("LLM not configured. Set MEMORIX_LLM_API_KEY or OPENAI_API_KEY.");
@@ -1748,7 +1967,7 @@ __export(quality_exports, {
1748
1967
  });
1749
1968
  async function compressNarrative(narrative, facts, type) {
1750
1969
  const originalTokens = estimateTokens(narrative);
1751
- if (!isLLMEnabled() || narrative.length <= 80) {
1970
+ if (!isLLMEnabled() || narrative.length <= 150) {
1752
1971
  return { compressed: narrative, saved: 0, usedLLM: false };
1753
1972
  }
1754
1973
  if (shouldSkipCompression(narrative, type)) {
@@ -1758,7 +1977,9 @@ async function compressNarrative(narrative, facts, type) {
1758
1977
  const factsContext = facts && facts.length > 0 ? `
1759
1978
 
1760
1979
  Separate facts (already stored, don't repeat): ${facts.join("; ")}` : "";
1761
- const response = await callLLM(COMPRESS_PROMPT, narrative + factsContext);
1980
+ const HIGH_VALUE_TYPES = /* @__PURE__ */ new Set(["decision", "trade-off", "why-it-exists", "how-it-works"]);
1981
+ const prompt = type && HIGH_VALUE_TYPES.has(type) ? COMPRESS_PROMPT_GENTLE : COMPRESS_PROMPT;
1982
+ const response = await callLLM(prompt, narrative + factsContext);
1762
1983
  const compressed = response.content.trim();
1763
1984
  if (!compressed || compressed.length >= narrative.length) {
1764
1985
  return { compressed: narrative, saved: 0, usedLLM: true };
@@ -1830,7 +2051,7 @@ function estimateTokens(text) {
1830
2051
  const otherChars = text.length - cjkChars;
1831
2052
  return Math.ceil(cjkChars / 1.5 + otherChars / 4);
1832
2053
  }
1833
- var COMPRESS_PROMPT, RERANK_PROMPT, SKIP_PATTERNS, LOW_COMPRESSION_TYPES;
2054
+ var COMPRESS_PROMPT, COMPRESS_PROMPT_GENTLE, RERANK_PROMPT, SKIP_PATTERNS, LOW_COMPRESSION_TYPES;
1834
2055
  var init_quality = __esm({
1835
2056
  "src/llm/quality.ts"() {
1836
2057
  "use strict";
@@ -1838,13 +2059,12 @@ var init_quality = __esm({
1838
2059
  init_provider2();
1839
2060
  COMPRESS_PROMPT = `You are a memory compression engine for a coding assistant.
1840
2061
 
1841
- Compress the given narrative into the shortest possible form that preserves ALL technical facts.
2062
+ Compress the given narrative while preserving ALL technical facts and reasoning.
1842
2063
 
1843
2064
  Rules:
1844
- - Aggressively remove: filler words, background context, debugging journey, repeated info
1845
- - Compress to MINIMUM viable length \u2014 aim for 50% or less of original
1846
- - Keep ONLY: specific values, file paths, error messages, version numbers, config keys, causal relationships
1847
- - Merge related points into single dense sentences
2065
+ - Remove: filler words, debugging journey, repeated info already in facts
2066
+ - Keep: specific values, file paths, error messages, version numbers, config keys, causal relationships, design reasoning
2067
+ - Merge related points into dense sentences
1848
2068
  - If facts are provided separately, do NOT repeat them in the compressed narrative
1849
2069
  - Output the compressed text ONLY, no explanation or wrapper
1850
2070
 
@@ -1854,6 +2074,16 @@ Output: "JWT refresh\u65E0\u81EA\u52A8\u7EED\u7B7E\u219224h\u540E\u9759\u9ED8\u8
1854
2074
 
1855
2075
  Input: "Final deployment model for shadcn-blog is stable: GitHub Actions build locally, SCP artifacts to VPS, systemd manages the process. Docker was considered but rejected due to complexity overhead for a simple blog. The whole pipeline takes about 2 minutes from push to live."
1856
2076
  Output: "shadcn-blog\u90E8\u7F72: GH Actions\u6784\u5EFA\u2192SCP\u5230VPS\u2192systemd\u7BA1\u7406, \u5F03Docker(\u590D\u6742\u5EA6\u8FC7\u9AD8), push\u5230\u4E0A\u7EBF~2min"`;
2077
+ COMPRESS_PROMPT_GENTLE = `You are a memory compression engine for a coding assistant.
2078
+
2079
+ Lightly compress the given narrative \u2014 preserve reasoning, trade-offs, and "why" context.
2080
+
2081
+ Rules:
2082
+ - Only remove: obvious filler, debugging detours, info already in the separate facts list
2083
+ - PRESERVE: design reasoning, rejected alternatives, trade-off analysis, causal chains
2084
+ - Aim for ~70-80% of original length, NOT aggressive compression
2085
+ - If facts are provided separately, do NOT repeat them in the compressed narrative
2086
+ - Output the compressed text ONLY, no explanation or wrapper`;
1857
2087
  RERANK_PROMPT = `You are a memory relevance ranker for a coding assistant.
1858
2088
 
1859
2089
  Given a QUERY (what the user/agent is looking for) and a list of CANDIDATE memories,
@@ -1867,7 +2097,7 @@ Rules:
1867
2097
  - Output ONLY a JSON array of IDs in order of relevance (most relevant first)
1868
2098
  - Include ALL candidate IDs, just reorder them
1869
2099
 
1870
- Example output: [42, 15, 87, 3, 21]`;
2100
+ Example output: ["r1", "r3", "r2"]`;
1871
2101
  SKIP_PATTERNS = [
1872
2102
  /^(?:Command|Run|Execute):\s/i,
1873
2103
  // Shell commands
@@ -1899,11 +2129,18 @@ __export(orama_store_exports, {
1899
2129
  getTimeline: () => getTimeline,
1900
2130
  insertObservation: () => insertObservation,
1901
2131
  isEmbeddingEnabled: () => isEmbeddingEnabled,
2132
+ makeOramaObservationId: () => makeOramaObservationId,
1902
2133
  removeObservation: () => removeObservation,
1903
2134
  resetDb: () => resetDb,
1904
2135
  searchObservations: () => searchObservations
1905
2136
  });
1906
2137
  import { create, insert, search, remove, update, count } from "@orama/orama";
2138
+ function makeOramaObservationId(projectId, observationId) {
2139
+ return `obs-${encodeURIComponent(projectId)}-${observationId}`;
2140
+ }
2141
+ function makeEntryKey(projectId, observationId) {
2142
+ return `${projectId ?? ""}::${observationId}`;
2143
+ }
1907
2144
  async function getDb() {
1908
2145
  if (db) return db;
1909
2146
  const provider2 = await getEmbeddingProvider();
@@ -1923,7 +2160,8 @@ async function getDb() {
1923
2160
  projectId: "string",
1924
2161
  accessCount: "number",
1925
2162
  lastAccessedAt: "string",
1926
- status: "string"
2163
+ status: "string",
2164
+ source: "string"
1927
2165
  };
1928
2166
  const dims = provider2?.dimensions ?? 384;
1929
2167
  const schema = embeddingEnabled ? { ...baseSchema, embedding: `vector[${dims}]` } : baseSchema;
@@ -1978,6 +2216,9 @@ async function searchObservations(options) {
1978
2216
  if (options.type) {
1979
2217
  filters["type"] = options.type;
1980
2218
  }
2219
+ if (options.source) {
2220
+ filters["source"] = options.source;
2221
+ }
1981
2222
  const hasQuery = options.query && options.query.trim().length > 0;
1982
2223
  const intentResult = hasQuery ? detectQueryIntent(options.query) : null;
1983
2224
  const requestLimit = projectIds && projectIds.length > 1 ? (options.limit ?? 20) * 3 : options.limit ?? 20;
@@ -2006,7 +2247,12 @@ async function searchObservations(options) {
2006
2247
  try {
2007
2248
  const provider2 = await getEmbeddingProvider();
2008
2249
  if (provider2) {
2009
- queryVector = await provider2.embed(options.query);
2250
+ const EMBEDDING_TIMEOUT_MS = 15e3;
2251
+ const embedPromise = provider2.embed(options.query);
2252
+ const timeoutPromise = new Promise(
2253
+ (_, reject) => setTimeout(() => reject(new Error(`Embedding timeout after ${EMBEDDING_TIMEOUT_MS}ms`)), EMBEDDING_TIMEOUT_MS)
2254
+ );
2255
+ queryVector = await Promise.race([embedPromise, timeoutPromise]);
2010
2256
  const cjkRatio = (options.query.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / options.query.length;
2011
2257
  const isCJKHeavy = cjkRatio > 0.3;
2012
2258
  searchParams = {
@@ -2020,7 +2266,8 @@ async function searchObservations(options) {
2020
2266
  hybridWeights: isCJKHeavy ? { text: 0.2, vector: 0.8 } : { text: 0.6, vector: 0.4 }
2021
2267
  };
2022
2268
  }
2023
- } catch {
2269
+ } catch (error) {
2270
+ console.error("[memorix] Embedding failed or timed out, falling back to fulltext search");
2024
2271
  }
2025
2272
  }
2026
2273
  let results = await search(database, searchParams);
@@ -2068,7 +2315,9 @@ async function searchObservations(options) {
2068
2315
  icon: OBSERVATION_ICONS[obsType] ?? "\u2753",
2069
2316
  title: doc.title,
2070
2317
  tokens: doc.tokens,
2071
- score: (hit.score ?? 1) * recencyBoost
2318
+ score: (hit.score ?? 1) * recencyBoost,
2319
+ projectId: doc.projectId,
2320
+ source: doc.source || "agent"
2072
2321
  };
2073
2322
  });
2074
2323
  if (intentResult && intentResult.confidence > 0.3) {
@@ -2077,6 +2326,14 @@ async function searchObservations(options) {
2077
2326
  score: applyIntentBoost(entry.score, entry.type, intentResult)
2078
2327
  }));
2079
2328
  }
2329
+ if (intentResult && intentResult.confidence > 0.3 && intentResult.sourceBoosts) {
2330
+ const srcBoosts = intentResult.sourceBoosts;
2331
+ intermediate = intermediate.map((entry) => {
2332
+ const boost = srcBoosts[entry.source] ?? 1;
2333
+ const effectiveBoost = 1 + (boost - 1) * intentResult.confidence;
2334
+ return { ...entry, score: entry.score * effectiveBoost };
2335
+ });
2336
+ }
2080
2337
  if (intentResult?.preferChronological) {
2081
2338
  intermediate.sort((a, b) => new Date(b.rawTime).getTime() - new Date(a.rawTime).getTime());
2082
2339
  } else {
@@ -2130,34 +2387,40 @@ async function searchObservations(options) {
2130
2387
  const narrativeMap = /* @__PURE__ */ new Map();
2131
2388
  for (const hit of results.hits) {
2132
2389
  const doc = hit.document;
2133
- narrativeMap.set(doc.observationId, doc.narrative);
2390
+ narrativeMap.set(makeEntryKey(doc.projectId, doc.observationId), doc.narrative);
2134
2391
  }
2135
- const candidates = intermediate.map((e) => ({
2136
- id: e.id,
2392
+ const candidates = intermediate.map((e, index) => ({
2393
+ id: `r${index + 1}`,
2137
2394
  title: e.title,
2138
2395
  type: e.type,
2139
2396
  score: e.score,
2140
- narrative: narrativeMap.get(e.id)
2397
+ narrative: narrativeMap.get(makeEntryKey(e.projectId, e.id))
2141
2398
  }));
2142
- const { reranked, usedLLM } = await rerankResults2(options.query, candidates);
2399
+ const RERANK_TIMEOUT_MS = 1e4;
2400
+ const rerankPromise = rerankResults2(options.query, candidates);
2401
+ const timeoutPromise = new Promise(
2402
+ (_, reject) => setTimeout(() => reject(new Error(`LLM rerank timeout after ${RERANK_TIMEOUT_MS}ms`)), RERANK_TIMEOUT_MS)
2403
+ );
2404
+ const { reranked, usedLLM } = await Promise.race([rerankPromise, timeoutPromise]);
2143
2405
  if (usedLLM) {
2144
- const intermediateMap = new Map(intermediate.map((e) => [e.id, e]));
2145
- const rerankedIntermediate = reranked.map((r) => intermediateMap.get(r.id)).filter((e) => e != null);
2406
+ const candidateMap = new Map(candidates.map((candidate, index) => [candidate.id, intermediate[index]]));
2407
+ const rerankedIntermediate = reranked.map((r) => candidateMap.get(r.id)).filter((e) => e != null);
2146
2408
  if (rerankedIntermediate.length > 0) {
2147
2409
  intermediate = rerankedIntermediate;
2148
2410
  }
2149
2411
  }
2150
- } catch {
2412
+ } catch (error) {
2413
+ console.error("[memorix] LLM rerank failed or timed out, using original order");
2151
2414
  }
2152
2415
  }
2153
2416
  let entries = intermediate.map(({ rawTime: _, ...rest }) => rest);
2154
2417
  if (hasQuery && options.query) {
2155
2418
  const queryLower = options.query.toLowerCase();
2156
2419
  const queryTokens = queryLower.split(/\s+/).filter((t) => t.length > 1);
2157
- const entryMap = new Map(entries.map((e) => [e.id, e]));
2420
+ const entryMap = new Map(entries.map((e) => [makeEntryKey(e.projectId, e.id), e]));
2158
2421
  for (const hit of results.hits) {
2159
2422
  const doc = hit.document;
2160
- const entry = entryMap.get(doc.observationId);
2423
+ const entry = entryMap.get(makeEntryKey(doc.projectId, doc.observationId));
2161
2424
  if (!entry) continue;
2162
2425
  const reasons = [];
2163
2426
  const fields = [
@@ -2173,7 +2436,7 @@ async function searchObservations(options) {
2173
2436
  if (queryTokens.some((t) => valueLower.includes(t))) reasons.push(name);
2174
2437
  }
2175
2438
  if (reasons.length === 0) reasons.push("fuzzy");
2176
- entry["matchedFields"] = reasons;
2439
+ entry.matchedFields = reasons;
2177
2440
  }
2178
2441
  }
2179
2442
  if (options.maxTokens && options.maxTokens > 0) {
@@ -2327,7 +2590,19 @@ function extractEntities(content) {
2327
2590
  seen.add(key);
2328
2591
  switch (kind) {
2329
2592
  case "file":
2330
- result.files.push(entity);
2593
+ const isLikelyFilePath = (() => {
2594
+ if (entity.includes("/") || entity.includes("\\")) return true;
2595
+ if (entity.startsWith("./") || entity.startsWith("../")) return true;
2596
+ if (/^(?:process\.env|config\.|options\.|params\.|props\.|this\.|self\.|module\.|exports\.|require\.|import\.)/i.test(entity)) return false;
2597
+ const dotCount = (entity.match(/\./g) || []).length;
2598
+ if (dotCount >= 2) return false;
2599
+ const ext = entity.split(".").pop()?.toLowerCase() ?? "";
2600
+ const FILE_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "js", "tsx", "jsx", "mts", "mjs", "cjs", "json", "yaml", "yml", "toml", "xml", "csv", "py", "rb", "go", "rs", "java", "kt", "swift", "c", "cpp", "h", "html", "css", "scss", "less", "vue", "svelte", "sh", "bash", "zsh", "ps1", "cmd", "bat", "md", "txt", "rst", "log", "sql", "graphql", "prisma", "env", "gitignore", "dockerignore", "dockerfile", "lock", "config"]);
2601
+ return FILE_EXTENSIONS.has(ext);
2602
+ })();
2603
+ if (isLikelyFilePath) {
2604
+ result.files.push(entity);
2605
+ }
2331
2606
  break;
2332
2607
  case "module":
2333
2608
  result.modules.push(entity);
@@ -2462,11 +2737,15 @@ async function storeObservation(input) {
2462
2737
  revisionCount: 1,
2463
2738
  sessionId: input.sessionId,
2464
2739
  status: "active",
2465
- progress: input.progress
2740
+ progress: input.progress,
2741
+ source: input.source,
2742
+ commitHash: input.commitHash,
2743
+ relatedCommits: input.relatedCommits,
2744
+ relatedEntities: input.relatedEntities
2466
2745
  };
2467
2746
  observations.push(observation);
2468
2747
  const doc = {
2469
- id: `obs-${id}`,
2748
+ id: makeOramaObservationId(input.projectId, id),
2470
2749
  observationId: id,
2471
2750
  entityName: input.entityName,
2472
2751
  type: input.type,
@@ -2480,7 +2759,8 @@ async function storeObservation(input) {
2480
2759
  projectId: input.projectId,
2481
2760
  accessCount: 0,
2482
2761
  lastAccessedAt: "",
2483
- status: "active"
2762
+ status: "active",
2763
+ source: input.source ?? "agent"
2484
2764
  };
2485
2765
  await insertObservation(doc);
2486
2766
  if (projectDir) {
@@ -2503,7 +2783,7 @@ async function storeObservation(input) {
2503
2783
  if (embedding) {
2504
2784
  try {
2505
2785
  const { removeObservation: removeObs } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
2506
- await removeObs(`obs-${id}`);
2786
+ await removeObs(makeOramaObservationId(input.projectId, id));
2507
2787
  await insertObservation(Object.assign({}, doc, { embedding }));
2508
2788
  } catch {
2509
2789
  }
@@ -2539,7 +2819,7 @@ async function upsertObservation(existing, input, now) {
2539
2819
  if (input.sessionId) existing.sessionId = input.sessionId;
2540
2820
  if (input.progress) existing.progress = input.progress;
2541
2821
  const doc = {
2542
- id: `obs-${existing.id}`,
2822
+ id: makeOramaObservationId(existing.projectId, existing.id),
2543
2823
  observationId: existing.id,
2544
2824
  entityName: existing.entityName,
2545
2825
  type: existing.type,
@@ -2553,11 +2833,12 @@ async function upsertObservation(existing, input, now) {
2553
2833
  projectId: existing.projectId,
2554
2834
  accessCount: 0,
2555
2835
  lastAccessedAt: "",
2556
- status: "active"
2836
+ status: "active",
2837
+ source: existing.source ?? "agent"
2557
2838
  };
2558
2839
  try {
2559
2840
  const { removeObservation: removeObservation3 } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
2560
- await removeObservation3(`obs-${existing.id}`);
2841
+ await removeObservation3(makeOramaObservationId(existing.projectId, existing.id));
2561
2842
  } catch {
2562
2843
  }
2563
2844
  await insertObservation(doc);
@@ -2580,7 +2861,7 @@ async function upsertObservation(existing, input, now) {
2580
2861
  if (embedding) {
2581
2862
  try {
2582
2863
  const { removeObservation: removeObs } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
2583
- await removeObs(`obs-${obsId}`);
2864
+ await removeObs(makeOramaObservationId(existing.projectId, obsId));
2584
2865
  await insertObservation(Object.assign({}, doc, { embedding }));
2585
2866
  } catch {
2586
2867
  }
@@ -2611,9 +2892,9 @@ async function resolveObservations(ids, status = "resolved") {
2611
2892
  resolved.push(id);
2612
2893
  try {
2613
2894
  const { removeObservation: removeObs } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
2614
- await removeObs(`obs-${id}`);
2895
+ await removeObs(makeOramaObservationId(obs.projectId, id));
2615
2896
  const doc = {
2616
- id: `obs-${obs.id}`,
2897
+ id: makeOramaObservationId(obs.projectId, obs.id),
2617
2898
  observationId: obs.id,
2618
2899
  entityName: obs.entityName,
2619
2900
  type: obs.type,
@@ -2627,7 +2908,8 @@ async function resolveObservations(ids, status = "resolved") {
2627
2908
  projectId: obs.projectId,
2628
2909
  accessCount: 0,
2629
2910
  lastAccessedAt: "",
2630
- status
2911
+ status,
2912
+ source: obs.source ?? "agent"
2631
2913
  };
2632
2914
  await insertObservation(doc);
2633
2915
  const obsId = obs.id;
@@ -2712,7 +2994,7 @@ async function reindexObservations() {
2712
2994
  const obs = observations[i];
2713
2995
  try {
2714
2996
  const embedding = embeddings[i] ?? null;
2715
- const docId = `obs-${obs.id}`;
2997
+ const docId = makeOramaObservationId(obs.projectId, obs.id);
2716
2998
  const doc = {
2717
2999
  id: docId,
2718
3000
  observationId: obs.id,
@@ -2729,6 +3011,7 @@ async function reindexObservations() {
2729
3011
  accessCount: 0,
2730
3012
  lastAccessedAt: "",
2731
3013
  status: obs.status ?? "active",
3014
+ source: obs.source ?? "agent",
2732
3015
  ...embedding ? { embedding } : {}
2733
3016
  };
2734
3017
  await insertObservation(doc);
@@ -2756,6 +3039,126 @@ var init_observations = __esm({
2756
3039
  }
2757
3040
  });
2758
3041
 
3042
+ // src/project/detector.ts
3043
+ var detector_exports = {};
3044
+ __export(detector_exports, {
3045
+ detectProject: () => detectProject,
3046
+ findGitInSubdirs: () => findGitInSubdirs,
3047
+ isSystemDirectory: () => isSystemDirectory
3048
+ });
3049
+ import { execSync } from "child_process";
3050
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
3051
+ import path5 from "path";
3052
+ function detectProject(cwd) {
3053
+ const basePath = cwd ?? process.cwd();
3054
+ const gitRoot = getGitRoot(basePath);
3055
+ if (!gitRoot) {
3056
+ return null;
3057
+ }
3058
+ const gitRemote = getGitRemote(gitRoot);
3059
+ if (gitRemote) {
3060
+ const id2 = normalizeGitRemote(gitRemote);
3061
+ const name2 = id2.split("/").pop() ?? path5.basename(gitRoot);
3062
+ return { id: id2, name: name2, gitRemote, rootPath: gitRoot };
3063
+ }
3064
+ const name = path5.basename(gitRoot);
3065
+ const id = `local/${name}`;
3066
+ return { id, name, rootPath: gitRoot };
3067
+ }
3068
+ function getGitRoot(cwd) {
3069
+ let dir = path5.resolve(cwd);
3070
+ const fsRoot = path5.parse(dir).root;
3071
+ while (dir !== fsRoot) {
3072
+ if (existsSync4(path5.join(dir, ".git"))) return dir;
3073
+ dir = path5.dirname(dir);
3074
+ }
3075
+ try {
3076
+ const root = execSync("git -c safe.directory=* rev-parse --show-toplevel", {
3077
+ cwd,
3078
+ encoding: "utf-8",
3079
+ stdio: ["pipe", "pipe", "pipe"],
3080
+ timeout: 5e3
3081
+ }).trim();
3082
+ return root || null;
3083
+ } catch {
3084
+ return null;
3085
+ }
3086
+ }
3087
+ function getGitRemote(cwd) {
3088
+ const fsRemote = readGitConfigRemote(cwd);
3089
+ if (fsRemote) return fsRemote;
3090
+ try {
3091
+ const remote = execSync("git -c safe.directory=* remote get-url origin", {
3092
+ cwd,
3093
+ encoding: "utf-8",
3094
+ stdio: ["pipe", "pipe", "pipe"],
3095
+ timeout: 5e3
3096
+ }).trim();
3097
+ return remote || null;
3098
+ } catch {
3099
+ return null;
3100
+ }
3101
+ }
3102
+ function readGitConfigRemote(cwd) {
3103
+ try {
3104
+ const configPath = path5.join(cwd, ".git", "config");
3105
+ if (!existsSync4(configPath)) return null;
3106
+ const content = readFileSync4(configPath, "utf-8");
3107
+ const remoteMatch = content.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
3108
+ if (!remoteMatch) return null;
3109
+ const urlMatch = remoteMatch[1].match(/^\s*url\s*=\s*(.+)$/m);
3110
+ return urlMatch ? urlMatch[1].trim() : null;
3111
+ } catch {
3112
+ return null;
3113
+ }
3114
+ }
3115
+ function isSystemDirectory(dir) {
3116
+ const lower = dir.toLowerCase().replace(/\\/g, "/");
3117
+ return lower.includes("/windows/") || lower.endsWith("/windows") || lower.includes("/program files") || lower.includes("/appdata/") || // IDE installation directories
3118
+ /\/(windsurf|cursor|code|vscode)\/\1/i.test(lower) || /\/windsurf\b/i.test(lower) && !lower.includes(".windsurf") || // Node / npm internal paths
3119
+ lower.includes("/node_modules/") || lower.includes("/nvm") || // System root
3120
+ /^[a-z]:\/$/i.test(lower);
3121
+ }
3122
+ function findGitInSubdirs(dir) {
3123
+ try {
3124
+ const resolved = path5.resolve(dir);
3125
+ const entries = readdirSync(resolved);
3126
+ for (const entry of entries) {
3127
+ if (entry.startsWith(".")) continue;
3128
+ const fullPath = path5.join(resolved, entry);
3129
+ try {
3130
+ if (statSync(fullPath).isDirectory() && existsSync4(path5.join(fullPath, ".git"))) {
3131
+ return fullPath;
3132
+ }
3133
+ } catch {
3134
+ }
3135
+ }
3136
+ } catch {
3137
+ }
3138
+ return null;
3139
+ }
3140
+ function normalizeGitRemote(remote) {
3141
+ let normalized = remote;
3142
+ normalized = normalized.replace(/\.git$/, "");
3143
+ const sshMatch = normalized.match(/^[\w-]+@[\w.-]+:(.+)$/);
3144
+ if (sshMatch) {
3145
+ return sshMatch[1];
3146
+ }
3147
+ try {
3148
+ const url = new URL(normalized);
3149
+ return url.pathname.replace(/^\//, "");
3150
+ } catch {
3151
+ const segments = normalized.split("/").filter(Boolean);
3152
+ return segments.slice(-2).join("/");
3153
+ }
3154
+ }
3155
+ var init_detector = __esm({
3156
+ "src/project/detector.ts"() {
3157
+ "use strict";
3158
+ init_esm_shims();
3159
+ }
3160
+ });
3161
+
2759
3162
  // src/llm/memory-manager.ts
2760
3163
  var memory_manager_exports = {};
2761
3164
  __export(memory_manager_exports, {
@@ -2883,59 +3286,994 @@ function mergeFacts(oldFactsStr, newFactsStr) {
2883
3286
  merged.push(f);
2884
3287
  }
2885
3288
  }
2886
- return merged;
3289
+ return merged;
3290
+ }
3291
+ async function deduplicateMemory(newMemory, existingMemories) {
3292
+ if (!isLLMEnabled()) return null;
3293
+ if (existingMemories.length === 0) return { action: "ADD", reason: "No existing memories", usedLLM: false };
3294
+ const asExisting = existingMemories.map((m) => ({
3295
+ ...m,
3296
+ score: 0.8
3297
+ // Batch dedup assumes high similarity (pre-filtered)
3298
+ }));
3299
+ return compactOnWrite(newMemory, asExisting);
3300
+ }
3301
+ var COMPACT_ON_WRITE_PROMPT, SIMILARITY_HIGH, SIMILARITY_MEDIUM;
3302
+ var init_memory_manager = __esm({
3303
+ "src/llm/memory-manager.ts"() {
3304
+ "use strict";
3305
+ init_esm_shims();
3306
+ init_provider2();
3307
+ COMPACT_ON_WRITE_PROMPT = `You are a smart coding memory manager. You control the memory of a cross-IDE coding assistant.
3308
+
3309
+ You receive a NEW MEMORY to store and a list of EXISTING similar memories. Your job:
3310
+ 1. Extract the key facts from the new memory
3311
+ 2. Compare with existing memories
3312
+ 3. Decide the best action
3313
+
3314
+ Actions:
3315
+ - ADD: New memory contains unique information. Store it as-is.
3316
+ - UPDATE: New memory supersedes or improves an existing one. Merge them into a single, comprehensive memory.
3317
+ - DELETE: An existing memory is outdated/contradicted by the new one. Remove it.
3318
+ - NONE: New memory is redundant. Existing memories already cover this. Skip storing.
3319
+
3320
+ Decision rules:
3321
+ - Same topic updated (e.g., "MySQL \u2192 PostgreSQL"): UPDATE the old memory with merged content
3322
+ - Bug fixed that was reported as open: UPDATE the bug report to include the fix
3323
+ - Task completed that was tracked as in-progress: UPDATE to mark completed
3324
+ - Minor variation of existing memory: NONE (skip)
3325
+ - Completely new topic: ADD
3326
+ - Old info directly contradicted: DELETE the old one
3327
+ - Prefer UPDATE over ADD \u2014 keep memory count low, merge information
3328
+
3329
+ For UPDATE: write a merged narrative that combines the best of both old and new, preserving all important details. Also merge the facts lists, removing duplicates.
3330
+
3331
+ Respond in JSON only:
3332
+ {
3333
+ "action": "ADD" | "UPDATE" | "DELETE" | "NONE",
3334
+ "targetId": null or existing_memory_id_number,
3335
+ "reason": "brief explanation of decision",
3336
+ "mergedNarrative": "merged narrative text (required for UPDATE, null otherwise)",
3337
+ "mergedFacts": ["merged fact 1", "merged fact 2"] or null,
3338
+ "extractedFacts": ["fact extracted from new content 1", "fact 2"]
3339
+ }`;
3340
+ SIMILARITY_HIGH = 0.75;
3341
+ SIMILARITY_MEDIUM = 0.5;
3342
+ }
3343
+ });
3344
+
3345
+ // src/memory/formation/extract.ts
3346
+ function extractFacts(narrative, existingFacts) {
3347
+ const existingLower = new Set(existingFacts.map((f) => f.toLowerCase().trim()));
3348
+ const extracted = [];
3349
+ const seen = /* @__PURE__ */ new Set();
3350
+ for (const { pattern, format } of FACT_PATTERNS) {
3351
+ pattern.lastIndex = 0;
3352
+ let match;
3353
+ while ((match = pattern.exec(narrative)) !== null) {
3354
+ const fact = format(match);
3355
+ const normalized = fact.toLowerCase().trim();
3356
+ if (existingLower.has(normalized) || seen.has(normalized)) continue;
3357
+ if (fact.length < 5 || fact.length > 120) continue;
3358
+ seen.add(normalized);
3359
+ extracted.push(fact);
3360
+ }
3361
+ }
3362
+ return extracted.slice(0, 10);
3363
+ }
3364
+ function improveTitle(title, narrative) {
3365
+ const isGeneric = GENERIC_TITLE_PATTERNS.some((p) => p.test(title));
3366
+ if (!isGeneric) return { title, improved: false };
3367
+ const sentences = narrative.replace(/```[\s\S]*?```/g, "").split(/[.。!!?\n]/).map((s) => s.trim()).filter((s) => s.length >= 15);
3368
+ if (sentences.length > 0) {
3369
+ return { title: sentences[0].slice(0, 60), improved: true };
3370
+ }
3371
+ return { title, improved: false };
3372
+ }
3373
+ function resolveEntity(entityName, existingEntities) {
3374
+ if (existingEntities.length === 0) return { entityName, resolved: false };
3375
+ const lower = entityName.toLowerCase().replace(/[-_]/g, "");
3376
+ for (const existing of existingEntities) {
3377
+ const existingLower = existing.toLowerCase().replace(/[-_]/g, "");
3378
+ if (lower === existingLower) {
3379
+ return { entityName: existing, resolved: existing !== entityName };
3380
+ }
3381
+ if (lower.length >= 3 && existingLower.length >= 3) {
3382
+ if (existingLower.includes(lower) || lower.includes(existingLower)) {
3383
+ const canonical = existing.length >= entityName.length ? existing : entityName;
3384
+ return { entityName: canonical, resolved: canonical !== entityName };
3385
+ }
3386
+ }
3387
+ }
3388
+ return { entityName, resolved: false };
3389
+ }
3390
+ function verifyType(declaredType, narrative, title) {
3391
+ const content = `${title} ${narrative}`;
3392
+ const scores = [];
3393
+ for (const { type, patterns } of TYPE_SIGNALS) {
3394
+ let score = 0;
3395
+ for (const p of patterns) {
3396
+ const regex = new RegExp(p.source, p.flags.includes("g") ? p.flags : p.flags + "g");
3397
+ const matches = [...content.matchAll(regex)];
3398
+ score += matches.length;
3399
+ }
3400
+ if (score > 0) scores.push({ type, score });
3401
+ }
3402
+ if (scores.length === 0) return { type: declaredType, corrected: false };
3403
+ scores.sort((a, b) => b.score - a.score);
3404
+ const best = scores[0];
3405
+ if (best.type !== declaredType && best.score >= 2) {
3406
+ const declaredScore = scores.find((s) => s.type === declaredType)?.score ?? 0;
3407
+ if (declaredScore === 0) {
3408
+ return { type: best.type, corrected: true };
3409
+ }
3410
+ }
3411
+ return { type: declaredType, corrected: false };
3412
+ }
3413
+ async function extractFactsWithLLM(narrative, title, existingFacts) {
3414
+ try {
3415
+ const { callLLM: callLLM2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports2));
3416
+ const input = `Title: ${title}
3417
+ Content: ${narrative}${existingFacts.length > 0 ? `
3418
+ Already known facts (don't repeat): ${existingFacts.join("; ")}` : ""}`;
3419
+ const response = await callLLM2(LLM_EXTRACT_PROMPT, input);
3420
+ const text = response.content.trim();
3421
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
3422
+ if (!jsonMatch) return [];
3423
+ const parsed = JSON.parse(jsonMatch[0]);
3424
+ const facts = parsed.facts;
3425
+ if (!Array.isArray(facts)) return [];
3426
+ const existingLower = new Set(existingFacts.map((f) => f.toLowerCase().trim()));
3427
+ return facts.filter((f) => typeof f === "string" && f.length >= 5).filter((f) => !existingLower.has(f.toLowerCase().trim())).slice(0, 10);
3428
+ } catch {
3429
+ return [];
3430
+ }
3431
+ }
3432
+ async function runExtract(input, existingEntities, useLLM = false) {
3433
+ const callerFacts = input.facts ?? [];
3434
+ let extractedFacts;
3435
+ if (useLLM) {
3436
+ extractedFacts = await extractFactsWithLLM(input.narrative, input.title, callerFacts);
3437
+ if (extractedFacts.length === 0) {
3438
+ extractedFacts = extractFacts(input.narrative, callerFacts);
3439
+ }
3440
+ } else {
3441
+ extractedFacts = extractFacts(input.narrative, callerFacts);
3442
+ }
3443
+ const allFacts = [...callerFacts, ...extractedFacts];
3444
+ const { title, improved: titleImproved } = improveTitle(input.title, input.narrative);
3445
+ const { entityName, resolved: entityResolved } = resolveEntity(
3446
+ input.entityName,
3447
+ existingEntities
3448
+ );
3449
+ const { type, corrected: typeCorrected } = verifyType(
3450
+ input.type,
3451
+ input.narrative,
3452
+ input.title
3453
+ );
3454
+ return {
3455
+ title,
3456
+ titleImproved,
3457
+ narrative: input.narrative,
3458
+ facts: allFacts,
3459
+ extractedFacts,
3460
+ entityName,
3461
+ entityResolved,
3462
+ type,
3463
+ typeCorrected
3464
+ };
3465
+ }
3466
+ var FACT_PATTERNS, GENERIC_TITLE_PATTERNS, TYPE_SIGNALS, LLM_EXTRACT_PROMPT;
3467
+ var init_extract = __esm({
3468
+ "src/memory/formation/extract.ts"() {
3469
+ "use strict";
3470
+ init_esm_shims();
3471
+ FACT_PATTERNS = [
3472
+ // Key: Value pairs (e.g., "Port: 3000", "Timeout = 60s")
3473
+ {
3474
+ pattern: /\b([A-Z][a-zA-Z_-]{2,30})\s*[:=]\s*([^\n,;]{2,60})/g,
3475
+ format: (m) => `${m[1]}: ${m[2].trim()}`
3476
+ },
3477
+ // Arrow notation (e.g., "MySQL → PostgreSQL", "v1.0 → v2.0")
3478
+ {
3479
+ pattern: /\b(\S{2,30})\s*[→➜\->]+\s*(\S{2,30})/g,
3480
+ format: (m) => `${m[1]} \u2192 ${m[2]}`
3481
+ },
3482
+ // Version numbers (e.g., "v1.2.3", "version 2.0")
3483
+ {
3484
+ pattern: /\b(?:v(?:ersion)?\s*)(\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)\b/gi,
3485
+ format: (m) => `Version: ${m[1]}`
3486
+ },
3487
+ // Error messages (e.g., "Error: ...", "ERR_...")
3488
+ {
3489
+ pattern: /\b(?:Error|ERR|ENOENT|ECONNREFUSED|TypeError|RangeError|SyntaxError|ReferenceError)[:\s]+([^\n]{5,80})/gi,
3490
+ format: (m) => `Error: ${m[1].trim()}`
3491
+ },
3492
+ // Port numbers in context
3493
+ {
3494
+ pattern: /\b(?:port|PORT)\s*[:=]?\s*(\d{2,5})\b/gi,
3495
+ format: (m) => `Port: ${m[1]}`
3496
+ },
3497
+ // Environment variables
3498
+ {
3499
+ pattern: /\b([A-Z][A-Z0-9_]{3,30})\s*=\s*(\S{1,60})/g,
3500
+ format: (m) => `${m[1]}=${m[2]}`
3501
+ },
3502
+ // npm/package versions (e.g., "react@18.2.0")
3503
+ {
3504
+ pattern: /\b([@a-z][\w./-]+)@(\d+\.\d+\.\d+(?:-[\w.]+)?)\b/g,
3505
+ format: (m) => `${m[1]}@${m[2]}`
3506
+ }
3507
+ ];
3508
+ GENERIC_TITLE_PATTERNS = [
3509
+ /^Updated \S+\.\w+$/i,
3510
+ /^Created \S+\.\w+$/i,
3511
+ /^Deleted \S+\.\w+$/i,
3512
+ /^Modified \S+\.\w+$/i,
3513
+ /^Changed \S+\.\w+$/i,
3514
+ /^Session activity/i,
3515
+ /^Activity \(/i,
3516
+ /^Used \w+$/i,
3517
+ /^Ran: /i
3518
+ ];
3519
+ TYPE_SIGNALS = [
3520
+ {
3521
+ type: "problem-solution",
3522
+ patterns: [
3523
+ /\b(fix|fixed|bug|error|issue|crash|broken|resolved|workaround|patch)\b/i,
3524
+ /\b(修复|修正|解决|报错|崩溃|异常)\b/
3525
+ ]
3526
+ },
3527
+ {
3528
+ type: "gotcha",
3529
+ patterns: [
3530
+ /\b(gotcha|pitfall|trap|careful|warning|caveat|footgun|unexpected|beware)\b/i,
3531
+ /\b(坑|陷阱|注意|小心|踩坑)\b/
3532
+ ]
3533
+ },
3534
+ {
3535
+ type: "decision",
3536
+ patterns: [
3537
+ /\b(decided|chose|chosen|selected|adopted|rejected|evaluated|compared)\b/i,
3538
+ /\b(决定|选择|采用|弃用|对比|评估)\b/
3539
+ ]
3540
+ },
3541
+ {
3542
+ type: "what-changed",
3543
+ patterns: [
3544
+ /\b(changed|migrated|upgraded|refactored|replaced|renamed|moved|removed|added)\b/i,
3545
+ /\b(改|迁移|升级|重构|替换|重命名|删除|新增)\b/
3546
+ ]
3547
+ },
3548
+ {
3549
+ type: "how-it-works",
3550
+ patterns: [
3551
+ /\b(works by|architecture|mechanism|pipeline|flow|under the hood|internally)\b/i,
3552
+ /\b(原理|机制|流程|架构|内部)\b/
3553
+ ]
3554
+ },
3555
+ {
3556
+ type: "trade-off",
3557
+ patterns: [
3558
+ /\b(trade.?off|compromise|downside|cost|benefit|pro|con|versus|vs)\b/i,
3559
+ /\b(权衡|折中|代价|收益|优缺点)\b/
3560
+ ]
3561
+ }
3562
+ ];
3563
+ LLM_EXTRACT_PROMPT = `You are a Software Engineering Knowledge Extractor.
3564
+ Extract structured facts from the given development context.
3565
+
3566
+ Focus on:
3567
+ 1. Technical decisions and their reasoning
3568
+ 2. Bug root causes and fixes
3569
+ 3. Configuration values (ports, versions, env vars)
3570
+ 4. Architecture patterns and constraints
3571
+ 5. Gotchas, pitfalls, and workarounds
3572
+ 6. File paths and their roles
3573
+
3574
+ Rules:
3575
+ - Return ONLY a JSON object with a "facts" key containing an array of strings
3576
+ - Each fact should be a concise, self-contained statement
3577
+ - Include specific values (versions, ports, paths) when present
3578
+ - Detect the language of the input and record facts in the same language
3579
+ - If no meaningful facts exist, return {"facts": []}
3580
+ - Do NOT include trivial information (file read, directory listing)
3581
+ - Maximum 10 facts
3582
+
3583
+ Example:
3584
+ Input: "Fixed Redis connection leak. The pool wasn't being closed on shutdown. Added defer pool.Close() in main.go. Port 6379."
3585
+ Output: {"facts": ["Redis connection leak caused by pool not closed on shutdown", "Fix: added defer pool.Close() in main.go", "Redis port: 6379"]}`;
3586
+ }
3587
+ });
3588
+
3589
+ // src/memory/formation/resolve.ts
3590
+ function wordOverlap(a, b) {
3591
+ const wordsA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
3592
+ const wordsB = new Set(b.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
3593
+ if (wordsA.size === 0 || wordsB.size === 0) return 0;
3594
+ let intersection = 0;
3595
+ for (const w of wordsA) {
3596
+ if (wordsB.has(w)) intersection++;
3597
+ }
3598
+ return intersection / Math.max(wordsA.size, wordsB.size);
3599
+ }
3600
+ function entitiesMatch(a, b) {
3601
+ const na = a.toLowerCase().replace(/[-_]/g, "");
3602
+ const nb = b.toLowerCase().replace(/[-_]/g, "");
3603
+ if (na === nb) return true;
3604
+ if (na.length >= 3 && nb.length >= 3) {
3605
+ if (na.includes(nb) || nb.includes(na)) return true;
3606
+ }
3607
+ return false;
3608
+ }
3609
+ function hasContradiction(oldText, newText) {
3610
+ const negationPatterns = [
3611
+ /\bnot\s+(\w+)/gi,
3612
+ /\bno longer\b/i,
3613
+ /\binstead of\b/i,
3614
+ /\breplaced\b.*\bwith\b/i,
3615
+ /\bremoved\b/i,
3616
+ /\bdeprecated\b/i,
3617
+ /\bobsolete\b/i,
3618
+ /不再/,
3619
+ /已弃用/,
3620
+ /替换为/,
3621
+ /改为/
3622
+ ];
3623
+ return negationPatterns.some((p) => p.test(newText));
3624
+ }
3625
+ function mergeNarratives(oldNarrative, newNarrative) {
3626
+ if (newNarrative.length > oldNarrative.length * 1.5) return newNarrative;
3627
+ if (oldNarrative.length > newNarrative.length * 1.5) return oldNarrative;
3628
+ return `${newNarrative}
3629
+
3630
+ [Previous context]: ${oldNarrative}`;
3631
+ }
3632
+ function mergeFacts2(oldFacts, newFacts) {
3633
+ const seen = /* @__PURE__ */ new Set();
3634
+ const merged = [];
3635
+ for (const f of newFacts) {
3636
+ const norm = f.toLowerCase().trim();
3637
+ if (!seen.has(norm) && f.trim().length > 0) {
3638
+ seen.add(norm);
3639
+ merged.push(f);
3640
+ }
3641
+ }
3642
+ for (const f of oldFacts) {
3643
+ const norm = f.toLowerCase().trim();
3644
+ if (!seen.has(norm) && f.trim().length > 0) {
3645
+ seen.add(norm);
3646
+ merged.push(f);
3647
+ }
3648
+ }
3649
+ return merged;
3650
+ }
3651
+ async function resolveWithLLM(extracted, hits, getObservation2) {
3652
+ try {
3653
+ const { callLLM: callLLM2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports2));
3654
+ const existingMemories = hits.slice(0, 5).map((h, i) => ({
3655
+ id: h.observationId,
3656
+ index: i,
3657
+ title: h.title,
3658
+ content: h.narrative.substring(0, 300),
3659
+ facts: h.facts.substring(0, 200)
3660
+ }));
3661
+ const input = `NEW MEMORY:
3662
+ Title: ${extracted.title}
3663
+ Content: ${extracted.narrative.substring(0, 500)}
3664
+ Facts: ${extracted.facts.join("; ")}
3665
+
3666
+ EXISTING MEMORIES:
3667
+ ${existingMemories.map((m) => `[ID:${m.id}] ${m.title} | ${m.content} | Facts: ${m.facts}`).join("\n")}`;
3668
+ const response = await callLLM2(LLM_RESOLVE_PROMPT, input);
3669
+ const text = response.content.trim();
3670
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
3671
+ if (!jsonMatch) return null;
3672
+ const parsed = JSON.parse(jsonMatch[0]);
3673
+ const action = parsed.action?.toUpperCase();
3674
+ const targetId = parsed.targetId ? Number(parsed.targetId) : void 0;
3675
+ const reason = parsed.reason || "LLM decision";
3676
+ if (action === "NOOP") {
3677
+ return { action: "discard", targetId, reason: `LLM: ${reason}` };
3678
+ }
3679
+ if (action === "ADD") {
3680
+ return { action: "new", reason: `LLM: ${reason}` };
3681
+ }
3682
+ if (action === "UPDATE" && targetId) {
3683
+ const existing = getObservation2(targetId);
3684
+ const oldFacts = existing?.facts ?? [];
3685
+ return {
3686
+ action: "merge",
3687
+ targetId,
3688
+ reason: `LLM: ${reason}`,
3689
+ mergedNarrative: parsed.mergedText || mergeNarratives(existing?.narrative ?? "", extracted.narrative),
3690
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
3691
+ };
3692
+ }
3693
+ if (action === "DELETE" && targetId) {
3694
+ const existing = getObservation2(targetId);
3695
+ const oldFacts = existing?.facts ?? [];
3696
+ return {
3697
+ action: "evolve",
3698
+ targetId,
3699
+ reason: `LLM: ${reason}`,
3700
+ mergedNarrative: extracted.narrative,
3701
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
3702
+ };
3703
+ }
3704
+ return null;
3705
+ } catch {
3706
+ return null;
3707
+ }
3708
+ }
3709
+ function scoreCandidate(extracted, candidate) {
3710
+ const entityMatch = entitiesMatch(extracted.entityName, candidate.entityName);
3711
+ const contentOverlap = wordOverlap(
3712
+ `${extracted.title} ${extracted.narrative}`,
3713
+ `${candidate.title} ${candidate.narrative}`
3714
+ );
3715
+ const score = candidate.score * 0.6 + (entityMatch ? 0.2 : 0) + contentOverlap * 0.2;
3716
+ const newLength = extracted.narrative.length + extracted.facts.join(" ").length;
3717
+ const oldLength = candidate.narrative.length + candidate.facts.length;
3718
+ const richer = newLength > oldLength * 1.15;
3719
+ const contradiction = hasContradiction(candidate.narrative, extracted.narrative);
3720
+ return { score, entityMatch, richer, contradiction };
3721
+ }
3722
+ async function runResolve(extracted, projectId, searchMemories, getObservation2, useLLM = false) {
3723
+ const query = `${extracted.title} ${extracted.narrative.substring(0, 200)}`;
3724
+ let hits;
3725
+ try {
3726
+ hits = await searchMemories(query, 5, projectId);
3727
+ } catch {
3728
+ return { action: "new", reason: "Search unavailable, defaulting to new" };
3729
+ }
3730
+ if (hits.length === 0) {
3731
+ return { action: "new", reason: "No similar existing memories found" };
3732
+ }
3733
+ if (useLLM) {
3734
+ const llmResult = await resolveWithLLM(extracted, hits, getObservation2);
3735
+ if (llmResult) return llmResult;
3736
+ }
3737
+ const scored = hits.map((hit) => ({
3738
+ hit,
3739
+ ...scoreCandidate(extracted, hit)
3740
+ }));
3741
+ scored.sort((a, b) => b.score - a.score);
3742
+ const best = scored[0];
3743
+ if (best.hit.score >= SIMILARITY_DUPLICATE) {
3744
+ if (best.richer) {
3745
+ const existing = getObservation2(best.hit.observationId);
3746
+ const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
3747
+ return {
3748
+ action: "evolve",
3749
+ targetId: best.hit.observationId,
3750
+ reason: `Near-duplicate of #${best.hit.observationId} but richer content (score: ${best.score.toFixed(2)})`,
3751
+ mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
3752
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
3753
+ };
3754
+ }
3755
+ return {
3756
+ action: "discard",
3757
+ targetId: best.hit.observationId,
3758
+ reason: `Duplicate of #${best.hit.observationId} (score: ${best.score.toFixed(2)})`
3759
+ };
3760
+ }
3761
+ if (best.score >= SIMILARITY_HIGH2) {
3762
+ if (best.contradiction) {
3763
+ const existing = getObservation2(best.hit.observationId);
3764
+ const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
3765
+ return {
3766
+ action: "evolve",
3767
+ targetId: best.hit.observationId,
3768
+ reason: `Supersedes #${best.hit.observationId}: contradiction detected (score: ${best.score.toFixed(2)})`,
3769
+ mergedNarrative: extracted.narrative,
3770
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
3771
+ };
3772
+ }
3773
+ if (best.richer) {
3774
+ const existing = getObservation2(best.hit.observationId);
3775
+ const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
3776
+ return {
3777
+ action: "merge",
3778
+ targetId: best.hit.observationId,
3779
+ reason: `Merging with #${best.hit.observationId}: same topic, new content is richer (score: ${best.score.toFixed(2)})`,
3780
+ mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
3781
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
3782
+ };
3783
+ }
3784
+ return {
3785
+ action: "discard",
3786
+ targetId: best.hit.observationId,
3787
+ reason: `Already covered by #${best.hit.observationId} (score: ${best.score.toFixed(2)})`
3788
+ };
3789
+ }
3790
+ if (best.score >= SIMILARITY_MEDIUM2 && best.entityMatch) {
3791
+ const existing = getObservation2(best.hit.observationId);
3792
+ const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
3793
+ const newFactCount = extracted.facts.length;
3794
+ const oldFactCount = oldFacts.length;
3795
+ if (newFactCount > oldFactCount) {
3796
+ return {
3797
+ action: "merge",
3798
+ targetId: best.hit.observationId,
3799
+ reason: `Same entity "${extracted.entityName}", new memory has more facts (${newFactCount} > ${oldFactCount})`,
3800
+ mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
3801
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
3802
+ };
3803
+ }
3804
+ }
3805
+ return { action: "new", reason: `Different from existing memories (best score: ${best.score.toFixed(2)})` };
3806
+ }
3807
+ var SIMILARITY_HIGH2, SIMILARITY_MEDIUM2, SIMILARITY_DUPLICATE, LLM_RESOLVE_PROMPT;
3808
+ var init_resolve = __esm({
3809
+ "src/memory/formation/resolve.ts"() {
3810
+ "use strict";
3811
+ init_esm_shims();
3812
+ SIMILARITY_HIGH2 = 0.75;
3813
+ SIMILARITY_MEDIUM2 = 0.5;
3814
+ SIMILARITY_DUPLICATE = 0.9;
3815
+ LLM_RESOLVE_PROMPT = `You are a Memory Consolidation Manager for a software engineering knowledge base.
3816
+
3817
+ You must decide what to do with a NEW memory given EXISTING memories that are similar.
3818
+
3819
+ Operations:
3820
+ - ADD: The new memory contains genuinely new information not present in existing memories.
3821
+ - UPDATE: The new memory adds to or refines an existing memory. Specify which existing memory ID to update.
3822
+ - DELETE: The new memory contradicts an existing memory. Specify which existing memory ID to delete.
3823
+ - NOOP: The new memory is redundant (already covered by existing memories). Skip storage.
3824
+
3825
+ Rules:
3826
+ - Return ONLY a JSON object
3827
+ - If UPDATE: merge the best information from both old and new
3828
+ - If DELETE: the new memory supersedes the old (contradiction detected)
3829
+ - Prefer UPDATE over ADD when the topic is the same but information differs
3830
+ - Prefer NOOP over ADD when the information is essentially the same
3831
+
3832
+ Response format:
3833
+ {"action": "ADD|UPDATE|DELETE|NOOP", "targetId": <number or null>, "reason": "<brief explanation>", "mergedText": "<merged content for UPDATE, or null>"}`;
3834
+ }
3835
+ });
3836
+
3837
+ // src/memory/formation/evaluate.ts
3838
+ function factDensity(facts, narrativeLength) {
3839
+ if (narrativeLength === 0) return 0;
3840
+ const structuredChars = facts.reduce((sum, f) => sum + f.length, 0);
3841
+ return Math.min(1, structuredChars / Math.max(narrativeLength, 100));
3842
+ }
3843
+ function specificityScore(content) {
3844
+ let count2 = 0;
3845
+ for (const p of SPECIFICITY_PATTERNS) {
3846
+ p.lastIndex = 0;
3847
+ if (p.test(content)) count2++;
3848
+ }
3849
+ return Math.min(1, count2 / 3);
3850
+ }
3851
+ function causalScore(content) {
3852
+ let count2 = 0;
3853
+ for (const p of CAUSAL_PATTERNS) {
3854
+ p.lastIndex = 0;
3855
+ if (p.test(content)) count2++;
3856
+ }
3857
+ return Math.min(1, count2 / 2);
3858
+ }
3859
+ function noiseScore(title, narrative) {
3860
+ let noisiness = 0;
3861
+ for (const p of NOISE_PATTERNS) {
3862
+ if (p.test(title)) {
3863
+ noisiness += 0.3;
3864
+ break;
3865
+ }
3866
+ }
3867
+ const lines = narrative.split("\n").filter((l) => l.trim().length > 0);
3868
+ let toolOutputLines = 0;
3869
+ for (const line of lines) {
3870
+ for (const p of TOOL_OUTPUT_PATTERNS) {
3871
+ if (p.test(line)) {
3872
+ toolOutputLines++;
3873
+ break;
3874
+ }
3875
+ }
3876
+ }
3877
+ if (lines.length > 0) {
3878
+ noisiness += toolOutputLines / lines.length * 0.5;
3879
+ }
3880
+ if (narrative.length < 50) noisiness += 0.2;
3881
+ return Math.min(1, noisiness);
3882
+ }
3883
+ function categorize(score) {
3884
+ if (score >= 0.6) return "core";
3885
+ if (score >= 0.35) return "contextual";
3886
+ return "ephemeral";
3887
+ }
3888
+ function buildReason(typeWeight, factDens, specificity, causal, noise, category) {
3889
+ const parts = [];
3890
+ if (typeWeight >= 0.7) parts.push("high-value type");
3891
+ else if (typeWeight <= 0.45) parts.push("low-value type");
3892
+ if (factDens > 0.3) parts.push("fact-dense");
3893
+ if (specificity > 0.3) parts.push("specific (versions/codes/paths)");
3894
+ if (causal > 0.3) parts.push("causal reasoning");
3895
+ if (noise > 0.3) parts.push("noisy content");
3896
+ const detail = parts.length > 0 ? parts.join(", ") : "average content";
3897
+ return `${category}: ${detail}`;
3898
+ }
3899
+ function runEvaluate(extracted) {
3900
+ const content = `${extracted.title} ${extracted.narrative} ${extracted.facts.join(" ")}`;
3901
+ const typeWeight = TYPE_WEIGHTS[extracted.type] ?? 0.5;
3902
+ const factDens = factDensity(extracted.facts, extracted.narrative.length);
3903
+ const specificity = specificityScore(content);
3904
+ const causal = causalScore(content);
3905
+ const noise = noiseScore(extracted.title, extracted.narrative);
3906
+ const rawScore = typeWeight * 0.5 + factDens * 0.12 + specificity * 0.12 + causal * 0.12 - noise * 0.14;
3907
+ const extractionBonus = extracted.extractedFacts.length > 0 ? 0.05 : 0;
3908
+ const titlePenalty = extracted.titleImproved ? -0.03 : 0;
3909
+ const correctionBonus = extracted.typeCorrected ? 0.03 : 0;
3910
+ const score = Math.max(0, Math.min(1, rawScore + extractionBonus + titlePenalty + correctionBonus));
3911
+ const category = categorize(score);
3912
+ const reason = buildReason(typeWeight, factDens, specificity, causal, noise, category);
3913
+ return { score, category, reason };
3914
+ }
3915
+ var TYPE_WEIGHTS, SPECIFICITY_PATTERNS, CAUSAL_PATTERNS, NOISE_PATTERNS, TOOL_OUTPUT_PATTERNS;
3916
+ var init_evaluate = __esm({
3917
+ "src/memory/formation/evaluate.ts"() {
3918
+ "use strict";
3919
+ init_esm_shims();
3920
+ TYPE_WEIGHTS = {
3921
+ "gotcha": 0.85,
3922
+ "decision": 0.8,
3923
+ "problem-solution": 0.75,
3924
+ "trade-off": 0.7,
3925
+ "reasoning": 0.7,
3926
+ "why-it-exists": 0.65,
3927
+ "how-it-works": 0.6,
3928
+ "discovery": 0.55,
3929
+ "what-changed": 0.45,
3930
+ "session-request": 0.4
3931
+ };
3932
+ SPECIFICITY_PATTERNS = [
3933
+ /\b\d+\.\d+\.\d+\b/,
3934
+ // Semantic version numbers
3935
+ /\b(ERR_|ENOENT|ECONNREFUSED|E[A-Z]{3,})\b/,
3936
+ // Error codes
3937
+ /\b(port|PORT)\s*[:=]?\s*\d{2,5}\b/i,
3938
+ // Port numbers
3939
+ /\bhttps?:\/\/\S+/,
3940
+ // URLs
3941
+ /`[^`]{3,60}`/,
3942
+ // Inline code references
3943
+ /\b[A-Z][A-Z0-9_]{3,}\b/,
3944
+ // Constants (e.g., MAX_RETRIES)
3945
+ /\b\d+\s*(ms|s|sec|min|MB|GB|KB)\b/i
3946
+ // Measurements with units
3947
+ ];
3948
+ CAUSAL_PATTERNS = [
3949
+ /\b(because|therefore|due to|caused by|as a result|fixed by|resolved by)\b/i,
3950
+ /\b(so that|in order to|leads to|results in|prevents)\b/i,
3951
+ /(?:因为|所以|由于|导致|造成|因此|为了|解决)/
3952
+ ];
3953
+ NOISE_PATTERNS = [
3954
+ /^Session activity/i,
3955
+ /^Updated \S+\.\w+$/i,
3956
+ /^Created \S+\.\w+$/i,
3957
+ /^Deleted \S+\.\w+$/i,
3958
+ /^File written successfully/i,
3959
+ /^Command executed/i,
3960
+ /^Tool: (read_file|list_dir|find_by_name)/i,
3961
+ /^\s*$/
3962
+ ];
3963
+ TOOL_OUTPUT_PATTERNS = [
3964
+ /^(file|directory|folder)\s+(created|deleted|moved|copied)/i,
3965
+ /^Successfully\s+(installed|updated|removed)/i,
3966
+ /^\d+ files? changed/i,
3967
+ /^npm (WARN|notice)/i,
3968
+ /^\s*at\s+\S+\s+\(/
3969
+ // Stack trace lines
3970
+ ];
3971
+ }
3972
+ });
3973
+
3974
+ // src/memory/formation/index.ts
3975
+ var formation_exports = {};
3976
+ __export(formation_exports, {
3977
+ clearFormationMetrics: () => clearFormationMetrics,
3978
+ getBeforeAfterMetrics: () => getBeforeAfterMetrics,
3979
+ getFormationMetrics: () => getFormationMetrics,
3980
+ getMetricsSummary: () => getMetricsSummary,
3981
+ recordBeforeAfterMetrics: () => recordBeforeAfterMetrics,
3982
+ runFormation: () => runFormation
3983
+ });
3984
+ function getFormationMetrics() {
3985
+ return metricsBuffer;
3986
+ }
3987
+ function clearFormationMetrics() {
3988
+ metricsBuffer.length = 0;
3989
+ }
3990
+ function recordBeforeAfterMetrics(data) {
3991
+ if (beforeAfterBuffer.length >= MAX_BEFORE_AFTER_BUFFER) {
3992
+ beforeAfterBuffer.shift();
3993
+ }
3994
+ beforeAfterBuffer.push(data);
3995
+ }
3996
+ function getBeforeAfterMetrics() {
3997
+ const totalProcessed = beforeAfterBuffer.length;
3998
+ if (totalProcessed === 0) {
3999
+ return {
4000
+ totalProcessed: 0,
4001
+ agreements: 0,
4002
+ disagreements: 0,
4003
+ disagreementBreakdown: {
4004
+ formationDiscardedCompactAdded: 0,
4005
+ formationMergedCompactAdded: 0,
4006
+ formationAddedCompactDiscarded: 0,
4007
+ formationAddedCompactMerged: 0,
4008
+ formationEvolvedCompactAdded: 0,
4009
+ other: 0
4010
+ },
4011
+ quality: {
4012
+ formationDiscardedLowValue: 0,
4013
+ formationMergedDuplicates: 0,
4014
+ formationEvolvedOutdated: 0,
4015
+ compactMissedDuplicates: 0,
4016
+ compactKeptLowValue: 0
4017
+ },
4018
+ duration: {
4019
+ formationAvgMs: 0,
4020
+ compactAvgMs: 0,
4021
+ diffMs: 0
4022
+ }
4023
+ };
4024
+ }
4025
+ let agreements = 0;
4026
+ let disagreements = 0;
4027
+ const disagreementBreakdown = {
4028
+ formationDiscardedCompactAdded: 0,
4029
+ formationMergedCompactAdded: 0,
4030
+ formationAddedCompactDiscarded: 0,
4031
+ formationAddedCompactMerged: 0,
4032
+ formationEvolvedCompactAdded: 0,
4033
+ other: 0
4034
+ };
4035
+ const quality = {
4036
+ formationDiscardedLowValue: 0,
4037
+ formationMergedDuplicates: 0,
4038
+ formationEvolvedOutdated: 0,
4039
+ compactMissedDuplicates: 0,
4040
+ compactKeptLowValue: 0
4041
+ };
4042
+ let formationTotalDuration = 0;
4043
+ let compactTotalDuration = 0;
4044
+ let compactDurationCount = 0;
4045
+ for (const data of beforeAfterBuffer) {
4046
+ formationTotalDuration += data.formationDurationMs;
4047
+ if (data.compactDurationMs !== void 0) {
4048
+ compactTotalDuration += data.compactDurationMs;
4049
+ compactDurationCount++;
4050
+ }
4051
+ const formationAction = data.formationAction;
4052
+ const oldCompactAction = data.oldCompactAction;
4053
+ let formationMapped = "ADD";
4054
+ if (formationAction === "merge" || formationAction === "evolve") {
4055
+ formationMapped = "UPDATE";
4056
+ } else if (formationAction === "discard") {
4057
+ formationMapped = "NONE";
4058
+ }
4059
+ if (formationMapped === oldCompactAction) {
4060
+ agreements++;
4061
+ } else {
4062
+ disagreements++;
4063
+ if (formationAction === "discard" && oldCompactAction === "ADD") {
4064
+ disagreementBreakdown.formationDiscardedCompactAdded++;
4065
+ if (data.formationValueCategory === "ephemeral") {
4066
+ quality.formationDiscardedLowValue++;
4067
+ }
4068
+ } else if (formationAction === "merge" && oldCompactAction === "ADD") {
4069
+ disagreementBreakdown.formationMergedCompactAdded++;
4070
+ quality.formationMergedDuplicates++;
4071
+ } else if (formationAction === "new" && oldCompactAction === "NONE") {
4072
+ disagreementBreakdown.formationAddedCompactDiscarded++;
4073
+ quality.compactMissedDuplicates++;
4074
+ } else if (formationAction === "new" && oldCompactAction === "UPDATE") {
4075
+ disagreementBreakdown.formationAddedCompactMerged++;
4076
+ } else if (formationAction === "evolve" && oldCompactAction === "ADD") {
4077
+ disagreementBreakdown.formationEvolvedCompactAdded++;
4078
+ quality.formationEvolvedOutdated++;
4079
+ } else if (formationAction === "new" && oldCompactAction === "ADD") {
4080
+ if (data.formationValueCategory === "ephemeral") {
4081
+ quality.compactKeptLowValue++;
4082
+ }
4083
+ } else {
4084
+ disagreementBreakdown.other++;
4085
+ }
4086
+ }
4087
+ }
4088
+ const compactAvgMs = compactDurationCount > 0 ? compactTotalDuration / compactDurationCount : 0;
4089
+ const formationAvgMs = totalProcessed > 0 ? formationTotalDuration / totalProcessed : 0;
4090
+ return {
4091
+ totalProcessed,
4092
+ agreements,
4093
+ disagreements,
4094
+ disagreementBreakdown,
4095
+ quality,
4096
+ duration: {
4097
+ formationAvgMs,
4098
+ compactAvgMs,
4099
+ diffMs: compactAvgMs - formationAvgMs
4100
+ }
4101
+ };
4102
+ }
4103
+ function getMetricsSummary() {
4104
+ const total = metricsBuffer.length;
4105
+ if (total === 0) {
4106
+ return {
4107
+ total: 0,
4108
+ avgValueScore: 0,
4109
+ avgExtractedFacts: 0,
4110
+ titleImprovedRate: 0,
4111
+ entityResolvedRate: 0,
4112
+ typeCorectedRate: 0,
4113
+ resolutionBreakdown: {},
4114
+ categoryBreakdown: {},
4115
+ avgDurationMs: 0
4116
+ };
4117
+ }
4118
+ const sum = (fn) => metricsBuffer.reduce((s, m) => s + fn(m), 0);
4119
+ const resolutionBreakdown = {};
4120
+ const categoryBreakdown = {};
4121
+ for (const m of metricsBuffer) {
4122
+ resolutionBreakdown[m.resolutionAction] = (resolutionBreakdown[m.resolutionAction] ?? 0) + 1;
4123
+ categoryBreakdown[m.valueCategory] = (categoryBreakdown[m.valueCategory] ?? 0) + 1;
4124
+ }
4125
+ return {
4126
+ total,
4127
+ avgValueScore: sum((m) => m.valueScore) / total,
4128
+ avgExtractedFacts: sum((m) => m.systemExtractedFacts) / total,
4129
+ titleImprovedRate: sum((m) => m.titleImproved ? 1 : 0) / total,
4130
+ entityResolvedRate: sum((m) => m.entityResolved ? 1 : 0) / total,
4131
+ typeCorectedRate: sum((m) => m.typeCorrected ? 1 : 0) / total,
4132
+ resolutionBreakdown,
4133
+ categoryBreakdown,
4134
+ avgDurationMs: sum((m) => m.durationMs) / total
4135
+ };
4136
+ }
4137
+ async function runFormation(input, config) {
4138
+ const startTime = Date.now();
4139
+ let stagesCompleted = 0;
4140
+ const existingEntities = config.getEntityNames();
4141
+ const extraction = await runExtract(input, existingEntities, config.useLLM);
4142
+ stagesCompleted = 1;
4143
+ let resolution;
4144
+ if (input.topicKey) {
4145
+ resolution = {
4146
+ action: "new",
4147
+ reason: "TopicKey upsert \u2014 bypasses resolve stage"
4148
+ };
4149
+ } else {
4150
+ resolution = await runResolve(
4151
+ extraction,
4152
+ input.projectId,
4153
+ config.searchMemories,
4154
+ config.getObservation,
4155
+ config.useLLM
4156
+ );
4157
+ }
4158
+ stagesCompleted = 2;
4159
+ const evaluation = runEvaluate(extraction);
4160
+ stagesCompleted = 3;
4161
+ const durationMs = Date.now() - startTime;
4162
+ const formed = {
4163
+ // Final enriched data
4164
+ entityName: extraction.entityName,
4165
+ type: extraction.type,
4166
+ title: extraction.title,
4167
+ narrative: resolution.mergedNarrative ?? extraction.narrative,
4168
+ facts: resolution.mergedFacts ?? extraction.facts,
4169
+ // Stage results
4170
+ extraction,
4171
+ resolution,
4172
+ evaluation,
4173
+ // Pipeline metadata
4174
+ pipeline: {
4175
+ mode: config.useLLM ? "llm" : "rules",
4176
+ durationMs,
4177
+ stagesCompleted,
4178
+ shadow: config.mode === "shadow"
4179
+ },
4180
+ // Governance fields
4181
+ governance: {
4182
+ provenance: {
4183
+ creator: input.source === "explicit" ? "user" : "system",
4184
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4185
+ source: input.source
4186
+ },
4187
+ confidence: {
4188
+ score: evaluation.score,
4189
+ breakdown: {
4190
+ extractionConfidence: extraction.extractedFacts.length > 0 ? 0.8 : 0.5,
4191
+ resolutionConfidence: resolution.action === "new" ? 0.7 : 0.9,
4192
+ evaluationConfidence: evaluation.score
4193
+ },
4194
+ reason: `Value score ${evaluation.score.toFixed(2)} in ${evaluation.category} category`
4195
+ },
4196
+ supersession: (resolution.action === "merge" || resolution.action === "evolve") && resolution.targetId ? {
4197
+ replacedIds: [resolution.targetId],
4198
+ reason: resolution.reason,
4199
+ replacementType: resolution.action === "evolve" ? "hard" : "soft"
4200
+ } : void 0
4201
+ }
4202
+ };
4203
+ const metrics = {
4204
+ systemExtractedFacts: extraction.extractedFacts.length,
4205
+ titleImproved: extraction.titleImproved,
4206
+ entityResolved: extraction.entityResolved,
4207
+ typeCorrected: extraction.typeCorrected,
4208
+ resolutionAction: resolution.action,
4209
+ valueScore: evaluation.score,
4210
+ valueCategory: evaluation.category,
4211
+ durationMs,
4212
+ mode: "rules"
4213
+ };
4214
+ if (metricsBuffer.length >= MAX_METRICS_BUFFER) {
4215
+ metricsBuffer.shift();
4216
+ }
4217
+ metricsBuffer.push(metrics);
4218
+ return formed;
4219
+ }
4220
+ var metricsBuffer, MAX_METRICS_BUFFER, beforeAfterBuffer, MAX_BEFORE_AFTER_BUFFER;
4221
+ var init_formation = __esm({
4222
+ "src/memory/formation/index.ts"() {
4223
+ "use strict";
4224
+ init_esm_shims();
4225
+ init_extract();
4226
+ init_resolve();
4227
+ init_evaluate();
4228
+ metricsBuffer = [];
4229
+ MAX_METRICS_BUFFER = 500;
4230
+ beforeAfterBuffer = [];
4231
+ MAX_BEFORE_AFTER_BUFFER = 500;
4232
+ }
4233
+ });
4234
+
4235
+ // src/config/behavior.ts
4236
+ var behavior_exports = {};
4237
+ __export(behavior_exports, {
4238
+ getBehaviorConfig: () => getBehaviorConfig,
4239
+ resetBehaviorConfigCache: () => resetBehaviorConfigCache
4240
+ });
4241
+ import { readFileSync as readFileSync6 } from "fs";
4242
+ import { join as join17 } from "path";
4243
+ import { homedir as homedir16 } from "os";
4244
+ function getBehaviorConfig() {
4245
+ if (cached) return cached;
4246
+ try {
4247
+ const configPath = join17(homedir16(), ".memorix", "config.json");
4248
+ const raw = readFileSync6(configPath, "utf-8");
4249
+ const config = JSON.parse(raw);
4250
+ const behavior = config.behavior ?? {};
4251
+ cached = {
4252
+ sessionInject: behavior.sessionInject ?? DEFAULTS.sessionInject,
4253
+ syncAdvisory: behavior.syncAdvisory ?? DEFAULTS.syncAdvisory,
4254
+ autoCleanup: behavior.autoCleanup ?? DEFAULTS.autoCleanup,
4255
+ formationMode: behavior.formationMode ?? DEFAULTS.formationMode
4256
+ };
4257
+ } catch {
4258
+ cached = { ...DEFAULTS };
4259
+ }
4260
+ return cached;
2887
4261
  }
2888
- async function deduplicateMemory(newMemory, existingMemories) {
2889
- if (!isLLMEnabled()) return null;
2890
- if (existingMemories.length === 0) return { action: "ADD", reason: "No existing memories", usedLLM: false };
2891
- const asExisting = existingMemories.map((m) => ({
2892
- ...m,
2893
- score: 0.8
2894
- // Batch dedup assumes high similarity (pre-filtered)
2895
- }));
2896
- return compactOnWrite(newMemory, asExisting);
4262
+ function resetBehaviorConfigCache() {
4263
+ cached = null;
2897
4264
  }
2898
- var COMPACT_ON_WRITE_PROMPT, SIMILARITY_HIGH, SIMILARITY_MEDIUM;
2899
- var init_memory_manager = __esm({
2900
- "src/llm/memory-manager.ts"() {
4265
+ var DEFAULTS, cached;
4266
+ var init_behavior = __esm({
4267
+ "src/config/behavior.ts"() {
2901
4268
  "use strict";
2902
4269
  init_esm_shims();
2903
- init_provider2();
2904
- COMPACT_ON_WRITE_PROMPT = `You are a smart coding memory manager. You control the memory of a cross-IDE coding assistant.
2905
-
2906
- You receive a NEW MEMORY to store and a list of EXISTING similar memories. Your job:
2907
- 1. Extract the key facts from the new memory
2908
- 2. Compare with existing memories
2909
- 3. Decide the best action
2910
-
2911
- Actions:
2912
- - ADD: New memory contains unique information. Store it as-is.
2913
- - UPDATE: New memory supersedes or improves an existing one. Merge them into a single, comprehensive memory.
2914
- - DELETE: An existing memory is outdated/contradicted by the new one. Remove it.
2915
- - NONE: New memory is redundant. Existing memories already cover this. Skip storing.
2916
-
2917
- Decision rules:
2918
- - Same topic updated (e.g., "MySQL \u2192 PostgreSQL"): UPDATE the old memory with merged content
2919
- - Bug fixed that was reported as open: UPDATE the bug report to include the fix
2920
- - Task completed that was tracked as in-progress: UPDATE to mark completed
2921
- - Minor variation of existing memory: NONE (skip)
2922
- - Completely new topic: ADD
2923
- - Old info directly contradicted: DELETE the old one
2924
- - Prefer UPDATE over ADD \u2014 keep memory count low, merge information
2925
-
2926
- For UPDATE: write a merged narrative that combines the best of both old and new, preserving all important details. Also merge the facts lists, removing duplicates.
2927
-
2928
- Respond in JSON only:
2929
- {
2930
- "action": "ADD" | "UPDATE" | "DELETE" | "NONE",
2931
- "targetId": null or existing_memory_id_number,
2932
- "reason": "brief explanation of decision",
2933
- "mergedNarrative": "merged narrative text (required for UPDATE, null otherwise)",
2934
- "mergedFacts": ["merged fact 1", "merged fact 2"] or null,
2935
- "extractedFacts": ["fact extracted from new content 1", "fact 2"]
2936
- }`;
2937
- SIMILARITY_HIGH = 0.75;
2938
- SIMILARITY_MEDIUM = 0.5;
4270
+ DEFAULTS = {
4271
+ sessionInject: "minimal",
4272
+ syncAdvisory: true,
4273
+ autoCleanup: true,
4274
+ formationMode: "active"
4275
+ };
4276
+ cached = null;
2939
4277
  }
2940
4278
  });
2941
4279
 
@@ -3187,7 +4525,8 @@ async function archiveExpired(projectDir2, referenceTime, accessMap) {
3187
4525
  projectId: obs.projectId,
3188
4526
  accessCount: access2?.accessCount ?? 0,
3189
4527
  lastAccessedAt: access2?.lastAccessedAt ?? "",
3190
- status: obs.status ?? "active"
4528
+ status: obs.status ?? "active",
4529
+ source: obs.source ?? "agent"
3191
4530
  };
3192
4531
  };
3193
4532
  const toArchive = [];
@@ -3232,6 +4571,7 @@ var init_retention = __esm({
3232
4571
  gotcha: "high",
3233
4572
  decision: "high",
3234
4573
  "trade-off": "high",
4574
+ reasoning: "high",
3235
4575
  "problem-solution": "medium",
3236
4576
  "how-it-works": "medium",
3237
4577
  "what-changed": "low",
@@ -3249,10 +4589,10 @@ var engine_exports = {};
3249
4589
  __export(engine_exports, {
3250
4590
  SkillsEngine: () => SkillsEngine
3251
4591
  });
3252
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
3253
- import { join as join15 } from "path";
3254
- import { homedir as homedir14 } from "os";
3255
- var SKILLS_DIRS, SKILL_WORTHY_TYPES, MIN_OBS_FOR_SKILL, MIN_SCORE_FOR_SKILL, SkillsEngine;
4592
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
4593
+ import { join as join18 } from "path";
4594
+ import { homedir as homedir17 } from "os";
4595
+ var SKILLS_DIRS, SKILL_WORTHY_TYPES, MIN_OBS_FOR_SKILL, MIN_SCORE_FOR_SKILL, LOW_SIGNAL_TITLE_PATTERNS, LOW_SIGNAL_ENTITY_PATTERNS, COMMAND_TRACE_PATTERNS, SkillsEngine;
3256
4596
  var init_engine = __esm({
3257
4597
  "src/skills/engine.ts"() {
3258
4598
  "use strict";
@@ -3277,6 +4617,31 @@ var init_engine = __esm({
3277
4617
  ]);
3278
4618
  MIN_OBS_FOR_SKILL = 3;
3279
4619
  MIN_SCORE_FOR_SKILL = 5;
4620
+ LOW_SIGNAL_TITLE_PATTERNS = [
4621
+ /^ran:/i,
4622
+ /^command:/i
4623
+ ];
4624
+ LOW_SIGNAL_ENTITY_PATTERNS = [
4625
+ /^(?:bash|sh|cmd|powershell|pwsh|node|npm|npx|pnpm|yarn|gh|git)$/i,
4626
+ /^mcp[_-]/i
4627
+ ];
4628
+ COMMAND_TRACE_PATTERNS = [
4629
+ /\bcommand:\b/i,
4630
+ /\b2>&1\b/i,
4631
+ /\bselect-string\b/i,
4632
+ /\bget-content\b/i,
4633
+ /\bget-command\b/i,
4634
+ /\bpowershell\b/i,
4635
+ /\bcmd(?:\.exe)?\b/i,
4636
+ /\bnpm\b/i,
4637
+ /\bnpx\b/i,
4638
+ /\bpnpm\b/i,
4639
+ /\byarn\b/i,
4640
+ /\bgit\b/i,
4641
+ /\bgh\b/i,
4642
+ /\|/,
4643
+ /&&/
4644
+ ];
3280
4645
  SkillsEngine = class {
3281
4646
  constructor(projectRoot, options) {
3282
4647
  this.projectRoot = projectRoot;
@@ -3292,30 +4657,30 @@ var init_engine = __esm({
3292
4657
  listSkills() {
3293
4658
  const skills = [];
3294
4659
  const seen = /* @__PURE__ */ new Set();
3295
- const home = homedir14();
4660
+ const home = homedir17();
3296
4661
  for (const [agent, dirs] of Object.entries(SKILLS_DIRS)) {
3297
4662
  for (const dir of dirs) {
3298
- const paths = [join15(this.projectRoot, dir)];
4663
+ const paths = [join18(this.projectRoot, dir)];
3299
4664
  if (!this.skipGlobal) {
3300
- paths.push(join15(home, dir));
4665
+ paths.push(join18(home, dir));
3301
4666
  }
3302
4667
  for (const skillsRoot of paths) {
3303
- if (!existsSync5(skillsRoot)) continue;
4668
+ if (!existsSync7(skillsRoot)) continue;
3304
4669
  try {
3305
- const entries = readdirSync2(skillsRoot, { withFileTypes: true });
4670
+ const entries = readdirSync3(skillsRoot, { withFileTypes: true });
3306
4671
  for (const entry of entries) {
3307
4672
  if (!entry.isDirectory()) continue;
3308
4673
  const name = entry.name;
3309
4674
  if (seen.has(name)) continue;
3310
- const skillMd = join15(skillsRoot, name, "SKILL.md");
3311
- if (!existsSync5(skillMd)) continue;
4675
+ const skillMd = join18(skillsRoot, name, "SKILL.md");
4676
+ if (!existsSync7(skillMd)) continue;
3312
4677
  try {
3313
- const content = readFileSync4(skillMd, "utf-8");
4678
+ const content = readFileSync7(skillMd, "utf-8");
3314
4679
  const description = this.parseDescription(content);
3315
4680
  skills.push({
3316
4681
  name,
3317
4682
  description,
3318
- sourcePath: join15(skillsRoot, name),
4683
+ sourcePath: join18(skillsRoot, name),
3319
4684
  sourceAgent: agent,
3320
4685
  content,
3321
4686
  generated: false
@@ -3339,7 +4704,8 @@ var init_engine = __esm({
3339
4704
  * rich knowledge accumulation.
3340
4705
  */
3341
4706
  generateFromObservations(observations2) {
3342
- const clusters = this.clusterByEntity(observations2);
4707
+ const candidates = observations2.filter((obs) => !this.isLowSignalObservation(obs));
4708
+ const clusters = this.clusterByEntity(candidates);
3343
4709
  for (const cluster of clusters.values()) {
3344
4710
  cluster.score = this.scoreCluster(cluster);
3345
4711
  }
@@ -3357,11 +4723,11 @@ var init_engine = __esm({
3357
4723
  writeSkill(skill, target) {
3358
4724
  const dirs = SKILLS_DIRS[target];
3359
4725
  if (!dirs || dirs.length === 0) return null;
3360
- const targetDir = join15(this.projectRoot, dirs[0], skill.name);
4726
+ const targetDir = join18(this.projectRoot, dirs[0], skill.name);
3361
4727
  try {
3362
4728
  mkdirSync3(targetDir, { recursive: true });
3363
- writeFileSync2(join15(targetDir, "SKILL.md"), skill.content, "utf-8");
3364
- return join15(dirs[0], skill.name, "SKILL.md");
4729
+ writeFileSync2(join18(targetDir, "SKILL.md"), skill.content, "utf-8");
4730
+ return join18(dirs[0], skill.name, "SKILL.md");
3365
4731
  } catch {
3366
4732
  return null;
3367
4733
  }
@@ -3383,6 +4749,24 @@ var init_engine = __esm({
3383
4749
  const match = content.match(/^---[\s\S]*?description:\s*["']?(.+?)["']?\s*$/m);
3384
4750
  return match ? match[1] : "";
3385
4751
  }
4752
+ isLowSignalObservation(obs) {
4753
+ if (obs.status === "archived") return true;
4754
+ const title = obs.title.trim();
4755
+ const narrative = obs.narrative.trim();
4756
+ const entity = (obs.entityName || "").trim();
4757
+ const haystack = `${title}
4758
+ ${narrative}`;
4759
+ if (LOW_SIGNAL_TITLE_PATTERNS.some((pattern) => pattern.test(title))) {
4760
+ return true;
4761
+ }
4762
+ if (LOW_SIGNAL_ENTITY_PATTERNS.some((pattern) => pattern.test(entity))) {
4763
+ return true;
4764
+ }
4765
+ if (obs.source !== "git" && COMMAND_TRACE_PATTERNS.filter((pattern) => pattern.test(haystack)).length >= 2) {
4766
+ return true;
4767
+ }
4768
+ return false;
4769
+ }
3386
4770
  clusterByEntity(observations2) {
3387
4771
  const clusters = /* @__PURE__ */ new Map();
3388
4772
  for (const obs of observations2) {
@@ -3577,7 +4961,7 @@ async function promoteToMiniSkill(projectDir2, projectId, observations2, options
3577
4961
  const title = generateTitle(observations2);
3578
4962
  const instruction = options?.instruction || generateInstruction(observations2);
3579
4963
  const trigger = options?.trigger || generateTrigger(observations2);
3580
- const facts = extractFacts(observations2);
4964
+ const facts = extractFacts2(observations2);
3581
4965
  const tags = [
3582
4966
  ...options?.tags || [],
3583
4967
  ...extractTags(observations2)
@@ -3685,7 +5069,7 @@ function generateTrigger(observations2) {
3685
5069
  }
3686
5070
  return parts.length > 0 ? parts.join("; ") : `Related to ${obs.title}`;
3687
5071
  }
3688
- function extractFacts(observations2) {
5072
+ function extractFacts2(observations2) {
3689
5073
  const facts = /* @__PURE__ */ new Set();
3690
5074
  for (const obs of observations2) {
3691
5075
  for (const f of obs.facts) {
@@ -4113,6 +5497,45 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir) {
4113
5497
  const t = obs.type || "unknown";
4114
5498
  typeCounts[t] = (typeCounts[t] || 0) + 1;
4115
5499
  }
5500
+ const sourceCounts = { git: 0, agent: 0, manual: 0 };
5501
+ const gitMemories = [];
5502
+ const now = Date.now();
5503
+ const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1e3;
5504
+ let recentGitCount = 0;
5505
+ for (const obs of observations2) {
5506
+ const src = obs.source || "agent";
5507
+ sourceCounts[src] = (sourceCounts[src] || 0) + 1;
5508
+ if (src === "git") {
5509
+ gitMemories.push(obs);
5510
+ if (obs.createdAt && new Date(obs.createdAt).getTime() > sevenDaysAgo) {
5511
+ recentGitCount++;
5512
+ }
5513
+ }
5514
+ }
5515
+ const gitSorted = [...gitMemories].sort((a, b) => (b.id || 0) - (a.id || 0));
5516
+ const recentGitMemories = gitSorted.slice(0, 8).map((o) => ({
5517
+ id: o.id,
5518
+ title: o.title,
5519
+ type: o.type,
5520
+ commitHash: o.commitHash,
5521
+ entityName: o.entityName,
5522
+ createdAt: o.createdAt,
5523
+ filesModified: o.filesModified
5524
+ }));
5525
+ let retentionSummary = { active: 0, stale: 0, archive: 0, immune: 0 };
5526
+ for (const obs of observations2) {
5527
+ const age = now - new Date(obs.createdAt || now).getTime();
5528
+ const ageHours = age / (1e3 * 60 * 60);
5529
+ const importance = obs.importance ?? 5;
5530
+ const accessCount = obs.accessCount ?? 0;
5531
+ const lambda = 0.01;
5532
+ const score = Math.min(importance * Math.exp(-lambda * ageHours) + Math.min(accessCount * 0.5, 3), 10);
5533
+ const isImmune2 = importance >= 8 || obs.type === "gotcha" || obs.type === "decision";
5534
+ if (isImmune2) retentionSummary.immune++;
5535
+ if (score >= 3) retentionSummary.active++;
5536
+ else if (score >= 1) retentionSummary.stale++;
5537
+ else retentionSummary.archive++;
5538
+ }
4116
5539
  const sorted = [...observations2].sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
4117
5540
  let embeddingStatus = { enabled: false, provider: "", dimensions: 0 };
4118
5541
  try {
@@ -4131,8 +5554,15 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir) {
4131
5554
  observations: observations2.length,
4132
5555
  nextId: nextId2,
4133
5556
  typeCounts,
5557
+ sourceCounts,
4134
5558
  recentObservations: sorted,
4135
- embedding: embeddingStatus
5559
+ embedding: embeddingStatus,
5560
+ gitSummary: {
5561
+ total: gitMemories.length,
5562
+ recentWeek: recentGitCount,
5563
+ recentMemories: recentGitMemories
5564
+ },
5565
+ retentionSummary
4136
5566
  });
4137
5567
  break;
4138
5568
  }
@@ -4172,6 +5602,109 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir) {
4172
5602
  });
4173
5603
  break;
4174
5604
  }
5605
+ case "/config": {
5606
+ const os4 = await import("os");
5607
+ const { existsSync: existsSync10 } = await import("fs");
5608
+ const { join: join21 } = await import("path");
5609
+ let yml = {};
5610
+ try {
5611
+ const { loadYamlConfig: loadYamlConfig2 } = await Promise.resolve().then(() => (init_yaml_loader(), yaml_loader_exports));
5612
+ yml = loadYamlConfig2(effectiveProjectId.includes("/") ? void 0 : void 0);
5613
+ } catch {
5614
+ }
5615
+ const projectRoot = process.cwd();
5616
+ const files = {
5617
+ "project memorix.yml": { exists: false, path: "" },
5618
+ "user memorix.yml": { exists: false, path: "" },
5619
+ "project .env": { exists: false, path: "" },
5620
+ "user .env": { exists: false, path: "" },
5621
+ "legacy config.json": { exists: false, path: "" }
5622
+ };
5623
+ try {
5624
+ const home = os4.homedir();
5625
+ const paths = {
5626
+ "project memorix.yml": join21(projectRoot, "memorix.yml"),
5627
+ "user memorix.yml": join21(home, ".memorix", "memorix.yml"),
5628
+ "project .env": join21(projectRoot, ".env"),
5629
+ "user .env": join21(home, ".memorix", ".env"),
5630
+ "legacy config.json": join21(home, ".memorix", "config.json")
5631
+ };
5632
+ for (const [key, fpath] of Object.entries(paths)) {
5633
+ files[key] = { exists: existsSync10(fpath), path: fpath };
5634
+ }
5635
+ } catch {
5636
+ }
5637
+ const values = [];
5638
+ const llmProvider = process.env.MEMORIX_LLM_PROVIDER || yml.llm?.provider;
5639
+ if (llmProvider) values.push({ key: "llm.provider", value: llmProvider, source: process.env.MEMORIX_LLM_PROVIDER ? "env" : "memorix.yml" });
5640
+ const llmModel = process.env.MEMORIX_LLM_MODEL || yml.llm?.model;
5641
+ if (llmModel) values.push({ key: "llm.model", value: llmModel, source: process.env.MEMORIX_LLM_MODEL ? "env" : "memorix.yml" });
5642
+ const llmKey = process.env.MEMORIX_LLM_API_KEY || process.env.MEMORIX_API_KEY || yml.llm?.apiKey || process.env.OPENAI_API_KEY;
5643
+ if (llmKey) {
5644
+ let src = "unknown";
5645
+ if (process.env.MEMORIX_LLM_API_KEY) src = "env:MEMORIX_LLM_API_KEY";
5646
+ else if (process.env.MEMORIX_API_KEY) src = "env:MEMORIX_API_KEY";
5647
+ else if (yml.llm?.apiKey) src = "memorix.yml (move to .env!)";
5648
+ else if (process.env.OPENAI_API_KEY) src = "env:OPENAI_API_KEY";
5649
+ values.push({ key: "llm.apiKey", value: "****" + llmKey.slice(-4), source: src, sensitive: true });
5650
+ } else {
5651
+ values.push({ key: "llm.apiKey", value: "not set", source: "none" });
5652
+ }
5653
+ const embProvider = process.env.MEMORIX_EMBEDDING || yml.embedding?.provider || "off";
5654
+ values.push({ key: "embedding.provider", value: embProvider, source: process.env.MEMORIX_EMBEDDING ? "env" : yml.embedding?.provider ? "memorix.yml" : "default" });
5655
+ values.push({ key: "git.autoHook", value: String(yml.git?.autoHook ?? false), source: yml.git?.autoHook !== void 0 ? "memorix.yml" : "default" });
5656
+ values.push({ key: "git.skipMergeCommits", value: String(yml.git?.skipMergeCommits ?? true), source: yml.git?.skipMergeCommits !== void 0 ? "memorix.yml" : "default" });
5657
+ if (yml.behavior?.formationMode) values.push({ key: "behavior.formationMode", value: yml.behavior.formationMode, source: "memorix.yml" });
5658
+ if (yml.behavior?.sessionInject) values.push({ key: "behavior.sessionInject", value: yml.behavior.sessionInject, source: "memorix.yml" });
5659
+ values.push({ key: "server.transport", value: yml.server?.transport || "stdio", source: yml.server?.transport ? "memorix.yml" : "default" });
5660
+ values.push({ key: "server.dashboard", value: String(yml.server?.dashboard ?? true), source: yml.server?.dashboard !== void 0 ? "memorix.yml" : "default" });
5661
+ sendJson(res, { files, values });
5662
+ break;
5663
+ }
5664
+ case "/identity": {
5665
+ const allObs = await loadObservationsJson(baseDir);
5666
+ const allProjectIds = [...new Set(allObs.map((o) => o.projectId).filter(Boolean))];
5667
+ const dirtyPatterns = [
5668
+ /^placeholder\//,
5669
+ /System32/i,
5670
+ /Microsoft VS Code/i,
5671
+ /node_modules/i,
5672
+ /\.vscode/i,
5673
+ /^local\/[A-Z]:\\/
5674
+ ];
5675
+ const dirtyIds = allProjectIds.filter((id) => dirtyPatterns.some((p) => p.test(id)));
5676
+ let aliasGroups = [];
5677
+ let canonicalId = effectiveProjectId;
5678
+ try {
5679
+ const aliasModule = await Promise.resolve().then(() => (init_aliases(), aliases_exports));
5680
+ canonicalId = await aliasModule.getCanonicalId(effectiveProjectId);
5681
+ const { promises: fsP } = await import("fs");
5682
+ const registryPath = path7.join(baseDir, ".project-aliases.json");
5683
+ const raw = await fsP.readFile(registryPath, "utf-8");
5684
+ const registry = JSON.parse(raw);
5685
+ aliasGroups = registry.groups || [];
5686
+ } catch {
5687
+ }
5688
+ const currentGroup = aliasGroups.find((g) => g.aliases?.includes(effectiveProjectId) || g.canonical === effectiveProjectId);
5689
+ const aliases = currentGroup?.aliases || [effectiveProjectId];
5690
+ const hasDirtyIds = dirtyIds.length > 0;
5691
+ const hasMultipleUnmerged = allProjectIds.length > aliasGroups.length + 1;
5692
+ const isHealthy = !hasDirtyIds && !hasMultipleUnmerged;
5693
+ sendJson(res, {
5694
+ currentProjectId: effectiveProjectId,
5695
+ canonicalId,
5696
+ aliases,
5697
+ allProjectIds,
5698
+ dirtyIds,
5699
+ aliasGroups: aliasGroups.length,
5700
+ isHealthy,
5701
+ healthIssues: [
5702
+ ...hasDirtyIds ? [`${dirtyIds.length} dirty project ID(s) detected`] : [],
5703
+ ...hasMultipleUnmerged ? ["Possible unmerged project identity splits"] : []
5704
+ ]
5705
+ });
5706
+ break;
5707
+ }
4175
5708
  default: {
4176
5709
  const deleteMatch = apiPath.match(/^\/observations\/(\d+)$/);
4177
5710
  if (deleteMatch && req.method === "DELETE") {
@@ -4294,7 +5827,11 @@ async function startDashboard(dataDir, port, staticDir, projectId, projectName,
4294
5827
  }
4295
5828
  return;
4296
5829
  }
4297
- if (url.startsWith("/api/team") && teamInstances) {
5830
+ if (url.startsWith("/api/team")) {
5831
+ if (!teamInstances) {
5832
+ sendJson(res, { unavailable: true, reason: "http-transport-required" });
5833
+ return;
5834
+ }
4298
5835
  try {
4299
5836
  teamInstances.fileLocks.cleanExpired();
4300
5837
  const agents = teamInstances.registry.listAgents();
@@ -4945,7 +6482,7 @@ __export(persistence_exports2, {
4945
6482
  TeamPersistence: () => TeamPersistence
4946
6483
  });
4947
6484
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
4948
- import { statSync, renameSync as renameSync2, existsSync as existsSync6 } from "fs";
6485
+ import { statSync as statSync2, renameSync as renameSync2, existsSync as existsSync8 } from "fs";
4949
6486
  import { dirname as dirname2 } from "path";
4950
6487
  var TeamPersistence;
4951
6488
  var init_persistence2 = __esm({
@@ -4964,8 +6501,8 @@ var init_persistence2 = __esm({
4964
6501
  /** Reload state from disk if the file changed since last read */
4965
6502
  async sync() {
4966
6503
  try {
4967
- if (!existsSync6(this.filePath)) return;
4968
- const st = statSync(this.filePath);
6504
+ if (!existsSync8(this.filePath)) return;
6505
+ const st = statSync2(this.filePath);
4969
6506
  if (st.mtimeMs <= this.lastMtimeMs) return;
4970
6507
  } catch {
4971
6508
  return;
@@ -4979,7 +6516,7 @@ var init_persistence2 = __esm({
4979
6516
  this.taskManager.hydrate(snap.tasks);
4980
6517
  this.fileLocks.hydrate(snap.locks);
4981
6518
  try {
4982
- this.lastMtimeMs = statSync(this.filePath).mtimeMs;
6519
+ this.lastMtimeMs = statSync2(this.filePath).mtimeMs;
4983
6520
  } catch {
4984
6521
  }
4985
6522
  } catch {
@@ -5009,7 +6546,7 @@ var init_persistence2 = __esm({
5009
6546
  }
5010
6547
  }
5011
6548
  try {
5012
- this.lastMtimeMs = statSync(this.filePath).mtimeMs;
6549
+ this.lastMtimeMs = statSync2(this.filePath).mtimeMs;
5013
6550
  } catch {
5014
6551
  }
5015
6552
  }
@@ -5017,17 +6554,118 @@ var init_persistence2 = __esm({
5017
6554
  }
5018
6555
  });
5019
6556
 
6557
+ // src/audit/index.ts
6558
+ var audit_exports = {};
6559
+ __export(audit_exports, {
6560
+ getAllAuditEntries: () => getAllAuditEntries,
6561
+ getProjectFiles: () => getProjectFiles,
6562
+ getProjectId: () => getProjectId,
6563
+ loadAudit: () => loadAudit,
6564
+ recordFile: () => recordFile,
6565
+ removeFile: () => removeFile,
6566
+ saveAudit: () => saveAudit
6567
+ });
6568
+ import * as fs6 from "fs/promises";
6569
+ import * as path8 from "path";
6570
+ import { homedir as homedir18 } from "os";
6571
+ async function loadAudit() {
6572
+ try {
6573
+ const content = await fs6.readFile(AUDIT_FILE, "utf-8");
6574
+ return JSON.parse(content);
6575
+ } catch {
6576
+ return {
6577
+ version: "1.0.0",
6578
+ projects: {}
6579
+ };
6580
+ }
6581
+ }
6582
+ async function saveAudit(data) {
6583
+ const dir = path8.dirname(AUDIT_FILE);
6584
+ await fs6.mkdir(dir, { recursive: true });
6585
+ await fs6.writeFile(AUDIT_FILE, JSON.stringify(data, null, 2), "utf-8");
6586
+ }
6587
+ function getProjectId(projectRoot) {
6588
+ try {
6589
+ const { detectProject: detectProject2 } = (init_detector(), __toCommonJS(detector_exports));
6590
+ const project = detectProject2(projectRoot);
6591
+ if (project) return project.id;
6592
+ } catch {
6593
+ }
6594
+ const normalized = projectRoot.replace(/\\/g, "/");
6595
+ return `untracked/${normalized}`;
6596
+ }
6597
+ async function recordFile(projectRoot, type, filePath, agent) {
6598
+ const data = await loadAudit();
6599
+ const projectId = getProjectId(projectRoot);
6600
+ if (!data.projects[projectId]) {
6601
+ data.projects[projectId] = {
6602
+ projectRoot,
6603
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
6604
+ entries: []
6605
+ };
6606
+ }
6607
+ const existingIndex = data.projects[projectId].entries.findIndex(
6608
+ (e) => e.path === filePath
6609
+ );
6610
+ if (existingIndex === -1) {
6611
+ data.projects[projectId].entries.push({
6612
+ type,
6613
+ agent,
6614
+ path: filePath,
6615
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6616
+ });
6617
+ }
6618
+ await saveAudit(data);
6619
+ }
6620
+ async function getProjectFiles(projectRoot) {
6621
+ const data = await loadAudit();
6622
+ const projectId = getProjectId(projectRoot);
6623
+ return data.projects[projectId]?.entries || [];
6624
+ }
6625
+ async function removeFile(projectRoot, filePath) {
6626
+ const data = await loadAudit();
6627
+ const projectId = getProjectId(projectRoot);
6628
+ if (!data.projects[projectId]) return;
6629
+ data.projects[projectId].entries = data.projects[projectId].entries.filter(
6630
+ (e) => e.path !== filePath
6631
+ );
6632
+ if (data.projects[projectId].entries.length === 0) {
6633
+ delete data.projects[projectId];
6634
+ }
6635
+ await saveAudit(data);
6636
+ }
6637
+ async function getAllAuditEntries() {
6638
+ const data = await loadAudit();
6639
+ const entries = [];
6640
+ for (const [projectId, project] of Object.entries(data.projects)) {
6641
+ for (const entry of project.entries) {
6642
+ entries.push({ projectId, entry });
6643
+ }
6644
+ }
6645
+ return entries;
6646
+ }
6647
+ var AUDIT_FILE;
6648
+ var init_audit = __esm({
6649
+ "src/audit/index.ts"() {
6650
+ "use strict";
6651
+ init_esm_shims();
6652
+ AUDIT_FILE = path8.join(homedir18(), ".memorix", "audit.json");
6653
+ }
6654
+ });
6655
+
5020
6656
  // src/hooks/installers/index.ts
5021
6657
  var installers_exports = {};
5022
6658
  __export(installers_exports, {
5023
6659
  detectInstalledAgents: () => detectInstalledAgents,
6660
+ getGlobalConfigPath: () => getGlobalConfigPath,
5024
6661
  getHookStatus: () => getHookStatus,
6662
+ getProjectConfigPath: () => getProjectConfigPath,
5025
6663
  installHooks: () => installHooks,
5026
6664
  uninstallHooks: () => uninstallHooks
5027
6665
  });
5028
- import * as fs6 from "fs/promises";
5029
- import * as path8 from "path";
5030
- import * as os4 from "os";
6666
+ import * as fs7 from "fs/promises";
6667
+ import * as path9 from "path";
6668
+ import * as os3 from "os";
5031
6669
  function resolveHookCommand() {
5032
6670
  if (process.platform === "win32") {
5033
6671
  return "memorix.cmd";
@@ -5261,101 +6899,101 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
5261
6899
  function getProjectConfigPath(agent, projectRoot) {
5262
6900
  switch (agent) {
5263
6901
  case "claude":
5264
- return path8.join(projectRoot, ".claude", "settings.local.json");
6902
+ return path9.join(projectRoot, ".claude", "settings.local.json");
5265
6903
  case "copilot":
5266
- return path8.join(projectRoot, ".github", "hooks", "memorix.json");
6904
+ return path9.join(projectRoot, ".github", "hooks", "memorix.json");
5267
6905
  case "windsurf":
5268
- return path8.join(projectRoot, ".windsurf", "hooks.json");
6906
+ return path9.join(projectRoot, ".windsurf", "hooks.json");
5269
6907
  case "cursor":
5270
- return path8.join(projectRoot, ".cursor", "hooks.json");
6908
+ return path9.join(projectRoot, ".cursor", "hooks.json");
5271
6909
  case "kiro":
5272
- return path8.join(projectRoot, ".kiro", "hooks", "memorix-agent-stop.kiro.hook");
6910
+ return path9.join(projectRoot, ".kiro", "hooks", "memorix-agent-stop.kiro.hook");
5273
6911
  case "codex":
5274
- return path8.join(projectRoot, "AGENTS.md");
6912
+ return path9.join(projectRoot, "AGENTS.md");
5275
6913
  case "trae":
5276
- return path8.join(projectRoot, ".trae", "rules", "project_rules.md");
6914
+ return path9.join(projectRoot, ".trae", "rules", "project_rules.md");
5277
6915
  case "opencode":
5278
- return path8.join(projectRoot, ".opencode", "plugins", "memorix.js");
6916
+ return path9.join(projectRoot, ".opencode", "plugins", "memorix.js");
5279
6917
  case "antigravity":
5280
- return path8.join(projectRoot, ".gemini", "settings.json");
6918
+ return path9.join(projectRoot, ".gemini", "settings.json");
5281
6919
  default:
5282
- return path8.join(projectRoot, ".memorix", "hooks.json");
6920
+ return path9.join(projectRoot, ".memorix", "hooks.json");
5283
6921
  }
5284
6922
  }
5285
6923
  function getGlobalConfigPath(agent) {
5286
- const home = os4.homedir();
6924
+ const home = os3.homedir();
5287
6925
  switch (agent) {
5288
6926
  case "claude":
5289
6927
  case "copilot":
5290
- return path8.join(home, ".claude", "settings.json");
6928
+ return path9.join(home, ".claude", "settings.json");
5291
6929
  case "windsurf":
5292
- return path8.join(home, ".codeium", "windsurf", "hooks.json");
6930
+ return path9.join(home, ".codeium", "windsurf", "hooks.json");
5293
6931
  case "cursor":
5294
- return path8.join(home, ".cursor", "hooks.json");
6932
+ return path9.join(home, ".cursor", "hooks.json");
5295
6933
  case "antigravity":
5296
- return path8.join(home, ".gemini", "settings.json");
6934
+ return path9.join(home, ".gemini", "settings.json");
5297
6935
  case "opencode":
5298
- return path8.join(home, ".config", "opencode", "plugins", "memorix.js");
6936
+ return path9.join(home, ".config", "opencode", "plugins", "memorix.js");
5299
6937
  case "trae":
5300
- return path8.join(home, ".trae", "rules", "project_rules.md");
6938
+ return path9.join(home, ".trae", "rules", "project_rules.md");
5301
6939
  default:
5302
- return path8.join(home, ".memorix", "hooks.json");
6940
+ return path9.join(home, ".memorix", "hooks.json");
5303
6941
  }
5304
6942
  }
5305
6943
  async function detectInstalledAgents() {
5306
6944
  const agents = [];
5307
- const home = os4.homedir();
5308
- const claudeDir = path8.join(home, ".claude");
6945
+ const home = os3.homedir();
6946
+ const claudeDir = path9.join(home, ".claude");
5309
6947
  try {
5310
- await fs6.access(claudeDir);
6948
+ await fs7.access(claudeDir);
5311
6949
  agents.push("claude");
5312
6950
  } catch {
5313
6951
  }
5314
- const windsurfDir = path8.join(home, ".codeium", "windsurf");
6952
+ const windsurfDir = path9.join(home, ".codeium", "windsurf");
5315
6953
  try {
5316
- await fs6.access(windsurfDir);
6954
+ await fs7.access(windsurfDir);
5317
6955
  agents.push("windsurf");
5318
6956
  } catch {
5319
6957
  }
5320
- const cursorDir = path8.join(home, ".cursor");
6958
+ const cursorDir = path9.join(home, ".cursor");
5321
6959
  try {
5322
- await fs6.access(cursorDir);
6960
+ await fs7.access(cursorDir);
5323
6961
  agents.push("cursor");
5324
6962
  } catch {
5325
6963
  }
5326
- const vscodeDir = path8.join(home, ".vscode");
6964
+ const vscodeDir = path9.join(home, ".vscode");
5327
6965
  try {
5328
- await fs6.access(vscodeDir);
6966
+ await fs7.access(vscodeDir);
5329
6967
  agents.push("copilot");
5330
6968
  } catch {
5331
6969
  }
5332
- const kiroConfig = path8.join(home, ".kiro");
6970
+ const kiroConfig = path9.join(home, ".kiro");
5333
6971
  try {
5334
- await fs6.access(kiroConfig);
6972
+ await fs7.access(kiroConfig);
5335
6973
  agents.push("kiro");
5336
6974
  } catch {
5337
6975
  }
5338
- const codexDir = path8.join(home, ".codex");
6976
+ const codexDir = path9.join(home, ".codex");
5339
6977
  try {
5340
- await fs6.access(codexDir);
6978
+ await fs7.access(codexDir);
5341
6979
  agents.push("codex");
5342
6980
  } catch {
5343
6981
  }
5344
- const geminiDir = path8.join(home, ".gemini");
6982
+ const geminiDir = path9.join(home, ".gemini");
5345
6983
  try {
5346
- await fs6.access(geminiDir);
6984
+ await fs7.access(geminiDir);
5347
6985
  agents.push("antigravity");
5348
6986
  } catch {
5349
6987
  }
5350
- const opencodeDir = path8.join(home, ".config", "opencode");
6988
+ const opencodeDir = path9.join(home, ".config", "opencode");
5351
6989
  try {
5352
- await fs6.access(opencodeDir);
6990
+ await fs7.access(opencodeDir);
5353
6991
  agents.push("opencode");
5354
6992
  } catch {
5355
6993
  }
5356
- const traeDir = path8.join(home, ".trae");
6994
+ const traeDir = path9.join(home, ".trae");
5357
6995
  try {
5358
- await fs6.access(traeDir);
6996
+ await fs7.access(traeDir);
5359
6997
  agents.push("trae");
5360
6998
  } catch {
5361
6999
  }
@@ -5402,8 +7040,13 @@ async function installHooks(agent, projectRoot, global = false) {
5402
7040
  case "opencode": {
5403
7041
  const pluginContent = generateOpenCodePlugin();
5404
7042
  const pluginPath = global ? getGlobalConfigPath(agent) : getProjectConfigPath(agent, projectRoot);
5405
- await fs6.mkdir(path8.dirname(pluginPath), { recursive: true });
5406
- await fs6.writeFile(pluginPath, pluginContent, "utf-8");
7043
+ await fs7.mkdir(path9.dirname(pluginPath), { recursive: true });
7044
+ await fs7.writeFile(pluginPath, pluginContent, "utf-8");
7045
+ try {
7046
+ const { recordFile: recordFile2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
7047
+ await recordFile2(projectRoot, "hook", pluginPath, agent);
7048
+ } catch {
7049
+ }
5407
7050
  await installAgentRules(agent, projectRoot);
5408
7051
  return {
5409
7052
  agent,
@@ -5415,18 +7058,24 @@ async function installHooks(agent, projectRoot, global = false) {
5415
7058
  default:
5416
7059
  generated = generateClaudeConfig();
5417
7060
  }
5418
- await fs6.mkdir(path8.dirname(configPath), { recursive: true });
7061
+ await fs7.mkdir(path9.dirname(configPath), { recursive: true });
5419
7062
  if (agent === "kiro") {
5420
7063
  const hookFiles = generateKiroHookFiles();
5421
- const hooksDir = path8.join(path8.dirname(configPath));
5422
- await fs6.mkdir(hooksDir, { recursive: true });
7064
+ const hooksDir = path9.join(path9.dirname(configPath));
7065
+ await fs7.mkdir(hooksDir, { recursive: true });
5423
7066
  for (const hf of hookFiles) {
5424
- await fs6.writeFile(path8.join(hooksDir, hf.filename), hf.content, "utf-8");
7067
+ const hookPath = path9.join(hooksDir, hf.filename);
7068
+ await fs7.writeFile(hookPath, hf.content, "utf-8");
7069
+ try {
7070
+ const { recordFile: recordFile2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
7071
+ await recordFile2(projectRoot, "hook", hookPath, agent);
7072
+ } catch {
7073
+ }
5425
7074
  }
5426
7075
  } else {
5427
7076
  let existing = {};
5428
7077
  try {
5429
- const content = await fs6.readFile(configPath, "utf-8");
7078
+ const content = await fs7.readFile(configPath, "utf-8");
5430
7079
  existing = JSON.parse(content);
5431
7080
  } catch {
5432
7081
  }
@@ -5458,7 +7107,12 @@ async function installHooks(agent, projectRoot, global = false) {
5458
7107
  const h = merged.hooks;
5459
7108
  if (h) delete h.preToolUse;
5460
7109
  }
5461
- await fs6.writeFile(configPath, JSON.stringify(merged, null, 2), "utf-8");
7110
+ await fs7.writeFile(configPath, JSON.stringify(merged, null, 2), "utf-8");
7111
+ try {
7112
+ const { recordFile: recordFile2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
7113
+ await recordFile2(projectRoot, "hook", configPath, agent);
7114
+ } catch {
7115
+ }
5462
7116
  }
5463
7117
  const events = [];
5464
7118
  switch (agent) {
@@ -5494,51 +7148,61 @@ async function installAgentRules(agent, projectRoot) {
5494
7148
  let rulesPath;
5495
7149
  switch (agent) {
5496
7150
  case "windsurf":
5497
- rulesPath = path8.join(projectRoot, ".windsurf", "rules", "memorix.md");
7151
+ rulesPath = path9.join(projectRoot, ".windsurf", "rules", "memorix.md");
5498
7152
  break;
5499
7153
  case "cursor":
5500
- rulesPath = path8.join(projectRoot, ".cursor", "rules", "memorix.mdc");
7154
+ rulesPath = path9.join(projectRoot, ".cursor", "rules", "memorix.mdc");
5501
7155
  break;
5502
7156
  case "claude":
5503
7157
  case "copilot":
5504
- rulesPath = path8.join(projectRoot, ".github", "copilot-instructions.md");
7158
+ rulesPath = path9.join(projectRoot, ".github", "copilot-instructions.md");
5505
7159
  break;
5506
7160
  case "codex":
5507
- rulesPath = path8.join(projectRoot, "AGENTS.md");
7161
+ rulesPath = path9.join(projectRoot, "AGENTS.md");
5508
7162
  break;
5509
7163
  case "kiro":
5510
- rulesPath = path8.join(projectRoot, ".kiro", "steering", "memorix.md");
7164
+ rulesPath = path9.join(projectRoot, ".kiro", "steering", "memorix.md");
5511
7165
  break;
5512
7166
  case "opencode":
5513
- rulesPath = path8.join(projectRoot, "AGENTS.md");
7167
+ rulesPath = path9.join(projectRoot, "AGENTS.md");
5514
7168
  break;
5515
7169
  case "antigravity":
5516
- rulesPath = path8.join(projectRoot, "GEMINI.md");
7170
+ rulesPath = path9.join(projectRoot, "GEMINI.md");
5517
7171
  break;
5518
7172
  case "trae":
5519
- rulesPath = path8.join(projectRoot, ".trae", "rules", "project_rules.md");
7173
+ rulesPath = path9.join(projectRoot, ".trae", "rules", "project_rules.md");
5520
7174
  break;
5521
7175
  default:
5522
- rulesPath = path8.join(projectRoot, ".agent", "rules", "memorix.md");
7176
+ rulesPath = path9.join(projectRoot, ".agent", "rules", "memorix.md");
5523
7177
  break;
5524
7178
  }
5525
7179
  try {
5526
- await fs6.mkdir(path8.dirname(rulesPath), { recursive: true });
7180
+ await fs7.mkdir(path9.dirname(rulesPath), { recursive: true });
5527
7181
  if (agent === "codex" || agent === "opencode" || agent === "antigravity") {
5528
7182
  try {
5529
- const existing = await fs6.readFile(rulesPath, "utf-8");
7183
+ const existing = await fs7.readFile(rulesPath, "utf-8");
5530
7184
  if (existing.includes("Memorix")) {
5531
7185
  return;
5532
7186
  }
5533
- await fs6.writeFile(rulesPath, existing + "\n\n" + rulesContent, "utf-8");
7187
+ await fs7.writeFile(rulesPath, existing + "\n\n" + rulesContent, "utf-8");
7188
+ try {
7189
+ const { recordFile: recordFile2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
7190
+ await recordFile2(projectRoot, "rule", rulesPath, agent);
7191
+ } catch {
7192
+ }
5534
7193
  } catch {
5535
- await fs6.writeFile(rulesPath, rulesContent, "utf-8");
7194
+ await fs7.writeFile(rulesPath, rulesContent, "utf-8");
5536
7195
  }
5537
7196
  } else {
5538
7197
  try {
5539
- await fs6.access(rulesPath);
7198
+ await fs7.access(rulesPath);
5540
7199
  } catch {
5541
- await fs6.writeFile(rulesPath, rulesContent, "utf-8");
7200
+ await fs7.writeFile(rulesPath, rulesContent, "utf-8");
7201
+ try {
7202
+ const { recordFile: recordFile2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
7203
+ await recordFile2(projectRoot, "rule", rulesPath, agent);
7204
+ } catch {
7205
+ }
5542
7206
  }
5543
7207
  }
5544
7208
  } catch {
@@ -5670,15 +7334,15 @@ async function uninstallHooks(agent, projectRoot, global = false) {
5670
7334
  const configPath = global ? getGlobalConfigPath(agent) : getProjectConfigPath(agent, projectRoot);
5671
7335
  try {
5672
7336
  if (agent === "kiro") {
5673
- await fs6.unlink(configPath);
7337
+ await fs7.unlink(configPath);
5674
7338
  } else {
5675
- const content = await fs6.readFile(configPath, "utf-8");
7339
+ const content = await fs7.readFile(configPath, "utf-8");
5676
7340
  const config = JSON.parse(content);
5677
7341
  delete config.hooks;
5678
7342
  if (Object.keys(config).length === 0) {
5679
- await fs6.unlink(configPath);
7343
+ await fs7.unlink(configPath);
5680
7344
  } else {
5681
- await fs6.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
7345
+ await fs7.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
5682
7346
  }
5683
7347
  }
5684
7348
  return true;
@@ -5695,11 +7359,11 @@ async function getHookStatus(projectRoot) {
5695
7359
  let installed = false;
5696
7360
  let usedPath = projectPath;
5697
7361
  try {
5698
- await fs6.access(projectPath);
7362
+ await fs7.access(projectPath);
5699
7363
  installed = true;
5700
7364
  } catch {
5701
7365
  try {
5702
- await fs6.access(globalPath);
7366
+ await fs7.access(globalPath);
5703
7367
  installed = true;
5704
7368
  usedPath = globalPath;
5705
7369
  } catch {
@@ -5716,6 +7380,60 @@ var init_installers = __esm({
5716
7380
  }
5717
7381
  });
5718
7382
 
7383
+ // src/git/hooks-path.ts
7384
+ var hooks_path_exports = {};
7385
+ __export(hooks_path_exports, {
7386
+ ensureHooksDir: () => ensureHooksDir,
7387
+ resolveGitDir: () => resolveGitDir,
7388
+ resolveHooksDir: () => resolveHooksDir
7389
+ });
7390
+ import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync3, mkdirSync as mkdirSync4 } from "fs";
7391
+ import path10 from "path";
7392
+ function resolveGitDir(projectRoot) {
7393
+ const dotGit = path10.join(projectRoot, ".git");
7394
+ if (!existsSync9(dotGit)) return null;
7395
+ const stat = statSync3(dotGit);
7396
+ if (stat.isDirectory()) {
7397
+ return dotGit;
7398
+ }
7399
+ if (stat.isFile()) {
7400
+ try {
7401
+ const content = readFileSync8(dotGit, "utf-8").trim();
7402
+ const match = content.match(/^gitdir:\s*(.+)$/);
7403
+ if (match) {
7404
+ const gitdir = match[1].trim();
7405
+ const resolved = path10.isAbsolute(gitdir) ? gitdir : path10.resolve(projectRoot, gitdir);
7406
+ if (existsSync9(resolved)) {
7407
+ return resolved;
7408
+ }
7409
+ }
7410
+ } catch {
7411
+ }
7412
+ }
7413
+ return null;
7414
+ }
7415
+ function resolveHooksDir(projectRoot) {
7416
+ const gitDir = resolveGitDir(projectRoot);
7417
+ if (!gitDir) return null;
7418
+ const hooksDir = path10.join(gitDir, "hooks");
7419
+ return {
7420
+ hooksDir,
7421
+ hookPath: path10.join(hooksDir, "post-commit")
7422
+ };
7423
+ }
7424
+ function ensureHooksDir(projectRoot) {
7425
+ const resolved = resolveHooksDir(projectRoot);
7426
+ if (!resolved) return null;
7427
+ mkdirSync4(resolved.hooksDir, { recursive: true });
7428
+ return resolved;
7429
+ }
7430
+ var init_hooks_path = __esm({
7431
+ "src/git/hooks-path.ts"() {
7432
+ "use strict";
7433
+ init_esm_shims();
7434
+ }
7435
+ });
7436
+
5719
7437
  // src/index.ts
5720
7438
  init_esm_shims();
5721
7439
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -5841,6 +7559,10 @@ var KnowledgeGraphManager = class {
5841
7559
  );
5842
7560
  await this.save();
5843
7561
  }
7562
+ /** Get all entity names (for Formation Pipeline entity resolution) */
7563
+ getEntityNames() {
7564
+ return this.entities.map((e) => e.name);
7565
+ }
5844
7566
  /** Read the entire graph */
5845
7567
  async readGraph() {
5846
7568
  await this.init();
@@ -5946,38 +7668,38 @@ init_observations();
5946
7668
 
5947
7669
  // src/compact/index-format.ts
5948
7670
  init_esm_shims();
5949
- function formatIndexTable(entries, query) {
7671
+ function formatIndexTable(entries, query, forceProjectColumn = false) {
5950
7672
  if (entries.length === 0) {
5951
7673
  return query ? `No observations found matching "${query}".` : "No observations found.";
5952
7674
  }
5953
7675
  const lines = [];
5954
7676
  if (query) {
5955
- lines.push(`Found ${entries.length} observation(s) matching "${query}":
5956
- `);
7677
+ lines.push(`Found ${entries.length} observation(s) matching "${query}":`);
7678
+ lines.push("");
7679
+ }
7680
+ const distinctProjects = [...new Set(entries.map((entry) => entry.projectId).filter(Boolean))];
7681
+ const hasProject = forceProjectColumn || distinctProjects.length > 1;
7682
+ const hasExplanation = entries.some((entry) => (entry.matchedFields?.length ?? 0) > 0);
7683
+ const header = ["ID", "Time", "T", "Title", "Tokens"];
7684
+ const divider = ["----", "------", "---", "-------", "--------"];
7685
+ if (hasProject) {
7686
+ header.push("Project");
7687
+ divider.push("---------");
5957
7688
  }
5958
- lines.push("| ID | Time | T | Title | Tokens |");
5959
- lines.push("|----|------|---|-------|--------|");
5960
- const hasExplanation = entries.some((e) => e["matchedFields"]);
5961
7689
  if (hasExplanation) {
5962
- lines.pop();
5963
- lines.pop();
5964
- lines.push("| ID | Time | T | Title | Tokens | Matched |");
5965
- lines.push("|----|------|---|-------|--------|---------|");
7690
+ header.push("Matched");
7691
+ divider.push("---------");
5966
7692
  }
7693
+ lines.push(`| ${header.join(" | ")} |`);
7694
+ lines.push(`|${divider.map((part) => ` ${part} `).join("|")}|`);
5967
7695
  for (const entry of entries) {
5968
- const matched = entry["matchedFields"];
5969
- if (hasExplanation && matched) {
5970
- lines.push(
5971
- `| #${entry.id} | ${entry.time} | ${entry.icon} | ${entry.title} | ~${entry.tokens} | ${matched.join(", ")} |`
5972
- );
5973
- } else {
5974
- lines.push(
5975
- `| #${entry.id} | ${entry.time} | ${entry.icon} | ${entry.title} | ~${entry.tokens} |`
5976
- );
5977
- }
7696
+ const row = [`#${entry.id}`, entry.time, entry.icon, entry.title, `~${entry.tokens}`];
7697
+ if (hasProject) row.push(entry.projectId ?? "-");
7698
+ if (hasExplanation) row.push(entry.matchedFields?.join(", ") ?? "-");
7699
+ lines.push(`| ${row.join(" | ")} |`);
5978
7700
  }
5979
7701
  lines.push("");
5980
- lines.push(PROGRESSIVE_DISCLOSURE_HINT);
7702
+ lines.push(getProgressiveDisclosureHint(hasProject));
5981
7703
  return lines.join("\n");
5982
7704
  }
5983
7705
  function formatTimeline(timeline) {
@@ -5985,8 +7707,8 @@ function formatTimeline(timeline) {
5985
7707
  return `Observation #${timeline.anchorId} not found.`;
5986
7708
  }
5987
7709
  const lines = [];
5988
- lines.push(`Timeline around #${timeline.anchorId}:
5989
- `);
7710
+ lines.push(`Timeline around #${timeline.anchorId}:`);
7711
+ lines.push("");
5990
7712
  if (timeline.before.length > 0) {
5991
7713
  lines.push("**Before:**");
5992
7714
  lines.push("| ID | Time | T | Title | Tokens |");
@@ -5996,11 +7718,11 @@ function formatTimeline(timeline) {
5996
7718
  }
5997
7719
  lines.push("");
5998
7720
  }
5999
- lines.push("**\u25BA Anchor:**");
7721
+ lines.push("**Anchor:**");
6000
7722
  lines.push("| ID | Time | T | Title | Tokens |");
6001
7723
  lines.push("|----|------|---|-------|--------|");
6002
- const a = timeline.anchorEntry;
6003
- lines.push(`| #${a.id} | ${a.time} | ${a.icon} | ${a.title} | ~${a.tokens} |`);
7724
+ const anchor = timeline.anchorEntry;
7725
+ lines.push(`| #${anchor.id} | ${anchor.time} | ${anchor.icon} | ${anchor.title} | ~${anchor.tokens} |`);
6004
7726
  lines.push("");
6005
7727
  if (timeline.after.length > 0) {
6006
7728
  lines.push("**After:**");
@@ -6011,14 +7733,14 @@ function formatTimeline(timeline) {
6011
7733
  }
6012
7734
  lines.push("");
6013
7735
  }
6014
- lines.push(PROGRESSIVE_DISCLOSURE_HINT);
7736
+ lines.push(getProgressiveDisclosureHint(false));
6015
7737
  return lines.join("\n");
6016
7738
  }
6017
7739
  function formatObservationDetail(doc) {
6018
7740
  const icon = getTypeIcon(doc.type);
6019
7741
  const lines = [];
6020
7742
  lines.push(`#${doc.observationId} ${icon} ${doc.title}`);
6021
- lines.push("\u2500".repeat(50));
7743
+ lines.push("=".repeat(50));
6022
7744
  lines.push(`Date: ${new Date(doc.createdAt).toLocaleString()}`);
6023
7745
  lines.push(`Type: ${doc.type}`);
6024
7746
  lines.push(`Entity: ${doc.entityName}`);
@@ -6057,20 +7779,29 @@ function getTypeIcon(type) {
6057
7779
  "discovery": "\u{1F7E3}",
6058
7780
  "why-it-exists": "\u{1F7E0}",
6059
7781
  "decision": "\u{1F7E4}",
6060
- "trade-off": "\u2696\uFE0F"
7782
+ "trade-off": "\u2696\uFE0F",
7783
+ "reasoning": "\u{1F9E0}"
6061
7784
  };
6062
7785
  return icons[type] ?? "\u2753";
6063
7786
  }
6064
- var PROGRESSIVE_DISCLOSURE_HINT = `\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists and retrieval COST.
6065
- - Use \`memorix_detail\` to fetch full observation details by ID
6066
- - Use \`memorix_timeline\` to see chronological context around an observation
6067
- - Critical types (\u{1F534} gotcha, \u{1F7E4} decision, \u2696\uFE0F trade-off) are often worth fetching immediately`;
7787
+ function getProgressiveDisclosureHint(hasProject) {
7788
+ const lines = [
7789
+ "\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists and retrieval COST.",
7790
+ "- Use `memorix_detail` to fetch full observation details by ID",
7791
+ "- Use `memorix_timeline` to see chronological context around an observation",
7792
+ "- Critical types (\u{1F534} gotcha, \u{1F7E4} decision, \u2696\uFE0F trade-off) are often worth fetching immediately"
7793
+ ];
7794
+ if (hasProject) {
7795
+ lines.push("- For global results, prefer `memorix_detail refs=[{ id, projectId }]` to avoid cross-project ID ambiguity");
7796
+ }
7797
+ return lines.join("\n");
7798
+ }
6068
7799
 
6069
7800
  // src/compact/engine.ts
6070
7801
  init_token_budget();
6071
7802
  async function compactSearch(options) {
6072
7803
  const entries = await searchObservations(options);
6073
- const formatted = formatIndexTable(entries, options.query);
7804
+ const formatted = formatIndexTable(entries, options.query, !options.projectId);
6074
7805
  const totalTokens = countTextTokens(formatted);
6075
7806
  return { entries, formatted, totalTokens };
6076
7807
  }
@@ -6086,13 +7817,18 @@ async function compactTimeline(anchorId, projectId, depthBefore = 3, depthAfter
6086
7817
  const totalTokens = countTextTokens(formatted);
6087
7818
  return { timeline, formatted, totalTokens };
6088
7819
  }
6089
- async function compactDetail(ids) {
6090
- const documents = [];
6091
- for (const id of ids) {
6092
- const obs = getObservation(id);
6093
- if (obs) {
6094
- documents.push({
6095
- id: `obs-${obs.id}`,
7820
+ async function compactDetail(idsOrRefs) {
7821
+ const refs = idsOrRefs.map(
7822
+ (item) => typeof item === "number" ? { id: item } : item
7823
+ );
7824
+ const toRefKey = (ref) => `${ref.projectId ?? ""}::${ref.id}`;
7825
+ const documentMap = /* @__PURE__ */ new Map();
7826
+ const missingRefs = [];
7827
+ for (const ref of refs) {
7828
+ const obs = getObservation(ref.id);
7829
+ if (obs && (!ref.projectId || obs.projectId === ref.projectId)) {
7830
+ documentMap.set(toRefKey(ref), {
7831
+ id: makeOramaObservationId(obs.projectId, obs.id),
6096
7832
  observationId: obs.id,
6097
7833
  entityName: obs.entityName,
6098
7834
  type: obs.type,
@@ -6106,166 +7842,90 @@ async function compactDetail(ids) {
6106
7842
  projectId: obs.projectId,
6107
7843
  accessCount: 0,
6108
7844
  lastAccessedAt: "",
6109
- status: obs.status ?? "active"
7845
+ status: obs.status ?? "active",
7846
+ source: obs.source ?? "agent"
6110
7847
  });
7848
+ } else {
7849
+ missingRefs.push(ref);
6111
7850
  }
6112
7851
  }
6113
- const formattedParts = documents.map(
6114
- (doc) => formatObservationDetail(doc)
6115
- );
6116
- const formatted = formattedParts.join("\n\n" + "\u2550".repeat(50) + "\n\n");
6117
- const totalTokens = countTextTokens(formatted);
6118
- return { documents, formatted, totalTokens };
6119
- }
6120
-
6121
- // src/project/detector.ts
6122
- init_esm_shims();
6123
- import { execSync } from "child_process";
6124
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
6125
- import os3 from "os";
6126
- import path5 from "path";
6127
- function detectProject(cwd) {
6128
- const basePath = cwd ?? process.cwd();
6129
- const rootPath = getGitRoot(basePath) ?? findPackageRoot(basePath) ?? basePath;
6130
- const gitRemote = getGitRemote(rootPath);
6131
- if (gitRemote) {
6132
- const id2 = normalizeGitRemote(gitRemote);
6133
- const name2 = id2.split("/").pop() ?? path5.basename(rootPath);
6134
- return { id: id2, name: name2, gitRemote, rootPath };
6135
- }
6136
- if (isDangerousRoot(rootPath)) {
6137
- const name2 = path5.basename(rootPath) || "unknown";
6138
- const id2 = `placeholder/${name2}`;
6139
- console.error(`[memorix] WARNING: cwd "${rootPath}" is not a project directory \u2014 using degraded mode (${id2})`);
6140
- console.error(`[memorix] For best results, set MEMORIX_PROJECT_ROOT or --cwd to your project path.`);
6141
- return { id: id2, name: name2, rootPath };
6142
- }
6143
- const name = path5.basename(rootPath);
6144
- const id = `local/${name}`;
6145
- return { id, name, rootPath };
6146
- }
6147
- function isDangerousRoot(dirPath) {
6148
- const resolved = path5.resolve(dirPath);
6149
- const home = path5.resolve(os3.homedir());
6150
- if (resolved === home) return true;
6151
- if (resolved === path5.parse(resolved).root) return true;
6152
- const basename2 = path5.basename(resolved).toLowerCase();
6153
- const knownNonProjectDirs = /* @__PURE__ */ new Set([
6154
- // IDE / editor config dirs
6155
- ".vscode",
6156
- ".cursor",
6157
- ".windsurf",
6158
- ".kiro",
6159
- ".codex",
6160
- ".gemini",
6161
- ".claude",
6162
- ".github",
6163
- ".git",
6164
- // OS / system dirs
6165
- "desktop",
6166
- "documents",
6167
- "downloads",
6168
- "pictures",
6169
- "videos",
6170
- "music",
6171
- "appdata",
6172
- "application data",
6173
- "library",
6174
- // Package manager / tool dirs
6175
- "node_modules",
6176
- ".npm",
6177
- ".yarn",
6178
- ".pnpm-store",
6179
- ".config",
6180
- ".local",
6181
- ".cache",
6182
- ".ssh",
6183
- ".memorix"
6184
- ]);
6185
- if (knownNonProjectDirs.has(basename2)) {
6186
- const parent = path5.resolve(path5.dirname(resolved));
6187
- if (parent === home || parent === path5.parse(parent).root) {
6188
- return true;
6189
- }
6190
- }
6191
- return false;
6192
- }
6193
- function findPackageRoot(cwd) {
6194
- let dir = path5.resolve(cwd);
6195
- const root = path5.parse(dir).root;
6196
- while (dir !== root) {
6197
- if (isDangerousRoot(dir)) return null;
6198
- if (existsSync2(path5.join(dir, "package.json"))) {
6199
- return dir;
6200
- }
6201
- dir = path5.dirname(dir);
6202
- }
6203
- return null;
6204
- }
6205
- function getGitRoot(cwd) {
6206
- let dir = path5.resolve(cwd);
6207
- const fsRoot = path5.parse(dir).root;
6208
- while (dir !== fsRoot) {
6209
- if (existsSync2(path5.join(dir, ".git"))) return dir;
6210
- dir = path5.dirname(dir);
6211
- }
6212
- try {
6213
- const root = execSync("git -c safe.directory=* rev-parse --show-toplevel", {
6214
- cwd,
6215
- encoding: "utf-8",
6216
- stdio: ["pipe", "pipe", "pipe"],
6217
- timeout: 5e3
6218
- }).trim();
6219
- return root || null;
6220
- } catch {
6221
- return null;
6222
- }
6223
- }
6224
- function getGitRemote(cwd) {
6225
- const fsRemote = readGitConfigRemote(cwd);
6226
- if (fsRemote) return fsRemote;
6227
- try {
6228
- const remote = execSync("git -c safe.directory=* remote get-url origin", {
6229
- cwd,
6230
- encoding: "utf-8",
6231
- stdio: ["pipe", "pipe", "pipe"],
6232
- timeout: 5e3
6233
- }).trim();
6234
- return remote || null;
6235
- } catch {
6236
- return null;
6237
- }
6238
- }
6239
- function readGitConfigRemote(cwd) {
6240
- try {
6241
- const configPath = path5.join(cwd, ".git", "config");
6242
- if (!existsSync2(configPath)) return null;
6243
- const content = readFileSync2(configPath, "utf-8");
6244
- const remoteMatch = content.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
6245
- if (!remoteMatch) return null;
6246
- const urlMatch = remoteMatch[1].match(/^\s*url\s*=\s*(.+)$/m);
6247
- return urlMatch ? urlMatch[1].trim() : null;
6248
- } catch {
6249
- return null;
6250
- }
6251
- }
6252
- function normalizeGitRemote(remote) {
6253
- let normalized = remote;
6254
- normalized = normalized.replace(/\.git$/, "");
6255
- const sshMatch = normalized.match(/^[\w-]+@[\w.-]+:(.+)$/);
6256
- if (sshMatch) {
6257
- return sshMatch[1];
7852
+ if (missingRefs.length > 0) {
7853
+ for (const ref of missingRefs) {
7854
+ const fallbackDocs = await getObservationsByIds([ref.id], ref.projectId);
7855
+ const doc = fallbackDocs[0];
7856
+ if (doc) {
7857
+ documentMap.set(toRefKey(ref), doc);
7858
+ }
7859
+ }
6258
7860
  }
6259
- try {
6260
- const url = new URL(normalized);
6261
- return url.pathname.replace(/^\//, "");
6262
- } catch {
6263
- const segments = normalized.split("/").filter(Boolean);
6264
- return segments.slice(-2).join("/");
7861
+ const documents = refs.map((ref) => documentMap.get(toRefKey(ref))).filter((doc) => Boolean(doc));
7862
+ const allObs = getAllObservations();
7863
+ const crossRefMap = /* @__PURE__ */ new Map();
7864
+ for (const ref of refs) {
7865
+ const obs = getObservation(ref.id);
7866
+ const doc = documentMap.get(toRefKey(ref));
7867
+ if (!obs && !doc) continue;
7868
+ const refs2 = [];
7869
+ if (obs?.source === "git" && obs.commitHash) {
7870
+ refs2.push(`Source: git commit ${obs.commitHash.substring(0, 7)}`);
7871
+ } else if (obs?.source) {
7872
+ refs2.push(`Source: ${obs.source}`);
7873
+ } else if (doc?.source) {
7874
+ refs2.push(`Source: ${doc.source}`);
7875
+ }
7876
+ if (!obs) {
7877
+ if (refs2.length > 0 && doc) crossRefMap.set(doc.id, refs2);
7878
+ continue;
7879
+ }
7880
+ if (obs.relatedCommits && obs.relatedCommits.length > 0) {
7881
+ refs2.push(`Related commits: ${obs.relatedCommits.map((h) => h.substring(0, 7)).join(", ")}`);
7882
+ const gitMems = allObs.filter((o) => o.source === "git" && o.commitHash && obs.relatedCommits.includes(o.commitHash));
7883
+ for (const gm of gitMems) {
7884
+ refs2.push(` \u2192 #${gm.id} \u{1F7E2} ${gm.title}`);
7885
+ }
7886
+ }
7887
+ if (obs.relatedEntities && obs.relatedEntities.length > 0) {
7888
+ refs2.push(`Related entities: ${obs.relatedEntities.join(", ")}`);
7889
+ }
7890
+ if (obs.source === "git") {
7891
+ const reasoning = allObs.filter(
7892
+ (o) => o.type === "reasoning" && o.entityName === obs.entityName && o.id !== obs.id && o.status !== "archived"
7893
+ ).slice(0, 3);
7894
+ if (reasoning.length > 0) {
7895
+ refs2.push("Related reasoning:");
7896
+ for (const r of reasoning) {
7897
+ refs2.push(` \u2192 #${r.id} \u{1F9E0} ${r.title}`);
7898
+ }
7899
+ }
7900
+ }
7901
+ if (obs.type === "reasoning") {
7902
+ const gitMems = allObs.filter(
7903
+ (o) => o.source === "git" && o.entityName === obs.entityName && o.id !== obs.id && o.status !== "archived"
7904
+ ).slice(0, 3);
7905
+ if (gitMems.length > 0) {
7906
+ refs2.push("Related commits:");
7907
+ for (const g of gitMems) {
7908
+ refs2.push(` \u2192 #${g.id} \u{1F7E2} ${g.title}`);
7909
+ }
7910
+ }
7911
+ }
7912
+ if (refs2.length > 0) crossRefMap.set(makeOramaObservationId(obs.projectId, obs.id), refs2);
6265
7913
  }
7914
+ const formattedParts = documents.map((doc) => {
7915
+ let detail = formatObservationDetail(doc);
7916
+ const refs2 = crossRefMap.get(doc.id);
7917
+ if (refs2 && refs2.length > 0) {
7918
+ detail += "\n\nCross-references:\n" + refs2.join("\n");
7919
+ }
7920
+ return detail;
7921
+ });
7922
+ const formatted = formattedParts.join("\n\n" + "\u2550".repeat(50) + "\n\n");
7923
+ const totalTokens = countTextTokens(formatted);
7924
+ return { documents, formatted, totalTokens };
6266
7925
  }
6267
7926
 
6268
7927
  // src/server.ts
7928
+ init_detector();
6269
7929
  init_aliases();
6270
7930
  init_persistence();
6271
7931
 
@@ -6998,14 +8658,14 @@ var RulesSyncer = class {
6998
8658
 
6999
8659
  // src/workspace/engine.ts
7000
8660
  init_esm_shims();
7001
- import { readFileSync as readFileSync3, readdirSync, existsSync as existsSync4, cpSync, mkdirSync as mkdirSync2 } from "fs";
7002
- import { join as join14 } from "path";
7003
- import { homedir as homedir13 } from "os";
8661
+ import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync6, cpSync, mkdirSync as mkdirSync2 } from "fs";
8662
+ import { join as join16 } from "path";
8663
+ import { homedir as homedir15 } from "os";
7004
8664
 
7005
8665
  // src/workspace/mcp-adapters/windsurf.ts
7006
8666
  init_esm_shims();
7007
- import { homedir as homedir4 } from "os";
7008
- import { join as join4 } from "path";
8667
+ import { homedir as homedir6 } from "os";
8668
+ import { join as join6 } from "path";
7009
8669
  var WindsurfMCPAdapter = class {
7010
8670
  source = "windsurf";
7011
8671
  parse(content) {
@@ -7062,14 +8722,14 @@ var WindsurfMCPAdapter = class {
7062
8722
  return JSON.stringify({ mcpServers }, null, 2);
7063
8723
  }
7064
8724
  getConfigPath(_projectRoot) {
7065
- return join4(homedir4(), ".codeium", "windsurf", "mcp_config.json");
8725
+ return join6(homedir6(), ".codeium", "windsurf", "mcp_config.json");
7066
8726
  }
7067
8727
  };
7068
8728
 
7069
8729
  // src/workspace/mcp-adapters/cursor.ts
7070
8730
  init_esm_shims();
7071
- import { homedir as homedir5 } from "os";
7072
- import { join as join5 } from "path";
8731
+ import { homedir as homedir7 } from "os";
8732
+ import { join as join7 } from "path";
7073
8733
  var CursorMCPAdapter = class {
7074
8734
  source = "cursor";
7075
8735
  parse(content) {
@@ -7106,16 +8766,16 @@ var CursorMCPAdapter = class {
7106
8766
  }
7107
8767
  getConfigPath(projectRoot) {
7108
8768
  if (projectRoot) {
7109
- return join5(projectRoot, ".cursor", "mcp.json");
8769
+ return join7(projectRoot, ".cursor", "mcp.json");
7110
8770
  }
7111
- return join5(homedir5(), ".cursor", "mcp.json");
8771
+ return join7(homedir7(), ".cursor", "mcp.json");
7112
8772
  }
7113
8773
  };
7114
8774
 
7115
8775
  // src/workspace/mcp-adapters/codex.ts
7116
8776
  init_esm_shims();
7117
- import { homedir as homedir6 } from "os";
7118
- import { join as join6 } from "path";
8777
+ import { homedir as homedir8 } from "os";
8778
+ import { join as join8 } from "path";
7119
8779
  var CodexMCPAdapter = class {
7120
8780
  source = "codex";
7121
8781
  parse(content) {
@@ -7205,9 +8865,9 @@ var CodexMCPAdapter = class {
7205
8865
  }
7206
8866
  getConfigPath(projectRoot) {
7207
8867
  if (projectRoot) {
7208
- return join6(projectRoot, ".codex", "config.toml");
8868
+ return join8(projectRoot, ".codex", "config.toml");
7209
8869
  }
7210
- return join6(homedir6(), ".codex", "config.toml");
8870
+ return join8(homedir8(), ".codex", "config.toml");
7211
8871
  }
7212
8872
  // ---- TOML helpers ----
7213
8873
  parseTomlString(raw) {
@@ -7254,8 +8914,8 @@ var CodexMCPAdapter = class {
7254
8914
 
7255
8915
  // src/workspace/mcp-adapters/claude-code.ts
7256
8916
  init_esm_shims();
7257
- import { homedir as homedir7 } from "os";
7258
- import { join as join7 } from "path";
8917
+ import { homedir as homedir9 } from "os";
8918
+ import { join as join9 } from "path";
7259
8919
  var ClaudeCodeMCPAdapter = class {
7260
8920
  source = "claude-code";
7261
8921
  parse(content) {
@@ -7292,16 +8952,16 @@ var ClaudeCodeMCPAdapter = class {
7292
8952
  }
7293
8953
  getConfigPath(projectRoot) {
7294
8954
  if (projectRoot) {
7295
- return join7(projectRoot, ".claude", "settings.json");
8955
+ return join9(projectRoot, ".claude", "settings.json");
7296
8956
  }
7297
- return join7(homedir7(), ".claude.json");
8957
+ return join9(homedir9(), ".claude.json");
7298
8958
  }
7299
8959
  };
7300
8960
 
7301
8961
  // src/workspace/mcp-adapters/copilot.ts
7302
8962
  init_esm_shims();
7303
- import { homedir as homedir8 } from "os";
7304
- import { join as join8 } from "path";
8963
+ import { homedir as homedir10 } from "os";
8964
+ import { join as join10 } from "path";
7305
8965
  var CopilotMCPAdapter = class {
7306
8966
  source = "copilot";
7307
8967
  parse(content) {
@@ -7356,23 +9016,23 @@ var CopilotMCPAdapter = class {
7356
9016
  }
7357
9017
  getConfigPath(projectRoot) {
7358
9018
  if (projectRoot) {
7359
- return join8(projectRoot, ".vscode", "mcp.json");
9019
+ return join10(projectRoot, ".vscode", "mcp.json");
7360
9020
  }
7361
- const home = homedir8();
9021
+ const home = homedir10();
7362
9022
  if (process.platform === "win32") {
7363
- return join8(home, "AppData", "Roaming", "Code", "User", "settings.json");
9023
+ return join10(home, "AppData", "Roaming", "Code", "User", "settings.json");
7364
9024
  } else if (process.platform === "darwin") {
7365
- return join8(home, "Library", "Application Support", "Code", "User", "settings.json");
9025
+ return join10(home, "Library", "Application Support", "Code", "User", "settings.json");
7366
9026
  } else {
7367
- return join8(home, ".config", "Code", "User", "settings.json");
9027
+ return join10(home, ".config", "Code", "User", "settings.json");
7368
9028
  }
7369
9029
  }
7370
9030
  };
7371
9031
 
7372
9032
  // src/workspace/mcp-adapters/antigravity.ts
7373
9033
  init_esm_shims();
7374
- import { homedir as homedir9 } from "os";
7375
- import { join as join9 } from "path";
9034
+ import { homedir as homedir11 } from "os";
9035
+ import { join as join11 } from "path";
7376
9036
  var AntigravityMCPAdapter = class {
7377
9037
  source = "antigravity";
7378
9038
  parse(content) {
@@ -7430,16 +9090,16 @@ var AntigravityMCPAdapter = class {
7430
9090
  }
7431
9091
  getConfigPath(projectRoot) {
7432
9092
  if (projectRoot) {
7433
- return join9(projectRoot, ".gemini", "settings.json");
9093
+ return join11(projectRoot, ".gemini", "settings.json");
7434
9094
  }
7435
- return join9(homedir9(), ".gemini", "settings.json");
9095
+ return join11(homedir11(), ".gemini", "settings.json");
7436
9096
  }
7437
9097
  };
7438
9098
 
7439
9099
  // src/workspace/mcp-adapters/kiro.ts
7440
9100
  init_esm_shims();
7441
- import { homedir as homedir10 } from "os";
7442
- import { join as join10 } from "path";
9101
+ import { homedir as homedir12 } from "os";
9102
+ import { join as join12 } from "path";
7443
9103
  var KiroMCPAdapter = class {
7444
9104
  source = "kiro";
7445
9105
  parse(content) {
@@ -7479,16 +9139,16 @@ var KiroMCPAdapter = class {
7479
9139
  }
7480
9140
  getConfigPath(projectRoot) {
7481
9141
  if (projectRoot) {
7482
- return join10(projectRoot, ".kiro", "settings", "mcp.json");
9142
+ return join12(projectRoot, ".kiro", "settings", "mcp.json");
7483
9143
  }
7484
- return join10(homedir10(), ".kiro", "settings", "mcp.json");
9144
+ return join12(homedir12(), ".kiro", "settings", "mcp.json");
7485
9145
  }
7486
9146
  };
7487
9147
 
7488
9148
  // src/workspace/mcp-adapters/opencode.ts
7489
9149
  init_esm_shims();
7490
- import { homedir as homedir11 } from "os";
7491
- import { join as join11 } from "path";
9150
+ import { homedir as homedir13 } from "os";
9151
+ import { join as join13 } from "path";
7492
9152
  var OpenCodeMCPAdapter = class {
7493
9153
  source = "opencode";
7494
9154
  parse(content) {
@@ -7553,16 +9213,16 @@ var OpenCodeMCPAdapter = class {
7553
9213
  }
7554
9214
  getConfigPath(projectRoot) {
7555
9215
  if (projectRoot) {
7556
- return join11(projectRoot, "opencode.json");
9216
+ return join13(projectRoot, "opencode.json");
7557
9217
  }
7558
- return join11(homedir11(), ".config", "opencode", "opencode.json");
9218
+ return join13(homedir13(), ".config", "opencode", "opencode.json");
7559
9219
  }
7560
9220
  };
7561
9221
 
7562
9222
  // src/workspace/mcp-adapters/trae.ts
7563
9223
  init_esm_shims();
7564
- import { join as join12 } from "path";
7565
- import { homedir as homedir12 } from "os";
9224
+ import { join as join14 } from "path";
9225
+ import { homedir as homedir14 } from "os";
7566
9226
  var TraeMCPAdapter = class {
7567
9227
  source = "trae";
7568
9228
  parse(content) {
@@ -7626,14 +9286,14 @@ var TraeMCPAdapter = class {
7626
9286
  return JSON.stringify({ mcpServers }, null, 2);
7627
9287
  }
7628
9288
  getConfigPath(_projectRoot) {
7629
- const home = homedir12();
9289
+ const home = homedir14();
7630
9290
  if (process.platform === "win32") {
7631
- return join12(process.env.APPDATA || join12(home, "AppData", "Roaming"), "Trae", "User", "mcp.json");
9291
+ return join14(process.env.APPDATA || join14(home, "AppData", "Roaming"), "Trae", "User", "mcp.json");
7632
9292
  }
7633
9293
  if (process.platform === "darwin") {
7634
- return join12(home, "Library", "Application Support", "Trae", "User", "mcp.json");
9294
+ return join14(home, "Library", "Application Support", "Trae", "User", "mcp.json");
7635
9295
  }
7636
- return join12(home, ".config", "Trae", "User", "mcp.json");
9296
+ return join14(home, ".config", "Trae", "User", "mcp.json");
7637
9297
  }
7638
9298
  };
7639
9299
 
@@ -7765,7 +9425,7 @@ function sanitize(input) {
7765
9425
 
7766
9426
  // src/workspace/applier.ts
7767
9427
  init_esm_shims();
7768
- import { existsSync as existsSync3, mkdirSync, copyFileSync, writeFileSync, unlinkSync, renameSync } from "fs";
9428
+ import { existsSync as existsSync5, mkdirSync, copyFileSync, writeFileSync, unlinkSync, renameSync } from "fs";
7769
9429
  import { dirname } from "path";
7770
9430
  var WorkspaceSyncApplier = class {
7771
9431
  /**
@@ -7791,7 +9451,7 @@ var WorkspaceSyncApplier = class {
7791
9451
  for (const file of files) {
7792
9452
  try {
7793
9453
  const dir = dirname(file.filePath);
7794
- if (!existsSync3(dir)) {
9454
+ if (!existsSync5(dir)) {
7795
9455
  mkdirSync(dir, { recursive: true });
7796
9456
  }
7797
9457
  } catch (err) {
@@ -7800,7 +9460,7 @@ var WorkspaceSyncApplier = class {
7800
9460
  }
7801
9461
  }
7802
9462
  for (const file of files) {
7803
- if (existsSync3(file.filePath)) {
9463
+ if (existsSync5(file.filePath)) {
7804
9464
  try {
7805
9465
  const backupPath = file.filePath + `.backup-${Date.now()}`;
7806
9466
  copyFileSync(file.filePath, backupPath);
@@ -7851,7 +9511,7 @@ var WorkspaceSyncApplier = class {
7851
9511
  cleanBackups(backups) {
7852
9512
  for (const backup of backups) {
7853
9513
  try {
7854
- if (existsSync3(backup.backupPath)) {
9514
+ if (existsSync5(backup.backupPath)) {
7855
9515
  unlinkSync(backup.backupPath);
7856
9516
  }
7857
9517
  } catch {
@@ -7901,13 +9561,13 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
7901
9561
  const globalPath = adapter.getConfigPath();
7902
9562
  const pathsToCheck = [configPath, globalPath];
7903
9563
  if (target === "antigravity") {
7904
- pathsToCheck.push(join14(homedir13(), ".gemini", "antigravity", "mcp_config.json"));
9564
+ pathsToCheck.push(join16(homedir15(), ".gemini", "antigravity", "mcp_config.json"));
7905
9565
  }
7906
9566
  const merged = /* @__PURE__ */ new Map();
7907
- for (const path9 of pathsToCheck) {
7908
- if (existsSync4(path9)) {
9567
+ for (const path11 of pathsToCheck) {
9568
+ if (existsSync6(path11)) {
7909
9569
  try {
7910
- const content = readFileSync3(path9, "utf-8");
9570
+ const content = readFileSync5(path11, "utf-8");
7911
9571
  const servers = adapter.parse(content);
7912
9572
  for (const s of servers) {
7913
9573
  if (!merged.has(s.name)) merged.set(s.name, s);
@@ -7959,9 +9619,9 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
7959
9619
  const adapter = this.adapters.get(target);
7960
9620
  const configPath = adapter.getConfigPath(this.projectRoot);
7961
9621
  let configContent;
7962
- if (target === "antigravity" && existsSync4(configPath)) {
9622
+ if (target === "antigravity" && existsSync6(configPath)) {
7963
9623
  try {
7964
- const existing = JSON.parse(readFileSync3(configPath, "utf-8"));
9624
+ const existing = JSON.parse(readFileSync5(configPath, "utf-8"));
7965
9625
  const generated = JSON.parse(adapter.generate(result.mcpServers.scanned));
7966
9626
  existing.mcpServers = { ...existing.mcpServers ?? {}, ...generated.mcpServers };
7967
9627
  configContent = JSON.stringify(existing, null, 2);
@@ -8014,7 +9674,7 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
8014
9674
  getTargetSkillsDir(target) {
8015
9675
  const dirs = _WorkspaceSyncEngine.SKILLS_DIRS[target];
8016
9676
  if (!dirs || dirs.length === 0) return null;
8017
- return join14(this.projectRoot, dirs[0]);
9677
+ return join16(this.projectRoot, dirs[0]);
8018
9678
  }
8019
9679
  /**
8020
9680
  * Scan all agent skills directories and collect unique skills.
@@ -8023,24 +9683,24 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
8023
9683
  const skills = [];
8024
9684
  const conflicts = [];
8025
9685
  const seen = /* @__PURE__ */ new Map();
8026
- const home = homedir13();
9686
+ const home = homedir15();
8027
9687
  for (const [agent, dirs] of Object.entries(_WorkspaceSyncEngine.SKILLS_DIRS)) {
8028
9688
  for (const dir of dirs) {
8029
9689
  const paths = [
8030
- join14(this.projectRoot, dir),
8031
- join14(home, dir)
9690
+ join16(this.projectRoot, dir),
9691
+ join16(home, dir)
8032
9692
  ];
8033
9693
  for (const skillsRoot of paths) {
8034
- if (!existsSync4(skillsRoot)) continue;
9694
+ if (!existsSync6(skillsRoot)) continue;
8035
9695
  try {
8036
- const entries = readdirSync(skillsRoot, { withFileTypes: true });
9696
+ const entries = readdirSync2(skillsRoot, { withFileTypes: true });
8037
9697
  for (const entry of entries) {
8038
9698
  if (!entry.isDirectory()) continue;
8039
- const skillMd = join14(skillsRoot, entry.name, "SKILL.md");
8040
- if (!existsSync4(skillMd)) continue;
9699
+ const skillMd = join16(skillsRoot, entry.name, "SKILL.md");
9700
+ if (!existsSync6(skillMd)) continue;
8041
9701
  let description = "";
8042
9702
  try {
8043
- const content = readFileSync3(skillMd, "utf-8");
9703
+ const content = readFileSync5(skillMd, "utf-8");
8044
9704
  const match = content.match(/^---[\s\S]*?description:\s*["']?(.+?)["']?\s*$/m);
8045
9705
  if (match) description = match[1];
8046
9706
  } catch {
@@ -8048,7 +9708,7 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
8048
9708
  const newEntry = {
8049
9709
  name: entry.name,
8050
9710
  description,
8051
- sourcePath: join14(skillsRoot, entry.name),
9711
+ sourcePath: join16(skillsRoot, entry.name),
8052
9712
  sourceAgent: agent
8053
9713
  };
8054
9714
  const existing = seen.get(entry.name);
@@ -8085,8 +9745,8 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
8085
9745
  }
8086
9746
  for (const skill of skills) {
8087
9747
  if (skill.sourceAgent === target) continue;
8088
- const dest = join14(targetDir, skill.name);
8089
- if (existsSync4(dest)) {
9748
+ const dest = join16(targetDir, skill.name);
9749
+ if (existsSync6(dest)) {
8090
9750
  skipped.push(`${skill.name} (already exists in ${target})`);
8091
9751
  continue;
8092
9752
  }
@@ -8101,13 +9761,13 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
8101
9761
  }
8102
9762
  scanWorkflows() {
8103
9763
  const workflows = [];
8104
- const wfDir = join14(this.projectRoot, ".windsurf", "workflows");
8105
- if (!existsSync4(wfDir)) return workflows;
9764
+ const wfDir = join16(this.projectRoot, ".windsurf", "workflows");
9765
+ if (!existsSync6(wfDir)) return workflows;
8106
9766
  try {
8107
- const files = readdirSync(wfDir).filter((f) => f.endsWith(".md"));
9767
+ const files = readdirSync2(wfDir).filter((f) => f.endsWith(".md"));
8108
9768
  for (const file of files) {
8109
9769
  try {
8110
- const content = readFileSync3(join14(wfDir, file), "utf-8");
9770
+ const content = readFileSync5(join16(wfDir, file), "utf-8");
8111
9771
  workflows.push(this.workflowSyncer.parseWindsurfWorkflow(file, content));
8112
9772
  } catch {
8113
9773
  }
@@ -8207,6 +9867,7 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
8207
9867
  // src/server.ts
8208
9868
  init_provider2();
8209
9869
  init_memory_manager();
9870
+ init_formation();
8210
9871
  var lastInternalWriteMs = 0;
8211
9872
  var markInternalWrite = () => {
8212
9873
  lastInternalWriteMs = Date.now();
@@ -8215,6 +9876,7 @@ var OBSERVATION_TYPES = [
8215
9876
  "session-request",
8216
9877
  "gotcha",
8217
9878
  "problem-solution",
9879
+ "reasoning",
8218
9880
  "how-it-works",
8219
9881
  "what-changed",
8220
9882
  "discovery",
@@ -8233,6 +9895,29 @@ function coerceNumberArray(val) {
8233
9895
  }
8234
9896
  return [];
8235
9897
  }
9898
+ function coerceObservationRefs(val) {
9899
+ if (Array.isArray(val)) {
9900
+ const refs = [];
9901
+ for (const item of val) {
9902
+ if (!item || typeof item !== "object") continue;
9903
+ const record = item;
9904
+ const id = Number(record["id"]);
9905
+ if (!Number.isFinite(id) || id <= 0) continue;
9906
+ const projectId = typeof record["projectId"] === "string" ? record["projectId"] : void 0;
9907
+ refs.push(projectId ? { id, projectId } : { id });
9908
+ }
9909
+ return refs;
9910
+ }
9911
+ if (typeof val === "string") {
9912
+ try {
9913
+ const parsed = JSON.parse(val);
9914
+ return coerceObservationRefs(parsed);
9915
+ } catch {
9916
+ return [];
9917
+ }
9918
+ }
9919
+ return [];
9920
+ }
8236
9921
  function coerceNumber(val, fallback) {
8237
9922
  if (typeof val === "number") return val;
8238
9923
  if (typeof val === "string") {
@@ -8275,7 +9960,17 @@ function coerceObjectArray(val) {
8275
9960
  return [];
8276
9961
  }
8277
9962
  async function createMemorixServer(cwd, existingServer, sharedTeam) {
8278
- const rawProject = detectProject(cwd);
9963
+ const detectedProject = detectProject(cwd);
9964
+ let rawProject;
9965
+ if (detectedProject) {
9966
+ rawProject = detectedProject;
9967
+ } else {
9968
+ const basePath = cwd ?? process.cwd();
9969
+ const name = (await import("path")).basename(basePath) || "unknown";
9970
+ rawProject = { id: `untracked/${name}`, name, rootPath: basePath };
9971
+ console.error(`[memorix] WARNING: No .git found in "${basePath}" \u2014 project isolation degraded`);
9972
+ console.error(`[memorix] Run "git init" in your project for proper isolation.`);
9973
+ }
8279
9974
  try {
8280
9975
  const { migrateSubdirsToFlat: migrateSubdirsToFlat2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
8281
9976
  const migrated = await migrateSubdirsToFlat2();
@@ -8284,14 +9979,21 @@ async function createMemorixServer(cwd, existingServer, sharedTeam) {
8284
9979
  }
8285
9980
  } catch {
8286
9981
  }
8287
- const projectDir2 = await getProjectDataDir(rawProject.id);
9982
+ let projectDir2 = await getProjectDataDir(rawProject.id);
8288
9983
  initAliasRegistry(projectDir2);
8289
9984
  const canonicalId = await registerAlias(rawProject);
8290
- const project = { ...rawProject, id: canonicalId };
9985
+ let project = { ...rawProject, id: canonicalId };
8291
9986
  if (canonicalId !== rawProject.id) {
8292
9987
  console.error(`[memorix] Alias resolved: ${rawProject.id} \u2192 ${canonicalId}`);
8293
9988
  }
8294
- const graphManager = new KnowledgeGraphManager(projectDir2);
9989
+ try {
9990
+ const { initProjectRoot: initProjectRoot2 } = await Promise.resolve().then(() => (init_yaml_loader(), yaml_loader_exports));
9991
+ initProjectRoot2(project.rootPath);
9992
+ const { loadDotenv: loadDotenv2 } = await Promise.resolve().then(() => (init_dotenv_loader(), dotenv_loader_exports));
9993
+ loadDotenv2(project.rootPath);
9994
+ } catch {
9995
+ }
9996
+ let graphManager = new KnowledgeGraphManager(projectDir2);
8295
9997
  await graphManager.init();
8296
9998
  await initObservations(projectDir2);
8297
9999
  try {
@@ -8338,7 +10040,7 @@ async function createMemorixServer(cwd, existingServer, sharedTeam) {
8338
10040
  let syncAdvisory = null;
8339
10041
  const server = existingServer ?? new McpServer({
8340
10042
  name: "memorix",
8341
- version: true ? "1.0.2" : "1.0.1"
10043
+ version: true ? "1.0.4" : "1.0.1"
8342
10044
  });
8343
10045
  server.registerTool(
8344
10046
  "memorix_store",
@@ -8360,16 +10062,148 @@ async function createMemorixServer(cwd, existingServer, sharedTeam) {
8360
10062
  feature: z.string().describe("Feature or task name"),
8361
10063
  status: z.enum(["in-progress", "completed", "blocked"]).describe("Current status"),
8362
10064
  completion: z.number().optional().describe("Completion percentage 0-100")
8363
- }).optional().describe("Progress tracking for task/feature observations")
10065
+ }).optional().describe("Progress tracking for task/feature observations"),
10066
+ relatedCommits: z.array(z.string()).optional().describe("Git commit hashes this memory relates to (links ground truth \u2194 reasoning)"),
10067
+ relatedEntities: z.array(z.string()).optional().describe("Other entity names this memory cross-references")
8364
10068
  }
8365
10069
  },
8366
- async ({ entityName, type, title, narrative, facts, filesModified, concepts, topicKey, progress }) => {
8367
- const safeFacts = facts ? coerceStringArray(facts) : void 0;
10070
+ async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities }) => {
10071
+ let entityName = rawEntityName;
10072
+ let type = rawType;
10073
+ let title = rawTitle;
10074
+ let safeFacts = facts ? coerceStringArray(facts) : void 0;
8368
10075
  const safeFiles = filesModified ? coerceStringArray(filesModified) : void 0;
8369
10076
  const safeConcepts = concepts ? coerceStringArray(concepts) : void 0;
10077
+ let formationMode = "active";
10078
+ if (process.env.MEMORIX_FORMATION_MODE) {
10079
+ formationMode = process.env.MEMORIX_FORMATION_MODE;
10080
+ } else {
10081
+ try {
10082
+ const { getBehaviorConfig: getBehaviorConfig2 } = await Promise.resolve().then(() => (init_behavior(), behavior_exports));
10083
+ formationMode = getBehaviorConfig2().formationMode;
10084
+ } catch {
10085
+ }
10086
+ }
10087
+ const useFormation = formationMode === "active";
10088
+ let formationResult = null;
10089
+ let formationNote = "";
10090
+ if (useFormation && !topicKey && !progress) {
10091
+ try {
10092
+ const formationConfig = {
10093
+ mode: "active",
10094
+ useLLM: isLLMEnabled(),
10095
+ minValueScore: 0.3,
10096
+ searchMemories: async (q, limit, pid) => {
10097
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: "active" });
10098
+ if (result.entries.length === 0) return [];
10099
+ const details = await compactDetail(result.entries.map((e) => e.id));
10100
+ return details.documents.map((d, i) => ({
10101
+ id: Number(d.id.replace("obs-", "")),
10102
+ observationId: d.observationId,
10103
+ title: d.title,
10104
+ narrative: d.narrative,
10105
+ facts: d.facts,
10106
+ entityName: d.entityName,
10107
+ type: d.type,
10108
+ score: result.entries[i]?.score ?? 0
10109
+ }));
10110
+ },
10111
+ getObservation: (id) => {
10112
+ const o = getObservation(id);
10113
+ if (!o) return null;
10114
+ return {
10115
+ id: o.id,
10116
+ entityName: o.entityName,
10117
+ type: o.type,
10118
+ title: o.title,
10119
+ narrative: o.narrative,
10120
+ facts: o.facts,
10121
+ topicKey: o.topicKey
10122
+ };
10123
+ },
10124
+ getEntityNames: () => graphManager.getEntityNames()
10125
+ };
10126
+ formationResult = await runFormation({
10127
+ entityName,
10128
+ type,
10129
+ title,
10130
+ narrative,
10131
+ facts: safeFacts,
10132
+ projectId: project.id,
10133
+ source: "explicit"
10134
+ }, formationConfig);
10135
+ const modeIcon = "\u26A1";
10136
+ formationNote = `
10137
+ ${modeIcon} Formation[active]: ${formationResult.evaluation.category} (${formationResult.evaluation.score.toFixed(2)}) | ${formationResult.resolution.action} | ${formationResult.pipeline.durationMs}ms`;
10138
+ if (formationResult.extraction.extractedFacts.length > 0) {
10139
+ formationNote += ` | +${formationResult.extraction.extractedFacts.length} facts`;
10140
+ }
10141
+ if (formationResult.extraction.titleImproved) formationNote += " | title\u2191";
10142
+ if (formationResult.extraction.entityResolved) formationNote += ` | entity\u2192${formationResult.entityName}`;
10143
+ if (formationResult.extraction.typeCorrected) formationNote += ` | type\u2192${formationResult.type}`;
10144
+ } catch {
10145
+ }
10146
+ }
10147
+ if (useFormation && formationResult && formationResult.resolution.action !== "new") {
10148
+ const { action: action2, targetId, reason } = formationResult.resolution;
10149
+ if (action2 === "merge" && targetId) {
10150
+ const targetObs = getObservation(targetId);
10151
+ if (targetObs) {
10152
+ markInternalWrite();
10153
+ await storeObservation({
10154
+ entityName: targetObs.entityName,
10155
+ type: targetObs.type,
10156
+ title: formationResult.title,
10157
+ narrative: formationResult.narrative,
10158
+ facts: formationResult.facts,
10159
+ filesModified: safeFiles,
10160
+ concepts: safeConcepts,
10161
+ projectId: project.id,
10162
+ topicKey: targetObs.topicKey,
10163
+ progress
10164
+ });
10165
+ return {
10166
+ content: [{
10167
+ type: "text",
10168
+ text: `\u{1F504} Formation MERGE: merged into #${targetId} (${reason})${formationNote}`
10169
+ }]
10170
+ };
10171
+ }
10172
+ } else if (action2 === "evolve" && targetId) {
10173
+ const targetObs = getObservation(targetId);
10174
+ if (targetObs) {
10175
+ markInternalWrite();
10176
+ await storeObservation({
10177
+ entityName: targetObs.entityName,
10178
+ type: targetObs.type,
10179
+ title: formationResult.title,
10180
+ narrative: formationResult.narrative,
10181
+ facts: formationResult.facts,
10182
+ filesModified: safeFiles,
10183
+ concepts: safeConcepts,
10184
+ projectId: project.id,
10185
+ topicKey: targetObs.topicKey,
10186
+ progress
10187
+ });
10188
+ return {
10189
+ content: [{
10190
+ type: "text",
10191
+ text: `\u{1F504} Formation EVOLVE: evolved #${targetId} (${reason})${formationNote}`
10192
+ }]
10193
+ };
10194
+ }
10195
+ } else if (action2 === "discard") {
10196
+ return {
10197
+ content: [{
10198
+ type: "text",
10199
+ text: `\u23ED\uFE0F Formation DISCARD: ${reason}${formationNote}`
10200
+ }]
10201
+ };
10202
+ }
10203
+ }
8370
10204
  let compactAction = "";
8371
10205
  let compactMerged = false;
8372
- if (!topicKey && !progress) {
10206
+ if (!useFormation && !topicKey && !progress) {
8373
10207
  try {
8374
10208
  const searchResult = await compactSearch({
8375
10209
  query: `${title} ${narrative.substring(0, 200)}`,
@@ -8443,6 +10277,26 @@ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
8443
10277
  } catch {
8444
10278
  }
8445
10279
  }
10280
+ if (formationResult && formationResult.resolution.action === "new") {
10281
+ const llmFacts = formationResult.extraction.extractedFacts;
10282
+ if (llmFacts.length > 0) {
10283
+ const currentFacts = safeFacts ?? [];
10284
+ const currentLower = new Set(currentFacts.map((f) => f.toLowerCase().trim()));
10285
+ const newFacts = llmFacts.filter((f) => !currentLower.has(f.toLowerCase().trim()));
10286
+ if (newFacts.length > 0) {
10287
+ safeFacts = [...currentFacts, ...newFacts];
10288
+ }
10289
+ }
10290
+ if (formationResult.extraction.titleImproved && formationResult.title) {
10291
+ title = formationResult.title;
10292
+ }
10293
+ if (formationResult.extraction.typeCorrected && formationResult.type) {
10294
+ type = formationResult.type;
10295
+ }
10296
+ if (formationResult.extraction.entityResolved && formationResult.entityName) {
10297
+ entityName = formationResult.entityName;
10298
+ }
10299
+ }
8446
10300
  await graphManager.createEntities([
8447
10301
  { name: entityName, entityType: "auto", observations: [] }
8448
10302
  ]);
@@ -8476,7 +10330,9 @@ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
8476
10330
  projectId: project.id,
8477
10331
  topicKey,
8478
10332
  sessionId,
8479
- progress
10333
+ progress,
10334
+ relatedCommits,
10335
+ relatedEntities
8480
10336
  });
8481
10337
  await graphManager.addObservations([
8482
10338
  { entityName, contents: [`[#${obs.id}] ${title}`] }
@@ -8494,12 +10350,123 @@ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
8494
10350
  const enrichment = enrichmentParts.length > 0 ? `
8495
10351
  Auto-enriched: ${enrichmentParts.join(", ")}` : "";
8496
10352
  const action = upserted ? "\u{1F504} Updated" : "\u2705 Stored";
10353
+ if (!useFormation) {
10354
+ try {
10355
+ let oldCompactDecision = null;
10356
+ if (!topicKey && !progress) {
10357
+ try {
10358
+ const compactStart = Date.now();
10359
+ const searchResult = await compactSearch({
10360
+ query: `${title} ${narrative.substring(0, 200)}`,
10361
+ limit: 5,
10362
+ projectId: project.id,
10363
+ status: "active"
10364
+ });
10365
+ const similarEntries = searchResult.entries.map((e) => e);
10366
+ if (similarEntries.length > 0) {
10367
+ const similarIds = similarEntries.map((e) => e.id);
10368
+ const details = await compactDetail(similarIds);
10369
+ const existingMemories = details.documents.map((d, i) => ({
10370
+ id: d.observationId,
10371
+ title: d.title,
10372
+ narrative: d.narrative,
10373
+ facts: d.facts,
10374
+ score: similarEntries[i]?.score ?? 0
10375
+ }));
10376
+ const decision = await compactOnWrite(
10377
+ { title, narrative, facts: safeFacts ?? [] },
10378
+ existingMemories
10379
+ );
10380
+ const compactDuration = Date.now() - compactStart;
10381
+ oldCompactDecision = {
10382
+ action: decision.action,
10383
+ targetId: decision.targetId,
10384
+ reason: decision.reason,
10385
+ durationMs: compactDuration
10386
+ };
10387
+ }
10388
+ } catch {
10389
+ }
10390
+ }
10391
+ const formationConfig = {
10392
+ mode: formationMode,
10393
+ useLLM: isLLMEnabled(),
10394
+ minValueScore: 0.3,
10395
+ searchMemories: async (q, limit, pid) => {
10396
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: "active" });
10397
+ if (result.entries.length === 0) return [];
10398
+ const details = await compactDetail(result.entries.map((e) => e.id));
10399
+ return details.documents.map((d, i) => ({
10400
+ id: Number(d.id.replace("obs-", "")),
10401
+ observationId: d.observationId,
10402
+ title: d.title,
10403
+ narrative: d.narrative,
10404
+ facts: d.facts,
10405
+ entityName: d.entityName,
10406
+ type: d.type,
10407
+ score: result.entries[i]?.score ?? 0
10408
+ }));
10409
+ },
10410
+ getObservation: (id) => {
10411
+ const o = getObservation(id);
10412
+ if (!o) return null;
10413
+ return {
10414
+ id: o.id,
10415
+ entityName: o.entityName,
10416
+ type: o.type,
10417
+ title: o.title,
10418
+ narrative: o.narrative,
10419
+ facts: o.facts,
10420
+ topicKey: o.topicKey
10421
+ };
10422
+ },
10423
+ getEntityNames: () => graphManager.getEntityNames()
10424
+ };
10425
+ const formed = await runFormation({
10426
+ entityName,
10427
+ type,
10428
+ title,
10429
+ narrative,
10430
+ facts: safeFacts,
10431
+ projectId: project.id,
10432
+ source: "explicit",
10433
+ topicKey
10434
+ }, formationConfig);
10435
+ const { recordBeforeAfterMetrics: recordBeforeAfterMetrics2 } = await Promise.resolve().then(() => (init_formation(), formation_exports));
10436
+ if (oldCompactDecision) {
10437
+ recordBeforeAfterMetrics2({
10438
+ formationAction: formed.resolution.action,
10439
+ formationTargetId: formed.resolution.targetId,
10440
+ oldCompactAction: oldCompactDecision.action,
10441
+ oldCompactTargetId: oldCompactDecision.targetId,
10442
+ oldCompactReason: oldCompactDecision.reason,
10443
+ formationValueScore: formed.evaluation.score,
10444
+ formationValueCategory: formed.evaluation.category,
10445
+ formationDurationMs: formed.pipeline.durationMs,
10446
+ compactDurationMs: oldCompactDecision.durationMs
10447
+ });
10448
+ }
10449
+ const modeIcon = formationMode === "shadow" ? "\u{1F52C}" : "\u{1F6E1}\uFE0F";
10450
+ formationNote = `
10451
+ ${modeIcon} Formation[${formationMode}]: ${formed.evaluation.category} (${formed.evaluation.score.toFixed(2)}) | ${formed.resolution.action} | ${formed.pipeline.durationMs}ms`;
10452
+ if (oldCompactDecision) {
10453
+ formationNote += ` | Compact: ${oldCompactDecision.action}${oldCompactDecision.targetId ? ` #${oldCompactDecision.targetId}` : ""}${oldCompactDecision.durationMs ? ` (${oldCompactDecision.durationMs}ms)` : ""}`;
10454
+ }
10455
+ if (formed.extraction.extractedFacts.length > 0) {
10456
+ formationNote += ` | +${formed.extraction.extractedFacts.length} facts`;
10457
+ }
10458
+ if (formed.extraction.titleImproved) formationNote += " | title\u2191";
10459
+ if (formed.extraction.entityResolved) formationNote += ` | entity\u2192${formed.entityName}`;
10460
+ if (formed.extraction.typeCorrected) formationNote += ` | type\u2192${formed.type}`;
10461
+ } catch {
10462
+ }
10463
+ }
8497
10464
  return {
8498
10465
  content: [
8499
10466
  {
8500
10467
  type: "text",
8501
10468
  text: `${action} observation #${obs.id} "${title}" (~${obs.tokens} tokens)
8502
- Entity: ${entityName} | Type: ${type} | Project: ${project.id}${obs.topicKey ? ` | Topic: ${obs.topicKey}` : ""}${compactAction}${compressionNote}${enrichment}`
10469
+ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${obs.topicKey ? ` | Topic: ${obs.topicKey}` : ""}${compactAction}${compressionNote}${enrichment}${formationNote}`
8503
10470
  }
8504
10471
  ]
8505
10472
  };
@@ -8548,13 +10515,17 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
8548
10515
  until: z.string().optional().describe('Only return observations created before this date (ISO 8601 or natural like "2025-02-01")'),
8549
10516
  status: z.enum(["active", "resolved", "archived", "all"]).optional().default("active").describe(
8550
10517
  'Filter by memory status. "active" (default) shows current memories, "all" includes resolved/archived.'
10518
+ ),
10519
+ source: z.enum(["agent", "git", "manual"]).optional().describe(
10520
+ 'Filter by memory source. "git" returns only commit-derived ground truth memories. Omit for all sources.'
8551
10521
  )
8552
10522
  }
8553
10523
  },
8554
- async ({ query, limit, type, maxTokens, scope, since, until, status }) => {
10524
+ async ({ query, limit, type, maxTokens, scope, since, until, status, source }) => {
8555
10525
  const safeLimit = limit != null ? coerceNumber(limit, 20) : void 0;
8556
10526
  const safeMaxTokens = maxTokens != null ? coerceNumber(maxTokens, 0) : void 0;
8557
- const result = await compactSearch({
10527
+ const TIMEOUT_MS = 3e4;
10528
+ const searchPromise = compactSearch({
8558
10529
  query,
8559
10530
  limit: safeLimit,
8560
10531
  type,
@@ -8564,8 +10535,29 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
8564
10535
  // Default to project-scoped search to prevent cross-project pollution.
8565
10536
  // Use scope: 'global' to explicitly search all projects.
8566
10537
  projectId: scope === "global" ? void 0 : project.id,
8567
- status: status ?? "active"
10538
+ status: status ?? "active",
10539
+ source
8568
10540
  });
10541
+ const timeoutPromise = new Promise(
10542
+ (_, reject) => setTimeout(() => reject(new Error(`Search timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
10543
+ );
10544
+ let result;
10545
+ try {
10546
+ result = await Promise.race([searchPromise, timeoutPromise]);
10547
+ } catch (error) {
10548
+ if (error instanceof Error && error.message.includes("timeout")) {
10549
+ return {
10550
+ content: [
10551
+ {
10552
+ type: "text",
10553
+ text: `Error: Search timeout after ${TIMEOUT_MS}ms. Try a simpler query or check if the service is responsive.`
10554
+ }
10555
+ ],
10556
+ isError: true
10557
+ };
10558
+ }
10559
+ throw error;
10560
+ }
8569
10561
  let text = result.formatted;
8570
10562
  if (!syncAdvisoryShown && syncAdvisory) {
8571
10563
  text += syncAdvisory;
@@ -8610,6 +10602,102 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
8610
10602
  };
8611
10603
  }
8612
10604
  );
10605
+ server.registerTool(
10606
+ "memorix_store_reasoning",
10607
+ {
10608
+ title: "Store Reasoning Trace",
10609
+ description: "Store a reasoning trace \u2014 WHY you chose this approach, what alternatives you considered, and what outcome you expect. This creates a searchable record of your decision-making process. Use this when making non-trivial technical decisions, choosing between approaches, or solving complex problems. Unlike regular memories that record WHAT happened, reasoning memories record HOW you thought about it.",
10610
+ inputSchema: {
10611
+ entityName: z.string().describe('The entity this reasoning applies to (e.g., "auth-module", "database-schema")'),
10612
+ decision: z.string().describe("What was decided or chosen"),
10613
+ alternatives: z.array(z.string()).optional().describe("Other options that were considered"),
10614
+ rationale: z.string().describe("Why this approach was chosen over alternatives"),
10615
+ constraints: z.array(z.string()).optional().describe("Constraints that influenced the decision (time, perf, compat, etc.)"),
10616
+ expectedOutcome: z.string().optional().describe("What outcome is expected from this decision"),
10617
+ risks: z.array(z.string()).optional().describe("Known risks or potential downsides"),
10618
+ concepts: z.array(z.string()).optional().describe("Related technical concepts"),
10619
+ filesModified: z.array(z.string()).optional().describe("Files related to this reasoning"),
10620
+ relatedCommits: z.array(z.string()).optional().describe("Git commit hashes this reasoning explains (links ground truth \u2194 reasoning)"),
10621
+ relatedEntities: z.array(z.string()).optional().describe("Other entity names this reasoning relates to (cross-references)")
10622
+ }
10623
+ },
10624
+ async ({ entityName, decision, alternatives, rationale, constraints, expectedOutcome, risks, concepts, filesModified, relatedCommits, relatedEntities }) => {
10625
+ const narrativeParts = [rationale];
10626
+ if (alternatives && alternatives.length > 0) {
10627
+ narrativeParts.push(`Alternatives considered: ${alternatives.join("; ")}`);
10628
+ }
10629
+ if (constraints && constraints.length > 0) {
10630
+ narrativeParts.push(`Constraints: ${constraints.join("; ")}`);
10631
+ }
10632
+ if (expectedOutcome) {
10633
+ narrativeParts.push(`Expected outcome: ${expectedOutcome}`);
10634
+ }
10635
+ const narrative = narrativeParts.join(". ");
10636
+ const facts = [`Decision: ${decision}`];
10637
+ if (alternatives) alternatives.forEach((a) => facts.push(`Alternative considered: ${a}`));
10638
+ if (constraints) constraints.forEach((c) => facts.push(`Constraint: ${c}`));
10639
+ if (risks) risks.forEach((r) => facts.push(`Risk: ${r}`));
10640
+ if (expectedOutcome) facts.push(`Expected outcome: ${expectedOutcome}`);
10641
+ await graphManager.createEntities([
10642
+ { name: entityName, entityType: "auto", observations: [] }
10643
+ ]);
10644
+ markInternalWrite();
10645
+ const { observation: obs } = await storeObservation({
10646
+ entityName,
10647
+ type: "reasoning",
10648
+ title: decision.length > 80 ? decision.substring(0, 77) + "..." : decision,
10649
+ narrative,
10650
+ facts,
10651
+ concepts: concepts ?? [],
10652
+ filesModified: filesModified ?? [],
10653
+ projectId: project.id,
10654
+ source: "agent",
10655
+ relatedCommits,
10656
+ relatedEntities
10657
+ });
10658
+ await graphManager.addObservations([
10659
+ { entityName, contents: [`[#${obs.id}] \u{1F9E0} ${decision}`] }
10660
+ ]);
10661
+ return {
10662
+ content: [{
10663
+ type: "text",
10664
+ text: `\u{1F9E0} Reasoning trace stored #${obs.id}: "${decision}"
10665
+ Entity: ${entityName} | ${facts.length} facts | ${obs.tokens} tokens`
10666
+ }]
10667
+ };
10668
+ }
10669
+ );
10670
+ server.registerTool(
10671
+ "memorix_search_reasoning",
10672
+ {
10673
+ title: "Search Reasoning Patterns",
10674
+ description: "Search past reasoning traces to understand WHY decisions were made. Returns reasoning memories that explain the thought process behind technical choices. Use this when revisiting code, questioning a design decision, or looking for precedent on how similar problems were solved before.",
10675
+ inputSchema: {
10676
+ query: z.string().describe('Search query \u2014 describe what reasoning you want to find (e.g., "why did we choose PostgreSQL", "auth approach rationale")'),
10677
+ limit: z.number().optional().describe("Max results (default: 10)"),
10678
+ scope: z.enum(["project", "global"]).optional().default("project").describe("Search scope")
10679
+ }
10680
+ },
10681
+ async ({ query, limit, scope }) => {
10682
+ const safeLimit = limit != null ? coerceNumber(limit, 10) : 10;
10683
+ const result = await compactSearch({
10684
+ query,
10685
+ limit: safeLimit,
10686
+ type: "reasoning",
10687
+ projectId: scope === "global" ? void 0 : project.id,
10688
+ status: "active"
10689
+ });
10690
+ if (result.entries.length === 0) {
10691
+ return {
10692
+ content: [{ type: "text", text: "No reasoning traces found. Use memorix_store_reasoning to record decision rationale." }]
10693
+ };
10694
+ }
10695
+ return {
10696
+ content: [{ type: "text", text: `\u{1F9E0} Reasoning Traces:
10697
+ ${result.formatted}` }]
10698
+ };
10699
+ }
10700
+ );
8613
10701
  server.registerTool(
8614
10702
  "memorix_deduplicate",
8615
10703
  {
@@ -8742,19 +10830,27 @@ ${actions.join("\n")}`
8742
10830
  "memorix_detail",
8743
10831
  {
8744
10832
  title: "Memory Details",
8745
- description: "Fetch full observation details by IDs (~500-1000 tokens each). Always use memorix_search first to find relevant IDs, then fetch only what you need.",
10833
+ description: "Fetch full observation details by IDs (~500-1000 tokens each). Always use memorix_search first to find relevant IDs, then fetch only what you need. For global search results, prefer refs with projectId to avoid cross-project ID ambiguity.",
8746
10834
  inputSchema: {
8747
- ids: z.array(z.number()).describe("Observation IDs to fetch (from memorix_search results)")
10835
+ ids: z.array(z.number()).optional().describe("Observation IDs to fetch (from memorix_search results)"),
10836
+ refs: z.array(
10837
+ z.object({
10838
+ id: z.number().describe("Observation ID"),
10839
+ projectId: z.string().optional().describe("Project ID for global-search disambiguation")
10840
+ })
10841
+ ).optional().describe("Explicit observation refs. Prefer this for global search results.")
8748
10842
  }
8749
10843
  },
8750
- async ({ ids }) => {
10844
+ async ({ ids, refs }) => {
8751
10845
  const safeIds = coerceNumberArray(ids);
8752
- const result = await compactDetail(safeIds);
10846
+ const safeRefs = coerceObservationRefs(refs);
10847
+ const detailInput = safeRefs.length > 0 ? safeRefs : safeIds;
10848
+ const result = await compactDetail(detailInput);
8753
10849
  return {
8754
10850
  content: [
8755
10851
  {
8756
10852
  type: "text",
8757
- text: result.documents.length > 0 ? result.formatted : `No observations found for IDs: ${safeIds.join(", ")}`
10853
+ text: result.documents.length > 0 ? result.formatted : safeRefs.length > 0 ? `No observations found for refs: ${safeRefs.map((ref) => `${ref.projectId ?? "current"}#${ref.id}`).join(", ")}` : `No observations found for IDs: ${safeIds.join(", ")}`
8758
10854
  }
8759
10855
  ]
8760
10856
  };
@@ -8804,7 +10900,8 @@ Archived memories can be restored manually if needed.` }]
8804
10900
  projectId: obs.projectId,
8805
10901
  accessCount: 0,
8806
10902
  lastAccessedAt: "",
8807
- status: obs.status ?? "active"
10903
+ status: obs.status ?? "active",
10904
+ source: obs.source ?? "agent"
8808
10905
  }));
8809
10906
  if (docs.length === 0) {
8810
10907
  return {
@@ -8854,12 +10951,89 @@ Archived memories can be restored manually if needed.` }]
8854
10951
  };
8855
10952
  }
8856
10953
  );
10954
+ server.registerTool(
10955
+ "memorix_formation_metrics",
10956
+ {
10957
+ title: "Formation Pipeline Metrics",
10958
+ description: "Show aggregated metrics from recent Memory Formation Pipeline runs. Reports value scores, resolution actions, fact extraction rates, and processing times.",
10959
+ inputSchema: {}
10960
+ },
10961
+ async () => {
10962
+ const summary = getMetricsSummary();
10963
+ const beforeAfter = getBeforeAfterMetrics();
10964
+ if (summary.total === 0) {
10965
+ return {
10966
+ content: [{
10967
+ type: "text",
10968
+ text: "\u{1F4CA} Formation Pipeline: No metrics collected yet.\nStore some observations to start collecting runtime data."
10969
+ }]
10970
+ };
10971
+ }
10972
+ const lines = [
10973
+ "\u{1F4CA} **Formation Pipeline Metrics**",
10974
+ "",
10975
+ `**Total observations processed:** ${summary.total}`,
10976
+ `**Average value score:** ${summary.avgValueScore.toFixed(3)}`,
10977
+ `**Average processing time:** ${summary.avgDurationMs.toFixed(1)}ms`,
10978
+ "",
10979
+ "### Quality Indicators",
10980
+ `- **Avg system-extracted facts:** ${summary.avgExtractedFacts.toFixed(1)} per observation`,
10981
+ `- **Title improved rate:** ${(summary.titleImprovedRate * 100).toFixed(1)}%`,
10982
+ `- **Entity resolved rate:** ${(summary.entityResolvedRate * 100).toFixed(1)}%`,
10983
+ `- **Type corrected rate:** ${(summary.typeCorectedRate * 100).toFixed(1)}%`,
10984
+ "",
10985
+ "### Value Categories"
10986
+ ];
10987
+ for (const [cat, count2] of Object.entries(summary.categoryBreakdown)) {
10988
+ const pct = (count2 / summary.total * 100).toFixed(1);
10989
+ const icon = cat === "core" ? "\u{1F7E2}" : cat === "contextual" ? "\u{1F7E1}" : "\u{1F534}";
10990
+ lines.push(`- ${icon} **${cat}:** ${count2} (${pct}%)`);
10991
+ }
10992
+ lines.push("", "### Resolution Actions");
10993
+ for (const [action, count2] of Object.entries(summary.resolutionBreakdown)) {
10994
+ const pct = (count2 / summary.total * 100).toFixed(1);
10995
+ lines.push(`- **${action}:** ${count2} (${pct}%)`);
10996
+ }
10997
+ if (beforeAfter.totalProcessed > 0) {
10998
+ lines.push(
10999
+ "",
11000
+ "### Before/After Comparison (Formation vs Old Compact)",
11001
+ `**Total comparisons:** ${beforeAfter.totalProcessed}`,
11002
+ `**Agreements:** ${beforeAfter.agreements} (${(beforeAfter.agreements / beforeAfter.totalProcessed * 100).toFixed(1)}%)`,
11003
+ `**Disagreements:** ${beforeAfter.disagreements} (${(beforeAfter.disagreements / beforeAfter.totalProcessed * 100).toFixed(1)}%)`,
11004
+ "",
11005
+ "### Disagreement Breakdown",
11006
+ `- Formation discarded, Compact added: ${beforeAfter.disagreementBreakdown.formationDiscardedCompactAdded}`,
11007
+ `- Formation merged, Compact added: ${beforeAfter.disagreementBreakdown.formationMergedCompactAdded}`,
11008
+ `- Formation added, Compact discarded: ${beforeAfter.disagreementBreakdown.formationAddedCompactDiscarded}`,
11009
+ "- Formation added, Compact merged: " + beforeAfter.disagreementBreakdown.formationAddedCompactMerged,
11010
+ "- Formation evolved, Compact added: " + beforeAfter.disagreementBreakdown.formationEvolvedCompactAdded,
11011
+ "- Other: " + beforeAfter.disagreementBreakdown.other,
11012
+ "",
11013
+ "### Quality Improvements",
11014
+ `- Formation discarded low-value: ${beforeAfter.quality.formationDiscardedLowValue}`,
11015
+ `- Formation merged duplicates: ${beforeAfter.quality.formationMergedDuplicates}`,
11016
+ `- Formation evolved outdated: ${beforeAfter.quality.formationEvolvedOutdated}`,
11017
+ `- Compact missed duplicates: ${beforeAfter.quality.compactMissedDuplicates}`,
11018
+ `- Compact kept low-value: ${beforeAfter.quality.compactKeptLowValue}`,
11019
+ "",
11020
+ `### Duration Comparison`,
11021
+ `- Formation avg: ${beforeAfter.duration.formationAvgMs.toFixed(1)}ms`,
11022
+ `- Compact avg: ${beforeAfter.duration.compactAvgMs.toFixed(1)}ms`,
11023
+ `- Diff: ${beforeAfter.duration.diffMs.toFixed(1)}ms`
11024
+ );
11025
+ }
11026
+ return {
11027
+ content: [{ type: "text", text: lines.join("\n") }]
11028
+ };
11029
+ }
11030
+ );
8857
11031
  let enableKG = false;
8858
11032
  try {
8859
- const { homedir: homedir16 } = await import("os");
8860
- const { join: join17 } = await import("path");
8861
- const { readFile: readFile5 } = await import("fs/promises");
8862
- const raw = await readFile5(join17(homedir16(), ".memorix", "settings.json"), "utf-8");
11033
+ const { homedir: homedir20 } = await import("os");
11034
+ const { join: join21 } = await import("path");
11035
+ const { readFile: readFile6 } = await import("fs/promises");
11036
+ const raw = await readFile6(join21(homedir20(), ".memorix", "settings.json"), "utf-8");
8863
11037
  const s = JSON.parse(raw);
8864
11038
  if (s.knowledgeGraph === true) enableKG = true;
8865
11039
  } catch {
@@ -9257,7 +11431,9 @@ ${skill.content}` }]
9257
11431
  facts: o.facts,
9258
11432
  concepts: o.concepts,
9259
11433
  filesModified: o.filesModified,
9260
- createdAt: o.createdAt
11434
+ createdAt: o.createdAt,
11435
+ status: o.status,
11436
+ source: o.source
9261
11437
  }));
9262
11438
  const generated = engine.generateFromObservations(obsData);
9263
11439
  if (generated.length === 0) {
@@ -9276,9 +11452,9 @@ ${skill.content}` }]
9276
11452
  lines.push(`- **Description**: ${sk.description}`);
9277
11453
  lines.push(`- **Observations**: ${sk.content.split("\n").length} lines of knowledge`);
9278
11454
  if (write && target) {
9279
- const path9 = engine.writeSkill(sk, target);
9280
- if (path9) {
9281
- lines.push(`- \u2705 **Written**: \`${path9}\``);
11455
+ const path11 = engine.writeSkill(sk, target);
11456
+ if (path11) {
11457
+ lines.push(`- \u2705 **Written**: \`${path11}\``);
9282
11458
  } else {
9283
11459
  lines.push(`- \u274C Failed to write`);
9284
11460
  }
@@ -9734,9 +11910,9 @@ ${json}
9734
11910
  let teamPersist = null;
9735
11911
  if (!sharedTeam) {
9736
11912
  const { TeamPersistence: TeamPersistence2 } = await Promise.resolve().then(() => (init_persistence2(), persistence_exports2));
9737
- const { join: join17 } = await import("path");
11913
+ const { join: join21 } = await import("path");
9738
11914
  teamPersist = new TeamPersistence2(
9739
- join17(projectDir2, "team-state.json"),
11915
+ join21(projectDir2, "team-state.json"),
9740
11916
  teamRegistry,
9741
11917
  messageBus,
9742
11918
  taskManager,
@@ -9969,58 +12145,67 @@ ${lines.join("\n")}` }] };
9969
12145
  );
9970
12146
  const deferredInit = async () => {
9971
12147
  try {
9972
- let autoInstall = true;
9973
- try {
9974
- const { homedir: homedir16 } = await import("os");
9975
- const { join: join17 } = await import("path");
9976
- const { readFile: readFile5 } = await import("fs/promises");
9977
- const settingsPath = join17(homedir16(), ".memorix", "settings.json");
9978
- const raw = await readFile5(settingsPath, "utf-8");
9979
- const settings = JSON.parse(raw);
9980
- if (settings.autoInstallHooks === false) {
9981
- autoInstall = false;
9982
- console.error("[memorix] autoInstallHooks disabled in ~/.memorix/settings.json \u2014 skipping hook auto-install");
9983
- }
9984
- } catch {
12148
+ const { getHookStatus: getHookStatus2 } = await Promise.resolve().then(() => (init_installers(), installers_exports));
12149
+ const workDir = cwd ?? process.cwd();
12150
+ const statuses = await getHookStatus2(workDir);
12151
+ const installedAgents = statuses.filter((s) => s.installed).map((s) => s.agent);
12152
+ if (installedAgents.length === 0) {
12153
+ console.error('[memorix] No hooks installed. Run "memorix hooks install" to set up auto-capture.');
12154
+ } else {
12155
+ console.error(`[memorix] Hooks active: ${installedAgents.join(", ")}`);
9985
12156
  }
9986
- if (autoInstall) {
9987
- const { getHookStatus: getHookStatus2, installHooks: installHooks2, detectInstalledAgents: detectInstalledAgents2 } = await Promise.resolve().then(() => (init_installers(), installers_exports));
9988
- const { join: join17 } = await import("path");
9989
- const { access: access2 } = await import("fs/promises");
9990
- const workDir = cwd ?? process.cwd();
9991
- const statuses = await getHookStatus2(workDir);
9992
- const installedAgents = new Set(statuses.filter((s) => s.installed).map((s) => s.agent));
9993
- const detectedAgents = await detectInstalledAgents2();
9994
- const AGENT_MARKER_DIR = {
9995
- claude: ".claude",
9996
- windsurf: ".windsurf",
9997
- cursor: ".cursor",
9998
- copilot: ".vscode",
9999
- opencode: ".opencode",
10000
- kiro: ".kiro",
10001
- antigravity: ".gemini",
10002
- trae: ".trae"
10003
- };
10004
- for (const agent of detectedAgents) {
10005
- if (installedAgents.has(agent)) continue;
10006
- const markerDir = AGENT_MARKER_DIR[agent];
10007
- if (markerDir) {
12157
+ } catch {
12158
+ }
12159
+ try {
12160
+ const { getGitConfig: getGitConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
12161
+ const gitCfg = getGitConfig2();
12162
+ if (gitCfg.autoHook && project.rootPath) {
12163
+ const { ensureHooksDir: ensureHooksDir2 } = await Promise.resolve().then(() => (init_hooks_path(), hooks_path_exports));
12164
+ const resolved = ensureHooksDir2(project.rootPath);
12165
+ if (resolved) {
12166
+ const { existsSync: existsSync10, readFileSync: readFileSync9, writeFileSync: writeFileSync3, chmodSync } = await import("fs");
12167
+ const { hookPath } = resolved;
12168
+ const HOOK_MARKER = "# [memorix-git-hook]";
12169
+ const needsInstall = !existsSync10(hookPath) || !readFileSync9(hookPath, "utf-8").includes(HOOK_MARKER);
12170
+ if (needsInstall) {
12171
+ const hookScript = `#!/bin/sh
12172
+ ${HOOK_MARKER}
12173
+ # Memorix: Auto-ingest git commits as memories
12174
+ if command -v memorix >/dev/null 2>&1; then
12175
+ memorix ingest commit --auto >/dev/null 2>&1 &
12176
+ fi
12177
+ `;
12178
+ if (existsSync10(hookPath)) {
12179
+ const existing = readFileSync9(hookPath, "utf-8");
12180
+ writeFileSync3(hookPath, existing.trimEnd() + `
12181
+
12182
+ ${HOOK_MARKER}
12183
+ if command -v memorix >/dev/null 2>&1; then
12184
+ memorix ingest commit --auto >/dev/null 2>&1 &
12185
+ fi
12186
+ `, "utf-8");
12187
+ } else {
12188
+ writeFileSync3(hookPath, hookScript, "utf-8");
12189
+ }
10008
12190
  try {
10009
- await access2(join17(workDir, markerDir));
12191
+ chmodSync(hookPath, 493);
10010
12192
  } catch {
10011
- continue;
10012
12193
  }
10013
- }
10014
- try {
10015
- const config = await installHooks2(agent, workDir);
10016
- console.error(`[memorix] Auto-installed hooks for ${agent} \u2192 ${config.configPath}`);
10017
- } catch {
12194
+ console.error("[memorix] Auto-installed git post-commit hook (git.autoHook: true)");
10018
12195
  }
10019
12196
  }
10020
12197
  }
10021
12198
  } catch {
10022
12199
  }
12200
+ let behaviorConfig = { syncAdvisory: true, autoCleanup: true };
10023
12201
  try {
12202
+ const { getBehaviorConfig: getBehaviorConfig2 } = await Promise.resolve().then(() => (init_behavior(), behavior_exports));
12203
+ behaviorConfig = getBehaviorConfig2();
12204
+ } catch {
12205
+ }
12206
+ if (!behaviorConfig.syncAdvisory) {
12207
+ console.error("[memorix] Sync advisory disabled via config.");
12208
+ } else try {
10024
12209
  const engine = new WorkspaceSyncEngine(project.rootPath);
10025
12210
  const scan = await engine.scan();
10026
12211
  const lines = [];
@@ -10062,59 +12247,63 @@ ${lines.join("\n")}` }] };
10062
12247
  console.error(`[memorix] Sync advisory: ${syncAdvisory ? "available" : "nothing to sync"}`);
10063
12248
  } catch {
10064
12249
  }
10065
- try {
10066
- const { archiveExpired: archiveExpired2 } = await Promise.resolve().then(() => (init_retention(), retention_exports));
10067
- const archiveResult = await archiveExpired2(projectDir2);
10068
- if (archiveResult.archived > 0) {
10069
- console.error(`[memorix] Auto-archived ${archiveResult.archived} expired observation(s)`);
12250
+ if (!behaviorConfig.autoCleanup) {
12251
+ console.error("[memorix] Auto-cleanup disabled via config.");
12252
+ } else {
12253
+ try {
12254
+ const { archiveExpired: archiveExpired2 } = await Promise.resolve().then(() => (init_retention(), retention_exports));
12255
+ const archiveResult = await archiveExpired2(projectDir2);
12256
+ if (archiveResult.archived > 0) {
12257
+ console.error(`[memorix] Auto-archived ${archiveResult.archived} expired observation(s)`);
12258
+ }
12259
+ } catch {
10070
12260
  }
10071
- } catch {
10072
- }
10073
- try {
10074
- if (isLLMEnabled()) {
10075
- const { getAllObservations: getAllObservations2, resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
10076
- const { deduplicateMemory: deduplicateMemory2 } = await Promise.resolve().then(() => (init_memory_manager(), memory_manager_exports));
10077
- const allObs = getAllObservations2().filter((o) => (o.status ?? "active") === "active" && o.projectId === project.id);
10078
- if (allObs.length > 10) {
10079
- const grouped = /* @__PURE__ */ new Map();
10080
- for (const obs of allObs) {
10081
- const key = `${obs.entityName}::${obs.type}`;
10082
- if (!grouped.has(key)) grouped.set(key, []);
10083
- grouped.get(key).push(obs);
10084
- }
10085
- const toResolve = [];
10086
- for (const [, group] of grouped) {
10087
- if (group.length < 2) continue;
10088
- group.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
10089
- for (let i = 0; i < group.length - 1 && i < 5; i++) {
10090
- try {
10091
- const older = group[i], newer = group[i + 1];
10092
- const decision = await deduplicateMemory2(
10093
- { title: newer.title, narrative: newer.narrative, facts: newer.facts },
10094
- [{ id: older.id, title: older.title, narrative: older.narrative, facts: older.facts.join("\n") }]
10095
- );
10096
- if (decision && (decision.action === "UPDATE" || decision.action === "NONE")) {
10097
- toResolve.push(decision.action === "UPDATE" ? older.id : newer.id);
10098
- } else if (decision?.action === "DELETE" && decision.targetId) {
10099
- toResolve.push(decision.targetId);
12261
+ try {
12262
+ if (isLLMEnabled()) {
12263
+ const { getAllObservations: getAllObservations2, resolveObservations: resolveObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
12264
+ const { deduplicateMemory: deduplicateMemory2 } = await Promise.resolve().then(() => (init_memory_manager(), memory_manager_exports));
12265
+ const allObs = getAllObservations2().filter((o) => (o.status ?? "active") === "active" && o.projectId === project.id);
12266
+ if (allObs.length > 10) {
12267
+ const grouped = /* @__PURE__ */ new Map();
12268
+ for (const obs of allObs) {
12269
+ const key = `${obs.entityName}::${obs.type}`;
12270
+ if (!grouped.has(key)) grouped.set(key, []);
12271
+ grouped.get(key).push(obs);
12272
+ }
12273
+ const toResolve = [];
12274
+ for (const [, group] of grouped) {
12275
+ if (group.length < 2) continue;
12276
+ group.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
12277
+ for (let i = 0; i < group.length - 1 && i < 5; i++) {
12278
+ try {
12279
+ const older = group[i], newer = group[i + 1];
12280
+ const decision = await deduplicateMemory2(
12281
+ { title: newer.title, narrative: newer.narrative, facts: newer.facts },
12282
+ [{ id: older.id, title: older.title, narrative: older.narrative, facts: older.facts.join("\n") }]
12283
+ );
12284
+ if (decision && (decision.action === "UPDATE" || decision.action === "NONE")) {
12285
+ toResolve.push(decision.action === "UPDATE" ? older.id : newer.id);
12286
+ } else if (decision?.action === "DELETE" && decision.targetId) {
12287
+ toResolve.push(decision.targetId);
12288
+ }
12289
+ } catch {
10100
12290
  }
10101
- } catch {
10102
12291
  }
10103
12292
  }
12293
+ if (toResolve.length > 0) {
12294
+ await resolveObservations2([...new Set(toResolve)], "resolved");
12295
+ console.error(`[memorix] Auto-dedup (LLM): resolved ${toResolve.length} redundant observation(s)`);
12296
+ }
10104
12297
  }
10105
- if (toResolve.length > 0) {
10106
- await resolveObservations2([...new Set(toResolve)], "resolved");
10107
- console.error(`[memorix] Auto-dedup (LLM): resolved ${toResolve.length} redundant observation(s)`);
12298
+ } else {
12299
+ const { executeConsolidation: executeConsolidation2 } = await Promise.resolve().then(() => (init_consolidation(), consolidation_exports));
12300
+ const result = await executeConsolidation2(projectDir2, project.id, { threshold: 0.55 });
12301
+ if (result.observationsMerged > 0) {
12302
+ console.error(`[memorix] Auto-consolidated: merged ${result.observationsMerged} duplicate(s) across ${result.clustersFound} cluster(s)`);
10108
12303
  }
10109
12304
  }
10110
- } else {
10111
- const { executeConsolidation: executeConsolidation2 } = await Promise.resolve().then(() => (init_consolidation(), consolidation_exports));
10112
- const result = await executeConsolidation2(projectDir2, project.id, { threshold: 0.55 });
10113
- if (result.observationsMerged > 0) {
10114
- console.error(`[memorix] Auto-consolidated: merged ${result.observationsMerged} duplicate(s) across ${result.clustersFound} cluster(s)`);
10115
- }
12305
+ } catch {
10116
12306
  }
10117
- } catch {
10118
12307
  }
10119
12308
  const observationsFile = projectDir2 + "/observations.json";
10120
12309
  let reloadDebounce = null;
@@ -10145,7 +12334,32 @@ ${lines.join("\n")}` }] };
10145
12334
  console.error(`[memorix] Warning: could not watch observations file for hot-reload`);
10146
12335
  }
10147
12336
  };
10148
- return { server, graphManager, projectId: project.id, deferredInit };
12337
+ const switchProject = async (newCwd) => {
12338
+ const newDetected = detectProject(newCwd);
12339
+ if (!newDetected) return false;
12340
+ const newCanonicalId = await registerAlias(newDetected);
12341
+ if (newCanonicalId === project.id) return false;
12342
+ console.error(`[memorix] Switching project: ${project.id} \u2192 ${newCanonicalId}`);
12343
+ const newProjectDir = await getProjectDataDir(newCanonicalId);
12344
+ project = { ...newDetected, id: newCanonicalId };
12345
+ projectDir2 = newProjectDir;
12346
+ try {
12347
+ const { initProjectRoot: initProjectRoot2 } = await Promise.resolve().then(() => (init_yaml_loader(), yaml_loader_exports));
12348
+ initProjectRoot2(project.rootPath);
12349
+ const { resetDotenv: resetDotenv2, loadDotenv: loadDotenv2 } = await Promise.resolve().then(() => (init_dotenv_loader(), dotenv_loader_exports));
12350
+ resetDotenv2();
12351
+ loadDotenv2(project.rootPath);
12352
+ } catch {
12353
+ }
12354
+ graphManager = new KnowledgeGraphManager(projectDir2);
12355
+ await graphManager.init();
12356
+ await initObservations(projectDir2);
12357
+ await reindexObservations();
12358
+ console.error(`[memorix] Project switched to: ${project.id} (${project.name})`);
12359
+ console.error(`[memorix] Data dir: ${projectDir2}`);
12360
+ return true;
12361
+ };
12362
+ return { server, graphManager, projectId: project.id, deferredInit, switchProject };
10149
12363
  }
10150
12364
 
10151
12365
  // src/index.ts