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.
- package/.claude-plugin/plugin.json +17 -0
- package/CLAUDE.md +98 -0
- package/CONFIG_REFERENCE.md +495 -0
- package/README.md +40 -0
- package/commands/entity.md +64 -0
- package/commands/forget.md +69 -0
- package/commands/remember.md +60 -0
- package/commands/status.md +90 -0
- package/commands/summarize.md +69 -0
- package/hooks/hooks.json +123 -0
- package/package.json +12 -0
- package/scripts/mem-add.mjs +59 -0
- package/scripts/mem-entity.mjs +143 -0
- package/scripts/mem-forget.mjs +245 -0
- package/scripts/mem-status.mjs +319 -0
- package/scripts/mem-summarize.mjs +338 -0
- package/scripts/post-compact.mjs +132 -0
- package/scripts/post-tool-use.mjs +353 -0
- package/scripts/pre-compact.mjs +491 -0
- package/scripts/session-start.mjs +283 -0
- package/scripts/session-stop.mjs +31 -0
- package/scripts/stop-capture.mjs +294 -0
- package/scripts/subagent-stop.mjs +203 -0
- package/scripts/summarize.mjs +428 -0
- package/scripts/sync.mjs +609 -0
- package/scripts/user-prompt-submit.mjs +77 -0
- package/scripts/utils.mjs +2142 -0
- package/scripts/utils.test.mjs +1465 -0
|
@@ -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
|
+
}
|