memory-eternal 0.12.0 → 0.14.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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.en.md +16 -5
- package/README.md +16 -5
- package/cursor-extension/package.json +1 -1
- package/dsh-tui-plugin/README.md +71 -0
- package/dsh-tui-plugin/demo-audit.mjs +535 -0
- package/dsh-tui-plugin/demo.mjs +743 -0
- package/dsh-tui-plugin/dsh-plugin.json +7 -2
- package/dsh-tui-plugin/main.js +43 -0
- package/dsh-tui-plugin/package.json +3 -1
- package/dsh-tui-plugin/scene.js +151 -0
- package/lib/client.js +28 -16
- package/orca-plugin/README.md +12 -0
- package/orca-plugin/orca-plugin.json +1 -1
- package/orca-plugin/panel.html +27 -12
- package/package.json +1 -1
- package/web/app.js +28 -16
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"$schema": "urn:dsh-std:community-draft:dsh-plugin:0.15",
|
|
3
3
|
"id": "memory-eternal-tui",
|
|
4
4
|
"name": "memory-eternal-tui",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.14.0",
|
|
6
6
|
"manifestVersion": "0.15",
|
|
7
|
-
"description": "Open
|
|
7
|
+
"description": "Open Memory Atlas from dsh-TUI: native /audit scene plus browser launcher.",
|
|
8
8
|
"facets": {
|
|
9
9
|
"host": {
|
|
10
10
|
"entry": "main.js",
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
"id": "memory-eternal-tui.open",
|
|
27
27
|
"title": "memory",
|
|
28
28
|
"description": "Open Memory Atlas in the browser"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "memory-eternal-tui.audit",
|
|
32
|
+
"title": "audit",
|
|
33
|
+
"description": "Open the native Memory Atlas audit scene"
|
|
29
34
|
}
|
|
30
35
|
]
|
|
31
36
|
},
|
package/dsh-tui-plugin/main.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
// 这里不做第二套 TUI UI:命令只负责把用户送到同一套 Memory Atlas Web UI。
|
|
4
4
|
// 审批仍然在 Web 里由人完成,不新增 agent 工具,也不触碰审核守卫。
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
6
|
+
import { AuditScene } from './scene.js'
|
|
6
7
|
|
|
7
8
|
export const name = 'memory-eternal-tui'
|
|
8
9
|
export const inject = ['agents']
|
|
9
10
|
|
|
10
11
|
const URL = 'http://127.0.0.1:7999/'
|
|
11
12
|
const COMMAND_ID = 'memory-eternal-tui.open'
|
|
13
|
+
const AUDIT_COMMAND_ID = 'memory-eternal-tui.audit'
|
|
14
|
+
const AUDIT_SCENE_ID = 'memory-eternal-tui-audit'
|
|
12
15
|
|
|
13
16
|
function openUrl() {
|
|
14
17
|
const cmd = process.platform === 'win32' ? 'rundll32' : process.platform === 'darwin' ? 'open' : 'xdg-open'
|
|
@@ -41,6 +44,32 @@ export function apply(ctx) {
|
|
|
41
44
|
const host = ctx.get('tuiPluginHost', false)
|
|
42
45
|
if (!host) return
|
|
43
46
|
|
|
47
|
+
// tuiScenes can mount after this activation on a cold boot. Soft-probe and
|
|
48
|
+
// retry briefly instead of silently leaving /audit dead for the session.
|
|
49
|
+
let sceneDisposer = null
|
|
50
|
+
let sceneTimer = null
|
|
51
|
+
let attempts = 0
|
|
52
|
+
const registerScene = () => {
|
|
53
|
+
sceneTimer = null
|
|
54
|
+
const scenes = ctx.get('tuiScenes', false)
|
|
55
|
+
if (scenes && !sceneDisposer) {
|
|
56
|
+
try {
|
|
57
|
+
sceneDisposer = scenes.register({ id: AUDIT_SCENE_ID, title: 'Memory Atlas / 待审', component: AuditScene }, ctx)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn('[memory-eternal-tui] scene registration failed:', error)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!sceneDisposer && attempts < 40) {
|
|
63
|
+
attempts += 1
|
|
64
|
+
sceneTimer = setTimeout(registerScene, 50)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
registerScene()
|
|
68
|
+
ctx.effect(() => {
|
|
69
|
+
if (sceneTimer) clearTimeout(sceneTimer)
|
|
70
|
+
if (sceneDisposer) sceneDisposer()
|
|
71
|
+
})
|
|
72
|
+
|
|
44
73
|
const dispose = host.registerCommand(ctx, COMMAND_ID, {
|
|
45
74
|
recordInput: false,
|
|
46
75
|
handler: () => {
|
|
@@ -49,6 +78,20 @@ export function apply(ctx) {
|
|
|
49
78
|
},
|
|
50
79
|
})
|
|
51
80
|
ctx.effect(() => dispose)
|
|
81
|
+
|
|
82
|
+
const disposeAudit = host.registerCommand(ctx, AUDIT_COMMAND_ID, {
|
|
83
|
+
recordInput: false,
|
|
84
|
+
handler: () => {
|
|
85
|
+
const runtime = ctx.get('tuiScenes', false)
|
|
86
|
+
if (!runtime) return { kind: 'error', text: '当前 dsh-tui 没有 scenes 运行时' }
|
|
87
|
+
if (runtime.active?.id === AUDIT_SCENE_ID) runtime.close()
|
|
88
|
+
const opened = runtime.open(AUDIT_SCENE_ID)
|
|
89
|
+
return opened
|
|
90
|
+
? { kind: 'success', text: 'Memory Atlas / 待审 已打开' }
|
|
91
|
+
: { kind: 'error', text: '审核场景未注册' }
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
ctx.effect(() => disposeAudit)
|
|
52
95
|
}
|
|
53
96
|
|
|
54
97
|
export default { name, inject, apply }
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Open Memory Atlas from dsh-TUI with /memory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"main.js",
|
|
9
|
+
"scene.js",
|
|
10
|
+
"demo.mjs",
|
|
9
11
|
"dsh-plugin.json",
|
|
10
12
|
"cordis.patch.yml",
|
|
11
13
|
"README.md"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// dsh-tui 原生审核场景(最小闭环)
|
|
2
|
+
//
|
|
3
|
+
// 不复制 Web UI:只把「待审队列 + 全选 + 批准/驳回」做成 dsh-tui 原生场景。
|
|
4
|
+
// 日常浏览/长文仍可回到 Web 或外部 TUI 客户端。
|
|
5
|
+
|
|
6
|
+
const API = process.env.MEMORY_ETERNAL_API || 'http://127.0.0.1:7999/memory-eternal/api'
|
|
7
|
+
|
|
8
|
+
async function apiJson(path, options = {}) {
|
|
9
|
+
const res = await fetch(API + path, { ...options, signal: AbortSignal.timeout(10000) })
|
|
10
|
+
const data = await res.json().catch(() => null)
|
|
11
|
+
if (!res.ok || !data || data.ok === false) throw new Error((data && data.error) || `HTTP ${res.status}`)
|
|
12
|
+
return data
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function timeOf(value) {
|
|
16
|
+
const d = new Date(value || Date.now())
|
|
17
|
+
return Number.isNaN(d.getTime()) ? '' : d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function AuditScene(props) {
|
|
21
|
+
const { React, ui, close } = props
|
|
22
|
+
const { Box, Text, useInput, useTerminalSize } = ui
|
|
23
|
+
const { useState, useEffect } = React
|
|
24
|
+
const size = useTerminalSize ? useTerminalSize() : { columns: 110, rows: 34 }
|
|
25
|
+
const columns = size.columns || 110
|
|
26
|
+
const rows = size.rows || 34
|
|
27
|
+
|
|
28
|
+
const [cards, setCards] = useState([])
|
|
29
|
+
const [index, setIndex] = useState(0)
|
|
30
|
+
const [marked, setMarked] = useState(() => new Set())
|
|
31
|
+
const [status, setStatus] = useState('读取待审队列…')
|
|
32
|
+
const [loading, setLoading] = useState(true)
|
|
33
|
+
|
|
34
|
+
const load = async () => {
|
|
35
|
+
setLoading(true)
|
|
36
|
+
setStatus('读取待审队列…')
|
|
37
|
+
try {
|
|
38
|
+
const data = await apiJson('/cards?status=pending&limit=300')
|
|
39
|
+
setCards((data.cards || []).map((c) => ({
|
|
40
|
+
path: c.path,
|
|
41
|
+
kind: c.kind || 'knowledge',
|
|
42
|
+
title: c.title || '无标题',
|
|
43
|
+
summary: String(c.summary || '').replace(/^#{1,6}\s*/gm, '').replace(/\s+/g, ' ').slice(0, 260),
|
|
44
|
+
source: c.submittedBy || 'memory',
|
|
45
|
+
time: timeOf(c.updated || c.created),
|
|
46
|
+
})))
|
|
47
|
+
setIndex(0)
|
|
48
|
+
setMarked(new Set())
|
|
49
|
+
setStatus('')
|
|
50
|
+
} catch (err) {
|
|
51
|
+
setStatus('读取失败:' + err.message)
|
|
52
|
+
} finally {
|
|
53
|
+
setLoading(false)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
useEffect(() => { void load() }, [])
|
|
58
|
+
|
|
59
|
+
const current = cards[index]
|
|
60
|
+
const visible = cards
|
|
61
|
+
const selected = marked.size
|
|
62
|
+
const apply = async (decision) => {
|
|
63
|
+
const targets = selected ? visible.filter((c) => marked.has(c.path)) : (current ? [current] : [])
|
|
64
|
+
if (!targets.length) return
|
|
65
|
+
setStatus(decision === 'a' ? `批准 ${targets.length} 张…` : `驳回 ${targets.length} 张…`)
|
|
66
|
+
try {
|
|
67
|
+
await Promise.all(targets.map((c) => apiJson('/audit/' + (decision === 'a' ? 'approve' : 'reject'), {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ path: c.path }),
|
|
71
|
+
})))
|
|
72
|
+
const ids = new Set(targets.map((c) => c.path))
|
|
73
|
+
setCards((prev) => prev.filter((c) => !ids.has(c.path)))
|
|
74
|
+
setIndex(0)
|
|
75
|
+
setMarked(new Set())
|
|
76
|
+
setStatus(`✓ ${decision === 'a' ? '已批准' : '已驳回'} ${targets.length} 张`)
|
|
77
|
+
} catch (err) {
|
|
78
|
+
setStatus('操作失败:' + err.message)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
useInput((input, key) => {
|
|
83
|
+
if (key.escape || input === 'q') { close(); return }
|
|
84
|
+
if (input === 'r' && !key.ctrl) { void apply('r'); return }
|
|
85
|
+
if (input === 'a' && !key.ctrl) { void apply('a'); return }
|
|
86
|
+
if (input === 's' || (key.ctrl && input === 'a')) {
|
|
87
|
+
setMarked((prev) => {
|
|
88
|
+
const all = visible.length > 0 && visible.every((c) => prev.has(c.path))
|
|
89
|
+
return all ? new Set() : new Set(visible.map((c) => c.path))
|
|
90
|
+
})
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if (key.upArrow) { setIndex((i) => Math.max(0, i - 1)); return }
|
|
94
|
+
if (key.downArrow) { setIndex((i) => Math.min(Math.max(0, visible.length - 1), i + 1)); return }
|
|
95
|
+
if (key.space || input === ' ') {
|
|
96
|
+
if (!current) return
|
|
97
|
+
setMarked((prev) => {
|
|
98
|
+
const next = new Set(prev)
|
|
99
|
+
if (next.has(current.path)) next.delete(current.path)
|
|
100
|
+
else next.add(current.path)
|
|
101
|
+
return next
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const listH = Math.max(3, rows - 7)
|
|
107
|
+
const rowW = Math.max(36, Math.floor((columns - 3) * 0.42))
|
|
108
|
+
const previewW = Math.max(20, columns - rowW - 3)
|
|
109
|
+
const start = Math.max(0, Math.min(index - Math.floor(listH / 2), Math.max(0, visible.length - listH)))
|
|
110
|
+
const list = (visible || []).slice(start, start + listH)
|
|
111
|
+
const previewLines = []
|
|
112
|
+
if (current) {
|
|
113
|
+
previewLines.push(current.kind + ' · ' + current.time + ' · ' + current.source)
|
|
114
|
+
previewLines.push('')
|
|
115
|
+
previewLines.push(current.title)
|
|
116
|
+
previewLines.push('')
|
|
117
|
+
for (let i = 0; i < current.summary.length; i += Math.max(16, previewW - 4)) {
|
|
118
|
+
previewLines.push(current.summary.slice(i, i + Math.max(16, previewW - 4)))
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
previewLines.push(loading ? '读取中…' : '没有待审卡')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const h = React.createElement
|
|
125
|
+
return h(Box, { flexDirection: 'column', width: columns, height: rows },
|
|
126
|
+
h(Box, { flexShrink: 0 },
|
|
127
|
+
h(Text, { color: 'remember', bold: true }, '● MEMORY ATLAS'),
|
|
128
|
+
h(Text, { dimColor: true }, ` / 待审 ${cards.length} 张` + (selected ? ` 已选 ${selected}` : '')),
|
|
129
|
+
),
|
|
130
|
+
h(Text, { dimColor: true }, '─'.repeat(Math.max(1, columns - 1))),
|
|
131
|
+
h(Box, { flexDirection: 'row', flexGrow: 1, overflow: 'hidden' },
|
|
132
|
+
h(Box, { flexDirection: 'column', width: rowW, borderStyle: 'round', flexShrink: 0 },
|
|
133
|
+
h(Text, { dimColor: true }, ` ${selected ? '▣' : '☐'} 全选 ${visible.length} s 全选`),
|
|
134
|
+
...list.map((c, i) => {
|
|
135
|
+
const on = start + i === index
|
|
136
|
+
const mark = marked.has(c.path) ? '◉' : '○'
|
|
137
|
+
const label = ` ${on ? '▌' : ' '}${mark} [${c.kind}] ${c.title}`
|
|
138
|
+
return h(Text, { key: c.path, inverse: on, wrap: 'truncate' }, label.slice(0, rowW - 2))
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
h(Box, { flexDirection: 'column', width: previewW, borderStyle: 'round', flexGrow: 1 },
|
|
142
|
+
...previewLines.slice(0, Math.max(1, rows - 4)).map((line, i) =>
|
|
143
|
+
h(Text, { key: i, wrap: 'truncate', color: i === 0 ? 'remember' : undefined }, ' ' + line)),
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
h(Text, { color: status.startsWith('✓') ? 'success' : status.startsWith('操作失败') || status.startsWith('读取失败') ? 'error' : undefined, dimColor: !status },
|
|
147
|
+
' ' + (status || '↑↓ 移动 Space 多选 s 全选 a 批准 r 驳回 Esc 退出')),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export default AuditScene
|