claude-mem-lite 6.9.0 → 6.9.1
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.mjs +40 -0
- package/install.mjs +71 -6
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/search-scoring.mjs +69 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.9.
|
|
12
|
+
"version": "6.9.1",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.9.
|
|
3
|
+
"version": "6.9.1",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/cli.mjs
CHANGED
|
@@ -136,6 +136,46 @@ const INSTALL_COMMANDS = new Set([
|
|
|
136
136
|
'release',
|
|
137
137
|
]);
|
|
138
138
|
|
|
139
|
+
// A reader that leaves is not an error. `claude-mem-lite search x | head -1`,
|
|
140
|
+
// `| grep -q`, or quitting `less` closes the read end while we are still writing;
|
|
141
|
+
// Node then emits 'error' on the stdout Socket, and with no listener that is an
|
|
142
|
+
// UNHANDLED error event — a ~20-line stack ending in `outVerbatim` where the user
|
|
143
|
+
// expected the shell prompt.
|
|
144
|
+
//
|
|
145
|
+
// WHICH COMMANDS, measured rather than generalised (20 trials each, `| head -1`,
|
|
146
|
+
// pre-fix): `search`, `export`, `recent`, `stats`, `doctor`, `timeline`,
|
|
147
|
+
// `citation-stats` 20/20; `browse` 19/20; `help`, `status`, `context`, `get`,
|
|
148
|
+
// `memdir-audit` 0/20. So NOT "every stdout-bearing command" — what decides it is
|
|
149
|
+
// whether a write is still pending when the reader goes, which depends on how many
|
|
150
|
+
// lines the consumer takes and how the output is batched — NOT on the 64 KB pipe
|
|
151
|
+
// buffer, which an earlier draft of this comment blamed: pre-ship review found
|
|
152
|
+
// `stats` crashing at `head -20` on an output far under it. That output's size is
|
|
153
|
+
// corpus dependent, so no byte count is quoted here. This is also why the crash
|
|
154
|
+
// survived so long — it is invisible to exactly the pipe depths a smoke test picks.
|
|
155
|
+
//
|
|
156
|
+
// Lives HERE, at the published `bin`, and not at `cli/common.mjs`'s `out()`: the
|
|
157
|
+
// crash reproduces on `doctor` too, whose writes are `console.log` inside
|
|
158
|
+
// install.mjs, so a chokepoint fix would cover the CLI half and leave the installer
|
|
159
|
+
// half loud. One process-level listener covers both routes below.
|
|
160
|
+
//
|
|
161
|
+
// SWALLOW, DO NOT EXIT. The first cut called `process.exit(0)` here, on the
|
|
162
|
+
// reasoning that a CLI whose consumer has gone should stop rather than serialise a
|
|
163
|
+
// whole-DB `export` into a dead pipe. Pre-ship review measured what that costs:
|
|
164
|
+
// `doctor | head -1` under `pipefail` exited 0 on 10/10 runs while the same doctor
|
|
165
|
+
// exits 1 unpiped, because `runDoctor` assigns `process.exitCode = 1` AFTER its last
|
|
166
|
+
// print (install.mjs, "Diagnostic-tool exit-code contract") and the forced exit lands
|
|
167
|
+
// first. That silently turns a failing `claude-mem-lite doctor || alert` — the
|
|
168
|
+
// wrapper that contract names — into a passing one. `process.exitCode ?? 0` does not
|
|
169
|
+
// rescue it: the verdict does not exist yet at kill time. Returning instead reads
|
|
170
|
+
// exit 1 on 10/10 and keeps the crash fixed (doctor 0/10, search 0/10 EPIPE stacks).
|
|
171
|
+
// Correctness over the saved work: the process finishes into a pipe nobody reads,
|
|
172
|
+
// which is wasted effort but never a wrong answer. Non-EPIPE is rethrown — this is a
|
|
173
|
+
// classifier, not a blanket swallow, the same charter `explainBrokenInstall` follows.
|
|
174
|
+
process.stdout.on('error', (err) => {
|
|
175
|
+
if (err && err.code === 'EPIPE') return;
|
|
176
|
+
throw err;
|
|
177
|
+
});
|
|
178
|
+
|
|
139
179
|
const cmd = process.argv[2];
|
|
140
180
|
|
|
141
181
|
// `version` and `-V` are aliases, not extra syntax: the bare subcommand is what a user
|
package/install.mjs
CHANGED
|
@@ -44,6 +44,15 @@ const MEM_RUNTIME_DIR = resolveRuntimeDir(MEM_DATA_DIR);
|
|
|
44
44
|
const DB_PATH = join(MEM_DATA_DIR, 'claude-mem-lite.db');
|
|
45
45
|
const OLD_DATA_DIR = join(homedir(), '.claude-mem');
|
|
46
46
|
|
|
47
|
+
// The two directories `createCliSymlink` can land the `claude-mem-lite` command in, in the
|
|
48
|
+
// order it tries them. Uninstall already swept exactly this pair as an inline literal, and
|
|
49
|
+
// `status` now has to ask the same question ("is the command installed somewhere, just not
|
|
50
|
+
// on PATH?"), so the list is named once rather than typed a third time. `createCliSymlink`
|
|
51
|
+
// itself is deliberately NOT rewritten to iterate it: its shape is primary-then-fallback
|
|
52
|
+
// with different remedies per branch, and flattening that into a loop is a refactor wearing
|
|
53
|
+
// a constant's clothes.
|
|
54
|
+
const CLI_BIN_DIRS = [join(homedir(), '.local', 'bin'), '/usr/local/bin'];
|
|
55
|
+
|
|
47
56
|
// Detect ephemeral context (npx) — files won't persist after exit
|
|
48
57
|
const IS_NPX =
|
|
49
58
|
process.env.npm_command === 'exec' || PROJECT_DIR.includes('_npx') || PROJECT_DIR.includes('.npm/_');
|
|
@@ -1271,7 +1280,7 @@ async function uninstall() {
|
|
|
1271
1280
|
if (!removedAny) warn('MCP server not found or already removed');
|
|
1272
1281
|
|
|
1273
1282
|
// 1b. Remove CLI symlink
|
|
1274
|
-
for (const binDir of
|
|
1283
|
+
for (const binDir of CLI_BIN_DIRS) {
|
|
1275
1284
|
const cliLink = join(binDir, 'claude-mem-lite');
|
|
1276
1285
|
// No try/catch: clearLinkPath swallows a permissions failure and returns false, so the
|
|
1277
1286
|
// wrapper this used to have was unreachable once the existsSync gate moved inside it.
|
|
@@ -1654,14 +1663,70 @@ async function status() {
|
|
|
1654
1663
|
push('warn', 'database', 'Database: not found', { exists: false });
|
|
1655
1664
|
}
|
|
1656
1665
|
|
|
1657
|
-
// CLI
|
|
1666
|
+
// CLI.
|
|
1667
|
+
//
|
|
1668
|
+
// The probe resolves a BARE name, so a failure conflates three different worlds and the
|
|
1669
|
+
// old single remedy ("run install again to create symlink") was correct in only one of
|
|
1670
|
+
// them. On the common one — `createCliSymlink` put a working link in ~/.local/bin, a
|
|
1671
|
+
// directory a non-login shell frequently does not have on PATH — the advice sends the
|
|
1672
|
+
// user to re-run an installer that will create the very symlink that already exists,
|
|
1673
|
+
// report ✓, and leave `status` saying the same thing. Advice that cannot converge is
|
|
1674
|
+
// worse than the silence it replaced.
|
|
1675
|
+
//
|
|
1676
|
+
// Split on `err.code`: ENOENT is "the name did not resolve" and is the only world the
|
|
1677
|
+
// symlink question applies to. Anything else means the command WAS found and then failed
|
|
1678
|
+
// or timed out (a broken native binding is the live example), where naming PATH is a
|
|
1679
|
+
// second wrong answer — report what actually happened instead.
|
|
1680
|
+
// `linked` is a NEW field on this check, and `--json` republishes every extra key
|
|
1681
|
+
// (`const { level, key, message, ...extra }` below), so it is part of that face's output,
|
|
1682
|
+
// not an internal detail. Nothing in this repo reads it; external consumers of
|
|
1683
|
+
// `status --json` now see `linked: <path>|null`.
|
|
1658
1684
|
try {
|
|
1659
1685
|
execFileSync('claude-mem-lite', ['--help'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
|
|
1660
1686
|
push('ok', 'cli', 'CLI: claude-mem-lite command available', { available: true });
|
|
1661
|
-
} catch {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1687
|
+
} catch (e) {
|
|
1688
|
+
if (e && e.code !== 'ENOENT') {
|
|
1689
|
+
// `e.message` already CARRIES the child's stderr — with stdio:'pipe' Node formats it
|
|
1690
|
+
// as "Command failed: <cmd>\n<stderr>", so the live example (a broken native binding,
|
|
1691
|
+
// whose `nativeBindingRepairHint` line is on stderr) reaches the user unaided.
|
|
1692
|
+
// Measured, because pre-ship review asserted the opposite and a redundant `e.stderr`
|
|
1693
|
+
// suffix was written and then withdrawn: printing both duplicates the text.
|
|
1694
|
+
// Note `e.code` is UNDEFINED for a non-zero exit — only a spawn failure sets ENOENT —
|
|
1695
|
+
// so `!== 'ENOENT'` is what routes this branch, not a truthiness check on the code.
|
|
1696
|
+
push('warn', 'cli', `CLI: on PATH but "claude-mem-lite --help" failed — ${e.message}`, {
|
|
1697
|
+
available: false,
|
|
1698
|
+
linked: null,
|
|
1699
|
+
});
|
|
1700
|
+
} else {
|
|
1701
|
+
// Two properties of `existsSync` matter here and they pull in opposite directions:
|
|
1702
|
+
// - it FOLLOWS the link, so a DANGLING one reads as absent. That is the answer we
|
|
1703
|
+
// want: a link pointing at a deleted install is the installer's problem, not
|
|
1704
|
+
// PATH's, and falls through to the reinstall remedy.
|
|
1705
|
+
// - it is also true for a DIRECTORY of that name, which would make us print
|
|
1706
|
+
// "installed at … add it to PATH" about something that can never be executed —
|
|
1707
|
+
// the exact non-converging advice this block exists to stop. Hence isFile().
|
|
1708
|
+
const isLinkedCli = (d) => {
|
|
1709
|
+
try {
|
|
1710
|
+
return statSync(join(d, 'claude-mem-lite')).isFile();
|
|
1711
|
+
} catch {
|
|
1712
|
+
return false; // ENOENT (absent or dangling), EACCES on the dir, anything else
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
const binDir = CLI_BIN_DIRS.find(isLinkedCli);
|
|
1716
|
+
if (binDir) {
|
|
1717
|
+
push(
|
|
1718
|
+
'warn',
|
|
1719
|
+
'cli',
|
|
1720
|
+
`CLI: installed at ${join(binDir, 'claude-mem-lite')} but ${binDir} is not on PATH — add it: export PATH="${binDir}:$PATH"`,
|
|
1721
|
+
{ available: false, linked: join(binDir, 'claude-mem-lite') },
|
|
1722
|
+
);
|
|
1723
|
+
} else {
|
|
1724
|
+
push('warn', 'cli', 'CLI: command not on PATH — run install again to create symlink', {
|
|
1725
|
+
available: false,
|
|
1726
|
+
linked: null,
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1665
1730
|
}
|
|
1666
1731
|
|
|
1667
1732
|
// Old system
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.9.
|
|
3
|
+
"version": "6.9.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.9.
|
|
9
|
+
"version": "6.9.1",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.9.
|
|
3
|
+
"version": "6.9.1",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/search-scoring.mjs
CHANGED
|
@@ -12,6 +12,18 @@ import { CLI_INVOKE } from './cli-path.mjs';
|
|
|
12
12
|
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
13
13
|
|
|
14
14
|
import { DAY_MS } from './lib/time-constants.mjs';
|
|
15
|
+
// The pinned-but-uncited threshold, from the module that OWNS the rule (`demotePinned` and
|
|
16
|
+
// its forecast both read it there). Imported rather than restated: a second hand-typed copy
|
|
17
|
+
// of this number is exactly what produced the `inj>=8` report-string drift in server.mjs.
|
|
18
|
+
// (The v3.76.0 scan-vs-execute drift was a different constant — two copies of the FLOOR,
|
|
19
|
+
// `importance > 1`; do not cite it for this one.) The edge is cheap: `lib/maintain-core.mjs`
|
|
20
|
+
// has five direct imports — `utils.mjs`, `lib/dedup-constants.mjs`,
|
|
21
|
+
// `lib/inject-search-core.mjs`, `lib/time-constants.mjs` and `lib/db-backup.mjs` — three of
|
|
22
|
+
// which this module already imports directly, and the 16-file closure resolves to nothing
|
|
23
|
+
// but node builtins (`fs`, `path`, `child_process`, `node:os`, `node:path`) — no
|
|
24
|
+
// third-party package, so no native dependency, and no edge back to this file (the three
|
|
25
|
+
// "search-scoring" strings in that graph are all comments).
|
|
26
|
+
import { PINNED_INJ_THRESHOLD } from './lib/maintain-core.mjs';
|
|
15
27
|
// ─── MCP Server Instructions Builder ───────────────────────────────────────
|
|
16
28
|
// Phase A (v2.31.3+): when quiet=true, drops WHEN-TO-USE proactive-trigger and
|
|
17
29
|
// Decision-rules sections; keeps the irreducible CLI/MCP tool list. Intended
|
|
@@ -303,6 +315,57 @@ export function expandQueryByConcepts(db, ftsQuery, project) {
|
|
|
303
315
|
* Boost importance to 2 for observations that have been accessed multiple times
|
|
304
316
|
* (access_count >= 2) but still have default importance (1).
|
|
305
317
|
* Called after incrementing access_count in mem_get.
|
|
318
|
+
*
|
|
319
|
+
* A PROMOTER OUTSIDE THE MAINTENANCE RUN. `lib/maintain-core.mjs`'s DEFAULT_MAINTAIN_OPS
|
|
320
|
+
* docblock records `boostAccessed` and `demotePinned` as opponents, and names TWO separate
|
|
321
|
+
* defects it closed — keep them apart: the automatic path promoted and never demoted
|
|
322
|
+
* because `demote_pinned` was in nobody's default set and hook.mjs did not import it, while
|
|
323
|
+
* the two faces that DID wire the op ran it in opposite orders. The 148/148 figure quoted
|
|
324
|
+
* there is rows sitting back at importance>=3 that were BOOST-ELIGIBLE, not rows this op
|
|
325
|
+
* would have moved — CHANGELOG.md narrows the reachable-by-demotePinned count to 7.
|
|
326
|
+
*
|
|
327
|
+
* Both of those fixes act INSIDE a maintenance run. This function fires from
|
|
328
|
+
* `fetchObsDetail`, so it is a promoter neither a default-set nor an ordering fix can reach:
|
|
329
|
+
* every `get` / `mem_get` was another chance to hand the row straight back — the same
|
|
330
|
+
* sentence that docblock uses for the bug it closed.
|
|
331
|
+
*
|
|
332
|
+
* Reproduced end-to-end before this clause: `maintain execute --ops demote_pinned` floors a
|
|
333
|
+
* pinned-but-uncited row 2 -> 1, and ONE subsequent `get` returned it to 2. So an op in the
|
|
334
|
+
* default maintain set (`lib/maintain-core.mjs` DEFAULT_MAINTAIN_OPS) had an effect any read
|
|
335
|
+
* reverted, on its own target population.
|
|
336
|
+
*
|
|
337
|
+
* Population, stated rather than implied (doctrine rule 3): on the maintainer's DB sampled
|
|
338
|
+
* 2026-09-14T20:14:16Z, 67 live rows, 3 pinned-but-uncited, and 0 of those had reached
|
|
339
|
+
* access_count >= 2 — `injection_count` does not bump `access_count`, so the overlap needs
|
|
340
|
+
* two explicit reads and was unrealised there. The mechanism is deterministic; the observed
|
|
341
|
+
* incidence on that corpus is zero, and this is a snapshot of one corpus, not a property.
|
|
342
|
+
*
|
|
343
|
+
* WHAT THIS DELIBERATELY DOES NOT FIX. `importance = 1` is also what an explicit
|
|
344
|
+
* `claude-mem-lite update N --importance 1` writes, and that demotion is reverted by the
|
|
345
|
+
* next read exactly the same way — worse, INSPECTING the row is what pushes access_count to
|
|
346
|
+
* 2 in the first place. Separating "1 because nobody set it" from "1 because a human said
|
|
347
|
+
* so" needs a marker this schema does not have, and `demoted_at` is not it — for a reason
|
|
348
|
+
* the first draft of this paragraph got wrong, so it is stated precisely: nothing keys on
|
|
349
|
+
* `demoted_at IS NULL` (the sole non-test reader, `mem-cli.mjs`'s decay-queue report, keys
|
|
350
|
+
* on IS NOT NULL), so a write here would not evict anything — it would ADD the row to that
|
|
351
|
+
* report, and `applyCitationDecay` CLEARS the column on the row's first citation, silently
|
|
352
|
+
* discarding a human's demotion. Wrong owner, wrong lifetime. Left as a design decision
|
|
353
|
+
* rather than patched around.
|
|
354
|
+
*
|
|
355
|
+
* THE EXCLUSION IS demotePinned's FLOOR, NOT ITS TRIGGER. `PINNED_FLOOR_SQL` in
|
|
356
|
+
* lib/maintain-core.mjs is `CASE WHEN <no lesson> THEN 1 ELSE 2 END`, so a LESSON-BEARING
|
|
357
|
+
* pinned row is floored at 2 and boosting it 1 -> 2 lands it exactly on that floor. The
|
|
358
|
+
* first cut of this clause copied the trigger (`inj >= N AND cited = 0`) without the floor
|
|
359
|
+
* and stranded those rows at 1, below the bound maintain-core declares for them — and 16 of
|
|
360
|
+
* the 17 rows that op would have moved on the maintainer's DB were lesson-bearing. The
|
|
361
|
+
* predicate below must keep selecting exactly the rows demotePinned would floor to 1.
|
|
362
|
+
*
|
|
363
|
+
* The no-lesson clause is COPIED rather than imported: `NO_LESSON_SQL` and `PINNED_FLOOR_SQL`
|
|
364
|
+
* are module-private in maintain-core by an explicit decision recorded there ("exporting by
|
|
365
|
+
* habit is how the knip baseline drifts"), and five verbatim copies already live in that
|
|
366
|
+
* file. The prose stays out here in the docblock rather than inside the SQL string, because
|
|
367
|
+
* a backtick in a template literal ends it — that cost one parse error on this very edit.
|
|
368
|
+
*
|
|
306
369
|
* @param {object} db better-sqlite3 database handle
|
|
307
370
|
* @param {number[]} ids Array of observation IDs to check
|
|
308
371
|
*/
|
|
@@ -315,6 +378,12 @@ export function autoBoostIfNeeded(db, ids) {
|
|
|
315
378
|
WHERE id IN (${placeholders})
|
|
316
379
|
AND COALESCE(importance, 1) = 1
|
|
317
380
|
AND COALESCE(access_count, 0) >= 2
|
|
381
|
+
-- Exactly the rows demotePinned would floor to 1 (see the docblock above).
|
|
382
|
+
AND NOT (
|
|
383
|
+
COALESCE(injection_count, 0) >= ${PINNED_INJ_THRESHOLD}
|
|
384
|
+
AND COALESCE(cited_count, 0) = 0
|
|
385
|
+
AND (lesson_learned IS NULL OR lesson_learned = '' OR lesson_learned = 'none')
|
|
386
|
+
)
|
|
318
387
|
`,
|
|
319
388
|
).run(...ids);
|
|
320
389
|
}
|