claude-mem-lite 2.95.1 → 2.97.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/cli/common.mjs +18 -0
- package/hook-llm.mjs +14 -4
- package/hook-update.mjs +16 -2
- package/install.mjs +72 -2
- package/mem-cli.mjs +18 -25
- package/package.json +1 -1
- package/server.mjs +6 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "2.
|
|
13
|
+
"version": "2.97.0",
|
|
14
14
|
"source": "./",
|
|
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)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.97.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
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/cli/common.mjs
CHANGED
|
@@ -98,6 +98,24 @@ export function fmtDateShort(iso) {
|
|
|
98
98
|
return iso.slice(0, 10);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Integer epoch-ms time fields on the observations table that `get`/`mem_get`
|
|
102
|
+
// render. Shared by the CLI (mem-cli.mjs) and the MCP server (server.mjs) so the
|
|
103
|
+
// two `get` paths can't drift — pre-2.97 the MCP path printed bare ms
|
|
104
|
+
// (`last_accessed_at: 1781024049720`) while the CLI showed `<ms> (<relative>)`,
|
|
105
|
+
// because the formatter lived only in mem-cli.mjs.
|
|
106
|
+
export const OBS_TIME_FIELDS = ['superseded_at', 'last_accessed_at'];
|
|
107
|
+
|
|
108
|
+
// Pure formatter — null/undefined/non-time pass through; integer time fields
|
|
109
|
+
// render as `<raw> (<relative>)` so callers get both an audit value and a
|
|
110
|
+
// human/LLM-scannable hint, mirroring `recent`/`timeline`/`recall`.
|
|
111
|
+
export function formatObsFieldValue(field, val) {
|
|
112
|
+
if (val === null || val === undefined) return val;
|
|
113
|
+
if (OBS_TIME_FIELDS.includes(field) && typeof val === 'number') {
|
|
114
|
+
return `${val} (${relativeTime(val)})`;
|
|
115
|
+
}
|
|
116
|
+
return val;
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
// ─── ID Token Parsing ────────────────────────────────────────────────────────
|
|
102
120
|
// Re-exported from lib/id-routing.mjs so CLI and MCP (server.mjs) share a single
|
|
103
121
|
// parser — parity per #8050. Keep this re-export for back-compat with the
|
package/hook-llm.mjs
CHANGED
|
@@ -601,7 +601,12 @@ export async function handleLLMEpisode() {
|
|
|
601
601
|
await sleep(delayMs);
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
|
|
604
|
+
// `episode.files` is normally a [] from createEpisode, but a malformed or
|
|
605
|
+
// older-format tmp file can omit it — `.map()` on undefined would throw here,
|
|
606
|
+
// before any cleanup, leaking the tmp file (which is then retried and crashes
|
|
607
|
+
// forever). Guard defensively, mirroring buildImmediateObservation's `|| []`.
|
|
608
|
+
const episodeFiles = Array.isArray(episode.files) ? episode.files : [];
|
|
609
|
+
const fileList = episodeFiles.map(f => basename(f)).join(', ') || '(multiple)';
|
|
605
610
|
|
|
606
611
|
// Defense-in-depth (cso F#4): split static instructions (system) from
|
|
607
612
|
// per-call data (user). Episode descriptions and file paths come from tool
|
|
@@ -622,7 +627,7 @@ search_aliases: 2-6 alternative search terms someone might use to find this memo
|
|
|
622
627
|
JSON: {"type":"decision|bugfix|feature|refactor|discovery|change","title":"concise ≤80 char description","narrative":"what changed, why, and outcome (2-3 sentences)","concepts":["kw1","kw2"],"facts":["fact1","fact2"],"importance":1,"lesson_learned":"non-obvious insight a future session needs, or null","search_aliases":["alt query 1","alt query 2"]}
|
|
623
628
|
${SHARED_OBS_SCHEMA_TAIL}`;
|
|
624
629
|
const user = `Tool: ${e.tool}
|
|
625
|
-
File: ${
|
|
630
|
+
File: ${episodeFiles.join(', ') || 'unknown'}
|
|
626
631
|
Action: ${e.desc}
|
|
627
632
|
Error: ${e.isError ? 'yes' : 'no'}`;
|
|
628
633
|
prompt = { system, user };
|
|
@@ -743,7 +748,7 @@ ${actionList}`;
|
|
|
743
748
|
narrative: truncate(parsed.narrative || '', 500),
|
|
744
749
|
concepts: Array.isArray(parsed.concepts) ? parsed.concepts.slice(0, 10) : [],
|
|
745
750
|
facts: Array.isArray(parsed.facts) ? parsed.facts.slice(0, 10) : [],
|
|
746
|
-
files:
|
|
751
|
+
files: episodeFiles,
|
|
747
752
|
filesRead: episode.filesRead || [],
|
|
748
753
|
// v2.33.1: when lesson is low-signal, don't trust Haiku's importance
|
|
749
754
|
// inflation. v2.54.0: extended from {change, discovery} to all types
|
|
@@ -752,7 +757,12 @@ ${actionList}`;
|
|
|
752
757
|
// imp=2-3 even when lesson is null after retry. Keep `decision` exempt:
|
|
753
758
|
// it's rare (39 obs / 94.9% hit-rate) and the retry path already gave
|
|
754
759
|
// it a second chance; a no-lesson decision is still a worthwhile signal.
|
|
755
|
-
|
|
760
|
+
// `!retryRecovered`: when the P3 retry recovered a substantive lesson,
|
|
761
|
+
// the obs is no longer low-signal — capping it to 1 would negate the
|
|
762
|
+
// retry's entire purpose (a recovered bugfix lesson would silently drop
|
|
763
|
+
// out of --importance 2 searches and the working tier). Gate the cap on
|
|
764
|
+
// the *effective* low-signal state, not the pre-retry flag.
|
|
765
|
+
importance: isLessonLowSignal && !retryRecovered && parsed.type !== 'decision'
|
|
756
766
|
? Math.min(ruleImportance, 1)
|
|
757
767
|
: Math.max(ruleImportance, clampImportance(parsed.importance)),
|
|
758
768
|
lessonLearned,
|
package/hook-update.mjs
CHANGED
|
@@ -155,8 +155,22 @@ function isPluginMode() {
|
|
|
155
155
|
// ── Dev Mode Detection ─────────────────────────────────────
|
|
156
156
|
function isDevMode() {
|
|
157
157
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
// A dev checkout always carries a .git dir. This catches a whole-directory
|
|
159
|
+
// symlink (~/.claude-mem-lite -> /repo): lstat on server.mjs there follows the
|
|
160
|
+
// intermediate symlink and sees a regular file, so the per-file probe below
|
|
161
|
+
// would return false and auto-update would clobber the working tree.
|
|
162
|
+
if (existsSync(join(INSTALL_DIR, '.git'))) return true;
|
|
163
|
+
// Standard `install --dev` symlinks individual source files; checking several
|
|
164
|
+
// core files (not just server.mjs) survives the drift case where one file was
|
|
165
|
+
// replaced by a plain copy while the install is still symlink-provisioned —
|
|
166
|
+
// mirrors lib/doctor-drift.mjs's "any symlink ⇒ dev" detection. A copy-based
|
|
167
|
+
// real install (install.mjs non-dev) has no symlinks and no .git, so this
|
|
168
|
+
// cannot false-positive into never auto-updating.
|
|
169
|
+
for (const f of ['server.mjs', 'hook.mjs', 'cli.mjs', 'mem-cli.mjs']) {
|
|
170
|
+
const p = join(INSTALL_DIR, f);
|
|
171
|
+
if (existsSync(p) && lstatSync(p).isSymbolicLink()) return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
160
174
|
} catch { return false; }
|
|
161
175
|
}
|
|
162
176
|
|
package/install.mjs
CHANGED
|
@@ -302,6 +302,43 @@ function isDevInstall() {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
// Decide what to check out of a registry repo. We only ever copy the manifest's
|
|
306
|
+
// `entry.path` subdirs into managed/skills|agents, so the working tree never
|
|
307
|
+
// needs the rest of the repo — a partial+sparse clone fetches just those subtrees
|
|
308
|
+
// (e.g. davila7/claude-code-templates: 197MB whole repo → a few MB of 3 paths).
|
|
309
|
+
// Returns { full, paths }: `full` forces a normal checkout when any entry maps to
|
|
310
|
+
// the repo root ('.') — sparse buys nothing there. Unsafe paths ('..'/absolute)
|
|
311
|
+
// are dropped here exactly as the copy loop drops them, so they never reach
|
|
312
|
+
// `sparse-checkout set`. Pure + exported for unit testing.
|
|
313
|
+
export function planRepoSparsePaths(entries) {
|
|
314
|
+
let needsFull = false;
|
|
315
|
+
const paths = [];
|
|
316
|
+
for (const e of entries || []) {
|
|
317
|
+
const p = e && e.path;
|
|
318
|
+
if (!p || p === '.' || p === './') { needsFull = true; continue; }
|
|
319
|
+
if (isAbsolute(p) || String(p).includes('..')) continue; // unsafe — skipped at copy too
|
|
320
|
+
const norm = String(p).replace(/^\.\//, '').replace(/\/+$/, '');
|
|
321
|
+
if (norm && !paths.includes(norm)) paths.push(norm);
|
|
322
|
+
}
|
|
323
|
+
// No usable sparse paths (all root or all unsafe) → a full checkout is the only
|
|
324
|
+
// thing that can produce content; sparse would be an empty, pointless tree.
|
|
325
|
+
return { full: needsFull || paths.length === 0, paths };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// True only for a clone this code produced (partial-clone promisor + sparse-checkout
|
|
329
|
+
// both on). A legacy full clone returns false → caller re-clones it slim. Detection
|
|
330
|
+
// erring false only costs a one-time re-clone (the dir is a rebuildable cache), so
|
|
331
|
+
// over-eager migration is safe; under-eager just keeps a fat clone one more cycle.
|
|
332
|
+
function isPartialSparseClone(clonePath) {
|
|
333
|
+
const cfg = (key) => {
|
|
334
|
+
try {
|
|
335
|
+
return execFileSync('git', ['-C', clonePath, 'config', '--get', key],
|
|
336
|
+
{ encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
337
|
+
} catch { return ''; } // missing key → git exits non-zero → treat as unset
|
|
338
|
+
};
|
|
339
|
+
return cfg('remote.origin.promisor') === 'true' && cfg('core.sparseCheckout') === 'true';
|
|
340
|
+
}
|
|
341
|
+
|
|
305
342
|
// ─── Install ────────────────────────────────────────────────────────────────
|
|
306
343
|
|
|
307
344
|
async function install() {
|
|
@@ -748,11 +785,39 @@ async function install() {
|
|
|
748
785
|
const clonePath = join(managedDir, 'repos', repoName);
|
|
749
786
|
let repoReady = false;
|
|
750
787
|
|
|
788
|
+
const plan = planRepoSparsePaths(entries);
|
|
789
|
+
const cloneUrl = `${repoUrl.replace(/\.git$/, '')}.git`;
|
|
790
|
+
// Clone only what we extract: a partial (blob:none) + sparse clone fetches
|
|
791
|
+
// just the manifest subpaths' subtrees instead of the whole repo. Falls
|
|
792
|
+
// back to a plain shallow clone if partial-clone/sparse-checkout is
|
|
793
|
+
// unsupported (old git/server) — identical to the prior behavior.
|
|
794
|
+
const cloneSlim = () => {
|
|
795
|
+
if (plan.full) {
|
|
796
|
+
execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
execFileSync('git', ['clone', '--depth', '1', '--filter=blob:none', '--no-checkout', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
|
|
801
|
+
execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], { stdio: 'pipe', timeout: 30000 });
|
|
802
|
+
execFileSync('git', ['-C', clonePath, 'checkout'], { stdio: 'pipe', timeout: 30000 });
|
|
803
|
+
} catch {
|
|
804
|
+
try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
|
|
805
|
+
execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], { stdio: 'pipe', timeout: 30000 });
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
// Migrate a legacy full clone: drop it so the fresh-clone path below
|
|
810
|
+
// rebuilds it slim. managed/repos is a rebuildable cache, so this loses
|
|
811
|
+
// nothing and reclaims the bulk of its footprint on the next install run.
|
|
812
|
+
if (!plan.full && existsSync(clonePath) && !isPartialSparseClone(clonePath)) {
|
|
813
|
+
try { rmSync(clonePath, { recursive: true, force: true }); } catch {}
|
|
814
|
+
}
|
|
815
|
+
|
|
751
816
|
if (!existsSync(clonePath)) {
|
|
752
|
-
// Fresh clone
|
|
817
|
+
// Fresh clone (also the rebuild path for a just-migrated legacy clone)
|
|
753
818
|
try {
|
|
754
819
|
mkdirSync(join(managedDir, 'repos'), { recursive: true });
|
|
755
|
-
|
|
820
|
+
cloneSlim();
|
|
756
821
|
cloned++;
|
|
757
822
|
repoReady = true;
|
|
758
823
|
} catch (err) {
|
|
@@ -767,6 +832,11 @@ async function install() {
|
|
|
767
832
|
} else {
|
|
768
833
|
// Update existing: fetch latest and fast-forward
|
|
769
834
|
try {
|
|
835
|
+
// Re-assert the sparse set so a newer manifest that adds a subpath to
|
|
836
|
+
// an already-slim clone checks it out (idempotent; no-op for full clones).
|
|
837
|
+
if (!plan.full && isPartialSparseClone(clonePath)) {
|
|
838
|
+
try { execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], { stdio: 'pipe', timeout: 30000 }); } catch {}
|
|
839
|
+
}
|
|
770
840
|
const localHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
771
841
|
execFileSync('git', ['-C', clonePath, 'fetch', '--depth', '1', 'origin'], { stdio: 'pipe', timeout: 30000 });
|
|
772
842
|
const remoteHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'FETCH_HEAD'], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
package/mem-cli.mjs
CHANGED
|
@@ -33,7 +33,7 @@ import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
|
33
33
|
// v2.41: shared CLI helpers extracted to cli/common.mjs. Keep this file as the
|
|
34
34
|
// router + remaining-command bodies during the incremental split. Future work:
|
|
35
35
|
// move each cmdXxx into its own cli/<cmd>.mjs; mem-cli.mjs becomes pure dispatch.
|
|
36
|
-
import { parseArgs, out, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints, rejectBareStringFlags } from './cli/common.mjs';
|
|
36
|
+
import { parseArgs, out, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints, rejectBareStringFlags, OBS_TIME_FIELDS, formatObsFieldValue } from './cli/common.mjs';
|
|
37
37
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
38
38
|
import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
39
39
|
import { countRecentHookErrors } from './lib/hook-telemetry.mjs';
|
|
@@ -559,24 +559,12 @@ function cmdRecall(db, args) {
|
|
|
559
559
|
|
|
560
560
|
const OBS_FIELDS = ['id', 'type', 'title', 'subtitle', 'narrative', 'text', 'facts', 'concepts', 'lesson_learned', 'search_aliases', 'files_read', 'files_modified', 'project', 'created_at', 'memory_session_id', 'prompt_number', 'importance', 'related_ids', 'access_count', 'branch', 'superseded_at', 'superseded_by', 'last_accessed_at'];
|
|
561
561
|
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
export
|
|
568
|
-
|
|
569
|
-
// Pure formatter — null/undefined/non-time pass through; time fields on
|
|
570
|
-
// integer values render as `<raw> (<relative>)` mirroring the convention
|
|
571
|
-
// already used by `recent` / `timeline` / `recall`. Pre-2.63.0 the get
|
|
572
|
-
// path printed bare ms (e.g. `last_accessed_at: 1778357330957`).
|
|
573
|
-
export function formatObsFieldValue(field, val) {
|
|
574
|
-
if (val === null || val === undefined) return val;
|
|
575
|
-
if (OBS_TIME_FIELDS.includes(field) && typeof val === 'number') {
|
|
576
|
-
return `${val} (${relativeTime(val)})`;
|
|
577
|
-
}
|
|
578
|
-
return val;
|
|
579
|
-
}
|
|
562
|
+
// Time-field formatting moved to cli/common.mjs so the CLI `get` and the MCP
|
|
563
|
+
// `mem_get` (server.mjs) share one source and can't drift (the drift bug:
|
|
564
|
+
// MCP printed bare ms while CLI showed `<ms> (<relative>)`). Imported at the
|
|
565
|
+
// top; re-exported here for back-compat with existing importers
|
|
566
|
+
// (tests/get-time-format.test.mjs).
|
|
567
|
+
export { OBS_TIME_FIELDS, formatObsFieldValue };
|
|
580
568
|
|
|
581
569
|
function renderObsRows(db, ids, requestedFields) {
|
|
582
570
|
const placeholders = ids.map(() => '?').join(',');
|
|
@@ -1797,12 +1785,15 @@ function cmdExport(db, args) {
|
|
|
1797
1785
|
|
|
1798
1786
|
// Full round-trippable column set so `restore` rebuilds observations faithfully —
|
|
1799
1787
|
// content + value-signals (access/cited/uncited/injection/decay) + branch + timing.
|
|
1800
|
-
//
|
|
1801
|
-
//
|
|
1802
|
-
//
|
|
1788
|
+
// `search_aliases` is an FTS5-indexed column (BM25 weight 5) — dropping it on
|
|
1789
|
+
// export silently lost the LLM-generated alternate query terms on restore, so a
|
|
1790
|
+
// restored memory became unfindable by its aliases. Additive vs the pre-v2.90
|
|
1791
|
+
// 13-col shape; existing `export | jq '.[].title'` consumers are unaffected.
|
|
1792
|
+
// id + memory_session_id are informational (restore remaps id and buckets under
|
|
1793
|
+
// a restore session).
|
|
1803
1794
|
const rows = db.prepare(`
|
|
1804
1795
|
SELECT id, memory_session_id, project, type, title, subtitle, narrative, concepts, facts,
|
|
1805
|
-
files_read, files_modified, lesson_learned, importance, branch,
|
|
1796
|
+
files_read, files_modified, lesson_learned, search_aliases, importance, branch,
|
|
1806
1797
|
access_count, cited_count, uncited_streak, injection_count, decay_seen_count,
|
|
1807
1798
|
last_accessed_at, created_at, created_at_epoch
|
|
1808
1799
|
FROM observations WHERE ${wheres.join(' AND ')}
|
|
@@ -1863,7 +1854,7 @@ function cmdRestore(db, argv) {
|
|
|
1863
1854
|
|
|
1864
1855
|
const dupCheck = db.prepare('SELECT id FROM observations WHERE project = ? AND title = ? AND created_at_epoch = ? LIMIT 1');
|
|
1865
1856
|
const signalUpdate = db.prepare(`UPDATE observations SET
|
|
1866
|
-
subtitle = ?, concepts = ?, facts = ?, files_read = ?, branch = COALESCE(?, branch),
|
|
1857
|
+
subtitle = ?, concepts = ?, facts = ?, search_aliases = ?, files_read = ?, branch = COALESCE(?, branch),
|
|
1867
1858
|
access_count = ?, cited_count = ?, uncited_streak = ?, injection_count = ?,
|
|
1868
1859
|
decay_seen_count = ?, last_accessed_at = ?
|
|
1869
1860
|
WHERE id = ?`);
|
|
@@ -1893,8 +1884,10 @@ function cmdRestore(db, argv) {
|
|
|
1893
1884
|
});
|
|
1894
1885
|
if (res.kind !== 'saved') { skipped++; continue; } // saveObservation Jaccard dedup
|
|
1895
1886
|
// Re-apply the fields saveObservation zeros/derives so the backup is faithful.
|
|
1887
|
+
// search_aliases is its own FTS5 column, so this UPDATE re-syncs the index
|
|
1888
|
+
// (via the observations FTS triggers) and restored aliases stay searchable.
|
|
1896
1889
|
signalUpdate.run(
|
|
1897
|
-
r.subtitle || '', r.concepts || '', r.facts || '', r.files_read || '[]', r.branch ?? null,
|
|
1890
|
+
r.subtitle || '', r.concepts || '', r.facts || '', r.search_aliases ?? null, r.files_read || '[]', r.branch ?? null,
|
|
1898
1891
|
num(r.access_count), num(r.cited_count), num(r.uncited_streak), num(r.injection_count),
|
|
1899
1892
|
num(r.decay_seen_count), r.last_accessed_at ?? null,
|
|
1900
1893
|
res.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.97.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",
|
package/server.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from './lib/maintain-core.mjs';
|
|
21
21
|
import { effectiveQuiet, RUNTIME_DIR } from './hook-shared.mjs';
|
|
22
22
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
23
|
+
import { formatObsFieldValue } from './cli/common.mjs';
|
|
23
24
|
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';
|
|
24
25
|
|
|
25
26
|
// Lookup helper: all user-facing tool descriptions live in tool-schemas.mjs
|
|
@@ -801,8 +802,12 @@ server.registerTool(
|
|
|
801
802
|
const val = row[f];
|
|
802
803
|
if (val === null || val === undefined || val === '') continue;
|
|
803
804
|
if (f === 'text' && row.narrative && typeof val === 'string' && val.startsWith(row.narrative)) continue;
|
|
805
|
+
// Shared formatter (cli/common.mjs) renders epoch-ms time fields as
|
|
806
|
+
// `<ms> (<relative>)` — parity with the CLI `get` path so an LLM reader
|
|
807
|
+
// gets a scannable hint instead of a bare millisecond integer.
|
|
808
|
+
const display = formatObsFieldValue(f, val);
|
|
804
809
|
const maxLen = f === 'narrative' ? 1000 : f === 'lesson_learned' ? 500 : f === 'text' ? 500 : 200;
|
|
805
|
-
lines.push(`${f}: ${typeof
|
|
810
|
+
lines.push(`${f}: ${typeof display === 'string' && display.length > maxLen ? display.slice(0, maxLen) + '…' : display}`);
|
|
806
811
|
}
|
|
807
812
|
sections.push(lines.join('\n'));
|
|
808
813
|
}
|