@memtensor/memos-local-openclaw-plugin 1.0.4-beta.0 → 1.0.4-beta.10

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 (98) hide show
  1. package/.env.example +7 -0
  2. package/README.md +24 -24
  3. package/dist/capture/index.d.ts +1 -1
  4. package/dist/capture/index.d.ts.map +1 -1
  5. package/dist/capture/index.js +34 -2
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/client/connector.d.ts +5 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +173 -14
  10. package/dist/client/connector.js.map +1 -1
  11. package/dist/client/hub.d.ts.map +1 -1
  12. package/dist/client/hub.js +22 -0
  13. package/dist/client/hub.js.map +1 -1
  14. package/dist/client/skill-sync.d.ts +7 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -1
  16. package/dist/client/skill-sync.js +10 -0
  17. package/dist/client/skill-sync.js.map +1 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +9 -11
  20. package/dist/config.js.map +1 -1
  21. package/dist/hub/server.d.ts +7 -0
  22. package/dist/hub/server.d.ts.map +1 -1
  23. package/dist/hub/server.js +301 -106
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +3 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +18 -1
  28. package/dist/hub/user-manager.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +7 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/ingest/providers/index.d.ts.map +1 -1
  33. package/dist/ingest/providers/index.js +37 -6
  34. package/dist/ingest/providers/index.js.map +1 -1
  35. package/dist/recall/engine.d.ts.map +1 -1
  36. package/dist/recall/engine.js +91 -1
  37. package/dist/recall/engine.js.map +1 -1
  38. package/dist/shared/llm-call.d.ts +1 -0
  39. package/dist/shared/llm-call.d.ts.map +1 -1
  40. package/dist/shared/llm-call.js +82 -8
  41. package/dist/shared/llm-call.js.map +1 -1
  42. package/dist/sharing/types.d.ts +1 -1
  43. package/dist/sharing/types.d.ts.map +1 -1
  44. package/dist/skill/evolver.d.ts +2 -0
  45. package/dist/skill/evolver.d.ts.map +1 -1
  46. package/dist/skill/evolver.js +3 -0
  47. package/dist/skill/evolver.js.map +1 -1
  48. package/dist/storage/ensure-binding.d.ts +12 -0
  49. package/dist/storage/ensure-binding.d.ts.map +1 -0
  50. package/dist/storage/ensure-binding.js +53 -0
  51. package/dist/storage/ensure-binding.js.map +1 -0
  52. package/dist/storage/sqlite.d.ts +74 -20
  53. package/dist/storage/sqlite.d.ts.map +1 -1
  54. package/dist/storage/sqlite.js +301 -207
  55. package/dist/storage/sqlite.js.map +1 -1
  56. package/dist/telemetry.d.ts +12 -5
  57. package/dist/telemetry.d.ts.map +1 -1
  58. package/dist/telemetry.js +156 -40
  59. package/dist/telemetry.js.map +1 -1
  60. package/dist/tools/memory-search.d.ts +3 -1
  61. package/dist/tools/memory-search.d.ts.map +1 -1
  62. package/dist/tools/memory-search.js +3 -1
  63. package/dist/tools/memory-search.js.map +1 -1
  64. package/dist/types.d.ts +1 -2
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js.map +1 -1
  67. package/dist/viewer/html.d.ts.map +1 -1
  68. package/dist/viewer/html.js +2991 -1041
  69. package/dist/viewer/html.js.map +1 -1
  70. package/dist/viewer/server.d.ts +32 -8
  71. package/dist/viewer/server.d.ts.map +1 -1
  72. package/dist/viewer/server.js +1122 -261
  73. package/dist/viewer/server.js.map +1 -1
  74. package/index.ts +384 -43
  75. package/openclaw.plugin.json +1 -1
  76. package/package.json +3 -2
  77. package/scripts/postinstall.cjs +1 -1
  78. package/skill/memos-memory-guide/SKILL.md +64 -26
  79. package/src/capture/index.ts +37 -1
  80. package/src/client/connector.ts +173 -16
  81. package/src/client/hub.ts +18 -0
  82. package/src/client/skill-sync.ts +14 -0
  83. package/src/config.ts +9 -11
  84. package/src/hub/server.ts +285 -98
  85. package/src/hub/user-manager.ts +20 -3
  86. package/src/index.ts +10 -2
  87. package/src/ingest/providers/index.ts +41 -7
  88. package/src/recall/engine.ts +84 -1
  89. package/src/shared/llm-call.ts +97 -9
  90. package/src/sharing/types.ts +1 -1
  91. package/src/skill/evolver.ts +5 -0
  92. package/src/storage/ensure-binding.ts +52 -0
  93. package/src/storage/sqlite.ts +310 -233
  94. package/src/telemetry.ts +172 -41
  95. package/src/tools/memory-search.ts +2 -1
  96. package/src/types.ts +1 -2
  97. package/src/viewer/html.ts +2991 -1041
  98. package/src/viewer/server.ts +984 -190
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.4-beta.0",
3
+ "version": "1.0.4-beta.10",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -10,8 +10,10 @@
10
10
  "src",
11
11
  "dist",
12
12
  "skill",
13
+ "prebuilds",
13
14
  "scripts/postinstall.cjs",
14
15
  "openclaw.plugin.json",
16
+ "telemetry.credentials.json",
15
17
  "README.md",
16
18
  ".env.example"
17
19
  ],
@@ -50,7 +52,6 @@
50
52
  "@huggingface/transformers": "^3.8.0",
51
53
  "@sinclair/typebox": "^0.34.48",
52
54
  "better-sqlite3": "^12.6.2",
53
- "posthog-node": "^5.28.0",
54
55
  "puppeteer": "^24.38.0",
55
56
  "semver": "^7.7.4",
56
57
  "uuid": "^10.0.0"
@@ -112,7 +112,7 @@ try {
112
112
  function ensureDependencies() {
113
113
  phase(0, "检测核心依赖 / Check core dependencies");
114
114
 
115
- const coreDeps = ["@sinclair/typebox", "uuid", "posthog-node", "@huggingface/transformers"];
115
+ const coreDeps = ["@sinclair/typebox", "uuid", "@huggingface/transformers"];
116
116
  const missing = [];
117
117
  for (const dep of coreDeps) {
118
118
  try {
@@ -1,17 +1,22 @@
1
1
  ---
2
2
  name: memos-memory-guide
3
- description: "Use the MemOS Local memory system to search and use the user's past conversations. Use this skill whenever the user refers to past chats, their own preferences or history, or when you need to answer from prior context. When auto-recall returns nothing (long or unclear user query), generate your own short search query and call memory_search. Available tools: memory_search, memory_get, memory_write_public, task_summary, skill_get, skill_search, skill_install, skill_publish, skill_unpublish, memory_timeline, memory_viewer."
3
+ description: "Use the MemOS Local memory system to search and use the user's past conversations. Use this skill whenever the user refers to past chats, their own preferences or history, or when you need to answer from prior context. When auto-recall returns nothing (long or unclear user query), generate your own short search query and call memory_search. Available tools: memory_search, memory_get, memory_write_public, memory_share, memory_unshare, task_summary, skill_get, skill_search, skill_install, skill_publish, skill_unpublish, network_memory_detail, network_skill_pull, network_team_info, memory_timeline, memory_viewer."
4
4
  ---
5
5
 
6
6
  # MemOS Local Memory — Agent Guide
7
7
 
8
- This skill describes how to use the MemOS memory tools so you can reliably search and use the user's long-term conversation history, query Hub-shared team data, share tasks, and discover or pull reusable skills.
8
+ This skill describes how to use the MemOS memory tools so you can reliably search and use the user's long-term conversation history, query team-shared data, share tasks, and discover or pull reusable skills.
9
+
10
+ Two sharing planes exist and must not be confused:
11
+
12
+ - **Local agent sharing:** visible to agents in the same OpenClaw workspace only.
13
+ - **Team sharing:** visible to teammates through the configured team server.
9
14
 
10
15
  ## How memory is provided each turn
11
16
 
12
17
  - **Automatic recall (hook):** At the start of each turn, the system runs a memory search using the user's current message and injects relevant past memories into your context. You do not need to call any tool for that.
13
18
  - **When that is not enough:** If the user's message is very long, vague, or the automatic search returns **no memories**, you should **generate your own short, focused query** and call `memory_search` yourself.
14
- - **Memory isolation:** Each agent can only see its own local private memories and local `public` memories. Hub-shared data only appears when you search with `scope="group"` or `scope="all"`.
19
+ - **Memory isolation:** Each agent can only see its own local private memories and local `public` memories. Team-shared data only appears when you search with `scope="group"` or `scope="all"`.
15
20
 
16
21
  ## Tools — what they do and when to call
17
22
 
@@ -24,9 +29,10 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
24
29
  - You need to search with a different angle (e.g. filter by `role='user'`).
25
30
  - **Parameters:**
26
31
  - `query` (string, **required**) — Natural language search query.
27
- - `maxResults` (number, optional) — Max results, default 20, max 20.
28
- - `minScore` (number, optional) — Minimum score 0–1, default 0.45, floor 0.35.
29
- - `role` (string, optional) — Filter by role: `'user'`, `'assistant'`, or `'tool'`. Use `'user'` to find what the user said.
32
+ - `scope` (string, optional) — `'local'` (default) for current agent + local shared memories, or `'group'` / `'all'` to include team-shared memories.
33
+ - `maxResults` (number, optional) — Increase when the first search is too narrow.
34
+ - `minScore` (number, optional) — Lower slightly if recall is too strict.
35
+ - `role` (string, optional) — Filter local results by `'user'`, `'assistant'`, `'tool'`, or `'system'`.
30
36
 
31
37
  ### memory_get
32
38
 
@@ -38,12 +44,32 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
38
44
 
39
45
  ### memory_write_public
40
46
 
41
- - **What it does:** Write a piece of information to public memory. Public memories are visible to all agents during `memory_search`. Use for shared knowledge, team decisions, or cross-agent coordination information.
42
- - **When to call:** In multi-agent or collaborative scenarios, when you have persistent information useful to everyone (e.g. shared decisions, conventions, configurations, workflows). Do not write session-only or purely private content.
47
+ - **What it does:** Create a brand new local shared memory. These memories are visible to all agents in the same OpenClaw workspace during `memory_search`. This does **not** publish anything to the team server.
48
+ - **When to call:** In multi-agent or collaborative scenarios, when you want to create a new persistent shared note from scratch (e.g. shared decisions, conventions, configurations, workflows). Do not use it if you already have a specific memory chunk to expose.
43
49
  - **Parameters:**
44
- - `content` (string, **required**) — The content to write to public memory.
50
+ - `content` (string, **required**) — The content to write to local shared memory.
45
51
  - `summary` (string, optional) — Short summary of the content.
46
52
 
53
+ ### memory_share
54
+
55
+ - **What it does:** Share an existing memory either with local OpenClaw agents, to the team, or to both.
56
+ - **When to call:** You already have a useful memory chunk and want to expose it beyond the current agent.
57
+ - **Do not use when:** You are creating a new shared note from scratch. In that case use `memory_write_public`.
58
+ - **Parameters:**
59
+ - `chunkId` (string, **required**) — Existing memory chunk ID.
60
+ - `target` (string, optional) — `'agents'` (default), `'hub'`, or `'both'`.
61
+ - `visibility` (string, optional) — Team visibility when target includes team: `'public'` (default) or `'group'`.
62
+ - `groupId` (string, optional) — Optional team group ID when `visibility='group'`.
63
+
64
+ ### memory_unshare
65
+
66
+ - **What it does:** Remove an existing memory from local agent sharing, team sharing, or both.
67
+ - **When to call:** A memory should no longer be visible outside the current agent or should be removed from the team.
68
+ - **Parameters:**
69
+ - `chunkId` (string, **required**) — Existing memory chunk ID.
70
+ - `target` (string, optional) — `'agents'`, `'hub'`, or `'all'` (default).
71
+ - `privateOwner` (string, optional) — Rare fallback only for older public memories that have no recorded original owner.
72
+
47
73
  ### task_summary
48
74
 
49
75
  - **What it does:** Get the detailed summary of a complete task: title, status, narrative summary, and related skills. Use when `memory_search` returns a hit with a `task_id` and you need the full story. Preserves critical information: URLs, file paths, commands, error codes, step-by-step instructions.
@@ -62,11 +88,11 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
62
88
 
63
89
  ### skill_search
64
90
 
65
- - **What it does:** Search available skills by natural language. Searches your own skills, public skills, or both controlled by the `scope` parameter.
91
+ - **What it does:** Search available skills by natural language. Searches your own skills, local shared skills, or both. It can also include team skills.
66
92
  - **When to call:** The current task requires a capability or guide you don't have. Use `skill_search` to find one first; after finding it, use `skill_get` to read it, then `skill_install` to load it for future turns.
67
93
  - **Parameters:**
68
94
  - `query` (string, **required**) — Natural language description of the needed skill.
69
- - `scope` (string, optional) — Search scope: `'mix'` (default, self + public), `'self'` (own only), `'public'` (public only).
95
+ - `scope` (string, optional) — `'mix'` (default, self + local shared), `'self'`, `'public'` (local shared only), or `'group'` / `'all'` to include team results.
70
96
 
71
97
  ### skill_install
72
98
 
@@ -77,40 +103,46 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
77
103
 
78
104
  ### skill_publish
79
105
 
80
- - **What it does:** Make a skill public so other agents can discover and install it via `skill_search`.
81
- - **When to call:** You have a useful skill that other agents could benefit from, and you want to share it.
106
+ - **What it does:** Share a skill with local agents, or publish it to the team.
107
+ - **When to call:** You have a useful skill that other agents or your team could benefit from.
82
108
  - **Parameters:**
83
109
  - `skillId` (string, **required**) — The skill ID to publish.
110
+ - `target` (string, optional) — `'agents'` (default) or `'hub'`.
111
+ - `visibility` (string, optional) — When `target='hub'`, use `'public'` (default) or `'group'`.
112
+ - `groupId` (string, optional) — Optional team group ID when `target='hub'` and `visibility='group'`.
113
+ - `scope` (string, optional) — Backward-compatible alias for old calls. Prefer `target` + `visibility` in new calls.
84
114
 
85
115
  ### skill_unpublish
86
116
 
87
- - **What it does:** Make a skill private again. Other agents will no longer be able to discover it.
117
+ - **What it does:** Stop local agent sharing, remove a team-published copy, or do both.
88
118
  - **When to call:** You want to stop sharing a previously published skill.
89
119
  - **Parameters:**
90
120
  - `skillId` (string, **required**) — The skill ID to unpublish.
121
+ - `target` (string, optional) — `'agents'` (default), `'hub'`, or `'all'`.
91
122
 
92
123
  ### network_memory_detail
93
124
 
94
- - **What it does:** Fetches the full content behind a Hub search hit.
95
- - **When to call:** A `memory_search` result came from the Hub and you need the full shared memory content.
125
+ - **What it does:** Fetches the full content behind a team search hit.
126
+ - **When to call:** A `memory_search` result came from the team and you need the full shared memory content.
96
127
  - **Parameters:** `remoteHitId`.
97
128
 
98
129
  ### task_share / task_unshare
99
130
 
100
- - **What they do:** Share a local task to the Hub, or remove it later.
131
+ - **What they do:** Share a local task to the team, or remove it later.
101
132
  - **When to call:** A task is valuable to your group or to the whole team and should be discoverable via shared search.
102
133
  - **Parameters:** `taskId`, plus sharing visibility/scope when required.
103
134
 
104
135
  ### network_skill_pull
105
136
 
106
- - **What it does:** Pulls a Hub-shared skill bundle down into local storage.
107
- - **When to call:** `skill_search` found a useful Hub skill and you want to use it locally or offline.
137
+ - **What it does:** Pulls a team-shared skill bundle down into local storage.
138
+ - **When to call:** `skill_search` found a useful team skill and you want to use it locally or offline.
108
139
  - **Parameters:** `skillId`.
109
140
 
110
141
  ### network_team_info
111
142
 
112
- - **What it does:** Returns current Hub connection information, user, role, and groups.
143
+ - **What it does:** Returns current team server connection information, user, role, and groups.
113
144
  - **When to call:** You need to confirm whether team sharing is configured or which groups the current client belongs to.
145
+ - **Call this first before:** `memory_share(... target='hub'|'both')`, `memory_unshare(... target='hub'|'all')`, `task_share`, `task_unshare`, `skill_publish(... target='hub')`, `skill_unpublish(... target='hub'|'all')`, or `network_skill_pull`.
114
146
  - **Parameters:** none.
115
147
 
116
148
  ### memory_timeline
@@ -147,13 +179,19 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
147
179
  6. **You need a capability/guide that you don't have**
148
180
  → Call `skill_search(query="...", scope="mix")` to discover available skills.
149
181
 
150
- 7. **You have shared knowledge useful to all agents**
151
- → Call `memory_write_public(content="...")` to persist it in public memory.
182
+ 7. **You have new shared knowledge useful to all local agents**
183
+ → Call `memory_write_public(content="...")`.
184
+
185
+ 8. **You already have an existing memory chunk and want to expose or hide it**
186
+ → Call `memory_share(chunkId="...", target="agents|hub|both")` or `memory_unshare(chunkId="...", target="agents|hub|all")`.
187
+
188
+ 9. **You are about to do anything team-sharing-related**
189
+ → Call `network_team_info()` first if team server availability is uncertain.
152
190
 
153
- 8. **You want to share/stop sharing a skill with other agents**
154
- Call `skill_publish(skillId="...")` or `skill_unpublish(skillId="...")`.
191
+ 10. **You want to share/stop sharing a skill with local agents or team**
192
+ Prefer `skill_publish(skillId="...", target="agents|hub", visibility=...)` and `skill_unpublish(skillId="...", target="agents|hub|all")`.
155
193
 
156
- 9. **User asks where to see or manage their memories**
194
+ 11. **User asks where to see or manage their memories**
157
195
  → Call `memory_viewer()` and share the URL.
158
196
 
159
197
  ## Writing good search queries
@@ -168,6 +206,6 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
168
206
  Each memory is tagged with an `owner` (e.g. `agent:main`, `agent:sales-bot`). This is handled **automatically** — you do not need to pass any owner parameter.
169
207
 
170
208
  - **Your memories:** All tools (`memory_search`, `memory_get`, `memory_timeline`) automatically scope queries to your agent's own memories.
171
- - **Public memories:** Memories marked as `public` are visible to all agents. Use `memory_write_public` to write shared knowledge.
209
+ - **Local shared memories:** Memories marked as local shared are visible to all agents in the same OpenClaw workspace. Use `memory_write_public` to create them, or `memory_share(target='agents')` to expose an existing chunk.
172
210
  - **Cross-agent isolation:** You cannot see memories owned by other agents (unless they are public).
173
211
  - **How it works:** The system identifies your agent ID from the OpenClaw runtime context and applies owner filtering automatically on every search, recall, and retrieval.
@@ -33,6 +33,9 @@ const SENTINEL_FAST_RE = new RegExp(
33
33
  const ENVELOPE_PREFIX_RE =
34
34
  /^\s*\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+[A-Z]{3}[+-]\d{1,2}\]\s*/;
35
35
 
36
+ const ENVELOPE_EXTRACT_RE =
37
+ /^\s*\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}(?::\d{2})?)\s+([A-Z]{3}[+-]\d{1,2})\]/;
38
+
36
39
  /**
37
40
  * Extract writable messages from a conversation turn.
38
41
  *
@@ -47,9 +50,11 @@ export function captureMessages(
47
50
  evidenceTag: string,
48
51
  log: Logger,
49
52
  owner?: string,
53
+ userSearchTime?: number,
50
54
  ): ConversationMessage[] {
51
55
  const now = Date.now();
52
56
  const result: ConversationMessage[] = [];
57
+ let lastTimestamp = 0;
53
58
 
54
59
  for (const msg of messages) {
55
60
  const role = msg.role as Role;
@@ -70,14 +75,24 @@ export function captureMessages(
70
75
  if (role === "user") {
71
76
  content = stripInboundMetadata(content);
72
77
  } else {
78
+ content = stripThinkingTags(content);
73
79
  content = stripEvidenceWrappers(content, evidenceTag);
74
80
  }
75
81
  if (!content.trim()) continue;
76
82
 
83
+ let ts: number;
84
+ if (role === "user" && userSearchTime && userSearchTime > 0) {
85
+ ts = userSearchTime;
86
+ } else {
87
+ ts = now;
88
+ }
89
+ if (ts <= lastTimestamp) ts = lastTimestamp + 1;
90
+ lastTimestamp = ts;
91
+
77
92
  result.push({
78
93
  role,
79
94
  content,
80
- timestamp: now,
95
+ timestamp: ts,
81
96
  turnId,
82
97
  sessionKey,
83
98
  toolName: role === "tool" ? msg.toolName : undefined,
@@ -149,6 +164,27 @@ export function stripInboundMetadata(text: string): string {
149
164
  return stripEnvelopePrefix(result.join("\n")).trim();
150
165
  }
151
166
 
167
+ /** Strip <think…>…</think> blocks emitted by DeepSeek-style reasoning models. */
168
+ const THINKING_TAG_RE = /<think[\s>][\s\S]*?<\/think>\s*/gi;
169
+
170
+ function stripThinkingTags(text: string): string {
171
+ return text.replace(THINKING_TAG_RE, "");
172
+ }
173
+
174
+ function extractEnvelopeTimestamp(text: string): number | null {
175
+ const m = ENVELOPE_EXTRACT_RE.exec(text);
176
+ if (!m) return null;
177
+ const [, date, time, tz] = m;
178
+ const timeStr = time.includes(":") && time.split(":").length === 3 ? time : time + ":00";
179
+ const offsetMatch = tz.match(/([+-])(\d{1,2})$/);
180
+ const offsetStr = offsetMatch
181
+ ? `${offsetMatch[1]}${offsetMatch[2].padStart(2, "0")}:00`
182
+ : "+00:00";
183
+ const iso = `${date}T${timeStr}${offsetStr}`;
184
+ const ts = new Date(iso).getTime();
185
+ return Number.isNaN(ts) ? null : ts;
186
+ }
187
+
152
188
  function stripEnvelopePrefix(text: string): string {
153
189
  return text.replace(ENVELOPE_PREFIX_RE, "");
154
190
  }
@@ -1,5 +1,5 @@
1
1
  import type { Logger, MemosLocalConfig } from "../types";
2
- import type { GroupInfo, UserRole, UserStatus } from "../sharing/types";
2
+ import type { UserRole, UserStatus } from "../sharing/types";
3
3
  import type { SqliteStore } from "../storage/sqlite";
4
4
  import { hubRequestJson, normalizeHubUrl } from "./hub";
5
5
 
@@ -20,7 +20,6 @@ export interface HubStatusInfo {
20
20
  username: string;
21
21
  role: UserRole;
22
22
  status: UserStatus | string;
23
- groups: GroupInfo[];
24
23
  };
25
24
  }
26
25
 
@@ -35,6 +34,41 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
35
34
 
36
35
  if (!userToken && config.sharing?.client?.teamToken) {
37
36
  if (!log) throw new Error("hub client connection is not configured (no userToken, has teamToken but no logger for auto-join)");
37
+
38
+ // If DB has a pending connection (userId exists, no token), check registration-status first
39
+ const persisted = store.getClientHubConnection();
40
+ if (persisted?.userId && !persisted.userToken && hubAddress) {
41
+ const hubUrl = normalizeHubUrl(hubAddress);
42
+ const teamToken = config.sharing.client!.teamToken!;
43
+ try {
44
+ const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/registration-status", {
45
+ method: "POST",
46
+ body: JSON.stringify({ teamToken, userId: persisted.userId }),
47
+ }) as any;
48
+ if (result.status === "active" && result.userToken) {
49
+ log.info(`Pending user approved! Connecting with token. userId=${persisted.userId}`);
50
+ store.setClientHubConnection({
51
+ hubUrl,
52
+ userId: persisted.userId,
53
+ username: persisted.username || "",
54
+ userToken: result.userToken,
55
+ role: "member",
56
+ connectedAt: Date.now(),
57
+ });
58
+ return store.getClientHubConnection()!;
59
+ }
60
+ if (result.status === "pending") {
61
+ throw new PendingApprovalError(persisted.userId);
62
+ }
63
+ if (result.status === "rejected") {
64
+ throw new Error("Join request was rejected by the Hub admin.");
65
+ }
66
+ } catch (err) {
67
+ if (err instanceof PendingApprovalError) throw err;
68
+ log.warn(`registration-status check failed, falling back to autoJoinHub: ${err}`);
69
+ }
70
+ }
71
+
38
72
  return autoJoinHub(store, config, log);
39
73
  }
40
74
 
@@ -57,32 +91,117 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
57
91
 
58
92
  export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig): Promise<HubStatusInfo> {
59
93
  const conn = store.getClientHubConnection();
60
- const hubAddress = conn?.hubUrl || config.sharing?.client?.hubAddress || "";
94
+ const configHubAddress = config.sharing?.client?.hubAddress || "";
95
+ const hubAddress = conn?.hubUrl || (configHubAddress ? normalizeHubUrl(configHubAddress) : "");
61
96
  const userToken = conn?.userToken || config.sharing?.client?.userToken || "";
97
+
98
+ // If DB has a connection to a different Hub than config, the DB data is stale
99
+ if (conn && configHubAddress && conn.hubUrl && normalizeHubUrl(configHubAddress) !== conn.hubUrl) {
100
+ store.clearClientHubConnection();
101
+ return { connected: false, user: null };
102
+ }
103
+
104
+ if (conn && conn.userId && (!userToken || userToken === "")) {
105
+ const teamToken = config.sharing?.client?.teamToken ?? "";
106
+ if (hubAddress && teamToken) {
107
+ try {
108
+ const result = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/registration-status", {
109
+ method: "POST",
110
+ body: JSON.stringify({ teamToken, userId: conn.userId }),
111
+ }) as any;
112
+ if (result.status === "pending") {
113
+ return {
114
+ connected: false,
115
+ hubUrl: normalizeHubUrl(hubAddress),
116
+ user: {
117
+ id: conn.userId,
118
+ username: conn.username || "",
119
+ role: "member",
120
+ status: "pending",
121
+ },
122
+ };
123
+ }
124
+ if (result.status === "active" && result.userToken) {
125
+ store.setClientHubConnection({
126
+ hubUrl: normalizeHubUrl(hubAddress),
127
+ userId: conn.userId,
128
+ username: conn.username || "",
129
+ userToken: result.userToken,
130
+ role: "member",
131
+ connectedAt: Date.now(),
132
+ });
133
+ const me = await hubRequestJson(normalizeHubUrl(hubAddress), result.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
134
+ return {
135
+ connected: true,
136
+ hubUrl: normalizeHubUrl(hubAddress),
137
+ user: {
138
+ id: String(me.id),
139
+ username: String(me.username ?? ""),
140
+ role: String(me.role ?? "member") as UserRole,
141
+ status: String(me.status ?? "active"),
142
+ },
143
+ };
144
+ }
145
+ if (result.status === "rejected") {
146
+ return {
147
+ connected: false,
148
+ hubUrl: normalizeHubUrl(hubAddress),
149
+ user: {
150
+ id: conn.userId,
151
+ username: conn.username || "",
152
+ role: "member",
153
+ status: "rejected",
154
+ },
155
+ };
156
+ }
157
+ } catch { /* fall through */ }
158
+ }
159
+ return { connected: false, user: null };
160
+ }
161
+
62
162
  if (!hubAddress || !userToken) {
63
163
  return { connected: false, user: null };
64
164
  }
65
165
 
66
166
  try {
67
167
  const me = await hubRequestJson(normalizeHubUrl(hubAddress), userToken, "/api/v1/hub/me", { method: "GET" }) as any;
168
+ const latestUsername = String(me.username ?? "");
169
+ const latestRole = String(me.role ?? "member") as UserRole;
170
+ if (conn && (conn.username !== latestUsername || conn.role !== latestRole)) {
171
+ store.setClientHubConnection({
172
+ hubUrl: conn.hubUrl,
173
+ userId: conn.userId,
174
+ username: latestUsername,
175
+ userToken: conn.userToken,
176
+ role: latestRole,
177
+ connectedAt: conn.connectedAt,
178
+ });
179
+ }
68
180
  return {
69
181
  connected: true,
70
182
  hubUrl: normalizeHubUrl(hubAddress),
71
183
  user: {
72
184
  id: String(me.id),
73
- username: String(me.username ?? ""),
74
- role: String(me.role ?? "member") as UserRole,
185
+ username: latestUsername,
186
+ role: latestRole,
75
187
  status: String(me.status ?? "active"),
76
- groups: Array.isArray(me.groups)
77
- ? me.groups.map((group: any) => ({
78
- id: String(group.id),
79
- name: String(group.name),
80
- description: typeof group.description === "string" ? group.description : undefined,
81
- }))
82
- : [],
83
188
  },
84
189
  };
85
- } catch {
190
+ } catch (err: any) {
191
+ const is401 = typeof err?.message === "string" && err.message.includes("(401)");
192
+ if (is401 && conn) {
193
+ store.clearClientHubConnection();
194
+ return {
195
+ connected: false,
196
+ hubUrl: normalizeHubUrl(hubAddress),
197
+ user: {
198
+ id: conn.userId,
199
+ username: conn.username || "",
200
+ role: "member",
201
+ status: "removed",
202
+ },
203
+ };
204
+ }
86
205
  return { connected: false, user: null };
87
206
  }
88
207
  }
@@ -98,15 +217,44 @@ export async function autoJoinHub(
98
217
  throw new Error("hubAddress and teamToken are required for auto-join");
99
218
  }
100
219
  const hubUrl = normalizeHubUrl(hubAddress);
101
- const hostname = typeof globalThis.process !== "undefined" ? (await import("os")).hostname() : "unknown";
102
- const username = typeof globalThis.process !== "undefined" ? (await import("os")).userInfo().username : "user";
220
+ const osModule = typeof globalThis.process !== "undefined" ? await import("os") : null;
221
+ const hostname = osModule ? osModule.hostname() : "unknown";
222
+ const nickname = config.sharing?.client?.nickname;
223
+ const username = nickname || (osModule ? osModule.userInfo().username : "user");
224
+ let clientIp = "";
225
+ if (osModule) {
226
+ const nets = osModule.networkInterfaces();
227
+ for (const name of Object.keys(nets)) {
228
+ for (const net of nets[name] ?? []) {
229
+ if (net.family === "IPv4" && !net.internal) { clientIp = net.address; break; }
230
+ }
231
+ if (clientIp) break;
232
+ }
233
+ }
103
234
 
104
235
  log.info(`Joining Hub at ${hubUrl} as "${username}"...`);
105
236
  const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
106
237
  method: "POST",
107
- body: JSON.stringify({ teamToken, username, deviceName: hostname }),
238
+ body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp }),
108
239
  }) as any;
109
240
 
241
+ if (result.status === "pending") {
242
+ log.info(`Join request submitted, awaiting admin approval. userId=${result.userId}`);
243
+ store.setClientHubConnection({
244
+ hubUrl,
245
+ userId: String(result.userId),
246
+ username,
247
+ userToken: "",
248
+ role: "member",
249
+ connectedAt: Date.now(),
250
+ });
251
+ throw new PendingApprovalError(result.userId);
252
+ }
253
+
254
+ if (result.status === "rejected") {
255
+ throw new Error(`Join request was rejected by the Hub admin.`);
256
+ }
257
+
110
258
  if (!result.userToken) {
111
259
  throw new Error(`Hub join failed: ${JSON.stringify(result)}`);
112
260
  }
@@ -122,3 +270,12 @@ export async function autoJoinHub(
122
270
  });
123
271
  return store.getClientHubConnection()!;
124
272
  }
273
+
274
+ export class PendingApprovalError extends Error {
275
+ public readonly userId: string;
276
+ constructor(userId: string) {
277
+ super("Awaiting admin approval");
278
+ this.name = "PendingApprovalError";
279
+ this.userId = userId;
280
+ }
281
+ }
package/src/client/hub.ts CHANGED
@@ -157,16 +157,34 @@ export async function hubUpdateUsername(
157
157
  return result;
158
158
  }
159
159
 
160
+ let _cachedClientIp: string | null = null;
161
+ function getClientIp(): string {
162
+ if (_cachedClientIp !== null) return _cachedClientIp;
163
+ try {
164
+ const os = require("os");
165
+ const nets = os.networkInterfaces();
166
+ for (const name of Object.keys(nets)) {
167
+ for (const net of nets[name] ?? []) {
168
+ if (net.family === "IPv4" && !net.internal) { _cachedClientIp = net.address; return _cachedClientIp!; }
169
+ }
170
+ }
171
+ } catch { /* browser or no os module */ }
172
+ _cachedClientIp = "";
173
+ return "";
174
+ }
175
+
160
176
  export async function hubRequestJson(
161
177
  hubUrl: string,
162
178
  userToken: string,
163
179
  route: string,
164
180
  init: RequestInit = {},
165
181
  ): Promise<unknown> {
182
+ const clientIp = getClientIp();
166
183
  const res = await fetch(`${normalizeHubUrl(hubUrl)}${route}`, {
167
184
  ...init,
168
185
  headers: {
169
186
  authorization: `Bearer ${userToken}`,
187
+ ...(clientIp ? { "x-client-ip": clientIp } : {}),
170
188
  ...(init.body ? { "content-type": "application/json" } : {}),
171
189
  ...(init.headers ?? {}),
172
190
  },
@@ -89,6 +89,20 @@ export async function publishSkillBundleToHub(
89
89
  }) as Promise<{ skillId: string; visibility: "public" | "group" }>;
90
90
  }
91
91
 
92
+ export async function unpublishSkillBundleFromHub(
93
+ store: SqliteStore,
94
+ ctx: PluginContext,
95
+ input: { skillId: string; hubAddress?: string; userToken?: string },
96
+ ): Promise<{ ok: boolean }> {
97
+ const client = await resolveHubClient(store, ctx, { hubAddress: input.hubAddress, userToken: input.userToken });
98
+ return hubRequestJson(client.hubUrl, client.userToken, "/api/v1/hub/skills/unpublish", {
99
+ method: "POST",
100
+ body: JSON.stringify({
101
+ sourceSkillId: input.skillId,
102
+ }),
103
+ }) as Promise<{ ok: boolean }>;
104
+ }
105
+
92
106
  export async function fetchHubSkillBundle(
93
107
  store: SqliteStore,
94
108
  ctx: PluginContext,
package/src/config.ts CHANGED
@@ -75,8 +75,6 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
75
75
  },
76
76
  telemetry: {
77
77
  enabled: telemetryEnabled,
78
- posthogApiKey: cfg.telemetry?.posthogApiKey ?? process.env.POSTHOG_API_KEY ?? "",
79
- posthogHost: cfg.telemetry?.posthogHost ?? process.env.POSTHOG_HOST ?? "",
80
78
  },
81
79
  summarizer: (() => {
82
80
  const summarizerConfig = resolveProviderFallback<SummarizerConfig>(
@@ -117,22 +115,22 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
117
115
  : undefined;
118
116
  })(),
119
117
  } : undefined,
120
- sharing: {
121
- enabled: cfg.sharing?.enabled ?? false,
122
- role: cfg.sharing?.role ?? "client",
123
- hub: {
118
+ sharing: (() => {
119
+ const role = cfg.sharing?.role ?? "client";
120
+ const enabled = cfg.sharing?.enabled ?? false;
121
+ const hub = role === "hub" ? {
124
122
  port: cfg.sharing?.hub?.port ?? 18800,
125
123
  teamName: cfg.sharing?.hub?.teamName ?? "",
126
124
  teamToken: cfg.sharing?.hub?.teamToken ?? "",
127
- },
128
- client: {
125
+ } : { port: 18800, teamName: "", teamToken: "" };
126
+ const client = role === "client" ? {
129
127
  hubAddress: cfg.sharing?.client?.hubAddress ?? "",
130
128
  userToken: cfg.sharing?.client?.userToken ?? "",
131
129
  teamToken: cfg.sharing?.client?.teamToken ?? "",
132
130
  pendingUserId: cfg.sharing?.client?.pendingUserId ?? "",
133
- },
134
- capabilities: sharingCapabilities,
135
- },
131
+ } : { hubAddress: "", userToken: "", teamToken: "", pendingUserId: "" };
132
+ return { enabled, role, hub, client, capabilities: sharingCapabilities };
133
+ })(),
136
134
  };
137
135
  }
138
136