@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.
Files changed (48) hide show
  1. package/README.md +25 -28
  2. package/dist/config-files.js +63 -0
  3. package/dist/context-analysis/claude-native-canonical.js +2 -2
  4. package/dist/context-analysis/vendored-canonical.js +2 -2
  5. package/dist/context-analysis/workspace-report.js +3 -3
  6. package/dist/hud/hooks.js +43 -31
  7. package/dist/index.js +5 -8
  8. package/dist/local-jsonl.js +87 -0
  9. package/dist/migrate.js +6 -4
  10. package/dist/onboarding-stats.js +16 -0
  11. package/dist/save-checkpoint-hook.js +1 -1
  12. package/dist/setup-page/client-core.js +18 -372
  13. package/dist/setup-page/client-extraction.js +19 -134
  14. package/dist/setup-page/client-lifecycle.js +31 -101
  15. package/dist/setup-page/client.js +0 -2
  16. package/dist/setup-page/styles-extraction.js +1 -31
  17. package/dist/setup-page/styles-foundation.js +0 -89
  18. package/dist/setup-page/styles-mvp.js +10 -155
  19. package/dist/setup-page/styles-website-alignment.js +0 -204
  20. package/dist/setup-page/styles.js +0 -4
  21. package/dist/setup-page.js +4 -4
  22. package/dist/setup-preview.js +4 -212
  23. package/dist/setup.js +378 -668
  24. package/dist/v1-contract.js +0 -8
  25. package/package.json +9 -7
  26. package/dist/city/README.md +0 -9
  27. package/dist/city/echo-ai-city-only.html +0 -2232
  28. package/dist/city/echo-extraction-plate.html +0 -330
  29. package/dist/city/echo-face-cutout.png +0 -0
  30. package/dist/city/personality_stickers/bossy.png +0 -0
  31. package/dist/city/personality_stickers/ghosty.png +0 -0
  32. package/dist/city/personality_stickers/loopy.png +0 -0
  33. package/dist/city/personality_stickers/lusty.png +0 -0
  34. package/dist/city/personality_stickers/maxxy.png +0 -0
  35. package/dist/city/personality_stickers/tabby.png +0 -0
  36. package/dist/city/vendor/OrbitControls.js +0 -1417
  37. package/dist/city/vendor/RoundedBoxGeometry.js +0 -155
  38. package/dist/city/vendor/echo_general-file-21.riv +0 -0
  39. package/dist/city/vendor/rive.js +0 -8139
  40. package/dist/city/vendor/rive.wasm +0 -0
  41. package/dist/city/vendor/three.module.min.js +0 -6
  42. package/dist/forensics.js +0 -1531
  43. package/dist/report.js +0 -721
  44. package/dist/setup-page/client-report-audit.js +0 -819
  45. package/dist/setup-page/client-report-city.js +0 -356
  46. package/dist/setup-page/client-report.js +0 -6
  47. package/dist/setup-page/styles-city-report.js +0 -880
  48. package/dist/setup-page/styles-context-audit.js +0 -470
package/README.md CHANGED
@@ -35,22 +35,36 @@ Unencrypted accounts are unaffected. `search_memories` returns retrieved memorie
35
35
  your MCP client does the final answer generation; pass `includeAnswer: true` only if you need the
36
36
  legacy EchoMem-synthesized recall answer.
37
37
 
38
- ## Quick start (recommended)
38
+ ## Quick start
39
39
 
40
- Install Echo Desktop, sign in once, then open **Connect MCP** and choose **Connect Echo**. The app
41
- ships and installs the MCP runtime, configures the detected Codex and Claude hosts, mirrors the same
42
- device credential used by the desktop app, and verifies the MCP handshake. No terminal login is
43
- required.
40
+ The standalone package installs and manages its own durable per-user MCP runtime. It configures
41
+ detected Codex and Claude hosts, connects this device, imports selected local coding history, and
42
+ keeps compatible runtime updates ready for the next agent session.
44
43
 
45
- The standalone HUD has been retired; Echo Desktop is the persistent setup and status surface. The
46
- bridge still reads local agent logs for on-demand context-health tools, but it no longer publishes or
47
- auto-launches an Electron overlay.
44
+ Prerequisite: **Node.js 18 or newer**. Run the command in the same environment as the agent you
45
+ want to configure. Native Windows and WSL have separate home directories and client configs, so a
46
+ WSL install does not configure native Windows apps.
48
47
 
49
- For headless systems and development, the granular CLI commands remain available:
48
+ macOS, Linux, or WSL:
49
+
50
+ ```bash
51
+ npx -y @echomem/mcp@latest init
52
+ ```
53
+
54
+ Windows PowerShell:
55
+
56
+ ```powershell
57
+ npx.cmd -y @echomem/mcp@latest init
58
+ ```
59
+
60
+ The one-off `npx` command is the recommended install path; it stages a durable per-user runtime
61
+ and does not require a global npm install. On Windows, use a new PowerShell window after installing
62
+ Node if `npx.cmd` is not found.
50
63
 
51
64
  | Command | What it does |
52
65
  |---|---|
53
- | `npm i -g @echomem/mcp@latest && echomem-mcp init` | Legacy/headless setup for installed agents + login |
66
+ | `npx -y @echomem/mcp@latest init` | One-command setup for installed agents + login (use `npx.cmd` in Windows PowerShell) |
67
+ | `npm i -g @echomem/mcp@latest`, then `echomem-mcp init` | Optional global install for a persistent CLI command |
54
68
  | `npm i -g @echomem/mcp@latest && echomem-mcp setup` | Install the CLI globally and configure just the detected editor |
55
69
  | `npx -y @echomem/mcp@latest setup` | One-off setup without keeping a global CLI command |
56
70
  | `echomem-mcp setup [--client cursor\|windsurf\|claude-desktop\|claude-code\|codex]` | Write client config + log in |
@@ -95,23 +109,6 @@ Both hook entries are merged idempotently into `~/.codex/hooks.json` and
95
109
  `~/.claude/settings.json`; unrelated user hooks and settings are preserved. Hooks fail open when
96
110
  their local input is missing so they cannot prevent a conversation from starting or finishing.
97
111
 
98
- ## Agent Doctor — local workspace forensics (new in 1.4.9)
99
-
100
- `echomem-mcp init` / `setup` now ends with an **Agent Doctor report**: a local scan of your
101
- Codex and Claude Code session logs that shows what your AI coding actually cost and where the
102
- context went dirty — rendered as an interactive 3D "repo city" plus a full forensic breakdown.
103
-
104
- - **Unified cost ledger.** Every provider request is priced individually from official API list
105
- rates (model, cache write/read, long-context premiums, intro-pricing windows), so the headline
106
- TOTAL SPENT, per-repo costs, per-model costs, and cost components all come from one path and
107
- reconcile exactly. It is an **API-equivalent estimate of volume, not a bill you paid**.
108
- - **Fail-closed validation.** The report refuses to render unless every ledger reconciles
109
- (tokens, costs, canonical waste analysis). Stale or preview data can never appear as your data.
110
- - **Wasteful-context analysis.** Canonical context windows label input tokens useful vs wasteful
111
- (re-reads of unchanged files, re-finding, dead work) and project the waste over your billed input.
112
- - **Fully local, zero upload.** The scan reads local JSONL logs only: no transcripts are uploaded
113
- and no LLM calls are made to build the report.
114
-
115
112
  ### Manual / headless (SSH, containers, CI)
116
113
 
117
114
  No browser? Provide secrets directly — this is the documented headless path:
@@ -140,7 +137,7 @@ npm test # crypto compat + encrypted-local integration tests
140
137
 
141
138
  ### Add to your IDE (Cursor or Windsurf)
142
139
 
143
- > Prefer `npm i -g @echomem/mcp@latest && echomem-mcp init` above — it writes these files for you and keeps secrets out of
140
+ > Prefer `npx -y @echomem/mcp@latest init` above (`npx.cmd` in Windows PowerShell) — it writes these files for you and keeps secrets out of
144
141
  > the client config. The manual steps below are the fallback.
145
142
 
146
143
  #### For Cursor
@@ -0,0 +1,63 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ function isErrno(error, code) {
5
+ return error instanceof Error
6
+ && "code" in error
7
+ && error.code === code;
8
+ }
9
+ /**
10
+ * Read a user-owned JSON configuration without treating corruption as an empty file.
11
+ * Setup must fail closed here: replacing malformed JSON would destroy unrelated client settings.
12
+ */
13
+ export function readJsonObjectFile(file, label = "configuration") {
14
+ let raw;
15
+ try {
16
+ raw = fs.readFileSync(file, "utf8");
17
+ }
18
+ catch (error) {
19
+ if (isErrno(error, "ENOENT"))
20
+ return {};
21
+ throw new Error(`Could not read ${label} at ${file}: ${error instanceof Error ? error.message : String(error)}`);
22
+ }
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(raw);
26
+ }
27
+ catch (error) {
28
+ throw new Error(`Could not safely update ${label} at ${file} because the existing file is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
29
+ }
30
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
31
+ throw new Error(`Could not safely update ${label} at ${file} because the existing JSON root is not an object.`);
32
+ }
33
+ return parsed;
34
+ }
35
+ /** Write beside the destination and rename only after the complete replacement is durable locally. */
36
+ export function atomicWriteTextFile(file, content) {
37
+ const directory = path.dirname(file);
38
+ fs.mkdirSync(directory, { recursive: true });
39
+ const temporary = path.join(directory, `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
40
+ let mode;
41
+ try {
42
+ mode = fs.statSync(file).mode;
43
+ }
44
+ catch {
45
+ /* A fresh file uses the process default mode. */
46
+ }
47
+ try {
48
+ fs.writeFileSync(temporary, content, mode === undefined ? undefined : { mode });
49
+ fs.renameSync(temporary, file);
50
+ }
51
+ catch (error) {
52
+ try {
53
+ fs.rmSync(temporary, { force: true });
54
+ }
55
+ catch {
56
+ /* Preserve the original error. */
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+ export function atomicWriteJsonObject(file, value) {
62
+ atomicWriteTextFile(file, JSON.stringify(value, null, 2));
63
+ }
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { StringDecoder } from "node:string_decoder";
5
5
  import { resolveClaudeProjectsDir } from "../local-data-paths.js";
6
- import { walk } from "../report.js";
6
+ import { walkLocalFiles } from "../local-jsonl.js";
7
7
  import { isStrongPositiveFeedback, } from "./workspace-report.js";
8
8
  const OUTPUT_TOKEN_CAP = 12_000;
9
9
  const IMAGE_TOKENS = 4_000;
@@ -33,7 +33,7 @@ const PROBLEM_META = {
33
33
  const PROBLEM_IDS = Object.keys(PROBLEM_META);
34
34
  export function buildClaudeNativeCanonicalReport(opts) {
35
35
  const claudeRoot = opts?.sessionPaths ? null : resolveClaudeProjectsDir();
36
- const discovered = (opts?.sessionPaths ?? (claudeRoot ? walk(claudeRoot, (candidate) => candidate.endsWith(".jsonl") && !candidate.includes(`${path.sep}subagents${path.sep}`) && !candidate.includes(`${path.sep}workflows${path.sep}`), () => false) : [])).sort();
36
+ const discovered = (opts?.sessionPaths ?? (claudeRoot ? walkLocalFiles(claudeRoot, (candidate) => candidate.endsWith(".jsonl") && !candidate.includes(`${path.sep}subagents${path.sep}`) && !candidate.includes(`${path.sep}workflows${path.sep}`), () => false) : [])).sort();
37
37
  const files = typeof opts?.limitFiles === "number" ? discovered.slice(-Math.max(0, opts.limitFiles)) : discovered;
38
38
  const sessions = [];
39
39
  const errors = [];
@@ -6,7 +6,7 @@ import readline from "node:readline";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { discoverCodexSessionFiles } from "../codex-session-files.js";
8
8
  import { resolveClaudeProjectsDir } from "../local-data-paths.js";
9
- import { walk } from "../report.js";
9
+ import { walkLocalFiles } from "../local-jsonl.js";
10
10
  import { buildClaudeNativeCanonicalReport } from "./claude-native-canonical.js";
11
11
  const PROBLEM_IDS = [
12
12
  "P01", "P02", "P03", "P04", "P05", "P06", "P07", "P08", "P09", "P10", "P11", "P12", "P13",
@@ -89,7 +89,7 @@ export async function buildVendoredCanonicalReport(opts) {
89
89
  .filter(isUserInitiatedSession)
90
90
  : [];
91
91
  const discoveredClaude = sources.includes("claude-code")
92
- ? (opts?.claudeSessionPaths ?? (claudeRoot ? walk(claudeRoot, (candidate) => candidate.endsWith(".jsonl") && !candidate.includes(`${path.sep}subagents${path.sep}`) && !candidate.includes(`${path.sep}workflows${path.sep}`), () => false) : [])).sort()
92
+ ? (opts?.claudeSessionPaths ?? (claudeRoot ? walkLocalFiles(claudeRoot, (candidate) => candidate.endsWith(".jsonl") && !candidate.includes(`${path.sep}subagents${path.sep}`) && !candidate.includes(`${path.sep}workflows${path.sep}`), () => false) : [])).sort()
93
93
  : [];
94
94
  const codexFiles = limitFiles(discoveredCodex, opts?.limitFiles);
95
95
  const claudeFiles = limitFiles(discoveredClaude, opts?.limitFiles);
@@ -3,7 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { discoverCodexSessionFiles } from "../codex-session-files.js";
5
5
  import { resolveClaudeProjectsDir } from "../local-data-paths.js";
6
- import { eachLine, walk } from "../report.js";
6
+ import { eachJsonLine, walkLocalFiles } from "../local-jsonl.js";
7
7
  const PROBLEMS = {
8
8
  P01: { id: "P01", label: "Outdated Images & Screenshots", bucket: "dead", category: "Runtime Bug", confidence: "high" },
9
9
  P02: { id: "P02", label: "Ignored User Instructions", bucket: "refind", category: "Model Behavior", confidence: "medium" },
@@ -52,7 +52,7 @@ export function buildWorkspaceContextReport(opts) {
52
52
  ? discoveredCodexFiles.filter(isUserInitiatedCodexSession)
53
53
  : discoveredCodexFiles;
54
54
  const claudeFiles = sources.includes("claude-code")
55
- ? (opts?.claudeSessionPaths ?? (claudeRoot ? walk(claudeRoot, (p) => p.endsWith(".jsonl") && !p.includes(`${path.sep}subagents${path.sep}`) && !p.includes(`${path.sep}workflows${path.sep}`), () => false) : [])).sort()
55
+ ? (opts?.claudeSessionPaths ?? (claudeRoot ? walkLocalFiles(claudeRoot, (p) => p.endsWith(".jsonl") && !p.includes(`${path.sep}subagents${path.sep}`) && !p.includes(`${path.sep}workflows${path.sep}`), () => false) : [])).sort()
56
56
  : [];
57
57
  const selectedCodex = typeof opts?.limitFiles === "number" ? codexFiles.slice(-Math.max(0, opts.limitFiles)) : codexFiles;
58
58
  const selectedClaude = typeof opts?.limitFiles === "number" ? claudeFiles.slice(-Math.max(0, opts.limitFiles)) : claudeFiles;
@@ -472,7 +472,7 @@ function parseCodexSession(file) {
472
472
  const turns = [];
473
473
  const ledgerItems = [];
474
474
  const callMeta = new Map();
475
- eachLine(file, (raw) => {
475
+ eachJsonLine(file, (raw) => {
476
476
  const top = isRecord(raw) ? raw : {};
477
477
  const payload = isRecord(top.payload) ? top.payload : top;
478
478
  const type = typeof payload.type === "string" ? payload.type : typeof top.type === "string" ? top.type : "";
package/dist/hud/hooks.js CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { atomicWriteJsonObject, readJsonObjectFile } from "../config-files.js";
5
6
  export function installHooks(mode) {
6
7
  const written = [];
7
8
  if (mode === "codex" || mode === "both" || mode === "auto") {
@@ -33,16 +34,16 @@ export function installSourceSessionHooks(mode) {
33
34
  return written;
34
35
  }
35
36
  function installCodexHooks() {
36
- const dir = path.join(os.homedir(), ".codex");
37
+ const dir = profileDirectory("CODEX_HOME", ".codex");
37
38
  const file = path.join(dir, "hooks.json");
38
39
  fs.mkdirSync(dir, { recursive: true });
39
40
  const hookCommand = `${JSON.stringify(process.execPath)} ${JSON.stringify(process.argv[1])} summary --client codex --json`;
40
41
  const content = readHooksFile(file);
41
42
  content.hooks = content.hooks || {};
42
- content.hooks.PostToolUse = mergeHookGroup(content.hooks.PostToolUse, { matcher: "*", hooks: [{ type: "command", command: hookCommand, timeout: 5 }] });
43
- content.hooks.PostCompact = mergeHookGroup(content.hooks.PostCompact, { hooks: [{ type: "command", command: hookCommand, timeout: 5 }] });
44
- content.hooks.Stop = mergeHookGroup(content.hooks.Stop, { hooks: [{ type: "command", command: hookCommand, timeout: 5 }] });
45
- fs.writeFileSync(file, JSON.stringify(content, null, 2));
43
+ content.hooks.PostToolUse = mergeHookGroup(content.hooks.PostToolUse, { matcher: "*", hooks: [codexCommandHook(hookCommand, 5)] });
44
+ content.hooks.PostCompact = mergeHookGroup(content.hooks.PostCompact, { hooks: [codexCommandHook(hookCommand, 5)] });
45
+ content.hooks.Stop = mergeHookGroup(content.hooks.Stop, { hooks: [codexCommandHook(hookCommand, 5)] });
46
+ atomicWriteJsonObject(file, content);
46
47
  return file;
47
48
  }
48
49
  function saveCheckpointCommand() {
@@ -54,24 +55,19 @@ function sourceSessionCommand() {
54
55
  return `${JSON.stringify(process.execPath)} ${JSON.stringify(lifecycleCli)} bind-source-session`;
55
56
  }
56
57
  function installCodexSourceSessionHook() {
57
- const dir = path.join(os.homedir(), ".codex");
58
+ const dir = profileDirectory("CODEX_HOME", ".codex");
58
59
  const file = path.join(dir, "hooks.json");
59
60
  fs.mkdirSync(dir, { recursive: true });
60
61
  const content = readHooksFile(file);
61
62
  content.hooks = content.hooks || {};
62
63
  content.hooks.SessionStart = mergeSourceSessionGroup(content.hooks.SessionStart, {
63
- hooks: [{
64
- type: "command",
65
- command: sourceSessionCommand(),
66
- timeout: 5,
67
- statusMessage: "Binding EchoMem to this conversation",
68
- }],
64
+ hooks: [codexCommandHook(sourceSessionCommand(), 5, "Binding EchoMem to this conversation")],
69
65
  });
70
- fs.writeFileSync(file, JSON.stringify(content, null, 2));
66
+ atomicWriteJsonObject(file, content);
71
67
  return file;
72
68
  }
73
69
  function installClaudeCodeSourceSessionHook() {
74
- const dir = path.join(os.homedir(), ".claude");
70
+ const dir = profileDirectory("CLAUDE_CONFIG_DIR", ".claude");
75
71
  const file = path.join(dir, "settings.json");
76
72
  fs.mkdirSync(dir, { recursive: true });
77
73
  const content = readHooksFile(file);
@@ -84,28 +80,23 @@ function installClaudeCodeSourceSessionHook() {
84
80
  statusMessage: "Binding EchoMem to this conversation",
85
81
  }],
86
82
  });
87
- fs.writeFileSync(file, JSON.stringify(content, null, 2));
83
+ atomicWriteJsonObject(file, content);
88
84
  return file;
89
85
  }
90
86
  function installCodexSaveCheckpointHook() {
91
- const dir = path.join(os.homedir(), ".codex");
87
+ const dir = profileDirectory("CODEX_HOME", ".codex");
92
88
  const file = path.join(dir, "hooks.json");
93
89
  fs.mkdirSync(dir, { recursive: true });
94
90
  const content = readHooksFile(file);
95
91
  content.hooks = content.hooks || {};
96
92
  content.hooks.Stop = mergeSaveCheckpointGroup(content.hooks.Stop, {
97
- hooks: [{
98
- type: "command",
99
- command: saveCheckpointCommand(),
100
- timeout: 10,
101
- statusMessage: "Checking whether completed work should be remembered",
102
- }],
93
+ hooks: [codexCommandHook(saveCheckpointCommand(), 10, "Checking whether completed work should be remembered")],
103
94
  });
104
- fs.writeFileSync(file, JSON.stringify(content, null, 2));
95
+ atomicWriteJsonObject(file, content);
105
96
  return file;
106
97
  }
107
98
  function installClaudeCodeSaveCheckpointHook() {
108
- const dir = path.join(os.homedir(), ".claude");
99
+ const dir = profileDirectory("CLAUDE_CONFIG_DIR", ".claude");
109
100
  const file = path.join(dir, "settings.json");
110
101
  fs.mkdirSync(dir, { recursive: true });
111
102
  const content = readHooksFile(file);
@@ -118,22 +109,43 @@ function installClaudeCodeSaveCheckpointHook() {
118
109
  statusMessage: "Checking whether completed work should be remembered",
119
110
  }],
120
111
  });
121
- fs.writeFileSync(file, JSON.stringify(content, null, 2));
112
+ atomicWriteJsonObject(file, content);
122
113
  return file;
123
114
  }
124
115
  function installClaudeCodeSnippet() {
125
- const dir = path.join(os.homedir(), ".claude", "echo-ctx");
116
+ const dir = path.join(profileDirectory("CLAUDE_CONFIG_DIR", ".claude"), "echo-ctx");
126
117
  fs.mkdirSync(dir, { recursive: true });
127
118
  return dir;
128
119
  }
129
120
  function readHooksFile(file) {
130
- try {
131
- const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
132
- return typeof parsed === "object" && parsed !== null ? parsed : {};
121
+ return readJsonObjectFile(file, "agent hooks");
122
+ }
123
+ function profileDirectory(envKey, fallbackName) {
124
+ const home = os.homedir();
125
+ const configured = process.env[envKey]?.trim();
126
+ if (!configured)
127
+ return path.join(home, fallbackName);
128
+ if (configured === "~")
129
+ return home;
130
+ if (configured.startsWith("~/") || configured.startsWith("~\\")) {
131
+ return path.join(home, configured.slice(2));
133
132
  }
134
- catch {
135
- return {};
133
+ if (!path.isAbsolute(configured)) {
134
+ throw new Error(`${envKey} must be an absolute path or start with ~/`);
136
135
  }
136
+ return path.normalize(configured);
137
+ }
138
+ function codexCommandHook(command, timeout, statusMessage) {
139
+ return {
140
+ type: "command",
141
+ command,
142
+ // Codex supports an explicit Windows override. Keeping it identical is intentional: the
143
+ // absolute node.exe and CLI paths are already quoted, and Codex can select this field without
144
+ // interpreting a POSIX-only fallback.
145
+ commandWindows: command,
146
+ timeout,
147
+ ...(statusMessage ? { statusMessage } : {}),
148
+ };
137
149
  }
138
150
  function mergeHookGroup(existing, group) {
139
151
  const groups = Array.isArray(existing) ? existing.filter((item) => !isEchoHudGroup(item)) : [];
package/dist/index.js CHANGED
@@ -7,12 +7,11 @@ import { ZodError } from "zod";
7
7
  import { bindSourceSessionSchema, canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, recordMemoryCitationsSchema, requestGroupSessionSharingSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } from "./v1-contract.js";
8
8
  import { KeyStore } from "./keystore.js";
9
9
  import { EventLogger, hashText } from "./events.js";
10
- import { buildReportText } from "./report.js";
11
10
  import { contextHealthMarkdown, recomposeCapsuleMarkdown } from "./hud/api.js";
12
11
  import { createHash, randomUUID } from "node:crypto";
13
12
  import { AsyncLocalStorage } from "node:async_hooks";
14
13
  import { fetchEncryptionConfig, decryptMemoryFields, verifyKeyB64, } from "./encryption.js";
15
- import { runCli } from "./setup.js";
14
+ import { refreshInstalledMemoryGuidance, runCli } from "./setup.js";
16
15
  import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS, MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, } from "./package-metadata.js";
17
16
  import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
18
17
  import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
@@ -1411,10 +1410,6 @@ class EchoMemMCPServer {
1411
1410
  if (recallRoute.error) {
1412
1411
  throw new McpError(ErrorCode.InvalidParams, recallRoute.error);
1413
1412
  }
1414
- // The usage report is a local, $0 audit — works with no login (value before signup).
1415
- if (canonicalName === canonicalToolNames.report) {
1416
- return { content: [{ type: "text", text: await buildReportText(false) }] };
1417
- }
1418
1413
  if (canonicalName === canonicalToolNames.updateStatus) {
1419
1414
  if (DESKTOP_MANAGED) {
1420
1415
  return {
@@ -1610,8 +1605,7 @@ class EchoMemMCPServer {
1610
1605
  rec.ok = rec.error_kind === "none";
1611
1606
  rec.latency_ms = Date.now() - t0;
1612
1607
  this.events.record(rec);
1613
- if (canonicalName !== canonicalToolNames.report &&
1614
- canonicalName !== canonicalToolNames.updateStatus &&
1608
+ if (canonicalName !== canonicalToolNames.updateStatus &&
1615
1609
  canonicalName !== canonicalToolNames.contextHealth &&
1616
1610
  canonicalName !== canonicalToolNames.recompose &&
1617
1611
  canonicalName !== canonicalToolNames.save &&
@@ -2703,6 +2697,9 @@ async function main() {
2703
2697
  // inherited descriptor — can't leave the terminal hanging after setup/migrate completes.
2704
2698
  process.exit(process.exitCode ?? 0);
2705
2699
  }
2700
+ // Existing standalone users may receive a new runtime without re-running setup. Refresh only
2701
+ // EchoMem's marker-owned block so stale Desktop-era instructions disappear automatically.
2702
+ refreshInstalledMemoryGuidance();
2706
2703
  const store = new KeyStore();
2707
2704
  // Serve mode is meant to be SPAWNED by the MCP client (editor), which drives us over a piped stdin.
2708
2705
  // A human who runs the bare server in a terminal instead gets a process that blocks forever on stdio
@@ -0,0 +1,87 @@
1
+ import fs from "node:fs";
2
+ import { StringDecoder } from "node:string_decoder";
3
+ import path from "node:path";
4
+ /**
5
+ * Stream a JSONL file without loading large agent transcripts into one string.
6
+ * Malformed lines and unreadable files are skipped so local discovery remains recoverable.
7
+ */
8
+ export function eachJsonLine(file, visit) {
9
+ let descriptor;
10
+ try {
11
+ descriptor = fs.openSync(file, "r");
12
+ }
13
+ catch {
14
+ return;
15
+ }
16
+ try {
17
+ const decoder = new StringDecoder("utf8");
18
+ const buffer = Buffer.allocUnsafe(1 << 20);
19
+ let remainder = "";
20
+ let bytesRead;
21
+ const consume = (chunk) => {
22
+ const lines = (remainder + chunk).split("\n");
23
+ remainder = lines.pop() ?? "";
24
+ for (const line of lines) {
25
+ const value = line.trim();
26
+ if (!value)
27
+ continue;
28
+ try {
29
+ visit(JSON.parse(value));
30
+ }
31
+ catch {
32
+ /* Ignore malformed or partially-written rows. */
33
+ }
34
+ }
35
+ };
36
+ while ((bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, null)) > 0) {
37
+ consume(decoder.write(buffer.subarray(0, bytesRead)));
38
+ }
39
+ const finalValue = (remainder + decoder.end()).trim();
40
+ if (finalValue) {
41
+ try {
42
+ visit(JSON.parse(finalValue));
43
+ }
44
+ catch {
45
+ /* Ignore a malformed or partially-written final row. */
46
+ }
47
+ }
48
+ }
49
+ finally {
50
+ try {
51
+ fs.closeSync(descriptor);
52
+ }
53
+ catch {
54
+ /* Best effort. */
55
+ }
56
+ }
57
+ }
58
+ /** Recursively list matching regular files without following child symlinks. */
59
+ export function walkLocalFiles(directory, matches, skipDirectory, output = []) {
60
+ let entries;
61
+ try {
62
+ entries = fs.readdirSync(directory, { withFileTypes: true });
63
+ }
64
+ catch {
65
+ return output;
66
+ }
67
+ for (const entry of entries) {
68
+ const absolute = path.join(directory, entry.name);
69
+ let stat;
70
+ try {
71
+ stat = fs.lstatSync(absolute);
72
+ }
73
+ catch {
74
+ continue;
75
+ }
76
+ if (stat.isSymbolicLink())
77
+ continue;
78
+ if (stat.isDirectory()) {
79
+ if (!skipDirectory(entry.name))
80
+ walkLocalFiles(absolute, matches, skipDirectory, output);
81
+ }
82
+ else if (stat.isFile() && matches(absolute)) {
83
+ output.push(absolute);
84
+ }
85
+ }
86
+ return output;
87
+ }
package/dist/migrate.js CHANGED
@@ -28,7 +28,7 @@ import { KeyStore, echoConfigDir } from "./keystore.js";
28
28
  import { fetchEncryptionConfig } from "./encryption.js";
29
29
  import { discoverCodexSessionFiles } from "./codex-session-files.js";
30
30
  import { resolveClaudeProjectsDir } from "./local-data-paths.js";
31
- import { walk, eachLine } from "./report.js";
31
+ import { eachJsonLine, walkLocalFiles } from "./local-jsonl.js";
32
32
  const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
33
33
  const RATE_MAX = 28; // stay under the import-jobs /run limit of 30 / 60s
34
34
  const RATE_WINDOW_MS = 60_000;
@@ -56,7 +56,8 @@ export function assembleCodex(file) {
56
56
  let firstTs = null;
57
57
  let title = "";
58
58
  const turns = [];
59
- eachLine(file, (o) => {
59
+ eachJsonLine(file, (value) => {
60
+ const o = value;
60
61
  if (o && o.type === "session_meta") {
61
62
  sessionId = (o.payload && typeof o.payload.id === "string" && o.payload.id) || sessionId;
62
63
  cwd = (o.payload && typeof o.payload.cwd === "string" && o.payload.cwd) || cwd;
@@ -108,7 +109,8 @@ export function assembleClaude(file) {
108
109
  let firstTs = null;
109
110
  let title = "";
110
111
  const turns = [];
111
- eachLine(file, (o) => {
112
+ eachJsonLine(file, (value) => {
113
+ const o = value;
112
114
  if (!cwd && typeof o.cwd === "string")
113
115
  cwd = o.cwd;
114
116
  if (!sessionId && typeof o.sessionId === "string")
@@ -199,7 +201,7 @@ function userCreatedCodexFiles(codexRoot) {
199
201
  return userCreatedCodexSessionFiles(codexRoot).map((file) => file.path);
200
202
  }
201
203
  function userCreatedClaudeFiles(claudeRoot) {
202
- return walk(claudeRoot, (candidate) => candidate.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows").filter(isUserCreatedClaudeSessionFile);
204
+ return walkLocalFiles(claudeRoot, (candidate) => candidate.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows").filter(isUserCreatedClaudeSessionFile);
203
205
  }
204
206
  /** Discover every user-created local session, newest first (by first-turn timestamp). */
205
207
  export function discoverSessions(opts = {}) {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The onboarding page only needs session discovery and import state. Usage-audit calculations
3
+ * (tokens, cost, context reads, recall ratios, and memory totals) are intentionally excluded.
4
+ */
5
+ export function buildOnboardingStatsPayload(input) {
6
+ return {
7
+ schemaVersion: 1,
8
+ ...(input.partial ? { partial: true } : {}),
9
+ discovery: input.discovery,
10
+ generatedFrom: ["~/.codex/sessions", "~/.claude/projects"],
11
+ llmCallsUsed: 0,
12
+ transcriptsUploaded: false,
13
+ sessions: input.sessions,
14
+ migratable: input.migratable,
15
+ };
16
+ }
@@ -10,7 +10,7 @@ const CHECKPOINT_REASON = [
10
10
  "Private persistence happens first. For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save.",
11
11
  "On the first request_group_session_sharing 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. A Share decision syncs eligible memories from later saves carrying the same scope to that group; saves automatically sync to every approved group. Keep private keeps them private for that group.",
12
12
  "Flagged memories are withheld from automatic conversation sync and remain private.",
13
- "If EchoMem reports that the encrypted vault is locked, tell the user to open Echo Desktop and unlock the vault there; on a headless system, use `echomem-mcp unlock`. Never silently skip a qualifying checkpoint.",
13
+ "If EchoMem reports that the encrypted vault is locked, relay its local unlock instruction; for a standalone installation, use `echomem-mcp unlock`. Never silently skip a qualifying checkpoint.",
14
14
  ].join(" ");
15
15
  function currentTurnSlice(transcript) {
16
16
  const lines = transcript.split(/\r?\n/);