claude-mem-lite 2.32.7 → 2.33.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/adopt-cli.mjs +53 -1
- package/hook.mjs +30 -0
- package/package.json +1 -1
- package/scripts/prompt-search-utils.mjs +40 -2
- package/scripts/user-prompt-search.js +19 -1
package/adopt-cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// --dry-run = print intent without writing
|
|
10
10
|
// --status = list all adopted projects + versions
|
|
11
11
|
|
|
12
|
-
import { existsSync, readdirSync, statSync } from 'fs';
|
|
12
|
+
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'fs';
|
|
13
13
|
import { homedir } from 'os';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import {
|
|
@@ -121,6 +121,58 @@ function adoptOne(memdir, { force, dryRun, all }) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* silentAutoAdopt — plugin-mode first-run auto-adopt helper (v2.33.0).
|
|
126
|
+
*
|
|
127
|
+
* Preconditions (caller must gate): CLAUDE_PLUGIN_ROOT set, MEM_NO_AUTO_ADOPT!=1,
|
|
128
|
+
* MEM_QUIET_HOOKS!=1, first-attempt marker absent. This helper does NOT re-check
|
|
129
|
+
* those — it only does the write + marker persistence.
|
|
130
|
+
*
|
|
131
|
+
* Behavior:
|
|
132
|
+
* - Writes plugin sentinel + detail doc to the memdir for `cwd`.
|
|
133
|
+
* - Writes a per-project first-attempt marker under `markerDir` so a later
|
|
134
|
+
* `/unadopt` is respected (no re-adopt loop).
|
|
135
|
+
* - Silent: never logs, never throws. Returns structured result.
|
|
136
|
+
*
|
|
137
|
+
* Returns { ok, action, reason } — caller uses for telemetry / debugLog only.
|
|
138
|
+
*/
|
|
139
|
+
export function silentAutoAdopt({ cwd, markerDir, markerKey }) {
|
|
140
|
+
const memdir = memdirPath(cwd);
|
|
141
|
+
try {
|
|
142
|
+
if (isAdopted(memdir, PLUGIN_SLUG)) {
|
|
143
|
+
writeMarker(markerDir, markerKey);
|
|
144
|
+
return { ok: true, action: 'already-adopted' };
|
|
145
|
+
}
|
|
146
|
+
writePluginSection(memdir, {
|
|
147
|
+
slug: PLUGIN_SLUG,
|
|
148
|
+
version: CURRENT_SENTINEL_VERSION,
|
|
149
|
+
contentLine: getIndexLine(),
|
|
150
|
+
force: false,
|
|
151
|
+
});
|
|
152
|
+
writePluginDoc(memdir, PLUGIN_SLUG, getDetailDoc());
|
|
153
|
+
writeMarker(markerDir, markerKey);
|
|
154
|
+
return { ok: true, action: 'adopted' };
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// Budget exceeded, user-edited conflict, or FS error — write marker so we
|
|
157
|
+
// don't retry on every SessionStart. User can run /adopt --force manually.
|
|
158
|
+
try { writeMarker(markerDir, markerKey); } catch { /* marker best-effort */ }
|
|
159
|
+
const reason = e instanceof UserEditedError ? 'user-edited'
|
|
160
|
+
: e instanceof BudgetExceededError ? 'budget-exceeded'
|
|
161
|
+
: 'error';
|
|
162
|
+
return { ok: false, action: 'skipped', reason, err: e };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeMarker(markerDir, markerKey) {
|
|
167
|
+
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
|
|
168
|
+
const path = join(markerDir, `.auto-adopt-${markerKey}`);
|
|
169
|
+
writeFileSync(path, JSON.stringify({ firstAttemptAt: new Date().toISOString() }));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function hasAutoAdoptMarker(markerDir, markerKey) {
|
|
173
|
+
return existsSync(join(markerDir, `.auto-adopt-${markerKey}`));
|
|
174
|
+
}
|
|
175
|
+
|
|
124
176
|
function statusAll() {
|
|
125
177
|
const dirs = listAllMemdirs();
|
|
126
178
|
log('[adopt --status] scanning ~/.claude/projects/*/memory/');
|
package/hook.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { searchRelevantMemories } from './hook-memory.mjs';
|
|
|
32
32
|
import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
33
33
|
import { checkForUpdate } from './hook-update.mjs';
|
|
34
34
|
import { handleLLMOptimize } from './hook-optimize.mjs';
|
|
35
|
+
import { silentAutoAdopt, hasAutoAdoptMarker } from './adopt-cli.mjs';
|
|
35
36
|
// plugin-cache-guard.mjs loaded dynamically — pre-2.31.2 installs that auto-upgraded
|
|
36
37
|
// from an older hook-update.mjs SOURCE_FILES (which did not list this module) would
|
|
37
38
|
// crash on static import. Degrade gracefully to no-op when the module is absent.
|
|
@@ -425,6 +426,35 @@ async function handleSessionStart() {
|
|
|
425
426
|
}
|
|
426
427
|
} catch (e) { debugCatch(e, 'session-start-cache-heal'); }
|
|
427
428
|
|
|
429
|
+
// v2.33.0: plugin-mode first-run auto-adopt. /plugin install IS consent to
|
|
430
|
+
// integration — writing the MEMORY.md sentinel once per project on first
|
|
431
|
+
// SessionStart avoids the opt-in friction. Scope is narrow:
|
|
432
|
+
// - gated by CLAUDE_PLUGIN_ROOT (npm/npx installs stay opt-in)
|
|
433
|
+
// - gated by !MEM_NO_AUTO_ADOPT (explicit escape hatch)
|
|
434
|
+
// - gated by !MEM_QUIET_HOOKS (quiet = no side-effects semantics)
|
|
435
|
+
// - first-attempt marker persists in RUNTIME_DIR so a subsequent /unadopt
|
|
436
|
+
// is respected (no re-adopt loop).
|
|
437
|
+
// Failures (user-edited sentinel, budget exceeded, FS errors) are swallowed;
|
|
438
|
+
// the marker is still written so we don't retry on every SessionStart.
|
|
439
|
+
try {
|
|
440
|
+
if (
|
|
441
|
+
process.env.CLAUDE_PLUGIN_ROOT
|
|
442
|
+
&& process.env.MEM_NO_AUTO_ADOPT !== '1'
|
|
443
|
+
&& process.env.MEM_QUIET_HOOKS !== '1'
|
|
444
|
+
) {
|
|
445
|
+
const project = inferProject();
|
|
446
|
+
if (!hasAutoAdoptMarker(RUNTIME_DIR, project)) {
|
|
447
|
+
const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
448
|
+
const r = silentAutoAdopt({ cwd, markerDir: RUNTIME_DIR, markerKey: project });
|
|
449
|
+
if (r.ok) {
|
|
450
|
+
debugLog('DEBUG', 'session-start-auto-adopt', `action=${r.action} project=${project}`);
|
|
451
|
+
} else {
|
|
452
|
+
debugLog('DEBUG', 'session-start-auto-adopt', `skipped project=${project} reason=${r.reason}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} catch (e) { debugCatch(e, 'session-start-auto-adopt'); }
|
|
457
|
+
|
|
428
458
|
// Read CC real session_id from hook stdin — used to scope handoff rows so parallel
|
|
429
459
|
// sessions for the same project don't clobber each other (see docs/bug.txt).
|
|
430
460
|
let ccSessionId = null;
|
package/package.json
CHANGED
|
@@ -49,8 +49,8 @@ export const INTENTS = [
|
|
|
49
49
|
// CJK: 开发/编写/创建/构建/做一个/写一个 from real prompts
|
|
50
50
|
{ pattern: /implement|feature\b|add\s+(?:a\s+)?new|实现|添加|新功能|新增|开发|编写|创建|构建|做一个|加一个|写一个/i, type: null, limit: 3 },
|
|
51
51
|
// Recall/history intent (catch-all temporal, lowest priority)
|
|
52
|
-
// CJK: 刚才/历史/回顾 from real prompts
|
|
53
|
-
{ pattern: /before|previously|last time|remember
|
|
52
|
+
// CJK: 刚才/历史/回顾 from real prompts; 碰到过|遇到过|见过|同样的问题 from spoken CN
|
|
53
|
+
{ pattern: /before|previously|last time|remember|seen this|same\s+issue|之前|上次|以前|记得|刚才|历史|回顾|碰到过|遇到过|见过|同样的问题|类似的问题/i, type: null, limit: 5, useRecent: true },
|
|
54
54
|
];
|
|
55
55
|
|
|
56
56
|
export function detectIntent(text) {
|
|
@@ -75,6 +75,44 @@ export function detectIntent(text) {
|
|
|
75
75
|
return first;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// ─── Error Signature Extraction ─────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extract a canonical error signature from prompt text.
|
|
82
|
+
*
|
|
83
|
+
* Matches named exception/error classes like:
|
|
84
|
+
* - "TypeError: Cannot read properties of undefined (reading 'foo')"
|
|
85
|
+
* - "Error [ERR_MODULE_NOT_FOUND]: module X not found"
|
|
86
|
+
* - "AssertionError: expected 'a' to equal 'b'"
|
|
87
|
+
* - "ValueError: invalid literal for int()"
|
|
88
|
+
* - "thread 'main' panicked at ..." (Rust) → captured via Panic class
|
|
89
|
+
*
|
|
90
|
+
* Intentionally skips bare "Error: ..." without a typed class, and skips
|
|
91
|
+
* lowercase matches — those carry too little signal vs. the intent-based
|
|
92
|
+
* FTS path which already catches them.
|
|
93
|
+
*
|
|
94
|
+
* Returns { className, errorCode, message, signature } or null.
|
|
95
|
+
* `signature` is suitable for direct FTS5 search (sanitizeFtsQuery applies).
|
|
96
|
+
*/
|
|
97
|
+
export function extractErrorSignature(text) {
|
|
98
|
+
if (!text || typeof text !== 'string') return null;
|
|
99
|
+
// Pass 1: typed class — "<CapCase>(Error|Exception|Panic)" with optional [ERR_CODE]
|
|
100
|
+
const TYPED_RE = /\b([A-Z][A-Za-z0-9]+(?:Error|Exception|Panic))(?:\s*\[([A-Z_][A-Z0-9_]*)\])?\s*:\s*([^\n]{3,200})/;
|
|
101
|
+
// Pass 2: bare "Error|Exception|Panic" followed by required [ERR_CODE] (Node idiom).
|
|
102
|
+
// Bare class without a code is skipped — too noisy; intent-based path catches those.
|
|
103
|
+
const BARE_CODED_RE = /\b(Error|Exception|Panic)\s*\[([A-Z_][A-Z0-9_]*)\]\s*:\s*([^\n]{3,200})/;
|
|
104
|
+
const m = text.match(TYPED_RE) || text.match(BARE_CODED_RE);
|
|
105
|
+
if (!m) return null;
|
|
106
|
+
const className = m[1];
|
|
107
|
+
const errorCode = m[2] || null;
|
|
108
|
+
const message = m[3].trim().replace(/\s+/g, ' ').replace(/[`'"]+$/, '');
|
|
109
|
+
const sigMsg = message.slice(0, 80);
|
|
110
|
+
const signature = errorCode
|
|
111
|
+
? `${className} ${errorCode} ${sigMsg}`
|
|
112
|
+
: `${className} ${sigMsg}`;
|
|
113
|
+
return { className, errorCode, message, signature };
|
|
114
|
+
}
|
|
115
|
+
|
|
78
116
|
// ─── Result Dedup ───────────────────────────────────────────────────────────
|
|
79
117
|
|
|
80
118
|
export const MAX_SESSION_INJECTIONS = 15;
|
|
@@ -8,7 +8,7 @@ import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject,
|
|
|
8
8
|
import { writeFileSync, readFileSync, existsSync, renameSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import Database from 'better-sqlite3';
|
|
11
|
-
import { shouldSkip, detectIntent, shouldSkipByDedup, extractFiles, DEDUP_STALE_MS, matchRegistrySkillName } from './prompt-search-utils.mjs';
|
|
11
|
+
import { shouldSkip, detectIntent, shouldSkipByDedup, extractFiles, extractErrorSignature, DEDUP_STALE_MS, matchRegistrySkillName } from './prompt-search-utils.mjs';
|
|
12
12
|
|
|
13
13
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
@@ -267,6 +267,18 @@ async function main() {
|
|
|
267
267
|
const intent = detectIntent(promptText);
|
|
268
268
|
let rows = [];
|
|
269
269
|
|
|
270
|
+
// A (v2.32.8): precision pass for named errors. When the prompt contains
|
|
271
|
+
// a typed exception signature (TypeError/ValueError/ReferenceError/...),
|
|
272
|
+
// seed results with exact-match bugfix observations before the intent-
|
|
273
|
+
// based FTS flow runs. These hits are the most directly relevant and
|
|
274
|
+
// take priority slots in the merged output.
|
|
275
|
+
const errSig = extractErrorSignature(promptText);
|
|
276
|
+
const sigRows = errSig
|
|
277
|
+
? searchByFts(db, errSig.signature, project, 2, 'bugfix').filter(r =>
|
|
278
|
+
typeof r.relevance === 'number' && Math.abs(r.relevance) >= BM25_MIN_SCORE
|
|
279
|
+
)
|
|
280
|
+
: [];
|
|
281
|
+
|
|
270
282
|
if (intent?.useRecent) {
|
|
271
283
|
// Recall intent: show recent observations
|
|
272
284
|
rows = searchRecent(db, project, intent.limit);
|
|
@@ -302,6 +314,12 @@ async function main() {
|
|
|
302
314
|
rows = rows.slice(0, MAX_RESULTS);
|
|
303
315
|
}
|
|
304
316
|
|
|
317
|
+
// A (v2.32.8): prepend error-signature hits (higher precision), dedup, cap.
|
|
318
|
+
if (sigRows.length > 0) {
|
|
319
|
+
const sigIds = new Set(sigRows.map(r => r.id));
|
|
320
|
+
rows = [...sigRows, ...rows.filter(r => !sigIds.has(r.id))].slice(0, MAX_RESULTS);
|
|
321
|
+
}
|
|
322
|
+
|
|
305
323
|
const candidateIds = rows.map(r => r.id);
|
|
306
324
|
const dedupSkip = shouldSkipByDedup(candidateIds, INJECTED_IDS_FILE);
|
|
307
325
|
|