claude-mem-lite 6.25.3 → 6.26.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 +2 -2
- package/README.zh-CN.md +2 -2
- package/deep-search.mjs +18 -1
- package/haiku-client.mjs +96 -27
- package/hook-llm.mjs +59 -44
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.
|
|
12
|
+
"version": "6.26.0",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.26.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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/README.md
CHANGED
|
@@ -1134,10 +1134,10 @@ claude-mem-lite.
|
|
|
1134
1134
|
| `CLAUDE_MEM_MODEL` | LLM model for background calls (episode extraction, session summaries). Accepts `haiku` or `sonnet`. | `haiku` |
|
|
1135
1135
|
| `ANTHROPIC_API_KEY` | Anthropic API key. When set, all background LLM calls go directly to the Anthropic Messages API (with prompt caching) - or to the `ANTHROPIC_BASE_URL` gateway when that is set. Highest priority. | _(unset → CLI)_ |
|
|
1136
1136
|
| `ANTHROPIC_BASE_URL` | Base URL for the direct Messages API when an Anthropic-compatible gateway serves the models (Azure AI Foundry, LiteLLM, Bedrock/Vertex proxies). No `/v1` suffix — the endpoint path is appended. The `claude -p` fallback reads the same variable, so one value covers both transports. Plain `http://` is accepted only for a loopback host; anything else would send the API key unencrypted. That request always goes straight to the loopback host, even when `NODE_USE_ENV_PROXY=1` sends other requests through `HTTP_PROXY`. A value that is set but unusable skips the direct API leg, so background calls fall back to the `claude -p` CLI, and doctor warns. | `https://api.anthropic.com` |
|
|
1137
|
-
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Model ID or gateway deployment name for the `haiku` tier, honored on the direct API **only when `ANTHROPIC_BASE_URL` is set**. Without a base URL it stays CLI-only, so the Bedrock/Vertex form of this alias is never sent to `api.anthropic.com`. Use it when the gateway routes on deployment names rather than Anthropic model IDs (Azure Foundry deployments). The `claude -p` fallback resolves its `--model haiku` alias through it too. | built-in `claude-haiku-
|
|
1137
|
+
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Model ID or gateway deployment name for the `haiku` tier, honored on the direct API **only when `ANTHROPIC_BASE_URL` is set**. Without a base URL it stays CLI-only, so the Bedrock/Vertex form of this alias is never sent to `api.anthropic.com`. Use it when the gateway routes on deployment names rather than Anthropic model IDs (Azure Foundry deployments). The `claude -p` fallback resolves its `--model haiku` alias through it too. | built-in `claude-haiku-5-5` |
|
|
1138
1138
|
| `ANTHROPIC_DEFAULT_SONNET_MODEL` | Same as above for the `sonnet` tier. | built-in `claude-sonnet-4-5-…` |
|
|
1139
1139
|
| `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)_ |
|
|
1140
|
-
| `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-
|
|
1140
|
+
| `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-5.5` (haiku) or `anthropic/claude-sonnet-4.5` (sonnet). | _(tier default)_ |
|
|
1141
1141
|
| `CLAUDE_MEM_DEBUG` | Enable debug logging (`1` to enable). | _(disabled)_ |
|
|
1142
1142
|
| `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)_ |
|
|
1143
1143
|
| `CLAUDE_MEM_SESSION_EVENTS` | `1`/`on` restores the SessionStart `### Key Events` section (recent high-importance rows from the `events` table). **Off by default since v6.13.0**: an audit of 30 events read 2 accurate and 16 wrong. The UserPromptSubmit events block and PreToolUse recall are query-matched and stay on; `mem_search` still reaches every event. | _(off)_ |
|
package/README.zh-CN.md
CHANGED
|
@@ -863,10 +863,10 @@ npm run benchmark:gate # CI 门控:指标回退超过 5% 容差时失败
|
|
|
863
863
|
| `CLAUDE_MEM_MODEL` | 后台 LLM 调用模型(Episode 提取、会话总结、调度)。可选 `haiku` 或 `sonnet`。 | `haiku` |
|
|
864
864
|
| `ANTHROPIC_API_KEY` | Anthropic API key。设置后所有后台 LLM 调用直连 Anthropic Messages API(带 prompt caching);设了 `ANTHROPIC_BASE_URL` 时改发往那个网关。优先级最高。 | _(未设 → CLI)_ |
|
|
865
865
|
| `ANTHROPIC_BASE_URL` | 由兼容 Anthropic 的网关(Azure AI Foundry、LiteLLM、Bedrock/Vertex 代理)提供模型时,直连 Messages API 使用的基础 URL。不要带 `/v1` 后缀,端点路径会自动拼上。`claude -p` 兜底读取同一个变量,所以一个值同时作用于两条路径。明文 `http://` 只接受回环主机,否则 API key 会以明文发送。这个请求总是直连回环主机,即使 `NODE_USE_ENV_PROXY=1` 让其他请求走 `HTTP_PROXY`。设了但不可用的值会跳过直连这一路,后台调用改走 `claude -p`,doctor 会提示。 | `https://api.anthropic.com` |
|
|
866
|
-
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `haiku` 档的模型 ID 或网关部署名,**仅在设了 `ANTHROPIC_BASE_URL` 时**用于直连 API。没有基础 URL 时只给 CLI 用,所以这个别名的 Bedrock/Vertex 写法不会被发往 `api.anthropic.com`。网关按部署名而不是 Anthropic 模型 ID 路由时(Azure Foundry 部署)使用。`claude -p` 兜底的 `--model haiku` 别名也经由它解析。 | 内置 `claude-haiku-
|
|
866
|
+
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `haiku` 档的模型 ID 或网关部署名,**仅在设了 `ANTHROPIC_BASE_URL` 时**用于直连 API。没有基础 URL 时只给 CLI 用,所以这个别名的 Bedrock/Vertex 写法不会被发往 `api.anthropic.com`。网关按部署名而不是 Anthropic 模型 ID 路由时(Azure Foundry 部署)使用。`claude -p` 兜底的 `--model haiku` 别名也经由它解析。 | 内置 `claude-haiku-5-5` |
|
|
867
867
|
| `ANTHROPIC_DEFAULT_SONNET_MODEL` | 同上,用于 `sonnet` 档。 | 内置 `claude-sonnet-4-5-…` |
|
|
868
868
|
| `OPENROUTER_API_KEY` | OpenRouter API key(OpenAI 兼容)。当**未设** `ANTHROPIC_API_KEY` 时用于后台 LLM 调用;两者都未设则回退到 `claude -p` CLI。 | _(未设)_ |
|
|
869
|
-
| `OPENROUTER_MODEL` | 覆盖**所有**后台调用的 OpenRouter 模型 slug(如 `openai/gpt-4o-mini`、`qwen/qwen-2.5-72b-instruct`)。未设时按 `CLAUDE_MEM_MODEL` 分层映射到 `anthropic/claude-haiku-
|
|
869
|
+
| `OPENROUTER_MODEL` | 覆盖**所有**后台调用的 OpenRouter 模型 slug(如 `openai/gpt-4o-mini`、`qwen/qwen-2.5-72b-instruct`)。未设时按 `CLAUDE_MEM_MODEL` 分层映射到 `anthropic/claude-haiku-5.5`(haiku)或 `anthropic/claude-sonnet-4.5`(sonnet)。 | _(分层默认)_ |
|
|
870
870
|
| `CLAUDE_MEM_DEBUG` | 启用调试日志(设为 `1` 启用)。 | _(禁用)_ |
|
|
871
871
|
| `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`。** | _(禁用)_ |
|
|
872
872
|
| `CLAUDE_MEM_SESSION_EVENTS` | 设为 `1`/`on` 时恢复 SessionStart 的 `### Key Events` 一节(`events` 表中最近的高重要度条目)。**v6.13.0 起默认关闭**:对 30 条 event 的核验只有 2 条属实、16 条错误。UserPromptSubmit 的 events 块与 PreToolUse 召回按查询匹配,保持开启;`mem_search` 仍可检索全部 event。 | _(关闭)_ |
|
package/deep-search.mjs
CHANGED
|
@@ -269,6 +269,23 @@ export const REWRITE_SYSTEM =
|
|
|
269
269
|
'Emit exactly 3 non-empty variants. If unsure, still emit at least the keyword form as v1.\n' +
|
|
270
270
|
INJECTION_GUARD;
|
|
271
271
|
|
|
272
|
+
/**
|
|
273
|
+
* The rewrite's output as a structured-output schema. Sent only to models that get the
|
|
274
|
+
* strict body (haiku-client's STRICT_BODY_MODELS / _SLUGS): with thinking off, Claude
|
|
275
|
+
* Haiku 5.5's answer gave fewer than two variants on 7 of 72 replays (the ones printed were
|
|
276
|
+
* prose questions about a bare one-word query, "React"), and on 0 of 72 with this schema
|
|
277
|
+
* (2026-10-15, docs/audits/20261015-v6.26.0-replays.md).
|
|
278
|
+
*/
|
|
279
|
+
export const REWRITE_JSON_SCHEMA = {
|
|
280
|
+
name: 'rewrite_variants',
|
|
281
|
+
schema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: { variants: { type: 'array', items: { type: 'string' } } },
|
|
284
|
+
required: ['variants'],
|
|
285
|
+
additionalProperties: false,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
272
289
|
/**
|
|
273
290
|
* Build the split-form rewrite prompt. The constant instructions live in the
|
|
274
291
|
* system slot; the untrusted query goes verbatim into the user/data slot so an
|
|
@@ -372,7 +389,7 @@ export function makeThrottled(llm, { intervalMs = AUTO_DEEP_THROTTLE_MS } = {})
|
|
|
372
389
|
// never load the LLM client.
|
|
373
390
|
async function callRewriteLLM(prompt, { timeout }) {
|
|
374
391
|
const { callModelJSONAsync } = await import('./haiku-client.mjs');
|
|
375
|
-
return callModelJSONAsync(prompt, 'haiku', { timeout, maxTokens: 400 });
|
|
392
|
+
return callModelJSONAsync(prompt, 'haiku', { timeout, maxTokens: 400, jsonSchema: REWRITE_JSON_SCHEMA });
|
|
376
393
|
}
|
|
377
394
|
|
|
378
395
|
// Default (explicit deep=true) provider: patient timeout, no throttle/cache.
|
package/haiku-client.mjs
CHANGED
|
@@ -55,11 +55,30 @@ function cliSpawnCwd() {
|
|
|
55
55
|
// ─── Model Resolution ────────────────────────────────────────────────────────
|
|
56
56
|
|
|
57
57
|
// CLI name → API model ID mapping
|
|
58
|
+
// haiku moved from claude-haiku-4-5-20251001 on 2026-10-15, after
|
|
59
|
+
// benchmark/haiku-model-ab.mjs read usability parity on the three haiku-tier sites it
|
|
60
|
+
// samples (episode, rewrite, rerank) and a blind read of the kept lessons put 5.5 ahead
|
|
61
|
+
// (docs/measurement/rulers.md, "haiku-tier model A/B"). It is also what
|
|
62
|
+
// `claude -p --model haiku` resolves to on the Anthropic API since Claude Code 2.1.293
|
|
63
|
+
// (Bedrock, Vertex, Foundry and Claude Platform on AWS still resolve it to 4.5), so there
|
|
64
|
+
// all three legs run the same model.
|
|
58
65
|
const MODEL_MAP = {
|
|
59
|
-
haiku: 'claude-haiku-
|
|
66
|
+
haiku: 'claude-haiku-5-5',
|
|
60
67
|
sonnet: 'claude-sonnet-4-5-20250929',
|
|
61
68
|
};
|
|
62
69
|
|
|
70
|
+
// Models that get the STRICT body. Claude Haiku 5.5 answers any non-default `temperature`
|
|
71
|
+
// with a 400 and thinks unless told not to, so a reply can open with a thinking block, and
|
|
72
|
+
// thinking spends the max_tokens these short fixed-schema extractions are sized for. Every
|
|
73
|
+
// leg runs it with thinking off: the CLI sets MAX_THINKING_TOKENS=0, OpenRouter gets
|
|
74
|
+
// `reasoning: {enabled: false}` (STRICT_BODY_SLUGS), and the direct leg sends no sampling
|
|
75
|
+
// field and thinking disabled. With thinking off it can answer a bare one-word query with a
|
|
76
|
+
// prose question instead of JSON, so a caller's `jsonSchema` becomes a structured output on
|
|
77
|
+
// these models (rewrite replays 2026-10-15: 7/72 below two variants without it, 0/72 with
|
|
78
|
+
// it; docs/audits/20261015-v6.26.0-replays.md). Keyed by the ID actually sent, so an
|
|
79
|
+
// override naming another model keeps the old body.
|
|
80
|
+
const STRICT_BODY_MODELS = new Set(['claude-haiku-5-5']);
|
|
81
|
+
|
|
63
82
|
// Claude Code's tier-alias override vars, honored on the direct-API leg too:
|
|
64
83
|
// gateway deployments route on the DEPLOYMENT name (Azure Foundry rejects the
|
|
65
84
|
// Anthropic model ID when it differs), and one env set then covers both the
|
|
@@ -94,6 +113,9 @@ function apiModelId(tier, base = resolveAnthropicBaseUrl()) {
|
|
|
94
113
|
// to 0 so the provider default (~1.0) doesn't inject wording variance that breaks
|
|
95
114
|
// JSON parsing or defeats the wording-sensitive MinHash near-duplicate detector.
|
|
96
115
|
// A call that genuinely needs sampling can pass opts.temperature to override.
|
|
116
|
+
// The pin holds only where the model takes it: Claude Haiku 5.5 accepts no temperature
|
|
117
|
+
// but its default (the direct leg omits the field, OpenRouter drops it, the CLI never sent
|
|
118
|
+
// one), so on that model the extraction runs at its default sampling.
|
|
97
119
|
const DEFAULT_LLM_TEMPERATURE = 0;
|
|
98
120
|
|
|
99
121
|
/**
|
|
@@ -134,13 +156,21 @@ export function resolveModel() {
|
|
|
134
156
|
// OpenRouter uses its own slug namespace (OpenAI-compatible API). Map the
|
|
135
157
|
// project's haiku/sonnet tiers to the matching anthropic/* slugs so the quality
|
|
136
158
|
// tiering is preserved when routing through OpenRouter. Slugs verified against
|
|
137
|
-
// openrouter.ai (2026-06)
|
|
138
|
-
// native MODEL_MAP IDs above.
|
|
159
|
+
// openrouter.ai: claude-sonnet-4.5 (2026-06) and claude-haiku-5.5 (2026-10-15) mirror
|
|
160
|
+
// the native MODEL_MAP IDs above.
|
|
139
161
|
const OPENROUTER_MODEL_MAP = {
|
|
140
|
-
haiku: 'anthropic/claude-haiku-
|
|
162
|
+
haiku: 'anthropic/claude-haiku-5.5',
|
|
141
163
|
sonnet: 'anthropic/claude-sonnet-4.5',
|
|
142
164
|
};
|
|
143
165
|
|
|
166
|
+
// STRICT_BODY_MODELS on OpenRouter's slug namespace. Measured 2026-10-15 on six rerank
|
|
167
|
+
// prompts, three of them picked because they had failed, three calls each (300-token
|
|
168
|
+
// budget): the JSON came back cut off (finish_reason "length"; the two printed spent 259 and
|
|
169
|
+
// 275 tokens thinking) on 8 of 18 calls, 0 of 18 with reasoning off
|
|
170
|
+
// (docs/audits/20261015-v6.26.0-replays.md). A one-line extraction did not think at all, so
|
|
171
|
+
// a single easy probe reads this as fine.
|
|
172
|
+
const STRICT_BODY_SLUGS = new Set(['anthropic/claude-haiku-5.5']);
|
|
173
|
+
|
|
144
174
|
/**
|
|
145
175
|
* Resolve the OpenRouter model slug for a given tier.
|
|
146
176
|
* OPENROUTER_MODEL (if set, non-blank) overrides every tier with an explicit
|
|
@@ -242,11 +272,13 @@ export function flattenForCLI(input) {
|
|
|
242
272
|
* @param {object} [opts] Options
|
|
243
273
|
* @param {number} [opts.timeout=10000] Timeout in milliseconds
|
|
244
274
|
* @param {number} [opts.maxTokens=500] Max tokens in response
|
|
275
|
+
* @param {{name: string, schema: object}|null} [opts.jsonSchema] structured output for the
|
|
276
|
+
* models that get the strict body (STRICT_BODY_MODELS / _SLUGS); ignored elsewhere and on the CLI
|
|
245
277
|
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
246
278
|
*/
|
|
247
279
|
export async function callHaiku(
|
|
248
280
|
prompt,
|
|
249
|
-
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
281
|
+
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
250
282
|
) {
|
|
251
283
|
if (!prompt) return null;
|
|
252
284
|
|
|
@@ -275,8 +307,13 @@ export async function callHaiku(
|
|
|
275
307
|
// the difference between 1.4s and 13.5s.
|
|
276
308
|
primary =
|
|
277
309
|
mode === 'api'
|
|
278
|
-
? await callModelAPI(prompt, resolveModel().cli, { timeout, maxTokens, temperature })
|
|
279
|
-
: await callOpenRouterAPI(prompt, resolveModel().cli, {
|
|
310
|
+
? await callModelAPI(prompt, resolveModel().cli, { timeout, maxTokens, temperature, jsonSchema })
|
|
311
|
+
: await callOpenRouterAPI(prompt, resolveModel().cli, {
|
|
312
|
+
timeout,
|
|
313
|
+
maxTokens,
|
|
314
|
+
temperature,
|
|
315
|
+
jsonSchema,
|
|
316
|
+
});
|
|
280
317
|
} catch (e) {
|
|
281
318
|
debugCatch(e, `callHaiku:${mode}`);
|
|
282
319
|
}
|
|
@@ -323,14 +360,15 @@ export async function callHaikuJSON(prompt, opts) {
|
|
|
323
360
|
* Defaults also mirror callHaiku (10s / 500 tokens), not callModelJSONAsync's
|
|
324
361
|
* 15s / 1000: a caller that omits opts must get the sync twin's budget.
|
|
325
362
|
* @param {string|{system?:string,user:string}} prompt
|
|
326
|
-
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
363
|
+
* @param {{timeout?:number,maxTokens?:number,temperature?:number,jsonSchema?:{name:string,schema:object}|null}} [opts]
|
|
364
|
+
* jsonSchema: structured output for strict-body models (see callLLMWithModel)
|
|
327
365
|
* @returns {Promise<object|null>} Parsed JSON or null
|
|
328
366
|
*/
|
|
329
367
|
export async function callHaikuJSONAsync(
|
|
330
368
|
prompt,
|
|
331
|
-
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
369
|
+
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
332
370
|
) {
|
|
333
|
-
return callModelJSONAsync(prompt, resolveModel().cli, { timeout, maxTokens, temperature });
|
|
371
|
+
return callModelJSONAsync(prompt, resolveModel().cli, { timeout, maxTokens, temperature, jsonSchema });
|
|
334
372
|
}
|
|
335
373
|
|
|
336
374
|
// ─── Model-Selectable API ────────────────────────────────────────────────────
|
|
@@ -345,12 +383,14 @@ export async function callHaikuJSONAsync(
|
|
|
345
383
|
* @param {object} [opts] Options
|
|
346
384
|
* @param {number} [opts.timeout=15000] Timeout in milliseconds
|
|
347
385
|
* @param {number} [opts.maxTokens=1000] Max tokens in response
|
|
386
|
+
* @param {{name: string, schema: object}|null} [opts.jsonSchema] structured output for the
|
|
387
|
+
* models that get the strict body (STRICT_BODY_MODELS / _SLUGS); ignored elsewhere and on the CLI
|
|
348
388
|
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
349
389
|
*/
|
|
350
390
|
export async function callLLMWithModel(
|
|
351
391
|
prompt,
|
|
352
392
|
model = 'haiku',
|
|
353
|
-
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
393
|
+
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
354
394
|
) {
|
|
355
395
|
if (!prompt) return null;
|
|
356
396
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
@@ -372,8 +412,8 @@ export async function callLLMWithModel(
|
|
|
372
412
|
try {
|
|
373
413
|
primary =
|
|
374
414
|
mode === 'api'
|
|
375
|
-
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
376
|
-
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
415
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema })
|
|
416
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema });
|
|
377
417
|
} catch (e) {
|
|
378
418
|
debugCatch(e, `callLLMWithModel:${mode}:${resolvedModel}`);
|
|
379
419
|
}
|
|
@@ -401,13 +441,14 @@ export async function callLLMWithModel(
|
|
|
401
441
|
* salvage. Only the CLI transport differs.
|
|
402
442
|
* @param {string|{system?:string,user:string}} prompt
|
|
403
443
|
* @param {'haiku'|'sonnet'} model
|
|
404
|
-
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
444
|
+
* @param {{timeout?:number,maxTokens?:number,temperature?:number,jsonSchema?:{name:string,schema:object}|null}} [opts]
|
|
445
|
+
* jsonSchema: structured output for strict-body models (see callLLMWithModel)
|
|
405
446
|
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
406
447
|
*/
|
|
407
448
|
export async function callLLMWithModelAsync(
|
|
408
449
|
prompt,
|
|
409
450
|
model = 'haiku',
|
|
410
|
-
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
451
|
+
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
411
452
|
) {
|
|
412
453
|
if (!prompt) return null;
|
|
413
454
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
@@ -420,8 +461,8 @@ export async function callLLMWithModelAsync(
|
|
|
420
461
|
try {
|
|
421
462
|
primary =
|
|
422
463
|
mode === 'api'
|
|
423
|
-
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
424
|
-
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
464
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema })
|
|
465
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema });
|
|
425
466
|
} catch (e) {
|
|
426
467
|
debugCatch(e, `callLLMWithModelAsync:${mode}:${resolvedModel}`);
|
|
427
468
|
}
|
|
@@ -457,13 +498,14 @@ export async function callModelJSON(prompt, model = 'haiku', opts) {
|
|
|
457
498
|
* throws; returns parsed JSON or null.
|
|
458
499
|
* @param {string|{system?:string,user:string}} prompt
|
|
459
500
|
* @param {'haiku'|'sonnet'} model
|
|
460
|
-
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
501
|
+
* @param {{timeout?:number,maxTokens?:number,temperature?:number,jsonSchema?:{name:string,schema:object}|null}} [opts]
|
|
502
|
+
* jsonSchema: structured output for strict-body models (see callLLMWithModel)
|
|
461
503
|
* @returns {Promise<object|null>}
|
|
462
504
|
*/
|
|
463
505
|
export async function callModelJSONAsync(
|
|
464
506
|
prompt,
|
|
465
507
|
model = 'haiku',
|
|
466
|
-
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
508
|
+
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null } = {},
|
|
467
509
|
) {
|
|
468
510
|
if (!prompt) return null;
|
|
469
511
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
@@ -480,8 +522,8 @@ export async function callModelJSONAsync(
|
|
|
480
522
|
try {
|
|
481
523
|
primary =
|
|
482
524
|
mode === 'api'
|
|
483
|
-
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
484
|
-
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
525
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema })
|
|
526
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature, jsonSchema });
|
|
485
527
|
} catch (e) {
|
|
486
528
|
debugCatch(e, `callModelJSONAsync:${mode}:${resolvedModel}`);
|
|
487
529
|
}
|
|
@@ -501,7 +543,11 @@ export function _resetTemperatureCompat() {
|
|
|
501
543
|
_temperatureDeprecated.clear();
|
|
502
544
|
}
|
|
503
545
|
|
|
504
|
-
async function callModelAPI(
|
|
546
|
+
async function callModelAPI(
|
|
547
|
+
prompt,
|
|
548
|
+
model,
|
|
549
|
+
{ timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null },
|
|
550
|
+
) {
|
|
505
551
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
506
552
|
if (!apiKey) return null;
|
|
507
553
|
|
|
@@ -535,8 +581,11 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
|
|
|
535
581
|
// field outright (Azure Foundry's claude-sonnet-5 answers 400
|
|
536
582
|
// "`temperature` is deprecated for this model."). Keep sending it until a
|
|
537
583
|
// model refuses, then drop it for that model only - the retry at the bottom
|
|
538
|
-
// pays for itself once.
|
|
539
|
-
if (
|
|
584
|
+
// pays for itself once. A model known to refuse it never gets it.
|
|
585
|
+
if (STRICT_BODY_MODELS.has(modelId)) {
|
|
586
|
+
body.thinking = { type: 'disabled' };
|
|
587
|
+
if (jsonSchema) body.output_config = { format: { type: 'json_schema', schema: jsonSchema.schema } };
|
|
588
|
+
} else if (!_temperatureDeprecated.has(modelId)) body.temperature = temperature;
|
|
540
589
|
// System slot is constant per call type (instructions, schema, type taxonomy)
|
|
541
590
|
// — mark it cache_control:ephemeral so repeated calls within the 5-min cache
|
|
542
591
|
// window pay the cached-input rate (~0.10× base). Sub-1024-token systems still
|
|
@@ -624,7 +673,17 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
|
|
|
624
673
|
}
|
|
625
674
|
|
|
626
675
|
const data = await res.json();
|
|
627
|
-
|
|
676
|
+
// A safety classifier's decline is a 200 that may carry no text or part of an answer.
|
|
677
|
+
// null hands it to the CLI fallback like any other failure; the category, logged only
|
|
678
|
+
// under CLAUDE_MEM_DEBUG, is the only trace of why.
|
|
679
|
+
if (data.stop_reason === 'refusal') {
|
|
680
|
+
debugLog('WARN', `${model}-api`, `refusal (${data.stop_details?.category ?? 'no category'})`);
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
// By content, not position: a thinking block (no `text`) can come first.
|
|
684
|
+
const text = Array.isArray(data.content)
|
|
685
|
+
? data.content.find((b) => typeof b?.text === 'string')?.text
|
|
686
|
+
: undefined;
|
|
628
687
|
return text ? { text } : null;
|
|
629
688
|
} finally {
|
|
630
689
|
clearTimeout(timer);
|
|
@@ -1014,7 +1073,7 @@ export async function callModelCLIAsync(prompt, model, { timeout }) {
|
|
|
1014
1073
|
async function callOpenRouterAPI(
|
|
1015
1074
|
prompt,
|
|
1016
1075
|
tier,
|
|
1017
|
-
{ timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE },
|
|
1076
|
+
{ timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE, jsonSchema = null },
|
|
1018
1077
|
) {
|
|
1019
1078
|
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
1020
1079
|
if (!apiKey) return null;
|
|
@@ -1036,7 +1095,17 @@ async function callOpenRouterAPI(
|
|
|
1036
1095
|
// Optional OpenRouter attribution headers (ignored by the API if absent).
|
|
1037
1096
|
'X-Title': 'claude-mem-lite',
|
|
1038
1097
|
};
|
|
1039
|
-
const
|
|
1098
|
+
const payload = { model, max_tokens: maxTokens, temperature, messages };
|
|
1099
|
+
if (STRICT_BODY_SLUGS.has(model)) {
|
|
1100
|
+
payload.reasoning = { enabled: false };
|
|
1101
|
+
if (jsonSchema) {
|
|
1102
|
+
payload.response_format = {
|
|
1103
|
+
type: 'json_schema',
|
|
1104
|
+
json_schema: { name: jsonSchema.name, strict: true, schema: jsonSchema.schema },
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const reqBody = JSON.stringify(payload);
|
|
1040
1109
|
// Native fetch ignores HTTP(S)_PROXY; when a proxy is configured, tunnel the
|
|
1041
1110
|
// request through it — a direct fetch to openrouter.ai times out behind one.
|
|
1042
1111
|
const proxy = httpConnectProxyFor(url);
|
package/hook-llm.mjs
CHANGED
|
@@ -933,6 +933,64 @@ ${diagnosisBlock(diag)}`;
|
|
|
933
933
|
|
|
934
934
|
// ─── Background: LLM Episode Extraction (Tier 2 F) ──────────────────────────
|
|
935
935
|
|
|
936
|
+
/**
|
|
937
|
+
* The episode-extraction prompt, `{ system, user }`. Exported so
|
|
938
|
+
* benchmark/haiku-model-ab.mjs sends the SHIPPED prompt rather than a retyped copy
|
|
939
|
+
* (buildCompressPrompt was extracted for the same reason).
|
|
940
|
+
* @param {object} episode
|
|
941
|
+
* @param {string[]} [diag] episodeDiagnosis(episode); the handler passes the one it keeps
|
|
942
|
+
* @returns {{system: string, user: string}}
|
|
943
|
+
*/
|
|
944
|
+
export function buildEpisodePrompt(episode, diag = episodeDiagnosis(episode)) {
|
|
945
|
+
// Same guard as the handler: an older-format tmp file can omit `files`.
|
|
946
|
+
const episodeFiles = Array.isArray(episode.files) ? episode.files : [];
|
|
947
|
+
const fileList = episodeFiles.map((f) => basename(f)).join(', ') || '(multiple)';
|
|
948
|
+
|
|
949
|
+
// Defense-in-depth (cso F#4): split static instructions (system) from
|
|
950
|
+
// per-call data (user). Episode descriptions and file paths come from tool
|
|
951
|
+
// events; treating them as a separate role + boundary marker reduces the
|
|
952
|
+
// attack surface for memory poisoning via crafted file content.
|
|
953
|
+
const SHARED_OBS_SCHEMA_TAIL = `${MEMORY_INPUT_GUARD}
|
|
954
|
+
Grounding: state only outcomes the user message shows. An action description may be cut short ("…"); if its result is not visible, say what was run, not how it turned out. Never write that something passed, failed, was verified or confirmed unless that result appears in the user message.
|
|
955
|
+
type: pick by strongest signal. decision = explicit tradeoff / "chose X over Y because Z" / rejected an approach (e.g. "Rejected schema migration — single-source module + sync test instead"; "Heterogeneous hook events → heterogeneous context budgets"). bugfix = prior-failing path fixed with a named root cause; a test that failed only until this episode's implementation landed (written first, then made to pass) is TDD, not a bugfix — type that feature or refactor. feature = new user-visible capability. refactor = behavior unchanged but structure improved. discovery = learned how a system works (read-heavy, no writes). change = routine edit with no new principle (default if unsure and nothing else fits).
|
|
956
|
+
Facts: each MUST be (1) atomic—one claim, (2) self-contained—no pronouns, include file/function name, (3) specific—"refreshToken() in auth.ts:45 uses 1h TTL" not "handles tokens"
|
|
957
|
+
importance: Be strict — default to 1. 0=pure browsing with zero learning value. 1=routine file edits, standard changes, normal workflow (MOST episodes). 2=notable ONLY if it reveals something non-obvious: error fix with discovered root cause, architectural decision with explicit tradeoff, config change with unexpected side effects. 3=critical: breaking change affecting users, security vulnerability fix, data migration. Ask yourself: "would a future session benefit from knowing this?" — if not, it's importance=1.
|
|
958
|
+
lesson_learned: The non-obvious insight a future session would benefit from, resting ONLY on the DIAGNOSIS lines: copy at least 4 consecutive words verbatim, in double quotes, from one DIAGNOSIS line, and claim no mechanism that line does not state. Example: FTS5's "default tokenizer doesn't split CJK" — index bigrams instead. No DIAGNOSIS line, or none that states a cause → output JSON null; a lesson without such a quote is discarded. Do NOT invent a lesson. A mutation/probe run (a file changed on purpose, a checksum, a restore) and the agent's own script errors are not product bugs. Do NOT write the strings "none"/"n/a"/"todo"/"tbd"/"-" — those will be discarded as noise.
|
|
959
|
+
scope: ${SCOPE_PROMPT_LEGEND}
|
|
960
|
+
search_aliases: 2-6 alternative search terms someone might use to find this memory later (include CJK if project uses Chinese)`;
|
|
961
|
+
|
|
962
|
+
if (episode.entries.length === 1) {
|
|
963
|
+
const e = episode.entries[0];
|
|
964
|
+
const system = `Extract a structured observation from this code change. Return ONLY valid JSON, no markdown fences.
|
|
965
|
+
|
|
966
|
+
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","scope":"file|module|project|environment","search_aliases":["alt query 1","alt query 2"]}
|
|
967
|
+
${SHARED_OBS_SCHEMA_TAIL}`;
|
|
968
|
+
const user = `Tool: ${e.tool}
|
|
969
|
+
File: ${episodeFiles.join(', ') || 'unknown'}
|
|
970
|
+
Action: ${e.desc}
|
|
971
|
+
Error: ${e.isError ? 'yes' : 'no'}
|
|
972
|
+
|
|
973
|
+
${diagnosisBlock(diag)}`;
|
|
974
|
+
return { system, user };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const actionList = episode.entries
|
|
978
|
+
.map((e, i) => `${i + 1}. [${e.tool}] ${e.desc}${e.isError ? ' (ERROR)' : ''}`)
|
|
979
|
+
.join('\n');
|
|
980
|
+
|
|
981
|
+
const system = `Summarize this coding episode as ONE coherent observation. Return ONLY valid JSON, no markdown fences.
|
|
982
|
+
|
|
983
|
+
JSON: {"type":"decision|bugfix|feature|refactor|discovery|change","title":"coherent ≤80 char summary","narrative":"what was done, why, and outcome (3-5 sentences)","concepts":["keyword1","keyword2"],"facts":["specific fact 1","specific fact 2"],"importance":1,"lesson_learned":"non-obvious insight a future session needs, or null","scope":"file|module|project|environment","search_aliases":["alt query 1","alt query 2"]}
|
|
984
|
+
${SHARED_OBS_SCHEMA_TAIL}`;
|
|
985
|
+
const user = `Project: ${episode.project}
|
|
986
|
+
Files: ${fileList}
|
|
987
|
+
Actions (${episode.entries.length} total):
|
|
988
|
+
${actionList}
|
|
989
|
+
|
|
990
|
+
${diagnosisBlock(diag)}`;
|
|
991
|
+
return { system, user };
|
|
992
|
+
}
|
|
993
|
+
|
|
936
994
|
export async function handleLLMEpisode() {
|
|
937
995
|
const tmpFile = process.argv[3];
|
|
938
996
|
if (!tmpFile) return;
|
|
@@ -974,51 +1032,8 @@ export async function handleLLMEpisode() {
|
|
|
974
1032
|
const fileList = episodeFiles.map((f) => basename(f)).join(', ') || '(multiple)';
|
|
975
1033
|
const edgeFiles = splitEpisodeFiles(episode);
|
|
976
1034
|
|
|
977
|
-
// Defense-in-depth (cso F#4): split static instructions (system) from
|
|
978
|
-
// per-call data (user). Episode descriptions and file paths come from tool
|
|
979
|
-
// events; treating them as a separate role + boundary marker reduces the
|
|
980
|
-
// attack surface for memory poisoning via crafted file content.
|
|
981
|
-
const SHARED_OBS_SCHEMA_TAIL = `${MEMORY_INPUT_GUARD}
|
|
982
|
-
Grounding: state only outcomes the user message shows. An action description may be cut short ("…"); if its result is not visible, say what was run, not how it turned out. Never write that something passed, failed, was verified or confirmed unless that result appears in the user message.
|
|
983
|
-
type: pick by strongest signal. decision = explicit tradeoff / "chose X over Y because Z" / rejected an approach (e.g. "Rejected schema migration — single-source module + sync test instead"; "Heterogeneous hook events → heterogeneous context budgets"). bugfix = prior-failing path fixed with a named root cause; a test that failed only until this episode's implementation landed (written first, then made to pass) is TDD, not a bugfix — type that feature or refactor. feature = new user-visible capability. refactor = behavior unchanged but structure improved. discovery = learned how a system works (read-heavy, no writes). change = routine edit with no new principle (default if unsure and nothing else fits).
|
|
984
|
-
Facts: each MUST be (1) atomic—one claim, (2) self-contained—no pronouns, include file/function name, (3) specific—"refreshToken() in auth.ts:45 uses 1h TTL" not "handles tokens"
|
|
985
|
-
importance: Be strict — default to 1. 0=pure browsing with zero learning value. 1=routine file edits, standard changes, normal workflow (MOST episodes). 2=notable ONLY if it reveals something non-obvious: error fix with discovered root cause, architectural decision with explicit tradeoff, config change with unexpected side effects. 3=critical: breaking change affecting users, security vulnerability fix, data migration. Ask yourself: "would a future session benefit from knowing this?" — if not, it's importance=1.
|
|
986
|
-
lesson_learned: The non-obvious insight a future session would benefit from, resting ONLY on the DIAGNOSIS lines: copy at least 4 consecutive words verbatim, in double quotes, from one DIAGNOSIS line, and claim no mechanism that line does not state. Example: FTS5's "default tokenizer doesn't split CJK" — index bigrams instead. No DIAGNOSIS line, or none that states a cause → output JSON null; a lesson without such a quote is discarded. Do NOT invent a lesson. A mutation/probe run (a file changed on purpose, a checksum, a restore) and the agent's own script errors are not product bugs. Do NOT write the strings "none"/"n/a"/"todo"/"tbd"/"-" — those will be discarded as noise.
|
|
987
|
-
scope: ${SCOPE_PROMPT_LEGEND}
|
|
988
|
-
search_aliases: 2-6 alternative search terms someone might use to find this memory later (include CJK if project uses Chinese)`;
|
|
989
|
-
|
|
990
1035
|
const diag = episodeDiagnosis(episode);
|
|
991
|
-
|
|
992
|
-
if (episode.entries.length === 1) {
|
|
993
|
-
const e = episode.entries[0];
|
|
994
|
-
const system = `Extract a structured observation from this code change. Return ONLY valid JSON, no markdown fences.
|
|
995
|
-
|
|
996
|
-
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","scope":"file|module|project|environment","search_aliases":["alt query 1","alt query 2"]}
|
|
997
|
-
${SHARED_OBS_SCHEMA_TAIL}`;
|
|
998
|
-
const user = `Tool: ${e.tool}
|
|
999
|
-
File: ${episodeFiles.join(', ') || 'unknown'}
|
|
1000
|
-
Action: ${e.desc}
|
|
1001
|
-
Error: ${e.isError ? 'yes' : 'no'}
|
|
1002
|
-
|
|
1003
|
-
${diagnosisBlock(diag)}`;
|
|
1004
|
-
prompt = { system, user };
|
|
1005
|
-
} else {
|
|
1006
|
-
const actionList = episode.entries
|
|
1007
|
-
.map((e, i) => `${i + 1}. [${e.tool}] ${e.desc}${e.isError ? ' (ERROR)' : ''}`)
|
|
1008
|
-
.join('\n');
|
|
1009
|
-
|
|
1010
|
-
const system = `Summarize this coding episode as ONE coherent observation. Return ONLY valid JSON, no markdown fences.
|
|
1011
|
-
|
|
1012
|
-
JSON: {"type":"decision|bugfix|feature|refactor|discovery|change","title":"coherent ≤80 char summary","narrative":"what was done, why, and outcome (3-5 sentences)","concepts":["keyword1","keyword2"],"facts":["specific fact 1","specific fact 2"],"importance":1,"lesson_learned":"non-obvious insight a future session needs, or null","scope":"file|module|project|environment","search_aliases":["alt query 1","alt query 2"]}
|
|
1013
|
-
${SHARED_OBS_SCHEMA_TAIL}`;
|
|
1014
|
-
const user = `Project: ${episode.project}
|
|
1015
|
-
Files: ${fileList}
|
|
1016
|
-
Actions (${episode.entries.length} total):
|
|
1017
|
-
${actionList}
|
|
1018
|
-
|
|
1019
|
-
${diagnosisBlock(diag)}`;
|
|
1020
|
-
prompt = { system, user };
|
|
1021
|
-
}
|
|
1036
|
+
const prompt = buildEpisodePrompt(episode, diag);
|
|
1022
1037
|
|
|
1023
1038
|
const ruleImportance = computeRuleImportance(episode);
|
|
1024
1039
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.26.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.
|
|
9
|
+
"version": "6.26.0",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.26.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword 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",
|