@echomem/mcp 1.4.8 → 1.4.10
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 +23 -3
- package/assets/canonical-scorer/README.md +18 -0
- package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
- package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
- package/assets/canonical-scorer/golden_anchors.mjs +83 -0
- package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
- package/assets/hud/github.svg +1 -0
- package/dist/city/README.md +9 -0
- package/dist/city/echo-ai-city-only.html +1067 -107
- package/dist/codex-session-files.js +283 -0
- package/dist/codex-sync.js +7 -2
- package/dist/context-analysis/canonical-golden.js +47 -0
- package/dist/context-analysis/claude-canonical-adapter.js +315 -0
- package/dist/context-analysis/claude-native-canonical.js +1216 -0
- package/dist/context-analysis/vendored-canonical.js +793 -0
- package/dist/context-analysis/workspace-report.js +1838 -0
- package/dist/context-metrics/calculate.js +43 -0
- package/dist/context-metrics/estimator.js +45 -0
- package/dist/context-metrics/ledger.js +507 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/parse-claude.js +227 -0
- package/dist/context-metrics/parse-codex.js +276 -0
- package/dist/context-metrics/types.js +1 -0
- package/dist/forensics-10-problems.js +7 -6
- package/dist/forensics.js +863 -132
- package/dist/hud/adapters.js +77 -202
- package/dist/hud/cli.js +0 -0
- package/dist/hud/efficiency.js +447 -0
- package/dist/hud/electron-main.js +3 -2
- package/dist/hud/fs.js +14 -0
- package/dist/hud/metric.js +17 -102
- package/dist/hud/monitor.js +136 -28
- package/dist/hud/render.js +4 -3
- package/dist/hud/server.js +30 -0
- package/dist/hud/web.js +622 -353
- package/dist/index.js +7 -3
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +37 -29
- package/dist/report.js +101 -40
- package/dist/setup-page/client-core.js +475 -0
- package/dist/setup-page/client-extraction.js +550 -0
- package/dist/setup-page/client-lifecycle.js +116 -0
- package/dist/setup-page/client-report-audit.js +818 -0
- package/dist/setup-page/client-report-city.js +204 -0
- package/dist/setup-page/client-report.js +6 -0
- package/dist/setup-page/client.js +15 -0
- package/dist/setup-page/document.js +37 -0
- package/dist/setup-page/styles-city-report.js +880 -0
- package/dist/setup-page/styles-context-audit.js +470 -0
- package/dist/setup-page/styles-extraction.js +821 -0
- package/dist/setup-page/styles-foundation.js +231 -0
- package/dist/setup-page/styles.js +11 -0
- package/dist/setup-page.js +15 -2538
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +456 -44
- package/package.json +8 -7
- package/templates/echomem-recall.md +2 -2
- package/dist/city/10-problems-report.html +0 -649
- package/dist/city/_live.html +0 -37
- package/dist/city/_serve.mjs +0 -45
- package/dist/city/card-data.json +0 -15
- package/dist/city/city-data.json +0 -248
- package/dist/city/echo-ai-city-only.template.html +0 -1272
- package/dist/city/generate-echo-city-only.mjs +0 -112
package/dist/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `echomem-mcp setup | login | unlock | status | logout` — onboarding for the local bridge (spec §8).
|
|
2
|
+
* `echomem-mcp init | setup | login | unlock | status | logout` — onboarding for the local bridge (spec §8).
|
|
3
3
|
*
|
|
4
4
|
* Design goals from the spec:
|
|
5
5
|
* - One command → one browser approval → one reload.
|
|
@@ -20,7 +20,7 @@ import fs from "node:fs";
|
|
|
20
20
|
import os from "node:os";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import readline from "node:readline";
|
|
23
|
-
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
24
|
import axios from "axios";
|
|
25
25
|
import { KeyStore } from "./keystore.js";
|
|
26
26
|
import { fetchEncryptionConfig, deriveAndVerifyKey, verifyKeyB64 } from "./encryption.js";
|
|
@@ -28,7 +28,8 @@ import { collect, runReport, buildStatsPayload } from "./report.js";
|
|
|
28
28
|
import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, discoverMigratableFastDiscovery, discoverMigratableSessions, discoverPendingSessionsTargeted, estimateMigrationEta, fetchProcessedImportKeys, isImportStatusUnsupported, markAccountImportStatusFailed, markAccountImportStatusUnavailable, markFastAccountImportStatusUnavailable, startMigration, summarizeFastMigratableDiscovery, MIGRATE_CONCURRENCY, } from "./migrate.js";
|
|
29
29
|
import { syncCodexUsage } from "./codex-sync.js";
|
|
30
30
|
import { renderSetupPage } from "./setup-page.js";
|
|
31
|
-
import {
|
|
31
|
+
import { parseSetupPreviewState } from "./setup-preview.js";
|
|
32
|
+
import { repoLabel, validateForensicReportForSetup } from "./forensics.js";
|
|
32
33
|
import { installHooks } from "./hud/hooks.js";
|
|
33
34
|
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
34
35
|
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
@@ -40,6 +41,50 @@ const API_BASE_URL = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.
|
|
|
40
41
|
function home(...p) {
|
|
41
42
|
return path.join(os.homedir(), ...p);
|
|
42
43
|
}
|
|
44
|
+
/** Map a source entry to its compiled sibling without ever guessing outside this package. */
|
|
45
|
+
export function compiledDistPathForSource(entry) {
|
|
46
|
+
if (!entry.endsWith(".ts") || !path.isAbsolute(entry))
|
|
47
|
+
return null;
|
|
48
|
+
const normalized = path.normalize(entry);
|
|
49
|
+
const srcSegment = `${path.sep}src${path.sep}`;
|
|
50
|
+
const srcIndex = normalized.lastIndexOf(srcSegment);
|
|
51
|
+
if (srcIndex < 0)
|
|
52
|
+
return null;
|
|
53
|
+
const packageRoot = normalized.slice(0, srcIndex) || path.parse(normalized).root;
|
|
54
|
+
const relativeSource = normalized.slice(srcIndex + srcSegment.length);
|
|
55
|
+
const candidate = path.join(packageRoot, "dist", relativeSource.replace(/\.ts$/, ".js"));
|
|
56
|
+
try {
|
|
57
|
+
const real = fs.realpathSync(candidate);
|
|
58
|
+
if (!fs.statSync(real).isFile())
|
|
59
|
+
return null;
|
|
60
|
+
fs.accessSync(real, fs.constants.R_OK);
|
|
61
|
+
return real;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function runtimeModuleUrl(name) {
|
|
68
|
+
const jsUrl = new URL(`./${name}.js`, import.meta.url);
|
|
69
|
+
if (jsUrl.protocol !== "file:")
|
|
70
|
+
return jsUrl.href;
|
|
71
|
+
const jsPath = fileURLToPath(jsUrl);
|
|
72
|
+
try {
|
|
73
|
+
if (fs.statSync(jsPath).isFile())
|
|
74
|
+
return jsUrl.href;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* Source-mode runs do not have an adjacent .js file. */
|
|
78
|
+
}
|
|
79
|
+
const tsPath = fileURLToPath(new URL(`./${name}.ts`, import.meta.url));
|
|
80
|
+
const compiled = compiledDistPathForSource(tsPath);
|
|
81
|
+
if (compiled)
|
|
82
|
+
return pathToFileURL(compiled).href;
|
|
83
|
+
if (fs.existsSync(tsPath)) {
|
|
84
|
+
throw new Error(`The local ${name} worker is not built. Run npm --prefix packages/mcp-server run build and retry.`);
|
|
85
|
+
}
|
|
86
|
+
return jsUrl.href;
|
|
87
|
+
}
|
|
43
88
|
/** Known clients and where their MCP server map lives. */
|
|
44
89
|
export function knownClients() {
|
|
45
90
|
const appSupport = process.platform === "darwin"
|
|
@@ -81,9 +126,15 @@ export function buildServerEntry(opts = {}) {
|
|
|
81
126
|
// Trade-off: the node path is version-specific under nvm — re-run `setup` after a Node upgrade.
|
|
82
127
|
try {
|
|
83
128
|
const entry = fs.realpathSync(process.argv[1] || "");
|
|
84
|
-
if (entry && fs.existsSync(entry)) {
|
|
85
|
-
return { command: process.execPath, args: [entry] };
|
|
129
|
+
if (entry && fs.existsSync(entry) && !isEphemeralNpxPath(entry)) {
|
|
130
|
+
return { command: process.execPath, args: [compiledDistPathForSource(entry) ?? entry] };
|
|
86
131
|
}
|
|
132
|
+
// We're running from an EPHEMERAL npx cache (`npx @echomem/mcp …` with no global install). npx GCs
|
|
133
|
+
// those `_npx/<hash>` dirs, so pinning a client to this path works until the next cleanup, then the
|
|
134
|
+
// MCP server silently vanishes. Never write it — prefer a durable global install of the same package.
|
|
135
|
+
const globalEntry = resolveGlobalEntry();
|
|
136
|
+
if (globalEntry)
|
|
137
|
+
return { command: process.execPath, args: [globalEntry] };
|
|
87
138
|
}
|
|
88
139
|
catch {
|
|
89
140
|
/* couldn't resolve a local install — fall through to npx */
|
|
@@ -91,6 +142,33 @@ export function buildServerEntry(opts = {}) {
|
|
|
91
142
|
// Fallback (unresolved local install): at least drop `-y` so npx doesn't auto-INSTALL on every start.
|
|
92
143
|
return { command: "npx", args: ["@echomem/mcp"] };
|
|
93
144
|
}
|
|
145
|
+
/** True when a resolved entry lives inside npx's throwaway cache (`…/_npx/<hash>/…`). */
|
|
146
|
+
function isEphemeralNpxPath(entry) {
|
|
147
|
+
return entry.split(path.sep).includes("_npx");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Locate a DURABLE global install of the bridge (the one `npm i -g @echomem/mcp` creates). Global
|
|
151
|
+
* modules sit next to the running node — `<node>/../lib/node_modules` (nvm/unix) or `<node>/node_modules`
|
|
152
|
+
* (Windows). Returns the realpath'd dist entry, or null when the package isn't globally installed.
|
|
153
|
+
*/
|
|
154
|
+
function resolveGlobalEntry() {
|
|
155
|
+
const nodeDir = path.dirname(process.execPath);
|
|
156
|
+
const pkgParts = MCP_PACKAGE_NAME.split("/"); // ["@echomem", "mcp"]
|
|
157
|
+
const candidates = [
|
|
158
|
+
path.join(nodeDir, "..", "lib", "node_modules", ...pkgParts, "dist", "index.js"),
|
|
159
|
+
path.join(nodeDir, "node_modules", ...pkgParts, "dist", "index.js"),
|
|
160
|
+
];
|
|
161
|
+
for (const candidate of candidates) {
|
|
162
|
+
try {
|
|
163
|
+
if (fs.existsSync(candidate))
|
|
164
|
+
return fs.realpathSync(candidate);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* keep trying */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
94
172
|
/** The TOML block EchoMem adds to ~/.codex/config.toml. No secret — the bridge reads the keystore. */
|
|
95
173
|
export function codexTomlBlock(entry) {
|
|
96
174
|
const command = JSON.stringify(String(entry.command));
|
|
@@ -484,6 +562,63 @@ function openClaudeDesktop() {
|
|
|
484
562
|
detail: failures.join(" | "),
|
|
485
563
|
};
|
|
486
564
|
}
|
|
565
|
+
const LOCAL_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
566
|
+
// `claude --resume <id>` only finds sessions that belong to the current project directory, so
|
|
567
|
+
// recover the session's original cwd from its transcript before resuming.
|
|
568
|
+
function claudeSessionCwd(sessionId) {
|
|
569
|
+
try {
|
|
570
|
+
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
571
|
+
for (const dir of fs.readdirSync(projectsDir)) {
|
|
572
|
+
const file = path.join(projectsDir, dir, `${sessionId}.jsonl`);
|
|
573
|
+
if (!fs.existsSync(file))
|
|
574
|
+
continue;
|
|
575
|
+
const fd = fs.openSync(file, "r");
|
|
576
|
+
try {
|
|
577
|
+
const head = Buffer.alloc(65536);
|
|
578
|
+
const read = fs.readSync(fd, head, 0, head.length, 0);
|
|
579
|
+
const match = head.toString("utf8", 0, read).match(/"cwd"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
580
|
+
return match ? JSON.parse(`"${match[1]}"`) : null;
|
|
581
|
+
}
|
|
582
|
+
finally {
|
|
583
|
+
fs.closeSync(fd);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
/* fall through to a plain resume */
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
function shellQuote(value) {
|
|
593
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
594
|
+
}
|
|
595
|
+
function openExistingAgentSession(source, sessionId) {
|
|
596
|
+
if (process.platform !== "darwin")
|
|
597
|
+
return { ok: false, message: "Opening local agent sessions is currently available on macOS." };
|
|
598
|
+
if (!LOCAL_SESSION_ID_RE.test(sessionId))
|
|
599
|
+
return { ok: false, message: "The local session identifier is invalid." };
|
|
600
|
+
try {
|
|
601
|
+
if (source === "codex") {
|
|
602
|
+
execFileSync("open", [`codex://threads/${sessionId}`], { stdio: "pipe" });
|
|
603
|
+
return { ok: true, message: "Opened the original session in Codex." };
|
|
604
|
+
}
|
|
605
|
+
if (source === "claude-code") {
|
|
606
|
+
const claude = firstExisting(["/opt/homebrew/bin/claude", "/usr/local/bin/claude"]) || "claude";
|
|
607
|
+
const cwd = claudeSessionCwd(sessionId);
|
|
608
|
+
const resume = `${claude} --resume ${sessionId}`;
|
|
609
|
+
const command = cwd && fs.existsSync(cwd) ? `cd ${shellQuote(cwd)} && ${resume}` : resume;
|
|
610
|
+
execFileSync("osascript", [
|
|
611
|
+
"-e", "tell application \"Terminal\" to activate",
|
|
612
|
+
"-e", `tell application \"Terminal\" to do script ${JSON.stringify(command)}`,
|
|
613
|
+
], { stdio: "pipe" });
|
|
614
|
+
return { ok: true, message: "Opened the original Claude Code session in Terminal." };
|
|
615
|
+
}
|
|
616
|
+
return { ok: false, message: "Unsupported agent session source." };
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
return { ok: false, message: "Could not open the original local session.", detail: commandFailureMessage(error) };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
487
622
|
function commandFailureMessage(error) {
|
|
488
623
|
const maybe = error;
|
|
489
624
|
if (Buffer.isBuffer(maybe.stderr)) {
|
|
@@ -630,6 +765,12 @@ function repoCityArtifactsRoot() {
|
|
|
630
765
|
function serveRepoCityAsset(reqPath, res) {
|
|
631
766
|
const root = repoCityArtifactsRoot();
|
|
632
767
|
const rel = reqPath === "/city" || reqPath === "/city/" ? "echo-ai-city-only.html" : decodeURIComponent(reqPath.slice("/city/".length));
|
|
768
|
+
// Archives stay in the checkout for recovery, but must never become a localhost UI surface.
|
|
769
|
+
const normalizedRel = rel.replace(/\\/g, "/");
|
|
770
|
+
if (normalizedRel === "archive" || normalizedRel.startsWith("archive/")) {
|
|
771
|
+
res.writeHead(404).end("not found");
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
633
774
|
const filePath = path.resolve(root, rel);
|
|
634
775
|
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
635
776
|
if (!filePath.startsWith(rootWithSep)) {
|
|
@@ -749,7 +890,7 @@ function serveAgentIcon(reqPath, res) {
|
|
|
749
890
|
return true;
|
|
750
891
|
}
|
|
751
892
|
function discoverMigratableSessionsOffThread() {
|
|
752
|
-
const migrateUrl =
|
|
893
|
+
const migrateUrl = runtimeModuleUrl("migrate");
|
|
753
894
|
const code = `
|
|
754
895
|
import { parentPort } from "node:worker_threads";
|
|
755
896
|
import { discoverMigratableSessions } from ${JSON.stringify(migrateUrl)};
|
|
@@ -795,15 +936,29 @@ function discoverMigratableSessionsOffThread() {
|
|
|
795
936
|
});
|
|
796
937
|
});
|
|
797
938
|
}
|
|
939
|
+
function forensicStageLabel(stage) {
|
|
940
|
+
if (stage === "reading-transcripts")
|
|
941
|
+
return "Reading transcript files";
|
|
942
|
+
if (stage === "building-summary")
|
|
943
|
+
return "Building scan summary";
|
|
944
|
+
if (stage === "classifying-repeated-context")
|
|
945
|
+
return "Classifying repeated context";
|
|
946
|
+
if (stage === "finalizing-report")
|
|
947
|
+
return "Finalizing report";
|
|
948
|
+
return "Starting local scan";
|
|
949
|
+
}
|
|
798
950
|
/** Build the local forensic "Context Doctor" report on a worker thread so the multi-file scan never
|
|
799
951
|
* blocks the bridge's event loop (the freeze postmortem: any unbounded sync work on this path is risky). */
|
|
800
|
-
function buildForensicReportOffThread(onProgress) {
|
|
801
|
-
const forensicsUrl =
|
|
952
|
+
export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
953
|
+
const forensicsUrl = runtimeModuleUrl("forensics");
|
|
802
954
|
const code = `
|
|
803
955
|
import { parentPort } from "node:worker_threads";
|
|
804
956
|
import { buildForensicReport } from ${JSON.stringify(forensicsUrl)};
|
|
805
957
|
try {
|
|
806
|
-
const report = buildForensicReport({
|
|
958
|
+
const report = await buildForensicReport({
|
|
959
|
+
includeLegacyGoldenStandard: false,
|
|
960
|
+
onProgress: (done, total, stage, detail) => parentPort?.postMessage({ progress: { done, total, stage, detail } }),
|
|
961
|
+
});
|
|
807
962
|
parentPort?.postMessage({ ok: true, report });
|
|
808
963
|
} catch (error) {
|
|
809
964
|
parentPort?.postMessage({ ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
@@ -812,30 +967,50 @@ function buildForensicReportOffThread(onProgress) {
|
|
|
812
967
|
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
813
968
|
return new Promise((resolve, reject) => {
|
|
814
969
|
let settled = false;
|
|
970
|
+
const requestedTimeoutMs = options.timeoutMs ?? 15 * 60_000;
|
|
971
|
+
const timeoutMs = Number.isFinite(requestedTimeoutMs) ? Math.max(1, requestedTimeoutMs) : 15 * 60_000;
|
|
972
|
+
const timeout = setTimeout(() => {
|
|
973
|
+
if (settled)
|
|
974
|
+
return;
|
|
975
|
+
settled = true;
|
|
976
|
+
void worker.terminate();
|
|
977
|
+
const error = new Error(`Local forensic report timed out after ${timeoutMs}ms`);
|
|
978
|
+
error.code = "REPORT_SCAN_TIMEOUT";
|
|
979
|
+
reject(error);
|
|
980
|
+
}, timeoutMs);
|
|
981
|
+
timeout.unref?.();
|
|
982
|
+
const finish = (result) => {
|
|
983
|
+
if (settled)
|
|
984
|
+
return;
|
|
985
|
+
settled = true;
|
|
986
|
+
clearTimeout(timeout);
|
|
987
|
+
void worker.terminate();
|
|
988
|
+
if (result.ok)
|
|
989
|
+
resolve(result.report);
|
|
990
|
+
else
|
|
991
|
+
reject(result.error);
|
|
992
|
+
};
|
|
815
993
|
worker.on("message", (message) => {
|
|
994
|
+
if (settled)
|
|
995
|
+
return;
|
|
816
996
|
const msg = message;
|
|
817
997
|
if (msg.progress) {
|
|
818
|
-
onProgress?.(msg.progress
|
|
998
|
+
onProgress?.(msg.progress);
|
|
819
999
|
return;
|
|
820
1000
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
void worker.terminate();
|
|
1001
|
+
if (msg.ok === true && msg.report && typeof msg.report === "object") {
|
|
1002
|
+
finish({ ok: true, report: msg.report });
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
finish({ ok: false, error: new Error(typeof msg.message === "string" ? msg.message : "Local forensic report failed") });
|
|
827
1006
|
});
|
|
828
1007
|
worker.once("error", (error) => {
|
|
829
|
-
|
|
830
|
-
return;
|
|
831
|
-
settled = true;
|
|
832
|
-
reject(error);
|
|
1008
|
+
finish({ ok: false, error });
|
|
833
1009
|
});
|
|
834
1010
|
worker.once("exit", (code) => {
|
|
835
1011
|
if (settled)
|
|
836
1012
|
return;
|
|
837
|
-
|
|
838
|
-
reject(new Error(`Forensic report worker exited (code ${code}) without a result`));
|
|
1013
|
+
finish({ ok: false, error: new Error(`Forensic report worker exited (code ${code}) without a result`) });
|
|
839
1014
|
});
|
|
840
1015
|
});
|
|
841
1016
|
}
|
|
@@ -844,15 +1019,62 @@ export function respondMigrate(res, body, status = 200) {
|
|
|
844
1019
|
return;
|
|
845
1020
|
res.writeHead(status, { "Content-Type": "application/json" }).end(JSON.stringify(body));
|
|
846
1021
|
}
|
|
1022
|
+
function safeForensicError(error) {
|
|
1023
|
+
const code = error && typeof error === "object" && "code" in error
|
|
1024
|
+
? String(error.code || "")
|
|
1025
|
+
: "";
|
|
1026
|
+
if (code === "REPORT_SCAN_TIMEOUT") {
|
|
1027
|
+
return {
|
|
1028
|
+
code,
|
|
1029
|
+
message: "The local workspace scan took too long and was stopped. No backup data was substituted. Rerun setup to retry.",
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
code: "REPORT_BUILD_FAILED",
|
|
1034
|
+
message: "EchoMem could not finish the local workspace scan. No backup data was substituted. Rerun setup to retry.",
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function publicRunningForensicProgress(value) {
|
|
1038
|
+
if (!value || typeof value !== "object")
|
|
1039
|
+
return null;
|
|
1040
|
+
const progress = value;
|
|
1041
|
+
if (progress.status !== "running")
|
|
1042
|
+
return null;
|
|
1043
|
+
const safeCount = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
|
|
1044
|
+
? Math.floor(candidate)
|
|
1045
|
+
: 0);
|
|
1046
|
+
const safeDuration = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
|
|
1047
|
+
? candidate
|
|
1048
|
+
: 0);
|
|
1049
|
+
const total = safeCount(progress.total);
|
|
1050
|
+
const rawStage = typeof progress.stage === "string" ? progress.stage : "starting";
|
|
1051
|
+
const stage = ["starting", "reading-transcripts", "building-summary", "classifying-repeated-context", "finalizing-report"].includes(rawStage)
|
|
1052
|
+
? rawStage
|
|
1053
|
+
: "starting";
|
|
1054
|
+
return {
|
|
1055
|
+
status: "running",
|
|
1056
|
+
scanned: total > 0 ? Math.min(safeCount(progress.scanned), total) : 0,
|
|
1057
|
+
total,
|
|
1058
|
+
stage,
|
|
1059
|
+
label: forensicStageLabel(stage),
|
|
1060
|
+
elapsedMs: safeDuration(progress.elapsedMs),
|
|
1061
|
+
stageElapsedMs: safeDuration(progress.stageElapsedMs),
|
|
1062
|
+
updatedAt: safeDuration(progress.updatedAt) || Date.now(),
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
847
1065
|
/**
|
|
848
1066
|
* Start the persistent localhost bridge used by the connect-device page. It accepts the token,
|
|
849
1067
|
* serves local Wrapped stats, and holds the /migrate response until cmdLogin has created a cloud
|
|
850
1068
|
* import session.
|
|
851
1069
|
*/
|
|
852
1070
|
export function startCallbackServer(opts = {}) {
|
|
853
|
-
const timeoutMs = opts.timeoutMs ??
|
|
1071
|
+
const timeoutMs = opts.timeoutMs ?? 15 * 60_000;
|
|
1072
|
+
const approvalTimeoutLabel = timeoutMs >= 60_000
|
|
1073
|
+
? `${Math.round(timeoutMs / 60_000)} minutes`
|
|
1074
|
+
: `${Math.ceil(timeoutMs / 1000)} seconds`;
|
|
854
1075
|
const dashboardTimeoutMs = 4 * 60 * 60 * 1000;
|
|
855
1076
|
const expectedNonce = opts.nonce;
|
|
1077
|
+
const scanId = opts.scanId ?? randomUUID();
|
|
856
1078
|
return new Promise((resolveOuter, rejectOuter) => {
|
|
857
1079
|
const onToken = deferred();
|
|
858
1080
|
const decision = deferred();
|
|
@@ -868,6 +1090,10 @@ export function startCallbackServer(opts = {}) {
|
|
|
868
1090
|
let timer;
|
|
869
1091
|
let closed = false;
|
|
870
1092
|
let server;
|
|
1093
|
+
// The browser setup page holds a keep-alive socket (and polls /progress). server.close() only stops
|
|
1094
|
+
// accepting NEW connections and waits for existing ones to end — so without destroying these the
|
|
1095
|
+
// handle never releases and the CLI hangs after migration. Track live sockets and kill them on close.
|
|
1096
|
+
const sockets = new Set();
|
|
871
1097
|
const checkNonce = (nonce) => !expectedNonce || nonce === expectedNonce;
|
|
872
1098
|
const text = (res, status, body = "") => res.writeHead(status, { "Content-Type": "text/plain" }).end(body);
|
|
873
1099
|
const json = (res, status, body) => res.writeHead(status, { "Content-Type": "application/json" }).end(JSON.stringify(body));
|
|
@@ -879,6 +1105,9 @@ export function startCallbackServer(opts = {}) {
|
|
|
879
1105
|
return;
|
|
880
1106
|
closed = true;
|
|
881
1107
|
server.close();
|
|
1108
|
+
for (const socket of sockets)
|
|
1109
|
+
socket.destroy();
|
|
1110
|
+
sockets.clear();
|
|
882
1111
|
};
|
|
883
1112
|
const armTimeout = () => {
|
|
884
1113
|
if (timer)
|
|
@@ -886,7 +1115,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
886
1115
|
const waitMs = onToken.settled() ? dashboardTimeoutMs : timeoutMs;
|
|
887
1116
|
timer = setTimeout(() => {
|
|
888
1117
|
if (!onToken.settled()) {
|
|
889
|
-
onToken.reject(new Error(
|
|
1118
|
+
onToken.reject(new Error(`browser approval did not finish in ${approvalTimeoutLabel}`));
|
|
890
1119
|
}
|
|
891
1120
|
else if (!decision.settled()) {
|
|
892
1121
|
decision.resolve("timeout");
|
|
@@ -900,6 +1129,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
900
1129
|
return void text(res, 403, "bad nonce");
|
|
901
1130
|
if (!token)
|
|
902
1131
|
return void text(res, 400, "missing token");
|
|
1132
|
+
console.log(`[${new Date().toISOString()}] Browser approval callback received.`);
|
|
903
1133
|
const firstToken = !onToken.settled();
|
|
904
1134
|
connected = true;
|
|
905
1135
|
const setupPath = `/setup?nonce=${encodeURIComponent(nonce || "")}&connected=1`;
|
|
@@ -939,10 +1169,21 @@ export function startCallbackServer(opts = {}) {
|
|
|
939
1169
|
return;
|
|
940
1170
|
}
|
|
941
1171
|
if (route === "/setup" && req.method === "GET") {
|
|
1172
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1173
|
+
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1174
|
+
return void text(res, 403, "bad nonce");
|
|
1175
|
+
const hasPreview = url.searchParams.has("preview");
|
|
1176
|
+
if (hasPreview && opts.allowPreview !== true) {
|
|
1177
|
+
return void text(res, 403, "Design preview is disabled. No sample data was shown.");
|
|
1178
|
+
}
|
|
1179
|
+
const previewState = hasPreview ? parseSetupPreviewState(url.searchParams.get("preview")) : null;
|
|
1180
|
+
if (hasPreview && !previewState) {
|
|
1181
|
+
return void text(res, 400, "Unknown setup preview state.");
|
|
1182
|
+
}
|
|
942
1183
|
res.writeHead(200, {
|
|
943
1184
|
"Content-Type": "text/html; charset=utf-8",
|
|
944
1185
|
"Cache-Control": "no-store",
|
|
945
|
-
}).end(renderSetupPage());
|
|
1186
|
+
}).end(renderSetupPage(previewState ? { previewState } : undefined));
|
|
946
1187
|
return;
|
|
947
1188
|
}
|
|
948
1189
|
if (route === "/config" && req.method === "GET") {
|
|
@@ -969,6 +1210,21 @@ export function startCallbackServer(opts = {}) {
|
|
|
969
1210
|
json(res, result.ok ? 200 : 501, result);
|
|
970
1211
|
return;
|
|
971
1212
|
}
|
|
1213
|
+
if (route === "/open-session" && req.method === "POST") {
|
|
1214
|
+
let body;
|
|
1215
|
+
try {
|
|
1216
|
+
body = await readJsonBody(req);
|
|
1217
|
+
}
|
|
1218
|
+
catch {
|
|
1219
|
+
text(res, 400, "bad json");
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (!checkNonce(asString(body.nonce)))
|
|
1223
|
+
return void text(res, 403, "bad nonce");
|
|
1224
|
+
const result = openExistingAgentSession(asString(body.source) || "", asString(body.sessionId) || "");
|
|
1225
|
+
json(res, result.ok ? 200 : 400, result);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
972
1228
|
if (route === "/callback" && req.method === "GET") {
|
|
973
1229
|
handleCallback(res, url.searchParams.get("token") || undefined, url.searchParams.get("key") || undefined, url.searchParams.get("nonce") || undefined);
|
|
974
1230
|
return;
|
|
@@ -996,15 +1252,99 @@ export function startCallbackServer(opts = {}) {
|
|
|
996
1252
|
}
|
|
997
1253
|
if (route === "/report" && req.method === "GET") {
|
|
998
1254
|
// Local forensic "Context Doctor" report — computed locally, served BEFORE auth (scan-first).
|
|
1255
|
+
res.setHeader("Cache-Control", "no-store");
|
|
999
1256
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1000
1257
|
return void text(res, 403, "bad nonce");
|
|
1001
|
-
|
|
1258
|
+
let payload;
|
|
1259
|
+
try {
|
|
1260
|
+
payload = opts.getReport ? opts.getReport() : null;
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
return void json(res, 500, {
|
|
1264
|
+
schemaVersion: 1,
|
|
1265
|
+
kind: "failed",
|
|
1266
|
+
mode: "production",
|
|
1267
|
+
scanId,
|
|
1268
|
+
error: {
|
|
1269
|
+
code: "REPORT_STATE_UNAVAILABLE",
|
|
1270
|
+
message: "EchoMem could not read the local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
1271
|
+
},
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1002
1274
|
if (payload == null) {
|
|
1003
1275
|
// 202 carries scan progress so the page can show a live "scanned N/total" indicator.
|
|
1004
|
-
|
|
1005
|
-
|
|
1276
|
+
let prog;
|
|
1277
|
+
try {
|
|
1278
|
+
prog = opts.getReportProgress ? opts.getReportProgress() : {
|
|
1279
|
+
status: "running",
|
|
1280
|
+
scanned: 0,
|
|
1281
|
+
total: 0,
|
|
1282
|
+
stage: "starting",
|
|
1283
|
+
label: "Starting local scan",
|
|
1284
|
+
elapsedMs: 0,
|
|
1285
|
+
stageElapsedMs: 0,
|
|
1286
|
+
updatedAt: Date.now(),
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
return void json(res, 500, {
|
|
1291
|
+
schemaVersion: 1,
|
|
1292
|
+
kind: "failed",
|
|
1293
|
+
mode: "production",
|
|
1294
|
+
scanId,
|
|
1295
|
+
error: {
|
|
1296
|
+
code: "REPORT_STATE_UNAVAILABLE",
|
|
1297
|
+
message: "EchoMem could not read the local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
1298
|
+
},
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
if (prog && typeof prog === "object" && prog.status === "failed") {
|
|
1302
|
+
return void json(res, 500, {
|
|
1303
|
+
schemaVersion: 1,
|
|
1304
|
+
kind: "failed",
|
|
1305
|
+
mode: "production",
|
|
1306
|
+
scanId,
|
|
1307
|
+
error: safeForensicError(prog.error),
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
const publicProgress = publicRunningForensicProgress(prog);
|
|
1311
|
+
if (!publicProgress) {
|
|
1312
|
+
return void json(res, 500, {
|
|
1313
|
+
schemaVersion: 1,
|
|
1314
|
+
kind: "failed",
|
|
1315
|
+
mode: "production",
|
|
1316
|
+
scanId,
|
|
1317
|
+
error: {
|
|
1318
|
+
code: "REPORT_STATE_INVALID",
|
|
1319
|
+
message: "EchoMem received an invalid local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
return void json(res, 202, {
|
|
1324
|
+
schemaVersion: 1,
|
|
1325
|
+
kind: "scanning",
|
|
1326
|
+
mode: "production",
|
|
1327
|
+
scanId,
|
|
1328
|
+
progress: publicProgress,
|
|
1329
|
+
});
|
|
1006
1330
|
}
|
|
1007
|
-
|
|
1331
|
+
const validation = validateForensicReportForSetup(payload);
|
|
1332
|
+
if (!validation.ok) {
|
|
1333
|
+
return void json(res, 500, {
|
|
1334
|
+
schemaVersion: 1,
|
|
1335
|
+
kind: "failed",
|
|
1336
|
+
mode: "production",
|
|
1337
|
+
scanId,
|
|
1338
|
+
error: { code: validation.code, message: validation.message },
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
json(res, 200, {
|
|
1342
|
+
schemaVersion: 1,
|
|
1343
|
+
kind: validation.kind,
|
|
1344
|
+
mode: "production",
|
|
1345
|
+
scanId,
|
|
1346
|
+
report: validation.report,
|
|
1347
|
+
});
|
|
1008
1348
|
return;
|
|
1009
1349
|
}
|
|
1010
1350
|
if (route === "/progress" && req.method === "GET") {
|
|
@@ -1091,9 +1431,13 @@ export function startCallbackServer(opts = {}) {
|
|
|
1091
1431
|
text(res, 500, e instanceof Error ? e.message : String(e));
|
|
1092
1432
|
});
|
|
1093
1433
|
});
|
|
1434
|
+
server.on("connection", (socket) => {
|
|
1435
|
+
sockets.add(socket);
|
|
1436
|
+
socket.on("close", () => sockets.delete(socket));
|
|
1437
|
+
});
|
|
1094
1438
|
armTimeout();
|
|
1095
1439
|
server.on("error", (e) => rejectOuter(e));
|
|
1096
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1440
|
+
server.listen(opts.port ?? 0, "127.0.0.1", () => {
|
|
1097
1441
|
const addr = server.address();
|
|
1098
1442
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1099
1443
|
resolveOuter({
|
|
@@ -1300,13 +1644,15 @@ async function cmdInit(flags) {
|
|
|
1300
1644
|
await cmdSetupHud(flags);
|
|
1301
1645
|
// 3. Start onboarding — opens the browser dashboard (scan → connect → extraction) and waits there.
|
|
1302
1646
|
console.log("");
|
|
1303
|
-
if (!flags["skip-login"] && !flags["no-login"])
|
|
1304
|
-
|
|
1647
|
+
if (!flags["skip-login"] && !flags["no-login"] && !await cmdLogin(flags)) {
|
|
1648
|
+
console.log("\nEchoMem is configured, but this device was not connected. Restart EchoMem and use the new page it opens.");
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1305
1651
|
console.log("");
|
|
1306
1652
|
console.log("🎉 EchoMem is ready.");
|
|
1307
1653
|
console.log(" • MCP memory is configured for every coding agent installed on this machine.");
|
|
1308
1654
|
if (!flags["no-hud"]) {
|
|
1309
|
-
console.log(' • The context HUD is running (top-right). Right-click it → "
|
|
1655
|
+
console.log(' • The context HUD is running (top-right). Right-click it → "Show after restart" to keep it,');
|
|
1310
1656
|
console.log(' or just tell your agent "open the EchoMem HUD" anytime (it runs: echomem-hud app).');
|
|
1311
1657
|
}
|
|
1312
1658
|
else {
|
|
@@ -1385,9 +1731,21 @@ async function cmdSetupHud(flags) {
|
|
|
1385
1731
|
}
|
|
1386
1732
|
function resolveHudCliPath() {
|
|
1387
1733
|
const entry = fs.realpathSync(process.argv[1] || "");
|
|
1734
|
+
const compiledEntry = compiledDistPathForSource(entry);
|
|
1735
|
+
if (compiledEntry) {
|
|
1736
|
+
const compiledHud = path.join(path.dirname(compiledEntry), "hud", "cli.js");
|
|
1737
|
+
if (fs.existsSync(compiledHud))
|
|
1738
|
+
return compiledHud;
|
|
1739
|
+
}
|
|
1388
1740
|
const base = path.dirname(entry);
|
|
1389
1741
|
const candidate = path.join(base, "hud", "cli.js");
|
|
1390
|
-
|
|
1742
|
+
if (fs.existsSync(candidate))
|
|
1743
|
+
return candidate;
|
|
1744
|
+
const sourceCandidate = path.join(base, "hud", "cli.ts");
|
|
1745
|
+
if (fs.existsSync(sourceCandidate)) {
|
|
1746
|
+
throw new Error("The local HUD CLI is not built. Run npm --prefix packages/mcp-server run build and retry.");
|
|
1747
|
+
}
|
|
1748
|
+
return candidate;
|
|
1391
1749
|
}
|
|
1392
1750
|
function parseHudClient(value) {
|
|
1393
1751
|
return value === "codex" || value === "claude-code" || value === "claude-desktop" || value === "both" || value === "auto"
|
|
@@ -1404,16 +1762,39 @@ async function cmdLogin(flags) {
|
|
|
1404
1762
|
});
|
|
1405
1763
|
if (!ok)
|
|
1406
1764
|
process.exitCode = 1;
|
|
1407
|
-
return;
|
|
1765
|
+
return ok;
|
|
1408
1766
|
}
|
|
1409
1767
|
// Browser path: open a localhost dashboard. It briefly leaves for hosted auth, then returns here
|
|
1410
1768
|
// after the web page has delivered the token+key to the callback. The nonce gates every local route.
|
|
1411
1769
|
console.log("Opening your browser to approve this device…");
|
|
1412
|
-
const
|
|
1770
|
+
const devPortRaw = typeof flags["dev-port"] === "string" ? Number(flags["dev-port"]) : undefined;
|
|
1771
|
+
if (devPortRaw !== undefined && (!Number.isInteger(devPortRaw) || devPortRaw < 1024 || devPortRaw > 65535)) {
|
|
1772
|
+
throw new Error("--dev-port must be an integer between 1024 and 65535");
|
|
1773
|
+
}
|
|
1774
|
+
const devNonce = typeof flags["dev-nonce"] === "string" ? flags["dev-nonce"].trim() : undefined;
|
|
1775
|
+
if (devNonce && devPortRaw === undefined)
|
|
1776
|
+
throw new Error("--dev-nonce requires --dev-port");
|
|
1777
|
+
if (devNonce && !/^[A-Za-z0-9-]{16,128}$/.test(devNonce)) {
|
|
1778
|
+
throw new Error("--dev-nonce must contain 16-128 letters, numbers, or hyphens");
|
|
1779
|
+
}
|
|
1780
|
+
const nonce = devNonce || randomUUID();
|
|
1413
1781
|
let stats = null;
|
|
1414
1782
|
let forensicReport = null;
|
|
1415
|
-
|
|
1783
|
+
const forensicStartedAt = Date.now();
|
|
1784
|
+
let forensicStageStartedAt = forensicStartedAt;
|
|
1785
|
+
let forensicStage = "starting";
|
|
1786
|
+
let forensicProgress = {
|
|
1787
|
+
status: "running",
|
|
1788
|
+
scanned: 0,
|
|
1789
|
+
total: 0,
|
|
1790
|
+
stage: forensicStage,
|
|
1791
|
+
label: forensicStageLabel(forensicStage),
|
|
1792
|
+
elapsedMs: 0,
|
|
1793
|
+
stageElapsedMs: 0,
|
|
1794
|
+
updatedAt: forensicStartedAt,
|
|
1795
|
+
};
|
|
1416
1796
|
const srv = await startCallbackServer({
|
|
1797
|
+
port: devPortRaw,
|
|
1417
1798
|
nonce,
|
|
1418
1799
|
getStats: () => stats,
|
|
1419
1800
|
getReport: () => forensicReport,
|
|
@@ -1430,15 +1811,45 @@ async function cmdLogin(flags) {
|
|
|
1430
1811
|
srv.setAuthUrl(connectUrl, switchAccountUrl.toString());
|
|
1431
1812
|
openBrowser(localSetupUrl);
|
|
1432
1813
|
console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
|
|
1814
|
+
console.log("Waiting for browser approval for up to 15 minutes…");
|
|
1433
1815
|
// Scan-first: build the local forensic "Context Doctor" report off-thread so the page shows it
|
|
1434
1816
|
// BEFORE the user connects an account (the scan is local-only; nothing leaves the machine).
|
|
1435
|
-
buildForensicReportOffThread((
|
|
1436
|
-
|
|
1817
|
+
buildForensicReportOffThread((progress) => {
|
|
1818
|
+
const now = Date.now();
|
|
1819
|
+
const nextStage = progress.stage || forensicStage;
|
|
1820
|
+
if (nextStage !== forensicStage) {
|
|
1821
|
+
forensicStage = nextStage;
|
|
1822
|
+
forensicStageStartedAt = now;
|
|
1823
|
+
console.log(`Local scan: ${forensicStageLabel(forensicStage)}…`);
|
|
1824
|
+
}
|
|
1825
|
+
forensicProgress = {
|
|
1826
|
+
status: "running",
|
|
1827
|
+
scanned: progress.done,
|
|
1828
|
+
total: progress.total,
|
|
1829
|
+
stage: forensicStage,
|
|
1830
|
+
label: forensicStageLabel(forensicStage),
|
|
1831
|
+
detail: progress.detail,
|
|
1832
|
+
elapsedMs: now - forensicStartedAt,
|
|
1833
|
+
stageElapsedMs: now - forensicStageStartedAt,
|
|
1834
|
+
updatedAt: now,
|
|
1835
|
+
};
|
|
1437
1836
|
})
|
|
1438
1837
|
.then((r) => {
|
|
1439
1838
|
forensicReport = r;
|
|
1440
1839
|
})
|
|
1441
1840
|
.catch((e) => {
|
|
1841
|
+
const now = Date.now();
|
|
1842
|
+
forensicProgress = {
|
|
1843
|
+
status: "failed",
|
|
1844
|
+
scanned: forensicProgress.scanned,
|
|
1845
|
+
total: forensicProgress.total,
|
|
1846
|
+
stage: "failed",
|
|
1847
|
+
label: "Local scan failed",
|
|
1848
|
+
elapsedMs: now - forensicStartedAt,
|
|
1849
|
+
stageElapsedMs: now - forensicStageStartedAt,
|
|
1850
|
+
updatedAt: now,
|
|
1851
|
+
error: safeForensicError(e),
|
|
1852
|
+
};
|
|
1442
1853
|
console.error(`Could not build the local report: ${e instanceof Error ? e.message : String(e)}`);
|
|
1443
1854
|
});
|
|
1444
1855
|
let token;
|
|
@@ -1448,14 +1859,14 @@ async function cmdLogin(flags) {
|
|
|
1448
1859
|
}
|
|
1449
1860
|
catch (e) {
|
|
1450
1861
|
srv.close();
|
|
1451
|
-
console.error(`❌ ${e?.message || e}.
|
|
1862
|
+
console.error(`❌ ${e?.message || e}. Restart EchoMem and use the new page it opens.`);
|
|
1452
1863
|
process.exitCode = 1;
|
|
1453
|
-
return;
|
|
1864
|
+
return false;
|
|
1454
1865
|
}
|
|
1455
1866
|
if (!await verifyAndPrint({ token, key })) {
|
|
1456
1867
|
srv.close();
|
|
1457
1868
|
process.exitCode = 1;
|
|
1458
|
-
return;
|
|
1869
|
+
return false;
|
|
1459
1870
|
}
|
|
1460
1871
|
let disc = null;
|
|
1461
1872
|
let exactDiscovery = Promise.resolve(null);
|
|
@@ -1788,7 +2199,7 @@ async function cmdLogin(flags) {
|
|
|
1788
2199
|
sendMigrate({ error: "IMPORT_START_FAILED", message: "Local session discovery did not finish." }, 500);
|
|
1789
2200
|
srv.close();
|
|
1790
2201
|
process.exitCode = 1;
|
|
1791
|
-
return;
|
|
2202
|
+
return true;
|
|
1792
2203
|
}
|
|
1793
2204
|
activeJobCount = exact.pending.length;
|
|
1794
2205
|
const updateProgress = (patch) => {
|
|
@@ -1820,7 +2231,7 @@ async function cmdLogin(flags) {
|
|
|
1820
2231
|
sendMigrate({ error: "NO_PENDING_SESSIONS" }, 409);
|
|
1821
2232
|
srv.close();
|
|
1822
2233
|
console.log("Setup complete — no unprocessed local conversations to extract.");
|
|
1823
|
-
return;
|
|
2234
|
+
return true;
|
|
1824
2235
|
}
|
|
1825
2236
|
updateProgress({ status: "starting", running: 0, queued: exact.pending.length, latest: "Creating import session." });
|
|
1826
2237
|
// Plan caps limit how many conversations one import session accepts (IMPORT_LIMIT_EXCEEDED →
|
|
@@ -1934,6 +2345,7 @@ async function cmdLogin(flags) {
|
|
|
1934
2345
|
console.log("Setup complete — run `echomem-mcp migrate` later to back-fill your history.");
|
|
1935
2346
|
srv.close();
|
|
1936
2347
|
}
|
|
2348
|
+
return true;
|
|
1937
2349
|
}
|
|
1938
2350
|
async function cmdUnlock(flags) {
|
|
1939
2351
|
const store = new KeyStore();
|