claude-memory-admin 1.0.0 → 1.1.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 +111 -9
- package/package.json +8 -2
- package/public/app.mjs +633 -362
- package/public/graph.mjs +32 -76
- package/public/index.html +34 -29
- package/public/markdown.mjs +74 -0
- package/public/styles.css +2 -402
- package/public/ui.mjs +291 -0
- package/server.mjs +61 -20
- package/src/instructions.mjs +423 -0
- package/src/model.mjs +44 -11
- package/src/mutate.mjs +17 -23
- package/src/pathcache.mjs +93 -0
- package/src/projects.mjs +120 -34
- package/src/search.mjs +13 -11
- package/src/settings.mjs +151 -0
- package/src/stats.mjs +105 -12
- package/src/stores.mjs +146 -0
package/src/stores.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Discovery of every memory store on the machine, of both kinds Claude Code
|
|
2
|
+
// keeps.
|
|
3
|
+
//
|
|
4
|
+
// Auto memory is one store per project under the projects root. Subagents with a
|
|
5
|
+
// `memory:` field in their frontmatter get their own directories as well, in one
|
|
6
|
+
// of three scopes, and those hold exactly the same thing: a MEMORY.md index, a
|
|
7
|
+
// topic file per memory, the same 200-line/25KB load limit and the same
|
|
8
|
+
// instruction to keep the index short. So they are the same object here, and
|
|
9
|
+
// everything downstream - parsing, the meter, the graph, health, prune, trash -
|
|
10
|
+
// works on them without knowing which kind it has.
|
|
11
|
+
//
|
|
12
|
+
// The project-scoped agent directories live inside repositories, which this tool
|
|
13
|
+
// only knows about because it already recovers each project's real path from the
|
|
14
|
+
// session transcripts. Nothing here guesses at a path that was not confirmed
|
|
15
|
+
// somewhere else first.
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { listProjects, memoryDir, projectsRoot, shortLabel } from './projects.mjs';
|
|
21
|
+
|
|
22
|
+
export const AGENT_USER_DIR = path.join(os.homedir(), '.claude', 'agent-memory');
|
|
23
|
+
export const AGENT_PROJECT_DIR = path.join('.claude', 'agent-memory');
|
|
24
|
+
export const AGENT_LOCAL_DIR = path.join('.claude', 'agent-memory-local');
|
|
25
|
+
|
|
26
|
+
export const STORE_KINDS = {
|
|
27
|
+
auto: { label: 'Project', scope: null },
|
|
28
|
+
'agent-user': { label: 'Subagent', scope: 'user' },
|
|
29
|
+
'agent-project': { label: 'Subagent', scope: 'project' },
|
|
30
|
+
'agent-local': { label: 'Subagent', scope: 'local' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A store id is opaque and URL-safe, and is only ever handed back to us from a
|
|
35
|
+
* list we produced. Encoding the kind and the location keeps it stable across
|
|
36
|
+
* restarts without a registry file to keep in sync.
|
|
37
|
+
*/
|
|
38
|
+
function storeId(kind, key) {
|
|
39
|
+
return `${kind}:${Buffer.from(key, 'utf8').toString('base64url')}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function agentDirs(parent) {
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(parent, { withFileTypes: true });
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return entries
|
|
50
|
+
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
51
|
+
.map((entry) => entry.name)
|
|
52
|
+
.filter((name) => !name.startsWith('.'))
|
|
53
|
+
.sort();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Files in a store directory, excluding the index and the trash folder. */
|
|
57
|
+
function countMemories(dir) {
|
|
58
|
+
try {
|
|
59
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
60
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md') && e.name !== 'MEMORY.md')
|
|
61
|
+
.length;
|
|
62
|
+
} catch {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function agentStore(kind, agentName, dir, projectPath = null) {
|
|
68
|
+
return {
|
|
69
|
+
id: storeId(kind, dir),
|
|
70
|
+
kind,
|
|
71
|
+
dir,
|
|
72
|
+
agentName,
|
|
73
|
+
projectPath,
|
|
74
|
+
label: agentName,
|
|
75
|
+
// Which repository a project- or local-scoped agent store belongs to; the
|
|
76
|
+
// same agent can have a different memory in every checkout.
|
|
77
|
+
sublabel: projectPath ? shortLabel(projectPath) : 'all projects',
|
|
78
|
+
memoryCount: countMemories(dir),
|
|
79
|
+
hasMemoryDir: true,
|
|
80
|
+
hasIndex: fs.existsSync(path.join(dir, 'MEMORY.md')),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Subagent memory directories, across all three scopes. Both roots are
|
|
86
|
+
* parameters rather than constants so this is a total function of where it is
|
|
87
|
+
* told to look, which is also what makes it testable without a real home dir.
|
|
88
|
+
*/
|
|
89
|
+
export function listAgentStores({ userDir = AGENT_USER_DIR, projectPaths = [] } = {}) {
|
|
90
|
+
const stores = [];
|
|
91
|
+
|
|
92
|
+
for (const name of agentDirs(userDir)) {
|
|
93
|
+
stores.push(agentStore('agent-user', name, path.join(userDir, name)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A repository can be reached through more than one project entry (a worktree
|
|
97
|
+
// and its root), so the same directory must not be listed twice.
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
for (const projectPath of projectPaths) {
|
|
100
|
+
if (!projectPath || !path.isAbsolute(projectPath) || seen.has(projectPath)) continue;
|
|
101
|
+
seen.add(projectPath);
|
|
102
|
+
for (const [kind, relative] of [['agent-project', AGENT_PROJECT_DIR], ['agent-local', AGENT_LOCAL_DIR]]) {
|
|
103
|
+
const parent = path.join(projectPath, relative);
|
|
104
|
+
for (const name of agentDirs(parent)) {
|
|
105
|
+
stores.push(agentStore(kind, name, path.join(parent, name), projectPath));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return stores.sort((a, b) => b.memoryCount - a.memoryCount
|
|
111
|
+
|| a.label.localeCompare(b.label)
|
|
112
|
+
|| a.sublabel.localeCompare(b.sublabel));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** An auto-memory project, in the shape the rest of the app expects of a store. */
|
|
116
|
+
function autoStore(project, root) {
|
|
117
|
+
return {
|
|
118
|
+
...project,
|
|
119
|
+
id: storeId('auto', project.slug),
|
|
120
|
+
kind: 'auto',
|
|
121
|
+
dir: memoryDir(root, project.slug),
|
|
122
|
+
sublabel: null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
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.
|
|
130
|
+
*/
|
|
131
|
+
export function listStores(root = projectsRoot()) {
|
|
132
|
+
const projects = listProjects(root);
|
|
133
|
+
const projectPaths = projects
|
|
134
|
+
.filter((project) => project.pathExists)
|
|
135
|
+
.flatMap((project) => [project.path, ...(project.workingDirs || [])]);
|
|
136
|
+
|
|
137
|
+
return [
|
|
138
|
+
...projects.map((project) => autoStore(project, root)),
|
|
139
|
+
...listAgentStores({ projectPaths }),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Resolve an id against the discovered list, so only real stores are ever addressed. */
|
|
144
|
+
export function findStore(root, id) {
|
|
145
|
+
return listStores(root).find((store) => store.id === id) || null;
|
|
146
|
+
}
|