claude-mem-lite 3.74.0 → 3.75.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 +122 -4
- package/README.zh-CN.md +18 -10
- package/bash-utils.mjs +49 -18
- package/hook-context.mjs +4 -7
- package/hook-llm.mjs +7 -2
- package/hook-memory.mjs +4 -6
- package/hook.mjs +72 -175
- package/hooks/hooks.json +1 -1
- package/install.mjs +35 -2
- package/lib/citation-tracker.mjs +15 -46
- package/lib/cite-back-hint.mjs +4 -18
- package/lib/doctor-drift.mjs +8 -1
- package/lib/fast-summary.mjs +87 -0
- package/lib/get-core.mjs +35 -0
- package/lib/maintain-core.mjs +95 -1
- package/lib/registry-core.mjs +94 -0
- package/lib/resolve-data-dir.mjs +53 -4
- package/lib/summary-extractor.mjs +2 -8
- package/lib/transcript-scan.mjs +66 -0
- package/mem-cli.mjs +43 -39
- package/nlp.mjs +20 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +5 -1
- package/registry-recommend.mjs +30 -4
- package/scoring-sql.mjs +21 -7
- package/scripts/pre-agent-inject.sh +43 -0
- package/scripts/user-prompt-search.js +17 -3
- package/server.mjs +30 -38
- package/source-files.mjs +12 -0
package/nlp.mjs
CHANGED
|
@@ -245,12 +245,26 @@ export const FTS_STOP_WORDS = new Set([...BASE_STOP_WORDS, ...CONTRACTION_FRAGME
|
|
|
245
245
|
/**
|
|
246
246
|
* Sanitize and expand a user query into a valid FTS5 query string.
|
|
247
247
|
* Strips special characters, expands synonyms, and joins with AND/space.
|
|
248
|
+
*
|
|
249
|
+
* Unbounded by default — an explicit `search` from a human should search what they
|
|
250
|
+
* typed. The two caps exist for the AUTOMATIC surface (audit 2026-08-22 P2-13):
|
|
251
|
+
* UserPromptSubmit runs this on every prompt the user sends, with only a 64KB byte
|
|
252
|
+
* guard upstream, and cost grows with prompt length: 0.8ms on a normal prompt, 6.2ms
|
|
253
|
+
* on a 64KB ASCII one, 31.8ms on a 64KB CJK one (extractCjkKeywords is O(len x dict)
|
|
254
|
+
* over an unsegmented run). That is paid before the model sees the turn.
|
|
255
|
+
*
|
|
248
256
|
* @param {string} query Raw user search query
|
|
257
|
+
* @param {object} [opts]
|
|
258
|
+
* @param {number} [opts.maxChars] Truncate the raw query first (bounds CJK segmentation,
|
|
259
|
+
* which sees one huge token where ASCII sees many small ones).
|
|
260
|
+
* @param {number} [opts.maxTokens] Keep at most this many meaningful terms, applied
|
|
261
|
+
* after stopword filtering and before expansion.
|
|
249
262
|
* @returns {string|null} FTS5-safe query or null if empty
|
|
250
263
|
*/
|
|
251
|
-
export function sanitizeFtsQuery(query) {
|
|
264
|
+
export function sanitizeFtsQuery(query, opts = {}) {
|
|
252
265
|
if (!query) return null;
|
|
253
|
-
const
|
|
266
|
+
const { maxChars = 0, maxTokens = 0 } = opts;
|
|
267
|
+
const cleaned = (maxChars > 0 ? String(query).slice(0, maxChars) : query)
|
|
254
268
|
// Strip ASCII control chars / NUL FIRST. A NUL survives tokenization (it's not
|
|
255
269
|
// \s), gets phrase-quoted by expandToken, and then terminates SQLite's C string
|
|
256
270
|
// mid-phrase → FTS5 "unterminated string" throw, breaking the documented
|
|
@@ -293,6 +307,10 @@ export function sanitizeFtsQuery(query) {
|
|
|
293
307
|
// Filter stop words (but keep all if filtering would empty the query)
|
|
294
308
|
const filtered = tokens.filter(t => !FTS_STOP_WORDS.has(t.toLowerCase()));
|
|
295
309
|
if (filtered.length > 0) tokens = filtered;
|
|
310
|
+
// Cap AFTER stopword filtering and BEFORE expansion: the terms kept are meaningful
|
|
311
|
+
// ones, and everything downstream (CJK segmentation, synonym expansion, bigrams) is
|
|
312
|
+
// per-token work that the cap therefore bounds too.
|
|
313
|
+
if (maxTokens > 0 && tokens.length > maxTokens) tokens = tokens.slice(0, maxTokens);
|
|
296
314
|
// Split unsegmented CJK tokens into known vocabulary words using CJK_COMPOUNDS dictionary.
|
|
297
315
|
// Uses broader dictionary than synonym-only extraction for better recall.
|
|
298
316
|
// e.g. "这个函数是做什么的" → ["函数"] (not noisy bigrams)
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.75.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.75.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
12
12
|
"better-sqlite3": "^12.6.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.75.0",
|
|
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",
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"lib/save-observation.mjs",
|
|
97
97
|
"lib/observation-write.mjs",
|
|
98
98
|
"lib/recall-core.mjs",
|
|
99
|
+
"lib/registry-core.mjs",
|
|
99
100
|
"lib/timeline-core.mjs",
|
|
100
101
|
"lib/search-core.mjs",
|
|
101
102
|
"lib/recent-core.mjs",
|
|
@@ -110,6 +111,8 @@
|
|
|
110
111
|
"lib/save-enrich.mjs",
|
|
111
112
|
"lib/persist-reminder.mjs",
|
|
112
113
|
"lib/maintain-core.mjs",
|
|
114
|
+
"lib/fast-summary.mjs",
|
|
115
|
+
"lib/transcript-scan.mjs",
|
|
113
116
|
"lib/dedup-constants.mjs",
|
|
114
117
|
"lib/deferred-work.mjs",
|
|
115
118
|
"lib/upgrade-banner.mjs",
|
|
@@ -166,6 +169,7 @@
|
|
|
166
169
|
"scripts/pre-tool-recall.js",
|
|
167
170
|
"scripts/post-tool-recall.js",
|
|
168
171
|
"scripts/pre-skill-bridge.js",
|
|
172
|
+
"scripts/pre-agent-inject.sh",
|
|
169
173
|
"scripts/pre-agent-inject.js",
|
|
170
174
|
"scripts/prompt-search-utils.mjs",
|
|
171
175
|
"scripts/hook-launcher.mjs",
|
package/registry-recommend.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Intent-based skill recommendation — Phase 1 (shadow).
|
|
2
2
|
// See docs/superpowers/specs/2026-06-23-skill-recommendation-loop-design.md
|
|
3
3
|
//
|
|
4
|
-
// Phase-1 invariant:
|
|
5
|
-
// invocations/recommend_count. Live injection is Phase 2
|
|
4
|
+
// Phase-1 invariant: the engine only LOGS. It never emits to stdout and never writes
|
|
5
|
+
// invocations/recommend_count. Live injection is Phase 2 and does not exist yet, so
|
|
6
|
+
// `live` resolves to shadow with a one-time warning (see UNIMPLEMENTED_MODES).
|
|
7
|
+
// `off` skips all work.
|
|
6
8
|
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, appendFileSync, readdirSync, unlinkSync } from 'fs';
|
|
7
9
|
import { join } from 'path';
|
|
8
10
|
import { resolveDataDir } from './lib/resolve-data-dir.mjs';
|
|
@@ -10,13 +12,37 @@ import { searchResources, cjkIntentTokens } from './registry-retriever.mjs';
|
|
|
10
12
|
|
|
11
13
|
import { DAY_MS } from './lib/time-constants.mjs';
|
|
12
14
|
const VALID_MODES = new Set(['shadow', 'live', 'off']);
|
|
15
|
+
// Phase 2 (live injection) was never built. `live` stayed in VALID_MODES and
|
|
16
|
+
// getRecommendMode returned it verbatim, so setting it bought a user exactly
|
|
17
|
+
// nothing and said nothing — the run behaved as shadow while the mode field in
|
|
18
|
+
// every logged row claimed 'live'. Audit 2026-08-22 P2-5: an accepted value that
|
|
19
|
+
// silently means something else is worse than an unsupported one. It still parses
|
|
20
|
+
// (nobody's config breaks), it now resolves to what actually happens, and it says
|
|
21
|
+
// so once per process.
|
|
22
|
+
export const RECOMMEND_MODE_UNIMPLEMENTED = new Set(['live']);
|
|
23
|
+
let warnedUnimplemented = false;
|
|
13
24
|
|
|
14
|
-
/**
|
|
15
|
-
export function
|
|
25
|
+
/** The mode the environment ASKED for — 'live' included. For diagnostics (doctor). */
|
|
26
|
+
export function getRequestedRecommendMode() {
|
|
16
27
|
const raw = (process.env.CLAUDE_MEM_RECOMMEND_MODE || 'shadow').toLowerCase().trim();
|
|
17
28
|
return VALID_MODES.has(raw) ? raw : 'shadow';
|
|
18
29
|
}
|
|
19
30
|
|
|
31
|
+
/** Recommendation mode actually in effect; default 'shadow', unknown → 'shadow'. */
|
|
32
|
+
export function getRecommendMode() {
|
|
33
|
+
const requested = getRequestedRecommendMode();
|
|
34
|
+
if (!RECOMMEND_MODE_UNIMPLEMENTED.has(requested)) return requested;
|
|
35
|
+
if (!warnedUnimplemented && !process.env.MEM_QUIET_HOOKS) {
|
|
36
|
+
warnedUnimplemented = true;
|
|
37
|
+
// stderr, once: hook stdout is a parsed envelope and must not carry this.
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
`[claude-mem-lite] CLAUDE_MEM_RECOMMEND_MODE=${requested} is not implemented `
|
|
40
|
+
+ '(live injection is Phase 2); running in shadow mode — nothing is injected. '
|
|
41
|
+
+ 'Set shadow or off to silence this.\n');
|
|
42
|
+
}
|
|
43
|
+
return 'shadow';
|
|
44
|
+
}
|
|
45
|
+
|
|
20
46
|
// Provisional gate thresholds — calibrated from shadow data before Phase 2 (spec §8).
|
|
21
47
|
// relevance is raw bm25 (negative; more negative = better). Candidate must clear |floor|.
|
|
22
48
|
export const RECO_BM25_FLOOR = -1.5; // floor stays on raw bm25 (absolute topical relevance)
|
package/scoring-sql.mjs
CHANGED
|
@@ -80,15 +80,29 @@ export const TYPE_DECAY_CASE = `(
|
|
|
80
80
|
* The old (pre-R2) table had bugfix=0.75 < change=0.8, inverted vs reality.
|
|
81
81
|
* Applied as: BM25 × time_decay × TYPE_QUALITY × project_boost × importance
|
|
82
82
|
*/
|
|
83
|
+
export const TYPE_QUALITY = {
|
|
84
|
+
decision: 1.5,
|
|
85
|
+
discovery: 1.3,
|
|
86
|
+
bugfix: 1.1,
|
|
87
|
+
feature: 1.0,
|
|
88
|
+
refactor: 0.6,
|
|
89
|
+
change: 0.5,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Multiplier for a type not in the table (legacy rows, manual saves). */
|
|
93
|
+
export const TYPE_QUALITY_DEFAULT = 1.0;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The SQL form, generated from TYPE_QUALITY rather than written beside it.
|
|
97
|
+
* The table used to exist three times — here, hook-context.mjs, hook-memory.mjs — with
|
|
98
|
+
* "aligned with scoring-sql.mjs (R2)" comments as the only thing keeping them equal
|
|
99
|
+
* (audit 2026-08-22, P2-10). Values happened to agree; the next re-weighting is what the
|
|
100
|
+
* copies were waiting for. Both JS consumers now import TYPE_QUALITY from here.
|
|
101
|
+
*/
|
|
83
102
|
export const TYPE_QUALITY_CASE = `(
|
|
84
103
|
CASE o.type
|
|
85
|
-
WHEN '
|
|
86
|
-
|
|
87
|
-
WHEN 'bugfix' THEN 1.1
|
|
88
|
-
WHEN 'feature' THEN 1.0
|
|
89
|
-
WHEN 'refactor' THEN 0.6
|
|
90
|
-
WHEN 'change' THEN 0.5
|
|
91
|
-
ELSE 1.0
|
|
104
|
+
${Object.entries(TYPE_QUALITY).map(([t, w]) => ` WHEN '${t}' THEN ${w.toFixed(1)}`).join('\n')}
|
|
105
|
+
ELSE ${TYPE_QUALITY_DEFAULT.toFixed(1)}
|
|
92
106
|
END
|
|
93
107
|
)`;
|
|
94
108
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# claude-mem-lite: Fast bash pre-filter for the PreToolUse:Agent|Task hook.
|
|
3
|
+
#
|
|
4
|
+
# Subagent dispatch-time injection is DEFAULT OFF (CLAUDE_MEM_SUBAGENT_INJECT=on|1),
|
|
5
|
+
# and pre-agent-inject.js's off path is already the cheapest thing a Node script can
|
|
6
|
+
# do — it reads one env var and returns. The cost that remained was Node itself:
|
|
7
|
+
# every Agent/Task dispatch paid a full interpreter start for a feature that was
|
|
8
|
+
# switched off. Measured on this machine, 20 dispatches each, flag unset: **22.6ms**
|
|
9
|
+
# through `node scripts/hook-launcher.mjs scripts/pre-agent-inject.js` vs **2.4ms**
|
|
10
|
+
# here. With the flag ON the prefilter costs 1.8ms on top of the 44.9ms the feature
|
|
11
|
+
# already spends (46.7ms) — the trade is a rounding error against a real saving.
|
|
12
|
+
#
|
|
13
|
+
# Measure with the flag EXPLICITLY unset (`env -u CLAUDE_MEM_SUBAGENT_INJECT …`). A
|
|
14
|
+
# maintainer shell that dogfoods the feature exports it, and the first pass at these
|
|
15
|
+
# numbers timed the ON path twice and called it the off-path cost.
|
|
16
|
+
#
|
|
17
|
+
# Audit 2026-08-22 P2-5 asked for "don't register the hook when the flag is off".
|
|
18
|
+
# A plugin manifest is static JSON — Claude Code reads hooks/hooks.json once and
|
|
19
|
+
# nothing in it can consult an env var — so conditional registration would mean
|
|
20
|
+
# deleting the entry and making opt-in require re-running the installer. This gets
|
|
21
|
+
# the same saving with the opt-in path intact, and mirrors scripts/post-tool-use.sh,
|
|
22
|
+
# the prefilter already doing this for PostToolUse.
|
|
23
|
+
#
|
|
24
|
+
# Fail-open, like every hook here: never exit non-zero, never block a dispatch.
|
|
25
|
+
|
|
26
|
+
# ON: hand off untouched. exec BEFORE reading anything — pre-agent-inject.js echoes
|
|
27
|
+
# the whole subagent prompt back via updatedInput, so stdin must reach it verbatim.
|
|
28
|
+
# hook-launcher.mjs sits next to this file in every install shape (plugin cache,
|
|
29
|
+
# managed copy, dev symlink), which is also how it locates its own install dir.
|
|
30
|
+
case "${CLAUDE_MEM_SUBAGENT_INJECT:-}" in
|
|
31
|
+
on|1)
|
|
32
|
+
_mem_dir=$(dirname "${BASH_SOURCE[0]}")
|
|
33
|
+
exec node "${_mem_dir}/hook-launcher.mjs" scripts/pre-agent-inject.js
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
|
|
37
|
+
# OFF: drain stdin before exiting. The Node path did not read it either, so this is
|
|
38
|
+
# no worse than today and strictly safer — the host writes the event payload before
|
|
39
|
+
# waiting on us, and a >64KB prompt would otherwise leave that write against a pipe
|
|
40
|
+
# whose reader vanished. Builtin loop, no subprocess: `head -c` costs ~1ms more than
|
|
41
|
+
# the entire rest of this script.
|
|
42
|
+
while IFS= read -r _; do :; done
|
|
43
|
+
exit 0
|
|
@@ -343,7 +343,21 @@ export function hasExplicitSignal(text, { errSig, files, intent } = {}) {
|
|
|
343
343
|
// ×3 runs) → VERDICT NET-POSITIVE. The only behavior delta is on-topic eagerness (naming
|
|
344
344
|
// an identifier surfaces its obs) — the highest-precision injection trigger there is. The
|
|
345
345
|
// prose stop-list (IDENTIFIER_STOPWORDS) keeps the extractor off ordinary English.
|
|
346
|
-
export
|
|
346
|
+
export // Query caps for THIS surface only (audit 2026-08-22 P2-13). The 64KB byte guard on
|
|
347
|
+
// stdin bounds what we read, not what we compute: sanitizeFtsQuery costs 0.8ms on a
|
|
348
|
+
// normal prompt, 6.2ms on a 64KB ASCII one and 31.8ms on a 64KB CJK one, and this hook
|
|
349
|
+
// runs on every prompt before the model sees the turn. 2000 characters is a long prompt
|
|
350
|
+
// by any measure, and past ~64 meaningful AND-joined terms an FTS5 query matches nothing
|
|
351
|
+
// anyway and survives only through the OR fallback. An explicit `search` stays uncapped
|
|
352
|
+
// — a person who types a long query meant it.
|
|
353
|
+
const UPS_QUERY_CAPS = { maxChars: 2000, maxTokens: 64 };
|
|
354
|
+
|
|
355
|
+
/** The capped query builder both search paths on this surface go through. */
|
|
356
|
+
export function upsFtsQuery(text) {
|
|
357
|
+
return sanitizeFtsQuery(text, UPS_QUERY_CAPS);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const IDENTIFIER_BYPASS = process.env.CLAUDE_MEM_UPS_IDENTIFIER_BYPASS !== '0';
|
|
347
361
|
const TECH_IDENTIFIER_RE_G = new RegExp(TECH_IDENTIFIER_RE.source, 'g');
|
|
348
362
|
|
|
349
363
|
// All tech-identifier tokens in `text`, lowercased + de-duped (for case-insensitive
|
|
@@ -381,7 +395,7 @@ export function rowMatchesIdentifier(row, idsLower) {
|
|
|
381
395
|
// importance/type/decay inflation.
|
|
382
396
|
export function searchByFts(db, queryText, project, limit, typeFilter,
|
|
383
397
|
{ nowT = Date.now(), epochTo = null } = {}) {
|
|
384
|
-
const ftsQuery =
|
|
398
|
+
const ftsQuery = upsFtsQuery(queryText);
|
|
385
399
|
if (!ftsQuery) return { rows: [], mode: null };
|
|
386
400
|
|
|
387
401
|
const cutoff = nowT - LOOKBACK_MS;
|
|
@@ -483,7 +497,7 @@ function searchByFile(db, files, project, limit) {
|
|
|
483
497
|
// sparser and more surface-form than observations; the gate would rarely
|
|
484
498
|
// fire and mostly kill real hits).
|
|
485
499
|
function searchByUserPrompts(db, queryText, project, limit) {
|
|
486
|
-
const ftsQuery =
|
|
500
|
+
const ftsQuery = upsFtsQuery(queryText);
|
|
487
501
|
if (!ftsQuery) return [];
|
|
488
502
|
|
|
489
503
|
const cutoff = Date.now() - LOOKBACK_MS;
|
package/server.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from './lib/maintain-core.mjs';
|
|
24
24
|
import { snapshotDb } from './lib/db-backup.mjs';
|
|
25
25
|
import { deleteObservations, previewDeleteRows } from './lib/delete-core.mjs';
|
|
26
|
-
import { fetchObsDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS, supersededNotice } from './lib/get-core.mjs';
|
|
26
|
+
import { fetchObsDetail, fetchPromptDetail, fetchEventDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS, PROMPT_DETAIL_FIELDS, EVENT_DETAIL_FIELDS, supersededNotice } from './lib/get-core.mjs';
|
|
27
27
|
import { collectBrowseTiers, getActiveMemorySessionId, BROWSE_TIERS, BROWSE_TIER_LABELS } from './lib/browse-core.mjs';
|
|
28
28
|
import { effectiveQuiet, RUNTIME_DIR } from './hook-shared.mjs';
|
|
29
29
|
import { computeStatsFeed } from './lib/stats-core.mjs';
|
|
@@ -46,7 +46,8 @@ function descriptionOf(name) {
|
|
|
46
46
|
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
47
47
|
import { join, sep } from 'path';
|
|
48
48
|
import { homedir } from 'os';
|
|
49
|
-
import { ensureRegistryDb,
|
|
49
|
+
import { ensureRegistryDb, collectRegistryStats, listResourcesRanked, formatRegistryListLine } from './registry.mjs';
|
|
50
|
+
import { IMPORT_STRING_FIELDS, importResource, removeResource, reindexResources } from './lib/registry-core.mjs';
|
|
50
51
|
import { searchResources } from './registry-retriever.mjs';
|
|
51
52
|
import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
|
|
52
53
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
@@ -666,34 +667,31 @@ server.registerTool(
|
|
|
666
667
|
}
|
|
667
668
|
|
|
668
669
|
if (bySrc.prompt.length > 0) {
|
|
669
|
-
const
|
|
670
|
-
const rows = db.prepare(`SELECT * FROM user_prompts WHERE id IN (${ph}) ORDER BY created_at_epoch ASC`).all(...bySrc.prompt);
|
|
671
|
-
for (const row of rows) {
|
|
670
|
+
for (const row of fetchPromptDetail(db, bySrc.prompt)) {
|
|
672
671
|
foundBySource.prompt.add(row.id);
|
|
673
672
|
const lines = [`── P#${row.id} ──`];
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
673
|
+
for (const f of PROMPT_DETAIL_FIELDS) {
|
|
674
|
+
if (f === 'id') continue; // already in the header
|
|
675
|
+
const val = row[f];
|
|
676
|
+
if (val === null || val === undefined || val === '') continue;
|
|
677
|
+
lines.push(`${f}: ${typeof val === 'string' && val.length > 500 ? val.slice(0, 500) + '…' : val}`);
|
|
678
|
+
}
|
|
678
679
|
sections.push(lines.join('\n'));
|
|
679
680
|
}
|
|
680
681
|
}
|
|
681
682
|
|
|
682
683
|
if (bySrc.event.length > 0) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
// No created_at ISO column (only *_epoch) — render the epoch as an ISO string for a scannable date.
|
|
687
|
-
for (const row of rows) {
|
|
684
|
+
// fetchEventDetail derives created_at from created_at_epoch (events have no ISO
|
|
685
|
+
// column), so EVENT_DETAIL_FIELDS names it like any other field on both faces.
|
|
686
|
+
for (const row of fetchEventDetail(db, bySrc.event)) {
|
|
688
687
|
foundBySource.event.add(row.id);
|
|
689
688
|
const lines = [`── E#${row.id} [${row.event_type}] ──`];
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (row.created_at_epoch) lines.push(`created_at: ${new Date(row.created_at_epoch).toISOString()}`);
|
|
689
|
+
for (const f of EVENT_DETAIL_FIELDS) {
|
|
690
|
+
if (f === 'id' || f === 'event_type') continue; // already in the header
|
|
691
|
+
const val = row[f];
|
|
692
|
+
if (val === null || val === undefined || val === '') continue;
|
|
693
|
+
lines.push(`${f}: ${typeof val === 'string' && val.length > 500 ? val.slice(0, 500) + '…' : val}`);
|
|
694
|
+
}
|
|
697
695
|
sections.push(lines.join('\n'));
|
|
698
696
|
}
|
|
699
697
|
}
|
|
@@ -1454,19 +1452,14 @@ server.registerTool(
|
|
|
1454
1452
|
if (!args.name || !args.resource_type) {
|
|
1455
1453
|
return { content: [{ type: 'text', text: 'import requires name and resource_type' }], isError: true };
|
|
1456
1454
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
//
|
|
1460
|
-
|
|
1461
|
-
// github/preinstalled row without `source` must not flip it to 'user'.
|
|
1462
|
-
let source = args.source;
|
|
1463
|
-
if (!source) {
|
|
1464
|
-
const existing = rdb.prepare('SELECT source FROM resources WHERE type = ? AND name = ?').get(args.resource_type, args.name);
|
|
1465
|
-
source = existing ? existing.source : 'user';
|
|
1466
|
-
}
|
|
1467
|
-
const fields = { name: args.name, type: args.resource_type, status: 'active', source };
|
|
1455
|
+
// Provenance preservation + the 'installed' tier grant live in lib/registry-core.mjs,
|
|
1456
|
+
// shared with the `registry import` CLI twin (audit 2026-08-22 P1-3). Before that
|
|
1457
|
+
// extraction this side wrote its own SQL and silently skipped the tier grant.
|
|
1458
|
+
const fields = {};
|
|
1468
1459
|
for (const f of IMPORT_STRING_FIELDS) fields[f] = args[f] || '';
|
|
1469
|
-
const id =
|
|
1460
|
+
const { id } = importResource(rdb, {
|
|
1461
|
+
name: args.name, type: args.resource_type, source: args.source, fields,
|
|
1462
|
+
});
|
|
1470
1463
|
return { content: [{ type: 'text', text: `Imported: ${args.resource_type}:${args.name} (id=${id})` }] };
|
|
1471
1464
|
}
|
|
1472
1465
|
|
|
@@ -1474,14 +1467,13 @@ server.registerTool(
|
|
|
1474
1467
|
if (!args.name || !args.resource_type) {
|
|
1475
1468
|
return { content: [{ type: 'text', text: 'remove requires name and resource_type' }], isError: true };
|
|
1476
1469
|
}
|
|
1477
|
-
const
|
|
1478
|
-
return { content: [{ type: 'text', text:
|
|
1470
|
+
const { removed } = removeResource(rdb, { name: args.name, type: args.resource_type });
|
|
1471
|
+
return { content: [{ type: 'text', text: removed ? `Removed: ${args.resource_type}:${args.name}` : 'Not found.' }] };
|
|
1479
1472
|
}
|
|
1480
1473
|
|
|
1481
1474
|
if (action === 'reindex') {
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
return { content: [{ type: 'text', text: `FTS5 reindexed. ${count.c} active resources.` }] };
|
|
1475
|
+
const { activeCount } = reindexResources(rdb);
|
|
1476
|
+
return { content: [{ type: 'text', text: `FTS5 reindexed. ${activeCount} active resources.` }] };
|
|
1485
1477
|
}
|
|
1486
1478
|
|
|
1487
1479
|
if (action === 'import_url') {
|
package/source-files.mjs
CHANGED
|
@@ -164,6 +164,10 @@ export const SOURCE_FILES = [
|
|
|
164
164
|
// auto-update. Same single-source-of-truth pattern (see #8217).
|
|
165
165
|
'lib/observation-write.mjs',
|
|
166
166
|
'lib/recall-core.mjs',
|
|
167
|
+
// Shared registry write core (import/remove/reindex + the 'installed' tier grant).
|
|
168
|
+
// Statically imported by mem-cli.mjs AND server.mjs — missing it from the manifest
|
|
169
|
+
// would break `registry import|remove|reindex` and mem_registry on auto-update.
|
|
170
|
+
'lib/registry-core.mjs',
|
|
167
171
|
// Shared timeline core (anchor resolution + before/after window) and shared
|
|
168
172
|
// cross-source search core (sessions/prompts FTS, CJK fallback, normalization,
|
|
169
173
|
// pagination math). Statically imported by mem-cli.mjs AND server.mjs — same
|
|
@@ -187,6 +191,8 @@ export const SOURCE_FILES = [
|
|
|
187
191
|
// Statically imported by mem-cli.mjs (cmdMaintain), server.mjs (mem_maintain),
|
|
188
192
|
// and hook.mjs (handleAutoMaintain) — missing it would break maintain on auto-update.
|
|
189
193
|
'lib/maintain-core.mjs',
|
|
194
|
+
'lib/fast-summary.mjs',
|
|
195
|
+
'lib/transcript-scan.mjs',
|
|
190
196
|
// Pre-maintenance VACUUM INTO snapshot (MED-2). Statically imported by mem-cli.mjs,
|
|
191
197
|
// server.mjs, and hook.mjs before their destructive purge/cleanup — missing it
|
|
192
198
|
// would crash maintain on auto-update with an unresolved import.
|
|
@@ -266,6 +272,12 @@ export const HOOK_SCRIPT_FILES = [
|
|
|
266
272
|
'pre-tool-recall.js',
|
|
267
273
|
'post-tool-recall.js',
|
|
268
274
|
'pre-skill-bridge.js',
|
|
275
|
+
// The Agent|Task hook command in BOTH registration sites is now the .sh prefilter,
|
|
276
|
+
// which execs the .js only when CLAUDE_MEM_SUBAGENT_INJECT is on (audit P2-5). Both
|
|
277
|
+
// must be materialized: shipping the prefilter without its target turns every
|
|
278
|
+
// opt-in dispatch into a silent no-op, and shipping the target without the
|
|
279
|
+
// prefilter leaves the registered hook command pointing at a file that is not there.
|
|
280
|
+
'pre-agent-inject.sh',
|
|
269
281
|
'pre-agent-inject.js',
|
|
270
282
|
// v2.84: self-heal wrapper that detects ERR_MODULE_NOT_FOUND under the
|
|
271
283
|
// install dir and runs install.mjs repair before retrying the entry.
|