linksee-memory 0.17.1 → 0.17.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/README.md +124 -822
- package/dist/bin/migrate-legacy.d.ts +33 -0
- package/dist/bin/migrate-legacy.js +501 -0
- package/dist/bin/runtime.d.ts +52 -0
- package/dist/bin/runtime.js +381 -0
- package/dist/bin/setup.js +1 -1
- package/dist/db/migrate.d.ts +53 -2
- package/dist/db/migrate.js +106 -37
- package/dist/lib/hook-runtime.js +9 -4
- package/dist/lib/test-faults.d.ts +3 -0
- package/dist/lib/test-faults.js +30 -0
- package/dist/mcp/server.js +2 -0
- package/package.json +1 -1
package/dist/db/migrate.js
CHANGED
|
@@ -11,34 +11,63 @@ const DB_PATH = join(DEFAULT_DB_DIR, 'memory.db');
|
|
|
11
11
|
export function getDbPath() {
|
|
12
12
|
return DB_PATH;
|
|
13
13
|
}
|
|
14
|
+
/** This package's version (dist/db/migrate.js → ../../package.json), for exact-version guidance. */
|
|
15
|
+
export function packageVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
18
|
+
return String(JSON.parse(readFileSync(pkg, 'utf8')).version || '');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
14
24
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* are writes to a file we are about to refuse. So the schema is read through a read-only
|
|
19
|
-
* connection first (SQLITE_OPEN_READONLY: no checkpoint, no rollback, memory.db and -wal untouched).
|
|
20
|
-
* • schema 1–5 → LegacySchemaError
|
|
21
|
-
* • a hot rollback journal → HotJournalError (it cannot be read without rolling back)
|
|
22
|
-
* • not a DB → return; the normal open's corruption path moves it aside
|
|
23
|
-
* • native addon failed → rethrown as is
|
|
24
|
-
* • anything else (busy, locked, unreadable) → PrecheckError: could not check, so no writing
|
|
25
|
-
* When the file is refused, the -wal/-shm that the read-only open itself created are removed
|
|
26
|
-
* again, so the folder is left as it was.
|
|
25
|
+
* The command that migrates / recovers this file. Pinned to this exact version: an unpinned
|
|
26
|
+
* `npx linksee-memory` could resolve to a cached older version, and versions before 0.17.2 have
|
|
27
|
+
* no `migrate` subcommand — they would start the MCP server instead (L-1-130).
|
|
27
28
|
*/
|
|
28
|
-
function
|
|
29
|
+
export function migrateCommand() {
|
|
30
|
+
const v = packageVersion();
|
|
31
|
+
// Never fall back to an unpinned command (Codex review of #19): if this version cannot be read,
|
|
32
|
+
// name the first version that has `migrate`, as a fixed string.
|
|
33
|
+
return /^\d+\.\d+\.\d+/.test(v) ? `npx -y linksee-memory@${v} migrate` : `npx -y linksee-memory@${MIGRATE_SINCE} migrate`;
|
|
34
|
+
}
|
|
35
|
+
/** First version with the `migrate` subcommand. */
|
|
36
|
+
export const MIGRATE_SINCE = '0.17.2';
|
|
37
|
+
/**
|
|
38
|
+
* Read the schema through a read-only connection (no checkpoint, no rollback). Returns what it
|
|
39
|
+
* found plus `tidy`, which removes the -wal/-shm that the read-only open itself added.
|
|
40
|
+
* Throws the native load error as is, and PrecheckError when the file could not be checked at all.
|
|
41
|
+
*/
|
|
42
|
+
export function probeReadonly(path) {
|
|
29
43
|
if (!existsSync(path))
|
|
30
|
-
return
|
|
44
|
+
return { probe: { kind: 'missing' }, tidy: () => { } };
|
|
31
45
|
const had = { wal: existsSync(path + '-wal'), shm: existsSync(path + '-shm') };
|
|
46
|
+
const walStat = had.wal ? statSync(path + '-wal') : null;
|
|
32
47
|
// Remove only what the read-only open itself added: a -wal that was absent and is still empty,
|
|
33
48
|
// and a -shm that was absent (e.g. a leftover -wal with no -shm — the reader builds one).
|
|
34
|
-
//
|
|
35
|
-
//
|
|
49
|
+
// 0.17.2 (Codex fifth review of #15, P2): another process may open the file in the same instant
|
|
50
|
+
// and start using the -shm. Two guards: (1) if the -wal changed while we looked (size or mtime),
|
|
51
|
+
// someone else is active → leave both; (2) the -shm is renamed aside first — on Windows a file
|
|
52
|
+
// another process has open cannot be renamed, so an in-use -shm stays. On POSIX an open -shm can
|
|
53
|
+
// still be renamed, so a writer that starts in the exact window remains a known limit.
|
|
36
54
|
const tidy = () => {
|
|
37
55
|
try {
|
|
38
|
-
if (
|
|
56
|
+
if (had.wal) {
|
|
57
|
+
const now = existsSync(path + '-wal') ? statSync(path + '-wal') : null;
|
|
58
|
+
if (!now || !walStat || now.size !== walStat.size || now.mtimeMs !== walStat.mtimeMs)
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
else if (existsSync(path + '-wal')) {
|
|
62
|
+
if (statSync(path + '-wal').size !== 0)
|
|
63
|
+
return; // someone wrote to it: not ours
|
|
39
64
|
unlinkSync(path + '-wal');
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
}
|
|
66
|
+
if (!had.shm && existsSync(path + '-shm')) {
|
|
67
|
+
const aside = `${path}-shm.tidy-${process.pid}`;
|
|
68
|
+
renameSync(path + '-shm', aside);
|
|
69
|
+
unlinkSync(aside);
|
|
70
|
+
}
|
|
42
71
|
}
|
|
43
72
|
catch { /* leave them; memory.db itself is untouched either way */ }
|
|
44
73
|
};
|
|
@@ -56,24 +85,46 @@ function precheckBeforeWrite(path) {
|
|
|
56
85
|
const code = e?.code ?? '';
|
|
57
86
|
const msg = e?.message ?? String(e);
|
|
58
87
|
// The file vanished between the check and the open: a new DB, as above.
|
|
59
|
-
if (!existsSync(path) && (code === 'SQLITE_CANTOPEN' || code === 'ENOENT' || /unable to open|does not exist/i.test(msg)))
|
|
60
|
-
return;
|
|
88
|
+
if (!existsSync(path) && (code === 'SQLITE_CANTOPEN' || code === 'ENOENT' || /unable to open|does not exist/i.test(msg))) {
|
|
89
|
+
return { probe: { kind: 'missing' }, tidy: () => { } };
|
|
90
|
+
}
|
|
61
91
|
// The native addon itself failed: let it surface as such (the hooks turn it into one line).
|
|
62
92
|
if (isNativeLoadError(e))
|
|
63
93
|
throw e;
|
|
64
|
-
// Not a database at all
|
|
65
|
-
if (code === 'SQLITE_NOTADB' || code === 'SQLITE_CORRUPT' || /not a database|malformed|disk image/i.test(msg))
|
|
66
|
-
return;
|
|
94
|
+
// Not a database at all.
|
|
95
|
+
if (code === 'SQLITE_NOTADB' || code === 'SQLITE_CORRUPT' || /not a database|malformed|disk image/i.test(msg)) {
|
|
96
|
+
return { probe: { kind: 'notadb' }, tidy };
|
|
97
|
+
}
|
|
67
98
|
tidy();
|
|
68
99
|
if (code === 'SQLITE_READONLY_ROLLBACK')
|
|
69
|
-
|
|
100
|
+
return { probe: { kind: 'hot' }, tidy: () => { } };
|
|
70
101
|
// Anything else (SQLITE_BUSY, locked, permission, I/O): we could not check it, so we do not
|
|
71
102
|
// write to it either — fail closed (0.17.1, Codex third review of #15).
|
|
72
103
|
throw new PrecheckError(path, code || msg.split('\n')[0]);
|
|
73
104
|
}
|
|
74
|
-
|
|
105
|
+
return { probe: { kind: 'version', version: v }, tidy };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Decide, WITHOUT a writable connection, whether this file may be opened for writing at all.
|
|
109
|
+
* (0.17.1, Codex re-review of #15.) A writable connection that is the last one to close
|
|
110
|
+
* checkpoints a leftover WAL into memory.db and deletes it, and rolls back a hot journal — both
|
|
111
|
+
* are writes to a file we are about to refuse. So the schema is read through a read-only
|
|
112
|
+
* connection first (SQLITE_OPEN_READONLY: no checkpoint, no rollback, memory.db and -wal untouched).
|
|
113
|
+
* • schema 1–5 → LegacySchemaError
|
|
114
|
+
* • a hot rollback journal → HotJournalError (it cannot be read without rolling back)
|
|
115
|
+
* • not a DB → return; the normal open's corruption path moves it aside
|
|
116
|
+
* • native addon failed → rethrown as is
|
|
117
|
+
* • anything else (busy, locked, unreadable) → PrecheckError: could not check, so no writing
|
|
118
|
+
* When the file is refused, the -wal/-shm that the read-only open itself created are removed
|
|
119
|
+
* again, so the folder is left as it was.
|
|
120
|
+
*/
|
|
121
|
+
function precheckBeforeWrite(path) {
|
|
122
|
+
const { probe, tidy } = probeReadonly(path);
|
|
123
|
+
if (probe.kind === 'hot')
|
|
124
|
+
throw new HotJournalError(path);
|
|
125
|
+
if (probe.kind === 'version' && probe.version > 0 && probe.version < MIN_SUPPORTED_SCHEMA) {
|
|
75
126
|
tidy();
|
|
76
|
-
throw new LegacySchemaError(
|
|
127
|
+
throw new LegacySchemaError(probe.version, path);
|
|
77
128
|
}
|
|
78
129
|
}
|
|
79
130
|
function openAt(path) {
|
|
@@ -139,7 +190,7 @@ export class DbRefusedError extends Error {
|
|
|
139
190
|
* No old version is recommended as a stepping stone: 0.8.0–0.17.0 stop on `no such table:
|
|
140
191
|
* drift_anchors` after partly altering the file, and 0.7.2 has no subcommands — `npx
|
|
141
192
|
* linksee-memory@0.7.2 <anything>` starts its MCP server, whose start-up consolidate can delete
|
|
142
|
-
* original rows (Codex review of PR #15).
|
|
193
|
+
* original rows (Codex review of PR #15). 0.17.2: `linksee-memory migrate` (src/bin/migrate-legacy.ts).
|
|
143
194
|
*/
|
|
144
195
|
export class LegacySchemaError extends DbRefusedError {
|
|
145
196
|
schemaVersion;
|
|
@@ -153,7 +204,9 @@ export class LegacySchemaError extends DbRefusedError {
|
|
|
153
204
|
\n` +
|
|
154
205
|
` 3. Do not open this file with another linksee-memory version in the meantime — older versions can delete or half-migrate rows.
|
|
155
206
|
\n` +
|
|
156
|
-
`
|
|
207
|
+
` 4. Migrate it (it backs the file up first; --dry only reports): ${migrateCommand()} --dry, then the same without --dry.
|
|
208
|
+
\n` +
|
|
209
|
+
`To start fresh instead, point LINKSEE_MEMORY_DIR at an empty folder.`);
|
|
157
210
|
this.name = 'LegacySchemaError';
|
|
158
211
|
this.schemaVersion = schemaVersion;
|
|
159
212
|
}
|
|
@@ -175,8 +228,8 @@ export class HotJournalError extends DbRefusedError {
|
|
|
175
228
|
super(`[linksee-memory] ${dbPath} has an unfinished write from an interrupted process (memory.db-journal), ` +
|
|
176
229
|
`so it cannot be checked without changing it. Stopped before writing anything.
|
|
177
230
|
\n` +
|
|
178
|
-
` Back up the folder that holds memory.db (all memory.db* files) before anything else opens it
|
|
179
|
-
`
|
|
231
|
+
` Back up the folder that holds memory.db (all memory.db* files) before anything else opens it, ` +
|
|
232
|
+
`then run: ${migrateCommand()} (it copies the files first, then lets SQLite roll the unfinished write back).`);
|
|
180
233
|
this.name = 'HotJournalError';
|
|
181
234
|
}
|
|
182
235
|
}
|
|
@@ -188,7 +241,8 @@ export function readSchemaVersion(db) {
|
|
|
188
241
|
const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
189
242
|
return row ? Number(row.value) || 0 : 0;
|
|
190
243
|
}
|
|
191
|
-
export function runMigrations(db) {
|
|
244
|
+
export function runMigrations(db, opts = {}) {
|
|
245
|
+
const step = (name) => opts.onStep?.(name, db);
|
|
192
246
|
const __filename = fileURLToPath(import.meta.url);
|
|
193
247
|
const schemaPath = join(dirname(__filename), 'schema.sql');
|
|
194
248
|
const sql = readFileSync(schemaPath, 'utf8');
|
|
@@ -198,14 +252,16 @@ export function runMigrations(db) {
|
|
|
198
252
|
// 0.17.1 (L-1-106 / audit F3): refuse schema 1–5 BEFORE any write. Their path through here
|
|
199
253
|
// (v5 → v6 generated columns, then the v5 entity merge running after the version has already
|
|
200
254
|
// been bumped to 16, with no transaction around it) can leave a half-migrated file. A safe,
|
|
201
|
-
// tested
|
|
202
|
-
//
|
|
203
|
-
|
|
255
|
+
// tested path is the `migrate` command (0.17.2): it backs up, sets allowLegacy and runs all of
|
|
256
|
+
// this inside one transaction. openDb() already refused these before any write; this is the
|
|
257
|
+
// second line for callers that hand in their own connection.
|
|
258
|
+
if (currentVersion > 0 && currentVersion < MIN_SUPPORTED_SCHEMA && !opts.allowLegacy) {
|
|
204
259
|
throw new LegacySchemaError(currentVersion, getDbPath());
|
|
205
260
|
}
|
|
206
261
|
// v3 → v4: rebuild memories_fts with trigram tokenizer for JP/CJK support.
|
|
207
262
|
// Only runs when upgrading an existing DB from schema v1-3.
|
|
208
263
|
if (currentVersion > 0 && currentVersion < 4) {
|
|
264
|
+
step('v3-v4');
|
|
209
265
|
db.exec(`
|
|
210
266
|
DROP TRIGGER IF EXISTS trg_memories_fts_ai;
|
|
211
267
|
DROP TRIGGER IF EXISTS trg_memories_fts_ad;
|
|
@@ -216,6 +272,7 @@ export function runMigrations(db) {
|
|
|
216
272
|
// v4 → v5: add normalized_name column BEFORE schema.sql runs,
|
|
217
273
|
// so the CREATE INDEX IF NOT EXISTS on (kind, normalized_name) succeeds.
|
|
218
274
|
if (currentVersion > 0 && currentVersion < 5) {
|
|
275
|
+
step('v4-v5');
|
|
219
276
|
const cols = db.prepare("PRAGMA table_info(entities)").all();
|
|
220
277
|
if (!cols.some(c => c.name === 'normalized_name')) {
|
|
221
278
|
db.exec('ALTER TABLE entities ADD COLUMN normalized_name TEXT');
|
|
@@ -229,6 +286,7 @@ export function runMigrations(db) {
|
|
|
229
286
|
// NOTE: VIRTUAL generated columns are hidden from PRAGMA table_info.
|
|
230
287
|
// Use PRAGMA table_xinfo (hidden=2 = VIRTUAL generated column) to detect them.
|
|
231
288
|
if (currentVersion > 0 && currentVersion < 6) {
|
|
289
|
+
step('v5-v6');
|
|
232
290
|
const xcols = db.prepare("PRAGMA table_xinfo(memories)").all();
|
|
233
291
|
const hasAltitude = xcols.some(c => c.name === 'altitude' && c.hidden === 2);
|
|
234
292
|
if (hasAltitude) {
|
|
@@ -249,6 +307,7 @@ export function runMigrations(db) {
|
|
|
249
307
|
// thread_id groups related memories (session-level or decision chains).
|
|
250
308
|
// memory_edges creates directed relationships between individual memories.
|
|
251
309
|
if (currentVersion > 0 && currentVersion < 7) {
|
|
310
|
+
step('v6-v7');
|
|
252
311
|
const cols = db.prepare("PRAGMA table_info(memories)").all();
|
|
253
312
|
if (!cols.some(c => c.name === 'thread_id')) {
|
|
254
313
|
db.exec('ALTER TABLE memories ADD COLUMN thread_id TEXT');
|
|
@@ -270,6 +329,7 @@ export function runMigrations(db) {
|
|
|
270
329
|
// with all of these columns.
|
|
271
330
|
const hasDriftAnchors = !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='drift_anchors'").get();
|
|
272
331
|
if (currentVersion > 0 && currentVersion < 9 && hasDriftAnchors) {
|
|
332
|
+
step('v8-v9');
|
|
273
333
|
const have = new Set(db.prepare('PRAGMA table_info(drift_anchors)').all().map((c) => c.name));
|
|
274
334
|
const addCol = (name, ddl) => {
|
|
275
335
|
if (!have.has(name))
|
|
@@ -292,6 +352,7 @@ export function runMigrations(db) {
|
|
|
292
352
|
// without them). Only ALTER if map_nodes already exists; a v10→v12 jump creates
|
|
293
353
|
// it fresh (with the columns) via db.exec(sql) below.
|
|
294
354
|
if (currentVersion > 0 && currentVersion < 12) {
|
|
355
|
+
step('v11-v12');
|
|
295
356
|
const hasMapNodes = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='map_nodes'").get();
|
|
296
357
|
if (hasMapNodes) {
|
|
297
358
|
const have = new Set(db.prepare('PRAGMA table_info(map_nodes)').all().map((c) => c.name));
|
|
@@ -305,6 +366,7 @@ export function runMigrations(db) {
|
|
|
305
366
|
}
|
|
306
367
|
// v12 → v13: edge strength + accounted-for expiry (anti-noise / anti-graveyard).
|
|
307
368
|
if (currentVersion > 0 && currentVersion < 13) {
|
|
369
|
+
step('v12-v13');
|
|
308
370
|
const has = (table, col) => db.prepare(`PRAGMA table_info(${table})`).all().some((c) => c.name === col);
|
|
309
371
|
if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='map_edges'").get()) {
|
|
310
372
|
if (!has('map_edges', 'strength'))
|
|
@@ -322,8 +384,10 @@ export function runMigrations(db) {
|
|
|
322
384
|
// node or a `readme→docs-site` edge. SQLite can't alter a PK/UNIQUE, so drop + recreate;
|
|
323
385
|
// safe because importMap rebuilds both from map.yaml on every run.
|
|
324
386
|
if (currentVersion > 0 && currentVersion < 14) {
|
|
387
|
+
step('v13-v14');
|
|
325
388
|
db.exec('DROP TABLE IF EXISTS map_nodes; DROP TABLE IF EXISTS map_edges;');
|
|
326
389
|
}
|
|
390
|
+
step('schema.sql');
|
|
327
391
|
db.exec(sql);
|
|
328
392
|
if (currentVersion > 0 && currentVersion < 4) {
|
|
329
393
|
db.exec(`INSERT INTO memories_fts(rowid, content) SELECT id, content FROM memories;`);
|
|
@@ -345,13 +409,14 @@ export function runMigrations(db) {
|
|
|
345
409
|
// v4 → v5: entity name normalization for dedup prevention.
|
|
346
410
|
// Adds normalized_name column + backfills from existing entity names.
|
|
347
411
|
if (currentVersion > 0 && currentVersion < 5) {
|
|
348
|
-
|
|
412
|
+
step('v5-backfill');
|
|
413
|
+
migrateV5EntityNormalization(db, opts.mergeDuplicateEntities !== false);
|
|
349
414
|
}
|
|
350
415
|
}
|
|
351
416
|
/**
|
|
352
417
|
* v5 migration: add normalized_name column and backfill + merge duplicates.
|
|
353
418
|
*/
|
|
354
|
-
function migrateV5EntityNormalization(db) {
|
|
419
|
+
function migrateV5EntityNormalization(db, merge) {
|
|
355
420
|
// Column + index already added before db.exec(sql) above.
|
|
356
421
|
// Now backfill normalized_name for all entities.
|
|
357
422
|
const entities = db.prepare('SELECT id, name FROM entities').all();
|
|
@@ -369,6 +434,10 @@ function migrateV5EntityNormalization(db) {
|
|
|
369
434
|
GROUP BY kind, normalized_name
|
|
370
435
|
HAVING COUNT(*) > 1
|
|
371
436
|
`).all();
|
|
437
|
+
if (dupes.length > 0 && !merge) {
|
|
438
|
+
console.log(`[linksee-memory] v5 migration: ${dupes.length} duplicate entity clusters left as they are (not merged)`);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
372
441
|
if (dupes.length > 0) {
|
|
373
442
|
console.log(`[linksee-memory] v5 migration: merging ${dupes.length} duplicate entity clusters`);
|
|
374
443
|
db.transaction(() => {
|
package/dist/lib/hook-runtime.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// a git clone is already stable and is used where it is.
|
|
12
12
|
//
|
|
13
13
|
// Codex review of #15: copying npx's node_modules tree was replaced by this install — a real
|
|
14
|
-
// install has no links back into the cache
|
|
14
|
+
// install has no links back into the cache and its size is known. Old versions are never removed
|
|
15
|
+
// here; `linksee-memory runtime prune` (src/bin/runtime.ts, 0.17.2) removes unreferenced ones.
|
|
15
16
|
// • the new runtime must load better-sqlite3 before anything else happens;
|
|
16
17
|
// • older runtime/<version> folders are NOT deleted (0.17.1); setup says where they are;
|
|
17
18
|
// • on any failure the existing runtime is left as it was and the hooks keep the npx form.
|
|
@@ -143,12 +144,16 @@ export function resolveHookRuntime(o) {
|
|
|
143
144
|
const spec = o.spec ?? `linksee-memory@${o.version}`;
|
|
144
145
|
const staging = join(base, `.tmp-${o.version}-${process.pid}`);
|
|
145
146
|
const stagingRoot = join(staging, 'node_modules', 'linksee-memory');
|
|
146
|
-
|
|
147
|
+
// `previousAt`: the same-version swap moved the old runtime aside and could not put it back —
|
|
148
|
+
// then "left as it was" would be false, so the note says where it is instead (Codex 5th review, P2).
|
|
149
|
+
const fail = (why, previousAt) => {
|
|
147
150
|
try {
|
|
148
151
|
rmSync(staging, { recursive: true, force: true });
|
|
149
152
|
}
|
|
150
153
|
catch { /* ignore */ }
|
|
151
|
-
return npx(
|
|
154
|
+
return npx(previousAt
|
|
155
|
+
? `${why}; kept the npx form. The previous runtime could not be moved back: it is at ${previousAt} — rename it to ${dest} to restore it`
|
|
156
|
+
: `${why}; kept the npx form, existing runtime left as it was`);
|
|
152
157
|
};
|
|
153
158
|
try {
|
|
154
159
|
rmSync(staging, { recursive: true, force: true });
|
|
@@ -183,7 +188,7 @@ export function resolveHookRuntime(o) {
|
|
|
183
188
|
catch { /* reported below */ }
|
|
184
189
|
}
|
|
185
190
|
const restored = !movedAside || existsSync(dest);
|
|
186
|
-
return fail(`could not move the runtime into ${dest} (${e?.message ?? e})
|
|
191
|
+
return fail(`could not move the runtime into ${dest} (${e?.message ?? e})`, restored ? undefined : aside);
|
|
187
192
|
}
|
|
188
193
|
if (movedAside) {
|
|
189
194
|
try {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// test-faults — explicit failure injection for the rename / remove steps of `migrate` and
|
|
2
|
+
// `runtime prune` (0.17.2, Codex second review of #19). Windows file locking cannot be relied on
|
|
3
|
+
// to make these fail in a test (libuv opens files with FILE_SHARE_DELETE), and CI runs on Linux.
|
|
4
|
+
//
|
|
5
|
+
// LINKSEE_TEST_FAIL_RENAME=2,3 the 2nd and 3rd guarded rename in this process throw
|
|
6
|
+
// LINKSEE_TEST_FAIL_RM=1 the 1st guarded recursive remove throws
|
|
7
|
+
//
|
|
8
|
+
// Unset (the normal case) these are plain renameSync / rmSync.
|
|
9
|
+
import { renameSync, rmSync } from 'node:fs';
|
|
10
|
+
const counters = {};
|
|
11
|
+
function inject(kind) {
|
|
12
|
+
const spec = process.env[`LINKSEE_TEST_FAIL_${kind}`];
|
|
13
|
+
if (!spec)
|
|
14
|
+
return;
|
|
15
|
+
counters[kind] = (counters[kind] ?? 0) + 1;
|
|
16
|
+
if (spec.split(',').map((s) => Number(s.trim())).includes(counters[kind])) {
|
|
17
|
+
const e = new Error(`injected ${kind.toLowerCase()} failure #${counters[kind]}`);
|
|
18
|
+
e.code = 'EINJECTED';
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function renameGuarded(from, to) {
|
|
23
|
+
inject('RENAME');
|
|
24
|
+
renameSync(from, to);
|
|
25
|
+
}
|
|
26
|
+
export function rmGuarded(path, opts) {
|
|
27
|
+
inject('RM');
|
|
28
|
+
rmSync(path, opts);
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=test-faults.js.map
|
package/dist/mcp/server.js
CHANGED
|
@@ -64,6 +64,8 @@ const SUBCOMMANDS = {
|
|
|
64
64
|
detect: 'detect-drift.js',
|
|
65
65
|
export: 'export-report.js',
|
|
66
66
|
'share-draft': 'share-draft.js',
|
|
67
|
+
migrate: 'migrate-legacy.js', // 0.17.2: schema 1–5 → current, without starting the server
|
|
68
|
+
runtime: 'runtime.js', // 0.17.2: runtime list | prune
|
|
67
69
|
};
|
|
68
70
|
const subcommand = process.argv[2];
|
|
69
71
|
if (subcommand && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, subcommand)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linksee-memory",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"mcpName": "io.github.michielinksee/linksee-memory",
|
|
5
5
|
"description": "Hand a project over — to your next session, to Cursor/Codex/Gemini, to your successor — with the reasons attached. Local-first memory MCP for coding agents: decisions re-injected before the agent acts, drift detection across every project you run. One SQLite file, nothing leaves your machine.",
|
|
6
6
|
"type": "module",
|