claude-mem-lite 2.9.1 → 2.9.3
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 +9 -2
- package/README.zh-CN.md +9 -2
- package/cli.mjs +3 -1
- package/dispatch.mjs +45 -7
- package/hook-update.mjs +123 -43
- package/hook.mjs +21 -1
- package/install.mjs +180 -75
- package/package.json +1 -1
- package/registry-retriever.mjs +5 -0
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ The original sends **everything to the LLM and hopes it filters well**. claude-m
|
|
|
121
121
|
/plugin install claude-mem-lite
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
Plugin mode manages its own hooks/runtime. On session start it only **checks and reports** new claude-mem-lite versions; it does **not** self-overwrite plugin files in place. Update plugin-mode installs through Claude's plugin workflow.
|
|
125
125
|
|
|
126
126
|
### Method 2: npx (one-liner)
|
|
127
127
|
|
|
@@ -375,6 +375,8 @@ node install.mjs uninstall # Remove (keep data)
|
|
|
375
375
|
node install.mjs uninstall --purge # Remove and delete all data
|
|
376
376
|
node install.mjs status # Show current status
|
|
377
377
|
node install.mjs doctor # Diagnose issues
|
|
378
|
+
node install.mjs cleanup-hooks # Remove only stale claude-mem-lite hooks from settings.json
|
|
379
|
+
node install.mjs update # Force-check for updates and install them (direct install / npx mode)
|
|
378
380
|
|
|
379
381
|
# npx install:
|
|
380
382
|
npx claude-mem-lite # Install / reinstall
|
|
@@ -382,13 +384,18 @@ npx claude-mem-lite uninstall # Remove (keep data)
|
|
|
382
384
|
npx claude-mem-lite doctor # Diagnose issues
|
|
383
385
|
```
|
|
384
386
|
|
|
387
|
+
Notes:
|
|
388
|
+
- Plugin mode only reports available updates; it does not self-update plugin files.
|
|
389
|
+
- Direct install / npx mode keeps auto-update enabled and uses staged replacement with rollback on install failure.
|
|
390
|
+
- If you disabled the plugin but still have old mem hooks in `~/.claude/settings.json`, run `node install.mjs cleanup-hooks`.
|
|
391
|
+
|
|
385
392
|
### doctor
|
|
386
393
|
|
|
387
394
|
Checks Node.js version, dependencies, server/hook files, database integrity, FTS5 indexes, and stale processes.
|
|
388
395
|
|
|
389
396
|
### status
|
|
390
397
|
|
|
391
|
-
Shows MCP registration, hook configuration, and database stats (observation/session counts).
|
|
398
|
+
Shows MCP registration, hook configuration, plugin disabled state, and database stats (observation/session counts).
|
|
392
399
|
|
|
393
400
|
## Uninstall
|
|
394
401
|
|
package/README.zh-CN.md
CHANGED
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
/plugin install claude-mem-lite
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
插件模式会管理自己的运行时与钩子。SessionStart 时它现在只会**检查并提示**新版本,不会直接覆盖插件目录中的文件。插件模式请通过 Claude 的插件更新流程完成升级。
|
|
125
125
|
|
|
126
126
|
### 方式二:npx(一行命令)
|
|
127
127
|
|
|
@@ -375,6 +375,8 @@ node install.mjs uninstall # 移除(保留数据)
|
|
|
375
375
|
node install.mjs uninstall --purge # 移除并删除所有数据
|
|
376
376
|
node install.mjs status # 显示当前状态
|
|
377
377
|
node install.mjs doctor # 诊断问题
|
|
378
|
+
node install.mjs cleanup-hooks # 只清理 settings.json 中残留的 claude-mem-lite hooks
|
|
379
|
+
node install.mjs update # 强制检查并安装更新(direct install / npx 模式)
|
|
378
380
|
|
|
379
381
|
# npx 安装:
|
|
380
382
|
npx claude-mem-lite # 安装 / 重新安装
|
|
@@ -382,13 +384,18 @@ npx claude-mem-lite uninstall # 移除(保留数据)
|
|
|
382
384
|
npx claude-mem-lite doctor # 诊断问题
|
|
383
385
|
```
|
|
384
386
|
|
|
387
|
+
说明:
|
|
388
|
+
- 插件模式只提示可用更新,不会自更新插件文件。
|
|
389
|
+
- direct install / npx 模式保留自动更新,并使用 staged replacement;若依赖安装失败会回滚。
|
|
390
|
+
- 如果你禁用了插件,但 `~/.claude/settings.json` 里还有旧的 mem hooks,可运行 `node install.mjs cleanup-hooks`。
|
|
391
|
+
|
|
385
392
|
### doctor
|
|
386
393
|
|
|
387
394
|
检查 Node.js 版本、依赖、服务器/钩子文件、数据库完整性、FTS5 索引和残留进程。
|
|
388
395
|
|
|
389
396
|
### status
|
|
390
397
|
|
|
391
|
-
显示 MCP
|
|
398
|
+
显示 MCP 注册状态、钩子配置、插件禁用状态和数据库统计(观察/会话数量)。
|
|
392
399
|
|
|
393
400
|
## 卸载
|
|
394
401
|
|
package/cli.mjs
CHANGED
package/dispatch.mjs
CHANGED
|
@@ -216,7 +216,7 @@ const NEGATION_CJK = /(?:不要|别|不用|先别|暂时不|不需要|跳过|停
|
|
|
216
216
|
|
|
217
217
|
// Test-run vs test-write disambiguation (module-scoped for performance)
|
|
218
218
|
const _RUN_TEST = /\b(run\w*\s+(?:the\s+)?tests?|npm\s+test|npx\s+(?:vitest|jest|mocha|pytest)|yarn\s+test|pnpm\s+test|make\s+test|cargo\s+test|go\s+test|check\s+(?:if\s+)?tests?\s+pass|execute\s+(?:the\s+)?tests?)\b/i;
|
|
219
|
-
const _RUN_TEST_CJK = /(
|
|
219
|
+
const _RUN_TEST_CJK = /(?:运行测试|跑测试|跑一下测试|跑单测|跑一下单测|执行测试|执行单测|测试跑|看测试|看单测)/;
|
|
220
220
|
const _WRITE_TEST = /\b(write\s+tests?|add\s+tests?|create\s+tests?|need\s+tests?|missing\s+tests?|tdd|test.?driven|red.?green|increase\s+coverage|improve\s+coverage)\b/i;
|
|
221
221
|
const _WRITE_TEST_CJK = /(?:写测试|加测试|补测试|补单测|缺测试|测试覆盖)/;
|
|
222
222
|
|
|
@@ -259,7 +259,7 @@ const _INTENT_PATTERNS = (() => {
|
|
|
259
259
|
// ── Chinese patterns ──
|
|
260
260
|
[/(测试|写测试|单测|单元测试|用例|覆盖率)/, 'test'],
|
|
261
261
|
[/(修复|修bug|改bug|找bug|有bug|调试|排错|报错|出错|有问题|不工作|跑不起来|不能用|挂了|崩溃)/, 'fix'],
|
|
262
|
-
[/(
|
|
262
|
+
[/(审查|审核|审计|代码审查|评审|代码审核|看看代码|review)/, 'review'],
|
|
263
263
|
[/(提交|推送|上传)/, 'commit'],
|
|
264
264
|
[/(部署|上线|发布|回滚)/, 'deploy'],
|
|
265
265
|
[/(规划|架构|方案|设计方案)/, 'plan'],
|
|
@@ -273,6 +273,10 @@ const _INTENT_PATTERNS = (() => {
|
|
|
273
273
|
[/(优化|性能|卡顿|耗时|太慢|慢死了|好慢|缓存)/, 'fast'],
|
|
274
274
|
[/(格式化|代码风格|代码规范|类型检查)/, 'lint'],
|
|
275
275
|
[/(界面|前端|样式|页面|组件|布局)/, 'design'],
|
|
276
|
+
// search: only unambiguous web/info search indicators — NOT code search (grep/find).
|
|
277
|
+
// "搜索" alone is ambiguous (code search vs web search), so require context modifiers.
|
|
278
|
+
[/(联网搜索|网上搜索|在线搜索|上网查|搜索.{0,2}最新|搜一下.{0,2}最新|查.{0,2}最新|查资料|找资料|搜索资料|搜索文档)/, 'search'],
|
|
279
|
+
[/\b(google|search\s+online|web\s+search|look\s+up\s+(?:the\s+)?(?:latest|newest|recent|docs?|documentation))\b/i, 'search'],
|
|
276
280
|
];
|
|
277
281
|
// Pre-compile global variants for matchAll — avoids creating new RegExp on every extractIntent call
|
|
278
282
|
return raw.map(([p, tag]) => [p, new RegExp(p.source, p.flags.includes('g') ? p.flags : p.flags + 'g'), tag]);
|
|
@@ -311,15 +315,19 @@ function extractIntent(prompt) {
|
|
|
311
315
|
}
|
|
312
316
|
|
|
313
317
|
const found = [];
|
|
318
|
+
const suppressed = [];
|
|
314
319
|
for (const tag of tagMatched) {
|
|
315
320
|
if (tagHasAffirmative.get(tag) && !found.includes(tag)) {
|
|
316
321
|
found.push(tag);
|
|
322
|
+
} else if (!tagHasAffirmative.get(tag)) {
|
|
323
|
+
// Tag was matched but ALL instances were negated → suppress it.
|
|
324
|
+
// This feeds the text-fallback filter to prevent recommending negated resources.
|
|
325
|
+
suppressed.push(tag);
|
|
317
326
|
}
|
|
318
327
|
}
|
|
319
328
|
|
|
320
329
|
// Distinguish test-running from test-writing: "run tests" / "npm test" / "运行测试" should NOT
|
|
321
330
|
// trigger TDD recommendations. Only keep 'test' intent when the prompt implies *writing* tests.
|
|
322
|
-
const suppressed = [];
|
|
323
331
|
if (found.includes('test')) {
|
|
324
332
|
const isRunning = _RUN_TEST.test(prompt) || _RUN_TEST_CJK.test(prompt);
|
|
325
333
|
const isWriting = _WRITE_TEST.test(prompt) || _WRITE_TEST_CJK.test(prompt);
|
|
@@ -810,8 +818,13 @@ function applyAdoptionDecay(results, db) {
|
|
|
810
818
|
*/
|
|
811
819
|
function passesConfidenceGate(results, signals) {
|
|
812
820
|
// BM25 absolute minimum: filter weak text matches.
|
|
813
|
-
//
|
|
814
|
-
|
|
821
|
+
// Threshold is relative to the top result's score to handle varying corpus sizes:
|
|
822
|
+
// small corpora (< 50 resources) naturally produce lower BM25 IDF values,
|
|
823
|
+
// so an absolute threshold would over-filter genuine matches.
|
|
824
|
+
const baseThreshold = results.length >= 3 ? BM25_MIN_THRESHOLD : 0.5;
|
|
825
|
+
const topScore = results.length > 0 ? Math.abs(results[0].composite_score ?? results[0].relevance ?? 0) : 0;
|
|
826
|
+
// Use the lower of: absolute threshold OR 30% of top score (corpus-size-adaptive floor)
|
|
827
|
+
const minThreshold = topScore > 0 ? Math.min(baseThreshold, topScore * 0.3) : baseThreshold;
|
|
815
828
|
results = results.filter(r => {
|
|
816
829
|
const raw = r.composite_score ?? r.relevance;
|
|
817
830
|
if (raw === null || raw === undefined) return true; // no score → pass (pre-scored or synthetic result)
|
|
@@ -821,10 +834,18 @@ function passesConfidenceGate(results, signals) {
|
|
|
821
834
|
// Gap check: if top-2 results are too close in score, the query is ambiguous.
|
|
822
835
|
// This prevents recommending when multiple resources match equally well,
|
|
823
836
|
// which usually means the match is incidental rather than precise.
|
|
837
|
+
// Skip the gap check when rawKeywords promoted #1 (keyword re-ranking changes order,
|
|
838
|
+
// so the BM25 gap no longer reflects true relevance — the keyword match is extra signal).
|
|
824
839
|
if (results.length >= 2) {
|
|
825
840
|
const top1 = Math.abs(results[0].composite_score ?? results[0].relevance ?? 0);
|
|
826
841
|
const top2 = Math.abs(results[1].composite_score ?? results[1].relevance ?? 0);
|
|
827
|
-
|
|
842
|
+
// After keyword re-ranking, #1 may have lower raw BM25 than #2.
|
|
843
|
+
// Only skip gap check if #1 was actually promoted by a keyword match
|
|
844
|
+
// (not just any rawKeywords present with incidentally inverted scores).
|
|
845
|
+
const top1Tags = (results[0].intent_tags || '').toLowerCase();
|
|
846
|
+
const top1MatchesKw = signals?.rawKeywords?.some(kw => top1Tags.includes(kw));
|
|
847
|
+
const wasReRanked = top1MatchesKw && top1 < top2;
|
|
848
|
+
if (!wasReRanked && top1 > 0) {
|
|
828
849
|
const gapRatio = (top1 - top2) / top1;
|
|
829
850
|
if (gapRatio < 0.2) {
|
|
830
851
|
// Top-1 has no clear lead — ambiguous match, suppress recommendation
|
|
@@ -976,7 +997,24 @@ function decideTier(resource, signals) {
|
|
|
976
997
|
// Normalize: typical good matches score 5-50, great matches 20+
|
|
977
998
|
// Sigmoid-like mapping to 0-1 range
|
|
978
999
|
const normalized = raw / (raw + 5.0); // 5→0.5, 10→0.67, 20→0.8, 50→0.91
|
|
979
|
-
|
|
1000
|
+
|
|
1001
|
+
// Signal-based confidence floor: if the result passed structured intent matching
|
|
1002
|
+
// + keyword re-ranking, BM25 score alone shouldn't downgrade to 'silent'.
|
|
1003
|
+
// Small corpora produce low BM25 scores even for strong matches.
|
|
1004
|
+
let signalBoost = 0;
|
|
1005
|
+
if (signals?.primaryIntent) {
|
|
1006
|
+
const tags = (resource.intent_tags || '').toLowerCase().split(/[\s,]+/);
|
|
1007
|
+
// Direct intent match: resource's intent_tags contain the detected primary intent.
|
|
1008
|
+
// Strong boost (0.3) ensures small-corpus matches still reach 'hint' tier.
|
|
1009
|
+
if (tags.includes(signals.primaryIntent)) signalBoost += 0.3;
|
|
1010
|
+
else signalBoost += 0.1;
|
|
1011
|
+
}
|
|
1012
|
+
if (signals?.rawKeywords?.length > 0) {
|
|
1013
|
+
const tagArr = (resource.intent_tags || '').toLowerCase().split(/[\s,]+/);
|
|
1014
|
+
if (signals.rawKeywords.some(kw => tagArr.includes(kw))) signalBoost += 0.2;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const confidence = Math.min(1.0, normalized + patternBoost * 0.3 + signalBoost);
|
|
980
1018
|
|
|
981
1019
|
if (confidence >= 0.55) return 'full';
|
|
982
1020
|
if (confidence >= 0.3) return 'hint';
|
package/hook-update.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Skips in dev mode (symlinked installs). Silent on network failure.
|
|
4
4
|
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
|
-
import { readFileSync, writeFileSync, copyFileSync, readdirSync, existsSync, lstatSync, mkdirSync, rmSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
6
|
+
import { readFileSync, writeFileSync, copyFileSync, readdirSync, existsSync, lstatSync, mkdirSync, rmSync, renameSync } from 'node:fs';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { DB_DIR } from './schema.mjs';
|
|
10
10
|
import { debugCatch, debugLog } from './utils.mjs';
|
|
@@ -16,17 +16,29 @@ const STATE_FILE = join(INSTALL_DIR, 'runtime', 'update-state.json');
|
|
|
16
16
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
17
17
|
const FETCH_TIMEOUT_MS = 3000; // 3s network timeout
|
|
18
18
|
const RATE_LIMIT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h if rate-limited
|
|
19
|
+
const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
|
|
19
20
|
|
|
20
21
|
// ── Main Entry ─────────────────────────────────────────────
|
|
21
|
-
export async function checkForUpdate() {
|
|
22
|
+
export async function checkForUpdate(options = {}) {
|
|
22
23
|
try {
|
|
24
|
+
const pluginMode = isPluginMode();
|
|
25
|
+
const force = Boolean(options.force);
|
|
26
|
+
const allowInstall = options.allowInstall ?? !pluginMode;
|
|
27
|
+
|
|
23
28
|
if (isDevMode() || process.env.CLAUDE_MEM_SKIP_UPDATE) return null;
|
|
24
29
|
|
|
25
30
|
const state = readState();
|
|
26
|
-
if (!shouldCheck(state)) {
|
|
31
|
+
if (!force && !shouldCheck(state)) {
|
|
27
32
|
// Return cached update info if previously detected
|
|
28
33
|
if (state.updateAvailable && state.latestVersion) {
|
|
29
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
updateAvailable: true,
|
|
36
|
+
updated: false,
|
|
37
|
+
from: state.installedVersion,
|
|
38
|
+
to: state.latestVersion,
|
|
39
|
+
installDeferred: pluginMode || !allowInstall,
|
|
40
|
+
pluginMode,
|
|
41
|
+
};
|
|
30
42
|
}
|
|
31
43
|
return null;
|
|
32
44
|
}
|
|
@@ -42,7 +54,8 @@ export async function checkForUpdate() {
|
|
|
42
54
|
|
|
43
55
|
if (hasUpdate) {
|
|
44
56
|
debugLog('DEBUG', 'hook-update', `Update available: ${currentVersion} → ${latest.version}`);
|
|
45
|
-
const
|
|
57
|
+
const canInstall = !pluginMode && Boolean(allowInstall);
|
|
58
|
+
const success = canInstall ? await downloadAndInstall(latest.tarballUrl) : false;
|
|
46
59
|
const newState = {
|
|
47
60
|
lastCheck: new Date().toISOString(),
|
|
48
61
|
installedVersion: success ? latest.version : currentVersion,
|
|
@@ -58,6 +71,8 @@ export async function checkForUpdate() {
|
|
|
58
71
|
updated: success,
|
|
59
72
|
from: currentVersion,
|
|
60
73
|
to: latest.version,
|
|
74
|
+
installDeferred: !canInstall,
|
|
75
|
+
pluginMode,
|
|
61
76
|
};
|
|
62
77
|
}
|
|
63
78
|
|
|
@@ -76,6 +91,10 @@ export async function checkForUpdate() {
|
|
|
76
91
|
}
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
function isPluginMode() {
|
|
95
|
+
return Boolean(process.env.CLAUDE_PLUGIN_ROOT);
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
// ── Dev Mode Detection ─────────────────────────────────────
|
|
80
99
|
function isDevMode() {
|
|
81
100
|
try {
|
|
@@ -172,12 +191,13 @@ const SOURCE_FILES = [
|
|
|
172
191
|
'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
|
|
173
192
|
'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs',
|
|
174
193
|
'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs',
|
|
175
|
-
'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'skill.md',
|
|
194
|
+
'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
|
|
176
195
|
'registry.mjs', 'registry-scanner.mjs', 'registry-indexer.mjs',
|
|
177
196
|
'registry-retriever.mjs', 'resource-discovery.mjs',
|
|
178
|
-
'dispatch.mjs', 'dispatch-inject.mjs', 'dispatch-feedback.mjs', 'dispatch-workflow.mjs',
|
|
197
|
+
'dispatch.mjs', 'dispatch-inject.mjs', 'dispatch-feedback.mjs', 'dispatch-patterns.mjs', 'dispatch-workflow.mjs',
|
|
179
198
|
'install.mjs',
|
|
180
199
|
];
|
|
200
|
+
const SWITCHABLE_PATHS = [...SOURCE_FILES, 'scripts', 'registry', 'node_modules'];
|
|
181
201
|
|
|
182
202
|
// ── Download & Install ─────────────────────────────────────
|
|
183
203
|
// Direct file copy instead of running old install.mjs (avoids symlink overwrite in dev)
|
|
@@ -197,53 +217,113 @@ async function downloadAndInstall(tarballUrl) {
|
|
|
197
217
|
{ timeout: 30000, stdio: 'pipe' }
|
|
198
218
|
);
|
|
199
219
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
220
|
+
return installExtractedRelease(tmpDir);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
debugCatch(err, 'downloadAndInstall');
|
|
223
|
+
return false;
|
|
224
|
+
} finally {
|
|
225
|
+
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR) {
|
|
230
|
+
const ts = `${Date.now()}-${process.pid}`;
|
|
231
|
+
const stagingDir = join(targetDir, `.update-staging-${ts}`);
|
|
232
|
+
const backupDir = join(targetDir, `.update-backup-${ts}`);
|
|
233
|
+
const backedUp = [];
|
|
234
|
+
const installed = [];
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
238
|
+
mkdirSync(backupDir, { recursive: true });
|
|
239
|
+
|
|
240
|
+
copyReleaseIntoStaging(sourceDir, stagingDir);
|
|
241
|
+
execSync(NPM_INSTALL_CMD, {
|
|
242
|
+
cwd: stagingDir,
|
|
243
|
+
timeout: 60000,
|
|
244
|
+
stdio: 'pipe',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
for (const relPath of SWITCHABLE_PATHS) {
|
|
248
|
+
const stagedPath = join(stagingDir, relPath);
|
|
249
|
+
if (!existsSync(stagedPath)) continue;
|
|
250
|
+
|
|
251
|
+
const targetPath = join(targetDir, relPath);
|
|
252
|
+
const backupPath = join(backupDir, relPath);
|
|
211
253
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
copyFileSync(join(srcScripts, f), join(destScripts, f));
|
|
254
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
255
|
+
mkdirSync(dirname(backupPath), { recursive: true });
|
|
256
|
+
|
|
257
|
+
if (existsSync(targetPath)) {
|
|
258
|
+
renameSync(targetPath, backupPath);
|
|
259
|
+
backedUp.push(relPath);
|
|
219
260
|
}
|
|
261
|
+
|
|
262
|
+
renameSync(stagedPath, targetPath);
|
|
263
|
+
installed.push(relPath);
|
|
220
264
|
}
|
|
221
265
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
266
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
267
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
268
|
+
debugLog('DEBUG', 'hook-update', `Auto-update: switched ${installed.length} paths`);
|
|
269
|
+
return true;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
debugCatch(err, 'installExtractedRelease');
|
|
272
|
+
|
|
273
|
+
for (const relPath of installed.reverse()) {
|
|
274
|
+
try { rmSync(join(targetDir, relPath), { recursive: true, force: true }); } catch {}
|
|
275
|
+
}
|
|
276
|
+
for (const relPath of backedUp.reverse()) {
|
|
277
|
+
const backupPath = join(backupDir, relPath);
|
|
278
|
+
const targetPath = join(targetDir, relPath);
|
|
225
279
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
debugCatch(err, 'downloadAndInstall-npm');
|
|
233
|
-
// Non-fatal: old node_modules may still work
|
|
280
|
+
if (existsSync(backupPath)) {
|
|
281
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
282
|
+
renameSync(backupPath, targetPath);
|
|
283
|
+
}
|
|
284
|
+
} catch (restoreErr) {
|
|
285
|
+
debugCatch(restoreErr, `installExtractedRelease-restore-${relPath}`);
|
|
234
286
|
}
|
|
235
287
|
}
|
|
236
288
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
} catch (err) {
|
|
240
|
-
debugCatch(err, 'downloadAndInstall');
|
|
289
|
+
try { rmSync(stagingDir, { recursive: true, force: true }); } catch {}
|
|
290
|
+
try { rmSync(backupDir, { recursive: true, force: true }); } catch {}
|
|
241
291
|
return false;
|
|
242
|
-
} finally {
|
|
243
|
-
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
244
292
|
}
|
|
245
293
|
}
|
|
246
294
|
|
|
295
|
+
function copyReleaseIntoStaging(sourceDir, stagingDir) {
|
|
296
|
+
let copied = 0;
|
|
297
|
+
|
|
298
|
+
for (const f of SOURCE_FILES) {
|
|
299
|
+
const src = join(sourceDir, f);
|
|
300
|
+
const dest = join(stagingDir, f);
|
|
301
|
+
if (!existsSync(src)) continue;
|
|
302
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
303
|
+
copyFileSync(src, dest);
|
|
304
|
+
copied++;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const dirName of ['scripts', 'registry']) {
|
|
308
|
+
const srcDir = join(sourceDir, dirName);
|
|
309
|
+
const destDir = join(stagingDir, dirName);
|
|
310
|
+
if (!existsSync(srcDir)) continue;
|
|
311
|
+
mkdirSync(destDir, { recursive: true });
|
|
312
|
+
for (const entry of readdirSync(srcDir)) {
|
|
313
|
+
copyFileSync(join(srcDir, entry), join(destDir, entry));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const stagedScripts = join(stagingDir, 'scripts');
|
|
318
|
+
if (existsSync(stagedScripts)) {
|
|
319
|
+
for (const sf of readdirSync(stagedScripts).filter(n => n.endsWith('.sh'))) {
|
|
320
|
+
try { execSync(`chmod +x "${join(stagedScripts, sf)}"`, { stdio: 'pipe' }); } catch {}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
debugLog('DEBUG', 'hook-update', `Auto-update staged ${copied} source files`);
|
|
325
|
+
}
|
|
326
|
+
|
|
247
327
|
// ── State Persistence ──────────────────────────────────────
|
|
248
328
|
function readState() {
|
|
249
329
|
try {
|
package/hook.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { randomUUID } from 'crypto';
|
|
8
8
|
import { join, basename } from 'path';
|
|
9
9
|
import { readFileSync, writeFileSync, unlinkSync, readdirSync, renameSync, statSync } from 'fs';
|
|
10
|
+
import { homedir } from 'os';
|
|
10
11
|
import {
|
|
11
12
|
truncate, typeIcon, inferProject, detectBashSignificance,
|
|
12
13
|
extractErrorKeywords, extractFilePaths, isRelatedToEpisode,
|
|
@@ -40,6 +41,22 @@ import { checkForUpdate } from './hook-update.mjs';
|
|
|
40
41
|
// Background workers (llm-episode, llm-summary, resource-scan) are exempt — they're ours
|
|
41
42
|
const event = process.argv[2];
|
|
42
43
|
const BG_EVENTS = new Set(['llm-episode', 'llm-summary', 'resource-scan']);
|
|
44
|
+
|
|
45
|
+
// Respect Claude Code plugin disable state even when legacy settings.json hooks remain.
|
|
46
|
+
// install.mjs writes direct hooks into ~/.claude/settings.json, so disabling the plugin
|
|
47
|
+
// in Claude UI does not automatically remove them. Exit early to make disable actually work.
|
|
48
|
+
const PLUGIN_KEY = 'claude-mem-lite@sdsrss';
|
|
49
|
+
function isPluginExplicitlyDisabled() {
|
|
50
|
+
try {
|
|
51
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
52
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
53
|
+
return settings.enabledPlugins?.[PLUGIN_KEY] === false;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (event && isPluginExplicitlyDisabled()) process.exit(0);
|
|
43
60
|
if (process.env.CLAUDE_MEM_HOOK_RUNNING && !BG_EVENTS.has(event)) process.exit(0);
|
|
44
61
|
|
|
45
62
|
// Crash-safe: flush episode buffer on unexpected termination to prevent data loss
|
|
@@ -820,7 +837,10 @@ async function handleSessionStart() {
|
|
|
820
837
|
if (updateResult?.updated) {
|
|
821
838
|
process.stdout.write(`\n🔄 claude-mem-lite: v${updateResult.from} → v${updateResult.to} updated\n`);
|
|
822
839
|
} else if (updateResult?.updateAvailable) {
|
|
823
|
-
|
|
840
|
+
const hint = updateResult.installDeferred
|
|
841
|
+
? ' — plugin mode only checks for updates; reinstall/update the plugin to apply it'
|
|
842
|
+
: '';
|
|
843
|
+
process.stdout.write(`\n📦 claude-mem-lite: v${updateResult.to} available (current: v${updateResult.from})${hint}\n`);
|
|
824
844
|
}
|
|
825
845
|
} catch (e) { debugCatch(e, 'session-start-update'); }
|
|
826
846
|
|
package/install.mjs
CHANGED
|
@@ -22,6 +22,8 @@ const INSTALL_DIR = DATA_DIR;
|
|
|
22
22
|
const SERVER_PATH = join(INSTALL_DIR, 'server.mjs');
|
|
23
23
|
const HOOK_PATH = join(INSTALL_DIR, 'hook.mjs');
|
|
24
24
|
const MARKETPLACE_KEY = 'sdsrss';
|
|
25
|
+
const PLUGIN_KEY = `claude-mem-lite@${MARKETPLACE_KEY}`;
|
|
26
|
+
const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
|
|
25
27
|
|
|
26
28
|
// ─── Curated Resource Metadata ───────────────────────────────────────────────
|
|
27
29
|
// Replaces generic name-echo fallback with FTS5-optimized metadata per resource.
|
|
@@ -2033,8 +2035,8 @@ function registerVirtualResources(rdb) {
|
|
|
2033
2035
|
return count;
|
|
2034
2036
|
}
|
|
2035
2037
|
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
+
let cmd = process.argv[2];
|
|
2039
|
+
let flags = new Set(process.argv.slice(3));
|
|
2038
2040
|
|
|
2039
2041
|
function log(msg) { console.log(` ${msg}`); }
|
|
2040
2042
|
function ok(msg) { console.log(` ✓ ${msg}`); }
|
|
@@ -2063,7 +2065,7 @@ async function install() {
|
|
|
2063
2065
|
'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
|
|
2064
2066
|
'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs',
|
|
2065
2067
|
'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs',
|
|
2066
|
-
'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'skill.md',
|
|
2068
|
+
'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
|
|
2067
2069
|
'registry.mjs', 'registry-scanner.mjs', 'registry-indexer.mjs',
|
|
2068
2070
|
'registry-retriever.mjs', 'resource-discovery.mjs',
|
|
2069
2071
|
'dispatch.mjs', 'dispatch-inject.mjs', 'dispatch-feedback.mjs', 'dispatch-patterns.mjs', 'dispatch-workflow.mjs',
|
|
@@ -2120,17 +2122,15 @@ async function install() {
|
|
|
2120
2122
|
// 2. npm install (skip for --dev: node_modules is symlinked)
|
|
2121
2123
|
if (IS_DEV) {
|
|
2122
2124
|
ok('Dependencies: using dev dir (symlinked)');
|
|
2123
|
-
} else
|
|
2124
|
-
log('
|
|
2125
|
+
} else {
|
|
2126
|
+
log('Ensuring dependencies installed...');
|
|
2125
2127
|
try {
|
|
2126
|
-
execSync(
|
|
2128
|
+
execSync(NPM_INSTALL_CMD, { cwd: INSTALL_DIR, stdio: 'pipe' });
|
|
2127
2129
|
ok('Dependencies installed');
|
|
2128
2130
|
} catch (e) {
|
|
2129
2131
|
fail('npm install failed: ' + e.message);
|
|
2130
2132
|
process.exit(1);
|
|
2131
2133
|
}
|
|
2132
|
-
} else {
|
|
2133
|
-
ok('Dependencies already installed');
|
|
2134
2134
|
}
|
|
2135
2135
|
|
|
2136
2136
|
// 3. Register MCP server
|
|
@@ -2187,6 +2187,9 @@ async function install() {
|
|
|
2187
2187
|
// 4. Configure hooks (merge: preserve user's existing hooks, replace ours)
|
|
2188
2188
|
log('Configuring hooks...');
|
|
2189
2189
|
const settings = readSettings();
|
|
2190
|
+
if (clearPluginDisabledMarkerForDirectInstall(settings)) {
|
|
2191
|
+
ok('Cleared stale disabled plugin flag so install.mjs-managed hooks can run');
|
|
2192
|
+
}
|
|
2190
2193
|
settings.hooks = settings.hooks || {};
|
|
2191
2194
|
|
|
2192
2195
|
const PREFILTER_PATH = join(INSTALL_DIR, 'scripts', 'post-tool-use.sh');
|
|
@@ -2543,74 +2546,72 @@ async function uninstall() {
|
|
|
2543
2546
|
|
|
2544
2547
|
// 2. Remove hooks from settings.json (match both npx and git-clone install paths)
|
|
2545
2548
|
const settings = readSettings();
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2549
|
+
cleanupMemHooksFromSettings(settings);
|
|
2550
|
+
|
|
2551
|
+
// 3. Clean plugin registry entries conservatively (avoid deleting other plugins
|
|
2552
|
+
// from the same marketplace publisher)
|
|
2553
|
+
const pluginsDir = join(homedir(), '.claude', 'plugins');
|
|
2554
|
+
const installedPath = join(pluginsDir, 'installed_plugins.json');
|
|
2555
|
+
let canRemoveMarketplaceArtifacts;
|
|
2556
|
+
try {
|
|
2557
|
+
const installed = JSON.parse(readFileSync(installedPath, 'utf8'));
|
|
2558
|
+
const plugins = getInstalledPluginEntries(installed);
|
|
2559
|
+
let cleaned = false;
|
|
2560
|
+
if (PLUGIN_KEY in plugins) {
|
|
2561
|
+
delete plugins[PLUGIN_KEY];
|
|
2562
|
+
cleaned = true;
|
|
2551
2563
|
}
|
|
2552
|
-
|
|
2564
|
+
canRemoveMarketplaceArtifacts = !hasOtherMarketplacePlugins(installed);
|
|
2565
|
+
if (cleaned) {
|
|
2566
|
+
writeFileSync(installedPath, JSON.stringify(installed, null, 2) + '\n');
|
|
2567
|
+
ok('Removed from installed_plugins.json');
|
|
2568
|
+
}
|
|
2569
|
+
} catch {
|
|
2570
|
+
// Conservative default: if registry shape is unknown, preserve marketplace cache.
|
|
2571
|
+
canRemoveMarketplaceArtifacts = false;
|
|
2553
2572
|
}
|
|
2554
2573
|
|
|
2555
|
-
//
|
|
2556
|
-
const pluginKey = `claude-mem-lite@${MARKETPLACE_KEY}`;
|
|
2574
|
+
// 4. Clean plugin system entries from settings.json
|
|
2557
2575
|
const marketplaceKey = MARKETPLACE_KEY;
|
|
2558
2576
|
if (settings.enabledPlugins) {
|
|
2559
|
-
delete settings.enabledPlugins[
|
|
2577
|
+
delete settings.enabledPlugins[PLUGIN_KEY];
|
|
2560
2578
|
}
|
|
2561
|
-
if (settings.extraKnownMarketplaces) {
|
|
2579
|
+
if (settings.extraKnownMarketplaces && canRemoveMarketplaceArtifacts) {
|
|
2562
2580
|
delete settings.extraKnownMarketplaces[marketplaceKey];
|
|
2563
2581
|
}
|
|
2564
2582
|
writeSettings(settings);
|
|
2565
2583
|
ok('Hooks and plugin settings cleaned');
|
|
2566
2584
|
|
|
2567
|
-
//
|
|
2568
|
-
const pluginsDir = join(homedir(), '.claude', 'plugins');
|
|
2569
|
-
|
|
2570
|
-
// 4a. Remove marketplace directory
|
|
2585
|
+
// 5. Clean plugin system registry files (only if no other marketplace plugins remain)
|
|
2571
2586
|
const marketplaceDir = join(pluginsDir, 'marketplaces', marketplaceKey);
|
|
2572
|
-
if (existsSync(marketplaceDir)) {
|
|
2587
|
+
if (canRemoveMarketplaceArtifacts && existsSync(marketplaceDir)) {
|
|
2573
2588
|
rmSync(marketplaceDir, { recursive: true, force: true });
|
|
2574
2589
|
ok('Marketplace directory removed');
|
|
2575
2590
|
}
|
|
2576
2591
|
|
|
2577
|
-
//
|
|
2592
|
+
// 5b. Remove cache directory
|
|
2578
2593
|
const cacheDir = join(pluginsDir, 'cache', marketplaceKey);
|
|
2579
|
-
if (existsSync(cacheDir)) {
|
|
2594
|
+
if (canRemoveMarketplaceArtifacts && existsSync(cacheDir)) {
|
|
2580
2595
|
rmSync(cacheDir, { recursive: true, force: true });
|
|
2581
2596
|
ok('Plugin cache removed');
|
|
2582
2597
|
}
|
|
2583
2598
|
|
|
2584
|
-
//
|
|
2599
|
+
// 5c. Clean known_marketplaces.json
|
|
2585
2600
|
const knownPath = join(pluginsDir, 'known_marketplaces.json');
|
|
2586
2601
|
try {
|
|
2587
2602
|
const known = JSON.parse(readFileSync(knownPath, 'utf8'));
|
|
2588
|
-
if (marketplaceKey in known) {
|
|
2603
|
+
if (canRemoveMarketplaceArtifacts && marketplaceKey in known) {
|
|
2589
2604
|
delete known[marketplaceKey];
|
|
2590
2605
|
writeFileSync(knownPath, JSON.stringify(known, null, 2) + '\n');
|
|
2591
2606
|
ok('Removed from known_marketplaces.json');
|
|
2592
2607
|
}
|
|
2593
2608
|
} catch { /* file may not exist */ }
|
|
2594
2609
|
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
const installed = JSON.parse(readFileSync(installedPath, 'utf8'));
|
|
2599
|
-
const plugins = installed.plugins || installed;
|
|
2600
|
-
let cleaned = false;
|
|
2601
|
-
for (const key of Object.keys(plugins)) {
|
|
2602
|
-
if (key.includes('claude-mem-lite') || key.includes('sdsrss')) {
|
|
2603
|
-
delete plugins[key];
|
|
2604
|
-
cleaned = true;
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
if (cleaned) {
|
|
2608
|
-
writeFileSync(installedPath, JSON.stringify(installed, null, 2) + '\n');
|
|
2609
|
-
ok('Removed from installed_plugins.json');
|
|
2610
|
-
}
|
|
2611
|
-
} catch { /* file may not exist */ }
|
|
2610
|
+
if (!canRemoveMarketplaceArtifacts && (existsSync(marketplaceDir) || existsSync(cacheDir))) {
|
|
2611
|
+
log('Marketplace cache preserved (other plugins may still depend on sdsrss marketplace)');
|
|
2612
|
+
}
|
|
2612
2613
|
|
|
2613
|
-
//
|
|
2614
|
+
// 6. Purge data if requested
|
|
2614
2615
|
if (flags.has('--purge')) {
|
|
2615
2616
|
const expectedPurgePath = join(homedir(), '.claude-mem-lite');
|
|
2616
2617
|
if (existsSync(DATA_DIR) && DATA_DIR === expectedPurgePath) {
|
|
@@ -2626,6 +2627,24 @@ async function uninstall() {
|
|
|
2626
2627
|
console.log('\n Done!\n');
|
|
2627
2628
|
}
|
|
2628
2629
|
|
|
2630
|
+
// ─── Cleanup Hooks ───────────────────────────────────────────────────────────
|
|
2631
|
+
|
|
2632
|
+
async function cleanupHooks() {
|
|
2633
|
+
console.log('\nclaude-mem-lite cleanup-hooks\n');
|
|
2634
|
+
|
|
2635
|
+
const settings = readSettings();
|
|
2636
|
+
const removed = cleanupMemHooksFromSettings(settings);
|
|
2637
|
+
|
|
2638
|
+
if (removed > 0) {
|
|
2639
|
+
writeSettings(settings);
|
|
2640
|
+
ok(`Removed ${removed} claude-mem-lite hook configuration${removed === 1 ? '' : 's'} from settings.json`);
|
|
2641
|
+
} else {
|
|
2642
|
+
ok('No claude-mem-lite hooks found in settings.json');
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
console.log('');
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2629
2648
|
// ─── Status ─────────────────────────────────────────────────────────────────
|
|
2630
2649
|
|
|
2631
2650
|
async function status() {
|
|
@@ -2645,11 +2664,24 @@ async function status() {
|
|
|
2645
2664
|
|
|
2646
2665
|
// Hooks
|
|
2647
2666
|
const settings = readSettings();
|
|
2648
|
-
const hasHooks =
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2667
|
+
const hasHooks = hasMemHooksConfigured(settings);
|
|
2668
|
+
const pluginDisabled = isPluginExplicitlyDisabled(settings);
|
|
2669
|
+
const pluginEnabled = settings.enabledPlugins?.[PLUGIN_KEY] === true;
|
|
2670
|
+
|
|
2671
|
+
if (pluginEnabled) {
|
|
2672
|
+
ok('Plugin: enabled in settings');
|
|
2673
|
+
} else if (pluginDisabled) {
|
|
2674
|
+
warn('Plugin: disabled in settings');
|
|
2675
|
+
} else {
|
|
2676
|
+
warn('Plugin: not present in enabledPlugins');
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
if (hasHooks && pluginDisabled) {
|
|
2680
|
+
warn('Hooks: still configured in settings.json while plugin is disabled (runtime ignores them; run cleanup-hooks or uninstall to clean up)');
|
|
2681
|
+
} else if (hasHooks) {
|
|
2652
2682
|
ok('Hooks: configured');
|
|
2683
|
+
} else if (pluginDisabled) {
|
|
2684
|
+
ok('Hooks: not configured');
|
|
2653
2685
|
} else {
|
|
2654
2686
|
fail('Hooks: not configured');
|
|
2655
2687
|
}
|
|
@@ -2727,6 +2759,21 @@ async function doctor() {
|
|
|
2727
2759
|
issues++;
|
|
2728
2760
|
}
|
|
2729
2761
|
|
|
2762
|
+
// Plugin/hook lifecycle state
|
|
2763
|
+
const settings = readSettings();
|
|
2764
|
+
const hasHooks = hasMemHooksConfigured(settings);
|
|
2765
|
+
const pluginDisabled = isPluginExplicitlyDisabled(settings);
|
|
2766
|
+
if (pluginDisabled && hasHooks) {
|
|
2767
|
+
fail('Plugin lifecycle: plugin is disabled but claude-mem-lite hooks still remain in settings.json');
|
|
2768
|
+
issues++;
|
|
2769
|
+
} else if (pluginDisabled) {
|
|
2770
|
+
ok('Plugin lifecycle: disabled cleanly (no active mem hooks)');
|
|
2771
|
+
} else if (hasHooks) {
|
|
2772
|
+
ok('Plugin lifecycle: hooks active');
|
|
2773
|
+
} else {
|
|
2774
|
+
warn('Plugin lifecycle: hooks not configured');
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2730
2777
|
// Database
|
|
2731
2778
|
if (existsSync(DB_PATH)) {
|
|
2732
2779
|
try {
|
|
@@ -2799,6 +2846,50 @@ function isMemHook(cfg) {
|
|
|
2799
2846
|
});
|
|
2800
2847
|
}
|
|
2801
2848
|
|
|
2849
|
+
function hasMemHooksConfigured(settings) {
|
|
2850
|
+
if (!settings?.hooks) return false;
|
|
2851
|
+
return Object.values(settings.hooks).some(configs =>
|
|
2852
|
+
Array.isArray(configs) && configs.some(cfg => isMemHook(cfg))
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
export function clearPluginDisabledMarkerForDirectInstall(settings) {
|
|
2857
|
+
if (settings?.enabledPlugins?.[PLUGIN_KEY] !== false) return false;
|
|
2858
|
+
delete settings.enabledPlugins[PLUGIN_KEY];
|
|
2859
|
+
if (Object.keys(settings.enabledPlugins).length === 0) delete settings.enabledPlugins;
|
|
2860
|
+
return true;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
function cleanupMemHooksFromSettings(settings) {
|
|
2864
|
+
if (!settings?.hooks) return 0;
|
|
2865
|
+
|
|
2866
|
+
let removed = 0;
|
|
2867
|
+
for (const [event, configs] of Object.entries(settings.hooks)) {
|
|
2868
|
+
if (!Array.isArray(configs)) continue;
|
|
2869
|
+
const kept = configs.filter(cfg => !isMemHook(cfg));
|
|
2870
|
+
removed += configs.length - kept.length;
|
|
2871
|
+
if (kept.length > 0) settings.hooks[event] = kept;
|
|
2872
|
+
else delete settings.hooks[event];
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
2876
|
+
return removed;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
function isPluginExplicitlyDisabled(settings) {
|
|
2880
|
+
return settings?.enabledPlugins?.[PLUGIN_KEY] === false;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
function getInstalledPluginEntries(installed) {
|
|
2884
|
+
if (installed?.plugins && typeof installed.plugins === 'object') return installed.plugins;
|
|
2885
|
+
return installed && typeof installed === 'object' ? installed : {};
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
export function hasOtherMarketplacePlugins(installed, marketplaceKey = MARKETPLACE_KEY, pluginKey = PLUGIN_KEY) {
|
|
2889
|
+
const plugins = getInstalledPluginEntries(installed);
|
|
2890
|
+
return Object.keys(plugins).some(key => key !== pluginKey && key.endsWith(`@${marketplaceKey}`));
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2802
2893
|
function readSettings() {
|
|
2803
2894
|
try {
|
|
2804
2895
|
return JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
|
|
@@ -2823,10 +2914,12 @@ async function manualUpdate() {
|
|
|
2823
2914
|
// Force check by importing hook-update (bypasses throttle for manual use)
|
|
2824
2915
|
const { checkForUpdate, getCurrentVersion } = await import('./hook-update.mjs');
|
|
2825
2916
|
log('Checking for updates...');
|
|
2826
|
-
const result = await checkForUpdate();
|
|
2917
|
+
const result = await checkForUpdate({ force: true, allowInstall: true });
|
|
2827
2918
|
|
|
2828
2919
|
if (result?.updated) {
|
|
2829
2920
|
ok(`Updated: v${result.from} → v${result.to}`);
|
|
2921
|
+
} else if (result?.updateAvailable && result?.installDeferred) {
|
|
2922
|
+
warn(`v${result.to} available — plugin mode only checks for updates, reinstall/update the plugin to apply it`);
|
|
2830
2923
|
} else if (result?.updateAvailable) {
|
|
2831
2924
|
warn(`v${result.to} available but install failed — try: node install.mjs install`);
|
|
2832
2925
|
} else {
|
|
@@ -2881,31 +2974,38 @@ function syncVersions() {
|
|
|
2881
2974
|
|
|
2882
2975
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
2883
2976
|
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
break;
|
|
2891
|
-
case 'status':
|
|
2892
|
-
await status();
|
|
2893
|
-
break;
|
|
2894
|
-
case 'doctor':
|
|
2895
|
-
await doctor();
|
|
2896
|
-
break;
|
|
2897
|
-
case 'update':
|
|
2898
|
-
await manualUpdate();
|
|
2899
|
-
break;
|
|
2900
|
-
case 'release':
|
|
2901
|
-
syncVersions();
|
|
2902
|
-
break;
|
|
2903
|
-
default:
|
|
2904
|
-
if (IS_NPX) {
|
|
2905
|
-
// npx claude-mem-lite (no args) → auto install
|
|
2977
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
2978
|
+
cmd = argv[0];
|
|
2979
|
+
flags = new Set(argv.slice(1));
|
|
2980
|
+
|
|
2981
|
+
switch (cmd) {
|
|
2982
|
+
case 'install':
|
|
2906
2983
|
await install();
|
|
2907
|
-
|
|
2908
|
-
|
|
2984
|
+
break;
|
|
2985
|
+
case 'uninstall':
|
|
2986
|
+
await uninstall();
|
|
2987
|
+
break;
|
|
2988
|
+
case 'status':
|
|
2989
|
+
await status();
|
|
2990
|
+
break;
|
|
2991
|
+
case 'doctor':
|
|
2992
|
+
await doctor();
|
|
2993
|
+
break;
|
|
2994
|
+
case 'cleanup-hooks':
|
|
2995
|
+
await cleanupHooks();
|
|
2996
|
+
break;
|
|
2997
|
+
case 'update':
|
|
2998
|
+
await manualUpdate();
|
|
2999
|
+
break;
|
|
3000
|
+
case 'release':
|
|
3001
|
+
syncVersions();
|
|
3002
|
+
break;
|
|
3003
|
+
default:
|
|
3004
|
+
if (IS_NPX) {
|
|
3005
|
+
// npx claude-mem-lite (no args) → auto install
|
|
3006
|
+
await install();
|
|
3007
|
+
} else {
|
|
3008
|
+
console.log(`
|
|
2909
3009
|
claude-mem-lite — Lightweight memory system for Claude Code
|
|
2910
3010
|
|
|
2911
3011
|
Usage:
|
|
@@ -2915,10 +3015,15 @@ Usage:
|
|
|
2915
3015
|
node install.mjs uninstall --purge Remove and delete all data
|
|
2916
3016
|
node install.mjs status Show current status
|
|
2917
3017
|
node install.mjs doctor Diagnose issues
|
|
3018
|
+
node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
|
|
2918
3019
|
node install.mjs update Check for and install updates
|
|
2919
3020
|
node install.mjs release Sync version to plugin.json + marketplace.json
|
|
2920
3021
|
|
|
2921
3022
|
npx claude-mem-lite Install via npx (one-liner)
|
|
2922
3023
|
`);
|
|
2923
|
-
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
2924
3026
|
}
|
|
3027
|
+
|
|
3028
|
+
const IS_MAIN = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
3029
|
+
if (IS_MAIN) await main();
|
package/package.json
CHANGED
package/registry-retriever.mjs
CHANGED
|
@@ -23,6 +23,7 @@ export const DISPATCH_SYNONYMS = {
|
|
|
23
23
|
'plan': ['planning', 'architecture', 'spec', 'blueprint', 'rfc', 'proposal', 'roadmap'],
|
|
24
24
|
'build': ['compile', 'bundle', 'webpack', 'vite', 'typescript', 'tsc', 'esbuild', 'rollup', 'parcel', 'babel', 'swc', 'transpile'],
|
|
25
25
|
'lint': ['eslint', 'prettier', 'biome', 'stylelint', 'format', 'style'],
|
|
26
|
+
'search': ['lookup', 'latest', 'best-practices', 'perplexity'],
|
|
26
27
|
// Chinese intent mappings
|
|
27
28
|
'清理': ['refactor', 'clean', 'lint', 'format', 'simplify'],
|
|
28
29
|
'测试': ['test', 'testing', 'tdd', 'qa', 'spec', 'jest', 'vitest', 'pytest'],
|
|
@@ -44,6 +45,7 @@ export const DISPATCH_SYNONYMS = {
|
|
|
44
45
|
'打包': ['bundle', 'build', 'webpack', 'vite'],
|
|
45
46
|
'容器': ['docker', 'container', 'kubernetes', 'infrastructure'],
|
|
46
47
|
'运维': ['devops', 'infrastructure', 'deploy', 'docker'],
|
|
48
|
+
'搜索': ['search', 'lookup', 'latest', 'perplexity'],
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
// ─── CJK Tokenization ───────────────────────────────────────────────────────
|
|
@@ -98,6 +100,9 @@ const CJK_INTENT_MAP = {
|
|
|
98
100
|
'接口': 'api', '路由': 'route',
|
|
99
101
|
// plan
|
|
100
102
|
'规划': 'planning', '架构': 'architecture', '方案': 'plan', '设计方案': 'architecture',
|
|
103
|
+
// search — only web/info search, NOT code search (grep/find)
|
|
104
|
+
'联网搜索': 'search', '网上搜索': 'search', '查资料': 'search', '找资料': 'search',
|
|
105
|
+
'搜索最新': 'search', '搜索资料': 'search', '搜索文档': 'search',
|
|
101
106
|
};
|
|
102
107
|
|
|
103
108
|
// Merge all CJK keys from both maps, longest-first to avoid partial matches
|