@echomem/mcp 1.4.46 → 1.4.48
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-jsonl.js +87 -0
- package/dist/migrate.js +6 -4
- 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 +167 -622
- 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,11 @@ 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 { repoLabel, validateForensicReportForSetup } from "./forensics.js";
|
|
34
33
|
import { installSaveCheckpointHooks, installSourceSessionHooks } from "./hud/hooks.js";
|
|
35
34
|
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
36
35
|
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
@@ -70,15 +69,25 @@ const CODEX_SKILL_NAMES = [
|
|
|
70
69
|
function home(...p) {
|
|
71
70
|
return path.join(os.homedir(), ...p);
|
|
72
71
|
}
|
|
73
|
-
function
|
|
74
|
-
const configured = process.env
|
|
72
|
+
function configuredProfileDirectory(envKey, fallbackName) {
|
|
73
|
+
const configured = process.env[envKey]?.trim();
|
|
75
74
|
if (!configured)
|
|
76
|
-
return home(
|
|
75
|
+
return home(fallbackName);
|
|
77
76
|
if (configured === "~")
|
|
78
77
|
return os.homedir();
|
|
79
|
-
if (configured.startsWith(
|
|
78
|
+
if (configured.startsWith("~/") || configured.startsWith("~\\")) {
|
|
80
79
|
return path.join(os.homedir(), configured.slice(2));
|
|
81
|
-
|
|
80
|
+
}
|
|
81
|
+
if (!path.isAbsolute(configured)) {
|
|
82
|
+
throw new Error(`${envKey} must be an absolute path or start with ~/`);
|
|
83
|
+
}
|
|
84
|
+
return path.normalize(configured);
|
|
85
|
+
}
|
|
86
|
+
function codexHome() {
|
|
87
|
+
return configuredProfileDirectory("CODEX_HOME", ".codex");
|
|
88
|
+
}
|
|
89
|
+
function claudeConfigHome() {
|
|
90
|
+
return configuredProfileDirectory("CLAUDE_CONFIG_DIR", ".claude");
|
|
82
91
|
}
|
|
83
92
|
function filesEqual(left, right) {
|
|
84
93
|
try {
|
|
@@ -401,7 +410,7 @@ function echomemGuidanceBlock() {
|
|
|
401
410
|
"- 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
411
|
"- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
|
|
403
412
|
'- 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,
|
|
413
|
+
"- 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
414
|
'- 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
415
|
"- 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
416
|
"- 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 +421,7 @@ function echomemGuidanceBlock() {
|
|
|
412
421
|
"- 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
422
|
"- 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
423
|
"- 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,
|
|
424
|
+
"- 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
425
|
"- 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
426
|
"- 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
427
|
"- 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 +453,35 @@ export function writeAgentsMemoryGuidance(filePath) {
|
|
|
444
453
|
fs.appendFileSync(filePath, sep + block + "\n");
|
|
445
454
|
return "wrote";
|
|
446
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Refresh marker-owned guidance for users upgrading an existing standalone MCP install.
|
|
458
|
+
* This intentionally does not create global memory files: setup owns first installation,
|
|
459
|
+
* while server startup only migrates guidance that EchoMem already installed.
|
|
460
|
+
*/
|
|
461
|
+
export function refreshInstalledMemoryGuidance() {
|
|
462
|
+
let candidates;
|
|
463
|
+
try {
|
|
464
|
+
candidates = [
|
|
465
|
+
path.join(codexHome(), "AGENTS.md"),
|
|
466
|
+
path.join(claudeConfigHome(), "CLAUDE.md"),
|
|
467
|
+
];
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
for (const filePath of candidates) {
|
|
473
|
+
try {
|
|
474
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
475
|
+
const start = content.indexOf(AGENTS_MD_BEGIN);
|
|
476
|
+
const end = content.indexOf(AGENTS_MD_END);
|
|
477
|
+
if (start >= 0 && end > start)
|
|
478
|
+
writeAgentsMemoryGuidance(filePath);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// Missing, unreadable, or externally managed files are left untouched.
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
447
485
|
/** Merge the EchoMem entry into a JSON client's `mcpServers` map without clobbering siblings. */
|
|
448
486
|
export function writeJsonClientConfig(configPath, entry, options = {}) {
|
|
449
487
|
let config = {};
|
|
@@ -622,6 +660,7 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
622
660
|
// Older CLI versions wrote local/project entries, which take precedence over user scope and can
|
|
623
661
|
// keep launching a deleted npm cache or stale runtime. Migrate those only after user scope is safe.
|
|
624
662
|
const configPath = options.configPath ?? home(".claude.json");
|
|
663
|
+
let failureReason;
|
|
625
664
|
const emptyResult = () => ({
|
|
626
665
|
state: "unavailable",
|
|
627
666
|
removedLocalProjects: [],
|
|
@@ -629,6 +668,7 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
629
668
|
failedLocalProjects: [],
|
|
630
669
|
restoredPreviousUserEntry: false,
|
|
631
670
|
preservedDesktopManaged: false,
|
|
671
|
+
failureReason,
|
|
632
672
|
});
|
|
633
673
|
const runClaude = (args, cwd) => {
|
|
634
674
|
try {
|
|
@@ -638,9 +678,11 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
638
678
|
stdio: ["ignore", "pipe", "pipe"],
|
|
639
679
|
timeout: 10000,
|
|
640
680
|
}, options.claudeCommands);
|
|
681
|
+
failureReason = undefined;
|
|
641
682
|
return true;
|
|
642
683
|
}
|
|
643
|
-
catch {
|
|
684
|
+
catch (error) {
|
|
685
|
+
failureReason = commandFailureMessage(error);
|
|
644
686
|
return false;
|
|
645
687
|
}
|
|
646
688
|
};
|
|
@@ -659,8 +701,10 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
659
701
|
if (previousUserEntry && !removeUser())
|
|
660
702
|
return emptyResult();
|
|
661
703
|
if (!addUser(desiredUserEntry)) {
|
|
704
|
+
const addFailureReason = failureReason;
|
|
662
705
|
if (previousUserEntry)
|
|
663
706
|
restoredPreviousUserEntry = addUser(previousUserEntry);
|
|
707
|
+
failureReason = addFailureReason;
|
|
664
708
|
return { ...emptyResult(), restoredPreviousUserEntry };
|
|
665
709
|
}
|
|
666
710
|
}
|
|
@@ -1223,7 +1267,7 @@ function withTimeout(promise, ms, code, onTimeout) {
|
|
|
1223
1267
|
function delay(ms) {
|
|
1224
1268
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1225
1269
|
}
|
|
1226
|
-
const
|
|
1270
|
+
const LOCAL_ASSET_TYPES = {
|
|
1227
1271
|
".html": "text/html; charset=utf-8",
|
|
1228
1272
|
".js": "text/javascript; charset=utf-8",
|
|
1229
1273
|
".json": "application/json; charset=utf-8",
|
|
@@ -1232,39 +1276,11 @@ const CITY_ASSET_TYPES = {
|
|
|
1232
1276
|
".png": "image/png",
|
|
1233
1277
|
".svg": "image/svg+xml",
|
|
1234
1278
|
};
|
|
1235
|
-
function
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
// Published install: fall back to the copy bundled into dist/city by prepack (bundle-city.mjs).
|
|
1241
|
-
return fileURLToPath(new URL("./city/", import.meta.url));
|
|
1242
|
-
}
|
|
1243
|
-
function serveRepoCityAsset(reqPath, res) {
|
|
1244
|
-
const root = repoCityArtifactsRoot();
|
|
1245
|
-
const rel = reqPath === "/city" || reqPath === "/city/" ? "echo-ai-city-only.html" : decodeURIComponent(reqPath.slice("/city/".length));
|
|
1246
|
-
// Archives stay in the checkout for recovery, but must never become a localhost UI surface.
|
|
1247
|
-
const normalizedRel = rel.replace(/\\/g, "/");
|
|
1248
|
-
if (normalizedRel === "archive" || normalizedRel.startsWith("archive/")) {
|
|
1249
|
-
res.writeHead(404).end("not found");
|
|
1250
|
-
return true;
|
|
1251
|
-
}
|
|
1252
|
-
const filePath = path.resolve(root, rel);
|
|
1253
|
-
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
1254
|
-
if (!filePath.startsWith(rootWithSep)) {
|
|
1255
|
-
res.writeHead(403).end("forbidden");
|
|
1256
|
-
return true;
|
|
1257
|
-
}
|
|
1258
|
-
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
1259
|
-
res.writeHead(404).end("not found");
|
|
1260
|
-
return true;
|
|
1261
|
-
}
|
|
1262
|
-
res.writeHead(200, {
|
|
1263
|
-
"Content-Type": CITY_ASSET_TYPES[path.extname(filePath)] || "application/octet-stream",
|
|
1264
|
-
"Cache-Control": "no-store",
|
|
1265
|
-
});
|
|
1266
|
-
fs.createReadStream(filePath).pipe(res);
|
|
1267
|
-
return true;
|
|
1279
|
+
function repoLabel(cwd) {
|
|
1280
|
+
if (!cwd)
|
|
1281
|
+
return "";
|
|
1282
|
+
const normalized = cwd.replace(/[\\/]+$/, "");
|
|
1283
|
+
return path.basename(normalized) || normalized;
|
|
1268
1284
|
}
|
|
1269
1285
|
function hudAssetsRoot() {
|
|
1270
1286
|
return fileURLToPath(new URL("../assets/hud/", import.meta.url));
|
|
@@ -1283,7 +1299,7 @@ function serveHudAsset(reqPath, res) {
|
|
|
1283
1299
|
return true;
|
|
1284
1300
|
}
|
|
1285
1301
|
res.writeHead(200, {
|
|
1286
|
-
"Content-Type":
|
|
1302
|
+
"Content-Type": LOCAL_ASSET_TYPES[path.extname(filePath)] || "application/octet-stream",
|
|
1287
1303
|
"Cache-Control": "no-store",
|
|
1288
1304
|
});
|
|
1289
1305
|
fs.createReadStream(filePath).pipe(res);
|
|
@@ -1460,61 +1476,6 @@ export function discoverMigratableFastOffThread(opts = {}) {
|
|
|
1460
1476
|
});
|
|
1461
1477
|
});
|
|
1462
1478
|
}
|
|
1463
|
-
/** Build the full local-history dashboard payload away from the callback server's event loop.
|
|
1464
|
-
* `collect()` can synchronously parse hundreds of JSONL files for tens of seconds; doing that on
|
|
1465
|
-
* the bridge thread prevents even localhost actions such as account switch from receiving a reply. */
|
|
1466
|
-
export function buildCollectedStatsPayloadOffThread(inject) {
|
|
1467
|
-
const reportUrl = runtimeModuleUrl("report");
|
|
1468
|
-
const serializedInject = JSON.stringify(inject);
|
|
1469
|
-
const code = `
|
|
1470
|
-
import { parentPort } from "node:worker_threads";
|
|
1471
|
-
import { collect, buildStatsPayload } from ${JSON.stringify(reportUrl)};
|
|
1472
|
-
|
|
1473
|
-
try {
|
|
1474
|
-
const payload = await buildStatsPayload(collect(), ${serializedInject});
|
|
1475
|
-
parentPort?.postMessage({ ok: true, payload });
|
|
1476
|
-
} catch (error) {
|
|
1477
|
-
parentPort?.postMessage({
|
|
1478
|
-
ok: false,
|
|
1479
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1480
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1481
|
-
});
|
|
1482
|
-
}
|
|
1483
|
-
`;
|
|
1484
|
-
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
1485
|
-
return new Promise((resolve, reject) => {
|
|
1486
|
-
let settled = false;
|
|
1487
|
-
const finish = (result) => {
|
|
1488
|
-
if (settled)
|
|
1489
|
-
return;
|
|
1490
|
-
settled = true;
|
|
1491
|
-
void worker.terminate();
|
|
1492
|
-
if (result.ok)
|
|
1493
|
-
resolve(result.payload);
|
|
1494
|
-
else
|
|
1495
|
-
reject(result.error);
|
|
1496
|
-
};
|
|
1497
|
-
worker.once("message", (message) => {
|
|
1498
|
-
const msg = message;
|
|
1499
|
-
if (msg.ok === true) {
|
|
1500
|
-
finish({ ok: true, payload: msg.payload });
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
const error = new Error(typeof msg.message === "string" ? msg.message : "Full local stats worker failed");
|
|
1504
|
-
if (typeof msg.stack === "string")
|
|
1505
|
-
error.stack = msg.stack;
|
|
1506
|
-
finish({ ok: false, error });
|
|
1507
|
-
});
|
|
1508
|
-
worker.once("error", (error) => {
|
|
1509
|
-
finish({ ok: false, error });
|
|
1510
|
-
});
|
|
1511
|
-
worker.once("exit", (code) => {
|
|
1512
|
-
if (settled)
|
|
1513
|
-
return;
|
|
1514
|
-
finish({ ok: false, error: new Error(`Full local stats worker exited (code ${code}) without a result`) });
|
|
1515
|
-
});
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
1479
|
export function createLocalDiscoveryCache(loaders = {}) {
|
|
1519
1480
|
const loadQuick = loaders.loadQuick ?? (() => discoverMigratableFastOffThread());
|
|
1520
1481
|
const loadExact = loaders.loadExact ?? (() => discoverMigratableSessionsOffThread());
|
|
@@ -1563,45 +1524,6 @@ export function createLocalDiscoveryCache(loaders = {}) {
|
|
|
1563
1524
|
},
|
|
1564
1525
|
};
|
|
1565
1526
|
}
|
|
1566
|
-
function forensicStageLabel(stage) {
|
|
1567
|
-
if (stage === "reading-transcripts")
|
|
1568
|
-
return "Reading transcript files";
|
|
1569
|
-
if (stage === "building-summary")
|
|
1570
|
-
return "Building scan summary";
|
|
1571
|
-
if (stage === "classifying-repeated-context")
|
|
1572
|
-
return "Classifying repeated context";
|
|
1573
|
-
if (stage === "finalizing-report")
|
|
1574
|
-
return "Finalizing report";
|
|
1575
|
-
return "Starting local scan";
|
|
1576
|
-
}
|
|
1577
|
-
/** Build the local forensic "Context Doctor" report on a worker thread so the multi-file scan never
|
|
1578
|
-
* blocks the bridge's event loop (the freeze postmortem: any unbounded sync work on this path is risky). */
|
|
1579
|
-
export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
1580
|
-
let lastProgress = null;
|
|
1581
|
-
const recordProgress = (progress) => {
|
|
1582
|
-
lastProgress = progress;
|
|
1583
|
-
onProgress?.(progress);
|
|
1584
|
-
};
|
|
1585
|
-
return runForensicReportWorker(recordProgress, options).catch(async (primaryError) => {
|
|
1586
|
-
if (options.failOpen === false)
|
|
1587
|
-
throw primaryError;
|
|
1588
|
-
const failureCode = errorCode(primaryError) || "REPORT_BUILD_FAILED";
|
|
1589
|
-
console.error(`[echomem] local scan degraded after ${failureCode}; continuing without local-history analysis`);
|
|
1590
|
-
onProgress?.({
|
|
1591
|
-
done: lastProgress?.done || 0,
|
|
1592
|
-
total: lastProgress?.total || 0,
|
|
1593
|
-
stage: "finalizing-report",
|
|
1594
|
-
detail: "finishing setup without optional local-history analysis",
|
|
1595
|
-
overall: 0.99,
|
|
1596
|
-
stageDone: 0,
|
|
1597
|
-
stageTotal: 0,
|
|
1598
|
-
});
|
|
1599
|
-
return runForensicReportWorker(undefined, {
|
|
1600
|
-
timeoutMs: 30_000,
|
|
1601
|
-
maxOldGenerationSizeMb: Math.max(64, options.maxOldGenerationSizeMb || 0),
|
|
1602
|
-
}, [], failureCode);
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
1527
|
function errorCode(error) {
|
|
1606
1528
|
return error && typeof error === "object" && "code" in error
|
|
1607
1529
|
? String(error.code || "")
|
|
@@ -1621,7 +1543,6 @@ export function completeOptionalStatsPayload(payload, reason, countsTrusted) {
|
|
|
1621
1543
|
transcriptsUploaded: false,
|
|
1622
1544
|
sessions: { total: 0, codex: 0, claudeCode: 0, cowork: 0 },
|
|
1623
1545
|
migratable: { pending: 0, alreadyMigrated: 0 },
|
|
1624
|
-
memoriesCaptured: null,
|
|
1625
1546
|
};
|
|
1626
1547
|
const completed = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
1627
1548
|
? { ...payload }
|
|
@@ -1634,155 +1555,11 @@ export function completeOptionalStatsPayload(payload, reason, countsTrusted) {
|
|
|
1634
1555
|
};
|
|
1635
1556
|
return completed;
|
|
1636
1557
|
}
|
|
1637
|
-
function runForensicReportWorker(onProgress, options, sources, degradedReason) {
|
|
1638
|
-
const forensicsUrl = runtimeModuleUrl("forensics");
|
|
1639
|
-
const serializedSources = sources === undefined ? "undefined" : JSON.stringify(sources);
|
|
1640
|
-
const serializedDegradedReason = JSON.stringify(degradedReason || "");
|
|
1641
|
-
const code = `
|
|
1642
|
-
import { parentPort } from "node:worker_threads";
|
|
1643
|
-
import { buildForensicReport, validateForensicReportForSetup } from ${JSON.stringify(forensicsUrl)};
|
|
1644
|
-
try {
|
|
1645
|
-
const report = await buildForensicReport({
|
|
1646
|
-
sources: ${serializedSources},
|
|
1647
|
-
includeLegacyGoldenStandard: false,
|
|
1648
|
-
onProgress: (done, total, stage, detail, overall, stageDone, stageTotal) => parentPort?.postMessage({
|
|
1649
|
-
progress: { done, total, stage, detail, overall, stageDone, stageTotal },
|
|
1650
|
-
}),
|
|
1651
|
-
});
|
|
1652
|
-
const degradedReason = ${serializedDegradedReason};
|
|
1653
|
-
if (degradedReason) {
|
|
1654
|
-
report.scanDiagnostics = {
|
|
1655
|
-
degraded: true,
|
|
1656
|
-
reason: degradedReason,
|
|
1657
|
-
skippedSources: ["codex", "claude"],
|
|
1658
|
-
};
|
|
1659
|
-
}
|
|
1660
|
-
const validation = validateForensicReportForSetup(report);
|
|
1661
|
-
if (!validation.ok) {
|
|
1662
|
-
const error = new Error(validation.message);
|
|
1663
|
-
error.code = validation.code;
|
|
1664
|
-
throw error;
|
|
1665
|
-
}
|
|
1666
|
-
parentPort?.postMessage({ ok: true, report });
|
|
1667
|
-
} catch (error) {
|
|
1668
|
-
parentPort?.postMessage({
|
|
1669
|
-
ok: false,
|
|
1670
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1671
|
-
code: error && typeof error === "object" && "code" in error ? String(error.code || "") : "",
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
`;
|
|
1675
|
-
const requestedHeapMb = options.maxOldGenerationSizeMb;
|
|
1676
|
-
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`), Number.isFinite(requestedHeapMb)
|
|
1677
|
-
? { resourceLimits: { maxOldGenerationSizeMb: Math.max(16, Math.floor(requestedHeapMb)) } }
|
|
1678
|
-
: undefined);
|
|
1679
|
-
return new Promise((resolve, reject) => {
|
|
1680
|
-
let settled = false;
|
|
1681
|
-
const requestedTimeoutMs = options.timeoutMs ?? 15 * 60_000;
|
|
1682
|
-
const timeoutMs = Number.isFinite(requestedTimeoutMs) ? Math.max(1, requestedTimeoutMs) : 15 * 60_000;
|
|
1683
|
-
const timeout = setTimeout(() => {
|
|
1684
|
-
if (settled)
|
|
1685
|
-
return;
|
|
1686
|
-
settled = true;
|
|
1687
|
-
void worker.terminate();
|
|
1688
|
-
const error = new Error(`Local forensic report timed out after ${timeoutMs}ms`);
|
|
1689
|
-
error.code = "REPORT_SCAN_TIMEOUT";
|
|
1690
|
-
reject(error);
|
|
1691
|
-
}, timeoutMs);
|
|
1692
|
-
timeout.unref?.();
|
|
1693
|
-
const finish = (result) => {
|
|
1694
|
-
if (settled)
|
|
1695
|
-
return;
|
|
1696
|
-
settled = true;
|
|
1697
|
-
clearTimeout(timeout);
|
|
1698
|
-
void worker.terminate();
|
|
1699
|
-
if (result.ok)
|
|
1700
|
-
resolve(result.report);
|
|
1701
|
-
else
|
|
1702
|
-
reject(result.error);
|
|
1703
|
-
};
|
|
1704
|
-
worker.on("message", (message) => {
|
|
1705
|
-
if (settled)
|
|
1706
|
-
return;
|
|
1707
|
-
const msg = message;
|
|
1708
|
-
if (msg.progress) {
|
|
1709
|
-
onProgress?.(msg.progress);
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
if (msg.ok === true && msg.report && typeof msg.report === "object") {
|
|
1713
|
-
finish({ ok: true, report: msg.report });
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
const error = new Error(typeof msg.message === "string" ? msg.message : "Local forensic report failed");
|
|
1717
|
-
if (typeof msg.code === "string" && msg.code)
|
|
1718
|
-
error.code = msg.code;
|
|
1719
|
-
finish({ ok: false, error });
|
|
1720
|
-
});
|
|
1721
|
-
worker.once("error", (error) => {
|
|
1722
|
-
finish({ ok: false, error });
|
|
1723
|
-
});
|
|
1724
|
-
worker.once("exit", (code) => {
|
|
1725
|
-
if (settled)
|
|
1726
|
-
return;
|
|
1727
|
-
finish({ ok: false, error: new Error(`Forensic report worker exited (code ${code}) without a result`) });
|
|
1728
|
-
});
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
1558
|
export function respondMigrate(res, body, status = 200) {
|
|
1732
1559
|
if (res.writableEnded)
|
|
1733
1560
|
return;
|
|
1734
1561
|
res.writeHead(status, { "Content-Type": "application/json" }).end(JSON.stringify(body));
|
|
1735
1562
|
}
|
|
1736
|
-
function safeForensicError(error) {
|
|
1737
|
-
const code = error && typeof error === "object" && "code" in error
|
|
1738
|
-
? String(error.code || "")
|
|
1739
|
-
: "";
|
|
1740
|
-
if (code === "REPORT_SCAN_TIMEOUT") {
|
|
1741
|
-
return {
|
|
1742
|
-
code,
|
|
1743
|
-
message: "The local workspace scan took too long and was stopped. No backup data was substituted. Rerun setup to retry.",
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
return {
|
|
1747
|
-
code: "REPORT_BUILD_FAILED",
|
|
1748
|
-
message: "EchoMem could not finish the local workspace scan. No backup data was substituted. Rerun setup to retry.",
|
|
1749
|
-
};
|
|
1750
|
-
}
|
|
1751
|
-
function publicRunningForensicProgress(value) {
|
|
1752
|
-
if (!value || typeof value !== "object")
|
|
1753
|
-
return null;
|
|
1754
|
-
const progress = value;
|
|
1755
|
-
if (progress.status !== "running")
|
|
1756
|
-
return null;
|
|
1757
|
-
const safeCount = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
|
|
1758
|
-
? Math.floor(candidate)
|
|
1759
|
-
: 0);
|
|
1760
|
-
const safeDuration = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
|
|
1761
|
-
? candidate
|
|
1762
|
-
: 0);
|
|
1763
|
-
const safeFraction = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate)
|
|
1764
|
-
? Math.min(1, Math.max(0, candidate))
|
|
1765
|
-
: 0);
|
|
1766
|
-
const total = safeCount(progress.total);
|
|
1767
|
-
const stageTotal = safeCount(progress.stageTotal);
|
|
1768
|
-
const rawStage = typeof progress.stage === "string" ? progress.stage : "starting";
|
|
1769
|
-
const stage = ["starting", "reading-transcripts", "building-summary", "classifying-repeated-context", "finalizing-report"].includes(rawStage)
|
|
1770
|
-
? rawStage
|
|
1771
|
-
: "starting";
|
|
1772
|
-
return {
|
|
1773
|
-
status: "running",
|
|
1774
|
-
scanned: total > 0 ? Math.min(safeCount(progress.scanned), total) : 0,
|
|
1775
|
-
total,
|
|
1776
|
-
stage,
|
|
1777
|
-
label: forensicStageLabel(stage),
|
|
1778
|
-
stageDone: stageTotal > 0 ? Math.min(safeCount(progress.stageDone), stageTotal) : 0,
|
|
1779
|
-
stageTotal,
|
|
1780
|
-
overall: safeFraction(progress.overall),
|
|
1781
|
-
elapsedMs: safeDuration(progress.elapsedMs),
|
|
1782
|
-
stageElapsedMs: safeDuration(progress.stageElapsedMs),
|
|
1783
|
-
updatedAt: safeDuration(progress.updatedAt) || Date.now(),
|
|
1784
|
-
};
|
|
1785
|
-
}
|
|
1786
1563
|
/**
|
|
1787
1564
|
* Start the persistent localhost bridge used by the setup page. It sends/verifies OTP through the
|
|
1788
1565
|
* hosted API, accepts the local passphrase, serves local Wrapped stats, and holds the /migrate
|
|
@@ -1795,12 +1572,11 @@ export function startCallbackServer(opts = {}) {
|
|
|
1795
1572
|
: `${Math.ceil(timeoutMs / 1000)} seconds`;
|
|
1796
1573
|
const dashboardTimeoutMs = 4 * 60 * 60 * 1000;
|
|
1797
1574
|
const expectedNonce = opts.nonce;
|
|
1798
|
-
const scanId = opts.scanId ?? randomUUID();
|
|
1799
1575
|
const flow = opts.flow ?? "onboarding";
|
|
1800
1576
|
const isLoginFlow = flow === "login";
|
|
1801
1577
|
// A login screen must not be blocked by a local-history permission. That permission belongs to
|
|
1802
1578
|
// onboarding and is intentionally enforced separately below.
|
|
1803
|
-
const
|
|
1579
|
+
const requiresLocalHistoryConsent = opts.requireLocalHistoryConsent === true && !isLoginFlow;
|
|
1804
1580
|
return new Promise((resolveOuter, rejectOuter) => {
|
|
1805
1581
|
const onToken = deferred();
|
|
1806
1582
|
const setupExit = deferred();
|
|
@@ -1813,7 +1589,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1813
1589
|
let activeDeviceToken = opts.initialToken?.token || "";
|
|
1814
1590
|
let activeAccountEmail = "";
|
|
1815
1591
|
let pendingLocalAuth = null;
|
|
1816
|
-
let
|
|
1592
|
+
let localHistoryConsentGranted = !requiresLocalHistoryConsent;
|
|
1817
1593
|
let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
1818
1594
|
let migrateStarted = false;
|
|
1819
1595
|
let tokenRefreshHandler = null;
|
|
@@ -1880,7 +1656,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1880
1656
|
const handleCallback = (res, token, key, nonce) => {
|
|
1881
1657
|
if (!checkNonce(nonce))
|
|
1882
1658
|
return void text(res, 403, "bad nonce");
|
|
1883
|
-
if (
|
|
1659
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
1884
1660
|
return void json(res, 403, {
|
|
1885
1661
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1886
1662
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1910,7 +1686,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1910
1686
|
text(res, 403, "bad nonce");
|
|
1911
1687
|
return true;
|
|
1912
1688
|
}
|
|
1913
|
-
if (
|
|
1689
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
1914
1690
|
json(res, 403, {
|
|
1915
1691
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1916
1692
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1937,8 +1713,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1937
1713
|
armTimeout();
|
|
1938
1714
|
};
|
|
1939
1715
|
const isOnboardingOnlyRoute = (route) => [
|
|
1940
|
-
"/
|
|
1941
|
-
"/report",
|
|
1716
|
+
"/local-history-consent",
|
|
1942
1717
|
"/stats",
|
|
1943
1718
|
"/billing-status",
|
|
1944
1719
|
"/billing-checkout",
|
|
@@ -2131,10 +1906,6 @@ export function startCallbackServer(opts = {}) {
|
|
|
2131
1906
|
message: "Run `echomem-mcp init` to access local-history onboarding.",
|
|
2132
1907
|
});
|
|
2133
1908
|
}
|
|
2134
|
-
if ((route === "/city" || route.startsWith("/city/")) && req.method === "GET") {
|
|
2135
|
-
serveRepoCityAsset(route, res);
|
|
2136
|
-
return;
|
|
2137
|
-
}
|
|
2138
1909
|
if (route.startsWith("/hud-assets/") && req.method === "GET") {
|
|
2139
1910
|
serveHudAsset(route, res);
|
|
2140
1911
|
return;
|
|
@@ -2172,8 +1943,13 @@ export function startCallbackServer(opts = {}) {
|
|
|
2172
1943
|
localOnly: true,
|
|
2173
1944
|
localAuth: true,
|
|
2174
1945
|
workspacePath: process.cwd(),
|
|
2175
|
-
consentRequired:
|
|
2176
|
-
consentGranted:
|
|
1946
|
+
consentRequired: requiresLocalHistoryConsent,
|
|
1947
|
+
consentGranted: localHistoryConsentGranted,
|
|
1948
|
+
platform: process.platform,
|
|
1949
|
+
capabilities: {
|
|
1950
|
+
openClaudeDesktop: process.platform === "darwin" || process.platform === "win32",
|
|
1951
|
+
openAgentSessions: process.platform === "darwin" || process.platform === "win32",
|
|
1952
|
+
},
|
|
2177
1953
|
});
|
|
2178
1954
|
return;
|
|
2179
1955
|
}
|
|
@@ -2277,7 +2053,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2277
2053
|
if (route === "/stats" && req.method === "GET") {
|
|
2278
2054
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2279
2055
|
return void text(res, 403, "bad nonce");
|
|
2280
|
-
if (
|
|
2056
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2281
2057
|
return void json(res, 403, {
|
|
2282
2058
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2283
2059
|
message: "Allow local history access before continuing setup.",
|
|
@@ -2293,7 +2069,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2293
2069
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
2294
2070
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2295
2071
|
return void text(res, 403, "bad nonce");
|
|
2296
|
-
if (
|
|
2072
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2297
2073
|
return void json(res, 403, {
|
|
2298
2074
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2299
2075
|
message: "Allow local history access before continuing setup.",
|
|
@@ -2417,7 +2193,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2417
2193
|
}
|
|
2418
2194
|
if (!checkNonce(asString(body.nonce)))
|
|
2419
2195
|
return void text(res, 403, "bad nonce");
|
|
2420
|
-
if (
|
|
2196
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2421
2197
|
return void json(res, 403, {
|
|
2422
2198
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2423
2199
|
message: "Allow local history access before managing an onboarding plan.",
|
|
@@ -2483,114 +2259,10 @@ export function startCallbackServer(opts = {}) {
|
|
|
2483
2259
|
}
|
|
2484
2260
|
return;
|
|
2485
2261
|
}
|
|
2486
|
-
if (route === "/report" && req.method === "GET") {
|
|
2487
|
-
// Local forensic "Context Doctor" report — computed locally, served BEFORE auth (scan-first).
|
|
2488
|
-
res.setHeader("Cache-Control", "no-store");
|
|
2489
|
-
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2490
|
-
return void text(res, 403, "bad nonce");
|
|
2491
|
-
if (requiresReportConsent && !reportConsentGranted) {
|
|
2492
|
-
return void json(res, 403, {
|
|
2493
|
-
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2494
|
-
message: "Allow local history access before starting the local scan.",
|
|
2495
|
-
});
|
|
2496
|
-
}
|
|
2497
|
-
let payload;
|
|
2498
|
-
try {
|
|
2499
|
-
payload = opts.getReport ? opts.getReport() : null;
|
|
2500
|
-
}
|
|
2501
|
-
catch {
|
|
2502
|
-
return void json(res, 500, {
|
|
2503
|
-
schemaVersion: 1,
|
|
2504
|
-
kind: "failed",
|
|
2505
|
-
mode: "production",
|
|
2506
|
-
scanId,
|
|
2507
|
-
error: {
|
|
2508
|
-
code: "REPORT_STATE_UNAVAILABLE",
|
|
2509
|
-
message: "EchoMem could not read the local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
2510
|
-
},
|
|
2511
|
-
});
|
|
2512
|
-
}
|
|
2513
|
-
if (payload == null) {
|
|
2514
|
-
// 202 carries scan progress so the page can show a live "scanned N/total" indicator.
|
|
2515
|
-
let prog;
|
|
2516
|
-
try {
|
|
2517
|
-
prog = opts.getReportProgress ? opts.getReportProgress() : {
|
|
2518
|
-
status: "running",
|
|
2519
|
-
scanned: 0,
|
|
2520
|
-
total: 0,
|
|
2521
|
-
stage: "starting",
|
|
2522
|
-
label: "Starting local scan",
|
|
2523
|
-
elapsedMs: 0,
|
|
2524
|
-
stageElapsedMs: 0,
|
|
2525
|
-
updatedAt: Date.now(),
|
|
2526
|
-
};
|
|
2527
|
-
}
|
|
2528
|
-
catch {
|
|
2529
|
-
return void json(res, 500, {
|
|
2530
|
-
schemaVersion: 1,
|
|
2531
|
-
kind: "failed",
|
|
2532
|
-
mode: "production",
|
|
2533
|
-
scanId,
|
|
2534
|
-
error: {
|
|
2535
|
-
code: "REPORT_STATE_UNAVAILABLE",
|
|
2536
|
-
message: "EchoMem could not read the local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
2537
|
-
},
|
|
2538
|
-
});
|
|
2539
|
-
}
|
|
2540
|
-
if (prog && typeof prog === "object" && prog.status === "failed") {
|
|
2541
|
-
return void json(res, 500, {
|
|
2542
|
-
schemaVersion: 1,
|
|
2543
|
-
kind: "failed",
|
|
2544
|
-
mode: "production",
|
|
2545
|
-
scanId,
|
|
2546
|
-
error: safeForensicError(prog.error),
|
|
2547
|
-
});
|
|
2548
|
-
}
|
|
2549
|
-
const publicProgress = publicRunningForensicProgress(prog);
|
|
2550
|
-
if (!publicProgress) {
|
|
2551
|
-
return void json(res, 500, {
|
|
2552
|
-
schemaVersion: 1,
|
|
2553
|
-
kind: "failed",
|
|
2554
|
-
mode: "production",
|
|
2555
|
-
scanId,
|
|
2556
|
-
error: {
|
|
2557
|
-
code: "REPORT_STATE_INVALID",
|
|
2558
|
-
message: "EchoMem received an invalid local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
2559
|
-
},
|
|
2560
|
-
});
|
|
2561
|
-
}
|
|
2562
|
-
return void json(res, 202, {
|
|
2563
|
-
schemaVersion: 1,
|
|
2564
|
-
kind: "scanning",
|
|
2565
|
-
mode: "production",
|
|
2566
|
-
scanId,
|
|
2567
|
-
progress: publicProgress,
|
|
2568
|
-
});
|
|
2569
|
-
}
|
|
2570
|
-
const validation = validateForensicReportForSetup(payload);
|
|
2571
|
-
if (!validation.ok) {
|
|
2572
|
-
console.error(`[echomem] local report validation failed: ${validation.code} — ${validation.message}`);
|
|
2573
|
-
return void json(res, 500, {
|
|
2574
|
-
schemaVersion: 1,
|
|
2575
|
-
kind: "failed",
|
|
2576
|
-
mode: "production",
|
|
2577
|
-
scanId,
|
|
2578
|
-
error: { code: validation.code, message: validation.message },
|
|
2579
|
-
});
|
|
2580
|
-
}
|
|
2581
|
-
json(res, 200, {
|
|
2582
|
-
schemaVersion: 1,
|
|
2583
|
-
kind: validation.kind,
|
|
2584
|
-
mode: "production",
|
|
2585
|
-
scanId,
|
|
2586
|
-
report: validation.report,
|
|
2587
|
-
});
|
|
2588
|
-
return;
|
|
2589
|
-
}
|
|
2590
2262
|
if (route === "/progress" && req.method === "GET") {
|
|
2591
2263
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2592
2264
|
return void text(res, 403, "bad nonce");
|
|
2593
|
-
if (
|
|
2265
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2594
2266
|
return void json(res, 403, {
|
|
2595
2267
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2596
2268
|
message: "Allow local history access before continuing setup.",
|
|
@@ -2599,7 +2271,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2599
2271
|
json(res, 200, progress);
|
|
2600
2272
|
return;
|
|
2601
2273
|
}
|
|
2602
|
-
if (route === "/
|
|
2274
|
+
if (route === "/local-history-consent" && req.method === "POST") {
|
|
2603
2275
|
let body;
|
|
2604
2276
|
try {
|
|
2605
2277
|
body = await readJsonBody(req);
|
|
@@ -2611,8 +2283,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2611
2283
|
if (!checkNonce(asString(body.nonce)))
|
|
2612
2284
|
return void text(res, 403, "bad nonce");
|
|
2613
2285
|
const allowed = body.allowed === true;
|
|
2614
|
-
|
|
2615
|
-
opts.onReportConsent?.(allowed);
|
|
2286
|
+
localHistoryConsentGranted = allowed;
|
|
2616
2287
|
json(res, 200, { ok: true, allowed });
|
|
2617
2288
|
return;
|
|
2618
2289
|
}
|
|
@@ -2671,7 +2342,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2671
2342
|
}
|
|
2672
2343
|
if (!checkNonce(asString(body.nonce)))
|
|
2673
2344
|
return void text(res, 403, "bad nonce");
|
|
2674
|
-
if (
|
|
2345
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2675
2346
|
return void json(res, 403, {
|
|
2676
2347
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2677
2348
|
message: "Allow local history access before starting extraction.",
|
|
@@ -2933,6 +2604,7 @@ async function cmdSetup(flags) {
|
|
|
2933
2604
|
// --dev is already an explicit request to replace the managed runtime with a checkout.
|
|
2934
2605
|
const forceHeadless = flags["force-headless"] === true || typeof flags.dev === "string";
|
|
2935
2606
|
const configurationFailures = [];
|
|
2607
|
+
const configuredTargets = [];
|
|
2936
2608
|
if (targets.length === 0) {
|
|
2937
2609
|
console.log("No client auto-detected. Add this MCP server entry manually:\n");
|
|
2938
2610
|
console.log(JSON.stringify({ echomem: entry }, null, 2));
|
|
@@ -2940,66 +2612,90 @@ async function cmdSetup(flags) {
|
|
|
2940
2612
|
}
|
|
2941
2613
|
else {
|
|
2942
2614
|
for (const c of targets) {
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
else {
|
|
2949
|
-
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath}`);
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
else if (c.kind === "command") {
|
|
2953
|
-
const result = writeCodexConfig(c.configPath, entry, { forceHeadless });
|
|
2954
|
-
if (result === "wrote")
|
|
2955
|
-
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath} — start a new Codex session to load it.`);
|
|
2956
|
-
else if (result === "desktop-managed")
|
|
2957
|
-
console.log(`✅ Kept the valid Echo Desktop-managed EchoMem entry for ${c.label}: ${c.configPath}`);
|
|
2958
|
-
else
|
|
2959
|
-
console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
|
|
2960
|
-
}
|
|
2961
|
-
else {
|
|
2962
|
-
const result = c.id === "claude-code"
|
|
2963
|
-
? writeClaudeCodeConfig(entry, { forceHeadless })
|
|
2964
|
-
: "unavailable";
|
|
2965
|
-
if (result !== "unavailable" && result.state === "wrote") {
|
|
2966
|
-
if (result.preservedDesktopManaged) {
|
|
2967
|
-
console.log(`✅ Kept the valid Echo Desktop-managed EchoMem user entry for ${c.label}.`);
|
|
2615
|
+
try {
|
|
2616
|
+
if (c.kind === "json") {
|
|
2617
|
+
const result = writeJsonClientConfig(c.configPath, entry, { forceHeadless });
|
|
2618
|
+
if (result === "desktop-managed") {
|
|
2619
|
+
console.log(`✅ Kept the valid externally managed EchoMem entry for ${c.label}: ${c.configPath}`);
|
|
2968
2620
|
}
|
|
2969
2621
|
else {
|
|
2970
|
-
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}
|
|
2971
|
-
}
|
|
2972
|
-
if (result.removedLocalProjects.length > 0) {
|
|
2973
|
-
console.log(`✅ Removed ${result.removedLocalProjects.length} stale Claude Code project-local EchoMem ${result.removedLocalProjects.length === 1 ? "entry" : "entries"}.`);
|
|
2974
|
-
}
|
|
2975
|
-
if (result.skippedLocalProjects.length > 0) {
|
|
2976
|
-
console.log(`ℹ️ Ignored ${result.skippedLocalProjects.length} EchoMem local ${result.skippedLocalProjects.length === 1 ? "entry" : "entries"} for deleted project directories; they cannot shadow the user entry.`);
|
|
2622
|
+
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath}`);
|
|
2977
2623
|
}
|
|
2624
|
+
configuredTargets.push(c);
|
|
2625
|
+
}
|
|
2626
|
+
else if (c.kind === "command") {
|
|
2627
|
+
const result = writeCodexConfig(c.configPath, entry, { forceHeadless });
|
|
2628
|
+
if (result === "wrote")
|
|
2629
|
+
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath} — start a new Codex session to load it.`);
|
|
2630
|
+
else if (result === "desktop-managed")
|
|
2631
|
+
console.log(`✅ Kept the valid externally managed EchoMem entry for ${c.label}: ${c.configPath}`);
|
|
2632
|
+
else
|
|
2633
|
+
console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
|
|
2634
|
+
configuredTargets.push(c);
|
|
2978
2635
|
}
|
|
2979
2636
|
else {
|
|
2980
|
-
const
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2637
|
+
const result = c.id === "claude-code"
|
|
2638
|
+
? writeClaudeCodeConfig(entry, { forceHeadless })
|
|
2639
|
+
: "unavailable";
|
|
2640
|
+
if (result !== "unavailable" && result.state === "wrote") {
|
|
2641
|
+
if (result.preservedDesktopManaged) {
|
|
2642
|
+
console.log(`✅ Kept the valid externally managed EchoMem user entry for ${c.label}.`);
|
|
2643
|
+
}
|
|
2644
|
+
else {
|
|
2645
|
+
console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
|
|
2646
|
+
}
|
|
2647
|
+
if (result.removedLocalProjects.length > 0) {
|
|
2648
|
+
console.log(`✅ Removed ${result.removedLocalProjects.length} stale Claude Code project-local EchoMem ${result.removedLocalProjects.length === 1 ? "entry" : "entries"}.`);
|
|
2649
|
+
}
|
|
2650
|
+
if (result.skippedLocalProjects.length > 0) {
|
|
2651
|
+
console.log(`ℹ️ Ignored ${result.skippedLocalProjects.length} EchoMem local ${result.skippedLocalProjects.length === 1 ? "entry" : "entries"} for deleted project directories; they cannot shadow the user entry.`);
|
|
2652
|
+
}
|
|
2653
|
+
configuredTargets.push(c);
|
|
2654
|
+
}
|
|
2655
|
+
else {
|
|
2656
|
+
const failedProjects = result === "unavailable" ? [] : result.failedLocalProjects;
|
|
2657
|
+
const reason = result === "unavailable" ? undefined : result.failureReason;
|
|
2658
|
+
if (result !== "unavailable" && result.state === "needs-repair")
|
|
2659
|
+
configuredTargets.push(c);
|
|
2660
|
+
configurationFailures.push({
|
|
2661
|
+
client: c,
|
|
2662
|
+
reason: failedProjects.length > 0
|
|
2663
|
+
? `${c.label} still has project-local EchoMem overrides in: ${failedProjects.join(", ")}`
|
|
2664
|
+
: `${c.label} user-scoped EchoMem entry could not be verified${reason ? `: ${reason}` : ""}`,
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2984
2667
|
}
|
|
2985
2668
|
}
|
|
2669
|
+
catch (error) {
|
|
2670
|
+
configurationFailures.push({
|
|
2671
|
+
client: c,
|
|
2672
|
+
reason: `${c.label} configuration was left unchanged: ${error instanceof Error ? error.message : String(error)}`,
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2986
2675
|
}
|
|
2987
2676
|
}
|
|
2988
2677
|
if (configurationFailures.length > 0) {
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2678
|
+
const explicitSingleClient = Boolean(requested && requested !== "all" && flags.all !== true);
|
|
2679
|
+
const failureMessage = [
|
|
2680
|
+
explicitSingleClient
|
|
2681
|
+
? "EchoMem could not configure the requested MCP client."
|
|
2682
|
+
: "EchoMem skipped MCP clients that were unavailable or could not be configured.",
|
|
2683
|
+
...configurationFailures.map((failure) => `- ${failure.reason}`),
|
|
2684
|
+
...configurationFailures.map((failure) => `Retry ${failure.client.label} later with: ${MCP_UPDATE_COMMAND} --client ${failure.client.id}`),
|
|
2685
|
+
].join("\n");
|
|
2686
|
+
if (explicitSingleClient)
|
|
2687
|
+
throw new Error(failureMessage);
|
|
2688
|
+
console.log(`⚠️ ${failureMessage}`);
|
|
2689
|
+
console.log("Continuing with the available clients; account connection and local-history import are not blocked.");
|
|
2994
2690
|
}
|
|
2995
2691
|
if (!flags["no-agents-md"]) {
|
|
2996
|
-
writeMemoryGuidanceForTargets(
|
|
2692
|
+
writeMemoryGuidanceForTargets(configuredTargets);
|
|
2997
2693
|
}
|
|
2998
2694
|
if (!flags["no-codex-skills"]) {
|
|
2999
|
-
writeCodexSkillsForTargets(
|
|
2695
|
+
writeCodexSkillsForTargets(configuredTargets);
|
|
3000
2696
|
}
|
|
3001
2697
|
if (flags["no-save-hooks"] !== true) {
|
|
3002
|
-
writeLifecycleHooksForTargets(
|
|
2698
|
+
writeLifecycleHooksForTargets(configuredTargets);
|
|
3003
2699
|
}
|
|
3004
2700
|
console.log("");
|
|
3005
2701
|
if (flags["skip-login"] || flags["no-login"]) {
|
|
@@ -3011,14 +2707,14 @@ async function cmdSetup(flags) {
|
|
|
3011
2707
|
await cmdLogin(flags);
|
|
3012
2708
|
}
|
|
3013
2709
|
if (flags["with-hud"]) {
|
|
3014
|
-
console.log("ℹ️ The standalone EchoMem HUD has been retired.
|
|
2710
|
+
console.log("ℹ️ The standalone EchoMem HUD has been retired. Use `echomem-mcp status` to inspect this installation.");
|
|
3015
2711
|
}
|
|
3016
2712
|
}
|
|
3017
2713
|
/**
|
|
3018
2714
|
* `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
|
|
3019
2715
|
* machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), installs EchoMem's
|
|
3020
2716
|
* Codex skills and writes the AGENTS.md memory guidance. One browser
|
|
3021
|
-
* bridge then runs permission →
|
|
2717
|
+
* bridge then runs permission → login → plan if needed → extraction in that order.
|
|
3022
2718
|
* `setup`/`login`/`update` remain granular primitives; init picks the full product defaults.
|
|
3023
2719
|
*/
|
|
3024
2720
|
async function cmdInit(flags) {
|
|
@@ -3032,7 +2728,7 @@ async function cmdInit(flags) {
|
|
|
3032
2728
|
"init-quiet": true,
|
|
3033
2729
|
"install-save-hooks": flags["no-save-hooks"] !== true,
|
|
3034
2730
|
});
|
|
3035
|
-
// 2. Start one ordered onboarding bridge. A fresh device logs in only after consent
|
|
2731
|
+
// 2. Start one ordered onboarding bridge. A fresh device logs in only after consent.
|
|
3036
2732
|
console.log("");
|
|
3037
2733
|
if (!flags["skip-login"] && !flags["no-login"] && !await cmdOnboarding(flags)) {
|
|
3038
2734
|
console.log("\nEchoMem is configured, but onboarding did not finish. Run `echomem-mcp init` again when you are ready.");
|
|
@@ -3040,8 +2736,8 @@ async function cmdInit(flags) {
|
|
|
3040
2736
|
}
|
|
3041
2737
|
console.log("");
|
|
3042
2738
|
console.log("🎉 EchoMem is ready.");
|
|
3043
|
-
console.log(" • MCP memory is configured for
|
|
3044
|
-
console.log(" •
|
|
2739
|
+
console.log(" • MCP memory is configured for the coding agents that connected successfully.");
|
|
2740
|
+
console.log(" • Use `echomem-mcp status` to inspect this device, and `login`, `unlock`, or `update` to manage it.");
|
|
3045
2741
|
console.log(' • Try it now: ask your agent — "search my EchoMem for what I\'ve been working on and recap it."');
|
|
3046
2742
|
}
|
|
3047
2743
|
/**
|
|
@@ -3157,7 +2853,7 @@ async function cmdLogin(flags) {
|
|
|
3157
2853
|
return true;
|
|
3158
2854
|
}
|
|
3159
2855
|
// Browser path: this bridge does only account/device authentication. It intentionally exposes
|
|
3160
|
-
// no local-history routes; `init` owns
|
|
2856
|
+
// no local-history routes; `init` owns local-history consent and optional extraction.
|
|
3161
2857
|
console.log("Opening your browser to connect this device locally…");
|
|
3162
2858
|
const { port, nonce } = localBridgeOptions(flags);
|
|
3163
2859
|
const srv = await startCallbackServer({ port, nonce, flow: "login" });
|
|
@@ -3185,7 +2881,7 @@ async function cmdLogin(flags) {
|
|
|
3185
2881
|
}
|
|
3186
2882
|
/**
|
|
3187
2883
|
* The local-history onboarding flow. Existing device credentials are reused when available; a
|
|
3188
|
-
* fresh device stays in this same bridge and asks for login only after permission
|
|
2884
|
+
* fresh device stays in this same bridge and asks for login only after permission.
|
|
3189
2885
|
*/
|
|
3190
2886
|
async function cmdOnboarding(flags) {
|
|
3191
2887
|
const store = new KeyStore();
|
|
@@ -3197,149 +2893,17 @@ async function cmdOnboarding(flags) {
|
|
|
3197
2893
|
console.log("Opening your browser for EchoMem onboarding…");
|
|
3198
2894
|
const { port, nonce } = localBridgeOptions(flags);
|
|
3199
2895
|
let stats = null;
|
|
3200
|
-
let forensicReport = null;
|
|
3201
|
-
let forensicConsent = "pending";
|
|
3202
|
-
let forensicScanStarted = false;
|
|
3203
|
-
const forensicStartedAt = Date.now();
|
|
3204
|
-
let forensicStageStartedAt = forensicStartedAt;
|
|
3205
|
-
let forensicStage = "starting";
|
|
3206
|
-
let forensicOverall = 0;
|
|
3207
|
-
let forensicProgress = {
|
|
3208
|
-
status: "running",
|
|
3209
|
-
scanned: 0,
|
|
3210
|
-
total: 0,
|
|
3211
|
-
stage: forensicStage,
|
|
3212
|
-
label: forensicStageLabel(forensicStage),
|
|
3213
|
-
stageDone: 0,
|
|
3214
|
-
stageTotal: 0,
|
|
3215
|
-
overall: 0,
|
|
3216
|
-
elapsedMs: 0,
|
|
3217
|
-
stageElapsedMs: 0,
|
|
3218
|
-
updatedAt: forensicStartedAt,
|
|
3219
|
-
};
|
|
3220
2896
|
const srv = await startCallbackServer({
|
|
3221
2897
|
port,
|
|
3222
2898
|
nonce,
|
|
3223
2899
|
flow: "onboarding",
|
|
3224
2900
|
initialToken,
|
|
3225
|
-
|
|
3226
|
-
getStats: () => stats,
|
|
3227
|
-
getReport: () => forensicReport,
|
|
3228
|
-
getReportProgress: () => {
|
|
3229
|
-
if (forensicProgress.status !== "running")
|
|
3230
|
-
return forensicProgress;
|
|
3231
|
-
const now = Date.now();
|
|
3232
|
-
const sinceWorkerUpdate = Math.max(0, now - forensicProgress.updatedAt);
|
|
3233
|
-
return {
|
|
3234
|
-
...forensicProgress,
|
|
3235
|
-
elapsedMs: forensicProgress.elapsedMs + sinceWorkerUpdate,
|
|
3236
|
-
stageElapsedMs: forensicProgress.stageElapsedMs + sinceWorkerUpdate,
|
|
3237
|
-
updatedAt: now,
|
|
3238
|
-
};
|
|
3239
|
-
},
|
|
3240
|
-
onReportConsent: (allowed) => {
|
|
3241
|
-
if (!allowed) {
|
|
3242
|
-
forensicConsent = "declined";
|
|
3243
|
-
forensicProgress = {
|
|
3244
|
-
status: "failed",
|
|
3245
|
-
scanned: 0,
|
|
3246
|
-
total: 0,
|
|
3247
|
-
stage: "failed",
|
|
3248
|
-
label: "Local scan skipped",
|
|
3249
|
-
stageDone: 0,
|
|
3250
|
-
stageTotal: 0,
|
|
3251
|
-
overall: 0,
|
|
3252
|
-
elapsedMs: Date.now() - forensicStartedAt,
|
|
3253
|
-
stageElapsedMs: Date.now() - forensicStageStartedAt,
|
|
3254
|
-
updatedAt: Date.now(),
|
|
3255
|
-
error: {
|
|
3256
|
-
code: "REPORT_SCAN_DECLINED",
|
|
3257
|
-
message: "Local file analysis was skipped. EchoMem can still connect, save conversations, and import memories.",
|
|
3258
|
-
},
|
|
3259
|
-
};
|
|
3260
|
-
return;
|
|
3261
|
-
}
|
|
3262
|
-
forensicConsent = "allowed";
|
|
3263
|
-
const now = Date.now();
|
|
3264
|
-
forensicStage = "starting";
|
|
3265
|
-
forensicStageStartedAt = now;
|
|
3266
|
-
forensicOverall = 0;
|
|
3267
|
-
forensicProgress = {
|
|
3268
|
-
status: "running",
|
|
3269
|
-
scanned: 0,
|
|
3270
|
-
total: 0,
|
|
3271
|
-
stage: forensicStage,
|
|
3272
|
-
label: forensicStageLabel(forensicStage),
|
|
3273
|
-
stageDone: 0,
|
|
3274
|
-
stageTotal: 0,
|
|
3275
|
-
overall: 0,
|
|
3276
|
-
elapsedMs: now - forensicStartedAt,
|
|
3277
|
-
stageElapsedMs: 0,
|
|
3278
|
-
updatedAt: now,
|
|
3279
|
-
};
|
|
3280
|
-
startForensicScan();
|
|
3281
|
-
},
|
|
2901
|
+
requireLocalHistoryConsent: true,
|
|
3282
2902
|
});
|
|
3283
2903
|
const localSetupUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
|
|
3284
2904
|
openBrowser(localSetupUrl);
|
|
3285
2905
|
console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
|
|
3286
2906
|
console.log("Waiting for local-history onboarding for up to 15 minutes…");
|
|
3287
|
-
const startForensicScan = () => {
|
|
3288
|
-
if (forensicScanStarted || forensicConsent !== "allowed")
|
|
3289
|
-
return;
|
|
3290
|
-
forensicScanStarted = true;
|
|
3291
|
-
// Build the local forensic "Context Doctor" report off-thread only after explicit consent.
|
|
3292
|
-
buildForensicReportOffThread((progress) => {
|
|
3293
|
-
const now = Date.now();
|
|
3294
|
-
const nextStage = progress.stage || forensicStage;
|
|
3295
|
-
if (nextStage !== forensicStage) {
|
|
3296
|
-
forensicStage = nextStage;
|
|
3297
|
-
forensicStageStartedAt = now;
|
|
3298
|
-
console.log(`Local scan: ${forensicStageLabel(forensicStage)}…`);
|
|
3299
|
-
}
|
|
3300
|
-
// Latched, so a caller that ever reports a smaller fraction cannot walk the bar backwards.
|
|
3301
|
-
forensicOverall = Math.max(forensicOverall, typeof progress.overall === "number" && Number.isFinite(progress.overall) ? progress.overall : 0);
|
|
3302
|
-
forensicProgress = {
|
|
3303
|
-
status: "running",
|
|
3304
|
-
scanned: progress.done,
|
|
3305
|
-
total: progress.total,
|
|
3306
|
-
stage: forensicStage,
|
|
3307
|
-
label: forensicStageLabel(forensicStage),
|
|
3308
|
-
detail: progress.detail,
|
|
3309
|
-
stageDone: typeof progress.stageDone === "number" && Number.isFinite(progress.stageDone)
|
|
3310
|
-
? Math.max(0, Math.floor(progress.stageDone))
|
|
3311
|
-
: 0,
|
|
3312
|
-
stageTotal: typeof progress.stageTotal === "number" && Number.isFinite(progress.stageTotal)
|
|
3313
|
-
? Math.max(0, Math.floor(progress.stageTotal))
|
|
3314
|
-
: 0,
|
|
3315
|
-
overall: forensicOverall,
|
|
3316
|
-
elapsedMs: now - forensicStartedAt,
|
|
3317
|
-
stageElapsedMs: now - forensicStageStartedAt,
|
|
3318
|
-
updatedAt: now,
|
|
3319
|
-
};
|
|
3320
|
-
})
|
|
3321
|
-
.then((r) => {
|
|
3322
|
-
forensicReport = r;
|
|
3323
|
-
})
|
|
3324
|
-
.catch((e) => {
|
|
3325
|
-
const now = Date.now();
|
|
3326
|
-
forensicProgress = {
|
|
3327
|
-
status: "failed",
|
|
3328
|
-
scanned: forensicProgress.scanned,
|
|
3329
|
-
total: forensicProgress.total,
|
|
3330
|
-
stage: "failed",
|
|
3331
|
-
label: "Local scan failed",
|
|
3332
|
-
stageDone: forensicProgress.stageDone,
|
|
3333
|
-
stageTotal: forensicProgress.stageTotal,
|
|
3334
|
-
overall: forensicOverall,
|
|
3335
|
-
elapsedMs: now - forensicStartedAt,
|
|
3336
|
-
stageElapsedMs: now - forensicStageStartedAt,
|
|
3337
|
-
updatedAt: now,
|
|
3338
|
-
error: safeForensicError(e),
|
|
3339
|
-
};
|
|
3340
|
-
console.error(`Could not build the local report: ${e instanceof Error ? e.message : String(e)}`);
|
|
3341
|
-
});
|
|
3342
|
-
};
|
|
3343
2907
|
let token;
|
|
3344
2908
|
let key;
|
|
3345
2909
|
try {
|
|
@@ -3430,9 +2994,8 @@ async function cmdOnboarding(flags) {
|
|
|
3430
2994
|
claudeCode: quick.claudeCodeCount,
|
|
3431
2995
|
cowork: quick.coworkCount,
|
|
3432
2996
|
};
|
|
3433
|
-
stats =
|
|
2997
|
+
stats = buildOnboardingStatsPayload({
|
|
3434
2998
|
partial: true,
|
|
3435
|
-
skipMemoryCount: true,
|
|
3436
2999
|
sessions: sessionSummary,
|
|
3437
3000
|
migratable,
|
|
3438
3001
|
discovery: { phase: "quick", exact: false },
|
|
@@ -3467,9 +3030,8 @@ async function cmdOnboarding(flags) {
|
|
|
3467
3030
|
claudeCode: cloudSummary.claudeCodeCount,
|
|
3468
3031
|
cowork: cloudSummary.coworkCount,
|
|
3469
3032
|
};
|
|
3470
|
-
const cloudPayload =
|
|
3033
|
+
const cloudPayload = buildOnboardingStatsPayload({
|
|
3471
3034
|
partial: true,
|
|
3472
|
-
skipMemoryCount: true,
|
|
3473
3035
|
sessions: sessionSummary,
|
|
3474
3036
|
migratable,
|
|
3475
3037
|
discovery: { phase: "account", exact: false },
|
|
@@ -3505,9 +3067,8 @@ async function cmdOnboarding(flags) {
|
|
|
3505
3067
|
claudeCode: unavailableSummary.claudeCodeCount,
|
|
3506
3068
|
cowork: unavailableSummary.coworkCount,
|
|
3507
3069
|
};
|
|
3508
|
-
const unavailablePayload =
|
|
3070
|
+
const unavailablePayload = buildOnboardingStatsPayload({
|
|
3509
3071
|
partial: true,
|
|
3510
|
-
skipMemoryCount: true,
|
|
3511
3072
|
sessions: sessionSummary,
|
|
3512
3073
|
migratable,
|
|
3513
3074
|
discovery: { phase: "account", exact: false },
|
|
@@ -3543,9 +3104,8 @@ async function cmdOnboarding(flags) {
|
|
|
3543
3104
|
migratable = migratableFromDiscovery(initialExact);
|
|
3544
3105
|
latestPendingEstimate = migratable.pending;
|
|
3545
3106
|
sessionSummary = sessionsFromDiscovery(initialExact);
|
|
3546
|
-
const partialPayload = withCandidateSessions(
|
|
3107
|
+
const partialPayload = withCandidateSessions(buildOnboardingStatsPayload({
|
|
3547
3108
|
partial: true,
|
|
3548
|
-
skipMemoryCount: true,
|
|
3549
3109
|
sessions: sessionSummary,
|
|
3550
3110
|
migratable,
|
|
3551
3111
|
discovery: { phase: "exact", exact: true },
|
|
@@ -3593,12 +3153,10 @@ async function cmdOnboarding(flags) {
|
|
|
3593
3153
|
migratable = migratableFromDiscovery(reconciled);
|
|
3594
3154
|
latestPendingEstimate = migratable.pending;
|
|
3595
3155
|
sessionSummary = sessionsFromDiscovery(reconciled);
|
|
3596
|
-
const reconciledPayload = withCandidateSessions(
|
|
3597
|
-
partial: true,
|
|
3598
|
-
skipMemoryCount: true,
|
|
3156
|
+
const reconciledPayload = withCandidateSessions(buildOnboardingStatsPayload({
|
|
3599
3157
|
sessions: sessionSummary,
|
|
3600
3158
|
migratable,
|
|
3601
|
-
discovery: { phase: "
|
|
3159
|
+
discovery: { phase: "full", exact: true },
|
|
3602
3160
|
}), reconciled);
|
|
3603
3161
|
if (generation !== refreshGeneration)
|
|
3604
3162
|
return;
|
|
@@ -3613,20 +3171,11 @@ async function cmdOnboarding(flags) {
|
|
|
3613
3171
|
failed: 0,
|
|
3614
3172
|
extracted: 0,
|
|
3615
3173
|
});
|
|
3616
|
-
const fullPayload = withCandidateSessions(await buildCollectedStatsPayloadOffThread({
|
|
3617
|
-
sessions: sessionSummary,
|
|
3618
|
-
migratable,
|
|
3619
|
-
discovery: { phase: "full", exact: true },
|
|
3620
|
-
}), reconciled);
|
|
3621
|
-
if (generation !== refreshGeneration)
|
|
3622
|
-
return;
|
|
3623
|
-
stats = fullPayload;
|
|
3624
|
-
srv.setStats(fullPayload);
|
|
3625
3174
|
})().catch((e) => {
|
|
3626
3175
|
if (generation !== refreshGeneration)
|
|
3627
3176
|
return;
|
|
3628
|
-
console.error(`[echomem] optional
|
|
3629
|
-
publishOptionalStatsFallback(generation, "
|
|
3177
|
+
console.error(`[echomem] optional account reconciliation unavailable; continuing (${errorCode(e) || "ACCOUNT_RECONCILIATION_FAILED"})`);
|
|
3178
|
+
publishOptionalStatsFallback(generation, "ACCOUNT_RECONCILIATION_FAILED", true);
|
|
3630
3179
|
});
|
|
3631
3180
|
return initialExact;
|
|
3632
3181
|
}).catch((e) => {
|
|
@@ -4086,7 +3635,7 @@ Usage:
|
|
|
4086
3635
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
4087
3636
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
|
|
4088
3637
|
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
4089
|
-
echomem-mcp setup --force-headless Explicitly replace valid
|
|
3638
|
+
echomem-mcp setup --force-headless Explicitly replace a valid externally managed entry
|
|
4090
3639
|
echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
|
|
4091
3640
|
echomem-mcp update --all Install this bridge durably + repoint detected clients; no login/browser
|
|
4092
3641
|
echomem-mcp update --client X Repoint one MCP client; no login/browser
|
|
@@ -4097,7 +3646,6 @@ Usage:
|
|
|
4097
3646
|
echomem-mcp status Show token/key/clients
|
|
4098
3647
|
echomem-mcp doctor [--no-network] Diagnose configured client bridge versions
|
|
4099
3648
|
echomem-mcp logout Remove stored credentials
|
|
4100
|
-
echomem-mcp report [--json] Your AI coding memory audit (local, no login, $0)
|
|
4101
3649
|
echomem-mcp migrate [--since D] Import your existing Codex/Claude history into your memory
|
|
4102
3650
|
echomem-mcp migrate --estimate Estimate migration size/time metadata without uploading transcripts
|
|
4103
3651
|
echomem-mcp migrate --max-chars N Import only sessions up to N assembled text chars
|
|
@@ -4149,9 +3697,6 @@ export async function runCli(argv) {
|
|
|
4149
3697
|
case "logout":
|
|
4150
3698
|
cmdLogout();
|
|
4151
3699
|
return true;
|
|
4152
|
-
case "report":
|
|
4153
|
-
await runReport(flags);
|
|
4154
|
-
return true;
|
|
4155
3700
|
case "migrate":
|
|
4156
3701
|
await cmdMigrate(flags);
|
|
4157
3702
|
return true;
|