@memtensor/memos-local-openclaw-plugin 1.0.4-beta.15 → 1.0.4-beta.17

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/index.ts CHANGED
@@ -180,7 +180,7 @@ const memosLocalPlugin = {
180
180
  }
181
181
 
182
182
  let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
183
- const stateDir = api.resolvePath("~/.openclaw");
183
+ const stateDir = process.env.OPENCLAW_STATE_DIR || api.resolvePath("~/.openclaw");
184
184
 
185
185
  // Fallback: read config from file if not provided by OpenClaw
186
186
  const configPath = path.join(stateDir, "state", "memos-local", "config.json");
@@ -1314,7 +1314,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1314
1314
 
1315
1315
  // ─── Tool: memory_viewer ───
1316
1316
 
1317
- const viewerPort = (pluginCfg as any).viewerPort ?? 18799;
1317
+ const gatewayPort = (api.config as any)?.gateway?.port ?? 18789;
1318
+ const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
1318
1319
 
1319
1320
  api.registerTool(
1320
1321
  {
@@ -2297,6 +2298,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2297
2298
 
2298
2299
  // ─── Memory Viewer (web UI) ───
2299
2300
 
2301
+ const derivedHubPort = gatewayPort + 11;
2302
+
2300
2303
  const viewer = new ViewerServer({
2301
2304
  store,
2302
2305
  embedder,
@@ -2304,10 +2307,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2304
2307
  log: ctx.log,
2305
2308
  dataDir: stateDir,
2306
2309
  ctx,
2310
+ defaultHubPort: derivedHubPort,
2307
2311
  });
2308
-
2309
2312
  const hubServer = ctx.config.sharing?.enabled && ctx.config.sharing.role === "hub"
2310
- ? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder })
2313
+ ? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder, defaultHubPort: derivedHubPort })
2311
2314
  : null;
2312
2315
 
2313
2316
  // ─── Service lifecycle ───
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "memos-local-openclaw-plugin",
3
3
  "name": "MemOS Local Memory",
4
- "description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency). Provides memory_search, memory_get, task_summary, memory_timeline, memory_viewer for layered retrieval.",
4
+ "description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency), task summarization, skill evolution, and team sharing (Hub-Client). Provides memory_search, memory_get, task_summary, skill_search, task_share, network_skill_pull, network_team_info, memory_viewer for layered retrieval and team collaboration.",
5
5
  "kind": "memory",
6
6
  "version": "0.1.12",
7
7
  "skills": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.4-beta.15",
3
+ "version": "1.0.4-beta.17",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/src/hub/server.ts CHANGED
@@ -14,6 +14,7 @@ type HubServerOptions = {
14
14
  config: MemosLocalConfig;
15
15
  dataDir: string;
16
16
  embedder?: Embedder;
17
+ defaultHubPort?: number;
17
18
  };
18
19
 
19
20
  type HubAuthState = {
@@ -79,18 +80,31 @@ export class HubServer {
79
80
  }
80
81
  });
81
82
 
83
+ const MAX_PORT_RETRIES = 3;
84
+ let hubPort = this.port;
82
85
  await new Promise<void>((resolve, reject) => {
83
- const onError = (err: Error) => {
84
- this.server?.off("listening", onListening);
85
- reject(err);
86
+ let retries = 0;
87
+ const onError = (err: NodeJS.ErrnoException) => {
88
+ if (err.code === "EADDRINUSE" && retries < MAX_PORT_RETRIES) {
89
+ retries++;
90
+ hubPort = this.port + retries;
91
+ this.opts.log.warn(`Hub port ${hubPort - 1} in use, trying ${hubPort}`);
92
+ this.server!.listen(hubPort, "0.0.0.0");
93
+ } else {
94
+ this.server?.off("listening", onListening);
95
+ reject(err);
96
+ }
86
97
  };
87
98
  const onListening = () => {
88
99
  this.server?.off("error", onError);
100
+ if (hubPort !== this.port) {
101
+ this.opts.log.info(`Hub started on fallback port ${hubPort} (configured: ${this.port})`);
102
+ }
89
103
  resolve();
90
104
  };
91
- this.server!.once("error", onError);
105
+ this.server!.on("error", onError);
92
106
  this.server!.once("listening", onListening);
93
- this.server!.listen(this.port, "0.0.0.0");
107
+ this.server!.listen(hubPort, "0.0.0.0");
94
108
  });
95
109
 
96
110
  const bootstrap = this.userManager.ensureBootstrapAdmin(
@@ -109,19 +123,37 @@ export class HubServer {
109
123
  this.initOnlineTracking();
110
124
  this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
111
125
 
112
- return `http://127.0.0.1:${this.port}`;
126
+ return `http://127.0.0.1:${hubPort}`;
113
127
  }
114
128
 
115
129
  async stop(): Promise<void> {
116
130
  if (this.offlineCheckTimer) { clearInterval(this.offlineCheckTimer); this.offlineCheckTimer = undefined; }
117
131
  if (!this.server) return;
132
+
133
+ try {
134
+ const activeUsers = this.opts.store.listHubUsers("active");
135
+ const ownerId = this.authState.bootstrapAdminUserId || "";
136
+ for (const u of activeUsers) {
137
+ if (u.id === ownerId) continue;
138
+ try {
139
+ this.opts.store.insertHubNotification({
140
+ id: randomUUID(), userId: u.id, type: "hub_shutdown",
141
+ resource: "system", title: `Team server "${this.teamName}" has been shut down by the admin.`,
142
+ });
143
+ } catch { /* best-effort */ }
144
+ }
145
+ } catch { /* best-effort */ }
146
+
118
147
  const server = this.server;
119
148
  this.server = undefined;
120
149
  await new Promise<void>((resolve) => server.close(() => resolve()));
121
150
  }
122
151
 
123
152
  private get port(): number {
124
- return this.opts.config.sharing?.hub?.port ?? 18800;
153
+ const configured = this.opts.config.sharing?.hub?.port;
154
+ const derived = this.opts.defaultHubPort;
155
+ if (derived && (!configured || configured === 18800)) return derived;
156
+ return configured ?? 18800;
125
157
  }
126
158
 
127
159
  private get teamName(): string {
@@ -320,6 +352,22 @@ export class HubServer {
320
352
  return this.json(res, 200, { status: user.status });
321
353
  }
322
354
 
355
+ if (req.method === "POST" && routePath === "/api/v1/hub/withdraw-pending") {
356
+ const body = await this.readJson(req);
357
+ if (!body || body.teamToken !== this.teamToken) {
358
+ return this.json(res, 403, { error: "invalid_team_token" });
359
+ }
360
+ const userId = String(body.userId || "");
361
+ if (!userId) return this.json(res, 400, { error: "missing_user_id" });
362
+ const user = this.opts.store.getHubUser(userId);
363
+ if (!user) return this.json(res, 200, { ok: true });
364
+ if (user.status === "pending") {
365
+ this.userManager.markUserLeft(userId);
366
+ this.opts.log.info(`Hub: user "${user.username}" (${userId}) withdrew pending application`);
367
+ }
368
+ return this.json(res, 200, { ok: true });
369
+ }
370
+
323
371
  // All endpoints below require authentication + rate limiting
324
372
  const auth = this.authenticate(req);
325
373
  if (!auth) return this.json(res, 401, { error: "unauthorized" });
@@ -336,7 +384,7 @@ export class HubServer {
336
384
  if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
337
385
  this.userManager.markUserLeft(auth.userId);
338
386
  this.knownOnlineUsers.delete(auth.userId);
339
- this.notifyAdmins("user_offline", "user", auth.username, auth.userId);
387
+ this.notifyAdmins("user_left", "user", auth.username, auth.userId);
340
388
  this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily, status set to "left"`);
341
389
  return this.json(res, 200, { ok: true });
342
390
  }
@@ -441,6 +489,13 @@ export class HubServer {
441
489
  const updatedUser = { ...user, role: newRole as "admin" | "member" };
442
490
  this.opts.store.upsertHubUser(updatedUser);
443
491
  this.opts.log.info(`Hub: admin "${auth.userId}" changed role of "${userId}" to "${newRole}"`);
492
+ try {
493
+ const notifType = newRole === "admin" ? "role_promoted" : "role_demoted";
494
+ this.opts.store.insertHubNotification({
495
+ id: randomUUID(), userId, type: notifType,
496
+ resource: "user", title: `Your role in team "${this.teamName}" has been changed to ${newRole}.`,
497
+ });
498
+ } catch { /* best-effort */ }
444
499
  return this.json(res, 200, { ok: true, role: newRole });
445
500
  }
446
501
 
@@ -478,9 +533,16 @@ export class HubServer {
478
533
  if (!userId) return this.json(res, 400, { error: "missing_user_id" });
479
534
  if (userId === auth.userId) return this.json(res, 400, { error: "cannot_remove_self" });
480
535
  if (userId === this.authState.bootstrapAdminUserId) return this.json(res, 403, { error: "cannot_remove_owner", message: "The hub owner cannot be removed" });
536
+ try {
537
+ this.opts.store.insertHubNotification({
538
+ id: randomUUID(), userId, type: "membership_removed",
539
+ resource: "user", title: `You have been removed from team "${this.teamName}" by the admin.`,
540
+ });
541
+ } catch { /* best-effort */ }
481
542
  const cleanResources = body?.cleanResources === true;
482
543
  const deleted = this.opts.store.deleteHubUser(userId, cleanResources);
483
544
  if (!deleted) return this.json(res, 404, { error: "not_found" });
545
+ this.knownOnlineUsers.delete(userId);
484
546
  this.opts.log.info(`Hub: admin "${auth.userId}" removed user "${userId}" (cleanResources=${cleanResources})`);
485
547
  return this.json(res, 200, { ok: true });
486
548
  }
@@ -49,8 +49,8 @@ function normalizeEndpointForProvider(
49
49
  function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
50
50
  try {
51
51
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
52
- const ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
53
- const cfgPath = path.join(ocHome, "openclaw.json");
52
+ const cfgPath = process.env.OPENCLAW_CONFIG_PATH
53
+ || path.join(process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw"), "openclaw.json");
54
54
  if (!fs.existsSync(cfgPath)) return undefined;
55
55
 
56
56
  const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
@@ -37,7 +37,8 @@ function defaultEndpointForProvider(provider: SummaryProvider, baseUrl: string):
37
37
  export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
38
38
  try {
39
39
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
40
- const cfgPath = path.join(home, ".openclaw", "openclaw.json");
40
+ const cfgPath = process.env.OPENCLAW_CONFIG_PATH
41
+ || path.join(process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw"), "openclaw.json");
41
42
  if (!fs.existsSync(cfgPath)) return undefined;
42
43
 
43
44
  const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));