memory-eternal 0.11.0 → 0.12.0

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memory-eternal",
4
- "version": "0.11.0",
4
+ "version": "0.12.0",
5
5
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
6
6
  "author": {
7
7
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
package/README.en.md CHANGED
@@ -20,7 +20,7 @@
20
20
  <p align="center">
21
21
  <img src="https://img.shields.io/badge/DeepSeek%20Harness-plugin-3B82F6" alt="DSH plugin" />
22
22
  <img src="https://img.shields.io/npm/v/memory-eternal" alt="npm version" />
23
- <img src="https://img.shields.io/badge/release-v0.11.0-FF7A45" alt="v0.11.0" />
23
+ <img src="https://img.shields.io/badge/release-v0.12.0-FF7A45" alt="v0.12.0" />
24
24
  <a href="https://eternalnight996.github.io/memory-eternal/"><img src="https://img.shields.io/badge/Live%20Demo-online-FF7A45" alt="Live demo" /></a>
25
25
  <img src="https://img.shields.io/github/stars/EternalNight996/memory-eternal?style=flat" alt="GitHub stars" />
26
26
  <img src="https://img.shields.io/github/license/EternalNight996/memory-eternal" alt="license" />
@@ -48,6 +48,22 @@
48
48
 
49
49
  ---
50
50
 
51
+ ## 🧩 Supported platforms
52
+
53
+ | Platform / host | Memory Atlas UI | Review / approve | Knowledge graph | Entry and status |
54
+ |---|---|---|---|---|
55
+ | **DSH desktop / Web** | Fully embedded | ✅ | ✅ | Sidebar “Memory”, supported |
56
+ | **Orca** | Sidebar launcher | In Web UI | In Web UI | Right sidebar “Memory”, [new in this release](orca-plugin/README.md) |
57
+ | **dsh-tui** | Opens browser | In Web UI | In Web UI | `/memory`, [new in this release](dsh-tui-plugin/README.md) |
58
+ | **Codex** | Opens browser | In Web UI | In Web UI | [`memory-open` skill](skills/memory-open/SKILL.md), new in this release |
59
+ | **Claude Code** | Browser / Pane | In Web UI | In Web UI | `/memory-eternal:memory-open`, new in this release |
60
+ | **Cursor** | Embedded Webview | ✅ | ✅ | [Extension Webview](cursor-extension/README.md), new in this release |
61
+ | **Pure Web** | Canonical implementation | ✅ | ✅ | `http://127.0.0.1:7999/` |
62
+
63
+ > Principle: **one Web UI is the single source of truth**. Hosts that can embed use a Webview; hosts that cannot get a thin launcher only. Every entry opens the same version, so DSH and Web never drift.
64
+
65
+ ---
66
+
51
67
  ## ✨ What problem does it actually solve
52
68
 
53
69
  | What you hit | Before | After |
@@ -82,7 +98,7 @@ cd ~/.dsh/profiles/web && pnpm add memory-eternal@latest
82
98
  # right after a release, @latest can be held back by pnpm's minimum-release-age gate or a cached
83
99
  # packument (it silently stays on the old version): pin the version instead — pnpm adds it to the
84
100
  # workspace's minimumReleaseAgeExclude automatically
85
- cd ~/.dsh/profiles/web && pnpm add memory-eternal@0.11.0
101
+ cd ~/.dsh/profiles/web && pnpm add memory-eternal@0.12.0
86
102
 
87
103
  # verify the version, then restart the host
88
104
  node -e "console.log(require('memory-eternal/package.json').version)"
@@ -390,14 +406,15 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # is the standalone we
390
406
 
391
407
  | Version | Date | Highlights |
392
408
  |---|---|---|
409
+ | **v0.12.0** | 2026-10-22 | **Cross-host launchers**: Orca right-sidebar Memory panel, dsh-tui `/memory`, Codex / Claude `memory-open` skill, and a Cursor / VS Code Webview extension; README gains a platform support table; one Web UI stays the single source of truth — Webview where it works, a thin launcher everywhere else, with no new agent permissions. |
393
410
  | **v0.11.0** | 2026-10-22 | **Memory Atlas stable release**: the full V5 UI lands — 4-column library cards with daily resurface and mini connection graphs; review top type bar + queue + right verdict sidebar (no bottom bar, no popups); knowledge graph page head/cluster legend + DPR, grid and edge rendering optimizations; four runtime status cards; reader TOC and related memories; Jinwu brand icon; click-to-zoom QR codes; onboarding starts with Star/Fork; GitHub/Gitee/npm shipped together. |
394
- | **v0.11.0-rc.1** | 2026-10-21 | **V5 preview RC**: Memory Atlas dark ember theme, 228px rail and topbar; V5 card grid and page head; audit top type bar + queue + verdict preview + select-all batch approve/reject; smaller graph default; first-run guide (👍 emphasised), reader TOC/related memory, and support rail (community/sponsor/legacy/bug report); public demo updated to V5. |
395
411
 
396
412
  <details>
397
413
  <summary>Older release notes (v0.10.52 and earlier · 79 releases, click to expand)</summary>
398
414
 
399
415
  | Version | Date | Highlights |
400
416
  |---|---|---|
417
+ | **v0.11.0-rc.1** | 2026-10-21 | **V5 preview RC**: Memory Atlas dark ember theme, 228px rail and topbar; V5 card grid and page head; audit top type bar + queue + verdict preview + select-all batch approve/reject; smaller graph default; first-run guide (👍 emphasised), reader TOC/related memory, and support rail (community/sponsor/legacy/bug report); public demo updated to V5. |
401
418
  | **v0.10.54** | 2026-10-20 | **More reliable distillation with local models (#48), plus a refreshed README with new screenshots**. ① **#48**: a local 30B model takes 40-60s to cold start while the distill timeout was **hardcoded to 45s**, so every cold start aborted; the timeout is now configurable via **captureTimeoutMs** (still 45s by default, 120000+ recommended for local inference) and **a timeout retries the same candidate once with a longer budget** instead of switching models. ② **The second shape of #48**: when the provider does not report max-tokens, a truncated answer only shows up as unterminated JSON, so a dedicated **TRUNCATED_OUTPUT** code now joins MAX_TOKENS on the "double the output budget and retry the same candidate" ladder. ③ The settings page gains a distill timeout (ms) field. ④ **README refresh**: all real screenshots (cards, a graph with 897 nodes / 4257 edges, health overview, audit centre, settings) plus a "what problem does it solve" comparison table and a clearer value proposition and star call-to-action (both languages). npm test now runs **440 checks** |
402
419
  | **v0.10.53** | 2026-10-20 | **Health panel (reliability line): all previously invisible state in one place**. A new "Health" card at the top of the usage page shows: ① **database** (journal mode, writability, last health check, with doctor guidance when bad); ② **backups** (count and age of the newest, amber past 7 days, red at zero); ③ **auto capture** (last success/failure with reason); ④ **versions** (disk / resident / host, plus a Start check button because align needs the npm registry and cannot run automatically). Data comes from existing endpoints: /overview now includes a **backups summary** (count/newestAt/last five filenames) alongside /capture-log and /align. Also **fixes a white-screen bug**: the card initially used tf(), which only exists in the ConfigPanel scope, so opening the usage page blanked the whole app; the panel checker gained two gates — **page alive (textLen<20 counts as blank)** and **Runtime.exceptionThrown capture** — because a blank page previously passed the overflow assertions. Adds tests/health-panel.test.mjs (4 checks). npm test now runs **437 checks** |
403
420
  | **v0.10.52** | 2026-10-19 | **Redaction hole in the diagnostic bundle (privacy fix; upgrade recommended)**. The 0.10.51 one-command bundle had a gap: the home directory appears in **JSON-escaped form** (C:\\Users\\x) inside doctor/align --json output and meta.json, while only the plain and forward-slash variants were replaced, so the bundle could still be grepped for the home path. All three variants (plain, forward slash, JSON escaped) now become <home>, with a test assert added for the escaped form; a freshly generated bundle on this machine verifies as **no home path and no sk- keys**. If you already generated and shared a bundle with 0.10.51, please regenerate it before sharing. npm test now runs **433 checks** |
package/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  <p align="center">
21
21
  <img src="https://img.shields.io/badge/DeepSeek%20Harness-plugin-3B82F6" alt="DSH plugin" />
22
22
  <img src="https://img.shields.io/npm/v/memory-eternal" alt="npm version" />
23
- <img src="https://img.shields.io/badge/release-v0.11.0-FF7A45" alt="v0.11.0" />
23
+ <img src="https://img.shields.io/badge/release-v0.12.0-FF7A45" alt="v0.12.0" />
24
24
  <a href="https://eternalnight996.github.io/memory-eternal/"><img src="https://img.shields.io/badge/Live%20Demo-online-FF7A45" alt="Live demo" /></a>
25
25
  <img src="https://img.shields.io/github/stars/EternalNight996/memory-eternal?style=flat" alt="GitHub stars" />
26
26
  <img src="https://img.shields.io/github/license/EternalNight996/memory-eternal" alt="license" />
@@ -48,6 +48,22 @@
48
48
 
49
49
  ---
50
50
 
51
+ ## 🧩 支持平台
52
+
53
+ | 平台 / 宿主 | Memory Atlas UI | 待审 / 批驳 | 知识星图 | 入口与状态 |
54
+ |---|---|---|---|---|
55
+ | **DSH 桌面版 / Web** | 完整内嵌 | ✅ | ✅ | 左栏「记忆」,已支持 |
56
+ | **Orca** | 侧栏启动器 | Web 内完成 | Web 内完成 | 右栏「记忆」,[本版新增](orca-plugin/README.md) |
57
+ | **dsh-tui** | 浏览器打开 | Web 内完成 | Web 内完成 | `/memory`,[本版新增](dsh-tui-plugin/README.md) |
58
+ | **Codex** | 浏览器打开 | Web 内完成 | Web 内完成 | [`memory-open` skill](skills/memory-open/SKILL.md),本版新增 |
59
+ | **Claude Code** | 浏览器打开 / Pane | Web 内完成 | Web 内完成 | `/memory-eternal:memory-open`,本版新增 |
60
+ | **Cursor** | Webview 内嵌 | ✅ | ✅ | [扩展 Webview](cursor-extension/README.md),本版新增 |
61
+ | **纯 Web** | 主实现 | ✅ | ✅ | `http://127.0.0.1:7999/` |
62
+
63
+ > 原则:**一套 Web UI 作为唯一真源**;能内嵌的宿主用 Webview,不能内嵌的宿主只做轻量启动器,不复制第二套 UI。所有入口打开同一版本,避免 DSH / Web 版本漂移。
64
+
65
+ ---
66
+
51
67
  ## ✨ 它到底解决什么问题
52
68
 
53
69
  | 你遇到的 | 以前的做法 | 用了之后 |
@@ -81,7 +97,7 @@ cd ~/.dsh/profiles/web && pnpm add memory-eternal@latest
81
97
 
82
98
  # 刚发新版时 @latest 可能被 pnpm 的最小发布年龄门槛/元数据缓存挡住(静默停在旧版):
83
99
  # 直接指定版本号最快,pnpm 会自动把它加进 workspace 的 minimumReleaseAgeExclude
84
- cd ~/.dsh/profiles/web && pnpm add memory-eternal@0.11.0
100
+ cd ~/.dsh/profiles/web && pnpm add memory-eternal@0.12.0
85
101
 
86
102
  # 验版本(应等于上面装的号),然后重启对应宿主生效
87
103
  node -e "console.log(require('memory-eternal/package.json').version)"
@@ -422,14 +438,15 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
422
438
 
423
439
  | 版本 | 日期 | 关键改动 |
424
440
  |---|---|---|
441
+ | **v0.12.0** | 2026-10-22 | **跨宿主入口**:Orca 右侧栏「记忆」启动器、dsh-tui `/memory` 命令、Codex / Claude `memory-open` skill、Cursor / VS Code Webview 扩展;README 新增平台支持表;坚持一套 Web UI 唯一真源,能内嵌的用 Webview,不能内嵌的只做启动器;不新增 agent 权限。 |
425
442
  | **v0.11.0** | 2026-10-22 | **Memory Atlas 正式版**:V5 UI 全量落地 —— 记忆库 4 列知识卡 + 今日回看 + 固定/连接微缩图;待审顶部类型条 + 队列 + 右侧判词栏(无底部栏、无弹窗);知识星图 V5 页头/连接簇图例 + DPR/网格/边渲染优化;运行状态四张状态卡;读卡器右侧目录/相关记忆;金乌品牌图标;二维码点击放大;首次引导先 Star/Fork;GitHub/Gitee/npm 三端同步。 |
426
- | **v0.11.0-rc.1** | 2026-10-21 | **V5 预览 RC**:Memory Atlas 深色余烬视觉、228px 侧栏与顶栏;记忆库 V5 卡片网格 + 页头;审核顶部类型条 + 队列 + 判词预览 + 全选批量批准/驳回;星图默认缩小;首次引导(重点 👍)、读卡器目录/相关记忆与左栏交流群/赞助/旧程序/异常反馈;公开 Demo 同步 V5。 |
427
443
 
428
444
  <details>
429
445
  <summary>更早的更新日志(v0.10.52 及以前 · 共 79 版,点开查看)</summary>
430
446
 
431
447
  | 版本 | 日期 | 关键改动 |
432
448
  |---|---|---|
449
+ | **v0.11.0-rc.1** | 2026-10-21 | **V5 预览 RC**:Memory Atlas 深色余烬视觉、228px 侧栏与顶栏;记忆库 V5 卡片网格 + 页头;审核顶部类型条 + 队列 + 判词预览 + 全选批量批准/驳回;星图默认缩小;首次引导(重点 👍)、读卡器目录/相关记忆与左栏交流群/赞助/旧程序/异常反馈;公开 Demo 同步 V5。 |
433
450
  | **v0.10.54** | 2026-10-20 | **本地大模型蒸馏更稳(#48)+ README 焕新与最新截图**。① **#48**:本地 30B 模型冷启动要 40–60 秒,而蒸馏超时**硬编码 45 秒** → 每次冷启动必报 ABORTED;现在超时可配 **captureTimeoutMs**(默认仍 45s,本地推理建议 120000+),且**超时后自动用更长超时重试同一候选一次**(不直接换模型)。② **#48 的第二种形态**:模型不给 max-tokens finish 时,截断只表现为「JSON 未闭合」→ 新增独立错误码 **TRUNCATED_OUTPUT**,与 MAX_TOKENS 一起走「加倍输出预算重试同一候选」的阶梯。③ 配置页新增「蒸馏超时(毫秒)」输入项。④ **README 焕新**:全部换成真实运行截图(知识卡 / 知识图谱 897 节点 4257 连线 / 健康概览 / 审核中心 / 配置页)+ 新增「它到底解决什么问题」对照表 + 更清晰的卖点与 Star 引导(中英同步)。npm test 共 **440 项** |
434
451
  | **v0.10.53** | 2026-10-20 | **健康面板(可靠性线):把看不见的状态集中一处**。在「用量/今日」顶部新增「🩺 健康概览」:① **知识库**(journal 模式 / 可写 / 最后一次体检时间,异常时给 doctor 指引);② **备份**(数量 + 最近一次多久前,>7 天标黄、0 备份标红);③ **自动沉淀**(最近一次成功/失败与原因);④ **版本**(磁盘 / 常驻 / 宿主;未检查时给「开始检查」按钮,因为 align 要联网比 npm 不能自动跑)。数据来自既有接口:/overview 新增 **backups 摘要**(count/newestAt/最近 5 个文件名),加上 /capture-log 与 /align。**顺带修一个白屏 bug**:健康卡最初误用了 ConfigPanel 作用域的 tf() → 点开「用量/今日」整页白屏;同时给 panel-check 补了两道闸 —— **页面存活(textLen<20 判空白页)** 与 **Runtime.exceptionThrown 捕获**,因为「白屏没有横向溢出」原本会让检查通过。新增 tests/health-panel.test.mjs(4 项)。npm test 共 **437 项** |
435
452
  | **v0.10.52** | 2026-10-19 | **诊断包脱敏补漏(隐私修复,建议尽快升级)**。0.10.51 的「一键诊断包」有个漏点:doctor/align --json 与 meta.json 里的家目录是 **JSON 转义形态**(C:\\Users\\x),而我只替换了原样/正斜杠两种写法 → 包里仍能 grep 到家目录明文。现在三种写法(原样、正斜杠、JSON 转义)都替换成 <home>,并在测试里加了一条「JSON 转义形态不能漏」的断言;本机重新生成诊断包实测:**无家目录明文、无 sk- 密钥**。**如果你已经用 0.10.51 生成过诊断包并分享出去,建议重新生成一次再分享。** npm test 共 **433 项** |
@@ -0,0 +1,41 @@
1
+ # Cursor / VS Code extension — Memory Atlas
2
+
3
+ 在 Cursor / VS Code 左侧 Activity Bar 添加「Memory Atlas」,用 Webview 加载同一套 Memory Atlas Web UI。
4
+
5
+ ## 安装
6
+
7
+ 1. 安装依赖并打包扩展:
8
+
9
+ ```bash
10
+ cd cursor-extension
11
+ npx @vscode/vsce package
12
+ ```
13
+
14
+ 2. Cursor / VS Code:
15
+
16
+ ```text
17
+ Extensions → ... → Install from VSIX
18
+ ```
19
+
20
+ 或开发模式:
21
+
22
+ ```bash
23
+ code --extensionDevelopmentPath=.
24
+ ```
25
+
26
+ ## 使用
27
+
28
+ - 左侧 Activity Bar 出现 Memory Atlas 图标
29
+ - 面板内 iframe 加载 `http://127.0.0.1:7999/?embed=1`
30
+ - 命令面板:`Memory Atlas: Open`
31
+
32
+ ## 前提
33
+
34
+ - `memory-eternal` 的本地 Web 服务在 `127.0.0.1:7999` 运行
35
+ - 启动命令:`dsh-memory open`
36
+
37
+ ## 说明
38
+
39
+ - 这是真正的 Webview 内嵌,不是外部浏览器跳转
40
+ - 与 DSH/Web 共用同一份 `web/app.js`,版本一致
41
+ - 不新增 agent 权限;审批仍在 Web UI 由人完成
@@ -0,0 +1,58 @@
1
+ 'use strict'
2
+
3
+ const vscode = require('vscode')
4
+
5
+ const URL = 'http://127.0.0.1:7999/?embed=1'
6
+
7
+ function frameHtml(webview) {
8
+ const csp = [
9
+ "default-src 'none'",
10
+ 'frame-src http://127.0.0.1:7999',
11
+ `style-src ${webview.cspSource} 'unsafe-inline'`,
12
+ `font-src ${webview.cspSource}`,
13
+ ].join('; ')
14
+ return `<!doctype html>
15
+ <html lang="zh-CN">
16
+ <head>
17
+ <meta charset="utf-8">
18
+ <meta http-equiv="Content-Security-Policy" content="${csp}">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1">
20
+ <style>
21
+ html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #1e2228; }
22
+ iframe { width: 100%; height: 100%; border: 0; display: block; background: #1e2228; }
23
+ </style>
24
+ </head>
25
+ <body><iframe src="${URL}" title="Memory Atlas"></iframe></body>
26
+ </html>`
27
+ }
28
+
29
+ class MemoryAtlasViewProvider {
30
+ resolveWebviewView(view) {
31
+ view.webview.options = { enableScripts: true }
32
+ view.webview.html = frameHtml(view.webview)
33
+ }
34
+ }
35
+
36
+ function activate(context) {
37
+ context.subscriptions.push(
38
+ vscode.window.registerWebviewViewProvider('memory-eternal.view', new MemoryAtlasViewProvider(), {
39
+ webviewOptions: { retainContextWhenHidden: true },
40
+ }),
41
+ )
42
+
43
+ context.subscriptions.push(
44
+ vscode.commands.registerCommand('memory-eternal.open', () => {
45
+ const panel = vscode.window.createWebviewPanel(
46
+ 'memory-eternal.panel',
47
+ 'Memory Atlas',
48
+ vscode.ViewColumn.One,
49
+ { enableScripts: true, retainContextWhenHidden: true },
50
+ )
51
+ panel.webview.html = frameHtml(panel.webview)
52
+ }),
53
+ )
54
+ }
55
+
56
+ function deactivate() {}
57
+
58
+ module.exports = { activate, deactivate }
Binary file
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "memory-eternal",
3
+ "displayName": "Memory Atlas",
4
+ "description": "Memory-eternal: open the local Memory Atlas library, review queue and knowledge graph in a Cursor/VS Code webview.",
5
+ "version": "0.12.0",
6
+ "publisher": "EternalNight996",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/EternalNight996/memory-eternal"
11
+ },
12
+ "engines": {
13
+ "vscode": "^1.80.0"
14
+ },
15
+ "categories": [
16
+ "Other"
17
+ ],
18
+ "activationEvents": [
19
+ "onView:memory-eternal.view",
20
+ "onCommand:memory-eternal.open"
21
+ ],
22
+ "main": "./extension.js",
23
+ "contributes": {
24
+ "viewsContainers": {
25
+ "activitybar": [
26
+ {
27
+ "id": "memory-eternal",
28
+ "title": "Memory Atlas",
29
+ "icon": "icon.png"
30
+ }
31
+ ]
32
+ },
33
+ "views": {
34
+ "memory-eternal": [
35
+ {
36
+ "id": "memory-eternal.view",
37
+ "name": "记忆",
38
+ "type": "webview"
39
+ }
40
+ ]
41
+ },
42
+ "commands": [
43
+ {
44
+ "command": "memory-eternal.open",
45
+ "title": "Memory Atlas: Open"
46
+ }
47
+ ]
48
+ },
49
+ "files": [
50
+ "extension.js",
51
+ "icon.png",
52
+ "README.md"
53
+ ]
54
+ }
@@ -0,0 +1,48 @@
1
+ # dsh-TUI plugin — `/memory`
2
+
3
+ 在 dsh-TUI 里增加一个用户命令:
4
+
5
+ ```text
6
+ /memory
7
+ ```
8
+
9
+ 执行后确保 `memory-eternal` 的本地 Web 服务存活,并打开同一套 Memory Atlas Web UI。
10
+
11
+ ## 安装
12
+
13
+ 在 `memory-eternal` 仓库根目录:
14
+
15
+ ```bash
16
+ # 本地 profile(推荐先用这个验证)
17
+ dsh plugin --profile dsh-tui add ./dsh-tui-plugin
18
+ ```
19
+
20
+ 或手动:
21
+
22
+ 1. 把本目录复制到 dsh-TUI profile 的 node_modules;
23
+ 2. 在 profile 的 `cordis.patch.yml` 中加入:
24
+
25
+ ```yaml
26
+ - insert:
27
+ - id: memory-eternal-tui
28
+ name: 'memory-eternal-tui'
29
+ ```
30
+
31
+ 3. 重启 dsh-TUI。
32
+
33
+ ## 使用
34
+
35
+ ```text
36
+ /memory
37
+ ```
38
+
39
+ - 优先执行 `dsh-memory open`(确保 7999 存活 + 打开浏览器)
40
+ - 找不到 CLI 时直接打开 `http://127.0.0.1:7999/`
41
+ - 不会在 TUI 内嵌 WebView,也不复制第二套 UI
42
+ - 审批仍在 Web 里由人完成,不新增 agent 工具
43
+
44
+ ## 要求
45
+
46
+ - `memory-eternal` 已安装在当前机器
47
+ - 终端能执行 `dsh-memory`(否则走 URL 兜底)
48
+ - dsh-TUI >= 0.9.0(commands seam)
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: memory-eternal-tui
3
+ name: 'memory-eternal-tui'
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "urn:dsh-std:community-draft:dsh-plugin:0.15",
3
+ "id": "memory-eternal-tui",
4
+ "name": "memory-eternal-tui",
5
+ "version": "0.12.0",
6
+ "manifestVersion": "0.15",
7
+ "description": "Open the local Memory Atlas Web UI from dsh-TUI.",
8
+ "facets": {
9
+ "host": {
10
+ "entry": "main.js",
11
+ "apiVersion": "v1alpha1"
12
+ }
13
+ },
14
+ "requires": {
15
+ "contracts": [
16
+ {
17
+ "apiVersion": "commands.dsh/v1alpha1",
18
+ "kind": "Command"
19
+ }
20
+ ]
21
+ },
22
+ "permissions": [],
23
+ "contributes": {
24
+ "commands": [
25
+ {
26
+ "id": "memory-eternal-tui.open",
27
+ "title": "memory",
28
+ "description": "Open Memory Atlas in the browser"
29
+ }
30
+ ]
31
+ },
32
+ "subscriptions": [],
33
+ "license": "MIT",
34
+ "source": {
35
+ "repository": "https://github.com/EternalNight996/memory-eternal"
36
+ }
37
+ }
@@ -0,0 +1,54 @@
1
+ // memory-eternal-tui — dsh-TUI 的官方命令入口。
2
+ //
3
+ // 这里不做第二套 TUI UI:命令只负责把用户送到同一套 Memory Atlas Web UI。
4
+ // 审批仍然在 Web 里由人完成,不新增 agent 工具,也不触碰审核守卫。
5
+ import { spawn } from 'node:child_process'
6
+
7
+ export const name = 'memory-eternal-tui'
8
+ export const inject = ['agents']
9
+
10
+ const URL = 'http://127.0.0.1:7999/'
11
+ const COMMAND_ID = 'memory-eternal-tui.open'
12
+
13
+ function openUrl() {
14
+ const cmd = process.platform === 'win32' ? 'rundll32' : process.platform === 'darwin' ? 'open' : 'xdg-open'
15
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', URL] : [URL]
16
+ try {
17
+ const child = spawn(cmd, args, { detached: true, stdio: 'ignore', windowsHide: true })
18
+ child.on('error', () => {})
19
+ child.unref()
20
+ } catch { /* 平台没有 opener 时由调用方提示手动访问 */ }
21
+ }
22
+
23
+ function openMemory() {
24
+ // 优先让 CLI 确保常驻 Web 已启动并打开浏览器;命令不存在时直接打开 URL。
25
+ try {
26
+ const child = spawn('dsh-memory', ['open'], {
27
+ detached: true,
28
+ stdio: 'ignore',
29
+ shell: true,
30
+ windowsHide: true,
31
+ })
32
+ child.on('error', () => openUrl())
33
+ child.on('exit', (code) => { if (code !== 0) openUrl() })
34
+ child.unref()
35
+ } catch {
36
+ openUrl()
37
+ }
38
+ }
39
+
40
+ export function apply(ctx) {
41
+ const host = ctx.get('tuiPluginHost', false)
42
+ if (!host) return
43
+
44
+ const dispose = host.registerCommand(ctx, COMMAND_ID, {
45
+ recordInput: false,
46
+ handler: () => {
47
+ openMemory()
48
+ return { kind: 'success', text: `Memory Atlas 已打开:${URL}` }
49
+ },
50
+ })
51
+ ctx.effect(() => dispose)
52
+ }
53
+
54
+ export default { name, inject, apply }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "memory-eternal-tui",
3
+ "version": "0.12.0",
4
+ "description": "Open Memory Atlas from dsh-TUI with /memory.",
5
+ "type": "module",
6
+ "main": "main.js",
7
+ "files": [
8
+ "main.js",
9
+ "dsh-plugin.json",
10
+ "cordis.patch.yml",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "dsh",
15
+ "dsh-tui",
16
+ "memory-eternal",
17
+ "memory",
18
+ "plugin"
19
+ ],
20
+ "license": "MIT",
21
+ "dsh": {
22
+ "bundle": {
23
+ "patch": "./cordis.patch.yml"
24
+ }
25
+ },
26
+ "peerDependencies": {
27
+ "@deepseek-ai/cordis": "^4.0.1"
28
+ }
29
+ }
package/lib/client.js CHANGED
@@ -701,7 +701,7 @@ Option A, because:
701
701
  .mc-audit-batchcard > .mc-btn { width: 100%; }
702
702
  .mc-audit-batchcard > div[style*="flex: 1"] { display: none; }
703
703
  }
704
- `;function Ea(t){t.effect(()=>t.locale.register(kt,{zh:da,en:ca}),"memory-eternal: locale");let a=t.locale.bind(kt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:kt,order:25,label:()=>a("nav"),locale:kt,inject:()=>({})},()=>o.default.createElement(Wa,{t:a}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${kt}:footer`,order:100,label:()=>a("nav"),locale:kt,inject:()=>({})},i=>o.default.createElement($a,{t:a,locale:t.locale,wide:!(i&&i.wide===!1)}))),"memory-eternal: sidebar footer action")}function Oa(){let[t,a]=(0,o.useState)($+"/ui/app");return(0,o.useEffect)(()=>{let i=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{i&&c&&c.ok&&c.alive!==!1&&c.url&&a(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch($+"/ui/app",{method:"HEAD"}).then(c=>{if(i&&!c.ok)return g()}).catch(()=>i?g():void 0),()=>{i=!1}},[]),t}function Wa({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsx)(ua,{t,onReload:()=>{},version:0})]})}function $a({t,locale:a,wide:i}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${i?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>c(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(Ua,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(_a,{t,locale:a,onClose:()=>c(!1)})]})}function _a({t,locale:a,onClose:i,tab:g}){let[c,s]=(0,o.useState)(!1),h=(0,o.useRef)(null),k=Oa(),S=(0,o.useCallback)(N=>a&&typeof a.subscribe=="function"?a.subscribe(N):()=>{},[a]),L=(0,o.useCallback)(()=>a&&typeof a.getSnapshot=="function"?a.getSnapshot():null,[a]),M=(0,o.useSyncExternalStore)(S,L),x=M&&(M.active==="zh"||M.active==="en")?M.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let N=m.current?`lang=${m.current}`:"";if(g==="config")return`${$}/ui/config?tab=config${N?`&${N}`:""}`;let P=k.replace(/\/$/,""),T=P.endsWith("/ui/app")?"?":"/?";return g?`${P}${T}tab=${g}${N?`&${N}`:""}`:N?`${P}${T}${N}`:k},[k,g]);return(0,o.useEffect)(()=>{let N=h.current&&h.current.contentWindow;N&&x&&N.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let N=P=>{P.key==="Escape"&&i()};return window.addEventListener("keydown",N),()=>window.removeEventListener("keydown",N)},[i]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:i,children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:N=>N.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{style:{position:"absolute",top:8,right:8,zIndex:10,display:"flex",gap:4,padding:"4px 6px",background:"var(--dsw-alias-bg-overlay, rgba(17,24,39,0.45))",borderRadius:8,backdropFilter:"blur(4px)"},children:[(0,e.jsx)("button",{type:"button",onClick:()=>s(N=>!N),"aria-label":t(c?"exitFullscreen":"enterFullscreen"),title:t(c?"exitFullscreen":"enterFullscreen"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:14,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:c?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:i,"aria-label":t("close"),title:t("close"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:16,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:"\xD7"})]}),(0,e.jsx)("iframe",{ref:h,src:z,title:"memory-eternal",allow:"clipboard-write; clipboard-read",style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"var(--dsw-alias-bg-base, #fff)"}})]})})]})}function Ua(){return(0,e.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,e.jsx)("defs",{children:(0,e.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,e.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,e.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,e.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,e.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,e.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var St="0.11.0";function Ga({t,inModal:a,onClose:i}){let g=typeof window<"u"&&(!!window.__DSH_BOOT__||String(location.pathname||"").includes("/memory-eternal/api/ui/app")),[c,s]=(0,o.useState)(null),[h,k]=(0,o.useState)([]),[S,L]=(0,o.useState)(!0),[M,x]=(0,o.useState)(null),[m,z]=(0,o.useState)(""),[N,P]=(0,o.useState)("all"),[T,E]=(0,o.useState)(""),[K,se]=(0,o.useState)("all"),[V,Z]=(0,o.useState)("all"),[te,G]=(0,o.useState)(()=>{try{return new Set(JSON.parse(localStorage.getItem("me-pinned-v5")||"[]"))}catch{return new Set}}),[fe,ue]=(0,o.useState)(!1),_=(0,o.useCallback)(b=>{G(r=>{let v=new Set(r);v.has(b)?v.delete(b):v.add(b);try{localStorage.setItem("me-pinned-v5",JSON.stringify([...v]))}catch{}return v})},[]),[Q,je]=(0,o.useState)(""),[O,Be]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),r={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&r[b])return r[b]}catch{}return"cards"}),[f,F]=(0,o.useState)(()=>new Set([O])),W=(0,o.useCallback)(b=>{F(r=>r.has(b)?r:new Set(r).add(b)),Be(b)},[]),[B,Ce]=(0,o.useState)("recent"),[ze,pe]=(0,o.useState)(null),ve=(0,o.useRef)(null),y=(0,o.useRef)(null),[ae,X]=(0,o.useState)(!1),[Ne,ne]=(0,o.useState)(null),[xe,Ge]=(0,o.useState)(!1),[p,D]=(0,o.useState)(!1),[A,Y]=(0,o.useState)(!1),[Se,le]=(0,o.useState)("group"),[H,we]=(0,o.useState)(!1),be=(0,o.useCallback)(()=>{we(!1);try{localStorage.setItem("me-onboard-v2","1")}catch{}},[]),u=(0,o.useCallback)((b="group")=>{le(b),Y(!0)},[]);(0,o.useEffect)(()=>{try{if(localStorage.getItem("me-onboard-v2")||O!=="cards")return;let b=setTimeout(()=>we(!0),900);return()=>clearTimeout(b)}catch{}},[O]);let[d,J]=(0,o.useState)(!0),[he,Me]=(0,o.useState)(null),[Te,n]=(0,o.useState)(0),ot=(0,o.useCallback)(()=>n(b=>b+1),[]),ct=c?.quarantine?.pending??0,Ke=c?.quarantine?.deleted??0,Pe=(0,o.useRef)(null),[Xe,bt]=(0,o.useState)(0),[it,Re]=(0,o.useState)(!1),et=(0,o.useRef)(null),Fe=(0,o.useRef)(null),l=(0,o.useCallback)(async(b=N,r=T,v=V,C={})=>{let R=C.append===!0;try{R?Re(!0):(L(!0),z(""));let j=new URLSearchParams;b&&b!=="all"&&j.set("kind",b),r.trim()&&j.set("q",r.trim());let ee=C.tag!==void 0?C.tag:Q;ee&&j.set("tag",ee),j.set("status",v||"all"),j.set("limit",String(ja)),j.set("offset",String(C.offset||0)),B==="title"&&j.set("sort","title");let ge=await(await fetch(`${$}/cards?${j.toString()}`)).json();if(ge.ok){let Ae=ge.cards||[];k(De=>R?[...De,...Ae.filter(ye=>!De.some(ke=>ke.path===ye.path))]:Ae),bt(Number(ge.total)||Ae.length)}}catch(j){z(String(j&&j.message?j.message:j))}finally{R?Re(!1):L(!1)}},[N,T,V,B,Q]),I=(0,o.useCallback)(async()=>{try{let[b,r]=await Promise.all([fetch(`${$}/overview`).then(v=>v.json()),fetch(`${$}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&s(b),x(r&&r.ok&&r.available&&r.health||null)}catch(b){z(String(b&&b.message?b.message:b))}finally{L(!1)}},[]),U=async b=>{try{let v=await(await fetch(`${$}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(pe({ok:!0,msg:t("deleted")}),await I(),ot()):pe({ok:!1,msg:v.error||t("deleteFail")})}catch{pe({ok:!1,msg:t("deleteFail")})}ve.current&&clearTimeout(ve.current),ve.current=setTimeout(()=>pe(null),2e3)};(0,o.useEffect)(()=>(I(),()=>{Pe.current&&clearTimeout(Pe.current)}),[I]);let oe=b=>{E(b),Pe.current&&clearTimeout(Pe.current),Pe.current=setTimeout(()=>l(N,b,V),280)},ie=b=>{P(b),l(b,T,V)},q=b=>{let r=String(b||"");je(r),ne(null),l(N,T,V,{tag:r})};(0,o.useEffect)(()=>{O==="cards"&&l(N,T,V)},[V,O,Te,B,Q]),(0,o.useEffect)(()=>{if(O!=="cards"||S||it||h.length>=Xe)return;let b=et.current,r=Fe.current;if(!b)return;let v=new IntersectionObserver(C=>{C[0]&&C[0].isIntersecting&&l(N,T,V,{append:!0,offset:h.length})},{root:r||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[O,h,Xe,S,it,N,T,V,l]);let re=async b=>{try{let r=b.status;if(!r&&b.path)try{let ee=((await fetch(`${$}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(de=>de.json())).cards||[]).find(de=>de.path===b.path);ee&&(r=ee.status)}catch{}let v=r&&r!=="approved"?`&status=${encodeURIComponent(r)}`:"",C=await fetch(`${$}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),R=await C.json();R.ok?ne({path:R.path,title:b.title,text:R.text}):z(R.error||`HTTP ${C.status}`)}catch(r){z(String(r&&r.message?r.message:r))}},He=async(b,r)=>{X(!0);try{let v=await fetch(`${$}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let C=await v.json();if(!C.ok)throw new Error(C.error||"export failed");let R,j,ee;if(b==="json"){j="memory-vault.json",ee="application/json";let Ae=C.cards.map(De=>({path:De.path,title:De.title,kind:De.kind,status:De.status,store:De.store,text:De.text}));R=JSON.stringify({format:C.format||"memory-eternal-vault",formatVersion:C.formatVersion||1,exportedAt:C.exportedAt||new Date().toISOString(),count:Ae.length,cards:Ae},null,2)}else j="memory-vault.md",ee="text/markdown;charset=utf-8",R=C.cards.map(Ae=>Ae.text.trim()).filter(Boolean).join(`
704
+ `;function Ea(t){t.effect(()=>t.locale.register(kt,{zh:da,en:ca}),"memory-eternal: locale");let a=t.locale.bind(kt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:kt,order:25,label:()=>a("nav"),locale:kt,inject:()=>({})},()=>o.default.createElement(Wa,{t:a}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${kt}:footer`,order:100,label:()=>a("nav"),locale:kt,inject:()=>({})},i=>o.default.createElement($a,{t:a,locale:t.locale,wide:!(i&&i.wide===!1)}))),"memory-eternal: sidebar footer action")}function Oa(){let[t,a]=(0,o.useState)($+"/ui/app");return(0,o.useEffect)(()=>{let i=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{i&&c&&c.ok&&c.alive!==!1&&c.url&&a(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch($+"/ui/app",{method:"HEAD"}).then(c=>{if(i&&!c.ok)return g()}).catch(()=>i?g():void 0),()=>{i=!1}},[]),t}function Wa({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsx)(ua,{t,onReload:()=>{},version:0})]})}function $a({t,locale:a,wide:i}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${i?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>c(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(Ua,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(_a,{t,locale:a,onClose:()=>c(!1)})]})}function _a({t,locale:a,onClose:i,tab:g}){let[c,s]=(0,o.useState)(!1),h=(0,o.useRef)(null),k=Oa(),S=(0,o.useCallback)(N=>a&&typeof a.subscribe=="function"?a.subscribe(N):()=>{},[a]),L=(0,o.useCallback)(()=>a&&typeof a.getSnapshot=="function"?a.getSnapshot():null,[a]),M=(0,o.useSyncExternalStore)(S,L),x=M&&(M.active==="zh"||M.active==="en")?M.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let N=m.current?`lang=${m.current}`:"";if(g==="config")return`${$}/ui/config?tab=config${N?`&${N}`:""}`;let P=k.replace(/\/$/,""),T=P.endsWith("/ui/app")?"?":"/?";return g?`${P}${T}tab=${g}${N?`&${N}`:""}`:N?`${P}${T}${N}`:k},[k,g]);return(0,o.useEffect)(()=>{let N=h.current&&h.current.contentWindow;N&&x&&N.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let N=P=>{P.key==="Escape"&&i()};return window.addEventListener("keydown",N),()=>window.removeEventListener("keydown",N)},[i]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:i,children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:N=>N.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{style:{position:"absolute",top:8,right:8,zIndex:10,display:"flex",gap:4,padding:"4px 6px",background:"var(--dsw-alias-bg-overlay, rgba(17,24,39,0.45))",borderRadius:8,backdropFilter:"blur(4px)"},children:[(0,e.jsx)("button",{type:"button",onClick:()=>s(N=>!N),"aria-label":t(c?"exitFullscreen":"enterFullscreen"),title:t(c?"exitFullscreen":"enterFullscreen"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:14,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:c?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:i,"aria-label":t("close"),title:t("close"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:16,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:"\xD7"})]}),(0,e.jsx)("iframe",{ref:h,src:z,title:"memory-eternal",allow:"clipboard-write; clipboard-read",style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"var(--dsw-alias-bg-base, #fff)"}})]})})]})}function Ua(){return(0,e.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,e.jsx)("defs",{children:(0,e.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,e.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,e.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,e.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,e.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,e.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var St="0.12.0";function Ga({t,inModal:a,onClose:i}){let g=typeof window<"u"&&(!!window.__DSH_BOOT__||String(location.pathname||"").includes("/memory-eternal/api/ui/app")),[c,s]=(0,o.useState)(null),[h,k]=(0,o.useState)([]),[S,L]=(0,o.useState)(!0),[M,x]=(0,o.useState)(null),[m,z]=(0,o.useState)(""),[N,P]=(0,o.useState)("all"),[T,E]=(0,o.useState)(""),[K,se]=(0,o.useState)("all"),[V,Z]=(0,o.useState)("all"),[te,G]=(0,o.useState)(()=>{try{return new Set(JSON.parse(localStorage.getItem("me-pinned-v5")||"[]"))}catch{return new Set}}),[fe,ue]=(0,o.useState)(!1),_=(0,o.useCallback)(b=>{G(r=>{let v=new Set(r);v.has(b)?v.delete(b):v.add(b);try{localStorage.setItem("me-pinned-v5",JSON.stringify([...v]))}catch{}return v})},[]),[Q,je]=(0,o.useState)(""),[O,Be]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),r={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&r[b])return r[b]}catch{}return"cards"}),[f,F]=(0,o.useState)(()=>new Set([O])),W=(0,o.useCallback)(b=>{F(r=>r.has(b)?r:new Set(r).add(b)),Be(b)},[]),[B,Ce]=(0,o.useState)("recent"),[ze,pe]=(0,o.useState)(null),ve=(0,o.useRef)(null),y=(0,o.useRef)(null),[ae,X]=(0,o.useState)(!1),[Ne,ne]=(0,o.useState)(null),[xe,Ge]=(0,o.useState)(!1),[p,D]=(0,o.useState)(!1),[A,Y]=(0,o.useState)(!1),[Se,le]=(0,o.useState)("group"),[H,we]=(0,o.useState)(!1),be=(0,o.useCallback)(()=>{we(!1);try{localStorage.setItem("me-onboard-v2","1")}catch{}},[]),u=(0,o.useCallback)((b="group")=>{le(b),Y(!0)},[]);(0,o.useEffect)(()=>{try{if(localStorage.getItem("me-onboard-v2")||O!=="cards")return;let b=setTimeout(()=>we(!0),900);return()=>clearTimeout(b)}catch{}},[O]);let[d,J]=(0,o.useState)(!0),[he,Me]=(0,o.useState)(null),[Te,n]=(0,o.useState)(0),ot=(0,o.useCallback)(()=>n(b=>b+1),[]),ct=c?.quarantine?.pending??0,Ke=c?.quarantine?.deleted??0,Pe=(0,o.useRef)(null),[Xe,bt]=(0,o.useState)(0),[it,Re]=(0,o.useState)(!1),et=(0,o.useRef)(null),Fe=(0,o.useRef)(null),l=(0,o.useCallback)(async(b=N,r=T,v=V,C={})=>{let R=C.append===!0;try{R?Re(!0):(L(!0),z(""));let j=new URLSearchParams;b&&b!=="all"&&j.set("kind",b),r.trim()&&j.set("q",r.trim());let ee=C.tag!==void 0?C.tag:Q;ee&&j.set("tag",ee),j.set("status",v||"all"),j.set("limit",String(ja)),j.set("offset",String(C.offset||0)),B==="title"&&j.set("sort","title");let ge=await(await fetch(`${$}/cards?${j.toString()}`)).json();if(ge.ok){let Ae=ge.cards||[];k(De=>R?[...De,...Ae.filter(ye=>!De.some(ke=>ke.path===ye.path))]:Ae),bt(Number(ge.total)||Ae.length)}}catch(j){z(String(j&&j.message?j.message:j))}finally{R?Re(!1):L(!1)}},[N,T,V,B,Q]),I=(0,o.useCallback)(async()=>{try{let[b,r]=await Promise.all([fetch(`${$}/overview`).then(v=>v.json()),fetch(`${$}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&s(b),x(r&&r.ok&&r.available&&r.health||null)}catch(b){z(String(b&&b.message?b.message:b))}finally{L(!1)}},[]),U=async b=>{try{let v=await(await fetch(`${$}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(pe({ok:!0,msg:t("deleted")}),await I(),ot()):pe({ok:!1,msg:v.error||t("deleteFail")})}catch{pe({ok:!1,msg:t("deleteFail")})}ve.current&&clearTimeout(ve.current),ve.current=setTimeout(()=>pe(null),2e3)};(0,o.useEffect)(()=>(I(),()=>{Pe.current&&clearTimeout(Pe.current)}),[I]);let oe=b=>{E(b),Pe.current&&clearTimeout(Pe.current),Pe.current=setTimeout(()=>l(N,b,V),280)},ie=b=>{P(b),l(b,T,V)},q=b=>{let r=String(b||"");je(r),ne(null),l(N,T,V,{tag:r})};(0,o.useEffect)(()=>{O==="cards"&&l(N,T,V)},[V,O,Te,B,Q]),(0,o.useEffect)(()=>{if(O!=="cards"||S||it||h.length>=Xe)return;let b=et.current,r=Fe.current;if(!b)return;let v=new IntersectionObserver(C=>{C[0]&&C[0].isIntersecting&&l(N,T,V,{append:!0,offset:h.length})},{root:r||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[O,h,Xe,S,it,N,T,V,l]);let re=async b=>{try{let r=b.status;if(!r&&b.path)try{let ee=((await fetch(`${$}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(de=>de.json())).cards||[]).find(de=>de.path===b.path);ee&&(r=ee.status)}catch{}let v=r&&r!=="approved"?`&status=${encodeURIComponent(r)}`:"",C=await fetch(`${$}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),R=await C.json();R.ok?ne({path:R.path,title:b.title,text:R.text}):z(R.error||`HTTP ${C.status}`)}catch(r){z(String(r&&r.message?r.message:r))}},He=async(b,r)=>{X(!0);try{let v=await fetch(`${$}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let C=await v.json();if(!C.ok)throw new Error(C.error||"export failed");let R,j,ee;if(b==="json"){j="memory-vault.json",ee="application/json";let Ae=C.cards.map(De=>({path:De.path,title:De.title,kind:De.kind,status:De.status,store:De.store,text:De.text}));R=JSON.stringify({format:C.format||"memory-eternal-vault",formatVersion:C.formatVersion||1,exportedAt:C.exportedAt||new Date().toISOString(),count:Ae.length,cards:Ae},null,2)}else j="memory-vault.md",ee="text/markdown;charset=utf-8",R=C.cards.map(Ae=>Ae.text.trim()).filter(Boolean).join(`
705
705
 
706
706
  ---
707
707
 
@@ -0,0 +1,46 @@
1
+ # Orca plugin — Memory Atlas 启动器
2
+
3
+ 在 Orca 右侧栏添加一个「记忆」面板,一键在终端执行 `dsh-memory open`,打开本地 Memory Atlas Web UI。
4
+
5
+ ## 安装
6
+
7
+ 1. 确认本机已安装并运行 `memory-eternal`(默认 Web 服务 `http://127.0.0.1:7999/`)。
8
+ 2. 确认终端能找到 CLI:
9
+
10
+ ```bash
11
+ dsh-memory --help
12
+ ```
13
+
14
+ 3. 在 Orca 中安装本目录:
15
+
16
+ ```text
17
+ Orca → Plugins → Install plugin → 选择 orca-plugin/
18
+ ```
19
+
20
+ 4. 打开右侧栏的「记忆」图标,选择终端,点「打开 Memory Atlas」。
21
+
22
+ ## 它做什么
23
+
24
+ - 调 Orca 官方 `workspace.readContext` 读取当前工作区终端
25
+ - 调 Orca 官方 `terminal.sendText` 执行 `dsh-memory open`
26
+ - 不 iframe、不 fetch、不复制第二套 UI
27
+
28
+ ## 为什么不是完整内嵌 UI
29
+
30
+ Orca panel 使用严格沙箱 CSP:
31
+
32
+ ```text
33
+ default-src 'none'
34
+ connect-src 'none'
35
+ img-src data:
36
+ ```
37
+
38
+ 因此 panel 内不能 iframe `127.0.0.1:7999`、不能 fetch API、不能 `window.open`。
39
+ 完整 Memory Atlas 仍由 Web UI 提供;Orca 只做官方能力内的启动器。
40
+
41
+ ## 权限
42
+
43
+ - `workspace:read`:读取当前工作区终端列表
44
+ - `terminal:send`:向指定终端发送命令
45
+
46
+ 没有任何内存卡写入、审核批准或 agent 权限。
Binary file
@@ -0,0 +1,36 @@
1
+ {
2
+ "manifestVersion": 1,
3
+ "id": "memory-eternal",
4
+ "publisher": "eternalnight996",
5
+ "name": "memory-eternal",
6
+ "version": "0.12.0",
7
+ "description": "Memory Atlas for Orca: open the local memory vault, pending review and knowledge graph from a sandboxed Orca panel.",
8
+ "author": {
9
+ "name": "EternalNight996",
10
+ "url": "https://github.com/EternalNight996"
11
+ },
12
+ "repository": "https://github.com/EternalNight996/memory-eternal",
13
+ "icon": "assets/icon.png",
14
+ "engines": {
15
+ "orca": ">=1.4.0"
16
+ },
17
+ "pluginApi": 1,
18
+ "contributes": {
19
+ "panels": [
20
+ {
21
+ "id": "memory",
22
+ "title": "记忆",
23
+ "icon": "brain",
24
+ "entry": "panel.html"
25
+ }
26
+ ]
27
+ },
28
+ "capabilities": [
29
+ {
30
+ "kind": "workspace:read"
31
+ },
32
+ {
33
+ "kind": "terminal:send"
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,200 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Memory Atlas</title>
7
+ <style>
8
+ :root { color-scheme: dark; }
9
+ * { box-sizing: border-box; }
10
+ body {
11
+ margin: 0;
12
+ min-height: 100vh;
13
+ padding: 18px 16px 22px;
14
+ font-family: var(--font-sans, system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif);
15
+ font-size: 12.5px;
16
+ line-height: 1.6;
17
+ color: var(--foreground, #f3f5f8);
18
+ background:
19
+ radial-gradient(120% 80% at 100% 0%, rgba(255,122,69,.10), transparent 55%),
20
+ var(--background, #1e2228);
21
+ }
22
+ .brand { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
23
+ .mark {
24
+ width: 38px; height: 38px; flex: none;
25
+ display: flex; align-items: center; justify-content: center;
26
+ border-radius: 11px; font-size: 21px;
27
+ background: rgba(255,122,69,.10);
28
+ border: 1px solid rgba(255,122,69,.24);
29
+ }
30
+ .brand h1 { margin: 0; font-size: 16px; line-height: 1.2; font-weight: 650; }
31
+ .brand p { margin: 2px 0 0; font-size: 10px; letter-spacing: .12em; color: var(--muted-foreground, #99a1ac); }
32
+ .card {
33
+ padding: 14px;
34
+ border: 1px solid var(--border, #3a404a);
35
+ border-radius: 12px;
36
+ background: var(--card, rgba(38,43,51,.92));
37
+ }
38
+ .card h2 { margin: 0 0 5px; font-size: 12.5px; font-weight: 600; }
39
+ .card p { margin: 0; color: var(--muted-foreground, #b0b7c1); font-size: 11px; }
40
+ .row { display: flex; flex-direction: column; gap: 5px; margin-top: 13px; }
41
+ .row label { font-size: 10px; color: var(--muted-foreground, #99a1ac); }
42
+ select {
43
+ width: 100%;
44
+ padding: 7px 8px;
45
+ border-radius: 7px;
46
+ border: 1px solid var(--border, #3a404a);
47
+ background: var(--secondary, #2a2f37);
48
+ color: var(--foreground, #f3f5f8);
49
+ font: inherit;
50
+ font-size: 11px;
51
+ }
52
+ button {
53
+ width: 100%;
54
+ margin-top: 13px;
55
+ padding: 9px 10px;
56
+ border: 1px solid transparent;
57
+ border-radius: 8px;
58
+ background: var(--primary, #ff7a45);
59
+ color: var(--primary-foreground, #1b1f25);
60
+ font: inherit;
61
+ font-size: 12px;
62
+ font-weight: 650;
63
+ cursor: pointer;
64
+ }
65
+ button:hover { filter: brightness(1.06); }
66
+ button:disabled { opacity: .45; cursor: default; }
67
+ .status { min-height: 1.6em; margin: 10px 0 0; font-size: 10.5px; color: var(--muted-foreground, #99a1ac); }
68
+ .fallback { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border, #3a404a); font-size: 10.5px; color: var(--muted-foreground, #99a1ac); }
69
+ .fallback code {
70
+ display: inline-block;
71
+ margin-top: 4px;
72
+ padding: 2px 6px;
73
+ border-radius: 4px;
74
+ background: rgba(127,127,127,.12);
75
+ color: var(--foreground, #f3f5f8);
76
+ font-family: ui-monospace, "Cascadia Code", Consolas, monospace;
77
+ font-size: 10px;
78
+ }
79
+ .warn { color: var(--destructive, #f87171); }
80
+ .ok { color: #4ade80; }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <div class="brand">
85
+ <div class="mark">🧠</div>
86
+ <div>
87
+ <h1>Memory Atlas</h1>
88
+ <p>MEMORY ETERNAL</p>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="card">
93
+ <h2>本地记忆库</h2>
94
+ <p>记忆库、待审、读卡器与知识星图都在 Web 面板里;Orca 只负责一键打开,不复制第二套 UI。</p>
95
+
96
+ <div class="row">
97
+ <label for="terminal">发送到终端</label>
98
+ <select id="terminal"><option value="">正在读取工作区…</option></select>
99
+ </div>
100
+
101
+ <button id="open" disabled>打开 Memory Atlas</button>
102
+ <p id="status" class="status"></p>
103
+
104
+ <div class="fallback">
105
+ 如果按钮无响应,在任意终端执行:
106
+ <code>dsh-memory open</code>
107
+ <br/>Web 地址:<code>http://127.0.0.1:7999/</code>
108
+ </div>
109
+ </div>
110
+
111
+ <script>
112
+ 'use strict'
113
+
114
+ var seq = 0
115
+ var pending = new Map()
116
+ var statusEl = document.getElementById('status')
117
+ var terminalEl = document.getElementById('terminal')
118
+ var openBtn = document.getElementById('open')
119
+
120
+ function setStatus(text, cls) {
121
+ statusEl.textContent = text || ''
122
+ statusEl.className = 'status' + (cls ? ' ' + cls : '')
123
+ }
124
+
125
+ function call(action, params) {
126
+ return new Promise(function (resolve, reject) {
127
+ var requestId = 'req-' + (++seq)
128
+ pending.set(requestId, { resolve: resolve, reject: reject })
129
+ window.parent.postMessage({
130
+ type: 'orca-panel-action',
131
+ requestId: requestId,
132
+ action: action,
133
+ params: params || {}
134
+ }, '*')
135
+ })
136
+ }
137
+
138
+ window.addEventListener('message', function (event) {
139
+ var data = event.data
140
+ if (!data || data.type !== 'orca-panel-action-result') return
141
+ var entry = pending.get(data.requestId)
142
+ if (!entry) return
143
+ pending.delete(data.requestId)
144
+ if (data.ok) entry.resolve(data.value)
145
+ else entry.reject(new Error(data.error || data.errorCode || 'action failed'))
146
+ })
147
+
148
+ async function loadTerminals() {
149
+ try {
150
+ var ctx = await call('workspace.readContext')
151
+ var terminals = (ctx && ctx.terminals) || []
152
+ terminalEl.innerHTML = ''
153
+ if (!terminals.length) {
154
+ var opt = document.createElement('option')
155
+ opt.value = ''
156
+ opt.textContent = '请先打开一个 Orca 终端'
157
+ terminalEl.appendChild(opt)
158
+ openBtn.disabled = true
159
+ setStatus('需要一个终端来执行 dsh-memory open。', 'warn')
160
+ return
161
+ }
162
+ terminals.forEach(function (term, index) {
163
+ var opt = document.createElement('option')
164
+ opt.value = term.id
165
+ opt.textContent = '终端 ' + (index + 1)
166
+ terminalEl.appendChild(opt)
167
+ })
168
+ openBtn.disabled = false
169
+ setStatus('已就绪。')
170
+ } catch (err) {
171
+ setStatus('无法读取工作区终端:' + err.message, 'warn')
172
+ }
173
+ }
174
+
175
+ openBtn.addEventListener('click', async function () {
176
+ var terminalId = terminalEl.value
177
+ if (!terminalId) {
178
+ setStatus('请先选择一个终端。', 'warn')
179
+ return
180
+ }
181
+ openBtn.disabled = true
182
+ setStatus('正在拉起 Memory Atlas…')
183
+ try {
184
+ await call('terminal.sendText', {
185
+ terminalId: terminalId,
186
+ text: 'dsh-memory open',
187
+ enter: true
188
+ })
189
+ setStatus('已发送 dsh-memory open。浏览器/WebView 打开后即可使用。', 'ok')
190
+ } catch (err) {
191
+ setStatus('打开失败:' + err.message + '。请在终端手动执行 dsh-memory open。', 'warn')
192
+ } finally {
193
+ openBtn.disabled = false
194
+ }
195
+ })
196
+
197
+ loadTerminals()
198
+ </script>
199
+ </body>
200
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
5
5
  "icon": "assets/icon.png",
6
6
  "license": "MIT",
@@ -34,6 +34,9 @@
34
34
  "hooks",
35
35
  "skills",
36
36
  "assets/icon.png",
37
+ "orca-plugin",
38
+ "dsh-tui-plugin",
39
+ "cursor-extension",
37
40
  ".claude-plugin",
38
41
  ".codex-plugin",
39
42
  ".cursor-plugin",
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: memory-open
3
+ description: Open the local Memory Atlas Web UI (memory-eternal) from Codex or Claude Code. Use when the user asks to open the memory library, review queue or knowledge graph.
4
+ allowed-tools: Bash(dsh-memory open)
5
+ ---
6
+
7
+ # Open Memory Atlas
8
+
9
+ Run this once, from the workspace root:
10
+
11
+ ```bash
12
+ dsh-memory open
13
+ ```
14
+
15
+ It ensures the local memory Web UI is running on `http://127.0.0.1:7999/` and opens the default browser.
16
+
17
+ Rules:
18
+
19
+ - Do not approve or reject cards from here. Review is done by the person in the Web UI.
20
+ - Do not write or mutate memory cards.
21
+ - If `dsh-memory` is not on PATH, read the repository `README.md` for the CLI install path, or ask the user to run the command manually.
22
+ - After opening, tell the user the review queue is available in the Web UI; keep the reply short.
package/web/app.js CHANGED
@@ -703,7 +703,7 @@ Option A, because:
703
703
  .mc-audit-batchcard > .mc-btn { width: 100%; }
704
704
  .mc-audit-batchcard > div[style*="flex: 1"] { display: none; }
705
705
  }
706
- `;var ro="0.11.0";function mf({t:e,inModal:t,onClose:n}){let r=typeof window<"u"&&(!!window.__DSH_BOOT__||String(location.pathname||"").includes("/memory-eternal/api/ui/app")),[a,i]=(0,g.useState)(null),[l,s]=(0,g.useState)([]),[d,p]=(0,g.useState)(!0),[v,h]=(0,g.useState)(null),[u,S]=(0,g.useState)(""),[N,M]=(0,g.useState)("all"),[_,f]=(0,g.useState)(""),[c,y]=(0,g.useState)("all"),[C,L]=(0,g.useState)("all"),[j,P]=(0,g.useState)(()=>{try{return new Set(JSON.parse(localStorage.getItem("me-pinned-v5")||"[]"))}catch{return new Set}}),[H,ne]=(0,g.useState)(!1),B=(0,g.useCallback)(E=>{P(x=>{let D=new Set(x);D.has(E)?D.delete(E):D.add(E);try{localStorage.setItem("me-pinned-v5",JSON.stringify([...D]))}catch{}return D})},[]),[J,Ve]=(0,g.useState)(""),[X,Ye]=(0,g.useState)(()=>{try{let E=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),x={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(E&&x[E])return x[E]}catch{}return"cards"}),[T,Y]=(0,g.useState)(()=>new Set([X])),q=(0,g.useCallback)(E=>{Y(x=>x.has(E)?x:new Set(x).add(E)),Ye(E)},[]),[U,De]=(0,g.useState)("recent"),[Be,ye]=(0,g.useState)(null),Ce=(0,g.useRef)(null),R=(0,g.useRef)(null),[le,ae]=(0,g.useState)(!1),[_e,se]=(0,g.useState)(null),[Ne,kt]=(0,g.useState)(!1),[k,W]=(0,g.useState)(!1),[$,re]=(0,g.useState)(!1),[Ee,fe]=(0,g.useState)("group"),[Q,ze]=(0,g.useState)(!1),be=(0,g.useCallback)(()=>{ze(!1);try{localStorage.setItem("me-onboard-v2","1")}catch{}},[]),z=(0,g.useCallback)((E="group")=>{fe(E),re(!0)},[]);(0,g.useEffect)(()=>{try{if(localStorage.getItem("me-onboard-v2")||X!=="cards")return;let E=setTimeout(()=>ze(!0),900);return()=>clearTimeout(E)}catch{}},[X]);let[w,oe]=(0,g.useState)(!0),[ke,Ie]=(0,g.useState)(null),[je,m]=(0,g.useState)(0),gn=(0,g.useCallback)(()=>m(E=>E+1),[]),Ln=a?.quarantine?.pending??0,St=a?.quarantine?.deleted??0,et=(0,g.useRef)(null),[Gt,wr]=(0,g.useState)(0),[hn,Fe]=(0,g.useState)(!1),on=(0,g.useRef)(null),tt=(0,g.useRef)(null),b=(0,g.useCallback)(async(E=N,x=_,D=C,F={})=>{let O=F.append===!0;try{O?Fe(!0):(p(!0),S(""));let K=new URLSearchParams;E&&E!=="all"&&K.set("kind",E),x.trim()&&K.set("q",x.trim());let ie=F.tag!==void 0?F.tag:J;ie&&K.set("tag",ie),K.set("status",D||"all"),K.set("limit",String(ah)),K.set("offset",String(F.offset||0)),U==="title"&&K.set("sort","title");let xe=await(await fetch(`${ee}/cards?${K.toString()}`)).json();if(xe.ok){let Qe=xe.cards||[];s(Ue=>O?[...Ue,...Qe.filter(Se=>!Ue.some(Te=>Te.path===Se.path))]:Qe),wr(Number(xe.total)||Qe.length)}}catch(K){S(String(K&&K.message?K.message:K))}finally{O?Fe(!1):p(!1)}},[N,_,C,U,J]),V=(0,g.useCallback)(async()=>{try{let[E,x]=await Promise.all([fetch(`${ee}/overview`).then(D=>D.json()),fetch(`${ee}/capture-log`).then(D=>D.json()).catch(()=>null)]);E.ok&&i(E),h(x&&x.ok&&x.available&&x.health||null)}catch(E){S(String(E&&E.message?E.message:E))}finally{p(!1)}},[]),Z=async E=>{try{let D=await(await fetch(`${ee}/delete?path=${encodeURIComponent(E)}`)).json();D.ok?(ye({ok:!0,msg:e("deleted")}),await V(),gn()):ye({ok:!1,msg:D.error||e("deleteFail")})}catch{ye({ok:!1,msg:e("deleteFail")})}Ce.current&&clearTimeout(Ce.current),Ce.current=setTimeout(()=>ye(null),2e3)};(0,g.useEffect)(()=>(V(),()=>{et.current&&clearTimeout(et.current)}),[V]);let ce=E=>{f(E),et.current&&clearTimeout(et.current),et.current=setTimeout(()=>b(N,E,C),280)},ue=E=>{M(E),b(E,_,C)},te=E=>{let x=String(E||"");Ve(x),se(null),b(N,_,C,{tag:x})};(0,g.useEffect)(()=>{X==="cards"&&b(N,_,C)},[C,X,je,U,J]),(0,g.useEffect)(()=>{if(X!=="cards"||d||hn||l.length>=Gt)return;let E=on.current,x=tt.current;if(!E)return;let D=new IntersectionObserver(F=>{F[0]&&F[0].isIntersecting&&b(N,_,C,{append:!0,offset:l.length})},{root:x||null,rootMargin:"400px"});return D.observe(E),()=>D.disconnect()},[X,l,Gt,d,hn,N,_,C,b]);let pe=async E=>{try{let x=E.status;if(!x&&E.path)try{let ie=((await fetch(`${ee}/cards?status=all&q=${encodeURIComponent(E.title||"")}&limit=50`).then(me=>me.json())).cards||[]).find(me=>me.path===E.path);ie&&(x=ie.status)}catch{}let D=x&&x!=="approved"?`&status=${encodeURIComponent(x)}`:"",F=await fetch(`${ee}/card?path=${encodeURIComponent(E.path||E.id)}${D}`),O=await F.json();O.ok?se({path:O.path,title:E.title,text:O.text}):S(O.error||`HTTP ${F.status}`)}catch(x){S(String(x&&x.message?x.message:x))}},nt=async(E,x)=>{ae(!0);try{let D=await fetch(`${ee}/export`);if(!D.ok)throw new Error("HTTP "+D.status);let F=await D.json();if(!F.ok)throw new Error(F.error||"export failed");let O,K,ie;if(E==="json"){K="memory-vault.json",ie="application/json";let Qe=F.cards.map(Ue=>({path:Ue.path,title:Ue.title,kind:Ue.kind,status:Ue.status,store:Ue.store,text:Ue.text}));O=JSON.stringify({format:F.format||"memory-eternal-vault",formatVersion:F.formatVersion||1,exportedAt:F.exportedAt||new Date().toISOString(),count:Qe.length,cards:Qe},null,2)}else K="memory-vault.md",ie="text/markdown;charset=utf-8",O=F.cards.map(Qe=>Qe.text.trim()).filter(Boolean).join(`
706
+ `;var ro="0.12.0";function mf({t:e,inModal:t,onClose:n}){let r=typeof window<"u"&&(!!window.__DSH_BOOT__||String(location.pathname||"").includes("/memory-eternal/api/ui/app")),[a,i]=(0,g.useState)(null),[l,s]=(0,g.useState)([]),[d,p]=(0,g.useState)(!0),[v,h]=(0,g.useState)(null),[u,S]=(0,g.useState)(""),[N,M]=(0,g.useState)("all"),[_,f]=(0,g.useState)(""),[c,y]=(0,g.useState)("all"),[C,L]=(0,g.useState)("all"),[j,P]=(0,g.useState)(()=>{try{return new Set(JSON.parse(localStorage.getItem("me-pinned-v5")||"[]"))}catch{return new Set}}),[H,ne]=(0,g.useState)(!1),B=(0,g.useCallback)(E=>{P(x=>{let D=new Set(x);D.has(E)?D.delete(E):D.add(E);try{localStorage.setItem("me-pinned-v5",JSON.stringify([...D]))}catch{}return D})},[]),[J,Ve]=(0,g.useState)(""),[X,Ye]=(0,g.useState)(()=>{try{let E=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),x={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(E&&x[E])return x[E]}catch{}return"cards"}),[T,Y]=(0,g.useState)(()=>new Set([X])),q=(0,g.useCallback)(E=>{Y(x=>x.has(E)?x:new Set(x).add(E)),Ye(E)},[]),[U,De]=(0,g.useState)("recent"),[Be,ye]=(0,g.useState)(null),Ce=(0,g.useRef)(null),R=(0,g.useRef)(null),[le,ae]=(0,g.useState)(!1),[_e,se]=(0,g.useState)(null),[Ne,kt]=(0,g.useState)(!1),[k,W]=(0,g.useState)(!1),[$,re]=(0,g.useState)(!1),[Ee,fe]=(0,g.useState)("group"),[Q,ze]=(0,g.useState)(!1),be=(0,g.useCallback)(()=>{ze(!1);try{localStorage.setItem("me-onboard-v2","1")}catch{}},[]),z=(0,g.useCallback)((E="group")=>{fe(E),re(!0)},[]);(0,g.useEffect)(()=>{try{if(localStorage.getItem("me-onboard-v2")||X!=="cards")return;let E=setTimeout(()=>ze(!0),900);return()=>clearTimeout(E)}catch{}},[X]);let[w,oe]=(0,g.useState)(!0),[ke,Ie]=(0,g.useState)(null),[je,m]=(0,g.useState)(0),gn=(0,g.useCallback)(()=>m(E=>E+1),[]),Ln=a?.quarantine?.pending??0,St=a?.quarantine?.deleted??0,et=(0,g.useRef)(null),[Gt,wr]=(0,g.useState)(0),[hn,Fe]=(0,g.useState)(!1),on=(0,g.useRef)(null),tt=(0,g.useRef)(null),b=(0,g.useCallback)(async(E=N,x=_,D=C,F={})=>{let O=F.append===!0;try{O?Fe(!0):(p(!0),S(""));let K=new URLSearchParams;E&&E!=="all"&&K.set("kind",E),x.trim()&&K.set("q",x.trim());let ie=F.tag!==void 0?F.tag:J;ie&&K.set("tag",ie),K.set("status",D||"all"),K.set("limit",String(ah)),K.set("offset",String(F.offset||0)),U==="title"&&K.set("sort","title");let xe=await(await fetch(`${ee}/cards?${K.toString()}`)).json();if(xe.ok){let Qe=xe.cards||[];s(Ue=>O?[...Ue,...Qe.filter(Se=>!Ue.some(Te=>Te.path===Se.path))]:Qe),wr(Number(xe.total)||Qe.length)}}catch(K){S(String(K&&K.message?K.message:K))}finally{O?Fe(!1):p(!1)}},[N,_,C,U,J]),V=(0,g.useCallback)(async()=>{try{let[E,x]=await Promise.all([fetch(`${ee}/overview`).then(D=>D.json()),fetch(`${ee}/capture-log`).then(D=>D.json()).catch(()=>null)]);E.ok&&i(E),h(x&&x.ok&&x.available&&x.health||null)}catch(E){S(String(E&&E.message?E.message:E))}finally{p(!1)}},[]),Z=async E=>{try{let D=await(await fetch(`${ee}/delete?path=${encodeURIComponent(E)}`)).json();D.ok?(ye({ok:!0,msg:e("deleted")}),await V(),gn()):ye({ok:!1,msg:D.error||e("deleteFail")})}catch{ye({ok:!1,msg:e("deleteFail")})}Ce.current&&clearTimeout(Ce.current),Ce.current=setTimeout(()=>ye(null),2e3)};(0,g.useEffect)(()=>(V(),()=>{et.current&&clearTimeout(et.current)}),[V]);let ce=E=>{f(E),et.current&&clearTimeout(et.current),et.current=setTimeout(()=>b(N,E,C),280)},ue=E=>{M(E),b(E,_,C)},te=E=>{let x=String(E||"");Ve(x),se(null),b(N,_,C,{tag:x})};(0,g.useEffect)(()=>{X==="cards"&&b(N,_,C)},[C,X,je,U,J]),(0,g.useEffect)(()=>{if(X!=="cards"||d||hn||l.length>=Gt)return;let E=on.current,x=tt.current;if(!E)return;let D=new IntersectionObserver(F=>{F[0]&&F[0].isIntersecting&&b(N,_,C,{append:!0,offset:l.length})},{root:x||null,rootMargin:"400px"});return D.observe(E),()=>D.disconnect()},[X,l,Gt,d,hn,N,_,C,b]);let pe=async E=>{try{let x=E.status;if(!x&&E.path)try{let ie=((await fetch(`${ee}/cards?status=all&q=${encodeURIComponent(E.title||"")}&limit=50`).then(me=>me.json())).cards||[]).find(me=>me.path===E.path);ie&&(x=ie.status)}catch{}let D=x&&x!=="approved"?`&status=${encodeURIComponent(x)}`:"",F=await fetch(`${ee}/card?path=${encodeURIComponent(E.path||E.id)}${D}`),O=await F.json();O.ok?se({path:O.path,title:E.title,text:O.text}):S(O.error||`HTTP ${F.status}`)}catch(x){S(String(x&&x.message?x.message:x))}},nt=async(E,x)=>{ae(!0);try{let D=await fetch(`${ee}/export`);if(!D.ok)throw new Error("HTTP "+D.status);let F=await D.json();if(!F.ok)throw new Error(F.error||"export failed");let O,K,ie;if(E==="json"){K="memory-vault.json",ie="application/json";let Qe=F.cards.map(Ue=>({path:Ue.path,title:Ue.title,kind:Ue.kind,status:Ue.status,store:Ue.store,text:Ue.text}));O=JSON.stringify({format:F.format||"memory-eternal-vault",formatVersion:F.formatVersion||1,exportedAt:F.exportedAt||new Date().toISOString(),count:Qe.length,cards:Qe},null,2)}else K="memory-vault.md",ie="text/markdown;charset=utf-8",O=F.cards.map(Qe=>Qe.text.trim()).filter(Boolean).join(`
707
707
 
708
708
  ---
709
709