memory-eternal 0.17.0 → 0.18.1
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 +27 -27
- package/.codex-plugin/plugin.json +24 -24
- package/.cursor-plugin/plugin.json +14 -14
- package/README.md +138 -138
- package/README.zh-CN.md +138 -138
- package/cursor-extension/package.json +54 -54
- package/dsh-tui-plugin/dsh-plugin.json +42 -42
- package/dsh-tui-plugin/package.json +51 -51
- package/index.js +70 -8
- package/lib/api.js +48 -6
- package/lib/capture.js +37 -2
- package/lib/client.js +34 -21
- package/lib/core-memory.js +45 -0
- package/lib/db.js +2 -0
- package/lib/setup.js +242 -33
- package/lib/vault.js +20 -0
- package/orca-plugin/orca-plugin.json +36 -36
- package/package.json +2 -2
- package/web/app.js +39 -26
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// 核心记忆(前置强制记忆):被人工批准并标记为 core 的卡片,
|
|
2
|
+
// 每次会话都注入 systemPrompt,不依赖模型主动调用 memory_recall。
|
|
3
|
+
//
|
|
4
|
+
// 注意:只读取主库(cards 表)里 kind='core' 的已批准卡片;
|
|
5
|
+
// 待审/驳回卡永远不会进入这里。
|
|
6
|
+
|
|
7
|
+
import { listCards } from './vault.js'
|
|
8
|
+
|
|
9
|
+
export const CORE_KIND = 'core'
|
|
10
|
+
export const CORE_HEADER = '【核心记忆 · 必须遵守】\n以下内容已被用户批准为核心记忆,每次会话都必须优先遵守;与其他记忆或默认习惯冲突时,以此处为准。'
|
|
11
|
+
|
|
12
|
+
export function normalizeCoreOptions({ enabled = true, limit = 6, maxChars = 2000 } = {}) {
|
|
13
|
+
return {
|
|
14
|
+
enabled: enabled !== false,
|
|
15
|
+
limit: Math.min(Math.max(Math.floor(Number(limit) || 6), 1), 20),
|
|
16
|
+
maxChars: Math.min(Math.max(Math.floor(Number(maxChars) || 2000), 200), 10000),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 生成要注入 systemPrompt 的核心记忆文本;没有核心卡时返回空文本。 */
|
|
21
|
+
export async function coreMemoryPrompt(root, opts = {}) {
|
|
22
|
+
const { enabled, limit, maxChars } = normalizeCoreOptions(opts)
|
|
23
|
+
if (!enabled) return { ok: true, count: 0, text: '', chars: 0 }
|
|
24
|
+
let cards = []
|
|
25
|
+
try {
|
|
26
|
+
cards = await listCards(root, { kind: CORE_KIND, limit })
|
|
27
|
+
} catch {
|
|
28
|
+
return { ok: false, count: 0, text: '', chars: 0 }
|
|
29
|
+
}
|
|
30
|
+
const lines = []
|
|
31
|
+
let chars = CORE_HEADER.length
|
|
32
|
+
for (const card of cards) {
|
|
33
|
+
const title = String(card.title || '').replace(/\s+/g, ' ').trim()
|
|
34
|
+
const summary = String(card.summary || '').replace(/\s+/g, ' ').trim()
|
|
35
|
+
if (!title && !summary) continue
|
|
36
|
+
let line = '- ' + (title || '核心记忆') + (summary ? ':' + summary : '')
|
|
37
|
+
if (line.length > maxChars) line = line.slice(0, maxChars)
|
|
38
|
+
if (chars + 1 + line.length > maxChars && lines.length) break
|
|
39
|
+
lines.push(line)
|
|
40
|
+
chars += 1 + line.length
|
|
41
|
+
}
|
|
42
|
+
if (!lines.length) return { ok: true, count: 0, text: '', chars: 0 }
|
|
43
|
+
const text = CORE_HEADER + '\n' + lines.join('\n')
|
|
44
|
+
return { ok: true, count: lines.length, text: text.slice(0, maxChars), chars: Math.min(chars, maxChars) }
|
|
45
|
+
}
|
package/lib/db.js
CHANGED
|
@@ -373,6 +373,8 @@ export function checkMainStoreInvariant(root) {
|
|
|
373
373
|
* @returns {'pending' | 'approved'}
|
|
374
374
|
*/
|
|
375
375
|
export function enforceAudit(root, kind, submittedBy, fallback = 'pending') {
|
|
376
|
+
// 核心记忆必须人工审核:无论 auditMode / 免审名单如何配置,写入时一律待审。
|
|
377
|
+
if (String(kind) === 'core') return 'pending'
|
|
376
378
|
const db = getDb(root)
|
|
377
379
|
const get = (key) => { try { return db.prepare('SELECT value FROM config WHERE key = ?').get(key)?.value } catch { return null } }
|
|
378
380
|
const mode = get('auditMode')
|
package/lib/setup.js
CHANGED
|
@@ -13,6 +13,7 @@ import { promises as fs } from 'node:fs'
|
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import os from 'node:os'
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { nodeBinary, usesElectronBinary } from './node-bin.js'
|
|
16
17
|
|
|
17
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
18
19
|
export const PACKAGE_ROOT = path.join(__dirname, '..')
|
|
@@ -63,8 +64,150 @@ async function backupOnce(file) {
|
|
|
63
64
|
return bak
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
/** 这个 node 路径是否其实是 Electron 宿主二进制(直接运行会拉起 GUI)。 */
|
|
68
|
+
export function needsElectronEnv(nodePath = nodeBinary()) {
|
|
69
|
+
const p = String(nodePath || '')
|
|
70
|
+
if (!p) return false
|
|
71
|
+
if (usesElectronBinary()) return true
|
|
72
|
+
if (!process.versions.electron) return false
|
|
73
|
+
try { return path.resolve(p) === path.resolve(process.execPath) } catch { return false }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 静态 hook 命令:真实 node 直接用;只能退回 Electron 二进制时补 ELECTRON_RUN_AS_NODE。 */
|
|
77
|
+
export function hookCommand(nodePath = nodeBinary(), script, suffix = '') {
|
|
78
|
+
const base = `${JSON.stringify(nodePath)} ${JSON.stringify(script)}${suffix ? ' ' + suffix : ''}`
|
|
79
|
+
if (!needsElectronEnv(nodePath)) return base
|
|
80
|
+
if (process.platform === 'win32') {
|
|
81
|
+
// cmd.exe 不支持 POSIX 的 VAR=value 前缀;用 set 后再执行
|
|
82
|
+
return `cmd /d /s /c "set ELECTRON_RUN_AS_NODE=1&& ${base.replace(/"/g, '\\"')}"`
|
|
83
|
+
}
|
|
84
|
+
return `ELECTRON_RUN_AS_NODE=1 ${base}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 历史配置自检:命令指向 Electron 二进制、却没有 ELECTRON_RUN_AS_NODE。 */
|
|
88
|
+
export function isLegacyElectronCommand(command) {
|
|
89
|
+
const s = String(command || '')
|
|
90
|
+
if (!process.versions.electron) return false // 普通 node 进程不能用 execPath 名字判断
|
|
91
|
+
if (!s || /ELECTRON_RUN_AS_NODE/i.test(s)) return false
|
|
92
|
+
const exe = path.basename(process.execPath || '').toLowerCase()
|
|
93
|
+
if (!exe) return false
|
|
94
|
+
return s.toLowerCase().includes(exe)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function mcpCommand(nodePath = nodeBinary()) {
|
|
98
|
+
const out = { command: nodePath, args: [BIN_PATH, 'mcp'] }
|
|
99
|
+
if (needsElectronEnv(nodePath)) out.env = { ELECTRON_RUN_AS_NODE: '1' }
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 升级自动迁移(issue #53):只重写「我们自己写过、且指向 Electron 宿主 exe」的条目。
|
|
105
|
+
* 不碰用户自定义命令;写入前备份。返回 changed / details,方便宿主日志。
|
|
106
|
+
*/
|
|
107
|
+
export async function repairLegacySetup({ homeDir = home, dryRun = false, log = () => {}, legacyCheck = isLegacyElectronCommand } = {}) {
|
|
108
|
+
const root = homeDir
|
|
109
|
+
const node = nodeBinary()
|
|
110
|
+
const isLegacy = (cmd) => Boolean(legacyCheck(cmd))
|
|
111
|
+
const want = mcpCommand(node)
|
|
112
|
+
const captureScript = path.join(PACKAGE_ROOT, 'hooks', 'capture.mjs')
|
|
113
|
+
const guardScript = path.join(PACKAGE_ROOT, 'hooks', 'secret-guard.mjs')
|
|
114
|
+
const details = []
|
|
115
|
+
let changed = 0
|
|
116
|
+
|
|
117
|
+
const rewriteHooksTree = (value, agent, marker) => {
|
|
118
|
+
let hit = 0
|
|
119
|
+
const walk = (v) => {
|
|
120
|
+
if (!v) return
|
|
121
|
+
if (Array.isArray(v)) { for (const x of v) walk(x); return }
|
|
122
|
+
if (typeof v !== 'object') return
|
|
123
|
+
if (typeof v.command === 'string' && v.command.includes(marker) && isLegacy(v.command)) {
|
|
124
|
+
v.command = marker === 'secret-guard.mjs' ? hookCommand(node, guardScript) : hookCommand(node, captureScript, agent)
|
|
125
|
+
hit++
|
|
126
|
+
}
|
|
127
|
+
for (const x of Object.values(v)) walk(x)
|
|
128
|
+
}
|
|
129
|
+
walk(value)
|
|
130
|
+
return hit
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Claude Code MCP + hooks
|
|
134
|
+
const claudeJson = path.join(root, '.claude.json')
|
|
135
|
+
if (await exists(claudeJson)) {
|
|
136
|
+
try {
|
|
137
|
+
const cfg = JSON.parse(await fs.readFile(claudeJson, 'utf8'))
|
|
138
|
+
const entry = cfg?.mcpServers?.memory
|
|
139
|
+
if (entry && isLegacy(entry.command)) {
|
|
140
|
+
if (!dryRun) { await backupOnce(claudeJson); cfg.mcpServers.memory = { type: 'stdio', ...want }; await fs.writeFile(claudeJson, JSON.stringify(cfg, null, 2), 'utf8') }
|
|
141
|
+
details.push('claude.mcp'); changed++
|
|
142
|
+
}
|
|
143
|
+
} catch { /* 解析失败不碰 */ }
|
|
144
|
+
}
|
|
145
|
+
const claudeSettings = path.join(root, '.claude', 'settings.json')
|
|
146
|
+
if (await exists(claudeSettings)) {
|
|
147
|
+
try {
|
|
148
|
+
const cfg = JSON.parse(await fs.readFile(claudeSettings, 'utf8'))
|
|
149
|
+
const a = rewriteHooksTree(cfg.hooks, 'claude', 'capture.mjs')
|
|
150
|
+
const b = rewriteHooksTree(cfg.hooks, 'claude', 'secret-guard.mjs')
|
|
151
|
+
if (a + b) {
|
|
152
|
+
if (!dryRun) { await backupOnce(claudeSettings); await fs.writeFile(claudeSettings, JSON.stringify(cfg, null, 2), 'utf8') }
|
|
153
|
+
details.push('claude.hooks'); changed += a + b
|
|
154
|
+
}
|
|
155
|
+
} catch { /* 解析失败不碰 */ }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Codex MCP + hooks
|
|
159
|
+
const codexToml = path.join(root, '.codex', 'config.toml')
|
|
160
|
+
if (await exists(codexToml)) {
|
|
161
|
+
try {
|
|
162
|
+
const text = await fs.readFile(codexToml, 'utf8')
|
|
163
|
+
const entry = parseCodexMemoryEntry(text)
|
|
164
|
+
if (entry.present && isLegacy(entry.command)) {
|
|
165
|
+
const out = alignCodexMemorySection(text, want)
|
|
166
|
+
if (out.changed) {
|
|
167
|
+
if (!dryRun) { await backupOnce(codexToml); await fs.writeFile(codexToml, out.text, 'utf8') }
|
|
168
|
+
details.push('codex.mcp'); changed++
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch { /* 解析失败不碰 */ }
|
|
172
|
+
}
|
|
173
|
+
const codexHooks = path.join(root, '.codex', 'hooks.json')
|
|
174
|
+
if (await exists(codexHooks)) {
|
|
175
|
+
try {
|
|
176
|
+
const cfg = JSON.parse(await fs.readFile(codexHooks, 'utf8'))
|
|
177
|
+
const n = rewriteHooksTree(cfg.hooks, 'codex', 'capture.mjs')
|
|
178
|
+
if (n) {
|
|
179
|
+
if (!dryRun) { await backupOnce(codexHooks); await fs.writeFile(codexHooks, JSON.stringify(cfg, null, 2), 'utf8') }
|
|
180
|
+
details.push('codex.hooks'); changed += n
|
|
181
|
+
}
|
|
182
|
+
} catch { /* 解析失败不碰 */ }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Cursor MCP + hooks
|
|
186
|
+
const cursorJson = path.join(root, '.cursor', 'mcp.json')
|
|
187
|
+
if (await exists(cursorJson)) {
|
|
188
|
+
try {
|
|
189
|
+
const cfg = JSON.parse(await fs.readFile(cursorJson, 'utf8'))
|
|
190
|
+
const entry = cfg?.mcpServers?.memory
|
|
191
|
+
if (entry && isLegacy(entry.command)) {
|
|
192
|
+
if (!dryRun) { await backupOnce(cursorJson); cfg.mcpServers.memory = want; await fs.writeFile(cursorJson, JSON.stringify(cfg, null, 2), 'utf8') }
|
|
193
|
+
details.push('cursor.mcp'); changed++
|
|
194
|
+
}
|
|
195
|
+
} catch { /* 解析失败不碰 */ }
|
|
196
|
+
}
|
|
197
|
+
const cursorHooks = path.join(root, '.cursor', 'hooks.json')
|
|
198
|
+
if (await exists(cursorHooks)) {
|
|
199
|
+
try {
|
|
200
|
+
const cfg = JSON.parse(await fs.readFile(cursorHooks, 'utf8'))
|
|
201
|
+
const n = rewriteHooksTree(cfg.hooks, 'cursor', 'capture.mjs')
|
|
202
|
+
if (n) {
|
|
203
|
+
if (!dryRun) { await backupOnce(cursorHooks); await fs.writeFile(cursorHooks, JSON.stringify(cfg, null, 2), 'utf8') }
|
|
204
|
+
details.push('cursor.hooks'); changed += n
|
|
205
|
+
}
|
|
206
|
+
} catch { /* 解析失败不碰 */ }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (changed) log('↻ 已自动迁移旧 Electron 配置:' + details.join(' / '))
|
|
210
|
+
return { ok: true, changed, details, nodePath: node }
|
|
68
211
|
}
|
|
69
212
|
|
|
70
213
|
function sameMcpEntry(existing, want) {
|
|
@@ -72,12 +215,28 @@ function sameMcpEntry(existing, want) {
|
|
|
72
215
|
if (existing.type === 'stdio' || existing.type === undefined) {
|
|
73
216
|
return existing.command === want.command
|
|
74
217
|
&& JSON.stringify(existing.args ?? []) === JSON.stringify(want.args)
|
|
218
|
+
&& (!want.env || JSON.stringify(existing.env ?? {}) === JSON.stringify(want.env))
|
|
75
219
|
}
|
|
76
220
|
return false
|
|
77
221
|
}
|
|
78
222
|
|
|
79
223
|
// 是否已写入我们(capture.mjs)的会话结束 hook。
|
|
80
224
|
// marker 用于区分宿主:codex 走 hooks.json 的 Stop;cursor 走 hooks.json 的 stop/sessionEnd。
|
|
225
|
+
/** hooks 文件里是否有「指向 Electron 宿主 exe、却没带 ELECTRON_RUN_AS_NODE」的历史命令。 */
|
|
226
|
+
async function hooksNeedRepair(hooksJson, markers) {
|
|
227
|
+
try {
|
|
228
|
+
const cfg = JSON.parse(await fs.readFile(hooksJson, 'utf8'))
|
|
229
|
+
const walk = (v) => {
|
|
230
|
+
if (!v) return false
|
|
231
|
+
if (typeof v === 'string') return markers.some((m) => v.includes(m)) && isLegacyElectronCommand(v)
|
|
232
|
+
if (Array.isArray(v)) return v.some(walk)
|
|
233
|
+
if (typeof v === 'object') return Object.values(v).some(walk)
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
return walk(cfg)
|
|
237
|
+
} catch { return false }
|
|
238
|
+
}
|
|
239
|
+
|
|
81
240
|
async function hookState(hooksJson, agent, cursorStyle = false) {
|
|
82
241
|
try {
|
|
83
242
|
const cfg = JSON.parse(await fs.readFile(hooksJson, 'utf8'))
|
|
@@ -148,7 +307,7 @@ async function setupClaudeHooks({ nodePath, dryRun, log }) {
|
|
|
148
307
|
// ① SessionEnd:会话结束自动沉淀
|
|
149
308
|
const want = {
|
|
150
309
|
matcher: '',
|
|
151
|
-
hooks: [{ type: 'command', command:
|
|
310
|
+
hooks: [{ type: 'command', command: hookCommand(nodePath, hookScript, 'claude') }],
|
|
152
311
|
}
|
|
153
312
|
const hookKey = 'SessionEnd'
|
|
154
313
|
cfg.hooks[hookKey] = Array.isArray(cfg.hooks[hookKey]) ? cfg.hooks[hookKey] : []
|
|
@@ -158,7 +317,7 @@ async function setupClaudeHooks({ nodePath, dryRun, log }) {
|
|
|
158
317
|
// 或 npm 版本装到别处),旧实现只判「有没有 capture.mjs」就跳过,于是 hook 永远
|
|
159
318
|
// 指着那个旧副本(本机实测:Claude Code 一直在跑 web profile 的 0.9.23)。
|
|
160
319
|
// 与 Codex 分支同样的口径:只重写自己那条命令,其它 hook 一律不动。
|
|
161
|
-
const wanted =
|
|
320
|
+
const wanted = hookCommand(nodePath, hookScript, 'claude')
|
|
162
321
|
let rewrote = false
|
|
163
322
|
for (const c of existingCapture.hooks ?? []) {
|
|
164
323
|
if (typeof c?.command === 'string' && c.command.includes('capture.mjs') && c.command !== wanted) {
|
|
@@ -184,13 +343,25 @@ async function setupClaudeHooks({ nodePath, dryRun, log }) {
|
|
|
184
343
|
if (await exists(guardScript)) {
|
|
185
344
|
const guardKey = 'PreToolUse'
|
|
186
345
|
cfg.hooks[guardKey] = Array.isArray(cfg.hooks[guardKey]) ? cfg.hooks[guardKey] : []
|
|
187
|
-
|
|
188
|
-
|
|
346
|
+
const wantedGuard = hookCommand(nodePath, guardScript)
|
|
347
|
+
const existingGuard = cfg.hooks[guardKey].find((h) => h?.hooks?.some((c) => c?.command?.includes('secret-guard.mjs')))
|
|
348
|
+
if (existingGuard) {
|
|
349
|
+
// 已存在也要修命令:旧版本把 Electron 宿主 exe 写进了 hook(issue #53),
|
|
350
|
+
// 只判「有没有 secret-guard」会永远跳过修复。
|
|
351
|
+
let rewrote = false
|
|
352
|
+
for (const c of existingGuard.hooks ?? []) {
|
|
353
|
+
if (typeof c?.command === 'string' && c.command.includes('secret-guard.mjs') && c.command !== wantedGuard) {
|
|
354
|
+
c.command = wantedGuard
|
|
355
|
+
rewrote = true
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (rewrote) { written.push(guardKey + '(path)'); changed = true; log('↻ Claude Code PreToolUse hook 路径已更新') }
|
|
359
|
+
else log('✓ Claude Code PreToolUse 凭据守卫已存在,跳过')
|
|
189
360
|
} else {
|
|
190
361
|
// 只追加一个独立条目,保留用户已有的 PreToolUse 配置
|
|
191
362
|
cfg.hooks[guardKey].push({
|
|
192
363
|
matcher: 'Read|Bash|Grep|Glob',
|
|
193
|
-
hooks: [{ type: 'command', command:
|
|
364
|
+
hooks: [{ type: 'command', command: wantedGuard }],
|
|
194
365
|
})
|
|
195
366
|
written.push(guardKey)
|
|
196
367
|
changed = true
|
|
@@ -245,18 +416,40 @@ export function parseCodexMemoryEntry(text) {
|
|
|
245
416
|
* 纯函数:把 `[mcp_servers.memory]` 段对齐到 want(只动这一段的两行)。
|
|
246
417
|
* @returns {{text:string, changed:boolean}}
|
|
247
418
|
*/
|
|
419
|
+
export function codexEnvLine(env) {
|
|
420
|
+
if (!env || typeof env !== 'object') return ''
|
|
421
|
+
const keys = Object.keys(env)
|
|
422
|
+
if (!keys.length) return ''
|
|
423
|
+
return 'env = { ' + keys.map((k) => `${k} = ${JSON.stringify(String(env[k]))}`).join(', ') + ' }'
|
|
424
|
+
}
|
|
425
|
+
|
|
248
426
|
export function alignCodexMemorySection(text, want) {
|
|
249
427
|
const lines = String(text || '').split(/\r?\n/)
|
|
250
428
|
let inSec = false
|
|
251
429
|
let changed = false
|
|
430
|
+
let lastRelevant = -1
|
|
431
|
+
let envAt = -1
|
|
432
|
+
const wantCmd = 'command = ' + JSON.stringify(want.command)
|
|
433
|
+
const wantArgs = 'args = ' + JSON.stringify(want.args)
|
|
434
|
+
const wantEnv = codexEnvLine(want.env)
|
|
252
435
|
for (let i = 0; i < lines.length; i++) {
|
|
253
436
|
if (/^\s*\[/.test(lines[i])) { inSec = /^\s*\[mcp_servers\.memory\]\s*$/.test(lines[i].trim()); continue }
|
|
254
437
|
if (!inSec) continue
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (/^\s*
|
|
259
|
-
|
|
438
|
+
if (/^\s*command\s*=/.test(lines[i])) {
|
|
439
|
+
if (lines[i] !== wantCmd) { lines[i] = wantCmd; changed = true }
|
|
440
|
+
lastRelevant = i
|
|
441
|
+
} else if (/^\s*args\s*=/.test(lines[i])) {
|
|
442
|
+
if (lines[i] !== wantArgs) { lines[i] = wantArgs; changed = true }
|
|
443
|
+
lastRelevant = i
|
|
444
|
+
} else if (/^\s*env\s*=/.test(lines[i])) {
|
|
445
|
+
envAt = i
|
|
446
|
+
if (wantEnv && lines[i] !== wantEnv) { lines[i] = wantEnv; changed = true }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (wantEnv && envAt < 0) {
|
|
450
|
+
const at = lastRelevant >= 0 ? lastRelevant + 1 : 0
|
|
451
|
+
lines.splice(at, 0, wantEnv)
|
|
452
|
+
changed = true
|
|
260
453
|
}
|
|
261
454
|
return { text: changed ? lines.join('\n') : String(text || ''), changed }
|
|
262
455
|
}
|
|
@@ -275,10 +468,12 @@ export async function setupCodex({ nodePath, dryRun = false, log = () => {} } =
|
|
|
275
468
|
// issue #44 现场的教训:TOML 里可能是**单引号**(`command = 'C:\…'`),老代码只认双引号 →
|
|
276
469
|
// 既读不到(status 报「配置里是 (空)」)也改不掉(`replace` 不命中),**却照样打印「已更新」**。
|
|
277
470
|
// 所以:① 读写都认两种引号;② 改完必须**校验文本真的变了**,没变就如实报失败。
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
|
|
471
|
+
// 必须按 [mcp_servers.memory] 段读取;直接在全文抓 command 会抓到别的 server(issue #44)。
|
|
472
|
+
const entry = parseCodexMemoryEntry(text)
|
|
473
|
+
const curCmd = entry.command
|
|
474
|
+
const argsOk = entry.args.length === want.args.length && want.args.every((a, i) => entry.args[i] === a)
|
|
475
|
+
const envOk = !want.env || /ELECTRON_RUN_AS_NODE\s*=\s*["']1["']/.test(text)
|
|
476
|
+
if (curCmd === want.command && argsOk && envOk) {
|
|
282
477
|
log('✓ Codex MCP 已是最新,跳过')
|
|
283
478
|
return [{ agent: 'codex', ok: true, reason: 'section exists' }]
|
|
284
479
|
}
|
|
@@ -303,6 +498,7 @@ export async function setupCodex({ nodePath, dryRun = false, log = () => {} } =
|
|
|
303
498
|
'[mcp_servers.memory]',
|
|
304
499
|
`command = ${JSON.stringify(want.command)}`,
|
|
305
500
|
`args = ${JSON.stringify(want.args)}`,
|
|
501
|
+
...(want.env ? [codexEnvLine(want.env)] : []),
|
|
306
502
|
'',
|
|
307
503
|
].join('\n')
|
|
308
504
|
if (!dryRun) {
|
|
@@ -367,9 +563,10 @@ export async function runSetup(opts = {}) {
|
|
|
367
563
|
}
|
|
368
564
|
const all = []
|
|
369
565
|
const want = (name) => only.length === 0 || only.includes(name)
|
|
370
|
-
|
|
371
|
-
if (want('
|
|
372
|
-
if (want('
|
|
566
|
+
const nodePath = nodeBinary()
|
|
567
|
+
if (want('claude')) all.push(...await setupClaude({ nodePath, withHooks, dryRun, log }))
|
|
568
|
+
if (want('codex')) all.push(...await setupCodex({ nodePath, dryRun, log }))
|
|
569
|
+
if (want('cursor')) all.push(...await setupCursor({ nodePath, dryRun, log }))
|
|
373
570
|
return { ok: true, results: all }
|
|
374
571
|
}
|
|
375
572
|
|
|
@@ -377,7 +574,7 @@ export async function runSetup(opts = {}) {
|
|
|
377
574
|
* 只读查询各 agent 的 MCP 配置状态。不修改任何文件。
|
|
378
575
|
* @returns {Promise<{ok:true, agents: Array, lastCheckedAt: number}>}
|
|
379
576
|
*/
|
|
380
|
-
export async function getSetupStatus({ nodePath =
|
|
577
|
+
export async function getSetupStatus({ nodePath = nodeBinary() } = {}) {
|
|
381
578
|
const agents = []
|
|
382
579
|
const want = mcpCommand(nodePath)
|
|
383
580
|
// claude-code
|
|
@@ -413,6 +610,10 @@ export async function getSetupStatus({ nodePath = process.execPath } = {}) {
|
|
|
413
610
|
} else {
|
|
414
611
|
agents[agents.length - 1].hook = 'missing'
|
|
415
612
|
}
|
|
613
|
+
const last = agents[agents.length - 1]
|
|
614
|
+
last.mcpNeedsRepair = mcpExists && isLegacyElectronCommand(entry?.command)
|
|
615
|
+
last.hookNeedsRepair = await hooksNeedRepair(path.join(home, '.claude', 'settings.json'), ['capture.mjs', 'secret-guard.mjs'])
|
|
616
|
+
last.needsRepair = Boolean(last.mcpNeedsRepair || last.hookNeedsRepair)
|
|
416
617
|
}
|
|
417
618
|
// codex
|
|
418
619
|
const codexToml = path.join(home, '.codex', 'config.toml')
|
|
@@ -433,12 +634,15 @@ export async function getSetupStatus({ nodePath = process.execPath } = {}) {
|
|
|
433
634
|
servers: listCodexMcpServers(text),
|
|
434
635
|
installed: true,
|
|
435
636
|
mcpConfigured: has,
|
|
436
|
-
mcpMatchesCurrentNode: has && configuredNodePath === nodePath,
|
|
637
|
+
mcpMatchesCurrentNode: has && configuredNodePath === nodePath && (!want.env || /ELECTRON_RUN_AS_NODE\s*=\s*["']1["']/.test(text)),
|
|
437
638
|
currentNodePath: nodePath,
|
|
438
639
|
configuredNodePath,
|
|
439
640
|
mcpPath: codexToml,
|
|
440
641
|
hook: await hookState(path.join(home, '.codex', 'hooks.json'), 'codex'),
|
|
642
|
+
mcpNeedsRepair: has && isLegacyElectronCommand(configuredNodePath),
|
|
643
|
+
hookNeedsRepair: await hooksNeedRepair(path.join(home, '.codex', 'hooks.json'), ['capture.mjs']),
|
|
441
644
|
})
|
|
645
|
+
agents[agents.length - 1].needsRepair = Boolean(agents[agents.length - 1].mcpNeedsRepair || agents[agents.length - 1].hookNeedsRepair)
|
|
442
646
|
}
|
|
443
647
|
// cursor
|
|
444
648
|
const cursorJson = path.join(home, '.cursor', 'mcp.json')
|
|
@@ -464,7 +668,10 @@ export async function getSetupStatus({ nodePath = process.execPath } = {}) {
|
|
|
464
668
|
configuredNodePath: entry?.command || '',
|
|
465
669
|
mcpPath: cursorJson,
|
|
466
670
|
hook: await hookState(path.join(home, '.cursor', 'hooks.json'), 'cursor', true),
|
|
671
|
+
mcpNeedsRepair: mcpExists && isLegacyElectronCommand(entry?.command),
|
|
672
|
+
hookNeedsRepair: await hooksNeedRepair(path.join(home, '.cursor', 'hooks.json'), ['capture.mjs']),
|
|
467
673
|
})
|
|
674
|
+
agents[agents.length - 1].needsRepair = Boolean(agents[agents.length - 1].mcpNeedsRepair || agents[agents.length - 1].hookNeedsRepair)
|
|
468
675
|
} catch { agents.push({ name: 'cursor', installed: true, mcpConfigured: false, mcpPath: cursorJson, reason: 'JSON 解析失败' }) }
|
|
469
676
|
}
|
|
470
677
|
// 兼容性探测:只列出「本机确实存在配置文件」的其它 MCP 客户端(只读,不提供挂载按钮)
|
|
@@ -484,10 +691,11 @@ export async function getSetupStatus({ nodePath = process.execPath } = {}) {
|
|
|
484
691
|
const summary = {
|
|
485
692
|
detectedAgents: agents.filter((a) => a.installed).length,
|
|
486
693
|
mountedAgents: agents.filter((a) => a.mcpConfigured).length,
|
|
694
|
+
needsRepair: agents.filter((a) => a.needsRepair).length,
|
|
487
695
|
totalServers: allServers.length,
|
|
488
696
|
allServers,
|
|
489
697
|
}
|
|
490
|
-
return { ok: true, agents, summary, lastCheckedAt: Date.now(), want: { command: nodePath, args: want.args } }
|
|
698
|
+
return { ok: true, agents, summary, lastCheckedAt: Date.now(), want: { command: nodePath, args: want.args, ...(want.env ? { env: want.env } : {}) } }
|
|
491
699
|
}
|
|
492
700
|
|
|
493
701
|
// -- 会话结束自动沉淀 hook(connect <agent>,agentmemory 的 `connect --with-hooks` 兜底路径)--
|
|
@@ -496,9 +704,9 @@ export async function getSetupStatus({ nodePath = process.execPath } = {}) {
|
|
|
496
704
|
|
|
497
705
|
/** capture.mjs 的完整调用(绝对路径,避免升级后路径内嵌版本号而失效)。 */
|
|
498
706
|
function captureCmd(agent) {
|
|
499
|
-
const node =
|
|
707
|
+
const node = nodeBinary()
|
|
500
708
|
const script = path.join(PACKAGE_ROOT, 'hooks', 'capture.mjs')
|
|
501
|
-
return
|
|
709
|
+
return hookCommand(node, script, agent)
|
|
502
710
|
}
|
|
503
711
|
|
|
504
712
|
/** 写一个幂等 hooks.json:events → [{command}](cursor 风格)或 {hooks:[{type,command}]}(codex 风格)。
|
|
@@ -550,7 +758,7 @@ export async function connectCursorHooks(opts = {}) {
|
|
|
550
758
|
/** 统一入口:claude 复用 setupClaude 的 SessionEnd(capture.mjs);codex/cursor 写 capture.mjs hooks。 */
|
|
551
759
|
export async function connectAgentHooks(agent, opts = {}) {
|
|
552
760
|
if (agent === 'claude' || agent === 'claude-code') {
|
|
553
|
-
const results = await setupClaude({ nodePath:
|
|
761
|
+
const results = await setupClaude({ nodePath: nodeBinary(), withHooks: true, log: opts.log })
|
|
554
762
|
return { ok: results.every((r) => r.ok), results }
|
|
555
763
|
}
|
|
556
764
|
if (agent === 'codex') return connectCodexHooks(opts)
|
|
@@ -578,13 +786,14 @@ export async function removeClaudeMcp({ dryRun = false, log = () => {} } = {}) {
|
|
|
578
786
|
if (await exists(settingsJson)) {
|
|
579
787
|
try {
|
|
580
788
|
const scfg = JSON.parse(await fs.readFile(settingsJson, 'utf8'))
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
789
|
+
let dirty = false
|
|
790
|
+
for (const key of ['SessionEnd', 'PreToolUse']) {
|
|
791
|
+
const list = scfg.hooks?.[key]
|
|
792
|
+
if (!Array.isArray(list)) continue
|
|
793
|
+
const kept = list.filter((h) => !(h.hooks || []).some((c) => c?.command?.includes('capture.mjs') || c?.command?.includes('secret-guard.mjs')))
|
|
794
|
+
if (kept.length !== list.length) { scfg.hooks[key] = kept; dirty = true; removed = true }
|
|
587
795
|
}
|
|
796
|
+
if (dirty && !dryRun) { await backupOnce(settingsJson); await fs.writeFile(settingsJson, JSON.stringify(scfg, null, 2), 'utf8') }
|
|
588
797
|
} catch {}
|
|
589
798
|
}
|
|
590
799
|
log(removed ? (dryRun ? '[dry-run] 将卸载 Claude Code MCP' : '✓ 已卸载 Claude Code MCP') : '✓ Claude Code MCP 未配置')
|
|
@@ -599,7 +808,7 @@ export async function removeCodexMcp({ dryRun = false, log = () => {} } = {}) {
|
|
|
599
808
|
const sectionRe = /^\s*\[mcp_servers\.memory\]\s*$/m
|
|
600
809
|
if (!sectionRe.test(text)) { log('✓ Codex MCP 未配置'); return { agent: 'codex', ok: true, removed: false } }
|
|
601
810
|
if (!dryRun) {
|
|
602
|
-
const newText = text.replace(/^\s*# memory-eternal.*\n?/m, '').replace(/^\s*\[mcp_servers\.memory\]\s*\n(?:command\s*=.*\n|args\s*=.*\n)+\s*/m, '')
|
|
811
|
+
const newText = text.replace(/^\s*# memory-eternal.*\n?/m, '').replace(/^\s*\[mcp_servers\.memory\]\s*\n(?:command\s*=.*\n|args\s*=.*\n|env\s*=.*\n)+\s*/m, '')
|
|
603
812
|
await backupOnce(codexToml)
|
|
604
813
|
await fs.writeFile(codexToml, newText.replace(/\n{3,}/g, '\n\n'), 'utf8')
|
|
605
814
|
}
|
|
@@ -626,7 +835,7 @@ export async function removeCursorMcp({ dryRun = false, log = () => {} } = {}) {
|
|
|
626
835
|
/** 对单个智能体执行安装/卸载 MCP。action: 'install' | 'uninstall'。agent: 'claude-code'|'codex'|'cursor' */
|
|
627
836
|
export async function mcpAgentAction(agent, action, { dryRun = false, log = () => {} } = {}) {
|
|
628
837
|
if (action === 'install') {
|
|
629
|
-
const nodePath =
|
|
838
|
+
const nodePath = nodeBinary()
|
|
630
839
|
if (agent === 'claude-code') return { ok: true, results: await setupClaude({ nodePath, dryRun, log }) }
|
|
631
840
|
if (agent === 'codex') return { ok: true, results: await setupCodex({ nodePath, dryRun, log }) }
|
|
632
841
|
if (agent === 'cursor') return { ok: true, results: await setupCursor({ nodePath, dryRun, log }) }
|
package/lib/vault.js
CHANGED
|
@@ -10,6 +10,7 @@ import { minhashSignature, lshCandidates } from './minhash.js'
|
|
|
10
10
|
|
|
11
11
|
/** 主题目录根。 */
|
|
12
12
|
export const KIND_ROOTS = {
|
|
13
|
+
core: '09-Core',
|
|
13
14
|
project: '02-Projects',
|
|
14
15
|
knowledge: '03-Knowledge',
|
|
15
16
|
content: '04-Content',
|
|
@@ -19,6 +20,9 @@ export const KIND_ROOTS = {
|
|
|
19
20
|
mistake: '08-Mistakes',
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** 核心记忆:人工批准后,每次会话注入 systemPrompt 的卡片类型。 */
|
|
24
|
+
export const CORE_KIND = 'core'
|
|
25
|
+
|
|
22
26
|
export const CAPTURE_KINDS = ['project', 'knowledge', 'content', 'prompt', 'business', 'tool', 'mistake']
|
|
23
27
|
|
|
24
28
|
/** 从 Markdown 文本提取 frontmatter 与正文。 */
|
|
@@ -593,6 +597,22 @@ export function normalizeTags(tags) {
|
|
|
593
597
|
.slice(0, 20)
|
|
594
598
|
}
|
|
595
599
|
|
|
600
|
+
/**
|
|
601
|
+
* 把一张已批准的卡片设为核心记忆(kind=core),或恢复成普通知识卡。
|
|
602
|
+
* 只有主库(approved)卡片可以设为核心;审核中的卡必须先批准。
|
|
603
|
+
*/
|
|
604
|
+
export async function setCardKind(root, rel, kind) {
|
|
605
|
+
const db = getDb(root)
|
|
606
|
+
const target = String(kind || '').trim()
|
|
607
|
+
const existing = findCardRow(db, rel)
|
|
608
|
+
if (!existing) throw new Error('卡片不存在: ' + rel)
|
|
609
|
+
if (existing.table !== 'cards') throw new Error('只有已批准卡片才能设为核心记忆,请先在审核中心批准')
|
|
610
|
+
if (target !== CORE_KIND && !CAPTURE_KINDS.includes(target)) throw new Error('不支持的卡片类型: ' + target)
|
|
611
|
+
const now = new Date().toISOString()
|
|
612
|
+
db.prepare('UPDATE cards SET kind = ?, updated_at = ? WHERE path = ?').run(target, now, rel)
|
|
613
|
+
return { ok: true, path: rel, kind: target, updatedAt: now }
|
|
614
|
+
}
|
|
615
|
+
|
|
596
616
|
export async function writeCard(root, { kind, title, tags = [], body, source = '', status = 'pending', submittedBy = '', severity = 'info', reason = '', deletedAt = '' }, { threshold = 0.62, dedup = true, dedupAgainst = null } = {}) {
|
|
597
617
|
const db = getDb(root)
|
|
598
618
|
const kindRoot = KIND_ROOTS[kind] || KIND_ROOTS.knowledge
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"manifestVersion": 1,
|
|
3
|
-
"id": "memory-eternal",
|
|
4
|
-
"publisher": "eternalnight996",
|
|
5
|
-
"name": "memory-eternal",
|
|
6
|
-
"version": "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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"manifestVersion": 1,
|
|
3
|
+
"id": "memory-eternal",
|
|
4
|
+
"publisher": "eternalnight996",
|
|
5
|
+
"name": "memory-eternal",
|
|
6
|
+
"version": "0.18.1",
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"icon": "assets/icon.png",
|
|
6
6
|
"license": "MIT",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"release": "node scripts/release.mjs",
|
|
71
71
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
72
72
|
"panel-check": "node scripts/panel-check.mjs",
|
|
73
|
-
"test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/graph-warmup.test.mjs && node tests/loading-hints.test.mjs && node tests/web-entry.test.mjs && node tests/tui-scroll.test.mjs && node tests/tui-audit-scene.test.mjs && node tests/tui-plugin-boot.test.mjs && node tests/capture-session-id.test.mjs && node tests/ssr.test.mjs && node tests/window-controls.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.test.mjs && node tests/host-managed.test.mjs && node tests/replace-claim.test.mjs && node tests/version-check-self.test.mjs && node tests/setup-codex.test.mjs && node tests/listener-netstat.test.mjs && node tests/issue-46-overlay.test.mjs && node tests/graph-initial-zoom.test.mjs && node tests/card-head-actions.test.mjs && node tests/card-reader-ux.test.mjs && node tests/tag-filter.test.mjs && node tests/ui-overflow.test.mjs && node tests/version-card-state.test.mjs && node tests/llm-routes-proxy.test.mjs && node tests/model-strikes.test.mjs && node tests/support-bundle.test.mjs && node tests/health-panel.test.mjs && node tests/issue-48-timeout.test.mjs && node tests/ledger.test.mjs"
|
|
73
|
+
"test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/graph-warmup.test.mjs && node tests/loading-hints.test.mjs && node tests/web-entry.test.mjs && node tests/tui-scroll.test.mjs && node tests/tui-audit-scene.test.mjs && node tests/tui-plugin-boot.test.mjs && node tests/capture-session-id.test.mjs && node tests/ssr.test.mjs && node tests/window-controls.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.test.mjs && node tests/host-managed.test.mjs && node tests/replace-claim.test.mjs && node tests/version-check-self.test.mjs && node tests/setup-codex.test.mjs && node tests/listener-netstat.test.mjs && node tests/issue-46-overlay.test.mjs && node tests/graph-initial-zoom.test.mjs && node tests/card-head-actions.test.mjs && node tests/card-reader-ux.test.mjs && node tests/tag-filter.test.mjs && node tests/ui-overflow.test.mjs && node tests/version-card-state.test.mjs && node tests/llm-routes-proxy.test.mjs && node tests/model-strikes.test.mjs && node tests/support-bundle.test.mjs && node tests/health-panel.test.mjs && node tests/issue-48-timeout.test.mjs && node tests/setup-node-path.test.mjs && node tests/capture-kind.test.mjs && node tests/core-memory.test.mjs && node tests/ledger.test.mjs"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@deepseek-ai/schemastery": "^3.18.1"
|