claude-mem-lite 2.72.0 → 2.73.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/adopt-cli.mjs +22 -1
- package/cli/activity.mjs +6 -2
- package/cli/fts-check.mjs +7 -1
- package/install.mjs +98 -36
- package/mem-cli.mjs +163 -35
- package/package.json +1 -1
- package/schema.mjs +4 -1
package/adopt-cli.mjs
CHANGED
|
@@ -192,9 +192,22 @@ function statusAll() {
|
|
|
192
192
|
/**
|
|
193
193
|
* cmdUnadopt — precise removal of sentinel section + plugin doc.
|
|
194
194
|
* Exit code stays 0: unadopt is idempotent; "absent" isn't an error.
|
|
195
|
+
*
|
|
196
|
+
* Flags:
|
|
197
|
+
* --all Operate on every memdir under ~/.claude/projects/*\/memory/
|
|
198
|
+
* --status Read-only: list currently-adopted memdirs (mirrors `adopt --status`).
|
|
199
|
+
* --dry-run Preview what would be removed; no filesystem writes.
|
|
200
|
+
*
|
|
201
|
+
* Pre-fix history: unrecognized flags (e.g. `--status` extrapolated from `adopt --status`,
|
|
202
|
+
* or `--dry-run` extrapolated from `adopt --dry-run`) were silently ignored and the
|
|
203
|
+
* destructive default ran anyway, removing the sentinel block when the user expected
|
|
204
|
+
* a read-only probe.
|
|
195
205
|
*/
|
|
196
206
|
export function cmdUnadopt(args = []) {
|
|
207
|
+
if (hasFlag(args, '--status')) return statusAll();
|
|
208
|
+
|
|
197
209
|
const all = hasFlag(args, '--all');
|
|
210
|
+
const dryRun = hasFlag(args, '--dry-run');
|
|
198
211
|
const targets = all
|
|
199
212
|
? listAllMemdirs().map((m) => m.memdir)
|
|
200
213
|
: [memdirPath(detectCwd())];
|
|
@@ -206,6 +219,13 @@ export function cmdUnadopt(args = []) {
|
|
|
206
219
|
|
|
207
220
|
let removed = 0, absent = 0;
|
|
208
221
|
for (const memdir of targets) {
|
|
222
|
+
if (dryRun) {
|
|
223
|
+
const adopted = isAdopted(memdir, PLUGIN_SLUG);
|
|
224
|
+
const action = adopted ? 'would-remove' : 'absent';
|
|
225
|
+
log(`[unadopt --dry-run] ${memdir} → ${action}`);
|
|
226
|
+
if (adopted) removed++; else absent++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
209
229
|
const r = removePluginSection(memdir, PLUGIN_SLUG);
|
|
210
230
|
removePluginDoc(memdir, PLUGIN_SLUG);
|
|
211
231
|
if (r.action === 'removed') removed++;
|
|
@@ -214,5 +234,6 @@ export function cmdUnadopt(args = []) {
|
|
|
214
234
|
}
|
|
215
235
|
|
|
216
236
|
log('');
|
|
217
|
-
|
|
237
|
+
const verb = dryRun ? 'would remove' : 'removed';
|
|
238
|
+
log(`[unadopt${dryRun ? ' --dry-run' : ''}] ${targets.length} target(s): ${removed} ${verb}, ${absent} absent`);
|
|
218
239
|
}
|
package/cli/activity.mjs
CHANGED
|
@@ -20,7 +20,7 @@ function formatActivityResults(rows) {
|
|
|
20
20
|
export async function cmdActivity(db, args) {
|
|
21
21
|
const sub = args[0];
|
|
22
22
|
if (!sub) {
|
|
23
|
-
fail('[mem] Usage: claude-mem-lite activity <save|search|recent|show> ...');
|
|
23
|
+
fail('[mem] Usage: claude-mem-lite activity <save|search|recent|show|delete> ...');
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -105,7 +105,11 @@ export async function cmdActivity(db, args) {
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
const row = getEvent(db, id);
|
|
108
|
-
|
|
108
|
+
if (row) {
|
|
109
|
+
out(JSON.stringify(row, null, 2));
|
|
110
|
+
} else {
|
|
111
|
+
out(`[mem] activity show: event #${id} Not found`);
|
|
112
|
+
}
|
|
109
113
|
return;
|
|
110
114
|
}
|
|
111
115
|
|
package/cli/fts-check.mjs
CHANGED
|
@@ -7,10 +7,16 @@ import { parseArgs, out, fail } from './common.mjs';
|
|
|
7
7
|
export function cmdFtsCheck(db, args) {
|
|
8
8
|
const { positional } = parseArgs(args);
|
|
9
9
|
const action = positional[0];
|
|
10
|
-
if (!action
|
|
10
|
+
if (!action) {
|
|
11
11
|
fail('[mem] Usage: claude-mem-lite fts-check <check|rebuild>');
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
+
if (!['check', 'rebuild'].includes(action)) {
|
|
15
|
+
// Tell the user what was wrong rather than dumping the usage — they passed
|
|
16
|
+
// something concrete, the error should name the invalid token.
|
|
17
|
+
fail(`[mem] Invalid action "${action}". Use: check, rebuild`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
if (action === 'check') {
|
|
16
22
|
const result = checkFTSIntegrity(db);
|
package/install.mjs
CHANGED
|
@@ -1057,18 +1057,21 @@ async function cleanupHooks() {
|
|
|
1057
1057
|
// ─── Status ─────────────────────────────────────────────────────────────────
|
|
1058
1058
|
|
|
1059
1059
|
async function status() {
|
|
1060
|
-
|
|
1060
|
+
// Dogfood-8: support --json so CI / setup scripts can probe install state
|
|
1061
|
+
// without scraping text. Collect each check as a structured record first,
|
|
1062
|
+
// then print text OR JSON. Text path keeps identical wording so existing
|
|
1063
|
+
// users / docs / screenshots stay correct.
|
|
1064
|
+
const json = flags.has('--json');
|
|
1065
|
+
const checks = [];
|
|
1066
|
+
const push = (level, key, message, extra = {}) => checks.push({ level, key, message, ...extra });
|
|
1061
1067
|
|
|
1062
1068
|
// MCP
|
|
1063
1069
|
try {
|
|
1064
1070
|
const list = execFileSync('claude', ['mcp', 'list'], { encoding: 'utf8' });
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
} else {
|
|
1068
|
-
fail('MCP server: not registered');
|
|
1069
|
-
}
|
|
1071
|
+
const registered = list.includes('mem:') || list.includes('mem ');
|
|
1072
|
+
push(registered ? 'ok' : 'fail', 'mcp', registered ? 'MCP server: registered' : 'MCP server: not registered', { registered });
|
|
1070
1073
|
} catch {
|
|
1071
|
-
|
|
1074
|
+
push('warn', 'mcp', 'Could not check MCP status', { registered: null });
|
|
1072
1075
|
}
|
|
1073
1076
|
|
|
1074
1077
|
// Hooks
|
|
@@ -1077,34 +1080,29 @@ async function status() {
|
|
|
1077
1080
|
const pluginDisabled = isPluginExplicitlyDisabled(settings);
|
|
1078
1081
|
const pluginEnabled = settings.enabledPlugins?.[PLUGIN_KEY] === true;
|
|
1079
1082
|
|
|
1080
|
-
if (pluginEnabled) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
warn('Plugin: disabled in settings');
|
|
1084
|
-
} else {
|
|
1085
|
-
warn('Plugin: not present in enabledPlugins');
|
|
1086
|
-
}
|
|
1083
|
+
if (pluginEnabled) push('ok', 'plugin', 'Plugin: enabled in settings', { enabled: true, disabled: false });
|
|
1084
|
+
else if (pluginDisabled) push('warn', 'plugin', 'Plugin: disabled in settings', { enabled: false, disabled: true });
|
|
1085
|
+
else push('warn', 'plugin', 'Plugin: not present in enabledPlugins', { enabled: false, disabled: false });
|
|
1087
1086
|
|
|
1088
1087
|
if (hasHooks && pluginDisabled) {
|
|
1089
|
-
|
|
1088
|
+
push('warn', 'hooks', 'Hooks: still configured in settings.json while plugin is disabled (runtime ignores them; run cleanup-hooks or uninstall to clean up)', { configured: true });
|
|
1090
1089
|
} else if (hasHooks) {
|
|
1091
|
-
|
|
1090
|
+
push('ok', 'hooks', 'Hooks: configured', { configured: true });
|
|
1092
1091
|
} else if (pluginDisabled) {
|
|
1093
|
-
|
|
1092
|
+
push('ok', 'hooks', 'Hooks: not configured', { configured: false });
|
|
1094
1093
|
} else {
|
|
1095
|
-
|
|
1094
|
+
push('fail', 'hooks', 'Hooks: not configured', { configured: false });
|
|
1096
1095
|
}
|
|
1097
1096
|
|
|
1098
1097
|
// Plugin cache pollution: populated hooks.json in cache AND install.mjs-managed
|
|
1099
1098
|
// settings.json hooks → runtime registers both → duplicate firing.
|
|
1100
1099
|
const polluted = scanPluginCacheHookPollution();
|
|
1101
1100
|
if (polluted.length > 0 && hasHooks) {
|
|
1102
|
-
fail
|
|
1101
|
+
push('fail', 'plugin_cache', `Plugin cache: stale hooks.json in version(s) ${polluted.join(', ')} — duplicate firing alongside settings.json (run 'install' to auto-clear)`, { polluted_versions: polluted });
|
|
1103
1102
|
} else if (polluted.length > 0) {
|
|
1104
|
-
|
|
1105
|
-
ok(`Plugin cache: ${polluted.length} version(s) with hooks.json (plugin-only mode)`);
|
|
1103
|
+
push('ok', 'plugin_cache', `Plugin cache: ${polluted.length} version(s) with hooks.json (plugin-only mode)`, { polluted_versions: polluted });
|
|
1106
1104
|
} else if (pluginEnabled || hasHooks) {
|
|
1107
|
-
|
|
1105
|
+
push('ok', 'plugin_cache', 'Plugin cache: no stale hooks.json (no duplicate firing)', { polluted_versions: [] });
|
|
1108
1106
|
}
|
|
1109
1107
|
|
|
1110
1108
|
// Database
|
|
@@ -1115,35 +1113,67 @@ async function status() {
|
|
|
1115
1113
|
const obs = db.prepare('SELECT COUNT(*) as c FROM observations').get();
|
|
1116
1114
|
const sess = db.prepare('SELECT COUNT(*) as c FROM session_summaries').get();
|
|
1117
1115
|
db.close();
|
|
1118
|
-
ok
|
|
1116
|
+
push('ok', 'database', `Database: ${obs.c} observations, ${sess.c} sessions`, { exists: true, observations: obs.c, sessions: sess.c });
|
|
1119
1117
|
} catch (e) {
|
|
1120
|
-
|
|
1118
|
+
push('warn', 'database', 'Database: exists but check failed — ' + e.message, { exists: true, error: e.message });
|
|
1121
1119
|
}
|
|
1122
1120
|
} else {
|
|
1123
|
-
|
|
1121
|
+
push('warn', 'database', 'Database: not found', { exists: false });
|
|
1124
1122
|
}
|
|
1125
1123
|
|
|
1126
1124
|
// CLI
|
|
1127
1125
|
try {
|
|
1128
1126
|
execFileSync('claude-mem-lite', ['--help'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
|
|
1129
|
-
|
|
1127
|
+
push('ok', 'cli', 'CLI: claude-mem-lite command available', { available: true });
|
|
1130
1128
|
} catch {
|
|
1131
|
-
|
|
1129
|
+
push('warn', 'cli', 'CLI: command not on PATH — run install again to create symlink', { available: false });
|
|
1132
1130
|
}
|
|
1133
1131
|
|
|
1134
1132
|
// Old system
|
|
1135
1133
|
const vectorDb = join(OLD_DATA_DIR, 'vector-db');
|
|
1136
1134
|
if (existsSync(vectorDb)) {
|
|
1137
|
-
|
|
1135
|
+
push('warn', 'old_data', 'Old vector-db still exists (can be removed)', { vector_db_exists: true });
|
|
1138
1136
|
}
|
|
1139
1137
|
|
|
1138
|
+
if (json) {
|
|
1139
|
+
const out = {};
|
|
1140
|
+
for (const c of checks) {
|
|
1141
|
+
const { level, key, message, ...extra } = c;
|
|
1142
|
+
out[key] = { level, message, ...extra };
|
|
1143
|
+
}
|
|
1144
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
console.log('\nclaude-mem-lite status\n');
|
|
1149
|
+
for (const c of checks) {
|
|
1150
|
+
if (c.level === 'ok') ok(c.message);
|
|
1151
|
+
else if (c.level === 'warn') warn(c.message);
|
|
1152
|
+
else fail(c.message);
|
|
1153
|
+
}
|
|
1140
1154
|
console.log('');
|
|
1141
1155
|
}
|
|
1142
1156
|
|
|
1143
1157
|
// ─── Doctor ─────────────────────────────────────────────────────────────────
|
|
1144
1158
|
|
|
1145
1159
|
async function doctor() {
|
|
1146
|
-
|
|
1160
|
+
// Dogfood-9: structured --json output for CI / wrapper scripts that want to
|
|
1161
|
+
// act on individual checks (e.g. "fail my deploy if FTS5 integrity not ok").
|
|
1162
|
+
// Implementation strategy: shadow ok/warn/fail/log inside doctor() so every
|
|
1163
|
+
// existing call site automatically captures into `checks`, and route final
|
|
1164
|
+
// output to JSON or text. Mirror install.mjs::status() shape — { key: {...} }
|
|
1165
|
+
// would lose ordering, so use a flat array of { level, message } objects
|
|
1166
|
+
// (doctor checks are ordered by significance: deps → server → DB → drift).
|
|
1167
|
+
const json = flags.has('--json');
|
|
1168
|
+
const checks = [];
|
|
1169
|
+
if (!json) console.log('\nclaude-mem-lite doctor\n');
|
|
1170
|
+
|
|
1171
|
+
// Shadow file-level helpers so every call site auto-records.
|
|
1172
|
+
const ok = (msg) => { checks.push({ level: 'ok', message: msg }); if (!json) console.log(` ✓ ${msg}`); };
|
|
1173
|
+
const warn = (msg) => { checks.push({ level: 'warn', message: msg }); if (!json) console.log(` ⚠ ${msg}`); };
|
|
1174
|
+
const fail = (msg) => { checks.push({ level: 'fail', message: msg }); if (!json) console.log(` ✗ ${msg}`); };
|
|
1175
|
+
const log = (msg) => { if (!json) console.log(` ${msg}`); };
|
|
1176
|
+
|
|
1147
1177
|
let issues = 0;
|
|
1148
1178
|
let warnings = 0;
|
|
1149
1179
|
// Doctor-local ⚠ helper: visually identical to the file-level `warn`, but
|
|
@@ -1151,7 +1181,7 @@ async function doctor() {
|
|
|
1151
1181
|
// "warnings present". Used for informational ⚠ checks; the two ⚠ paths
|
|
1152
1182
|
// that ALSO bump `issues` (stale procs, dev drift) keep using the file-level
|
|
1153
1183
|
// `warn` directly to avoid double-counting.
|
|
1154
|
-
const dwarn = (msg) => { warnings++;
|
|
1184
|
+
const dwarn = (msg) => { warnings++; warn(msg); };
|
|
1155
1185
|
|
|
1156
1186
|
// Node version
|
|
1157
1187
|
const nodeVer = process.version;
|
|
@@ -1398,7 +1428,16 @@ async function doctor() {
|
|
|
1398
1428
|
} catch {}
|
|
1399
1429
|
}
|
|
1400
1430
|
|
|
1401
|
-
|
|
1431
|
+
if (json) {
|
|
1432
|
+
console.log(JSON.stringify({
|
|
1433
|
+
issues,
|
|
1434
|
+
warnings,
|
|
1435
|
+
summary: buildDoctorSummary(issues, warnings),
|
|
1436
|
+
checks,
|
|
1437
|
+
}, null, 2));
|
|
1438
|
+
} else {
|
|
1439
|
+
console.log(`\n ${buildDoctorSummary(issues, warnings)}\n`);
|
|
1440
|
+
}
|
|
1402
1441
|
// Diagnostic-tool exit-code contract: any ✗-level finding must propagate non-zero
|
|
1403
1442
|
// so CI / wrapper scripts (`claude-mem-lite doctor || alert`) actually trip. Keeps
|
|
1404
1443
|
// ⚠-only states at exit 0 (#8268 already established the visual ⚠ vs counted-issue
|
|
@@ -1532,7 +1571,12 @@ function writeSettings(settings) {
|
|
|
1532
1571
|
// ─── Cleanup Stale Files ─────────────────────────────────────────────────────
|
|
1533
1572
|
|
|
1534
1573
|
function cleanup() {
|
|
1535
|
-
|
|
1574
|
+
// Dogfood-7 addition: --dry-run lists which files would be removed without
|
|
1575
|
+
// touching disk. Useful before running cleanup on a remote/CI machine where
|
|
1576
|
+
// accidentally pruning the wrong file would be costly. Doctor reports stale
|
|
1577
|
+
// file counts and points users here; --dry-run lets them confirm the list.
|
|
1578
|
+
const dryRun = flags.has('--dry-run');
|
|
1579
|
+
console.log(`\nclaude-mem-lite cleanup${dryRun ? ' (--dry-run)' : ''}\n`);
|
|
1536
1580
|
let removed = 0;
|
|
1537
1581
|
|
|
1538
1582
|
// Clean .update-staging-* / .update-backup-* in INSTALL_DIR
|
|
@@ -1540,6 +1584,11 @@ function cleanup() {
|
|
|
1540
1584
|
if (existsSync(INSTALL_DIR)) {
|
|
1541
1585
|
for (const f of readdirSync(INSTALL_DIR)) {
|
|
1542
1586
|
if (stalePatterns.some(p => f.startsWith(p))) {
|
|
1587
|
+
if (dryRun) {
|
|
1588
|
+
ok(`Would remove: ${f}`);
|
|
1589
|
+
removed++;
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1543
1592
|
try {
|
|
1544
1593
|
rmSync(join(INSTALL_DIR, f), { recursive: true, force: true });
|
|
1545
1594
|
ok(`Removed: ${f}`);
|
|
@@ -1556,6 +1605,11 @@ function cleanup() {
|
|
|
1556
1605
|
if (existsSync(runtimeDir)) {
|
|
1557
1606
|
for (const f of readdirSync(runtimeDir)) {
|
|
1558
1607
|
if (f.startsWith('pending-') || f.startsWith('ep-flush-')) {
|
|
1608
|
+
if (dryRun) {
|
|
1609
|
+
ok(`Would remove: runtime/${f}`);
|
|
1610
|
+
removed++;
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1559
1613
|
try {
|
|
1560
1614
|
rmSync(join(runtimeDir, f), { force: true });
|
|
1561
1615
|
ok(`Removed: runtime/${f}`);
|
|
@@ -1567,7 +1621,8 @@ function cleanup() {
|
|
|
1567
1621
|
}
|
|
1568
1622
|
}
|
|
1569
1623
|
|
|
1570
|
-
|
|
1624
|
+
const verb = dryRun ? 'would be removed' : 'removed';
|
|
1625
|
+
console.log(`\n ${removed === 0 ? 'No stale files found.' : `${removed} stale file(s) ${verb}.`}\n`);
|
|
1571
1626
|
}
|
|
1572
1627
|
|
|
1573
1628
|
// ─── Manual Update ───────────────────────────────────────────────────────────
|
|
@@ -1707,6 +1762,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1707
1762
|
// npx claude-mem-lite (no args) → auto install
|
|
1708
1763
|
await install();
|
|
1709
1764
|
} else {
|
|
1765
|
+
// Name the unknown token before the usage block. Pre-fix `install frobnicate`
|
|
1766
|
+
// dumped usage silently, which read like the user had typed nothing — they had
|
|
1767
|
+
// no idea their command was rejected.
|
|
1768
|
+
if (cmd) {
|
|
1769
|
+
console.error(`[install] Unknown command: "${cmd}"`);
|
|
1770
|
+
process.exitCode = 1;
|
|
1771
|
+
}
|
|
1710
1772
|
console.log(`
|
|
1711
1773
|
claude-mem-lite — Lightweight memory system for Claude Code
|
|
1712
1774
|
|
|
@@ -1715,9 +1777,9 @@ Usage:
|
|
|
1715
1777
|
node install.mjs install --dev Install dev mode (symlinks to dev dir)
|
|
1716
1778
|
node install.mjs uninstall Remove (keep data)
|
|
1717
1779
|
node install.mjs uninstall --purge Remove and delete all data
|
|
1718
|
-
node install.mjs status Show current status
|
|
1719
|
-
node install.mjs doctor Diagnose issues
|
|
1720
|
-
node install.mjs cleanup Remove stale temp/staging files
|
|
1780
|
+
node install.mjs status Show current status (use --json for structured output)
|
|
1781
|
+
node install.mjs doctor Diagnose issues (use --json for structured output)
|
|
1782
|
+
node install.mjs cleanup Remove stale temp/staging files (use --dry-run to preview)
|
|
1721
1783
|
node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
|
|
1722
1784
|
node install.mjs self-update Check for and install updates
|
|
1723
1785
|
node install.mjs release Sync versions (plugin/marketplace/CLAUDE.md) + regen lockfile via npm@10.9.2 (use --no-lock to skip lock regen)
|
package/mem-cli.mjs
CHANGED
|
@@ -104,13 +104,16 @@ function cmdSearch(db, args) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Warn if obs-only filters used with non-observation source
|
|
107
|
-
if (source && source !== 'observations' && (type || tier || minImportance)) {
|
|
108
|
-
const ignored = [type && '--type', tier && '--tier', minImportance && '--importance'].filter(Boolean);
|
|
107
|
+
if (source && source !== 'observations' && (type || tier || minImportance || branch)) {
|
|
108
|
+
const ignored = [type && '--type', tier && '--tier', minImportance && '--importance', branch && '--branch'].filter(Boolean);
|
|
109
109
|
process.stderr.write(`[mem] Note: ${ignored.join(', ')} only apply to observations, ignored for --source ${source}\n`);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// When --type/--tier/--importance (obs-only fields) is specified, implicitly restrict to observations
|
|
113
|
-
|
|
112
|
+
// When --type/--tier/--importance/--branch (obs-only fields) is specified, implicitly restrict to observations.
|
|
113
|
+
// --branch was previously cross-source: sessions/prompts have no branch column, so a query like
|
|
114
|
+
// `search "cache" --branch main` would include unrelated session/prompt rows, surprising users
|
|
115
|
+
// who passed --branch expecting a branch-scoped result.
|
|
116
|
+
const effectiveSource = source || ((type || tier || minImportance || branch) ? 'observations' : null);
|
|
114
117
|
|
|
115
118
|
// Cross-source mode: each source needs more candidates than the final limit
|
|
116
119
|
// so the post-merge sort has room to pick the best from each (paired-path with
|
|
@@ -392,9 +395,21 @@ function cmdRecent(db, args) {
|
|
|
392
395
|
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
393
396
|
const jsonOutput = flags.json === true || flags.json === 'true';
|
|
394
397
|
|
|
398
|
+
// `recent --type bugfix` previously parsed as a silent no-op — users naturally
|
|
399
|
+
// try this for "show recent bugfixes". Mirror cmdSearch's enum validation.
|
|
400
|
+
const type = flags.type || null;
|
|
401
|
+
if (type) {
|
|
402
|
+
const validObsTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
|
|
403
|
+
if (!validObsTypes.has(type)) {
|
|
404
|
+
fail(`[mem] Invalid --type "${type}". Valid: ${[...validObsTypes].join(', ')}`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
395
409
|
const params = [];
|
|
396
410
|
const wheres = ['COALESCE(compressed_into, 0) = 0', 'superseded_at IS NULL'];
|
|
397
411
|
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
412
|
+
if (type) { wheres.push('type = ?'); params.push(type); }
|
|
398
413
|
params.push(limit);
|
|
399
414
|
|
|
400
415
|
const rows = db.prepare(`
|
|
@@ -409,6 +424,7 @@ function cmdRecent(db, args) {
|
|
|
409
424
|
out(JSON.stringify({
|
|
410
425
|
project: project || null,
|
|
411
426
|
limit,
|
|
427
|
+
type: type || null,
|
|
412
428
|
total: rows.length,
|
|
413
429
|
results: rows.map(r => ({
|
|
414
430
|
id: r.id,
|
|
@@ -1010,6 +1026,13 @@ function cmdDeferAdd(db, args) {
|
|
|
1010
1026
|
fail('[mem] Usage: claude-mem-lite defer add "<title>" [--priority 1|2|3] [--detail T] [--files f1,f2] [--project P]');
|
|
1011
1027
|
return;
|
|
1012
1028
|
}
|
|
1029
|
+
// Mirror MCP memDeferSchema.title (z.string().min(1).max(200)). CLI used to
|
|
1030
|
+
// accept multi-line / 1000-char titles, then `defer list` would render them
|
|
1031
|
+
// as one wrapped row that pushed every other item off-screen.
|
|
1032
|
+
if (title.length > 200) {
|
|
1033
|
+
fail(`[mem] defer add: title too long (${title.length} chars, max 200). Move detail to --detail "<text>".`);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1013
1036
|
const priority = flags.priority !== undefined ? parseInt(flags.priority, 10) : 2;
|
|
1014
1037
|
if (![1, 2, 3].includes(priority)) {
|
|
1015
1038
|
fail(`[mem] Invalid --priority "${flags.priority}". Must be 1 (low), 2 (normal), or 3 (urgent).`);
|
|
@@ -1054,7 +1077,7 @@ function cmdDeferList(db, args) {
|
|
|
1054
1077
|
function cmdDeferDrop(db, args) {
|
|
1055
1078
|
const { positional, flags } = parseArgs(args);
|
|
1056
1079
|
if (positional.length === 0) {
|
|
1057
|
-
fail('[mem] Usage: claude-mem-lite defer drop <id-or-D#N> --reason "<reason>" [--project P]');
|
|
1080
|
+
fail('[mem] Usage: claude-mem-lite defer drop <id-or-D#N>[,id2,...] --reason "<reason>" [--project P]');
|
|
1058
1081
|
return;
|
|
1059
1082
|
}
|
|
1060
1083
|
const reason = flags.reason;
|
|
@@ -1062,26 +1085,34 @@ function cmdDeferDrop(db, args) {
|
|
|
1062
1085
|
fail('[mem] defer drop requires --reason "<non-empty string>"');
|
|
1063
1086
|
return;
|
|
1064
1087
|
}
|
|
1065
|
-
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1069
|
-
const
|
|
1088
|
+
// Accept either a single token or a comma-separated batch. `save --closes-deferred`
|
|
1089
|
+
// already accepts the batch form (cmdSave uses resolveDeferredIds on a split list);
|
|
1090
|
+
// drop now mirrors that ergonomic so users can prune multiple items in one call
|
|
1091
|
+
// without N shell invocations.
|
|
1092
|
+
const rawTokens = positional.join(' ').split(',').map(s => s.trim()).filter(Boolean);
|
|
1093
|
+
const tokens = rawTokens.map(t => /^\d+$/.test(t) ? parseInt(t, 10) : t);
|
|
1070
1094
|
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
1071
1095
|
|
|
1072
|
-
let
|
|
1096
|
+
let realIds;
|
|
1073
1097
|
try {
|
|
1074
|
-
|
|
1098
|
+
realIds = resolveDeferredIds(db, project, tokens);
|
|
1075
1099
|
} catch (e) {
|
|
1076
1100
|
fail(`[mem] defer drop: ${e.message}`);
|
|
1077
1101
|
return;
|
|
1078
1102
|
}
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1103
|
+
const dropped = [];
|
|
1104
|
+
const noop = [];
|
|
1105
|
+
for (const realId of realIds) {
|
|
1106
|
+
const r = dropDeferred(db, realId, reason);
|
|
1107
|
+
if (r.changed === 0) noop.push(realId);
|
|
1108
|
+
else dropped.push(realId);
|
|
1109
|
+
}
|
|
1110
|
+
if (dropped.length > 0) {
|
|
1111
|
+
out(`[mem] Dropped ${dropped.map(id => `D#${id}`).join(', ')} in project "${project}". Reason: ${reason.trim()}`);
|
|
1112
|
+
}
|
|
1113
|
+
if (noop.length > 0) {
|
|
1114
|
+
out(`[mem] No-op (not in 'open' status): ${noop.map(id => `D#${id}`).join(', ')}`);
|
|
1083
1115
|
}
|
|
1084
|
-
out(`[mem] Dropped D#${realId} in project "${project}". Reason: ${reason.trim()}`);
|
|
1085
1116
|
}
|
|
1086
1117
|
|
|
1087
1118
|
// N-1: Quality-focused stats for R-2 A/B baseline.
|
|
@@ -1563,7 +1594,15 @@ function cmdUpdate(db, args) {
|
|
|
1563
1594
|
|
|
1564
1595
|
const updates = [];
|
|
1565
1596
|
const params = [];
|
|
1566
|
-
if (flags.title !== undefined) {
|
|
1597
|
+
if (flags.title !== undefined) {
|
|
1598
|
+
// Reject empty title — clears the observation's identifier and would render it
|
|
1599
|
+
// as `(untitled)` in every listing. Almost always an accidental shell-stripped arg.
|
|
1600
|
+
if (typeof flags.title === 'string' && flags.title.trim() === '') {
|
|
1601
|
+
fail('[mem] --title cannot be empty. Pass a non-empty string or omit the flag to leave the title unchanged.');
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
updates.push('title = ?'); params.push(scrubSecrets(flags.title));
|
|
1605
|
+
}
|
|
1567
1606
|
if (flags.narrative !== undefined) { updates.push('narrative = ?'); params.push(scrubSecrets(flags.narrative)); }
|
|
1568
1607
|
if (flags.type) {
|
|
1569
1608
|
const validTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
|
|
@@ -1581,7 +1620,18 @@ function cmdUpdate(db, args) {
|
|
|
1581
1620
|
}
|
|
1582
1621
|
updates.push('importance = ?'); params.push(imp);
|
|
1583
1622
|
}
|
|
1584
|
-
if (flags.lesson !== undefined || flags['lesson-learned'] !== undefined) {
|
|
1623
|
+
if (flags.lesson !== undefined || flags['lesson-learned'] !== undefined) {
|
|
1624
|
+
const rawLesson = flags.lesson ?? flags['lesson-learned'] ?? '';
|
|
1625
|
+
// Mirror cmdSave's 500-char cap — pre-fix `update --lesson <501-char>` was silently
|
|
1626
|
+
// accepted, letting overlong lessons leak into the DB through the update path
|
|
1627
|
+
// even though save's path rejected them. MCP memSaveSchema also caps at 500.
|
|
1628
|
+
if (typeof rawLesson === 'string' && rawLesson.length > 500) {
|
|
1629
|
+
fail(`[mem] --lesson too long (${rawLesson.length} chars, max 500).`);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
updates.push('lesson_learned = ?');
|
|
1633
|
+
params.push(scrubSecrets(rawLesson));
|
|
1634
|
+
}
|
|
1585
1635
|
if (flags.concepts !== undefined) { updates.push('concepts = ?'); params.push(flags.concepts); }
|
|
1586
1636
|
|
|
1587
1637
|
if (updates.length === 0) {
|
|
@@ -1632,7 +1682,16 @@ function cmdExport(db, args) {
|
|
|
1632
1682
|
|
|
1633
1683
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1634
1684
|
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
1635
|
-
if (flags.type) {
|
|
1685
|
+
if (flags.type) {
|
|
1686
|
+
// Reject unknown types — silently returning [] for `--type bogus` looked like a
|
|
1687
|
+
// legitimate empty filter result, hiding the typo. Mirrors cmdSearch / cmdSave / cmdUpdate.
|
|
1688
|
+
const validObsTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
|
|
1689
|
+
if (!validObsTypes.has(flags.type)) {
|
|
1690
|
+
fail(`[mem] Invalid --type "${flags.type}". Valid: ${[...validObsTypes].join(', ')}`);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
wheres.push('type = ?'); params.push(flags.type);
|
|
1694
|
+
}
|
|
1636
1695
|
let exportFromEpoch = null;
|
|
1637
1696
|
let exportToEpoch = null;
|
|
1638
1697
|
if (flags.from) {
|
|
@@ -1690,8 +1749,18 @@ function cmdExport(db, args) {
|
|
|
1690
1749
|
function cmdCompress(db, args) {
|
|
1691
1750
|
const { flags } = parseArgs(args);
|
|
1692
1751
|
const preview = flags.execute !== true && flags.execute !== 'true';
|
|
1693
|
-
|
|
1694
|
-
|
|
1752
|
+
// Reject malformed --age-days explicitly. The prior fallback (`|| 30`) silently used
|
|
1753
|
+
// the default whenever the value parsed as NaN or <1, so users typing `--age-days abc`
|
|
1754
|
+
// got the 30-day cutoff without knowing their input was discarded.
|
|
1755
|
+
let ageDays = 30;
|
|
1756
|
+
if (flags['age-days'] !== undefined) {
|
|
1757
|
+
const parsed = parseInt(flags['age-days'], 10);
|
|
1758
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
1759
|
+
fail(`[mem] Invalid --age-days "${flags['age-days']}". Must be a positive integer.`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
ageDays = parsed;
|
|
1763
|
+
}
|
|
1695
1764
|
const cutoff = Date.now() - ageDays * 86400000;
|
|
1696
1765
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1697
1766
|
const projectFilter = project ? 'AND project = ?' : '';
|
|
@@ -2084,6 +2153,20 @@ function cmdRegistry(_memDb, args) {
|
|
|
2084
2153
|
return;
|
|
2085
2154
|
}
|
|
2086
2155
|
|
|
2156
|
+
// `--type` and `--resource-type` are both constrained to skill|agent across
|
|
2157
|
+
// registry sub-actions. Validating once here means search/list/import/remove
|
|
2158
|
+
// all reject typos like `--type sklil` instead of silently returning
|
|
2159
|
+
// "No resources found." (which looked like the registry was empty for that
|
|
2160
|
+
// type, not like a typo).
|
|
2161
|
+
if (flags.type !== undefined && flags.type !== 'skill' && flags.type !== 'agent') {
|
|
2162
|
+
fail(`[mem] Invalid --type "${flags.type}". Use: skill, agent`);
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
if (flags['resource-type'] !== undefined && flags['resource-type'] !== 'skill' && flags['resource-type'] !== 'agent') {
|
|
2166
|
+
fail(`[mem] Invalid --resource-type "${flags['resource-type']}". Use: skill, agent`);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2087
2170
|
try {
|
|
2088
2171
|
if (action === 'search') {
|
|
2089
2172
|
const query = flags.query || positional.slice(1).join(' ');
|
|
@@ -2199,7 +2282,9 @@ function cmdRegistry(_memDb, args) {
|
|
|
2199
2282
|
const resourceType = flags['resource-type'];
|
|
2200
2283
|
if (!name || !resourceType) { fail('[mem] Usage: claude-mem-lite registry remove --name N --resource-type skill|agent'); return; }
|
|
2201
2284
|
const result = rdb.prepare('DELETE FROM resources WHERE type = ? AND name = ?').run(resourceType, name);
|
|
2202
|
-
out(result.changes > 0
|
|
2285
|
+
out(result.changes > 0
|
|
2286
|
+
? `[mem] Removed: ${resourceType}:${name}`
|
|
2287
|
+
: `[mem] Not found: ${resourceType}:${name}`);
|
|
2203
2288
|
return;
|
|
2204
2289
|
}
|
|
2205
2290
|
|
|
@@ -2286,7 +2371,7 @@ Commands:
|
|
|
2286
2371
|
--project P Filter by project
|
|
2287
2372
|
--from DATE Start date (YYYY-MM-DD or ISO 8601)
|
|
2288
2373
|
--to DATE End date (YYYY-MM-DD or ISO 8601)
|
|
2289
|
-
--importance N Minimum importance (1
|
|
2374
|
+
--importance N Minimum importance (1=routine, 2=notable, 3=critical)
|
|
2290
2375
|
--branch B Filter by git branch
|
|
2291
2376
|
--offset N Skip first N results (pagination)
|
|
2292
2377
|
--tier T Filter by tier (working|active|archive, observations only)
|
|
@@ -2298,7 +2383,8 @@ Commands:
|
|
|
2298
2383
|
recent [N] Show N most recent observations (default 10)
|
|
2299
2384
|
--limit N Sibling-parity alias for [N] (max 1000)
|
|
2300
2385
|
--project P Filter by project
|
|
2301
|
-
--
|
|
2386
|
+
--type T Filter obs type (bugfix|decision|discovery|feature|refactor|change)
|
|
2387
|
+
--json Output as JSON: {project,limit,type,total,results:[…]}
|
|
2302
2388
|
|
|
2303
2389
|
recall <file> Show observations related to a file
|
|
2304
2390
|
--limit N Max results (default 10)
|
|
@@ -2328,22 +2414,22 @@ Commands:
|
|
|
2328
2414
|
save "<text>" Save a new observation
|
|
2329
2415
|
--type T Observation type (default: discovery)
|
|
2330
2416
|
--title T Title (auto-generated if omitted)
|
|
2331
|
-
--importance N 1
|
|
2417
|
+
--importance N 1=routine, 2=notable, 3=critical (default: 2)
|
|
2332
2418
|
--project P Project name
|
|
2333
2419
|
--files f1,f2 Comma-separated file paths
|
|
2334
2420
|
--lesson T Lesson learned (≤500 chars; alias: --lesson-learned)
|
|
2335
2421
|
--closes-deferred 1,D#42 Close deferred items in same transaction
|
|
2336
2422
|
|
|
2337
2423
|
defer <action> First-class deferred work (v2.70+)
|
|
2338
|
-
add "<title>" Mark deferred work for next session
|
|
2339
|
-
--priority N 1
|
|
2424
|
+
add "<title>" Mark deferred work for next session (≤200 chars)
|
|
2425
|
+
--priority N 1=low, 2=normal, 3=urgent (default: 2)
|
|
2340
2426
|
--detail T Constraint + why deferred
|
|
2341
2427
|
--files f1,f2 Comma-separated file paths
|
|
2342
2428
|
--project P Project name
|
|
2343
2429
|
list List open deferred items
|
|
2344
2430
|
--limit N Max results (default 10)
|
|
2345
2431
|
--project P Filter by project
|
|
2346
|
-
drop <D#N|ordinal> Drop
|
|
2432
|
+
drop <D#N|ordinal>[,...] Drop one or more deferred items (no fix needed)
|
|
2347
2433
|
--reason "..." Required audit trail
|
|
2348
2434
|
|
|
2349
2435
|
delete <id1,id2,...> Delete observations by ID
|
|
@@ -2352,7 +2438,7 @@ Commands:
|
|
|
2352
2438
|
update <id> Update an existing observation
|
|
2353
2439
|
--title T New title
|
|
2354
2440
|
--type T New type
|
|
2355
|
-
--importance N New importance (1
|
|
2441
|
+
--importance N New importance (1=routine, 2=notable, 3=critical)
|
|
2356
2442
|
--lesson T Add/update lesson learned (alias: --lesson-learned)
|
|
2357
2443
|
--narrative T New narrative
|
|
2358
2444
|
--concepts T Space-separated concept tags
|
|
@@ -2451,6 +2537,8 @@ Commands:
|
|
|
2451
2537
|
|
|
2452
2538
|
unadopt Precise removal of the sentinel block + plugin_claude_mem_lite.md.
|
|
2453
2539
|
--all Unadopt every project
|
|
2540
|
+
--status Read-only: list adopted projects (same as adopt --status)
|
|
2541
|
+
--dry-run Preview what would be removed; no filesystem writes
|
|
2454
2542
|
|
|
2455
2543
|
memdir-audit Audit memdir feedback_*.md / project_*.md for the
|
|
2456
2544
|
body-structure contract (**Why:** + **How to apply:**).
|
|
@@ -2551,16 +2639,30 @@ async function cmdImportJsonl(db, argv) {
|
|
|
2551
2639
|
if (files.length === 0) { out('[mem] No .jsonl files found.'); return; }
|
|
2552
2640
|
|
|
2553
2641
|
const { importJsonl } = await import('./lib/import-jsonl.mjs');
|
|
2554
|
-
let totalPrompts = 0, totalObs = 0, totalSkip = 0, totalOrphans = 0;
|
|
2642
|
+
let totalPrompts = 0, totalObs = 0, totalSkip = 0, totalOrphans = 0, errorCount = 0;
|
|
2555
2643
|
for (const f of files) {
|
|
2556
|
-
|
|
2644
|
+
// Per-file isolation: one unreadable file (EACCES, EBUSY, mid-batch IO error)
|
|
2645
|
+
// shouldn't crash the whole import — readFileSync inside importJsonl would
|
|
2646
|
+
// otherwise throw an unhandled exception with a node stack trace, leaving
|
|
2647
|
+
// earlier successes uncommitted-looking from the user's perspective.
|
|
2648
|
+
let r;
|
|
2649
|
+
try {
|
|
2650
|
+
r = await importJsonl(db, f, { project });
|
|
2651
|
+
} catch (e) {
|
|
2652
|
+
errorCount++;
|
|
2653
|
+
// e.message for node fs errors already begins with the code (e.g. "EACCES: permission denied, ...");
|
|
2654
|
+
// don't double-prefix.
|
|
2655
|
+
process.stderr.write(`[mem] ${f}: import failed — ${e.message}\n`);
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2557
2658
|
totalPrompts += r.prompts;
|
|
2558
2659
|
totalObs += r.observations;
|
|
2559
2660
|
totalSkip += r.skipped;
|
|
2560
2661
|
totalOrphans += r.orphans || 0;
|
|
2561
2662
|
out(`[mem] ${f}: +${r.prompts} prompts, +${r.observations} observations, ${r.orphans || 0} orphan tool_use, ${r.skipped} skipped`);
|
|
2562
2663
|
}
|
|
2563
|
-
|
|
2664
|
+
const errorTail = errorCount > 0 ? `, ${errorCount} file(s) errored` : '';
|
|
2665
|
+
out(`[mem] Total: ${totalPrompts} prompts, ${totalObs} observations, ${totalOrphans} orphan tool_use, ${totalSkip} skipped from ${files.length} file(s)${errorTail}.`);
|
|
2564
2666
|
if (totalPrompts > 0 || totalObs > 0) {
|
|
2565
2667
|
out(`[mem] Try: claude-mem-lite recent 5 --project ${project}`);
|
|
2566
2668
|
}
|
|
@@ -2648,8 +2750,17 @@ async function cmdOptimize(db, args) {
|
|
|
2648
2750
|
}
|
|
2649
2751
|
// R-7 micro: --scope wide targets bugfix/refactor/feature/decision with narrative but no
|
|
2650
2752
|
// lesson_learned (the "Haiku judged 'none'" cases). Default 'narrow' preserves old behavior.
|
|
2753
|
+
// Validate explicitly so `--scope wlde` (typo) doesn't silently become narrow and waste an LLM run.
|
|
2651
2754
|
const scopeIdx = args.indexOf('--scope');
|
|
2652
|
-
|
|
2755
|
+
let reenrichScope = 'narrow';
|
|
2756
|
+
if (scopeIdx >= 0 && args[scopeIdx + 1] !== undefined) {
|
|
2757
|
+
const raw = args[scopeIdx + 1];
|
|
2758
|
+
if (raw !== 'narrow' && raw !== 'wide') {
|
|
2759
|
+
fail(`[mem] Invalid --scope "${raw}". Use: narrow, wide`);
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
reenrichScope = raw;
|
|
2763
|
+
}
|
|
2653
2764
|
// --project <name> filters all 4 tasks to one project. Opt-in; absence
|
|
2654
2765
|
// preserves prior cross-project default. `.` or `current` auto-resolve via
|
|
2655
2766
|
// inferProject() so users don't need to remember the exact name.
|
|
@@ -2758,7 +2869,11 @@ export async function run(argv) {
|
|
|
2758
2869
|
const JSON_SUPPORTED_CMDS = new Set([
|
|
2759
2870
|
'search', 'context', 'recent', 'recall', 'timeline', 'stats', 'browse', 'export',
|
|
2760
2871
|
]);
|
|
2761
|
-
|
|
2872
|
+
// `doctor --benchmark` already emits JSON on its own — don't print the misleading
|
|
2873
|
+
// "doctor outputs text" note for that subpath. Without --benchmark, doctor is text
|
|
2874
|
+
// and the note is still useful.
|
|
2875
|
+
const doctorBenchmark = cmd === 'doctor' && cmdArgs.includes('--benchmark');
|
|
2876
|
+
if (cmdArgs.includes('--json') && !JSON_SUPPORTED_CMDS.has(cmd) && !doctorBenchmark) {
|
|
2762
2877
|
process.stderr.write(`[mem] Note: --json is supported only on: ${[...JSON_SUPPORTED_CMDS].join(', ')}. "${cmd}" outputs text.\n`);
|
|
2763
2878
|
}
|
|
2764
2879
|
|
|
@@ -2792,6 +2907,19 @@ export async function run(argv) {
|
|
|
2792
2907
|
out('[mem] Run "claude-mem-lite help" for usage');
|
|
2793
2908
|
process.exitCode = 1;
|
|
2794
2909
|
}
|
|
2910
|
+
} catch (e) {
|
|
2911
|
+
// SQLITE_BUSY / SQLITE_LOCKED + extended variants (SQLITE_BUSY_SNAPSHOT,
|
|
2912
|
+
// SQLITE_BUSY_RECOVERY, SQLITE_LOCKED_SHAREDCACHE…). All mean the same thing
|
|
2913
|
+
// to the user: writer contention past the 5s busy_timeout. Pre-fix this
|
|
2914
|
+
// raised an unhandled SqliteError with a node stack trace.
|
|
2915
|
+
const code = e && typeof e.code === 'string' ? e.code : '';
|
|
2916
|
+
if (code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED' ||
|
|
2917
|
+
code.startsWith('SQLITE_BUSY_') || code.startsWith('SQLITE_LOCKED_')) {
|
|
2918
|
+
process.stderr.write(`[mem] Database busy — another process held the writer past the 5s timeout. Retry shortly.\n`);
|
|
2919
|
+
process.exitCode = 1;
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
throw e;
|
|
2795
2923
|
} finally {
|
|
2796
2924
|
try { db.close(); } catch {}
|
|
2797
2925
|
}
|
package/package.json
CHANGED
package/schema.mjs
CHANGED
|
@@ -696,7 +696,10 @@ export function ensureDb() {
|
|
|
696
696
|
const db = new Database(DB_PATH);
|
|
697
697
|
try { chmodSync(DB_PATH, 0o600); } catch {}
|
|
698
698
|
db.pragma('journal_mode = WAL');
|
|
699
|
-
|
|
699
|
+
// 5000ms matches the MCP server (server.mjs) — 3000ms wasn't enough under realistic
|
|
700
|
+
// concurrency (parallel CLI saves + a long-running FTS rebuild can push individual
|
|
701
|
+
// transactions past 3s, triggering SQLITE_BUSY on the third caller).
|
|
702
|
+
db.pragma('busy_timeout = 5000');
|
|
700
703
|
db.pragma('synchronous = NORMAL');
|
|
701
704
|
db.pragma('foreign_keys = OFF'); // Enabled after dedup migration
|
|
702
705
|
|