@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.
- package/clawdbot.plugin.json +1 -1
- package/index.js +193 -56
- package/lib/config.js +4 -0
- package/lib/local-server.js +224 -83
- package/lib/local-ui/dist/assets/index-CKT0_swv.css +1 -0
- package/lib/local-ui/dist/assets/index-D9Bbjf7A.js +54 -0
- package/lib/local-ui/dist/index.html +2 -2
- package/lib/state.js +59 -4
- package/lib/sync.js +42 -15
- package/moltbot.plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/lib/local-ui/dist/assets/index-BoyzFR9Q.css +0 -1
- package/lib/local-ui/dist/assets/index-Cnb-zSN2.js +0 -54
package/clawdbot.plugin.json
CHANGED
|
@@ -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.
|
|
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 =
|
|
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 =
|
|
58
|
-
const
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
api.logger?.warn?.(`[echo-memory] local server failed: ${String(error?.message ?? error)}`);
|
|
240
|
+
return;
|
|
187
241
|
}
|
|
188
242
|
|
|
189
|
-
if (
|
|
190
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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 {
|