memory-eternal 0.1.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.
@@ -0,0 +1,201 @@
1
+ // 记忆核心 · 看门狗(独立进程保活 lib/web.js)。
2
+ //
3
+ // 用法(经 CLI):dsh-memory watchdog [--port 7999] [--interval 5] [--max-restart 10]
4
+ //
5
+ // 行为:
6
+ // - 启动时探测端口:活则只监督不接管;死则 spawn 新 web server
7
+ // - 周期性探活:HTTP GET /memory-eternal/api/overview(不是我们服务视为死)
8
+ // - 探到死:清理残留进程 → spawn 新 web server → restart 计数 +1
9
+ // - restart 超过 max-restart(默认 10)→ fatal 退出,不再保活
10
+ // - SIGINT/SIGTERM:优雅退出(停掉 watchdog 自己拉起的 web,但不动用户手动起的)
11
+ // - 所有状态输出到 stderr(看门狗 daemon 的标准约定)
12
+ //
13
+ // 独立性:watchdog 完全独立于 DSH 宿主——即便旧版插件(无 autoWeb)跑的
14
+ // DSH 环境,watchdog 也能拉起并保活 web server,让浏览器始终可达。
15
+
16
+ import http from 'node:http'
17
+ import net from 'node:net'
18
+ import { spawn } from 'node:child_process'
19
+ import path from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
21
+ import process from 'node:process'
22
+
23
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
24
+ const WEB_JS = path.join(__dirname, 'web.js')
25
+ const API_PREFIX = '/memory-eternal/api'
26
+
27
+ const ts = () => new Date().toISOString().slice(11, 19) // HH:MM:SS
28
+ const log = (...args) => process.stderr.write(`[${ts()}] [watchdog] ${args.join(' ')}\n`)
29
+
30
+ /** 探测某端口是否在响应 + 是我们的服务(响应含 vaultDir 标记)。 */
31
+ export async function probe(port, timeoutMs = 1000) {
32
+ return new Promise((resolve) => {
33
+ const req = http.request({ host: '127.0.0.1', port, path: `${API_PREFIX}/overview`, method: 'GET', timeout: timeoutMs }, (res) => {
34
+ if (res.statusCode !== 200) return resolve(null)
35
+ const chunks = []
36
+ res.on('data', (c) => chunks.push(c))
37
+ res.on('end', () => {
38
+ try {
39
+ const data = JSON.parse(Buffer.concat(chunks).toString('utf8'))
40
+ if (data && data.ok === true && typeof data.vaultDir === 'string') return resolve(data)
41
+ resolve(null)
42
+ } catch { resolve(null) }
43
+ })
44
+ res.on('error', () => resolve(null))
45
+ })
46
+ req.on('error', () => resolve(null))
47
+ req.on('timeout', () => { req.destroy(); resolve(null) })
48
+ req.end()
49
+ })
50
+ }
51
+
52
+ /** 探测端口是否有任何 TCP 监听(即便不是我们的服务也占用)。 */
53
+ export async function probeTcp(port, timeoutMs = 600) {
54
+ return new Promise((resolve) => {
55
+ const s = new net.Socket()
56
+ s.setTimeout(timeoutMs)
57
+ s.once('connect', () => { s.destroy(); resolve(true) })
58
+ s.once('timeout', () => { s.destroy(); resolve(false) })
59
+ s.once('error', () => resolve(false))
60
+ s.connect(port, '127.0.0.1')
61
+ })
62
+ }
63
+
64
+ /**
65
+ * 启动看门狗。返回对象含 .stop() 优雅退出。
66
+ * @param {object} opts
67
+ * @param {number} [opts.port=7999]
68
+ * @param {string} [opts.vaultRoot] // 默认 ~/.dsh/memory-vault
69
+ * @param {number} [opts.interval=5000]
70
+ * @param {number} [opts.maxRestart=10]
71
+ * @param {boolean} [opts.autoStart=true] // 首次探测到死时是否自动拉起
72
+ */
73
+ export function startWatchdog({ port = 7999, vaultRoot, interval = 5000, maxRestart = 10, autoStart = true } = {}) {
74
+ let child = null // 我们 spawn 的 web 进程
75
+ let ownedByUs = false
76
+ let restartCount = 0
77
+ let stopped = false
78
+ let timer = null
79
+
80
+ // 找空闲端口:port, port+1, ... 最多 +10
81
+ const findFreePort = async (start) => {
82
+ for (let p = start; p < start + 10; p++) {
83
+ if (await probeTcp(p) === false) return p
84
+ }
85
+ return start
86
+ }
87
+
88
+ const spawnWeb = async () => {
89
+ if (child && !child.killed) {
90
+ try { child.kill('SIGTERM') } catch {}
91
+ }
92
+ const targetPort = await findFreePort(port)
93
+ const args = [WEB_JS, '--port', String(targetPort)]
94
+ if (vaultRoot) args.push('--vault', vaultRoot)
95
+ log(`spawn web: node ${args.join(' ')}`)
96
+ const c = spawn(process.execPath, args, {
97
+ detached: true,
98
+ stdio: 'ignore',
99
+ env: { ...process.env, MEMORY_VAULT_DIR: vaultRoot || process.env.MEMORY_VAULT_DIR || '' },
100
+ windowsHide: true,
101
+ })
102
+ c.on('exit', (code, sig) => {
103
+ if (!stopped && code !== 0) log(`web exited code=${code} sig=${sig}`)
104
+ })
105
+ c.unref()
106
+ child = c
107
+ ownedByUs = true
108
+ return targetPort
109
+ }
110
+
111
+ const tick = async () => {
112
+ if (stopped) return
113
+ const alive = await probe(port)
114
+ if (alive) {
115
+ // 是我们的服务
116
+ if (!ownedByUs) {
117
+ log(`port ${port} 已存活(外部实例)→ 切换为保活模式(不接管)`)
118
+ ownedByUs = true // 至少逻辑上"知道"它活了
119
+ }
120
+ return
121
+ }
122
+ // 端口死
123
+ if (restartCount >= maxRestart) {
124
+ log(`restart 次数达上限 ${maxRestart} → 退出看门狗(请人工排查)`)
125
+ stop()
126
+ return
127
+ }
128
+ if (!autoStart) {
129
+ log(`port ${port} 已死,但 --no-restart 设置,跳过`)
130
+ return
131
+ }
132
+ restartCount += 1
133
+ log(`port ${port} 离线 → 第 ${restartCount} 次拉起 web server`)
134
+ try {
135
+ const newPort = await spawnWeb()
136
+ // 等待 ready
137
+ for (let i = 0; i < 30; i++) {
138
+ await new Promise((r) => setTimeout(r, 500))
139
+ const a = await probe(newPort)
140
+ if (a) {
141
+ log(`web 在 ${newPort} 就绪(vault: ${a.vaultDir})`)
142
+ if (newPort !== port) log(`提示:目标端口 ${port} 被占,watchdog 在 ${newPort} 上保活`)
143
+ return
144
+ }
145
+ }
146
+ log(`web 未能在 15s 内就绪`)
147
+ } catch (error) {
148
+ log(`spawn 失败:${error?.message || error}`)
149
+ }
150
+ }
151
+
152
+ const stop = () => {
153
+ if (stopped) return
154
+ stopped = true
155
+ if (timer) { clearInterval(timer); timer = null }
156
+ if (child && !child.killed) {
157
+ try { child.kill('SIGTERM') } catch {}
158
+ setTimeout(() => { try { child && child.kill('SIGKILL') } catch {} }, 1000).unref()
159
+ }
160
+ log('看门狗退出')
161
+ process.exit(0)
162
+ }
163
+
164
+ // 首次探测
165
+ ;(async () => {
166
+ const alive = await probe(port)
167
+ if (alive) {
168
+ log(`port ${port} 已活(外部实例),启动保活监督(间隔 ${interval}ms)`)
169
+ } else if (autoStart) {
170
+ log(`port ${port} 离线 → 首次拉起 web`)
171
+ restartCount += 1
172
+ try {
173
+ const np = await spawnWeb()
174
+ for (let i = 0; i < 30; i++) {
175
+ await new Promise((r) => setTimeout(r, 500))
176
+ if (await probe(np)) { log(`web 在 ${np} 就绪`); break }
177
+ }
178
+ } catch (e) { log(`首次拉起失败:${e?.message || e}`) }
179
+ }
180
+ timer = setInterval(tick, interval)
181
+ })()
182
+
183
+ process.on('SIGINT', stop)
184
+ process.on('SIGTERM', stop)
185
+ return { stop, get port() { return port }, get restartCount() { return restartCount } }
186
+ }
187
+
188
+ // 脚本入口:node lib/watchdog.js [--port N] [--interval MS] [--max-restart N] [--vault DIR]
189
+ if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
190
+ const argv = process.argv.slice(2)
191
+ const argOf = (name) => {
192
+ const i = argv.indexOf(name)
193
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null
194
+ }
195
+ const port = Number(argOf('--port')) || 7999
196
+ const interval = Number(argOf('--interval')) || 5000
197
+ const maxRestart = Number(argOf('--max-restart')) || 10
198
+ const vaultRoot = argOf('--vault') || undefined
199
+ const autoStart = !argv.includes('--no-restart')
200
+ startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
201
+ }
package/lib/web.js ADDED
@@ -0,0 +1,149 @@
1
+ // 记忆核心 · 独立 Web server(UI 唯一真源)。
2
+ //
3
+ // 职责:
4
+ // - 静态服务 web/index.html + web/app.js(自包含 bundle,react 打入)
5
+ // - 同源挂载 /memory-eternal/api/*(复用 lib/api.js,与 DSH 内完全同一实现)
6
+ // - iframe 友好:不设 X-Frame-Options(DSH 宿主内嵌加载本页)
7
+ //
8
+ // 常驻方式:ensureWebServer() 探活 → 未活则 detached spawn 独立进程
9
+ // (DSH 插件激活 / MCP server 启动 / CLI 调用都会走它,实现「默认开启」)。
10
+ //
11
+ // 直接运行:node lib/web.js --port 7999 [--vault <dir>]
12
+
13
+ import http from 'node:http'
14
+ import { promises as fs, readFileSync } from 'node:fs'
15
+ import path from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+ import { createApi, API_PREFIX } from './api.js'
18
+ import { defaultVaultDir, configFilePath } from './capture-run.js'
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
21
+ const WEB_ROOT = path.join(__dirname, '..', 'web')
22
+ export const DEFAULT_WEB_PORT = Number(process.env.MEMORY_WEB_PORT) || 7999
23
+ const VERSION = (() => { try { return JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version } catch { return '' } })()
24
+ process.env.MEMORY_ETERNAL_VERSION = VERSION
25
+
26
+ const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' }
27
+
28
+ export function startWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVaultDir(), host = '127.0.0.1' } = {}) {
29
+ const handleApi = createApi({
30
+ vaultDir: () => vaultRoot,
31
+ vaultRoots: () => [{ name: '', root: vaultRoot }],
32
+ // 读 DSH 写入的共享配置文件 → web 端与 DSH 设置同步(不同步修复)
33
+ getSettings: () => { try { return JSON.parse(readFileSync(configFilePath(process.env), 'utf8')) } catch { return {} } },
34
+ getDshInfo: () => ({ name: 'deepseek-harness', label: 'DeepSeek Harness(当前宿主)', installed: true, memoryRecallTool: true, autoCapture: true, autoRecall: true, vaultDir: vaultRoot, version: VERSION }),
35
+ })
36
+
37
+ const server = http.createServer(async (req, res) => {
38
+ try {
39
+ const url = new URL(req.url, 'http://localhost')
40
+ const p = url.pathname
41
+ if (p.startsWith(API_PREFIX)) return await handleApi(req, res)
42
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
43
+ res.writeHead(405, { 'Content-Type': 'application/json' })
44
+ return res.end(JSON.stringify({ ok: false, error: 'method not allowed' }))
45
+ }
46
+ if (p === '/' || p === '/index.html') {
47
+ const buf = await fs.readFile(path.join(WEB_ROOT, 'index.html'))
48
+ res.writeHead(200, { 'Content-Type': MIME['.html'], 'Cache-Control': 'no-store' })
49
+ return res.end(buf)
50
+ }
51
+ if (p === '/app.js') {
52
+ const buf = await fs.readFile(path.join(WEB_ROOT, 'app.js'))
53
+ res.writeHead(200, { 'Content-Type': MIME['.js'], 'Cache-Control': 'no-store' })
54
+ return res.end(buf)
55
+ }
56
+ res.writeHead(404, { 'Content-Type': 'application/json' })
57
+ res.end(JSON.stringify({ ok: false, error: 'not found' }))
58
+ } catch (error) {
59
+ if (!res.headersSent) res.writeHead(500, { 'Content-Type': 'application/json' })
60
+ res.end(JSON.stringify({ ok: false, error: String(error?.message || error) }))
61
+ }
62
+ })
63
+
64
+ return new Promise((resolve, reject) => {
65
+ server.once('error', reject)
66
+ server.listen(port, host, () => {
67
+ process.stderr.write(`[memory-eternal] web server on http://${host}:${port} (vault: ${vaultRoot})\n`)
68
+ resolve({ server, port, host, url: `http://${host}:${port}` })
69
+ })
70
+ })
71
+ }
72
+
73
+ /** 探测某端口是否已是本项目的 web server(响应 /api/overview 且带 vaultDir 标记)。 */
74
+ export async function probeWebServer(port = DEFAULT_WEB_PORT, timeoutMs = 800) {
75
+ try {
76
+ const res = await fetch(`http://127.0.0.1:${port}${API_PREFIX}/overview`, { signal: AbortSignal.timeout(timeoutMs) })
77
+ if (!res.ok) return null
78
+ const data = await res.json()
79
+ if (data && data.ok === true && typeof data.vaultDir === 'string') return data
80
+ return null
81
+ } catch {
82
+ return null
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 确保 web server 存活(默认开启的实现核心)。
88
+ * 已活 → 直接返回 URL;未活 → detached spawn 独立进程并轮询探活。
89
+ * 端口被非本项目进程占用时向后漂移(+1..+10)。
90
+ * @returns {Promise<{url:string, port:number, spawned:boolean}>}
91
+ */
92
+ export async function ensureWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVaultDir(), spawnEnv = {}, totalTimeoutMs = 15000 } = {}) {
93
+ // 探测可用端口:先找「已是我们的服务」的端口,再找空闲端口
94
+ let target = port
95
+ let found = null
96
+ for (let p = port; p < port + 10; p++) {
97
+ const alive = await probeWebServer(p)
98
+ if (alive) { found = { url: `http://127.0.0.1:${p}`, port: p, spawned: false }; break }
99
+ // 该端口不是我们的服务:若连不上(空闲)就用它;连得上但不是我们的(被占用)则漂移
100
+ const isFree = await probeWebServer(p, 300) === null && await isPortConnectable(p) === false
101
+ if (isFree) { target = p; break }
102
+ target = p + 1
103
+ }
104
+ if (found) return found
105
+
106
+ // detached spawn:独立进程,父进程退出不影响(常驻);stdio ignore 防阻塞
107
+ const { spawn } = await import('node:child_process')
108
+ const child = spawn(
109
+ process.execPath,
110
+ [path.join(__dirname, 'web.js'), '--port', String(target), '--vault', vaultRoot],
111
+ { detached: true, stdio: 'ignore', env: { ...process.env, ...spawnEnv, MEMORY_VAULT_DIR: vaultRoot }, windowsHide: true },
112
+ )
113
+ child.unref()
114
+
115
+ const deadline = Date.now() + totalTimeoutMs
116
+ while (Date.now() < deadline) {
117
+ await new Promise((r) => setTimeout(r, 400))
118
+ const alive = await probeWebServer(target)
119
+ if (alive) return { url: `http://127.0.0.1:${target}`, port: target, spawned: true }
120
+ }
121
+ throw new Error(`[memory-eternal] web server 未能在 ${totalTimeoutMs}ms 内就绪 (port ${target})`)
122
+ }
123
+
124
+ async function isPortConnectable(port) {
125
+ const net = await import('node:net')
126
+ return new Promise((resolve) => {
127
+ const s = new net.Socket()
128
+ s.setTimeout(300)
129
+ s.once('connect', () => { s.destroy(); resolve(true) })
130
+ s.once('timeout', () => { s.destroy(); resolve(false) })
131
+ s.once('error', () => resolve(false))
132
+ s.connect(port, '127.0.0.1')
133
+ })
134
+ }
135
+
136
+ // -- 脚本入口 -----------------------------------------------------------------
137
+ const argv = process.argv.slice(2)
138
+ const argOf = (name) => {
139
+ const i = argv.indexOf(name)
140
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null
141
+ }
142
+ if (process.argv[1] && /[\\/]web\.js$/.test(process.argv[1])) {
143
+ const port = Number(argOf('--port')) || DEFAULT_WEB_PORT
144
+ const vault = argOf('--vault') || defaultVaultDir()
145
+ startWebServer({ port, vaultRoot: path.resolve(vault) })
146
+ // 常驻:不主动退出;Ctrl+C / 进程被杀即停
147
+ process.on('SIGINT', () => process.exit(0))
148
+ process.on('SIGTERM', () => process.exit(0))
149
+ }
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "memory-eternal",
3
+ "version": "0.1.0",
4
+ "description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/EternalNight996/memory-eternal.git"
9
+ },
10
+ "homepage": "https://github.com/EternalNight996/memory-eternal",
11
+ "author": "EternalNight996",
12
+ "publishConfig": {
13
+ "registry": "https://registry.npmjs.org/"
14
+ },
15
+ "type": "module",
16
+ "main": "index.js",
17
+ "bin": {
18
+ "dsh-memory": "bin/dsh-memory.mjs"
19
+ },
20
+ "exports": {
21
+ ".": "./index.js",
22
+ "./client": "./lib/client.js",
23
+ "./cordis.patch.yml": "./cordis.patch.yml",
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "index.js",
28
+ "lib",
29
+ "bin",
30
+ "web",
31
+ "hooks",
32
+ "skills",
33
+ ".claude-plugin",
34
+ ".codex-plugin",
35
+ ".cursor-plugin",
36
+ ".mcp.json",
37
+ "cordis.patch.yml",
38
+ "README.md",
39
+ "README.en.md",
40
+ "LICENSE"
41
+ ],
42
+ "engines": {
43
+ "node": ">=22"
44
+ },
45
+ "keywords": [
46
+ "deepseek-harness",
47
+ "dsh",
48
+ "dsh-plugin",
49
+ "plugin",
50
+ "memory",
51
+ "knowledge-base",
52
+ "vault",
53
+ "markdown",
54
+ "knowledge-graph",
55
+ "rag",
56
+ "second-brain"
57
+ ],
58
+ "scripts": {
59
+ "build": "node build.mjs",
60
+ "build:client": "node build.mjs",
61
+ "prepublishOnly": "node build.mjs",
62
+ "test": "node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/api.test.mjs"
63
+ },
64
+ "dependencies": {
65
+ "@deepseek-ai/schemastery": "^3.18.1"
66
+ },
67
+ "peerDependencies": {
68
+ "@deepseek-ai/cordis": "^4.0.1",
69
+ "@deepseek-ai/dsh-llm": ">=0.1.0-rc.2 <0.2.0",
70
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.2 <0.2.0"
71
+ },
72
+ "devDependencies": {
73
+ "@types/react": "^18.3.1",
74
+ "esbuild": "^0.24.0",
75
+ "react": "18.3.1",
76
+ "react-dom": "18.3.1"
77
+ },
78
+ "dsh": {
79
+ "client": {
80
+ "platform": "web",
81
+ "inject": [
82
+ "@deepseek-ai/dsh-client-runtime",
83
+ "@deepseek-ai/dsh-client-ui-settings",
84
+ "@deepseek-ai/dsh-client-locale",
85
+ "@deepseek-ai/dsh-client-connection",
86
+ "@deepseek-ai/dsh-api-remotes"
87
+ ]
88
+ },
89
+ "bundle": {
90
+ "patch": "./cordis.patch.yml"
91
+ },
92
+ "marketplace": {
93
+ "profiles": [
94
+ "web"
95
+ ],
96
+ "requiresBuildApproval": false,
97
+ "requiresRestart": true,
98
+ "manualSteps": false
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: memory
3
+ description: Recall relevant knowledge cards from the unified memory vault and capture durable findings. Use memory_recall before answering when prior context helps; call memory_capture when a session produces a durable insight.
4
+ ---
5
+
6
+ # Memory Eternal
7
+
8
+ This agent has access to `memory_recall` (retrieve cards) and `memory_capture` (store a card) via the `memory` MCP server.
9
+
10
+ ## When to recall
11
+ Before answering when the user's question may depend on prior project decisions, past bugs, or preferences. Combine with `memory_stats` to gauge coverage.
12
+
13
+ ## When to capture
14
+ At the end of a session that produced a durable, reusable conclusion (a fix, a decision, an architecture rationale, a constraint). Capture concise findings rather than raw transcripts; the hook also auto-captures at session end into the same vault (pending → audit). Source attribution is automatic.