claude-memory-admin 1.5.1 → 1.5.2

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,65 @@
1
+ import * as ui from '/ui.mjs';
2
+ import { el, node } from '/dom.mjs';
3
+ import { state } from '/state.mjs';
4
+ import { openStore } from '/store.mjs';
5
+
6
+ function storeSubtitle(store) {
7
+ if (store.kind === 'global') return store.dir;
8
+ if (store.kind === 'auto') return store.path;
9
+ const scope = store.kind === 'agent-user' ? 'user'
10
+ : store.kind === 'agent-project' ? 'project' : 'local';
11
+ return `${scope} · ${store.sublabel}`;
12
+ }
13
+
14
+ function issueTitle(store) {
15
+ const parts = [];
16
+ if (store.issueCount) parts.push(`${store.issueCount} in Health`);
17
+ if (store.context) parts.push(`${store.context} in Context`);
18
+ if (store.settings) parts.push(`${store.settings} in Settings`);
19
+ if (store.kind === 'auto' && store.resolvedBy === 'unresolved') {
20
+ parts.push('Real path could not be resolved - showing the raw folder name');
21
+ }
22
+ return parts.length ? `${store.dir}\n${parts.join(', ')}` : store.dir;
23
+ }
24
+
25
+ function storeButton(store) {
26
+ const global = store.kind === 'global';
27
+ const health = !global && !store.hasMemoryDir ? 'none' : store.severity || 'ok';
28
+ const off = store.autoMemory && store.autoMemory.known && !store.autoMemory.enabled;
29
+ return node('button', {
30
+ class: ui.storeItem({ active: store.id === state.storeId, empty: !global && !store.hasMemoryDir }),
31
+ onclick: () => openStore(store.id),
32
+ title: issueTitle(store),
33
+ }, [
34
+ node('span', { class: ui.storeRow }, [
35
+ node('span', { class: ui.dot(health) }),
36
+ node('span', { class: ui.storeName, text: store.label }),
37
+ off ? node('span', { class: ui.offMarker, text: 'off', title: 'Auto memory is disabled for this project' }) : null,
38
+ global ? null : node('span', { class: ui.storeCount, text: store.hasMemoryDir ? String(store.memoryCount) : '-' }),
39
+ ]),
40
+ node('span', { class: ui.storePath, text: storeSubtitle(store) }),
41
+ ]);
42
+ }
43
+
44
+ export function renderStores() {
45
+ const list = el('project-list');
46
+ list.textContent = '';
47
+
48
+ const visible = state.stores.filter((s) => s.kind === 'global' || state.showAll || s.hasMemoryDir);
49
+ if (!visible.length) {
50
+ list.append(node('p', { class: ui.note, text: 'No memory stores found.' }));
51
+ return;
52
+ }
53
+
54
+ const groups = [
55
+ ['Global', visible.filter((s) => s.kind === 'global')],
56
+ ['Projects', visible.filter((s) => s.kind === 'auto')],
57
+ ['Subagents', visible.filter((s) => s.kind !== 'auto' && s.kind !== 'global')],
58
+ ];
59
+
60
+ const filled = groups.filter(([, stores]) => stores.length);
61
+ for (const [title, stores] of filled) {
62
+ if (filled.length > 1) list.append(node('div', { class: ui.sidebarGroup, text: title }));
63
+ for (const store of stores) list.append(storeButton(store));
64
+ }
65
+ }
@@ -0,0 +1,37 @@
1
+ import * as ui from '/ui.mjs';
2
+ import { node } from '/dom.mjs';
3
+ import { state } from '/state.mjs';
4
+ import { restoreFromTrash } from '/store.mjs';
5
+
6
+ const INDEX_EDIT_LABELS = {
7
+ hook: 'MEMORY.md hook shortened',
8
+ move: 'MEMORY.md entry moved',
9
+ add: 'MEMORY.md entry added',
10
+ };
11
+
12
+ export function renderTrash(container) {
13
+ const trash = state.store.trash;
14
+ if (!trash.length) {
15
+ container.append(node('p', { class: ui.note, text: 'Nothing in the trash. Deleted memories land here and can be restored.' }));
16
+ return;
17
+ }
18
+ for (const record of trash) {
19
+ const when = String(record.deletedAt).replace('T', ' ').replace(/\..*$/, '');
20
+ const detail = record.kind === 'wikilink'
21
+ ? `unlinked in ${record.sourceFile} · ${when}`
22
+ : record.kind === 'index-edit'
23
+ ? `${INDEX_EDIT_LABELS[record.op] || 'MEMORY.md edited'} · ${when}`
24
+ : record.kind === 'merge'
25
+ ? `merged into ${record.into} · ${when} · ${record.backups?.length || 0} file(s) rewritten`
26
+ : `${record.files.length} file(s)${record.indexTrashedFile ? ' + MEMORY.md' : ''} · ${when} · ${record.removedLines?.length || 0} index line(s) removed`;
27
+ container.append(node('div', { class: ui.issue(!record.present) }, [
28
+ node('div', { class: ui.issueBody }, [
29
+ node('div', { class: ui.issueTitle, text: record.label || record.id }),
30
+ node('div', { class: ui.issueDetail, text: record.present ? detail : `${detail}, backup missing, cannot restore` }),
31
+ ]),
32
+ record.present
33
+ ? node('button', { class: ui.buttonPrimarySmall, text: 'Restore', onclick: () => restoreFromTrash(record.id) })
34
+ : null,
35
+ ]));
36
+ }
37
+ }
package/src/sessions.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { HEAD_READS, readHead } from './projects.mjs';
4
- import { cleanupPeriodDays } from './settings.mjs';
4
+ import { cleanupPeriodDays, DEFAULT_CLEANUP_PERIOD_DAYS } from './settings.mjs';
5
5
  import { ageInDays } from './stats.mjs';
6
6
 
7
7
  export const EXPIRY_WARNING_DAYS = 7;
@@ -145,6 +145,73 @@ export function retention(sessions, { projectDir = null, now = Date.now() } = {}
145
145
  };
146
146
  }
147
147
 
148
+ export const HEAT_WEEKS_MAX = 53;
149
+ export const HEAT_LEVELS = 4;
150
+
151
+ const DAY_MS = 86400000;
152
+ const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
153
+
154
+ const dayKey = (ms) => new Date(ms).toISOString().slice(0, 10);
155
+ const startOfDay = (ms) => Math.floor(ms / DAY_MS) * DAY_MS;
156
+ const mondayOf = (ms) => ms - ((new Date(ms).getUTCDay() + 6) % 7) * DAY_MS;
157
+ const sundayOf = (ms) => ms + ((7 - new Date(ms).getUTCDay()) % 7) * DAY_MS;
158
+
159
+ export function heatLevel(count, max) {
160
+ if (!count) return 0;
161
+ if (max <= HEAT_LEVELS) return count;
162
+ return Math.min(HEAT_LEVELS, Math.ceil((count / max) * HEAT_LEVELS));
163
+ }
164
+
165
+ export function heatmap(sessions, { days = DEFAULT_CLEANUP_PERIOD_DAYS, now = Date.now() } = {}) {
166
+ const today = startOfDay(now);
167
+ const counts = new Map();
168
+ for (const session of sessions) {
169
+ if (typeof session.modified !== 'string') continue;
170
+ const date = session.modified.slice(0, 10);
171
+ counts.set(date, (counts.get(date) || 0) + 1);
172
+ }
173
+
174
+ const span = Math.max(1, Math.round(days));
175
+ let earliest = today - (span - 1) * DAY_MS;
176
+ for (const date of counts.keys()) {
177
+ const at = Date.parse(`${date}T00:00:00.000Z`);
178
+ if (!Number.isNaN(at) && at < earliest) earliest = at;
179
+ }
180
+
181
+ const end = sundayOf(today);
182
+ let start = mondayOf(earliest);
183
+ if (Math.round((end + DAY_MS - start) / (7 * DAY_MS)) > HEAT_WEEKS_MAX) {
184
+ start = end - (HEAT_WEEKS_MAX * 7 - 1) * DAY_MS;
185
+ }
186
+
187
+ const max = counts.size ? Math.max(...counts.values()) : 0;
188
+ const weeks = [];
189
+ const months = [];
190
+ let labelled = null;
191
+ for (let monday = start; monday <= end; monday += 7 * DAY_MS) {
192
+ const week = [];
193
+ for (let offset = 0; offset < 7; offset += 1) {
194
+ const ms = monday + offset * DAY_MS;
195
+ const date = dayKey(ms);
196
+ const count = counts.get(date) || 0;
197
+ week.push({ date, count, level: heatLevel(count, max), future: ms > today });
198
+ }
199
+ const month = new Date(monday).getUTCMonth();
200
+ months.push(month === labelled ? null : MONTH_LABELS[month]);
201
+ labelled = month;
202
+ weeks.push(week);
203
+ }
204
+
205
+ return {
206
+ weeks,
207
+ months,
208
+ max,
209
+ total: [...counts.values()].reduce((sum, value) => sum + value, 0),
210
+ start: dayKey(start),
211
+ end: dayKey(end),
212
+ };
213
+ }
214
+
148
215
  export function originSession(slugDir, sessionId) {
149
216
  if (typeof sessionId !== 'string' || !SESSION_ID.test(sessionId)) return null;
150
217
  const file = path.join(slugDir, `${sessionId}.jsonl`);
@@ -166,6 +233,7 @@ export function sessionsWithSummaries(slugDir, { projectDir = null, now = Date.n
166
233
  const report = retention(listSessions(slugDir), { projectDir, now });
167
234
  return {
168
235
  ...report,
236
+ heat: heatmap(report.sessions, { days: report.days, now }),
169
237
  sessions: report.sessions.map((session) => ({
170
238
  ...session,
171
239
  ...sessionSummary(session.file, { bytes: session.bytes }),