linksee-memory 0.17.1 → 0.18.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/README.md +126 -822
- package/dist/bin/import-sessions.js +13 -3
- 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/bin/sync-session.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/supersede.d.ts +35 -0
- package/dist/lib/supersede.js +130 -0
- package/dist/lib/test-faults.d.ts +3 -0
- package/dist/lib/test-faults.js +30 -0
- package/dist/mcp/server.js +187 -16
- package/dist/skill/SKILL.md +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// linksee-memory runtime list | prune [--yes --scope-confirmed] [--settings <file>]… (0.17.2, L-1-148)
|
|
3
|
+
//
|
|
4
|
+
// setup (run through npx) installs each version under <memory dir>/runtime/<version>/ (about
|
|
5
|
+
// 35 MB each) and never deletes older ones. This command lists them and deletes, only when asked
|
|
6
|
+
// twice over (--yes AND --scope-confirmed), the versions nothing is confirmed to reference.
|
|
7
|
+
//
|
|
8
|
+
// Where a reference can be:
|
|
9
|
+
// user: ~/.claude/settings.json, ~/.claude/settings.local.json, ~/.claude.json, ~/.codex/config.toml
|
|
10
|
+
// project: <cwd>/.claude/settings.json, <cwd>/.claude/settings.local.json
|
|
11
|
+
// extra: every --settings <file>
|
|
12
|
+
// Project settings of OTHER repos (where `setup --project` was run) cannot be found from here.
|
|
13
|
+
// So --yes alone deletes nothing: --scope-confirmed says "I checked the other repos' .claude/
|
|
14
|
+
// settings*.json, or passed them all with --settings" (Codex review of #19).
|
|
15
|
+
//
|
|
16
|
+
// How a file is read (Codex review of #19: no raw-text grep as the only test):
|
|
17
|
+
// • JSON: parsed; every string (keys and values, at any depth) is checked for runtime/<version>
|
|
18
|
+
// after normalizing \ → /. Parse failure → could not confirm.
|
|
19
|
+
// • TOML (~/.codex/config.toml): a deliberately small line reader (Codex second review of #19:
|
|
20
|
+
// do not be clever). The ONLY lines it accepts are: blank, `# comment`, a `[table]` /
|
|
21
|
+
// `[[array]]` header of bare keys, and `bare.key = <value>` where the value is one string
|
|
22
|
+
// ("…" with escapes decoded, or '…'), a one-line array of such strings, a number, or
|
|
23
|
+
// true/false. Anything else - a quoted key, an inline table, a multi-line array or string, a
|
|
24
|
+
// date, an unknown form - makes the whole file "could not confirm". Every string it read
|
|
25
|
+
// (whatever the key) is checked for a runtime path.
|
|
26
|
+
// • In both, the raw text is ALSO searched (a match there only keeps a version, never deletes one).
|
|
27
|
+
// A default file that does not exist (ENOENT) references nothing. Anything else that cannot be
|
|
28
|
+
// confirmed → nothing is deleted, the reason is printed, exit 1.
|
|
29
|
+
// Never touched: .tmp-* (an install in progress), *.old-* (kept from a failed same-version swap),
|
|
30
|
+
// any name that is not a version, and the version this command runs from.
|
|
31
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
32
|
+
import { homedir } from 'node:os';
|
|
33
|
+
import { join, resolve } from 'node:path';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
import { dirBytes, formatBytes } from '../lib/hook-runtime.js';
|
|
36
|
+
import { renameGuarded, rmGuarded } from '../lib/test-faults.js';
|
|
37
|
+
const VERSION_DIR = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]+)?$/;
|
|
38
|
+
export function defaultSources(home, cwd) {
|
|
39
|
+
return [
|
|
40
|
+
join(home, '.claude', 'settings.json'),
|
|
41
|
+
join(home, '.claude', 'settings.local.json'),
|
|
42
|
+
join(home, '.claude.json'),
|
|
43
|
+
join(home, '.codex', 'config.toml'),
|
|
44
|
+
join(cwd, '.claude', 'settings.json'),
|
|
45
|
+
join(cwd, '.claude', 'settings.local.json'),
|
|
46
|
+
].map((path) => ({ path, explicit: false }));
|
|
47
|
+
}
|
|
48
|
+
export const normPath = (t) => {
|
|
49
|
+
const s = t.replace(/\\+/g, '/');
|
|
50
|
+
return process.platform === 'win32' ? s.toLowerCase() : s;
|
|
51
|
+
};
|
|
52
|
+
function jsonStrings(v, out) {
|
|
53
|
+
if (typeof v === 'string')
|
|
54
|
+
out.push(v);
|
|
55
|
+
else if (Array.isArray(v))
|
|
56
|
+
for (const x of v)
|
|
57
|
+
jsonStrings(x, out);
|
|
58
|
+
else if (v && typeof v === 'object')
|
|
59
|
+
for (const [k, x] of Object.entries(v)) {
|
|
60
|
+
out.push(k);
|
|
61
|
+
jsonStrings(x, out);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Parse one TOML string token at s[i]; returns [value, nextIndex] or null. */
|
|
65
|
+
function tomlString(s, i) {
|
|
66
|
+
if (s[i] === "'") {
|
|
67
|
+
if (s.startsWith("'''", i))
|
|
68
|
+
return null; // multi-line literal: not handled → unconfirmable
|
|
69
|
+
const j = s.indexOf("'", i + 1);
|
|
70
|
+
return j < 0 ? null : [s.slice(i + 1, j), j + 1];
|
|
71
|
+
}
|
|
72
|
+
if (s[i] !== '"' || s.startsWith('"""', i))
|
|
73
|
+
return null;
|
|
74
|
+
let out = '';
|
|
75
|
+
let j = i + 1;
|
|
76
|
+
while (j < s.length) {
|
|
77
|
+
const c = s[j];
|
|
78
|
+
if (c === '"')
|
|
79
|
+
return [out, j + 1];
|
|
80
|
+
if (c === '\\') {
|
|
81
|
+
const n = s[j + 1];
|
|
82
|
+
const map = { '"': '"', '\\': '\\', '/': '/', b: '\b', t: '\t', n: '\n', f: '\f', r: '\r' };
|
|
83
|
+
if (n in map) {
|
|
84
|
+
out += map[n];
|
|
85
|
+
j += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (n === 'u' || n === 'U') {
|
|
89
|
+
const len = n === 'u' ? 4 : 8;
|
|
90
|
+
const hex = s.slice(j + 2, j + 2 + len);
|
|
91
|
+
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== len)
|
|
92
|
+
return null;
|
|
93
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
94
|
+
j += 2 + len;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
out += c;
|
|
100
|
+
j++;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const BARE = '[A-Za-z0-9_-]+(?:\\s*\\.\\s*[A-Za-z0-9_-]+)*';
|
|
105
|
+
const TOML_HEADER = new RegExp(`^\\[\\[?\\s*${BARE}\\s*\\]\\]?\\s*(#.*)?$`);
|
|
106
|
+
const TOML_KV = new RegExp(`^(${BARE})\\s*=\\s*(.*)$`);
|
|
107
|
+
const TOML_SCALAR = /^(?:true|false|[+-]?(?:0|[1-9](?:_?\d)*)(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d+)?)/;
|
|
108
|
+
/**
|
|
109
|
+
* Strict, small TOML line reader. Returns every string value it read, or an error naming the first
|
|
110
|
+
* line it does not accept (then the caller treats the file as "could not confirm").
|
|
111
|
+
*/
|
|
112
|
+
export function tomlStrings(text) {
|
|
113
|
+
const strings = [];
|
|
114
|
+
const lines = text.replace(/^\uFEFF/, '').split(/\r?\n/);
|
|
115
|
+
for (let n = 0; n < lines.length; n++) {
|
|
116
|
+
const line = lines[n].trim();
|
|
117
|
+
const bad = (why) => ({ strings, error: `line ${n + 1}: ${why}` });
|
|
118
|
+
if (line === '' || line.startsWith('#'))
|
|
119
|
+
continue;
|
|
120
|
+
if (line.startsWith('[')) {
|
|
121
|
+
if (TOML_HEADER.test(line))
|
|
122
|
+
continue;
|
|
123
|
+
return bad('not a [table] header of bare keys');
|
|
124
|
+
}
|
|
125
|
+
const m = line.match(TOML_KV);
|
|
126
|
+
if (!m)
|
|
127
|
+
return bad('not `bare_key = value` (quoted keys are not read)');
|
|
128
|
+
const rest = m[2];
|
|
129
|
+
let i = 0;
|
|
130
|
+
const skip = () => { while (i < rest.length && (rest[i] === ' ' || rest[i] === '\t'))
|
|
131
|
+
i++; };
|
|
132
|
+
const endOk = () => { skip(); return i >= rest.length || rest[i] === '#'; };
|
|
133
|
+
if (rest[0] === '"' || rest[0] === "'") {
|
|
134
|
+
const t = tomlString(rest, 0);
|
|
135
|
+
if (!t)
|
|
136
|
+
return bad('a string it cannot read (multi-line or unterminated)');
|
|
137
|
+
strings.push(t[0]);
|
|
138
|
+
i = t[1];
|
|
139
|
+
}
|
|
140
|
+
else if (rest[0] === '[') {
|
|
141
|
+
i = 1;
|
|
142
|
+
skip();
|
|
143
|
+
while (rest[i] !== ']') {
|
|
144
|
+
const t = tomlString(rest, i);
|
|
145
|
+
if (!t)
|
|
146
|
+
return bad('an array that is not one line of strings');
|
|
147
|
+
strings.push(t[0]);
|
|
148
|
+
i = t[1];
|
|
149
|
+
skip();
|
|
150
|
+
if (rest[i] === ',') {
|
|
151
|
+
i++;
|
|
152
|
+
skip();
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (rest[i] !== ']')
|
|
156
|
+
return bad('an array that is not one line of strings');
|
|
157
|
+
}
|
|
158
|
+
i++;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const sm = rest.match(TOML_SCALAR);
|
|
162
|
+
if (!sm)
|
|
163
|
+
return bad('a value type it does not read (inline table, date, ...)');
|
|
164
|
+
i = sm[0].length;
|
|
165
|
+
}
|
|
166
|
+
if (!endOk())
|
|
167
|
+
return bad('unexpected text after the value');
|
|
168
|
+
}
|
|
169
|
+
return { strings };
|
|
170
|
+
}
|
|
171
|
+
/** Read every source into normalized strings, or say why it could not be confirmed. */
|
|
172
|
+
export function readSources(sources) {
|
|
173
|
+
const texts = [];
|
|
174
|
+
const problems = [];
|
|
175
|
+
for (const s of sources) {
|
|
176
|
+
let raw;
|
|
177
|
+
try {
|
|
178
|
+
raw = readFileSync(s.path, 'utf8');
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
const code = e?.code;
|
|
182
|
+
if (code === 'ENOENT' && !s.explicit)
|
|
183
|
+
continue; // absent → references nothing
|
|
184
|
+
problems.push(`${s.path}: ${code === 'ENOENT' ? 'not found (passed with --settings)' : `could not read (${code ?? e?.message})`}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const strings = [raw];
|
|
188
|
+
if (/\.toml$/i.test(s.path)) {
|
|
189
|
+
const t = tomlStrings(raw);
|
|
190
|
+
if (t.error) {
|
|
191
|
+
problems.push(`${s.path}: could not read as TOML (${t.error})`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
strings.push(...t.strings);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
let parsed;
|
|
198
|
+
try {
|
|
199
|
+
parsed = JSON.parse(raw.replace(/^/, ''));
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
problems.push(`${s.path}: not valid JSON (${e?.message?.split('\n')[0]})`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
jsonStrings(parsed, strings);
|
|
206
|
+
}
|
|
207
|
+
texts.push({ path: s.path, strings: strings.map(normPath) });
|
|
208
|
+
}
|
|
209
|
+
return { texts, problems };
|
|
210
|
+
}
|
|
211
|
+
export function listRuntimes(memoryDir) {
|
|
212
|
+
const base = join(memoryDir, 'runtime');
|
|
213
|
+
if (!existsSync(base))
|
|
214
|
+
return { base, versions: [], others: [] };
|
|
215
|
+
let names;
|
|
216
|
+
try {
|
|
217
|
+
names = readdirSync(base);
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
return { base, versions: [], others: [], error: e?.message };
|
|
221
|
+
}
|
|
222
|
+
const versions = [];
|
|
223
|
+
const others = [];
|
|
224
|
+
for (const n of names.sort()) {
|
|
225
|
+
let dir = false;
|
|
226
|
+
try {
|
|
227
|
+
dir = statSync(join(base, n)).isDirectory();
|
|
228
|
+
}
|
|
229
|
+
catch { /* treat as other */ }
|
|
230
|
+
(dir && VERSION_DIR.test(n) ? versions : others).push(n);
|
|
231
|
+
}
|
|
232
|
+
return { base, versions, others };
|
|
233
|
+
}
|
|
234
|
+
export function pruneRuntimes(o) {
|
|
235
|
+
const log = o.log ?? ((l) => console.log(l));
|
|
236
|
+
const { base, versions, others, error } = listRuntimes(o.memoryDir);
|
|
237
|
+
if (error) {
|
|
238
|
+
log(`Could not list ${base}: ${error}. Nothing deleted.`);
|
|
239
|
+
return { code: 1, entries: [], problems: [error] };
|
|
240
|
+
}
|
|
241
|
+
if (versions.length === 0) {
|
|
242
|
+
log(`No runtime versions under ${base}.`);
|
|
243
|
+
for (const n of others)
|
|
244
|
+
log(` (left alone) ${n}`);
|
|
245
|
+
return { code: 0, entries: [], problems: [] };
|
|
246
|
+
}
|
|
247
|
+
const { texts, problems } = readSources(o.sources);
|
|
248
|
+
const self = o.selfRoot ? normPath(safeReal(o.selfRoot)) : '';
|
|
249
|
+
const entries = versions.map((v) => {
|
|
250
|
+
const dir = join(base, v);
|
|
251
|
+
const needle = `runtime/${normPath(v)}`;
|
|
252
|
+
const hit = (s) => s.includes(needle + '/') || s.endsWith(needle) || s.includes(needle + '"') || s.includes(needle + "'");
|
|
253
|
+
const referencedBy = texts.filter((t) => t.strings.some(hit)).map((t) => t.path);
|
|
254
|
+
const e = { version: v, dir, bytes: dirBytes(dir), referencedBy, decision: 'delete' };
|
|
255
|
+
const dirNorm = normPath(safeReal(dir)) + '/';
|
|
256
|
+
if (self && (self + '/').startsWith(dirNorm)) {
|
|
257
|
+
e.decision = 'keep-running';
|
|
258
|
+
e.reason = 'this command is running from it';
|
|
259
|
+
}
|
|
260
|
+
else if (referencedBy.length) {
|
|
261
|
+
e.decision = 'keep-referenced';
|
|
262
|
+
e.reason = `referenced by ${referencedBy.join(', ')}`;
|
|
263
|
+
}
|
|
264
|
+
else if (problems.length) {
|
|
265
|
+
e.decision = 'keep-unconfirmed';
|
|
266
|
+
e.reason = 'could not confirm that nothing references it (see below)';
|
|
267
|
+
}
|
|
268
|
+
return e;
|
|
269
|
+
});
|
|
270
|
+
const willDelete = o.yes && !!o.scopeConfirmed && problems.length === 0;
|
|
271
|
+
log(`Runtimes under ${base}:`);
|
|
272
|
+
for (const e of entries) {
|
|
273
|
+
const what = e.decision === 'delete'
|
|
274
|
+
? (willDelete ? 'delete' : 'no reference found (deletable with --yes --scope-confirmed)')
|
|
275
|
+
: `keep — ${e.reason}`;
|
|
276
|
+
log(` ${e.version.padEnd(10)} ${formatBytes(e.bytes).padStart(7)} ${what}`);
|
|
277
|
+
}
|
|
278
|
+
for (const n of others)
|
|
279
|
+
log(` (left alone) ${n}`);
|
|
280
|
+
if (problems.length) {
|
|
281
|
+
log(`Could not confirm references, so nothing is deleted:`);
|
|
282
|
+
for (const p of problems)
|
|
283
|
+
log(` • ${p}`);
|
|
284
|
+
}
|
|
285
|
+
log(`Checked: ${o.sources.map((s) => s.path).join(', ')}.`);
|
|
286
|
+
log(`Not checked: project settings of other repos. If you ran \`setup --project\` elsewhere, pass each repo's .claude/settings.json (and settings.local.json) with --settings.`);
|
|
287
|
+
let code = problems.length ? 1 : 0;
|
|
288
|
+
if (o.yes && !o.scopeConfirmed) {
|
|
289
|
+
log(`Nothing deleted: add --scope-confirmed once you have checked (or passed with --settings) the .claude/settings*.json of every other repo where you ran setup --project.`);
|
|
290
|
+
return { code: 1, entries, problems };
|
|
291
|
+
}
|
|
292
|
+
if (willDelete) {
|
|
293
|
+
for (const e of entries.filter((x) => x.decision === 'delete')) {
|
|
294
|
+
// Rename first so the version disappears at once (no half-deleted runtime under its own
|
|
295
|
+
// name), then remove the renamed folder.
|
|
296
|
+
const aside = join(base, `.pruning-${e.version}-${process.pid}`);
|
|
297
|
+
try {
|
|
298
|
+
renameGuarded(e.dir, aside);
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
e.decision = 'delete-failed';
|
|
302
|
+
e.reason = err?.message;
|
|
303
|
+
code = 1;
|
|
304
|
+
log(` could not delete ${e.version} (${e.reason}); left as it was`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
rmGuarded(aside, { recursive: true, force: true });
|
|
309
|
+
if (existsSync(aside))
|
|
310
|
+
throw new Error('folder still there');
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
e.decision = 'delete-failed';
|
|
314
|
+
e.reason = `moved to ${aside} but not fully removed (${err?.message})`;
|
|
315
|
+
code = 1;
|
|
316
|
+
log(` ${e.version}: ${e.reason}; delete that folder by hand`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
e.decision = 'deleted';
|
|
320
|
+
log(` deleted ${e.version} (${formatBytes(e.bytes)})`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { code, entries, problems };
|
|
324
|
+
}
|
|
325
|
+
function safeReal(p) {
|
|
326
|
+
try {
|
|
327
|
+
return realpathSync(p);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return resolve(p);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function usage() {
|
|
334
|
+
return `Usage: npx -y linksee-memory runtime list [--settings <file>]...
|
|
335
|
+
npx -y linksee-memory runtime prune [--yes --scope-confirmed] [--settings <file>]...
|
|
336
|
+
|
|
337
|
+
Lists <memory dir>/runtime/<version> (installed by setup when run through npx) with sizes and
|
|
338
|
+
what references each one. prune deletes only versions no settings file references, and only with
|
|
339
|
+
both --yes and --scope-confirmed (= you checked, or passed with --settings, the .claude/settings*.json
|
|
340
|
+
of every other repo where you ran setup --project). If any settings file cannot be read or parsed,
|
|
341
|
+
nothing is deleted.`;
|
|
342
|
+
}
|
|
343
|
+
function main(argv) {
|
|
344
|
+
const [cmd, ...rest] = argv;
|
|
345
|
+
if (!cmd || cmd === '--help' || cmd === '-h' || (cmd !== 'list' && cmd !== 'prune')) {
|
|
346
|
+
console.log(usage());
|
|
347
|
+
return cmd && cmd !== '--help' && cmd !== '-h' ? 1 : 0;
|
|
348
|
+
}
|
|
349
|
+
const extra = [];
|
|
350
|
+
let yes = false;
|
|
351
|
+
let scopeConfirmed = false;
|
|
352
|
+
for (let i = 0; i < rest.length; i++) {
|
|
353
|
+
if (rest[i] === '--yes' && cmd === 'prune')
|
|
354
|
+
yes = true;
|
|
355
|
+
else if (rest[i] === '--scope-confirmed' && cmd === 'prune')
|
|
356
|
+
scopeConfirmed = true;
|
|
357
|
+
else if (rest[i] === '--settings' && rest[i + 1])
|
|
358
|
+
extra.push({ path: resolve(rest[++i]), explicit: true });
|
|
359
|
+
else {
|
|
360
|
+
console.error(`Unknown argument: ${rest[i]}\n\n${usage()}`);
|
|
361
|
+
return 1;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const home = homedir();
|
|
365
|
+
const memoryDir = process.env.LINKSEE_MEMORY_DIR ?? join(home, '.linksee-memory');
|
|
366
|
+
const selfRoot = join(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
367
|
+
return pruneRuntimes({ memoryDir, sources: [...defaultSources(home, process.cwd()), ...extra], yes, scopeConfirmed, selfRoot }).code;
|
|
368
|
+
}
|
|
369
|
+
const isMain = (() => {
|
|
370
|
+
try {
|
|
371
|
+
const a = process.argv[1] ? realpathSync(process.argv[1]) : '';
|
|
372
|
+
const b = realpathSync(fileURLToPath(import.meta.url));
|
|
373
|
+
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
})();
|
|
379
|
+
if (isMain)
|
|
380
|
+
process.exit(main(process.argv.slice(2)));
|
|
381
|
+
//# sourceMappingURL=runtime.js.map
|
package/dist/bin/setup.js
CHANGED
|
@@ -457,7 +457,7 @@ console.log('');
|
|
|
457
457
|
// removing one a hook still uses). Say what they cost and how to remove old ones by hand.
|
|
458
458
|
if (runtime.mode === 'pinned' && runtime.runtimeBase && !dryRun) {
|
|
459
459
|
console.log(` ${DIM}• Hooks run from ${runtime.runtimeBase}${sep}<version> (${runtime.bytes !== undefined ? formatBytes(runtime.bytes) : 'about 35 MB'} each). Older versions are not removed automatically;${RESET}`);
|
|
460
|
-
console.log(` ${DIM}
|
|
460
|
+
console.log(` ${DIM} List them and remove the ones no settings file uses: npx -y linksee-memory runtime prune (see its output before adding --yes --scope-confirmed)${RESET}`);
|
|
461
461
|
console.log('');
|
|
462
462
|
}
|
|
463
463
|
handWiredNotice(SETTINGS_PATH);
|
package/dist/bin/sync-session.js
CHANGED
|
@@ -77,7 +77,7 @@ async function main() {
|
|
|
77
77
|
log(`transcript missing: ${transcriptPath}`);
|
|
78
78
|
process.exit(0);
|
|
79
79
|
}
|
|
80
|
-
const r = spawnSync(process.execPath, [importerPath, '--session-file', transcriptPath], {
|
|
80
|
+
const r = spawnSync(process.execPath, [importerPath, '--session-file', transcriptPath, '--via', 'stop-hook'], {
|
|
81
81
|
encoding: 'utf8',
|
|
82
82
|
timeout: 30000,
|
|
83
83
|
});
|
package/dist/db/migrate.d.ts
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
export declare function getDbPath(): string;
|
|
3
|
+
/** This package's version (dist/db/migrate.js → ../../package.json), for exact-version guidance. */
|
|
4
|
+
export declare function packageVersion(): string;
|
|
5
|
+
/**
|
|
6
|
+
* The command that migrates / recovers this file. Pinned to this exact version: an unpinned
|
|
7
|
+
* `npx linksee-memory` could resolve to a cached older version, and versions before 0.17.2 have
|
|
8
|
+
* no `migrate` subcommand — they would start the MCP server instead (L-1-130).
|
|
9
|
+
*/
|
|
10
|
+
export declare function migrateCommand(): string;
|
|
11
|
+
/** First version with the `migrate` subcommand. */
|
|
12
|
+
export declare const MIGRATE_SINCE = "0.17.2";
|
|
13
|
+
/**
|
|
14
|
+
* Decide, WITHOUT a writable connection, whether this file may be opened for writing at all.
|
|
15
|
+
* (0.17.1, Codex re-review of #15.) A writable connection that is the last one to close
|
|
16
|
+
* checkpoints a leftover WAL into memory.db and deletes it, and rolls back a hot journal — both
|
|
17
|
+
* are writes to a file we are about to refuse. So the schema is read through a read-only
|
|
18
|
+
* connection first (SQLITE_OPEN_READONLY: no checkpoint, no rollback, memory.db and -wal untouched).
|
|
19
|
+
* • schema 1–5 → LegacySchemaError
|
|
20
|
+
* • a hot rollback journal → HotJournalError (it cannot be read without rolling back)
|
|
21
|
+
* • not a DB → return; the normal open's corruption path moves it aside
|
|
22
|
+
* • native addon failed → rethrown as is
|
|
23
|
+
* • anything else (busy, locked, unreadable) → PrecheckError: could not check, so no writing
|
|
24
|
+
* When the file is refused, the -wal/-shm that the read-only open itself created are removed
|
|
25
|
+
* again, so the folder is left as it was.
|
|
26
|
+
*/
|
|
27
|
+
export type ReadonlyProbe = {
|
|
28
|
+
kind: 'missing';
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'notadb';
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'hot';
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'version';
|
|
35
|
+
version: number;
|
|
36
|
+
};
|
|
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 declare function probeReadonly(path: string): {
|
|
43
|
+
probe: ReadonlyProbe;
|
|
44
|
+
tidy: () => void;
|
|
45
|
+
};
|
|
3
46
|
export declare function openDb(): Database.Database;
|
|
4
47
|
/** Oldest schema this version migrates in place. Older files are refused before any write. */
|
|
5
48
|
export declare const MIN_SUPPORTED_SCHEMA = 6;
|
|
@@ -14,7 +57,7 @@ export declare class DbRefusedError extends Error {
|
|
|
14
57
|
* No old version is recommended as a stepping stone: 0.8.0–0.17.0 stop on `no such table:
|
|
15
58
|
* drift_anchors` after partly altering the file, and 0.7.2 has no subcommands — `npx
|
|
16
59
|
* linksee-memory@0.7.2 <anything>` starts its MCP server, whose start-up consolidate can delete
|
|
17
|
-
* original rows (Codex review of PR #15).
|
|
60
|
+
* original rows (Codex review of PR #15). 0.17.2: `linksee-memory migrate` (src/bin/migrate-legacy.ts).
|
|
18
61
|
*/
|
|
19
62
|
export declare class LegacySchemaError extends DbRefusedError {
|
|
20
63
|
readonly schemaVersion: number;
|
|
@@ -31,4 +74,12 @@ export declare class HotJournalError extends DbRefusedError {
|
|
|
31
74
|
}
|
|
32
75
|
/** Read meta.schema_version without writing (0 when the table or row is absent). */
|
|
33
76
|
export declare function readSchemaVersion(db: Database.Database): number;
|
|
34
|
-
export
|
|
77
|
+
export type MigrateOptions = {
|
|
78
|
+
/** Migrate schema 1–5 too. Only the `migrate` command sets this (inside its own transaction). */
|
|
79
|
+
allowLegacy?: boolean;
|
|
80
|
+
/** v4→v5 dedupe merges duplicate entities (deletes rows). Default true; `migrate` passes false. */
|
|
81
|
+
mergeDuplicateEntities?: boolean;
|
|
82
|
+
/** Called before each step (tests inject a failure here). */
|
|
83
|
+
onStep?: (step: string, db: Database.Database) => void;
|
|
84
|
+
};
|
|
85
|
+
export declare function runMigrations(db: Database.Database, opts?: MigrateOptions): void;
|