@echomem/echo-memory-cloud-openclaw-plugin 0.2.0 → 0.2.2

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 CHANGED
@@ -59,8 +59,11 @@ Path resolution order for `memoryDir`:
59
59
  Supported runtime `.env` locations:
60
60
 
61
61
  - `~/.openclaw/.env`
62
- - `~/.moltbot/.env`
63
- - `~/.clawdbot/.env`
62
+
63
+ Legacy one-release migration bridge:
64
+
65
+ - if `~/.openclaw/.env` is missing, EchoMemory can still read `~/.moltbot/.env` or `~/.clawdbot/.env`
66
+ - new saves and updated setup should go to `~/.openclaw/.env`
64
67
 
65
68
  Supported environment variables:
66
69
 
@@ -132,6 +135,11 @@ Older configs may still include `baseUrl` or `webBaseUrl`. Those keys are deprec
132
135
 
133
136
  ### Install from a local path
134
137
 
138
+ Version note:
139
+
140
+ - on OpenClaw `2026.3.22+`, avoid bare plugin names during install because plugin source precedence changed
141
+ - use an exact local path, `--link`, or the exact scoped npm package
142
+
135
143
  On Windows, quote the path if your username or folders contain spaces:
136
144
 
137
145
  ```powershell
@@ -229,6 +237,7 @@ The plugin starts a localhost workspace UI during gateway startup and can auto-o
229
237
 
230
238
  - first run can automatically trigger `npm install` and `npm run build` under `lib/local-ui`
231
239
  - browser auto-open is skipped automatically for SSH, CI, and headless Linux sessions
240
+ - when the gateway restarts, an already-open local UI tab reconnects and refreshes itself instead of spawning a redundant new tab
232
241
  - `/echo-memory view` returns the current localhost URL for the local markdown workspace UI and also tries to open the browser
233
242
  - natural-language requests can use the `echo_memory_local_ui` tool to get the exact live URL instead of guessing the port
234
243
  - the local markdown archive stays fully browsable even when no Echo Cloud API key is configured
@@ -2,7 +2,7 @@
2
2
  "id": "echo-memory-cloud-openclaw-plugin",
3
3
  "name": "Echo Memory Cloud OpenClaw Plugin",
4
4
  "description": "Sync OpenClaw local markdown memory files to Echo cloud",
5
- "version": "0.2.0",
5
+ "version": "0.2.2",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "memoryDir": {
24
24
  "type": "string",
25
- "description": "Absolute path to the markdown memory directory. Falls back to ECHOMEM_MEMORY_DIR, then ~/.clawdbot/workspace/memory."
25
+ "description": "Absolute path to the markdown memory directory. Falls back to ECHOMEM_MEMORY_DIR, then ~/.openclaw/workspace/memory."
26
26
  },
27
27
  "localOnlyMode": {
28
28
  "type": "boolean",
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
- import { buildConfig, getEnvFileStatus } from "./lib/config.js";
3
+ import { buildConfig, getEnvFileStatus, getOpenClawHome } from "./lib/config.js";
4
4
  import { createApiClient } from "./lib/api-client.js";
5
5
  import { formatSearchResultsText } from "./lib/echo-memory-search.js";
6
6
  import {
@@ -15,7 +15,16 @@ import {
15
15
  import { buildOnboardingText } from "./lib/onboarding.js";
16
16
  import { createSyncRunner, formatStatusText } from "./lib/sync.js";
17
17
  import { readLastSyncState } from "./lib/state.js";
18
- import { openUrlInDefaultBrowser, startLocalServer, stopLocalServer } from "./lib/local-server.js";
18
+ import {
19
+ hasRecentLocalUiPresence,
20
+ openUrlInDefaultBrowser,
21
+ startLocalServer,
22
+ stopLocalServer,
23
+ waitForLocalUiClient,
24
+ } from "./lib/local-server.js";
25
+
26
+ const LOCAL_UI_RECONNECT_GRACE_MS = 4000;
27
+ const LOCAL_UI_PRESENCE_GRACE_MS = 75000;
19
28
 
20
29
  function resolveCommandLabel(channel) {
21
30
  return channel === "discord" ? "/echomemory" : "/echo-memory";
@@ -47,13 +56,15 @@ export default {
47
56
  const cfg = buildConfig(api.pluginConfig);
48
57
  const client = createApiClient(cfg);
49
58
  const workspaceDir = path.resolve(path.dirname(cfg.memoryDir), "..");
50
- const openclawHome = path.resolve(workspaceDir, "..");
51
- const fallbackStateDir = path.join(openclawHome, "state", "plugins", "echo-memory-cloud-openclaw-plugin");
59
+ const openclawHome = getOpenClawHome();
60
+ const legacyPluginStateDir = path.join(openclawHome, "state", "plugins", "echo-memory-cloud-openclaw-plugin");
61
+ const stableStateDir = path.join(openclawHome, "state", "echo-memory-cloud-openclaw-plugin");
52
62
  const syncRunner = createSyncRunner({
53
63
  api,
54
64
  cfg,
55
65
  client,
56
- fallbackStateDir,
66
+ fallbackStateDir: legacyPluginStateDir,
67
+ stableStateDir,
57
68
  });
58
69
  let startupBrowserOpenAttempted = false;
59
70
  let backgroundStarted = false;
@@ -75,10 +86,20 @@ export default {
75
86
  let openedInBrowser = false;
76
87
  let openReason = "not_requested";
77
88
  if (openInBrowser) {
78
- const openResult = await openUrlInDefaultBrowser(url, {
79
- logger: api.logger,
80
- force: trigger !== "gateway-start",
81
- });
89
+ const existingPageDetected = trigger === "gateway-start"
90
+ ? await hasRecentLocalUiPresence(syncRunner, { maxAgeMs: LOCAL_UI_PRESENCE_GRACE_MS })
91
+ : false;
92
+ const existingClientDetected = existingPageDetected || (
93
+ trigger === "gateway-start"
94
+ ? await waitForLocalUiClient({ timeoutMs: LOCAL_UI_RECONNECT_GRACE_MS })
95
+ : false
96
+ );
97
+ const openResult = existingClientDetected
98
+ ? { opened: false, reason: existingPageDetected ? "existing_page_detected" : "existing_client_reconnected" }
99
+ : await openUrlInDefaultBrowser(url, {
100
+ logger: api.logger,
101
+ force: trigger !== "gateway-start",
102
+ });
82
103
  openedInBrowser = openResult.opened;
83
104
  openReason = openResult.reason;
84
105
  }
@@ -130,7 +151,18 @@ export default {
130
151
  });
131
152
  }
132
153
 
133
- const envStatus = getEnvFileStatus();
154
+ const envStatus = getEnvFileStatus();
155
+ if (envStatus.usingLegacyBridge) {
156
+ api.logger?.warn?.(
157
+ `[echo-memory] Legacy env file detected (${envStatus.legacyPaths.join(", ")}). ` +
158
+ `EchoMemory is using a one-release migration bridge and will write future changes to ${envStatus.primaryPath}.`,
159
+ );
160
+ } else if (envStatus.legacyPaths.length > 0) {
161
+ api.logger?.info?.(
162
+ `[echo-memory] Legacy env file still present (${envStatus.legacyPaths.join(", ")}). ` +
163
+ `Current settings should be kept in ${envStatus.primaryPath}.`,
164
+ );
165
+ }
134
166
  if (!envStatus.found) {
135
167
  api.logger?.warn?.(
136
168
  `[echo-memory] No .env file found in ${envStatus.searchPaths.join(", ")}. Using plugin config or process env.`,
@@ -142,7 +174,7 @@ export default {
142
174
  return;
143
175
  }
144
176
  backgroundStarted = true;
145
- await syncRunner.initialize(stateDir || fallbackStateDir);
177
+ await syncRunner.initialize(stateDir || legacyPluginStateDir);
146
178
 
147
179
  try {
148
180
  const shouldOpenBrowser = cfg.localUiAutoOpenOnGatewayStart && !startupBrowserOpenAttempted;
@@ -196,7 +228,7 @@ export default {
196
228
  if (serviceStartObserved) {
197
229
  return;
198
230
  }
199
- startBackgroundFeatures({ stateDir: fallbackStateDir, trigger: "compat-startup" }).catch((error) => {
231
+ startBackgroundFeatures({ stateDir: legacyPluginStateDir, trigger: "compat-startup" }).catch((error) => {
200
232
  api.logger?.warn?.(`[echo-memory] compatibility startup failed: ${String(error?.message ?? error)}`);
201
233
  });
202
234
  });
package/lib/config.js CHANGED
@@ -4,12 +4,25 @@ import { dirname, join } from "node:path";
4
4
 
5
5
  const DEFAULT_BASE_URL = "https://echo-mem-chrome.vercel.app";
6
6
  const DEFAULT_WEB_BASE_URL = "https://www.iditor.com";
7
- const DEFAULT_CONFIG_PATH = process.env.OPENCLAW_CONFIG_PATH || join(homedir(), ".openclaw", "openclaw.json");
8
- const ENV_SOURCES = [
9
- join(homedir(), ".openclaw", ".env"),
7
+ function resolveOpenClawHome() {
8
+ const configPath = process.env.OPENCLAW_CONFIG_PATH?.trim();
9
+ if (configPath) {
10
+ return dirname(configPath);
11
+ }
12
+ return join(homedir(), ".openclaw");
13
+ }
14
+
15
+ const OPENCLAW_HOME = resolveOpenClawHome();
16
+ const PRIMARY_ENV_PATH = join(OPENCLAW_HOME, ".env");
17
+ const LEGACY_ENV_SOURCES = [
10
18
  join(homedir(), ".moltbot", ".env"),
11
19
  join(homedir(), ".clawdbot", ".env"),
12
20
  ];
21
+ const DEFAULT_CONFIG_PATH = process.env.OPENCLAW_CONFIG_PATH || join(OPENCLAW_HOME, "openclaw.json");
22
+ const ENV_SOURCES = [
23
+ PRIMARY_ENV_PATH,
24
+ ...LEGACY_ENV_SOURCES,
25
+ ];
13
26
 
14
27
  let cachedEnv = null;
15
28
 
@@ -97,13 +110,22 @@ function parseInteger(value, fallback, { min = Number.NEGATIVE_INFINITY, max = N
97
110
 
98
111
  export function getEnvFileStatus() {
99
112
  const env = loadEnvFiles();
113
+ const legacyPaths = env.foundPaths.filter((envPath) => LEGACY_ENV_SOURCES.includes(envPath));
100
114
  return {
101
115
  found: env.foundPaths.length > 0,
102
116
  paths: env.foundPaths,
103
117
  searchPaths: env.searchPaths,
118
+ primaryPath: PRIMARY_ENV_PATH,
119
+ foundPrimary: env.foundPaths.includes(PRIMARY_ENV_PATH),
120
+ legacyPaths,
121
+ usingLegacyBridge: !env.foundPaths.includes(PRIMARY_ENV_PATH) && legacyPaths.length > 0,
104
122
  };
105
123
  }
106
124
 
125
+ export function getOpenClawHome() {
126
+ return OPENCLAW_HOME;
127
+ }
128
+
107
129
  function resolveConfigValue(pluginConfig, configKey, envKey, fallback) {
108
130
  if (pluginConfig?.[configKey] !== undefined && pluginConfig?.[configKey] !== null && pluginConfig?.[configKey] !== "") {
109
131
  return {
@@ -142,7 +164,6 @@ function maskValue(value, { keepStart = 2, keepEnd = 2 } = {}) {
142
164
  export function getLocalUiSetupState(pluginConfig = {}, cfg = null) {
143
165
  const runtimeCfg = cfg ?? buildConfig(pluginConfig);
144
166
  const envStatus = getEnvFileStatus();
145
- const targetEnvPath = envStatus.paths[0] || envStatus.searchPaths[0];
146
167
  const localOnlyMode = resolveConfigValue(
147
168
  pluginConfig,
148
169
  "localOnlyMode",
@@ -162,9 +183,12 @@ export function getLocalUiSetupState(pluginConfig = {}, cfg = null) {
162
183
  targetPath: DEFAULT_CONFIG_PATH,
163
184
  },
164
185
  envFile: {
165
- targetPath: targetEnvPath,
186
+ targetPath: envStatus.primaryPath,
187
+ activePath: envStatus.paths[0] || null,
166
188
  foundPaths: envStatus.paths,
167
189
  searchPaths: envStatus.searchPaths,
190
+ legacyPaths: envStatus.legacyPaths,
191
+ usingLegacyBridge: envStatus.usingLegacyBridge,
168
192
  },
169
193
  localOnlyMode: {
170
194
  enabled: Boolean(runtimeCfg.localOnlyMode),
@@ -190,12 +214,13 @@ export function getLocalUiSetupState(pluginConfig = {}, cfg = null) {
190
214
 
191
215
  export function saveLocalUiSetup(values = {}) {
192
216
  const envStatus = getEnvFileStatus();
193
- const targetPath = envStatus.paths[0] || envStatus.searchPaths[0];
217
+ const targetPath = envStatus.primaryPath;
218
+ const sourcePath = envStatus.foundPrimary ? envStatus.primaryPath : (envStatus.paths[0] || envStatus.primaryPath);
194
219
  mkdirSync(dirname(targetPath), { recursive: true });
195
220
 
196
221
  let lines = [];
197
222
  try {
198
- lines = readFileSync(targetPath, "utf8").split(/\r?\n/);
223
+ lines = readFileSync(sourcePath, "utf8").split(/\r?\n/);
199
224
  } catch {
200
225
  lines = [];
201
226
  }
@@ -237,6 +262,8 @@ export function saveLocalUiSetup(values = {}) {
237
262
  invalidateEnvCache();
238
263
  return {
239
264
  targetPath,
265
+ sourcePath,
266
+ migratedFrom: sourcePath !== targetPath ? sourcePath : null,
240
267
  savedKeys: [...managedKeys],
241
268
  };
242
269
  }
@@ -275,7 +302,7 @@ export function buildConfig(pluginConfig = {}) {
275
302
  memoryDir: String(
276
303
  cfg.memoryDir
277
304
  || loadEnvVar("ECHOMEM_MEMORY_DIR")
278
- || join(homedir(), ".openclaw", "workspace", "memory"),
305
+ || join(OPENCLAW_HOME, "workspace", "memory"),
279
306
  ).trim(),
280
307
  };
281
308
  }