@echomem/mcp 1.4.49 → 1.4.50
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/dist/durable-entry.js +122 -0
- package/dist/hud/hooks.js +17 -6
- package/dist/local-data-paths.js +1 -1
- package/dist/migrate.js +36 -3
- package/dist/setup.js +260 -89
- package/package.json +2 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { installHeadlessRuntimeSync, readHeadlessRuntimeInstallation } from "./headless-runtime.js";
|
|
6
|
+
import { MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION } from "./package-metadata.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolving a DURABLE path to this package's own files.
|
|
9
|
+
*
|
|
10
|
+
* Anything we write into another program's config — an MCP server entry, a lifecycle hook command —
|
|
11
|
+
* outlives the process that wrote it. Pinning such a path to the npx cache (`…/_npx/<hash>/…`) works
|
|
12
|
+
* until npm garbage-collects that directory, after which the entry silently points at nothing. So
|
|
13
|
+
* prefer, in order: the managed runtime under ~/.echomem, the running copy when it is not itself
|
|
14
|
+
* ephemeral, then a global install.
|
|
15
|
+
*/
|
|
16
|
+
/** True when a resolved entry lives inside npx's throwaway cache (`…/_npx/<hash>/…`). */
|
|
17
|
+
export function isEphemeralNpxPath(entry) {
|
|
18
|
+
return entry.split(path.sep).includes("_npx");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Locate a DURABLE global install of the bridge (the one `npm i -g @echomem/mcp` creates). Global
|
|
22
|
+
* modules sit next to the running node — `<node>/../lib/node_modules` (nvm/unix) or `<node>/node_modules`
|
|
23
|
+
* (Windows). Returns the realpath'd dist entry, or null when the package isn't globally installed.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveGlobalEntry() {
|
|
26
|
+
const nodeDir = path.dirname(process.execPath);
|
|
27
|
+
const pkgParts = MCP_PACKAGE_NAME.split("/"); // ["@echomem", "mcp"]
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.join(nodeDir, "..", "lib", "node_modules", ...pkgParts, "dist", "index.js"),
|
|
30
|
+
path.join(nodeDir, "node_modules", ...pkgParts, "dist", "index.js"),
|
|
31
|
+
];
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(candidate))
|
|
35
|
+
return fs.realpathSync(candidate);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* keep trying */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** The dist directory this module was loaded from. */
|
|
44
|
+
function runningDistDir() {
|
|
45
|
+
return fileURLToPath(new URL("./", import.meta.url));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Absolute path to `dist/<relative>` from the most durable copy of this package available.
|
|
49
|
+
*
|
|
50
|
+
* `ephemeral` in the result flags the last-resort case: no durable copy exists, so the caller is
|
|
51
|
+
* about to persist a path that npm may collect. A working hook now beats no hook at all, but the
|
|
52
|
+
* caller should say so rather than fail silently.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveDurableDistPath(relative) {
|
|
55
|
+
const managed = readHeadlessRuntimeInstallation();
|
|
56
|
+
if (managed?.entry) {
|
|
57
|
+
const candidate = path.join(path.dirname(managed.entry), relative);
|
|
58
|
+
if (fs.existsSync(candidate))
|
|
59
|
+
return { path: candidate, ephemeral: false };
|
|
60
|
+
}
|
|
61
|
+
const local = path.join(runningDistDir(), relative);
|
|
62
|
+
const localIsEphemeral = isEphemeralNpxPath(local);
|
|
63
|
+
if (!localIsEphemeral && fs.existsSync(local))
|
|
64
|
+
return { path: local, ephemeral: false };
|
|
65
|
+
const globalEntry = resolveGlobalEntry();
|
|
66
|
+
if (globalEntry) {
|
|
67
|
+
const candidate = path.join(path.dirname(globalEntry), relative);
|
|
68
|
+
if (fs.existsSync(candidate))
|
|
69
|
+
return { path: candidate, ephemeral: false };
|
|
70
|
+
}
|
|
71
|
+
return { path: local, ephemeral: localIsEphemeral };
|
|
72
|
+
}
|
|
73
|
+
/** Set on the child so a re-exec can never recurse. */
|
|
74
|
+
const REEXEC_GUARD = "ECHO_DURABLE_REEXEC";
|
|
75
|
+
/**
|
|
76
|
+
* Hand a config-writing command over to the durable runtime before it writes anything.
|
|
77
|
+
*
|
|
78
|
+
* `npx @echomem/mcp init` runs from `_npx/<hash>`, a directory npm garbage-collects. Every path such
|
|
79
|
+
* a process persists into someone else's config — hook commands, MCP entries — inherits that
|
|
80
|
+
* lifetime. Re-executing from `~/.echomem/mcp-runtime` first makes those writes durable by
|
|
81
|
+
* construction, instead of relying on each call site to remember.
|
|
82
|
+
*
|
|
83
|
+
* Version skew is resolved by upgrading rather than skipping: the durable copy is installed at the
|
|
84
|
+
* invoking package's version before control passes to it, so a newer `npx` never hands off to an
|
|
85
|
+
* older runtime. Installation is idempotent, so the child re-verifying costs nothing.
|
|
86
|
+
*
|
|
87
|
+
* Returns the child's exit status when it ran (the caller should exit with it), or null when the
|
|
88
|
+
* command should proceed in this process. Every failure path returns null: a re-exec that cannot
|
|
89
|
+
* happen must degrade to the old behaviour, never dead-end.
|
|
90
|
+
*/
|
|
91
|
+
export function reexecFromDurableRuntime(argv) {
|
|
92
|
+
if (process.env[REEXEC_GUARD] === "1")
|
|
93
|
+
return null;
|
|
94
|
+
if (process.env.ECHO_DISABLE_RUNTIME_BOOTSTRAP === "1")
|
|
95
|
+
return null;
|
|
96
|
+
if (!isEphemeralNpxPath(runningDistDir()))
|
|
97
|
+
return null;
|
|
98
|
+
// Version comes from the package.json beside dist/. If that could not be read we would ask the
|
|
99
|
+
// registry for a version that does not exist, so stay in-process rather than spend the round trip.
|
|
100
|
+
if (MCP_PACKAGE_VERSION === "0.0.0")
|
|
101
|
+
return null;
|
|
102
|
+
let installation = readHeadlessRuntimeInstallation();
|
|
103
|
+
if (installation?.version !== MCP_PACKAGE_VERSION) {
|
|
104
|
+
try {
|
|
105
|
+
installation = installHeadlessRuntimeSync(MCP_PACKAGE_VERSION);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!installation?.entry || !fs.existsSync(installation.entry))
|
|
112
|
+
return null;
|
|
113
|
+
console.log(`Running from the durable ${MCP_PACKAGE_NAME}@${installation.version} runtime so configured paths outlive the npx cache.`);
|
|
114
|
+
const result = spawnSync(process.execPath, [installation.entry, ...argv], {
|
|
115
|
+
stdio: "inherit",
|
|
116
|
+
env: { ...process.env, [REEXEC_GUARD]: "1" },
|
|
117
|
+
windowsHide: true,
|
|
118
|
+
});
|
|
119
|
+
if (result.error)
|
|
120
|
+
return null;
|
|
121
|
+
return result.status ?? 0;
|
|
122
|
+
}
|
package/dist/hud/hooks.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveDurableDistPath } from "../durable-entry.js";
|
|
5
|
+
/**
|
|
6
|
+
* Hook commands are persisted into another program's settings and must keep working long after this
|
|
7
|
+
* process exits. Resolving them from `import.meta.url` pins them to whatever copy happens to be
|
|
8
|
+
* running — under `npx` that is the `_npx/<hash>` cache, which npm garbage-collects, leaving a hook
|
|
9
|
+
* that points at nothing. Always resolve through the durable-entry helper instead.
|
|
10
|
+
*/
|
|
11
|
+
function hookCommandFor(distRelative, subcommand) {
|
|
12
|
+
const cli = resolveDurableDistPath(distRelative);
|
|
13
|
+
if (cli.ephemeral) {
|
|
14
|
+
console.warn(`⚠️ EchoMem hooks are being pinned to a temporary npx path (${cli.path}). They will stop working once npm clears its cache — install the durable runtime or a global package to make them permanent.`);
|
|
15
|
+
}
|
|
16
|
+
return `${JSON.stringify(process.execPath)} ${JSON.stringify(cli.path)} ${subcommand}`;
|
|
17
|
+
}
|
|
5
18
|
export function installHooks(mode) {
|
|
6
19
|
const written = [];
|
|
7
20
|
if (mode === "codex" || mode === "both" || mode === "auto") {
|
|
@@ -36,7 +49,7 @@ function installCodexHooks() {
|
|
|
36
49
|
const dir = path.join(os.homedir(), ".codex");
|
|
37
50
|
const file = path.join(dir, "hooks.json");
|
|
38
51
|
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
-
const hookCommand =
|
|
52
|
+
const hookCommand = hookCommandFor("index.js", "summary --client codex --json");
|
|
40
53
|
const content = readHooksFile(file);
|
|
41
54
|
content.hooks = content.hooks || {};
|
|
42
55
|
content.hooks.PostToolUse = mergeHookGroup(content.hooks.PostToolUse, { matcher: "*", hooks: [{ type: "command", command: hookCommand, timeout: 5 }] });
|
|
@@ -46,12 +59,10 @@ function installCodexHooks() {
|
|
|
46
59
|
return file;
|
|
47
60
|
}
|
|
48
61
|
function saveCheckpointCommand() {
|
|
49
|
-
|
|
50
|
-
return `${JSON.stringify(process.execPath)} ${JSON.stringify(lifecycleCli)} save-checkpoint`;
|
|
62
|
+
return hookCommandFor(path.join("hud", "cli.js"), "save-checkpoint");
|
|
51
63
|
}
|
|
52
64
|
function sourceSessionCommand() {
|
|
53
|
-
|
|
54
|
-
return `${JSON.stringify(process.execPath)} ${JSON.stringify(lifecycleCli)} bind-source-session`;
|
|
65
|
+
return hookCommandFor(path.join("hud", "cli.js"), "bind-source-session");
|
|
55
66
|
}
|
|
56
67
|
function installCodexSourceSessionHook() {
|
|
57
68
|
const dir = path.join(os.homedir(), ".codex");
|
package/dist/local-data-paths.js
CHANGED
|
@@ -97,7 +97,7 @@ export function resolveClaudeDesktopSupportRoots(opts = {}) {
|
|
|
97
97
|
if (typeof configured === "string" && configured.trim()) {
|
|
98
98
|
candidates.push(expandCurrentUserHome(configured, homeDir));
|
|
99
99
|
}
|
|
100
|
-
if (platform === "darwin") {
|
|
100
|
+
else if (platform === "darwin") {
|
|
101
101
|
candidates.push(path.join(homeDir, "Library", "Application Support", "Claude"));
|
|
102
102
|
}
|
|
103
103
|
else if (platform === "win32") {
|
package/dist/migrate.js
CHANGED
|
@@ -320,16 +320,49 @@ function hasClaudeText(content) {
|
|
|
320
320
|
return false;
|
|
321
321
|
return content.some((block) => isRecord(block) && block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0);
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* True when this turn opens the conversation rather than continuing one.
|
|
325
|
+
*
|
|
326
|
+
* `parentUuid === null` is the common case, but a session started by dropping in a file opens with
|
|
327
|
+
* an `attachment` record and parents its first real turn to that — requiring a null parent silently
|
|
328
|
+
* discarded those whole transcripts. Walk the chain instead: a turn opens the conversation when
|
|
329
|
+
* nothing conversational precedes it in this file. A parent the file does not contain means the
|
|
330
|
+
* thread continues from somewhere else, which is not a user-created session.
|
|
331
|
+
*/
|
|
332
|
+
function startsConversation(obj, byUuid) {
|
|
333
|
+
let parentUuid = obj.parentUuid;
|
|
334
|
+
for (let depth = 0; depth < 64; depth += 1) {
|
|
335
|
+
if (parentUuid === null || parentUuid === undefined)
|
|
336
|
+
return true;
|
|
337
|
+
if (typeof parentUuid !== "string")
|
|
338
|
+
return false;
|
|
339
|
+
const parent = byUuid.get(parentUuid);
|
|
340
|
+
if (!parent)
|
|
341
|
+
return false;
|
|
342
|
+
if (parent.type === "user" || parent.type === "assistant")
|
|
343
|
+
return false;
|
|
344
|
+
parentUuid = parent.parentUuid;
|
|
345
|
+
}
|
|
346
|
+
return false; // pathological chain; treat as not session-initiating rather than loop
|
|
347
|
+
}
|
|
323
348
|
function isUserCreatedClaudeSessionFile(file) {
|
|
324
|
-
|
|
325
|
-
|
|
349
|
+
const objects = initialJsonObjects(file).filter(isRecord);
|
|
350
|
+
const byUuid = new Map();
|
|
351
|
+
for (const obj of objects) {
|
|
352
|
+
if (typeof obj.uuid === "string" && obj.uuid)
|
|
353
|
+
byUuid.set(obj.uuid, obj);
|
|
354
|
+
}
|
|
355
|
+
return objects.some((obj) => {
|
|
356
|
+
if (obj.type !== "user")
|
|
326
357
|
return false;
|
|
327
|
-
if (obj.userType !== "external" || obj.isSidechain !== false
|
|
358
|
+
if (obj.userType !== "external" || obj.isSidechain !== false)
|
|
328
359
|
return false;
|
|
329
360
|
if (obj.isMeta === true || obj.isCompactSummary === true)
|
|
330
361
|
return false;
|
|
331
362
|
if (typeof obj.agentId === "string" && obj.agentId.trim())
|
|
332
363
|
return false;
|
|
364
|
+
if (!startsConversation(obj, byUuid))
|
|
365
|
+
return false;
|
|
333
366
|
const message = isRecord(obj.message) ? obj.message : {};
|
|
334
367
|
return hasClaudeText(message.content);
|
|
335
368
|
});
|
package/dist/setup.js
CHANGED
|
@@ -26,11 +26,12 @@ import axios from "axios";
|
|
|
26
26
|
import { KeyStore } from "./keystore.js";
|
|
27
27
|
import { fetchEncryptionConfig, deriveAndVerifyKey, setupNewEncryptionKey, verifyKeyB64 } from "./encryption.js";
|
|
28
28
|
import { buildOnboardingStatsPayload } from "./onboarding-stats.js";
|
|
29
|
-
import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, discoverMigratableSessions, discoverPendingSessionsTargeted, estimateMigrationEta, fetchProcessedImportKeys, isImportStatusUnsupported, markAccountImportStatusFailed, markAccountImportStatusUnavailable, markFastAccountImportStatusUnavailable, startMigration, summarizeFastMigratableDiscovery, MIGRATE_CONCURRENCY, } from "./migrate.js";
|
|
29
|
+
import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, discoverMigratableSessions, discoverPendingSessionsTargeted, estimateMigrationEta, fetchProcessedImportKeys, isImportStatusUnsupported, markAccountImportStatusFailed, markAccountImportStatusUnavailable, markFastAccountImportStatusUnavailable, startMigration, summarizeFastMigratableDiscovery, discoverSessions, dedupeSessionsByConversation, MIGRATE_CONCURRENCY, } from "./migrate.js";
|
|
30
30
|
import { syncCodexUsage } from "./codex-sync.js";
|
|
31
31
|
import { renderSetupPage } from "./setup-page.js";
|
|
32
32
|
import { parseSetupPreviewState } from "./setup-preview.js";
|
|
33
|
-
import { resolveClaudeDesktopSupportRoots } from "./local-data-paths.js";
|
|
33
|
+
import { resolveClaudeCoworkSessionRoots, resolveClaudeDesktopSupportRoots, resolveClaudeProjectsDir, resolveCodexSessionRoots, } from "./local-data-paths.js";
|
|
34
|
+
import { isEphemeralNpxPath, reexecFromDurableRuntime, resolveDurableDistPath, resolveGlobalEntry } from "./durable-entry.js";
|
|
34
35
|
import { installSaveCheckpointHooks, installSourceSessionHooks } from "./hud/hooks.js";
|
|
35
36
|
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
36
37
|
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
@@ -216,18 +217,45 @@ function runtimeModuleUrl(name) {
|
|
|
216
217
|
}
|
|
217
218
|
return jsUrl.href;
|
|
218
219
|
}
|
|
220
|
+
function claudeDesktopRootHasBundledCode(supportRoot, platform) {
|
|
221
|
+
return versionedClaudeCodeExecutables(path.join(supportRoot, "claude-code"), platform === "win32" ? "claude.exe" : "claude", 1).length > 0;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Config targeting needs installation evidence; history discovery intentionally does not.
|
|
225
|
+
* A stale support directory can still contain importable Cowork history, but must not make setup
|
|
226
|
+
* claim that Claude Desktop is installed. An explicit support-root override is itself evidence.
|
|
227
|
+
*/
|
|
228
|
+
export function claudeDesktopConfigRoots(defaultRoot, options = {}) {
|
|
229
|
+
const env = options.env ?? process.env;
|
|
230
|
+
const platform = options.platform ?? process.platform;
|
|
231
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
232
|
+
const supportRoots = resolveClaudeDesktopSupportRoots(options);
|
|
233
|
+
if (env.CLAUDE_DESKTOP_SUPPORT_DIR?.trim())
|
|
234
|
+
return supportRoots;
|
|
235
|
+
if (platform === "darwin") {
|
|
236
|
+
const installed = [
|
|
237
|
+
"/Applications/Claude.app",
|
|
238
|
+
path.join(homeDir, "Applications", "Claude.app"),
|
|
239
|
+
].some((appPath) => fs.existsSync(appPath));
|
|
240
|
+
if (!installed)
|
|
241
|
+
return [];
|
|
242
|
+
return supportRoots.length > 0 ? supportRoots : [defaultRoot];
|
|
243
|
+
}
|
|
244
|
+
return supportRoots.filter((supportRoot) => claudeDesktopRootHasBundledCode(supportRoot, platform));
|
|
245
|
+
}
|
|
219
246
|
/** Known clients and where their MCP server map lives. */
|
|
220
247
|
export function knownClients() {
|
|
221
248
|
const appSupport = process.platform === "darwin"
|
|
222
249
|
? home("Library", "Application Support")
|
|
223
250
|
: process.env.APPDATA || home(".config");
|
|
224
251
|
const defaultClaudeDesktopRoot = path.join(appSupport, "Claude");
|
|
225
|
-
const claudeDesktopRoots =
|
|
252
|
+
const claudeDesktopRoots = claudeDesktopConfigRoots(defaultClaudeDesktopRoot);
|
|
226
253
|
const claudeDesktopClients = (claudeDesktopRoots.length > 0 ? claudeDesktopRoots : [defaultClaudeDesktopRoot]).map((supportRoot) => ({
|
|
227
254
|
id: "claude-desktop",
|
|
228
255
|
label: "Claude Desktop",
|
|
229
256
|
kind: "json",
|
|
230
257
|
configPath: path.join(supportRoot, "claude_desktop_config.json"),
|
|
258
|
+
detected: claudeDesktopRoots.length > 0,
|
|
231
259
|
}));
|
|
232
260
|
return [
|
|
233
261
|
{ id: "cursor", label: "Cursor", kind: "json", configPath: home(".cursor", "mcp.json") },
|
|
@@ -238,14 +266,27 @@ export function knownClients() {
|
|
|
238
266
|
];
|
|
239
267
|
}
|
|
240
268
|
/** A client is "present" if its config dir already exists — a cheap, side-effect-free heuristic. */
|
|
269
|
+
/**
|
|
270
|
+
* Claude Code is present when its CLI runs OR when it has left a profile behind.
|
|
271
|
+
*
|
|
272
|
+
* The CLI probe alone misses a Store-packaged Claude Desktop, whose bundled binary never reaches
|
|
273
|
+
* PATH; a profile alone used to mean "detected but unconfigurable". Now that writeClaudeCodeConfig
|
|
274
|
+
* can merge the user entry into ~/.claude.json without a CLI, a profile is enough to act on.
|
|
275
|
+
*/
|
|
276
|
+
export function claudeCodeInstalled() {
|
|
277
|
+
return fs.existsSync(home(".claude")) || fs.existsSync(home(".claude.json")) || claudeCodeCliAvailable();
|
|
278
|
+
}
|
|
241
279
|
export function detectClients() {
|
|
242
280
|
return knownClients().filter((c) => {
|
|
243
|
-
if (c.kind === "json")
|
|
281
|
+
if (c.kind === "json") {
|
|
282
|
+
if (c.id === "claude-desktop")
|
|
283
|
+
return c.detected === true;
|
|
244
284
|
return fs.existsSync(path.dirname(c.configPath));
|
|
285
|
+
}
|
|
245
286
|
if (c.kind === "command")
|
|
246
287
|
return fs.existsSync(c.detectDir);
|
|
247
288
|
if (c.id === "claude-code")
|
|
248
|
-
return
|
|
289
|
+
return claudeCodeInstalled();
|
|
249
290
|
return false;
|
|
250
291
|
});
|
|
251
292
|
}
|
|
@@ -287,33 +328,6 @@ export function buildServerEntry(opts = {}) {
|
|
|
287
328
|
// Fallback (unresolved local install): at least drop `-y` so npx doesn't auto-INSTALL on every start.
|
|
288
329
|
return { command: "npx", args: ["@echomem/mcp"] };
|
|
289
330
|
}
|
|
290
|
-
/** True when a resolved entry lives inside npx's throwaway cache (`…/_npx/<hash>/…`). */
|
|
291
|
-
function isEphemeralNpxPath(entry) {
|
|
292
|
-
return entry.split(path.sep).includes("_npx");
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Locate a DURABLE global install of the bridge (the one `npm i -g @echomem/mcp` creates). Global
|
|
296
|
-
* modules sit next to the running node — `<node>/../lib/node_modules` (nvm/unix) or `<node>/node_modules`
|
|
297
|
-
* (Windows). Returns the realpath'd dist entry, or null when the package isn't globally installed.
|
|
298
|
-
*/
|
|
299
|
-
function resolveGlobalEntry() {
|
|
300
|
-
const nodeDir = path.dirname(process.execPath);
|
|
301
|
-
const pkgParts = MCP_PACKAGE_NAME.split("/"); // ["@echomem", "mcp"]
|
|
302
|
-
const candidates = [
|
|
303
|
-
path.join(nodeDir, "..", "lib", "node_modules", ...pkgParts, "dist", "index.js"),
|
|
304
|
-
path.join(nodeDir, "node_modules", ...pkgParts, "dist", "index.js"),
|
|
305
|
-
];
|
|
306
|
-
for (const candidate of candidates) {
|
|
307
|
-
try {
|
|
308
|
-
if (fs.existsSync(candidate))
|
|
309
|
-
return fs.realpathSync(candidate);
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
/* keep trying */
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
331
|
function installDurableHeadlessRuntime() {
|
|
318
332
|
if (process.env.ECHO_DISABLE_RUNTIME_BOOTSTRAP === "1")
|
|
319
333
|
return;
|
|
@@ -552,7 +566,7 @@ function claudeCodeLocalEchoMemProjects(configPath) {
|
|
|
552
566
|
.map(([projectPath]) => projectPath)
|
|
553
567
|
.sort();
|
|
554
568
|
}
|
|
555
|
-
function versionedClaudeCodeExecutables(root, executable) {
|
|
569
|
+
function versionedClaudeCodeExecutables(root, executable, limit = Number.POSITIVE_INFINITY) {
|
|
556
570
|
let versions;
|
|
557
571
|
try {
|
|
558
572
|
versions = fs.readdirSync(root, { withFileTypes: true });
|
|
@@ -564,7 +578,8 @@ function versionedClaudeCodeExecutables(root, executable) {
|
|
|
564
578
|
.filter((entry) => entry.isDirectory())
|
|
565
579
|
.sort((left, right) => right.name.localeCompare(left.name, undefined, { numeric: true }))
|
|
566
580
|
.map((entry) => path.join(root, entry.name, executable))
|
|
567
|
-
.filter((candidate) => fs.existsSync(candidate))
|
|
581
|
+
.filter((candidate) => fs.existsSync(candidate))
|
|
582
|
+
.slice(0, limit);
|
|
568
583
|
}
|
|
569
584
|
/**
|
|
570
585
|
* Return every safe Claude Code launcher location worth trying. Claude Desktop bundles the CLI,
|
|
@@ -583,13 +598,13 @@ export function claudeCodeCommandCandidates(options = {}) {
|
|
|
583
598
|
if (platform === "win32") {
|
|
584
599
|
const appData = env.APPDATA?.trim() || path.join(homeDir, "AppData", "Roaming");
|
|
585
600
|
const localAppData = env.LOCALAPPDATA?.trim() || path.join(homeDir, "AppData", "Local");
|
|
586
|
-
candidates.push(...versionedClaudeCodeExecutables(path.join(appData, "Claude", "claude-code"), "claude.exe"), ...versionedClaudeCodeExecutables(path.join(localAppData, "Claude", "claude-code"), "claude.exe"));
|
|
601
|
+
candidates.push(...versionedClaudeCodeExecutables(path.join(appData, "Claude", "claude-code"), "claude.exe", 1), ...versionedClaudeCodeExecutables(path.join(localAppData, "Claude", "claude-code"), "claude.exe", 1));
|
|
587
602
|
const packagesRoot = path.join(localAppData, "Packages");
|
|
588
603
|
try {
|
|
589
604
|
for (const entry of fs.readdirSync(packagesRoot, { withFileTypes: true })) {
|
|
590
605
|
if (!entry.isDirectory() || !/^Claude_/i.test(entry.name))
|
|
591
606
|
continue;
|
|
592
|
-
candidates.push(...versionedClaudeCodeExecutables(path.join(packagesRoot, entry.name, "LocalCache", "Roaming", "Claude", "claude-code"), "claude.exe"));
|
|
607
|
+
candidates.push(...versionedClaudeCodeExecutables(path.join(packagesRoot, entry.name, "LocalCache", "Roaming", "Claude", "claude-code"), "claude.exe", 1));
|
|
593
608
|
}
|
|
594
609
|
}
|
|
595
610
|
catch {
|
|
@@ -622,55 +637,106 @@ function escapeWindowsCmdArgument(value) {
|
|
|
622
637
|
escaped = `"${escaped}"`;
|
|
623
638
|
return escaped.replace(/([()\][%!^"`<>&|;, *?])/g, "^$1");
|
|
624
639
|
}
|
|
625
|
-
function
|
|
640
|
+
function execClaudeCodeCommandSync(command, args, options) {
|
|
641
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
|
|
642
|
+
const shellCommand = [escapeWindowsCmdCommand(command), ...args.map(escapeWindowsCmdArgument)].join(" ");
|
|
643
|
+
const spawnOptions = {
|
|
644
|
+
...options,
|
|
645
|
+
windowsHide: true,
|
|
646
|
+
windowsVerbatimArguments: true,
|
|
647
|
+
};
|
|
648
|
+
const result = spawnSync(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${shellCommand}"`], spawnOptions);
|
|
649
|
+
if (result.error)
|
|
650
|
+
throw result.error;
|
|
651
|
+
if (result.status !== 0) {
|
|
652
|
+
throw new Error(result.stderr?.trim() || `Claude Code exited with status ${result.status ?? "unknown"}.`);
|
|
653
|
+
}
|
|
654
|
+
return result.stdout || "";
|
|
655
|
+
}
|
|
656
|
+
return execFileSync(command, args, process.platform === "win32" ? { ...options, windowsHide: true } : options);
|
|
657
|
+
}
|
|
658
|
+
function claudeCodeCommandsOnPath(timeout) {
|
|
659
|
+
if (process.platform !== "win32" || timeout <= 0)
|
|
660
|
+
return [];
|
|
661
|
+
try {
|
|
662
|
+
return execFileSync("where.exe", ["claude"], {
|
|
663
|
+
encoding: "utf8",
|
|
664
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
665
|
+
timeout,
|
|
666
|
+
windowsHide: true,
|
|
667
|
+
}).split(/\r?\n/).map((candidate) => candidate.trim()).filter(Boolean);
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
return [];
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
let cachedClaudeCodeCommand = null;
|
|
674
|
+
function resolveRunnableClaudeCodeCommand(commandCandidates, deadline = Date.now() + 3000) {
|
|
675
|
+
if (!commandCandidates && cachedClaudeCodeCommand)
|
|
676
|
+
return cachedClaudeCodeCommand;
|
|
626
677
|
let candidates = commandCandidates ? [...commandCandidates] : claudeCodeCommandCandidates();
|
|
627
|
-
if (
|
|
678
|
+
if (!commandCandidates) {
|
|
679
|
+
const remainingForPath = Math.max(1, Math.min(1000, deadline - Date.now()));
|
|
680
|
+
candidates = [...new Set([...claudeCodeCommandsOnPath(remainingForPath), ...candidates])];
|
|
681
|
+
}
|
|
682
|
+
for (const command of candidates) {
|
|
683
|
+
const remaining = deadline - Date.now();
|
|
684
|
+
if (remaining <= 0)
|
|
685
|
+
break;
|
|
628
686
|
try {
|
|
629
|
-
|
|
687
|
+
execClaudeCodeCommandSync(command, ["--version"], {
|
|
630
688
|
encoding: "utf8",
|
|
631
689
|
stdio: ["ignore", "pipe", "ignore"],
|
|
632
|
-
timeout:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
690
|
+
timeout: remaining,
|
|
691
|
+
});
|
|
692
|
+
if (!commandCandidates)
|
|
693
|
+
cachedClaudeCodeCommand = command;
|
|
694
|
+
return command;
|
|
636
695
|
}
|
|
637
696
|
catch {
|
|
638
|
-
/*
|
|
697
|
+
/* Keep trying only while the shared deadline has time left. */
|
|
639
698
|
}
|
|
640
699
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
windowsVerbatimArguments: true,
|
|
650
|
-
};
|
|
651
|
-
const result = spawnSync(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${shellCommand}"`], spawnOptions);
|
|
652
|
-
if (result.error)
|
|
653
|
-
throw result.error;
|
|
654
|
-
if (result.status !== 0) {
|
|
655
|
-
throw new Error(result.stderr?.trim() || `Claude Code exited with status ${result.status ?? "unknown"}.`);
|
|
656
|
-
}
|
|
657
|
-
return result.stdout || "";
|
|
658
|
-
}
|
|
659
|
-
return execFileSync(command, args, process.platform === "win32" ? { ...options, windowsHide: true } : options);
|
|
660
|
-
}
|
|
661
|
-
catch (error) {
|
|
662
|
-
lastError = error;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
throw lastError;
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
function execClaudeCodeSync(args, options, commandCandidates) {
|
|
703
|
+
const probeBudget = typeof options.timeout === "number" ? Math.min(options.timeout, 3000) : 3000;
|
|
704
|
+
const command = resolveRunnableClaudeCodeCommand(commandCandidates, Date.now() + probeBudget);
|
|
705
|
+
if (!command)
|
|
706
|
+
throw new Error("Claude Code CLI was not found within the setup time budget.");
|
|
707
|
+
return execClaudeCodeCommandSync(command, args, options);
|
|
666
708
|
}
|
|
667
709
|
export function claudeCodeCliAvailable() {
|
|
710
|
+
return resolveRunnableClaudeCodeCommand(undefined, Date.now() + 3000) !== null;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Merge the user-scope EchoMem entry straight into ~/.claude.json.
|
|
714
|
+
*
|
|
715
|
+
* Reserved for machines where NO Claude Code CLI can be executed at all — a Store-packaged Claude
|
|
716
|
+
* Desktop whose bundled binary never lands on PATH, an npm shim in a stripped environment. Without
|
|
717
|
+
* this, such a machine can never be configured, even though the file is trivial to update.
|
|
718
|
+
*
|
|
719
|
+
* Deliberately narrow: read immediately before write, touch only `mcpServers.echomem`, preserve
|
|
720
|
+
* every other key (this file also holds Claude account and session state), and swap it in via a
|
|
721
|
+
* same-directory temp file so a crash cannot truncate it. Project-local entries are still left to
|
|
722
|
+
* the CLI — removing those means knowing which project a scope belongs to.
|
|
723
|
+
*/
|
|
724
|
+
function writeUserEntryDirectly(configPath, entry) {
|
|
668
725
|
try {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
726
|
+
const raw = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8").trim() : "";
|
|
727
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
728
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
729
|
+
return false;
|
|
730
|
+
const config = parsed;
|
|
731
|
+
const existing = config.mcpServers;
|
|
732
|
+
const servers = typeof existing === "object" && existing !== null && !Array.isArray(existing)
|
|
733
|
+
? existing
|
|
734
|
+
: {};
|
|
735
|
+
servers.echomem = entry;
|
|
736
|
+
config.mcpServers = servers;
|
|
737
|
+
const tmp = `${configPath}.echomem-${process.pid}.tmp`;
|
|
738
|
+
fs.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}\n`);
|
|
739
|
+
fs.renameSync(tmp, configPath);
|
|
674
740
|
return true;
|
|
675
741
|
}
|
|
676
742
|
catch {
|
|
@@ -682,6 +748,7 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
682
748
|
// Older CLI versions wrote local/project entries, which take precedence over user scope and can
|
|
683
749
|
// keep launching a deleted npm cache or stale runtime. Migrate those only after user scope is safe.
|
|
684
750
|
const configPath = options.configPath ?? home(".claude.json");
|
|
751
|
+
const configurationDeadline = Date.now() + 5000;
|
|
685
752
|
let failureReason;
|
|
686
753
|
const emptyResult = () => ({
|
|
687
754
|
state: "unavailable",
|
|
@@ -692,14 +759,27 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
692
759
|
preservedDesktopManaged: false,
|
|
693
760
|
failureReason,
|
|
694
761
|
});
|
|
762
|
+
let runnableCommand;
|
|
763
|
+
const cliCommand = () => {
|
|
764
|
+
if (runnableCommand === undefined) {
|
|
765
|
+
runnableCommand = resolveRunnableClaudeCodeCommand(options.claudeCommands, configurationDeadline);
|
|
766
|
+
}
|
|
767
|
+
return runnableCommand;
|
|
768
|
+
};
|
|
695
769
|
const runClaude = (args, cwd) => {
|
|
770
|
+
const command = cliCommand();
|
|
771
|
+
const remaining = configurationDeadline - Date.now();
|
|
772
|
+
if (!command || remaining <= 0) {
|
|
773
|
+
failureReason = "Claude Code CLI was unavailable within the onboarding time budget.";
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
696
776
|
try {
|
|
697
|
-
|
|
777
|
+
execClaudeCodeCommandSync(command, args, {
|
|
698
778
|
cwd,
|
|
699
779
|
encoding: "utf8",
|
|
700
780
|
stdio: ["ignore", "pipe", "pipe"],
|
|
701
|
-
timeout:
|
|
702
|
-
}
|
|
781
|
+
timeout: Math.min(remaining, 5000),
|
|
782
|
+
});
|
|
703
783
|
failureReason = undefined;
|
|
704
784
|
return true;
|
|
705
785
|
}
|
|
@@ -718,16 +798,31 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
718
798
|
&& validDesktopManagedEntry(previousUserEntry);
|
|
719
799
|
const desiredUserEntry = preservedDesktopManaged ? previousUserEntry : entry;
|
|
720
800
|
let restoredPreviousUserEntry = false;
|
|
801
|
+
let usedDirectWrite = false;
|
|
802
|
+
const fallbackWrite = () => {
|
|
803
|
+
// A working CLI that refuses is a real failure; masking it would hide a genuine problem.
|
|
804
|
+
if (cliCommand())
|
|
805
|
+
return false;
|
|
806
|
+
if (!writeUserEntryDirectly(configPath, desiredUserEntry))
|
|
807
|
+
return false;
|
|
808
|
+
usedDirectWrite = true;
|
|
809
|
+
failureReason = undefined;
|
|
810
|
+
return true;
|
|
811
|
+
};
|
|
721
812
|
// Avoid interrupting active/new sessions when the correct global entry is already installed.
|
|
722
813
|
if (!claudeEntriesMatch(previousUserEntry, desiredUserEntry)) {
|
|
723
|
-
if (previousUserEntry && !removeUser())
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
814
|
+
if (previousUserEntry && !removeUser()) {
|
|
815
|
+
if (!fallbackWrite())
|
|
816
|
+
return emptyResult();
|
|
817
|
+
}
|
|
818
|
+
else if (!addUser(desiredUserEntry)) {
|
|
819
|
+
if (!fallbackWrite()) {
|
|
820
|
+
const addFailureReason = failureReason;
|
|
821
|
+
if (previousUserEntry)
|
|
822
|
+
restoredPreviousUserEntry = addUser(previousUserEntry);
|
|
823
|
+
failureReason = addFailureReason;
|
|
824
|
+
return { ...emptyResult(), restoredPreviousUserEntry };
|
|
825
|
+
}
|
|
731
826
|
}
|
|
732
827
|
}
|
|
733
828
|
const installedUserEntry = echoMemEntryFromServers(readClaudeCodeConfigFile(configPath).mcpServers);
|
|
@@ -768,6 +863,7 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
768
863
|
failedLocalProjects: unresolved,
|
|
769
864
|
restoredPreviousUserEntry,
|
|
770
865
|
preservedDesktopManaged,
|
|
866
|
+
usedDirectWrite,
|
|
771
867
|
};
|
|
772
868
|
}
|
|
773
869
|
function readJsonClientEntry(configPath) {
|
|
@@ -2611,6 +2707,11 @@ function parseFlags(argv) {
|
|
|
2611
2707
|
}
|
|
2612
2708
|
return flags;
|
|
2613
2709
|
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Configure MCP clients without starting local-history HTML onboarding. Echo Desktop on macOS owns
|
|
2712
|
+
* its native onboarding and invokes this setup surface with --skip-login; only CLI `init` below owns
|
|
2713
|
+
* the browser flow used by Windows/headless users.
|
|
2714
|
+
*/
|
|
2614
2715
|
async function cmdSetup(flags) {
|
|
2615
2716
|
if (!flags.dev && !flags["skip-runtime-install"]) {
|
|
2616
2717
|
try {
|
|
@@ -2663,6 +2764,9 @@ async function cmdSetup(flags) {
|
|
|
2663
2764
|
if (result.preservedDesktopManaged) {
|
|
2664
2765
|
console.log(`✅ Kept the valid externally managed EchoMem user entry for ${c.label}.`);
|
|
2665
2766
|
}
|
|
2767
|
+
else if (result.usedDirectWrite) {
|
|
2768
|
+
console.log(`✅ Wrote EchoMem MCP entry to ${c.label} directly (no runnable \`claude\` CLI was found) — start a new Claude Code session to load it.`);
|
|
2769
|
+
}
|
|
2666
2770
|
else {
|
|
2667
2771
|
console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
|
|
2668
2772
|
}
|
|
@@ -2733,7 +2837,8 @@ async function cmdSetup(flags) {
|
|
|
2733
2837
|
}
|
|
2734
2838
|
}
|
|
2735
2839
|
/**
|
|
2736
|
-
* `echomem-mcp init` — the one-command install
|
|
2840
|
+
* `echomem-mcp init` — the CLI-owned one-command install used by Windows/headless users. Configures
|
|
2841
|
+
* EVERY coding agent installed on this
|
|
2737
2842
|
* machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), installs EchoMem's
|
|
2738
2843
|
* Codex skills and writes the AGENTS.md memory guidance. One browser
|
|
2739
2844
|
* bridge then runs permission → login → plan if needed → extraction in that order.
|
|
@@ -2835,11 +2940,14 @@ async function cmdUpdate(flags) {
|
|
|
2835
2940
|
function selectSetupTargets(requested, all) {
|
|
2836
2941
|
if (all || requested === "all") {
|
|
2837
2942
|
return knownClients().filter((client) => {
|
|
2838
|
-
if (client.kind === "json")
|
|
2943
|
+
if (client.kind === "json") {
|
|
2944
|
+
if (client.id === "claude-desktop")
|
|
2945
|
+
return client.detected === true;
|
|
2839
2946
|
return fs.existsSync(path.dirname(client.configPath));
|
|
2947
|
+
}
|
|
2840
2948
|
if (client.kind === "command")
|
|
2841
2949
|
return fs.existsSync(client.detectDir);
|
|
2842
|
-
return client.id === "claude-code" &&
|
|
2950
|
+
return client.id === "claude-code" && claudeCodeInstalled();
|
|
2843
2951
|
});
|
|
2844
2952
|
}
|
|
2845
2953
|
return requested ? knownClients().filter((client) => client.id === requested) : detectClients();
|
|
@@ -3640,6 +3748,60 @@ async function cmdStatus(flags = {}) {
|
|
|
3640
3748
|
}
|
|
3641
3749
|
}
|
|
3642
3750
|
}
|
|
3751
|
+
/**
|
|
3752
|
+
* Where this install lives and what it can actually see.
|
|
3753
|
+
*
|
|
3754
|
+
* `status` answers "who am I logged in as". This answers "why is EchoMem not finding my sessions",
|
|
3755
|
+
* which is the question that needs a command — a bare zero cannot distinguish "no store here" from
|
|
3756
|
+
* "store found, nothing in it". Local only, so it still works with no network and no account.
|
|
3757
|
+
*/
|
|
3758
|
+
export function doctorReportLines() {
|
|
3759
|
+
const lines = ["Environment:"];
|
|
3760
|
+
const entry = resolveDurableDistPath("index.js");
|
|
3761
|
+
const runtime = readHeadlessRuntimeInstallation();
|
|
3762
|
+
lines.push(` package ${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`);
|
|
3763
|
+
lines.push(` entry point ${entry.path}${entry.ephemeral ? " [temporary npx cache — npm can delete this]" : ""}`);
|
|
3764
|
+
lines.push(` durable runtime ${runtime ? `${runtime.version} at ${runtime.entry}` : "not installed"}`);
|
|
3765
|
+
const candidates = claudeCodeCommandCandidates();
|
|
3766
|
+
lines.push(` claude CLI ${claudeCodeCliAvailable() ? "runnable" : `not runnable (${candidates.length} candidate${candidates.length === 1 ? "" : "s"} tried)`}`);
|
|
3767
|
+
const supportRoots = resolveClaudeDesktopSupportRoots();
|
|
3768
|
+
const containerised = supportRoots.some((root) => /[\\/]Packages[\\/]Claude_/i.test(root));
|
|
3769
|
+
lines.push(` Claude Desktop ${supportRoots.length === 0
|
|
3770
|
+
? "not detected"
|
|
3771
|
+
: `${containerised ? "Store/MSIX container" : "unpackaged install"} (${supportRoots.length} support root${supportRoots.length === 1 ? "" : "s"})`}`);
|
|
3772
|
+
const claudeRoot = resolveClaudeProjectsDir();
|
|
3773
|
+
const codexRoot = resolveCodexSessionRoots()[0]?.path;
|
|
3774
|
+
const coworkRoots = resolveClaudeCoworkSessionRoots();
|
|
3775
|
+
// Dedupe as the import path does: inside an MSIX container the plain and LocalCache support roots
|
|
3776
|
+
// expose the same files, and realpath does not collapse that, so raw discovery counts them twice.
|
|
3777
|
+
const sessions = dedupeSessionsByConversation(discoverSessions({ claudeRoot: claudeRoot ?? undefined, coworkRoots, codexRoot }));
|
|
3778
|
+
const row = (label, source, root) => {
|
|
3779
|
+
const found = sessions.filter((session) => session.source === source).length;
|
|
3780
|
+
if (found > 0)
|
|
3781
|
+
return ` ${label.padEnd(15)} ${found} ${root ?? ""}`.trimEnd();
|
|
3782
|
+
// A zero must say which zero it is.
|
|
3783
|
+
return ` ${label.padEnd(15)} 0 ${root ? `store found at ${root}, nothing importable in it` : "no store on this machine"}`;
|
|
3784
|
+
};
|
|
3785
|
+
lines.push("", "Local sessions:");
|
|
3786
|
+
lines.push(row("Claude Code", "claude-code", claudeRoot ?? undefined));
|
|
3787
|
+
lines.push(row("Codex", "codex", codexRoot));
|
|
3788
|
+
lines.push(row("Cowork", "claude-desktop", coworkRoots[0]));
|
|
3789
|
+
return lines;
|
|
3790
|
+
}
|
|
3791
|
+
function cmdDoctor() {
|
|
3792
|
+
console.log(`EchoMem doctor — ${MCP_PACKAGE_LABEL}\n`);
|
|
3793
|
+
for (const line of doctorReportLines())
|
|
3794
|
+
console.log(line);
|
|
3795
|
+
const reports = inspectClientConfigs(MCP_PACKAGE_VERSION);
|
|
3796
|
+
if (reports.length) {
|
|
3797
|
+
console.log("\nClient MCP configs:");
|
|
3798
|
+
for (const report of reports) {
|
|
3799
|
+
for (const line of formatClientConfigReport(report))
|
|
3800
|
+
console.log(line);
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
console.log("\nAccount, credentials and update checks: `status`.");
|
|
3804
|
+
}
|
|
3643
3805
|
function cmdLogout() {
|
|
3644
3806
|
const store = new KeyStore();
|
|
3645
3807
|
try {
|
|
@@ -3688,9 +3850,18 @@ Manual / headless:
|
|
|
3688
3850
|
Current bridge version: ${MCP_PACKAGE_VERSION}
|
|
3689
3851
|
`;
|
|
3690
3852
|
/** Returns true if argv was a recognized subcommand (and was handled). */
|
|
3853
|
+
/** Commands that persist paths into other programs' configs, and so must run from a durable copy. */
|
|
3854
|
+
const CONFIG_WRITING_COMMANDS = new Set(["init", "setup", "update"]);
|
|
3691
3855
|
export async function runCli(argv) {
|
|
3692
3856
|
const cmd = argv[0];
|
|
3693
3857
|
const flags = parseFlags(argv.slice(1));
|
|
3858
|
+
if (cmd && CONFIG_WRITING_COMMANDS.has(cmd)) {
|
|
3859
|
+
const status = reexecFromDurableRuntime(argv);
|
|
3860
|
+
if (status !== null) {
|
|
3861
|
+
process.exitCode = status;
|
|
3862
|
+
return true;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3694
3865
|
switch (cmd) {
|
|
3695
3866
|
case "init":
|
|
3696
3867
|
await cmdInit(flags);
|
|
@@ -3714,7 +3885,7 @@ export async function runCli(argv) {
|
|
|
3714
3885
|
await cmdStatus(flags);
|
|
3715
3886
|
return true;
|
|
3716
3887
|
case "doctor":
|
|
3717
|
-
|
|
3888
|
+
cmdDoctor();
|
|
3718
3889
|
return true;
|
|
3719
3890
|
case "logout":
|
|
3720
3891
|
cmdLogout();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.50",
|
|
4
4
|
"description": "EchoMem MCP bridge for cross-agent memory, local history import, and recall",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test:ui": "npm run build && node test/setup-ui.test.mjs",
|
|
32
32
|
"test:onboarding-resilience": "npm run build && node test/onboarding-resilience.test.mjs",
|
|
33
33
|
"test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
|
|
34
|
-
"test": "npm run build && node test/source-session.test.mjs && node test/source-session-hook.test.mjs && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/workspace-selector.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs",
|
|
34
|
+
"test": "npm run build && node test/source-session.test.mjs && node test/source-session-hook.test.mjs && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/workspace-selector.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs && node test/durable-entry.test.mjs && node test/durable-reexec.test.mjs && node test/doctor.test.mjs && node test/environment-matrix.test.mjs",
|
|
35
35
|
"prepack": "npm run build"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|