@echomem/mcp 1.4.44 → 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.
Files changed (50) hide show
  1. package/README.md +28 -25
  2. package/dist/city/README.md +9 -0
  3. package/dist/city/echo-ai-city-only.html +2232 -0
  4. package/dist/city/echo-extraction-plate.html +330 -0
  5. package/dist/city/echo-face-cutout.png +0 -0
  6. package/dist/city/personality_stickers/bossy.png +0 -0
  7. package/dist/city/personality_stickers/ghosty.png +0 -0
  8. package/dist/city/personality_stickers/loopy.png +0 -0
  9. package/dist/city/personality_stickers/lusty.png +0 -0
  10. package/dist/city/personality_stickers/maxxy.png +0 -0
  11. package/dist/city/personality_stickers/tabby.png +0 -0
  12. package/dist/city/vendor/OrbitControls.js +1417 -0
  13. package/dist/city/vendor/RoundedBoxGeometry.js +155 -0
  14. package/dist/city/vendor/echo_general-file-21.riv +0 -0
  15. package/dist/city/vendor/rive.js +8139 -0
  16. package/dist/city/vendor/rive.wasm +0 -0
  17. package/dist/city/vendor/three.module.min.js +6 -0
  18. package/dist/context-analysis/claude-native-canonical.js +2 -2
  19. package/dist/context-analysis/vendored-canonical.js +2 -2
  20. package/dist/context-analysis/workspace-report.js +3 -3
  21. package/dist/forensics.js +1531 -0
  22. package/dist/hud/hooks.js +31 -43
  23. package/dist/index.js +79 -15
  24. package/dist/local-data-paths.js +38 -0
  25. package/dist/migrate.js +140 -70
  26. package/dist/report.js +721 -0
  27. package/dist/save-checkpoint-hook.js +1 -1
  28. package/dist/setup-page/client-core.js +372 -18
  29. package/dist/setup-page/client-extraction.js +204 -37
  30. package/dist/setup-page/client-lifecycle.js +101 -31
  31. package/dist/setup-page/client-report-audit.js +819 -0
  32. package/dist/setup-page/client-report-city.js +356 -0
  33. package/dist/setup-page/client-report.js +6 -0
  34. package/dist/setup-page/client.js +2 -0
  35. package/dist/setup-page/styles-city-report.js +880 -0
  36. package/dist/setup-page/styles-context-audit.js +470 -0
  37. package/dist/setup-page/styles-extraction.js +31 -1
  38. package/dist/setup-page/styles-foundation.js +89 -0
  39. package/dist/setup-page/styles-mvp.js +155 -10
  40. package/dist/setup-page/styles-website-alignment.js +204 -0
  41. package/dist/setup-page/styles.js +4 -0
  42. package/dist/setup-page.js +4 -4
  43. package/dist/setup-preview.js +212 -4
  44. package/dist/setup.js +702 -321
  45. package/dist/source-session.js +3 -9
  46. package/dist/v1-contract.js +8 -0
  47. package/package.json +7 -9
  48. package/dist/config-files.js +0 -63
  49. package/dist/local-jsonl.js +0 -87
  50. package/dist/onboarding-stats.js +0 -16
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,8 +29,8 @@ 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";
31
- import { eachJsonLine, walkLocalFiles } from "./local-jsonl.js";
32
+ import { resolveClaudeCoworkSessionsDir, resolveClaudeProjectsDir } from "./local-data-paths.js";
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
34
36
  const RATE_WINDOW_MS = 60_000;
@@ -56,8 +58,7 @@ export function assembleCodex(file) {
56
58
  let firstTs = null;
57
59
  let title = "";
58
60
  const turns = [];
59
- eachJsonLine(file, (value) => {
60
- const o = value;
61
+ eachLine(file, (o) => {
61
62
  if (o && o.type === "session_meta") {
62
63
  sessionId = (o.payload && typeof o.payload.id === "string" && o.payload.id) || sessionId;
63
64
  cwd = (o.payload && typeof o.payload.cwd === "string" && o.payload.cwd) || cwd;
@@ -103,14 +104,13 @@ export function assembleCodex(file) {
103
104
  };
104
105
  }
105
106
  /** Claude Code <uuid>.jsonl → one session. User/assistant text turns are sent; tool calls/results/thinking stay local. */
106
- export function assembleClaude(file) {
107
+ export function assembleClaude(file, opts = {}) {
107
108
  let sessionId = null;
108
109
  let cwd = null;
109
110
  let firstTs = null;
110
111
  let title = "";
111
112
  const turns = [];
112
- eachJsonLine(file, (value) => {
113
- const o = value;
113
+ eachLine(file, (o) => {
114
114
  if (!cwd && typeof o.cwd === "string")
115
115
  cwd = o.cwd;
116
116
  if (!sessionId && typeof o.sessionId === "string")
@@ -159,10 +159,12 @@ export function assembleClaude(file) {
159
159
  if (!turns.length)
160
160
  return null;
161
161
  const stat = statSafe(file);
162
+ const source = opts.source ?? "claude-code";
163
+ const conversationId = opts.conversationId ?? sessionId ?? sha16(file);
162
164
  return {
163
165
  filePath: file,
164
- source: "claude-code",
165
- conversationKey: `claude-code:${sessionId || sha16(file)}`,
166
+ source,
167
+ conversationKey: `${source}:${conversationId}`,
166
168
  cwd: normalizeCwd(cwd),
167
169
  firstTs,
168
170
  title: title || "Claude text turns",
@@ -201,7 +203,46 @@ function userCreatedCodexFiles(codexRoot) {
201
203
  return userCreatedCodexSessionFiles(codexRoot).map((file) => file.path);
202
204
  }
203
205
  function userCreatedClaudeFiles(claudeRoot) {
204
- return walkLocalFiles(claudeRoot, (candidate) => candidate.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows").filter(isUserCreatedClaudeSessionFile);
206
+ return walk(claudeRoot, (candidate) => candidate.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows").filter(isUserCreatedClaudeSessionFile);
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;
205
246
  }
206
247
  /** Discover every user-created local session, newest first (by first-turn timestamp). */
207
248
  export function discoverSessions(opts = {}) {
@@ -211,17 +252,31 @@ export function discoverSessions(opts = {}) {
211
252
  if (s)
212
253
  out.push(s);
213
254
  }
214
- const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
215
- if (claudeRoot) {
216
- for (const f of userCreatedClaudeFiles(claudeRoot)) {
217
- const s = assembleClaude(f);
218
- if (s)
219
- out.push(s);
220
- }
255
+ for (const candidate of claudeSessionCandidates(opts)) {
256
+ const s = assembleClaude(candidate.filePath, candidate);
257
+ if (s)
258
+ out.push(s);
221
259
  }
222
260
  out.sort((a, b) => String(b.firstTs || "").localeCompare(String(a.firstTs || "")));
223
261
  return out;
224
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
+ }
225
280
  const IMPORT_STATUS_CHUNK_SIZE = 1000;
226
281
  function initialJsonObjects(file, maxBytes = 1024 * 1024) {
227
282
  let fd;
@@ -281,7 +336,7 @@ function isUserCreatedClaudeSessionFile(file) {
281
336
  });
282
337
  }
283
338
  function fastSessionInfo(file, source) {
284
- let conversationKey = `${source === "codex" ? "codex" : "claude-code"}:${sha16(file)}`;
339
+ let conversationKey = `${source}:${sha16(file)}`;
285
340
  let hasTextTurn = false;
286
341
  let hasRealKey = false;
287
342
  for (const obj of initialJsonObjects(file)) {
@@ -303,11 +358,11 @@ function fastSessionInfo(file, source) {
303
358
  hasTextTurn = true;
304
359
  continue;
305
360
  }
306
- if (source === "claude-code" && typeof obj.sessionId === "string" && obj.sessionId) {
307
- conversationKey = `claude-code:${obj.sessionId}`;
361
+ if (typeof obj.sessionId === "string" && obj.sessionId) {
362
+ conversationKey = `${source}:${obj.sessionId}`;
308
363
  hasRealKey = true;
309
364
  }
310
- if (source === "claude-code" && (obj.type === "user" || obj.type === "assistant")) {
365
+ if (obj.type === "user" || obj.type === "assistant") {
311
366
  const message = isRecord(obj.message) ? obj.message : {};
312
367
  if (hasClaudeText(message.content))
313
368
  hasTextTurn = true;
@@ -332,19 +387,25 @@ function fastSessionEntries(opts = {}) {
332
387
  mtimeMs: Math.round(file.mtimeMs),
333
388
  });
334
389
  }
335
- const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
336
- if (claudeRoot) {
337
- for (const filePath of userCreatedClaudeFiles(claudeRoot)) {
338
- const stat = statSafe(filePath);
339
- const filenameId = path.basename(filePath, path.extname(filePath));
340
- 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);
341
- const info = filenameHasStableId
342
- ? { conversationKey: `claude-code:${filenameId.toLowerCase()}`, hasTextTurn: true, hasRealKey: true }
343
- : fastSessionInfo(filePath, "claude-code");
344
- // Same rule as codex: include on text OR a real session id so large sessions aren't undercounted,
345
- // while keeping the key stable (claude-code sessionId appears on every line, so hasRealKey is reliable).
346
- if (info.hasTextTurn || info.hasRealKey)
347
- 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
+ });
348
409
  }
349
410
  }
350
411
  return out;
@@ -508,7 +569,7 @@ function printMigrationEstimate(e, useJson) {
508
569
  console.log("");
509
570
  console.log("Migration estimate (local metadata only; no transcripts uploaded)");
510
571
  console.log(` Sessions: ${humanNum(e.sessions)} total · ${humanNum(e.pending)} pending · ${humanNum(e.alreadyMigrated)} already processed`);
511
- 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`);
512
573
  console.log(` Assembled transcript size: ${humanNum(e.chars.total)} chars ≈ ${humanNum(e.approxInputTokens.total)} input tokens`);
513
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)}`);
514
575
  console.log(` Turns: ${humanNum(e.turns.total)} total · p50 ${humanNum(e.turns.p50)} · p90 ${humanNum(e.turns.p90)} · max ${humanNum(e.turns.max)}`);
@@ -617,7 +678,7 @@ function percentile(nums, p) {
617
678
  export function estimateMigration(sessions, pending) {
618
679
  const chars = pending.map((s) => s.rawData.length);
619
680
  const turns = pending.map((s) => s.turnCount);
620
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
681
+ const counts = sourceCounts(sessions, pending);
621
682
  const bucketDefs = [
622
683
  { label: "small <=10k chars", min: 0, max: 10_000 },
623
684
  { label: "routine 10k-30k", min: 10_000, max: 30_000 },
@@ -642,8 +703,10 @@ export function estimateMigration(sessions, pending) {
642
703
  alreadyMigrated: sessions.length - pending.length,
643
704
  codex: sessions.filter((s) => s.source === "codex").length,
644
705
  claudeCode: sessions.filter((s) => s.source === "claude-code").length,
645
- pendingCodex,
646
- pendingClaudeCode: pending.length - pendingCodex,
706
+ cowork: counts.coworkCount,
707
+ pendingCodex: counts.pendingCodex,
708
+ pendingClaudeCode: counts.pendingClaudeCode,
709
+ pendingCowork: counts.pendingCowork,
647
710
  chars: {
648
711
  total: sum(chars),
649
712
  p50: percentile(chars, 0.5),
@@ -725,7 +788,7 @@ export function estimateMigrationEtaFromLengths(lengths, skippedActive = 0, opts
725
788
  };
726
789
  }
727
790
  export function summarizeFastMigratableDiscovery(discovery) {
728
- const pendingCodex = discovery.pendingCodex ?? discovery.pending.filter((s) => s.source === "codex").length;
791
+ const counts = sourceCounts(discovery.sessions, discovery.pending);
729
792
  return {
730
793
  sessions: discovery.sessions.length,
731
794
  pending: discovery.pending.length,
@@ -734,8 +797,11 @@ export function summarizeFastMigratableDiscovery(discovery) {
734
797
  skippedActive: discovery.skippedActive,
735
798
  codexCount: discovery.codexCount,
736
799
  claudeCount: discovery.claudeCount,
737
- pendingCodex,
738
- 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,
739
805
  eta: estimateMigrationEtaFromLengths(discovery.pending.map((s) => s.size), discovery.skippedActive, {
740
806
  secondsPerSession: measuredSecondsPerSession() ?? undefined,
741
807
  }),
@@ -773,8 +839,7 @@ export function discoverMigratableFastDiscovery(opts = {}) {
773
839
  return !e || e.size !== s.size;
774
840
  });
775
841
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
776
- const codexCount = sessions.filter((s) => s.source === "codex").length;
777
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
842
+ const counts = sourceCounts(sessions, pending);
778
843
  return {
779
844
  sessions,
780
845
  pending,
@@ -782,10 +847,7 @@ export function discoverMigratableFastDiscovery(opts = {}) {
782
847
  alreadyMigrated: selectable.length - pendingAll.length,
783
848
  skippedActive,
784
849
  limited: pending.length < pendingAll.length,
785
- codexCount,
786
- claudeCount: sessions.length - codexCount,
787
- pendingCodex,
788
- pendingClaudeCode: pending.length - pendingCodex,
850
+ ...counts,
789
851
  };
790
852
  }
791
853
  export function discoverMigratableSummaryFast(opts = {}) {
@@ -843,8 +905,7 @@ export function discoverMigratableSessions(opts = {}) {
843
905
  return !e || e.size !== s.size;
844
906
  });
845
907
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
846
- const codexCount = sessions.filter((s) => s.source === "codex").length;
847
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
908
+ const counts = sourceCounts(sessions, pending);
848
909
  return {
849
910
  sessions,
850
911
  pending,
@@ -852,10 +913,7 @@ export function discoverMigratableSessions(opts = {}) {
852
913
  alreadyMigrated: selectableSessions.length - pendingAll.length,
853
914
  skippedActive,
854
915
  limited: pending.length < pendingAll.length,
855
- codexCount,
856
- claudeCount: sessions.length - codexCount,
857
- pendingCodex,
858
- pendingClaudeCode: pending.length - pendingCodex,
916
+ ...counts,
859
917
  };
860
918
  }
861
919
  /**
@@ -873,11 +931,16 @@ export function discoverPendingSessionsTargeted(processedKeys, opts = {}) {
873
931
  const filtered = processedKeys ? applyFastAccountImportStatus(fast, processedKeys, opts) : fast;
874
932
  const pending = [];
875
933
  for (const entry of filtered.pending) {
876
- 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
+ });
877
940
  if (s)
878
941
  pending.push(s);
879
942
  }
880
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
943
+ const pendingCounts = sourceCounts(pending, pending);
881
944
  return {
882
945
  sessions: pending,
883
946
  pending,
@@ -887,8 +950,11 @@ export function discoverPendingSessionsTargeted(processedKeys, opts = {}) {
887
950
  limited: filtered.limited,
888
951
  codexCount: filtered.codexCount,
889
952
  claudeCount: filtered.claudeCount,
890
- pendingCodex,
891
- pendingClaudeCode: pending.length - pendingCodex,
953
+ claudeCodeCount: filtered.claudeCodeCount,
954
+ coworkCount: filtered.coworkCount,
955
+ pendingCodex: pendingCounts.pendingCodex,
956
+ pendingClaudeCode: pendingCounts.pendingClaudeCode,
957
+ pendingCowork: pendingCounts.pendingCowork,
892
958
  accountChecked: filtered.accountChecked,
893
959
  accountCheckFailed: filtered.accountCheckFailed,
894
960
  accountCheckUnavailable: filtered.accountCheckUnavailable,
@@ -901,7 +967,7 @@ export function applyAccountImportStatus(discovery, processedKeys, opts = {}) {
901
967
  const skippedActive = discovery.sessions.length - selectableSessions.length;
902
968
  const pendingAll = selectableSessions.filter((s) => !processedKeys.has(s.conversationKey));
903
969
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
904
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
970
+ const counts = sourceCounts(discovery.sessions, pending);
905
971
  return {
906
972
  ...discovery,
907
973
  pending,
@@ -909,8 +975,9 @@ export function applyAccountImportStatus(discovery, processedKeys, opts = {}) {
909
975
  alreadyMigrated: selectableSessions.length - pendingAll.length,
910
976
  skippedActive,
911
977
  limited: pending.length < pendingAll.length,
912
- pendingCodex,
913
- pendingClaudeCode: pending.length - pendingCodex,
978
+ pendingCodex: counts.pendingCodex,
979
+ pendingClaudeCode: counts.pendingClaudeCode,
980
+ pendingCowork: counts.pendingCowork,
914
981
  accountChecked: true,
915
982
  accountCheckFailed: false,
916
983
  accountCheckUnavailable: false,
@@ -923,7 +990,7 @@ export function applyFastAccountImportStatus(discovery, processedKeys, opts = {}
923
990
  const skippedActive = discovery.sessions.length - selectableSessions.length;
924
991
  const pendingAll = selectableSessions.filter((s) => !processedKeys.has(s.conversationKey));
925
992
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
926
- const pendingCodex = pending.filter((s) => s.source === "codex").length;
993
+ const counts = sourceCounts(discovery.sessions, pending);
927
994
  return {
928
995
  ...discovery,
929
996
  pending,
@@ -931,8 +998,9 @@ export function applyFastAccountImportStatus(discovery, processedKeys, opts = {}
931
998
  alreadyMigrated: selectableSessions.length - pendingAll.length,
932
999
  skippedActive,
933
1000
  limited: pending.length < pendingAll.length,
934
- pendingCodex,
935
- pendingClaudeCode: pending.length - pendingCodex,
1001
+ pendingCodex: counts.pendingCodex,
1002
+ pendingClaudeCode: counts.pendingClaudeCode,
1003
+ pendingCowork: counts.pendingCowork,
936
1004
  accountChecked: true,
937
1005
  accountCheckFailed: false,
938
1006
  accountCheckUnavailable: false,
@@ -1238,10 +1306,10 @@ export async function cmdMigrate(flags) {
1238
1306
  }
1239
1307
  }
1240
1308
  }
1241
- 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;
1242
1310
  const estimateSessions = includeActive ? sessions : sessions.filter((s) => !isActiveMigrationSession(s));
1243
1311
  if (!sessions.length) {
1244
- 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.");
1245
1313
  return;
1246
1314
  }
1247
1315
  if (flags.estimate === true && flags.json === true) {
@@ -1254,7 +1322,7 @@ export async function cmdMigrate(flags) {
1254
1322
  const pendingText = limited
1255
1323
  ? `${c.bold(String(pending.length))} of ${c.bold(String(pendingTotal))} ${pendingName} selected`
1256
1324
  : `${c.bold(String(pending.length))} ${pendingName} to import`;
1257
- 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}` +
1258
1326
  (alreadyMigrated ? c.dim(`, ${alreadyMigrated} already migrated`) : "") +
1259
1327
  (skippedActive ? c.dim(`, ${skippedActive} active skipped`) : ""));
1260
1328
  if (accountChecked)
@@ -1279,7 +1347,8 @@ export async function cmdMigrate(flags) {
1279
1347
  if (flags["dry-run"] === true) {
1280
1348
  console.log(c.dim("\n--dry-run: discovered + assembled only, nothing sent.\n"));
1281
1349
  for (const s of pending.slice(0, 50)) {
1282
- 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`);
1283
1352
  }
1284
1353
  if (pending.length > 50)
1285
1354
  console.log(c.dim(` … and ${pending.length - 50} more`));
@@ -1304,7 +1373,8 @@ export async function cmdMigrate(flags) {
1304
1373
  metricsFile,
1305
1374
  selection,
1306
1375
  onProgress: (ev) => {
1307
- 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)}`;
1308
1378
  if (ev.error) {
1309
1379
  console.log(`${tag} ${c.red("✗ failed")} ${c.dim(truncate(ev.error, 60))}` + (ev.durationMs ? c.dim(` ${humanDuration(ev.durationMs)}`) : ""));
1310
1380
  return;