@echomem/mcp 1.4.47 → 1.4.49
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 +22 -26
- package/dist/context-analysis/claude-native-canonical.js +2 -2
- package/dist/context-analysis/vendored-canonical.js +2 -2
- package/dist/context-analysis/workspace-report.js +3 -3
- package/dist/index.js +5 -8
- package/dist/local-data-paths.js +68 -29
- package/dist/local-jsonl.js +87 -0
- package/dist/migrate.js +8 -9
- package/dist/onboarding-stats.js +16 -0
- package/dist/save-checkpoint-hook.js +1 -1
- package/dist/setup-page/client-core.js +18 -372
- package/dist/setup-page/client-extraction.js +14 -129
- package/dist/setup-page/client-lifecycle.js +28 -101
- package/dist/setup-page/client.js +0 -2
- package/dist/setup-page/styles-extraction.js +1 -31
- package/dist/setup-page/styles-foundation.js +0 -89
- package/dist/setup-page/styles-mvp.js +10 -155
- package/dist/setup-page/styles-website-alignment.js +0 -204
- package/dist/setup-page/styles.js +0 -4
- package/dist/setup-page.js +4 -4
- package/dist/setup-preview.js +4 -212
- package/dist/setup.js +119 -583
- package/dist/v1-contract.js +0 -8
- package/package.json +5 -7
- package/dist/city/README.md +0 -9
- package/dist/city/echo-ai-city-only.html +0 -2232
- package/dist/city/echo-extraction-plate.html +0 -330
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/city/vendor/OrbitControls.js +0 -1417
- package/dist/city/vendor/RoundedBoxGeometry.js +0 -155
- package/dist/city/vendor/echo_general-file-21.riv +0 -0
- package/dist/city/vendor/rive.js +0 -8139
- package/dist/city/vendor/rive.wasm +0 -0
- package/dist/city/vendor/three.module.min.js +0 -6
- package/dist/forensics.js +0 -1531
- package/dist/report.js +0 -721
- package/dist/setup-page/client-report-audit.js +0 -819
- package/dist/setup-page/client-report-city.js +0 -356
- package/dist/setup-page/client-report.js +0 -6
- package/dist/setup-page/styles-city-report.js +0 -880
- package/dist/setup-page/styles-context-audit.js +0 -470
package/dist/setup.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design goals from the spec:
|
|
5
5
|
* - `login` establishes the account and trusted device only; it never reads local history.
|
|
6
|
-
* - `init` runs one ordered flow: local-history permission
|
|
6
|
+
* - `init` runs one ordered flow: local-history permission, login, plan if needed, then extraction.
|
|
7
7
|
* - Both secrets (API token + encryption key) ride a single browser flow and land in the local
|
|
8
8
|
* keystore — never in the client's MCP config, never in the agent's chat context.
|
|
9
9
|
* - Re-unlock after the key's TTL is one step, not a re-setup.
|
|
@@ -25,12 +25,12 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
25
25
|
import axios from "axios";
|
|
26
26
|
import { KeyStore } from "./keystore.js";
|
|
27
27
|
import { fetchEncryptionConfig, deriveAndVerifyKey, setupNewEncryptionKey, verifyKeyB64 } from "./encryption.js";
|
|
28
|
-
import {
|
|
28
|
+
import { buildOnboardingStatsPayload } from "./onboarding-stats.js";
|
|
29
29
|
import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, discoverMigratableSessions, discoverPendingSessionsTargeted, estimateMigrationEta, fetchProcessedImportKeys, isImportStatusUnsupported, markAccountImportStatusFailed, markAccountImportStatusUnavailable, markFastAccountImportStatusUnavailable, startMigration, summarizeFastMigratableDiscovery, 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 {
|
|
33
|
+
import { resolveClaudeDesktopSupportRoots } from "./local-data-paths.js";
|
|
34
34
|
import { installSaveCheckpointHooks, installSourceSessionHooks } from "./hud/hooks.js";
|
|
35
35
|
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
36
36
|
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
@@ -70,15 +70,25 @@ const CODEX_SKILL_NAMES = [
|
|
|
70
70
|
function home(...p) {
|
|
71
71
|
return path.join(os.homedir(), ...p);
|
|
72
72
|
}
|
|
73
|
-
function
|
|
74
|
-
const configured = process.env
|
|
73
|
+
function configuredProfileDirectory(envKey, fallbackName) {
|
|
74
|
+
const configured = process.env[envKey]?.trim();
|
|
75
75
|
if (!configured)
|
|
76
|
-
return home(
|
|
76
|
+
return home(fallbackName);
|
|
77
77
|
if (configured === "~")
|
|
78
78
|
return os.homedir();
|
|
79
|
-
if (configured.startsWith(
|
|
79
|
+
if (configured.startsWith("~/") || configured.startsWith("~\\")) {
|
|
80
80
|
return path.join(os.homedir(), configured.slice(2));
|
|
81
|
-
|
|
81
|
+
}
|
|
82
|
+
if (!path.isAbsolute(configured)) {
|
|
83
|
+
throw new Error(`${envKey} must be an absolute path or start with ~/`);
|
|
84
|
+
}
|
|
85
|
+
return path.normalize(configured);
|
|
86
|
+
}
|
|
87
|
+
function codexHome() {
|
|
88
|
+
return configuredProfileDirectory("CODEX_HOME", ".codex");
|
|
89
|
+
}
|
|
90
|
+
function claudeConfigHome() {
|
|
91
|
+
return configuredProfileDirectory("CLAUDE_CONFIG_DIR", ".claude");
|
|
82
92
|
}
|
|
83
93
|
function filesEqual(left, right) {
|
|
84
94
|
try {
|
|
@@ -211,10 +221,18 @@ export function knownClients() {
|
|
|
211
221
|
const appSupport = process.platform === "darwin"
|
|
212
222
|
? home("Library", "Application Support")
|
|
213
223
|
: process.env.APPDATA || home(".config");
|
|
224
|
+
const defaultClaudeDesktopRoot = path.join(appSupport, "Claude");
|
|
225
|
+
const claudeDesktopRoots = resolveClaudeDesktopSupportRoots();
|
|
226
|
+
const claudeDesktopClients = (claudeDesktopRoots.length > 0 ? claudeDesktopRoots : [defaultClaudeDesktopRoot]).map((supportRoot) => ({
|
|
227
|
+
id: "claude-desktop",
|
|
228
|
+
label: "Claude Desktop",
|
|
229
|
+
kind: "json",
|
|
230
|
+
configPath: path.join(supportRoot, "claude_desktop_config.json"),
|
|
231
|
+
}));
|
|
214
232
|
return [
|
|
215
233
|
{ id: "cursor", label: "Cursor", kind: "json", configPath: home(".cursor", "mcp.json") },
|
|
216
234
|
{ id: "windsurf", label: "Windsurf", kind: "json", configPath: home(".codeium", "windsurf", "mcp_config.json") },
|
|
217
|
-
|
|
235
|
+
...claudeDesktopClients,
|
|
218
236
|
{ id: "claude-code", label: "Claude Code", kind: "snippet", note: "run: claude mcp add-json -s user echomem '<entry>' (or add to .mcp.json)" },
|
|
219
237
|
{ id: "codex", label: "Codex", kind: "command", detectDir: codexHome(), configPath: path.join(codexHome(), "config.toml"), note: "add to ~/.codex/config.toml under [mcp_servers.echomem]" },
|
|
220
238
|
];
|
|
@@ -227,7 +245,7 @@ export function detectClients() {
|
|
|
227
245
|
if (c.kind === "command")
|
|
228
246
|
return fs.existsSync(c.detectDir);
|
|
229
247
|
if (c.id === "claude-code")
|
|
230
|
-
return
|
|
248
|
+
return claudeCodeCliAvailable();
|
|
231
249
|
return false;
|
|
232
250
|
});
|
|
233
251
|
}
|
|
@@ -401,7 +419,7 @@ function echomemGuidanceBlock() {
|
|
|
401
419
|
"- EchoMem's local SessionStart hook automatically binds each new Codex or Claude Code conversation to its real source-session ID. Use `bind_source_session` with a fresh UUID only as a compatibility fallback when the hook is unavailable or reports that the session is unbound.",
|
|
402
420
|
"- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
|
|
403
421
|
'- If the final user-facing answer materially relies on one or more EchoMem memories, end it with a compact `EchoMem sources:` list containing only the memories actually used. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. Do not cite memories that were merely retrieved, and omit the section when no memory informed the answer.',
|
|
404
|
-
"- Before the final response for a task that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone: call `save_conversation`. This private checkpoint does not require a separate user request. Do not save secrets, credentials, incidental chatter, or trivial read-only work. If a qualifying save fails because the encrypted vault is locked,
|
|
422
|
+
"- Before the final response for a task that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone: call `save_conversation`. This private checkpoint does not require a separate user request. Do not save secrets, credentials, incidental chatter, or trivial read-only work. If a qualifying save fails because the encrypted vault is locked, relay the bridge's unlock instruction; for this standalone runtime, use `echomem-mcp unlock`. Never silently skip it.",
|
|
405
423
|
'- After `save_conversation` succeeds and returns one or more saved memory IDs, make the save visible in the final user-facing answer with a compact `EchoMem saved:` list containing every memory created by that call. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. This save receipt is separate from `EchoMem sources:` and does not imply the newly saved memories informed the answer.',
|
|
406
424
|
"- For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per `groupId`. Relay the tool's exact text question and call `set_group_session_sharing` only after an explicit Yes/No. Silence leaves that group's state unset; never infer an answer. Saves sync eligible memories to every approved group; a No keeps them private for that group.",
|
|
407
425
|
"- If the user pastes a session carryover/checkpoint: it may reference `get_checkpoint_by_context` — use it to pull the checkpoint/decision trail when you need more than the snapshot.",
|
|
@@ -412,7 +430,7 @@ function echomemGuidanceBlock() {
|
|
|
412
430
|
"- Group sharing is scoped to an opaque id carried only in the current conversation, not to the MCP transport session. Membership is rechecked for each sync. Flagged memories are withheld from automatic conversation sync and remain private.",
|
|
413
431
|
"- If a user asks to create a group, call `create_memory_group`; if they ask for a code to share, call `create_group_invite` and return the secret invite code only to that user. Never save the invite code to memory or include it in logs, analytics, summaries, or unrelated output.",
|
|
414
432
|
"- If a user supplies an `echo_grp_...` code and explicitly asks to join, call `join_memory_group`. Joining never authorizes publishing by itself and must not move a user out of another group. After joining, continue into the profile-and-publication preview instead of leaving title or responsibility blank.",
|
|
415
|
-
"- For requests such as “prepare my recent work memories,” “upload work from this ticket,” or “publish work since my last sync,” call `prepare_group_publication` first. This is a no-publication preview. For encrypted accounts,
|
|
433
|
+
"- For requests such as “prepare my recent work memories,” “upload work from this ticket,” or “publish work since my last sync,” call `prepare_group_publication` first. This is a no-publication preview. For encrypted accounts, relay the bridge's local unlock instruction if the tool reports that the key is required.",
|
|
416
434
|
"- After preparing, select only exact candidate memory IDs that match the user's stated scope and exclude already-published or exact-content duplicates. Use the candidate evidence to draft a concise title and responsibility summary for the current member, but label both as proposals rather than facts.",
|
|
417
435
|
"- Use one canonical evidence link for every memory: preserve the Memory ID and link to `https://echoknows.com/memory/<memory-id>`. The site resolves the authorized representation: an owner is sent to their private timeline, while current group/friend access opens an authorized publication snapshot or public memory. The visible Markdown label should use the memory key, not the raw URL or UUID.",
|
|
418
436
|
"- If the user asks to flag memories about a sensitive topic, search their own memories first, show the exact matches with owner-only personal links, and ask them to confirm. Only then call `flag_memories_for_publication_attention`; flagging does not publish, decrypt, change visibility, or retract an existing group snapshot.",
|
|
@@ -444,6 +462,35 @@ export function writeAgentsMemoryGuidance(filePath) {
|
|
|
444
462
|
fs.appendFileSync(filePath, sep + block + "\n");
|
|
445
463
|
return "wrote";
|
|
446
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Refresh marker-owned guidance for users upgrading an existing standalone MCP install.
|
|
467
|
+
* This intentionally does not create global memory files: setup owns first installation,
|
|
468
|
+
* while server startup only migrates guidance that EchoMem already installed.
|
|
469
|
+
*/
|
|
470
|
+
export function refreshInstalledMemoryGuidance() {
|
|
471
|
+
let candidates;
|
|
472
|
+
try {
|
|
473
|
+
candidates = [
|
|
474
|
+
path.join(codexHome(), "AGENTS.md"),
|
|
475
|
+
path.join(claudeConfigHome(), "CLAUDE.md"),
|
|
476
|
+
];
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
for (const filePath of candidates) {
|
|
482
|
+
try {
|
|
483
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
484
|
+
const start = content.indexOf(AGENTS_MD_BEGIN);
|
|
485
|
+
const end = content.indexOf(AGENTS_MD_END);
|
|
486
|
+
if (start >= 0 && end > start)
|
|
487
|
+
writeAgentsMemoryGuidance(filePath);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// Missing, unreadable, or externally managed files are left untouched.
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
447
494
|
/** Merge the EchoMem entry into a JSON client's `mcpServers` map without clobbering siblings. */
|
|
448
495
|
export function writeJsonClientConfig(configPath, entry, options = {}) {
|
|
449
496
|
let config = {};
|
|
@@ -617,6 +664,19 @@ function execClaudeCodeSync(args, options, commandCandidates) {
|
|
|
617
664
|
}
|
|
618
665
|
throw lastError;
|
|
619
666
|
}
|
|
667
|
+
export function claudeCodeCliAvailable() {
|
|
668
|
+
try {
|
|
669
|
+
execClaudeCodeSync(["--version"], {
|
|
670
|
+
encoding: "utf8",
|
|
671
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
672
|
+
timeout: 3000,
|
|
673
|
+
});
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
620
680
|
export function writeClaudeCodeConfig(entry, options = {}) {
|
|
621
681
|
// EchoMem belongs at user scope so every Claude Code project resolves the same durable runtime.
|
|
622
682
|
// Older CLI versions wrote local/project entries, which take precedence over user scope and can
|
|
@@ -1229,7 +1289,7 @@ function withTimeout(promise, ms, code, onTimeout) {
|
|
|
1229
1289
|
function delay(ms) {
|
|
1230
1290
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1231
1291
|
}
|
|
1232
|
-
const
|
|
1292
|
+
const LOCAL_ASSET_TYPES = {
|
|
1233
1293
|
".html": "text/html; charset=utf-8",
|
|
1234
1294
|
".js": "text/javascript; charset=utf-8",
|
|
1235
1295
|
".json": "application/json; charset=utf-8",
|
|
@@ -1238,39 +1298,11 @@ const CITY_ASSET_TYPES = {
|
|
|
1238
1298
|
".png": "image/png",
|
|
1239
1299
|
".svg": "image/svg+xml",
|
|
1240
1300
|
};
|
|
1241
|
-
function
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
// Published install: fall back to the copy bundled into dist/city by prepack (bundle-city.mjs).
|
|
1247
|
-
return fileURLToPath(new URL("./city/", import.meta.url));
|
|
1248
|
-
}
|
|
1249
|
-
function serveRepoCityAsset(reqPath, res) {
|
|
1250
|
-
const root = repoCityArtifactsRoot();
|
|
1251
|
-
const rel = reqPath === "/city" || reqPath === "/city/" ? "echo-ai-city-only.html" : decodeURIComponent(reqPath.slice("/city/".length));
|
|
1252
|
-
// Archives stay in the checkout for recovery, but must never become a localhost UI surface.
|
|
1253
|
-
const normalizedRel = rel.replace(/\\/g, "/");
|
|
1254
|
-
if (normalizedRel === "archive" || normalizedRel.startsWith("archive/")) {
|
|
1255
|
-
res.writeHead(404).end("not found");
|
|
1256
|
-
return true;
|
|
1257
|
-
}
|
|
1258
|
-
const filePath = path.resolve(root, rel);
|
|
1259
|
-
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
1260
|
-
if (!filePath.startsWith(rootWithSep)) {
|
|
1261
|
-
res.writeHead(403).end("forbidden");
|
|
1262
|
-
return true;
|
|
1263
|
-
}
|
|
1264
|
-
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
1265
|
-
res.writeHead(404).end("not found");
|
|
1266
|
-
return true;
|
|
1267
|
-
}
|
|
1268
|
-
res.writeHead(200, {
|
|
1269
|
-
"Content-Type": CITY_ASSET_TYPES[path.extname(filePath)] || "application/octet-stream",
|
|
1270
|
-
"Cache-Control": "no-store",
|
|
1271
|
-
});
|
|
1272
|
-
fs.createReadStream(filePath).pipe(res);
|
|
1273
|
-
return true;
|
|
1301
|
+
function repoLabel(cwd) {
|
|
1302
|
+
if (!cwd)
|
|
1303
|
+
return "";
|
|
1304
|
+
const normalized = cwd.replace(/[\\/]+$/, "");
|
|
1305
|
+
return path.basename(normalized) || normalized;
|
|
1274
1306
|
}
|
|
1275
1307
|
function hudAssetsRoot() {
|
|
1276
1308
|
return fileURLToPath(new URL("../assets/hud/", import.meta.url));
|
|
@@ -1289,7 +1321,7 @@ function serveHudAsset(reqPath, res) {
|
|
|
1289
1321
|
return true;
|
|
1290
1322
|
}
|
|
1291
1323
|
res.writeHead(200, {
|
|
1292
|
-
"Content-Type":
|
|
1324
|
+
"Content-Type": LOCAL_ASSET_TYPES[path.extname(filePath)] || "application/octet-stream",
|
|
1293
1325
|
"Cache-Control": "no-store",
|
|
1294
1326
|
});
|
|
1295
1327
|
fs.createReadStream(filePath).pipe(res);
|
|
@@ -1466,61 +1498,6 @@ export function discoverMigratableFastOffThread(opts = {}) {
|
|
|
1466
1498
|
});
|
|
1467
1499
|
});
|
|
1468
1500
|
}
|
|
1469
|
-
/** Build the full local-history dashboard payload away from the callback server's event loop.
|
|
1470
|
-
* `collect()` can synchronously parse hundreds of JSONL files for tens of seconds; doing that on
|
|
1471
|
-
* the bridge thread prevents even localhost actions such as account switch from receiving a reply. */
|
|
1472
|
-
export function buildCollectedStatsPayloadOffThread(inject) {
|
|
1473
|
-
const reportUrl = runtimeModuleUrl("report");
|
|
1474
|
-
const serializedInject = JSON.stringify(inject);
|
|
1475
|
-
const code = `
|
|
1476
|
-
import { parentPort } from "node:worker_threads";
|
|
1477
|
-
import { collect, buildStatsPayload } from ${JSON.stringify(reportUrl)};
|
|
1478
|
-
|
|
1479
|
-
try {
|
|
1480
|
-
const payload = await buildStatsPayload(collect(), ${serializedInject});
|
|
1481
|
-
parentPort?.postMessage({ ok: true, payload });
|
|
1482
|
-
} catch (error) {
|
|
1483
|
-
parentPort?.postMessage({
|
|
1484
|
-
ok: false,
|
|
1485
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1486
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
`;
|
|
1490
|
-
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
1491
|
-
return new Promise((resolve, reject) => {
|
|
1492
|
-
let settled = false;
|
|
1493
|
-
const finish = (result) => {
|
|
1494
|
-
if (settled)
|
|
1495
|
-
return;
|
|
1496
|
-
settled = true;
|
|
1497
|
-
void worker.terminate();
|
|
1498
|
-
if (result.ok)
|
|
1499
|
-
resolve(result.payload);
|
|
1500
|
-
else
|
|
1501
|
-
reject(result.error);
|
|
1502
|
-
};
|
|
1503
|
-
worker.once("message", (message) => {
|
|
1504
|
-
const msg = message;
|
|
1505
|
-
if (msg.ok === true) {
|
|
1506
|
-
finish({ ok: true, payload: msg.payload });
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
const error = new Error(typeof msg.message === "string" ? msg.message : "Full local stats worker failed");
|
|
1510
|
-
if (typeof msg.stack === "string")
|
|
1511
|
-
error.stack = msg.stack;
|
|
1512
|
-
finish({ ok: false, error });
|
|
1513
|
-
});
|
|
1514
|
-
worker.once("error", (error) => {
|
|
1515
|
-
finish({ ok: false, error });
|
|
1516
|
-
});
|
|
1517
|
-
worker.once("exit", (code) => {
|
|
1518
|
-
if (settled)
|
|
1519
|
-
return;
|
|
1520
|
-
finish({ ok: false, error: new Error(`Full local stats worker exited (code ${code}) without a result`) });
|
|
1521
|
-
});
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
1501
|
export function createLocalDiscoveryCache(loaders = {}) {
|
|
1525
1502
|
const loadQuick = loaders.loadQuick ?? (() => discoverMigratableFastOffThread());
|
|
1526
1503
|
const loadExact = loaders.loadExact ?? (() => discoverMigratableSessionsOffThread());
|
|
@@ -1569,45 +1546,6 @@ export function createLocalDiscoveryCache(loaders = {}) {
|
|
|
1569
1546
|
},
|
|
1570
1547
|
};
|
|
1571
1548
|
}
|
|
1572
|
-
function forensicStageLabel(stage) {
|
|
1573
|
-
if (stage === "reading-transcripts")
|
|
1574
|
-
return "Reading transcript files";
|
|
1575
|
-
if (stage === "building-summary")
|
|
1576
|
-
return "Building scan summary";
|
|
1577
|
-
if (stage === "classifying-repeated-context")
|
|
1578
|
-
return "Classifying repeated context";
|
|
1579
|
-
if (stage === "finalizing-report")
|
|
1580
|
-
return "Finalizing report";
|
|
1581
|
-
return "Starting local scan";
|
|
1582
|
-
}
|
|
1583
|
-
/** Build the local forensic "Context Doctor" report on a worker thread so the multi-file scan never
|
|
1584
|
-
* blocks the bridge's event loop (the freeze postmortem: any unbounded sync work on this path is risky). */
|
|
1585
|
-
export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
1586
|
-
let lastProgress = null;
|
|
1587
|
-
const recordProgress = (progress) => {
|
|
1588
|
-
lastProgress = progress;
|
|
1589
|
-
onProgress?.(progress);
|
|
1590
|
-
};
|
|
1591
|
-
return runForensicReportWorker(recordProgress, options).catch(async (primaryError) => {
|
|
1592
|
-
if (options.failOpen === false)
|
|
1593
|
-
throw primaryError;
|
|
1594
|
-
const failureCode = errorCode(primaryError) || "REPORT_BUILD_FAILED";
|
|
1595
|
-
console.error(`[echomem] local scan degraded after ${failureCode}; continuing without local-history analysis`);
|
|
1596
|
-
onProgress?.({
|
|
1597
|
-
done: lastProgress?.done || 0,
|
|
1598
|
-
total: lastProgress?.total || 0,
|
|
1599
|
-
stage: "finalizing-report",
|
|
1600
|
-
detail: "finishing setup without optional local-history analysis",
|
|
1601
|
-
overall: 0.99,
|
|
1602
|
-
stageDone: 0,
|
|
1603
|
-
stageTotal: 0,
|
|
1604
|
-
});
|
|
1605
|
-
return runForensicReportWorker(undefined, {
|
|
1606
|
-
timeoutMs: 30_000,
|
|
1607
|
-
maxOldGenerationSizeMb: Math.max(64, options.maxOldGenerationSizeMb || 0),
|
|
1608
|
-
}, [], failureCode);
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1611
1549
|
function errorCode(error) {
|
|
1612
1550
|
return error && typeof error === "object" && "code" in error
|
|
1613
1551
|
? String(error.code || "")
|
|
@@ -1627,7 +1565,6 @@ export function completeOptionalStatsPayload(payload, reason, countsTrusted) {
|
|
|
1627
1565
|
transcriptsUploaded: false,
|
|
1628
1566
|
sessions: { total: 0, codex: 0, claudeCode: 0, cowork: 0 },
|
|
1629
1567
|
migratable: { pending: 0, alreadyMigrated: 0 },
|
|
1630
|
-
memoriesCaptured: null,
|
|
1631
1568
|
};
|
|
1632
1569
|
const completed = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
1633
1570
|
? { ...payload }
|
|
@@ -1640,155 +1577,11 @@ export function completeOptionalStatsPayload(payload, reason, countsTrusted) {
|
|
|
1640
1577
|
};
|
|
1641
1578
|
return completed;
|
|
1642
1579
|
}
|
|
1643
|
-
function runForensicReportWorker(onProgress, options, sources, degradedReason) {
|
|
1644
|
-
const forensicsUrl = runtimeModuleUrl("forensics");
|
|
1645
|
-
const serializedSources = sources === undefined ? "undefined" : JSON.stringify(sources);
|
|
1646
|
-
const serializedDegradedReason = JSON.stringify(degradedReason || "");
|
|
1647
|
-
const code = `
|
|
1648
|
-
import { parentPort } from "node:worker_threads";
|
|
1649
|
-
import { buildForensicReport, validateForensicReportForSetup } from ${JSON.stringify(forensicsUrl)};
|
|
1650
|
-
try {
|
|
1651
|
-
const report = await buildForensicReport({
|
|
1652
|
-
sources: ${serializedSources},
|
|
1653
|
-
includeLegacyGoldenStandard: false,
|
|
1654
|
-
onProgress: (done, total, stage, detail, overall, stageDone, stageTotal) => parentPort?.postMessage({
|
|
1655
|
-
progress: { done, total, stage, detail, overall, stageDone, stageTotal },
|
|
1656
|
-
}),
|
|
1657
|
-
});
|
|
1658
|
-
const degradedReason = ${serializedDegradedReason};
|
|
1659
|
-
if (degradedReason) {
|
|
1660
|
-
report.scanDiagnostics = {
|
|
1661
|
-
degraded: true,
|
|
1662
|
-
reason: degradedReason,
|
|
1663
|
-
skippedSources: ["codex", "claude"],
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
const validation = validateForensicReportForSetup(report);
|
|
1667
|
-
if (!validation.ok) {
|
|
1668
|
-
const error = new Error(validation.message);
|
|
1669
|
-
error.code = validation.code;
|
|
1670
|
-
throw error;
|
|
1671
|
-
}
|
|
1672
|
-
parentPort?.postMessage({ ok: true, report });
|
|
1673
|
-
} catch (error) {
|
|
1674
|
-
parentPort?.postMessage({
|
|
1675
|
-
ok: false,
|
|
1676
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1677
|
-
code: error && typeof error === "object" && "code" in error ? String(error.code || "") : "",
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
`;
|
|
1681
|
-
const requestedHeapMb = options.maxOldGenerationSizeMb;
|
|
1682
|
-
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`), Number.isFinite(requestedHeapMb)
|
|
1683
|
-
? { resourceLimits: { maxOldGenerationSizeMb: Math.max(16, Math.floor(requestedHeapMb)) } }
|
|
1684
|
-
: undefined);
|
|
1685
|
-
return new Promise((resolve, reject) => {
|
|
1686
|
-
let settled = false;
|
|
1687
|
-
const requestedTimeoutMs = options.timeoutMs ?? 15 * 60_000;
|
|
1688
|
-
const timeoutMs = Number.isFinite(requestedTimeoutMs) ? Math.max(1, requestedTimeoutMs) : 15 * 60_000;
|
|
1689
|
-
const timeout = setTimeout(() => {
|
|
1690
|
-
if (settled)
|
|
1691
|
-
return;
|
|
1692
|
-
settled = true;
|
|
1693
|
-
void worker.terminate();
|
|
1694
|
-
const error = new Error(`Local forensic report timed out after ${timeoutMs}ms`);
|
|
1695
|
-
error.code = "REPORT_SCAN_TIMEOUT";
|
|
1696
|
-
reject(error);
|
|
1697
|
-
}, timeoutMs);
|
|
1698
|
-
timeout.unref?.();
|
|
1699
|
-
const finish = (result) => {
|
|
1700
|
-
if (settled)
|
|
1701
|
-
return;
|
|
1702
|
-
settled = true;
|
|
1703
|
-
clearTimeout(timeout);
|
|
1704
|
-
void worker.terminate();
|
|
1705
|
-
if (result.ok)
|
|
1706
|
-
resolve(result.report);
|
|
1707
|
-
else
|
|
1708
|
-
reject(result.error);
|
|
1709
|
-
};
|
|
1710
|
-
worker.on("message", (message) => {
|
|
1711
|
-
if (settled)
|
|
1712
|
-
return;
|
|
1713
|
-
const msg = message;
|
|
1714
|
-
if (msg.progress) {
|
|
1715
|
-
onProgress?.(msg.progress);
|
|
1716
|
-
return;
|
|
1717
|
-
}
|
|
1718
|
-
if (msg.ok === true && msg.report && typeof msg.report === "object") {
|
|
1719
|
-
finish({ ok: true, report: msg.report });
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
const error = new Error(typeof msg.message === "string" ? msg.message : "Local forensic report failed");
|
|
1723
|
-
if (typeof msg.code === "string" && msg.code)
|
|
1724
|
-
error.code = msg.code;
|
|
1725
|
-
finish({ ok: false, error });
|
|
1726
|
-
});
|
|
1727
|
-
worker.once("error", (error) => {
|
|
1728
|
-
finish({ ok: false, error });
|
|
1729
|
-
});
|
|
1730
|
-
worker.once("exit", (code) => {
|
|
1731
|
-
if (settled)
|
|
1732
|
-
return;
|
|
1733
|
-
finish({ ok: false, error: new Error(`Forensic report worker exited (code ${code}) without a result`) });
|
|
1734
|
-
});
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
1580
|
export function respondMigrate(res, body, status = 200) {
|
|
1738
1581
|
if (res.writableEnded)
|
|
1739
1582
|
return;
|
|
1740
1583
|
res.writeHead(status, { "Content-Type": "application/json" }).end(JSON.stringify(body));
|
|
1741
1584
|
}
|
|
1742
|
-
function safeForensicError(error) {
|
|
1743
|
-
const code = error && typeof error === "object" && "code" in error
|
|
1744
|
-
? String(error.code || "")
|
|
1745
|
-
: "";
|
|
1746
|
-
if (code === "REPORT_SCAN_TIMEOUT") {
|
|
1747
|
-
return {
|
|
1748
|
-
code,
|
|
1749
|
-
message: "The local workspace scan took too long and was stopped. No backup data was substituted. Rerun setup to retry.",
|
|
1750
|
-
};
|
|
1751
|
-
}
|
|
1752
|
-
return {
|
|
1753
|
-
code: "REPORT_BUILD_FAILED",
|
|
1754
|
-
message: "EchoMem could not finish the local workspace scan. No backup data was substituted. Rerun setup to retry.",
|
|
1755
|
-
};
|
|
1756
|
-
}
|
|
1757
|
-
function publicRunningForensicProgress(value) {
|
|
1758
|
-
if (!value || typeof value !== "object")
|
|
1759
|
-
return null;
|
|
1760
|
-
const progress = value;
|
|
1761
|
-
if (progress.status !== "running")
|
|
1762
|
-
return null;
|
|
1763
|
-
const safeCount = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
|
|
1764
|
-
? Math.floor(candidate)
|
|
1765
|
-
: 0);
|
|
1766
|
-
const safeDuration = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
|
|
1767
|
-
? candidate
|
|
1768
|
-
: 0);
|
|
1769
|
-
const safeFraction = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate)
|
|
1770
|
-
? Math.min(1, Math.max(0, candidate))
|
|
1771
|
-
: 0);
|
|
1772
|
-
const total = safeCount(progress.total);
|
|
1773
|
-
const stageTotal = safeCount(progress.stageTotal);
|
|
1774
|
-
const rawStage = typeof progress.stage === "string" ? progress.stage : "starting";
|
|
1775
|
-
const stage = ["starting", "reading-transcripts", "building-summary", "classifying-repeated-context", "finalizing-report"].includes(rawStage)
|
|
1776
|
-
? rawStage
|
|
1777
|
-
: "starting";
|
|
1778
|
-
return {
|
|
1779
|
-
status: "running",
|
|
1780
|
-
scanned: total > 0 ? Math.min(safeCount(progress.scanned), total) : 0,
|
|
1781
|
-
total,
|
|
1782
|
-
stage,
|
|
1783
|
-
label: forensicStageLabel(stage),
|
|
1784
|
-
stageDone: stageTotal > 0 ? Math.min(safeCount(progress.stageDone), stageTotal) : 0,
|
|
1785
|
-
stageTotal,
|
|
1786
|
-
overall: safeFraction(progress.overall),
|
|
1787
|
-
elapsedMs: safeDuration(progress.elapsedMs),
|
|
1788
|
-
stageElapsedMs: safeDuration(progress.stageElapsedMs),
|
|
1789
|
-
updatedAt: safeDuration(progress.updatedAt) || Date.now(),
|
|
1790
|
-
};
|
|
1791
|
-
}
|
|
1792
1585
|
/**
|
|
1793
1586
|
* Start the persistent localhost bridge used by the setup page. It sends/verifies OTP through the
|
|
1794
1587
|
* hosted API, accepts the local passphrase, serves local Wrapped stats, and holds the /migrate
|
|
@@ -1801,12 +1594,11 @@ export function startCallbackServer(opts = {}) {
|
|
|
1801
1594
|
: `${Math.ceil(timeoutMs / 1000)} seconds`;
|
|
1802
1595
|
const dashboardTimeoutMs = 4 * 60 * 60 * 1000;
|
|
1803
1596
|
const expectedNonce = opts.nonce;
|
|
1804
|
-
const scanId = opts.scanId ?? randomUUID();
|
|
1805
1597
|
const flow = opts.flow ?? "onboarding";
|
|
1806
1598
|
const isLoginFlow = flow === "login";
|
|
1807
1599
|
// A login screen must not be blocked by a local-history permission. That permission belongs to
|
|
1808
1600
|
// onboarding and is intentionally enforced separately below.
|
|
1809
|
-
const
|
|
1601
|
+
const requiresLocalHistoryConsent = opts.requireLocalHistoryConsent === true && !isLoginFlow;
|
|
1810
1602
|
return new Promise((resolveOuter, rejectOuter) => {
|
|
1811
1603
|
const onToken = deferred();
|
|
1812
1604
|
const setupExit = deferred();
|
|
@@ -1819,7 +1611,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1819
1611
|
let activeDeviceToken = opts.initialToken?.token || "";
|
|
1820
1612
|
let activeAccountEmail = "";
|
|
1821
1613
|
let pendingLocalAuth = null;
|
|
1822
|
-
let
|
|
1614
|
+
let localHistoryConsentGranted = !requiresLocalHistoryConsent;
|
|
1823
1615
|
let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
1824
1616
|
let migrateStarted = false;
|
|
1825
1617
|
let tokenRefreshHandler = null;
|
|
@@ -1886,7 +1678,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1886
1678
|
const handleCallback = (res, token, key, nonce) => {
|
|
1887
1679
|
if (!checkNonce(nonce))
|
|
1888
1680
|
return void text(res, 403, "bad nonce");
|
|
1889
|
-
if (
|
|
1681
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
1890
1682
|
return void json(res, 403, {
|
|
1891
1683
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1892
1684
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1916,7 +1708,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1916
1708
|
text(res, 403, "bad nonce");
|
|
1917
1709
|
return true;
|
|
1918
1710
|
}
|
|
1919
|
-
if (
|
|
1711
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
1920
1712
|
json(res, 403, {
|
|
1921
1713
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1922
1714
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1943,8 +1735,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1943
1735
|
armTimeout();
|
|
1944
1736
|
};
|
|
1945
1737
|
const isOnboardingOnlyRoute = (route) => [
|
|
1946
|
-
"/
|
|
1947
|
-
"/report",
|
|
1738
|
+
"/local-history-consent",
|
|
1948
1739
|
"/stats",
|
|
1949
1740
|
"/billing-status",
|
|
1950
1741
|
"/billing-checkout",
|
|
@@ -2137,10 +1928,6 @@ export function startCallbackServer(opts = {}) {
|
|
|
2137
1928
|
message: "Run `echomem-mcp init` to access local-history onboarding.",
|
|
2138
1929
|
});
|
|
2139
1930
|
}
|
|
2140
|
-
if ((route === "/city" || route.startsWith("/city/")) && req.method === "GET") {
|
|
2141
|
-
serveRepoCityAsset(route, res);
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
1931
|
if (route.startsWith("/hud-assets/") && req.method === "GET") {
|
|
2145
1932
|
serveHudAsset(route, res);
|
|
2146
1933
|
return;
|
|
@@ -2178,8 +1965,13 @@ export function startCallbackServer(opts = {}) {
|
|
|
2178
1965
|
localOnly: true,
|
|
2179
1966
|
localAuth: true,
|
|
2180
1967
|
workspacePath: process.cwd(),
|
|
2181
|
-
consentRequired:
|
|
2182
|
-
consentGranted:
|
|
1968
|
+
consentRequired: requiresLocalHistoryConsent,
|
|
1969
|
+
consentGranted: localHistoryConsentGranted,
|
|
1970
|
+
platform: process.platform,
|
|
1971
|
+
capabilities: {
|
|
1972
|
+
openClaudeDesktop: process.platform === "darwin" || process.platform === "win32",
|
|
1973
|
+
openAgentSessions: process.platform === "darwin" || process.platform === "win32",
|
|
1974
|
+
},
|
|
2183
1975
|
});
|
|
2184
1976
|
return;
|
|
2185
1977
|
}
|
|
@@ -2283,7 +2075,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2283
2075
|
if (route === "/stats" && req.method === "GET") {
|
|
2284
2076
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2285
2077
|
return void text(res, 403, "bad nonce");
|
|
2286
|
-
if (
|
|
2078
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2287
2079
|
return void json(res, 403, {
|
|
2288
2080
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2289
2081
|
message: "Allow local history access before continuing setup.",
|
|
@@ -2299,7 +2091,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2299
2091
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
2300
2092
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2301
2093
|
return void text(res, 403, "bad nonce");
|
|
2302
|
-
if (
|
|
2094
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2303
2095
|
return void json(res, 403, {
|
|
2304
2096
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2305
2097
|
message: "Allow local history access before continuing setup.",
|
|
@@ -2423,7 +2215,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2423
2215
|
}
|
|
2424
2216
|
if (!checkNonce(asString(body.nonce)))
|
|
2425
2217
|
return void text(res, 403, "bad nonce");
|
|
2426
|
-
if (
|
|
2218
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2427
2219
|
return void json(res, 403, {
|
|
2428
2220
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2429
2221
|
message: "Allow local history access before managing an onboarding plan.",
|
|
@@ -2489,114 +2281,10 @@ export function startCallbackServer(opts = {}) {
|
|
|
2489
2281
|
}
|
|
2490
2282
|
return;
|
|
2491
2283
|
}
|
|
2492
|
-
if (route === "/report" && req.method === "GET") {
|
|
2493
|
-
// Local forensic "Context Doctor" report — computed locally, served BEFORE auth (scan-first).
|
|
2494
|
-
res.setHeader("Cache-Control", "no-store");
|
|
2495
|
-
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2496
|
-
return void text(res, 403, "bad nonce");
|
|
2497
|
-
if (requiresReportConsent && !reportConsentGranted) {
|
|
2498
|
-
return void json(res, 403, {
|
|
2499
|
-
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2500
|
-
message: "Allow local history access before starting the local scan.",
|
|
2501
|
-
});
|
|
2502
|
-
}
|
|
2503
|
-
let payload;
|
|
2504
|
-
try {
|
|
2505
|
-
payload = opts.getReport ? opts.getReport() : null;
|
|
2506
|
-
}
|
|
2507
|
-
catch {
|
|
2508
|
-
return void json(res, 500, {
|
|
2509
|
-
schemaVersion: 1,
|
|
2510
|
-
kind: "failed",
|
|
2511
|
-
mode: "production",
|
|
2512
|
-
scanId,
|
|
2513
|
-
error: {
|
|
2514
|
-
code: "REPORT_STATE_UNAVAILABLE",
|
|
2515
|
-
message: "EchoMem could not read the local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
2516
|
-
},
|
|
2517
|
-
});
|
|
2518
|
-
}
|
|
2519
|
-
if (payload == null) {
|
|
2520
|
-
// 202 carries scan progress so the page can show a live "scanned N/total" indicator.
|
|
2521
|
-
let prog;
|
|
2522
|
-
try {
|
|
2523
|
-
prog = opts.getReportProgress ? opts.getReportProgress() : {
|
|
2524
|
-
status: "running",
|
|
2525
|
-
scanned: 0,
|
|
2526
|
-
total: 0,
|
|
2527
|
-
stage: "starting",
|
|
2528
|
-
label: "Starting local scan",
|
|
2529
|
-
elapsedMs: 0,
|
|
2530
|
-
stageElapsedMs: 0,
|
|
2531
|
-
updatedAt: Date.now(),
|
|
2532
|
-
};
|
|
2533
|
-
}
|
|
2534
|
-
catch {
|
|
2535
|
-
return void json(res, 500, {
|
|
2536
|
-
schemaVersion: 1,
|
|
2537
|
-
kind: "failed",
|
|
2538
|
-
mode: "production",
|
|
2539
|
-
scanId,
|
|
2540
|
-
error: {
|
|
2541
|
-
code: "REPORT_STATE_UNAVAILABLE",
|
|
2542
|
-
message: "EchoMem could not read the local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
2543
|
-
},
|
|
2544
|
-
});
|
|
2545
|
-
}
|
|
2546
|
-
if (prog && typeof prog === "object" && prog.status === "failed") {
|
|
2547
|
-
return void json(res, 500, {
|
|
2548
|
-
schemaVersion: 1,
|
|
2549
|
-
kind: "failed",
|
|
2550
|
-
mode: "production",
|
|
2551
|
-
scanId,
|
|
2552
|
-
error: safeForensicError(prog.error),
|
|
2553
|
-
});
|
|
2554
|
-
}
|
|
2555
|
-
const publicProgress = publicRunningForensicProgress(prog);
|
|
2556
|
-
if (!publicProgress) {
|
|
2557
|
-
return void json(res, 500, {
|
|
2558
|
-
schemaVersion: 1,
|
|
2559
|
-
kind: "failed",
|
|
2560
|
-
mode: "production",
|
|
2561
|
-
scanId,
|
|
2562
|
-
error: {
|
|
2563
|
-
code: "REPORT_STATE_INVALID",
|
|
2564
|
-
message: "EchoMem received an invalid local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
2565
|
-
},
|
|
2566
|
-
});
|
|
2567
|
-
}
|
|
2568
|
-
return void json(res, 202, {
|
|
2569
|
-
schemaVersion: 1,
|
|
2570
|
-
kind: "scanning",
|
|
2571
|
-
mode: "production",
|
|
2572
|
-
scanId,
|
|
2573
|
-
progress: publicProgress,
|
|
2574
|
-
});
|
|
2575
|
-
}
|
|
2576
|
-
const validation = validateForensicReportForSetup(payload);
|
|
2577
|
-
if (!validation.ok) {
|
|
2578
|
-
console.error(`[echomem] local report validation failed: ${validation.code} — ${validation.message}`);
|
|
2579
|
-
return void json(res, 500, {
|
|
2580
|
-
schemaVersion: 1,
|
|
2581
|
-
kind: "failed",
|
|
2582
|
-
mode: "production",
|
|
2583
|
-
scanId,
|
|
2584
|
-
error: { code: validation.code, message: validation.message },
|
|
2585
|
-
});
|
|
2586
|
-
}
|
|
2587
|
-
json(res, 200, {
|
|
2588
|
-
schemaVersion: 1,
|
|
2589
|
-
kind: validation.kind,
|
|
2590
|
-
mode: "production",
|
|
2591
|
-
scanId,
|
|
2592
|
-
report: validation.report,
|
|
2593
|
-
});
|
|
2594
|
-
return;
|
|
2595
|
-
}
|
|
2596
2284
|
if (route === "/progress" && req.method === "GET") {
|
|
2597
2285
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2598
2286
|
return void text(res, 403, "bad nonce");
|
|
2599
|
-
if (
|
|
2287
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2600
2288
|
return void json(res, 403, {
|
|
2601
2289
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2602
2290
|
message: "Allow local history access before continuing setup.",
|
|
@@ -2605,7 +2293,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2605
2293
|
json(res, 200, progress);
|
|
2606
2294
|
return;
|
|
2607
2295
|
}
|
|
2608
|
-
if (route === "/
|
|
2296
|
+
if (route === "/local-history-consent" && req.method === "POST") {
|
|
2609
2297
|
let body;
|
|
2610
2298
|
try {
|
|
2611
2299
|
body = await readJsonBody(req);
|
|
@@ -2617,8 +2305,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2617
2305
|
if (!checkNonce(asString(body.nonce)))
|
|
2618
2306
|
return void text(res, 403, "bad nonce");
|
|
2619
2307
|
const allowed = body.allowed === true;
|
|
2620
|
-
|
|
2621
|
-
opts.onReportConsent?.(allowed);
|
|
2308
|
+
localHistoryConsentGranted = allowed;
|
|
2622
2309
|
json(res, 200, { ok: true, allowed });
|
|
2623
2310
|
return;
|
|
2624
2311
|
}
|
|
@@ -2677,7 +2364,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2677
2364
|
}
|
|
2678
2365
|
if (!checkNonce(asString(body.nonce)))
|
|
2679
2366
|
return void text(res, 403, "bad nonce");
|
|
2680
|
-
if (
|
|
2367
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2681
2368
|
return void json(res, 403, {
|
|
2682
2369
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2683
2370
|
message: "Allow local history access before starting extraction.",
|
|
@@ -2951,7 +2638,7 @@ async function cmdSetup(flags) {
|
|
|
2951
2638
|
if (c.kind === "json") {
|
|
2952
2639
|
const result = writeJsonClientConfig(c.configPath, entry, { forceHeadless });
|
|
2953
2640
|
if (result === "desktop-managed") {
|
|
2954
|
-
console.log(`✅ Kept the valid
|
|
2641
|
+
console.log(`✅ Kept the valid externally managed EchoMem entry for ${c.label}: ${c.configPath}`);
|
|
2955
2642
|
}
|
|
2956
2643
|
else {
|
|
2957
2644
|
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath}`);
|
|
@@ -2963,7 +2650,7 @@ async function cmdSetup(flags) {
|
|
|
2963
2650
|
if (result === "wrote")
|
|
2964
2651
|
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath} — start a new Codex session to load it.`);
|
|
2965
2652
|
else if (result === "desktop-managed")
|
|
2966
|
-
console.log(`✅ Kept the valid
|
|
2653
|
+
console.log(`✅ Kept the valid externally managed EchoMem entry for ${c.label}: ${c.configPath}`);
|
|
2967
2654
|
else
|
|
2968
2655
|
console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
|
|
2969
2656
|
configuredTargets.push(c);
|
|
@@ -2974,7 +2661,7 @@ async function cmdSetup(flags) {
|
|
|
2974
2661
|
: "unavailable";
|
|
2975
2662
|
if (result !== "unavailable" && result.state === "wrote") {
|
|
2976
2663
|
if (result.preservedDesktopManaged) {
|
|
2977
|
-
console.log(`✅ Kept the valid
|
|
2664
|
+
console.log(`✅ Kept the valid externally managed EchoMem user entry for ${c.label}.`);
|
|
2978
2665
|
}
|
|
2979
2666
|
else {
|
|
2980
2667
|
console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
|
|
@@ -3042,14 +2729,14 @@ async function cmdSetup(flags) {
|
|
|
3042
2729
|
await cmdLogin(flags);
|
|
3043
2730
|
}
|
|
3044
2731
|
if (flags["with-hud"]) {
|
|
3045
|
-
console.log("ℹ️ The standalone EchoMem HUD has been retired.
|
|
2732
|
+
console.log("ℹ️ The standalone EchoMem HUD has been retired. Use `echomem-mcp status` to inspect this installation.");
|
|
3046
2733
|
}
|
|
3047
2734
|
}
|
|
3048
2735
|
/**
|
|
3049
2736
|
* `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
|
|
3050
2737
|
* machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), installs EchoMem's
|
|
3051
2738
|
* Codex skills and writes the AGENTS.md memory guidance. One browser
|
|
3052
|
-
* bridge then runs permission →
|
|
2739
|
+
* bridge then runs permission → login → plan if needed → extraction in that order.
|
|
3053
2740
|
* `setup`/`login`/`update` remain granular primitives; init picks the full product defaults.
|
|
3054
2741
|
*/
|
|
3055
2742
|
async function cmdInit(flags) {
|
|
@@ -3063,7 +2750,7 @@ async function cmdInit(flags) {
|
|
|
3063
2750
|
"init-quiet": true,
|
|
3064
2751
|
"install-save-hooks": flags["no-save-hooks"] !== true,
|
|
3065
2752
|
});
|
|
3066
|
-
// 2. Start one ordered onboarding bridge. A fresh device logs in only after consent
|
|
2753
|
+
// 2. Start one ordered onboarding bridge. A fresh device logs in only after consent.
|
|
3067
2754
|
console.log("");
|
|
3068
2755
|
if (!flags["skip-login"] && !flags["no-login"] && !await cmdOnboarding(flags)) {
|
|
3069
2756
|
console.log("\nEchoMem is configured, but onboarding did not finish. Run `echomem-mcp init` again when you are ready.");
|
|
@@ -3071,8 +2758,8 @@ async function cmdInit(flags) {
|
|
|
3071
2758
|
}
|
|
3072
2759
|
console.log("");
|
|
3073
2760
|
console.log("🎉 EchoMem is ready.");
|
|
3074
|
-
console.log(" • MCP memory is configured for
|
|
3075
|
-
console.log(" •
|
|
2761
|
+
console.log(" • MCP memory is configured for the coding agents that connected successfully.");
|
|
2762
|
+
console.log(" • Use `echomem-mcp status` to inspect this device, and `login`, `unlock`, or `update` to manage it.");
|
|
3076
2763
|
console.log(' • Try it now: ask your agent — "search my EchoMem for what I\'ve been working on and recap it."');
|
|
3077
2764
|
}
|
|
3078
2765
|
/**
|
|
@@ -3152,7 +2839,7 @@ function selectSetupTargets(requested, all) {
|
|
|
3152
2839
|
return fs.existsSync(path.dirname(client.configPath));
|
|
3153
2840
|
if (client.kind === "command")
|
|
3154
2841
|
return fs.existsSync(client.detectDir);
|
|
3155
|
-
return client.id === "claude-code" &&
|
|
2842
|
+
return client.id === "claude-code" && claudeCodeCliAvailable();
|
|
3156
2843
|
});
|
|
3157
2844
|
}
|
|
3158
2845
|
return requested ? knownClients().filter((client) => client.id === requested) : detectClients();
|
|
@@ -3188,7 +2875,7 @@ async function cmdLogin(flags) {
|
|
|
3188
2875
|
return true;
|
|
3189
2876
|
}
|
|
3190
2877
|
// Browser path: this bridge does only account/device authentication. It intentionally exposes
|
|
3191
|
-
// no local-history routes; `init` owns
|
|
2878
|
+
// no local-history routes; `init` owns local-history consent and optional extraction.
|
|
3192
2879
|
console.log("Opening your browser to connect this device locally…");
|
|
3193
2880
|
const { port, nonce } = localBridgeOptions(flags);
|
|
3194
2881
|
const srv = await startCallbackServer({ port, nonce, flow: "login" });
|
|
@@ -3216,7 +2903,7 @@ async function cmdLogin(flags) {
|
|
|
3216
2903
|
}
|
|
3217
2904
|
/**
|
|
3218
2905
|
* The local-history onboarding flow. Existing device credentials are reused when available; a
|
|
3219
|
-
* fresh device stays in this same bridge and asks for login only after permission
|
|
2906
|
+
* fresh device stays in this same bridge and asks for login only after permission.
|
|
3220
2907
|
*/
|
|
3221
2908
|
async function cmdOnboarding(flags) {
|
|
3222
2909
|
const store = new KeyStore();
|
|
@@ -3228,149 +2915,17 @@ async function cmdOnboarding(flags) {
|
|
|
3228
2915
|
console.log("Opening your browser for EchoMem onboarding…");
|
|
3229
2916
|
const { port, nonce } = localBridgeOptions(flags);
|
|
3230
2917
|
let stats = null;
|
|
3231
|
-
let forensicReport = null;
|
|
3232
|
-
let forensicConsent = "pending";
|
|
3233
|
-
let forensicScanStarted = false;
|
|
3234
|
-
const forensicStartedAt = Date.now();
|
|
3235
|
-
let forensicStageStartedAt = forensicStartedAt;
|
|
3236
|
-
let forensicStage = "starting";
|
|
3237
|
-
let forensicOverall = 0;
|
|
3238
|
-
let forensicProgress = {
|
|
3239
|
-
status: "running",
|
|
3240
|
-
scanned: 0,
|
|
3241
|
-
total: 0,
|
|
3242
|
-
stage: forensicStage,
|
|
3243
|
-
label: forensicStageLabel(forensicStage),
|
|
3244
|
-
stageDone: 0,
|
|
3245
|
-
stageTotal: 0,
|
|
3246
|
-
overall: 0,
|
|
3247
|
-
elapsedMs: 0,
|
|
3248
|
-
stageElapsedMs: 0,
|
|
3249
|
-
updatedAt: forensicStartedAt,
|
|
3250
|
-
};
|
|
3251
2918
|
const srv = await startCallbackServer({
|
|
3252
2919
|
port,
|
|
3253
2920
|
nonce,
|
|
3254
2921
|
flow: "onboarding",
|
|
3255
2922
|
initialToken,
|
|
3256
|
-
|
|
3257
|
-
getStats: () => stats,
|
|
3258
|
-
getReport: () => forensicReport,
|
|
3259
|
-
getReportProgress: () => {
|
|
3260
|
-
if (forensicProgress.status !== "running")
|
|
3261
|
-
return forensicProgress;
|
|
3262
|
-
const now = Date.now();
|
|
3263
|
-
const sinceWorkerUpdate = Math.max(0, now - forensicProgress.updatedAt);
|
|
3264
|
-
return {
|
|
3265
|
-
...forensicProgress,
|
|
3266
|
-
elapsedMs: forensicProgress.elapsedMs + sinceWorkerUpdate,
|
|
3267
|
-
stageElapsedMs: forensicProgress.stageElapsedMs + sinceWorkerUpdate,
|
|
3268
|
-
updatedAt: now,
|
|
3269
|
-
};
|
|
3270
|
-
},
|
|
3271
|
-
onReportConsent: (allowed) => {
|
|
3272
|
-
if (!allowed) {
|
|
3273
|
-
forensicConsent = "declined";
|
|
3274
|
-
forensicProgress = {
|
|
3275
|
-
status: "failed",
|
|
3276
|
-
scanned: 0,
|
|
3277
|
-
total: 0,
|
|
3278
|
-
stage: "failed",
|
|
3279
|
-
label: "Local scan skipped",
|
|
3280
|
-
stageDone: 0,
|
|
3281
|
-
stageTotal: 0,
|
|
3282
|
-
overall: 0,
|
|
3283
|
-
elapsedMs: Date.now() - forensicStartedAt,
|
|
3284
|
-
stageElapsedMs: Date.now() - forensicStageStartedAt,
|
|
3285
|
-
updatedAt: Date.now(),
|
|
3286
|
-
error: {
|
|
3287
|
-
code: "REPORT_SCAN_DECLINED",
|
|
3288
|
-
message: "Local file analysis was skipped. EchoMem can still connect, save conversations, and import memories.",
|
|
3289
|
-
},
|
|
3290
|
-
};
|
|
3291
|
-
return;
|
|
3292
|
-
}
|
|
3293
|
-
forensicConsent = "allowed";
|
|
3294
|
-
const now = Date.now();
|
|
3295
|
-
forensicStage = "starting";
|
|
3296
|
-
forensicStageStartedAt = now;
|
|
3297
|
-
forensicOverall = 0;
|
|
3298
|
-
forensicProgress = {
|
|
3299
|
-
status: "running",
|
|
3300
|
-
scanned: 0,
|
|
3301
|
-
total: 0,
|
|
3302
|
-
stage: forensicStage,
|
|
3303
|
-
label: forensicStageLabel(forensicStage),
|
|
3304
|
-
stageDone: 0,
|
|
3305
|
-
stageTotal: 0,
|
|
3306
|
-
overall: 0,
|
|
3307
|
-
elapsedMs: now - forensicStartedAt,
|
|
3308
|
-
stageElapsedMs: 0,
|
|
3309
|
-
updatedAt: now,
|
|
3310
|
-
};
|
|
3311
|
-
startForensicScan();
|
|
3312
|
-
},
|
|
2923
|
+
requireLocalHistoryConsent: true,
|
|
3313
2924
|
});
|
|
3314
2925
|
const localSetupUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
|
|
3315
2926
|
openBrowser(localSetupUrl);
|
|
3316
2927
|
console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
|
|
3317
2928
|
console.log("Waiting for local-history onboarding for up to 15 minutes…");
|
|
3318
|
-
const startForensicScan = () => {
|
|
3319
|
-
if (forensicScanStarted || forensicConsent !== "allowed")
|
|
3320
|
-
return;
|
|
3321
|
-
forensicScanStarted = true;
|
|
3322
|
-
// Build the local forensic "Context Doctor" report off-thread only after explicit consent.
|
|
3323
|
-
buildForensicReportOffThread((progress) => {
|
|
3324
|
-
const now = Date.now();
|
|
3325
|
-
const nextStage = progress.stage || forensicStage;
|
|
3326
|
-
if (nextStage !== forensicStage) {
|
|
3327
|
-
forensicStage = nextStage;
|
|
3328
|
-
forensicStageStartedAt = now;
|
|
3329
|
-
console.log(`Local scan: ${forensicStageLabel(forensicStage)}…`);
|
|
3330
|
-
}
|
|
3331
|
-
// Latched, so a caller that ever reports a smaller fraction cannot walk the bar backwards.
|
|
3332
|
-
forensicOverall = Math.max(forensicOverall, typeof progress.overall === "number" && Number.isFinite(progress.overall) ? progress.overall : 0);
|
|
3333
|
-
forensicProgress = {
|
|
3334
|
-
status: "running",
|
|
3335
|
-
scanned: progress.done,
|
|
3336
|
-
total: progress.total,
|
|
3337
|
-
stage: forensicStage,
|
|
3338
|
-
label: forensicStageLabel(forensicStage),
|
|
3339
|
-
detail: progress.detail,
|
|
3340
|
-
stageDone: typeof progress.stageDone === "number" && Number.isFinite(progress.stageDone)
|
|
3341
|
-
? Math.max(0, Math.floor(progress.stageDone))
|
|
3342
|
-
: 0,
|
|
3343
|
-
stageTotal: typeof progress.stageTotal === "number" && Number.isFinite(progress.stageTotal)
|
|
3344
|
-
? Math.max(0, Math.floor(progress.stageTotal))
|
|
3345
|
-
: 0,
|
|
3346
|
-
overall: forensicOverall,
|
|
3347
|
-
elapsedMs: now - forensicStartedAt,
|
|
3348
|
-
stageElapsedMs: now - forensicStageStartedAt,
|
|
3349
|
-
updatedAt: now,
|
|
3350
|
-
};
|
|
3351
|
-
})
|
|
3352
|
-
.then((r) => {
|
|
3353
|
-
forensicReport = r;
|
|
3354
|
-
})
|
|
3355
|
-
.catch((e) => {
|
|
3356
|
-
const now = Date.now();
|
|
3357
|
-
forensicProgress = {
|
|
3358
|
-
status: "failed",
|
|
3359
|
-
scanned: forensicProgress.scanned,
|
|
3360
|
-
total: forensicProgress.total,
|
|
3361
|
-
stage: "failed",
|
|
3362
|
-
label: "Local scan failed",
|
|
3363
|
-
stageDone: forensicProgress.stageDone,
|
|
3364
|
-
stageTotal: forensicProgress.stageTotal,
|
|
3365
|
-
overall: forensicOverall,
|
|
3366
|
-
elapsedMs: now - forensicStartedAt,
|
|
3367
|
-
stageElapsedMs: now - forensicStageStartedAt,
|
|
3368
|
-
updatedAt: now,
|
|
3369
|
-
error: safeForensicError(e),
|
|
3370
|
-
};
|
|
3371
|
-
console.error(`Could not build the local report: ${e instanceof Error ? e.message : String(e)}`);
|
|
3372
|
-
});
|
|
3373
|
-
};
|
|
3374
2929
|
let token;
|
|
3375
2930
|
let key;
|
|
3376
2931
|
try {
|
|
@@ -3461,9 +3016,8 @@ async function cmdOnboarding(flags) {
|
|
|
3461
3016
|
claudeCode: quick.claudeCodeCount,
|
|
3462
3017
|
cowork: quick.coworkCount,
|
|
3463
3018
|
};
|
|
3464
|
-
stats =
|
|
3019
|
+
stats = buildOnboardingStatsPayload({
|
|
3465
3020
|
partial: true,
|
|
3466
|
-
skipMemoryCount: true,
|
|
3467
3021
|
sessions: sessionSummary,
|
|
3468
3022
|
migratable,
|
|
3469
3023
|
discovery: { phase: "quick", exact: false },
|
|
@@ -3498,9 +3052,8 @@ async function cmdOnboarding(flags) {
|
|
|
3498
3052
|
claudeCode: cloudSummary.claudeCodeCount,
|
|
3499
3053
|
cowork: cloudSummary.coworkCount,
|
|
3500
3054
|
};
|
|
3501
|
-
const cloudPayload =
|
|
3055
|
+
const cloudPayload = buildOnboardingStatsPayload({
|
|
3502
3056
|
partial: true,
|
|
3503
|
-
skipMemoryCount: true,
|
|
3504
3057
|
sessions: sessionSummary,
|
|
3505
3058
|
migratable,
|
|
3506
3059
|
discovery: { phase: "account", exact: false },
|
|
@@ -3536,9 +3089,8 @@ async function cmdOnboarding(flags) {
|
|
|
3536
3089
|
claudeCode: unavailableSummary.claudeCodeCount,
|
|
3537
3090
|
cowork: unavailableSummary.coworkCount,
|
|
3538
3091
|
};
|
|
3539
|
-
const unavailablePayload =
|
|
3092
|
+
const unavailablePayload = buildOnboardingStatsPayload({
|
|
3540
3093
|
partial: true,
|
|
3541
|
-
skipMemoryCount: true,
|
|
3542
3094
|
sessions: sessionSummary,
|
|
3543
3095
|
migratable,
|
|
3544
3096
|
discovery: { phase: "account", exact: false },
|
|
@@ -3574,9 +3126,8 @@ async function cmdOnboarding(flags) {
|
|
|
3574
3126
|
migratable = migratableFromDiscovery(initialExact);
|
|
3575
3127
|
latestPendingEstimate = migratable.pending;
|
|
3576
3128
|
sessionSummary = sessionsFromDiscovery(initialExact);
|
|
3577
|
-
const partialPayload = withCandidateSessions(
|
|
3129
|
+
const partialPayload = withCandidateSessions(buildOnboardingStatsPayload({
|
|
3578
3130
|
partial: true,
|
|
3579
|
-
skipMemoryCount: true,
|
|
3580
3131
|
sessions: sessionSummary,
|
|
3581
3132
|
migratable,
|
|
3582
3133
|
discovery: { phase: "exact", exact: true },
|
|
@@ -3624,12 +3175,10 @@ async function cmdOnboarding(flags) {
|
|
|
3624
3175
|
migratable = migratableFromDiscovery(reconciled);
|
|
3625
3176
|
latestPendingEstimate = migratable.pending;
|
|
3626
3177
|
sessionSummary = sessionsFromDiscovery(reconciled);
|
|
3627
|
-
const reconciledPayload = withCandidateSessions(
|
|
3628
|
-
partial: true,
|
|
3629
|
-
skipMemoryCount: true,
|
|
3178
|
+
const reconciledPayload = withCandidateSessions(buildOnboardingStatsPayload({
|
|
3630
3179
|
sessions: sessionSummary,
|
|
3631
3180
|
migratable,
|
|
3632
|
-
discovery: { phase: "
|
|
3181
|
+
discovery: { phase: "full", exact: true },
|
|
3633
3182
|
}), reconciled);
|
|
3634
3183
|
if (generation !== refreshGeneration)
|
|
3635
3184
|
return;
|
|
@@ -3644,20 +3193,11 @@ async function cmdOnboarding(flags) {
|
|
|
3644
3193
|
failed: 0,
|
|
3645
3194
|
extracted: 0,
|
|
3646
3195
|
});
|
|
3647
|
-
const fullPayload = withCandidateSessions(await buildCollectedStatsPayloadOffThread({
|
|
3648
|
-
sessions: sessionSummary,
|
|
3649
|
-
migratable,
|
|
3650
|
-
discovery: { phase: "full", exact: true },
|
|
3651
|
-
}), reconciled);
|
|
3652
|
-
if (generation !== refreshGeneration)
|
|
3653
|
-
return;
|
|
3654
|
-
stats = fullPayload;
|
|
3655
|
-
srv.setStats(fullPayload);
|
|
3656
3196
|
})().catch((e) => {
|
|
3657
3197
|
if (generation !== refreshGeneration)
|
|
3658
3198
|
return;
|
|
3659
|
-
console.error(`[echomem] optional
|
|
3660
|
-
publishOptionalStatsFallback(generation, "
|
|
3199
|
+
console.error(`[echomem] optional account reconciliation unavailable; continuing (${errorCode(e) || "ACCOUNT_RECONCILIATION_FAILED"})`);
|
|
3200
|
+
publishOptionalStatsFallback(generation, "ACCOUNT_RECONCILIATION_FAILED", true);
|
|
3661
3201
|
});
|
|
3662
3202
|
return initialExact;
|
|
3663
3203
|
}).catch((e) => {
|
|
@@ -4117,7 +3657,7 @@ Usage:
|
|
|
4117
3657
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
4118
3658
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
|
|
4119
3659
|
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
4120
|
-
echomem-mcp setup --force-headless Explicitly replace valid
|
|
3660
|
+
echomem-mcp setup --force-headless Explicitly replace a valid externally managed entry
|
|
4121
3661
|
echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
|
|
4122
3662
|
echomem-mcp update --all Install this bridge durably + repoint detected clients; no login/browser
|
|
4123
3663
|
echomem-mcp update --client X Repoint one MCP client; no login/browser
|
|
@@ -4128,7 +3668,6 @@ Usage:
|
|
|
4128
3668
|
echomem-mcp status Show token/key/clients
|
|
4129
3669
|
echomem-mcp doctor [--no-network] Diagnose configured client bridge versions
|
|
4130
3670
|
echomem-mcp logout Remove stored credentials
|
|
4131
|
-
echomem-mcp report [--json] Your AI coding memory audit (local, no login, $0)
|
|
4132
3671
|
echomem-mcp migrate [--since D] Import your existing Codex/Claude history into your memory
|
|
4133
3672
|
echomem-mcp migrate --estimate Estimate migration size/time metadata without uploading transcripts
|
|
4134
3673
|
echomem-mcp migrate --max-chars N Import only sessions up to N assembled text chars
|
|
@@ -4180,9 +3719,6 @@ export async function runCli(argv) {
|
|
|
4180
3719
|
case "logout":
|
|
4181
3720
|
cmdLogout();
|
|
4182
3721
|
return true;
|
|
4183
|
-
case "report":
|
|
4184
|
-
await runReport(flags);
|
|
4185
|
-
return true;
|
|
4186
3722
|
case "migrate":
|
|
4187
3723
|
await cmdMigrate(flags);
|
|
4188
3724
|
return true;
|