@echomem/mcp 1.4.42 → 1.4.44
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 +25 -28
- package/dist/config-files.js +63 -0
- 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/hud/hooks.js +43 -31
- 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 +19 -134
- package/dist/setup-page/client-lifecycle.js +31 -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 +378 -668
- package/dist/v1-contract.js +0 -8
- package/package.json +9 -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.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import http from "node:http";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
18
|
+
import { execFileSync, spawn, spawnSync, } from "node:child_process";
|
|
19
19
|
import { Worker } from "node:worker_threads";
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import os from "node:os";
|
|
@@ -25,13 +25,13 @@ 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";
|
|
34
|
+
import { atomicWriteJsonObject, atomicWriteTextFile, readJsonObjectFile } from "./config-files.js";
|
|
35
35
|
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
36
36
|
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
37
37
|
import { installHeadlessRuntimeSync, readHeadlessRuntimeInstallation, } from "./headless-runtime.js";
|
|
@@ -70,15 +70,25 @@ const CODEX_SKILL_NAMES = [
|
|
|
70
70
|
function home(...p) {
|
|
71
71
|
return path.join(os.homedir(), ...p);
|
|
72
72
|
}
|
|
73
|
-
function
|
|
74
|
-
const configured = process.env
|
|
73
|
+
function configuredProfileDirectory(envKey, fallbackName) {
|
|
74
|
+
const configured = process.env[envKey]?.trim();
|
|
75
75
|
if (!configured)
|
|
76
|
-
return home(
|
|
76
|
+
return home(fallbackName);
|
|
77
77
|
if (configured === "~")
|
|
78
78
|
return os.homedir();
|
|
79
|
-
if (configured.startsWith(
|
|
79
|
+
if (configured.startsWith("~/") || configured.startsWith("~\\")) {
|
|
80
80
|
return path.join(os.homedir(), configured.slice(2));
|
|
81
|
-
|
|
81
|
+
}
|
|
82
|
+
if (!path.isAbsolute(configured)) {
|
|
83
|
+
throw new Error(`${envKey} must be an absolute path or start with ~/`);
|
|
84
|
+
}
|
|
85
|
+
return path.normalize(configured);
|
|
86
|
+
}
|
|
87
|
+
function codexHome() {
|
|
88
|
+
return configuredProfileDirectory("CODEX_HOME", ".codex");
|
|
89
|
+
}
|
|
90
|
+
function claudeConfigHome() {
|
|
91
|
+
return configuredProfileDirectory("CLAUDE_CONFIG_DIR", ".claude");
|
|
82
92
|
}
|
|
83
93
|
function filesEqual(left, right) {
|
|
84
94
|
try {
|
|
@@ -227,7 +237,7 @@ export function detectClients() {
|
|
|
227
237
|
if (c.kind === "command")
|
|
228
238
|
return fs.existsSync(c.detectDir);
|
|
229
239
|
if (c.id === "claude-code")
|
|
230
|
-
return
|
|
240
|
+
return claudeCodeCliAvailable();
|
|
231
241
|
return false;
|
|
232
242
|
});
|
|
233
243
|
}
|
|
@@ -309,12 +319,51 @@ export function codexTomlBlock(entry) {
|
|
|
309
319
|
const args = (Array.isArray(entry.args) ? entry.args : []).map((a) => JSON.stringify(String(a))).join(", ");
|
|
310
320
|
return `[mcp_servers.echomem]\ncommand = ${command}\nargs = [${args}]\n`;
|
|
311
321
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
322
|
+
function objectRecord(value) {
|
|
323
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
324
|
+
? value
|
|
325
|
+
: undefined;
|
|
326
|
+
}
|
|
327
|
+
function validDesktopManagedEntry(value) {
|
|
328
|
+
const entry = objectRecord(value);
|
|
329
|
+
const environment = objectRecord(entry?.env);
|
|
330
|
+
if (!entry || environment?.ECHO_DESKTOP_MANAGED !== "1")
|
|
331
|
+
return false;
|
|
332
|
+
const command = entry.command;
|
|
333
|
+
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
334
|
+
return typeof command === "string"
|
|
335
|
+
&& fs.existsSync(command)
|
|
336
|
+
&& typeof args[0] === "string"
|
|
337
|
+
&& fs.existsSync(args[0]);
|
|
338
|
+
}
|
|
339
|
+
function codexEntryFromBlock(lines, start, end) {
|
|
340
|
+
let command;
|
|
341
|
+
let args = [];
|
|
342
|
+
let desktopManaged = false;
|
|
343
|
+
for (const line of lines.slice(start + 1, end)) {
|
|
344
|
+
const commandMatch = line.match(/^\s*command\s*=\s*(.+?)\s*$/);
|
|
345
|
+
const argsMatch = line.match(/^\s*args\s*=\s*(.+?)\s*$/);
|
|
346
|
+
try {
|
|
347
|
+
if (commandMatch)
|
|
348
|
+
command = JSON.parse(commandMatch[1]);
|
|
349
|
+
if (argsMatch)
|
|
350
|
+
args = JSON.parse(argsMatch[1]);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
if (/ECHO_DESKTOP_MANAGED\s*=\s*["']1["']/.test(line))
|
|
356
|
+
desktopManaged = true;
|
|
357
|
+
}
|
|
358
|
+
if (typeof command !== "string")
|
|
359
|
+
return undefined;
|
|
360
|
+
return {
|
|
361
|
+
command,
|
|
362
|
+
args: Array.isArray(args) ? args : [],
|
|
363
|
+
...(desktopManaged ? { env: { ECHO_DESKTOP_MANAGED: "1" } } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
export function writeCodexConfig(configPath, entry, options = {}) {
|
|
318
367
|
let content = "";
|
|
319
368
|
try {
|
|
320
369
|
content = fs.readFileSync(configPath, "utf8");
|
|
@@ -331,14 +380,17 @@ export function writeCodexConfig(configPath, entry) {
|
|
|
331
380
|
let end = start + 1;
|
|
332
381
|
while (end < lines.length && !/^\s*\[/.test(lines[end]))
|
|
333
382
|
end++;
|
|
383
|
+
if (!options.forceHeadless && validDesktopManagedEntry(codexEntryFromBlock(lines, start, end))) {
|
|
384
|
+
return "desktop-managed";
|
|
385
|
+
}
|
|
334
386
|
if (lines.slice(start, end).join("\n").trimEnd() === block)
|
|
335
387
|
return "exists"; // already correct
|
|
336
388
|
const next = [...lines.slice(0, start), ...block.split("\n"), ...lines.slice(end)];
|
|
337
|
-
|
|
389
|
+
atomicWriteTextFile(configPath, next.join("\n").replace(/\n{3,}/g, "\n\n").replace(/^\n+/, ""));
|
|
338
390
|
return "wrote"; // replaced a stale entry → caller tells the user to restart Codex
|
|
339
391
|
}
|
|
340
392
|
const sep = content ? (content.endsWith("\n") ? "\n" : "\n\n") : "";
|
|
341
|
-
|
|
393
|
+
atomicWriteTextFile(configPath, content + sep + block + "\n");
|
|
342
394
|
return "wrote";
|
|
343
395
|
}
|
|
344
396
|
/**
|
|
@@ -359,7 +411,7 @@ function echomemGuidanceBlock() {
|
|
|
359
411
|
"- 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.",
|
|
360
412
|
"- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
|
|
361
413
|
'- 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.',
|
|
362
|
-
"- 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,
|
|
414
|
+
"- 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.",
|
|
363
415
|
'- 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.',
|
|
364
416
|
"- 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.",
|
|
365
417
|
"- 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.",
|
|
@@ -370,7 +422,7 @@ function echomemGuidanceBlock() {
|
|
|
370
422
|
"- 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.",
|
|
371
423
|
"- 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.",
|
|
372
424
|
"- 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.",
|
|
373
|
-
"- 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,
|
|
425
|
+
"- 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.",
|
|
374
426
|
"- 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.",
|
|
375
427
|
"- 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.",
|
|
376
428
|
"- 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.",
|
|
@@ -395,45 +447,59 @@ export function writeAgentsMemoryGuidance(filePath) {
|
|
|
395
447
|
const current = content.slice(start, end + AGENTS_MD_END.length);
|
|
396
448
|
if (current === block)
|
|
397
449
|
return "exists";
|
|
398
|
-
|
|
450
|
+
atomicWriteTextFile(filePath, content.slice(0, start) + block + content.slice(end + AGENTS_MD_END.length));
|
|
399
451
|
return "updated";
|
|
400
452
|
}
|
|
401
453
|
const sep = content ? (content.endsWith("\n") ? "\n" : "\n\n") : "";
|
|
402
|
-
|
|
454
|
+
atomicWriteTextFile(filePath, content + sep + block + "\n");
|
|
403
455
|
return "wrote";
|
|
404
456
|
}
|
|
405
|
-
/**
|
|
406
|
-
|
|
407
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Refresh marker-owned guidance for users upgrading an existing standalone MCP install.
|
|
459
|
+
* This intentionally does not create global memory files: setup owns first installation,
|
|
460
|
+
* while server startup only migrates guidance that EchoMem already installed.
|
|
461
|
+
*/
|
|
462
|
+
export function refreshInstalledMemoryGuidance() {
|
|
463
|
+
let candidates;
|
|
408
464
|
try {
|
|
409
|
-
|
|
465
|
+
candidates = [
|
|
466
|
+
path.join(codexHome(), "AGENTS.md"),
|
|
467
|
+
path.join(claudeConfigHome(), "CLAUDE.md"),
|
|
468
|
+
];
|
|
410
469
|
}
|
|
411
470
|
catch {
|
|
412
|
-
|
|
471
|
+
return;
|
|
413
472
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
473
|
+
for (const filePath of candidates) {
|
|
474
|
+
try {
|
|
475
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
476
|
+
const start = content.indexOf(AGENTS_MD_BEGIN);
|
|
477
|
+
const end = content.indexOf(AGENTS_MD_END);
|
|
478
|
+
if (start >= 0 && end > start)
|
|
479
|
+
writeAgentsMemoryGuidance(filePath);
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// Missing, unreadable, or externally managed files are left untouched.
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** Merge the EchoMem entry into a JSON client's `mcpServers` map without clobbering siblings. */
|
|
487
|
+
export function writeJsonClientConfig(configPath, entry, options = {}) {
|
|
488
|
+
const config = readJsonObjectFile(configPath, "MCP client configuration");
|
|
489
|
+
const servers = objectRecord(config.mcpServers) ?? {};
|
|
490
|
+
config.mcpServers = servers;
|
|
491
|
+
if (!options.forceHeadless && validDesktopManagedEntry(servers.echomem)) {
|
|
492
|
+
return "desktop-managed";
|
|
493
|
+
}
|
|
494
|
+
servers.echomem = entry;
|
|
495
|
+
atomicWriteJsonObject(configPath, config);
|
|
496
|
+
return "wrote";
|
|
418
497
|
}
|
|
419
498
|
function readClaudeCodeConfigFile(configPath) {
|
|
420
|
-
|
|
421
|
-
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
422
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
423
|
-
? parsed
|
|
424
|
-
: {};
|
|
425
|
-
}
|
|
426
|
-
catch {
|
|
427
|
-
return {};
|
|
428
|
-
}
|
|
499
|
+
return readJsonObjectFile(configPath, "Claude Code user configuration");
|
|
429
500
|
}
|
|
430
501
|
function echoMemEntryFromServers(value) {
|
|
431
|
-
|
|
432
|
-
return undefined;
|
|
433
|
-
const entry = value.echomem;
|
|
434
|
-
return entry && typeof entry === "object" && !Array.isArray(entry)
|
|
435
|
-
? entry
|
|
436
|
-
: undefined;
|
|
502
|
+
return objectRecord(objectRecord(value)?.echomem);
|
|
437
503
|
}
|
|
438
504
|
function claudeEntriesMatch(actual, expected) {
|
|
439
505
|
if (!actual || actual.command !== expected.command)
|
|
@@ -464,29 +530,100 @@ function claudeCodeLocalEchoMemProjects(configPath) {
|
|
|
464
530
|
.map(([projectPath]) => projectPath)
|
|
465
531
|
.sort();
|
|
466
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Run Claude Code without assuming its launcher is a native executable. npm installs expose
|
|
535
|
+
* `claude.cmd` on Windows, and Node cannot execute .cmd/.bat launchers through execFileSync
|
|
536
|
+
* directly. Resolve the command with `where.exe` and route command files through ComSpec while
|
|
537
|
+
* keeping native .exe installations on the direct exec path.
|
|
538
|
+
*/
|
|
539
|
+
function execClaudeCodeSync(args, options) {
|
|
540
|
+
if (process.platform !== "win32")
|
|
541
|
+
return execFileSync("claude", args, options);
|
|
542
|
+
let command = "claude";
|
|
543
|
+
try {
|
|
544
|
+
const resolved = execFileSync("where.exe", ["claude"], {
|
|
545
|
+
encoding: "utf8",
|
|
546
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
547
|
+
timeout: 3000,
|
|
548
|
+
windowsHide: true,
|
|
549
|
+
})
|
|
550
|
+
.split(/\r?\n/)
|
|
551
|
+
.map((candidate) => candidate.trim())
|
|
552
|
+
.find(Boolean);
|
|
553
|
+
if (resolved)
|
|
554
|
+
command = resolved;
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
// Preserve the normal command-not-found failure below so callers can report it consistently.
|
|
558
|
+
}
|
|
559
|
+
if (/\.(cmd|bat)$/i.test(command)) {
|
|
560
|
+
const shellCommand = [escapeWindowsCmdCommand(command), ...args.map(escapeWindowsCmdArgument)].join(" ");
|
|
561
|
+
const spawnOptions = {
|
|
562
|
+
...options,
|
|
563
|
+
windowsHide: true,
|
|
564
|
+
windowsVerbatimArguments: true,
|
|
565
|
+
};
|
|
566
|
+
const result = spawnSync(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${shellCommand}"`], spawnOptions);
|
|
567
|
+
if (result.error)
|
|
568
|
+
throw result.error;
|
|
569
|
+
if (result.status !== 0) {
|
|
570
|
+
throw new Error(result.stderr?.trim() || `Claude Code exited with status ${result.status ?? "unknown"}.`);
|
|
571
|
+
}
|
|
572
|
+
return result.stdout || "";
|
|
573
|
+
}
|
|
574
|
+
return execFileSync(command, args, { ...options, windowsHide: true });
|
|
575
|
+
}
|
|
576
|
+
function escapeWindowsCmdCommand(value) {
|
|
577
|
+
return value.replace(/([()\][%!^"`<>&|;, *?])/g, "^$1");
|
|
578
|
+
}
|
|
579
|
+
function escapeWindowsCmdArgument(value) {
|
|
580
|
+
let escaped = value
|
|
581
|
+
.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"")
|
|
582
|
+
.replace(/(?=(\\+?)?)\1$/g, "$1$1");
|
|
583
|
+
escaped = `"${escaped}"`;
|
|
584
|
+
return escaped.replace(/([()\][%!^"`<>&|;, *?])/g, "^$1");
|
|
585
|
+
}
|
|
586
|
+
export function claudeCodeCliAvailable() {
|
|
587
|
+
try {
|
|
588
|
+
execClaudeCodeSync(["--version"], {
|
|
589
|
+
encoding: "utf8",
|
|
590
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
591
|
+
timeout: 3000,
|
|
592
|
+
});
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
467
599
|
export function writeClaudeCodeConfig(entry, options = {}) {
|
|
468
600
|
// EchoMem belongs at user scope so every Claude Code project resolves the same durable runtime.
|
|
469
601
|
// Older CLI versions wrote local/project entries, which take precedence over user scope and can
|
|
470
602
|
// keep launching a deleted npm cache or stale runtime. Migrate those only after user scope is safe.
|
|
471
603
|
const configPath = options.configPath ?? home(".claude.json");
|
|
604
|
+
let failureReason;
|
|
472
605
|
const emptyResult = () => ({
|
|
473
606
|
state: "unavailable",
|
|
474
607
|
removedLocalProjects: [],
|
|
475
608
|
skippedLocalProjects: [],
|
|
476
609
|
failedLocalProjects: [],
|
|
477
610
|
restoredPreviousUserEntry: false,
|
|
611
|
+
preservedDesktopManaged: false,
|
|
612
|
+
failureReason,
|
|
478
613
|
});
|
|
479
614
|
const runClaude = (args, cwd) => {
|
|
480
615
|
try {
|
|
481
|
-
|
|
616
|
+
execClaudeCodeSync(args, {
|
|
482
617
|
cwd,
|
|
483
618
|
encoding: "utf8",
|
|
484
619
|
stdio: ["ignore", "pipe", "pipe"],
|
|
485
620
|
timeout: 10000,
|
|
486
621
|
});
|
|
622
|
+
failureReason = undefined;
|
|
487
623
|
return true;
|
|
488
624
|
}
|
|
489
|
-
catch {
|
|
625
|
+
catch (error) {
|
|
626
|
+
failureReason = commandFailureMessage(error);
|
|
490
627
|
return false;
|
|
491
628
|
}
|
|
492
629
|
};
|
|
@@ -496,20 +633,25 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
496
633
|
const removeUser = () => runClaude(["mcp", "remove", "echomem", "-s", "user"]);
|
|
497
634
|
const before = readClaudeCodeConfigFile(configPath);
|
|
498
635
|
const previousUserEntry = echoMemEntryFromServers(before.mcpServers);
|
|
636
|
+
const preservedDesktopManaged = !options.forceHeadless
|
|
637
|
+
&& validDesktopManagedEntry(previousUserEntry);
|
|
638
|
+
const desiredUserEntry = preservedDesktopManaged ? previousUserEntry : entry;
|
|
499
639
|
let restoredPreviousUserEntry = false;
|
|
500
640
|
// Avoid interrupting active/new sessions when the correct global entry is already installed.
|
|
501
|
-
if (!claudeEntriesMatch(previousUserEntry,
|
|
641
|
+
if (!claudeEntriesMatch(previousUserEntry, desiredUserEntry)) {
|
|
502
642
|
if (previousUserEntry && !removeUser())
|
|
503
643
|
return emptyResult();
|
|
504
|
-
if (!addUser(
|
|
644
|
+
if (!addUser(desiredUserEntry)) {
|
|
645
|
+
const addFailureReason = failureReason;
|
|
505
646
|
if (previousUserEntry)
|
|
506
647
|
restoredPreviousUserEntry = addUser(previousUserEntry);
|
|
648
|
+
failureReason = addFailureReason;
|
|
507
649
|
return { ...emptyResult(), restoredPreviousUserEntry };
|
|
508
650
|
}
|
|
509
651
|
}
|
|
510
652
|
const installedUserEntry = echoMemEntryFromServers(readClaudeCodeConfigFile(configPath).mcpServers);
|
|
511
|
-
if (!claudeEntriesMatch(installedUserEntry,
|
|
512
|
-
return { ...emptyResult(), restoredPreviousUserEntry };
|
|
653
|
+
if (!claudeEntriesMatch(installedUserEntry, desiredUserEntry)) {
|
|
654
|
+
return { ...emptyResult(), restoredPreviousUserEntry, preservedDesktopManaged };
|
|
513
655
|
}
|
|
514
656
|
const removedLocalProjects = [];
|
|
515
657
|
const skippedLocalProjects = [];
|
|
@@ -544,6 +686,7 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
544
686
|
skippedLocalProjects,
|
|
545
687
|
failedLocalProjects: unresolved,
|
|
546
688
|
restoredPreviousUserEntry,
|
|
689
|
+
preservedDesktopManaged,
|
|
547
690
|
};
|
|
548
691
|
}
|
|
549
692
|
function readJsonClientEntry(configPath) {
|
|
@@ -716,10 +859,10 @@ function inspectClientConfig(client, desiredVersion) {
|
|
|
716
859
|
};
|
|
717
860
|
}
|
|
718
861
|
function inspectClaudeCodeConfig(client, desiredVersion) {
|
|
719
|
-
if (!fs.existsSync(
|
|
862
|
+
if (!fs.existsSync(claudeConfigHome()) && !claudeCodeCliAvailable())
|
|
720
863
|
return null;
|
|
721
864
|
try {
|
|
722
|
-
const output =
|
|
865
|
+
const output = execClaudeCodeSync(["mcp", "list"], {
|
|
723
866
|
encoding: "utf8",
|
|
724
867
|
stdio: ["ignore", "pipe", "ignore"],
|
|
725
868
|
timeout: 3000,
|
|
@@ -799,6 +942,23 @@ function openBrowser(url) {
|
|
|
799
942
|
}
|
|
800
943
|
}
|
|
801
944
|
function openClaudeDesktop() {
|
|
945
|
+
if (process.platform === "win32") {
|
|
946
|
+
try {
|
|
947
|
+
spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", "start \"\" \"claude://\""], {
|
|
948
|
+
detached: true,
|
|
949
|
+
stdio: "ignore",
|
|
950
|
+
windowsHide: true,
|
|
951
|
+
}).unref();
|
|
952
|
+
return { ok: true, message: "Opened Claude Desktop. The prompt is copied - paste it into Claude." };
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
return {
|
|
956
|
+
ok: false,
|
|
957
|
+
message: "Could not auto-open Claude Desktop. The prompt is copied - open Claude Desktop and paste it.",
|
|
958
|
+
detail: commandFailureMessage(error),
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
}
|
|
802
962
|
if (process.platform !== "darwin") {
|
|
803
963
|
return { ok: false, message: "Could not auto-open Claude on this system. The prompt is copied - open Claude Desktop and paste it." };
|
|
804
964
|
}
|
|
@@ -834,7 +994,7 @@ const LOCAL_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-
|
|
|
834
994
|
// recover the session's original cwd from its transcript before resuming.
|
|
835
995
|
function claudeSessionCwd(sessionId) {
|
|
836
996
|
try {
|
|
837
|
-
const projectsDir = path.join(
|
|
997
|
+
const projectsDir = path.join(claudeConfigHome(), "projects");
|
|
838
998
|
for (const dir of fs.readdirSync(projectsDir)) {
|
|
839
999
|
const file = path.join(projectsDir, dir, `${sessionId}.jsonl`);
|
|
840
1000
|
if (!fs.existsSync(file))
|
|
@@ -860,16 +1020,40 @@ function shellQuote(value) {
|
|
|
860
1020
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
861
1021
|
}
|
|
862
1022
|
function openExistingAgentSession(source, sessionId) {
|
|
863
|
-
if (process.platform !== "darwin")
|
|
864
|
-
return { ok: false, message: "Opening local agent sessions is currently available on macOS." };
|
|
865
1023
|
if (!LOCAL_SESSION_ID_RE.test(sessionId))
|
|
866
1024
|
return { ok: false, message: "The local session identifier is invalid." };
|
|
867
1025
|
try {
|
|
868
1026
|
if (source === "codex") {
|
|
869
|
-
|
|
1027
|
+
const url = `codex://threads/${sessionId}`;
|
|
1028
|
+
if (process.platform === "win32") {
|
|
1029
|
+
spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `start "" "${url}"`], {
|
|
1030
|
+
detached: true,
|
|
1031
|
+
stdio: "ignore",
|
|
1032
|
+
windowsHide: true,
|
|
1033
|
+
}).unref();
|
|
1034
|
+
}
|
|
1035
|
+
else if (process.platform === "darwin") {
|
|
1036
|
+
execFileSync("open", [url], { stdio: "pipe" });
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
return { ok: false, message: "Opening local Codex sessions is not supported on this system yet." };
|
|
1040
|
+
}
|
|
870
1041
|
return { ok: true, message: "Opened the original session in Codex." };
|
|
871
1042
|
}
|
|
872
1043
|
if (source === "claude-code") {
|
|
1044
|
+
if (process.platform === "win32") {
|
|
1045
|
+
const cwd = claudeSessionCwd(sessionId);
|
|
1046
|
+
spawn(process.env.ComSpec || "cmd.exe", ["/d", "/k", `claude --resume ${sessionId}`], {
|
|
1047
|
+
cwd: cwd && fs.existsSync(cwd) ? cwd : process.cwd(),
|
|
1048
|
+
detached: true,
|
|
1049
|
+
stdio: "ignore",
|
|
1050
|
+
windowsHide: false,
|
|
1051
|
+
}).unref();
|
|
1052
|
+
return { ok: true, message: "Opened the original Claude Code session in a terminal." };
|
|
1053
|
+
}
|
|
1054
|
+
if (process.platform !== "darwin") {
|
|
1055
|
+
return { ok: false, message: "Opening local Claude Code sessions is not supported on this system yet." };
|
|
1056
|
+
}
|
|
873
1057
|
const claude = firstExisting(["/opt/homebrew/bin/claude", "/usr/local/bin/claude"]) || "claude";
|
|
874
1058
|
const cwd = claudeSessionCwd(sessionId);
|
|
875
1059
|
const resume = `${claude} --resume ${sessionId}`;
|
|
@@ -1040,7 +1224,7 @@ function withTimeout(promise, ms, code, onTimeout) {
|
|
|
1040
1224
|
function delay(ms) {
|
|
1041
1225
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1042
1226
|
}
|
|
1043
|
-
const
|
|
1227
|
+
const LOCAL_ASSET_TYPES = {
|
|
1044
1228
|
".html": "text/html; charset=utf-8",
|
|
1045
1229
|
".js": "text/javascript; charset=utf-8",
|
|
1046
1230
|
".json": "application/json; charset=utf-8",
|
|
@@ -1049,39 +1233,11 @@ const CITY_ASSET_TYPES = {
|
|
|
1049
1233
|
".png": "image/png",
|
|
1050
1234
|
".svg": "image/svg+xml",
|
|
1051
1235
|
};
|
|
1052
|
-
function
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
// Published install: fall back to the copy bundled into dist/city by prepack (bundle-city.mjs).
|
|
1058
|
-
return fileURLToPath(new URL("./city/", import.meta.url));
|
|
1059
|
-
}
|
|
1060
|
-
function serveRepoCityAsset(reqPath, res) {
|
|
1061
|
-
const root = repoCityArtifactsRoot();
|
|
1062
|
-
const rel = reqPath === "/city" || reqPath === "/city/" ? "echo-ai-city-only.html" : decodeURIComponent(reqPath.slice("/city/".length));
|
|
1063
|
-
// Archives stay in the checkout for recovery, but must never become a localhost UI surface.
|
|
1064
|
-
const normalizedRel = rel.replace(/\\/g, "/");
|
|
1065
|
-
if (normalizedRel === "archive" || normalizedRel.startsWith("archive/")) {
|
|
1066
|
-
res.writeHead(404).end("not found");
|
|
1067
|
-
return true;
|
|
1068
|
-
}
|
|
1069
|
-
const filePath = path.resolve(root, rel);
|
|
1070
|
-
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
1071
|
-
if (!filePath.startsWith(rootWithSep)) {
|
|
1072
|
-
res.writeHead(403).end("forbidden");
|
|
1073
|
-
return true;
|
|
1074
|
-
}
|
|
1075
|
-
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
1076
|
-
res.writeHead(404).end("not found");
|
|
1077
|
-
return true;
|
|
1078
|
-
}
|
|
1079
|
-
res.writeHead(200, {
|
|
1080
|
-
"Content-Type": CITY_ASSET_TYPES[path.extname(filePath)] || "application/octet-stream",
|
|
1081
|
-
"Cache-Control": "no-store",
|
|
1082
|
-
});
|
|
1083
|
-
fs.createReadStream(filePath).pipe(res);
|
|
1084
|
-
return true;
|
|
1236
|
+
function repoLabel(cwd) {
|
|
1237
|
+
if (!cwd)
|
|
1238
|
+
return "";
|
|
1239
|
+
const normalized = cwd.replace(/[\\/]+$/, "");
|
|
1240
|
+
return path.basename(normalized) || normalized;
|
|
1085
1241
|
}
|
|
1086
1242
|
function hudAssetsRoot() {
|
|
1087
1243
|
return fileURLToPath(new URL("../assets/hud/", import.meta.url));
|
|
@@ -1100,7 +1256,7 @@ function serveHudAsset(reqPath, res) {
|
|
|
1100
1256
|
return true;
|
|
1101
1257
|
}
|
|
1102
1258
|
res.writeHead(200, {
|
|
1103
|
-
"Content-Type":
|
|
1259
|
+
"Content-Type": LOCAL_ASSET_TYPES[path.extname(filePath)] || "application/octet-stream",
|
|
1104
1260
|
"Cache-Control": "no-store",
|
|
1105
1261
|
});
|
|
1106
1262
|
fs.createReadStream(filePath).pipe(res);
|
|
@@ -1277,61 +1433,6 @@ export function discoverMigratableFastOffThread(opts = {}) {
|
|
|
1277
1433
|
});
|
|
1278
1434
|
});
|
|
1279
1435
|
}
|
|
1280
|
-
/** Build the full local-history dashboard payload away from the callback server's event loop.
|
|
1281
|
-
* `collect()` can synchronously parse hundreds of JSONL files for tens of seconds; doing that on
|
|
1282
|
-
* the bridge thread prevents even localhost actions such as account switch from receiving a reply. */
|
|
1283
|
-
export function buildCollectedStatsPayloadOffThread(inject) {
|
|
1284
|
-
const reportUrl = runtimeModuleUrl("report");
|
|
1285
|
-
const serializedInject = JSON.stringify(inject);
|
|
1286
|
-
const code = `
|
|
1287
|
-
import { parentPort } from "node:worker_threads";
|
|
1288
|
-
import { collect, buildStatsPayload } from ${JSON.stringify(reportUrl)};
|
|
1289
|
-
|
|
1290
|
-
try {
|
|
1291
|
-
const payload = await buildStatsPayload(collect(), ${serializedInject});
|
|
1292
|
-
parentPort?.postMessage({ ok: true, payload });
|
|
1293
|
-
} catch (error) {
|
|
1294
|
-
parentPort?.postMessage({
|
|
1295
|
-
ok: false,
|
|
1296
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1297
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
`;
|
|
1301
|
-
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
1302
|
-
return new Promise((resolve, reject) => {
|
|
1303
|
-
let settled = false;
|
|
1304
|
-
const finish = (result) => {
|
|
1305
|
-
if (settled)
|
|
1306
|
-
return;
|
|
1307
|
-
settled = true;
|
|
1308
|
-
void worker.terminate();
|
|
1309
|
-
if (result.ok)
|
|
1310
|
-
resolve(result.payload);
|
|
1311
|
-
else
|
|
1312
|
-
reject(result.error);
|
|
1313
|
-
};
|
|
1314
|
-
worker.once("message", (message) => {
|
|
1315
|
-
const msg = message;
|
|
1316
|
-
if (msg.ok === true) {
|
|
1317
|
-
finish({ ok: true, payload: msg.payload });
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
const error = new Error(typeof msg.message === "string" ? msg.message : "Full local stats worker failed");
|
|
1321
|
-
if (typeof msg.stack === "string")
|
|
1322
|
-
error.stack = msg.stack;
|
|
1323
|
-
finish({ ok: false, error });
|
|
1324
|
-
});
|
|
1325
|
-
worker.once("error", (error) => {
|
|
1326
|
-
finish({ ok: false, error });
|
|
1327
|
-
});
|
|
1328
|
-
worker.once("exit", (code) => {
|
|
1329
|
-
if (settled)
|
|
1330
|
-
return;
|
|
1331
|
-
finish({ ok: false, error: new Error(`Full local stats worker exited (code ${code}) without a result`) });
|
|
1332
|
-
});
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
1436
|
export function createLocalDiscoveryCache(loaders = {}) {
|
|
1336
1437
|
const loadQuick = loaders.loadQuick ?? (() => discoverMigratableFastOffThread());
|
|
1337
1438
|
const loadExact = loaders.loadExact ?? (() => discoverMigratableSessionsOffThread());
|
|
@@ -1380,45 +1481,6 @@ export function createLocalDiscoveryCache(loaders = {}) {
|
|
|
1380
1481
|
},
|
|
1381
1482
|
};
|
|
1382
1483
|
}
|
|
1383
|
-
function forensicStageLabel(stage) {
|
|
1384
|
-
if (stage === "reading-transcripts")
|
|
1385
|
-
return "Reading transcript files";
|
|
1386
|
-
if (stage === "building-summary")
|
|
1387
|
-
return "Building scan summary";
|
|
1388
|
-
if (stage === "classifying-repeated-context")
|
|
1389
|
-
return "Classifying repeated context";
|
|
1390
|
-
if (stage === "finalizing-report")
|
|
1391
|
-
return "Finalizing report";
|
|
1392
|
-
return "Starting local scan";
|
|
1393
|
-
}
|
|
1394
|
-
/** Build the local forensic "Context Doctor" report on a worker thread so the multi-file scan never
|
|
1395
|
-
* blocks the bridge's event loop (the freeze postmortem: any unbounded sync work on this path is risky). */
|
|
1396
|
-
export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
1397
|
-
let lastProgress = null;
|
|
1398
|
-
const recordProgress = (progress) => {
|
|
1399
|
-
lastProgress = progress;
|
|
1400
|
-
onProgress?.(progress);
|
|
1401
|
-
};
|
|
1402
|
-
return runForensicReportWorker(recordProgress, options).catch(async (primaryError) => {
|
|
1403
|
-
if (options.failOpen === false)
|
|
1404
|
-
throw primaryError;
|
|
1405
|
-
const failureCode = errorCode(primaryError) || "REPORT_BUILD_FAILED";
|
|
1406
|
-
console.error(`[echomem] local scan degraded after ${failureCode}; continuing without local-history analysis`);
|
|
1407
|
-
onProgress?.({
|
|
1408
|
-
done: lastProgress?.done || 0,
|
|
1409
|
-
total: lastProgress?.total || 0,
|
|
1410
|
-
stage: "finalizing-report",
|
|
1411
|
-
detail: "finishing setup without optional local-history analysis",
|
|
1412
|
-
overall: 0.99,
|
|
1413
|
-
stageDone: 0,
|
|
1414
|
-
stageTotal: 0,
|
|
1415
|
-
});
|
|
1416
|
-
return runForensicReportWorker(undefined, {
|
|
1417
|
-
timeoutMs: 30_000,
|
|
1418
|
-
maxOldGenerationSizeMb: Math.max(64, options.maxOldGenerationSizeMb || 0),
|
|
1419
|
-
}, [], failureCode);
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
1484
|
function errorCode(error) {
|
|
1423
1485
|
return error && typeof error === "object" && "code" in error
|
|
1424
1486
|
? String(error.code || "")
|
|
@@ -1438,7 +1500,6 @@ export function completeOptionalStatsPayload(payload, reason, countsTrusted) {
|
|
|
1438
1500
|
transcriptsUploaded: false,
|
|
1439
1501
|
sessions: { total: 0, codex: 0, claudeCode: 0 },
|
|
1440
1502
|
migratable: { pending: 0, alreadyMigrated: 0 },
|
|
1441
|
-
memoriesCaptured: null,
|
|
1442
1503
|
};
|
|
1443
1504
|
const completed = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
1444
1505
|
? { ...payload }
|
|
@@ -1451,155 +1512,11 @@ export function completeOptionalStatsPayload(payload, reason, countsTrusted) {
|
|
|
1451
1512
|
};
|
|
1452
1513
|
return completed;
|
|
1453
1514
|
}
|
|
1454
|
-
function runForensicReportWorker(onProgress, options, sources, degradedReason) {
|
|
1455
|
-
const forensicsUrl = runtimeModuleUrl("forensics");
|
|
1456
|
-
const serializedSources = sources === undefined ? "undefined" : JSON.stringify(sources);
|
|
1457
|
-
const serializedDegradedReason = JSON.stringify(degradedReason || "");
|
|
1458
|
-
const code = `
|
|
1459
|
-
import { parentPort } from "node:worker_threads";
|
|
1460
|
-
import { buildForensicReport, validateForensicReportForSetup } from ${JSON.stringify(forensicsUrl)};
|
|
1461
|
-
try {
|
|
1462
|
-
const report = await buildForensicReport({
|
|
1463
|
-
sources: ${serializedSources},
|
|
1464
|
-
includeLegacyGoldenStandard: false,
|
|
1465
|
-
onProgress: (done, total, stage, detail, overall, stageDone, stageTotal) => parentPort?.postMessage({
|
|
1466
|
-
progress: { done, total, stage, detail, overall, stageDone, stageTotal },
|
|
1467
|
-
}),
|
|
1468
|
-
});
|
|
1469
|
-
const degradedReason = ${serializedDegradedReason};
|
|
1470
|
-
if (degradedReason) {
|
|
1471
|
-
report.scanDiagnostics = {
|
|
1472
|
-
degraded: true,
|
|
1473
|
-
reason: degradedReason,
|
|
1474
|
-
skippedSources: ["codex", "claude"],
|
|
1475
|
-
};
|
|
1476
|
-
}
|
|
1477
|
-
const validation = validateForensicReportForSetup(report);
|
|
1478
|
-
if (!validation.ok) {
|
|
1479
|
-
const error = new Error(validation.message);
|
|
1480
|
-
error.code = validation.code;
|
|
1481
|
-
throw error;
|
|
1482
|
-
}
|
|
1483
|
-
parentPort?.postMessage({ ok: true, report });
|
|
1484
|
-
} catch (error) {
|
|
1485
|
-
parentPort?.postMessage({
|
|
1486
|
-
ok: false,
|
|
1487
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1488
|
-
code: error && typeof error === "object" && "code" in error ? String(error.code || "") : "",
|
|
1489
|
-
});
|
|
1490
|
-
}
|
|
1491
|
-
`;
|
|
1492
|
-
const requestedHeapMb = options.maxOldGenerationSizeMb;
|
|
1493
|
-
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`), Number.isFinite(requestedHeapMb)
|
|
1494
|
-
? { resourceLimits: { maxOldGenerationSizeMb: Math.max(16, Math.floor(requestedHeapMb)) } }
|
|
1495
|
-
: undefined);
|
|
1496
|
-
return new Promise((resolve, reject) => {
|
|
1497
|
-
let settled = false;
|
|
1498
|
-
const requestedTimeoutMs = options.timeoutMs ?? 15 * 60_000;
|
|
1499
|
-
const timeoutMs = Number.isFinite(requestedTimeoutMs) ? Math.max(1, requestedTimeoutMs) : 15 * 60_000;
|
|
1500
|
-
const timeout = setTimeout(() => {
|
|
1501
|
-
if (settled)
|
|
1502
|
-
return;
|
|
1503
|
-
settled = true;
|
|
1504
|
-
void worker.terminate();
|
|
1505
|
-
const error = new Error(`Local forensic report timed out after ${timeoutMs}ms`);
|
|
1506
|
-
error.code = "REPORT_SCAN_TIMEOUT";
|
|
1507
|
-
reject(error);
|
|
1508
|
-
}, timeoutMs);
|
|
1509
|
-
timeout.unref?.();
|
|
1510
|
-
const finish = (result) => {
|
|
1511
|
-
if (settled)
|
|
1512
|
-
return;
|
|
1513
|
-
settled = true;
|
|
1514
|
-
clearTimeout(timeout);
|
|
1515
|
-
void worker.terminate();
|
|
1516
|
-
if (result.ok)
|
|
1517
|
-
resolve(result.report);
|
|
1518
|
-
else
|
|
1519
|
-
reject(result.error);
|
|
1520
|
-
};
|
|
1521
|
-
worker.on("message", (message) => {
|
|
1522
|
-
if (settled)
|
|
1523
|
-
return;
|
|
1524
|
-
const msg = message;
|
|
1525
|
-
if (msg.progress) {
|
|
1526
|
-
onProgress?.(msg.progress);
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
if (msg.ok === true && msg.report && typeof msg.report === "object") {
|
|
1530
|
-
finish({ ok: true, report: msg.report });
|
|
1531
|
-
return;
|
|
1532
|
-
}
|
|
1533
|
-
const error = new Error(typeof msg.message === "string" ? msg.message : "Local forensic report failed");
|
|
1534
|
-
if (typeof msg.code === "string" && msg.code)
|
|
1535
|
-
error.code = msg.code;
|
|
1536
|
-
finish({ ok: false, error });
|
|
1537
|
-
});
|
|
1538
|
-
worker.once("error", (error) => {
|
|
1539
|
-
finish({ ok: false, error });
|
|
1540
|
-
});
|
|
1541
|
-
worker.once("exit", (code) => {
|
|
1542
|
-
if (settled)
|
|
1543
|
-
return;
|
|
1544
|
-
finish({ ok: false, error: new Error(`Forensic report worker exited (code ${code}) without a result`) });
|
|
1545
|
-
});
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
1515
|
export function respondMigrate(res, body, status = 200) {
|
|
1549
1516
|
if (res.writableEnded)
|
|
1550
1517
|
return;
|
|
1551
1518
|
res.writeHead(status, { "Content-Type": "application/json" }).end(JSON.stringify(body));
|
|
1552
1519
|
}
|
|
1553
|
-
function safeForensicError(error) {
|
|
1554
|
-
const code = error && typeof error === "object" && "code" in error
|
|
1555
|
-
? String(error.code || "")
|
|
1556
|
-
: "";
|
|
1557
|
-
if (code === "REPORT_SCAN_TIMEOUT") {
|
|
1558
|
-
return {
|
|
1559
|
-
code,
|
|
1560
|
-
message: "The local workspace scan took too long and was stopped. No backup data was substituted. Rerun setup to retry.",
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
return {
|
|
1564
|
-
code: "REPORT_BUILD_FAILED",
|
|
1565
|
-
message: "EchoMem could not finish the local workspace scan. No backup data was substituted. Rerun setup to retry.",
|
|
1566
|
-
};
|
|
1567
|
-
}
|
|
1568
|
-
function publicRunningForensicProgress(value) {
|
|
1569
|
-
if (!value || typeof value !== "object")
|
|
1570
|
-
return null;
|
|
1571
|
-
const progress = value;
|
|
1572
|
-
if (progress.status !== "running")
|
|
1573
|
-
return null;
|
|
1574
|
-
const safeCount = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
|
|
1575
|
-
? Math.floor(candidate)
|
|
1576
|
-
: 0);
|
|
1577
|
-
const safeDuration = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
|
|
1578
|
-
? candidate
|
|
1579
|
-
: 0);
|
|
1580
|
-
const safeFraction = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate)
|
|
1581
|
-
? Math.min(1, Math.max(0, candidate))
|
|
1582
|
-
: 0);
|
|
1583
|
-
const total = safeCount(progress.total);
|
|
1584
|
-
const stageTotal = safeCount(progress.stageTotal);
|
|
1585
|
-
const rawStage = typeof progress.stage === "string" ? progress.stage : "starting";
|
|
1586
|
-
const stage = ["starting", "reading-transcripts", "building-summary", "classifying-repeated-context", "finalizing-report"].includes(rawStage)
|
|
1587
|
-
? rawStage
|
|
1588
|
-
: "starting";
|
|
1589
|
-
return {
|
|
1590
|
-
status: "running",
|
|
1591
|
-
scanned: total > 0 ? Math.min(safeCount(progress.scanned), total) : 0,
|
|
1592
|
-
total,
|
|
1593
|
-
stage,
|
|
1594
|
-
label: forensicStageLabel(stage),
|
|
1595
|
-
stageDone: stageTotal > 0 ? Math.min(safeCount(progress.stageDone), stageTotal) : 0,
|
|
1596
|
-
stageTotal,
|
|
1597
|
-
overall: safeFraction(progress.overall),
|
|
1598
|
-
elapsedMs: safeDuration(progress.elapsedMs),
|
|
1599
|
-
stageElapsedMs: safeDuration(progress.stageElapsedMs),
|
|
1600
|
-
updatedAt: safeDuration(progress.updatedAt) || Date.now(),
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
1520
|
/**
|
|
1604
1521
|
* Start the persistent localhost bridge used by the setup page. It sends/verifies OTP through the
|
|
1605
1522
|
* hosted API, accepts the local passphrase, serves local Wrapped stats, and holds the /migrate
|
|
@@ -1612,12 +1529,11 @@ export function startCallbackServer(opts = {}) {
|
|
|
1612
1529
|
: `${Math.ceil(timeoutMs / 1000)} seconds`;
|
|
1613
1530
|
const dashboardTimeoutMs = 4 * 60 * 60 * 1000;
|
|
1614
1531
|
const expectedNonce = opts.nonce;
|
|
1615
|
-
const scanId = opts.scanId ?? randomUUID();
|
|
1616
1532
|
const flow = opts.flow ?? "onboarding";
|
|
1617
1533
|
const isLoginFlow = flow === "login";
|
|
1618
1534
|
// A login screen must not be blocked by a local-history permission. That permission belongs to
|
|
1619
1535
|
// onboarding and is intentionally enforced separately below.
|
|
1620
|
-
const
|
|
1536
|
+
const requiresLocalHistoryConsent = opts.requireLocalHistoryConsent === true && !isLoginFlow;
|
|
1621
1537
|
return new Promise((resolveOuter, rejectOuter) => {
|
|
1622
1538
|
const onToken = deferred();
|
|
1623
1539
|
const setupExit = deferred();
|
|
@@ -1630,7 +1546,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1630
1546
|
let activeDeviceToken = opts.initialToken?.token || "";
|
|
1631
1547
|
let activeAccountEmail = "";
|
|
1632
1548
|
let pendingLocalAuth = null;
|
|
1633
|
-
let
|
|
1549
|
+
let localHistoryConsentGranted = !requiresLocalHistoryConsent;
|
|
1634
1550
|
let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
1635
1551
|
let migrateStarted = false;
|
|
1636
1552
|
let tokenRefreshHandler = null;
|
|
@@ -1697,7 +1613,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1697
1613
|
const handleCallback = (res, token, key, nonce) => {
|
|
1698
1614
|
if (!checkNonce(nonce))
|
|
1699
1615
|
return void text(res, 403, "bad nonce");
|
|
1700
|
-
if (
|
|
1616
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
1701
1617
|
return void json(res, 403, {
|
|
1702
1618
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1703
1619
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1727,7 +1643,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1727
1643
|
text(res, 403, "bad nonce");
|
|
1728
1644
|
return true;
|
|
1729
1645
|
}
|
|
1730
|
-
if (
|
|
1646
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
1731
1647
|
json(res, 403, {
|
|
1732
1648
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1733
1649
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1754,8 +1670,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1754
1670
|
armTimeout();
|
|
1755
1671
|
};
|
|
1756
1672
|
const isOnboardingOnlyRoute = (route) => [
|
|
1757
|
-
"/
|
|
1758
|
-
"/report",
|
|
1673
|
+
"/local-history-consent",
|
|
1759
1674
|
"/stats",
|
|
1760
1675
|
"/billing-status",
|
|
1761
1676
|
"/billing-checkout",
|
|
@@ -1948,10 +1863,6 @@ export function startCallbackServer(opts = {}) {
|
|
|
1948
1863
|
message: "Run `echomem-mcp init` to access local-history onboarding.",
|
|
1949
1864
|
});
|
|
1950
1865
|
}
|
|
1951
|
-
if ((route === "/city" || route.startsWith("/city/")) && req.method === "GET") {
|
|
1952
|
-
serveRepoCityAsset(route, res);
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
1866
|
if (route.startsWith("/hud-assets/") && req.method === "GET") {
|
|
1956
1867
|
serveHudAsset(route, res);
|
|
1957
1868
|
return;
|
|
@@ -1989,8 +1900,13 @@ export function startCallbackServer(opts = {}) {
|
|
|
1989
1900
|
localOnly: true,
|
|
1990
1901
|
localAuth: true,
|
|
1991
1902
|
workspacePath: process.cwd(),
|
|
1992
|
-
consentRequired:
|
|
1993
|
-
consentGranted:
|
|
1903
|
+
consentRequired: requiresLocalHistoryConsent,
|
|
1904
|
+
consentGranted: localHistoryConsentGranted,
|
|
1905
|
+
platform: process.platform,
|
|
1906
|
+
capabilities: {
|
|
1907
|
+
openClaudeDesktop: process.platform === "darwin" || process.platform === "win32",
|
|
1908
|
+
openAgentSessions: process.platform === "darwin" || process.platform === "win32",
|
|
1909
|
+
},
|
|
1994
1910
|
});
|
|
1995
1911
|
return;
|
|
1996
1912
|
}
|
|
@@ -2094,7 +2010,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2094
2010
|
if (route === "/stats" && req.method === "GET") {
|
|
2095
2011
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2096
2012
|
return void text(res, 403, "bad nonce");
|
|
2097
|
-
if (
|
|
2013
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2098
2014
|
return void json(res, 403, {
|
|
2099
2015
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2100
2016
|
message: "Allow local history access before continuing setup.",
|
|
@@ -2110,7 +2026,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2110
2026
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
2111
2027
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2112
2028
|
return void text(res, 403, "bad nonce");
|
|
2113
|
-
if (
|
|
2029
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2114
2030
|
return void json(res, 403, {
|
|
2115
2031
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2116
2032
|
message: "Allow local history access before continuing setup.",
|
|
@@ -2234,7 +2150,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2234
2150
|
}
|
|
2235
2151
|
if (!checkNonce(asString(body.nonce)))
|
|
2236
2152
|
return void text(res, 403, "bad nonce");
|
|
2237
|
-
if (
|
|
2153
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2238
2154
|
return void json(res, 403, {
|
|
2239
2155
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2240
2156
|
message: "Allow local history access before managing an onboarding plan.",
|
|
@@ -2300,114 +2216,10 @@ export function startCallbackServer(opts = {}) {
|
|
|
2300
2216
|
}
|
|
2301
2217
|
return;
|
|
2302
2218
|
}
|
|
2303
|
-
if (route === "/report" && req.method === "GET") {
|
|
2304
|
-
// Local forensic "Context Doctor" report — computed locally, served BEFORE auth (scan-first).
|
|
2305
|
-
res.setHeader("Cache-Control", "no-store");
|
|
2306
|
-
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2307
|
-
return void text(res, 403, "bad nonce");
|
|
2308
|
-
if (requiresReportConsent && !reportConsentGranted) {
|
|
2309
|
-
return void json(res, 403, {
|
|
2310
|
-
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2311
|
-
message: "Allow local history access before starting the local scan.",
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
let payload;
|
|
2315
|
-
try {
|
|
2316
|
-
payload = opts.getReport ? opts.getReport() : null;
|
|
2317
|
-
}
|
|
2318
|
-
catch {
|
|
2319
|
-
return void json(res, 500, {
|
|
2320
|
-
schemaVersion: 1,
|
|
2321
|
-
kind: "failed",
|
|
2322
|
-
mode: "production",
|
|
2323
|
-
scanId,
|
|
2324
|
-
error: {
|
|
2325
|
-
code: "REPORT_STATE_UNAVAILABLE",
|
|
2326
|
-
message: "EchoMem could not read the local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
2327
|
-
},
|
|
2328
|
-
});
|
|
2329
|
-
}
|
|
2330
|
-
if (payload == null) {
|
|
2331
|
-
// 202 carries scan progress so the page can show a live "scanned N/total" indicator.
|
|
2332
|
-
let prog;
|
|
2333
|
-
try {
|
|
2334
|
-
prog = opts.getReportProgress ? opts.getReportProgress() : {
|
|
2335
|
-
status: "running",
|
|
2336
|
-
scanned: 0,
|
|
2337
|
-
total: 0,
|
|
2338
|
-
stage: "starting",
|
|
2339
|
-
label: "Starting local scan",
|
|
2340
|
-
elapsedMs: 0,
|
|
2341
|
-
stageElapsedMs: 0,
|
|
2342
|
-
updatedAt: Date.now(),
|
|
2343
|
-
};
|
|
2344
|
-
}
|
|
2345
|
-
catch {
|
|
2346
|
-
return void json(res, 500, {
|
|
2347
|
-
schemaVersion: 1,
|
|
2348
|
-
kind: "failed",
|
|
2349
|
-
mode: "production",
|
|
2350
|
-
scanId,
|
|
2351
|
-
error: {
|
|
2352
|
-
code: "REPORT_STATE_UNAVAILABLE",
|
|
2353
|
-
message: "EchoMem could not read the local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
2354
|
-
},
|
|
2355
|
-
});
|
|
2356
|
-
}
|
|
2357
|
-
if (prog && typeof prog === "object" && prog.status === "failed") {
|
|
2358
|
-
return void json(res, 500, {
|
|
2359
|
-
schemaVersion: 1,
|
|
2360
|
-
kind: "failed",
|
|
2361
|
-
mode: "production",
|
|
2362
|
-
scanId,
|
|
2363
|
-
error: safeForensicError(prog.error),
|
|
2364
|
-
});
|
|
2365
|
-
}
|
|
2366
|
-
const publicProgress = publicRunningForensicProgress(prog);
|
|
2367
|
-
if (!publicProgress) {
|
|
2368
|
-
return void json(res, 500, {
|
|
2369
|
-
schemaVersion: 1,
|
|
2370
|
-
kind: "failed",
|
|
2371
|
-
mode: "production",
|
|
2372
|
-
scanId,
|
|
2373
|
-
error: {
|
|
2374
|
-
code: "REPORT_STATE_INVALID",
|
|
2375
|
-
message: "EchoMem received an invalid local scan state. No backup data was substituted. Rerun setup to retry.",
|
|
2376
|
-
},
|
|
2377
|
-
});
|
|
2378
|
-
}
|
|
2379
|
-
return void json(res, 202, {
|
|
2380
|
-
schemaVersion: 1,
|
|
2381
|
-
kind: "scanning",
|
|
2382
|
-
mode: "production",
|
|
2383
|
-
scanId,
|
|
2384
|
-
progress: publicProgress,
|
|
2385
|
-
});
|
|
2386
|
-
}
|
|
2387
|
-
const validation = validateForensicReportForSetup(payload);
|
|
2388
|
-
if (!validation.ok) {
|
|
2389
|
-
console.error(`[echomem] local report validation failed: ${validation.code} — ${validation.message}`);
|
|
2390
|
-
return void json(res, 500, {
|
|
2391
|
-
schemaVersion: 1,
|
|
2392
|
-
kind: "failed",
|
|
2393
|
-
mode: "production",
|
|
2394
|
-
scanId,
|
|
2395
|
-
error: { code: validation.code, message: validation.message },
|
|
2396
|
-
});
|
|
2397
|
-
}
|
|
2398
|
-
json(res, 200, {
|
|
2399
|
-
schemaVersion: 1,
|
|
2400
|
-
kind: validation.kind,
|
|
2401
|
-
mode: "production",
|
|
2402
|
-
scanId,
|
|
2403
|
-
report: validation.report,
|
|
2404
|
-
});
|
|
2405
|
-
return;
|
|
2406
|
-
}
|
|
2407
2219
|
if (route === "/progress" && req.method === "GET") {
|
|
2408
2220
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
2409
2221
|
return void text(res, 403, "bad nonce");
|
|
2410
|
-
if (
|
|
2222
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2411
2223
|
return void json(res, 403, {
|
|
2412
2224
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2413
2225
|
message: "Allow local history access before continuing setup.",
|
|
@@ -2416,7 +2228,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2416
2228
|
json(res, 200, progress);
|
|
2417
2229
|
return;
|
|
2418
2230
|
}
|
|
2419
|
-
if (route === "/
|
|
2231
|
+
if (route === "/local-history-consent" && req.method === "POST") {
|
|
2420
2232
|
let body;
|
|
2421
2233
|
try {
|
|
2422
2234
|
body = await readJsonBody(req);
|
|
@@ -2428,8 +2240,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2428
2240
|
if (!checkNonce(asString(body.nonce)))
|
|
2429
2241
|
return void text(res, 403, "bad nonce");
|
|
2430
2242
|
const allowed = body.allowed === true;
|
|
2431
|
-
|
|
2432
|
-
opts.onReportConsent?.(allowed);
|
|
2243
|
+
localHistoryConsentGranted = allowed;
|
|
2433
2244
|
json(res, 200, { ok: true, allowed });
|
|
2434
2245
|
return;
|
|
2435
2246
|
}
|
|
@@ -2488,7 +2299,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2488
2299
|
}
|
|
2489
2300
|
if (!checkNonce(asString(body.nonce)))
|
|
2490
2301
|
return void text(res, 403, "bad nonce");
|
|
2491
|
-
if (
|
|
2302
|
+
if (requiresLocalHistoryConsent && !localHistoryConsentGranted) {
|
|
2492
2303
|
return void json(res, 403, {
|
|
2493
2304
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
2494
2305
|
message: "Allow local history access before starting extraction.",
|
|
@@ -2747,7 +2558,10 @@ async function cmdSetup(flags) {
|
|
|
2747
2558
|
const entry = buildServerEntry({ devEntryPath: typeof flags.dev === "string" ? flags.dev : undefined });
|
|
2748
2559
|
const requested = typeof flags.client === "string" ? flags.client : undefined;
|
|
2749
2560
|
const targets = selectSetupTargets(requested, Boolean(flags.all));
|
|
2561
|
+
// --dev is already an explicit request to replace the managed runtime with a checkout.
|
|
2562
|
+
const forceHeadless = flags["force-headless"] === true || typeof flags.dev === "string";
|
|
2750
2563
|
const configurationFailures = [];
|
|
2564
|
+
const configuredTargets = [];
|
|
2751
2565
|
if (targets.length === 0) {
|
|
2752
2566
|
console.log("No client auto-detected. Add this MCP server entry manually:\n");
|
|
2753
2567
|
console.log(JSON.stringify({ echomem: entry }, null, 2));
|
|
@@ -2755,52 +2569,91 @@ async function cmdSetup(flags) {
|
|
|
2755
2569
|
}
|
|
2756
2570
|
else {
|
|
2757
2571
|
for (const c of targets) {
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
const result = writeCodexConfig(c.configPath, entry);
|
|
2764
|
-
if (result === "wrote")
|
|
2765
|
-
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath} — start a new Codex session to load it.`);
|
|
2766
|
-
else
|
|
2767
|
-
console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
|
|
2768
|
-
}
|
|
2769
|
-
else {
|
|
2770
|
-
const result = c.id === "claude-code" ? writeClaudeCodeConfig(entry) : "unavailable";
|
|
2771
|
-
if (result !== "unavailable" && result.state === "wrote") {
|
|
2772
|
-
console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
|
|
2773
|
-
if (result.removedLocalProjects.length > 0) {
|
|
2774
|
-
console.log(`✅ Removed ${result.removedLocalProjects.length} stale Claude Code project-local EchoMem ${result.removedLocalProjects.length === 1 ? "entry" : "entries"}.`);
|
|
2572
|
+
try {
|
|
2573
|
+
if (c.kind === "json") {
|
|
2574
|
+
const result = writeJsonClientConfig(c.configPath, entry, { forceHeadless });
|
|
2575
|
+
if (result === "desktop-managed") {
|
|
2576
|
+
console.log(`✅ Kept the valid externally managed EchoMem entry for ${c.label}: ${c.configPath}`);
|
|
2775
2577
|
}
|
|
2776
|
-
|
|
2777
|
-
console.log(
|
|
2578
|
+
else {
|
|
2579
|
+
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath}`);
|
|
2778
2580
|
}
|
|
2581
|
+
configuredTargets.push(c);
|
|
2582
|
+
}
|
|
2583
|
+
else if (c.kind === "command") {
|
|
2584
|
+
const result = writeCodexConfig(c.configPath, entry, { forceHeadless });
|
|
2585
|
+
if (result === "wrote")
|
|
2586
|
+
console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath} — start a new Codex session to load it.`);
|
|
2587
|
+
else if (result === "desktop-managed")
|
|
2588
|
+
console.log(`✅ Kept the valid externally managed EchoMem entry for ${c.label}: ${c.configPath}`);
|
|
2589
|
+
else
|
|
2590
|
+
console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
|
|
2591
|
+
configuredTargets.push(c);
|
|
2779
2592
|
}
|
|
2780
2593
|
else {
|
|
2781
|
-
const
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2594
|
+
const result = c.id === "claude-code"
|
|
2595
|
+
? writeClaudeCodeConfig(entry, { forceHeadless })
|
|
2596
|
+
: "unavailable";
|
|
2597
|
+
if (result !== "unavailable" && result.state === "wrote") {
|
|
2598
|
+
if (result.preservedDesktopManaged) {
|
|
2599
|
+
console.log(`✅ Kept the valid externally managed EchoMem user entry for ${c.label}.`);
|
|
2600
|
+
}
|
|
2601
|
+
else {
|
|
2602
|
+
console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
|
|
2603
|
+
}
|
|
2604
|
+
if (result.removedLocalProjects.length > 0) {
|
|
2605
|
+
console.log(`✅ Removed ${result.removedLocalProjects.length} stale Claude Code project-local EchoMem ${result.removedLocalProjects.length === 1 ? "entry" : "entries"}.`);
|
|
2606
|
+
}
|
|
2607
|
+
if (result.skippedLocalProjects.length > 0) {
|
|
2608
|
+
console.log(`ℹ️ Ignored ${result.skippedLocalProjects.length} EchoMem local ${result.skippedLocalProjects.length === 1 ? "entry" : "entries"} for deleted project directories; they cannot shadow the user entry.`);
|
|
2609
|
+
}
|
|
2610
|
+
configuredTargets.push(c);
|
|
2611
|
+
}
|
|
2612
|
+
else {
|
|
2613
|
+
const failedProjects = result === "unavailable" ? [] : result.failedLocalProjects;
|
|
2614
|
+
const reason = result === "unavailable" ? undefined : result.failureReason;
|
|
2615
|
+
configurationFailures.push({
|
|
2616
|
+
client: c,
|
|
2617
|
+
reason: failedProjects.length > 0
|
|
2618
|
+
? `${c.label} still has project-local EchoMem overrides in: ${failedProjects.join(", ")}`
|
|
2619
|
+
: `${c.label} user-scoped EchoMem entry could not be verified${reason ? `: ${reason}` : ""}`,
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2785
2622
|
}
|
|
2786
2623
|
}
|
|
2624
|
+
catch (error) {
|
|
2625
|
+
configurationFailures.push({
|
|
2626
|
+
client: c,
|
|
2627
|
+
reason: `${c.label} configuration was left unchanged: ${error instanceof Error ? error.message : String(error)}`,
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2787
2630
|
}
|
|
2788
2631
|
}
|
|
2789
2632
|
if (configurationFailures.length > 0) {
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2633
|
+
const continueOnClientError = flags["continue-on-client-error"] === true;
|
|
2634
|
+
const failureMessage = [
|
|
2635
|
+
continueOnClientError
|
|
2636
|
+
? "EchoMem could not configure every detected client."
|
|
2637
|
+
: "EchoMem MCP configuration is incomplete; onboarding was stopped before login/import.",
|
|
2638
|
+
...configurationFailures.map((failure) => `- ${failure.reason}`),
|
|
2639
|
+
...configurationFailures.map((failure) => `Retry ${failure.client.label} with: ${MCP_UPDATE_COMMAND} --client ${failure.client.id}`),
|
|
2640
|
+
].join("\n");
|
|
2641
|
+
if (continueOnClientError) {
|
|
2642
|
+
console.log(`⚠️ ${failureMessage}`);
|
|
2643
|
+
console.log("Continuing with the clients that connected successfully. You can repair the remaining client later.");
|
|
2644
|
+
}
|
|
2645
|
+
else {
|
|
2646
|
+
throw new Error(failureMessage);
|
|
2647
|
+
}
|
|
2795
2648
|
}
|
|
2796
2649
|
if (!flags["no-agents-md"]) {
|
|
2797
|
-
writeMemoryGuidanceForTargets(
|
|
2650
|
+
writeMemoryGuidanceForTargets(configuredTargets);
|
|
2798
2651
|
}
|
|
2799
2652
|
if (!flags["no-codex-skills"]) {
|
|
2800
|
-
writeCodexSkillsForTargets(
|
|
2653
|
+
writeCodexSkillsForTargets(configuredTargets);
|
|
2801
2654
|
}
|
|
2802
2655
|
if (flags["no-save-hooks"] !== true) {
|
|
2803
|
-
writeLifecycleHooksForTargets(
|
|
2656
|
+
writeLifecycleHooksForTargets(configuredTargets);
|
|
2804
2657
|
}
|
|
2805
2658
|
console.log("");
|
|
2806
2659
|
if (flags["skip-login"] || flags["no-login"]) {
|
|
@@ -2812,14 +2665,14 @@ async function cmdSetup(flags) {
|
|
|
2812
2665
|
await cmdLogin(flags);
|
|
2813
2666
|
}
|
|
2814
2667
|
if (flags["with-hud"]) {
|
|
2815
|
-
console.log("ℹ️ The standalone EchoMem HUD has been retired.
|
|
2668
|
+
console.log("ℹ️ The standalone EchoMem HUD has been retired. Use `echomem-mcp status` to inspect this installation.");
|
|
2816
2669
|
}
|
|
2817
2670
|
}
|
|
2818
2671
|
/**
|
|
2819
2672
|
* `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
|
|
2820
2673
|
* machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), installs EchoMem's
|
|
2821
2674
|
* Codex skills and writes the AGENTS.md memory guidance. One browser
|
|
2822
|
-
* bridge then runs permission →
|
|
2675
|
+
* bridge then runs permission → login → plan if needed → extraction in that order.
|
|
2823
2676
|
* `setup`/`login`/`update` remain granular primitives; init picks the full product defaults.
|
|
2824
2677
|
*/
|
|
2825
2678
|
async function cmdInit(flags) {
|
|
@@ -2828,12 +2681,13 @@ async function cmdInit(flags) {
|
|
|
2828
2681
|
await cmdSetup({
|
|
2829
2682
|
...flags,
|
|
2830
2683
|
all: true,
|
|
2684
|
+
"continue-on-client-error": true,
|
|
2831
2685
|
"skip-login": true,
|
|
2832
2686
|
"with-hud": false,
|
|
2833
2687
|
"init-quiet": true,
|
|
2834
2688
|
"install-save-hooks": flags["no-save-hooks"] !== true,
|
|
2835
2689
|
});
|
|
2836
|
-
// 2. Start one ordered onboarding bridge. A fresh device logs in only after consent
|
|
2690
|
+
// 2. Start one ordered onboarding bridge. A fresh device logs in only after consent.
|
|
2837
2691
|
console.log("");
|
|
2838
2692
|
if (!flags["skip-login"] && !flags["no-login"] && !await cmdOnboarding(flags)) {
|
|
2839
2693
|
console.log("\nEchoMem is configured, but onboarding did not finish. Run `echomem-mcp init` again when you are ready.");
|
|
@@ -2841,8 +2695,8 @@ async function cmdInit(flags) {
|
|
|
2841
2695
|
}
|
|
2842
2696
|
console.log("");
|
|
2843
2697
|
console.log("🎉 EchoMem is ready.");
|
|
2844
|
-
console.log(" • MCP memory is configured for
|
|
2845
|
-
console.log(" •
|
|
2698
|
+
console.log(" • MCP memory is configured for the coding agents that connected successfully.");
|
|
2699
|
+
console.log(" • Use `echomem-mcp status` to inspect this device, and `login`, `unlock`, or `update` to manage it.");
|
|
2846
2700
|
console.log(' • Try it now: ask your agent — "search my EchoMem for what I\'ve been working on and recap it."');
|
|
2847
2701
|
}
|
|
2848
2702
|
/**
|
|
@@ -2856,7 +2710,7 @@ function writeMemoryGuidanceForTargets(targets) {
|
|
|
2856
2710
|
if (t.id === "codex" && t.kind === "command")
|
|
2857
2711
|
files.set(path.join(t.detectDir, "AGENTS.md"), "Codex");
|
|
2858
2712
|
if (t.id === "claude-code" || t.id === "claude-desktop")
|
|
2859
|
-
files.set(
|
|
2713
|
+
files.set(path.join(claudeConfigHome(), "CLAUDE.md"), "Claude");
|
|
2860
2714
|
}
|
|
2861
2715
|
for (const [file, label] of files) {
|
|
2862
2716
|
try {
|
|
@@ -2891,22 +2745,28 @@ function writeCodexSkillsForTargets(targets) {
|
|
|
2891
2745
|
}
|
|
2892
2746
|
}
|
|
2893
2747
|
function writeLifecycleHooksForTargets(targets) {
|
|
2894
|
-
const clients =
|
|
2748
|
+
const clients = [];
|
|
2895
2749
|
if (targets.some((target) => target.id === "codex"))
|
|
2896
|
-
clients.
|
|
2750
|
+
clients.push("codex");
|
|
2897
2751
|
if (targets.some((target) => target.id === "claude-code"))
|
|
2898
|
-
clients.
|
|
2899
|
-
if (clients.
|
|
2752
|
+
clients.push("claude-code");
|
|
2753
|
+
if (clients.length === 0) {
|
|
2900
2754
|
console.log("ℹ️ No hook-capable Codex or Claude Code client was detected; private-save checkpoint hooks were not installed.");
|
|
2901
2755
|
return;
|
|
2902
2756
|
}
|
|
2903
|
-
const
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2757
|
+
for (const client of clients) {
|
|
2758
|
+
try {
|
|
2759
|
+
const sourcePaths = installSourceSessionHooks(client);
|
|
2760
|
+
const savePaths = installSaveCheckpointHooks(client);
|
|
2761
|
+
const paths = [...new Set([...sourcePaths, ...savePaths])];
|
|
2762
|
+
console.log(`✅ Installed EchoMem source-session and private-save hooks for ${client}:\n${paths.map((p) => ` - ${p}`).join("\n")}`);
|
|
2763
|
+
if (client === "codex") {
|
|
2764
|
+
console.log(" Codex: start a new session and run /hooks once to review and trust the hook.");
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
catch (error) {
|
|
2768
|
+
console.log(`ℹ️ Could not install EchoMem hooks for ${client}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2769
|
+
}
|
|
2910
2770
|
}
|
|
2911
2771
|
}
|
|
2912
2772
|
async function cmdUpdate(flags) {
|
|
@@ -2922,7 +2782,7 @@ function selectSetupTargets(requested, all) {
|
|
|
2922
2782
|
return fs.existsSync(path.dirname(client.configPath));
|
|
2923
2783
|
if (client.kind === "command")
|
|
2924
2784
|
return fs.existsSync(client.detectDir);
|
|
2925
|
-
return client.id === "claude-code" &&
|
|
2785
|
+
return client.id === "claude-code" && claudeCodeCliAvailable();
|
|
2926
2786
|
});
|
|
2927
2787
|
}
|
|
2928
2788
|
return requested ? knownClients().filter((client) => client.id === requested) : detectClients();
|
|
@@ -2958,7 +2818,7 @@ async function cmdLogin(flags) {
|
|
|
2958
2818
|
return true;
|
|
2959
2819
|
}
|
|
2960
2820
|
// Browser path: this bridge does only account/device authentication. It intentionally exposes
|
|
2961
|
-
// no local-history routes; `init` owns
|
|
2821
|
+
// no local-history routes; `init` owns local-history consent and optional extraction.
|
|
2962
2822
|
console.log("Opening your browser to connect this device locally…");
|
|
2963
2823
|
const { port, nonce } = localBridgeOptions(flags);
|
|
2964
2824
|
const srv = await startCallbackServer({ port, nonce, flow: "login" });
|
|
@@ -2986,7 +2846,7 @@ async function cmdLogin(flags) {
|
|
|
2986
2846
|
}
|
|
2987
2847
|
/**
|
|
2988
2848
|
* The local-history onboarding flow. Existing device credentials are reused when available; a
|
|
2989
|
-
* fresh device stays in this same bridge and asks for login only after permission
|
|
2849
|
+
* fresh device stays in this same bridge and asks for login only after permission.
|
|
2990
2850
|
*/
|
|
2991
2851
|
async function cmdOnboarding(flags) {
|
|
2992
2852
|
const store = new KeyStore();
|
|
@@ -2998,149 +2858,17 @@ async function cmdOnboarding(flags) {
|
|
|
2998
2858
|
console.log("Opening your browser for EchoMem onboarding…");
|
|
2999
2859
|
const { port, nonce } = localBridgeOptions(flags);
|
|
3000
2860
|
let stats = null;
|
|
3001
|
-
let forensicReport = null;
|
|
3002
|
-
let forensicConsent = "pending";
|
|
3003
|
-
let forensicScanStarted = false;
|
|
3004
|
-
const forensicStartedAt = Date.now();
|
|
3005
|
-
let forensicStageStartedAt = forensicStartedAt;
|
|
3006
|
-
let forensicStage = "starting";
|
|
3007
|
-
let forensicOverall = 0;
|
|
3008
|
-
let forensicProgress = {
|
|
3009
|
-
status: "running",
|
|
3010
|
-
scanned: 0,
|
|
3011
|
-
total: 0,
|
|
3012
|
-
stage: forensicStage,
|
|
3013
|
-
label: forensicStageLabel(forensicStage),
|
|
3014
|
-
stageDone: 0,
|
|
3015
|
-
stageTotal: 0,
|
|
3016
|
-
overall: 0,
|
|
3017
|
-
elapsedMs: 0,
|
|
3018
|
-
stageElapsedMs: 0,
|
|
3019
|
-
updatedAt: forensicStartedAt,
|
|
3020
|
-
};
|
|
3021
2861
|
const srv = await startCallbackServer({
|
|
3022
2862
|
port,
|
|
3023
2863
|
nonce,
|
|
3024
2864
|
flow: "onboarding",
|
|
3025
2865
|
initialToken,
|
|
3026
|
-
|
|
3027
|
-
getStats: () => stats,
|
|
3028
|
-
getReport: () => forensicReport,
|
|
3029
|
-
getReportProgress: () => {
|
|
3030
|
-
if (forensicProgress.status !== "running")
|
|
3031
|
-
return forensicProgress;
|
|
3032
|
-
const now = Date.now();
|
|
3033
|
-
const sinceWorkerUpdate = Math.max(0, now - forensicProgress.updatedAt);
|
|
3034
|
-
return {
|
|
3035
|
-
...forensicProgress,
|
|
3036
|
-
elapsedMs: forensicProgress.elapsedMs + sinceWorkerUpdate,
|
|
3037
|
-
stageElapsedMs: forensicProgress.stageElapsedMs + sinceWorkerUpdate,
|
|
3038
|
-
updatedAt: now,
|
|
3039
|
-
};
|
|
3040
|
-
},
|
|
3041
|
-
onReportConsent: (allowed) => {
|
|
3042
|
-
if (!allowed) {
|
|
3043
|
-
forensicConsent = "declined";
|
|
3044
|
-
forensicProgress = {
|
|
3045
|
-
status: "failed",
|
|
3046
|
-
scanned: 0,
|
|
3047
|
-
total: 0,
|
|
3048
|
-
stage: "failed",
|
|
3049
|
-
label: "Local scan skipped",
|
|
3050
|
-
stageDone: 0,
|
|
3051
|
-
stageTotal: 0,
|
|
3052
|
-
overall: 0,
|
|
3053
|
-
elapsedMs: Date.now() - forensicStartedAt,
|
|
3054
|
-
stageElapsedMs: Date.now() - forensicStageStartedAt,
|
|
3055
|
-
updatedAt: Date.now(),
|
|
3056
|
-
error: {
|
|
3057
|
-
code: "REPORT_SCAN_DECLINED",
|
|
3058
|
-
message: "Local file analysis was skipped. EchoMem can still connect, save conversations, and import memories.",
|
|
3059
|
-
},
|
|
3060
|
-
};
|
|
3061
|
-
return;
|
|
3062
|
-
}
|
|
3063
|
-
forensicConsent = "allowed";
|
|
3064
|
-
const now = Date.now();
|
|
3065
|
-
forensicStage = "starting";
|
|
3066
|
-
forensicStageStartedAt = now;
|
|
3067
|
-
forensicOverall = 0;
|
|
3068
|
-
forensicProgress = {
|
|
3069
|
-
status: "running",
|
|
3070
|
-
scanned: 0,
|
|
3071
|
-
total: 0,
|
|
3072
|
-
stage: forensicStage,
|
|
3073
|
-
label: forensicStageLabel(forensicStage),
|
|
3074
|
-
stageDone: 0,
|
|
3075
|
-
stageTotal: 0,
|
|
3076
|
-
overall: 0,
|
|
3077
|
-
elapsedMs: now - forensicStartedAt,
|
|
3078
|
-
stageElapsedMs: 0,
|
|
3079
|
-
updatedAt: now,
|
|
3080
|
-
};
|
|
3081
|
-
startForensicScan();
|
|
3082
|
-
},
|
|
2866
|
+
requireLocalHistoryConsent: true,
|
|
3083
2867
|
});
|
|
3084
2868
|
const localSetupUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
|
|
3085
2869
|
openBrowser(localSetupUrl);
|
|
3086
2870
|
console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
|
|
3087
2871
|
console.log("Waiting for local-history onboarding for up to 15 minutes…");
|
|
3088
|
-
const startForensicScan = () => {
|
|
3089
|
-
if (forensicScanStarted || forensicConsent !== "allowed")
|
|
3090
|
-
return;
|
|
3091
|
-
forensicScanStarted = true;
|
|
3092
|
-
// Build the local forensic "Context Doctor" report off-thread only after explicit consent.
|
|
3093
|
-
buildForensicReportOffThread((progress) => {
|
|
3094
|
-
const now = Date.now();
|
|
3095
|
-
const nextStage = progress.stage || forensicStage;
|
|
3096
|
-
if (nextStage !== forensicStage) {
|
|
3097
|
-
forensicStage = nextStage;
|
|
3098
|
-
forensicStageStartedAt = now;
|
|
3099
|
-
console.log(`Local scan: ${forensicStageLabel(forensicStage)}…`);
|
|
3100
|
-
}
|
|
3101
|
-
// Latched, so a caller that ever reports a smaller fraction cannot walk the bar backwards.
|
|
3102
|
-
forensicOverall = Math.max(forensicOverall, typeof progress.overall === "number" && Number.isFinite(progress.overall) ? progress.overall : 0);
|
|
3103
|
-
forensicProgress = {
|
|
3104
|
-
status: "running",
|
|
3105
|
-
scanned: progress.done,
|
|
3106
|
-
total: progress.total,
|
|
3107
|
-
stage: forensicStage,
|
|
3108
|
-
label: forensicStageLabel(forensicStage),
|
|
3109
|
-
detail: progress.detail,
|
|
3110
|
-
stageDone: typeof progress.stageDone === "number" && Number.isFinite(progress.stageDone)
|
|
3111
|
-
? Math.max(0, Math.floor(progress.stageDone))
|
|
3112
|
-
: 0,
|
|
3113
|
-
stageTotal: typeof progress.stageTotal === "number" && Number.isFinite(progress.stageTotal)
|
|
3114
|
-
? Math.max(0, Math.floor(progress.stageTotal))
|
|
3115
|
-
: 0,
|
|
3116
|
-
overall: forensicOverall,
|
|
3117
|
-
elapsedMs: now - forensicStartedAt,
|
|
3118
|
-
stageElapsedMs: now - forensicStageStartedAt,
|
|
3119
|
-
updatedAt: now,
|
|
3120
|
-
};
|
|
3121
|
-
})
|
|
3122
|
-
.then((r) => {
|
|
3123
|
-
forensicReport = r;
|
|
3124
|
-
})
|
|
3125
|
-
.catch((e) => {
|
|
3126
|
-
const now = Date.now();
|
|
3127
|
-
forensicProgress = {
|
|
3128
|
-
status: "failed",
|
|
3129
|
-
scanned: forensicProgress.scanned,
|
|
3130
|
-
total: forensicProgress.total,
|
|
3131
|
-
stage: "failed",
|
|
3132
|
-
label: "Local scan failed",
|
|
3133
|
-
stageDone: forensicProgress.stageDone,
|
|
3134
|
-
stageTotal: forensicProgress.stageTotal,
|
|
3135
|
-
overall: forensicOverall,
|
|
3136
|
-
elapsedMs: now - forensicStartedAt,
|
|
3137
|
-
stageElapsedMs: now - forensicStageStartedAt,
|
|
3138
|
-
updatedAt: now,
|
|
3139
|
-
error: safeForensicError(e),
|
|
3140
|
-
};
|
|
3141
|
-
console.error(`Could not build the local report: ${e instanceof Error ? e.message : String(e)}`);
|
|
3142
|
-
});
|
|
3143
|
-
};
|
|
3144
2872
|
let token;
|
|
3145
2873
|
let key;
|
|
3146
2874
|
try {
|
|
@@ -3230,9 +2958,8 @@ async function cmdOnboarding(flags) {
|
|
|
3230
2958
|
codex: quick.codexCount,
|
|
3231
2959
|
claudeCode: quick.claudeCount,
|
|
3232
2960
|
};
|
|
3233
|
-
stats =
|
|
2961
|
+
stats = buildOnboardingStatsPayload({
|
|
3234
2962
|
partial: true,
|
|
3235
|
-
skipMemoryCount: true,
|
|
3236
2963
|
sessions: sessionSummary,
|
|
3237
2964
|
migratable,
|
|
3238
2965
|
discovery: { phase: "quick", exact: false },
|
|
@@ -3266,9 +2993,8 @@ async function cmdOnboarding(flags) {
|
|
|
3266
2993
|
codex: cloudSummary.codexCount,
|
|
3267
2994
|
claudeCode: cloudSummary.claudeCount,
|
|
3268
2995
|
};
|
|
3269
|
-
const cloudPayload =
|
|
2996
|
+
const cloudPayload = buildOnboardingStatsPayload({
|
|
3270
2997
|
partial: true,
|
|
3271
|
-
skipMemoryCount: true,
|
|
3272
2998
|
sessions: sessionSummary,
|
|
3273
2999
|
migratable,
|
|
3274
3000
|
discovery: { phase: "account", exact: false },
|
|
@@ -3303,9 +3029,8 @@ async function cmdOnboarding(flags) {
|
|
|
3303
3029
|
codex: unavailableSummary.codexCount,
|
|
3304
3030
|
claudeCode: unavailableSummary.claudeCount,
|
|
3305
3031
|
};
|
|
3306
|
-
const unavailablePayload =
|
|
3032
|
+
const unavailablePayload = buildOnboardingStatsPayload({
|
|
3307
3033
|
partial: true,
|
|
3308
|
-
skipMemoryCount: true,
|
|
3309
3034
|
sessions: sessionSummary,
|
|
3310
3035
|
migratable,
|
|
3311
3036
|
discovery: { phase: "account", exact: false },
|
|
@@ -3341,9 +3066,8 @@ async function cmdOnboarding(flags) {
|
|
|
3341
3066
|
migratable = migratableFromDiscovery(initialExact);
|
|
3342
3067
|
latestPendingEstimate = migratable.pending;
|
|
3343
3068
|
sessionSummary = sessionsFromDiscovery(initialExact);
|
|
3344
|
-
const partialPayload = withCandidateSessions(
|
|
3069
|
+
const partialPayload = withCandidateSessions(buildOnboardingStatsPayload({
|
|
3345
3070
|
partial: true,
|
|
3346
|
-
skipMemoryCount: true,
|
|
3347
3071
|
sessions: sessionSummary,
|
|
3348
3072
|
migratable,
|
|
3349
3073
|
discovery: { phase: "exact", exact: true },
|
|
@@ -3391,12 +3115,10 @@ async function cmdOnboarding(flags) {
|
|
|
3391
3115
|
migratable = migratableFromDiscovery(reconciled);
|
|
3392
3116
|
latestPendingEstimate = migratable.pending;
|
|
3393
3117
|
sessionSummary = sessionsFromDiscovery(reconciled);
|
|
3394
|
-
const reconciledPayload = withCandidateSessions(
|
|
3395
|
-
partial: true,
|
|
3396
|
-
skipMemoryCount: true,
|
|
3118
|
+
const reconciledPayload = withCandidateSessions(buildOnboardingStatsPayload({
|
|
3397
3119
|
sessions: sessionSummary,
|
|
3398
3120
|
migratable,
|
|
3399
|
-
discovery: { phase: "
|
|
3121
|
+
discovery: { phase: "full", exact: true },
|
|
3400
3122
|
}), reconciled);
|
|
3401
3123
|
if (generation !== refreshGeneration)
|
|
3402
3124
|
return;
|
|
@@ -3411,20 +3133,11 @@ async function cmdOnboarding(flags) {
|
|
|
3411
3133
|
failed: 0,
|
|
3412
3134
|
extracted: 0,
|
|
3413
3135
|
});
|
|
3414
|
-
const fullPayload = withCandidateSessions(await buildCollectedStatsPayloadOffThread({
|
|
3415
|
-
sessions: sessionSummary,
|
|
3416
|
-
migratable,
|
|
3417
|
-
discovery: { phase: "full", exact: true },
|
|
3418
|
-
}), reconciled);
|
|
3419
|
-
if (generation !== refreshGeneration)
|
|
3420
|
-
return;
|
|
3421
|
-
stats = fullPayload;
|
|
3422
|
-
srv.setStats(fullPayload);
|
|
3423
3136
|
})().catch((e) => {
|
|
3424
3137
|
if (generation !== refreshGeneration)
|
|
3425
3138
|
return;
|
|
3426
|
-
console.error(`[echomem] optional
|
|
3427
|
-
publishOptionalStatsFallback(generation, "
|
|
3139
|
+
console.error(`[echomem] optional account reconciliation unavailable; continuing (${errorCode(e) || "ACCOUNT_RECONCILIATION_FAILED"})`);
|
|
3140
|
+
publishOptionalStatsFallback(generation, "ACCOUNT_RECONCILIATION_FAILED", true);
|
|
3428
3141
|
});
|
|
3429
3142
|
return initialExact;
|
|
3430
3143
|
}).catch((e) => {
|
|
@@ -3884,6 +3597,7 @@ Usage:
|
|
|
3884
3597
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
3885
3598
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
|
|
3886
3599
|
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
3600
|
+
echomem-mcp setup --force-headless Explicitly replace a valid externally managed entry
|
|
3887
3601
|
echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
|
|
3888
3602
|
echomem-mcp update --all Install this bridge durably + repoint detected clients; no login/browser
|
|
3889
3603
|
echomem-mcp update --client X Repoint one MCP client; no login/browser
|
|
@@ -3894,7 +3608,6 @@ Usage:
|
|
|
3894
3608
|
echomem-mcp status Show token/key/clients
|
|
3895
3609
|
echomem-mcp doctor [--no-network] Diagnose configured client bridge versions
|
|
3896
3610
|
echomem-mcp logout Remove stored credentials
|
|
3897
|
-
echomem-mcp report [--json] Your AI coding memory audit (local, no login, $0)
|
|
3898
3611
|
echomem-mcp migrate [--since D] Import your existing Codex/Claude history into your memory
|
|
3899
3612
|
echomem-mcp migrate --estimate Estimate migration size/time metadata without uploading transcripts
|
|
3900
3613
|
echomem-mcp migrate --max-chars N Import only sessions up to N assembled text chars
|
|
@@ -3946,9 +3659,6 @@ export async function runCli(argv) {
|
|
|
3946
3659
|
case "logout":
|
|
3947
3660
|
cmdLogout();
|
|
3948
3661
|
return true;
|
|
3949
|
-
case "report":
|
|
3950
|
-
await runReport(flags);
|
|
3951
|
-
return true;
|
|
3952
3662
|
case "migrate":
|
|
3953
3663
|
await cmdMigrate(flags);
|
|
3954
3664
|
return true;
|