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.
- package/CHANGELOG.md +36 -0
- package/dist/cli/index.js +577 -58
- package/dist/index.js +832 -321
- package/dist/maintenance-runner.js +31 -25
- package/dist/media-audio-runner.js +31 -25
- package/dist/media-video-runner.js +31 -25
- package/dist/memcode-runtime/CHANGELOG.md +36 -0
- package/dist/sdk.js +585 -315
- package/dist/vector-backfill-runner.js +31 -25
- package/docs/1.2.0-PROVIDER-QUALITY.md +15 -6
- package/docs/1.3.3-MULTIMODAL-MEDIA-AND-MCP-COMPAT.md +7 -6
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +13 -8
- package/docs/API_REFERENCE.md +14 -11
- package/docs/CONFIGURATION.md +2 -2
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/README.md +13 -7
- package/package.json +2 -1
- package/plugins/antigravity/memorix/plugin.json +2 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +3 -3
- package/src/cli/commands/serve-http.ts +10 -2
- package/src/cli/commands/serve.ts +26 -7
- package/src/codegraph/external-provider.ts +299 -19
- package/src/config/dotenv-loader.ts +9 -4
- package/src/config.ts +16 -16
- package/src/dashboard/maintenance.ts +4 -1
- package/src/llm/provider.ts +17 -4
- package/src/server/mcp-compat.ts +63 -0
- package/src/server/mcp-diagnostics.ts +13 -4
- package/src/server/mcp-discovery.ts +61 -0
- package/src/server/stdio-startup-gate.ts +263 -0
- 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
|
-
//
|
|
18
|
-
export const DEFAULT_EXTERNAL_CODEGRAPH_TIMEOUT_MS =
|
|
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)
|
|
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
|
|
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
|
-
|
|
653
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
914
|
+
...inspected,
|
|
915
|
+
outline: contextOutline,
|
|
916
|
+
diagnostics,
|
|
917
|
+
quality: providerQuality({ mode, health: inspected.health, selected: 'external' }),
|
|
658
918
|
};
|
|
659
919
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
938
|
+
...inspected,
|
|
939
|
+
outline: exploreOutline,
|
|
940
|
+
diagnostics,
|
|
941
|
+
quality: providerQuality({ mode, health: inspected.health, selected: 'external' }),
|
|
667
942
|
};
|
|
668
943
|
}
|
|
669
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
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 !==
|
|
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(
|
|
91
|
+
loadEnvFile(getGlobalDotenvPath(userHomeDir));
|
|
89
92
|
|
|
90
93
|
dotenvLoaded = true;
|
|
91
|
-
dotenvProjectRoot =
|
|
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',
|
package/src/llm/provider.ts
CHANGED
|
@@ -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
|
|
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'
|
|
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'
|
|
234
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
subscriptions: 'unsupported',
|
|
45
|
+
listCache: 'unsupported',
|
|
37
46
|
explicitProjectHandle: 'supported',
|
|
38
47
|
},
|
|
39
48
|
};
|