@echomem/mcp 1.4.43 → 1.4.45

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.
@@ -85,3 +85,41 @@ 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
+ * Resolve Claude Desktop's local Cowork sandbox root.
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 = {}) {
97
+ const homeDir = normalizedHomeDir(opts.homeDir ?? os.homedir());
98
+ if (!homeDir)
99
+ return null;
100
+ const env = opts.env ?? process.env;
101
+ const configured = env.CLAUDE_DESKTOP_SUPPORT_DIR;
102
+ let supportRoot = null;
103
+ if (typeof configured === "string" && configured.trim()) {
104
+ supportRoot = expandCurrentUserHome(configured, homeDir);
105
+ }
106
+ else {
107
+ const platform = opts.platform ?? process.platform;
108
+ if (platform === "darwin") {
109
+ supportRoot = path.join(homeDir, "Library", "Application Support", "Claude");
110
+ }
111
+ else if (platform === "win32") {
112
+ const appData = typeof env.APPDATA === "string" && env.APPDATA.trim()
113
+ ? expandCurrentUserHome(env.APPDATA, homeDir)
114
+ : path.join(homeDir, "AppData", "Roaming");
115
+ supportRoot = appData ? path.join(appData, "Claude") : null;
116
+ }
117
+ else {
118
+ const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim()
119
+ ? expandCurrentUserHome(env.XDG_CONFIG_HOME, homeDir)
120
+ : path.join(homeDir, ".config");
121
+ supportRoot = configHome ? path.join(configHome, "Claude") : null;
122
+ }
123
+ }
124
+ return resolveReadableDirectory(supportRoot ? path.join(supportRoot, "local-agent-mode-sessions") : null);
125
+ }
package/dist/migrate.js CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * `echomem-mcp migrate` — one-shot bulk back-fill of the user's EXISTING local coding-agent history
3
- * (Codex + Claude Code) into their Echo cloud. The onboarding "see it instantly know my work" moment.
3
+ * (Codex + Claude Code + local Claude Desktop Cowork) into their Echo cloud. The onboarding
4
+ * "see it instantly know my work" moment.
4
5
  *
5
6
  * Why this lives in the bridge (client-side): the session logs exist ONLY on the user's machine
6
- * (~/.codex/sessions, ~/.claude/projects). The bridge discovers each session, assembles it into a
7
+ * (~/.codex/sessions, ~/.claude/projects, Claude Desktop local-agent-mode-sessions). The bridge
8
+ * discovers each session, assembles it into a
7
9
  * text-turn-only `## ` transcript, and feeds it to the shared durable import queue
8
10
  * through MCP-owned policy entrypoints. POST /api/extension/mcp/historical-import-sessions
9
11
  * creates one import_jobs row per session, then the bridge POSTs each transcript to
@@ -27,7 +29,7 @@ import axios from "axios";
27
29
  import { KeyStore, echoConfigDir } from "./keystore.js";
28
30
  import { fetchEncryptionConfig } from "./encryption.js";
29
31
  import { discoverCodexSessionFiles } from "./codex-session-files.js";
30
- import { resolveClaudeProjectsDir } from "./local-data-paths.js";
32
+ import { resolveClaudeCoworkSessionsDir, resolveClaudeProjectsDir } from "./local-data-paths.js";
31
33
  import { walk, eachLine } from "./report.js";
32
34
  const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
33
35
  const RATE_MAX = 28; // stay under the import-jobs /run limit of 30 / 60s
@@ -102,7 +104,7 @@ export function assembleCodex(file) {
102
104
  };
103
105
  }
104
106
  /** Claude Code <uuid>.jsonl → one session. User/assistant text turns are sent; tool calls/results/thinking stay local. */
105
- export function assembleClaude(file) {
107
+ export function assembleClaude(file, opts = {}) {
106
108
  let sessionId = null;
107
109
  let cwd = null;
108
110
  let firstTs = null;
@@ -157,10 +159,12 @@ export function assembleClaude(file) {
157
159
  if (!turns.length)
158
160
  return null;
159
161
  const stat = statSafe(file);
162
+ const source = opts.source ?? "claude-code";
163
+ const conversationId = opts.conversationId ?? sessionId ?? sha16(file);
160
164
  return {
161
165
  filePath: file,
162
- source: "claude-code",
163
- conversationKey: `claude-code:${sessionId || sha16(file)}`,
166
+ source,
167
+ conversationKey: `${source}:${conversationId}`,
164
168
  cwd: normalizeCwd(cwd),
165
169
  firstTs,
166
170
  title: title || "Claude text turns",
@@ -201,6 +205,45 @@ function userCreatedCodexFiles(codexRoot) {
201
205
  function userCreatedClaudeFiles(claudeRoot) {
202
206
  return walk(claudeRoot, (candidate) => candidate.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows").filter(isUserCreatedClaudeSessionFile);
203
207
  }
208
+ function coworkConversationId(filePath, root) {
209
+ const relative = path.relative(root, filePath);
210
+ if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
211
+ return null;
212
+ const parts = relative.split(path.sep);
213
+ for (let index = 0; index + 2 < parts.length; index += 1) {
214
+ if (/^local_[A-Za-z0-9_-]{1,194}$/.test(parts[index] || "")
215
+ && parts[index + 1] === ".claude"
216
+ && parts[index + 2] === "projects") {
217
+ return parts[index] || null;
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+ function claudeSessionCandidates(opts = {}) {
223
+ const candidates = [];
224
+ const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
225
+ if (claudeRoot) {
226
+ for (const filePath of userCreatedClaudeFiles(claudeRoot)) {
227
+ candidates.push({ filePath, source: "claude-code" });
228
+ }
229
+ }
230
+ // Supplying an explicit Claude root is an isolated discovery request (tests, diagnostics, or a
231
+ // moved profile). Callers can still add Cowork roots explicitly. Normal onboarding supplies no
232
+ // roots, so the platform-default Cowork directory is included automatically.
233
+ const defaultCoworkRoot = opts.claudeRoot === undefined
234
+ ? resolveClaudeCoworkSessionsDir()
235
+ : null;
236
+ const coworkRoots = opts.coworkRoots ?? (defaultCoworkRoot ? [defaultCoworkRoot] : []);
237
+ for (const root of coworkRoots) {
238
+ for (const filePath of userCreatedClaudeFiles(root)) {
239
+ const conversationId = coworkConversationId(filePath, root);
240
+ if (!conversationId)
241
+ continue;
242
+ candidates.push({ filePath, source: "claude-desktop", conversationId });
243
+ }
244
+ }
245
+ return candidates;
246
+ }
204
247
  /** Discover every user-created local session, newest first (by first-turn timestamp). */
205
248
  export function discoverSessions(opts = {}) {
206
249
  const out = [];
@@ -209,17 +252,31 @@ export function discoverSessions(opts = {}) {
209
252
  if (s)
210
253
  out.push(s);
211
254
  }
212
- const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
213
- if (claudeRoot) {
214
- for (const f of userCreatedClaudeFiles(claudeRoot)) {
215
- const s = assembleClaude(f);
216
- if (s)
217
- out.push(s);
218
- }
255
+ for (const candidate of claudeSessionCandidates(opts)) {
256
+ const s = assembleClaude(candidate.filePath, candidate);
257
+ if (s)
258
+ out.push(s);
219
259
  }
220
260
  out.sort((a, b) => String(b.firstTs || "").localeCompare(String(a.firstTs || "")));
221
261
  return out;
222
262
  }
263
+ function sourceCounts(sessions, pending) {
264
+ const codexCount = sessions.filter((session) => session.source === "codex").length;
265
+ const claudeCodeCount = sessions.filter((session) => session.source === "claude-code").length;
266
+ const coworkCount = sessions.filter((session) => session.source === "claude-desktop").length;
267
+ const pendingCodex = pending.filter((session) => session.source === "codex").length;
268
+ const pendingClaudeCode = pending.filter((session) => session.source === "claude-code").length;
269
+ const pendingCowork = pending.filter((session) => session.source === "claude-desktop").length;
270
+ return {
271
+ codexCount,
272
+ claudeCount: claudeCodeCount + coworkCount,
273
+ claudeCodeCount,
274
+ coworkCount,
275
+ pendingCodex,
276
+ pendingClaudeCode,
277
+ pendingCowork,
278
+ };
279
+ }
223
280
  const IMPORT_STATUS_CHUNK_SIZE = 1000;
224
281
  function initialJsonObjects(file, maxBytes = 1024 * 1024) {
225
282
  let fd;
@@ -279,7 +336,7 @@ function isUserCreatedClaudeSessionFile(file) {
279
336
  });
280
337
  }
281
338
  function fastSessionInfo(file, source) {
282
- let conversationKey = `${source === "codex" ? "codex" : "claude-code"}:${sha16(file)}`;
339
+ let conversationKey = `${source}:${sha16(file)}`;
283
340
  let hasTextTurn = false;
284
341
  let hasRealKey = false;
285
342
  for (const obj of initialJsonObjects(file)) {
@@ -301,11 +358,11 @@ function fastSessionInfo(file, source) {
301
358
  hasTextTurn = true;
302
359
  continue;
303
360
  }
304
- if (source === "claude-code" && typeof obj.sessionId === "string" && obj.sessionId) {
305
- conversationKey = `claude-code:${obj.sessionId}`;
361
+ if (typeof obj.sessionId === "string" && obj.sessionId) {
362
+ conversationKey = `${source}:${obj.sessionId}`;
306
363
  hasRealKey = true;
307
364
  }
308
- if (source === "claude-code" && (obj.type === "user" || obj.type === "assistant")) {
365
+ if (obj.type === "user" || obj.type === "assistant") {
309
366
  const message = isRecord(obj.message) ? obj.message : {};
310
367
  if (hasClaudeText(message.content))
311
368
  hasTextTurn = true;
@@ -330,19 +387,25 @@ function fastSessionEntries(opts = {}) {
330
387
  mtimeMs: Math.round(file.mtimeMs),
331
388
  });
332
389
  }
333
- const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
334
- if (claudeRoot) {
335
- for (const filePath of userCreatedClaudeFiles(claudeRoot)) {
336
- const stat = statSafe(filePath);
337
- const filenameId = path.basename(filePath, path.extname(filePath));
338
- const filenameHasStableId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(filenameId);
339
- const info = filenameHasStableId
340
- ? { conversationKey: `claude-code:${filenameId.toLowerCase()}`, hasTextTurn: true, hasRealKey: true }
341
- : fastSessionInfo(filePath, "claude-code");
342
- // Same rule as codex: include on text OR a real session id so large sessions aren't undercounted,
343
- // while keeping the key stable (claude-code sessionId appears on every line, so hasRealKey is reliable).
344
- if (info.hasTextTurn || info.hasRealKey)
345
- out.push({ filePath, source: "claude-code", conversationKey: info.conversationKey, size: stat.size, mtimeMs: stat.mtimeMs });
390
+ for (const candidate of claudeSessionCandidates(opts)) {
391
+ const stat = statSafe(candidate.filePath);
392
+ const filenameId = path.basename(candidate.filePath, path.extname(candidate.filePath));
393
+ const filenameHasStableId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(filenameId);
394
+ const stableId = candidate.conversationId
395
+ ?? (filenameHasStableId ? filenameId.toLowerCase() : null);
396
+ const info = stableId
397
+ ? { conversationKey: `${candidate.source}:${stableId}`, hasTextTurn: true, hasRealKey: true }
398
+ : fastSessionInfo(candidate.filePath, candidate.source);
399
+ // Same rule as codex: include on text OR a real session id so large sessions aren't undercounted,
400
+ // while keeping the key stable. Cowork keys use the outer local_<id>, not the nested CLI UUID.
401
+ if (info.hasTextTurn || info.hasRealKey) {
402
+ out.push({
403
+ filePath: candidate.filePath,
404
+ source: candidate.source,
405
+ conversationKey: info.conversationKey,
406
+ size: stat.size,
407
+ mtimeMs: stat.mtimeMs,
408
+ });
346
409
  }
347
410
  }
348
411
  return out;
@@ -506,7 +569,7 @@ function printMigrationEstimate(e, useJson) {
506
569
  console.log("");
507
570
  console.log("Migration estimate (local metadata only; no transcripts uploaded)");
508
571
  console.log(` Sessions: ${humanNum(e.sessions)} total · ${humanNum(e.pending)} pending · ${humanNum(e.alreadyMigrated)} already processed`);
509
- console.log(` Sources: ${humanNum(e.pendingCodex)} Codex + ${humanNum(e.pendingClaudeCode)} Claude Code pending`);
572
+ console.log(` Sources: ${humanNum(e.pendingCodex)} Codex + ${humanNum(e.pendingClaudeCode)} Claude Code + ${humanNum(e.pendingCowork)} Cowork pending`);
510
573
  console.log(` Assembled transcript size: ${humanNum(e.chars.total)} chars ≈ ${humanNum(e.approxInputTokens.total)} input tokens`);
511
574
  console.log(` Size percentiles: p50 ${humanNum(e.chars.p50)} chars · p90 ${humanNum(e.chars.p90)} · p95 ${humanNum(e.chars.p95)} · max ${humanNum(e.chars.max)}`);
512
575
  console.log(` Turns: ${humanNum(e.turns.total)} total · p50 ${humanNum(e.turns.p50)} · p90 ${humanNum(e.turns.p90)} · max ${humanNum(e.turns.max)}`);
@@ -615,7 +678,7 @@ function percentile(nums, p) {
615
678
  export function estimateMigration(sessions, pending) {
616
679
  const chars = pending.map((s) => s.rawData.length);
617
680
  const turns = pending.map((s) => s.turnCount);
618
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
681
+ const counts = sourceCounts(sessions, pending);
619
682
  const bucketDefs = [
620
683
  { label: "small <=10k chars", min: 0, max: 10_000 },
621
684
  { label: "routine 10k-30k", min: 10_000, max: 30_000 },
@@ -640,8 +703,10 @@ export function estimateMigration(sessions, pending) {
640
703
  alreadyMigrated: sessions.length - pending.length,
641
704
  codex: sessions.filter((s) => s.source === "codex").length,
642
705
  claudeCode: sessions.filter((s) => s.source === "claude-code").length,
643
- pendingCodex,
644
- pendingClaudeCode: pending.length - pendingCodex,
706
+ cowork: counts.coworkCount,
707
+ pendingCodex: counts.pendingCodex,
708
+ pendingClaudeCode: counts.pendingClaudeCode,
709
+ pendingCowork: counts.pendingCowork,
645
710
  chars: {
646
711
  total: sum(chars),
647
712
  p50: percentile(chars, 0.5),
@@ -723,7 +788,7 @@ export function estimateMigrationEtaFromLengths(lengths, skippedActive = 0, opts
723
788
  };
724
789
  }
725
790
  export function summarizeFastMigratableDiscovery(discovery) {
726
- const pendingCodex = discovery.pendingCodex ?? discovery.pending.filter((s) => s.source === "codex").length;
791
+ const counts = sourceCounts(discovery.sessions, discovery.pending);
727
792
  return {
728
793
  sessions: discovery.sessions.length,
729
794
  pending: discovery.pending.length,
@@ -732,8 +797,11 @@ export function summarizeFastMigratableDiscovery(discovery) {
732
797
  skippedActive: discovery.skippedActive,
733
798
  codexCount: discovery.codexCount,
734
799
  claudeCount: discovery.claudeCount,
735
- pendingCodex,
736
- pendingClaudeCode: discovery.pendingClaudeCode ?? discovery.pending.length - pendingCodex,
800
+ claudeCodeCount: discovery.claudeCodeCount ?? counts.claudeCodeCount,
801
+ coworkCount: discovery.coworkCount ?? counts.coworkCount,
802
+ pendingCodex: discovery.pendingCodex ?? counts.pendingCodex,
803
+ pendingClaudeCode: discovery.pendingClaudeCode ?? counts.pendingClaudeCode,
804
+ pendingCowork: discovery.pendingCowork ?? counts.pendingCowork,
737
805
  eta: estimateMigrationEtaFromLengths(discovery.pending.map((s) => s.size), discovery.skippedActive, {
738
806
  secondsPerSession: measuredSecondsPerSession() ?? undefined,
739
807
  }),
@@ -771,8 +839,7 @@ export function discoverMigratableFastDiscovery(opts = {}) {
771
839
  return !e || e.size !== s.size;
772
840
  });
773
841
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
774
- const codexCount = sessions.filter((s) => s.source === "codex").length;
775
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
842
+ const counts = sourceCounts(sessions, pending);
776
843
  return {
777
844
  sessions,
778
845
  pending,
@@ -780,10 +847,7 @@ export function discoverMigratableFastDiscovery(opts = {}) {
780
847
  alreadyMigrated: selectable.length - pendingAll.length,
781
848
  skippedActive,
782
849
  limited: pending.length < pendingAll.length,
783
- codexCount,
784
- claudeCount: sessions.length - codexCount,
785
- pendingCodex,
786
- pendingClaudeCode: pending.length - pendingCodex,
850
+ ...counts,
787
851
  };
788
852
  }
789
853
  export function discoverMigratableSummaryFast(opts = {}) {
@@ -841,8 +905,7 @@ export function discoverMigratableSessions(opts = {}) {
841
905
  return !e || e.size !== s.size;
842
906
  });
843
907
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
844
- const codexCount = sessions.filter((s) => s.source === "codex").length;
845
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
908
+ const counts = sourceCounts(sessions, pending);
846
909
  return {
847
910
  sessions,
848
911
  pending,
@@ -850,10 +913,7 @@ export function discoverMigratableSessions(opts = {}) {
850
913
  alreadyMigrated: selectableSessions.length - pendingAll.length,
851
914
  skippedActive,
852
915
  limited: pending.length < pendingAll.length,
853
- codexCount,
854
- claudeCount: sessions.length - codexCount,
855
- pendingCodex,
856
- pendingClaudeCode: pending.length - pendingCodex,
916
+ ...counts,
857
917
  };
858
918
  }
859
919
  /**
@@ -871,11 +931,16 @@ export function discoverPendingSessionsTargeted(processedKeys, opts = {}) {
871
931
  const filtered = processedKeys ? applyFastAccountImportStatus(fast, processedKeys, opts) : fast;
872
932
  const pending = [];
873
933
  for (const entry of filtered.pending) {
874
- const s = entry.source === "codex" ? assembleCodex(entry.filePath) : assembleClaude(entry.filePath);
934
+ const s = entry.source === "codex"
935
+ ? assembleCodex(entry.filePath)
936
+ : assembleClaude(entry.filePath, {
937
+ source: entry.source,
938
+ conversationId: bareId(entry),
939
+ });
875
940
  if (s)
876
941
  pending.push(s);
877
942
  }
878
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
943
+ const pendingCounts = sourceCounts(pending, pending);
879
944
  return {
880
945
  sessions: pending,
881
946
  pending,
@@ -885,8 +950,11 @@ export function discoverPendingSessionsTargeted(processedKeys, opts = {}) {
885
950
  limited: filtered.limited,
886
951
  codexCount: filtered.codexCount,
887
952
  claudeCount: filtered.claudeCount,
888
- pendingCodex,
889
- pendingClaudeCode: pending.length - pendingCodex,
953
+ claudeCodeCount: filtered.claudeCodeCount,
954
+ coworkCount: filtered.coworkCount,
955
+ pendingCodex: pendingCounts.pendingCodex,
956
+ pendingClaudeCode: pendingCounts.pendingClaudeCode,
957
+ pendingCowork: pendingCounts.pendingCowork,
890
958
  accountChecked: filtered.accountChecked,
891
959
  accountCheckFailed: filtered.accountCheckFailed,
892
960
  accountCheckUnavailable: filtered.accountCheckUnavailable,
@@ -899,7 +967,7 @@ export function applyAccountImportStatus(discovery, processedKeys, opts = {}) {
899
967
  const skippedActive = discovery.sessions.length - selectableSessions.length;
900
968
  const pendingAll = selectableSessions.filter((s) => !processedKeys.has(s.conversationKey));
901
969
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
902
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
970
+ const counts = sourceCounts(discovery.sessions, pending);
903
971
  return {
904
972
  ...discovery,
905
973
  pending,
@@ -907,8 +975,9 @@ export function applyAccountImportStatus(discovery, processedKeys, opts = {}) {
907
975
  alreadyMigrated: selectableSessions.length - pendingAll.length,
908
976
  skippedActive,
909
977
  limited: pending.length < pendingAll.length,
910
- pendingCodex,
911
- pendingClaudeCode: pending.length - pendingCodex,
978
+ pendingCodex: counts.pendingCodex,
979
+ pendingClaudeCode: counts.pendingClaudeCode,
980
+ pendingCowork: counts.pendingCowork,
912
981
  accountChecked: true,
913
982
  accountCheckFailed: false,
914
983
  accountCheckUnavailable: false,
@@ -921,7 +990,7 @@ export function applyFastAccountImportStatus(discovery, processedKeys, opts = {}
921
990
  const skippedActive = discovery.sessions.length - selectableSessions.length;
922
991
  const pendingAll = selectableSessions.filter((s) => !processedKeys.has(s.conversationKey));
923
992
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
924
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
993
+ const counts = sourceCounts(discovery.sessions, pending);
925
994
  return {
926
995
  ...discovery,
927
996
  pending,
@@ -929,8 +998,9 @@ export function applyFastAccountImportStatus(discovery, processedKeys, opts = {}
929
998
  alreadyMigrated: selectableSessions.length - pendingAll.length,
930
999
  skippedActive,
931
1000
  limited: pending.length < pendingAll.length,
932
- pendingCodex,
933
- pendingClaudeCode: pending.length - pendingCodex,
1001
+ pendingCodex: counts.pendingCodex,
1002
+ pendingClaudeCode: counts.pendingClaudeCode,
1003
+ pendingCowork: counts.pendingCowork,
934
1004
  accountChecked: true,
935
1005
  accountCheckFailed: false,
936
1006
  accountCheckUnavailable: false,
@@ -1236,10 +1306,10 @@ export async function cmdMigrate(flags) {
1236
1306
  }
1237
1307
  }
1238
1308
  }
1239
- const { sessions, pending, pendingTotal, alreadyMigrated, skippedActive, limited, codexCount, claudeCount, accountChecked, accountCheckFailed, accountCheckUnavailable } = discovery;
1309
+ const { sessions, pending, pendingTotal, alreadyMigrated, skippedActive, limited, codexCount, claudeCodeCount, coworkCount, accountChecked, accountCheckFailed, accountCheckUnavailable } = discovery;
1240
1310
  const estimateSessions = includeActive ? sessions : sessions.filter((s) => !isActiveMigrationSession(s));
1241
1311
  if (!sessions.length) {
1242
- console.log("No local Codex/Claude Code sessions found to migrate.");
1312
+ console.log("No local Codex, Claude Code, or Cowork sessions found to migrate.");
1243
1313
  return;
1244
1314
  }
1245
1315
  if (flags.estimate === true && flags.json === true) {
@@ -1252,7 +1322,7 @@ export async function cmdMigrate(flags) {
1252
1322
  const pendingText = limited
1253
1323
  ? `${c.bold(String(pending.length))} of ${c.bold(String(pendingTotal))} ${pendingName} selected`
1254
1324
  : `${c.bold(String(pending.length))} ${pendingName} to import`;
1255
- console.log(`Found ${c.bold(String(sessions.length))} sessions (${codexCount} Codex + ${claudeCount} Claude Code) · ${pendingText}` +
1325
+ console.log(`Found ${c.bold(String(sessions.length))} sessions (${codexCount} Codex + ${claudeCodeCount} Claude Code + ${coworkCount} Cowork) · ${pendingText}` +
1256
1326
  (alreadyMigrated ? c.dim(`, ${alreadyMigrated} already migrated`) : "") +
1257
1327
  (skippedActive ? c.dim(`, ${skippedActive} active skipped`) : ""));
1258
1328
  if (accountChecked)
@@ -1277,7 +1347,8 @@ export async function cmdMigrate(flags) {
1277
1347
  if (flags["dry-run"] === true) {
1278
1348
  console.log(c.dim("\n--dry-run: discovered + assembled only, nothing sent.\n"));
1279
1349
  for (const s of pending.slice(0, 50)) {
1280
- console.log(` ${s.source === "codex" ? "codex " : "claude"} ${(s.firstTs || "").slice(0, 10)} ${humanNum(s.rawData.length)} chars ${s.turnCount} text turns`);
1350
+ const sourceLabel = s.source === "codex" ? "codex " : s.source === "claude-desktop" ? "cowork" : "claude";
1351
+ console.log(` ${sourceLabel} ${(s.firstTs || "").slice(0, 10)} ${humanNum(s.rawData.length)} chars ${s.turnCount} text turns`);
1281
1352
  }
1282
1353
  if (pending.length > 50)
1283
1354
  console.log(c.dim(` … and ${pending.length - 50} more`));
@@ -1302,7 +1373,8 @@ export async function cmdMigrate(flags) {
1302
1373
  metricsFile,
1303
1374
  selection,
1304
1375
  onProgress: (ev) => {
1305
- const tag = `${c.dim(`[${ev.index}/${ev.total}]`)} ${ev.session.source === "codex" ? "codex " : "claude"} ${(ev.session.firstTs || "").slice(0, 10)}`;
1376
+ const sourceLabel = ev.session.source === "codex" ? "codex " : ev.session.source === "claude-desktop" ? "cowork" : "claude";
1377
+ const tag = `${c.dim(`[${ev.index}/${ev.total}]`)} ${sourceLabel} ${(ev.session.firstTs || "").slice(0, 10)}`;
1306
1378
  if (ev.error) {
1307
1379
  console.log(`${tag} ${c.red("✗ failed")} ${c.dim(truncate(ev.error, 60))}` + (ev.durationMs ? c.dim(` ${humanDuration(ev.durationMs)}`) : ""));
1308
1380
  return;
@@ -231,6 +231,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
231
231
  var pendN = hasPendingCount ? pending : 0;
232
232
  var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
233
233
  var pendingClaudeCode = typeof migratable.pendingClaudeCode === "number" ? migratable.pendingClaudeCode : null;
234
+ var pendingCowork = typeof migratable.pendingCowork === "number" ? migratable.pendingCowork : null;
234
235
  if (connected && !billingStatusLoading && !setupPlanConfirmed()) {
235
236
  renderPlanGate();
236
237
  return;
@@ -312,6 +313,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
312
313
  document.getElementById("exSub").textContent = sub;
313
314
  var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
314
315
  var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
316
+ var coworkN = pendingTrusted && pendingCowork !== null ? pendingCowork : (sessions.cowork || 0);
315
317
  // Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
316
318
  var srcIcon = function (id, fallback) {
317
319
  var asset = id === "claude-desktop" ? "claude" : "codex";
@@ -325,7 +327,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
325
327
  ? '<span class="pf">' + srcIcon("codex", "CX") + '<strong>' + esc(number(codexN)) + '</strong> Codex</span>' +
326
328
  '<span class="pfDot">&middot;</span>' +
327
329
  '<span class="pf">' + srcIcon("claude-desktop", "CL") + '<strong>' + esc(number(claudeN)) + '</strong> Claude Code</span>' +
328
- '<span class="pfNote">found on this Mac</span>'
330
+ '<span class="pfDot">&middot;</span>' +
331
+ '<span class="pf">' + srcIcon("claude-desktop", "CW") + '<strong>' + esc(number(coworkN)) + '</strong> Cowork</span>' +
332
+ '<span class="pfNote">found on this computer</span>'
329
333
  : "");
330
334
  // Reassurances live at the moment of commitment — right under the button.
331
335
  document.getElementById("exEta").innerHTML = degradedWithoutCounts
@@ -776,7 +780,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
776
780
  }
777
781
  }
778
782
  function sessionSourceLabel(source) {
779
- return source === "claude-code" ? "Claude Code" : "Codex";
783
+ if (source === "claude-code") return "Claude Code";
784
+ if (source === "claude-desktop") return "Cowork";
785
+ return "Codex";
780
786
  }
781
787
  function sessionDateLabel(value) {
782
788
  if (!value) return "Date unavailable";
@@ -931,11 +937,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
931
937
  var key = String(session.key || "");
932
938
  var encodedKey = encodeURIComponent(key);
933
939
  var checked = !!selectedSessionKeys[key];
934
- var sourceIcon = session.source === "claude-code" ? "claude" : "codex";
935
- var sourceFallback = session.source === "claude-code" ? "CL" : "CX";
940
+ var isClaude = session.source === "claude-code" || session.source === "claude-desktop";
941
+ var sourceIcon = isClaude ? "claude" : "codex";
942
+ var sourceFallback = session.source === "claude-desktop" ? "CW" : (session.source === "claude-code" ? "CL" : "CX");
936
943
  return '<label class="sessionPickerRow' + (checked ? " is-selected" : "") + '">' +
937
944
  '<input type="checkbox" data-session-key="' + esc(encodedKey) + '"' + (checked ? " checked" : "") + ' />' +
938
- '<span class="sessionPickerSource ' + (session.source === "claude-code" ? "is-claude" : "is-codex") + '"><img src="/hud-assets/' + sourceIcon + '.svg" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="sessionPickerSourceFallback">' + sourceFallback + '</span></span>' +
945
+ '<span class="sessionPickerSource ' + (isClaude ? "is-claude" : "is-codex") + '"><img src="/hud-assets/' + sourceIcon + '.svg" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="sessionPickerSourceFallback">' + sourceFallback + '</span></span>' +
939
946
  '<span class="sessionPickerMain"><strong>' + esc(session.title || "Untitled coding session") + '</strong><small>' + esc(session.project || "No project detected") + ' · ' + esc(sessionDateLabel(session.date)) + '</small></span>' +
940
947
  '<span class="sessionPickerMeta">' + esc(compact(Number(session.approxInputTokens) || 0)) + ' tok</span>' +
941
948
  '</label>';
@@ -947,7 +954,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
947
954
  '<div class="sessionPickerShell">' +
948
955
  '<header class="sessionPickerHead"><div><h2 id="sessionPickerTitle">Choose conversations</h2><p class="sessionPickerSummary"><span class="sessionPickerQuota">Up to <strong>' + esc(number(limit)) + '</strong></span>' + inlineUpgrade + '<span class="sessionPickerOrder">&middot; newest selected first</span></p></div><button type="button" class="sessionPickerClose" id="closeSessionPicker" aria-label="Close">×</button></header>' +
949
956
  '<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
950
- '<label class="sessionSourceFilter"><span>Source</span><select id="sessionPickerSource"><option value="all"' + (sessionPickerSource === "all" ? " selected" : "") + '>All sources</option><option value="codex"' + (sessionPickerSource === "codex" ? " selected" : "") + '>Codex</option><option value="claude-code"' + (sessionPickerSource === "claude-code" ? " selected" : "") + '>Claude Code</option></select></label>' +
957
+ '<label class="sessionSourceFilter"><span>Source</span><select id="sessionPickerSource"><option value="all"' + (sessionPickerSource === "all" ? " selected" : "") + '>All sources</option><option value="codex"' + (sessionPickerSource === "codex" ? " selected" : "") + '>Codex</option><option value="claude-code"' + (sessionPickerSource === "claude-code" ? " selected" : "") + '>Claude Code</option><option value="claude-desktop"' + (sessionPickerSource === "claude-desktop" ? " selected" : "") + '>Cowork</option></select></label>' +
951
958
  '</div>' +
952
959
  '<div class="sessionPickerActions"><strong>' + esc(number(selected.length)) + ' / ' + esc(number(limit)) + ' selected</strong><span id="sessionPickerStatus" class="sessionPickerStatus" role="status" aria-live="polite">' + (limit === 0 ? "Plan limit reached. Upgrade to import more." : "") + '</span><button type="button" class="textButton" id="selectNewestSessions">Select newest</button><button type="button" class="textButton" id="clearSessions">Clear</button></div>' +
953
960
  '<div class="sessionPickerListWrap' + (limit === 0 ? " is-limit-blocked" : "") + '" id="sessionPickerListWrap"><div class="sessionPickerList"' + (limit === 0 ? " inert" : "") + '>' + (rows || '<div class="sessionPickerEmpty">No conversations match these filters.</div>') + overflow + '</div><div class="sessionPickerLimitOverlay" id="sessionPickerLimitOverlay" role="alert"' + (limit === 0 ? "" : " hidden") + '>' + (limit === 0 ? sessionPickerLimitNotice(false) : "") + '</div></div>' +
@@ -1207,13 +1214,18 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1207
1214
  function setupPlanDefinition(plan) {
1208
1215
  var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
1209
1216
  var limits = quotaLimitsForPlan(plan);
1217
+ var fallbackFeatures = plan === "power"
1218
+ ? ["2,000 past chats", "agent-heavy memory", "2,000 memory recalls / week"]
1219
+ : plan === "pro"
1220
+ ? ["500 past chats", "daily memory updates", "500 memory recalls / week"]
1221
+ : ["100 coding sessions", "a few new sessions / week", "100 memory recalls / week"];
1210
1222
  var features = limits
1211
1223
  ? [
1212
1224
  quotaNumber(limits.historicalConversationLimit) + " past chats",
1213
1225
  quotaTokens(limits.memoryProcessingInputTokensWeeklyLimit) + " new-chat tokens / week",
1214
1226
  quotaNumber(limits.memorySearchWeeklyLimit) + " memory recalls / week"
1215
1227
  ]
1216
- : ["Past-chat imports", "Weekly new-chat processing", "Weekly memory recalls"];
1228
+ : fallbackFeatures;
1217
1229
  if (plan === "power") return {
1218
1230
  id: "power",
1219
1231
  name: "Power",
package/dist/setup.js CHANGED
@@ -876,6 +876,7 @@ function openClaudeDesktop() {
876
876
  };
877
877
  }
878
878
  const LOCAL_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
879
+ const LOCAL_COWORK_SESSION_ID_RE = /^local_[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
879
880
  // `claude --resume <id>` only finds sessions that belong to the current project directory, so
880
881
  // recover the session's original cwd from its transcript before resuming.
881
882
  function claudeSessionCwd(sessionId) {
@@ -906,10 +907,14 @@ function shellQuote(value) {
906
907
  return `'${value.replace(/'/g, `'\\''`)}'`;
907
908
  }
908
909
  function openExistingAgentSession(source, sessionId) {
909
- if (process.platform !== "darwin")
910
- return { ok: false, message: "Opening local agent sessions is currently available on macOS." };
911
- if (!LOCAL_SESSION_ID_RE.test(sessionId))
910
+ const validSessionId = source === "claude-desktop"
911
+ ? LOCAL_COWORK_SESSION_ID_RE.test(sessionId)
912
+ : LOCAL_SESSION_ID_RE.test(sessionId);
913
+ if (!validSessionId)
912
914
  return { ok: false, message: "The local session identifier is invalid." };
915
+ if (source !== "claude-desktop" && process.platform !== "darwin") {
916
+ return { ok: false, message: "Opening local Codex and Claude Code sessions is currently available on macOS." };
917
+ }
913
918
  try {
914
919
  if (source === "codex") {
915
920
  execFileSync("open", [`codex://threads/${sessionId}`], { stdio: "pipe" });
@@ -926,6 +931,23 @@ function openExistingAgentSession(source, sessionId) {
926
931
  ], { stdio: "pipe" });
927
932
  return { ok: true, message: "Opened the original Claude Code session in Terminal." };
928
933
  }
934
+ if (source === "claude-desktop") {
935
+ const url = `claude://claude.ai/claude-code-desktop/${sessionId}`;
936
+ if (process.platform === "win32") {
937
+ spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `start "" "${url}"`], {
938
+ detached: true,
939
+ stdio: "ignore",
940
+ windowsHide: true,
941
+ }).unref();
942
+ }
943
+ else if (process.platform === "darwin") {
944
+ execFileSync("open", [url], { stdio: "pipe" });
945
+ }
946
+ else {
947
+ return { ok: false, message: "Opening local Cowork sessions is not supported on this system yet." };
948
+ }
949
+ return { ok: true, message: "Opened the original Cowork session in Claude Desktop." };
950
+ }
929
951
  return { ok: false, message: "Unsupported agent session source." };
930
952
  }
931
953
  catch (error) {
@@ -966,7 +988,8 @@ function migratableFromDiscovery(disc) {
966
988
  pending: disc.pending.length,
967
989
  pendingTotal: disc.pendingTotal,
968
990
  pendingCodex,
969
- pendingClaudeCode: disc.pendingClaudeCode ?? disc.pending.length - pendingCodex,
991
+ pendingClaudeCode: disc.pendingClaudeCode ?? disc.pending.filter((s) => s.source === "claude-code").length,
992
+ pendingCowork: disc.pendingCowork ?? disc.pending.filter((s) => s.source === "claude-desktop").length,
970
993
  alreadyMigrated: disc.alreadyMigrated,
971
994
  skippedActive: disc.skippedActive,
972
995
  limited: disc.limited,
@@ -985,7 +1008,8 @@ function sessionsFromDiscovery(disc) {
985
1008
  return {
986
1009
  total: disc.sessions.length,
987
1010
  codex: disc.codexCount,
988
- claudeCode: disc.claudeCount,
1011
+ claudeCode: disc.claudeCodeCount,
1012
+ cowork: disc.coworkCount,
989
1013
  };
990
1014
  }
991
1015
  function migratableFromFastSummary(summary) {
@@ -994,6 +1018,7 @@ function migratableFromFastSummary(summary) {
994
1018
  pendingTotal: summary.pendingTotal,
995
1019
  pendingCodex: summary.pendingCodex,
996
1020
  pendingClaudeCode: summary.pendingClaudeCode,
1021
+ pendingCowork: summary.pendingCowork,
997
1022
  alreadyMigrated: summary.alreadyMigrated,
998
1023
  skippedActive: summary.skippedActive,
999
1024
  eta: summary.eta,
@@ -1482,7 +1507,7 @@ export function completeOptionalStatsPayload(payload, reason, countsTrusted) {
1482
1507
  generatedFrom: ["~/.codex/sessions", "~/.claude/projects"],
1483
1508
  llmCallsUsed: 0,
1484
1509
  transcriptsUploaded: false,
1485
- sessions: { total: 0, codex: 0, claudeCode: 0 },
1510
+ sessions: { total: 0, codex: 0, claudeCode: 0, cowork: 0 },
1486
1511
  migratable: { pending: 0, alreadyMigrated: 0 },
1487
1512
  memoriesCaptured: null,
1488
1513
  };
@@ -3290,7 +3315,8 @@ async function cmdOnboarding(flags) {
3290
3315
  let sessionSummary = {
3291
3316
  total: quick.sessions,
3292
3317
  codex: quick.codexCount,
3293
- claudeCode: quick.claudeCount,
3318
+ claudeCode: quick.claudeCodeCount,
3319
+ cowork: quick.coworkCount,
3294
3320
  };
3295
3321
  stats = await buildStatsPayload([], {
3296
3322
  partial: true,
@@ -3326,7 +3352,8 @@ async function cmdOnboarding(flags) {
3326
3352
  sessionSummary = {
3327
3353
  total: cloudSummary.sessions,
3328
3354
  codex: cloudSummary.codexCount,
3329
- claudeCode: cloudSummary.claudeCount,
3355
+ claudeCode: cloudSummary.claudeCodeCount,
3356
+ cowork: cloudSummary.coworkCount,
3330
3357
  };
3331
3358
  const cloudPayload = await buildStatsPayload([], {
3332
3359
  partial: true,
@@ -3363,7 +3390,8 @@ async function cmdOnboarding(flags) {
3363
3390
  sessionSummary = {
3364
3391
  total: unavailableSummary.sessions,
3365
3392
  codex: unavailableSummary.codexCount,
3366
- claudeCode: unavailableSummary.claudeCount,
3393
+ claudeCode: unavailableSummary.claudeCodeCount,
3394
+ cowork: unavailableSummary.coworkCount,
3367
3395
  };
3368
3396
  const unavailablePayload = await buildStatsPayload([], {
3369
3397
  partial: true,
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
- import { resolveClaudeProjectsDir, resolveCodexSessionRoots } from "./local-data-paths.js";
3
+ import { resolveClaudeCoworkSessionsDir, resolveClaudeProjectsDir, resolveCodexSessionRoots, } from "./local-data-paths.js";
5
4
  export const SOURCE_SESSION_BINDING_EVIDENCE = "local_jsonl_tool_call";
6
5
  export const SOURCE_SESSION_HOOK_EVIDENCE = "local_session_start_hook";
7
6
  export const SOURCE_SESSION_MCP_METADATA_EVIDENCE = "local_mcp_session_metadata";
@@ -81,18 +80,13 @@ function readableDirectory(candidate) {
81
80
  return false;
82
81
  }
83
82
  }
84
- function coworkRoot() {
85
- const supportRoot = process.env.CLAUDE_DESKTOP_SUPPORT_DIR?.trim()
86
- || path.join(os.homedir(), "Library", "Application Support", "Claude");
87
- return path.join(supportRoot, "local-agent-mode-sessions");
88
- }
89
83
  function defaultRoots() {
90
84
  const claudeProjects = resolveClaudeProjectsDir();
91
- const desktopRoot = coworkRoot();
85
+ const desktopRoot = resolveClaudeCoworkSessionsDir();
92
86
  return {
93
87
  codex: resolveCodexSessionRoots().map((root) => root.path),
94
88
  claudeCode: claudeProjects ? [claudeProjects] : [],
95
- cowork: readableDirectory(desktopRoot) ? [desktopRoot] : [],
89
+ cowork: desktopRoot ? [desktopRoot] : [],
96
90
  };
97
91
  }
98
92
  function walkJsonlFiles(roots) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.43",
3
+ "version": "1.4.45",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",