@memtensor/memos-local-openclaw-plugin 0.1.0 → 0.1.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
@@ -18,15 +18,21 @@ Persistent local conversation memory for [OpenClaw](https://github.com/nicepkg/o
18
18
 
19
19
  ### 1. Install
20
20
 
21
+ **From npm (recommended):**
22
+
21
23
  ```bash
22
- openclaw plugins install memos-local
24
+ openclaw plugins install @memtensor/memos-local-openclaw-plugin
23
25
  ```
24
26
 
25
- Or from source:
27
+ The plugin is installed under `~/.openclaw/extensions/` and registered as `memos-local`. No clone or build required.
28
+
29
+ > **Important:** Installing the plugin does **not** start the Memory Viewer. The viewer HTTP service is started only when the **OpenClaw gateway** is running. After install, you must **configure** `openclaw.json` (step 2) and **start or restart the gateway** (step 3); then the viewer will be available at `http://127.0.0.1:18799`.
30
+
31
+ **From source (development):**
26
32
 
27
33
  ```bash
28
34
  git clone https://github.com/MemTensor/MemOS.git
29
- cd MemOS/apps/memos-local-openclaw-plugin
35
+ cd MemOS/apps/memos-local-openclaw
30
36
  npm install && npm run build
31
37
  openclaw plugins install .
32
38
  ```
@@ -99,14 +105,18 @@ Use `${ENV_VAR}` placeholders in config to avoid hardcoding keys:
99
105
  }
100
106
  ```
101
107
 
102
- ### 3. Restart Gateway
108
+ ### 3. Start or Restart the Gateway
109
+
110
+ The Memory Viewer and all plugin features only run when the OpenClaw gateway is running. After installing and configuring the plugin, start (or restart) the gateway:
103
111
 
104
112
  ```bash
105
- openclaw gateway stop
106
- openclaw gateway install
113
+ openclaw gateway stop # if already running
114
+ openclaw gateway install # ensure LaunchAgent is installed (macOS)
107
115
  openclaw gateway start
108
116
  ```
109
117
 
118
+ Once the gateway is up, the plugin loads and starts the Memory Viewer at `http://127.0.0.1:18799`.
119
+
110
120
  ### 4. Verify Installation
111
121
 
112
122
  ```bash
@@ -155,18 +165,16 @@ Stored chunk=107d7f32 kind=tool_result role=tool len=210 hasVec=true
155
165
 
156
166
  ### 6. Run the Smoke Test (Optional)
157
167
 
158
- For a comprehensive end-to-end test with your actual API:
168
+ If you have the source (e.g. cloned the repo or develop locally):
159
169
 
160
170
  ```bash
161
- # Create a .env file with your keys
171
+ cd MemOS/apps/memos-local-openclaw # or the path where the plugin source is
162
172
  cp .env.example .env
163
173
  # Edit .env with your actual API keys
164
-
165
- # Run the smoke test
166
174
  npx tsx scripts/smoke-test.ts
167
175
  ```
168
176
 
169
- The smoke test writes test conversations, searches for them, verifies timeline and get operations, and checks anti-writeback protection.
177
+ The smoke test writes test conversations, searches for them, verifies timeline and get operations, and checks anti-writeback protection. If you installed only from npm, you can skip this step.
170
178
 
171
179
  ## Agent Tools
172
180
 
@@ -185,6 +193,13 @@ The agent uses these automatically via the SKILL.md prompt guide.
185
193
 
186
194
  Open `http://127.0.0.1:18799` in your browser:
187
195
 
196
+ **Viewer won't open or page not loading?**
197
+
198
+ - The viewer is started by the plugin when the **gateway** starts. It does **not** run at install time.
199
+ - Ensure the gateway is running: `openclaw gateway start` (or restart with `openclaw gateway stop` then `openclaw gateway start`).
200
+ - Ensure the plugin is enabled in `~/.openclaw/openclaw.json`: `plugins.slots.memory` = `"memos-local"` and `plugins.entries.memos-local.enabled` = `true`.
201
+ - Check the gateway log: `tail -30 ~/.openclaw/logs/gateway.log` — you should see `MemOS Memory Viewer` and `→ http://127.0.0.1:18799`. If the viewer fails to bind (e.g. port in use), the log will show a warning.
202
+
188
203
  - First visit: set a password (min 4 chars)
189
204
  - Browse, search, create, edit, delete memories
190
205
  - Filter by role (user/assistant/tool), type, time range (down to seconds)
@@ -193,8 +208,10 @@ Open `http://127.0.0.1:18799` in your browser:
193
208
  **Forgot password?** Click "Forgot password?" on the login page and use the reset token from the gateway log:
194
209
 
195
210
  ```bash
196
- grep "reset token" ~/.openclaw/logs/gateway.log | tail -1
211
+ # 必须用 "password reset token:" 才能匹配到带 token 的那一行(不要用 "reset token")
212
+ grep "password reset token:" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1
197
213
  ```
214
+ 输出可能是纯文本一行,或一段 JSON;从中复制 `password reset token:` 后面的 32 位 hex 即可。
198
215
 
199
216
  ## Advanced Configuration
200
217
 
@@ -236,15 +253,18 @@ Query → FTS5 + Vector dual recall → RRF Fusion → MMR Rerank
236
253
  → Recency Decay → Score Filter → Top-K Results
237
254
  ```
238
255
 
239
- See [full documentation](www/docs/) for detailed architecture and algorithm explanations.
256
+ See the [full documentation](www/docs/) in the repo for detailed architecture and algorithm explanations.
240
257
 
241
258
  ## Data Location
242
259
 
260
+ Whether you install from npm or from source, the plugin stores data under your OpenClaw state directory:
261
+
243
262
  | File | Path |
244
263
  |---|---|
245
264
  | Database | `~/.openclaw/memos-local/memos.db` |
246
265
  | Viewer auth | `~/.openclaw/memos-local/viewer-auth.json` |
247
266
  | Gateway log | `~/.openclaw/logs/gateway.log` |
267
+ | Plugin code (npm install) | `~/.openclaw/extensions/` (managed by OpenClaw) |
248
268
 
249
269
  ## License
250
270
 
@@ -1,2 +1,2 @@
1
- export declare const viewerHTML = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>OpenClaw Memory - Powered by MemOS</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\n:root{\n --bg:#050510;--bg-card:rgba(255,255,255,.04);--bg-card-hover:rgba(255,255,255,.07);\n --border:rgba(255,255,255,.08);--border-glow:rgba(0,187,238,.25);\n --text:#f0f4f8;--text-sec:#8b95a5;--text-muted:#5a6373;\n --pri:#00bbee;--pri-glow:rgba(0,187,238,.15);--pri-dark:#0088aa;\n --pri-grad:linear-gradient(135deg,#00bbee,#00a0cc);\n --accent:#e63946;--accent-glow:rgba(230,57,70,.15);\n --green:#10b981;--green-bg:rgba(16,185,129,.12);\n --amber:#f59e0b;--amber-bg:rgba(245,158,11,.12);\n --violet:#8b5cf6;--rose:#f43f5e;--rose-bg:rgba(244,63,94,.12);\n --shadow-sm:0 1px 2px rgba(0,0,0,.2);--shadow:0 4px 12px rgba(0,0,0,.25);\n --shadow-lg:0 20px 40px rgba(0,0,0,.35);\n --radius:12px;--radius-lg:14px;--radius-xl:18px;\n}\nbody{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}\nbutton{cursor:pointer;font-family:inherit;font-size:inherit}\ninput,textarea,select{font-family:inherit;font-size:inherit}\n\n/* \u2500\u2500\u2500 Auth (Linkify \u914D\u8272: globals.css .dark + \u84DD\u7D2B\u6E10\u53D8) \u2500\u2500\u2500 */\n.auth-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;background:linear-gradient(135deg,rgb(36,0,255) 0%,rgb(0,135,255) 35%,rgb(108,39,157) 70%,rgb(105,30,255) 100%);position:relative;overflow:hidden}\n.auth-card{background:hsl(0 0% 100%);border:none;border-radius:8px;padding:48px 40px;width:100%;max-width:420px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);text-align:center;position:relative;z-index:1}\n.auth-card .logo{width:56px;height:56px;margin:0 auto 20px;display:flex;align-items:center;justify-content:center;font-size:48px;background:none;border-radius:0}\n.auth-card h1{font-size:22px;font-weight:700;margin-bottom:4px;color:hsl(0 0% 3.9%);letter-spacing:-.02em}\n.auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}\n.auth-card input{width:100%;padding:12px 16px;border:1px solid hsl(0 0% 89.8%);border-radius:8px;font-size:14px;transition:all .2s;margin-bottom:10px;outline:none;background:#fff;color:hsl(0 0% 3.9%)}\n.auth-card input::placeholder{color:hsl(0 0% 45.1%)}\n.auth-card input:focus{border-color:rgb(168,85,247);box-shadow:0 0 0 3px rgba(168,85,247,.2)}\n.auth-card .btn-auth{width:100%;padding:12px;border:none;border-radius:8px;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-weight:600;font-size:14px;transition:all .2s}\n.auth-card .btn-auth:hover{background:hsl(0 0% 14%);transform:translateY(-1px);box-shadow:0 8px 25px rgba(0,0,0,.2)}\n.auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}\n.auth-card .btn-text{color:hsl(0 0% 45.1%)}\n.auth-card .btn-text:hover{color:rgb(168,85,247)}\n\n.reset-guide{text-align:left;margin-bottom:20px}\n.reset-step{display:flex;gap:14px;margin-bottom:16px}\n.step-num{width:28px;height:28px;border-radius:50%;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}\n.step-body{flex:1;min-width:0}\n.step-title{font-size:14px;font-weight:600;color:hsl(0 0% 3.9%);margin-bottom:2px}\n.step-desc{font-size:13px;color:hsl(0 0% 45.1%);line-height:1.5}\n.cmd-box{margin-top:8px;background:hsl(0 0% 96.1%);border:1px solid hsl(0 0% 89.8%);border-radius:8px;padding:12px 14px;font-size:12px;font-family:ui-monospace,monospace;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:space-between;gap:8px;word-break:break-all;color:hsl(0 0% 3.9%)}\n.cmd-box:hover{border-color:rgb(168,85,247);background:rgba(168,85,247,.08)}\n.cmd-box code{flex:1}\n.copy-hint{font-size:11px;color:hsl(0 0% 45.1%);white-space:nowrap}\n.cmd-box.copied .copy-hint{color:hsl(142 71% 45%)}\n\n/* \u2500\u2500\u2500 App Layout (dark dashboard, same as www) \u2500\u2500\u2500 */\n.app{display:none;flex-direction:column;min-height:100vh}\n.topbar{background:rgba(5,5,16,.85);border-bottom:1px solid var(--border);padding:0 28px;height:64px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;backdrop-filter:blur(12px)}\n.topbar .brand{display:flex;align-items:center;gap:12px;font-weight:700;font-size:17px;color:var(--text);letter-spacing:-.02em}\n.topbar .brand .icon{width:38px;height:38px;display:flex;align-items:center;justify-content:center;font-size:26px;background:none;border-radius:0}\n.topbar .actions{display:flex;align-items:center;gap:10px}\n\n.main-content{display:flex;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}\n\n/* \u2500\u2500\u2500 Sidebar \u2500\u2500\u2500 */\n.sidebar{width:260px;flex-shrink:0}\n.sidebar .stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:24px}\n.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;transition:all .2s}\n.stat-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.stat-card .stat-value{font-size:22px;font-weight:700;color:var(--text);letter-spacing:-.02em}\n.stat-card .stat-label{font-size:12px;color:var(--text-sec);margin-top:4px;font-weight:500}\n.stat-card.pri .stat-value{color:var(--pri)}\n.stat-card.green .stat-value{color:var(--green)}\n.stat-card.amber .stat-value{color:var(--amber)}\n.stat-card.rose .stat-value{color:var(--rose)}\n\n.sidebar .section-title{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin:24px 0 12px;padding:0 2px}\n.sidebar .session-list{display:flex;flex-direction:column;gap:6px;max-height:280px;overflow-y:auto}\n.session-item{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;font-size:13px;color:var(--text)}\n.session-item:hover{border-color:var(--pri);background:var(--pri-glow)}\n.session-item.active{border-color:var(--pri);background:var(--pri-glow);font-weight:600;color:var(--pri)}\n.session-item .count{color:var(--text-sec);font-size:11px;font-weight:600;background:rgba(0,0,0,.2);padding:3px 8px;border-radius:8px}\n\n.provider-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--green-bg);color:var(--green);border-radius:999px;font-size:11px;font-weight:600;margin-top:10px}\n.provider-badge.offline{background:var(--amber-bg);color:var(--amber)}\n\n/* \u2500\u2500\u2500 Feed \u2500\u2500\u2500 */\n.feed{flex:1;min-width:0}\n.search-bar{display:flex;gap:12px;margin-bottom:16px;position:relative}\n.search-bar input{flex:1;padding:12px 16px 12px 44px;border:1px solid var(--border);border-radius:12px;font-size:14px;outline:none;background:var(--bg-card);color:var(--text);transition:all .2s}\n.search-bar input::placeholder{color:var(--text-muted)}\n.search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.search-bar .search-icon{position:absolute;left:16px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:15px;pointer-events:none}\n.search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}\n\n.filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}\n.filter-chip{padding:6px 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;transition:all .15s}\n.filter-chip:hover{border-color:var(--pri);color:var(--pri)}\n.filter-chip.active{background:var(--pri);color:#000;border-color:var(--pri)}\n\n.memory-list{display:flex;flex-direction:column;gap:16px}\n.memory-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;transition:all .2s}\n.memory-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.memory-card .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}\n.memory-card .meta{display:flex;align-items:center;gap:8px}\n.role-tag{padding:4px 10px;border-radius:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}\n.role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(0,187,238,.2)}\n.role-tag.assistant{background:var(--accent-glow);color:var(--accent);border:1px solid rgba(230,57,70,.2)}\n.role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}\n.kind-tag{padding:4px 10px;border-radius:8px;font-size:11px;color:var(--text-sec);background:rgba(0,0,0,.2);font-weight:500}\n.card-time{font-size:12px;color:var(--text-sec);display:flex;align-items:center;gap:8px}\n.session-tag{font-size:11px;font-family:ui-monospace,monospace;color:var(--text-muted);background:rgba(0,0,0,.2);padding:3px 8px;border-radius:6px;cursor:default}\n.card-summary{font-size:15px;font-weight:600;color:var(--text);margin-bottom:10px;line-height:1.5;letter-spacing:-.01em}\n.card-content{font-size:13px;color:var(--text-sec);line-height:1.65;max-height:0;overflow:hidden;transition:max-height .3s ease}\n.card-content.show{max-height:600px;overflow-y:auto}\n.card-content pre{white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.25);padding:14px;border-radius:10px;font-size:12px;font-family:ui-monospace,monospace;margin-top:10px;border:1px solid var(--border);color:var(--text-sec)}\n.card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}\n.vscore-badge{display:inline-flex;align-items:center;background:linear-gradient(135deg,var(--pri),var(--violet));color:#fff;font-size:10px;font-weight:700;padding:4px 10px;border-radius:8px;margin-left:auto}\n\n/* \u2500\u2500\u2500 Buttons \u2500\u2500\u2500 */\n.btn{padding:8px 16px;border-radius:10px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;font-weight:500;transition:all .15s;display:inline-flex;align-items:center;gap:6px}\n.btn:hover{border-color:var(--pri);color:var(--pri)}\n.btn-primary{background:var(--pri);color:#000;border:none}\n.btn-primary:hover{background:#4dd9ff;transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,187,238,.25)}\n.btn-danger{color:var(--accent);border-color:var(--accent)}\n.btn-danger:hover{background:var(--accent);color:#fff;border-color:var(--accent)}\n.btn-sm{padding:6px 12px;font-size:12px}\n.btn-icon{padding:6px 8px;font-size:14px}\n.btn-text{border:none;background:none;color:var(--text-sec);font-size:13px;padding:4px 8px}\n.btn-text:hover{color:var(--pri)}\n\n/* \u2500\u2500\u2500 Modal \u2500\u2500\u2500 */\n.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:500;align-items:center;justify-content:center;backdrop-filter:blur(8px)}\n.modal-overlay.show{display:flex}\n.modal{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);padding:32px;width:100%;max-width:520px;box-shadow:var(--shadow-lg);max-height:85vh;overflow-y:auto}\n.modal h2{font-size:20px;font-weight:700;margin-bottom:24px;color:var(--text);letter-spacing:-.02em}\n.form-group{margin-bottom:18px}\n.form-group label{display:block;font-size:13px;font-weight:600;color:var(--text-sec);margin-bottom:6px}\n.form-group input,.form-group textarea,.form-group select{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:10px;font-size:14px;outline:none;transition:all .2s;background:var(--bg-card);color:var(--text)}\n.form-group input::placeholder,.form-group textarea::placeholder{color:var(--text-muted)}\n.form-group input:focus,.form-group textarea:focus,.form-group select:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.form-group textarea{min-height:100px;resize:vertical}\n.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:28px}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\n.toast-container{position:fixed;top:80px;right:24px;z-index:1000;display:flex;flex-direction:column;gap:8px}\n.toast{padding:14px 20px;border-radius:10px;font-size:13px;font-weight:500;box-shadow:var(--shadow-lg);animation:slideIn .3s ease;display:flex;align-items:center;gap:10px;max-width:360px;border:1px solid}\n.toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}\n.toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}\n.toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(0,187,238,.3)}\n@keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}\n\n.empty{text-align:center;padding:64px 20px;color:var(--text-sec)}\n.empty .icon{font-size:52px;margin-bottom:16px;opacity:.5}\n.empty p{font-size:15px;font-weight:500}\n\n.spinner{width:40px;height:40px;border:3px solid var(--border);border-top-color:var(--pri);border-radius:50%;animation:spin .8s linear infinite;margin:48px auto}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n::-webkit-scrollbar{width:6px;height:6px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.25)}\n\n.filter-sep{width:1px;height:20px;background:var(--border);margin:0 4px}\n.filter-select{padding:6px 12px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;outline:none;cursor:pointer}\n.filter-select:focus{border-color:var(--pri)}\n.date-filter{display:flex;align-items:center;gap:10px;margin-bottom:18px;font-size:13px;color:var(--text-sec)}\n.date-filter input[type=\"datetime-local\"]{padding:6px 10px;border:1px solid var(--border);border-radius:8px;font-size:12px;outline:none;background:var(--bg-card);color:var(--text)}\n.date-filter input[type=\"datetime-local\"]:focus{border-color:var(--pri)}\n.date-filter label{font-weight:500}\n\n.pagination{display:flex;align-items:center;justify-content:center;gap:6px;padding:28px 0;flex-wrap:wrap}\n.pagination .pg-btn{min-width:38px;height:38px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s}\n.pagination .pg-btn:hover{border-color:var(--pri);color:var(--pri)}\n.pagination .pg-btn.active{background:var(--pri);color:#000;border-color:var(--pri)}\n.pagination .pg-btn.disabled{opacity:.4;pointer-events:none}\n.pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}\n\n@media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}}\n</style>\n</head>\n<body>\n\n<!-- \u2500\u2500\u2500 Auth: Setup Password \u2500\u2500\u2500 -->\n<div id=\"setupScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-card\">\n <div class=\"logo\">\uD83E\uDD9E</div>\n <h1>OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\">Powered by MemOS</p>\n <p>Set a password to protect your memories</p>\n <input type=\"password\" id=\"setupPw\" placeholder=\"Enter a password (4+ characters)\" autofocus>\n <input type=\"password\" id=\"setupPw2\" placeholder=\"Confirm password\">\n <button class=\"btn-auth\" onclick=\"doSetup()\">Set Password & Enter</button>\n <div class=\"error-msg\" id=\"setupErr\"></div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Auth: Login \u2500\u2500\u2500 -->\n<div id=\"loginScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-card\">\n <div class=\"logo\">\uD83E\uDD9E</div>\n <h1>OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\">Powered by MemOS</p>\n <p>Enter your password to access memories</p>\n <div id=\"loginForm\">\n <input type=\"password\" id=\"loginPw\" placeholder=\"Password\" autofocus>\n <button class=\"btn-auth\" onclick=\"doLogin()\">Unlock</button>\n <div class=\"error-msg\" id=\"loginErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showResetForm()\">Forgot password?</button>\n </div>\n <div id=\"resetForm\" style=\"display:none\">\n <div class=\"reset-guide\">\n <div class=\"reset-step\">\n <div class=\"step-num\">1</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Open Terminal</div>\n <div class=\"step-desc\">Run the following command to get your reset token:</div>\n <div class=\"cmd-box\" onclick=\"copyCmd(this)\">\n <code>grep \"reset token\" /tmp/openclaw/openclaw-*.log | tail -1</code>\n <span class=\"copy-hint\">Click to copy</span>\n </div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">2</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Find the token</div>\n <div class=\"step-desc\">In the output, look for a line like:<br><span style=\"font-family:monospace;font-size:12px;color:var(--pri)\">password reset token: <strong>a1b2c3d4e5f6...</strong></span><br>Copy the hex string after the colon.</div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">3</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Paste & reset</div>\n <div class=\"step-desc\">Paste the token below and set your new password.</div>\n </div>\n </div>\n </div>\n <input type=\"text\" id=\"resetToken\" placeholder=\"Paste reset token here\" style=\"margin-bottom:8px;font-family:monospace\">\n <input type=\"password\" id=\"resetNewPw\" placeholder=\"New password (4+ characters)\">\n <input type=\"password\" id=\"resetNewPw2\" placeholder=\"Confirm new password\">\n <button class=\"btn-auth\" onclick=\"doReset()\">Reset Password</button>\n <div class=\"error-msg\" id=\"resetErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showLoginForm()\">\u2190 Back to login</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Main App \u2500\u2500\u2500 -->\n<div class=\"app\" id=\"app\">\n <div class=\"topbar\">\n <div class=\"brand\">\n <div class=\"icon\">\uD83E\uDD9E</div>\n <span>OpenClaw Memory <span style=\"font-weight:400;color:var(--text-sec);font-size:12px\">by MemOS</span></span>\n </div>\n <div class=\"actions\">\n <button class=\"btn btn-primary\" onclick=\"openCreateModal()\">+ New Memory</button>\n <button class=\"btn\" onclick=\"loadAll()\">Refresh</button>\n <button class=\"btn btn-danger\" onclick=\"clearAll()\">Clear All</button>\n <button class=\"btn btn-text\" onclick=\"doLogout()\">Logout</button>\n </div>\n </div>\n\n <div class=\"main-content\">\n <div class=\"sidebar\" id=\"sidebar\">\n <div class=\"stats-grid\" id=\"statsGrid\">\n <div class=\"stat-card pri\"><div class=\"stat-value\" id=\"statTotal\">-</div><div class=\"stat-label\">Memories</div></div>\n <div class=\"stat-card green\"><div class=\"stat-value\" id=\"statSessions\">-</div><div class=\"stat-label\">Sessions</div></div>\n <div class=\"stat-card amber\"><div class=\"stat-value\" id=\"statEmbeddings\">-</div><div class=\"stat-label\">Embeddings</div></div>\n <div class=\"stat-card rose\"><div class=\"stat-value\" id=\"statTimeSpan\">-</div><div class=\"stat-label\">Days</div></div>\n </div>\n <div id=\"embeddingStatus\"></div>\n <div class=\"section-title\">Sessions</div>\n <div class=\"session-list\" id=\"sessionList\"></div>\n </div>\n\n <div class=\"feed\">\n <div class=\"search-bar\">\n <span class=\"search-icon\">\uD83D\uDD0D</span>\n <input type=\"text\" id=\"searchInput\" placeholder=\"Search memories (supports semantic search)...\" oninput=\"debounceSearch()\">\n </div>\n <div class=\"search-meta\" id=\"searchMeta\"></div>\n <div class=\"filter-bar\" id=\"filterBar\">\n <button class=\"filter-chip active\" data-role=\"\" onclick=\"setRoleFilter(this,'')\">All</button>\n <button class=\"filter-chip\" data-role=\"user\" onclick=\"setRoleFilter(this,'user')\">User</button>\n <button class=\"filter-chip\" data-role=\"assistant\" onclick=\"setRoleFilter(this,'assistant')\">Assistant</button>\n <button class=\"filter-chip\" data-role=\"system\" onclick=\"setRoleFilter(this,'system')\">System</button>\n <span class=\"filter-sep\"></span>\n <select id=\"filterKind\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"\">All kinds</option>\n <option value=\"paragraph\">Paragraph</option>\n <option value=\"code_block\">Code</option>\n <option value=\"dialog\">Dialog</option>\n <option value=\"list\">List</option>\n <option value=\"error_stack\">Error</option>\n <option value=\"command\">Command</option>\n </select>\n <select id=\"filterSort\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"newest\">Newest first</option>\n <option value=\"oldest\">Oldest first</option>\n </select>\n </div>\n <div class=\"date-filter\">\n <label>From</label><input type=\"datetime-local\" id=\"dateFrom\" step=\"1\" onchange=\"applyFilters()\">\n <label>To</label><input type=\"datetime-local\" id=\"dateTo\" step=\"1\" onchange=\"applyFilters()\">\n <button class=\"btn btn-sm btn-text\" onclick=\"clearDateFilter()\">Clear</button>\n </div>\n <div class=\"memory-list\" id=\"memoryList\"><div class=\"spinner\"></div></div>\n <div class=\"pagination\" id=\"pagination\"></div>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Memory Modal \u2500\u2500\u2500 -->\n<div class=\"modal-overlay\" id=\"modalOverlay\">\n <div class=\"modal\">\n <h2 id=\"modalTitle\">New Memory</h2>\n <div class=\"form-group\"><label>Role</label><select id=\"mRole\"><option value=\"user\">User</option><option value=\"assistant\">Assistant</option><option value=\"system\">System</option></select></div>\n <div class=\"form-group\"><label>Content</label><textarea id=\"mContent\" rows=\"4\" placeholder=\"Memory content...\"></textarea></div>\n <div class=\"form-group\"><label>Summary</label><input type=\"text\" id=\"mSummary\" placeholder=\"Brief summary (optional)\"></div>\n <div class=\"form-group\"><label>Kind</label><select id=\"mKind\"><option value=\"paragraph\">Paragraph</option><option value=\"code\">Code</option><option value=\"dialog\">Dialog</option></select></div>\n <div class=\"modal-actions\">\n <button class=\"btn\" onclick=\"closeModal()\">Cancel</button>\n <button class=\"btn btn-primary\" id=\"modalSubmit\" onclick=\"submitModal()\">Create</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Toast \u2500\u2500\u2500 -->\n<div class=\"toast-container\" id=\"toasts\"></div>\n\n<script>\nlet activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=30;\n\n/* \u2500\u2500\u2500 Auth flow \u2500\u2500\u2500 */\nasync function checkAuth(){\n const r=await fetch('/api/auth/status');\n const d=await r.json();\n if(d.needsSetup){\n document.getElementById('setupScreen').style.display='flex';\n document.getElementById('setupPw').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('setupPw2').focus()});\n document.getElementById('setupPw2').addEventListener('keydown',e=>{if(e.key==='Enter')doSetup()});\n } else if(!d.loggedIn){\n document.getElementById('loginScreen').style.display='flex';\n document.getElementById('loginPw').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});\n } else {\n enterApp();\n }\n}\n\nasync function doSetup(){\n const pw=document.getElementById('setupPw').value;\n const pw2=document.getElementById('setupPw2').value;\n const err=document.getElementById('setupErr');\n if(pw.length<4){err.textContent='Password must be at least 4 characters';return}\n if(pw!==pw2){err.textContent='Passwords do not match';return}\n const r=await fetch('/api/auth/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('setupScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||'Setup failed'}\n}\n\nasync function doLogin(){\n const pw=document.getElementById('loginPw').value;\n const err=document.getElementById('loginErr');\n const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent='Incorrect password';document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}\n}\n\nasync function doLogout(){\n await fetch('/api/auth/logout',{method:'POST'});\n location.reload();\n}\n\nfunction showResetForm(){\n document.getElementById('loginForm').style.display='none';\n document.getElementById('resetForm').style.display='block';\n document.getElementById('resetToken').focus();\n}\n\nfunction showLoginForm(){\n document.getElementById('resetForm').style.display='none';\n document.getElementById('loginForm').style.display='block';\n document.getElementById('loginPw').focus();\n}\n\nfunction copyCmd(el){\n const code=el.querySelector('code').textContent;\n navigator.clipboard.writeText(code).then(()=>{\n el.classList.add('copied');\n el.querySelector('.copy-hint').textContent='Copied!';\n setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent='Click to copy'},2000);\n });\n}\n\nasync function doReset(){\n const token=document.getElementById('resetToken').value.trim();\n const pw=document.getElementById('resetNewPw').value;\n const pw2=document.getElementById('resetNewPw2').value;\n const err=document.getElementById('resetErr');\n if(!token){err.textContent='Please enter the reset token';return}\n if(pw.length<4){err.textContent='Password must be at least 4 characters';return}\n if(pw!==pw2){err.textContent='Passwords do not match';return}\n const r=await fetch('/api/auth/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,newPassword:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||'Reset failed'}\n}\n\nfunction enterApp(){\n document.getElementById('app').style.display='flex';\n loadAll();\n}\n\n/* \u2500\u2500\u2500 Data loading \u2500\u2500\u2500 */\nasync function loadAll(){\n await Promise.all([loadStats(),loadMemories()]);\n}\n\nasync function loadStats(){\n const r=await fetch('/api/stats');\n const d=await r.json();\n document.getElementById('statTotal').textContent=d.totalMemories;\n document.getElementById('statSessions').textContent=d.totalSessions;\n document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;\n const days=d.timeRange.earliest?Math.max(1,Math.round((new Date(d.timeRange.latest)-new Date(d.timeRange.earliest))/(86400000))):0;\n document.getElementById('statTimeSpan').textContent=days;\n\n const provEl=document.getElementById('embeddingStatus');\n if(d.embeddingProvider && d.embeddingProvider!=='none'){\n provEl.innerHTML='<div class=\"provider-badge\"><span>\\u2713</span> Embedding: '+d.embeddingProvider+'</div>';\n } else {\n provEl.innerHTML='<div class=\"provider-badge offline\"><span>\\u26A0</span> No embedding model</div>';\n }\n\n const sl=document.getElementById('sessionList');\n sl.innerHTML='<div class=\"session-item'+(activeSession===null?' active':'')+'\" onclick=\"filterSession(null)\"><span>All Sessions</span><span class=\"count\">'+d.totalMemories+'</span></div>';\n (d.sessions||[]).forEach(s=>{\n const isActive=activeSession===s.session_key;\n const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;\n sl.innerHTML+='<div class=\"session-item'+(isActive?' active':'')+'\" onclick=\"filterSession(\\''+s.session_key.replace(/'/g,\"\\\\'\")+'\\')\"><span title=\"'+s.session_key+'\">'+name+'</span><span class=\"count\">'+s.count+'</span></div>';\n });\n}\n\nfunction getFilterParams(){\n const p=new URLSearchParams();\n if(activeSession) p.set('session',activeSession);\n if(activeRole) p.set('role',activeRole);\n const kind=document.getElementById('filterKind').value;\n if(kind) p.set('kind',kind);\n const df=document.getElementById('dateFrom').value;\n if(df) p.set('dateFrom',df);\n const dt=document.getElementById('dateTo').value;\n if(dt) p.set('dateTo',dt);\n const sort=document.getElementById('filterSort').value;\n if(sort==='oldest') p.set('sort','oldest');\n return p;\n}\n\nasync function loadMemories(page){\n if(page) currentPage=page;\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('limit',PAGE_SIZE);\n p.set('page',currentPage);\n const r=await fetch('/api/memories?'+p.toString());\n const d=await r.json();\n totalPages=d.totalPages||1;\n totalCount=d.total||0;\n document.getElementById('searchMeta').textContent=totalCount+' memories total';\n renderMemories(d.memories||[]);\n renderPagination();\n}\n\nasync function doSearch(q){\n if(!q.trim()){currentPage=1;loadMemories();return}\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('q',q);\n const r=await fetch('/api/search?'+p.toString());\n const d=await r.json();\n const meta=[];\n if(d.vectorCount>0) meta.push(d.vectorCount+' semantic');\n if(d.ftsCount>0) meta.push(d.ftsCount+' text');\n meta.push(d.total+' results');\n document.getElementById('searchMeta').textContent=meta.join(' \\u00B7 ');\n renderMemories(d.results||[]);\n document.getElementById('pagination').innerHTML='';\n}\n\nfunction debounceSearch(){\n clearTimeout(searchTimer);\n searchTimer=setTimeout(()=>doSearch(document.getElementById('searchInput').value),350);\n}\n\nfunction filterSession(key){\n activeSession=key;\n currentPage=1;\n loadAll();\n}\n\nfunction setRoleFilter(btn,role){\n activeRole=role;\n currentPage=1;\n document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n applyFilters();\n}\n\nfunction applyFilters(){\n currentPage=1;\n if(document.getElementById('searchInput').value.trim()){\n doSearch(document.getElementById('searchInput').value);\n } else {\n loadMemories();\n }\n}\n\nfunction clearDateFilter(){\n document.getElementById('dateFrom').value='';\n document.getElementById('dateTo').value='';\n applyFilters();\n}\n\n/* \u2500\u2500\u2500 Rendering \u2500\u2500\u2500 */\nfunction renderMemories(items){\n const list=document.getElementById('memoryList');\n if(!items.length){\n list.innerHTML='<div class=\"empty\"><div class=\"icon\">\\u{1F4ED}</div><p>No memories found</p></div>';\n return;\n }\n items.forEach(m=>{memoryCache[m.id]=m});\n list.innerHTML=items.map(m=>{\n const time=m.created_at?new Date(typeof m.created_at==='number'?m.created_at:m.created_at).toLocaleString('zh-CN'):'';\n const role=m.role||'user';\n const kind=m.kind||'paragraph';\n const summary=esc(m.summary||m.content?.slice(0,120)||'');\n const content=esc(m.content||'');\n const id=m.id;\n const vscore=m._vscore?'<span class=\"vscore-badge\">'+Math.round(m._vscore*100)+'%</span>':'';\n const sid=m.session_key||'';\n const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;\n return '<div class=\"memory-card\">'+\n '<div class=\"card-header\"><div class=\"meta\"><span class=\"role-tag '+role+'\">'+role+'</span><span class=\"kind-tag\">'+kind+'</span></div><span class=\"card-time\"><span class=\"session-tag\" title=\"'+esc(sid)+'\">'+esc(sidShort)+'</span> '+time+'</span></div>'+\n '<div class=\"card-summary\">'+summary+'</div>'+\n '<div class=\"card-content\" id=\"content-'+id+'\"><pre>'+content+'</pre></div>'+\n '<div class=\"card-actions\">'+\n '<button class=\"btn btn-sm btn-text\" onclick=\"toggleContent(\\''+id+'\\')\">Expand</button>'+\n '<button class=\"btn btn-sm\" onclick=\"openEditModal(\\''+id+'\\')\">Edit</button>'+\n '<button class=\"btn btn-sm btn-danger\" onclick=\"deleteMemory(\\''+id+'\\')\">Delete</button>'+\n vscore+\n '</div></div>';\n }).join('');\n}\n\nfunction renderPagination(){\n const el=document.getElementById('pagination');\n if(totalPages<=1){el.innerHTML='';return}\n let h='';\n h+='<button class=\"pg-btn'+(currentPage<=1?' disabled':'')+'\" onclick=\"goPage('+(currentPage-1)+')\">\u2039</button>';\n const range=[];\n range.push(1);\n for(let i=Math.max(2,currentPage-2);i<=Math.min(totalPages-1,currentPage+2);i++) range.push(i);\n if(totalPages>1) range.push(totalPages);\n const unique=[...new Set(range)].sort((a,b)=>a-b);\n let prev=0;\n for(const p of unique){\n if(p-prev>1) h+='<span class=\"pg-info\">...</span>';\n h+='<button class=\"pg-btn'+(p===currentPage?' active':'')+'\" onclick=\"goPage('+p+')\">'+p+'</button>';\n prev=p;\n }\n h+='<button class=\"pg-btn'+(currentPage>=totalPages?' disabled':'')+'\" onclick=\"goPage('+(currentPage+1)+')\">\u203A</button>';\n h+='<span class=\"pg-info\">'+totalCount+' total</span>';\n el.innerHTML=h;\n}\n\nfunction goPage(p){\n if(p<1||p>totalPages||p===currentPage) return;\n currentPage=p;\n loadMemories();\n document.getElementById('memoryList').scrollIntoView({behavior:'smooth',block:'start'});\n}\n\nfunction toggleContent(id){\n const el=document.getElementById('content-'+id);\n el.classList.toggle('show');\n}\n\nfunction esc(s){\n if(!s)return'';\n return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n}\n\n/* \u2500\u2500\u2500 CRUD \u2500\u2500\u2500 */\nfunction openCreateModal(){\n editingId=null;\n document.getElementById('modalTitle').textContent='New Memory';\n document.getElementById('modalSubmit').textContent='Create';\n document.getElementById('mRole').value='user';\n document.getElementById('mContent').value='';\n document.getElementById('mSummary').value='';\n document.getElementById('mKind').value='paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction openEditModal(id){\n const m=memoryCache[id];\n if(!m){toast('Memory not found in cache','error');return}\n editingId=id;\n document.getElementById('modalTitle').textContent='Edit Memory';\n document.getElementById('modalSubmit').textContent='Save';\n document.getElementById('mRole').value=m.role||'user';\n document.getElementById('mContent').value=m.content||'';\n document.getElementById('mSummary').value=m.summary||'';\n document.getElementById('mKind').value=m.kind||'paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction closeModal(){\n document.getElementById('modalOverlay').classList.remove('show');\n}\n\nasync function submitModal(){\n const data={\n role:document.getElementById('mRole').value,\n content:document.getElementById('mContent').value,\n summary:document.getElementById('mSummary').value,\n kind:document.getElementById('mKind').value,\n };\n if(!data.content.trim()){toast('Please enter content','error');return}\n let r;\n if(editingId){\n r=await fetch('/api/memory/'+editingId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n } else {\n r=await fetch('/api/memory',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n }\n const d=await r.json();\n if(d.ok){toast(editingId?'Memory updated':'Memory created','success');closeModal();loadAll();}\n else{toast(d.error||'Operation failed','error')}\n}\n\nasync function deleteMemory(id){\n if(!confirm('Delete this memory?'))return;\n const r=await fetch('/api/memory/'+id,{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast('Memory deleted','success');loadAll();}\n else{toast('Delete failed','error')}\n}\n\nasync function clearAll(){\n if(!confirm('Delete ALL memories? This cannot be undone.'))return;\n if(!confirm('Are you absolutely sure?'))return;\n const r=await fetch('/api/memories',{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast('All memories cleared','success');loadAll();}\n else{toast('Clear failed','error')}\n}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\nfunction toast(msg,type='info'){\n const c=document.getElementById('toasts');\n const t=document.createElement('div');\n t.className='toast '+type;\n const icons={success:'\\u2705',error:'\\u274C',info:'\\u2139\\uFE0F'};\n t.innerHTML=(icons[type]||'')+' '+esc(msg);\n c.appendChild(t);\n setTimeout(()=>t.remove(),3500);\n}\n\n/* \u2500\u2500\u2500 Init \u2500\u2500\u2500 */\ndocument.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});\ndocument.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});\ncheckAuth();\n</script>\n</body>\n</html>";
1
+ export declare const viewerHTML = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>OpenClaw Memory - Powered by MemOS</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\n:root{\n --bg:#050510;--bg-card:rgba(255,255,255,.04);--bg-card-hover:rgba(255,255,255,.07);\n --border:rgba(255,255,255,.08);--border-glow:rgba(0,187,238,.25);\n --text:#f0f4f8;--text-sec:#8b95a5;--text-muted:#5a6373;\n --pri:#00bbee;--pri-glow:rgba(0,187,238,.15);--pri-dark:#0088aa;\n --pri-grad:linear-gradient(135deg,#00bbee,#00a0cc);\n --accent:#e63946;--accent-glow:rgba(230,57,70,.15);\n --green:#10b981;--green-bg:rgba(16,185,129,.12);\n --amber:#f59e0b;--amber-bg:rgba(245,158,11,.12);\n --violet:#8b5cf6;--rose:#f43f5e;--rose-bg:rgba(244,63,94,.12);\n --shadow-sm:0 1px 2px rgba(0,0,0,.2);--shadow:0 4px 12px rgba(0,0,0,.25);\n --shadow-lg:0 20px 40px rgba(0,0,0,.35);\n --radius:12px;--radius-lg:14px;--radius-xl:18px;\n}\nbody{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}\nbutton{cursor:pointer;font-family:inherit;font-size:inherit}\ninput,textarea,select{font-family:inherit;font-size:inherit}\n\n/* \u2500\u2500\u2500 Auth (Linkify \u914D\u8272: globals.css .dark + \u84DD\u7D2B\u6E10\u53D8) \u2500\u2500\u2500 */\n.auth-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;background:linear-gradient(135deg,rgb(36,0,255) 0%,rgb(0,135,255) 35%,rgb(108,39,157) 70%,rgb(105,30,255) 100%);position:relative;overflow:hidden}\n.auth-card{background:hsl(0 0% 100%);border:none;border-radius:8px;padding:48px 40px;width:100%;max-width:420px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);text-align:center;position:relative;z-index:1}\n.auth-card .logo{width:56px;height:56px;margin:0 auto 20px;display:flex;align-items:center;justify-content:center;font-size:48px;background:none;border-radius:0}\n.auth-card h1{font-size:22px;font-weight:700;margin-bottom:4px;color:hsl(0 0% 3.9%);letter-spacing:-.02em}\n.auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}\n.auth-card input{width:100%;padding:12px 16px;border:1px solid hsl(0 0% 89.8%);border-radius:8px;font-size:14px;transition:all .2s;margin-bottom:10px;outline:none;background:#fff;color:hsl(0 0% 3.9%)}\n.auth-card input::placeholder{color:hsl(0 0% 45.1%)}\n.auth-card input:focus{border-color:rgb(168,85,247);box-shadow:0 0 0 3px rgba(168,85,247,.2)}\n.auth-card .btn-auth{width:100%;padding:12px;border:none;border-radius:8px;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-weight:600;font-size:14px;transition:all .2s}\n.auth-card .btn-auth:hover{background:hsl(0 0% 14%);transform:translateY(-1px);box-shadow:0 8px 25px rgba(0,0,0,.2)}\n.auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}\n.auth-card .btn-text{color:hsl(0 0% 45.1%)}\n.auth-card .btn-text:hover{color:rgb(168,85,247)}\n\n.reset-guide{text-align:left;margin-bottom:20px}\n.reset-step{display:flex;gap:14px;margin-bottom:16px}\n.step-num{width:28px;height:28px;border-radius:50%;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}\n.step-body{flex:1;min-width:0}\n.step-title{font-size:14px;font-weight:600;color:hsl(0 0% 3.9%);margin-bottom:2px}\n.step-desc{font-size:13px;color:hsl(0 0% 45.1%);line-height:1.5}\n.cmd-box{margin-top:8px;background:hsl(0 0% 96.1%);border:1px solid hsl(0 0% 89.8%);border-radius:8px;padding:12px 14px;font-size:12px;font-family:ui-monospace,monospace;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:space-between;gap:8px;word-break:break-all;color:hsl(0 0% 3.9%)}\n.cmd-box:hover{border-color:rgb(168,85,247);background:rgba(168,85,247,.08)}\n.cmd-box code{flex:1}\n.copy-hint{font-size:11px;color:hsl(0 0% 45.1%);white-space:nowrap}\n.cmd-box.copied .copy-hint{color:hsl(142 71% 45%)}\n\n/* \u2500\u2500\u2500 App Layout (dark dashboard, same as www) \u2500\u2500\u2500 */\n.app{display:none;flex-direction:column;min-height:100vh}\n.topbar{background:rgba(5,5,16,.85);border-bottom:1px solid var(--border);padding:0 28px;height:64px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;backdrop-filter:blur(12px)}\n.topbar .brand{display:flex;align-items:center;gap:12px;font-weight:700;font-size:17px;color:var(--text);letter-spacing:-.02em}\n.topbar .brand .icon{width:38px;height:38px;display:flex;align-items:center;justify-content:center;font-size:26px;background:none;border-radius:0}\n.topbar .actions{display:flex;align-items:center;gap:10px}\n\n.main-content{display:flex;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}\n\n/* \u2500\u2500\u2500 Sidebar \u2500\u2500\u2500 */\n.sidebar{width:260px;flex-shrink:0}\n.sidebar .stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:24px}\n.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;transition:all .2s}\n.stat-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.stat-card .stat-value{font-size:22px;font-weight:700;color:var(--text);letter-spacing:-.02em}\n.stat-card .stat-label{font-size:12px;color:var(--text-sec);margin-top:4px;font-weight:500}\n.stat-card.pri .stat-value{color:var(--pri)}\n.stat-card.green .stat-value{color:var(--green)}\n.stat-card.amber .stat-value{color:var(--amber)}\n.stat-card.rose .stat-value{color:var(--rose)}\n\n.sidebar .section-title{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin:24px 0 12px;padding:0 2px}\n.sidebar .session-list{display:flex;flex-direction:column;gap:6px;max-height:280px;overflow-y:auto}\n.session-item{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;font-size:13px;color:var(--text)}\n.session-item:hover{border-color:var(--pri);background:var(--pri-glow)}\n.session-item.active{border-color:var(--pri);background:var(--pri-glow);font-weight:600;color:var(--pri)}\n.session-item .count{color:var(--text-sec);font-size:11px;font-weight:600;background:rgba(0,0,0,.2);padding:3px 8px;border-radius:8px}\n\n.provider-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--green-bg);color:var(--green);border-radius:999px;font-size:11px;font-weight:600;margin-top:10px}\n.provider-badge.offline{background:var(--amber-bg);color:var(--amber)}\n\n/* \u2500\u2500\u2500 Feed \u2500\u2500\u2500 */\n.feed{flex:1;min-width:0}\n.search-bar{display:flex;gap:12px;margin-bottom:16px;position:relative}\n.search-bar input{flex:1;padding:12px 16px 12px 44px;border:1px solid var(--border);border-radius:12px;font-size:14px;outline:none;background:var(--bg-card);color:var(--text);transition:all .2s}\n.search-bar input::placeholder{color:var(--text-muted)}\n.search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.search-bar .search-icon{position:absolute;left:16px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:15px;pointer-events:none}\n.search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}\n\n.filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}\n.filter-chip{padding:6px 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;transition:all .15s}\n.filter-chip:hover{border-color:var(--pri);color:var(--pri)}\n.filter-chip.active{background:var(--pri);color:#000;border-color:var(--pri)}\n\n.memory-list{display:flex;flex-direction:column;gap:16px}\n.memory-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;transition:all .2s}\n.memory-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.memory-card .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}\n.memory-card .meta{display:flex;align-items:center;gap:8px}\n.role-tag{padding:4px 10px;border-radius:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}\n.role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(0,187,238,.2)}\n.role-tag.assistant{background:var(--accent-glow);color:var(--accent);border:1px solid rgba(230,57,70,.2)}\n.role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}\n.kind-tag{padding:4px 10px;border-radius:8px;font-size:11px;color:var(--text-sec);background:rgba(0,0,0,.2);font-weight:500}\n.card-time{font-size:12px;color:var(--text-sec);display:flex;align-items:center;gap:8px}\n.session-tag{font-size:11px;font-family:ui-monospace,monospace;color:var(--text-muted);background:rgba(0,0,0,.2);padding:3px 8px;border-radius:6px;cursor:default}\n.card-summary{font-size:15px;font-weight:600;color:var(--text);margin-bottom:10px;line-height:1.5;letter-spacing:-.01em}\n.card-content{font-size:13px;color:var(--text-sec);line-height:1.65;max-height:0;overflow:hidden;transition:max-height .3s ease}\n.card-content.show{max-height:600px;overflow-y:auto}\n.card-content pre{white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.25);padding:14px;border-radius:10px;font-size:12px;font-family:ui-monospace,monospace;margin-top:10px;border:1px solid var(--border);color:var(--text-sec)}\n.card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}\n.vscore-badge{display:inline-flex;align-items:center;background:linear-gradient(135deg,var(--pri),var(--violet));color:#fff;font-size:10px;font-weight:700;padding:4px 10px;border-radius:8px;margin-left:auto}\n\n/* \u2500\u2500\u2500 Buttons \u2500\u2500\u2500 */\n.btn{padding:8px 16px;border-radius:10px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;font-weight:500;transition:all .15s;display:inline-flex;align-items:center;gap:6px}\n.btn:hover{border-color:var(--pri);color:var(--pri)}\n.btn-primary{background:var(--pri);color:#000;border:none}\n.btn-primary:hover{background:#4dd9ff;transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,187,238,.25)}\n.btn-danger{color:var(--accent);border-color:var(--accent)}\n.btn-danger:hover{background:var(--accent);color:#fff;border-color:var(--accent)}\n.btn-sm{padding:6px 12px;font-size:12px}\n.btn-icon{padding:6px 8px;font-size:14px}\n.btn-text{border:none;background:none;color:var(--text-sec);font-size:13px;padding:4px 8px}\n.btn-text:hover{color:var(--pri)}\n\n/* \u2500\u2500\u2500 Modal \u2500\u2500\u2500 */\n.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:500;align-items:center;justify-content:center;backdrop-filter:blur(8px)}\n.modal-overlay.show{display:flex}\n.modal{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);padding:32px;width:100%;max-width:520px;box-shadow:var(--shadow-lg);max-height:85vh;overflow-y:auto}\n.modal h2{font-size:20px;font-weight:700;margin-bottom:24px;color:var(--text);letter-spacing:-.02em}\n.form-group{margin-bottom:18px}\n.form-group label{display:block;font-size:13px;font-weight:600;color:var(--text-sec);margin-bottom:6px}\n.form-group input,.form-group textarea,.form-group select{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:10px;font-size:14px;outline:none;transition:all .2s;background:var(--bg-card);color:var(--text)}\n.form-group input::placeholder,.form-group textarea::placeholder{color:var(--text-muted)}\n.form-group input:focus,.form-group textarea:focus,.form-group select:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.form-group textarea{min-height:100px;resize:vertical}\n.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:28px}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\n.toast-container{position:fixed;top:80px;right:24px;z-index:1000;display:flex;flex-direction:column;gap:8px}\n.toast{padding:14px 20px;border-radius:10px;font-size:13px;font-weight:500;box-shadow:var(--shadow-lg);animation:slideIn .3s ease;display:flex;align-items:center;gap:10px;max-width:360px;border:1px solid}\n.toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}\n.toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}\n.toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(0,187,238,.3)}\n@keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}\n\n.empty{text-align:center;padding:64px 20px;color:var(--text-sec)}\n.empty .icon{font-size:52px;margin-bottom:16px;opacity:.5}\n.empty p{font-size:15px;font-weight:500}\n\n.spinner{width:40px;height:40px;border:3px solid var(--border);border-top-color:var(--pri);border-radius:50%;animation:spin .8s linear infinite;margin:48px auto}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n::-webkit-scrollbar{width:6px;height:6px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.25)}\n\n.filter-sep{width:1px;height:20px;background:var(--border);margin:0 4px}\n.filter-select{padding:6px 12px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;outline:none;cursor:pointer}\n.filter-select:focus{border-color:var(--pri)}\n.date-filter{display:flex;align-items:center;gap:10px;margin-bottom:18px;font-size:13px;color:var(--text-sec)}\n.date-filter input[type=\"datetime-local\"]{padding:6px 10px;border:1px solid var(--border);border-radius:8px;font-size:12px;outline:none;background:var(--bg-card);color:var(--text)}\n.date-filter input[type=\"datetime-local\"]:focus{border-color:var(--pri)}\n.date-filter label{font-weight:500}\n\n.pagination{display:flex;align-items:center;justify-content:center;gap:6px;padding:28px 0;flex-wrap:wrap}\n.pagination .pg-btn{min-width:38px;height:38px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s}\n.pagination .pg-btn:hover{border-color:var(--pri);color:var(--pri)}\n.pagination .pg-btn.active{background:var(--pri);color:#000;border-color:var(--pri)}\n.pagination .pg-btn.disabled{opacity:.4;pointer-events:none}\n.pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}\n\n@media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}}\n</style>\n</head>\n<body>\n\n<!-- \u2500\u2500\u2500 Auth: Setup Password \u2500\u2500\u2500 -->\n<div id=\"setupScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-card\">\n <div class=\"logo\">\uD83E\uDD9E</div>\n <h1>OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\">Powered by MemOS</p>\n <p>Set a password to protect your memories</p>\n <input type=\"password\" id=\"setupPw\" placeholder=\"Enter a password (4+ characters)\" autofocus>\n <input type=\"password\" id=\"setupPw2\" placeholder=\"Confirm password\">\n <button class=\"btn-auth\" onclick=\"doSetup()\">Set Password & Enter</button>\n <div class=\"error-msg\" id=\"setupErr\"></div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Auth: Login \u2500\u2500\u2500 -->\n<div id=\"loginScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-card\">\n <div class=\"logo\">\uD83E\uDD9E</div>\n <h1>OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\">Powered by MemOS</p>\n <p>Enter your password to access memories</p>\n <div id=\"loginForm\">\n <input type=\"password\" id=\"loginPw\" placeholder=\"Password\" autofocus>\n <button class=\"btn-auth\" onclick=\"doLogin()\">Unlock</button>\n <div class=\"error-msg\" id=\"loginErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showResetForm()\">Forgot password?</button>\n </div>\n <div id=\"resetForm\" style=\"display:none\">\n <div class=\"reset-guide\">\n <div class=\"reset-step\">\n <div class=\"step-num\">1</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Open Terminal</div>\n <div class=\"step-desc\">Run the following command to get your reset token (use the pattern below so you get the line that contains the token):</div>\n <div class=\"cmd-box\" onclick=\"copyCmd(this)\">\n <code>grep \"password reset token:\" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1</code>\n <span class=\"copy-hint\">Click to copy</span>\n </div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">2</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Find the token</div>\n <div class=\"step-desc\">In the output, find <span style=\"font-family:monospace;font-size:12px;color:var(--pri)\">password reset token: <strong>a1b2c3d4e5f6...</strong></span> (plain line or inside JSON). Copy the 32-character hex string after the colon.</div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">3</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Paste & reset</div>\n <div class=\"step-desc\">Paste the token below and set your new password.</div>\n </div>\n </div>\n </div>\n <input type=\"text\" id=\"resetToken\" placeholder=\"Paste reset token here\" style=\"margin-bottom:8px;font-family:monospace\">\n <input type=\"password\" id=\"resetNewPw\" placeholder=\"New password (4+ characters)\">\n <input type=\"password\" id=\"resetNewPw2\" placeholder=\"Confirm new password\">\n <button class=\"btn-auth\" onclick=\"doReset()\">Reset Password</button>\n <div class=\"error-msg\" id=\"resetErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showLoginForm()\">\u2190 Back to login</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Main App \u2500\u2500\u2500 -->\n<div class=\"app\" id=\"app\">\n <div class=\"topbar\">\n <div class=\"brand\">\n <div class=\"icon\">\uD83E\uDD9E</div>\n <span>OpenClaw Memory <span style=\"font-weight:400;color:var(--text-sec);font-size:12px\">by MemOS</span></span>\n </div>\n <div class=\"actions\">\n <button class=\"btn btn-primary\" onclick=\"openCreateModal()\">+ New Memory</button>\n <button class=\"btn\" onclick=\"loadAll()\">Refresh</button>\n <button class=\"btn btn-danger\" onclick=\"clearAll()\">Clear All</button>\n <button class=\"btn btn-text\" onclick=\"doLogout()\">Logout</button>\n </div>\n </div>\n\n <div class=\"main-content\">\n <div class=\"sidebar\" id=\"sidebar\">\n <div class=\"stats-grid\" id=\"statsGrid\">\n <div class=\"stat-card pri\"><div class=\"stat-value\" id=\"statTotal\">-</div><div class=\"stat-label\">Memories</div></div>\n <div class=\"stat-card green\"><div class=\"stat-value\" id=\"statSessions\">-</div><div class=\"stat-label\">Sessions</div></div>\n <div class=\"stat-card amber\"><div class=\"stat-value\" id=\"statEmbeddings\">-</div><div class=\"stat-label\">Embeddings</div></div>\n <div class=\"stat-card rose\"><div class=\"stat-value\" id=\"statTimeSpan\">-</div><div class=\"stat-label\">Days</div></div>\n </div>\n <div id=\"embeddingStatus\"></div>\n <div class=\"section-title\">Sessions</div>\n <div class=\"session-list\" id=\"sessionList\"></div>\n </div>\n\n <div class=\"feed\">\n <div class=\"search-bar\">\n <span class=\"search-icon\">\uD83D\uDD0D</span>\n <input type=\"text\" id=\"searchInput\" placeholder=\"Search memories (supports semantic search)...\" oninput=\"debounceSearch()\">\n </div>\n <div class=\"search-meta\" id=\"searchMeta\"></div>\n <div class=\"filter-bar\" id=\"filterBar\">\n <button class=\"filter-chip active\" data-role=\"\" onclick=\"setRoleFilter(this,'')\">All</button>\n <button class=\"filter-chip\" data-role=\"user\" onclick=\"setRoleFilter(this,'user')\">User</button>\n <button class=\"filter-chip\" data-role=\"assistant\" onclick=\"setRoleFilter(this,'assistant')\">Assistant</button>\n <button class=\"filter-chip\" data-role=\"system\" onclick=\"setRoleFilter(this,'system')\">System</button>\n <span class=\"filter-sep\"></span>\n <select id=\"filterKind\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"\">All kinds</option>\n <option value=\"paragraph\">Paragraph</option>\n <option value=\"code_block\">Code</option>\n <option value=\"dialog\">Dialog</option>\n <option value=\"list\">List</option>\n <option value=\"error_stack\">Error</option>\n <option value=\"command\">Command</option>\n </select>\n <select id=\"filterSort\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"newest\">Newest first</option>\n <option value=\"oldest\">Oldest first</option>\n </select>\n </div>\n <div class=\"date-filter\">\n <label>From</label><input type=\"datetime-local\" id=\"dateFrom\" step=\"1\" onchange=\"applyFilters()\">\n <label>To</label><input type=\"datetime-local\" id=\"dateTo\" step=\"1\" onchange=\"applyFilters()\">\n <button class=\"btn btn-sm btn-text\" onclick=\"clearDateFilter()\">Clear</button>\n </div>\n <div class=\"memory-list\" id=\"memoryList\"><div class=\"spinner\"></div></div>\n <div class=\"pagination\" id=\"pagination\"></div>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Memory Modal \u2500\u2500\u2500 -->\n<div class=\"modal-overlay\" id=\"modalOverlay\">\n <div class=\"modal\">\n <h2 id=\"modalTitle\">New Memory</h2>\n <div class=\"form-group\"><label>Role</label><select id=\"mRole\"><option value=\"user\">User</option><option value=\"assistant\">Assistant</option><option value=\"system\">System</option></select></div>\n <div class=\"form-group\"><label>Content</label><textarea id=\"mContent\" rows=\"4\" placeholder=\"Memory content...\"></textarea></div>\n <div class=\"form-group\"><label>Summary</label><input type=\"text\" id=\"mSummary\" placeholder=\"Brief summary (optional)\"></div>\n <div class=\"form-group\"><label>Kind</label><select id=\"mKind\"><option value=\"paragraph\">Paragraph</option><option value=\"code\">Code</option><option value=\"dialog\">Dialog</option></select></div>\n <div class=\"modal-actions\">\n <button class=\"btn\" onclick=\"closeModal()\">Cancel</button>\n <button class=\"btn btn-primary\" id=\"modalSubmit\" onclick=\"submitModal()\">Create</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Toast \u2500\u2500\u2500 -->\n<div class=\"toast-container\" id=\"toasts\"></div>\n\n<script>\nlet activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=30;\n\n/* \u2500\u2500\u2500 Auth flow \u2500\u2500\u2500 */\nasync function checkAuth(){\n const r=await fetch('/api/auth/status');\n const d=await r.json();\n if(d.needsSetup){\n document.getElementById('setupScreen').style.display='flex';\n document.getElementById('setupPw').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('setupPw2').focus()});\n document.getElementById('setupPw2').addEventListener('keydown',e=>{if(e.key==='Enter')doSetup()});\n } else if(!d.loggedIn){\n document.getElementById('loginScreen').style.display='flex';\n document.getElementById('loginPw').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});\n } else {\n enterApp();\n }\n}\n\nasync function doSetup(){\n const pw=document.getElementById('setupPw').value;\n const pw2=document.getElementById('setupPw2').value;\n const err=document.getElementById('setupErr');\n if(pw.length<4){err.textContent='Password must be at least 4 characters';return}\n if(pw!==pw2){err.textContent='Passwords do not match';return}\n const r=await fetch('/api/auth/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('setupScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||'Setup failed'}\n}\n\nasync function doLogin(){\n const pw=document.getElementById('loginPw').value;\n const err=document.getElementById('loginErr');\n const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent='Incorrect password';document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}\n}\n\nasync function doLogout(){\n await fetch('/api/auth/logout',{method:'POST'});\n location.reload();\n}\n\nfunction showResetForm(){\n document.getElementById('loginForm').style.display='none';\n document.getElementById('resetForm').style.display='block';\n document.getElementById('resetToken').focus();\n}\n\nfunction showLoginForm(){\n document.getElementById('resetForm').style.display='none';\n document.getElementById('loginForm').style.display='block';\n document.getElementById('loginPw').focus();\n}\n\nfunction copyCmd(el){\n const code=el.querySelector('code').textContent;\n navigator.clipboard.writeText(code).then(()=>{\n el.classList.add('copied');\n el.querySelector('.copy-hint').textContent='Copied!';\n setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent='Click to copy'},2000);\n });\n}\n\nasync function doReset(){\n const token=document.getElementById('resetToken').value.trim();\n const pw=document.getElementById('resetNewPw').value;\n const pw2=document.getElementById('resetNewPw2').value;\n const err=document.getElementById('resetErr');\n if(!token){err.textContent='Please enter the reset token';return}\n if(pw.length<4){err.textContent='Password must be at least 4 characters';return}\n if(pw!==pw2){err.textContent='Passwords do not match';return}\n const r=await fetch('/api/auth/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,newPassword:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||'Reset failed'}\n}\n\nfunction enterApp(){\n document.getElementById('app').style.display='flex';\n loadAll();\n}\n\n/* \u2500\u2500\u2500 Data loading \u2500\u2500\u2500 */\nasync function loadAll(){\n await Promise.all([loadStats(),loadMemories()]);\n}\n\nasync function loadStats(){\n const r=await fetch('/api/stats');\n const d=await r.json();\n document.getElementById('statTotal').textContent=d.totalMemories;\n document.getElementById('statSessions').textContent=d.totalSessions;\n document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;\n const days=d.timeRange.earliest?Math.max(1,Math.round((new Date(d.timeRange.latest)-new Date(d.timeRange.earliest))/(86400000))):0;\n document.getElementById('statTimeSpan').textContent=days;\n\n const provEl=document.getElementById('embeddingStatus');\n if(d.embeddingProvider && d.embeddingProvider!=='none'){\n provEl.innerHTML='<div class=\"provider-badge\"><span>\\u2713</span> Embedding: '+d.embeddingProvider+'</div>';\n } else {\n provEl.innerHTML='<div class=\"provider-badge offline\"><span>\\u26A0</span> No embedding model</div>';\n }\n\n const sl=document.getElementById('sessionList');\n sl.innerHTML='<div class=\"session-item'+(activeSession===null?' active':'')+'\" onclick=\"filterSession(null)\"><span>All Sessions</span><span class=\"count\">'+d.totalMemories+'</span></div>';\n (d.sessions||[]).forEach(s=>{\n const isActive=activeSession===s.session_key;\n const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;\n sl.innerHTML+='<div class=\"session-item'+(isActive?' active':'')+'\" onclick=\"filterSession(\\''+s.session_key.replace(/'/g,\"\\\\'\")+'\\')\"><span title=\"'+s.session_key+'\">'+name+'</span><span class=\"count\">'+s.count+'</span></div>';\n });\n}\n\nfunction getFilterParams(){\n const p=new URLSearchParams();\n if(activeSession) p.set('session',activeSession);\n if(activeRole) p.set('role',activeRole);\n const kind=document.getElementById('filterKind').value;\n if(kind) p.set('kind',kind);\n const df=document.getElementById('dateFrom').value;\n if(df) p.set('dateFrom',df);\n const dt=document.getElementById('dateTo').value;\n if(dt) p.set('dateTo',dt);\n const sort=document.getElementById('filterSort').value;\n if(sort==='oldest') p.set('sort','oldest');\n return p;\n}\n\nasync function loadMemories(page){\n if(page) currentPage=page;\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('limit',PAGE_SIZE);\n p.set('page',currentPage);\n const r=await fetch('/api/memories?'+p.toString());\n const d=await r.json();\n totalPages=d.totalPages||1;\n totalCount=d.total||0;\n document.getElementById('searchMeta').textContent=totalCount+' memories total';\n renderMemories(d.memories||[]);\n renderPagination();\n}\n\nasync function doSearch(q){\n if(!q.trim()){currentPage=1;loadMemories();return}\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('q',q);\n const r=await fetch('/api/search?'+p.toString());\n const d=await r.json();\n const meta=[];\n if(d.vectorCount>0) meta.push(d.vectorCount+' semantic');\n if(d.ftsCount>0) meta.push(d.ftsCount+' text');\n meta.push(d.total+' results');\n document.getElementById('searchMeta').textContent=meta.join(' \\u00B7 ');\n renderMemories(d.results||[]);\n document.getElementById('pagination').innerHTML='';\n}\n\nfunction debounceSearch(){\n clearTimeout(searchTimer);\n searchTimer=setTimeout(()=>doSearch(document.getElementById('searchInput').value),350);\n}\n\nfunction filterSession(key){\n activeSession=key;\n currentPage=1;\n loadAll();\n}\n\nfunction setRoleFilter(btn,role){\n activeRole=role;\n currentPage=1;\n document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n applyFilters();\n}\n\nfunction applyFilters(){\n currentPage=1;\n if(document.getElementById('searchInput').value.trim()){\n doSearch(document.getElementById('searchInput').value);\n } else {\n loadMemories();\n }\n}\n\nfunction clearDateFilter(){\n document.getElementById('dateFrom').value='';\n document.getElementById('dateTo').value='';\n applyFilters();\n}\n\n/* \u2500\u2500\u2500 Rendering \u2500\u2500\u2500 */\nfunction renderMemories(items){\n const list=document.getElementById('memoryList');\n if(!items.length){\n list.innerHTML='<div class=\"empty\"><div class=\"icon\">\\u{1F4ED}</div><p>No memories found</p></div>';\n return;\n }\n items.forEach(m=>{memoryCache[m.id]=m});\n list.innerHTML=items.map(m=>{\n const time=m.created_at?new Date(typeof m.created_at==='number'?m.created_at:m.created_at).toLocaleString('zh-CN'):'';\n const role=m.role||'user';\n const kind=m.kind||'paragraph';\n const summary=esc(m.summary||m.content?.slice(0,120)||'');\n const content=esc(m.content||'');\n const id=m.id;\n const vscore=m._vscore?'<span class=\"vscore-badge\">'+Math.round(m._vscore*100)+'%</span>':'';\n const sid=m.session_key||'';\n const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;\n return '<div class=\"memory-card\">'+\n '<div class=\"card-header\"><div class=\"meta\"><span class=\"role-tag '+role+'\">'+role+'</span><span class=\"kind-tag\">'+kind+'</span></div><span class=\"card-time\"><span class=\"session-tag\" title=\"'+esc(sid)+'\">'+esc(sidShort)+'</span> '+time+'</span></div>'+\n '<div class=\"card-summary\">'+summary+'</div>'+\n '<div class=\"card-content\" id=\"content-'+id+'\"><pre>'+content+'</pre></div>'+\n '<div class=\"card-actions\">'+\n '<button class=\"btn btn-sm btn-text\" onclick=\"toggleContent(\\''+id+'\\')\">Expand</button>'+\n '<button class=\"btn btn-sm\" onclick=\"openEditModal(\\''+id+'\\')\">Edit</button>'+\n '<button class=\"btn btn-sm btn-danger\" onclick=\"deleteMemory(\\''+id+'\\')\">Delete</button>'+\n vscore+\n '</div></div>';\n }).join('');\n}\n\nfunction renderPagination(){\n const el=document.getElementById('pagination');\n if(totalPages<=1){el.innerHTML='';return}\n let h='';\n h+='<button class=\"pg-btn'+(currentPage<=1?' disabled':'')+'\" onclick=\"goPage('+(currentPage-1)+')\">\u2039</button>';\n const range=[];\n range.push(1);\n for(let i=Math.max(2,currentPage-2);i<=Math.min(totalPages-1,currentPage+2);i++) range.push(i);\n if(totalPages>1) range.push(totalPages);\n const unique=[...new Set(range)].sort((a,b)=>a-b);\n let prev=0;\n for(const p of unique){\n if(p-prev>1) h+='<span class=\"pg-info\">...</span>';\n h+='<button class=\"pg-btn'+(p===currentPage?' active':'')+'\" onclick=\"goPage('+p+')\">'+p+'</button>';\n prev=p;\n }\n h+='<button class=\"pg-btn'+(currentPage>=totalPages?' disabled':'')+'\" onclick=\"goPage('+(currentPage+1)+')\">\u203A</button>';\n h+='<span class=\"pg-info\">'+totalCount+' total</span>';\n el.innerHTML=h;\n}\n\nfunction goPage(p){\n if(p<1||p>totalPages||p===currentPage) return;\n currentPage=p;\n loadMemories();\n document.getElementById('memoryList').scrollIntoView({behavior:'smooth',block:'start'});\n}\n\nfunction toggleContent(id){\n const el=document.getElementById('content-'+id);\n el.classList.toggle('show');\n}\n\nfunction esc(s){\n if(!s)return'';\n return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n}\n\n/* \u2500\u2500\u2500 CRUD \u2500\u2500\u2500 */\nfunction openCreateModal(){\n editingId=null;\n document.getElementById('modalTitle').textContent='New Memory';\n document.getElementById('modalSubmit').textContent='Create';\n document.getElementById('mRole').value='user';\n document.getElementById('mContent').value='';\n document.getElementById('mSummary').value='';\n document.getElementById('mKind').value='paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction openEditModal(id){\n const m=memoryCache[id];\n if(!m){toast('Memory not found in cache','error');return}\n editingId=id;\n document.getElementById('modalTitle').textContent='Edit Memory';\n document.getElementById('modalSubmit').textContent='Save';\n document.getElementById('mRole').value=m.role||'user';\n document.getElementById('mContent').value=m.content||'';\n document.getElementById('mSummary').value=m.summary||'';\n document.getElementById('mKind').value=m.kind||'paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction closeModal(){\n document.getElementById('modalOverlay').classList.remove('show');\n}\n\nasync function submitModal(){\n const data={\n role:document.getElementById('mRole').value,\n content:document.getElementById('mContent').value,\n summary:document.getElementById('mSummary').value,\n kind:document.getElementById('mKind').value,\n };\n if(!data.content.trim()){toast('Please enter content','error');return}\n let r;\n if(editingId){\n r=await fetch('/api/memory/'+editingId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n } else {\n r=await fetch('/api/memory',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n }\n const d=await r.json();\n if(d.ok){toast(editingId?'Memory updated':'Memory created','success');closeModal();loadAll();}\n else{toast(d.error||'Operation failed','error')}\n}\n\nasync function deleteMemory(id){\n if(!confirm('Delete this memory?'))return;\n const r=await fetch('/api/memory/'+id,{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast('Memory deleted','success');loadAll();}\n else{toast('Delete failed','error')}\n}\n\nasync function clearAll(){\n if(!confirm('Delete ALL memories? This cannot be undone.'))return;\n if(!confirm('Are you absolutely sure?'))return;\n const r=await fetch('/api/memories',{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast('All memories cleared','success');loadAll();}\n else{toast('Clear failed','error')}\n}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\nfunction toast(msg,type='info'){\n const c=document.getElementById('toasts');\n const t=document.createElement('div');\n t.className='toast '+type;\n const icons={success:'\\u2705',error:'\\u274C',info:'\\u2139\\uFE0F'};\n t.innerHTML=(icons[type]||'')+' '+esc(msg);\n c.appendChild(t);\n setTimeout(()=>t.remove(),3500);\n}\n\n/* \u2500\u2500\u2500 Init \u2500\u2500\u2500 */\ndocument.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});\ndocument.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});\ncheckAuth();\n</script>\n</body>\n</html>";
2
2
  //# sourceMappingURL=html.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/viewer/html.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,w7qCAyqBf,CAAC"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/viewer/html.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,kkrCAyqBf,CAAC"}
@@ -219,9 +219,9 @@ input,textarea,select{font-family:inherit;font-size:inherit}
219
219
  <div class="step-num">1</div>
220
220
  <div class="step-body">
221
221
  <div class="step-title">Open Terminal</div>
222
- <div class="step-desc">Run the following command to get your reset token:</div>
222
+ <div class="step-desc">Run the following command to get your reset token (use the pattern below so you get the line that contains the token):</div>
223
223
  <div class="cmd-box" onclick="copyCmd(this)">
224
- <code>grep "reset token" /tmp/openclaw/openclaw-*.log | tail -1</code>
224
+ <code>grep "password reset token:" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1</code>
225
225
  <span class="copy-hint">Click to copy</span>
226
226
  </div>
227
227
  </div>
@@ -230,7 +230,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
230
230
  <div class="step-num">2</div>
231
231
  <div class="step-body">
232
232
  <div class="step-title">Find the token</div>
233
- <div class="step-desc">In the output, look for a line like:<br><span style="font-family:monospace;font-size:12px;color:var(--pri)">password reset token: <strong>a1b2c3d4e5f6...</strong></span><br>Copy the hex string after the colon.</div>
233
+ <div class="step-desc">In the output, find <span style="font-family:monospace;font-size:12px;color:var(--pri)">password reset token: <strong>a1b2c3d4e5f6...</strong></span> (plain line or inside JSON). Copy the 32-character hex string after the colon.</div>
234
234
  </div>
235
235
  </div>
236
236
  <div class="reset-step">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -216,9 +216,9 @@ input,textarea,select{font-family:inherit;font-size:inherit}
216
216
  <div class="step-num">1</div>
217
217
  <div class="step-body">
218
218
  <div class="step-title">Open Terminal</div>
219
- <div class="step-desc">Run the following command to get your reset token:</div>
219
+ <div class="step-desc">Run the following command to get your reset token (use the pattern below so you get the line that contains the token):</div>
220
220
  <div class="cmd-box" onclick="copyCmd(this)">
221
- <code>grep "reset token" /tmp/openclaw/openclaw-*.log | tail -1</code>
221
+ <code>grep "password reset token:" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1</code>
222
222
  <span class="copy-hint">Click to copy</span>
223
223
  </div>
224
224
  </div>
@@ -227,7 +227,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
227
227
  <div class="step-num">2</div>
228
228
  <div class="step-body">
229
229
  <div class="step-title">Find the token</div>
230
- <div class="step-desc">In the output, look for a line like:<br><span style="font-family:monospace;font-size:12px;color:var(--pri)">password reset token: <strong>a1b2c3d4e5f6...</strong></span><br>Copy the hex string after the colon.</div>
230
+ <div class="step-desc">In the output, find <span style="font-family:monospace;font-size:12px;color:var(--pri)">password reset token: <strong>a1b2c3d4e5f6...</strong></span> (plain line or inside JSON). Copy the 32-character hex string after the colon.</div>
231
231
  </div>
232
232
  </div>
233
233
  <div class="reset-step">
package/www/index.html CHANGED
@@ -396,7 +396,7 @@ body.lang-zh .lang-en{display:none !important}
396
396
  <div class="code-section" id="quickstart">
397
397
  <div class="code-text">
398
398
  <h3><span class="lang-zh">从 npm 安装并配置</span><span class="lang-en">Install from npm &amp; Configure</span></h3>
399
- <p><span class="lang-zh">从 npm 安装,无需克隆或构建。在 openclaw.json 中加上插件配置(embedding/summarizer 可选),然后启动 gateway。</span><span class="lang-en">Install from npm — no clone or build. Add OpenClaw config (embedding/summarizer optional), then start the gateway.</span></p>
399
+ <p><span class="lang-zh">从 npm 安装,无需克隆或构建。在 openclaw.json 中加上插件配置(embedding/summarizer 可选),然后<strong>启动或重启 gateway</strong>。Memory Viewer 只有在网关运行时才会在 http://127.0.0.1:18799 提供。</span><span class="lang-en">Install from npm — no clone or build. Add OpenClaw config (embedding/summarizer optional), then <strong>start or restart the gateway</strong>. The Memory Viewer is only available at http://127.0.0.1:18799 when the gateway is running.</span></p>
400
400
  <div>
401
401
  <span class="tag t-blue">npm</span>
402
402
  <span class="tag t-violet">OpenClaw</span>