claude-memory-admin 1.1.0 → 1.2.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/src/settings.mjs CHANGED
@@ -44,14 +44,38 @@ export function managedSettingsFiles() {
44
44
  return files;
45
45
  }
46
46
 
47
- function readJson(file) {
47
+ function readJsonDetailed(file) {
48
+ let raw;
48
49
  try {
49
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
50
- return parsed && typeof parsed === 'object' ? parsed : null;
51
- } catch {
52
- // Absent, unreadable, or not valid JSON. Claude Code would ignore it too.
53
- return null;
50
+ raw = fs.readFileSync(file, 'utf8');
51
+ } catch (err) {
52
+ const status = err.code === 'ENOENT' || err.code === 'ENOTDIR' ? 'absent' : 'unreadable';
53
+ return { status, data: null, error: status === 'absent' ? null : err.message };
54
+ }
55
+
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(raw);
59
+ } catch (err) {
60
+ return { status: 'unparseable', data: null, error: err.message };
61
+ }
62
+
63
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
64
+ return { status: 'not-object', data: null, error: 'the file is valid JSON but not an object' };
54
65
  }
66
+ return { status: 'ok', data: parsed, error: null };
67
+ }
68
+
69
+ function settingsCandidates({ projectDir = null, settingsFile = null } = {}) {
70
+ const candidates = [];
71
+ for (const file of managedSettingsFiles()) candidates.push({ scope: 'managed', file });
72
+ if (settingsFile) candidates.push({ scope: 'settings-flag', file: settingsFile });
73
+ if (projectDir) {
74
+ candidates.push({ scope: 'local', file: path.join(projectDir, '.claude', 'settings.local.json') });
75
+ candidates.push({ scope: 'project', file: path.join(projectDir, '.claude', 'settings.json') });
76
+ }
77
+ candidates.push({ scope: 'user', file: USER_SETTINGS });
78
+ return candidates;
55
79
  }
56
80
 
57
81
  /**
@@ -64,20 +88,11 @@ function readJson(file) {
64
88
  * cwd (for the store location, which is decided before any project is chosen) or
65
89
  * a specific project's resolved path (for that project's auto memory state).
66
90
  */
67
- export function settingsLayers({ projectDir = null, settingsFile = null } = {}) {
68
- const candidates = [];
69
- for (const file of managedSettingsFiles()) candidates.push({ scope: 'managed', file });
70
- if (settingsFile) candidates.push({ scope: 'settings-flag', file: settingsFile });
71
- if (projectDir) {
72
- candidates.push({ scope: 'local', file: path.join(projectDir, '.claude', 'settings.local.json') });
73
- candidates.push({ scope: 'project', file: path.join(projectDir, '.claude', 'settings.json') });
74
- }
75
- candidates.push({ scope: 'user', file: USER_SETTINGS });
76
-
91
+ export function settingsLayers(options = {}) {
77
92
  const layers = [];
78
- for (const candidate of candidates) {
79
- const data = readJson(candidate.file);
80
- if (data) layers.push({ ...candidate, data });
93
+ for (const candidate of settingsCandidates(options)) {
94
+ const read = readJsonDetailed(candidate.file);
95
+ if (read.status === 'ok') layers.push({ ...candidate, data: read.data });
81
96
  }
82
97
  return layers;
83
98
  }
@@ -149,3 +164,89 @@ export function cleanupPeriodDays(options = {}) {
149
164
  const value = Number(found?.value);
150
165
  return Number.isFinite(value) && value >= 1 ? value : DEFAULT_CLEANUP_PERIOD_DAYS;
151
166
  }
167
+
168
+ export const REPORTED_KEYS = [
169
+ {
170
+ key: 'autoMemoryEnabled',
171
+ fallback: true,
172
+ detail: 'Whether Claude Code still writes new memories for this project.',
173
+ },
174
+ {
175
+ key: 'autoMemoryDirectory',
176
+ fallback: null,
177
+ detail: 'Where the memory store lives. Unset means ~/.claude/projects.',
178
+ },
179
+ {
180
+ key: 'claudeMdExcludes',
181
+ fallback: [],
182
+ detail: 'Instruction files a session skips when it starts.',
183
+ },
184
+ {
185
+ key: 'cleanupPeriodDays',
186
+ fallback: DEFAULT_CLEANUP_PERIOD_DAYS,
187
+ detail: 'How long session transcripts are kept. Memory files are never swept.',
188
+ },
189
+ ];
190
+
191
+ export function settingsDiagnostics(options = {}) {
192
+ return settingsCandidates(options).map((candidate) => {
193
+ const read = readJsonDetailed(candidate.file);
194
+ return { scope: candidate.scope, file: candidate.file, status: read.status, error: read.error };
195
+ });
196
+ }
197
+
198
+ export function settingsReport(options = {}) {
199
+ const reads = settingsCandidates(options).map((candidate) => ({
200
+ ...candidate,
201
+ ...readJsonDetailed(candidate.file),
202
+ }));
203
+ const layers = reads.map(({ scope, file, status, error }) => ({ scope, file, status, error }));
204
+
205
+ const keys = REPORTED_KEYS.map(({ key, fallback, detail }) => {
206
+ const values = reads
207
+ .filter((read) => read.status === 'ok' && Object.prototype.hasOwnProperty.call(read.data, key))
208
+ .map((read) => ({ scope: read.scope, file: read.file, value: read.data[key], wins: false }));
209
+ if (values.length) values[0].wins = true;
210
+ const winner = values[0] || null;
211
+
212
+ return {
213
+ key,
214
+ detail,
215
+ fallback,
216
+ effective: winner ? { value: winner.value, scope: winner.scope, file: winner.file } : null,
217
+ values,
218
+ };
219
+ });
220
+
221
+ const cleanup = keys.find((entry) => entry.key === 'cleanupPeriodDays');
222
+ cleanup.normalized = cleanupPeriodDays(options);
223
+
224
+ const raw = process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY;
225
+ const disabling = Boolean(raw && raw !== '0' && raw !== 'false');
226
+
227
+ const problems = layers
228
+ .filter((layer) => layer.status !== 'ok' && layer.status !== 'absent')
229
+ .map((layer) => ({ kind: layer.status, scope: layer.scope, file: layer.file, detail: layer.error }));
230
+
231
+ const directory = resolveMemoryDirectory(options);
232
+ if (directory && directory.invalid) {
233
+ problems.push({
234
+ kind: 'invalid-auto-memory-directory',
235
+ scope: directory.scope,
236
+ file: directory.file,
237
+ detail: `"${directory.raw}" is ${directory.invalid}`,
238
+ });
239
+ }
240
+
241
+ return {
242
+ projectDir: options.projectDir ?? null,
243
+ layers,
244
+ keys,
245
+ env: {
246
+ name: 'CLAUDE_CODE_DISABLE_AUTO_MEMORY',
247
+ value: raw ?? null,
248
+ overrides: disabling ? 'autoMemoryEnabled' : null,
249
+ },
250
+ problems,
251
+ };
252
+ }
package/src/stores.mjs CHANGED
@@ -24,6 +24,7 @@ export const AGENT_PROJECT_DIR = path.join('.claude', 'agent-memory');
24
24
  export const AGENT_LOCAL_DIR = path.join('.claude', 'agent-memory-local');
25
25
 
26
26
  export const STORE_KINDS = {
27
+ global: { label: 'Global', scope: 'user' },
27
28
  auto: { label: 'Project', scope: null },
28
29
  'agent-user': { label: 'Subagent', scope: 'user' },
29
30
  'agent-project': { label: 'Subagent', scope: 'project' },
@@ -124,17 +125,46 @@ function autoStore(project, root) {
124
125
  }
125
126
 
126
127
  /**
127
- * Every store, auto memory first. Agent stores are found under the project paths
128
- * auto memory already resolved, so a repository this tool has never seen a
129
- * session for contributes nothing.
128
+ * The user scope, in the shape of a store so it can be selected like one.
129
+ *
130
+ * It holds no memory: ~/.claude is where the instructions live, not a MEMORY.md
131
+ * index and topic files. It is listed anyway because it is the one thing every
132
+ * session on the machine loads, and until now it had nowhere to be seen. Leaving
133
+ * `hasMemoryDir` false is what keeps it out of full-text search, which skips
134
+ * stores without one, and out of everything else that reads memory files.
130
135
  */
131
- export function listStores(root = projectsRoot()) {
136
+ function globalStore(home) {
137
+ const dir = path.join(home, '.claude');
138
+ return {
139
+ id: storeId('global', dir),
140
+ kind: 'global',
141
+ dir,
142
+ path: dir,
143
+ label: 'Global',
144
+ sublabel: 'user scope',
145
+ memoryCount: 0,
146
+ hasMemoryDir: false,
147
+ hasIndex: false,
148
+ pathExists: fs.existsSync(dir),
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Every store, the user scope first and auto memory after it. Agent stores are
154
+ * found under the project paths auto memory already resolved, so a repository this
155
+ * tool has never seen a session for contributes nothing.
156
+ *
157
+ * `home` is a parameter because the global entry is about the real home directory
158
+ * rather than the memory root, which --root can point somewhere else entirely.
159
+ */
160
+ export function listStores(root = projectsRoot(), { home = os.homedir() } = {}) {
132
161
  const projects = listProjects(root);
133
162
  const projectPaths = projects
134
163
  .filter((project) => project.pathExists)
135
164
  .flatMap((project) => [project.path, ...(project.workingDirs || [])]);
136
165
 
137
166
  return [
167
+ globalStore(home),
138
168
  ...projects.map((project) => autoStore(project, root)),
139
169
  ...listAgentStores({ projectPaths }),
140
170
  ];