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/server.mjs CHANGED
@@ -11,9 +11,10 @@ import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
11
11
  import { ensureDb, DB_PATH, REGISTRY_DB_PATH } from './schema.mjs';
12
12
  import { reRankWithContext, markSuperseded, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './server-internals.mjs';
13
13
  import { searchObservationsHybrid, findFtsAnchor } from './search-engine.mjs';
14
+ import { scrubRecord } from './lib/scrub-record.mjs';
14
15
  import { effectiveQuiet } from './hook-shared.mjs';
15
16
  import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
16
- import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
17
+ import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, memDeferSchema, memDeferListSchema, memDeferDropSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
17
18
 
18
19
  // Lookup helper: all user-facing tool descriptions live in tool-schemas.mjs
19
20
  // (discouragement-style, Task 5). This keeps server.mjs from drifting.
@@ -30,6 +31,10 @@ import { ensureRegistryDb, upsertResource } from './registry.mjs';
30
31
  import { searchResources } from './registry-retriever.mjs';
31
32
  import { probeOtherSources as probeIdSources, parseIdToken, bucketIdTokens } from './lib/id-routing.mjs';
32
33
  import { saveObservation } from './lib/save-observation.mjs';
34
+ import {
35
+ insertDeferred, listOpenWithOrdinal, dropDeferred,
36
+ resolveDeferredIds, closeDeferredItems,
37
+ } from './lib/deferred-work.mjs';
33
38
  import { getVocabulary, rebuildVocabulary, _resetVocabCache, computeVector } from './tfidf.mjs';
34
39
  import { createRequire } from 'module';
35
40
 
@@ -907,22 +912,121 @@ server.registerTool(
907
912
  safeHandler(async (args) => {
908
913
  if (args.project) args = { ...args, project: resolveProject(args.project) };
909
914
  const project = args.project || inferProject();
910
- const result = saveObservation(db, {
911
- content: args.content,
912
- title: args.title,
913
- type: args.type || 'discovery',
914
- importance: args.importance,
915
- project,
916
- files: args.files || [],
917
- lesson_learned: args.lesson_learned,
918
- });
915
+
916
+ let closesIds = null;
917
+ let result;
918
+ try {
919
+ result = db.transaction(() => {
920
+ const r = saveObservation(db, {
921
+ content: args.content,
922
+ title: args.title,
923
+ type: args.type || 'discovery',
924
+ importance: args.importance,
925
+ project,
926
+ files: args.files || [],
927
+ lesson_learned: args.lesson_learned,
928
+ });
929
+ if (r.kind === 'duplicate') return r; // dedup short-circuits BEFORE resolver — replay is idempotent
930
+ // Resolve INSIDE tx + after dedup check so duplicate replays don't throw on
931
+ // already-closed items. Mirrors mem-cli.mjs cmdSave shape.
932
+ if (args.closes_deferred && args.closes_deferred.length > 0) {
933
+ closesIds = resolveDeferredIds(db, project, args.closes_deferred);
934
+ closeDeferredItems(db, closesIds, r.id);
935
+ }
936
+ return r;
937
+ })();
938
+ } catch (e) {
939
+ if (args.closes_deferred && args.closes_deferred.length > 0) {
940
+ // Re-throw with a clearer prefix so MCP error response names the
941
+ // contract failure — gate on caller intent (args.closes_deferred) since
942
+ // closesIds is closure-scoped and may not have been assigned before throw.
943
+ throw new Error(`mem_save with closes_deferred failed: ${e.message}`, { cause: e });
944
+ }
945
+ throw e; // unwrapped — preserves original message + stack
946
+ }
919
947
 
920
948
  if (result.kind === 'duplicate') {
921
949
  return { content: [{ type: 'text', text: `Skipped: similar to existing #${result.existingId} in project "${project}". Use mem_get(ids=[${result.existingId}]) to review.` }] };
922
950
  }
923
951
 
924
952
  const lessonNote = result.lessonCaptured ? ` 💡lesson captured` : '';
925
- return { content: [{ type: 'text', text: `Saved as observation #${result.id} [${result.type}] in project "${project}".${lessonNote}` }] };
953
+ const closedNote = closesIds && closesIds.length > 0
954
+ ? ` Closed deferred: ${closesIds.map(i => `D#${i}`).join(', ')}.`
955
+ : '';
956
+ return { content: [{ type: 'text', text: `Saved as observation #${result.id} [${result.type}] in project "${project}".${lessonNote}${closedNote}` }] };
957
+ })
958
+ );
959
+
960
+ // ─── Tool: mem_defer ────────────────────────────────────────────────────────
961
+
962
+ server.registerTool(
963
+ 'mem_defer',
964
+ {
965
+ description: descriptionOf('mem_defer'),
966
+ inputSchema: memDeferSchema,
967
+ },
968
+ safeHandler(async (args) => {
969
+ if (args.project) args = { ...args, project: resolveProject(args.project) };
970
+ const project = args.project || inferProject();
971
+ const r = insertDeferred(db, {
972
+ project,
973
+ title: args.title,
974
+ priority: args.priority ?? 2,
975
+ detail: args.detail ?? null,
976
+ files: args.files ?? null,
977
+ });
978
+ // Compute the ordinal for the freshly-inserted row so the response is
979
+ // immediately actionable ("ok, I deferred this as item 1").
980
+ const open = listOpenWithOrdinal(db, project, 50);
981
+ const ord = open.find(o => o.id === r.id)?.ordinal ?? null;
982
+ return { content: [{ type: 'text', text:
983
+ `Deferred as D#${r.id} (item ${ord ?? '?'}) in project "${project}" — surfaces in next SessionStart banner.` }] };
984
+ })
985
+ );
986
+
987
+ // ─── Tool: mem_defer_list ───────────────────────────────────────────────────
988
+
989
+ server.registerTool(
990
+ 'mem_defer_list',
991
+ {
992
+ description: descriptionOf('mem_defer_list'),
993
+ inputSchema: memDeferListSchema,
994
+ },
995
+ safeHandler(async (args) => {
996
+ if (args.project) args = { ...args, project: resolveProject(args.project) };
997
+ const project = args.project || inferProject();
998
+ const list = listOpenWithOrdinal(db, project, args.limit ?? 10);
999
+ if (list.length === 0) {
1000
+ return { content: [{ type: 'text', text: `No open deferred items in project "${project}".` }] };
1001
+ }
1002
+ const lines = [`Open deferred items (project "${project}"):`];
1003
+ for (const r of list) {
1004
+ const pTag = r.priority === 3 ? '🔴' : r.priority === 1 ? '⚪' : '🟡';
1005
+ lines.push(`${r.ordinal}. ${pTag} [P${r.priority}] ${r.title} (D#${r.id})`);
1006
+ }
1007
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1008
+ })
1009
+ );
1010
+
1011
+ // ─── Tool: mem_defer_drop ───────────────────────────────────────────────────
1012
+
1013
+ server.registerTool(
1014
+ 'mem_defer_drop',
1015
+ {
1016
+ description: descriptionOf('mem_defer_drop'),
1017
+ inputSchema: memDeferDropSchema,
1018
+ },
1019
+ safeHandler(async (args) => {
1020
+ if (args.project) args = { ...args, project: resolveProject(args.project) };
1021
+ const project = args.project || inferProject();
1022
+ // Resolve id (accept D#N or ordinal int) via resolveDeferredIds with a
1023
+ // single-element array — reuses the same project + status validation.
1024
+ const [realId] = resolveDeferredIds(db, project, [args.id]);
1025
+ const r = dropDeferred(db, realId, args.reason);
1026
+ if (r.changed === 0) {
1027
+ return { content: [{ type: 'text', text: `D#${realId} was not in 'open' status — drop is a no-op.` }] };
1028
+ }
1029
+ return { content: [{ type: 'text', text: `Dropped D#${realId} in project "${project}". Reason: ${args.reason}` }] };
926
1030
  })
927
1031
  );
928
1032
 
@@ -1145,8 +1249,11 @@ server.registerTool(
1145
1249
  VALUES (?, ?, ?, ?, ?, 'active')
1146
1250
  `).run(sessionId, sessionId, proj, now.toISOString(), now.getTime());
1147
1251
 
1252
+ // Defense-in-depth: source rows already scrubbed at original ingest,
1253
+ // but the new compressed narrative is constructed here and re-persisted.
1254
+ const safe = scrubRecord('observations', { text: narrative, title, narrative });
1148
1255
  const summaryResult = insertSummary.run(
1149
- sessionId, proj, narrative, dominantType, title, narrative,
1256
+ sessionId, proj, safe.text, dominantType, safe.title, safe.narrative,
1150
1257
  medianDate.toISOString(), medianEpoch
1151
1258
  );
1152
1259
  const summaryId = Number(summaryResult.lastInsertRowid);
@@ -2038,11 +2145,15 @@ server.registerTool(
2038
2145
  );
2039
2146
 
2040
2147
  // ─── Hidden tool filter ─────────────────────────────────────────────────────
2041
- // All 17 tools are registered (so `tools/call <name>` still resolves for
2042
- // scripts and direct MCP clients), but only the 6 core tools appear in the
2043
- // `tools/list` response. Hiding the 11 maintenance/admin tools keeps Claude
2044
- // Code's startup context small while preserving the contract that the plugin
2045
- // dogfoods (see CLAUDE.md §Mem usage contract and adopt-content.mjs).
2148
+ // All tools are registered (so `tools/call <name>` still resolves for scripts
2149
+ // and direct MCP clients), but only the core tools appear in the `tools/list`
2150
+ // response. Hiding the maintenance/admin tools keeps Claude Code's startup
2151
+ // context small while preserving the contract that the plugin dogfoods (see
2152
+ // CLAUDE.md §Mem usage contract and adopt-content.mjs).
2153
+ // Surface counts as of v2.70.0: 9 core (mem_search/recent/timeline/get/save/
2154
+ // recall + mem_defer/mem_defer_list/mem_defer_drop) + 11 hidden (maintenance/
2155
+ // admin/specialized) = 20 registered; tests/tool-schemas.test.mjs is the
2156
+ // authoritative count.
2046
2157
  //
2047
2158
  // Safe because:
2048
2159
  // - Protocol-layer override: we replace the mcp.js default ListTools
@@ -2055,9 +2166,9 @@ const HIDDEN_TOOL_NAMES = new Set(
2055
2166
  );
2056
2167
 
2057
2168
  // Opt-out: setting CLAUDE_MEM_ALL_TOOLS=1 restores pre-v2.34.0 behavior where
2058
- // all 17 tools are visible in `tools/list`. Users who relied on Claude Code
2059
- // autonomously invoking the now-hidden maintenance tools can use this as an
2060
- // immediate escape hatch while adopting the CLI entry points documented in
2169
+ // every registered tool is visible in `tools/list`. Users who relied on Claude
2170
+ // Code autonomously invoking the now-hidden maintenance tools can use this as
2171
+ // an immediate escape hatch while adopting the CLI entry points documented in
2061
2172
  // adopt-content.mjs / README.
2062
2173
  const EXPOSE_ALL_TOOLS = process.env.CLAUDE_MEM_ALL_TOOLS === '1';
2063
2174
 
@@ -2080,7 +2191,7 @@ if (!EXPOSE_ALL_TOOLS) {
2080
2191
  // harnesses stay silent.
2081
2192
  if (!effectiveQuiet()) {
2082
2193
  const status = EXPOSE_ALL_TOOLS
2083
- ? 'all 17 tools exposed via CLAUDE_MEM_ALL_TOOLS=1'
2194
+ ? `all ${TOOL_DEFS.length} tools exposed via CLAUDE_MEM_ALL_TOOLS=1`
2084
2195
  : `tools/list narrowed to ${TOOL_DEFS.length - HIDDEN_TOOL_NAMES.size} core tools (${HIDDEN_TOOL_NAMES.size} hidden but callable by exact name; unset CLAUDE_MEM_ALL_TOOLS to keep, set =1 to restore all)`;
2085
2196
  process.stderr.write(`[claude-mem-lite v${PKG_VERSION}] ${status}\n`);
2086
2197
  }
package/source-files.mjs CHANGED
@@ -9,7 +9,7 @@ export const SOURCE_FILES = [
9
9
  'cli.mjs', 'server.mjs', 'server-internals.mjs', 'search-engine.mjs', 'tool-schemas.mjs',
10
10
  'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs', 'skip-tools.mjs',
11
11
  'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs',
12
- 'hook-update.mjs', 'hook-optimize.mjs',
12
+ 'hook-update.mjs', 'hook-optimize.mjs', 'hook-precompact.mjs',
13
13
  'plugin-cache-guard.mjs',
14
14
  'haiku-client.mjs', 'utils.mjs', 'schema.mjs',
15
15
  'package.json', 'package-lock.json', 'skill.md',
@@ -63,6 +63,22 @@ export const SOURCE_FILES = [
63
63
  // mem-cli.mjs::cmdSave and server.mjs::mem_save. Statically imported from both
64
64
  // entry points; missing it from the manifest broke MCP saves on auto-update.
65
65
  'lib/save-observation.mjs',
66
+ // v2.70 deferred-work: carry-forward TODO primitives. Statically imported by
67
+ // server.mjs (mem_defer family) and mem-cli.mjs (defer subcommand).
68
+ 'lib/deferred-work.mjs',
69
+ // v2.70 one-shot upgrade banner. Split out of hook.mjs because hook.mjs has
70
+ // module-level `process.exit(0)` side effects that abort vitest workers on
71
+ // direct import. Statically imported by hook.mjs SessionStart handler.
72
+ 'lib/upgrade-banner.mjs',
73
+ // Per-table scrub helper for defense-in-depth at text-write INSERT paths.
74
+ // Statically imported by hook-llm, hook-handoff, hook-optimize, hook,
75
+ // mem-cli; reached transitively from server.mjs and cli.mjs.
76
+ 'lib/scrub-record.mjs',
77
+ // Cold-start backfill: parses ~/.claude/projects/<encoded>/<uuid>.jsonl
78
+ // transcripts into user_prompts + observations. Dynamic-imported by
79
+ // mem-cli.mjs::cmdImportJsonl; listed here so source-files-sync.test.mjs
80
+ // and the npm tarball ship it on every release.
81
+ 'lib/import-jsonl.mjs',
66
82
  ];
67
83
 
68
84
  /**
package/tool-schemas.mjs CHANGED
@@ -140,6 +140,31 @@ export const memDeleteSchema = {
140
140
  confirm: coerceBool.describe('false=preview what will be deleted, true=execute deletion'),
141
141
  };
142
142
 
143
+ // Coerce closes_deferred input — accepts mixed array of integers (ordinals) and
144
+ // "D#<n>" strings (raw ids). Numeric strings are coerced to numbers (so MCP
145
+ // bridges that JSON-stringify ints don't drop them); D#-prefixed strings stay
146
+ // strings for the resolver. Other string shapes reject early at schema layer.
147
+ const coerceDeferredTokens = z.preprocess(
148
+ (v) => {
149
+ if (Array.isArray(v)) {
150
+ return v.map(x => {
151
+ if (typeof x === 'number') return x;
152
+ if (typeof x === 'string') {
153
+ const s = x.trim();
154
+ if (/^-?\d+$/.test(s)) return parseInt(s, 10);
155
+ return s;
156
+ }
157
+ return x;
158
+ });
159
+ }
160
+ return v;
161
+ },
162
+ z.array(z.union([
163
+ z.number().int().positive(),
164
+ z.string().regex(/^D#\d+$/, 'expected D#N (raw id) or positive integer (ordinal)'),
165
+ ])).min(1).max(20)
166
+ );
167
+
143
168
  export const memSaveSchema = {
144
169
  content: z.string().min(1).max(50000).describe('Memory content to save'),
145
170
  title: z.string().optional().describe('Short title'),
@@ -148,6 +173,7 @@ export const memSaveSchema = {
148
173
  importance: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('Importance level: 1=routine, 2=notable, 3=critical (default: 2 for explicit saves)'),
149
174
  files: coerceStringArray.optional().describe('File paths associated with this observation'),
150
175
  lesson_learned: z.string().max(500).optional().describe('Key lesson or takeaway (for bugfix: root cause & fix; for decision: rationale)'),
176
+ closes_deferred: coerceDeferredTokens.optional().describe('Close one or more deferred_work items in the same project. Mixed array: bare integer = ordinal-within-project, "D#<n>" string = raw id. Transactional with the obs insert — a single invalid id rolls back the whole save.'),
151
177
  };
152
178
 
153
179
  export const memStatsSchema = {
@@ -252,6 +278,28 @@ export const memBrowseSchema = {
252
278
  limit: coerceInt.pipe(z.number().int().min(1).max(100)).optional().describe('Max entries per tier (default 5, or 20 when filtering by tier)'),
253
279
  };
254
280
 
281
+ export const memDeferSchema = {
282
+ title: z.string().min(1).max(200).describe('One-line subject of the deferred item'),
283
+ priority: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('1=low, 2=normal, 3=urgent (default: 2)'),
284
+ detail: z.string().max(2000).optional().describe('Optional longer description / constraint / why deferred'),
285
+ files: coerceStringArray.optional().describe('Optional file paths this deferred item concerns'),
286
+ project: z.string().optional().describe('Project name (default: inferred from CWD)'),
287
+ };
288
+
289
+ export const memDeferListSchema = {
290
+ project: z.string().optional().describe('Project name (default: inferred from CWD)'),
291
+ limit: coerceInt.pipe(z.number().int().min(1).max(50)).optional().describe('Max results (default 10)'),
292
+ };
293
+
294
+ export const memDeferDropSchema = {
295
+ id: z.union([
296
+ coerceInt.pipe(z.number().int().positive()),
297
+ z.string().regex(/^D#\d+$/, 'expected D#N or positive integer'),
298
+ ]).describe('Deferred item id — accepts D#N (raw id) or positive integer (ordinal-within-project)'),
299
+ reason: z.string().min(1).max(500).describe('Why this item is being dropped (required for audit trail)'),
300
+ project: z.string().optional().describe('Project name (default: inferred from CWD)'),
301
+ };
302
+
255
303
  // ────────────────────────────────────────────────────────────────────────────
256
304
  // Tool descriptions — discouragement style (Task 5, v2.31)
257
305
  //
@@ -265,14 +313,16 @@ export const memBrowseSchema = {
265
313
  // 40-60% vs. encouragement-style ("use this to..."). See tests/tool-schemas.test.mjs
266
314
  // for the invariants this list must satisfy.
267
315
  //
268
- // Core vs hidden (v2.34.0): only 6 tools are exposed via MCP `tools/list`. The
316
+ // Core vs hidden (v2.34.0, expanded v2.70.0): only 9 tools are exposed via MCP
317
+ // `tools/list` (the original 6 + mem_defer/mem_defer_list/mem_defer_drop). The
269
318
  // remaining 11 stay registered — and are still callable by name at the MCP
270
319
  // protocol level (`tools/call` by exact name) — but are omitted from the list
271
320
  // response so they don't bloat every agent's startup context. The core set
272
321
  // covers the hot paths the invited-memory contract promises (recall before
273
- // Edit, save after bugfix, search/recent/timeline/get for retrieval). Hidden
274
- // tools are either maintenance (compress/maintain/optimize/fts_check),
275
- // admin/infra (stats/export/update/delete), or specialized browsers
322
+ // Edit, save after bugfix, search/recent/timeline/get for retrieval, defer
323
+ // for cross-session carry-forward). Hidden tools are either maintenance
324
+ // (compress/maintain/optimize/fts_check), admin/infra
325
+ // (stats/export/update/delete), or specialized browsers
276
326
  // (browse/registry/use) — all of which have CLI equivalents documented in
277
327
  // `adopt-content.mjs`.
278
328
  // ────────────────────────────────────────────────────────────────────────────
@@ -599,4 +649,57 @@ export const tools = [
599
649
  inputSchema: memBrowseSchema,
600
650
  hidden: true,
601
651
  },
652
+ {
653
+ name: 'mem_defer',
654
+ description:
655
+ 'Save a future-session TODO to the project carry-forward list. Surfaces in the next SessionStart `### Deferred Work` banner with an ordinal (e.g. "1") so user can say "处理1" / "handle item 1".\n' +
656
+ '\n' +
657
+ 'DO NOT use when:\n' +
658
+ ' - Work is in-flight this session (just do it; do not defer mid-task)\n' +
659
+ ' - This-PR follow-up cleanup (file in tasks/, not deferred_work)\n' +
660
+ ' - Already saved a bugfix obs for it (use mem_save closes_deferred instead)\n' +
661
+ '\n' +
662
+ 'USE when:\n' +
663
+ ' - User says "下次/next session/留给下个会话/defer to next round"\n' +
664
+ ' - Wrap-up phase enumerates follow-up items for the next session\n' +
665
+ ' - Bug surfaces but root cause is out of this session\'s scope\n' +
666
+ '\n' +
667
+ 'Equivalent CLI: claude-mem-lite defer add "<title>" [--priority 1|2|3] [--detail "..."] [--files a.mjs,b.mjs]',
668
+ inputSchema: memDeferSchema,
669
+ },
670
+ {
671
+ name: 'mem_defer_list',
672
+ description:
673
+ 'List open deferred_work items for a project, ordered by priority DESC then age. Each item carries a per-project ordinal (1, 2, 3...) for "处理1"-style reference.\n' +
674
+ '\n' +
675
+ 'DO NOT use when:\n' +
676
+ ' - The SessionStart `### Deferred Work` block already shows the items (reuse that — same data)\n' +
677
+ ' - You only need one item by id (use mem_get on the obs that closed it, or look up the row directly)\n' +
678
+ '\n' +
679
+ 'USE when:\n' +
680
+ ' - User asks "what was on the deferred list" / "what did I leave for next time"\n' +
681
+ ' - About to refer to "item N" and need to confirm what N points to\n' +
682
+ ' - Auditing carry-forward state across multiple sessions\n' +
683
+ '\n' +
684
+ 'Equivalent CLI: claude-mem-lite defer list [--project X] [--limit 10]',
685
+ inputSchema: memDeferListSchema,
686
+ },
687
+ {
688
+ name: 'mem_defer_drop',
689
+ description:
690
+ 'Mark a deferred_work item as dropped (no fix, no longer relevant) with a required reason. For real fixes, prefer mem_save({type:"bugfix", closes_deferred:[N]}) — establishes audit trail.\n' +
691
+ '\n' +
692
+ 'DO NOT use when:\n' +
693
+ ' - The item was actually fixed (use mem_save closes_deferred — audit linkage)\n' +
694
+ ' - You want to delete the row (drops are soft — status flips, row stays)\n' +
695
+ ' - The reason is trivial — drop reason is the only audit trail for "why no fix"\n' +
696
+ '\n' +
697
+ 'USE when:\n' +
698
+ ' - Originally-deferred item turns out to be a flaky test, not a real bug\n' +
699
+ ' - Scope changed and the work is no longer needed\n' +
700
+ ' - User explicitly says "drop the deferred X, never mind"\n' +
701
+ '\n' +
702
+ 'Equivalent CLI: claude-mem-lite defer drop <D#N|ordinal> --reason "..."',
703
+ inputSchema: memDeferDropSchema,
704
+ },
602
705
  ];