@echomem/echo-memory-cloud-openclaw-plugin 0.2.1 → 0.2.3

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.
@@ -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.1",
5
+ "version": "0.2.3",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
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 {
@@ -16,13 +16,34 @@ import { buildOnboardingText } from "./lib/onboarding.js";
16
16
  import { createSyncRunner, formatStatusText } from "./lib/sync.js";
17
17
  import { readLastSyncState } from "./lib/state.js";
18
18
  import {
19
+ hasRecentLocalUiPresence,
19
20
  openUrlInDefaultBrowser,
20
21
  startLocalServer,
21
22
  stopLocalServer,
22
23
  waitForLocalUiClient,
23
24
  } from "./lib/local-server.js";
24
25
 
25
- const LOCAL_UI_RECONNECT_GRACE_MS = 1500;
26
+ const LOCAL_UI_RECONNECT_GRACE_MS = 4000;
27
+ const LOCAL_UI_PRESENCE_GRACE_MS = 75000;
28
+ const COMPAT_STARTUP_DELAY_MS = 1500;
29
+ const PROCESS_STATE_KEY = Symbol.for("echo-memory-cloud-openclaw-plugin.process-state");
30
+
31
+ function getProcessState() {
32
+ const globalState = globalThis;
33
+ if (!globalState[PROCESS_STATE_KEY]) {
34
+ globalState[PROCESS_STATE_KEY] = {
35
+ backgroundActive: false,
36
+ backgroundOwnerId: null,
37
+ backgroundStartPromise: null,
38
+ browserOpenAttempted: false,
39
+ browserOpenPromise: null,
40
+ compatFallbackScheduled: false,
41
+ serviceStartObserved: false,
42
+ stopBackground: null,
43
+ };
44
+ }
45
+ return globalState[PROCESS_STATE_KEY];
46
+ }
26
47
 
27
48
  function resolveCommandLabel(channel) {
28
49
  return channel === "discord" ? "/echomemory" : "/echo-memory";
@@ -51,25 +72,67 @@ export default {
51
72
  kind: "lifecycle",
52
73
 
53
74
  register(api) {
75
+ const processState = getProcessState();
76
+ const registrationId = Symbol("echo-memory-cloud-openclaw-plugin.register");
54
77
  const cfg = buildConfig(api.pluginConfig);
55
78
  const client = createApiClient(cfg);
56
79
  const workspaceDir = path.resolve(path.dirname(cfg.memoryDir), "..");
57
- const openclawHome = path.resolve(workspaceDir, "..");
58
- const fallbackStateDir = path.join(openclawHome, "state", "plugins", "echo-memory-cloud-openclaw-plugin");
80
+ const openclawHome = getOpenClawHome();
81
+ const legacyPluginStateDir = path.join(openclawHome, "state", "plugins", "echo-memory-cloud-openclaw-plugin");
82
+ const stableStateDir = path.join(openclawHome, "state", "echo-memory-cloud-openclaw-plugin");
59
83
  const syncRunner = createSyncRunner({
60
84
  api,
61
85
  cfg,
62
86
  client,
63
- fallbackStateDir,
87
+ fallbackStateDir: legacyPluginStateDir,
88
+ stableStateDir,
64
89
  });
65
- let startupBrowserOpenAttempted = false;
66
- let backgroundStarted = false;
67
- let serviceStartObserved = false;
68
90
 
69
91
  if (!workspaceDir || workspaceDir === "." || workspaceDir === path.sep) {
70
92
  api.logger?.warn?.("[echo-memory] workspace resolution looks unusual; compatibility fallback may be limited");
71
93
  }
72
94
 
95
+ async function maybeAutoOpenLocalUi(url, { trigger = "manual" } = {}) {
96
+ const existingPageDetected = trigger === "gateway-start"
97
+ ? await hasRecentLocalUiPresence(syncRunner, { maxAgeMs: LOCAL_UI_PRESENCE_GRACE_MS })
98
+ : false;
99
+ const existingClientDetected = existingPageDetected || (
100
+ trigger === "gateway-start"
101
+ ? await waitForLocalUiClient({ timeoutMs: LOCAL_UI_RECONNECT_GRACE_MS })
102
+ : false
103
+ );
104
+ return existingClientDetected
105
+ ? { opened: false, reason: existingPageDetected ? "existing_page_detected" : "existing_client_reconnected" }
106
+ : openUrlInDefaultBrowser(url, {
107
+ logger: api.logger,
108
+ force: trigger !== "gateway-start",
109
+ });
110
+ }
111
+
112
+ async function maybeAutoOpenStartupBrowser(url) {
113
+ if (!cfg.localUiAutoOpenOnGatewayStart || processState.browserOpenAttempted) {
114
+ return { attempted: false, opened: false, reason: "disabled_or_already_attempted" };
115
+ }
116
+ if (processState.browserOpenPromise) {
117
+ const result = await processState.browserOpenPromise;
118
+ return { attempted: false, ...result };
119
+ }
120
+
121
+ const openPromise = maybeAutoOpenLocalUi(url, { trigger: "gateway-start" })
122
+ .then((result) => {
123
+ processState.browserOpenAttempted = true;
124
+ return result;
125
+ })
126
+ .finally(() => {
127
+ if (processState.browserOpenPromise === openPromise) {
128
+ processState.browserOpenPromise = null;
129
+ }
130
+ });
131
+ processState.browserOpenPromise = openPromise;
132
+ const result = await openPromise;
133
+ return { attempted: true, ...result };
134
+ }
135
+
73
136
  async function ensureLocalUi({ openInBrowser = false, trigger = "manual" } = {}) {
74
137
  const url = await startLocalServer(workspaceDir, {
75
138
  apiClient: client,
@@ -82,15 +145,7 @@ export default {
82
145
  let openedInBrowser = false;
83
146
  let openReason = "not_requested";
84
147
  if (openInBrowser) {
85
- const existingClientDetected = trigger === "gateway-start"
86
- ? await waitForLocalUiClient({ timeoutMs: LOCAL_UI_RECONNECT_GRACE_MS })
87
- : false;
88
- const openResult = existingClientDetected
89
- ? { opened: false, reason: "existing_client_reconnected" }
90
- : await openUrlInDefaultBrowser(url, {
91
- logger: api.logger,
92
- force: trigger !== "gateway-start",
93
- });
148
+ const openResult = await maybeAutoOpenLocalUi(url, { trigger });
94
149
  openedInBrowser = openResult.opened;
95
150
  openReason = openResult.reason;
96
151
  }
@@ -161,68 +216,150 @@ export default {
161
216
  }
162
217
 
163
218
  async function startBackgroundFeatures({ stateDir = null, trigger = "service" } = {}) {
164
- if (backgroundStarted) {
165
- return;
166
- }
167
- backgroundStarted = true;
168
- await syncRunner.initialize(stateDir || fallbackStateDir);
169
-
170
- try {
171
- const shouldOpenBrowser = cfg.localUiAutoOpenOnGatewayStart && !startupBrowserOpenAttempted;
172
- const { url, openedInBrowser, openReason } = await ensureLocalUi({
173
- openInBrowser: shouldOpenBrowser,
174
- trigger: trigger === "service" ? "gateway-start" : trigger,
175
- });
176
- api.logger?.info?.(`[echo-memory] Local workspace viewer: ${url}`);
177
- if (shouldOpenBrowser) {
178
- startupBrowserOpenAttempted = true;
179
- if (openedInBrowser) {
180
- api.logger?.info?.("[echo-memory] Opened local workspace viewer in the default browser");
181
- } else {
182
- api.logger?.info?.(`[echo-memory] Skipped browser auto-open (${openReason})`);
219
+ const effectiveTrigger = trigger === "service" ? "gateway-start" : trigger;
220
+
221
+ if (processState.backgroundActive) {
222
+ if (effectiveTrigger === "gateway-start") {
223
+ const { attempted, opened, reason } = await maybeAutoOpenStartupBrowser(
224
+ await startLocalServer(workspaceDir, {
225
+ apiClient: client,
226
+ syncRunner,
227
+ cfg,
228
+ logger: api.logger,
229
+ pluginConfig: api.pluginConfig,
230
+ }),
231
+ );
232
+ if (attempted) {
233
+ if (opened) {
234
+ api.logger?.info?.("[echo-memory] Opened local workspace viewer in the default browser");
235
+ } else {
236
+ api.logger?.info?.(`[echo-memory] Skipped browser auto-open (${reason})`);
237
+ }
183
238
  }
184
239
  }
185
- } catch (error) {
186
- api.logger?.warn?.(`[echo-memory] local server failed: ${String(error?.message ?? error)}`);
240
+ return;
187
241
  }
188
242
 
189
- if (!cfg.autoSync) {
190
- api.logger?.info?.("[echo-memory] autoSync disabled");
243
+ if (processState.backgroundStartPromise) {
244
+ await processState.backgroundStartPromise;
245
+ if (effectiveTrigger === "gateway-start") {
246
+ const { attempted, opened, reason } = await maybeAutoOpenStartupBrowser(
247
+ await startLocalServer(workspaceDir, {
248
+ apiClient: client,
249
+ syncRunner,
250
+ cfg,
251
+ logger: api.logger,
252
+ pluginConfig: api.pluginConfig,
253
+ }),
254
+ );
255
+ if (attempted) {
256
+ if (opened) {
257
+ api.logger?.info?.("[echo-memory] Opened local workspace viewer in the default browser");
258
+ } else {
259
+ api.logger?.info?.(`[echo-memory] Skipped browser auto-open (${reason})`);
260
+ }
261
+ }
262
+ }
191
263
  return;
192
264
  }
193
265
 
194
- await syncRunner.runSync(trigger === "service" ? "startup" : trigger).catch((error) => {
195
- api.logger?.warn?.(`[echo-memory] startup sync failed: ${String(error?.message ?? error)}`);
196
- });
266
+ const startPromise = (async () => {
267
+ processState.backgroundOwnerId = registrationId;
268
+ processState.stopBackground = () => {
269
+ syncRunner.stopInterval();
270
+ stopLocalServer();
271
+ };
272
+ await syncRunner.initialize(stateDir || legacyPluginStateDir);
273
+
274
+ let url = null;
275
+ try {
276
+ const localUi = await ensureLocalUi({
277
+ openInBrowser: false,
278
+ trigger: effectiveTrigger,
279
+ });
280
+ url = localUi.url;
281
+ api.logger?.info?.(`[echo-memory] Local workspace viewer: ${url}`);
282
+ } catch (error) {
283
+ api.logger?.warn?.(`[echo-memory] local server failed: ${String(error?.message ?? error)}`);
284
+ }
285
+
286
+ processState.backgroundActive = true;
287
+
288
+ if (effectiveTrigger === "gateway-start" && url) {
289
+ const { attempted, opened, reason } = await maybeAutoOpenStartupBrowser(url);
290
+ if (attempted) {
291
+ if (opened) {
292
+ api.logger?.info?.("[echo-memory] Opened local workspace viewer in the default browser");
293
+ } else {
294
+ api.logger?.info?.(`[echo-memory] Skipped browser auto-open (${reason})`);
295
+ }
296
+ }
297
+ }
298
+
299
+ if (!cfg.autoSync) {
300
+ api.logger?.info?.("[echo-memory] autoSync disabled");
301
+ return;
302
+ }
303
+
304
+ await syncRunner.runSync(trigger === "service" ? "startup" : trigger).catch((error) => {
305
+ api.logger?.warn?.(`[echo-memory] startup sync failed: ${String(error?.message ?? error)}`);
306
+ });
197
307
 
198
- syncRunner.startInterval();
308
+ syncRunner.startInterval();
309
+ })()
310
+ .catch((error) => {
311
+ if (processState.backgroundOwnerId === registrationId) {
312
+ processState.backgroundActive = false;
313
+ processState.backgroundOwnerId = null;
314
+ processState.stopBackground = null;
315
+ }
316
+ throw error;
317
+ })
318
+ .finally(() => {
319
+ if (processState.backgroundStartPromise === startPromise) {
320
+ processState.backgroundStartPromise = null;
321
+ }
322
+ });
323
+
324
+ processState.backgroundStartPromise = startPromise;
325
+ await startPromise;
199
326
  }
200
327
 
201
328
  if (typeof api.registerService === "function") {
202
329
  api.registerService({
203
330
  id: "echo-memory-cloud-openclaw-sync",
204
331
  start: async (ctx) => {
205
- serviceStartObserved = true;
332
+ processState.serviceStartObserved = true;
206
333
  await startBackgroundFeatures({ stateDir: ctx?.stateDir || null, trigger: "service" });
207
334
  },
208
335
  stop: async () => {
209
- syncRunner.stopInterval();
210
- stopLocalServer();
211
- backgroundStarted = false;
336
+ processState.stopBackground?.();
337
+ processState.backgroundActive = false;
338
+ processState.backgroundOwnerId = null;
339
+ processState.backgroundStartPromise = null;
340
+ processState.browserOpenAttempted = false;
341
+ processState.browserOpenPromise = null;
342
+ processState.compatFallbackScheduled = false;
343
+ processState.serviceStartObserved = false;
344
+ processState.stopBackground = null;
212
345
  },
213
346
  });
214
347
  }
215
348
 
216
349
  // Compatibility fallback for older hosts that discover the plugin but do not
217
350
  // reliably auto-start registered background services.
218
- queueMicrotask(() => {
219
- if (serviceStartObserved) {
220
- return;
221
- }
222
- startBackgroundFeatures({ stateDir: fallbackStateDir, trigger: "compat-startup" }).catch((error) => {
223
- api.logger?.warn?.(`[echo-memory] compatibility startup failed: ${String(error?.message ?? error)}`);
224
- });
225
- });
351
+ if (!processState.compatFallbackScheduled) {
352
+ processState.compatFallbackScheduled = true;
353
+ setTimeout(() => {
354
+ processState.compatFallbackScheduled = false;
355
+ if (processState.serviceStartObserved || processState.backgroundActive || processState.backgroundStartPromise) {
356
+ return;
357
+ }
358
+ startBackgroundFeatures({ stateDir: legacyPluginStateDir, trigger: "compat-startup" }).catch((error) => {
359
+ api.logger?.warn?.(`[echo-memory] compatibility startup failed: ${String(error?.message ?? error)}`);
360
+ });
361
+ }, COMPAT_STARTUP_DELAY_MS);
362
+ }
226
363
 
227
364
  if (typeof api.registerCommand === "function") {
228
365
  api.registerCommand({
package/lib/config.js CHANGED
@@ -122,6 +122,10 @@ export function getEnvFileStatus() {
122
122
  };
123
123
  }
124
124
 
125
+ export function getOpenClawHome() {
126
+ return OPENCLAW_HOME;
127
+ }
128
+
125
129
  function resolveConfigValue(pluginConfig, configKey, envKey, fallback) {
126
130
  if (pluginConfig?.[configKey] !== undefined && pluginConfig?.[configKey] !== null && pluginConfig?.[configKey] !== "") {
127
131
  return {