memorix 1.2.0 → 1.2.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 +30 -1
- package/README.md +18 -4
- package/README.zh-CN.md +18 -4
- package/TEAM.md +86 -86
- package/dist/cli/index.js +15919 -14055
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1997 -1021
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +8481 -8005
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +30 -1
- package/dist/sdk.d.ts +7 -2
- package/dist/sdk.js +2022 -1024
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +49 -1
- package/dist/types.js.map +1 -1
- package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
- package/docs/API_REFERENCE.md +27 -5
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/DEVELOPMENT.md +4 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +7 -1
- package/docs/dev-log/progress.txt +91 -11
- package/docs/knowledge/workflows/memorix-release.md +57 -0
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/command-guide.ts +192 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +5 -1
- package/src/cli/commands/codegraph.ts +17 -8
- package/src/cli/commands/context.ts +3 -2
- package/src/cli/commands/doctor.ts +4 -2
- package/src/cli/commands/explain.ts +9 -3
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +75 -61
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/identity.ts +116 -0
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +71 -69
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/knowledge.ts +40 -0
- package/src/cli/commands/lock.ts +93 -92
- package/src/cli/commands/memory.ts +58 -21
- package/src/cli/commands/message.ts +123 -118
- package/src/cli/commands/operator-shared.ts +98 -3
- package/src/cli/commands/poll.ts +74 -64
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +135 -121
- package/src/cli/commands/retention.ts +9 -4
- package/src/cli/commands/serve-http.ts +22 -43
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/session.ts +29 -3
- package/src/cli/commands/setup.ts +9 -3
- package/src/cli/commands/skills.ts +124 -119
- package/src/cli/commands/status.ts +4 -3
- package/src/cli/commands/task.ts +193 -184
- package/src/cli/commands/team.ts +14 -10
- package/src/cli/commands/transfer.ts +108 -55
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/identity.ts +89 -0
- package/src/cli/index.ts +96 -19
- package/src/cli/invocation.ts +115 -0
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +41 -18
- package/src/cli/tui/data.ts +23 -44
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/operator-context.ts +60 -0
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/tui/views/MemoryView.tsx +10 -8
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/auto-context.ts +34 -17
- package/src/codegraph/context-pack.ts +1 -0
- package/src/codegraph/current-facts.ts +19 -1
- package/src/codegraph/project-context.ts +2 -0
- package/src/codegraph/task-lens.ts +49 -5
- package/src/compact/engine.ts +26 -10
- package/src/compact/index-format.ts +25 -2
- package/src/compact/token-budget.ts +74 -74
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/server.ts +58 -52
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/hooks/admission.ts +117 -0
- package/src/hooks/handler.ts +98 -91
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/significance-filter.ts +250 -250
- package/src/knowledge/claims.ts +51 -1
- package/src/knowledge/context-assembly.ts +97 -0
- package/src/knowledge/types.ts +1 -0
- package/src/knowledge/workflows.ts +34 -3
- package/src/knowledge/workset.ts +179 -10
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/admission.ts +57 -0
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +21 -0
- package/src/memory/consolidation.ts +13 -2
- package/src/memory/disclosure-policy.ts +140 -135
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/export-import.ts +11 -3
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph-context.ts +8 -2
- package/src/memory/graph-scope.ts +46 -0
- package/src/memory/graph.ts +197 -197
- package/src/memory/observations.ts +162 -4
- package/src/memory/quality-audit.ts +2 -0
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +22 -2
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +5 -2
- package/src/memory/visibility.ts +80 -0
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +378 -340
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +33 -10
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/runtime/control-plane-maintenance.ts +1 -0
- package/src/runtime/isolated-maintenance.ts +1 -0
- package/src/runtime/lifecycle.ts +18 -0
- package/src/runtime/maintenance-jobs.ts +1 -0
- package/src/runtime/maintenance-runner.ts +2 -0
- package/src/runtime/project-maintenance.ts +89 -0
- package/src/sdk.ts +334 -304
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/server.ts +334 -93
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/orama-store.ts +61 -6
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/sqlite-db.ts +23 -1
- package/src/store/sqlite-store.ts +12 -2
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +168 -161
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/types.ts +51 -0
- package/src/wiki/generator.ts +2 -0
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
package/src/store/orama-store.ts
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { create, insert, search, remove, update, count, getByID, type AnyOrama } from '@orama/orama';
|
|
12
|
-
import type { MemorixDocument, SearchOptions, IndexEntry, KnowledgeLayer } from '../types.js';
|
|
12
|
+
import type { MemorixDocument, SearchOptions, IndexEntry, KnowledgeLayer, ObservationReader } from '../types.js';
|
|
13
13
|
import { OBSERVATION_ICONS, type ObservationType } from '../types.js';
|
|
14
14
|
import { resolveKnowledgeLayer } from '../skills/mini-skills.js';
|
|
15
|
+
import { canReadObservation } from '../memory/visibility.js';
|
|
15
16
|
import { getEmbeddingProvider, type EmbeddingProvider } from '../embedding/provider.js';
|
|
16
17
|
import { calculateProjectAffinity, extractProjectKeywords, type AffinityContext, type MemoryContent } from './project-affinity.js';
|
|
17
18
|
import { detectQueryIntent, applyIntentBoost } from '../search/intent-detector.js';
|
|
@@ -175,6 +176,11 @@ async function initializeDb(
|
|
|
175
176
|
source: 'string' as const,
|
|
176
177
|
sourceDetail: 'string' as const,
|
|
177
178
|
valueCategory: 'string' as const,
|
|
179
|
+
admissionState: 'string' as const,
|
|
180
|
+
admissionReason: 'string' as const,
|
|
181
|
+
visibility: 'string' as const,
|
|
182
|
+
createdByAgentId: 'string' as const,
|
|
183
|
+
sharedWithAgentIds: 'string' as const,
|
|
178
184
|
documentType: 'string' as const,
|
|
179
185
|
knowledgeLayer: 'string' as const,
|
|
180
186
|
};
|
|
@@ -390,6 +396,13 @@ export async function hydrateIndex(
|
|
|
390
396
|
lastAccessedAt: obs.lastAccessedAt || '',
|
|
391
397
|
status: obs.status ?? 'active',
|
|
392
398
|
source: obs.source || 'agent',
|
|
399
|
+
sourceDetail: obs.sourceDetail ?? '',
|
|
400
|
+
valueCategory: obs.valueCategory ?? '',
|
|
401
|
+
admissionState: obs.admissionState ?? '',
|
|
402
|
+
admissionReason: obs.admissionReason ?? '',
|
|
403
|
+
visibility: obs.visibility ?? 'project',
|
|
404
|
+
createdByAgentId: obs.createdByAgentId ?? '',
|
|
405
|
+
sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
|
|
393
406
|
documentType: 'observation',
|
|
394
407
|
knowledgeLayer: resolveKnowledgeLayer('observation', obs.sourceDetail, obs.source),
|
|
395
408
|
...(compatibleVector ? { embedding: compatibleVector } : {}),
|
|
@@ -443,6 +456,26 @@ export async function insertObservation(doc: MemorixDocument): Promise<void> {
|
|
|
443
456
|
rememberObservationDoc(doc);
|
|
444
457
|
}
|
|
445
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Update retrieval metadata without rebuilding an embedding. Admission is
|
|
461
|
+
* deliberately metadata-only: promotion must not create another embedding
|
|
462
|
+
* request or delay a background qualification job.
|
|
463
|
+
*/
|
|
464
|
+
export async function updateObservationMetadata(
|
|
465
|
+
projectId: string,
|
|
466
|
+
observationId: number,
|
|
467
|
+
patch: Partial<Pick<MemorixDocument, 'admissionState' | 'admissionReason' | 'visibility' | 'createdByAgentId' | 'sharedWithAgentIds'>>,
|
|
468
|
+
): Promise<boolean> {
|
|
469
|
+
const database = await getDb();
|
|
470
|
+
const id = makeOramaObservationId(projectId, observationId);
|
|
471
|
+
const existing = getByID(database, id) as MemorixDocument | undefined;
|
|
472
|
+
if (!existing) return false;
|
|
473
|
+
const next = { ...existing, ...patch };
|
|
474
|
+
await update(database, id, next);
|
|
475
|
+
rememberObservationDoc(next);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
446
479
|
/**
|
|
447
480
|
* Remove an observation document by its Orama internal ID.
|
|
448
481
|
*/
|
|
@@ -643,6 +676,9 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
643
676
|
const doc = hit.document as unknown as MemorixDocument;
|
|
644
677
|
return projectIds.includes(doc.projectId);
|
|
645
678
|
})
|
|
679
|
+
// Visibility is enforced before scoring/ranking so private records cannot
|
|
680
|
+
// affect result order, fallback behavior, or downstream detail reads.
|
|
681
|
+
.filter((hit) => canReadObservation(hit.document as unknown as MemorixDocument, options.reader))
|
|
646
682
|
// Post-filter by status (active/resolved/archived)
|
|
647
683
|
.filter((hit) => {
|
|
648
684
|
if (statusFilter === 'all') return true;
|
|
@@ -682,6 +718,8 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
682
718
|
source: (doc.source || 'agent') as 'agent' | 'git' | 'manual',
|
|
683
719
|
sourceDetail: (doc.sourceDetail || undefined) as 'explicit' | 'hook' | 'git-ingest' | undefined,
|
|
684
720
|
valueCategory: (doc.valueCategory || undefined) as 'core' | 'contextual' | 'ephemeral' | undefined,
|
|
721
|
+
admissionState: (doc.admissionState || undefined) as IndexEntry['admissionState'],
|
|
722
|
+
visibility: (doc.visibility || undefined) as IndexEntry['visibility'],
|
|
685
723
|
entityName: doc.entityName || undefined,
|
|
686
724
|
documentType: (doc.documentType || 'observation') as 'observation' | 'mini-skill',
|
|
687
725
|
knowledgeLayer: (doc.knowledgeLayer || 'project-truth') as KnowledgeLayer,
|
|
@@ -752,6 +790,16 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
752
790
|
}));
|
|
753
791
|
}
|
|
754
792
|
|
|
793
|
+
// Automatic capture is useful as an audit trail, but unqualified evidence
|
|
794
|
+
// should not crowd out durable project knowledge in ordinary retrieval.
|
|
795
|
+
// Keep it available as a fallback when it is all the project has.
|
|
796
|
+
if (hasQuery) {
|
|
797
|
+
const qualifiedEntries = intermediate.filter(
|
|
798
|
+
(entry) => entry.admissionState !== 'candidate' && entry.admissionState !== 'ephemeral',
|
|
799
|
+
);
|
|
800
|
+
if (qualifiedEntries.length > 0) intermediate = qualifiedEntries;
|
|
801
|
+
}
|
|
802
|
+
|
|
755
803
|
// Re-sort: chronological for WHEN queries, relevance for others
|
|
756
804
|
if (intentResult?.preferChronological) {
|
|
757
805
|
intermediate.sort((a, b) => new Date(b.rawTime).getTime() - new Date(a.rawTime).getTime());
|
|
@@ -960,7 +1008,8 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
960
1008
|
let entries: IndexEntry[] = intermediate.map(({ rawTime: _, _isCommandLog: _c, ...rest }: any) => rest);
|
|
961
1009
|
|
|
962
1010
|
for (const hit of results.hits) {
|
|
963
|
-
|
|
1011
|
+
const doc = hit.document as unknown as MemorixDocument;
|
|
1012
|
+
if (canReadObservation(doc, options.reader)) rememberObservationDoc(doc);
|
|
964
1013
|
}
|
|
965
1014
|
|
|
966
1015
|
// Explainable recall: annotate entries with match reasons (O(1) lookup via Map)
|
|
@@ -1013,7 +1062,10 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
1013
1062
|
|
|
1014
1063
|
// Record access for returned results (fire-and-forget, non-blocking).
|
|
1015
1064
|
if (options.trackAccess !== false) {
|
|
1016
|
-
const
|
|
1065
|
+
const returnedKeys = new Set(entries.map((entry) => makeEntryKey(entry.projectId, entry.id)));
|
|
1066
|
+
const hitDocs = results.hits
|
|
1067
|
+
.map((hit) => ({ id: hit.id, doc: hit.document as unknown as MemorixDocument }))
|
|
1068
|
+
.filter(({ doc }) => returnedKeys.has(makeEntryKey(doc.projectId, doc.observationId)));
|
|
1017
1069
|
recordAccessBatch(hitDocs).catch(() => {});
|
|
1018
1070
|
}
|
|
1019
1071
|
|
|
@@ -1071,6 +1123,7 @@ export async function getTimeline(
|
|
|
1071
1123
|
projectId?: string,
|
|
1072
1124
|
depthBefore = 3,
|
|
1073
1125
|
depthAfter = 3,
|
|
1126
|
+
reader?: ObservationReader,
|
|
1074
1127
|
): Promise<{ before: IndexEntry[]; anchor: IndexEntry | null; after: IndexEntry[] }> {
|
|
1075
1128
|
// Use in-memory observations for reliable lookup
|
|
1076
1129
|
// (Orama search with empty term is unreliable — same fix as compactDetail)
|
|
@@ -1079,9 +1132,9 @@ export async function getTimeline(
|
|
|
1079
1132
|
const rawObs = await withFreshIndex(() => getAllObservations());
|
|
1080
1133
|
|
|
1081
1134
|
// Filter by project if specified — prevents cross-project context leaking
|
|
1082
|
-
const allObs = projectId
|
|
1135
|
+
const allObs = (projectId
|
|
1083
1136
|
? rawObs.filter((o) => o.projectId === projectId)
|
|
1084
|
-
: rawObs;
|
|
1137
|
+
: rawObs).filter((observation) => canReadObservation(observation, reader));
|
|
1085
1138
|
|
|
1086
1139
|
// Sort by creation time
|
|
1087
1140
|
const sorted = allObs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
@@ -1093,7 +1146,7 @@ export async function getTimeline(
|
|
|
1093
1146
|
|
|
1094
1147
|
const toIndexEntry = (obs: {
|
|
1095
1148
|
id: number; type: string; title: string; tokens: number; createdAt: string;
|
|
1096
|
-
source?: string; sourceDetail?: string; valueCategory?: string;
|
|
1149
|
+
source?: string; sourceDetail?: string; valueCategory?: string; admissionState?: string; visibility?: string;
|
|
1097
1150
|
}): IndexEntry => {
|
|
1098
1151
|
const obsType = obs.type as ObservationType;
|
|
1099
1152
|
return {
|
|
@@ -1106,6 +1159,8 @@ export async function getTimeline(
|
|
|
1106
1159
|
source: (obs.source as IndexEntry['source']) || undefined,
|
|
1107
1160
|
sourceDetail: (obs.sourceDetail as IndexEntry['sourceDetail']) || undefined,
|
|
1108
1161
|
valueCategory: (obs.valueCategory as IndexEntry['valueCategory']) || undefined,
|
|
1162
|
+
admissionState: (obs.admissionState as IndexEntry['admissionState']) || undefined,
|
|
1163
|
+
visibility: (obs.visibility as IndexEntry['visibility']) || undefined,
|
|
1109
1164
|
};
|
|
1110
1165
|
};
|
|
1111
1166
|
|
|
@@ -1,212 +1,212 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JSON Persistence Helpers — Migration / Export / Debug Only
|
|
3
|
-
*
|
|
4
|
-
* These functions read/write JSON/JSONL files for one-time migration
|
|
5
|
-
* from legacy storage formats into SQLite, or for export/import and debug.
|
|
6
|
-
*
|
|
7
|
-
* NOT used as runtime canonical store — SQLite is the sole canonical backend.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { promises as fs } from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import { atomicWriteFile } from './file-lock.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Get the file path for the knowledge graph JSONL file.
|
|
16
|
-
* (MCP-compatible format, same as official Memory Server)
|
|
17
|
-
*/
|
|
18
|
-
export function getGraphFilePath(projectDir: string): string {
|
|
19
|
-
return path.join(projectDir, 'graph.jsonl');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Save the knowledge graph in JSONL format (MCP-compatible).
|
|
24
|
-
* Each line is a JSON object with type: "entity" or "relation".
|
|
25
|
-
*
|
|
26
|
-
* Format adopted from MCP Official Memory Server.
|
|
27
|
-
*/
|
|
28
|
-
export async function saveGraphJsonl(
|
|
29
|
-
projectDir: string,
|
|
30
|
-
entities: Array<{ name: string; entityType: string; observations: string[] }>,
|
|
31
|
-
relations: Array<{ from: string; to: string; relationType: string }>,
|
|
32
|
-
): Promise<void> {
|
|
33
|
-
const lines = [
|
|
34
|
-
...entities.map((e) =>
|
|
35
|
-
JSON.stringify({ type: 'entity', name: e.name, entityType: e.entityType, observations: e.observations }),
|
|
36
|
-
),
|
|
37
|
-
...relations.map((r) =>
|
|
38
|
-
JSON.stringify({ type: 'relation', from: r.from, to: r.to, relationType: r.relationType }),
|
|
39
|
-
),
|
|
40
|
-
];
|
|
41
|
-
await atomicWriteFile(getGraphFilePath(projectDir), lines.join('\n'));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Load the knowledge graph from JSONL format.
|
|
46
|
-
*/
|
|
47
|
-
export async function loadGraphJsonl(
|
|
48
|
-
projectDir: string,
|
|
49
|
-
): Promise<{
|
|
50
|
-
entities: Array<{ name: string; entityType: string; observations: string[] }>;
|
|
51
|
-
relations: Array<{ from: string; to: string; relationType: string }>;
|
|
52
|
-
}> {
|
|
53
|
-
const filePath = getGraphFilePath(projectDir);
|
|
54
|
-
try {
|
|
55
|
-
const data = await fs.readFile(filePath, 'utf-8');
|
|
56
|
-
const lines = data.split('\n').filter((line) => line.trim() !== '');
|
|
57
|
-
return lines.reduce(
|
|
58
|
-
(graph, line) => {
|
|
59
|
-
const item = JSON.parse(line);
|
|
60
|
-
if (item.type === 'entity') {
|
|
61
|
-
graph.entities.push({
|
|
62
|
-
name: item.name,
|
|
63
|
-
entityType: item.entityType,
|
|
64
|
-
observations: item.observations,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
if (item.type === 'relation') {
|
|
68
|
-
graph.relations.push({
|
|
69
|
-
from: item.from,
|
|
70
|
-
to: item.to,
|
|
71
|
-
relationType: item.relationType,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
return graph;
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
entities: [] as Array<{ name: string; entityType: string; observations: string[] }>,
|
|
78
|
-
relations: [] as Array<{ from: string; to: string; relationType: string }>
|
|
79
|
-
},
|
|
80
|
-
);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
83
|
-
return { entities: [], relations: [] };
|
|
84
|
-
}
|
|
85
|
-
throw error;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Save observation data as JSON (for Orama restore / export).
|
|
91
|
-
*/
|
|
92
|
-
export async function saveObservationsJson(
|
|
93
|
-
projectDir: string,
|
|
94
|
-
observations: unknown[],
|
|
95
|
-
): Promise<void> {
|
|
96
|
-
const filePath = path.join(projectDir, 'observations.json');
|
|
97
|
-
await atomicWriteFile(filePath, JSON.stringify(observations, null, 2));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Load observation data from JSON.
|
|
102
|
-
*/
|
|
103
|
-
export async function loadObservationsJson(projectDir: string): Promise<unknown[]> {
|
|
104
|
-
const filePath = path.join(projectDir, 'observations.json');
|
|
105
|
-
try {
|
|
106
|
-
const data = await fs.readFile(filePath, 'utf-8');
|
|
107
|
-
return JSON.parse(data);
|
|
108
|
-
} catch (error) {
|
|
109
|
-
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
110
|
-
return [];
|
|
111
|
-
}
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Save the next observation ID counter (legacy JSON format).
|
|
118
|
-
*/
|
|
119
|
-
export async function saveIdCounter(projectDir: string, nextId: number): Promise<void> {
|
|
120
|
-
const filePath = path.join(projectDir, 'counter.json');
|
|
121
|
-
await atomicWriteFile(filePath, JSON.stringify({ nextId }));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Load the next observation ID counter (legacy JSON format).
|
|
126
|
-
* For runtime use, prefer the SQLite meta table via SqliteBackend.
|
|
127
|
-
*/
|
|
128
|
-
export async function loadIdCounter(projectDir: string): Promise<number> {
|
|
129
|
-
const filePath = path.join(projectDir, 'counter.json');
|
|
130
|
-
try {
|
|
131
|
-
const data = await fs.readFile(filePath, 'utf-8');
|
|
132
|
-
return JSON.parse(data).nextId ?? 1;
|
|
133
|
-
} catch {
|
|
134
|
-
return 1;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Save mini-skills data as JSON (migration source only).
|
|
140
|
-
*/
|
|
141
|
-
export async function saveMiniSkillsJson(
|
|
142
|
-
projectDir: string,
|
|
143
|
-
skills: unknown[],
|
|
144
|
-
): Promise<void> {
|
|
145
|
-
const filePath = path.join(projectDir, 'mini-skills.json');
|
|
146
|
-
await atomicWriteFile(filePath, JSON.stringify(skills, null, 2));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Load mini-skills data from JSON (migration source only).
|
|
151
|
-
*/
|
|
152
|
-
export async function loadMiniSkillsJson(projectDir: string): Promise<unknown[]> {
|
|
153
|
-
const filePath = path.join(projectDir, 'mini-skills.json');
|
|
154
|
-
try {
|
|
155
|
-
const data = await fs.readFile(filePath, 'utf-8');
|
|
156
|
-
const parsed = JSON.parse(data);
|
|
157
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
158
|
-
} catch (error) {
|
|
159
|
-
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
160
|
-
return [];
|
|
161
|
-
}
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Load the mini-skills ID counter (legacy JSON format).
|
|
168
|
-
*/
|
|
169
|
-
export async function loadMiniSkillsCounter(projectDir: string): Promise<number> {
|
|
170
|
-
const filePath = path.join(projectDir, 'mini-skills-counter.json');
|
|
171
|
-
try {
|
|
172
|
-
const data = await fs.readFile(filePath, 'utf-8');
|
|
173
|
-
return JSON.parse(data).nextId ?? 1;
|
|
174
|
-
} catch {
|
|
175
|
-
return 1;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Save the mini-skills ID counter (legacy JSON format).
|
|
181
|
-
*/
|
|
182
|
-
export async function saveMiniSkillsCounter(projectDir: string, nextId: number): Promise<void> {
|
|
183
|
-
const filePath = path.join(projectDir, 'mini-skills-counter.json');
|
|
184
|
-
await atomicWriteFile(filePath, JSON.stringify({ nextId }));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Save sessions data as JSON (migration source only).
|
|
189
|
-
*/
|
|
190
|
-
export async function saveSessionsJson(
|
|
191
|
-
projectDir: string,
|
|
192
|
-
sessions: unknown[],
|
|
193
|
-
): Promise<void> {
|
|
194
|
-
const filePath = path.join(projectDir, 'sessions.json');
|
|
195
|
-
await atomicWriteFile(filePath, JSON.stringify(sessions, null, 2));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Load sessions data from JSON (migration source only).
|
|
200
|
-
*/
|
|
201
|
-
export async function loadSessionsJson(projectDir: string): Promise<unknown[]> {
|
|
202
|
-
const filePath = path.join(projectDir, 'sessions.json');
|
|
203
|
-
try {
|
|
204
|
-
const data = await fs.readFile(filePath, 'utf-8');
|
|
205
|
-
return JSON.parse(data);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
208
|
-
return [];
|
|
209
|
-
}
|
|
210
|
-
throw error;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* JSON Persistence Helpers — Migration / Export / Debug Only
|
|
3
|
+
*
|
|
4
|
+
* These functions read/write JSON/JSONL files for one-time migration
|
|
5
|
+
* from legacy storage formats into SQLite, or for export/import and debug.
|
|
6
|
+
*
|
|
7
|
+
* NOT used as runtime canonical store — SQLite is the sole canonical backend.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { promises as fs } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { atomicWriteFile } from './file-lock.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the file path for the knowledge graph JSONL file.
|
|
16
|
+
* (MCP-compatible format, same as official Memory Server)
|
|
17
|
+
*/
|
|
18
|
+
export function getGraphFilePath(projectDir: string): string {
|
|
19
|
+
return path.join(projectDir, 'graph.jsonl');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Save the knowledge graph in JSONL format (MCP-compatible).
|
|
24
|
+
* Each line is a JSON object with type: "entity" or "relation".
|
|
25
|
+
*
|
|
26
|
+
* Format adopted from MCP Official Memory Server.
|
|
27
|
+
*/
|
|
28
|
+
export async function saveGraphJsonl(
|
|
29
|
+
projectDir: string,
|
|
30
|
+
entities: Array<{ name: string; entityType: string; observations: string[] }>,
|
|
31
|
+
relations: Array<{ from: string; to: string; relationType: string }>,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const lines = [
|
|
34
|
+
...entities.map((e) =>
|
|
35
|
+
JSON.stringify({ type: 'entity', name: e.name, entityType: e.entityType, observations: e.observations }),
|
|
36
|
+
),
|
|
37
|
+
...relations.map((r) =>
|
|
38
|
+
JSON.stringify({ type: 'relation', from: r.from, to: r.to, relationType: r.relationType }),
|
|
39
|
+
),
|
|
40
|
+
];
|
|
41
|
+
await atomicWriteFile(getGraphFilePath(projectDir), lines.join('\n'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load the knowledge graph from JSONL format.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadGraphJsonl(
|
|
48
|
+
projectDir: string,
|
|
49
|
+
): Promise<{
|
|
50
|
+
entities: Array<{ name: string; entityType: string; observations: string[] }>;
|
|
51
|
+
relations: Array<{ from: string; to: string; relationType: string }>;
|
|
52
|
+
}> {
|
|
53
|
+
const filePath = getGraphFilePath(projectDir);
|
|
54
|
+
try {
|
|
55
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
56
|
+
const lines = data.split('\n').filter((line) => line.trim() !== '');
|
|
57
|
+
return lines.reduce(
|
|
58
|
+
(graph, line) => {
|
|
59
|
+
const item = JSON.parse(line);
|
|
60
|
+
if (item.type === 'entity') {
|
|
61
|
+
graph.entities.push({
|
|
62
|
+
name: item.name,
|
|
63
|
+
entityType: item.entityType,
|
|
64
|
+
observations: item.observations,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (item.type === 'relation') {
|
|
68
|
+
graph.relations.push({
|
|
69
|
+
from: item.from,
|
|
70
|
+
to: item.to,
|
|
71
|
+
relationType: item.relationType,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return graph;
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
entities: [] as Array<{ name: string; entityType: string; observations: string[] }>,
|
|
78
|
+
relations: [] as Array<{ from: string; to: string; relationType: string }>
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
83
|
+
return { entities: [], relations: [] };
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Save observation data as JSON (for Orama restore / export).
|
|
91
|
+
*/
|
|
92
|
+
export async function saveObservationsJson(
|
|
93
|
+
projectDir: string,
|
|
94
|
+
observations: unknown[],
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const filePath = path.join(projectDir, 'observations.json');
|
|
97
|
+
await atomicWriteFile(filePath, JSON.stringify(observations, null, 2));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Load observation data from JSON.
|
|
102
|
+
*/
|
|
103
|
+
export async function loadObservationsJson(projectDir: string): Promise<unknown[]> {
|
|
104
|
+
const filePath = path.join(projectDir, 'observations.json');
|
|
105
|
+
try {
|
|
106
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
107
|
+
return JSON.parse(data);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Save the next observation ID counter (legacy JSON format).
|
|
118
|
+
*/
|
|
119
|
+
export async function saveIdCounter(projectDir: string, nextId: number): Promise<void> {
|
|
120
|
+
const filePath = path.join(projectDir, 'counter.json');
|
|
121
|
+
await atomicWriteFile(filePath, JSON.stringify({ nextId }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load the next observation ID counter (legacy JSON format).
|
|
126
|
+
* For runtime use, prefer the SQLite meta table via SqliteBackend.
|
|
127
|
+
*/
|
|
128
|
+
export async function loadIdCounter(projectDir: string): Promise<number> {
|
|
129
|
+
const filePath = path.join(projectDir, 'counter.json');
|
|
130
|
+
try {
|
|
131
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
132
|
+
return JSON.parse(data).nextId ?? 1;
|
|
133
|
+
} catch {
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Save mini-skills data as JSON (migration source only).
|
|
140
|
+
*/
|
|
141
|
+
export async function saveMiniSkillsJson(
|
|
142
|
+
projectDir: string,
|
|
143
|
+
skills: unknown[],
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const filePath = path.join(projectDir, 'mini-skills.json');
|
|
146
|
+
await atomicWriteFile(filePath, JSON.stringify(skills, null, 2));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Load mini-skills data from JSON (migration source only).
|
|
151
|
+
*/
|
|
152
|
+
export async function loadMiniSkillsJson(projectDir: string): Promise<unknown[]> {
|
|
153
|
+
const filePath = path.join(projectDir, 'mini-skills.json');
|
|
154
|
+
try {
|
|
155
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
156
|
+
const parsed = JSON.parse(data);
|
|
157
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Load the mini-skills ID counter (legacy JSON format).
|
|
168
|
+
*/
|
|
169
|
+
export async function loadMiniSkillsCounter(projectDir: string): Promise<number> {
|
|
170
|
+
const filePath = path.join(projectDir, 'mini-skills-counter.json');
|
|
171
|
+
try {
|
|
172
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
173
|
+
return JSON.parse(data).nextId ?? 1;
|
|
174
|
+
} catch {
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Save the mini-skills ID counter (legacy JSON format).
|
|
181
|
+
*/
|
|
182
|
+
export async function saveMiniSkillsCounter(projectDir: string, nextId: number): Promise<void> {
|
|
183
|
+
const filePath = path.join(projectDir, 'mini-skills-counter.json');
|
|
184
|
+
await atomicWriteFile(filePath, JSON.stringify({ nextId }));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Save sessions data as JSON (migration source only).
|
|
189
|
+
*/
|
|
190
|
+
export async function saveSessionsJson(
|
|
191
|
+
projectDir: string,
|
|
192
|
+
sessions: unknown[],
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
const filePath = path.join(projectDir, 'sessions.json');
|
|
195
|
+
await atomicWriteFile(filePath, JSON.stringify(sessions, null, 2));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Load sessions data from JSON (migration source only).
|
|
200
|
+
*/
|
|
201
|
+
export async function loadSessionsJson(projectDir: string): Promise<unknown[]> {
|
|
202
|
+
const filePath = path.join(projectDir, 'sessions.json');
|
|
203
|
+
try {
|
|
204
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
205
|
+
return JSON.parse(data);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|