claude-memory-admin 1.10.1 → 1.10.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.
- package/README.md +36 -6
- package/package.json +1 -1
- package/public/views/cost.mjs +53 -10
- package/public/views/issue.mjs +24 -0
- package/public/views/stores.mjs +22 -0
- package/server.mjs +76 -8
- package/src/agents.mjs +94 -18
- package/src/checks.mjs +69 -0
- package/src/config.mjs +130 -0
- package/src/cost.mjs +2 -2
- package/src/instructions.mjs +51 -20
- package/src/liveSessions.mjs +3 -2
- package/src/model.mjs +2 -1
- package/src/parse.mjs +44 -10
- package/src/pathcheck.mjs +5 -0
- package/src/projects.mjs +43 -6
- package/src/settings.mjs +83 -17
- package/src/stores.mjs +78 -11
- package/src/toolrun.mjs +49 -9
package/README.md
CHANGED
|
@@ -108,9 +108,25 @@ safety net and not a place you browse.
|
|
|
108
108
|
`~/.claude/settings.json`. A value that is neither absolute nor `~/`-prefixed
|
|
109
109
|
is reported rather than quietly ignored.
|
|
110
110
|
- **Whether Claude is still writing.** A project with `autoMemoryEnabled` off, or
|
|
111
|
-
`CLAUDE_CODE_DISABLE_AUTO_MEMORY` set
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
`CLAUDE_CODE_DISABLE_AUTO_MEMORY` set - in the environment or in the `env`
|
|
112
|
+
block of any settings layer - has a store that will never grow again, which on
|
|
113
|
+
disk is indistinguishable from one Claude has not learned anything about yet.
|
|
114
|
+
The project header says which it is, and names the file that decided.
|
|
115
|
+
- **Which subagent still asks for its memory.** A subagent store exists because
|
|
116
|
+
some agent file carried a `memory: user | project | local` field, and it stays
|
|
117
|
+
exactly where it is after that field moves to another scope or is removed. Each
|
|
118
|
+
store is shown next to the definition that declares it - from `agents/` in the
|
|
119
|
+
user scope and in each repository - and one that nothing declares any more is
|
|
120
|
+
marked `orphan`, one whose agent moved scope `moved`. Because subagent memory
|
|
121
|
+
is part of auto memory, turning auto memory off marks every one of them
|
|
122
|
+
`inert`: the `memory:` field stops having any effect, and the agent starts with
|
|
123
|
+
no memory instructions and no file tools at all.
|
|
124
|
+
- **A config directory that is not `~/.claude`.** `CLAUDE_CONFIG_DIR` moves the
|
|
125
|
+
projects root, the agent definitions, the agent memory, the user `CLAUDE.md` and
|
|
126
|
+
rules, and the settings file together, and all of them are read from wherever it
|
|
127
|
+
points. The Environment tab names the directory and says whether the environment
|
|
128
|
+
or the default chose it. A `~/` written by hand is expanded here rather than left
|
|
129
|
+
to the shell, because `cmd` and PowerShell do not expand it.
|
|
114
130
|
- **Search across every project**: names, descriptions, bodies and index hooks,
|
|
115
131
|
with snippets and match highlighting. Press `/` to jump to it.
|
|
116
132
|
- **Read each memory** with its frontmatter as structured metadata and
|
|
@@ -291,15 +307,22 @@ five and shows, per key, the value that wins and the ones it shadows, struck
|
|
|
291
307
|
through, each labelled with the file it came from:
|
|
292
308
|
|
|
293
309
|
`autoMemoryEnabled`, `autoMemoryDirectory`, `claudeMdExcludes` and
|
|
294
|
-
`cleanupPeriodDays`, plus `CLAUDE_CODE_DISABLE_AUTO_MEMORY`
|
|
295
|
-
environment, where it outranks every file
|
|
310
|
+
`cleanupPeriodDays`, plus `CLAUDE_CODE_DISABLE_AUTO_MEMORY` wherever it is set:
|
|
311
|
+
in the environment, where it outranks every file, or in the `env` block of any of
|
|
312
|
+
the five, which a session exports before it starts and which reading `process.env`
|
|
313
|
+
alone would miss. `CLAUDE_CONFIG_DIR` is named here too, since it decides where
|
|
314
|
+
all five of those files are looked for in the first place.
|
|
296
315
|
|
|
297
316
|
It also names the failures that are otherwise silent:
|
|
298
317
|
|
|
299
318
|
- A settings file that exists but is not valid JSON. Claude Code ignores the
|
|
300
319
|
whole file, so every value in it is doing nothing, and nothing says so.
|
|
301
320
|
- A file that parses but is not an object, or cannot be read at all.
|
|
302
|
-
- An `autoMemoryDirectory` that is neither absolute nor `~/`-prefixed.
|
|
321
|
+
- An `autoMemoryDirectory` that is neither absolute nor `~/`-prefixed. A Windows
|
|
322
|
+
path is accepted on any platform, because a settings file is routinely shared
|
|
323
|
+
between machines.
|
|
324
|
+
- A `CLAUDE_CONFIG_DIR` that is not absolute, which Claude Code would not accept
|
|
325
|
+
either: the default is used and the fact is reported rather than swallowed.
|
|
303
326
|
- A value Claude Code accepts the key of but not the number, like a
|
|
304
327
|
`cleanupPeriodDays` below 1: it shows what is written *and* what applies.
|
|
305
328
|
|
|
@@ -453,6 +476,13 @@ claude-memory-admin --root /tmp/memory-snapshot
|
|
|
453
476
|
## Safety
|
|
454
477
|
|
|
455
478
|
- Binds `127.0.0.1`; no telemetry, no network calls.
|
|
479
|
+
- Runs on macOS, Linux and Windows. Line endings are the part of that which is not
|
|
480
|
+
cosmetic: a `MEMORY.md` saved by a Windows editor is CRLF, and a carriage return
|
|
481
|
+
is not something JavaScript's `.` matches, so a file like that once parsed as if
|
|
482
|
+
it were empty - no index entries, no frontmatter, every memory an orphan. It is
|
|
483
|
+
read correctly now, and a rewrite ends its lines the way it found them: a file
|
|
484
|
+
that came back half CRLF and half LF would show every line as changed in git, on
|
|
485
|
+
a change you never made.
|
|
456
486
|
- Reads only `~/.claude/projects`, the agent memory directories and the `CLAUDE.md`
|
|
457
487
|
chain, unless you switch on the path check above, which then also walks that one
|
|
458
488
|
project's directory. It only ever reads: no path a memory names is opened, only
|
package/package.json
CHANGED
package/public/views/cost.mjs
CHANGED
|
@@ -18,8 +18,37 @@ const AGENT_PROBLEMS = {
|
|
|
18
18
|
'name-mismatch': 'Name and filename disagree',
|
|
19
19
|
'unknown-model': 'Unrecognised model',
|
|
20
20
|
'unknown-effort': 'Unrecognised effort',
|
|
21
|
+
'unknown-memory-scope': 'Unrecognised memory scope',
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
function memoryLine(agent, stores) {
|
|
25
|
+
if (!agent.memory) {
|
|
26
|
+
const stale = stores.filter((store) => store.agentName === agent.name && !store.linked);
|
|
27
|
+
if (!stale.length) return null;
|
|
28
|
+
return node('div', { class: ui.agentTools }, [
|
|
29
|
+
node('span', { class: ui.badge('warn'), text: 'no memory:' }),
|
|
30
|
+
node('span', {
|
|
31
|
+
text: ` this agent declares no memory scope, but ${stale.length === 1 ? 'a directory it once wrote' : `${stale.length} directories it once wrote`} ${stale.length === 1 ? 'is' : 'are'} still on disk and nothing loads ${stale.length === 1 ? 'it' : 'them'}.`,
|
|
32
|
+
}),
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const mine = stores.filter((store) => store.agentName === agent.name && store.linked);
|
|
37
|
+
const inert = mine.some((store) => store.inert);
|
|
38
|
+
const count = mine.reduce((sum, store) => sum + store.memoryCount, 0);
|
|
39
|
+
|
|
40
|
+
const parts = [
|
|
41
|
+
node('span', { class: ui.scopeBadge(agent.memory), text: `memory: ${agent.memory}` }),
|
|
42
|
+
];
|
|
43
|
+
if (inert) parts.push(node('span', { class: ui.badge('warn'), text: 'inert' }));
|
|
44
|
+
parts.push(node('span', {
|
|
45
|
+
text: mine.length
|
|
46
|
+
? ` ${count} ${count === 1 ? 'memory' : 'memories'} in ${mine.length === 1 ? 'its store' : `${mine.length} stores`}${inert ? ', frozen while auto memory is off' : ''}.`
|
|
47
|
+
: ' no store yet - the directory appears the first time it saves something.',
|
|
48
|
+
}));
|
|
49
|
+
return node('div', { class: ui.agentTools }, parts);
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
const show = (value) => (value === undefined || value === null ? 'unset' : JSON.stringify(value));
|
|
24
53
|
|
|
25
54
|
const optionText = (option) => (option.note ? `${option.label} (${option.note})` : option.label);
|
|
@@ -79,7 +108,12 @@ async function saveAgent(file, field, value) {
|
|
|
79
108
|
body: JSON.stringify({ file, field, value }),
|
|
80
109
|
});
|
|
81
110
|
if (state.storeId !== id) return;
|
|
82
|
-
state.aux.cost = {
|
|
111
|
+
state.aux.cost = {
|
|
112
|
+
...state.aux.cost,
|
|
113
|
+
agents: data.agents,
|
|
114
|
+
agentsDirExists: data.agentsDirExists,
|
|
115
|
+
agentStores: data.agentStores,
|
|
116
|
+
};
|
|
83
117
|
toast(value === null ? `Cleared ${field} in ${file}` : `${file}: ${field} = ${value}`);
|
|
84
118
|
} catch (err) {
|
|
85
119
|
toast(err.message, { error: true });
|
|
@@ -138,21 +172,30 @@ function settingCard(entry, writable) {
|
|
|
138
172
|
return card;
|
|
139
173
|
}
|
|
140
174
|
|
|
141
|
-
function agentRow(agent, fields) {
|
|
175
|
+
function agentRow(agent, fields, stores) {
|
|
142
176
|
const row = node('div', { class: ui.agentRow });
|
|
143
177
|
|
|
144
|
-
|
|
178
|
+
const top = [
|
|
145
179
|
node('span', { class: ui.agentName, text: agent.name }),
|
|
146
|
-
node('span', { class: ui.agentFile, text: agent.file }),
|
|
147
|
-
]
|
|
180
|
+
node('span', { class: ui.agentFile, text: agent.projectPath ? `${agent.projectPath}/.claude/agents/${agent.file}` : agent.file }),
|
|
181
|
+
];
|
|
182
|
+
if (agent.scope === 'project') top.push(node('span', { class: ui.scopeBadge('project'), text: 'project' }));
|
|
183
|
+
row.append(node('div', { class: ui.agentTop }, top));
|
|
148
184
|
|
|
149
185
|
if (agent.description) row.append(node('p', { class: ui.agentDesc, text: agent.description }));
|
|
150
186
|
if (agent.tools) row.append(node('div', { class: ui.agentTools, text: `tools: ${agent.tools}` }));
|
|
151
187
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
188
|
+
const memory = memoryLine(agent, stores);
|
|
189
|
+
if (memory) row.append(memory);
|
|
190
|
+
|
|
191
|
+
if (agent.writable) {
|
|
192
|
+
row.append(node('div', { class: ui.agentControls }, [
|
|
193
|
+
labelled('model', picker(fields.model.options, agent.model, (value) => saveAgent(agent.file, 'model', value))),
|
|
194
|
+
labelled('effort', picker(fields.effort.options, agent.effort, (value) => saveAgent(agent.file, 'effort', value))),
|
|
195
|
+
]));
|
|
196
|
+
} else {
|
|
197
|
+
row.append(node('div', { class: ui.agentTools, text: `model: ${agent.model || 'inherit'}, effort: ${agent.effort || 'default'} - read-only, this file lives in a repository.` }));
|
|
198
|
+
}
|
|
156
199
|
|
|
157
200
|
for (const problem of agent.problems) {
|
|
158
201
|
row.append(issue(
|
|
@@ -181,7 +224,7 @@ function agentsCard(data) {
|
|
|
181
224
|
return card;
|
|
182
225
|
}
|
|
183
226
|
|
|
184
|
-
for (const agent of data.agents) card.append(agentRow(agent, data.agentFields));
|
|
227
|
+
for (const agent of data.agents) card.append(agentRow(agent, data.agentFields, data.agentStores || []));
|
|
185
228
|
return card;
|
|
186
229
|
}
|
|
187
230
|
|
package/public/views/issue.mjs
CHANGED
|
@@ -258,5 +258,29 @@ export function renderIssue(item, memories) {
|
|
|
258
258
|
);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
if (item.kind === 'agent-memory-inert') {
|
|
262
|
+
return issue(
|
|
263
|
+
'Auto memory is off, so this store is frozen',
|
|
264
|
+
`subagent memory is part of auto memory${item.setBy ? `, and ${item.setBy} turns it off` : ' and it is turned off'}. The memory: field on ${item.agentName} has no effect while that is the case: the agent starts with no memory instructions and no file tools, so nothing here is read and nothing new is written.`,
|
|
265
|
+
{ bad },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (item.kind === 'agent-store-orphan') {
|
|
270
|
+
return issue(
|
|
271
|
+
'Nothing declares this store any more',
|
|
272
|
+
`${item.defined ? `${item.agentName} exists but no longer carries a memory: field` : `no agent named ${item.agentName} was found in any scope`}. A store is created by that field and outlives it, so this directory is still holding what it learned and no session will read it again.`,
|
|
273
|
+
{ bad },
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (item.kind === 'agent-store-scope-mismatch') {
|
|
278
|
+
return issue(
|
|
279
|
+
`${item.agentName} keeps its memory in the ${item.declaredScope} scope now`,
|
|
280
|
+
`this store is the ${item.scope} one${item.declaredBy ? `, but ${item.declaredBy} declares memory: ${item.declaredScope}` : ''}. The live store is the ${item.declaredScope} one; this is what the agent wrote before the field changed.`,
|
|
281
|
+
{ bad },
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
261
285
|
return issue(item.kind, JSON.stringify(item));
|
|
262
286
|
}
|
package/public/views/stores.mjs
CHANGED
|
@@ -11,6 +11,26 @@ function storeSubtitle(store) {
|
|
|
11
11
|
return `${scope} · ${store.sublabel}`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function agentMarker(store) {
|
|
15
|
+
if (!String(store.kind).startsWith('agent-') || !store.linkage) return null;
|
|
16
|
+
if (store.inert) {
|
|
17
|
+
return { text: 'inert', title: `Auto memory is off${store.inertBy ? ` (${store.inertBy})` : ''}, so the memory: field has no effect and this store is frozen.` };
|
|
18
|
+
}
|
|
19
|
+
if (store.linked) return null;
|
|
20
|
+
if (store.declaredScope) {
|
|
21
|
+
return {
|
|
22
|
+
text: 'moved',
|
|
23
|
+
title: `${store.agentName} now declares memory: ${store.declaredScope}, so the live store is elsewhere and this one is stale.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
text: 'orphan',
|
|
28
|
+
title: store.defined
|
|
29
|
+
? `${store.agentName} exists but declares no memory: field any more, so nothing loads this store.`
|
|
30
|
+
: `No agent named ${store.agentName} was found in any scope, so nothing loads this store.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
function issueTitle(store) {
|
|
15
35
|
const parts = [];
|
|
16
36
|
if (store.issueCount) parts.push(`${store.issueCount} to fix in Cleanup`);
|
|
@@ -32,6 +52,7 @@ function storeButton(store) {
|
|
|
32
52
|
const global = store.kind === 'global';
|
|
33
53
|
const health = !global && !store.hasMemoryDir ? 'none' : store.severity || 'ok';
|
|
34
54
|
const off = store.autoMemory && store.autoMemory.known && !store.autoMemory.enabled;
|
|
55
|
+
const marker = agentMarker(store);
|
|
35
56
|
const active = state.activeSessions.filter((s) => s.storeId === store.id);
|
|
36
57
|
return node('button', {
|
|
37
58
|
class: ui.storeItem({ active: store.id === state.storeId, empty: !global && !store.hasMemoryDir }),
|
|
@@ -43,6 +64,7 @@ function storeButton(store) {
|
|
|
43
64
|
node('span', { class: ui.storeName, text: store.label }),
|
|
44
65
|
active.length ? node('span', { class: ui.dot('ok'), title: activeTitle(active) }) : null,
|
|
45
66
|
off ? node('span', { class: ui.offMarker, text: 'off', title: 'Auto memory is disabled for this project' }) : null,
|
|
67
|
+
marker ? node('span', { class: ui.offMarker, text: marker.text, title: marker.title }) : null,
|
|
46
68
|
global ? null : node('span', { class: ui.storeCount, text: store.hasMemoryDir ? String(store.memoryCount) : '-' }),
|
|
47
69
|
]),
|
|
48
70
|
node('span', { class: ui.storePath, text: storeSubtitle(store) }),
|
package/server.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { buildStore } from './src/model.mjs';
|
|
|
13
13
|
import { resolveGlobalInstructions, resolveInstructions, summarise } from './src/instructions.mjs';
|
|
14
14
|
import { settingsReport, summariseSettings } from './src/settings.mjs';
|
|
15
15
|
import { costReport, writeUserSetting } from './src/cost.mjs';
|
|
16
|
-
import { AGENTS_DIR, AGENT_FIELDS, agentsDirExists,
|
|
16
|
+
import { AGENTS_DIR, AGENT_FIELDS, agentsDirExists, listAllAgents, setAgentField } from './src/agents.mjs';
|
|
17
17
|
import { listStores } from './src/stores.mjs';
|
|
18
18
|
import { forgetPath, rememberPath } from './src/pathcache.mjs';
|
|
19
19
|
import { searchAll } from './src/search.mjs';
|
|
@@ -140,6 +140,67 @@ const VERSION = (() => {
|
|
|
140
140
|
}
|
|
141
141
|
})();
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Every agent definition on the machine: the user directory plus the agents
|
|
145
|
+
* directory of each repository auto memory has already resolved a path for.
|
|
146
|
+
* Nothing here guesses at a repository that was not confirmed somewhere else.
|
|
147
|
+
*/
|
|
148
|
+
function allAgents() {
|
|
149
|
+
const projectPaths = listStores(ROOT)
|
|
150
|
+
.filter((store) => store.kind === 'auto' && store.pathExists)
|
|
151
|
+
.flatMap((store) => [store.path, ...(store.workingDirs || [])]);
|
|
152
|
+
return listAllAgents({ projectPaths });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Each subagent memory store next to the definition that asks for it, for the agents panel. */
|
|
156
|
+
function agentStoreLinks() {
|
|
157
|
+
return listStores(ROOT)
|
|
158
|
+
.filter((store) => String(store.kind).startsWith('agent-'))
|
|
159
|
+
.map((store) => ({
|
|
160
|
+
id: store.id,
|
|
161
|
+
kind: store.kind,
|
|
162
|
+
agentName: store.agentName,
|
|
163
|
+
projectPath: store.projectPath,
|
|
164
|
+
memoryCount: store.memoryCount,
|
|
165
|
+
declaredBy: store.declaredBy ?? null,
|
|
166
|
+
declaredScope: store.declaredScope ?? null,
|
|
167
|
+
linked: Boolean(store.linked),
|
|
168
|
+
defined: Boolean(store.defined),
|
|
169
|
+
inert: Boolean(store.inert),
|
|
170
|
+
inertBy: store.inertBy ?? null,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Hand the address to whatever opens a URL here, and never let that decide
|
|
176
|
+
* whether the server runs.
|
|
177
|
+
*
|
|
178
|
+
* `start` is a cmd.exe builtin rather than a program, so spawning it by name
|
|
179
|
+
* fails on Windows; the documented form is `cmd /c start "" <url>`, where the
|
|
180
|
+
* empty string is the window title `start` would otherwise read the URL as. On
|
|
181
|
+
* Linux `xdg-open` is simply missing on a minimal install and inside some
|
|
182
|
+
* containers. Either way the failure arrives as an async 'error' event, which
|
|
183
|
+
* with no listener is an uncaught exception that would take down a server that
|
|
184
|
+
* had already printed its address and was working perfectly well.
|
|
185
|
+
*/
|
|
186
|
+
function openBrowser(address) {
|
|
187
|
+
const [command, args] = process.platform === 'darwin'
|
|
188
|
+
? ['open', [address]]
|
|
189
|
+
: process.platform === 'win32'
|
|
190
|
+
? [process.env.COMSPEC || 'cmd.exe', ['/d', '/s', '/c', 'start', '""', address.replace(/&/g, '^&')]]
|
|
191
|
+
: ['xdg-open', [address]];
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true, windowsHide: true });
|
|
195
|
+
child.on('error', () => {
|
|
196
|
+
console.log('Could not open a browser automatically - open the address above yourself.');
|
|
197
|
+
});
|
|
198
|
+
child.unref();
|
|
199
|
+
} catch {
|
|
200
|
+
console.log('Could not open a browser automatically - open the address above yourself.');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
143
204
|
function storeProjectDir(store) {
|
|
144
205
|
// The global store is the user scope itself, which no project owns.
|
|
145
206
|
if (store.kind === 'global') return null;
|
|
@@ -331,10 +392,15 @@ async function handleApi(req, res, url) {
|
|
|
331
392
|
if (action === 'cost' && req.method === 'GET') {
|
|
332
393
|
return sendJson(res, 200, {
|
|
333
394
|
settings: costReport(),
|
|
334
|
-
|
|
395
|
+
// Project-scope definitions come along read-only: an agent with
|
|
396
|
+
// `memory: project` lives in a repository rather than in the user
|
|
397
|
+
// directory, and leaving it out would show its memory store as belonging
|
|
398
|
+
// to no agent at all.
|
|
399
|
+
agents: allAgents(),
|
|
335
400
|
agentsDir: AGENTS_DIR,
|
|
336
401
|
agentsDirExists: agentsDirExists(),
|
|
337
402
|
agentFields: AGENT_FIELDS,
|
|
403
|
+
agentStores: agentStoreLinks(),
|
|
338
404
|
});
|
|
339
405
|
}
|
|
340
406
|
|
|
@@ -346,8 +412,13 @@ async function handleApi(req, res, url) {
|
|
|
346
412
|
|
|
347
413
|
if (action === 'cost/agent' && req.method === 'POST') {
|
|
348
414
|
const body = await readBody(req);
|
|
349
|
-
|
|
350
|
-
return sendJson(res, 200, {
|
|
415
|
+
setAgentField(body.file, body.field, body.value ?? null);
|
|
416
|
+
return sendJson(res, 200, {
|
|
417
|
+
agents: allAgents(),
|
|
418
|
+
agentsDir: AGENTS_DIR,
|
|
419
|
+
agentsDirExists: agentsDirExists(),
|
|
420
|
+
agentStores: agentStoreLinks(),
|
|
421
|
+
});
|
|
351
422
|
}
|
|
352
423
|
|
|
353
424
|
if (action === 'delete-preview' && req.method === 'POST') {
|
|
@@ -484,10 +555,7 @@ export function startServer({ port, root, open = true } = {}) {
|
|
|
484
555
|
const address = `http://localhost:${listenPort}`;
|
|
485
556
|
console.log(`Memory Admin -> ${address}`);
|
|
486
557
|
console.log(`Reading -> ${ROOT}`);
|
|
487
|
-
if (open && !process.env.NO_OPEN)
|
|
488
|
-
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
489
|
-
spawn(opener, [address], { stdio: 'ignore', detached: true }).unref();
|
|
490
|
-
}
|
|
558
|
+
if (open && !process.env.NO_OPEN) openBrowser(address);
|
|
491
559
|
});
|
|
492
560
|
return server;
|
|
493
561
|
}
|
package/src/agents.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Subagent definitions: agents/*.md, in the user scope and in each repository.
|
|
2
2
|
//
|
|
3
|
-
// Not to be confused with
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Not to be confused with agent-memory (src/stores.mjs), which is what those
|
|
4
|
+
// agents *remember*. This module is about what they *are*: a markdown file whose
|
|
5
|
+
// frontmatter names the agent and, optionally, pins the model and effort it runs
|
|
6
|
+
// at, and declares with `memory:` whether it keeps a memory directory at all.
|
|
7
|
+
// That last field is the link between the two: a store exists because some file
|
|
8
|
+
// here asked for it, and a store no file asks for any more is a store nothing
|
|
9
|
+
// loads. Pinning a summariser to Haiku while a reviewer stays on Opus is the
|
|
7
10
|
// finer-grained version of the CLAUDE_CODE_SUBAGENT_MODEL switch in src/cost.mjs,
|
|
8
11
|
// which - worth remembering when both are set - outranks everything here.
|
|
9
12
|
//
|
|
@@ -12,13 +15,21 @@
|
|
|
12
15
|
// somebody wrote on purpose, and this tool has no view on it.
|
|
13
16
|
|
|
14
17
|
import fs from 'node:fs';
|
|
15
|
-
import os from 'node:os';
|
|
16
18
|
import path from 'node:path';
|
|
17
19
|
|
|
20
|
+
import { canonicalPath, configPath } from './config.mjs';
|
|
18
21
|
import { parseFrontmatter } from './parse.mjs';
|
|
19
22
|
import { writeFileAtomic } from './mutate.mjs';
|
|
20
23
|
|
|
21
|
-
export const AGENTS_DIR =
|
|
24
|
+
export const AGENTS_DIR = configPath('agents');
|
|
25
|
+
export const PROJECT_AGENTS_DIR = path.join('.claude', 'agents');
|
|
26
|
+
|
|
27
|
+
/** The scopes `memory:` accepts, and the store kind each one produces. */
|
|
28
|
+
export const MEMORY_SCOPES = {
|
|
29
|
+
user: 'agent-user',
|
|
30
|
+
project: 'agent-project',
|
|
31
|
+
local: 'agent-local',
|
|
32
|
+
};
|
|
22
33
|
|
|
23
34
|
const MODEL_ID = /^claude-[A-Za-z0-9._[\]-]+$/;
|
|
24
35
|
|
|
@@ -55,6 +66,7 @@ export const AGENT_PROBLEM_SEVERITY = {
|
|
|
55
66
|
'name-mismatch': 'warn',
|
|
56
67
|
'unknown-model': 'warn',
|
|
57
68
|
'unknown-effort': 'warn',
|
|
69
|
+
'unknown-memory-scope': 'warn',
|
|
58
70
|
};
|
|
59
71
|
|
|
60
72
|
function known(field, value) {
|
|
@@ -92,9 +104,9 @@ export function safeAgentPath(dir, file) {
|
|
|
92
104
|
return full;
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
function describeAgent(dir, file) {
|
|
107
|
+
function describeAgent(dir, file, { scope = 'user', writable = true, projectPath = null } = {}) {
|
|
96
108
|
const full = path.join(dir, file);
|
|
97
|
-
const stem = file.replace(/\.md$/, '');
|
|
109
|
+
const stem = path.basename(file).replace(/\.md$/, '');
|
|
98
110
|
|
|
99
111
|
let text;
|
|
100
112
|
try {
|
|
@@ -102,11 +114,15 @@ function describeAgent(dir, file) {
|
|
|
102
114
|
} catch (err) {
|
|
103
115
|
return {
|
|
104
116
|
file,
|
|
117
|
+
scope,
|
|
118
|
+
writable: false,
|
|
119
|
+
projectPath,
|
|
105
120
|
name: stem,
|
|
106
121
|
description: '',
|
|
107
122
|
tools: '',
|
|
108
123
|
model: null,
|
|
109
124
|
effort: null,
|
|
125
|
+
memory: null,
|
|
110
126
|
bytes: 0,
|
|
111
127
|
problems: [{ kind: 'no-frontmatter', severity: 'bad', detail: err.message }],
|
|
112
128
|
};
|
|
@@ -117,6 +133,7 @@ function describeAgent(dir, file) {
|
|
|
117
133
|
const name = scalar('name') || stem;
|
|
118
134
|
const model = scalar('model') || null;
|
|
119
135
|
const effort = scalar('effort') || null;
|
|
136
|
+
const memory = scalar('memory') || null;
|
|
120
137
|
|
|
121
138
|
const problems = [];
|
|
122
139
|
const flag = (kind, detail) => problems.push({ kind, severity: AGENT_PROBLEM_SEVERITY[kind], detail });
|
|
@@ -129,26 +146,39 @@ function describeAgent(dir, file) {
|
|
|
129
146
|
if (scalar('name') && scalar('name') !== stem) flag('name-mismatch', `Named "${scalar('name')}" in a file called "${file}".`);
|
|
130
147
|
if (model && !known('model', model)) flag('unknown-model', `model: ${model} is neither an alias nor a claude- model name.`);
|
|
131
148
|
if (effort && !known('effort', effort)) flag('unknown-effort', `effort: ${effort} is not one of low, medium, high, xhigh or max.`);
|
|
149
|
+
if (memory && !Object.prototype.hasOwnProperty.call(MEMORY_SCOPES, memory)) {
|
|
150
|
+
flag('unknown-memory-scope', `memory: ${memory} is not one of user, project or local, so this agent keeps no memory directory.`);
|
|
151
|
+
}
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
return {
|
|
135
155
|
file,
|
|
156
|
+
scope,
|
|
157
|
+
// Only a plain .md at the top of the user directory is rewritable: writes go
|
|
158
|
+
// through safeAgentPath, which takes a bare basename, and nothing here has
|
|
159
|
+
// any business editing a file inside somebody's repository.
|
|
160
|
+
writable: writable && file === path.basename(file),
|
|
161
|
+
projectPath,
|
|
136
162
|
name,
|
|
137
163
|
description: scalar('description'),
|
|
138
164
|
tools: scalar('tools'),
|
|
139
165
|
model,
|
|
140
166
|
effort,
|
|
167
|
+
memory: memory && Object.prototype.hasOwnProperty.call(MEMORY_SCOPES, memory) ? memory : null,
|
|
168
|
+
memoryRaw: memory,
|
|
141
169
|
bytes: Buffer.byteLength(text, 'utf8'),
|
|
142
170
|
problems,
|
|
143
171
|
};
|
|
144
172
|
}
|
|
145
173
|
|
|
146
174
|
/**
|
|
147
|
-
* Every
|
|
148
|
-
*
|
|
149
|
-
*
|
|
175
|
+
* Every .md under an agents directory, including the subfolders people use to
|
|
176
|
+
* group them. Claude Code scans both agent roots recursively and identifies an
|
|
177
|
+
* agent by its `name` rather than by where the file sits, so a scan that stopped
|
|
178
|
+
* at the top level would miss agents that are running.
|
|
150
179
|
*/
|
|
151
|
-
|
|
180
|
+
function agentFiles(dir, prefix = '', depth = 0) {
|
|
181
|
+
if (depth > 4) return [];
|
|
152
182
|
let entries;
|
|
153
183
|
try {
|
|
154
184
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -156,12 +186,54 @@ export function listAgents({ dir = AGENTS_DIR } = {}) {
|
|
|
156
186
|
return [];
|
|
157
187
|
}
|
|
158
188
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
const files = [];
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
if (entry.name.startsWith('.')) continue;
|
|
192
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
193
|
+
if (entry.isDirectory()) files.push(...agentFiles(path.join(dir, entry.name), rel, depth + 1));
|
|
194
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) files.push(rel);
|
|
195
|
+
}
|
|
196
|
+
return files;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Every user-scope agent definition. A directory that is not there is the
|
|
201
|
+
* ordinary state of a machine whose owner has never written one, not a failure,
|
|
202
|
+
* so it answers with an empty list.
|
|
203
|
+
*/
|
|
204
|
+
export function listAgents({ dir = AGENTS_DIR } = {}) {
|
|
205
|
+
return agentFiles(dir)
|
|
206
|
+
.map((file) => describeAgent(dir, file, { scope: 'user', writable: true }))
|
|
207
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Project-scope definitions, from <repo>/.claude/agents. These are read and
|
|
212
|
+
* never written: they belong to a repository somebody else may share, and an
|
|
213
|
+
* agent with `memory: project` is normally defined here rather than in the user
|
|
214
|
+
* scope, so leaving them out would orphan the very stores this exists to explain.
|
|
215
|
+
*/
|
|
216
|
+
export function listProjectAgents(projectPath, { relative = PROJECT_AGENTS_DIR } = {}) {
|
|
217
|
+
if (!projectPath || !path.isAbsolute(projectPath)) return [];
|
|
218
|
+
const dir = path.join(projectPath, relative);
|
|
219
|
+
return agentFiles(dir)
|
|
220
|
+
.map((file) => describeAgent(dir, file, { scope: 'project', writable: false, projectPath }))
|
|
162
221
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
163
222
|
}
|
|
164
223
|
|
|
224
|
+
/** Every definition on the machine this tool can see: user scope plus each repository. */
|
|
225
|
+
export function listAllAgents({ dir = AGENTS_DIR, projectPaths = [] } = {}) {
|
|
226
|
+
const agents = listAgents({ dir });
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
for (const projectPath of projectPaths) {
|
|
229
|
+
const key = canonicalPath(projectPath || '');
|
|
230
|
+
if (!key || seen.has(key)) continue;
|
|
231
|
+
seen.add(key);
|
|
232
|
+
agents.push(...listProjectAgents(projectPath));
|
|
233
|
+
}
|
|
234
|
+
return agents;
|
|
235
|
+
}
|
|
236
|
+
|
|
165
237
|
/** Whether the directory itself exists, which is what tells an empty list from a missing one. */
|
|
166
238
|
export function agentsDirExists({ dir = AGENTS_DIR } = {}) {
|
|
167
239
|
try {
|
|
@@ -205,7 +277,11 @@ function insertionIndex(block) {
|
|
|
205
277
|
* that changed, never re-emit the document from a parse of it.
|
|
206
278
|
*/
|
|
207
279
|
export function rewriteAgentField(text, field, value) {
|
|
208
|
-
|
|
280
|
+
// Rejoin with whatever the file already used. A checkout on Windows is CRLF
|
|
281
|
+
// throughout, and splitting on \n then rejoining with it would leave every
|
|
282
|
+
// line this function did not touch ending \r\n and the one it did ending \n.
|
|
283
|
+
const eol = /\r\n/.test(text) ? '\r\n' : '\n';
|
|
284
|
+
const lines = text.split(/\r?\n/);
|
|
209
285
|
if (lines[0]?.trim() !== '---') {
|
|
210
286
|
throw new Error('This file has no frontmatter block, so there is nothing to set.');
|
|
211
287
|
}
|
|
@@ -230,7 +306,7 @@ export function rewriteAgentField(text, field, value) {
|
|
|
230
306
|
block[at] = `${field}: ${value}`;
|
|
231
307
|
}
|
|
232
308
|
|
|
233
|
-
return [lines[0], ...block, ...lines.slice(end)].join(
|
|
309
|
+
return [lines[0], ...block, ...lines.slice(end)].join(eol);
|
|
234
310
|
}
|
|
235
311
|
|
|
236
312
|
function normaliseAgentValue(field, value) {
|
package/src/checks.mjs
CHANGED
|
@@ -2,6 +2,13 @@ import path from 'node:path';
|
|
|
2
2
|
import { parseFrontmatter } from './parse.mjs';
|
|
3
3
|
|
|
4
4
|
export const VALID_TYPES = ['user', 'feedback', 'project', 'reference'];
|
|
5
|
+
|
|
6
|
+
/** The scope each subagent store kind stands for, for the messages below. */
|
|
7
|
+
const STORE_SCOPES = {
|
|
8
|
+
'agent-user': 'user',
|
|
9
|
+
'agent-project': 'project',
|
|
10
|
+
'agent-local': 'local',
|
|
11
|
+
};
|
|
5
12
|
export const EMPTY_BODY_CHARS = 40;
|
|
6
13
|
export const EMPTY_INSTRUCTION_CHARS = 10;
|
|
7
14
|
export const HOOK_ECHO_OVERLAP = 0.9;
|
|
@@ -218,6 +225,68 @@ export function checkNoMemoryDespiteSessions(store, memories, retention) {
|
|
|
218
225
|
}];
|
|
219
226
|
}
|
|
220
227
|
|
|
228
|
+
/**
|
|
229
|
+
* A subagent store nothing declares any more.
|
|
230
|
+
*
|
|
231
|
+
* The directory only exists because some agent file once carried a `memory:`
|
|
232
|
+
* field naming this scope. Rename the agent, move it to another scope or drop
|
|
233
|
+
* the field and the directory stays exactly where it is, still holding whatever
|
|
234
|
+
* it learned, and no session will ever read it again. That is invisible on disk,
|
|
235
|
+
* which is the whole reason to say it here.
|
|
236
|
+
*/
|
|
237
|
+
export function checkAgentStoreOrphan(store) {
|
|
238
|
+
if (!store || !store.linkage || store.linked) return [];
|
|
239
|
+
if (store.declaredScope) return [];
|
|
240
|
+
|
|
241
|
+
return [{
|
|
242
|
+
kind: 'agent-store-orphan',
|
|
243
|
+
severity: 'warn',
|
|
244
|
+
agentName: store.agentName,
|
|
245
|
+
scope: STORE_SCOPES[store.kind] || null,
|
|
246
|
+
defined: Boolean(store.defined),
|
|
247
|
+
}];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The agent still exists, but its `memory:` names a different scope than this store. */
|
|
251
|
+
export function checkAgentStoreScopeMismatch(store) {
|
|
252
|
+
if (!store || !store.linkage || store.linked) return [];
|
|
253
|
+
if (!store.declaredScope) return [];
|
|
254
|
+
|
|
255
|
+
return [{
|
|
256
|
+
kind: 'agent-store-scope-mismatch',
|
|
257
|
+
severity: 'warn',
|
|
258
|
+
agentName: store.agentName,
|
|
259
|
+
scope: STORE_SCOPES[store.kind] || null,
|
|
260
|
+
declaredScope: store.declaredScope,
|
|
261
|
+
declaredBy: store.declaredBy,
|
|
262
|
+
}];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Subagent memory is part of auto memory, so turning auto memory off turns this
|
|
267
|
+
* store off with it: the `memory:` field stops having any effect, the agent
|
|
268
|
+
* launches with no memory instructions and no file tools, and the directory can
|
|
269
|
+
* neither be read nor grow. Nothing about the files themselves shows that.
|
|
270
|
+
*/
|
|
271
|
+
export function checkAgentMemoryInert(store) {
|
|
272
|
+
if (!store || !store.linkage || !store.inert) return [];
|
|
273
|
+
|
|
274
|
+
return [{
|
|
275
|
+
kind: 'agent-memory-inert',
|
|
276
|
+
severity: 'warn',
|
|
277
|
+
agentName: store.agentName,
|
|
278
|
+
setBy: store.inertBy || null,
|
|
279
|
+
}];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function agentStoreChecks(store) {
|
|
283
|
+
return [
|
|
284
|
+
...checkAgentMemoryInert(store),
|
|
285
|
+
...checkAgentStoreOrphan(store),
|
|
286
|
+
...checkAgentStoreScopeMismatch(store),
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
|
|
221
290
|
export function sessionChecks(store, memories, retention, { remembered = false, resolveOrigin = null } = {}) {
|
|
222
291
|
if (!retention) return [];
|
|
223
292
|
return [
|