@echomem/mcp 1.4.47 → 1.4.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -26
- package/dist/context-analysis/claude-native-canonical.js +2 -2
- package/dist/context-analysis/vendored-canonical.js +2 -2
- package/dist/context-analysis/workspace-report.js +3 -3
- package/dist/index.js +5 -8
- package/dist/local-data-paths.js +68 -29
- package/dist/local-jsonl.js +87 -0
- package/dist/migrate.js +8 -9
- package/dist/onboarding-stats.js +16 -0
- package/dist/save-checkpoint-hook.js +1 -1
- package/dist/setup-page/client-core.js +18 -372
- package/dist/setup-page/client-extraction.js +14 -129
- package/dist/setup-page/client-lifecycle.js +28 -101
- package/dist/setup-page/client.js +0 -2
- package/dist/setup-page/styles-extraction.js +1 -31
- package/dist/setup-page/styles-foundation.js +0 -89
- package/dist/setup-page/styles-mvp.js +10 -155
- package/dist/setup-page/styles-website-alignment.js +0 -204
- package/dist/setup-page/styles.js +0 -4
- package/dist/setup-page.js +4 -4
- package/dist/setup-preview.js +4 -212
- package/dist/setup.js +119 -583
- package/dist/v1-contract.js +0 -8
- package/package.json +5 -7
- package/dist/city/README.md +0 -9
- package/dist/city/echo-ai-city-only.html +0 -2232
- package/dist/city/echo-extraction-plate.html +0 -330
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/city/vendor/OrbitControls.js +0 -1417
- package/dist/city/vendor/RoundedBoxGeometry.js +0 -155
- package/dist/city/vendor/echo_general-file-21.riv +0 -0
- package/dist/city/vendor/rive.js +0 -8139
- package/dist/city/vendor/rive.wasm +0 -0
- package/dist/city/vendor/three.module.min.js +0 -6
- package/dist/forensics.js +0 -1531
- package/dist/report.js +0 -721
- package/dist/setup-page/client-report-audit.js +0 -819
- package/dist/setup-page/client-report-city.js +0 -356
- package/dist/setup-page/client-report.js +0 -6
- package/dist/setup-page/styles-city-report.js +0 -880
- package/dist/setup-page/styles-context-audit.js +0 -470
package/README.md
CHANGED
|
@@ -35,18 +35,31 @@ 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
|
|
38
|
+
## Quick start
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|---|---|
|
|
@@ -95,23 +108,6 @@ Both hook entries are merged idempotently into `~/.codex/hooks.json` and
|
|
|
95
108
|
`~/.claude/settings.json`; unrelated user hooks and settings are preserved. Hooks fail open when
|
|
96
109
|
their local input is missing so they cannot prevent a conversation from starting or finishing.
|
|
97
110
|
|
|
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
111
|
### Manual / headless (SSH, containers, CI)
|
|
116
112
|
|
|
117
113
|
No browser? Provide secrets directly — this is the documented headless path:
|
|
@@ -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 {
|
|
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 ?
|
|
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 {
|
|
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 ?
|
|
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 {
|
|
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 ?
|
|
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
|
-
|
|
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/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";
|
|
@@ -1472,10 +1471,6 @@ class EchoMemMCPServer {
|
|
|
1472
1471
|
if (recallRoute.error) {
|
|
1473
1472
|
throw new McpError(ErrorCode.InvalidParams, recallRoute.error);
|
|
1474
1473
|
}
|
|
1475
|
-
// The usage report is a local, $0 audit — works with no login (value before signup).
|
|
1476
|
-
if (canonicalName === canonicalToolNames.report) {
|
|
1477
|
-
return { content: [{ type: "text", text: await buildReportText(false) }] };
|
|
1478
|
-
}
|
|
1479
1474
|
if (canonicalName === canonicalToolNames.updateStatus) {
|
|
1480
1475
|
if (DESKTOP_MANAGED) {
|
|
1481
1476
|
return {
|
|
@@ -1671,8 +1666,7 @@ class EchoMemMCPServer {
|
|
|
1671
1666
|
rec.ok = rec.error_kind === "none";
|
|
1672
1667
|
rec.latency_ms = Date.now() - t0;
|
|
1673
1668
|
this.events.record(rec);
|
|
1674
|
-
if (canonicalName !== canonicalToolNames.
|
|
1675
|
-
canonicalName !== canonicalToolNames.updateStatus &&
|
|
1669
|
+
if (canonicalName !== canonicalToolNames.updateStatus &&
|
|
1676
1670
|
canonicalName !== canonicalToolNames.contextHealth &&
|
|
1677
1671
|
canonicalName !== canonicalToolNames.recompose &&
|
|
1678
1672
|
canonicalName !== canonicalToolNames.save &&
|
|
@@ -2764,6 +2758,9 @@ async function main() {
|
|
|
2764
2758
|
// inherited descriptor — can't leave the terminal hanging after setup/migrate completes.
|
|
2765
2759
|
process.exit(process.exitCode ?? 0);
|
|
2766
2760
|
}
|
|
2761
|
+
// Existing standalone users may receive a new runtime without re-running setup. Refresh only
|
|
2762
|
+
// EchoMem's marker-owned block so stale Desktop-era instructions disappear automatically.
|
|
2763
|
+
refreshInstalledMemoryGuidance();
|
|
2767
2764
|
const store = new KeyStore();
|
|
2768
2765
|
// Serve mode is meant to be SPAWNED by the MCP client (editor), which drives us over a piped stdin.
|
|
2769
2766
|
// A human who runs the bare server in a terminal instead gets a process that blocks forever on stdio
|
package/dist/local-data-paths.js
CHANGED
|
@@ -85,41 +85,80 @@ export function resolveClaudeProjectsDir(opts = {}) {
|
|
|
85
85
|
const root = configuredRoot("CLAUDE_CONFIG_DIR", ".claude", opts);
|
|
86
86
|
return resolveReadableDirectory(root ? path.join(root, "projects") : null);
|
|
87
87
|
}
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
*
|
|
91
|
-
* Cowork does not write into the normal `~/.claude/projects` profile. Claude Desktop creates an
|
|
92
|
-
* OS-specific support directory and stores each local-agent session below
|
|
93
|
-
* `local-agent-mode-sessions/<org>/<conversation>/local_<id>/`. The actual transcript is nested
|
|
94
|
-
* again under that sandbox's `.claude/projects` directory.
|
|
95
|
-
*/
|
|
96
|
-
export function resolveClaudeCoworkSessionsDir(opts = {}) {
|
|
88
|
+
/** Resolve every readable Claude Desktop support directory, including Windows Store/MSIX data. */
|
|
89
|
+
export function resolveClaudeDesktopSupportRoots(opts = {}) {
|
|
97
90
|
const homeDir = normalizedHomeDir(opts.homeDir ?? os.homedir());
|
|
98
91
|
if (!homeDir)
|
|
99
|
-
return
|
|
92
|
+
return [];
|
|
100
93
|
const env = opts.env ?? process.env;
|
|
101
94
|
const configured = env.CLAUDE_DESKTOP_SUPPORT_DIR;
|
|
102
|
-
|
|
95
|
+
const platform = opts.platform ?? process.platform;
|
|
96
|
+
const candidates = [];
|
|
103
97
|
if (typeof configured === "string" && configured.trim()) {
|
|
104
|
-
|
|
98
|
+
candidates.push(expandCurrentUserHome(configured, homeDir));
|
|
105
99
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
100
|
+
if (platform === "darwin") {
|
|
101
|
+
candidates.push(path.join(homeDir, "Library", "Application Support", "Claude"));
|
|
102
|
+
}
|
|
103
|
+
else if (platform === "win32") {
|
|
104
|
+
const appData = typeof env.APPDATA === "string" && env.APPDATA.trim()
|
|
105
|
+
? expandCurrentUserHome(env.APPDATA, homeDir)
|
|
106
|
+
: path.join(homeDir, "AppData", "Roaming");
|
|
107
|
+
if (appData)
|
|
108
|
+
candidates.push(path.join(appData, "Claude"));
|
|
109
|
+
const localAppData = typeof env.LOCALAPPDATA === "string" && env.LOCALAPPDATA.trim()
|
|
110
|
+
? expandCurrentUserHome(env.LOCALAPPDATA, homeDir)
|
|
111
|
+
: path.join(homeDir, "AppData", "Local");
|
|
112
|
+
if (localAppData) {
|
|
113
|
+
const packagesRoot = path.join(localAppData, "Packages");
|
|
114
|
+
try {
|
|
115
|
+
for (const entry of fs.readdirSync(packagesRoot, { withFileTypes: true })) {
|
|
116
|
+
if (!entry.isDirectory() || !/^Claude_/i.test(entry.name))
|
|
117
|
+
continue;
|
|
118
|
+
candidates.push(path.join(packagesRoot, entry.name, "LocalCache", "Roaming", "Claude"));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* A non-Store install has no Packages directory. */
|
|
123
|
+
}
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
|
-
|
|
126
|
+
else {
|
|
127
|
+
const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim()
|
|
128
|
+
? expandCurrentUserHome(env.XDG_CONFIG_HOME, homeDir)
|
|
129
|
+
: path.join(homeDir, ".config");
|
|
130
|
+
if (configHome)
|
|
131
|
+
candidates.push(path.join(configHome, "Claude"));
|
|
132
|
+
}
|
|
133
|
+
const roots = [];
|
|
134
|
+
const seenRealpaths = new Set();
|
|
135
|
+
for (const candidate of candidates) {
|
|
136
|
+
const resolved = resolveReadableDirectory(candidate);
|
|
137
|
+
if (!resolved || seenRealpaths.has(resolved))
|
|
138
|
+
continue;
|
|
139
|
+
seenRealpaths.add(resolved);
|
|
140
|
+
roots.push(resolved);
|
|
141
|
+
}
|
|
142
|
+
return roots;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Resolve every Claude Desktop Cowork sandbox root. Cowork transcripts live below
|
|
146
|
+
* `local-agent-mode-sessions/<org>/<conversation>/local_<id>/.claude/projects`; Windows Store
|
|
147
|
+
* installs can have several private Claude package containers below `Packages`.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveClaudeCoworkSessionRoots(opts = {}) {
|
|
150
|
+
const roots = [];
|
|
151
|
+
const seenRealpaths = new Set();
|
|
152
|
+
for (const supportRoot of resolveClaudeDesktopSupportRoots(opts)) {
|
|
153
|
+
const resolved = resolveReadableDirectory(path.join(supportRoot, "local-agent-mode-sessions"));
|
|
154
|
+
if (!resolved || seenRealpaths.has(resolved))
|
|
155
|
+
continue;
|
|
156
|
+
seenRealpaths.add(resolved);
|
|
157
|
+
roots.push(resolved);
|
|
158
|
+
}
|
|
159
|
+
return roots;
|
|
160
|
+
}
|
|
161
|
+
/** Backward-compatible first-root resolver. New discovery code should scan every returned root. */
|
|
162
|
+
export function resolveClaudeCoworkSessionsDir(opts = {}) {
|
|
163
|
+
return resolveClaudeCoworkSessionRoots(opts)[0] ?? null;
|
|
125
164
|
}
|
|
@@ -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
|
@@ -29,8 +29,8 @@ import axios from "axios";
|
|
|
29
29
|
import { KeyStore, echoConfigDir } from "./keystore.js";
|
|
30
30
|
import { fetchEncryptionConfig } from "./encryption.js";
|
|
31
31
|
import { discoverCodexSessionFiles } from "./codex-session-files.js";
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
32
|
+
import { resolveClaudeCoworkSessionRoots, resolveClaudeProjectsDir } from "./local-data-paths.js";
|
|
33
|
+
import { eachJsonLine, walkLocalFiles } from "./local-jsonl.js";
|
|
34
34
|
const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
35
35
|
const RATE_MAX = 28; // stay under the import-jobs /run limit of 30 / 60s
|
|
36
36
|
const RATE_WINDOW_MS = 60_000;
|
|
@@ -58,7 +58,8 @@ export function assembleCodex(file) {
|
|
|
58
58
|
let firstTs = null;
|
|
59
59
|
let title = "";
|
|
60
60
|
const turns = [];
|
|
61
|
-
|
|
61
|
+
eachJsonLine(file, (value) => {
|
|
62
|
+
const o = value;
|
|
62
63
|
if (o && o.type === "session_meta") {
|
|
63
64
|
sessionId = (o.payload && typeof o.payload.id === "string" && o.payload.id) || sessionId;
|
|
64
65
|
cwd = (o.payload && typeof o.payload.cwd === "string" && o.payload.cwd) || cwd;
|
|
@@ -110,7 +111,8 @@ export function assembleClaude(file, opts = {}) {
|
|
|
110
111
|
let firstTs = null;
|
|
111
112
|
let title = "";
|
|
112
113
|
const turns = [];
|
|
113
|
-
|
|
114
|
+
eachJsonLine(file, (value) => {
|
|
115
|
+
const o = value;
|
|
114
116
|
if (!cwd && typeof o.cwd === "string")
|
|
115
117
|
cwd = o.cwd;
|
|
116
118
|
if (!sessionId && typeof o.sessionId === "string")
|
|
@@ -203,7 +205,7 @@ function userCreatedCodexFiles(codexRoot) {
|
|
|
203
205
|
return userCreatedCodexSessionFiles(codexRoot).map((file) => file.path);
|
|
204
206
|
}
|
|
205
207
|
function userCreatedClaudeFiles(claudeRoot) {
|
|
206
|
-
return
|
|
208
|
+
return walkLocalFiles(claudeRoot, (candidate) => candidate.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows").filter(isUserCreatedClaudeSessionFile);
|
|
207
209
|
}
|
|
208
210
|
function coworkConversationId(filePath, root) {
|
|
209
211
|
const relative = path.relative(root, filePath);
|
|
@@ -230,10 +232,7 @@ function claudeSessionCandidates(opts = {}) {
|
|
|
230
232
|
// Supplying an explicit Claude root is an isolated discovery request (tests, diagnostics, or a
|
|
231
233
|
// moved profile). Callers can still add Cowork roots explicitly. Normal onboarding supplies no
|
|
232
234
|
// roots, so the platform-default Cowork directory is included automatically.
|
|
233
|
-
const
|
|
234
|
-
? resolveClaudeCoworkSessionsDir()
|
|
235
|
-
: null;
|
|
236
|
-
const coworkRoots = opts.coworkRoots ?? (defaultCoworkRoot ? [defaultCoworkRoot] : []);
|
|
235
|
+
const coworkRoots = opts.coworkRoots ?? (opts.claudeRoot === undefined ? resolveClaudeCoworkSessionRoots() : []);
|
|
237
236
|
for (const root of coworkRoots) {
|
|
238
237
|
for (const filePath of userCreatedClaudeFiles(root)) {
|
|
239
238
|
const conversationId = coworkConversationId(filePath, root);
|
|
@@ -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,
|
|
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/);
|