claude-memory-admin 1.2.0 → 1.4.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.
@@ -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/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) => ({ kind: layer.status, scope: layer.scope, file: layer.file, detail: layer.error }));
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}`,