memorix 1.8.1 → 1.8.3

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/cli/index.js +577 -58
  3. package/dist/index.js +832 -321
  4. package/dist/maintenance-runner.js +31 -25
  5. package/dist/media-audio-runner.js +31 -25
  6. package/dist/media-video-runner.js +31 -25
  7. package/dist/memcode-runtime/CHANGELOG.md +36 -0
  8. package/dist/sdk.js +585 -315
  9. package/dist/vector-backfill-runner.js +31 -25
  10. package/docs/1.2.0-PROVIDER-QUALITY.md +15 -6
  11. package/docs/1.3.3-MULTIMODAL-MEDIA-AND-MCP-COMPAT.md +7 -6
  12. package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +13 -8
  13. package/docs/API_REFERENCE.md +14 -11
  14. package/docs/CONFIGURATION.md +2 -2
  15. package/docs/DEVELOPMENT.md +3 -2
  16. package/docs/README.md +13 -7
  17. package/package.json +2 -1
  18. package/plugins/antigravity/memorix/plugin.json +2 -1
  19. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  20. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  21. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  22. package/plugins/copilot/memorix/plugin.json +1 -1
  23. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  24. package/plugins/hermes/memorix/plugin.yaml +1 -1
  25. package/plugins/omp/memorix/package.json +1 -1
  26. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  27. package/plugins/pi/memorix/package.json +1 -1
  28. package/server.json +3 -3
  29. package/src/cli/commands/serve-http.ts +10 -2
  30. package/src/cli/commands/serve.ts +26 -7
  31. package/src/codegraph/external-provider.ts +299 -19
  32. package/src/config/dotenv-loader.ts +9 -4
  33. package/src/config.ts +16 -16
  34. package/src/dashboard/maintenance.ts +4 -1
  35. package/src/llm/provider.ts +17 -4
  36. package/src/server/mcp-compat.ts +63 -0
  37. package/src/server/mcp-diagnostics.ts +13 -4
  38. package/src/server/mcp-discovery.ts +61 -0
  39. package/src/server/stdio-startup-gate.ts +263 -0
  40. package/src/server.ts +4 -4
@@ -14,8 +14,8 @@ import type {
14
14
  } from './types.js';
15
15
 
16
16
  // The bundled Windows CodeGraph CLI takes roughly 600 ms just to start cold.
17
- // Keep the default useful in real sessions while still bounding a local call.
18
- export const DEFAULT_EXTERNAL_CODEGRAPH_TIMEOUT_MS = 1_200;
17
+ // Leave room for a cold project read while keeping each probe bounded.
18
+ export const DEFAULT_EXTERNAL_CODEGRAPH_TIMEOUT_MS = 3_000;
19
19
  export const MAX_EXTERNAL_CODEGRAPH_TIMEOUT_MS = 5_000;
20
20
  /** Explicit index maintenance is opt-in and may legitimately take longer. */
21
21
  export const EXTERNAL_CODEGRAPH_LIFECYCLE_TIMEOUT_MS = 30_000;
@@ -85,6 +85,28 @@ export interface ExternalCodeGraphContextResult extends InspectExternalCodeGraph
85
85
  outline?: ExternalCodeGraphOutline;
86
86
  /** Only present when a detected external graph could not safely contribute. */
87
87
  caution?: string;
88
+ diagnostics?: ExternalCodeGraphQueryDiagnostics;
89
+ }
90
+
91
+ export type ExternalCodeGraphQueryOutcome =
92
+ | 'not-attempted'
93
+ | 'success'
94
+ | 'failed'
95
+ | 'timed-out'
96
+ | 'output-limited'
97
+ | 'invalid-output';
98
+
99
+ export interface ExternalCodeGraphQueryDiagnostic {
100
+ command: 'context' | 'explore';
101
+ attempted: boolean;
102
+ outcome: ExternalCodeGraphQueryOutcome;
103
+ exitCode?: number;
104
+ reason?: string;
105
+ }
106
+
107
+ export interface ExternalCodeGraphQueryDiagnostics {
108
+ context: ExternalCodeGraphQueryDiagnostic;
109
+ explore: ExternalCodeGraphQueryDiagnostic;
88
110
  }
89
111
 
90
112
  /** Explicit lifecycle operations. Memorix never calls CodeGraph's `install`. */
@@ -410,6 +432,42 @@ function unavailableHealth(result: ExternalCodeGraphCommandResult): ExternalCode
410
432
  return { state: 'unavailable', reason: 'A local CodeGraph command did not complete.' };
411
433
  }
412
434
 
435
+ function notAttemptedDiagnostic(command: 'context' | 'explore'): ExternalCodeGraphQueryDiagnostic {
436
+ return { command, attempted: false, outcome: 'not-attempted' };
437
+ }
438
+
439
+ function commandDiagnostic(
440
+ command: 'context' | 'explore',
441
+ result: ExternalCodeGraphCommandResult,
442
+ outcome: ExternalCodeGraphQueryOutcome,
443
+ reason?: string,
444
+ ): ExternalCodeGraphQueryDiagnostic {
445
+ return {
446
+ command,
447
+ attempted: true,
448
+ outcome,
449
+ ...(typeof result.exitCode === 'number' ? { exitCode: result.exitCode } : {}),
450
+ ...(reason ? { reason: sanitizeCredentials(reason).slice(0, 240) } : {}),
451
+ };
452
+ }
453
+
454
+ function failedCommandDiagnostic(
455
+ command: 'context' | 'explore',
456
+ result: ExternalCodeGraphCommandResult,
457
+ ): ExternalCodeGraphQueryDiagnostic {
458
+ const outcome: ExternalCodeGraphQueryOutcome = result.timedOut
459
+ ? 'timed-out'
460
+ : result.outputLimited
461
+ ? 'output-limited'
462
+ : 'failed';
463
+ return commandDiagnostic(
464
+ command,
465
+ result,
466
+ outcome,
467
+ result.error || result.stderr || 'CodeGraph command did not complete.',
468
+ );
469
+ }
470
+
413
471
  async function runSafely(
414
472
  runner: ExternalCodeGraphRunner,
415
473
  input: ExternalCodeGraphCommandInput,
@@ -590,6 +648,7 @@ function parseOutline(value: unknown, projectRoot: string, exclude?: string[]):
590
648
  if (nodes.some(node => !node)) return undefined;
591
649
  const byId = new Map<string, ExternalCodeGraphSymbol>();
592
650
  for (const node of nodes as RawExternalNode[]) {
651
+ if (byId.has(node.id)) return undefined;
593
652
  byId.set(node.id, externalSymbol(node));
594
653
  }
595
654
  const entryPoints = value.entryPoints
@@ -609,7 +668,7 @@ function parseOutline(value: unknown, projectRoot: string, exclude?: string[]):
609
668
  if (!source || !target || !kind || (rawEdge.line !== undefined && (!line || line < 1))) return undefined;
610
669
  const from = byId.get(source);
611
670
  const to = byId.get(target);
612
- if (!from || !to) continue;
671
+ if (!from || !to) return undefined;
613
672
  relations.push({ from, to, kind, ...(line ? { line } : {}) });
614
673
  }
615
674
 
@@ -629,12 +688,202 @@ function parseOutline(value: unknown, projectRoot: string, exclude?: string[]):
629
688
  };
630
689
  }
631
690
 
691
+ function parseTextLocation(value: string): { filePath: string; line?: number } | undefined {
692
+ const match = value.trim().match(/^(.*?)(?::([1-9]\d*))?$/);
693
+ if (!match?.[1]) return undefined;
694
+ return {
695
+ filePath: match[1].trim(),
696
+ ...(match[2] ? { line: Number(match[2]) } : {}),
697
+ };
698
+ }
699
+
700
+ function parseExploreText(value: string, projectRoot: string, exclude?: string[]): ExternalCodeGraphOutline | undefined {
701
+ if (!value.trim() || Buffer.byteLength(value, 'utf8') > MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES) return undefined;
702
+
703
+ const entryPoints: RawExternalNode[] = [];
704
+ const nodes: RawExternalNode[] = [];
705
+ const relatedFiles = new Set<string>();
706
+ const byName = new Map<string, RawExternalNode>();
707
+ let section: 'entry' | 'related' | 'relationships' | null = null;
708
+ let relationKind = 'references';
709
+
710
+ const addNode = (node: RawExternalNode, entry: boolean): void => {
711
+ const key = `${node.filePath}\0${node.name}`;
712
+ const existing = byName.get(key);
713
+ if (existing) return;
714
+ byName.set(key, node);
715
+ relatedFiles.add(node.filePath);
716
+ (entry ? entryPoints : nodes).push(node);
717
+ };
718
+
719
+ for (const rawLine of value.split(/\r?\n/)) {
720
+ const line = rawLine.trim();
721
+ const fileHeader = line.match(/^\*\*`([^`]+)`\*\*\s+(?:—|-)\s+(.+)$/);
722
+ if (fileHeader) {
723
+ const filePath = safeRelativePath(projectRoot, fileHeader[1], exclude);
724
+ if (!filePath) return undefined;
725
+ const symbolText = fileHeader[2].split(/\s+·\s+/, 1)[0] ?? '';
726
+ const symbols = [...symbolText.matchAll(/([^,]+?)\(([^()]+)\)/g)];
727
+ if (symbols.length === 0) return undefined;
728
+ const isFirstFile = entryPoints.length === 0 && nodes.length === 0;
729
+ for (const symbol of symbols.slice(0, MAX_EXTERNAL_NODE_COUNT)) {
730
+ const name = symbol[1]?.trim();
731
+ const kind = symbol[2]?.trim();
732
+ if (!name || !kind) return undefined;
733
+ addNode({
734
+ id: `explore:${filePath}:${name}`,
735
+ kind,
736
+ name,
737
+ filePath,
738
+ }, isFirstFile);
739
+ }
740
+ section = null;
741
+ continue;
742
+ }
743
+ if (/^#{2,3}\s+Entry Points?\b/i.test(line)) {
744
+ section = 'entry';
745
+ continue;
746
+ }
747
+ if (/^#{2,3}\s+Related Symbols?\b/i.test(line)) {
748
+ section = 'related';
749
+ continue;
750
+ }
751
+ if (/^\*\*Relationships\*\*$/i.test(line)) {
752
+ section = 'relationships';
753
+ continue;
754
+ }
755
+ if (/^\*\*(?:Source Code|Code)\*\*$/i.test(line) || /^#{2,3}\s+/i.test(line)) {
756
+ section = null;
757
+ continue;
758
+ }
759
+
760
+ if (section === 'entry') {
761
+ const match = line.match(/^-\s+\*\*(.+?)\*\*\s+\(([^)]+)\)\s+-\s+(.+)$/);
762
+ if (!match) continue;
763
+ const location = parseTextLocation(match[3]);
764
+ if (!location) return undefined;
765
+ const filePath = safeRelativePath(projectRoot, location.filePath, exclude);
766
+ if (!filePath) return undefined;
767
+ addNode({
768
+ id: `explore:${filePath}:${match[1]}`,
769
+ kind: match[2],
770
+ name: match[1],
771
+ filePath,
772
+ ...(location.line ? { startLine: location.line, endLine: location.line } : {}),
773
+ }, true);
774
+ continue;
775
+ }
776
+
777
+ if (section === 'related') {
778
+ const match = line.match(/^-\s+([^:]+):\s+(.+)$/);
779
+ if (!match) continue;
780
+ const filePath = safeRelativePath(projectRoot, match[1], exclude);
781
+ if (!filePath) return undefined;
782
+ relatedFiles.add(filePath);
783
+ for (const rawSymbol of match[2].split(',')) {
784
+ const symbol = rawSymbol.trim().match(/^(.+?)(?::([1-9]\d*))?$/);
785
+ if (!symbol?.[1]) return undefined;
786
+ addNode({
787
+ id: `explore:${filePath}:${symbol[1].trim()}`,
788
+ kind: 'unknown',
789
+ name: symbol[1].trim(),
790
+ filePath,
791
+ ...(symbol[2] ? { startLine: Number(symbol[2]), endLine: Number(symbol[2]) } : {}),
792
+ }, false);
793
+ }
794
+ continue;
795
+ }
796
+
797
+ if (section === 'relationships') {
798
+ const kind = line.match(/^\*\*([^*]+):\*\*$/);
799
+ if (kind?.[1]) {
800
+ relationKind = kind[1].trim();
801
+ continue;
802
+ }
803
+ }
804
+ }
805
+
806
+ const bySymbolName = new Map<string, RawExternalNode>();
807
+ for (const node of [...entryPoints, ...nodes]) {
808
+ if (!bySymbolName.has(node.name)) bySymbolName.set(node.name, node);
809
+ }
810
+ const relations: ExternalCodeGraphRelation[] = [];
811
+ if (section === 'relationships') {
812
+ for (const rawLine of value.split(/\r?\n/)) {
813
+ const line = rawLine.trim();
814
+ const match = line.match(/^-\s+(.+?)\s+(?:\u2192|->)\s+(.+)$/);
815
+ if (!match) continue;
816
+ const from = bySymbolName.get(match[1].trim());
817
+ const to = bySymbolName.get(match[2].trim());
818
+ if (!from || !to) continue;
819
+ relations.push({
820
+ from: externalSymbol(from),
821
+ to: externalSymbol(to),
822
+ kind: relationKind,
823
+ });
824
+ }
825
+ } else {
826
+ // The relationship section may be followed by source sections. Re-scan
827
+ // its bounded region so relation parsing does not depend on the final
828
+ // section value after the loop.
829
+ const relationshipStart = value.split(/\r?\n/).findIndex((line) => /^\s*\*\*Relationships\*\*\s*$/i.test(line));
830
+ if (relationshipStart >= 0) {
831
+ const relationshipLines = value.split(/\r?\n/).slice(relationshipStart + 1);
832
+ for (const rawLine of relationshipLines) {
833
+ const line = rawLine.trim();
834
+ if (/^\*\*(?:Source Code|Code)\*\*$/i.test(line) || /^#{2,3}\s+/i.test(line)) break;
835
+ const kind = line.match(/^\*\*([^*]+):\*\*$/);
836
+ if (kind?.[1]) {
837
+ relationKind = kind[1].trim();
838
+ continue;
839
+ }
840
+ const match = line.match(/^-\s+(.+?)\s+(?:\u2192|->)\s+(.+)$/);
841
+ if (!match) continue;
842
+ const from = bySymbolName.get(match[1].trim());
843
+ const to = bySymbolName.get(match[2].trim());
844
+ if (from && to) relations.push({ from: externalSymbol(from), to: externalSymbol(to), kind: relationKind });
845
+ }
846
+ }
847
+ }
848
+
849
+ const allNodes = [...entryPoints, ...nodes];
850
+ if (allNodes.length === 0) return undefined;
851
+ return {
852
+ provider: 'external',
853
+ entryPoints: entryPoints.map(externalSymbol).slice(0, 5),
854
+ relations: relations.slice(0, 6),
855
+ relatedFiles: [...relatedFiles].slice(0, 5),
856
+ stats: {
857
+ nodes: allNodes.length,
858
+ edges: relations.length,
859
+ files: relatedFiles.size,
860
+ },
861
+ };
862
+ }
863
+
864
+ function parseExplorePayload(value: unknown, projectRoot: string, exclude?: string[]): ExternalCodeGraphOutline | undefined {
865
+ if (typeof value === 'string') return parseExploreText(value, projectRoot, exclude);
866
+ if (isRecord(value) && isRecord(value.result)) value = value.result;
867
+ if (isRecord(value) && Array.isArray(value.content)) {
868
+ const text = value.content
869
+ .filter((item): item is Record<string, unknown> => isRecord(item) && item.type === 'text' && typeof item.text === 'string')
870
+ .map(item => item.text as string)
871
+ .join('\n');
872
+ return parseExploreText(text, projectRoot, exclude);
873
+ }
874
+ return parseOutline(value, projectRoot, exclude);
875
+ }
876
+
632
877
  function fallbackCaution(health: ExternalCodeGraphHealth): string | undefined {
633
878
  if (health.state === 'disabled' || health.state === 'not-detected'
634
879
  || health.state === 'not-initialized' || health.state === 'ready') return undefined;
635
880
  return 'External semantic CodeGraph is ' + health.state + '; using Lite structural evidence for this brief.';
636
881
  }
637
882
 
883
+ function queryFallbackCaution(diagnostics: ExternalCodeGraphQueryDiagnostics): string {
884
+ return `External semantic CodeGraph context was unavailable (context: ${diagnostics.context.outcome}; explore: ${diagnostics.explore.outcome}); using Lite structural evidence.`;
885
+ }
886
+
638
887
  export async function getExternalCodeGraphContext(input: ExternalCodeGraphContextInput): Promise<ExternalCodeGraphContextResult> {
639
888
  const inspected = await inspectExternalCodeGraph(input);
640
889
  if (inspected.health.state !== 'ready') {
@@ -642,34 +891,65 @@ export async function getExternalCodeGraphContext(input: ExternalCodeGraphContex
642
891
  }
643
892
 
644
893
  const mode = input.mode ?? 'auto';
645
- const result = await runSafely(input.runner ?? defaultRunner, {
894
+ const runner = input.runner ?? defaultRunner;
895
+ const contextResult = await runSafely(runner, {
646
896
  command: input.command,
647
897
  args: ['context', '--path', input.projectRoot, '--format', 'json', '--max-nodes', '8', '--no-code', input.task],
648
898
  cwd: input.projectRoot,
649
899
  timeoutMs: boundedTimeout(input.timeoutMs),
650
900
  maxOutputBytes: MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES,
651
901
  });
652
- if (!result.ok) {
653
- const health = unavailableHealth(result);
902
+ const diagnostics: ExternalCodeGraphQueryDiagnostics = {
903
+ context: contextResult.ok
904
+ ? commandDiagnostic('context', contextResult, 'success')
905
+ : failedCommandDiagnostic('context', contextResult),
906
+ explore: notAttemptedDiagnostic('explore'),
907
+ };
908
+
909
+ const contextOutline = contextResult.ok
910
+ ? parseOutline(parseJson(contextResult.stdout), input.projectRoot, input.exclude)
911
+ : undefined;
912
+ if (contextOutline && (contextOutline.entryPoints.length > 0 || contextOutline.relations.length > 0 || contextOutline.relatedFiles.length > 0)) {
654
913
  return {
655
- health,
656
- quality: providerQuality({ mode, health }),
657
- caution: fallbackCaution(health),
914
+ ...inspected,
915
+ outline: contextOutline,
916
+ diagnostics,
917
+ quality: providerQuality({ mode, health: inspected.health, selected: 'external' }),
658
918
  };
659
919
  }
660
- const outline = parseOutline(parseJson(result.stdout), input.projectRoot, input.exclude);
661
- if (!outline) {
662
- const health: ExternalCodeGraphHealth = { state: 'invalid', reason: 'The local CodeGraph context payload failed validation.' };
920
+ if (contextResult.ok) diagnostics.context = commandDiagnostic('context', contextResult, 'invalid-output', 'The context payload failed validation or contained no safe outline.');
921
+
922
+ const exploreResult = await runSafely(runner, {
923
+ command: input.command,
924
+ args: ['explore', '--path', input.projectRoot, '--max-files', '4', input.task],
925
+ cwd: input.projectRoot,
926
+ timeoutMs: boundedTimeout(input.timeoutMs),
927
+ maxOutputBytes: MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES,
928
+ });
929
+ diagnostics.explore = exploreResult.ok
930
+ ? commandDiagnostic('explore', exploreResult, 'success')
931
+ : failedCommandDiagnostic('explore', exploreResult);
932
+
933
+ const exploreOutline = exploreResult.ok
934
+ ? parseExplorePayload(parseJson(exploreResult.stdout) ?? exploreResult.stdout, input.projectRoot, input.exclude)
935
+ : undefined;
936
+ if (exploreOutline && (exploreOutline.entryPoints.length > 0 || exploreOutline.relations.length > 0 || exploreOutline.relatedFiles.length > 0)) {
663
937
  return {
664
- health,
665
- quality: providerQuality({ mode, health }),
666
- caution: fallbackCaution(health),
938
+ ...inspected,
939
+ outline: exploreOutline,
940
+ diagnostics,
941
+ quality: providerQuality({ mode, health: inspected.health, selected: 'external' }),
667
942
  };
668
943
  }
669
- const contributes = outline.entryPoints.length > 0 || outline.relations.length > 0 || outline.relatedFiles.length > 0;
944
+ if (exploreResult.ok) diagnostics.explore = commandDiagnostic('explore', exploreResult, 'invalid-output', 'The explore payload failed validation or contained no safe outline.');
945
+
946
+ const health = exploreResult.ok
947
+ ? { state: 'invalid' as const, reason: 'The local CodeGraph context and explore payloads failed validation.' }
948
+ : unavailableHealth(exploreResult);
670
949
  return {
671
- ...inspected,
672
- ...(contributes ? { outline } : {}),
673
- quality: providerQuality({ mode, health: inspected.health, ...(contributes ? { selected: 'external' as const } : {}) }),
950
+ health,
951
+ quality: providerQuality({ mode, health }),
952
+ diagnostics,
953
+ caution: queryFallbackCaution(diagnostics),
674
954
  };
675
955
  }
@@ -28,6 +28,7 @@ import { getGlobalDotenvPath, getProjectDotenvPath } from './config-paths.js';
28
28
 
29
29
  let dotenvLoaded = false;
30
30
  let dotenvProjectRoot: string | null = null;
31
+ let dotenvHomeDir: string | null = null;
31
32
 
32
33
  /** Track which .env files were loaded (for diagnostics) */
33
34
  const loadedEnvFiles: string[] = [];
@@ -65,9 +66,11 @@ interface DotenvLoadOptions {
65
66
  * @param projectRoot - Project root directory (for project-level .env)
66
67
  */
67
68
  export function loadDotenv(projectRoot?: string, options: DotenvLoadOptions = {}): void {
68
- if (dotenvLoaded && dotenvProjectRoot === (projectRoot ?? null)) return;
69
+ const userHomeDir = options.userHomeDir ?? homedir();
70
+ const normalizedProjectRoot = projectRoot ?? null;
71
+ if (dotenvLoaded && dotenvProjectRoot === normalizedProjectRoot && dotenvHomeDir === userHomeDir) return;
69
72
 
70
- if (dotenvLoaded && dotenvProjectRoot !== (projectRoot ?? null)) {
73
+ if (dotenvLoaded && (dotenvProjectRoot !== normalizedProjectRoot || dotenvHomeDir !== userHomeDir)) {
71
74
  resetDotenv();
72
75
  }
73
76
 
@@ -85,10 +88,11 @@ export function loadDotenv(projectRoot?: string, options: DotenvLoadOptions = {}
85
88
 
86
89
  // 2. User-level .env (~/.memorix/.env) — lowest .env priority, load second
87
90
  // (override: false means it only fills in keys not already set)
88
- loadEnvFile(getGlobalDotenvPath(options.userHomeDir ?? homedir()));
91
+ loadEnvFile(getGlobalDotenvPath(userHomeDir));
89
92
 
90
93
  dotenvLoaded = true;
91
- dotenvProjectRoot = projectRoot ?? null;
94
+ dotenvProjectRoot = normalizedProjectRoot;
95
+ dotenvHomeDir = userHomeDir;
92
96
  }
93
97
 
94
98
  /**
@@ -101,6 +105,7 @@ export function resetDotenv(): void {
101
105
  injectedKeys.clear();
102
106
  dotenvLoaded = false;
103
107
  dotenvProjectRoot = null;
108
+ dotenvHomeDir = null;
104
109
  loadedEnvFiles.length = 0;
105
110
  loadedEnvKeys.clear();
106
111
  }
package/src/config.ts CHANGED
@@ -23,13 +23,13 @@ export { loadFileConfig, resetConfigCache, type MemorixConfig } from './config/l
23
23
  // ─── Resolved Getters ────────────────────────────────────────────────
24
24
 
25
25
  /** Memory/background LLM API key: env > memorix.yml > config.json > provider env fallbacks */
26
- export function getLLMApiKey(): string | undefined {
27
- return getResolvedMemoryLane().llm.apiKey;
26
+ export function getLLMApiKey(options: ResolvedLaneOptions = {}): string | undefined {
27
+ return getResolvedMemoryLane(options).llm.apiKey;
28
28
  }
29
29
 
30
30
  /** LLM provider: env > memorix.yml > config.json > auto-detect */
31
- export function getLLMProvider(): string {
32
- const provider = getResolvedMemoryLane().llm.provider;
31
+ export function getLLMProvider(options: ResolvedLaneOptions = {}): string {
32
+ const provider = getResolvedMemoryLane(options).llm.provider;
33
33
  if (provider) return provider;
34
34
  // Auto-detect from env var names
35
35
  if (process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) return 'anthropic';
@@ -38,33 +38,33 @@ export function getLLMProvider(): string {
38
38
  }
39
39
 
40
40
  /** LLM model: env > memorix.yml > config.json > provider default */
41
- export function getLLMModel(providerDefault: string): string {
42
- return getResolvedMemoryLane().llm.model || providerDefault;
41
+ export function getLLMModel(providerDefault: string, options: ResolvedLaneOptions = {}): string {
42
+ return getResolvedMemoryLane(options).llm.model || providerDefault;
43
43
  }
44
44
 
45
45
  /** LLM base URL: env > memorix.yml > config.json > provider default */
46
- export function getLLMBaseUrl(providerDefault: string): string {
47
- return getResolvedMemoryLane().llm.baseUrl || providerDefault;
46
+ export function getLLMBaseUrl(providerDefault: string, options: ResolvedLaneOptions = {}): string {
47
+ return getResolvedMemoryLane(options).llm.baseUrl || providerDefault;
48
48
  }
49
49
 
50
50
  /** TUI/chat agent API key: agent env > agent config > memory LLM fallback */
51
- export function getAgentLLMApiKey(): string | undefined {
52
- return getResolvedAgentLane().apiKey;
51
+ export function getAgentLLMApiKey(options: ResolvedLaneOptions = {}): string | undefined {
52
+ return getResolvedAgentLane(options).apiKey;
53
53
  }
54
54
 
55
55
  /** TUI/chat agent provider: agent env > agent config > memory LLM fallback */
56
- export function getAgentLLMProvider(): string {
57
- return getResolvedAgentLane().provider || getLLMProvider();
56
+ export function getAgentLLMProvider(options: ResolvedLaneOptions = {}): string {
57
+ return getResolvedAgentLane(options).provider || getLLMProvider(options);
58
58
  }
59
59
 
60
60
  /** TUI/chat agent model: agent env > agent config > memory LLM fallback */
61
- export function getAgentLLMModel(providerDefault: string): string {
62
- return getResolvedAgentLane().model || getLLMModel(providerDefault);
61
+ export function getAgentLLMModel(providerDefault: string, options: ResolvedLaneOptions = {}): string {
62
+ return getResolvedAgentLane(options).model || getLLMModel(providerDefault, options);
63
63
  }
64
64
 
65
65
  /** TUI/chat agent base URL: agent env > agent config > memory LLM fallback */
66
- export function getAgentLLMBaseUrl(providerDefault: string): string {
67
- return getResolvedAgentLane().baseUrl || getLLMBaseUrl(providerDefault);
66
+ export function getAgentLLMBaseUrl(providerDefault: string, options: ResolvedLaneOptions = {}): string {
67
+ return getResolvedAgentLane(options).baseUrl || getLLMBaseUrl(providerDefault, options);
68
68
  }
69
69
 
70
70
  /** Embedding mode: env > memorix.yml > config.json > 'off' */
@@ -1,6 +1,6 @@
1
1
  import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
2
 
3
- import { isLLMEnabled } from '../llm/provider.js';
3
+ import { initLLM, isLLMEnabled } from '../llm/provider.js';
4
4
  import { analyzeCleanupObservations, applyCleanupMutations } from '../memory/cleanup.js';
5
5
  import { findConsolidationCandidates, executeConsolidation } from '../memory/consolidation.js';
6
6
  import { applyDeduplicationPlan, planMemoryDeduplication } from '../memory/deduplication.js';
@@ -167,6 +167,9 @@ export async function executeCleanup(
167
167
  }
168
168
 
169
169
  export async function previewDeduplicate(context: DashboardMaintenanceContext) {
170
+ // Dashboard callers do not share the CLI/server bootstrap path. Resolve the
171
+ // memory lane here so standalone and HTTP previews use the selected project.
172
+ initLLM({ scope: 'memory', projectRoot: context.projectRoot });
170
173
  if (!isLLMEnabled()) {
171
174
  return {
172
175
  action: 'deduplicate',
@@ -210,6 +210,10 @@ export type LLMConfigScope = 'memory' | 'agent';
210
210
 
211
211
  export interface InitLLMOptions {
212
212
  scope?: LLMConfigScope;
213
+ /** Resolve project TOML/.env instead of relying on process.cwd(). */
214
+ projectRoot?: string | null;
215
+ /** Override the home directory used for global TOML/.env resolution. */
216
+ homeDir?: string;
213
217
  }
214
218
 
215
219
  /**
@@ -218,20 +222,29 @@ export interface InitLLMOptions {
218
222
  */
219
223
  export function initLLM(options: InitLLMOptions = {}): LLMConfig | null {
220
224
  const scope = options.scope ?? 'memory';
221
- const apiKey = scope === 'agent' ? getAgentLLMApiKey() : getLLMApiKey();
225
+ const configOptions = { projectRoot: options.projectRoot, homeDir: options.homeDir };
226
+ const apiKey = scope === 'agent'
227
+ ? getAgentLLMApiKey(configOptions)
228
+ : getLLMApiKey(configOptions);
222
229
  if (!apiKey) {
223
230
  currentConfig = null;
224
231
  return null;
225
232
  }
226
233
 
227
- const provider = (scope === 'agent' ? getAgentLLMProvider() : getLLMProvider()) as LLMConfig['provider'];
234
+ const provider = (scope === 'agent'
235
+ ? getAgentLLMProvider(configOptions)
236
+ : getLLMProvider(configOptions)) as LLMConfig['provider'];
228
237
  const defaults = PROVIDER_DEFAULTS[provider] ?? PROVIDER_DEFAULTS.openai;
229
238
 
230
239
  currentConfig = {
231
240
  provider,
232
241
  apiKey,
233
- model: scope === 'agent' ? getAgentLLMModel(defaults.model) : getLLMModel(defaults.model),
234
- baseUrl: scope === 'agent' ? getAgentLLMBaseUrl(defaults.baseUrl) : getLLMBaseUrl(defaults.baseUrl),
242
+ model: scope === 'agent'
243
+ ? getAgentLLMModel(defaults.model, configOptions)
244
+ : getLLMModel(defaults.model, configOptions),
245
+ baseUrl: scope === 'agent'
246
+ ? getAgentLLMBaseUrl(defaults.baseUrl, configOptions)
247
+ : getLLMBaseUrl(defaults.baseUrl, configOptions),
235
248
  };
236
249
 
237
250
  return currentConfig;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Small MCP 2026-07-28 discovery adapter for the pinned v1 SDK.
3
+ *
4
+ * The v1 SDK can route an extension request through its low-level Server, but
5
+ * it does not implement the modern discovery/cache negotiation itself. Keep
6
+ * this adapter deliberately narrow: advertise discovery and the existing
7
+ * legacy tool handlers, while leaving unsupported extensions unadvertised.
8
+ */
9
+
10
+ import { RequestSchema, type ServerCapabilities } from '@modelcontextprotocol/sdk/types.js';
11
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { z } from 'zod/v4';
13
+
14
+ import {
15
+ createMcpDiscoverResult,
16
+ MCP_SERVER_DISCOVER_METHOD,
17
+ type McpDiscoverResult,
18
+ type McpServerInfo,
19
+ } from './mcp-discovery.js';
20
+
21
+ export {
22
+ createMcpDiscoverResult,
23
+ getMcpServerInfo,
24
+ MCP_LEGACY_PROTOCOL_VERSION,
25
+ MCP_MODERN_PROTOCOL_VERSION,
26
+ MCP_OLDEST_LEGACY_PROTOCOL_VERSION,
27
+ MCP_SERVER_DISCOVER_METHOD,
28
+ MCP_SERVER_INFO_META_KEY,
29
+ } from './mcp-discovery.js';
30
+ export type { McpDiscoverResult, McpServerInfo } from './mcp-discovery.js';
31
+
32
+ const ServerDiscoverRequestSchema = RequestSchema.extend({
33
+ method: z.literal(MCP_SERVER_DISCOVER_METHOD),
34
+ params: z.looseObject({}).optional(),
35
+ });
36
+
37
+ const registeredServers = new WeakSet<McpServer>();
38
+
39
+ /** Build the wire result without exposing unsupported cache/task features. */
40
+ export function buildMcpDiscoverResult(server: McpServer, serverInfo: McpServerInfo): McpDiscoverResult {
41
+ // SDK 1.30 exposes this method at runtime but marks it private in its
42
+ // declaration file. The discovery response must reflect the capabilities
43
+ // already registered on this exact low-level Server instance.
44
+ const getCapabilities = (server.server as unknown as {
45
+ getCapabilities: () => ServerCapabilities;
46
+ }).getCapabilities.bind(server.server);
47
+ return createMcpDiscoverResult(getCapabilities(), serverInfo);
48
+ }
49
+
50
+ /**
51
+ * Register `server/discover` on the v1 low-level Server.
52
+ *
53
+ * This is idempotent for a single McpServer instance because HTTP/session
54
+ * setup can revisit the registration boundary while reusing an object.
55
+ */
56
+ export function registerMcpDiscovery(server: McpServer, serverInfo: McpServerInfo): void {
57
+ if (registeredServers.has(server)) return;
58
+
59
+ server.server.setRequestHandler(ServerDiscoverRequestSchema, async () => (
60
+ buildMcpDiscoverResult(server, serverInfo)
61
+ ));
62
+ registeredServers.add(server);
63
+ }
@@ -1,7 +1,8 @@
1
1
  /** MCP compatibility diagnostics shared by HTTP and CLI-facing checks. */
2
2
 
3
3
  export const CURRENT_MCP_PROTOCOL_VERSION = '2026-07-28';
4
- export const LEGACY_MCP_PROTOCOL_VERSION = '2024-11-05';
4
+ export const LEGACY_MCP_PROTOCOL_VERSION = '2025-11-25';
5
+ export const OLDEST_LEGACY_MCP_PROTOCOL_VERSION = '2024-11-05';
5
6
  export const MCP_SERVER_NAME = 'io.github.AVIDS2/memorix';
6
7
 
7
8
  export interface McpDiagnostics {
@@ -9,13 +10,17 @@ export interface McpDiagnostics {
9
10
  protocol: {
10
11
  current: string;
11
12
  legacy: string;
13
+ discovery: 'supported';
14
+ versionlessRequests: 'legacy-compatible';
15
+ modernResultMetadata: 'discovery-only';
12
16
  statelessRequests: 'supported' | 'compatibility-only' | 'unsupported';
13
17
  statefulStreamableHttp: 'supported';
14
18
  };
15
19
  capabilities: {
16
20
  tasks: 'supported' | 'unsupported';
17
21
  polling: 'supported' | 'unsupported';
18
- listCache: 'supported';
22
+ subscriptions: 'supported' | 'unsupported';
23
+ listCache: 'supported' | 'unsupported';
19
24
  explicitProjectHandle: 'supported';
20
25
  };
21
26
  }
@@ -27,13 +32,17 @@ export function getMcpDiagnostics(): McpDiagnostics {
27
32
  protocol: {
28
33
  current: CURRENT_MCP_PROTOCOL_VERSION,
29
34
  legacy: LEGACY_MCP_PROTOCOL_VERSION,
30
- statelessRequests: 'supported',
35
+ discovery: 'supported',
36
+ versionlessRequests: 'legacy-compatible',
37
+ modernResultMetadata: 'discovery-only',
38
+ statelessRequests: 'compatibility-only',
31
39
  statefulStreamableHttp: 'supported',
32
40
  },
33
41
  capabilities: {
34
42
  tasks: 'unsupported',
35
43
  polling: 'unsupported',
36
- listCache: 'supported',
44
+ subscriptions: 'unsupported',
45
+ listCache: 'unsupported',
37
46
  explicitProjectHandle: 'supported',
38
47
  },
39
48
  };