claude-mem-lite 2.85.0 → 2.87.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 +4 -1
- package/README.zh-CN.md +4 -1
- package/haiku-client.mjs +127 -22
- package/hook-llm.mjs +3 -3
- package/hook-shared.mjs +18 -5
- package/package.json +1 -1
- package/schema.mjs +33 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "2.
|
|
13
|
+
"version": "2.87.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. Alternative to claude-mem with 600x lower cost."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.87.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. Alternative to claude-mem with 600x lower cost.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
|
|
|
105
105
|
- **Resource registry** -- Indexes installed skills and agents with FTS5 search, composite scoring, and invocation tracking; searchable via `mem_registry` MCP tool
|
|
106
106
|
- **Unified resource discovery** -- Shared filesystem traversal layer (`resource-discovery.mjs`) used by both runtime scanner and offline indexer, supporting flat directories, plugin nesting, and loose `.md` files
|
|
107
107
|
- **Domain synonym expansion** -- Registry search queries expand to domain synonyms (e.g., "fix" → debug, bugfix, troubleshoot, diagnose, repair)
|
|
108
|
-
- **
|
|
108
|
+
- **Multi-provider LLM mode** -- Provider priority `ANTHROPIC_API_KEY` (direct Anthropic API) → `OPENROUTER_API_KEY` (OpenRouter, OpenAI-compatible — point it at any model via `OPENROUTER_MODEL`) → `claude -p` CLI fallback when no key is set
|
|
109
109
|
- **Lesson-learned indexing** -- `lesson_learned` field indexed in FTS5 with weight 8, making past debugging insights directly searchable
|
|
110
110
|
- **Cross-source normalization** -- `mem_search` normalizes scores across observations, sessions, and prompts before merging, preventing any source from dominating results
|
|
111
111
|
- **Exponential recency decay** -- Type-differentiated half-lives (decisions: 90d, discoveries: 60d, bugfixes: 14d, changes: 7d) consistently applied in all ranking paths
|
|
@@ -657,6 +657,9 @@ npm run benchmark:gate # CI gate: fails if metrics regress beyond 5% toleranc
|
|
|
657
657
|
|----------|-------------|---------|
|
|
658
658
|
| `CLAUDE_MEM_DIR` | Custom data directory. All databases, runtime files, and managed resources are stored here. | `~/.claude-mem-lite/` |
|
|
659
659
|
| `CLAUDE_MEM_MODEL` | LLM model for background calls (episode extraction, session summaries). Accepts `haiku` or `sonnet`. | `haiku` |
|
|
660
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key. When set, all background LLM calls go directly to the Anthropic Messages API (with prompt caching). Highest priority. | _(unset → CLI)_ |
|
|
661
|
+
| `OPENROUTER_API_KEY` | OpenRouter API key (OpenAI-compatible). Used for background LLM calls when `ANTHROPIC_API_KEY` is **not** set. If neither key is set, calls fall back to the `claude -p` CLI. | _(unset)_ |
|
|
662
|
+
| `OPENROUTER_MODEL` | Overrides the OpenRouter model slug for **all** background calls (e.g. `openai/gpt-4o-mini`, `qwen/qwen-2.5-72b-instruct`). When unset, the `CLAUDE_MEM_MODEL` tier maps to `anthropic/claude-haiku-4.5` (haiku) or `anthropic/claude-sonnet-4.5` (sonnet). | _(tier default)_ |
|
|
660
663
|
| `CLAUDE_MEM_DEBUG` | Enable debug logging (`1` to enable). | _(disabled)_ |
|
|
661
664
|
| `MEM_QUIET_HOOKS` | Low-noise hooks. `1` drops the `File Lessons` / `Key Context` sections from SessionStart injection, the lesson suffix from `[mem] Related memories`, and the `WHEN TO USE` / `Decision rules` blocks from MCP server instructions. IDs and the `Recent` table still surface so `mem_get(ids=[…])` remains reachable. Intended for users running the invited-memory adopt path or who otherwise want minimal auto-injection. **Since v2.82.0 this env no longer gates auto-adopt — use `MEM_NO_AUTO_ADOPT=1` for that.** | _(disabled)_ |
|
|
662
665
|
| `MEM_NO_AUTO_ADOPT` | Global opt-out for auto-adopt (v2.82.0+). `1` prevents the first-SessionStart auto-write of the invited-memory sentinel across **all** projects. For per-project opt-out use `claude-mem-lite adopt --disable` instead (writes a durable `<memdir>/.mem-no-auto-adopt` sentinel that survives marker deletion). | _(disabled)_ |
|
package/README.zh-CN.md
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
- **统一资源发现** -- 共享文件系统遍历层(`resource-discovery.mjs`),运行时扫描器和离线索引器共用,支持扁平目录、插件嵌套和松散 `.md` 文件
|
|
87
87
|
- **领域同义词扩展** -- 注册表搜索查询自动扩展领域同义词(如 "修复" → fix, debug, bugfix, repair, error)
|
|
88
88
|
- **持久化冷却机制** -- 5 分钟跨会话冷却 + 同会话去重,避免重复推荐 skill 自动加载
|
|
89
|
-
-
|
|
89
|
+
- **多 provider LLM 调用** -- provider 优先级 `ANTHROPIC_API_KEY`(直连 Anthropic API)→ `OPENROUTER_API_KEY`(OpenRouter,OpenAI 兼容,可用 `OPENROUTER_MODEL` 指向任意模型)→ 无 key 时回退 `claude -p` CLI
|
|
90
90
|
- **Haiku 熔断器** -- 连续 3 次 LLM 失败后,禁用 Haiku 调度 5 分钟,防止级联延迟
|
|
91
91
|
- **否定意图感知** -- 正确处理 "不要测试了,先修 bug" 等复杂提示,排除被否定的意图,支持中英文混合输入
|
|
92
92
|
- **可配置 LLM 模型** -- 通过 `CLAUDE_MEM_MODEL` 环境变量在 Haiku(快速/低成本)和 Sonnet(深度分析)之间切换
|
|
@@ -599,6 +599,9 @@ npm run benchmark:gate # CI 门控:指标回退超过 5% 容差时失败
|
|
|
599
599
|
|------|------|--------|
|
|
600
600
|
| `CLAUDE_MEM_DIR` | 自定义数据目录。所有数据库、运行时文件和托管资源均存储在此。 | `~/.claude-mem-lite/` |
|
|
601
601
|
| `CLAUDE_MEM_MODEL` | 后台 LLM 调用模型(Episode 提取、会话总结、调度)。可选 `haiku` 或 `sonnet`。 | `haiku` |
|
|
602
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key。设置后所有后台 LLM 调用直连 Anthropic Messages API(带 prompt caching),优先级最高。 | _(未设 → CLI)_ |
|
|
603
|
+
| `OPENROUTER_API_KEY` | OpenRouter API key(OpenAI 兼容)。当**未设** `ANTHROPIC_API_KEY` 时用于后台 LLM 调用;两者都未设则回退到 `claude -p` CLI。 | _(未设)_ |
|
|
604
|
+
| `OPENROUTER_MODEL` | 覆盖**所有**后台调用的 OpenRouter 模型 slug(如 `openai/gpt-4o-mini`、`qwen/qwen-2.5-72b-instruct`)。未设时按 `CLAUDE_MEM_MODEL` 分层映射到 `anthropic/claude-haiku-4.5`(haiku)或 `anthropic/claude-sonnet-4.5`(sonnet)。 | _(分层默认)_ |
|
|
602
605
|
| `CLAUDE_MEM_DEBUG` | 启用调试日志(设为 `1` 启用)。 | _(禁用)_ |
|
|
603
606
|
| `MEM_QUIET_HOOKS` | 低噪声 hook。设为 `1` 时,SessionStart 注入去掉 `File Lessons` / `Key Context` 两节,`[mem] Related memories` 去掉 lesson 后缀,MCP server instructions 去掉 `WHEN TO USE` / `Decision rules` 两段。ID 与 `Recent` 表仍保留,`mem_get(ids=[…])` 可继续展开细节。适用于启用了 invited-memory adopt 流程或偏好最小化自动注入的用户。**v2.82.0 起此 env 不再阻挡 auto-adopt——如需关闭 auto-adopt 用 `MEM_NO_AUTO_ADOPT=1`。** | _(禁用)_ |
|
|
604
607
|
| `MEM_NO_AUTO_ADOPT` | auto-adopt 全局关闭开关(v2.82.0+)。设为 `1` 阻止首次 SessionStart 在**所有**项目自动写入邀请式 memory 哨兵。项目级关闭走 `claude-mem-lite adopt --disable`(写 `<memdir>/.mem-no-auto-adopt` 哨兵,存活于 marker 删除)。 | _(禁用)_ |
|
package/haiku-client.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// claude-mem-lite: Unified LLM call wrapper
|
|
2
2
|
// Shared by memory (hook.mjs) and dispatch modules
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Provider priority: ANTHROPIC_API_KEY (direct Anthropic API) →
|
|
4
|
+
// OPENROUTER_API_KEY (OpenRouter, OpenAI-compatible) → claude CLI fallback
|
|
5
|
+
// Model configurable via CLAUDE_MEM_MODEL (haiku|sonnet); OpenRouter slug
|
|
6
|
+
// overridable via OPENROUTER_MODEL
|
|
5
7
|
|
|
6
8
|
import { execFileSync } from 'child_process';
|
|
7
9
|
import { readFileSync } from 'fs';
|
|
@@ -30,18 +32,47 @@ export function resolveModel() {
|
|
|
30
32
|
return { cli, api };
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
// OpenRouter uses its own slug namespace (OpenAI-compatible API). Map the
|
|
36
|
+
// project's haiku/sonnet tiers to the matching anthropic/* slugs so the quality
|
|
37
|
+
// tiering is preserved when routing through OpenRouter. Slugs verified against
|
|
38
|
+
// openrouter.ai (2026-06): claude-haiku-4.5 / claude-sonnet-4.5 mirror the
|
|
39
|
+
// native MODEL_MAP IDs above.
|
|
40
|
+
const OPENROUTER_MODEL_MAP = {
|
|
41
|
+
haiku: 'anthropic/claude-haiku-4.5',
|
|
42
|
+
sonnet: 'anthropic/claude-sonnet-4.5',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the OpenRouter model slug for a given tier.
|
|
47
|
+
* OPENROUTER_MODEL (if set, non-blank) overrides every tier with an explicit
|
|
48
|
+
* slug — this is how users point claude-mem-lite at any OpenRouter model
|
|
49
|
+
* (e.g. openai/gpt-4o-mini, qwen/...). Otherwise the tier maps to its default
|
|
50
|
+
* anthropic/* slug, falling back to the haiku slug for unknown tiers.
|
|
51
|
+
* @param {string} tier 'haiku' | 'sonnet'
|
|
52
|
+
* @returns {string} OpenRouter model slug
|
|
53
|
+
*/
|
|
54
|
+
export function resolveOpenRouterModel(tier) {
|
|
55
|
+
const override = (process.env.OPENROUTER_MODEL || '').trim();
|
|
56
|
+
if (override) return override;
|
|
57
|
+
return OPENROUTER_MODEL_MAP[tier] || OPENROUTER_MODEL_MAP.haiku;
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
// ─── Mode Detection ──────────────────────────────────────────────────────────
|
|
34
61
|
|
|
35
62
|
let _mode = null;
|
|
36
63
|
|
|
37
64
|
/**
|
|
38
|
-
* Detect
|
|
39
|
-
*
|
|
40
|
-
*
|
|
65
|
+
* Detect which provider to use for LLM calls. Priority (per user contract):
|
|
66
|
+
* ANTHROPIC_API_KEY → direct Anthropic API ('api', native, supports prompt
|
|
67
|
+
* caching), else OPENROUTER_API_KEY → OpenRouter ('openrouter', OpenAI-compat),
|
|
68
|
+
* else fall back to the `claude` CLI ('cli'). Cached after first call.
|
|
69
|
+
* @returns {'api'|'openrouter'|'cli'} The detected mode
|
|
41
70
|
*/
|
|
42
71
|
export function detectMode() {
|
|
43
72
|
if (_mode) return _mode;
|
|
44
|
-
|
|
73
|
+
if (process.env.ANTHROPIC_API_KEY) _mode = 'api';
|
|
74
|
+
else if (process.env.OPENROUTER_API_KEY) _mode = 'openrouter';
|
|
75
|
+
else _mode = 'cli';
|
|
45
76
|
const { cli } = resolveModel();
|
|
46
77
|
debugLog('DEBUG', 'haiku-client', `mode: ${_mode}, model: ${cli}`);
|
|
47
78
|
return _mode;
|
|
@@ -102,8 +133,9 @@ export function flattenForCLI(input) {
|
|
|
102
133
|
|
|
103
134
|
/**
|
|
104
135
|
* Call Haiku model with a prompt. Returns parsed text or null on failure.
|
|
105
|
-
*
|
|
106
|
-
*
|
|
136
|
+
* Provider priority ANTHROPIC_API_KEY → OPENROUTER_API_KEY → CLI; if the keyed
|
|
137
|
+
* provider call fails (HTTP error / network throw / empty), degrades to the
|
|
138
|
+
* `claude -p` CLI. Never throws — returns null only when every path fails.
|
|
107
139
|
*
|
|
108
140
|
* @param {string|{system?: string, user: string}} prompt Prompt text, or split form
|
|
109
141
|
* @param {object} [opts] Options
|
|
@@ -116,15 +148,28 @@ export async function callHaiku(prompt, { timeout = 10000, maxTokens = 500 } = {
|
|
|
116
148
|
|
|
117
149
|
const mode = detectMode();
|
|
118
150
|
|
|
151
|
+
// CLI is terminal — no provider to fall back to.
|
|
152
|
+
if (mode === 'cli') {
|
|
153
|
+
try { return callHaikuCLI(prompt, { timeout }); }
|
|
154
|
+
catch (e) { debugCatch(e, 'callHaiku'); return null; }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Keyed provider (api/openrouter): attempt it, then degrade to the CLI on any
|
|
158
|
+
// failure (HTTP error → null, or network/timeout throw). A region-blocked or
|
|
159
|
+
// out-of-credit key must not silently drop background summaries.
|
|
160
|
+
let primary = null;
|
|
119
161
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return callHaikuCLI(prompt, { timeout });
|
|
162
|
+
primary = mode === 'api'
|
|
163
|
+
? await callHaikuAPI(prompt, { timeout, maxTokens })
|
|
164
|
+
: await callOpenRouterAPI(prompt, resolveModel().cli, { timeout, maxTokens });
|
|
124
165
|
} catch (e) {
|
|
125
|
-
debugCatch(e,
|
|
126
|
-
return null;
|
|
166
|
+
debugCatch(e, `callHaiku:${mode}`);
|
|
127
167
|
}
|
|
168
|
+
if (primary) return primary;
|
|
169
|
+
|
|
170
|
+
debugLog('WARN', 'haiku-client', `${mode} call failed, falling back to claude CLI`);
|
|
171
|
+
try { return callHaikuCLI(prompt, { timeout }); }
|
|
172
|
+
catch (e) { debugCatch(e, 'callHaiku:cli-fallback'); return null; }
|
|
128
173
|
}
|
|
129
174
|
|
|
130
175
|
/**
|
|
@@ -143,8 +188,8 @@ export async function callHaikuJSON(prompt, opts) {
|
|
|
143
188
|
|
|
144
189
|
/**
|
|
145
190
|
* Call LLM with explicit model selection. Supports 'haiku' and 'sonnet'.
|
|
146
|
-
*
|
|
147
|
-
* Never throws — returns null
|
|
191
|
+
* Same provider priority + failure fallback to CLI as callHaiku.
|
|
192
|
+
* Never throws — returns null only when every path fails.
|
|
148
193
|
*
|
|
149
194
|
* @param {string} prompt The prompt text
|
|
150
195
|
* @param {'haiku'|'sonnet'} model Model to use (default: 'haiku')
|
|
@@ -158,15 +203,27 @@ export async function callLLMWithModel(prompt, model = 'haiku', { timeout = 1500
|
|
|
158
203
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
159
204
|
const mode = detectMode();
|
|
160
205
|
|
|
206
|
+
// CLI is terminal — no provider to fall back to.
|
|
207
|
+
if (mode === 'cli') {
|
|
208
|
+
try { return callModelCLI(prompt, resolvedModel, { timeout }); }
|
|
209
|
+
catch (e) { debugCatch(e, `callLLMWithModel:${resolvedModel}`); return null; }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Keyed provider (api/openrouter): attempt it, then degrade to the CLI on any
|
|
213
|
+
// failure so a region-blocked / out-of-credit key still produces output.
|
|
214
|
+
let primary = null;
|
|
161
215
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return callModelCLI(prompt, resolvedModel, { timeout });
|
|
216
|
+
primary = mode === 'api'
|
|
217
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens })
|
|
218
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens });
|
|
166
219
|
} catch (e) {
|
|
167
|
-
debugCatch(e, `callLLMWithModel:${resolvedModel}`);
|
|
168
|
-
return null;
|
|
220
|
+
debugCatch(e, `callLLMWithModel:${mode}:${resolvedModel}`);
|
|
169
221
|
}
|
|
222
|
+
if (primary) return primary;
|
|
223
|
+
|
|
224
|
+
debugLog('WARN', 'haiku-client', `${mode} call failed, falling back to claude CLI (${resolvedModel})`);
|
|
225
|
+
try { return callModelCLI(prompt, resolvedModel, { timeout }); }
|
|
226
|
+
catch (e) { debugCatch(e, `callLLMWithModel:cli-fallback:${resolvedModel}`); return null; }
|
|
170
227
|
}
|
|
171
228
|
|
|
172
229
|
/**
|
|
@@ -299,6 +356,54 @@ async function callHaikuAPI(prompt, { timeout, maxTokens }) {
|
|
|
299
356
|
}
|
|
300
357
|
}
|
|
301
358
|
|
|
359
|
+
// ─── OpenRouter Mode ─────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
// OpenRouter exposes an OpenAI-compatible chat-completions API (NOT the
|
|
362
|
+
// Anthropic Messages format), so the request/response shapes differ from
|
|
363
|
+
// callHaikuAPI/callModelAPI: Bearer auth, `messages` with a system-role entry,
|
|
364
|
+
// and the reply lives at choices[0].message.content. Anthropic's prompt-cache
|
|
365
|
+
// `cache_control` field has no OpenAI-format equivalent and is omitted.
|
|
366
|
+
// `tier` is the resolved model tier ('haiku'|'sonnet'); OPENROUTER_MODEL can
|
|
367
|
+
// override the resulting slug entirely (see resolveOpenRouterModel).
|
|
368
|
+
async function callOpenRouterAPI(prompt, tier, { timeout, maxTokens }) {
|
|
369
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
370
|
+
if (!apiKey) return null;
|
|
371
|
+
|
|
372
|
+
const model = resolveOpenRouterModel(tier);
|
|
373
|
+
const controller = new AbortController();
|
|
374
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const { system, user } = splitPrompt(prompt);
|
|
378
|
+
const messages = [];
|
|
379
|
+
if (system) messages.push({ role: 'system', content: system });
|
|
380
|
+
messages.push({ role: 'user', content: user });
|
|
381
|
+
|
|
382
|
+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: {
|
|
385
|
+
'Content-Type': 'application/json',
|
|
386
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
387
|
+
// Optional OpenRouter attribution headers (ignored by the API if absent).
|
|
388
|
+
'X-Title': 'claude-mem-lite',
|
|
389
|
+
},
|
|
390
|
+
body: JSON.stringify({ model, max_tokens: maxTokens, messages }),
|
|
391
|
+
signal: controller.signal,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (!res.ok) {
|
|
395
|
+
debugLog('WARN', `${tier}-openrouter`, `HTTP ${res.status}`);
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const data = await res.json();
|
|
400
|
+
const text = data.choices?.[0]?.message?.content;
|
|
401
|
+
return text ? { text } : null;
|
|
402
|
+
} finally {
|
|
403
|
+
clearTimeout(timer);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
302
407
|
// ─── CLI Mode ────────────────────────────────────────────────────────────────
|
|
303
408
|
|
|
304
409
|
function callHaikuCLI(prompt, { timeout }) {
|
package/hook-llm.mjs
CHANGED
|
@@ -674,7 +674,7 @@ ${actionList}`;
|
|
|
674
674
|
if (gotSlot) {
|
|
675
675
|
let raw, parsed;
|
|
676
676
|
try {
|
|
677
|
-
raw = callLLM(prompt);
|
|
677
|
+
raw = await callLLM(prompt);
|
|
678
678
|
parsed = parseJsonFromLLM(raw);
|
|
679
679
|
} finally {
|
|
680
680
|
releaseLLMSlot();
|
|
@@ -721,7 +721,7 @@ ${actionList}`;
|
|
|
721
721
|
retryAttempted = true;
|
|
722
722
|
try {
|
|
723
723
|
const retryPrompt = buildLessonRetryPrompt(episode, parsed);
|
|
724
|
-
const retryRaw = callLLM(retryPrompt, 10000);
|
|
724
|
+
const retryRaw = await callLLM(retryPrompt, 10000);
|
|
725
725
|
if (retryRaw) {
|
|
726
726
|
const retry = parseJsonFromLLM(retryRaw);
|
|
727
727
|
const retryLesson = typeof retry?.lesson === 'string' ? retry.lesson.trim() : '';
|
|
@@ -974,7 +974,7 @@ ${obsList}`;
|
|
|
974
974
|
|
|
975
975
|
let raw, llmParsed;
|
|
976
976
|
try {
|
|
977
|
-
raw = callLLM(prompt, 20000);
|
|
977
|
+
raw = await callLLM(prompt, 20000);
|
|
978
978
|
llmParsed = parseJsonFromLLM(raw);
|
|
979
979
|
} finally {
|
|
980
980
|
releaseLLMSlot();
|
package/hook-shared.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { join } from 'path';
|
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
8
8
|
import { inferProject, debugCatch } from './utils.mjs';
|
|
9
9
|
import { ensureDb, DB_DIR } from './schema.mjs';
|
|
10
|
-
import { getClaudePath as getClaudePathShared, resolveModel as resolveModelShared, flattenForCLI as _flattenForCLI } from './haiku-client.mjs';
|
|
10
|
+
import { getClaudePath as getClaudePathShared, resolveModel as resolveModelShared, flattenForCLI as _flattenForCLI, detectMode as detectLLMMode, callHaiku } from './haiku-client.mjs';
|
|
11
11
|
// Phase D: invited-memory sentinel detection. memdir.mjs only pulls in fs/path/os/crypto;
|
|
12
12
|
// adopt-content.mjs is pure strings. No circular deps — memdir doesn't import hook-shared.
|
|
13
13
|
import { memdirPath as _memdirPath, isAdopted as _isAdopted } from './memdir.mjs';
|
|
@@ -130,13 +130,26 @@ export function openDb() {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// ─── LLM
|
|
133
|
+
// ─── LLM (provider-routed: Anthropic API → OpenRouter → claude CLI) ─────────
|
|
134
134
|
|
|
135
135
|
// Accepts either a plain string (legacy) or {system, user} (defense-in-depth
|
|
136
136
|
// against prompt injection from poisoned user_prompts content — cso F#4 fix).
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
137
|
+
// Provider priority mirrors haiku-client (ANTHROPIC_API_KEY > OPENROUTER_API_KEY
|
|
138
|
+
// > CLI): when a key is present, delegate to callHaiku — it owns the Anthropic
|
|
139
|
+
// Messages / OpenRouter chat-completions request shapes, uses the system role
|
|
140
|
+
// natively, AND degrades to the `claude -p` CLI internally if the keyed provider
|
|
141
|
+
// fails (so a region-blocked / out-of-credit key still yields a summary). The
|
|
142
|
+
// keyless case shells out to `claude -p` directly here, where flattenForCLI
|
|
143
|
+
// renders {system, user} with an explicit data-boundary marker. Returns the raw
|
|
144
|
+
// response string (callers run parseJsonFromLLM themselves) or null.
|
|
145
|
+
// maxTokens is sized for session-summary / episode JSON (larger than the
|
|
146
|
+
// registry/optimize callers' budgets).
|
|
147
|
+
export async function callLLM(prompt, timeoutMs = 15000) {
|
|
148
|
+
if (detectLLMMode() !== 'cli') {
|
|
149
|
+
const result = await callHaiku(prompt, { timeout: timeoutMs, maxTokens: 2000 });
|
|
150
|
+
return result?.text ?? null;
|
|
151
|
+
}
|
|
152
|
+
|
|
140
153
|
const { cli: modelName } = resolveModelShared();
|
|
141
154
|
try {
|
|
142
155
|
const result = execFileSync(getClaudePathShared(), ['-p', '--model', modelName], {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.87.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. Alternative to claude-mem with 600x lower cost.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/schema.mjs
CHANGED
|
@@ -49,7 +49,12 @@ export const REGISTRY_DB_PATH = join(DB_DIR, 'resource-registry.db');
|
|
|
49
49
|
// cited_count, last_decided_session_id. Stop hook resolves injected obs as
|
|
50
50
|
// cited|uncited; 3 consecutive uncited → importance -1 (floor 0); 1 cited → +1
|
|
51
51
|
// (cap 3). last_decided_session_id makes Stop idempotent across multi-fire.
|
|
52
|
-
|
|
52
|
+
// v35 (v2.87.0): no DDL — version bumped only to force one full migration pass on
|
|
53
|
+
// existing DBs, which runs the one-shot observation_files orphan cleanup (and
|
|
54
|
+
// re-runs the v28 observation_vectors cleanup) to clear the backlog leaked while
|
|
55
|
+
// the warm-start fast-path left foreign_keys OFF. LATEST_MIGRATION_COLUMN is
|
|
56
|
+
// unchanged (no new column) — decay_seen_count still exists at v35.
|
|
57
|
+
export const CURRENT_SCHEMA_VERSION = 35;
|
|
53
58
|
|
|
54
59
|
// Sentinel column for the LATEST migration set. The fast-path uses this to
|
|
55
60
|
// self-heal half-migrated DBs — schema_version bumped but column ALTERs rolled
|
|
@@ -212,7 +217,16 @@ export function initSchema(db) {
|
|
|
212
217
|
// Self-heal: version-row says CURRENT but latest migration column may be
|
|
213
218
|
// absent (rolled back / never applied). Fall through to migration apply
|
|
214
219
|
// when the sentinel is missing — duplicates are caught in the loop.
|
|
215
|
-
if (row.version === CURRENT_SCHEMA_VERSION && hasLatestMigrationColumn(db))
|
|
220
|
+
if (row.version === CURRENT_SCHEMA_VERSION && hasLatestMigrationColumn(db)) {
|
|
221
|
+
// Warm-start post-condition: ensureDb() opened this handle with
|
|
222
|
+
// foreign_keys=OFF (early migrations require cascade disabled). The full
|
|
223
|
+
// migration path re-enables it at the end; this fast-path return must
|
|
224
|
+
// match that post-condition, else every DELETE on the returned handle
|
|
225
|
+
// skips ON DELETE CASCADE and silently orphans junction rows. Safe here:
|
|
226
|
+
// no transaction is open yet (BEGIN IMMEDIATE is below).
|
|
227
|
+
db.pragma('foreign_keys = ON');
|
|
228
|
+
return db;
|
|
229
|
+
}
|
|
216
230
|
if (row.version > CURRENT_SCHEMA_VERSION) {
|
|
217
231
|
throw new Error(
|
|
218
232
|
`DB schema is v${row.version} but this claude-mem-lite binary supports up to v${CURRENT_SCHEMA_VERSION}. ` +
|
|
@@ -237,6 +251,10 @@ export function initSchema(db) {
|
|
|
237
251
|
const underlock = db.prepare('SELECT version FROM schema_version LIMIT 1').get();
|
|
238
252
|
if (underlock && underlock.version === CURRENT_SCHEMA_VERSION && hasLatestMigrationColumn(db)) {
|
|
239
253
|
db.exec('COMMIT');
|
|
254
|
+
// COMMIT closed the transaction, so this PRAGMA takes effect (no-op inside a txn).
|
|
255
|
+
// Same FK post-condition as the fast-path above: a peer completed init while we
|
|
256
|
+
// were blocked, so we skip migrations and must still restore cascade enforcement.
|
|
257
|
+
db.pragma('foreign_keys = ON');
|
|
240
258
|
return db;
|
|
241
259
|
}
|
|
242
260
|
} catch { /* table absent — proceed */ }
|
|
@@ -472,6 +490,19 @@ export function initSchema(db) {
|
|
|
472
490
|
}
|
|
473
491
|
} catch { /* non-critical — migration can retry on next open */ }
|
|
474
492
|
|
|
493
|
+
// v35 (v2.87.0) P1: one-shot cleanup of orphaned observation_files. Same root
|
|
494
|
+
// cause as the v28 observation_vectors cleanup below — until the warm-start
|
|
495
|
+
// fast-path FK fix (initSchema early returns now restore foreign_keys=ON),
|
|
496
|
+
// ensureDb() handles ran with FK OFF, so ON DELETE CASCADE never fired and
|
|
497
|
+
// junction rows leaked (live DB: 6440/9569 = 67% orphans). Idempotent (NOT IN
|
|
498
|
+
// is empty on a clean DB); runs once per version bump via the fast-path gate.
|
|
499
|
+
try {
|
|
500
|
+
db.prepare(`
|
|
501
|
+
DELETE FROM observation_files
|
|
502
|
+
WHERE obs_id NOT IN (SELECT id FROM observations)
|
|
503
|
+
`).run();
|
|
504
|
+
} catch { /* non-critical — table-missing path handled by earlier CREATE */ }
|
|
505
|
+
|
|
475
506
|
// Observation vectors table for TF-IDF vector search
|
|
476
507
|
db.exec(`
|
|
477
508
|
CREATE TABLE IF NOT EXISTS observation_vectors (
|