claude-mneme 2.9.1

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.
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Remove entries from the project's persistent remembered.json
4
+ *
5
+ * Usage:
6
+ * node mem-forget.mjs --list List all entries with indices
7
+ * node mem-forget.mjs --remove 0,2,3 Remove entries at indices
8
+ * node mem-forget.mjs --match "description" AI-assisted matching
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
12
+ import { ensureDeps, ensureMemoryDirs, loadConfig, getProjectName, invalidateCache, withFileLock } from './utils.mjs';
13
+
14
+ const cwd = process.cwd();
15
+ const paths = ensureMemoryDirs(cwd);
16
+ const projectName = getProjectName(cwd);
17
+ const config = loadConfig();
18
+
19
+ // Parse arguments
20
+ const args = process.argv.slice(2);
21
+ const mode = args[0];
22
+
23
+ // Read existing entries
24
+ function readEntries() {
25
+ if (!existsSync(paths.remembered)) {
26
+ return [];
27
+ }
28
+ try {
29
+ return JSON.parse(readFileSync(paths.remembered, 'utf-8'));
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ // Write entries back
36
+ function writeEntries(entries) {
37
+ writeFileSync(paths.remembered, JSON.stringify(entries, null, 2) + '\n');
38
+ invalidateCache(cwd);
39
+ }
40
+
41
+ // Format entry for display
42
+ function formatEntry(entry, index) {
43
+ const date = new Date(entry.ts).toLocaleDateString();
44
+ return `[${index}] (${entry.type}) ${entry.content} — ${date}`;
45
+ }
46
+
47
+ // List mode
48
+ if (mode === '--list') {
49
+ const entries = readEntries();
50
+ if (entries.length === 0) {
51
+ console.log('No remembered items for this project.');
52
+ process.exit(0);
53
+ }
54
+
55
+ // Output as JSON for easy parsing by Claude
56
+ const output = entries.map((entry, index) => ({
57
+ index,
58
+ type: entry.type,
59
+ content: entry.content,
60
+ date: new Date(entry.ts).toLocaleDateString()
61
+ }));
62
+ console.log(JSON.stringify(output, null, 2));
63
+ process.exit(0);
64
+ }
65
+
66
+ // Remove mode
67
+ if (mode === '--remove') {
68
+ const indicesArg = args[1];
69
+ if (!indicesArg) {
70
+ console.error('Usage: node mem-forget.mjs --remove 0,2,3');
71
+ process.exit(1);
72
+ }
73
+
74
+ const indices = indicesArg.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
75
+ if (indices.length === 0) {
76
+ console.error('No valid indices provided.');
77
+ process.exit(1);
78
+ }
79
+
80
+ // Read-modify-write under lock to prevent lost updates from concurrent sessions
81
+ const lockPath = paths.remembered + '.lock';
82
+ const removed = withFileLock(lockPath, () => {
83
+ const entries = readEntries();
84
+ if (entries.length === 0) {
85
+ return null;
86
+ }
87
+
88
+ const invalid = indices.filter(i => i < 0 || i >= entries.length);
89
+ if (invalid.length > 0) {
90
+ console.error(`Invalid indices: ${invalid.join(', ')}. Valid range: 0-${entries.length - 1}`);
91
+ process.exit(1);
92
+ }
93
+
94
+ const removedItems = [];
95
+ const sortedIndices = [...indices].sort((a, b) => b - a);
96
+ for (const idx of sortedIndices) {
97
+ removedItems.unshift(entries[idx]);
98
+ entries.splice(idx, 1);
99
+ }
100
+
101
+ writeEntries(entries);
102
+ return removedItems;
103
+ }, 10);
104
+
105
+ if (!removed || removed.length === 0) {
106
+ console.log('No remembered items to remove.');
107
+ process.exit(0);
108
+ }
109
+
110
+ console.log(`Removed ${removed.length} item(s) from "${projectName}":`);
111
+ for (const entry of removed) {
112
+ console.log(` - [${entry.type}] ${entry.content}`);
113
+ }
114
+ process.exit(0);
115
+ }
116
+
117
+ // Match mode (AI-assisted)
118
+ if (mode === '--match') {
119
+ const query = args.slice(1).join(' ');
120
+ if (!query) {
121
+ console.error('Usage: node mem-forget.mjs --match "description of what to forget"');
122
+ process.exit(1);
123
+ }
124
+
125
+ const entries = readEntries();
126
+ if (entries.length === 0) {
127
+ console.log(JSON.stringify({ matches: [], message: 'No remembered items to search.' }));
128
+ process.exit(0);
129
+ }
130
+
131
+ // Format entries for the AI prompt
132
+ const entriesText = entries.map((entry, index) =>
133
+ `[${index}] (${entry.type}) ${entry.content}`
134
+ ).join('\n');
135
+
136
+ const prompt = `You are helping identify which remembered items match a user's forget request.
137
+
138
+ Here are all the remembered items:
139
+ ${entriesText}
140
+
141
+ The user wants to forget: "${query}"
142
+
143
+ Your task: Return ONLY a JSON object with the indices of items that match what the user wants to forget.
144
+ Format: {"indices": [0, 2], "reason": "brief explanation"}
145
+
146
+ If no items match, return: {"indices": [], "reason": "No matching items found"}
147
+
148
+ Be conservative - only match items that clearly relate to what the user described.
149
+ Return ONLY the JSON object, no other text.`;
150
+
151
+ try {
152
+ ensureDeps();
153
+ const { query: agentQuery } = await import('@anthropic-ai/claude-agent-sdk');
154
+
155
+ async function* messageGenerator() {
156
+ yield {
157
+ type: 'user',
158
+ message: { role: 'user', content: prompt },
159
+ session_id: `memory-forget-${Date.now()}`,
160
+ parent_tool_use_id: null,
161
+ isSynthetic: true
162
+ };
163
+ }
164
+
165
+ const queryResult = agentQuery({
166
+ prompt: messageGenerator(),
167
+ options: {
168
+ model: config.model,
169
+ disallowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Task', 'TodoWrite'],
170
+ pathToClaudeCodeExecutable: config.claudePath
171
+ }
172
+ });
173
+
174
+ let response = '';
175
+
176
+ try {
177
+ for await (const message of queryResult) {
178
+ if (message.type === 'assistant') {
179
+ const content = message.message.content;
180
+ response = Array.isArray(content)
181
+ ? content.filter(c => c.type === 'text').map(c => c.text).join('\n')
182
+ : typeof content === 'string' ? content : '';
183
+ }
184
+ }
185
+ } catch (iterError) {
186
+ // Agent SDK may throw on process exit even after getting response
187
+ if (!response && iterError.message?.includes('process exited')) {
188
+ console.error('[claude-mneme] Agent SDK process exit');
189
+ } else if (!response) {
190
+ throw iterError;
191
+ }
192
+ }
193
+
194
+ // Parse AI response
195
+ if (response) {
196
+ // Extract JSON from response (in case there's extra text)
197
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
198
+ if (jsonMatch) {
199
+ try {
200
+ const result = JSON.parse(jsonMatch[0]);
201
+ // Enrich with entry details
202
+ if (result.indices && result.indices.length > 0) {
203
+ result.matches = result.indices.map(idx => ({
204
+ index: idx,
205
+ type: entries[idx]?.type,
206
+ content: entries[idx]?.content
207
+ })).filter(m => m.content); // Filter out invalid indices
208
+ result.indices = result.matches.map(m => m.index);
209
+ } else {
210
+ result.matches = [];
211
+ }
212
+ console.log(JSON.stringify(result, null, 2));
213
+ process.exit(0);
214
+ } catch {
215
+ // JSON parse failed
216
+ }
217
+ }
218
+ }
219
+
220
+ // Fallback if AI response wasn't parseable
221
+ console.log(JSON.stringify({
222
+ indices: [],
223
+ matches: [],
224
+ reason: 'Could not determine matching items. Please use --list and specify indices manually.'
225
+ }));
226
+
227
+ } catch (error) {
228
+ console.error(`[claude-mneme] AI matching error: ${error.message}`);
229
+ console.log(JSON.stringify({
230
+ indices: [],
231
+ matches: [],
232
+ reason: `AI matching failed: ${error.message}. Use --list and specify indices manually.`
233
+ }));
234
+ process.exit(1);
235
+ }
236
+
237
+ process.exit(0);
238
+ }
239
+
240
+ // No valid mode specified
241
+ console.error(`Usage:
242
+ node mem-forget.mjs --list List all entries with indices
243
+ node mem-forget.mjs --remove 0,2,3 Remove entries at indices
244
+ node mem-forget.mjs --match "description" AI-assisted matching`);
245
+ process.exit(1);
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Plugin Health Check Script
4
+ *
5
+ * Checks the status of the claude-mneme plugin:
6
+ * - Config validity
7
+ * - Claude binary availability
8
+ * - Memory directories
9
+ * - Recent errors
10
+ * - Summary and log status
11
+ *
12
+ * Usage: node mem-status.mjs [--clear-errors]
13
+ */
14
+
15
+ import { existsSync, readFileSync, statSync, accessSync, constants } from 'fs';
16
+ import { execFileSync } from 'child_process';
17
+ import {
18
+ MEMORY_BASE,
19
+ CONFIG_FILE,
20
+ ensureMemoryDirs,
21
+ loadConfig,
22
+ getProjectName,
23
+ getRecentErrors,
24
+ getErrorsSince,
25
+ clearErrorLog,
26
+ getErrorLogPath
27
+ } from './utils.mjs';
28
+
29
+ const cwd = process.cwd();
30
+ const clearErrors = process.argv.includes('--clear-errors');
31
+
32
+ // Clear errors if requested
33
+ if (clearErrors) {
34
+ const cleared = clearErrorLog();
35
+ console.log(JSON.stringify({
36
+ action: 'clear_errors',
37
+ success: cleared
38
+ }));
39
+ process.exit(0);
40
+ }
41
+
42
+ const status = {
43
+ project: getProjectName(cwd),
44
+ timestamp: new Date().toISOString(),
45
+ overall: 'healthy', // Will be downgraded if issues found
46
+ checks: {},
47
+ errors: [],
48
+ warnings: []
49
+ };
50
+
51
+ // ============================================================================
52
+ // Check 1: Config
53
+ // ============================================================================
54
+ try {
55
+ const config = loadConfig();
56
+ status.checks.config = {
57
+ status: 'ok',
58
+ path: CONFIG_FILE,
59
+ exists: existsSync(CONFIG_FILE),
60
+ model: config.model || 'haiku',
61
+ maxLogEntries: config.maxLogEntriesBeforeSummarize || 50
62
+ };
63
+
64
+ // Check for potentially problematic settings
65
+ // Only warn about claudePath if it looks like an absolute path that doesn't exist
66
+ if (config.claudePath && config.claudePath.startsWith('/') && !existsSync(config.claudePath)) {
67
+ status.checks.config.warning = `claudePath "${config.claudePath}" does not exist`;
68
+ status.warnings.push(`Config: claudePath not found at ${config.claudePath}`);
69
+ }
70
+ } catch (err) {
71
+ status.checks.config = { status: 'error', message: err.message };
72
+ status.errors.push(`Config: ${err.message}`);
73
+ status.overall = 'unhealthy';
74
+ }
75
+
76
+ // ============================================================================
77
+ // Check 2: Claude Binary
78
+ // ============================================================================
79
+ try {
80
+ const config = loadConfig();
81
+ let claudePath = config.claudePath || 'claude';
82
+
83
+ // Try to find claude in PATH if not specified
84
+ try {
85
+ const result = execFileSync('which', [claudePath.split('/').pop()], {
86
+ encoding: 'utf8',
87
+ stdio: ['ignore', 'pipe', 'ignore']
88
+ }).trim();
89
+
90
+ if (result) {
91
+ status.checks.claudeBinary = {
92
+ status: 'ok',
93
+ path: result,
94
+ configured: config.claudePath || '(using PATH)'
95
+ };
96
+ }
97
+ } catch {
98
+ // which failed, try the configured path directly
99
+ if (config.claudePath && existsSync(config.claudePath)) {
100
+ status.checks.claudeBinary = {
101
+ status: 'ok',
102
+ path: config.claudePath,
103
+ configured: config.claudePath
104
+ };
105
+ } else {
106
+ status.checks.claudeBinary = {
107
+ status: 'error',
108
+ message: 'Claude binary not found in PATH or at configured claudePath'
109
+ };
110
+ status.errors.push('Claude binary not found - summarization will fail');
111
+ status.overall = 'unhealthy';
112
+ }
113
+ }
114
+ } catch (err) {
115
+ status.checks.claudeBinary = { status: 'error', message: err.message };
116
+ status.errors.push(`Claude binary: ${err.message}`);
117
+ status.overall = 'unhealthy';
118
+ }
119
+
120
+ // ============================================================================
121
+ // Check 3: Memory Directories
122
+ // ============================================================================
123
+ try {
124
+ const paths = ensureMemoryDirs(cwd);
125
+
126
+ // Check base directory
127
+ const baseWritable = checkWritable(MEMORY_BASE);
128
+
129
+ // Check project directory
130
+ const projectWritable = checkWritable(paths.project);
131
+
132
+ status.checks.directories = {
133
+ status: baseWritable && projectWritable ? 'ok' : 'error',
134
+ base: {
135
+ path: MEMORY_BASE,
136
+ exists: existsSync(MEMORY_BASE),
137
+ writable: baseWritable
138
+ },
139
+ project: {
140
+ path: paths.project,
141
+ exists: existsSync(paths.project),
142
+ writable: projectWritable
143
+ }
144
+ };
145
+
146
+ if (!baseWritable || !projectWritable) {
147
+ status.errors.push('Memory directories not writable');
148
+ status.overall = 'unhealthy';
149
+ }
150
+ } catch (err) {
151
+ status.checks.directories = { status: 'error', message: err.message };
152
+ status.errors.push(`Directories: ${err.message}`);
153
+ status.overall = 'unhealthy';
154
+ }
155
+
156
+ // ============================================================================
157
+ // Check 4: Memory Files Status
158
+ // ============================================================================
159
+ try {
160
+ const paths = ensureMemoryDirs(cwd);
161
+
162
+ const logExists = existsSync(paths.log);
163
+ const logEntries = logExists ? countLines(paths.log) : 0;
164
+
165
+ const summaryJsonExists = existsSync(paths.summaryJson);
166
+ const summaryMdExists = existsSync(paths.summary);
167
+
168
+ const rememberedExists = existsSync(paths.remembered);
169
+ let rememberedCount = 0;
170
+ if (rememberedExists) {
171
+ try {
172
+ const content = readFileSync(paths.remembered, 'utf-8');
173
+ rememberedCount = JSON.parse(content).length;
174
+ } catch {}
175
+ }
176
+
177
+ const entitiesExists = existsSync(paths.entities);
178
+
179
+ status.checks.memoryFiles = {
180
+ status: 'ok',
181
+ log: {
182
+ exists: logExists,
183
+ entries: logEntries,
184
+ needsSummarization: logEntries >= (loadConfig().maxLogEntriesBeforeSummarize || 50)
185
+ },
186
+ summary: {
187
+ jsonExists: summaryJsonExists,
188
+ mdExists: summaryMdExists,
189
+ lastUpdated: summaryJsonExists ? getFileAge(paths.summaryJson) : null
190
+ },
191
+ remembered: {
192
+ exists: rememberedExists,
193
+ count: rememberedCount
194
+ },
195
+ entities: {
196
+ exists: entitiesExists
197
+ }
198
+ };
199
+
200
+ // Warning if log needs summarization
201
+ if (status.checks.memoryFiles.log.needsSummarization) {
202
+ status.warnings.push(`Log has ${logEntries} entries - consider running /summarize`);
203
+ }
204
+ } catch (err) {
205
+ status.checks.memoryFiles = { status: 'error', message: err.message };
206
+ status.errors.push(`Memory files: ${err.message}`);
207
+ }
208
+
209
+ // ============================================================================
210
+ // Check 5: Recent Errors
211
+ // ============================================================================
212
+ try {
213
+ const recentErrors = getErrorsSince(24); // Last 24 hours
214
+ const allErrors = getRecentErrors(5); // Last 5 errors regardless of time
215
+
216
+ status.checks.errorLog = {
217
+ status: recentErrors.length === 0 ? 'ok' : 'warning',
218
+ path: getErrorLogPath(),
219
+ errorsLast24h: recentErrors.length,
220
+ recentErrors: allErrors.map(e => ({
221
+ time: e.ts,
222
+ context: e.context,
223
+ message: e.message
224
+ }))
225
+ };
226
+
227
+ if (recentErrors.length > 0) {
228
+ status.warnings.push(`${recentErrors.length} error(s) in the last 24 hours`);
229
+ if (status.overall === 'healthy') {
230
+ status.overall = 'degraded';
231
+ }
232
+ }
233
+ } catch (err) {
234
+ status.checks.errorLog = { status: 'error', message: err.message };
235
+ }
236
+
237
+ // ============================================================================
238
+ // Check 6: Sync Configuration (if enabled)
239
+ // ============================================================================
240
+ try {
241
+ const config = loadConfig();
242
+ const syncConfig = config.sync || {};
243
+
244
+ if (syncConfig.enabled) {
245
+ status.checks.sync = {
246
+ status: 'configured',
247
+ serverUrl: syncConfig.serverUrl,
248
+ hasApiKey: !!syncConfig.apiKey
249
+ };
250
+
251
+ // Try to reach the server
252
+ if (syncConfig.serverUrl) {
253
+ // We can't easily do async HTTP here, so just note it's configured
254
+ status.checks.sync.note = 'Server reachability not checked (use session-start to verify)';
255
+ }
256
+ } else {
257
+ status.checks.sync = {
258
+ status: 'disabled',
259
+ note: 'Local-only mode (default)'
260
+ };
261
+ }
262
+ } catch (err) {
263
+ status.checks.sync = { status: 'error', message: err.message };
264
+ }
265
+
266
+ // ============================================================================
267
+ // Output
268
+ // ============================================================================
269
+
270
+ // Set overall status based on errors/warnings
271
+ if (status.errors.length > 0) {
272
+ status.overall = 'unhealthy';
273
+ } else if (status.warnings.length > 0) {
274
+ status.overall = 'degraded';
275
+ }
276
+
277
+ console.log(JSON.stringify(status, null, 2));
278
+
279
+ // ============================================================================
280
+ // Helper Functions
281
+ // ============================================================================
282
+
283
+ function checkWritable(dir) {
284
+ try {
285
+ accessSync(dir, constants.W_OK);
286
+ return true;
287
+ } catch {
288
+ return false;
289
+ }
290
+ }
291
+
292
+ function countLines(filePath) {
293
+ try {
294
+ const content = readFileSync(filePath, 'utf-8').trim();
295
+ if (!content) return 0;
296
+ return content.split('\n').filter(l => l).length;
297
+ } catch {
298
+ return 0;
299
+ }
300
+ }
301
+
302
+ function getFileAge(filePath) {
303
+ try {
304
+ const stat = statSync(filePath);
305
+ const ageMs = Date.now() - stat.mtimeMs;
306
+ const ageHours = Math.round(ageMs / (1000 * 60 * 60) * 10) / 10;
307
+ if (ageHours < 1) {
308
+ const ageMinutes = Math.round(ageMs / (1000 * 60));
309
+ return `${ageMinutes} minutes ago`;
310
+ } else if (ageHours < 24) {
311
+ return `${ageHours} hours ago`;
312
+ } else {
313
+ const ageDays = Math.round(ageHours / 24 * 10) / 10;
314
+ return `${ageDays} days ago`;
315
+ }
316
+ } catch {
317
+ return 'unknown';
318
+ }
319
+ }