memorix 1.8.0 → 1.8.2
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 +29 -0
- package/dist/cli/index.js +407 -130
- package/dist/dashboard/static/app.js +37 -23
- package/dist/dashboard/static/index.html +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +633 -391
- 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 +29 -0
- package/dist/sdk.js +625 -390
- 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 +10 -8
- package/docs/API_REFERENCE.md +12 -8
- package/docs/ARCHITECTURE.md +3 -1
- package/docs/CONFIGURATION.md +2 -2
- package/docs/DASHBOARD.md +6 -0
- package/docs/DEVELOPMENT.md +2 -2
- package/docs/README.md +14 -8
- package/package.json +1 -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/dashboard.ts +1 -1
- package/src/cli/commands/serve-http.ts +38 -3
- 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/dashboard/server.ts +85 -86
- package/src/dashboard/static/index.html +1 -1
- package/src/llm/provider.ts +17 -4
- package/src/server/mcp-compat.ts +82 -0
- package/src/server/mcp-diagnostics.ts +13 -4
- package/src/server.ts +9 -6
|
@@ -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',
|