@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/hud/hooks.js CHANGED
@@ -2,7 +2,6 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { atomicWriteJsonObject, readJsonObjectFile } from "../config-files.js";
6
5
  export function installHooks(mode) {
7
6
  const written = [];
8
7
  if (mode === "codex" || mode === "both" || mode === "auto") {
@@ -34,16 +33,16 @@ export function installSourceSessionHooks(mode) {
34
33
  return written;
35
34
  }
36
35
  function installCodexHooks() {
37
- const dir = profileDirectory("CODEX_HOME", ".codex");
36
+ const dir = path.join(os.homedir(), ".codex");
38
37
  const file = path.join(dir, "hooks.json");
39
38
  fs.mkdirSync(dir, { recursive: true });
40
39
  const hookCommand = `${JSON.stringify(process.execPath)} ${JSON.stringify(process.argv[1])} summary --client codex --json`;
41
40
  const content = readHooksFile(file);
42
41
  content.hooks = content.hooks || {};
43
- content.hooks.PostToolUse = mergeHookGroup(content.hooks.PostToolUse, { matcher: "*", hooks: [codexCommandHook(hookCommand, 5)] });
44
- content.hooks.PostCompact = mergeHookGroup(content.hooks.PostCompact, { hooks: [codexCommandHook(hookCommand, 5)] });
45
- content.hooks.Stop = mergeHookGroup(content.hooks.Stop, { hooks: [codexCommandHook(hookCommand, 5)] });
46
- atomicWriteJsonObject(file, content);
42
+ content.hooks.PostToolUse = mergeHookGroup(content.hooks.PostToolUse, { matcher: "*", hooks: [{ type: "command", command: hookCommand, timeout: 5 }] });
43
+ content.hooks.PostCompact = mergeHookGroup(content.hooks.PostCompact, { hooks: [{ type: "command", command: hookCommand, timeout: 5 }] });
44
+ content.hooks.Stop = mergeHookGroup(content.hooks.Stop, { hooks: [{ type: "command", command: hookCommand, timeout: 5 }] });
45
+ fs.writeFileSync(file, JSON.stringify(content, null, 2));
47
46
  return file;
48
47
  }
49
48
  function saveCheckpointCommand() {
@@ -55,19 +54,24 @@ function sourceSessionCommand() {
55
54
  return `${JSON.stringify(process.execPath)} ${JSON.stringify(lifecycleCli)} bind-source-session`;
56
55
  }
57
56
  function installCodexSourceSessionHook() {
58
- const dir = profileDirectory("CODEX_HOME", ".codex");
57
+ const dir = path.join(os.homedir(), ".codex");
59
58
  const file = path.join(dir, "hooks.json");
60
59
  fs.mkdirSync(dir, { recursive: true });
61
60
  const content = readHooksFile(file);
62
61
  content.hooks = content.hooks || {};
63
62
  content.hooks.SessionStart = mergeSourceSessionGroup(content.hooks.SessionStart, {
64
- hooks: [codexCommandHook(sourceSessionCommand(), 5, "Binding EchoMem to this conversation")],
63
+ hooks: [{
64
+ type: "command",
65
+ command: sourceSessionCommand(),
66
+ timeout: 5,
67
+ statusMessage: "Binding EchoMem to this conversation",
68
+ }],
65
69
  });
66
- atomicWriteJsonObject(file, content);
70
+ fs.writeFileSync(file, JSON.stringify(content, null, 2));
67
71
  return file;
68
72
  }
69
73
  function installClaudeCodeSourceSessionHook() {
70
- const dir = profileDirectory("CLAUDE_CONFIG_DIR", ".claude");
74
+ const dir = path.join(os.homedir(), ".claude");
71
75
  const file = path.join(dir, "settings.json");
72
76
  fs.mkdirSync(dir, { recursive: true });
73
77
  const content = readHooksFile(file);
@@ -80,23 +84,28 @@ function installClaudeCodeSourceSessionHook() {
80
84
  statusMessage: "Binding EchoMem to this conversation",
81
85
  }],
82
86
  });
83
- atomicWriteJsonObject(file, content);
87
+ fs.writeFileSync(file, JSON.stringify(content, null, 2));
84
88
  return file;
85
89
  }
86
90
  function installCodexSaveCheckpointHook() {
87
- const dir = profileDirectory("CODEX_HOME", ".codex");
91
+ const dir = path.join(os.homedir(), ".codex");
88
92
  const file = path.join(dir, "hooks.json");
89
93
  fs.mkdirSync(dir, { recursive: true });
90
94
  const content = readHooksFile(file);
91
95
  content.hooks = content.hooks || {};
92
96
  content.hooks.Stop = mergeSaveCheckpointGroup(content.hooks.Stop, {
93
- hooks: [codexCommandHook(saveCheckpointCommand(), 10, "Checking whether completed work should be remembered")],
97
+ hooks: [{
98
+ type: "command",
99
+ command: saveCheckpointCommand(),
100
+ timeout: 10,
101
+ statusMessage: "Checking whether completed work should be remembered",
102
+ }],
94
103
  });
95
- atomicWriteJsonObject(file, content);
104
+ fs.writeFileSync(file, JSON.stringify(content, null, 2));
96
105
  return file;
97
106
  }
98
107
  function installClaudeCodeSaveCheckpointHook() {
99
- const dir = profileDirectory("CLAUDE_CONFIG_DIR", ".claude");
108
+ const dir = path.join(os.homedir(), ".claude");
100
109
  const file = path.join(dir, "settings.json");
101
110
  fs.mkdirSync(dir, { recursive: true });
102
111
  const content = readHooksFile(file);
@@ -109,43 +118,22 @@ function installClaudeCodeSaveCheckpointHook() {
109
118
  statusMessage: "Checking whether completed work should be remembered",
110
119
  }],
111
120
  });
112
- atomicWriteJsonObject(file, content);
121
+ fs.writeFileSync(file, JSON.stringify(content, null, 2));
113
122
  return file;
114
123
  }
115
124
  function installClaudeCodeSnippet() {
116
- const dir = path.join(profileDirectory("CLAUDE_CONFIG_DIR", ".claude"), "echo-ctx");
125
+ const dir = path.join(os.homedir(), ".claude", "echo-ctx");
117
126
  fs.mkdirSync(dir, { recursive: true });
118
127
  return dir;
119
128
  }
120
129
  function readHooksFile(file) {
121
- return readJsonObjectFile(file, "agent hooks");
122
- }
123
- function profileDirectory(envKey, fallbackName) {
124
- const home = os.homedir();
125
- const configured = process.env[envKey]?.trim();
126
- if (!configured)
127
- return path.join(home, fallbackName);
128
- if (configured === "~")
129
- return home;
130
- if (configured.startsWith("~/") || configured.startsWith("~\\")) {
131
- return path.join(home, configured.slice(2));
130
+ try {
131
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
132
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
132
133
  }
133
- if (!path.isAbsolute(configured)) {
134
- throw new Error(`${envKey} must be an absolute path or start with ~/`);
134
+ catch {
135
+ return {};
135
136
  }
136
- return path.normalize(configured);
137
- }
138
- function codexCommandHook(command, timeout, statusMessage) {
139
- return {
140
- type: "command",
141
- command,
142
- // Codex supports an explicit Windows override. Keeping it identical is intentional: the
143
- // absolute node.exe and CLI paths are already quoted, and Codex can select this field without
144
- // interpreting a POSIX-only fallback.
145
- commandWindows: command,
146
- timeout,
147
- ...(statusMessage ? { statusMessage } : {}),
148
- };
149
137
  }
150
138
  function mergeHookGroup(existing, group) {
151
139
  const groups = Array.isArray(existing) ? existing.filter((item) => !isEchoHudGroup(item)) : [];
package/dist/index.js CHANGED
@@ -7,11 +7,12 @@ 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";
10
11
  import { contextHealthMarkdown, recomposeCapsuleMarkdown } from "./hud/api.js";
11
12
  import { createHash, randomUUID } from "node:crypto";
12
13
  import { AsyncLocalStorage } from "node:async_hooks";
13
14
  import { fetchEncryptionConfig, decryptMemoryFields, verifyKeyB64, } from "./encryption.js";
14
- import { refreshInstalledMemoryGuidance, runCli } from "./setup.js";
15
+ import { runCli } from "./setup.js";
15
16
  import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS, MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, } from "./package-metadata.js";
16
17
  import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
17
18
  import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
@@ -612,6 +613,8 @@ function toolEventName(canonicalName, status) {
612
613
  class EchoMemApiClient {
613
614
  store;
614
615
  axios;
616
+ activeToken;
617
+ accountGeneration = 0;
615
618
  boundSourceSession = null;
616
619
  requestSourceSession = new AsyncLocalStorage();
617
620
  sourceSessionsByCanonicalKey = new Map();
@@ -622,6 +625,7 @@ class EchoMemApiClient {
622
625
  deleteConfirmations = new Map();
623
626
  constructor(store) {
624
627
  this.store = store;
628
+ this.activeToken = store.getToken();
625
629
  this.axios = axios.create({
626
630
  baseURL: ECHO_API_BASE_URL.replace(/\/$/, ""),
627
631
  headers: {
@@ -631,7 +635,7 @@ class EchoMemApiClient {
631
635
  // Read the token fresh on EVERY request (not baked in at construction) so a `login` that runs
632
636
  // after the editor already launched the bridge is picked up on the next call — no restart.
633
637
  this.axios.interceptors.request.use((config) => {
634
- const token = this.store.getToken();
638
+ const token = this.synchronizeAccountContext();
635
639
  if (token)
636
640
  config.headers.set("Authorization", `Bearer ${token}`);
637
641
  const sourceSession = this.getBoundSourceSession();
@@ -641,21 +645,46 @@ class EchoMemApiClient {
641
645
  return config;
642
646
  });
643
647
  }
648
+ /**
649
+ * Account-scoped state must move atomically with the credential file. The token is intentionally
650
+ * read fresh, but encryption, identity, source-session, and confirmation caches belong to the
651
+ * token that populated them and cannot survive an in-process account switch.
652
+ */
653
+ synchronizeAccountContext() {
654
+ const token = this.store.getToken();
655
+ if (token === this.activeToken)
656
+ return token;
657
+ this.activeToken = token;
658
+ this.accountGeneration += 1;
659
+ this.encConfigPromise = null;
660
+ this.whoamiCache = null;
661
+ this.boundSourceSession = null;
662
+ this.sourceSessionsByCanonicalKey.clear();
663
+ this.deleteConfirmations.clear();
664
+ return token;
665
+ }
666
+ /** Lets the MCP server invalidate its account-scoped tool-description caches too. */
667
+ refreshAccountContext() {
668
+ this.synchronizeAccountContext();
669
+ return this.accountGeneration;
670
+ }
644
671
  /** Whether a usable API token currently exists (read fresh from the keystore each call). */
645
672
  hasToken() {
646
- return !!this.store.getToken();
673
+ return !!this.synchronizeAccountContext();
647
674
  }
648
675
  /** The per-process session id — the join key telemetry shares with grouped saves. */
649
676
  getSessionId() {
650
677
  return this.sessionId;
651
678
  }
652
679
  getBoundSourceSession() {
680
+ this.synchronizeAccountContext();
653
681
  return this.requestSourceSession.getStore() ?? this.boundSourceSession;
654
682
  }
655
683
  async withSourceSession(sourceSession, operation) {
656
684
  return sourceSession ? this.requestSourceSession.run(sourceSession, operation) : operation();
657
685
  }
658
686
  async bindSourceSession(verified, persistForBridge = true) {
687
+ this.synchronizeAccountContext();
659
688
  const cached = this.sourceSessionsByCanonicalKey.get(verified.canonicalKey);
660
689
  if (cached) {
661
690
  if (persistForBridge)
@@ -738,16 +767,23 @@ class EchoMemApiClient {
738
767
  return undefined;
739
768
  }
740
769
  }
741
- /** Encryption config for the account, fetched once and cached. Failures are not cached. */
770
+ /** Encryption config for the current account, cached only for the lifetime of its token. */
742
771
  async getEncryptionConfig() {
772
+ const generation = this.refreshAccountContext();
743
773
  if (!this.encConfigPromise) {
744
- this.encConfigPromise = fetchEncryptionConfig(this.axios).catch((error) => {
745
- this.encConfigPromise = null; // allow retry next call
774
+ const request = fetchEncryptionConfig(this.axios).catch((error) => {
775
+ if (this.encConfigPromise === request)
776
+ this.encConfigPromise = null; // allow retry next call
746
777
  console.error(`[encryption] config fetch failed: ${describeError(error)} — refusing memory access`);
747
778
  throw new EncryptionStatusUnavailableError("EchoMem could not verify the account encryption state");
748
779
  });
780
+ this.encConfigPromise = request;
749
781
  }
750
- return this.encConfigPromise;
782
+ const request = this.encConfigPromise;
783
+ const config = await request;
784
+ if (this.refreshAccountContext() !== generation)
785
+ return this.getEncryptionConfig();
786
+ return config;
751
787
  }
752
788
  /**
753
789
  * Resolve the encryption state for a read/write. For encrypted accounts this REQUIRES a usable
@@ -755,13 +791,19 @@ class EchoMemApiClient {
755
791
  * handing the model ciphertext. For unencrypted accounts it returns `{ enabled: false }`.
756
792
  */
757
793
  async encState() {
794
+ const generation = this.refreshAccountContext();
758
795
  const cfg = await this.getEncryptionConfig();
796
+ if (this.refreshAccountContext() !== generation)
797
+ return this.encState();
759
798
  if (!cfg.enabled)
760
799
  return { enabled: false };
761
800
  const key = this.store.getKey();
762
801
  if (!key)
763
802
  throw new LockedError("EchoMem vault locked");
764
- if (!(await verifyKeyB64(key, cfg))) {
803
+ const isValid = await verifyKeyB64(key, cfg);
804
+ if (this.refreshAccountContext() !== generation)
805
+ return this.encState();
806
+ if (!isValid) {
765
807
  // A file-backed stale key can be removed safely; environment-provided
766
808
  // keys remain under the caller's control and are ignored until corrected.
767
809
  if (!process.env.ECHO_ENCRYPTION_KEY)
@@ -778,16 +820,23 @@ class EchoMemApiClient {
778
820
  }
779
821
  }
780
822
  async whoami() {
823
+ const generation = this.refreshAccountContext();
781
824
  if (!this.whoamiCache) {
782
- this.whoamiCache = this.axios
825
+ const request = this.axios
783
826
  .get("/api/openclaw/v1/whoami", { timeout: 6000 })
784
827
  .then((response) => response.data)
785
828
  .catch((error) => {
786
- this.whoamiCache = null; // don't pin a token-less failure; retry once a token exists
829
+ if (this.whoamiCache === request)
830
+ this.whoamiCache = null;
787
831
  throw error;
788
832
  });
833
+ this.whoamiCache = request;
789
834
  }
790
- return this.whoamiCache;
835
+ const request = this.whoamiCache;
836
+ const identity = await request;
837
+ if (this.refreshAccountContext() !== generation)
838
+ return this.whoami();
839
+ return identity;
791
840
  }
792
841
  async fetchMemoryById(id, enc) {
793
842
  try {
@@ -1254,6 +1303,7 @@ function capWait(pending) {
1254
1303
  class EchoMemMCPServer {
1255
1304
  server;
1256
1305
  client;
1306
+ accountGeneration = 0;
1257
1307
  mapCache = null;
1258
1308
  groupMapCache = null;
1259
1309
  events;
@@ -1326,8 +1376,19 @@ class EchoMemMCPServer {
1326
1376
  platform_source: hostPlatform,
1327
1377
  };
1328
1378
  }
1379
+ refreshAccountContext() {
1380
+ const generation = this.client.refreshAccountContext();
1381
+ if (generation === this.accountGeneration)
1382
+ return;
1383
+ this.accountGeneration = generation;
1384
+ this.mapCache = null;
1385
+ this.groupMapCache = null;
1386
+ this.mapInjected = false;
1387
+ this.groupMapInjected = false;
1388
+ }
1329
1389
  setupToolHandlers() {
1330
1390
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
1391
+ this.refreshAccountContext();
1331
1392
  const clientVersion = this.server.getClientVersion();
1332
1393
  this.mcpClientName = clientVersion?.name ?? this.mcpClientName;
1333
1394
  this.mcpClientVersion = clientVersion?.version ?? this.mcpClientVersion;
@@ -1359,6 +1420,7 @@ class EchoMemMCPServer {
1359
1420
  return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
1360
1421
  });
1361
1422
  this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1423
+ this.refreshAccountContext();
1362
1424
  const resolvedCanonicalName = resolveCanonicalToolName(request.params.name);
1363
1425
  const recallRoute = routePersonalRecallInvocation(resolvedCanonicalName, request.params.arguments);
1364
1426
  const canonicalName = recallRoute.canonicalName;
@@ -1410,6 +1472,10 @@ class EchoMemMCPServer {
1410
1472
  if (recallRoute.error) {
1411
1473
  throw new McpError(ErrorCode.InvalidParams, recallRoute.error);
1412
1474
  }
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
+ }
1413
1479
  if (canonicalName === canonicalToolNames.updateStatus) {
1414
1480
  if (DESKTOP_MANAGED) {
1415
1481
  return {
@@ -1605,7 +1671,8 @@ class EchoMemMCPServer {
1605
1671
  rec.ok = rec.error_kind === "none";
1606
1672
  rec.latency_ms = Date.now() - t0;
1607
1673
  this.events.record(rec);
1608
- if (canonicalName !== canonicalToolNames.updateStatus &&
1674
+ if (canonicalName !== canonicalToolNames.report &&
1675
+ canonicalName !== canonicalToolNames.updateStatus &&
1609
1676
  canonicalName !== canonicalToolNames.contextHealth &&
1610
1677
  canonicalName !== canonicalToolNames.recompose &&
1611
1678
  canonicalName !== canonicalToolNames.save &&
@@ -2697,9 +2764,6 @@ async function main() {
2697
2764
  // inherited descriptor — can't leave the terminal hanging after setup/migrate completes.
2698
2765
  process.exit(process.exitCode ?? 0);
2699
2766
  }
2700
- // Existing standalone users may receive a new runtime without re-running setup. Refresh only
2701
- // EchoMem's marker-owned block so stale Desktop-era instructions disappear automatically.
2702
- refreshInstalledMemoryGuidance();
2703
2767
  const store = new KeyStore();
2704
2768
  // Serve mode is meant to be SPAWNED by the MCP client (editor), which drives us over a piped stdin.
2705
2769
  // A human who runs the bare server in a terminal instead gets a process that blocks forever on stdio
@@ -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
+ }