claude-mem-lite 2.5.4 → 2.9.2
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/.mcp.json +0 -0
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/README.zh-CN.md +0 -0
- package/commands/mem.md +0 -0
- package/commands/memory.md +0 -0
- package/commands/tools.md +0 -0
- package/commands/update.md +0 -0
- package/dispatch-feedback.mjs +129 -24
- package/dispatch-inject.mjs +73 -34
- package/dispatch-patterns.mjs +173 -0
- package/dispatch-workflow.mjs +0 -0
- package/dispatch.mjs +359 -271
- package/haiku-client.mjs +0 -0
- package/hook-context.mjs +24 -6
- package/hook-episode.mjs +2 -2
- package/hook-handoff.mjs +38 -18
- package/hook-llm.mjs +98 -21
- package/hook-memory.mjs +47 -15
- package/hook-semaphore.mjs +0 -0
- package/hook-shared.mjs +21 -0
- package/hook-update.mjs +262 -0
- package/hook.mjs +165 -28
- package/hooks/hooks.json +0 -0
- package/install.mjs +149 -4
- package/package.json +3 -1
- package/registry/preinstalled.json +13 -0
- package/registry-indexer.mjs +0 -0
- package/registry-retriever.mjs +13 -8
- package/registry-scanner.mjs +0 -0
- package/registry.mjs +15 -7
- package/resource-discovery.mjs +0 -0
- package/schema.mjs +0 -0
- package/scripts/launch.mjs +0 -0
- package/server-internals.mjs +0 -0
- package/server.mjs +58 -13
- package/skill.md +0 -0
- package/tool-schemas.mjs +41 -16
- package/utils.mjs +87 -30
package/hook-update.mjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// claude-mem-lite: Auto-update via GitHub Releases
|
|
2
|
+
// Checks for new versions on SessionStart, downloads and installs automatically.
|
|
3
|
+
// Skips in dev mode (symlinked installs). Silent on network failure.
|
|
4
|
+
|
|
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';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { DB_DIR } from './schema.mjs';
|
|
10
|
+
import { debugCatch, debugLog } from './utils.mjs';
|
|
11
|
+
|
|
12
|
+
// ── Configuration ──────────────────────────────────────────
|
|
13
|
+
const GITHUB_REPO = 'sdsrss/claude-mem-lite';
|
|
14
|
+
const INSTALL_DIR = DB_DIR; // ~/.claude-mem-lite/
|
|
15
|
+
const STATE_FILE = join(INSTALL_DIR, 'runtime', 'update-state.json');
|
|
16
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
17
|
+
const FETCH_TIMEOUT_MS = 3000; // 3s network timeout
|
|
18
|
+
const RATE_LIMIT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h if rate-limited
|
|
19
|
+
|
|
20
|
+
// ── Main Entry ─────────────────────────────────────────────
|
|
21
|
+
export async function checkForUpdate() {
|
|
22
|
+
try {
|
|
23
|
+
if (isDevMode() || process.env.CLAUDE_MEM_SKIP_UPDATE) return null;
|
|
24
|
+
|
|
25
|
+
const state = readState();
|
|
26
|
+
if (!shouldCheck(state)) {
|
|
27
|
+
// Return cached update info if previously detected
|
|
28
|
+
if (state.updateAvailable && state.latestVersion) {
|
|
29
|
+
return { updateAvailable: true, from: state.installedVersion, to: state.latestVersion };
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const latest = await fetchLatestRelease();
|
|
35
|
+
if (!latest) {
|
|
36
|
+
saveState({ ...state, lastCheck: new Date().toISOString() });
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const currentVersion = getCurrentVersion();
|
|
41
|
+
const hasUpdate = compareVersions(latest.version, currentVersion) > 0;
|
|
42
|
+
|
|
43
|
+
if (hasUpdate) {
|
|
44
|
+
debugLog('DEBUG', 'hook-update', `Update available: ${currentVersion} → ${latest.version}`);
|
|
45
|
+
const success = await downloadAndInstall(latest.tarballUrl);
|
|
46
|
+
const newState = {
|
|
47
|
+
lastCheck: new Date().toISOString(),
|
|
48
|
+
installedVersion: success ? latest.version : currentVersion,
|
|
49
|
+
latestVersion: latest.version,
|
|
50
|
+
updateAvailable: !success,
|
|
51
|
+
lastUpdate: success ? new Date().toISOString() : (state.lastUpdate || null),
|
|
52
|
+
rateLimited: false,
|
|
53
|
+
};
|
|
54
|
+
saveState(newState);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
updateAvailable: !success,
|
|
58
|
+
updated: success,
|
|
59
|
+
from: currentVersion,
|
|
60
|
+
to: latest.version,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// No update needed
|
|
65
|
+
saveState({
|
|
66
|
+
...state,
|
|
67
|
+
lastCheck: new Date().toISOString(),
|
|
68
|
+
latestVersion: latest.version,
|
|
69
|
+
updateAvailable: false,
|
|
70
|
+
rateLimited: false,
|
|
71
|
+
});
|
|
72
|
+
return null;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
debugCatch(err, 'checkForUpdate');
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Dev Mode Detection ─────────────────────────────────────
|
|
80
|
+
function isDevMode() {
|
|
81
|
+
try {
|
|
82
|
+
const serverPath = join(INSTALL_DIR, 'server.mjs');
|
|
83
|
+
return existsSync(serverPath) && lstatSync(serverPath).isSymbolicLink();
|
|
84
|
+
} catch { return false; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Throttle ───────────────────────────────────────────────
|
|
88
|
+
function shouldCheck(state) {
|
|
89
|
+
if (!state.lastCheck) return true;
|
|
90
|
+
const elapsed = Date.now() - new Date(state.lastCheck).getTime();
|
|
91
|
+
const interval = state.rateLimited ? RATE_LIMIT_INTERVAL_MS : CHECK_INTERVAL_MS;
|
|
92
|
+
return elapsed >= interval;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── GitHub API ─────────────────────────────────────────────
|
|
96
|
+
// Try releases/latest first, fallback to tags (some repos only use tags)
|
|
97
|
+
async function fetchLatestRelease() {
|
|
98
|
+
const headers = {
|
|
99
|
+
'Accept': 'application/vnd.github+json',
|
|
100
|
+
'User-Agent': 'claude-mem-lite-updater/1.0',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Attempt 1: GitHub Releases API
|
|
104
|
+
const result = await fetchWithTimeout(
|
|
105
|
+
`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`,
|
|
106
|
+
headers,
|
|
107
|
+
);
|
|
108
|
+
if (result === 'rate-limited') return null;
|
|
109
|
+
if (result) {
|
|
110
|
+
return {
|
|
111
|
+
version: result.tag_name.replace(/^v/, ''),
|
|
112
|
+
tarballUrl: result.tarball_url,
|
|
113
|
+
releaseUrl: result.html_url,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Attempt 2: Tags API fallback (for repos without formal releases)
|
|
118
|
+
const tags = await fetchWithTimeout(
|
|
119
|
+
`https://api.github.com/repos/${GITHUB_REPO}/tags?per_page=1`,
|
|
120
|
+
headers,
|
|
121
|
+
);
|
|
122
|
+
if (tags === 'rate-limited') return null;
|
|
123
|
+
if (Array.isArray(tags) && tags.length > 0) {
|
|
124
|
+
const tag = tags[0];
|
|
125
|
+
return {
|
|
126
|
+
version: tag.name.replace(/^v/, ''),
|
|
127
|
+
tarballUrl: `https://api.github.com/repos/${GITHUB_REPO}/tarball/${tag.name}`,
|
|
128
|
+
releaseUrl: `https://github.com/${GITHUB_REPO}/releases/tag/${tag.name}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function fetchWithTimeout(url, headers) {
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(url, { signal: controller.signal, headers });
|
|
140
|
+
if (res.status === 403) {
|
|
141
|
+
const state = readState();
|
|
142
|
+
saveState({ ...state, rateLimited: true });
|
|
143
|
+
debugLog('DEBUG', 'hook-update', 'GitHub API rate limited, extending interval');
|
|
144
|
+
return 'rate-limited';
|
|
145
|
+
}
|
|
146
|
+
if (!res.ok) return null;
|
|
147
|
+
return await res.json();
|
|
148
|
+
} catch { return null; }
|
|
149
|
+
finally { clearTimeout(timeout); }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Version Comparison (semver) ────────────────────────────
|
|
153
|
+
export function compareVersions(a, b) {
|
|
154
|
+
const pa = String(a).replace(/^v/, '').split('.').map(Number);
|
|
155
|
+
const pb = String(b).replace(/^v/, '').split('.').map(Number);
|
|
156
|
+
for (let i = 0; i < 3; i++) {
|
|
157
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
158
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getCurrentVersion() {
|
|
164
|
+
try {
|
|
165
|
+
const pkg = JSON.parse(readFileSync(join(INSTALL_DIR, 'package.json'), 'utf8'));
|
|
166
|
+
return pkg.version;
|
|
167
|
+
} catch { return '0.0.0'; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Source files to copy (must match install.mjs SOURCE_FILES) ──
|
|
171
|
+
const SOURCE_FILES = [
|
|
172
|
+
'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
|
|
173
|
+
'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs',
|
|
174
|
+
'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',
|
|
176
|
+
'registry.mjs', 'registry-scanner.mjs', 'registry-indexer.mjs',
|
|
177
|
+
'registry-retriever.mjs', 'resource-discovery.mjs',
|
|
178
|
+
'dispatch.mjs', 'dispatch-inject.mjs', 'dispatch-feedback.mjs', 'dispatch-workflow.mjs',
|
|
179
|
+
'install.mjs',
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
// ── Download & Install ─────────────────────────────────────
|
|
183
|
+
// Direct file copy instead of running old install.mjs (avoids symlink overwrite in dev)
|
|
184
|
+
async function downloadAndInstall(tarballUrl) {
|
|
185
|
+
const tmpDir = join(tmpdir(), `claude-mem-lite-update-${Date.now()}`);
|
|
186
|
+
try {
|
|
187
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
188
|
+
|
|
189
|
+
// Download tarball via curl (available on all supported platforms)
|
|
190
|
+
// Validate URL to prevent command injection via crafted tarball URLs
|
|
191
|
+
if (!/^https:\/\/[a-zA-Z0-9./-]+$/.test(tarballUrl)) {
|
|
192
|
+
debugLog('WARN', 'hook-update', `Rejected suspicious tarball URL: ${tarballUrl}`);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
execSync(
|
|
196
|
+
`curl -sL -H "Accept: application/vnd.github+json" "${tarballUrl}" | tar xz -C "${tmpDir}" --strip-components=1`,
|
|
197
|
+
{ timeout: 30000, stdio: 'pipe' }
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Direct copy: overwrite source files in INSTALL_DIR
|
|
201
|
+
// Safer than running old install.mjs which may not respect CLAUDE_MEM_DIR
|
|
202
|
+
let copied = 0;
|
|
203
|
+
for (const f of SOURCE_FILES) {
|
|
204
|
+
const src = join(tmpDir, f);
|
|
205
|
+
const dest = join(INSTALL_DIR, f);
|
|
206
|
+
if (existsSync(src)) {
|
|
207
|
+
copyFileSync(src, dest);
|
|
208
|
+
copied++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Copy scripts/ directory if present
|
|
213
|
+
const srcScripts = join(tmpDir, 'scripts');
|
|
214
|
+
if (existsSync(srcScripts)) {
|
|
215
|
+
const destScripts = join(INSTALL_DIR, 'scripts');
|
|
216
|
+
mkdirSync(destScripts, { recursive: true });
|
|
217
|
+
for (const f of readdirSync(srcScripts)) {
|
|
218
|
+
copyFileSync(join(srcScripts, f), join(destScripts, f));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Run npm install for dependencies (skip if node_modules is a symlink = dev mode)
|
|
223
|
+
const nmPath = join(INSTALL_DIR, 'node_modules');
|
|
224
|
+
if (!existsSync(nmPath) || !lstatSync(nmPath).isSymbolicLink()) {
|
|
225
|
+
try {
|
|
226
|
+
execSync('npm install --omit=dev', {
|
|
227
|
+
cwd: INSTALL_DIR,
|
|
228
|
+
timeout: 60000,
|
|
229
|
+
stdio: 'pipe',
|
|
230
|
+
});
|
|
231
|
+
} catch (err) {
|
|
232
|
+
debugCatch(err, 'downloadAndInstall-npm');
|
|
233
|
+
// Non-fatal: old node_modules may still work
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
debugLog('DEBUG', 'hook-update', `Auto-update: ${copied} files copied`);
|
|
238
|
+
return true;
|
|
239
|
+
} catch (err) {
|
|
240
|
+
debugCatch(err, 'downloadAndInstall');
|
|
241
|
+
return false;
|
|
242
|
+
} finally {
|
|
243
|
+
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── State Persistence ──────────────────────────────────────
|
|
248
|
+
function readState() {
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
|
|
251
|
+
} catch {
|
|
252
|
+
return {};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function saveState(state) {
|
|
257
|
+
try {
|
|
258
|
+
const dir = join(INSTALL_DIR, 'runtime');
|
|
259
|
+
mkdirSync(dir, { recursive: true });
|
|
260
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
package/hook.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
truncate, typeIcon, inferProject, detectBashSignificance,
|
|
12
12
|
extractErrorKeywords, extractFilePaths, isRelatedToEpisode,
|
|
13
13
|
makeEntryDesc, scrubSecrets, EDIT_TOOLS, debugCatch, debugLog, fmtTime,
|
|
14
|
-
COMPRESSED_AUTO,
|
|
14
|
+
COMPRESSED_AUTO, COMPRESSED_PENDING_PURGE, isoWeekKey,
|
|
15
15
|
} from './utils.mjs';
|
|
16
16
|
import {
|
|
17
17
|
readEpisodeRaw, episodeFile,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
writePendingEntry, mergePendingEntries, episodeHasSignificantContent,
|
|
21
21
|
} from './hook-episode.mjs';
|
|
22
22
|
import { selectWithTokenBudget, updateClaudeMd, buildSummaryLines } from './hook-context.mjs';
|
|
23
|
-
import {
|
|
23
|
+
import { dispatchOnPreToolUse, dispatchOnUserPrompt } from './dispatch.mjs';
|
|
24
24
|
import { collectFeedback } from './dispatch-feedback.mjs';
|
|
25
25
|
import {
|
|
26
26
|
RUNTIME_DIR, EPISODE_BUFFER_SIZE, EPISODE_TIME_GAP_MS,
|
|
@@ -29,10 +29,12 @@ import {
|
|
|
29
29
|
sessionFile, getSessionId, createSessionId, openDb, getRegistryDb,
|
|
30
30
|
closeRegistryDb, spawnBackground, appendToolEvent, readAndClearToolEvents,
|
|
31
31
|
resetInjectionBudget, hasInjectionBudget, incrementInjection,
|
|
32
|
+
cachePrevContext, readAndClearPrevContext,
|
|
32
33
|
} from './hook-shared.mjs';
|
|
33
34
|
import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObservation } from './hook-llm.mjs';
|
|
34
35
|
import { searchRelevantMemories, recallForFile } from './hook-memory.mjs';
|
|
35
36
|
import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
37
|
+
import { checkForUpdate } from './hook-update.mjs';
|
|
36
38
|
|
|
37
39
|
// Prevent recursive hooks from background claude -p calls
|
|
38
40
|
// Background workers (llm-episode, llm-summary, resource-scan) are exempt — they're ours
|
|
@@ -150,7 +152,7 @@ async function handlePostToolUse() {
|
|
|
150
152
|
if (tool_name.startsWith('mem_') || tool_name.startsWith('mcp__mem__') || tool_name.startsWith('mcp__plugin_claude-mem-lite')) return;
|
|
151
153
|
if (tool_name.startsWith('mcp__sequential') || tool_name.startsWith('mcp__plugin_context7')) return;
|
|
152
154
|
|
|
153
|
-
const resp =
|
|
155
|
+
const resp = normalizeToolResponse(tool_response);
|
|
154
156
|
if (!resp || resp.length < 10) return;
|
|
155
157
|
|
|
156
158
|
const toolInput = typeof tool_input === 'string' ? tryParseJson(tool_input) : (tool_input || {});
|
|
@@ -354,6 +356,30 @@ async function handleStop() {
|
|
|
354
356
|
// Save handoff snapshot for cross-session continuity
|
|
355
357
|
try { buildAndSaveHandoff(db, sessionId, project, 'exit', episodeSnapshot); }
|
|
356
358
|
catch (e) { debugCatch(e, 'handleStop-handoff'); }
|
|
359
|
+
|
|
360
|
+
// Fast summary baseline — ensures summary exists even if background LLM fails
|
|
361
|
+
try {
|
|
362
|
+
const firstPrompt = db.prepare(`
|
|
363
|
+
SELECT prompt_text FROM user_prompts
|
|
364
|
+
WHERE content_session_id = ?
|
|
365
|
+
ORDER BY prompt_number ASC LIMIT 1
|
|
366
|
+
`).get(sessionId);
|
|
367
|
+
const recentObs = db.prepare(`
|
|
368
|
+
SELECT title FROM observations
|
|
369
|
+
WHERE memory_session_id = ? AND COALESCE(compressed_into, 0) = 0
|
|
370
|
+
ORDER BY created_at_epoch DESC LIMIT 5
|
|
371
|
+
`).all(sessionId);
|
|
372
|
+
const fastRequest = truncate(firstPrompt?.prompt_text || '', 200);
|
|
373
|
+
const fastCompleted = recentObs.map(o => o.title).filter(Boolean).join('; ');
|
|
374
|
+
if (fastRequest || fastCompleted) {
|
|
375
|
+
const now = new Date();
|
|
376
|
+
db.prepare(`
|
|
377
|
+
INSERT INTO session_summaries
|
|
378
|
+
(memory_session_id, project, request, investigated, learned, completed, next_steps, remaining_items, files_read, files_edited, notes, created_at, created_at_epoch)
|
|
379
|
+
VALUES (?, ?, ?, '', '', ?, '', '', '[]', '[]', 'fast', ?, ?)
|
|
380
|
+
`).run(sessionId, project, fastRequest, truncate(fastCompleted, 300), now.toISOString(), now.getTime());
|
|
381
|
+
}
|
|
382
|
+
} catch (e) { debugCatch(e, 'handleStop-fast-summary'); }
|
|
357
383
|
} finally {
|
|
358
384
|
db.close();
|
|
359
385
|
}
|
|
@@ -365,12 +391,9 @@ async function handleStop() {
|
|
|
365
391
|
// Always clear event file to prevent stale events accumulating if registry DB is unavailable.
|
|
366
392
|
try {
|
|
367
393
|
const sessionEvents = readAndClearToolEvents();
|
|
368
|
-
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
if (rdb) {
|
|
372
|
-
await collectFeedback(rdb, sessionId, sessionEvents);
|
|
373
|
-
}
|
|
394
|
+
const rdb = getRegistryDb();
|
|
395
|
+
if (rdb) {
|
|
396
|
+
await collectFeedback(rdb, sessionId, sessionEvents);
|
|
374
397
|
}
|
|
375
398
|
} catch (e) { debugCatch(e, 'handleStop-feedback'); }
|
|
376
399
|
|
|
@@ -464,6 +487,80 @@ async function handleSessionStart() {
|
|
|
464
487
|
}
|
|
465
488
|
})();
|
|
466
489
|
|
|
490
|
+
// Auto-purge: delete stale observations daily (COMPRESSED_PENDING_PURGE, 7-day retention)
|
|
491
|
+
const maintainFile = join(RUNTIME_DIR, 'last-auto-maintain.json');
|
|
492
|
+
let shouldMaintain = true;
|
|
493
|
+
try {
|
|
494
|
+
const last = JSON.parse(readFileSync(maintainFile, 'utf8'));
|
|
495
|
+
if (Date.now() - last.epoch < 24 * 3600000) shouldMaintain = false;
|
|
496
|
+
} catch {}
|
|
497
|
+
if (shouldMaintain) {
|
|
498
|
+
try {
|
|
499
|
+
const purged = db.prepare(`
|
|
500
|
+
DELETE FROM observations WHERE compressed_into = ${COMPRESSED_PENDING_PURGE}
|
|
501
|
+
AND created_at_epoch < ?
|
|
502
|
+
`).run(Date.now() - 7 * 86400000);
|
|
503
|
+
if (purged.changes > 0) {
|
|
504
|
+
debugLog('DEBUG', 'session-start', `auto-purged ${purged.changes} stale observations`);
|
|
505
|
+
}
|
|
506
|
+
// Auto-compress: group old marked observations into weekly summaries
|
|
507
|
+
const compressCutoff = Date.now() - 60 * 86400000; // 60 days
|
|
508
|
+
const compressCandidates = db.prepare(`
|
|
509
|
+
SELECT id, project, type, title, created_at_epoch
|
|
510
|
+
FROM observations
|
|
511
|
+
WHERE COALESCE(importance, 1) = 1 AND COALESCE(access_count, 0) = 0
|
|
512
|
+
AND created_at_epoch < ?
|
|
513
|
+
AND (compressed_into IS NULL OR compressed_into = ${COMPRESSED_AUTO})
|
|
514
|
+
ORDER BY project, created_at_epoch
|
|
515
|
+
`).all(compressCutoff);
|
|
516
|
+
if (compressCandidates.length >= 3) {
|
|
517
|
+
const groups = new Map();
|
|
518
|
+
for (const c of compressCandidates) {
|
|
519
|
+
const key = `${c.project}::${isoWeekKey(c.created_at_epoch)}`;
|
|
520
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
521
|
+
groups.get(key).push(c);
|
|
522
|
+
}
|
|
523
|
+
// Transact each group to prevent orphan summaries on crash
|
|
524
|
+
const compressGroup = db.transaction((proj, obs) => {
|
|
525
|
+
const types = {};
|
|
526
|
+
for (const o of obs) types[o.type] = (types[o.type] || 0) + 1;
|
|
527
|
+
const dominantType = Object.entries(types).sort((a, b) => b[1] - a[1])[0][0];
|
|
528
|
+
const title = `Weekly summary: ${obs.length} ${dominantType} observations`;
|
|
529
|
+
const narrative = obs.map(o => `- ${o.title || '(untitled)'}`).join('\n');
|
|
530
|
+
const sortedEpochs = obs.map(o => o.created_at_epoch).sort((a, b) => a - b);
|
|
531
|
+
const medianEpoch = sortedEpochs[Math.floor(sortedEpochs.length / 2)];
|
|
532
|
+
const sessionId = `compress-${proj}`;
|
|
533
|
+
const now = new Date();
|
|
534
|
+
db.prepare(`INSERT OR IGNORE INTO sdk_sessions
|
|
535
|
+
(content_session_id, memory_session_id, project, started_at, started_at_epoch, status)
|
|
536
|
+
VALUES (?,?,?,?,?,'active')`
|
|
537
|
+
).run(sessionId, sessionId, proj, now.toISOString(), now.getTime());
|
|
538
|
+
const summaryResult = db.prepare(`INSERT INTO observations
|
|
539
|
+
(memory_session_id, project, text, type, title, subtitle, narrative, concepts, facts,
|
|
540
|
+
files_read, files_modified, importance, created_at, created_at_epoch)
|
|
541
|
+
VALUES (?,?,?,?,?,'',?,'','','[]','[]',2,?,?)`
|
|
542
|
+
).run(sessionId, proj, narrative, dominantType, title, narrative, new Date(medianEpoch).toISOString(), medianEpoch);
|
|
543
|
+
const summaryId = Number(summaryResult.lastInsertRowid);
|
|
544
|
+
const obsIds = obs.map(o => o.id);
|
|
545
|
+
db.prepare(`UPDATE observations SET compressed_into = ? WHERE id IN (${obsIds.map(() => '?').join(',')})`)
|
|
546
|
+
.run(summaryId, ...obsIds);
|
|
547
|
+
return obs.length;
|
|
548
|
+
});
|
|
549
|
+
let totalCompressed = 0;
|
|
550
|
+
for (const [key, obs] of groups) {
|
|
551
|
+
if (obs.length < 3) continue;
|
|
552
|
+
const [proj] = key.split('::');
|
|
553
|
+
totalCompressed += compressGroup(proj, obs);
|
|
554
|
+
}
|
|
555
|
+
if (totalCompressed > 0) {
|
|
556
|
+
debugLog('DEBUG', 'session-start', `auto-compressed ${totalCompressed} observations into weekly summaries`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
writeFileSync(maintainFile, JSON.stringify({ epoch: Date.now() }));
|
|
561
|
+
} catch (e) { debugCatch(e, 'auto-maintain'); }
|
|
562
|
+
}
|
|
563
|
+
|
|
467
564
|
// ── Non-transactional operations (side effects, background work) ──
|
|
468
565
|
|
|
469
566
|
// Shared clear handoff reference — queried once, used by fast summary + working state
|
|
@@ -668,7 +765,8 @@ async function handleSessionStart() {
|
|
|
668
765
|
handoffLines.push('### Working State (from /clear)');
|
|
669
766
|
if (prevClearHandoff.working_on) handoffLines.push(`- Working on: ${truncate(prevClearHandoff.working_on, 200)}`);
|
|
670
767
|
if (prevClearHandoff.unfinished) {
|
|
671
|
-
|
|
768
|
+
const pendingSummary = extractUnfinishedSummary(prevClearHandoff.unfinished);
|
|
769
|
+
if (pendingSummary) handoffLines.push(`- Unfinished: ${truncate(pendingSummary, 200)}`);
|
|
672
770
|
}
|
|
673
771
|
if (prevClearHandoff.key_files) {
|
|
674
772
|
try {
|
|
@@ -701,20 +799,13 @@ async function handleSessionStart() {
|
|
|
701
799
|
// CLAUDE.md: slim (summary + handoff state — observations already in stdout)
|
|
702
800
|
updateClaudeMd([...summaryLines, ...handoffLines].join('\n'));
|
|
703
801
|
|
|
704
|
-
//
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const dispatchResult = await dispatchOnSessionStart(rdb, promptCtx, sessionId, { hasHandoff });
|
|
712
|
-
if (dispatchResult) {
|
|
713
|
-
process.stdout.write(dispatchResult + '\n');
|
|
714
|
-
incrementInjection();
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
} catch (e) { debugCatch(e, 'handleSessionStart-dispatch'); }
|
|
802
|
+
// Cache previous session context for user-prompt dispatch enrichment.
|
|
803
|
+
// Session-start has project history but zero user intent — dispatching here
|
|
804
|
+
// produced 0/119 adoption. Instead, cache next_steps and combine with
|
|
805
|
+
// the first user-prompt for richer signal (see handleUserPrompt).
|
|
806
|
+
if (latestSummary?.next_steps) {
|
|
807
|
+
cachePrevContext(latestSummary.next_steps);
|
|
808
|
+
}
|
|
718
809
|
|
|
719
810
|
// Background rescan: detect changed/new managed resources since last scan.
|
|
720
811
|
// TTL-based (1h) — avoids redundant filesystem scans on every session.
|
|
@@ -723,6 +814,16 @@ async function handleSessionStart() {
|
|
|
723
814
|
spawnBackground('resource-scan');
|
|
724
815
|
}
|
|
725
816
|
|
|
817
|
+
// Auto-update check (24h throttle, 3s timeout, silent on failure)
|
|
818
|
+
try {
|
|
819
|
+
const updateResult = await checkForUpdate();
|
|
820
|
+
if (updateResult?.updated) {
|
|
821
|
+
process.stdout.write(`\n🔄 claude-mem-lite: v${updateResult.from} → v${updateResult.to} updated\n`);
|
|
822
|
+
} else if (updateResult?.updateAvailable) {
|
|
823
|
+
process.stdout.write(`\n📦 claude-mem-lite: v${updateResult.to} available (current: v${updateResult.from})\n`);
|
|
824
|
+
}
|
|
825
|
+
} catch (e) { debugCatch(e, 'session-start-update'); }
|
|
826
|
+
|
|
726
827
|
} finally {
|
|
727
828
|
db.close();
|
|
728
829
|
}
|
|
@@ -860,13 +961,13 @@ async function handleUserPrompt() {
|
|
|
860
961
|
|
|
861
962
|
// Dispatch: recommend skill/agent based on user's actual prompt.
|
|
862
963
|
// This is the ideal dispatch point — fires when the user submits their prompt,
|
|
863
|
-
// before Claude starts working.
|
|
864
|
-
//
|
|
865
|
-
// Cooldown + session dedup (invocations table) prevents double-recommending with SessionStart.
|
|
964
|
+
// before Claude starts working. Previous session's next_steps (cached at session-start)
|
|
965
|
+
// enriches the signal when available, combining project history with user intent.
|
|
866
966
|
try {
|
|
867
967
|
const rdb = getRegistryDb();
|
|
868
968
|
if (rdb && hasInjectionBudget()) {
|
|
869
|
-
const
|
|
969
|
+
const prevContext = readAndClearPrevContext();
|
|
970
|
+
const result = await dispatchOnUserPrompt(rdb, promptText, sessionId, { prevContext });
|
|
870
971
|
if (result) {
|
|
871
972
|
process.stdout.write(result + '\n');
|
|
872
973
|
incrementInjection();
|
|
@@ -971,6 +1072,42 @@ function tryParseJson(str) {
|
|
|
971
1072
|
try { return JSON.parse(str); } catch { return {}; }
|
|
972
1073
|
}
|
|
973
1074
|
|
|
1075
|
+
// Strip ANSI escape codes and extract readable text from tool responses.
|
|
1076
|
+
// Bash responses come as {stdout, stderr} objects or JSON strings — extract the text content
|
|
1077
|
+
// instead of producing noisy `{"stdout":"\u001b[1m..."}` in episode descriptions.
|
|
1078
|
+
// eslint-disable-next-line no-control-regex
|
|
1079
|
+
const ANSI_RE = /\u001b\[[0-9;]*[a-zA-Z]/g;
|
|
1080
|
+
function extractStdio(obj) {
|
|
1081
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
1082
|
+
const { stdout, stderr } = obj;
|
|
1083
|
+
if (typeof stdout === 'string' || typeof stderr === 'string') {
|
|
1084
|
+
const parts = [];
|
|
1085
|
+
if (stdout) parts.push(stdout);
|
|
1086
|
+
if (stderr) parts.push(stderr);
|
|
1087
|
+
return parts.join('\n');
|
|
1088
|
+
}
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
function normalizeToolResponse(toolResponse) {
|
|
1092
|
+
if (typeof toolResponse === 'string') {
|
|
1093
|
+
// Try to parse JSON strings like '{"stdout":"...","stderr":"..."}'
|
|
1094
|
+
if (toolResponse.startsWith('{"stdout"') || toolResponse.startsWith('{"stderr"')) {
|
|
1095
|
+
try {
|
|
1096
|
+
const parsed = JSON.parse(toolResponse);
|
|
1097
|
+
const extracted = extractStdio(parsed);
|
|
1098
|
+
if (extracted) return extracted.replace(ANSI_RE, '');
|
|
1099
|
+
} catch {}
|
|
1100
|
+
}
|
|
1101
|
+
return toolResponse.replace(ANSI_RE, '');
|
|
1102
|
+
}
|
|
1103
|
+
if (toolResponse && typeof toolResponse === 'object') {
|
|
1104
|
+
const extracted = extractStdio(toolResponse);
|
|
1105
|
+
if (extracted) return extracted.replace(ANSI_RE, '');
|
|
1106
|
+
return JSON.stringify(toolResponse).replace(ANSI_RE, '');
|
|
1107
|
+
}
|
|
1108
|
+
return '';
|
|
1109
|
+
}
|
|
1110
|
+
|
|
974
1111
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
975
1112
|
|
|
976
1113
|
try {
|
package/hooks/hooks.json
CHANGED
|
File without changes
|