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 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, has a store that will never grow again,
112
- which on disk is indistinguishable from one Claude has not learned anything
113
- about yet. The project header says which it is, and names the file that decided.
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` when it is set in the
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-admin",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "Browse, audit and prune the auto memory Claude Code keeps under ~/.claude/projects",
5
5
  "keywords": [
6
6
  "claude",
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
 
@@ -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 = { ...state.aux.cost, agents: data.agents, agentsDirExists: data.agentsDirExists };
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
- row.append(node('div', { class: ui.agentTop }, [
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
- row.append(node('div', { class: ui.agentControls }, [
153
- labelled('model', picker(fields.model.options, agent.model, (value) => saveAgent(agent.file, 'model', value))),
154
- labelled('effort', picker(fields.effort.options, agent.effort, (value) => saveAgent(agent.file, 'effort', value))),
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
 
@@ -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.some((tool) => tool.found);
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;
@@ -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
  }
@@ -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) }),
@@ -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 tools = installed();
131
- if (!tools.length) return;
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
- if (!state.aux.tools) return gate(container, tools);
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
- container.append(node('div', { class: ui.listBar }, [
158
+ const bar = [
137
159
  node('span', { class: ui.sectionLabelInline, text: 'Companion tools' }),
138
160
  node('span', { class: ui.listSpacer }),
139
- node('button', { class: ui.buttonSmall, text: 'Re-read', onclick: readAll }),
140
- node('button', { class: ui.buttonSmall, text: 'Turn off', onclick: turnOff }),
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 tools) accordion(container, tool, state.aux.tools[tool.id]);
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, listAgents, setAgentField } from './src/agents.mjs';
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
- agents: listAgents(),
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
- const agents = setAgentField(body.file, body.field, body.value ?? null);
350
- return sendJson(res, 200, { agents, agentsDir: AGENTS_DIR, agentsDirExists: agentsDirExists() });
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
  }