claude-mem-lite 2.62.1 → 2.64.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/cli/fts-check.mjs +1 -1
- package/install.mjs +90 -35
- package/lib/doctor-drift.mjs +1 -0
- package/lib/save-observation.mjs +1 -1
- package/mem-cli.mjs +39 -14
- package/package.json +2 -1
- package/search-engine.mjs +5 -4
- package/server.mjs +9 -2
package/cli/fts-check.mjs
CHANGED
|
@@ -8,7 +8,7 @@ export function cmdFtsCheck(db, args) {
|
|
|
8
8
|
const { positional } = parseArgs(args);
|
|
9
9
|
const action = positional[0];
|
|
10
10
|
if (!action || !['check', 'rebuild'].includes(action)) {
|
|
11
|
-
fail('[mem] Usage: mem fts-check <check|rebuild>');
|
|
11
|
+
fail('[mem] Usage: claude-mem-lite fts-check <check|rebuild>');
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
|
package/install.mjs
CHANGED
|
@@ -280,6 +280,25 @@ function ok(msg) { console.log(` ✓ ${msg}`); }
|
|
|
280
280
|
function warn(msg) { console.log(` ⚠ ${msg}`); }
|
|
281
281
|
function fail(msg) { console.log(` ✗ ${msg}`); }
|
|
282
282
|
|
|
283
|
+
// Pure JSON-version field bumper for the release pipeline. Reads `filePath`,
|
|
284
|
+
// walks `keyPath` (e.g. `['version']` or `['plugins', 0, 'version']`), and
|
|
285
|
+
// rewrites only when the new value differs. Returns `{ changed, prev }` so
|
|
286
|
+
// callers can log "X → Y" with the captured-before-mutation value — pre-2.63.0
|
|
287
|
+
// the plugin.json branch in syncVersions logged "Y → Y" because it read the
|
|
288
|
+
// field after assignment.
|
|
289
|
+
export function bumpJsonField(filePath, keyPath, newVal) {
|
|
290
|
+
const json = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
291
|
+
let parent = json;
|
|
292
|
+
for (let i = 0; i < keyPath.length - 1; i++) parent = parent?.[keyPath[i]];
|
|
293
|
+
if (!parent) return { changed: false, prev: undefined };
|
|
294
|
+
const lastKey = keyPath[keyPath.length - 1];
|
|
295
|
+
const prev = parent[lastKey];
|
|
296
|
+
if (prev === newVal) return { changed: false, prev };
|
|
297
|
+
parent[lastKey] = newVal;
|
|
298
|
+
writeFileSync(filePath, JSON.stringify(json, null, 2) + '\n');
|
|
299
|
+
return { changed: true, prev };
|
|
300
|
+
}
|
|
301
|
+
|
|
283
302
|
// Doctor's final summary line. Pure function so the 4-way contract
|
|
284
303
|
// (clean / warnings-only / issues / mixed) is unit-testable without spinning
|
|
285
304
|
// up the full doctor pipeline. `issues` are ✗-level (action required);
|
|
@@ -1276,14 +1295,29 @@ async function doctor() {
|
|
|
1276
1295
|
dwarn('Database: not found (will be created)');
|
|
1277
1296
|
}
|
|
1278
1297
|
|
|
1279
|
-
// Check for stale processes
|
|
1298
|
+
// Check for stale processes — extends beyond legacy chroma/worker to
|
|
1299
|
+
// catch MCP launchers / servers from cached old plugin versions. Auto-update
|
|
1300
|
+
// bumps installed_plugins.json but cannot kill the MCP process spawned for
|
|
1301
|
+
// an active session, so v2.60.0/v2.61.0 launchers commonly outlive their
|
|
1302
|
+
// version (recurrent pattern, see #2580 for the gsd analogue). Filtering
|
|
1303
|
+
// strategy: legacy chroma/worker = always stale; cache-path launchers = only
|
|
1304
|
+
// when their version segment ≠ current package.json version; dev-install
|
|
1305
|
+
// paths (no version segment) are never flagged.
|
|
1280
1306
|
try {
|
|
1281
|
-
const procs = execFileSync('pgrep', ['-af', 'chroma|claude-mem.*worker'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim();
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1307
|
+
const procs = execFileSync('pgrep', ['-af', 'chroma|claude-mem-lite.*(scripts/launch|server)\\.mjs|claude-mem.*worker'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim();
|
|
1308
|
+
const lines = procs.split('\n').filter(l => l && !l.includes('pgrep'));
|
|
1309
|
+
let currentVersion = '';
|
|
1310
|
+
try { currentVersion = JSON.parse(readFileSync(join(PROJECT_DIR, 'package.json'), 'utf8')).version; } catch { /* fall through with empty version */ }
|
|
1311
|
+
const stale = lines.filter(l => {
|
|
1312
|
+
if (/chroma|claude-mem.*worker/.test(l)) return true;
|
|
1313
|
+
const m = l.match(/claude-mem-lite\/(\d+\.\d+\.\d+)\/(scripts\/launch|server)\.mjs/);
|
|
1314
|
+
return m && currentVersion && m[1] !== currentVersion;
|
|
1315
|
+
});
|
|
1316
|
+
if (stale.length > 0) {
|
|
1317
|
+
warn(`Old processes running${currentVersion ? ` (current: v${currentVersion})` : ''}:\n ` + stale.join('\n '));
|
|
1286
1318
|
issues++;
|
|
1319
|
+
} else {
|
|
1320
|
+
ok('No stale processes');
|
|
1287
1321
|
}
|
|
1288
1322
|
} catch {
|
|
1289
1323
|
ok('No stale processes');
|
|
@@ -1314,19 +1348,31 @@ async function doctor() {
|
|
|
1314
1348
|
}
|
|
1315
1349
|
|
|
1316
1350
|
// Dev drift: in dev-mode installs, all SOURCE_FILES entries should be
|
|
1317
|
-
// symlinks. A plain file means an earlier install (or manual cp) copied it
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1351
|
+
// symlinks. A plain file means an earlier install (or manual cp) copied it
|
|
1352
|
+
// (edits in the repo won't propagate). A missing entry (neither symlink nor
|
|
1353
|
+
// plain) means an earlier install never wrote the file — same divergence
|
|
1354
|
+
// class. Per #8043: "is this file present ≠ is this install consistent" —
|
|
1355
|
+
// missing is tracked separately by checkDevDrift but the caller MUST surface
|
|
1356
|
+
// it to honour #8268's "gate the all-green string on every counter" rule.
|
|
1320
1357
|
try {
|
|
1321
1358
|
const { checkDevDrift } = await import('./lib/doctor-drift.mjs');
|
|
1322
1359
|
const r = checkDevDrift(INSTALL_DIR, SOURCE_FILES);
|
|
1323
|
-
if (r.drift) {
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
|
|
1360
|
+
if (r.drift || (r.devMode && r.missingCount > 0)) {
|
|
1361
|
+
const parts = [];
|
|
1362
|
+
if (r.plainCount > 0) {
|
|
1363
|
+
const names = r.plainFiles.slice(0, 5).join(', ');
|
|
1364
|
+
const suffix = r.plainCount > 5 ? ` +${r.plainCount - 5} more` : '';
|
|
1365
|
+
parts.push(`${r.plainCount} non-symlink: ${names}${suffix}`);
|
|
1366
|
+
}
|
|
1367
|
+
if (r.missingCount > 0) {
|
|
1368
|
+
const names = r.missingFiles.join(', ');
|
|
1369
|
+
const suffix = r.missingCount > r.missingFiles.length ? ` +${r.missingCount - r.missingFiles.length} more` : '';
|
|
1370
|
+
parts.push(`${r.missingCount} missing: ${names}${suffix}`);
|
|
1371
|
+
}
|
|
1372
|
+
warn(`Dev drift: ${parts.join('; ')} (re-run: node ${join(PROJECT_DIR, 'install.mjs')} install --dev)`);
|
|
1327
1373
|
issues++;
|
|
1328
1374
|
} else if (r.devMode) {
|
|
1329
|
-
ok(`Dev drift: clean (${r.symlinkCount} symlinks, 0 plain)`);
|
|
1375
|
+
ok(`Dev drift: clean (${r.symlinkCount} symlinks, 0 plain, 0 missing)`);
|
|
1330
1376
|
}
|
|
1331
1377
|
// Prod (all plain) install: no message — dev-drift is a dev-only concern.
|
|
1332
1378
|
} catch (e) {
|
|
@@ -1593,34 +1639,19 @@ function syncVersions() {
|
|
|
1593
1639
|
const version = pkg.version;
|
|
1594
1640
|
log(`package.json version: ${version}`);
|
|
1595
1641
|
|
|
1596
|
-
// Sync plugin.json
|
|
1597
1642
|
const pluginJsonPath = join(PROJECT_DIR, '.claude-plugin', 'plugin.json');
|
|
1598
1643
|
if (existsSync(pluginJsonPath)) {
|
|
1599
|
-
const
|
|
1600
|
-
|
|
1601
|
-
pluginJson.version = version;
|
|
1602
|
-
writeFileSync(pluginJsonPath, JSON.stringify(pluginJson, null, 2) + '\n');
|
|
1603
|
-
ok(`plugin.json: ${pluginJson.version} → ${version}`);
|
|
1604
|
-
} else {
|
|
1605
|
-
ok(`plugin.json: already ${version}`);
|
|
1606
|
-
}
|
|
1644
|
+
const r = bumpJsonField(pluginJsonPath, ['version'], version);
|
|
1645
|
+
ok(r.changed ? `plugin.json: ${r.prev} → ${version}` : `plugin.json: already ${version}`);
|
|
1607
1646
|
} else {
|
|
1608
1647
|
warn('plugin.json not found');
|
|
1609
1648
|
}
|
|
1610
1649
|
|
|
1611
|
-
// Sync marketplace.json
|
|
1612
1650
|
const marketJsonPath = join(PROJECT_DIR, '.claude-plugin', 'marketplace.json');
|
|
1613
1651
|
if (existsSync(marketJsonPath)) {
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const prev = plugin.version;
|
|
1618
|
-
plugin.version = version;
|
|
1619
|
-
writeFileSync(marketJsonPath, JSON.stringify(marketJson, null, 2) + '\n');
|
|
1620
|
-
ok(`marketplace.json: ${prev} → ${version}`);
|
|
1621
|
-
} else if (plugin) {
|
|
1622
|
-
ok(`marketplace.json: already ${version}`);
|
|
1623
|
-
}
|
|
1652
|
+
const r = bumpJsonField(marketJsonPath, ['plugins', 0, 'version'], version);
|
|
1653
|
+
if (r.prev === undefined) warn('marketplace.json: plugins[0] not found');
|
|
1654
|
+
else ok(r.changed ? `marketplace.json: ${r.prev} → ${version}` : `marketplace.json: already ${version}`);
|
|
1624
1655
|
} else {
|
|
1625
1656
|
warn('marketplace.json not found');
|
|
1626
1657
|
}
|
|
@@ -1649,6 +1680,29 @@ function syncVersions() {
|
|
|
1649
1680
|
console.log('');
|
|
1650
1681
|
}
|
|
1651
1682
|
|
|
1683
|
+
// Regenerate package-lock.json via npm@10.9.2 to guarantee CI parity. The
|
|
1684
|
+
// drift this prevents: `npm install --package-lock-only` on npm@11+ silently
|
|
1685
|
+
// strips top-level `@emnapi/core` + `@emnapi/runtime` entries when those are
|
|
1686
|
+
// transitive deps of platform-optional bindings (e.g. `@oxc-parser/binding-*`
|
|
1687
|
+
// from knip), and CI's bundled npm@10 (Node 22 default in GitHub Actions)
|
|
1688
|
+
// then refuses `npm ci` with EUSAGE. Same recipe bit twice (#8271 / 2.58.2 /
|
|
1689
|
+
// 2.62.1) before this guard. The packageManager field in package.json
|
|
1690
|
+
// declares the same version for corepack-aware tooling. Network cost: ~5-30s
|
|
1691
|
+
// per release; release cadence makes this acceptable.
|
|
1692
|
+
function regenerateLockfile() {
|
|
1693
|
+
console.log('\nclaude-mem-lite release — regenerate lockfile (npm@10.9.2)\n');
|
|
1694
|
+
try {
|
|
1695
|
+
execFileSync('npx', ['--yes', 'npm@10.9.2', 'install'], {
|
|
1696
|
+
stdio: 'inherit',
|
|
1697
|
+
cwd: PROJECT_DIR,
|
|
1698
|
+
});
|
|
1699
|
+
ok('lockfile regenerated');
|
|
1700
|
+
} catch (e) {
|
|
1701
|
+
fail('lockfile regen failed: ' + e.message);
|
|
1702
|
+
throw e;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1652
1706
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
1653
1707
|
|
|
1654
1708
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -1680,6 +1734,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1680
1734
|
break;
|
|
1681
1735
|
case 'release':
|
|
1682
1736
|
syncVersions();
|
|
1737
|
+
if (!flags.has('--no-lock')) regenerateLockfile();
|
|
1683
1738
|
break;
|
|
1684
1739
|
default:
|
|
1685
1740
|
if (IS_NPX) {
|
|
@@ -1699,7 +1754,7 @@ Usage:
|
|
|
1699
1754
|
node install.mjs cleanup Remove stale temp/staging files
|
|
1700
1755
|
node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
|
|
1701
1756
|
node install.mjs self-update Check for and install updates
|
|
1702
|
-
node install.mjs release Sync
|
|
1757
|
+
node install.mjs release Sync versions (plugin/marketplace/CLAUDE.md) + regen lockfile via npm@10.9.2 (use --no-lock to skip lock regen)
|
|
1703
1758
|
|
|
1704
1759
|
npx claude-mem-lite Install via npx (one-liner)
|
|
1705
1760
|
`);
|
package/lib/doctor-drift.mjs
CHANGED
package/lib/save-observation.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Shared "save one observation" pipeline — used by both mem-cli.mjs::cmdSave
|
|
2
|
-
// (CLI `mem save`) and server.mjs::mem_save (MCP tool).
|
|
2
|
+
// (CLI `claude-mem-lite save`) and server.mjs::mem_save (MCP tool).
|
|
3
3
|
//
|
|
4
4
|
// Pre-extraction (v2.60.0) the same dedup → scrub → minhash → CJK-bigram →
|
|
5
5
|
// transactional INSERT block lived inline in both call sites (~110 lines × 2,
|
package/mem-cli.mjs
CHANGED
|
@@ -34,7 +34,7 @@ function cmdSearch(db, args) {
|
|
|
34
34
|
const { positional, flags } = parseArgs(args);
|
|
35
35
|
const query = positional.join(' ');
|
|
36
36
|
if (!query) {
|
|
37
|
-
fail('[mem] Usage: mem search <query> [--type TYPE] [--source SOURCE] [--limit N] [--project P] [--from DATE] [--to DATE] [--importance N] [--branch B] [--offset N] [--sort relevance|time|importance] [--include-noise]');
|
|
37
|
+
fail('[mem] Usage: claude-mem-lite search <query> [--type TYPE] [--source SOURCE] [--limit N] [--project P] [--from DATE] [--to DATE] [--importance N] [--branch B] [--offset N] [--sort relevance|time|importance] [--include-noise]');
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -400,7 +400,7 @@ function cmdRecall(db, args) {
|
|
|
400
400
|
const { positional, flags } = parseArgs(args);
|
|
401
401
|
const file = positional.join(' ');
|
|
402
402
|
if (!file) {
|
|
403
|
-
fail('[mem] Usage: mem recall <file> [--limit N] [--include-noise]');
|
|
403
|
+
fail('[mem] Usage: claude-mem-lite recall <file> [--limit N] [--include-noise]');
|
|
404
404
|
return;
|
|
405
405
|
}
|
|
406
406
|
|
|
@@ -447,6 +447,25 @@ function cmdRecall(db, args) {
|
|
|
447
447
|
|
|
448
448
|
const OBS_FIELDS = ['id', 'type', 'title', 'subtitle', 'narrative', 'text', 'facts', 'concepts', 'lesson_learned', 'search_aliases', 'files_read', 'files_modified', 'project', 'created_at', 'memory_session_id', 'prompt_number', 'importance', 'related_ids', 'access_count', 'branch', 'superseded_at', 'superseded_by', 'last_accessed_at'];
|
|
449
449
|
|
|
450
|
+
// Integer-typed time-epoch fields on the observations table that the `get`
|
|
451
|
+
// command renders. Callers expect raw ms (audit) AND a relative-time hint
|
|
452
|
+
// (human-scan), so formatObsFieldValue emits both. Other epoch fields like
|
|
453
|
+
// `created_at_epoch` / `optimized_at` / `last_injected_at` aren't in
|
|
454
|
+
// OBS_FIELDS so they don't surface via `get`.
|
|
455
|
+
export const OBS_TIME_FIELDS = ['superseded_at', 'last_accessed_at'];
|
|
456
|
+
|
|
457
|
+
// Pure formatter — null/undefined/non-time pass through; time fields on
|
|
458
|
+
// integer values render as `<raw> (<relative>)` mirroring the convention
|
|
459
|
+
// already used by `recent` / `timeline` / `recall`. Pre-2.63.0 the get
|
|
460
|
+
// path printed bare ms (e.g. `last_accessed_at: 1778357330957`).
|
|
461
|
+
export function formatObsFieldValue(field, val) {
|
|
462
|
+
if (val === null || val === undefined) return val;
|
|
463
|
+
if (OBS_TIME_FIELDS.includes(field) && typeof val === 'number') {
|
|
464
|
+
return `${val} (${relativeTime(val)})`;
|
|
465
|
+
}
|
|
466
|
+
return val;
|
|
467
|
+
}
|
|
468
|
+
|
|
450
469
|
function renderObsRows(db, ids, requestedFields) {
|
|
451
470
|
const placeholders = ids.map(() => '?').join(',');
|
|
452
471
|
try {
|
|
@@ -465,8 +484,9 @@ function renderObsRows(db, ids, requestedFields) {
|
|
|
465
484
|
const val = r[f];
|
|
466
485
|
if (val === null || val === undefined || val === '') continue;
|
|
467
486
|
if (f === 'text' && r.narrative && typeof val === 'string' && val.startsWith(r.narrative)) continue;
|
|
487
|
+
const formatted = formatObsFieldValue(f, val);
|
|
468
488
|
const maxLen = f === 'narrative' ? 1000 : f === 'lesson_learned' ? 500 : f === 'text' ? 500 : 200;
|
|
469
|
-
const display = typeof
|
|
489
|
+
const display = typeof formatted === 'string' && formatted.length > maxLen ? formatted.slice(0, maxLen) + '…' : formatted;
|
|
470
490
|
lines.push(`${f}: ${display}`);
|
|
471
491
|
}
|
|
472
492
|
parts.push(lines.join('\n'));
|
|
@@ -510,7 +530,7 @@ function cmdGet(db, args) {
|
|
|
510
530
|
const { positional, flags } = parseArgs(args);
|
|
511
531
|
const idStr = positional.join(',');
|
|
512
532
|
if (!idStr) {
|
|
513
|
-
fail('[mem] Usage: mem get <id1,id2,...> [--source obs|session|prompt] [--fields f1,f2,...]\n' +
|
|
533
|
+
fail('[mem] Usage: claude-mem-lite get <id1,id2,...> [--source obs|session|prompt] [--fields f1,f2,...]\n' +
|
|
514
534
|
' IDs accept prefix from search output: #123 (obs), P#123 (prompt), S#123 (session).');
|
|
515
535
|
return;
|
|
516
536
|
}
|
|
@@ -672,7 +692,12 @@ function cmdTimeline(db, args) {
|
|
|
672
692
|
if ((!anchorId || isNaN(anchorId)) && queryStr) {
|
|
673
693
|
const ftsQuery = sanitizeFtsQuery(queryStr);
|
|
674
694
|
const found = findFtsAnchor(db, { ftsQuery, project: project ?? null });
|
|
675
|
-
if (found)
|
|
695
|
+
if (found) {
|
|
696
|
+
anchorId = found.id;
|
|
697
|
+
if (found.relaxed && !anchorNote) {
|
|
698
|
+
anchorNote = `(query "${queryStr}" relaxed AND→OR — no row matched all terms)`;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
676
701
|
}
|
|
677
702
|
|
|
678
703
|
// No anchor: show most recent observations (aligned with MCP mem_timeline fallback)
|
|
@@ -760,7 +785,7 @@ function cmdSave(db, args) {
|
|
|
760
785
|
const { positional, flags } = parseArgs(args);
|
|
761
786
|
const text = positional.join(' ');
|
|
762
787
|
if (!text) {
|
|
763
|
-
fail('[mem] Usage: mem save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T]');
|
|
788
|
+
fail('[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T]');
|
|
764
789
|
return;
|
|
765
790
|
}
|
|
766
791
|
|
|
@@ -1112,7 +1137,7 @@ function cmdDelete(db, args) {
|
|
|
1112
1137
|
const { positional, flags } = parseArgs(args);
|
|
1113
1138
|
const idStr = positional.join(',');
|
|
1114
1139
|
if (!idStr) {
|
|
1115
|
-
fail('[mem] Usage: mem delete <id1,id2,...> [--confirm]');
|
|
1140
|
+
fail('[mem] Usage: claude-mem-lite delete <id1,id2,...> [--confirm]');
|
|
1116
1141
|
return;
|
|
1117
1142
|
}
|
|
1118
1143
|
|
|
@@ -1122,7 +1147,7 @@ function cmdDelete(db, args) {
|
|
|
1122
1147
|
const nonObs = tokens.filter(t => /^[PpSs]#?\d+$/.test(t));
|
|
1123
1148
|
if (nonObs.length > 0) {
|
|
1124
1149
|
fail(`[mem] delete only works on observations. Rejected: ${nonObs.join(', ')}. ` +
|
|
1125
|
-
`Prompts and sessions are append-only — inspect with \`mem get P#N --source prompt\` / \`--source session\`.`);
|
|
1150
|
+
`Prompts and sessions are append-only — inspect with \`claude-mem-lite get P#N --source prompt\` / \`--source session\`.`);
|
|
1126
1151
|
return;
|
|
1127
1152
|
}
|
|
1128
1153
|
const ids = tokens.map(t => {
|
|
@@ -1190,7 +1215,7 @@ function cmdUpdate(db, args) {
|
|
|
1190
1215
|
const parsed = raw ? parseIdToken(raw) : null;
|
|
1191
1216
|
const id = parsed && parsed.source === null ? parsed.id : parseInt(raw, 10);
|
|
1192
1217
|
if (!id || isNaN(id)) {
|
|
1193
|
-
fail('[mem] Usage: mem update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
|
|
1218
|
+
fail('[mem] Usage: claude-mem-lite update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
|
|
1194
1219
|
return;
|
|
1195
1220
|
}
|
|
1196
1221
|
|
|
@@ -1417,7 +1442,7 @@ function cmdMaintain(db, args) {
|
|
|
1417
1442
|
const { positional, flags } = parseArgs(args);
|
|
1418
1443
|
const action = positional[0];
|
|
1419
1444
|
if (!action || !['scan', 'execute'].includes(action)) {
|
|
1420
|
-
fail('[mem] Usage: mem maintain <scan|execute> [--ops cleanup,decay,boost,dedup,purge_stale,rebuild_vectors] [--project P] [--retain-days N] [--merge-ids keepId:removeId,...]');
|
|
1445
|
+
fail('[mem] Usage: claude-mem-lite maintain <scan|execute> [--ops cleanup,decay,boost,dedup,purge_stale,rebuild_vectors] [--project P] [--retain-days N] [--merge-ids keepId:removeId,...]');
|
|
1421
1446
|
return;
|
|
1422
1447
|
}
|
|
1423
1448
|
|
|
@@ -1697,7 +1722,7 @@ function cmdRegistry(_memDb, args) {
|
|
|
1697
1722
|
const { positional, flags } = parseArgs(args);
|
|
1698
1723
|
const action = positional[0];
|
|
1699
1724
|
if (!action || !['list', 'stats', 'search', 'import', 'remove', 'reindex'].includes(action)) {
|
|
1700
|
-
fail('[mem] Usage: mem registry <list|stats|search|import|remove|reindex> [--type skill|agent] [--query Q] [--name N] [--resource-type T]');
|
|
1725
|
+
fail('[mem] Usage: claude-mem-lite registry <list|stats|search|import|remove|reindex> [--type skill|agent] [--query Q] [--name N] [--resource-type T]');
|
|
1701
1726
|
return;
|
|
1702
1727
|
}
|
|
1703
1728
|
|
|
@@ -1713,7 +1738,7 @@ function cmdRegistry(_memDb, args) {
|
|
|
1713
1738
|
try {
|
|
1714
1739
|
if (action === 'search') {
|
|
1715
1740
|
const query = flags.query || positional.slice(1).join(' ');
|
|
1716
|
-
if (!query) { fail('[mem] Usage: mem registry search <query> [--type skill|agent] [--category C] [--quality Q]'); return; }
|
|
1741
|
+
if (!query) { fail('[mem] Usage: claude-mem-lite registry search <query> [--type skill|agent] [--category C] [--quality Q]'); return; }
|
|
1717
1742
|
let results = searchResources(rdb, query, {
|
|
1718
1743
|
type: flags.type || undefined,
|
|
1719
1744
|
limit: (flags.category || flags.quality) ? 20 : 10,
|
|
@@ -1803,7 +1828,7 @@ function cmdRegistry(_memDb, args) {
|
|
|
1803
1828
|
if (action === 'import') {
|
|
1804
1829
|
const name = flags.name;
|
|
1805
1830
|
const resourceType = flags['resource-type'];
|
|
1806
|
-
if (!name || !resourceType) { fail('[mem] Usage: mem registry import --name N --resource-type skill|agent [--invocation-name I] [--capability-summary S]'); return; }
|
|
1831
|
+
if (!name || !resourceType) { fail('[mem] Usage: claude-mem-lite registry import --name N --resource-type skill|agent [--invocation-name I] [--capability-summary S]'); return; }
|
|
1807
1832
|
const fields = { name, type: resourceType, status: 'active', source: flags.source || 'user' };
|
|
1808
1833
|
for (const f of ['repo-url', 'local-path', 'invocation-name', 'intent-tags', 'domain-tags', 'trigger-patterns', 'capability-summary', 'keywords', 'tech-stack', 'use-cases']) {
|
|
1809
1834
|
const camel = f.replace(/-([a-z])/g, (_, c) => '_' + c);
|
|
@@ -1824,7 +1849,7 @@ function cmdRegistry(_memDb, args) {
|
|
|
1824
1849
|
if (action === 'remove') {
|
|
1825
1850
|
const name = flags.name;
|
|
1826
1851
|
const resourceType = flags['resource-type'];
|
|
1827
|
-
if (!name || !resourceType) { fail('[mem] Usage: mem registry remove --name N --resource-type skill|agent'); return; }
|
|
1852
|
+
if (!name || !resourceType) { fail('[mem] Usage: claude-mem-lite registry remove --name N --resource-type skill|agent'); return; }
|
|
1828
1853
|
const result = rdb.prepare('DELETE FROM resources WHERE type = ? AND name = ?').run(resourceType, name);
|
|
1829
1854
|
out(result.changes > 0 ? `[mem] Removed: ${resourceType}:${name}` : '[mem] Not found.');
|
|
1830
1855
|
return;
|
package/package.json
CHANGED
package/search-engine.mjs
CHANGED
|
@@ -152,7 +152,7 @@ function expandObsByPRF(db, ctx, now, primaryCount, existingIds, results, includ
|
|
|
152
152
|
* 1. FTS5 MATCH with the sanitized query (AND-by-default), recency-weighted
|
|
153
153
|
* 2. If AND returns 0 → relaxFtsQueryToOr fallback (mirrors searchObservationsHybrid)
|
|
154
154
|
*
|
|
155
|
-
*
|
|
155
|
+
* Always skips compressed rows.
|
|
156
156
|
*
|
|
157
157
|
* @param {Database} db
|
|
158
158
|
* @param {object} opts
|
|
@@ -160,7 +160,8 @@ function expandObsByPRF(db, ctx, now, primaryCount, existingIds, results, includ
|
|
|
160
160
|
* @param {string|null} [opts.project] restrict to this project (boost-by-membership; null = no filter)
|
|
161
161
|
* @param {number} [opts.nowT] Date.now() override (for deterministic tests)
|
|
162
162
|
* @param {number} [opts.halfLifeMs] recency half-life (default DEFAULT_DECAY_HALF_LIFE_MS)
|
|
163
|
-
* @returns {number|null}
|
|
163
|
+
* @returns {{id:number, relaxed:boolean}|null} `relaxed:true` when AND returned 0 and OR rescued —
|
|
164
|
+
* callers should surface a "(relaxed AND→OR)" hint to mirror search transparency.
|
|
164
165
|
*/
|
|
165
166
|
export function findFtsAnchor(db, { ftsQuery, project = null, nowT = null, halfLifeMs = DEFAULT_DECAY_HALF_LIFE_MS } = {}) {
|
|
166
167
|
if (!ftsQuery) return null;
|
|
@@ -178,13 +179,13 @@ export function findFtsAnchor(db, { ftsQuery, project = null, nowT = null, halfL
|
|
|
178
179
|
const stmt = db.prepare(sql);
|
|
179
180
|
try {
|
|
180
181
|
const m = stmt.get(ftsQuery, project, project, now);
|
|
181
|
-
if (m) return m.id;
|
|
182
|
+
if (m) return { id: m.id, relaxed: false };
|
|
182
183
|
} catch (e) { debugCatch(e, 'findFtsAnchor-and'); }
|
|
183
184
|
const orQuery = relaxFtsQueryToOr(ftsQuery);
|
|
184
185
|
if (orQuery && orQuery !== ftsQuery) {
|
|
185
186
|
try {
|
|
186
187
|
const m = stmt.get(orQuery, project, project, now);
|
|
187
|
-
if (m) return m.id;
|
|
188
|
+
if (m) return { id: m.id, relaxed: true };
|
|
188
189
|
} catch (e) { debugCatch(e, 'findFtsAnchor-or'); }
|
|
189
190
|
}
|
|
190
191
|
return null;
|
package/server.mjs
CHANGED
|
@@ -614,11 +614,18 @@ server.registerTool(
|
|
|
614
614
|
|
|
615
615
|
// Auto-find anchor via FTS (with recency decay). Routes through shared
|
|
616
616
|
// findFtsAnchor so CLI `timeline --query` and MCP mem_timeline use
|
|
617
|
-
// identical AND→OR fallback semantics (paired-path per #8217).
|
|
617
|
+
// identical AND→OR fallback semantics (paired-path per #8217). When the
|
|
618
|
+
// OR fallback fired, surface a hint so the caller knows the match was
|
|
619
|
+
// not an exact AND coverage of the query — mirrors search transparency.
|
|
618
620
|
if (!anchorId && args.query) {
|
|
619
621
|
const ftsQuery = sanitizeFtsQuery(args.query);
|
|
620
622
|
const found = findFtsAnchor(db, { ftsQuery, project: args.project ?? null });
|
|
621
|
-
if (found)
|
|
623
|
+
if (found) {
|
|
624
|
+
anchorId = found.id;
|
|
625
|
+
if (found.relaxed && !anchorNote) {
|
|
626
|
+
anchorNote = `(query "${args.query}" relaxed AND→OR — no row matched all terms)`;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
622
629
|
}
|
|
623
630
|
|
|
624
631
|
// No anchor: return most recent
|