@linxiraos/pi-mnemopi 1.0.0
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 +248 -0
- package/README.md +108 -0
- package/dist/types/cli.d.ts +35 -0
- package/dist/types/config.d.ts +108 -0
- package/dist/types/core/aaak.d.ts +55 -0
- package/dist/types/core/annotations.d.ts +75 -0
- package/dist/types/core/banks.d.ts +33 -0
- package/dist/types/core/beam/consolidate.d.ts +38 -0
- package/dist/types/core/beam/helpers.d.ts +69 -0
- package/dist/types/core/beam/index.d.ts +59 -0
- package/dist/types/core/beam/recall.d.ts +52 -0
- package/dist/types/core/beam/schema.d.ts +2 -0
- package/dist/types/core/beam/store.d.ts +57 -0
- package/dist/types/core/beam/types.d.ts +268 -0
- package/dist/types/core/binary-vectors.d.ts +54 -0
- package/dist/types/core/chat-normalize.d.ts +13 -0
- package/dist/types/core/content-sanitizer.d.ts +18 -0
- package/dist/types/core/cost-log.d.ts +13 -0
- package/dist/types/core/embeddings.d.ts +93 -0
- package/dist/types/core/entities.d.ts +9 -0
- package/dist/types/core/episodic-graph.d.ts +89 -0
- package/dist/types/core/extraction/client.d.ts +35 -0
- package/dist/types/core/extraction/diagnostics.d.ts +51 -0
- package/dist/types/core/extraction/prompts.d.ts +2 -0
- package/dist/types/core/extraction.d.ts +34 -0
- package/dist/types/core/fastembed-model-cache.d.ts +2 -0
- package/dist/types/core/fastembed-runtime.d.ts +41 -0
- package/dist/types/core/index.d.ts +6 -0
- package/dist/types/core/llm-backends.d.ts +23 -0
- package/dist/types/core/local-llm.d.ts +20 -0
- package/dist/types/core/memory.d.ts +186 -0
- package/dist/types/core/migrations/e6-triplestore-split.d.ts +17 -0
- package/dist/types/core/migrations/index.d.ts +1 -0
- package/dist/types/core/mmr.d.ts +8 -0
- package/dist/types/core/orchestrator.d.ts +20 -0
- package/dist/types/core/patterns.d.ts +61 -0
- package/dist/types/core/plugins.d.ts +109 -0
- package/dist/types/core/polyphonic-recall.d.ts +66 -0
- package/dist/types/core/query-cache.d.ts +45 -0
- package/dist/types/core/query-intent.d.ts +20 -0
- package/dist/types/core/recall-diagnostics.d.ts +48 -0
- package/dist/types/core/runtime-options.d.ts +76 -0
- package/dist/types/core/shmr.d.ts +62 -0
- package/dist/types/core/streaming.d.ts +136 -0
- package/dist/types/core/synonyms.d.ts +46 -0
- package/dist/types/core/temporal-parser.d.ts +16 -0
- package/dist/types/core/token-counter.d.ts +8 -0
- package/dist/types/core/triples.d.ts +63 -0
- package/dist/types/core/typed-memory.d.ts +39 -0
- package/dist/types/core/vector-index.d.ts +16 -0
- package/dist/types/core/vector-math.d.ts +1 -0
- package/dist/types/core/veracity-consolidation.d.ts +60 -0
- package/dist/types/core/weibull.d.ts +96 -0
- package/dist/types/db.d.ts +18 -0
- package/dist/types/diagnose.d.ts +24 -0
- package/dist/types/dr/index.d.ts +1 -0
- package/dist/types/dr/recovery.d.ts +68 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/mcp-server.d.ts +40 -0
- package/dist/types/mcp-tools.d.ts +484 -0
- package/dist/types/migrations/e6-triplestore-split.d.ts +1 -0
- package/dist/types/migrations/index.d.ts +1 -0
- package/dist/types/types.d.ts +145 -0
- package/dist/types/util/datetime.d.ts +8 -0
- package/dist/types/util/env.d.ts +10 -0
- package/dist/types/util/ids.d.ts +3 -0
- package/dist/types/util/lru.d.ts +12 -0
- package/dist/types/util/regex.d.ts +11 -0
- package/package.json +108 -0
- package/src/cli.ts +398 -0
- package/src/config.ts +373 -0
- package/src/core/aaak.ts +142 -0
- package/src/core/annotations.ts +456 -0
- package/src/core/banks.ts +133 -0
- package/src/core/beam/consolidate.ts +1132 -0
- package/src/core/beam/helpers.ts +831 -0
- package/src/core/beam/index.ts +358 -0
- package/src/core/beam/recall.ts +1225 -0
- package/src/core/beam/schema.ts +426 -0
- package/src/core/beam/store.ts +1083 -0
- package/src/core/beam/types.ts +303 -0
- package/src/core/binary-vectors.ts +317 -0
- package/src/core/chat-normalize.ts +160 -0
- package/src/core/content-sanitizer.ts +136 -0
- package/src/core/cost-log.ts +101 -0
- package/src/core/embeddings.ts +624 -0
- package/src/core/entities.ts +263 -0
- package/src/core/episodic-graph.ts +708 -0
- package/src/core/extraction/client.ts +187 -0
- package/src/core/extraction/diagnostics.ts +193 -0
- package/src/core/extraction/prompts.ts +31 -0
- package/src/core/extraction.ts +491 -0
- package/src/core/fastembed-model-cache.ts +39 -0
- package/src/core/fastembed-runtime.ts +172 -0
- package/src/core/index.ts +39 -0
- package/src/core/llm-backends.ts +54 -0
- package/src/core/local-llm.ts +475 -0
- package/src/core/memory.ts +680 -0
- package/src/core/migrations/e6-triplestore-split.ts +211 -0
- package/src/core/migrations/index.ts +1 -0
- package/src/core/mmr.ts +101 -0
- package/src/core/orchestrator.ts +62 -0
- package/src/core/patterns.ts +484 -0
- package/src/core/plugins.ts +375 -0
- package/src/core/polyphonic-recall.ts +563 -0
- package/src/core/query-cache.ts +351 -0
- package/src/core/query-intent.ts +139 -0
- package/src/core/recall-diagnostics.ts +157 -0
- package/src/core/runtime-options.ts +130 -0
- package/src/core/shmr.ts +567 -0
- package/src/core/streaming.ts +419 -0
- package/src/core/synonyms.ts +197 -0
- package/src/core/temporal-parser.ts +363 -0
- package/src/core/token-counter.ts +30 -0
- package/src/core/triples.ts +452 -0
- package/src/core/typed-memory.ts +407 -0
- package/src/core/vector-index.ts +83 -0
- package/src/core/vector-math.ts +23 -0
- package/src/core/veracity-consolidation.ts +477 -0
- package/src/core/weibull.ts +124 -0
- package/src/db.ts +186 -0
- package/src/diagnose.ts +174 -0
- package/src/dr/index.ts +1 -0
- package/src/dr/recovery.ts +405 -0
- package/src/index.ts +33 -0
- package/src/mcp-server.ts +155 -0
- package/src/mcp-tools.ts +970 -0
- package/src/migrations/e6-triplestore-split.ts +1 -0
- package/src/migrations/index.ts +1 -0
- package/src/types.ts +157 -0
- package/src/util/datetime.ts +69 -0
- package/src/util/env.ts +65 -0
- package/src/util/ids.ts +19 -0
- package/src/util/lru.ts +48 -0
- package/src/util/regex.ts +165 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
copyFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
statSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
16
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
17
|
+
import { dataDir as configuredDataDir, dbPath as configuredDbPath, type Env } from "../config";
|
|
18
|
+
import { closeQuietly, openDatabase } from "../db";
|
|
19
|
+
|
|
20
|
+
type SerializableDatabase = Database & { serialize(): Uint8Array };
|
|
21
|
+
const SQLITE_HEADER = new Uint8Array([83, 81, 76, 105, 116, 101, 32, 102, 111, 114, 109, 97, 116, 32, 51, 0]);
|
|
22
|
+
const SQLITE_SIDECAR_SUFFIXES = ["-wal", "-shm", "-journal"] as const;
|
|
23
|
+
let uniqueCounter = 0;
|
|
24
|
+
|
|
25
|
+
export interface RecoveryPaths {
|
|
26
|
+
readonly dataDir: string;
|
|
27
|
+
readonly backupDir: string;
|
|
28
|
+
readonly dbPath: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BackupMetadata {
|
|
32
|
+
readonly timestamp: string;
|
|
33
|
+
readonly original_size: number;
|
|
34
|
+
readonly backup_size: number;
|
|
35
|
+
readonly db_checksum: string;
|
|
36
|
+
readonly backup_checksum: string;
|
|
37
|
+
readonly compressed: true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BackupResult extends BackupMetadata {
|
|
41
|
+
readonly backup_path: string;
|
|
42
|
+
readonly metadata_path: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RestoreResult {
|
|
46
|
+
readonly restored: true;
|
|
47
|
+
readonly backup_used: string;
|
|
48
|
+
readonly database_path: string;
|
|
49
|
+
readonly integrity_check: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface EmergencyRestoreResult {
|
|
53
|
+
readonly restored: true;
|
|
54
|
+
readonly backup_used: string;
|
|
55
|
+
readonly attempts: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface BackupInfo {
|
|
59
|
+
readonly file: string;
|
|
60
|
+
readonly name: string;
|
|
61
|
+
readonly size: number;
|
|
62
|
+
readonly modified: string;
|
|
63
|
+
readonly metadata?: BackupMetadata;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface RotateBackupsResult {
|
|
67
|
+
readonly total_backups: number;
|
|
68
|
+
readonly kept: number;
|
|
69
|
+
readonly deleted: number;
|
|
70
|
+
readonly deleted_files: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface HealthCheckResult {
|
|
74
|
+
readonly database: {
|
|
75
|
+
readonly exists: boolean;
|
|
76
|
+
readonly valid: boolean;
|
|
77
|
+
readonly path: string;
|
|
78
|
+
readonly message: string;
|
|
79
|
+
};
|
|
80
|
+
readonly backups: {
|
|
81
|
+
readonly total: number;
|
|
82
|
+
readonly latest: string | null;
|
|
83
|
+
readonly directory: string;
|
|
84
|
+
};
|
|
85
|
+
readonly status: "healthy" | "unhealthy";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function timestampForBackup(now = new Date()): string {
|
|
89
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
90
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sha256Hex16(bytes: NodeJS.ArrayBufferView): string {
|
|
94
|
+
return createHash("sha256").update(bytes).digest("hex").slice(0, 16);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function nextUniqueToken(): string {
|
|
98
|
+
uniqueCounter = (uniqueCounter + 1) % 0x1fffffff;
|
|
99
|
+
return `${Date.now().toString(36)}_${process.pid.toString(36)}_${uniqueCounter.toString(36)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasErrorCode(error: unknown, code: string): boolean {
|
|
103
|
+
return (
|
|
104
|
+
error !== null &&
|
|
105
|
+
typeof error === "object" &&
|
|
106
|
+
"code" in error &&
|
|
107
|
+
(error as { readonly code?: unknown }).code === code
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writeBackupFile(destinationDir: string, timestamp: string, bytes: Uint8Array): string {
|
|
112
|
+
for (let attempt = 0; attempt < 64; attempt += 1) {
|
|
113
|
+
const suffix = attempt === 0 ? "" : `_${nextUniqueToken()}`;
|
|
114
|
+
const backupPath = join(destinationDir, `mnemopi_backup_${timestamp}${suffix}.db.gz`);
|
|
115
|
+
try {
|
|
116
|
+
writeFileSync(backupPath, bytes, { flag: "wx" });
|
|
117
|
+
return backupPath;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (hasErrorCode(error, "EEXIST")) continue;
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Unable to allocate unique backup path in ${destinationDir}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function restoreTempPath(targetPath: string): string {
|
|
127
|
+
return join(dirname(targetPath), `.${basename(targetPath)}.${process.pid}.${nextUniqueToken()}.restore.tmp`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function defaultBackupDir(env: Env = process.env): string {
|
|
131
|
+
const explicit = env.MNEMOPI_BACKUP_DIR;
|
|
132
|
+
if (explicit !== undefined && explicit.length > 0) return explicit;
|
|
133
|
+
const dir = configuredDataDir(env);
|
|
134
|
+
return join(dirname(dir), "backups");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getDefaultPaths(env: Env = process.env): RecoveryPaths {
|
|
138
|
+
return {
|
|
139
|
+
dataDir: configuredDataDir(env),
|
|
140
|
+
backupDir: defaultBackupDir(env),
|
|
141
|
+
dbPath: configuredDbPath(env),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function createBackup(dbPath?: string | null, backupDir?: string | null): BackupResult {
|
|
145
|
+
const paths = getDefaultPaths();
|
|
146
|
+
const sourcePath = dbPath ?? paths.dbPath;
|
|
147
|
+
const destinationDir = backupDir ?? paths.backupDir;
|
|
148
|
+
|
|
149
|
+
if (!existsSync(sourcePath)) throw new FileNotFoundError(`Database not found: ${sourcePath}`);
|
|
150
|
+
|
|
151
|
+
mkdirSync(destinationDir, { recursive: true });
|
|
152
|
+
const timestamp = timestampForBackup();
|
|
153
|
+
|
|
154
|
+
let snapshot: Uint8Array | null = null;
|
|
155
|
+
let sourceDb: Database | null = null;
|
|
156
|
+
try {
|
|
157
|
+
sourceDb = openDatabase(sourcePath, { create: false, readwrite: false, pragmas: false });
|
|
158
|
+
snapshot = (sourceDb as SerializableDatabase).serialize();
|
|
159
|
+
} finally {
|
|
160
|
+
closeQuietly(sourceDb);
|
|
161
|
+
}
|
|
162
|
+
if (snapshot === null) throw new Error(`Unable to serialize database backup: ${sourcePath}`);
|
|
163
|
+
const backupPath = writeBackupFile(destinationDir, timestamp, gzipSync(snapshot));
|
|
164
|
+
|
|
165
|
+
const dbBytes = readFileSync(sourcePath);
|
|
166
|
+
const backupBytes = readFileSync(backupPath);
|
|
167
|
+
const metadata: BackupMetadata = {
|
|
168
|
+
timestamp,
|
|
169
|
+
original_size: statSync(sourcePath).size,
|
|
170
|
+
backup_size: statSync(backupPath).size,
|
|
171
|
+
db_checksum: sha256Hex16(dbBytes),
|
|
172
|
+
backup_checksum: sha256Hex16(backupBytes),
|
|
173
|
+
compressed: true,
|
|
174
|
+
};
|
|
175
|
+
const metadataPath = `${backupPath.slice(0, -3)}.gz.json`;
|
|
176
|
+
writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
177
|
+
|
|
178
|
+
return { backup_path: backupPath, metadata_path: metadataPath, ...metadata };
|
|
179
|
+
}
|
|
180
|
+
function isSqliteFile(bytes: Uint8Array): boolean {
|
|
181
|
+
if (bytes.length < SQLITE_HEADER.length) return false;
|
|
182
|
+
for (let i = 0; i < SQLITE_HEADER.length; i += 1) {
|
|
183
|
+
if (bytes[i] !== SQLITE_HEADER[i]) return false;
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function writeGzippedSqlDump(sql: string, tempPath: string): void {
|
|
189
|
+
let db: Database | null = null;
|
|
190
|
+
try {
|
|
191
|
+
db = openDatabase(tempPath, { pragmas: false });
|
|
192
|
+
db.exec(sql);
|
|
193
|
+
} finally {
|
|
194
|
+
closeQuietly(db);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sqliteSidecarPath(dbPath: string, suffix: (typeof SQLITE_SIDECAR_SUFFIXES)[number]): string {
|
|
199
|
+
return `${dbPath}${suffix}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function removeSqliteSidecars(dbPath: string): void {
|
|
203
|
+
for (const suffix of SQLITE_SIDECAR_SUFFIXES) rmSync(sqliteSidecarPath(dbPath, suffix), { force: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function emergencyBackupPath(targetPath: string): string {
|
|
207
|
+
const ext = extname(targetPath);
|
|
208
|
+
if (ext.length === 0) return `${targetPath}.emergency_backup.db`;
|
|
209
|
+
return `${targetPath.slice(0, -ext.length)}.emergency_backup.db`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function emergencyBackupSidecarPath(targetPath: string, suffix: (typeof SQLITE_SIDECAR_SUFFIXES)[number]): string {
|
|
213
|
+
return `${emergencyBackupPath(targetPath)}${suffix}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function snapshotCurrentDatabase(targetPath: string): void {
|
|
217
|
+
const mainBackup = emergencyBackupPath(targetPath);
|
|
218
|
+
rmSync(mainBackup, { force: true });
|
|
219
|
+
for (const suffix of SQLITE_SIDECAR_SUFFIXES)
|
|
220
|
+
rmSync(emergencyBackupSidecarPath(targetPath, suffix), { force: true });
|
|
221
|
+
if (existsSync(targetPath)) copyFileSync(targetPath, mainBackup);
|
|
222
|
+
for (const suffix of SQLITE_SIDECAR_SUFFIXES) {
|
|
223
|
+
const sidecar = sqliteSidecarPath(targetPath, suffix);
|
|
224
|
+
if (existsSync(sidecar)) copyFileSync(sidecar, emergencyBackupSidecarPath(targetPath, suffix));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function restoreCurrentDatabaseSnapshot(targetPath: string): void {
|
|
229
|
+
const mainBackup = emergencyBackupPath(targetPath);
|
|
230
|
+
if (!existsSync(mainBackup)) return;
|
|
231
|
+
copyFileSync(mainBackup, targetPath);
|
|
232
|
+
for (const suffix of SQLITE_SIDECAR_SUFFIXES) {
|
|
233
|
+
const sidecar = sqliteSidecarPath(targetPath, suffix);
|
|
234
|
+
rmSync(sidecar, { force: true });
|
|
235
|
+
const backupSidecar = emergencyBackupSidecarPath(targetPath, suffix);
|
|
236
|
+
if (existsSync(backupSidecar)) copyFileSync(backupSidecar, sidecar);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function writeRestoreCandidate(uncompressed: Buffer, tempPath: string): void {
|
|
241
|
+
if (isSqliteFile(uncompressed)) {
|
|
242
|
+
writeFileSync(tempPath, uncompressed, { flag: "wx" });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
writeGzippedSqlDump(uncompressed.toString("utf8"), tempPath);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function restoreBackup(backupPath: string, dbPath?: string | null): RestoreResult {
|
|
249
|
+
const targetPath = dbPath ?? getDefaultPaths().dbPath;
|
|
250
|
+
if (!existsSync(backupPath)) throw new FileNotFoundError(`Backup not found: ${backupPath}`);
|
|
251
|
+
|
|
252
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
253
|
+
|
|
254
|
+
const uncompressed = gunzipSync(readFileSync(backupPath));
|
|
255
|
+
const tempPath = restoreTempPath(targetPath);
|
|
256
|
+
let replacedTarget = false;
|
|
257
|
+
try {
|
|
258
|
+
writeRestoreCandidate(uncompressed, tempPath);
|
|
259
|
+
if (!verifyIntegrity(tempPath)) throw new Error(`Backup failed integrity check: ${backupPath}`);
|
|
260
|
+
snapshotCurrentDatabase(targetPath);
|
|
261
|
+
renameSync(tempPath, targetPath);
|
|
262
|
+
replacedTarget = true;
|
|
263
|
+
removeSqliteSidecars(targetPath);
|
|
264
|
+
const integrity = verifyIntegrity(targetPath);
|
|
265
|
+
if (!integrity) throw new Error(`Restored database failed integrity check: ${backupPath}`);
|
|
266
|
+
return {
|
|
267
|
+
restored: true,
|
|
268
|
+
backup_used: backupPath,
|
|
269
|
+
database_path: targetPath,
|
|
270
|
+
integrity_check: integrity,
|
|
271
|
+
};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
try {
|
|
274
|
+
rmSync(tempPath, { force: true });
|
|
275
|
+
} catch {
|
|
276
|
+
// Preserve the restore failure.
|
|
277
|
+
}
|
|
278
|
+
if (replacedTarget) {
|
|
279
|
+
try {
|
|
280
|
+
restoreCurrentDatabaseSnapshot(targetPath);
|
|
281
|
+
} catch {
|
|
282
|
+
// Preserve the restore failure.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
export function emergencyRestore(backupDir?: string | null, dbPath?: string | null): EmergencyRestoreResult {
|
|
289
|
+
const paths = getDefaultPaths();
|
|
290
|
+
const dir = backupDir ?? paths.backupDir;
|
|
291
|
+
const targetPath = dbPath ?? paths.dbPath;
|
|
292
|
+
const backups = existsSync(dir)
|
|
293
|
+
? readdirSync(dir)
|
|
294
|
+
.filter(name => /^mnemopi_backup_.*\.db\.gz$/.test(name))
|
|
295
|
+
.sort()
|
|
296
|
+
.reverse()
|
|
297
|
+
.map(name => join(dir, name))
|
|
298
|
+
: [];
|
|
299
|
+
|
|
300
|
+
if (backups.length === 0) throw new FileNotFoundError(`No backups found in ${dir}`);
|
|
301
|
+
|
|
302
|
+
let attempts = 0;
|
|
303
|
+
for (const backup of backups) {
|
|
304
|
+
attempts += 1;
|
|
305
|
+
try {
|
|
306
|
+
const result = restoreBackup(backup, targetPath);
|
|
307
|
+
if (result.integrity_check) return { restored: true, backup_used: backup, attempts };
|
|
308
|
+
} catch {
|
|
309
|
+
// Try the next backup, matching the Python recovery behavior.
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
throw new Error("All backups failed integrity check");
|
|
313
|
+
}
|
|
314
|
+
export function verifyIntegrity(dbPath?: string | null): boolean {
|
|
315
|
+
const targetPath = dbPath ?? getDefaultPaths().dbPath;
|
|
316
|
+
if (!existsSync(targetPath)) return false;
|
|
317
|
+
|
|
318
|
+
let db: Database | null = null;
|
|
319
|
+
try {
|
|
320
|
+
db = openDatabase(targetPath, { create: false, readwrite: false, pragmas: false });
|
|
321
|
+
const row = db.query("PRAGMA integrity_check").get() as { integrity_check: string } | null;
|
|
322
|
+
return row?.integrity_check === "ok";
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
} finally {
|
|
326
|
+
closeQuietly(db);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export function listBackups(backupDir?: string | null): BackupInfo[] {
|
|
330
|
+
const dir = backupDir ?? getDefaultPaths().backupDir;
|
|
331
|
+
if (!existsSync(dir)) return [];
|
|
332
|
+
|
|
333
|
+
return readdirSync(dir)
|
|
334
|
+
.filter(name => /^mnemopi_backup_.*\.db\.gz$/.test(name))
|
|
335
|
+
.sort()
|
|
336
|
+
.reverse()
|
|
337
|
+
.map(name => {
|
|
338
|
+
const file = join(dir, name);
|
|
339
|
+
const stat = statSync(file);
|
|
340
|
+
const metaFile = `${file.slice(0, -3)}.gz.json`;
|
|
341
|
+
const info: BackupInfo = {
|
|
342
|
+
file,
|
|
343
|
+
name,
|
|
344
|
+
size: stat.size,
|
|
345
|
+
modified: stat.mtime.toISOString(),
|
|
346
|
+
};
|
|
347
|
+
if (!existsSync(metaFile)) return info;
|
|
348
|
+
return { ...info, metadata: JSON.parse(readFileSync(metaFile, "utf8")) as BackupMetadata };
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
export function rotateBackups(backupDir?: string | null, keep = 10): RotateBackupsResult {
|
|
352
|
+
const dir = backupDir ?? getDefaultPaths().backupDir;
|
|
353
|
+
const backups = existsSync(dir)
|
|
354
|
+
? readdirSync(dir)
|
|
355
|
+
.filter(name => /^mnemopi_backup_.*\.db\.gz$/.test(name))
|
|
356
|
+
.sort()
|
|
357
|
+
.map(name => join(dir, name))
|
|
358
|
+
: [];
|
|
359
|
+
const toDelete = backups.length > keep ? backups.slice(0, backups.length - keep) : [];
|
|
360
|
+
const deletedFiles: string[] = [];
|
|
361
|
+
for (const backup of toDelete) {
|
|
362
|
+
unlinkSync(backup);
|
|
363
|
+
const meta = `${backup.slice(0, -3)}.gz.json`;
|
|
364
|
+
if (existsSync(meta)) unlinkSync(meta);
|
|
365
|
+
deletedFiles.push(basename(backup));
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
total_backups: backups.length,
|
|
369
|
+
kept: keep,
|
|
370
|
+
deleted: deletedFiles.length,
|
|
371
|
+
deleted_files: deletedFiles,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
export function healthCheck(): HealthCheckResult {
|
|
375
|
+
const paths = getDefaultPaths();
|
|
376
|
+
const dbExists = existsSync(paths.dbPath);
|
|
377
|
+
const dbValid = dbExists ? verifyIntegrity(paths.dbPath) : false;
|
|
378
|
+
const backups = listBackups(paths.backupDir)
|
|
379
|
+
.map(backup => backup.file)
|
|
380
|
+
.sort();
|
|
381
|
+
return {
|
|
382
|
+
database: {
|
|
383
|
+
exists: dbExists,
|
|
384
|
+
valid: dbValid,
|
|
385
|
+
path: paths.dbPath,
|
|
386
|
+
message: dbValid ? "Database integrity verified" : "Database missing or corrupt",
|
|
387
|
+
},
|
|
388
|
+
backups: {
|
|
389
|
+
total: backups.length,
|
|
390
|
+
latest: backups.at(-1) ?? null,
|
|
391
|
+
directory: paths.backupDir,
|
|
392
|
+
},
|
|
393
|
+
status: dbValid ? "healthy" : "unhealthy",
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
export class FileNotFoundError extends Error {
|
|
397
|
+
constructor(message: string) {
|
|
398
|
+
super(message);
|
|
399
|
+
this.name = "FileNotFoundError";
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function resetRecoveryForTests(): void {
|
|
404
|
+
// Recovery has no module state; exported for test harness symmetry.
|
|
405
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export { configureRecallFeatures, type RecallFeatureFlags } from "./config";
|
|
2
|
+
export * from "./core/beam/index";
|
|
3
|
+
export * from "./core/embeddings";
|
|
4
|
+
export * from "./core/llm-backends";
|
|
5
|
+
export * from "./core/memory";
|
|
6
|
+
export {
|
|
7
|
+
addMemory,
|
|
8
|
+
flushExtractions,
|
|
9
|
+
forget,
|
|
10
|
+
get,
|
|
11
|
+
getBank,
|
|
12
|
+
getContext,
|
|
13
|
+
getDefaultInstance,
|
|
14
|
+
getStats,
|
|
15
|
+
Mnemopi,
|
|
16
|
+
query,
|
|
17
|
+
recall,
|
|
18
|
+
recallEnhanced,
|
|
19
|
+
remember,
|
|
20
|
+
resetDefaultInstanceForTests,
|
|
21
|
+
resetMemoryForTests,
|
|
22
|
+
resetModuleStateForTests,
|
|
23
|
+
saveMemory,
|
|
24
|
+
scratchpadClear,
|
|
25
|
+
scratchpadRead,
|
|
26
|
+
scratchpadWrite,
|
|
27
|
+
search,
|
|
28
|
+
setBank,
|
|
29
|
+
sleep,
|
|
30
|
+
sleepAllSessions,
|
|
31
|
+
storeMemory,
|
|
32
|
+
update,
|
|
33
|
+
} from "./core/memory";
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { getToolDefinitions, handleToolCall, type ToolArguments, type ToolDefinition } from "./mcp-tools";
|
|
2
|
+
|
|
3
|
+
export interface JsonRpcRequest {
|
|
4
|
+
readonly jsonrpc?: string;
|
|
5
|
+
readonly id?: string | number | null;
|
|
6
|
+
readonly method?: string;
|
|
7
|
+
readonly params?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface JsonRpcResponse {
|
|
11
|
+
readonly jsonrpc: "2.0";
|
|
12
|
+
readonly id: string | number | null;
|
|
13
|
+
readonly result?: unknown;
|
|
14
|
+
readonly error?: { readonly code: number; readonly message: string };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ListToolsResponse {
|
|
18
|
+
readonly tools: readonly ToolDefinition[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CallToolContent {
|
|
22
|
+
readonly type: "text";
|
|
23
|
+
readonly text: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CallToolResponse {
|
|
27
|
+
readonly content: readonly CallToolContent[];
|
|
28
|
+
readonly isError?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WritableOutput {
|
|
32
|
+
write(chunk: string): unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ok(id: string | number | null, result: unknown): JsonRpcResponse {
|
|
36
|
+
return { jsonrpc: "2.0", id, result };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function err(id: string | number | null, code: number, message: string): JsonRpcResponse {
|
|
40
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requestId(request: JsonRpcRequest): string | number | null {
|
|
44
|
+
return typeof request.id === "string" || typeof request.id === "number" || request.id === null ? request.id : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasRequestId(request: JsonRpcRequest): boolean {
|
|
48
|
+
return Object.hasOwn(request, "id");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function listToolsJson(): ListToolsResponse {
|
|
52
|
+
return { tools: getToolDefinitions() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function callToolJson(name: string, args: ToolArguments = {}): Promise<CallToolResponse> {
|
|
56
|
+
try {
|
|
57
|
+
const result = await handleToolCall(name, args);
|
|
58
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: JSON.stringify({ status: "error", message }, null, 2) }],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function handleJsonRpc(request: JsonRpcRequest): Promise<JsonRpcResponse | null> {
|
|
69
|
+
const method = request.method ?? "";
|
|
70
|
+
if (method.startsWith("notifications/") || !hasRequestId(request)) return null;
|
|
71
|
+
const id = requestId(request);
|
|
72
|
+
if (method === "initialize") {
|
|
73
|
+
return ok(id, {
|
|
74
|
+
protocolVersion: "2024-11-05",
|
|
75
|
+
serverInfo: { name: "mnemopi", version: "3.1.2" },
|
|
76
|
+
capabilities: { tools: {} },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (method === "tools/list") return ok(id, listToolsJson());
|
|
80
|
+
if (method === "tools/call") {
|
|
81
|
+
const params = request.params ?? {};
|
|
82
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
83
|
+
const args =
|
|
84
|
+
params.arguments !== null && typeof params.arguments === "object" && !Array.isArray(params.arguments)
|
|
85
|
+
? (params.arguments as ToolArguments)
|
|
86
|
+
: {};
|
|
87
|
+
if (name.length === 0) return err(id, -32602, "tools/call requires params.name");
|
|
88
|
+
return ok(id, await callToolJson(name, args));
|
|
89
|
+
}
|
|
90
|
+
return err(id, -32601, `Unknown method: ${method}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function runStdio(
|
|
94
|
+
input: ReadableStream<Uint8Array> = Bun.stdin.stream(),
|
|
95
|
+
output: WritableOutput = Bun.stdout,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
const reader = input.getReader();
|
|
98
|
+
const decoder = new TextDecoder();
|
|
99
|
+
let buffer = "";
|
|
100
|
+
try {
|
|
101
|
+
while (true) {
|
|
102
|
+
const chunk = await reader.read();
|
|
103
|
+
if (chunk.done) break;
|
|
104
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
105
|
+
let newline = buffer.indexOf("\n");
|
|
106
|
+
while (newline >= 0) {
|
|
107
|
+
const line = buffer.slice(0, newline).trim();
|
|
108
|
+
buffer = buffer.slice(newline + 1);
|
|
109
|
+
if (line.length > 0) {
|
|
110
|
+
let parsed: unknown;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(line);
|
|
113
|
+
} catch {
|
|
114
|
+
output.write(`${JSON.stringify(err(null, -32700, "Parse error"))}\n`);
|
|
115
|
+
newline = buffer.indexOf("\n");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const response = await handleJsonRpc(parsed as JsonRpcRequest);
|
|
119
|
+
if (response !== null) output.write(`${JSON.stringify(response)}\n`);
|
|
120
|
+
}
|
|
121
|
+
newline = buffer.indexOf("\n");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
reader.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function runMcpServer(
|
|
130
|
+
transport = "stdio",
|
|
131
|
+
options: { port?: number; bank?: string; host?: string } = {},
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
if (options.bank !== undefined && options.bank.length > 0) process.env.MNEMOPI_MCP_BANK = options.bank;
|
|
134
|
+
if (transport !== "stdio") throw new Error("Only stdio transport is implemented in the TypeScript port");
|
|
135
|
+
return runStdio();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function main(argv: readonly string[] = Bun.argv.slice(2)): Promise<void> {
|
|
139
|
+
let transport = "stdio";
|
|
140
|
+
let port: number | undefined;
|
|
141
|
+
let bank: string | undefined;
|
|
142
|
+
let host: string | undefined;
|
|
143
|
+
for (let i = 0; i < argv.length; i++) {
|
|
144
|
+
const arg = argv[i];
|
|
145
|
+
if (arg === "--transport") transport = argv[++i] ?? "stdio";
|
|
146
|
+
else if (arg === "--port") {
|
|
147
|
+
const parsed = Number(argv[++i] ?? "");
|
|
148
|
+
if (Number.isFinite(parsed)) port = parsed;
|
|
149
|
+
} else if (arg === "--bank") bank = argv[++i] ?? "";
|
|
150
|
+
else if (arg === "--host") host = argv[++i] ?? "";
|
|
151
|
+
}
|
|
152
|
+
return runMcpServer(transport, { port, bank, host });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (import.meta.main) await main();
|