claude-mem-lite 2.69.0 → 2.71.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/README.md +6 -3
- package/cli.mjs +1 -1
- package/hook-context.mjs +20 -16
- package/hook-handoff.mjs +25 -7
- package/hook-llm.mjs +95 -19
- package/hook-optimize.mjs +45 -7
- package/hook-precompact.mjs +44 -0
- package/hook.mjs +79 -19
- package/hooks/hooks.json +12 -0
- package/lib/deferred-work.mjs +171 -0
- package/lib/git-state.mjs +15 -0
- package/lib/import-jsonl.mjs +225 -0
- package/lib/scrub-record.mjs +63 -0
- package/lib/upgrade-banner.mjs +31 -0
- package/mem-cli.mjs +235 -12
- package/package.json +6 -1
- package/schema.mjs +33 -1
- package/server.mjs +132 -21
- package/source-files.mjs +17 -1
- package/tool-schemas.mjs +107 -4
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// claude-mem-lite: per-table scrub helper. Applies scrubSecrets to the known
|
|
2
|
+
// text fields of a table row. Numeric / JSON-blob / id fields are passed
|
|
3
|
+
// through untouched.
|
|
4
|
+
//
|
|
5
|
+
// Failsafe policy: when the table is unknown, scrub every string field by
|
|
6
|
+
// default. Newly added tables stay safe even before TEXT_FIELDS_BY_TABLE is
|
|
7
|
+
// updated — over-scrubbing is the safe direction; under-scrubbing leaks.
|
|
8
|
+
//
|
|
9
|
+
// JSON-stringified array fields (e.g. session_handoffs.key_files,
|
|
10
|
+
// session_handoffs.match_keywords-when-array) are NOT listed here — running
|
|
11
|
+
// scrubSecrets over the JSON string can rewrite quoted values and break
|
|
12
|
+
// downstream JSON.parse. Pre-scrub each element upstream of the
|
|
13
|
+
// JSON.stringify call instead.
|
|
14
|
+
|
|
15
|
+
import { scrubSecrets } from '../secret-scrub.mjs';
|
|
16
|
+
|
|
17
|
+
export const TEXT_FIELDS_BY_TABLE = {
|
|
18
|
+
observations: [
|
|
19
|
+
'title', 'subtitle', 'text', 'narrative',
|
|
20
|
+
'concepts', 'facts', 'lesson_learned', 'search_aliases',
|
|
21
|
+
],
|
|
22
|
+
session_summaries: [
|
|
23
|
+
'request', 'investigated', 'learned',
|
|
24
|
+
'completed', 'next_steps', 'remaining_items', 'notes',
|
|
25
|
+
'lessons', 'key_decisions',
|
|
26
|
+
],
|
|
27
|
+
session_handoffs: [
|
|
28
|
+
'working_on', 'completed', 'unfinished',
|
|
29
|
+
// Excluded:
|
|
30
|
+
// key_files — JSON.stringify(array); pre-scrub elements at call site
|
|
31
|
+
// match_keywords — currently a space-joined plain string; keeping it
|
|
32
|
+
// here would scrub safely, but the value is built from
|
|
33
|
+
// tokenizeHandoff() output (alphanumeric tokens only),
|
|
34
|
+
// so secrets cannot survive the upstream tokenizer.
|
|
35
|
+
// Excluded to avoid double-work + future-proof against
|
|
36
|
+
// a refactor that switches to JSON.stringify.
|
|
37
|
+
// key_decisions is kept: call site uses '\n'.join (plain string), and
|
|
38
|
+
// decision titles can carry secrets verbatim (LLM output).
|
|
39
|
+
'key_decisions',
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scrub the text fields of a record before INSERT.
|
|
45
|
+
* Returns a shallow copy with string text-fields scrubbed; the input object
|
|
46
|
+
* is left untouched. Non-string values (numbers, null, JSON blobs the caller
|
|
47
|
+
* has already stringified) flow through unchanged.
|
|
48
|
+
*/
|
|
49
|
+
export function scrubRecord(table, row) {
|
|
50
|
+
if (!row || typeof row !== 'object') return row;
|
|
51
|
+
const fields = TEXT_FIELDS_BY_TABLE[table];
|
|
52
|
+
const out = { ...row };
|
|
53
|
+
if (fields) {
|
|
54
|
+
for (const f of fields) {
|
|
55
|
+
if (typeof out[f] === 'string') out[f] = scrubSecrets(out[f]);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
for (const k of Object.keys(out)) {
|
|
59
|
+
if (typeof out[k] === 'string') out[k] = scrubSecrets(out[k]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// One-shot v2.70.0 upgrade banner.
|
|
2
|
+
// Split out of hook.mjs because hook.mjs has module-level side effects
|
|
3
|
+
// (notably `if (!event) process.exit(0)` at top level) that abort vitest
|
|
4
|
+
// workers if imported directly from a test. See test
|
|
5
|
+
// tests/hook-upgrade-banner.test.mjs.
|
|
6
|
+
|
|
7
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One-shot stderr banner on first SessionStart after v2.70.0 upgrade.
|
|
12
|
+
* Notifies users that the `### Deferred Work` block now reads from the
|
|
13
|
+
* deferred_work table (not high-importance observations as in v2.69.x).
|
|
14
|
+
* Idempotent via a marker file in `runtimeDir`; subsequent calls in the
|
|
15
|
+
* same project are silent.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} args
|
|
18
|
+
* @param {string} args.project Project name (used in banner + marker filename).
|
|
19
|
+
* @param {string} args.runtimeDir RUNTIME_DIR (test override; production passes hook-shared.RUNTIME_DIR).
|
|
20
|
+
*/
|
|
21
|
+
export function emitV270UpgradeBanner({ project, runtimeDir }) {
|
|
22
|
+
const marker = join(runtimeDir, `.deferred-block-migrated-${project}`);
|
|
23
|
+
if (existsSync(marker)) return;
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
`[mem] v2.70.0 upgrade notice (project "${project}"): Deferred Work block now ` +
|
|
26
|
+
`backed by deferred_work table. To keep an obs visible there, run ` +
|
|
27
|
+
`\`claude-mem-lite defer add "<title>" --priority 3\`. ` +
|
|
28
|
+
`Pin to 2.69.x to revert.\n`
|
|
29
|
+
);
|
|
30
|
+
try { writeFileSync(marker, String(Date.now())); } catch { /* best-effort marker */ }
|
|
31
|
+
}
|
package/mem-cli.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { autoBoostIfNeeded, reRankWithContext, markSuperseded } from './server-i
|
|
|
14
14
|
import { searchObservationsHybrid, findFtsAnchor } from './search-engine.mjs';
|
|
15
15
|
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
16
16
|
import { searchResources } from './registry-retriever.mjs';
|
|
17
|
+
import { scrubRecord } from './lib/scrub-record.mjs';
|
|
17
18
|
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
18
19
|
import { buildSessionContextLines } from './hook-context.mjs';
|
|
19
20
|
import { cmdAdopt, cmdUnadopt } from './adopt-cli.mjs';
|
|
@@ -28,6 +29,10 @@ import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
|
28
29
|
// move each cmdXxx into its own cli/<cmd>.mjs; mem-cli.mjs becomes pure dispatch.
|
|
29
30
|
import { parseArgs, out, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints } from './cli/common.mjs';
|
|
30
31
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
32
|
+
import {
|
|
33
|
+
insertDeferred, listOpenWithOrdinal, dropDeferred,
|
|
34
|
+
resolveDeferredIds, closeDeferredItems,
|
|
35
|
+
} from './lib/deferred-work.mjs';
|
|
31
36
|
|
|
32
37
|
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
33
38
|
|
|
@@ -884,7 +889,7 @@ function cmdSave(db, args) {
|
|
|
884
889
|
const { positional, flags } = parseArgs(args);
|
|
885
890
|
const text = positional.join(' ');
|
|
886
891
|
if (!text) {
|
|
887
|
-
fail('[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T]');
|
|
892
|
+
fail('[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T] [--closes-deferred 1,D#42]');
|
|
888
893
|
return;
|
|
889
894
|
}
|
|
890
895
|
|
|
@@ -914,15 +919,60 @@ function cmdSave(db, args) {
|
|
|
914
919
|
return;
|
|
915
920
|
}
|
|
916
921
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
922
|
+
// --closes-deferred parsing: accepts comma-separated mixed tokens
|
|
923
|
+
// ("1,D#42,3") with bare integers treated as ordinals and "D#N" as raw ids.
|
|
924
|
+
// We pre-parse tokens here (cheap, syntax-only) but defer resolveDeferredIds
|
|
925
|
+
// INTO the transaction, AFTER the dedup check. Resolving outside the
|
|
926
|
+
// transaction would throw on the duplicate-replay path: the previously-
|
|
927
|
+
// closed deferred row is no longer 'open', so ordinal/id resolution would
|
|
928
|
+
// crash even though the duplicate short-circuit makes closure a no-op.
|
|
929
|
+
// Resolving inside the dedup-gated branch keeps "save the same content
|
|
930
|
+
// twice" idempotent (mirrors server.mjs:934 dedup-skip-closure intent).
|
|
931
|
+
let closesTokens = null;
|
|
932
|
+
if (flags['closes-deferred'] !== undefined && flags['closes-deferred'] !== false) {
|
|
933
|
+
const raw = String(flags['closes-deferred']);
|
|
934
|
+
closesTokens = raw.split(',').map(t => t.trim()).filter(Boolean).map(t => {
|
|
935
|
+
return /^\d+$/.test(t) ? parseInt(t, 10) : t;
|
|
936
|
+
});
|
|
937
|
+
if (closesTokens.length === 0) {
|
|
938
|
+
fail('[mem] --closes-deferred requires at least one token (integer ordinal or D#N)');
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
let result;
|
|
944
|
+
let closesIds = null;
|
|
945
|
+
try {
|
|
946
|
+
result = db.transaction(() => {
|
|
947
|
+
const r = saveObservation(db, {
|
|
948
|
+
content: text,
|
|
949
|
+
title: flags.title,
|
|
950
|
+
type,
|
|
951
|
+
importance: rawImp,
|
|
952
|
+
project,
|
|
953
|
+
files: saveFiles,
|
|
954
|
+
lesson_learned: rawLesson,
|
|
955
|
+
});
|
|
956
|
+
// Skip closure on dedup short-circuit — the obs row already exists, so
|
|
957
|
+
// the deferred item should NOT be re-closed by a duplicate save call.
|
|
958
|
+
// Resolving deferred ids only on the non-duplicate path keeps repeated
|
|
959
|
+
// save commands (with the same --closes-deferred) idempotent even after
|
|
960
|
+
// the deferred row has transitioned out of 'open'.
|
|
961
|
+
if (r.kind === 'duplicate') return r;
|
|
962
|
+
if (closesTokens) {
|
|
963
|
+
closesIds = resolveDeferredIds(db, project, closesTokens);
|
|
964
|
+
closeDeferredItems(db, closesIds, r.id);
|
|
965
|
+
}
|
|
966
|
+
return r;
|
|
967
|
+
})();
|
|
968
|
+
} catch (e) {
|
|
969
|
+
if (closesTokens) {
|
|
970
|
+
fail(`[mem] save with --closes-deferred failed: ${e.message}`);
|
|
971
|
+
} else {
|
|
972
|
+
fail(`[mem] save failed: ${e.message}`);
|
|
973
|
+
}
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
926
976
|
|
|
927
977
|
if (result.kind === 'duplicate') {
|
|
928
978
|
out(`[mem] Skipped: similar to existing #${result.existingId}. Use "claude-mem-lite get ${result.existingId}" to review.`);
|
|
@@ -930,7 +980,108 @@ function cmdSave(db, args) {
|
|
|
930
980
|
}
|
|
931
981
|
|
|
932
982
|
const lessonNote = result.lessonCaptured ? ' 💡lesson captured' : '';
|
|
933
|
-
|
|
983
|
+
const closedNote = closesIds && closesIds.length > 0
|
|
984
|
+
? ` Closed: ${closesIds.map(i => `D#${i}`).join(', ')}.`
|
|
985
|
+
: '';
|
|
986
|
+
out(`[mem] Saved #${result.id} [${result.type}] "${truncate(result.title, 80)}" (project: ${result.project})${lessonNote}${closedNote}`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ─── cmdDefer (sub-dispatch: add | list | drop) ──────────────────────────────
|
|
990
|
+
|
|
991
|
+
function cmdDefer(db, args) {
|
|
992
|
+
const sub = args[0];
|
|
993
|
+
const rest = args.slice(1);
|
|
994
|
+
switch (sub) {
|
|
995
|
+
case 'add': cmdDeferAdd(db, rest); break;
|
|
996
|
+
case 'list': cmdDeferList(db, rest); break;
|
|
997
|
+
case 'drop': cmdDeferDrop(db, rest); break;
|
|
998
|
+
default:
|
|
999
|
+
fail('[mem] Usage: claude-mem-lite defer <add|list|drop> ...');
|
|
1000
|
+
fail('[mem] defer add "<title>" [--priority 1|2|3] [--detail T] [--files f1,f2] [--project P]');
|
|
1001
|
+
fail('[mem] defer list [--project P] [--limit N]');
|
|
1002
|
+
fail('[mem] defer drop <id-or-D#N> --reason "<reason>" [--project P]');
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function cmdDeferAdd(db, args) {
|
|
1007
|
+
const { positional, flags } = parseArgs(args);
|
|
1008
|
+
const title = positional.join(' ').trim();
|
|
1009
|
+
if (!title) {
|
|
1010
|
+
fail('[mem] Usage: claude-mem-lite defer add "<title>" [--priority 1|2|3] [--detail T] [--files f1,f2] [--project P]');
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const priority = flags.priority !== undefined ? parseInt(flags.priority, 10) : 2;
|
|
1014
|
+
if (![1, 2, 3].includes(priority)) {
|
|
1015
|
+
fail(`[mem] Invalid --priority "${flags.priority}". Must be 1 (low), 2 (normal), or 3 (urgent).`);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
1019
|
+
const detail = typeof flags.detail === 'string' ? flags.detail : null;
|
|
1020
|
+
const files = flags.files
|
|
1021
|
+
? flags.files.split(',').map(f => f.trim()).filter(Boolean)
|
|
1022
|
+
: null;
|
|
1023
|
+
|
|
1024
|
+
let r;
|
|
1025
|
+
try {
|
|
1026
|
+
r = insertDeferred(db, { project, title, priority, detail, files });
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
fail(`[mem] defer add failed: ${e.message}`);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
// Compute the freshly-inserted row's ordinal for an immediately-actionable
|
|
1032
|
+
// response ("ok, deferred this as item N"). Mirrors server.mjs:980.
|
|
1033
|
+
const open = listOpenWithOrdinal(db, project, 50);
|
|
1034
|
+
const ord = open.find(o => o.id === r.id)?.ordinal ?? '?';
|
|
1035
|
+
out(`[mem] Deferred as D#${r.id} (item ${ord}) in project "${project}".`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function cmdDeferList(db, args) {
|
|
1039
|
+
const { flags } = parseArgs(args);
|
|
1040
|
+
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
1041
|
+
const limit = parseIntFlag(flags.limit, { name: '--limit', defaultValue: 10, max: 100 });
|
|
1042
|
+
const list = listOpenWithOrdinal(db, project, limit);
|
|
1043
|
+
if (list.length === 0) {
|
|
1044
|
+
out(`[mem] No open deferred items in project "${project}".`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
out(`[mem] Open deferred items (project "${project}"):`);
|
|
1048
|
+
for (const r of list) {
|
|
1049
|
+
const pTag = r.priority === 3 ? '🔴' : r.priority === 1 ? '⚪' : '🟡';
|
|
1050
|
+
out(` ${r.ordinal}. ${pTag} [P${r.priority}] ${r.title} (D#${r.id})`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function cmdDeferDrop(db, args) {
|
|
1055
|
+
const { positional, flags } = parseArgs(args);
|
|
1056
|
+
if (positional.length === 0) {
|
|
1057
|
+
fail('[mem] Usage: claude-mem-lite defer drop <id-or-D#N> --reason "<reason>" [--project P]');
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const reason = flags.reason;
|
|
1061
|
+
if (!reason || typeof reason !== 'string' || reason.trim().length === 0) {
|
|
1062
|
+
fail('[mem] defer drop requires --reason "<non-empty string>"');
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const rawTok = positional[0];
|
|
1066
|
+
// Accept both bare integer (ordinal) and "D#N" string. Mirrors the MCP
|
|
1067
|
+
// mem_defer_drop input contract (server.mjs:1025) by using the same
|
|
1068
|
+
// single-element resolveDeferredIds call.
|
|
1069
|
+
const token = /^\d+$/.test(rawTok) ? parseInt(rawTok, 10) : rawTok;
|
|
1070
|
+
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
1071
|
+
|
|
1072
|
+
let realId;
|
|
1073
|
+
try {
|
|
1074
|
+
[realId] = resolveDeferredIds(db, project, [token]);
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
fail(`[mem] defer drop: ${e.message}`);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const r = dropDeferred(db, realId, reason);
|
|
1080
|
+
if (r.changed === 0) {
|
|
1081
|
+
out(`[mem] D#${realId} was not in 'open' status — drop is a no-op.`);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
out(`[mem] Dropped D#${realId} in project "${project}". Reason: ${reason.trim()}`);
|
|
934
1085
|
}
|
|
935
1086
|
|
|
936
1087
|
// N-1: Quality-focused stats for R-2 A/B baseline.
|
|
@@ -1615,8 +1766,11 @@ function cmdCompress(db, args) {
|
|
|
1615
1766
|
VALUES (?, ?, ?, ?, ?, 'active')
|
|
1616
1767
|
`).run(sessionId, sessionId, proj, now.toISOString(), now.getTime());
|
|
1617
1768
|
|
|
1769
|
+
// Defense-in-depth: source rows already scrubbed at original ingest, but
|
|
1770
|
+
// the new compressed narrative is constructed here and re-persisted.
|
|
1771
|
+
const safe = scrubRecord('observations', { text: narrative, title, narrative });
|
|
1618
1772
|
const summaryResult = insertSummary.run(
|
|
1619
|
-
sessionId, proj,
|
|
1773
|
+
sessionId, proj, safe.text, dominantType, safe.title, safe.narrative,
|
|
1620
1774
|
medianDate.toISOString(), medianEpoch
|
|
1621
1775
|
);
|
|
1622
1776
|
const summaryId = Number(summaryResult.lastInsertRowid);
|
|
@@ -2178,6 +2332,19 @@ Commands:
|
|
|
2178
2332
|
--project P Project name
|
|
2179
2333
|
--files f1,f2 Comma-separated file paths
|
|
2180
2334
|
--lesson T Lesson learned (≤500 chars; alias: --lesson-learned)
|
|
2335
|
+
--closes-deferred 1,D#42 Close deferred items in same transaction
|
|
2336
|
+
|
|
2337
|
+
defer <action> First-class deferred work (v2.70+)
|
|
2338
|
+
add "<title>" Mark deferred work for next session
|
|
2339
|
+
--priority N 1-3 (default 2)
|
|
2340
|
+
--detail T Constraint + why deferred
|
|
2341
|
+
--files f1,f2 Comma-separated file paths
|
|
2342
|
+
--project P Project name
|
|
2343
|
+
list List open deferred items
|
|
2344
|
+
--limit N Max results (default 10)
|
|
2345
|
+
--project P Filter by project
|
|
2346
|
+
drop <D#N|ordinal> Drop a deferred item (no fix needed)
|
|
2347
|
+
--reason "..." Required audit trail
|
|
2181
2348
|
|
|
2182
2349
|
delete <id1,id2,...> Delete observations by ID
|
|
2183
2350
|
--confirm Execute deletion (preview by default)
|
|
@@ -2251,6 +2418,9 @@ Commands:
|
|
|
2251
2418
|
remove Remove resource --name N --resource-type T
|
|
2252
2419
|
reindex Rebuild FTS5 index
|
|
2253
2420
|
|
|
2421
|
+
import-jsonl <file-or-dir> Import Claude Code JSONL transcripts (cold-start backfill)
|
|
2422
|
+
--project P Project name (default: inferred from cwd)
|
|
2423
|
+
|
|
2254
2424
|
activity <action> Non-memdir event log (v2.31) — bugfix/lesson/bug/discovery/etc.
|
|
2255
2425
|
save --type T "<title>" [--body "<text>"] [--files f1,f2] [--file path] [--importance 1-3] [--project P]
|
|
2256
2426
|
search "<query>" Search events [--type T] [--limit N] [--project P]
|
|
@@ -2336,6 +2506,57 @@ async function cmdImport(argv) {
|
|
|
2336
2506
|
}
|
|
2337
2507
|
}
|
|
2338
2508
|
|
|
2509
|
+
// ─── Import (Claude Code JSONL transcript — cold-start backfill) ─────────────
|
|
2510
|
+
|
|
2511
|
+
async function cmdImportJsonl(db, argv) {
|
|
2512
|
+
const { positional, flags } = parseArgs(argv);
|
|
2513
|
+
const target = positional[0];
|
|
2514
|
+
if (!target) {
|
|
2515
|
+
fail('[mem] Usage: claude-mem-lite import-jsonl <file-or-dir> [--project <name>]');
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
const project = flags.project || inferProject();
|
|
2520
|
+
const fs = await import('fs');
|
|
2521
|
+
const { join: pjoin, resolve } = await import('path');
|
|
2522
|
+
const abs = resolve(target);
|
|
2523
|
+
|
|
2524
|
+
let files = [];
|
|
2525
|
+
let st;
|
|
2526
|
+
try { st = fs.statSync(abs); }
|
|
2527
|
+
catch (e) { fail(`[mem] Cannot stat ${abs}: ${e.message}`); return; }
|
|
2528
|
+
|
|
2529
|
+
if (st.isDirectory()) {
|
|
2530
|
+
const walk = (dir) => {
|
|
2531
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2532
|
+
const p = pjoin(dir, e.name);
|
|
2533
|
+
if (e.isDirectory()) walk(p);
|
|
2534
|
+
else if (e.isFile() && p.endsWith('.jsonl')) files.push(p);
|
|
2535
|
+
}
|
|
2536
|
+
};
|
|
2537
|
+
walk(abs);
|
|
2538
|
+
} else {
|
|
2539
|
+
files = [abs];
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
if (files.length === 0) { out('[mem] No .jsonl files found.'); return; }
|
|
2543
|
+
|
|
2544
|
+
const { importJsonl } = await import('./lib/import-jsonl.mjs');
|
|
2545
|
+
let totalPrompts = 0, totalObs = 0, totalSkip = 0, totalOrphans = 0;
|
|
2546
|
+
for (const f of files) {
|
|
2547
|
+
const r = await importJsonl(db, f, { project });
|
|
2548
|
+
totalPrompts += r.prompts;
|
|
2549
|
+
totalObs += r.observations;
|
|
2550
|
+
totalSkip += r.skipped;
|
|
2551
|
+
totalOrphans += r.orphans || 0;
|
|
2552
|
+
out(`[mem] ${f}: +${r.prompts} prompts, +${r.observations} observations, ${r.orphans || 0} orphan tool_use, ${r.skipped} skipped`);
|
|
2553
|
+
}
|
|
2554
|
+
out(`[mem] Total: ${totalPrompts} prompts, ${totalObs} observations, ${totalOrphans} orphan tool_use, ${totalSkip} skipped from ${files.length} file(s).`);
|
|
2555
|
+
if (totalPrompts > 0 || totalObs > 0) {
|
|
2556
|
+
out(`[mem] Try: claude-mem-lite recent 5 --project ${project}`);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2339
2560
|
// ─── Enrich ─────────────────────────────────────────────────────────────────
|
|
2340
2561
|
|
|
2341
2562
|
async function cmdEnrich(argv) {
|
|
@@ -2506,6 +2727,7 @@ export async function run(argv) {
|
|
|
2506
2727
|
case 'get': cmdGet(db, cmdArgs); break;
|
|
2507
2728
|
case 'timeline': cmdTimeline(db, cmdArgs); break;
|
|
2508
2729
|
case 'save': cmdSave(db, cmdArgs); break;
|
|
2730
|
+
case 'defer': cmdDefer(db, cmdArgs); break;
|
|
2509
2731
|
case 'delete': cmdDelete(db, cmdArgs); break;
|
|
2510
2732
|
case 'update': cmdUpdate(db, cmdArgs); break;
|
|
2511
2733
|
case 'export': cmdExport(db, cmdArgs); break;
|
|
@@ -2518,6 +2740,7 @@ export async function run(argv) {
|
|
|
2518
2740
|
case 'browse': cmdBrowse(db, cmdArgs); break;
|
|
2519
2741
|
case 'registry': cmdRegistry(db, cmdArgs); break;
|
|
2520
2742
|
case 'import': await cmdImport(cmdArgs); break;
|
|
2743
|
+
case 'import-jsonl': await cmdImportJsonl(db, cmdArgs); break;
|
|
2521
2744
|
case 'enrich': await cmdEnrich(cmdArgs); break;
|
|
2522
2745
|
case 'doctor': await cmdDoctor(db, cmdArgs); break;
|
|
2523
2746
|
case 'activity': await cmdActivity(db, cmdArgs); break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.71.0",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"hook-handoff.mjs",
|
|
40
40
|
"hook-update.mjs",
|
|
41
41
|
"hook-optimize.mjs",
|
|
42
|
+
"hook-precompact.mjs",
|
|
42
43
|
"plugin-cache-guard.mjs",
|
|
43
44
|
"memdir.mjs",
|
|
44
45
|
"adopt-content.mjs",
|
|
@@ -62,6 +63,10 @@
|
|
|
62
63
|
"lib/metrics.mjs",
|
|
63
64
|
"lib/mem-override.mjs",
|
|
64
65
|
"lib/save-observation.mjs",
|
|
66
|
+
"lib/deferred-work.mjs",
|
|
67
|
+
"lib/upgrade-banner.mjs",
|
|
68
|
+
"lib/scrub-record.mjs",
|
|
69
|
+
"lib/import-jsonl.mjs",
|
|
65
70
|
"cli/common.mjs",
|
|
66
71
|
"cli/fts-check.mjs",
|
|
67
72
|
"cli/doctor.mjs",
|
package/schema.mjs
CHANGED
|
@@ -40,7 +40,12 @@ export const REGISTRY_DB_PATH = join(DB_DIR, 'resource-registry.db');
|
|
|
40
40
|
// DROP+CREATE so DBs that picked up the strict v29 trigger get the UUID-
|
|
41
41
|
// gated body. Required because `CREATE TRIGGER IF NOT EXISTS` is a no-op
|
|
42
42
|
// when the trigger already exists, even with a different body.
|
|
43
|
-
|
|
43
|
+
// v31 (v2.70.0): deferred_work table — first-class carry-forward surface.
|
|
44
|
+
// Decoupled from observations: different decay semantics (no time decay; older
|
|
45
|
+
// items rank HIGHER as tech debt accumulates), different lifecycle (mutable
|
|
46
|
+
// status open→done|dropped vs immutable obs). Closure tied to obs via
|
|
47
|
+
// closed_by_obs_id FK with ON DELETE SET NULL (audit trail preserved).
|
|
48
|
+
export const CURRENT_SCHEMA_VERSION = 31;
|
|
44
49
|
|
|
45
50
|
const CORE_SCHEMA = `
|
|
46
51
|
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
|
@@ -541,6 +546,33 @@ export function initSchema(db) {
|
|
|
541
546
|
)
|
|
542
547
|
`);
|
|
543
548
|
|
|
549
|
+
// ─── v31 (v2.70.0): deferred_work — carry-forward TODOs ─────────────────────
|
|
550
|
+
// Independent table because decay semantics are inverted (older = higher
|
|
551
|
+
// priority signal) and lifecycle is mutable (status flips). Project-scoped
|
|
552
|
+
// queries; no FTS5 (per-project N expected ≪ 100). Idempotent migration.
|
|
553
|
+
db.exec(`
|
|
554
|
+
CREATE TABLE IF NOT EXISTS deferred_work (
|
|
555
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
556
|
+
project TEXT NOT NULL,
|
|
557
|
+
title TEXT NOT NULL,
|
|
558
|
+
detail TEXT,
|
|
559
|
+
priority INTEGER NOT NULL DEFAULT 2,
|
|
560
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','done','dropped')),
|
|
561
|
+
created_at_epoch INTEGER NOT NULL,
|
|
562
|
+
closed_at_epoch INTEGER,
|
|
563
|
+
closed_by_obs_id INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
564
|
+
drop_reason TEXT,
|
|
565
|
+
source_session_id TEXT,
|
|
566
|
+
source_prompt_id INTEGER REFERENCES user_prompts(id) ON DELETE SET NULL,
|
|
567
|
+
files TEXT
|
|
568
|
+
);
|
|
569
|
+
CREATE INDEX IF NOT EXISTS idx_deferred_open
|
|
570
|
+
ON deferred_work(project, priority DESC, created_at_epoch ASC)
|
|
571
|
+
WHERE status = 'open';
|
|
572
|
+
CREATE INDEX IF NOT EXISTS idx_deferred_closed_by
|
|
573
|
+
ON deferred_work(closed_by_obs_id) WHERE closed_by_obs_id IS NOT NULL;
|
|
574
|
+
`);
|
|
575
|
+
|
|
544
576
|
// Record schema version for fast-path on subsequent calls
|
|
545
577
|
db.exec('CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)');
|
|
546
578
|
db.transaction(() => {
|