claude-mem-lite 3.92.0 → 3.93.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.
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { ensureDb, DB_DIR, REGISTRY_DB_PATH } from '../schema.mjs';
7
7
  import { relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25, notLowSignalTitleClause, stripPrivate, neutralizeContextDelimiters, MAX_UPS_PROMPT_BYTES } from '../utils.mjs';
8
+ import { readHookStdin } from '../lib/hook-stdin.mjs';
9
+ import { resolveRuntimeDir } from '../lib/resolve-data-dir.mjs';
8
10
  import { liveObsFilterSql, injectionRelevanceSql } from '../lib/inject-search-core.mjs';
9
11
  import { fileMatchClause, fileMatchParams, basenameAnySep } from '../lib/file-edge-match.mjs';
10
12
  import { cjkPrecisionOk } from '../nlp.mjs';
@@ -25,13 +27,20 @@ import { DAY_MS } from '../lib/time-constants.mjs';
25
27
 
26
28
  // Telemetry sink (lib/hook-telemetry.mjs contract): env override for tests, else
27
29
  // <data-dir>/runtime — the same dir the sibling hook scripts + `stats` read.
28
- const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(DB_DIR, 'runtime');
30
+ // EVERY runtime path in this file goes through this constant, including the two
31
+ // below. They used to be `join(DB_DIR, 'runtime', …)`, which reproduced the exact
32
+ // defect P1-14 closed one seam over: this file's RUNTIME_DIR honoured the override
33
+ // while its MARKER did not, so under `CLAUDE_MEM_RUNTIME_DIR` the `fyi` face wrote
34
+ // the shared cross-hook marker to <data>/runtime while `pre-tool-recall` (`pretool`)
35
+ // wrote it to the override and `hook.mjs` (`ups`) read the override. Caught by the
36
+ // v3.93.0 pre-tag test-effectiveness review, from a surviving mutation.
37
+ const RUNTIME_DIR = resolveRuntimeDir(DB_DIR);
29
38
  // D#120: one marker file per CC session — payload-only session keying (M-6) let
30
39
  // two concurrent windows full-replace each other's marker, killing dedup between
31
40
  // them and resetting `count` on every alternation. Derived per invocation once
32
41
  // the session id is parsed from stdin; no session id → legacy project-keyed file.
33
42
  const injectedIdsFileFor = (sessionId) =>
34
- join(DB_DIR, 'runtime', injectedIdsFileName(inferProject(), sessionId));
43
+ join(RUNTIME_DIR, injectedIdsFileName(inferProject(), sessionId));
35
44
  // Per-prompt UPS cap. Cut from 5 → 3 after the 2026-05-09 per-hook recall
36
45
  // scan (#8255): UPS contributed 74% of silent injected IDs (131/177) at 26%
37
46
  // recall, vs PreToolUse:Read at 94% recall on a tighter file-keyed set.
@@ -482,28 +491,19 @@ function searchRecent(db, project, limit) {
482
491
 
483
492
  // ─── stdin Reader ───────────────────────────────────────────────────────────
484
493
 
485
- function readStdin() {
486
- return new Promise((resolve, reject) => {
487
- let data = '';
488
- const timeout = setTimeout(() => {
489
- process.stdin.destroy();
490
- reject(new Error('timeout'));
491
- }, 2000);
492
- process.stdin.setEncoding('utf8');
493
- process.stdin.on('data', chunk => {
494
- data += chunk;
495
- // Cap the prompt (#9494 huge-prompt guard) — deliberately tighter than the
496
- // 256KB full-payload tier; both tiers live in utils.mjs (G19).
497
- if (data.length > MAX_UPS_PROMPT_BYTES) {
498
- process.stdin.destroy();
499
- clearTimeout(timeout);
500
- resolve(data.slice(0, MAX_UPS_PROMPT_BYTES));
501
- }
502
- });
503
- process.stdin.on('end', () => { clearTimeout(timeout); resolve(data); });
504
- process.stdin.on('error', err => { clearTimeout(timeout); reject(err); });
505
- process.stdin.resume();
494
+ // P1-9: shared mechanism (lib/hook-stdin.mjs), this entry point's own caliber. 2 s and
495
+ // MAX_UPS_PROMPT_BYTES (64 KB, #9494's huge-prompt guard) are deliberately tighter than the
496
+ // 256 KB full-payload tier hook.mjs uses — both tiers live in utils.mjs (G19) — because the
497
+ // payload here is a user PROMPT, not a tool response. `rejectOnTimeout` matches the previous
498
+ // behaviour: the caller treats a timeout as "skip the injection".
499
+ // Returns a bare string, as this script's callers expect.
500
+ async function readStdin() {
501
+ const { text } = await readHookStdin({
502
+ timeoutMs: 2000,
503
+ maxBytes: MAX_UPS_PROMPT_BYTES,
504
+ rejectOnTimeout: true,
506
505
  });
506
+ return text;
507
507
  }
508
508
 
509
509
  // ─── Format Output ──────────────────────────────────────────────────────────
@@ -550,7 +550,7 @@ function formatPromptResults(rows) {
550
550
  // pointer line so Claude can decide to invoke via SkillTool. The cooldown
551
551
  // and match mechanics below are unchanged.
552
552
 
553
- const SKILL_COOLDOWN_FILE = join(DB_DIR, 'runtime', `.skill-cooldown-${inferProject()}`);
553
+ const SKILL_COOLDOWN_FILE = join(RUNTIME_DIR, `.skill-cooldown-${inferProject()}`);
554
554
  const SKILL_COOLDOWN_MS = 300_000; // 5 minutes
555
555
 
556
556
  function loadManagedSkillNames() {
package/server.mjs CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  // snapshotDb left with maintain-core: the pre-maintain snapshot is part of the op ORDER
22
22
  // (it must see the pre-existing pending rows), so it moved into runMaintainOps (P1-5).
23
23
  import { deleteObservations, previewDeleteRows } from './lib/delete-core.mjs';
24
- import { fetchObsDetail, fetchPromptDetail, fetchEventDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS, PROMPT_DETAIL_FIELDS, EVENT_DETAIL_FIELDS, supersededNotice } from './lib/get-core.mjs';
24
+ import { fetchObsDetail, fetchPromptDetail, fetchEventDetail, fetchSessionDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS, PROMPT_DETAIL_FIELDS, EVENT_DETAIL_FIELDS, supersededNotice } from './lib/get-core.mjs';
25
25
  import { collectBrowseTiers, getActiveMemorySessionId, BROWSE_TIERS, BROWSE_TIER_LABELS } from './lib/browse-core.mjs';
26
26
  import { effectiveQuiet, RUNTIME_DIR } from './hook-shared.mjs';
27
27
  import { computeStatsFeed } from './lib/stats-core.mjs';
@@ -45,13 +45,12 @@ import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
45
45
  import { join, sep } from 'path';
46
46
  import { homedir } from 'os';
47
47
  import { ensureRegistryDb, collectRegistryStats, listResourcesRanked, formatRegistryListLine } from './registry.mjs';
48
- import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources, enrichImportedResources, enrichNamedResource } from './lib/registry-core.mjs';
48
+ import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources, enrichImportedResources, enrichNamedResource, resourceUseHint } from './lib/registry-core.mjs';
49
49
  import { searchResources } from './registry-retriever.mjs';
50
50
  import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
51
51
  import { saveWithClosures, formatSupersedeSkipped, formatSupersededNote } from './lib/save-observation.mjs';
52
52
  import { applyObsUpdate } from './lib/observation-write.mjs';
53
- import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
54
- import { liveObsFilterSql } from './lib/inject-search-core.mjs';
53
+ import { EXPORT_COLUMNS_SQL, buildExportWhere } from './lib/export-columns.mjs';
55
54
  import { recallByFile } from './lib/recall-core.mjs';
56
55
  import { fetchRecent } from './lib/recent-core.mjs';
57
56
  import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
@@ -646,8 +645,7 @@ server.registerTool(
646
645
  }
647
646
 
648
647
  if (bySrc.session.length > 0) {
649
- const ph = bySrc.session.map(() => '?').join(',');
650
- const rows = db.prepare(`SELECT * FROM session_summaries WHERE id IN (${ph}) ORDER BY created_at_epoch ASC`).all(...bySrc.session);
648
+ const rows = fetchSessionDetail(db, bySrc.session);
651
649
  // SESSION_DETAIL_FIELDS (get-core, P2-12): shared full set — adds remaining_items
652
650
  // (searchable via FTS but previously unrendered on BOTH detail faces).
653
651
  const sessFields = SESSION_DETAIL_FIELDS;
@@ -1307,27 +1305,12 @@ server.registerTool(
1307
1305
  return { content: [{ type: 'text', text: `No matching resources for: "${args.query}"` }] };
1308
1306
  }
1309
1307
  const home = homedir();
1310
- const toPortable = (p) => p && p.startsWith(home) ? '~' + p.slice(home.length) : (p || '');
1311
1308
  const lines = results.map(r => {
1312
1309
  const qualityBadge = r.quality_tier === 'installed' ? '[✓]' : r.quality_tier === 'verified' ? '[★]' : '[○]';
1313
1310
  const categoryLabel = r.category ? ` [${r.category}]` : '';
1314
- const isManaged = r.local_path && r.local_path.includes(join(DB_DIR, 'managed') + sep);
1315
- const portablePath = isManaged ? toPortable(r.local_path) : '';
1316
- let howToUse;
1317
- if (isManaged) {
1318
- // Managed: use Read(path) or mem_use — Skill() won't work for managed resources
1319
- // Agents always have complete .md paths (e.g., agents/group/agents/name.md)
1320
- // Only skills can be directory paths (9 cases) — resolve to /SKILL.md
1321
- const resolvedPath = portablePath.endsWith('.md') ? portablePath : `${portablePath}/SKILL.md`;
1322
- howToUse = `Read("${resolvedPath}") or mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
1323
- } else if (r.invocation_name) {
1324
- // Native plugin/user skill: Skill() with full invocation name
1325
- howToUse = r.type === 'skill'
1326
- ? `Skill("${r.invocation_name}")`
1327
- : `Agent(subagent_type="${r.invocation_name}")`;
1328
- } else {
1329
- howToUse = `mem_use(name="${r.name}"${r.type === 'agent' ? ', type="agent"' : ''})`;
1330
- }
1311
+ // P2-6: the invocation rule is shared; only the Markdown bold and the two-space
1312
+ // indent below are this face's.
1313
+ const { portablePath, howToUse } = resourceUseHint(r, { home, managedPrefix: join(DB_DIR, 'managed') + sep });
1331
1314
  const pathLine = portablePath ? `\n Path: ${portablePath}` : '';
1332
1315
  return `${qualityBadge} ${r.type === 'skill' ? 'S' : 'A'} **${r.name}**${categoryLabel} — ${truncate(r.capability_summary || '', 80)}${pathLine}\n Use: ${howToUse}`;
1333
1316
  });
@@ -1594,33 +1577,32 @@ export async function handleExportForTest(db, args) {
1594
1577
  }
1595
1578
 
1596
1579
  async function runExport(db, args) {
1597
- const wheres = [];
1598
- const params = [];
1599
- // Composed, not hand-written: the CLI twin (mem-cli.mjs cmdExport) already
1600
- // routes through liveObsFilterSql, and a hand-rolled copy here is how the
1601
- // live-row predicate drifted apart on other surfaces (P2-11/D#123). With
1602
- // include_compressed the compressed half is dropped but retractions still
1603
- // are not — tombstone export is opt-in, supersession is never exported.
1604
- wheres.push(args.include_compressed ? 'superseded_at IS NULL' : liveObsFilterSql(''));
1605
- if (args.project) { wheres.push('project = ?'); params.push(_resolveProjectShared(db, args.project)); }
1606
- if (args.type) { wheres.push('type = ?'); params.push(args.type); }
1607
- // T3-P1-A: surface invalid dates instead of silently dropping the filter — mirrors
1608
- // mem_search, which threw. A dropped filter can quietly expand the export blast radius.
1580
+ // PARSE + VALIDATE here; the SQL predicate comes from buildExportWhere (P2-5), shared
1581
+ // with mem-cli.mjs cmdExport. This face THROWS where the CLI prints a usage message —
1582
+ // T3-P1-A: surface invalid dates instead of silently dropping the filter (mirrors
1583
+ // mem_search). A dropped filter can quietly expand the export blast radius.
1584
+ let fromEpoch = null;
1585
+ let toEpoch = null;
1609
1586
  if (args.date_from) {
1610
- const epoch = new Date(args.date_from).getTime();
1611
- if (isNaN(epoch)) throw new Error(`Invalid date_from: "${args.date_from}" (use ISO 8601 or YYYY-MM-DD)`);
1612
- wheres.push('created_at_epoch >= ?');
1613
- params.push(epoch);
1587
+ fromEpoch = new Date(args.date_from).getTime();
1588
+ if (isNaN(fromEpoch)) throw new Error(`Invalid date_from: "${args.date_from}" (use ISO 8601 or YYYY-MM-DD)`);
1614
1589
  }
1615
1590
  if (args.date_to) {
1616
1591
  const d = args.date_to.length === 10 ? args.date_to + 'T23:59:59.999Z' : args.date_to;
1617
- const epoch = new Date(d).getTime();
1618
- if (isNaN(epoch)) throw new Error(`Invalid date_to: "${args.date_to}" (use ISO 8601 or YYYY-MM-DD)`);
1619
- wheres.push('created_at_epoch <= ?');
1620
- params.push(epoch);
1592
+ toEpoch = new Date(d).getTime();
1593
+ if (isNaN(toEpoch)) throw new Error(`Invalid date_to: "${args.date_to}" (use ISO 8601 or YYYY-MM-DD)`);
1621
1594
  }
1622
-
1623
- const where = wheres.length > 0 ? 'WHERE ' + wheres.join(' AND ') : '';
1595
+ const { params, where } = buildExportWhere({
1596
+ includeCompressed: Boolean(args.include_compressed),
1597
+ // `|| args.project`: buildExportWhere gates the predicate on TRUTHINESS, so a falsy
1598
+ // resolution would DROP the project filter and export the whole store. The code this
1599
+ // replaced pushed `project = ?` unconditionally, so an unresolvable name yielded zero
1600
+ // rows — which is the safe direction, and the comment above is specifically about not
1601
+ // letting a dropped filter widen the export.
1602
+ project: args.project ? (_resolveProjectShared(db, args.project) || args.project) : null,
1603
+ type: args.type || null,
1604
+ fromEpoch, toEpoch,
1605
+ });
1624
1606
  // No clamp (audit 2026-08-14 A2): `Math.min(args.limit ?? 200, 1000)` made an MCP-driven
1625
1607
  // backup of a >1000-row store impossible, on the tool whose own description says "USE
1626
1608
  // when: Backing up memory before a migration or reinstall" — while the CLI twin exported
package/source-files.mjs CHANGED
@@ -110,6 +110,8 @@ export const SOURCE_FILES = [
110
110
  // Single-envelope stdout for hook processes — imported by hook.mjs. Claude Code
111
111
  // parses hook stdout as ONE JSON document; missing from the manifest → an updated
112
112
  // install throws ERR_MODULE_NOT_FOUND on every hook fire.
113
+ 'lib/hook-stdin.mjs',
114
+ 'lib/plugin-key.mjs',
113
115
  'lib/hook-stdout.mjs',
114
116
  // audit P0/P1: inter-process install lock + atomic config writes — imported by
115
117
  // install.mjs (settings.json + install lock) and hook-update.mjs (.claude.json