claude-memory-admin 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -3
- package/package.json +1 -1
- package/public/app.mjs +553 -46
- package/public/styles.css +1 -1
- package/public/ui.mjs +18 -2
- package/server.mjs +74 -3
- package/src/checks.mjs +245 -0
- package/src/instructions.mjs +37 -2
- package/src/model.mjs +45 -0
- package/src/pathcheck.mjs +108 -0
- package/src/projects.mjs +6 -2
- package/src/sessions.mjs +174 -0
- package/src/settings.mjs +23 -1
package/src/model.mjs
CHANGED
|
@@ -15,6 +15,9 @@ import { parseIndex, parseFrontmatter, extractWikilinks } from './parse.mjs';
|
|
|
15
15
|
import { listMemoryFiles, memoryDir, resolveProjectPath, shortLabel } from './projects.mjs';
|
|
16
16
|
import { autoMemoryState } from './settings.mjs';
|
|
17
17
|
import { ageInDays, estimateTokens, findDuplicates, indexStats } from './stats.mjs';
|
|
18
|
+
import { memoryChecks, sessionChecks } from './checks.mjs';
|
|
19
|
+
import { listSessions, originSession, retention, transcriptDir } from './sessions.mjs';
|
|
20
|
+
import { rememberedPath } from './pathcache.mjs';
|
|
18
21
|
|
|
19
22
|
export const TRASH_DIR = '.trash';
|
|
20
23
|
|
|
@@ -94,9 +97,30 @@ function buildResolver(memories) {
|
|
|
94
97
|
return (target) => byName.get(target) || byStem.get(target) || null;
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
function provenance(memory, resolveOrigin) {
|
|
101
|
+
const sessionId = memory.metadata?.originSessionId;
|
|
102
|
+
if (typeof sessionId !== 'string' || !sessionId) return null;
|
|
103
|
+
const session = resolveOrigin ? resolveOrigin(sessionId) : null;
|
|
104
|
+
return { sessionId, present: Boolean(session), modified: session ? session.modified : null };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sessionContext(store) {
|
|
108
|
+
const slugDir = transcriptDir(store);
|
|
109
|
+
if (!slugDir) return { slugDir: null, retention: null, resolveOrigin: null, remembered: false };
|
|
110
|
+
|
|
111
|
+
const projectDir = store.pathExists ? store.path : null;
|
|
112
|
+
return {
|
|
113
|
+
slugDir,
|
|
114
|
+
retention: retention(listSessions(slugDir), { projectDir }),
|
|
115
|
+
resolveOrigin: (sessionId) => originSession(slugDir, sessionId),
|
|
116
|
+
remembered: Boolean(store.slug && rememberedPath(store.slug)),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
97
120
|
export function buildStore(store) {
|
|
98
121
|
const { dir } = store;
|
|
99
122
|
const hasMemoryDir = fs.existsSync(dir);
|
|
123
|
+
const sessions = sessionContext(store);
|
|
100
124
|
|
|
101
125
|
const indexRaw = readIfExists(path.join(dir, 'MEMORY.md'));
|
|
102
126
|
const index = indexRaw === null ? null : parseIndex(indexRaw);
|
|
@@ -138,6 +162,7 @@ export function buildStore(store) {
|
|
|
138
162
|
: referencedFiles.has(memory.file)
|
|
139
163
|
? 'referenced'
|
|
140
164
|
: 'orphan';
|
|
165
|
+
memory.origin = provenance(memory, sessions.resolveOrigin);
|
|
141
166
|
}
|
|
142
167
|
|
|
143
168
|
const existingFiles = new Set(files);
|
|
@@ -173,8 +198,17 @@ export function buildStore(store) {
|
|
|
173
198
|
...(health.longHooks.length
|
|
174
199
|
? [{ kind: 'long-hooks', severity: 'warn', count: health.longHooks.length, longest: health.longHooks[0] }]
|
|
175
200
|
: []),
|
|
201
|
+
...memoryChecks(memories, index),
|
|
202
|
+
...sessionChecks(store, memories, sessions.retention, {
|
|
203
|
+
remembered: sessions.remembered,
|
|
204
|
+
resolveOrigin: sessions.resolveOrigin,
|
|
205
|
+
}),
|
|
176
206
|
];
|
|
207
|
+
health.issues.sort((a, b) => (a.severity === 'bad' ? 0 : 1) - (b.severity === 'bad' ? 0 : 1));
|
|
177
208
|
health.issueCount = health.issues.length;
|
|
209
|
+
health.severity = health.issues.some((item) => item.severity === 'bad')
|
|
210
|
+
? 'bad'
|
|
211
|
+
: health.issues.length ? 'warn' : 'ok';
|
|
178
212
|
|
|
179
213
|
return {
|
|
180
214
|
...store,
|
|
@@ -203,6 +237,16 @@ export function buildStore(store) {
|
|
|
203
237
|
},
|
|
204
238
|
duplicates: findDuplicates(memories),
|
|
205
239
|
trash: listTrash(dir),
|
|
240
|
+
sessions: sessions.retention
|
|
241
|
+
? {
|
|
242
|
+
count: sessions.retention.count,
|
|
243
|
+
bytes: sessions.retention.bytes,
|
|
244
|
+
retentionDays: sessions.retention.days,
|
|
245
|
+
evidenceExpiresInDays: sessions.retention.evidenceExpiresInDays,
|
|
246
|
+
expiringCount: sessions.retention.expiringCount,
|
|
247
|
+
remembered: sessions.remembered,
|
|
248
|
+
}
|
|
249
|
+
: null,
|
|
206
250
|
};
|
|
207
251
|
}
|
|
208
252
|
|
|
@@ -218,6 +262,7 @@ export function buildProject(root, slug) {
|
|
|
218
262
|
kind: 'auto',
|
|
219
263
|
dir: memoryDir(root, slug),
|
|
220
264
|
path: resolved.path,
|
|
265
|
+
pathExists: resolved.exists,
|
|
221
266
|
label: shortLabel(resolved.path),
|
|
222
267
|
resolvedBy: resolved.resolvedBy,
|
|
223
268
|
// The decode that could not be confirmed, offered as a starting point when
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const MAX_PATH_CHECKS = 200;
|
|
5
|
+
export const MAX_INDEXED_FILES = 60000;
|
|
6
|
+
|
|
7
|
+
export const SKIP_DIRS = new Set([
|
|
8
|
+
'.git', 'node_modules', 'bin', 'obj', 'dist', 'build', 'out', 'target', 'vendor',
|
|
9
|
+
'.next', '.nuxt', '.svelte-kit', '.venv', 'venv', '__pycache__', '.gradle', '.idea',
|
|
10
|
+
'Pods', 'DerivedData', 'coverage', '.turbo', '.cache', '.angular', '.parcel-cache',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const CODE_SPAN = /`([^`\n]+)`/g;
|
|
14
|
+
const SOURCE_EXT = /\.(mjs|cjs|jsx?|tsx?|json|jsonc|md|mdx|css|scss|less|html|vue|svelte|py|rb|go|rs|java|kt|swift|cs|fs|php|sh|zsh|ya?ml|toml|xml|sql|proto|lock|gradle|csproj|sln)$/i;
|
|
15
|
+
const BARE_EXTENSION = /^\.[A-Za-z0-9]+$/;
|
|
16
|
+
const LINE_SUFFIX = /:\d+(?:[-:]\d+)?$/;
|
|
17
|
+
const GLOB_OR_SHELL = /[*?{}[\]()<>|$=!"'\\]/;
|
|
18
|
+
|
|
19
|
+
function candidate(raw) {
|
|
20
|
+
let token = raw.trim();
|
|
21
|
+
if (!token || /\s/.test(token)) return null;
|
|
22
|
+
if (token.includes('://') || token.startsWith('@')) return null;
|
|
23
|
+
if (GLOB_OR_SHELL.test(token)) return null;
|
|
24
|
+
|
|
25
|
+
token = token.replace(LINE_SUFFIX, '').replace(/[.,;:]+$/, '').replace(/\/+$/, '');
|
|
26
|
+
if (token.startsWith('./')) token = token.slice(2);
|
|
27
|
+
if (!token || token.startsWith('/') || token.startsWith('~')) return null;
|
|
28
|
+
if (token.split('/').includes('..')) return null;
|
|
29
|
+
if (token.includes('...') || token.length > 200) return null;
|
|
30
|
+
if (BARE_EXTENSION.test(token)) return null;
|
|
31
|
+
if (!SOURCE_EXT.test(token)) return null;
|
|
32
|
+
if (token.split('/').some((segment) => SKIP_DIRS.has(segment))) return null;
|
|
33
|
+
|
|
34
|
+
return token;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function pathCandidates(body) {
|
|
38
|
+
const found = new Set();
|
|
39
|
+
for (const match of String(body || '').matchAll(CODE_SPAN)) {
|
|
40
|
+
const token = candidate(match[1]);
|
|
41
|
+
if (token) found.add(token);
|
|
42
|
+
}
|
|
43
|
+
return [...found];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function indexProject(root, { skipDirs = SKIP_DIRS, limit = MAX_INDEXED_FILES } = {}) {
|
|
47
|
+
const byLastSegment = new Map();
|
|
48
|
+
let count = 0;
|
|
49
|
+
let truncated = false;
|
|
50
|
+
|
|
51
|
+
const walk = (dir, prefix) => {
|
|
52
|
+
if (truncated) return;
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (count >= limit) {
|
|
61
|
+
truncated = true;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (skipDirs.has(entry.name)) continue;
|
|
65
|
+
|
|
66
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
67
|
+
count += 1;
|
|
68
|
+
if (!byLastSegment.has(entry.name)) byLastSegment.set(entry.name, []);
|
|
69
|
+
byLastSegment.get(entry.name).push(relative);
|
|
70
|
+
|
|
71
|
+
if (entry.isDirectory()) walk(path.join(dir, entry.name), relative);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
walk(root, '');
|
|
76
|
+
return { byLastSegment, count, truncated };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolvesInProject(index, token) {
|
|
80
|
+
const matches = index.byLastSegment.get(token.split('/').pop());
|
|
81
|
+
if (!matches) return false;
|
|
82
|
+
if (!token.includes('/')) return true;
|
|
83
|
+
return matches.some((relative) => relative === token || relative.endsWith(`/${token}`));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function verifyPaths(projectDir, memories) {
|
|
87
|
+
const root = path.resolve(projectDir);
|
|
88
|
+
const index = indexProject(root);
|
|
89
|
+
const missing = [];
|
|
90
|
+
let checked = 0;
|
|
91
|
+
let capped = false;
|
|
92
|
+
|
|
93
|
+
for (const memory of memories) {
|
|
94
|
+
for (const token of pathCandidates(memory.body)) {
|
|
95
|
+
if (checked >= MAX_PATH_CHECKS) {
|
|
96
|
+
capped = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
checked += 1;
|
|
100
|
+
if (!resolvesInProject(index, token)) {
|
|
101
|
+
missing.push({ kind: 'stale-path', severity: 'warn', file: memory.file, name: memory.name, token });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (capped) break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { projectDir: root, checked, capped, indexed: index.count, truncated: index.truncated, missing };
|
|
108
|
+
}
|
package/src/projects.mjs
CHANGED
|
@@ -51,12 +51,16 @@ export function projectsRoot() {
|
|
|
51
51
|
return resolveRoot().path;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// The escalation ladder for reading the head of a transcript: the small read
|
|
55
|
+
// almost always suffices and the large one is the fallback.
|
|
56
|
+
export const HEAD_READS = [16384, 131072];
|
|
57
|
+
|
|
54
58
|
/**
|
|
55
59
|
* Read the head of a file without pulling a multi-megabyte transcript into
|
|
56
60
|
* memory. The `cwd` field shows up within the first few lines in practice, so
|
|
57
61
|
* the small read almost always suffices and the large one is a fallback.
|
|
58
62
|
*/
|
|
59
|
-
function readHead(file, bytes) {
|
|
63
|
+
export function readHead(file, bytes) {
|
|
60
64
|
const fd = fs.openSync(file, 'r');
|
|
61
65
|
try {
|
|
62
66
|
const buf = Buffer.alloc(bytes);
|
|
@@ -90,7 +94,7 @@ function findTranscripts(dir, depth = 2) {
|
|
|
90
94
|
|
|
91
95
|
/** The first `cwd` in one transcript, escalating the read only if the small one missed. */
|
|
92
96
|
function cwdInTranscript(file) {
|
|
93
|
-
for (const size of
|
|
97
|
+
for (const size of HEAD_READS) {
|
|
94
98
|
let head;
|
|
95
99
|
try {
|
|
96
100
|
head = readHead(file, size);
|
package/src/sessions.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { HEAD_READS, readHead } from './projects.mjs';
|
|
4
|
+
import { cleanupPeriodDays } from './settings.mjs';
|
|
5
|
+
import { ageInDays } from './stats.mjs';
|
|
6
|
+
|
|
7
|
+
export const EXPIRY_WARNING_DAYS = 7;
|
|
8
|
+
export const PROMPT_TITLE_CHARS = 80;
|
|
9
|
+
export const PROMPT_PREVIEW_CHARS = 400;
|
|
10
|
+
|
|
11
|
+
const SESSION_ID = /^[0-9a-fA-F][0-9a-fA-F-]{7,63}$/;
|
|
12
|
+
const SELF_DESCRIBING_KEYS = ['slug', 'gitBranch', 'model', 'version', 'cwd'];
|
|
13
|
+
const WRAPPED_BY_CLAUDE_CODE = /^<(local-command-[a-z]+|command-(message|name|args)|system-reminder)>/;
|
|
14
|
+
|
|
15
|
+
const isSubagentTranscript = (name) => name.startsWith('agent-');
|
|
16
|
+
const isSessionTranscript = (name) => name.endsWith('.jsonl') && !isSubagentTranscript(name);
|
|
17
|
+
|
|
18
|
+
export function listSessions(slugDir) {
|
|
19
|
+
let entries;
|
|
20
|
+
try {
|
|
21
|
+
entries = fs.readdirSync(slugDir, { withFileTypes: true });
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sessions = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isFile() || !isSessionTranscript(entry.name)) continue;
|
|
29
|
+
const file = path.join(slugDir, entry.name);
|
|
30
|
+
let stat;
|
|
31
|
+
try {
|
|
32
|
+
stat = fs.statSync(file);
|
|
33
|
+
} catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
sessions.push({
|
|
37
|
+
id: entry.name.replace(/\.jsonl$/, ''),
|
|
38
|
+
file,
|
|
39
|
+
bytes: stat.size,
|
|
40
|
+
modified: new Date(stat.mtimeMs).toISOString(),
|
|
41
|
+
modifiedMs: stat.mtimeMs,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return sessions.sort((a, b) => b.modifiedMs - a.modifiedMs);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function promptText(content) {
|
|
48
|
+
if (typeof content === 'string') return content;
|
|
49
|
+
if (!Array.isArray(content)) return null;
|
|
50
|
+
const joined = content
|
|
51
|
+
.filter((block) => block && block.type === 'text' && typeof block.text === 'string')
|
|
52
|
+
.map((block) => block.text)
|
|
53
|
+
.join(' ');
|
|
54
|
+
return joined || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function absorbRecord(record, found) {
|
|
58
|
+
if (!record || typeof record !== 'object') return;
|
|
59
|
+
|
|
60
|
+
if (record.type === 'ai-title' && typeof record.aiTitle === 'string' && record.aiTitle) {
|
|
61
|
+
found.aiTitle = record.aiTitle;
|
|
62
|
+
}
|
|
63
|
+
for (const key of SELF_DESCRIBING_KEYS) {
|
|
64
|
+
const value = typeof record[key] === 'string' ? record[key] : record.message?.[key];
|
|
65
|
+
if (found[key] === null && typeof value === 'string' && value) found[key] = value;
|
|
66
|
+
}
|
|
67
|
+
if (found.prompt === null && record.type === 'user' && !record.isSidechain) {
|
|
68
|
+
const text = promptText(record.message?.content)?.trim();
|
|
69
|
+
if (text && !WRAPPED_BY_CLAUDE_CODE.test(text)) found.prompt = text;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function absorbChunk(head, found) {
|
|
74
|
+
for (const line of head.split('\n')) {
|
|
75
|
+
if (!line.startsWith('{')) continue;
|
|
76
|
+
try {
|
|
77
|
+
absorbRecord(JSON.parse(line), found);
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function condense(text, chars) {
|
|
85
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
86
|
+
return flat.length > chars ? `${flat.slice(0, chars - 1)}…` : flat;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function sessionSummary(file, { bytes = null } = {}) {
|
|
90
|
+
const found = { aiTitle: null, slug: null, gitBranch: null, model: null, version: null, cwd: null, prompt: null };
|
|
91
|
+
|
|
92
|
+
let size = bytes;
|
|
93
|
+
if (size === null) {
|
|
94
|
+
try {
|
|
95
|
+
size = fs.statSync(file).size;
|
|
96
|
+
} catch {
|
|
97
|
+
size = 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const read of HEAD_READS) {
|
|
102
|
+
let head;
|
|
103
|
+
try {
|
|
104
|
+
head = readHead(file, read);
|
|
105
|
+
} catch {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
absorbChunk(head, found);
|
|
109
|
+
if (found.aiTitle || size <= read) break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const titleFrom = found.aiTitle ? 'ai-title' : found.slug ? 'slug' : found.prompt ? 'prompt' : null;
|
|
113
|
+
const title = found.aiTitle
|
|
114
|
+
|| found.slug
|
|
115
|
+
|| (found.prompt ? condense(found.prompt, PROMPT_TITLE_CHARS) : null);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
title,
|
|
119
|
+
titleFrom,
|
|
120
|
+
slug: found.slug,
|
|
121
|
+
gitBranch: found.gitBranch,
|
|
122
|
+
model: found.model,
|
|
123
|
+
version: found.version,
|
|
124
|
+
cwd: found.cwd,
|
|
125
|
+
prompt: found.prompt ? condense(found.prompt, PROMPT_PREVIEW_CHARS) : null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function retention(sessions, { projectDir = null, now = Date.now() } = {}) {
|
|
130
|
+
const days = cleanupPeriodDays(projectDir ? { projectDir } : {});
|
|
131
|
+
const dated = sessions.map((session) => {
|
|
132
|
+
const ageDays = ageInDays(session.modified, now);
|
|
133
|
+
return { ...session, ageDays, expiresInDays: ageDays === null ? null : Math.max(0, days - ageDays) };
|
|
134
|
+
});
|
|
135
|
+
const remaining = dated.map((session) => session.expiresInDays).filter((value) => value !== null);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
days,
|
|
139
|
+
dateFrom: 'mtime',
|
|
140
|
+
sessions: dated,
|
|
141
|
+
count: dated.length,
|
|
142
|
+
bytes: dated.reduce((sum, session) => sum + session.bytes, 0),
|
|
143
|
+
evidenceExpiresInDays: remaining.length ? Math.max(...remaining) : null,
|
|
144
|
+
expiringCount: remaining.filter((value) => value <= EXPIRY_WARNING_DAYS).length,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function originSession(slugDir, sessionId) {
|
|
149
|
+
if (typeof sessionId !== 'string' || !SESSION_ID.test(sessionId)) return null;
|
|
150
|
+
const file = path.join(slugDir, `${sessionId}.jsonl`);
|
|
151
|
+
let stat;
|
|
152
|
+
try {
|
|
153
|
+
stat = fs.statSync(file);
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (!stat.isFile()) return null;
|
|
158
|
+
return { id: sessionId, file, bytes: stat.size, modified: new Date(stat.mtimeMs).toISOString() };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function transcriptDir(store) {
|
|
162
|
+
return store && store.kind === 'auto' && store.dir ? path.dirname(store.dir) : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function sessionsWithSummaries(slugDir, { projectDir = null, now = Date.now() } = {}) {
|
|
166
|
+
const report = retention(listSessions(slugDir), { projectDir, now });
|
|
167
|
+
return {
|
|
168
|
+
...report,
|
|
169
|
+
sessions: report.sessions.map((session) => ({
|
|
170
|
+
...session,
|
|
171
|
+
...sessionSummary(session.file, { bytes: session.bytes }),
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
}
|
package/src/settings.mjs
CHANGED
|
@@ -19,6 +19,21 @@ export const USER_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json')
|
|
|
19
19
|
/** Default when nothing sets cleanupPeriodDays; transcripts older than this are swept. */
|
|
20
20
|
export const DEFAULT_CLEANUP_PERIOD_DAYS = 30;
|
|
21
21
|
|
|
22
|
+
export const SETTINGS_SEVERITY = {
|
|
23
|
+
unparseable: 'bad',
|
|
24
|
+
unreadable: 'bad',
|
|
25
|
+
'not-object': 'bad',
|
|
26
|
+
'invalid-auto-memory-directory': 'bad',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function summariseSettings(problems) {
|
|
30
|
+
const list = problems || [];
|
|
31
|
+
return {
|
|
32
|
+
count: list.length,
|
|
33
|
+
severity: list.some((problem) => problem.severity === 'bad') ? 'bad' : list.length ? 'warn' : 'ok',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
function managedDir() {
|
|
23
38
|
if (process.platform === 'darwin') return '/Library/Application Support/ClaudeCode';
|
|
24
39
|
if (process.platform === 'win32') return 'C:\\Program Files\\ClaudeCode';
|
|
@@ -226,12 +241,19 @@ export function settingsReport(options = {}) {
|
|
|
226
241
|
|
|
227
242
|
const problems = layers
|
|
228
243
|
.filter((layer) => layer.status !== 'ok' && layer.status !== 'absent')
|
|
229
|
-
.map((layer) => ({
|
|
244
|
+
.map((layer) => ({
|
|
245
|
+
kind: layer.status,
|
|
246
|
+
severity: SETTINGS_SEVERITY[layer.status] || 'warn',
|
|
247
|
+
scope: layer.scope,
|
|
248
|
+
file: layer.file,
|
|
249
|
+
detail: layer.error,
|
|
250
|
+
}));
|
|
230
251
|
|
|
231
252
|
const directory = resolveMemoryDirectory(options);
|
|
232
253
|
if (directory && directory.invalid) {
|
|
233
254
|
problems.push({
|
|
234
255
|
kind: 'invalid-auto-memory-directory',
|
|
256
|
+
severity: SETTINGS_SEVERITY['invalid-auto-memory-directory'],
|
|
235
257
|
scope: directory.scope,
|
|
236
258
|
file: directory.file,
|
|
237
259
|
detail: `"${directory.raw}" is ${directory.invalid}`,
|