claude-memory-admin 1.10.0 → 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/ui.mjs +2 -0
- package/public/views/cost.mjs +53 -10
- package/public/views/header.mjs +1 -1
- package/public/views/issue.mjs +24 -0
- package/public/views/stores.mjs +22 -0
- package/public/views/tools.mjs +37 -9
- 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/ui.mjs
CHANGED
|
@@ -277,6 +277,8 @@ export const contextTags = 'mb-1 flex flex-wrap gap-1.5';
|
|
|
277
277
|
export const contextFile = 'font-mono text-2xs break-words text-fg';
|
|
278
278
|
export const contextSize = 'font-mono text-2xs whitespace-nowrap text-fg-subtle';
|
|
279
279
|
export const contextRowButton = 'flex w-full cursor-pointer items-baseline justify-between gap-3.5 border-0 border-b border-line bg-transparent px-1 py-1.5 text-left transition-colors last:border-b-0 hover:bg-surface-sunken';
|
|
280
|
+
export const contextRowStatic = 'flex w-full items-baseline justify-between gap-3.5 border-0 border-b border-line px-1 py-1.5 text-left last:border-b-0';
|
|
281
|
+
export const toolMissingBody = 'mb-2.5 px-1';
|
|
280
282
|
export const contextCaret = 'mr-1.5 inline-block font-mono text-2xs text-fg-subtle';
|
|
281
283
|
export const contextBody = 'mb-1.5 max-h-[28rem] overflow-y-auto rounded-md border border-line bg-surface-sunken px-3.5 py-2.5';
|
|
282
284
|
|
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/header.mjs
CHANGED
|
@@ -41,7 +41,7 @@ function segmentVisible(tab, segment, store) {
|
|
|
41
41
|
}
|
|
42
42
|
if (segment === 'instructions') return global || hasProjectDir(store);
|
|
43
43
|
if (segment === 'cost') return global;
|
|
44
|
-
if (segment === 'tools') return state.tools.
|
|
44
|
+
if (segment === 'tools') return state.tools.length > 0;
|
|
45
45
|
if (global) return false;
|
|
46
46
|
if (segment === 'sessions') return Boolean(store.sessions?.count);
|
|
47
47
|
return true;
|
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/public/views/tools.mjs
CHANGED
|
@@ -15,6 +15,7 @@ const PANELS = {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
const installed = () => state.tools.filter((tool) => tool.found && PANELS[tool.id]);
|
|
18
|
+
const missing = () => state.tools.filter((tool) => !tool.found && PANELS[tool.id]);
|
|
18
19
|
|
|
19
20
|
const openKey = (id) => `toolOpen:${id}`;
|
|
20
21
|
const isOpen = (id) => localStorage.getItem(openKey(id)) === '1';
|
|
@@ -126,19 +127,46 @@ function accordion(container, tool, slot) {
|
|
|
126
127
|
container.append(panel);
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
function notInstalled(container, tool) {
|
|
131
|
+
container.append(node('div', { class: ui.contextRowStatic }, [
|
|
132
|
+
node('div', { class: ui.contextMain }, [
|
|
133
|
+
node('div', { class: ui.contextFile, text: tool.label }),
|
|
134
|
+
]),
|
|
135
|
+
node('div', { class: ui.contextSize, text: 'not installed' }),
|
|
136
|
+
]));
|
|
137
|
+
|
|
138
|
+
container.append(node('div', { class: ui.toolMissingBody }, [
|
|
139
|
+
node('p', { class: ui.noteTight, text: tool.blurb }),
|
|
140
|
+
node('a', {
|
|
141
|
+
class: ui.linkButton,
|
|
142
|
+
href: tool.repo,
|
|
143
|
+
target: '_blank',
|
|
144
|
+
rel: 'noopener noreferrer',
|
|
145
|
+
text: 'View on GitHub \u2197',
|
|
146
|
+
}),
|
|
147
|
+
]));
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
export function renderTools(container) {
|
|
130
|
-
const
|
|
131
|
-
|
|
151
|
+
const found = installed();
|
|
152
|
+
const absent = missing();
|
|
153
|
+
if (!found.length && !absent.length) return;
|
|
132
154
|
|
|
133
|
-
if (!state.aux.tools && localStorage.getItem(OPT_IN) === '1') start({ remember: false });
|
|
134
|
-
|
|
155
|
+
if (found.length && !state.aux.tools && localStorage.getItem(OPT_IN) === '1') start({ remember: false });
|
|
156
|
+
const opted = Boolean(state.aux.tools);
|
|
135
157
|
|
|
136
|
-
|
|
158
|
+
const bar = [
|
|
137
159
|
node('span', { class: ui.sectionLabelInline, text: 'Companion tools' }),
|
|
138
160
|
node('span', { class: ui.listSpacer }),
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
161
|
+
];
|
|
162
|
+
if (found.length && opted) {
|
|
163
|
+
bar.push(node('button', { class: ui.buttonSmall, text: 'Re-read', onclick: readAll }));
|
|
164
|
+
bar.push(node('button', { class: ui.buttonSmall, text: 'Turn off', onclick: turnOff }));
|
|
165
|
+
}
|
|
166
|
+
container.append(node('div', { class: ui.listBar }, bar));
|
|
167
|
+
|
|
168
|
+
if (found.length && !opted) gate(container, found);
|
|
169
|
+
else for (const tool of found) accordion(container, tool, state.aux.tools[tool.id]);
|
|
142
170
|
|
|
143
|
-
for (const tool of
|
|
171
|
+
for (const tool of absent) notInstalled(container, tool);
|
|
144
172
|
}
|
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
|
}
|